core(M0): монорепо-каркас — CMake (posix+esp-idf), платслой, лог, мини-httpd

- CMakeLists в корне: ветвление ESP_PLATFORM (idf_component_register,
  lwip/esp_timer/esp_hw_support/pthread) / POSIX (статическая библиотека
  fgl-aircon, C++20, -fno-exceptions -fno-rtti, -Werror).
- src/ayla/platform: сокеты/потоки/CSPRNG/время; posix (getrandom, poll,
  pthread_join) и esp-idf (lwip_select, esp_fill_random, pthread-слой IDF);
  tcp_shutdown/tcp_local_port/thread_join для управляемой остановки.
- src/ayla: log (sink, без printf); мини-httpd/1.1 (keep-alive, Content-Length,
  лимиты заголовков/тела, ephemeral-порт, жизненный цикл с гарантией
  завершения потока: shutdown(active)→join→close).
- tests/ayla: platform (join, loopback+shutdown) и httpd (404, keep-alive,
  обработчик/парсинг, oversize-400, stop при живом соединении, стрим
  заголовков). doctest через FetchContent.
- scripts/ci.sh: сборка+ctest. ESP-IDF v5.5.5 esp32: смоук-сборка с ядром
  как компонентом — Project build complete.
Ревью под-агентом: 3 круга, все блокеры (жизненный цикл httpd) закрыты, APPROVED.
This commit is contained in:
2026-09-22 00:27:33 +03:00
parent 301cf5cf70
commit e6f6e2d5f4
15 changed files with 1362 additions and 1 deletions
+216
View File
@@ -0,0 +1,216 @@
// POSIX-реализация платформенного слоя (Linux).
#include "ayla/platform/platform.hpp"
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <netinet/tcp.h>
#include <netdb.h>
#include <poll.h>
#include <pthread.h>
#include <sys/random.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>
#include <new>
namespace fgl::plat {
uint64_t now_ms() {
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return static_cast<uint64_t>(ts.tv_sec) * 1000u +
static_cast<uint64_t>(ts.tv_nsec) / 1000000u;
}
bool random(uint8_t* buf, size_t len) {
size_t done = 0;
while (done < len) {
ssize_t n = ::getrandom(buf + done, len - done, 0);
if (n < 0) {
if (errno == EINTR) continue;
return false;
}
done += static_cast<size_t>(n);
}
return true;
}
namespace {
struct ThreadStart {
void (*fn)(void*);
void* ctx;
};
void* thread_trampoline(void* arg) {
auto* start = static_cast<ThreadStart*>(arg);
ThreadStart tmp = *start;
delete start;
tmp.fn(tmp.ctx);
return nullptr;
}
} // namespace
bool thread_create(void (*fn)(void*), void* ctx, const char* name,
uint32_t stack_bytes, ThreadId* out_id) {
auto* start = new (std::nothrow) ThreadStart{fn, ctx};
if (start == nullptr) return false;
pthread_t tid;
pthread_attr_t attr;
pthread_attr_init(&attr);
if (stack_bytes > 0) pthread_attr_setstacksize(&attr, stack_bytes);
int rc = pthread_create(&tid, &attr, thread_trampoline, start);
pthread_attr_destroy(&attr);
if (rc != 0) {
delete start;
return false;
}
pthread_setname_np(tid, name != nullptr ? name : "fgl");
if (out_id != nullptr) {
*out_id = reinterpret_cast<ThreadId>(tid);
} else {
pthread_detach(tid);
}
return true;
}
void thread_join(ThreadId id) {
auto tid = reinterpret_cast<pthread_t>(id);
pthread_join(tid, nullptr);
}
void sleep_ms(uint32_t ms) {
struct timespec req;
req.tv_sec = ms / 1000;
req.tv_nsec = static_cast<long>(ms % 1000) * 1000000L;
while (nanosleep(&req, &req) == -1 && errno == EINTR) {
}
}
int tcp_listen(uint16_t port) {
int fd = ::socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0) return -1;
int one = 1;
::setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
struct sockaddr_in addr {};
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(port);
if (::bind(fd, reinterpret_cast<struct sockaddr*>(&addr), sizeof(addr)) < 0 ||
::listen(fd, 4) < 0) {
::close(fd);
return -1;
}
return fd;
}
uint16_t tcp_local_port(int fd) {
struct sockaddr_in addr {};
socklen_t addrlen = sizeof(addr);
if (::getsockname(fd, reinterpret_cast<struct sockaddr*>(&addr), &addrlen) != 0) {
return 0;
}
return ntohs(addr.sin_port);
}
int tcp_accept(int listen_fd, uint32_t* peer_ip, uint16_t* peer_port) {
struct sockaddr_in addr {};
socklen_t addrlen = sizeof(addr);
int fd = ::accept(listen_fd, reinterpret_cast<struct sockaddr*>(&addr), &addrlen);
if (fd < 0) return -1;
int one = 1;
::setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one));
if (peer_ip != nullptr) *peer_ip = ntohl(addr.sin_addr.s_addr);
if (peer_port != nullptr) *peer_port = ntohs(addr.sin_port);
return fd;
}
int tcp_connect(const char* host, uint16_t port, uint32_t timeout_ms) {
struct addrinfo hints {};
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
struct addrinfo* list = nullptr;
if (::getaddrinfo(host, nullptr, &hints, &list) != 0 || list == nullptr) {
return -1;
}
int fd = -1;
for (struct addrinfo* ai = list; ai != nullptr; ai = ai->ai_next) {
auto* addr = reinterpret_cast<struct sockaddr_in*>(ai->ai_addr);
addr->sin_port = htons(port);
fd = ::socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
if (fd < 0) continue;
// Неблокирующее подключение + poll для таймаута.
int flags = ::fcntl(fd, F_GETFL, 0);
::fcntl(fd, F_SETFL, flags | O_NONBLOCK);
int rc = ::connect(fd, ai->ai_addr, ai->ai_addrlen);
if (rc == 0) {
::fcntl(fd, F_SETFL, flags);
break;
}
if (errno == EINPROGRESS) {
struct pollfd pfd {fd, POLLOUT, 0};
if (::poll(&pfd, 1, static_cast<int>(timeout_ms)) > 0) {
int soerr = 0;
socklen_t slen = sizeof(soerr);
::getsockopt(fd, SOL_SOCKET, SO_ERROR, &soerr, &slen);
if (soerr == 0) {
::fcntl(fd, F_SETFL, flags);
break;
}
}
}
::close(fd);
fd = -1;
}
::freeaddrinfo(list);
return fd;
}
long tcp_send(int fd, const void* buf, size_t len) {
const uint8_t* p = static_cast<const uint8_t*>(buf);
size_t done = 0;
while (done < len) {
long n = ::send(fd, p + done, len - done, MSG_NOSIGNAL);
if (n < 0) {
if (errno == EINTR) continue;
return -1;
}
done += static_cast<size_t>(n);
}
return static_cast<long>(done);
}
long tcp_recv(int fd, void* buf, size_t len) {
for (;;) {
long n = ::recv(fd, buf, len, 0);
if (n < 0 && errno == EINTR) continue;
return n;
}
}
bool tcp_set_timeout(int fd, uint32_t rx_ms, uint32_t tx_ms) {
struct timeval tv {};
tv.tv_sec = rx_ms / 1000;
tv.tv_usec = static_cast<long>(rx_ms % 1000) * 1000L;
if (::setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)) != 0) return false;
tv.tv_sec = tx_ms / 1000;
tv.tv_usec = static_cast<long>(tx_ms % 1000) * 1000L;
return ::setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)) == 0;
}
bool tcp_poll_readable(int fd, uint32_t timeout_ms) {
struct pollfd pfd {fd, POLLIN, 0};
return ::poll(&pfd, 1, static_cast<int>(timeout_ms)) > 0;
}
bool tcp_shutdown(int fd) {
return ::shutdown(fd, SHUT_RDWR) == 0;
}
void tcp_close(int fd) {
if (fd >= 0) ::close(fd);
}
} // namespace fgl::plat