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:
285
src/ayla/httpd.cpp
Normal file
285
src/ayla/httpd.cpp
Normal file
@@ -0,0 +1,285 @@
|
||||
#include "ayla/httpd.hpp"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <new>
|
||||
|
||||
#include "ayla/log.hpp"
|
||||
|
||||
namespace fgl::ayla {
|
||||
|
||||
struct ThreadCtx {
|
||||
HttpServer* self;
|
||||
};
|
||||
|
||||
namespace {
|
||||
|
||||
// Статусные строки для используемых кодов.
|
||||
struct StatusText { int code; const char* text; };
|
||||
constexpr StatusText kStatusTexts[] = {
|
||||
{200, "OK"}, {206, "Partial Content"}, {400, "Bad Request"},
|
||||
{401, "Unauthorized"}, {404, "Not Found"}, {412, "Precondition Failed"},
|
||||
{426, "Upgrade Required"}, {500, "Internal Server Error"},
|
||||
};
|
||||
|
||||
const char* status_text(int code) {
|
||||
for (const auto& st : kStatusTexts) {
|
||||
if (st.code == code) return st.text;
|
||||
}
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
bool recv_line(int fd, char* buf, size_t buflen, size_t* out_len) {
|
||||
size_t len = 0;
|
||||
for (;;) {
|
||||
if (len + 1 >= buflen) return false; // слишком длинно
|
||||
uint8_t ch;
|
||||
long n = fgl::plat::tcp_recv(fd, &ch, 1);
|
||||
if (n <= 0) return false;
|
||||
if (ch == '\n') {
|
||||
if (len > 0 && buf[len - 1] == '\r') len--; // CRLF
|
||||
buf[len] = '\0';
|
||||
*out_len = len;
|
||||
return true;
|
||||
}
|
||||
buf[len++] = static_cast<char>(ch);
|
||||
}
|
||||
}
|
||||
|
||||
// Регистронезависимое сравнение первых n символов.
|
||||
bool ieq_prefix(const char* a, const char* b, size_t n) {
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
char ca = a[i] | 0x20, cb = b[i] | 0x20;
|
||||
if (ca != cb) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Читает заголовки до пустой строки; суммарный объём ограничен
|
||||
// kHttpdMaxHeadersTotal (защита от бесконечного стрима заголовков).
|
||||
bool recv_headers(int fd, long* content_length, bool* connection_close) {
|
||||
char line[kHttpdMaxHeaderLine];
|
||||
size_t len;
|
||||
size_t total = 2; // завершающий CRLF
|
||||
*content_length = 0;
|
||||
*connection_close = false;
|
||||
for (;;) {
|
||||
if (!recv_line(fd, line, sizeof(line), &len)) return false;
|
||||
if (len == 0) return true; // конец заголовков
|
||||
total += len + 2;
|
||||
if (total > kHttpdMaxHeadersTotal) return false;
|
||||
if (ieq_prefix(line, "Content-Length:", 15)) {
|
||||
*content_length = strtol(line + 15, nullptr, 10);
|
||||
} else if (ieq_prefix(line, "Connection:", 11)) {
|
||||
const char* v = line + 11;
|
||||
while (*v == ' ') v++;
|
||||
if (ieq_prefix(v, "close", 5)) *connection_close = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// close_after — добавить Connection: close и не ждать продолжения.
|
||||
bool send_response(int fd, const HttpResponse& resp, bool close_after) {
|
||||
char head[192];
|
||||
int n = snprintf(head, sizeof(head),
|
||||
"HTTP/1.1 %d %s\r\n"
|
||||
"Content-Type: %s\r\n"
|
||||
"Content-Length: %u\r\n"
|
||||
"Connection: %s\r\n"
|
||||
"\r\n",
|
||||
resp.status, status_text(resp.status), resp.content_type,
|
||||
static_cast<unsigned>(resp.body_len),
|
||||
close_after ? "close" : "keep-alive");
|
||||
if (n <= 0 || static_cast<size_t>(n) >= sizeof(head)) return false;
|
||||
if (fgl::plat::tcp_send(fd, head, static_cast<size_t>(n)) !=
|
||||
static_cast<long>(n)) {
|
||||
return false;
|
||||
}
|
||||
if (resp.body != nullptr && resp.body_len > 0) {
|
||||
if (fgl::plat::tcp_send(fd, resp.body, resp.body_len) !=
|
||||
static_cast<long>(resp.body_len)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void parse_target(const char* full_target, HttpRequest* req) {
|
||||
const char* q = strchr(full_target, '?');
|
||||
size_t path_len = (q != nullptr) ? static_cast<size_t>(q - full_target)
|
||||
: strlen(full_target);
|
||||
if (path_len >= sizeof(req->target)) path_len = sizeof(req->target) - 1;
|
||||
memcpy(req->target, full_target, path_len);
|
||||
req->target[path_len] = '\0';
|
||||
req->query[0] = '\0';
|
||||
if (q != nullptr && q[1] != '\0') {
|
||||
size_t qlen = strlen(q + 1);
|
||||
if (qlen >= sizeof(req->query)) qlen = sizeof(req->query) - 1;
|
||||
memcpy(req->query, q + 1, qlen);
|
||||
req->query[qlen] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
constexpr uint32_t kHttpdThreadStack = 8192;
|
||||
constexpr uint32_t kAcceptPollMs = 100;
|
||||
constexpr uint32_t kClientRxTimeoutMs = 30000;
|
||||
constexpr uint32_t kClientTxTimeoutMs = 10000;
|
||||
|
||||
} // namespace
|
||||
|
||||
HttpServer::~HttpServer() { stop(); }
|
||||
|
||||
bool HttpServer::start(uint16_t port, HttpHandler handler, void* ctx,
|
||||
const char* thread_name) {
|
||||
if (running_.load(std::memory_order_acquire)) return false;
|
||||
handler_ = handler;
|
||||
ctx_ = ctx;
|
||||
int fd = fgl::plat::tcp_listen(port);
|
||||
if (fd < 0) {
|
||||
FGL_LOGE("httpd: tcp_listen(%u) failed", static_cast<unsigned>(port));
|
||||
return false;
|
||||
}
|
||||
listen_fd_ = fd;
|
||||
port_ = fgl::plat::tcp_local_port(fd);
|
||||
|
||||
auto* tc = new (std::nothrow) ThreadCtx{this};
|
||||
if (tc == nullptr) {
|
||||
fgl::plat::tcp_close(listen_fd_);
|
||||
listen_fd_ = -1;
|
||||
return false;
|
||||
}
|
||||
thread_ctx_ = tc;
|
||||
running_.store(true, std::memory_order_release);
|
||||
if (!fgl::plat::thread_create(
|
||||
[](void* p) {
|
||||
static_cast<ThreadCtx*>(p)->self->run();
|
||||
},
|
||||
tc, thread_name, kHttpdThreadStack, &thread_)) {
|
||||
running_.store(false, std::memory_order_release);
|
||||
delete tc;
|
||||
thread_ctx_ = nullptr;
|
||||
fgl::plat::tcp_close(listen_fd_);
|
||||
listen_fd_ = -1;
|
||||
return false;
|
||||
}
|
||||
FGL_LOGI("httpd: listening on port %u", static_cast<unsigned>(port_));
|
||||
return true;
|
||||
}
|
||||
|
||||
void HttpServer::stop() {
|
||||
if (!running_.exchange(false, std::memory_order_acq_rel)) {
|
||||
return;
|
||||
}
|
||||
// Прерываем активное соединение, чтобы поток не ждал rx-таймаута.
|
||||
int active = active_fd_.exchange(-1, std::memory_order_acq_rel);
|
||||
if (active >= 0) {
|
||||
fgl::plat::tcp_shutdown(active);
|
||||
}
|
||||
// Гарантированно ждём завершения потока; poll-цикл замечает running_
|
||||
// в пределах kAcceptPollMs.
|
||||
if (thread_ != nullptr) {
|
||||
fgl::plat::thread_join(thread_);
|
||||
thread_ = nullptr;
|
||||
}
|
||||
delete thread_ctx_;
|
||||
thread_ctx_ = nullptr;
|
||||
if (listen_fd_ >= 0) {
|
||||
fgl::plat::tcp_close(listen_fd_);
|
||||
listen_fd_ = -1;
|
||||
}
|
||||
}
|
||||
|
||||
void HttpServer::run() {
|
||||
while (running_.load(std::memory_order_acquire)) {
|
||||
if (!fgl::plat::tcp_poll_readable(listen_fd_, kAcceptPollMs)) {
|
||||
continue; // таймаут поллинга — перепроверяем running_
|
||||
}
|
||||
uint32_t peer_ip = 0;
|
||||
uint16_t peer_port = 0;
|
||||
int fd = fgl::plat::tcp_accept(listen_fd_, &peer_ip, &peer_port);
|
||||
if (fd < 0) {
|
||||
if (!running_.load(std::memory_order_acquire)) break;
|
||||
fgl::plat::sleep_ms(20);
|
||||
continue;
|
||||
}
|
||||
fgl::plat::tcp_set_timeout(fd, kClientRxTimeoutMs, kClientTxTimeoutMs);
|
||||
active_fd_.store(fd, std::memory_order_release);
|
||||
if (!running_.load(std::memory_order_acquire)) {
|
||||
// stop() мог произойти в зазоре accept->store: закрываем и выходим.
|
||||
// (Порядок store(fd)->load(running_) против exchange-ов в stop()
|
||||
// закрывает все интерливинги: либо stop() увидит fd и shutdown'нет.)
|
||||
active_fd_.store(-1, std::memory_order_release);
|
||||
fgl::plat::tcp_close(fd);
|
||||
break;
|
||||
}
|
||||
handle_connection(fd, peer_ip, peer_port);
|
||||
active_fd_.store(-1, std::memory_order_release);
|
||||
fgl::plat::tcp_close(fd);
|
||||
}
|
||||
}
|
||||
|
||||
void HttpServer::handle_connection(int fd, uint32_t peer_ip, uint16_t peer_port) {
|
||||
while (running_.load(std::memory_order_acquire)) { // keep-alive
|
||||
char line[kHttpdMaxTarget + kHttpdMaxQuery + 16];
|
||||
size_t len;
|
||||
if (!recv_line(fd, line, sizeof(line), &len)) return;
|
||||
|
||||
HttpRequest req {};
|
||||
req.peer_ip = peer_ip;
|
||||
req.peer_port = peer_port;
|
||||
|
||||
// "<METHOD> <target> HTTP/x.y"
|
||||
char* sp1 = strchr(line, ' ');
|
||||
if (sp1 == nullptr) return;
|
||||
*sp1 = '\0';
|
||||
if (strlen(line) >= sizeof(req.method)) return;
|
||||
strcpy(req.method, line);
|
||||
char* target = sp1 + 1;
|
||||
char* sp2 = strrchr(target, ' ');
|
||||
if (sp2 == nullptr) return;
|
||||
*sp2 = '\0';
|
||||
parse_target(target, &req);
|
||||
|
||||
long content_length = 0;
|
||||
bool connection_close = false;
|
||||
if (!recv_headers(fd, &content_length, &connection_close)) return;
|
||||
if (content_length < 0 ||
|
||||
static_cast<size_t>(content_length) > kHttpdMaxBody) {
|
||||
HttpResponse resp;
|
||||
resp.status = 400;
|
||||
send_response(fd, resp, /*close_after=*/true);
|
||||
// shutdown до close предотвращает RST, обгоняющий ответ
|
||||
// (recv после SHUT_RDWR сразу вернёт 0, дренаж не нужен).
|
||||
fgl::plat::tcp_shutdown(fd);
|
||||
return;
|
||||
}
|
||||
if (content_length > 0) {
|
||||
size_t need = static_cast<size_t>(content_length);
|
||||
size_t done = 0;
|
||||
while (done < need) {
|
||||
long n = fgl::plat::tcp_recv(fd, req.body + done, need - done);
|
||||
if (n <= 0) return;
|
||||
done += static_cast<size_t>(n);
|
||||
}
|
||||
req.body_len = done;
|
||||
}
|
||||
|
||||
FGL_LOGD("httpd: %s %s?%s from %u.%u.%u.%u", req.method, req.target,
|
||||
req.query, static_cast<unsigned>((peer_ip >> 24) & 0xffu),
|
||||
static_cast<unsigned>((peer_ip >> 16) & 0xffu),
|
||||
static_cast<unsigned>((peer_ip >> 8) & 0xffu),
|
||||
static_cast<unsigned>(peer_ip & 0xffu));
|
||||
|
||||
HttpResponse resp; // по умолчанию 404 — «пустой httpd»
|
||||
resp.status = 404;
|
||||
bool keep = true;
|
||||
if (handler_ != nullptr) {
|
||||
keep = handler_(req, resp, ctx_);
|
||||
}
|
||||
if (!send_response(fd, resp, /*close_after=*/!keep)) return;
|
||||
if (!keep || connection_close) return;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace fgl::ayla
|
||||
78
src/ayla/httpd.hpp
Normal file
78
src/ayla/httpd.hpp
Normal file
@@ -0,0 +1,78 @@
|
||||
// Мини-HTTP/1.1 сервер для входящих запросов модуля кондиционера.
|
||||
// Один поток, последовательная обработка соединений, keep-alive.
|
||||
// Поведенческие требования — docs/PROTOCOL.md §4-5: пути /local_lan/*,
|
||||
// коды 200/206/401/412/426, Content-Length-фрейминг, RST-обрывы — норма.
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
#include "ayla/platform/platform.hpp"
|
||||
|
||||
namespace fgl::ayla {
|
||||
|
||||
constexpr size_t kHttpdMaxTarget = 128; // путь без query
|
||||
constexpr size_t kHttpdMaxQuery = 96; // query-строка без '?'
|
||||
constexpr size_t kHttpdMaxHeaderLine = 256; // одна строка заголовка
|
||||
constexpr size_t kHttpdMaxBody = 2048; // тело запроса (envelope ~0.5КБ)
|
||||
constexpr size_t kHttpdMaxHeadersTotal = 1024; // суммарный лимит заголовков
|
||||
|
||||
struct HttpRequest {
|
||||
char method[8];
|
||||
char target[kHttpdMaxTarget]; // путь без query
|
||||
char query[kHttpdMaxQuery]; // query-строка без '?'
|
||||
uint8_t body[kHttpdMaxBody];
|
||||
size_t body_len;
|
||||
uint32_t peer_ip; // host byte order
|
||||
uint16_t peer_port;
|
||||
};
|
||||
|
||||
struct HttpResponse {
|
||||
int status = 200; // HTTP-код
|
||||
const char* content_type = "application/json";
|
||||
const uint8_t* body = nullptr;
|
||||
size_t body_len = 0;
|
||||
};
|
||||
|
||||
// Обработчик: заполняет resp. Возвращает true — соединение продолжается
|
||||
// (keep-alive), false — закрыть после ответа.
|
||||
using HttpHandler = bool (*)(const HttpRequest& req, HttpResponse& resp,
|
||||
void* ctx);
|
||||
|
||||
class HttpServer {
|
||||
public:
|
||||
HttpServer() = default;
|
||||
~HttpServer();
|
||||
|
||||
HttpServer(const HttpServer&) = delete;
|
||||
HttpServer& operator=(const HttpServer&) = delete;
|
||||
|
||||
// Запускает сервер (собственный поток). port==0 — любой свободный
|
||||
// (узнать выбранный: port()).
|
||||
// Возвращает false при ошибке bind/listen.
|
||||
bool start(uint16_t port, HttpHandler handler, void* ctx,
|
||||
const char* thread_name = "fgl_httpd");
|
||||
// Останавливает сервер и гарантированно завершает его поток
|
||||
// (активное соединение прерывается shutdown'ом).
|
||||
// КОНТРАКТ: start()/stop() вызываются из одного (управляющего) потока.
|
||||
void stop();
|
||||
|
||||
uint16_t port() const { return port_; }
|
||||
bool is_running() const { return running_.load(std::memory_order_acquire); }
|
||||
|
||||
private:
|
||||
void run();
|
||||
void handle_connection(int fd, uint32_t peer_ip, uint16_t peer_port);
|
||||
|
||||
HttpHandler handler_ = nullptr;
|
||||
void* ctx_ = nullptr;
|
||||
int listen_fd_ = -1;
|
||||
uint16_t port_ = 0;
|
||||
fgl::plat::ThreadId thread_ = nullptr; // владелец-поток, join в stop()
|
||||
struct ThreadCtx* thread_ctx_ = nullptr;
|
||||
std::atomic<bool> running_{false};
|
||||
std::atomic<int> active_fd_{-1}; // fd обрабатываемого соединения
|
||||
};
|
||||
|
||||
} // namespace fgl::ayla
|
||||
42
src/ayla/log.cpp
Normal file
42
src/ayla/log.cpp
Normal file
@@ -0,0 +1,42 @@
|
||||
#include "log.hpp"
|
||||
|
||||
#include <cstdarg>
|
||||
#include <cstdio>
|
||||
|
||||
namespace fgl::ayla::log {
|
||||
|
||||
namespace {
|
||||
Sink g_sink = nullptr;
|
||||
void* g_sink_ctx = nullptr;
|
||||
int g_min_level = kInfo;
|
||||
constexpr size_t kMaxLine = 256;
|
||||
} // namespace
|
||||
|
||||
void set_sink(Sink sink, void* ctx) {
|
||||
g_sink = sink;
|
||||
g_sink_ctx = ctx;
|
||||
}
|
||||
|
||||
int min_level() { return g_min_level; }
|
||||
void set_min_level(int level) { g_min_level = level; }
|
||||
|
||||
void write(int level, const char* fmt, ...) {
|
||||
if (g_sink == nullptr || level < g_min_level) {
|
||||
return;
|
||||
}
|
||||
char buf[kMaxLine];
|
||||
va_list args;
|
||||
va_start(args, fmt);
|
||||
int n = vsnprintf(buf, sizeof(buf), fmt, args);
|
||||
va_end(args);
|
||||
if (n < 0) {
|
||||
return;
|
||||
}
|
||||
size_t len = static_cast<size_t>(n);
|
||||
if (len >= sizeof(buf)) {
|
||||
len = sizeof(buf) - 1; // обрезано
|
||||
}
|
||||
g_sink(level, buf, len, g_sink_ctx);
|
||||
}
|
||||
|
||||
} // namespace fgl::ayla::log
|
||||
32
src/ayla/log.hpp
Normal file
32
src/ayla/log.hpp
Normal file
@@ -0,0 +1,32 @@
|
||||
// Логирование ядра: без printf в stdout, вывод через настраиваемый sink.
|
||||
// Вызовы из внутреннего потока сессии; sink должен быть быстрым и реентерабельным.
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
namespace fgl::ayla::log {
|
||||
|
||||
enum Level : int {
|
||||
kDebug = 0,
|
||||
kInfo = 1,
|
||||
kWarn = 2,
|
||||
kError = 3,
|
||||
};
|
||||
|
||||
using Sink = void (*)(int level, const char* msg, size_t len, void* ctx);
|
||||
|
||||
// Устанавливает приёмник логов (глобально). nullptr — логирование отключено.
|
||||
void set_sink(Sink sink, void* ctx);
|
||||
|
||||
// Форматирует в фиксированный буфер (без кучи) и отправляет в sink.
|
||||
void write(int level, const char* fmt, ...) __attribute__((format(printf, 2, 3)));
|
||||
|
||||
int min_level(); // по умолчанию kInfo
|
||||
void set_min_level(int level);
|
||||
|
||||
} // namespace fgl::ayla::log
|
||||
|
||||
#define FGL_LOGD(...) ::fgl::ayla::log::write(::fgl::ayla::log::kDebug, __VA_ARGS__)
|
||||
#define FGL_LOGI(...) ::fgl::ayla::log::write(::fgl::ayla::log::kInfo, __VA_ARGS__)
|
||||
#define FGL_LOGW(...) ::fgl::ayla::log::write(::fgl::ayla::log::kWarn, __VA_ARGS__)
|
||||
#define FGL_LOGE(...) ::fgl::ayla::log::write(::fgl::ayla::log::kError, __VA_ARGS__)
|
||||
214
src/ayla/platform/esp-idf/platform.cpp
Normal file
214
src/ayla/platform/esp-idf/platform.cpp
Normal file
@@ -0,0 +1,214 @@
|
||||
// ESP-IDF-реализация платформенного слоя (lwip + esp_timer + esp_random).
|
||||
// Потоки — через pthread-слой ESP-IDF (поддерживается pthread_join).
|
||||
#ifdef ESP_PLATFORM
|
||||
|
||||
#include "ayla/platform/platform.hpp"
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <lwip/sockets.h>
|
||||
#include <netdb.h>
|
||||
#include <pthread.h>
|
||||
|
||||
#include <sys/select.h>
|
||||
|
||||
#include "esp_random.h"
|
||||
#include "esp_timer.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
#include <new>
|
||||
|
||||
namespace fgl::plat {
|
||||
|
||||
uint64_t now_ms() {
|
||||
return static_cast<uint64_t>(esp_timer_get_time()) / 1000ull;
|
||||
}
|
||||
|
||||
bool random(uint8_t* buf, size_t len) {
|
||||
esp_fill_random(buf, len);
|
||||
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);
|
||||
// Стек ESP-IDF по умолчанию мал для httpd; задаём явно.
|
||||
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) {
|
||||
pthread_join(reinterpret_cast<pthread_t>(id), nullptr);
|
||||
}
|
||||
|
||||
void sleep_ms(uint32_t ms) { vTaskDelay(pdMS_TO_TICKS(ms)); }
|
||||
|
||||
int tcp_listen(uint16_t port) {
|
||||
int fd = lwip_socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (fd < 0) return -1;
|
||||
int one = 1;
|
||||
lwip_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 (lwip_bind(fd, reinterpret_cast<struct sockaddr*>(&addr), sizeof(addr)) < 0 ||
|
||||
lwip_listen(fd, 4) < 0) {
|
||||
lwip_close(fd);
|
||||
return -1;
|
||||
}
|
||||
return fd;
|
||||
}
|
||||
|
||||
uint16_t tcp_local_port(int fd) {
|
||||
struct sockaddr_in addr {};
|
||||
socklen_t addrlen = sizeof(addr);
|
||||
if (lwip_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 = lwip_accept(listen_fd, reinterpret_cast<struct sockaddr*>(&addr), &addrlen);
|
||||
if (fd < 0) return -1;
|
||||
int one = 1;
|
||||
lwip_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 (lwip_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 = lwip_socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
|
||||
if (fd < 0) continue;
|
||||
int flags = lwip_fcntl(fd, F_GETFL, 0);
|
||||
lwip_fcntl(fd, F_SETFL, flags | O_NONBLOCK);
|
||||
int rc = lwip_connect(fd, ai->ai_addr, ai->ai_addrlen);
|
||||
if (rc == 0) {
|
||||
lwip_fcntl(fd, F_SETFL, flags);
|
||||
break;
|
||||
}
|
||||
if (errno == EINPROGRESS) {
|
||||
fd_set wfds;
|
||||
FD_ZERO(&wfds);
|
||||
FD_SET(fd, &wfds);
|
||||
struct timeval tv {};
|
||||
tv.tv_sec = timeout_ms / 1000;
|
||||
tv.tv_usec = static_cast<long>(timeout_ms % 1000) * 1000L;
|
||||
if (lwip_select(fd + 1, nullptr, &wfds, nullptr, &tv) > 0) {
|
||||
int soerr = 0;
|
||||
socklen_t slen = sizeof(soerr);
|
||||
lwip_getsockopt(fd, SOL_SOCKET, SO_ERROR, &soerr, &slen);
|
||||
if (soerr == 0) {
|
||||
lwip_fcntl(fd, F_SETFL, flags);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
lwip_close(fd);
|
||||
fd = -1;
|
||||
}
|
||||
lwip_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 = lwip_send(fd, p + done, len - done, 0);
|
||||
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 = lwip_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 (lwip_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 lwip_setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)) == 0;
|
||||
}
|
||||
|
||||
bool tcp_poll_readable(int fd, uint32_t timeout_ms) {
|
||||
fd_set rfds;
|
||||
FD_ZERO(&rfds);
|
||||
FD_SET(fd, &rfds);
|
||||
struct timeval tv {};
|
||||
tv.tv_sec = timeout_ms / 1000;
|
||||
tv.tv_usec = static_cast<long>(timeout_ms % 1000) * 1000L;
|
||||
return lwip_select(fd + 1, &rfds, nullptr, nullptr, &tv) > 0;
|
||||
}
|
||||
|
||||
bool tcp_shutdown(int fd) {
|
||||
return lwip_shutdown(fd, SHUT_RDWR) == 0;
|
||||
}
|
||||
|
||||
void tcp_close(int fd) {
|
||||
if (fd >= 0) lwip_close(fd);
|
||||
}
|
||||
|
||||
} // namespace fgl::plat
|
||||
|
||||
#endif // ESP_PLATFORM
|
||||
57
src/ayla/platform/platform.hpp
Normal file
57
src/ayla/platform/platform.hpp
Normal file
@@ -0,0 +1,57 @@
|
||||
// Платформенный слой: сокеты, таймеры, CSPRNG, потоки.
|
||||
// Одна реализация на платформу: platform/posix (Linux) и platform/esp-idf.
|
||||
// Все функции — блокирующие; коды ошибок через возвращаемое значение (-1),
|
||||
// без исключений. IP-адреса — uint32_t в host byte order.
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace fgl::plat {
|
||||
|
||||
// ---- время ----
|
||||
// Монотонные миллисекунды с неопределённой точки отсчёта.
|
||||
uint64_t now_ms();
|
||||
|
||||
// ---- CSPRNG ----
|
||||
// Заполняет buf cryptographically-secure случайными байтами.
|
||||
bool random(uint8_t* buf, size_t len);
|
||||
|
||||
// ---- потоки ----
|
||||
// Идентификатор потока (непрозрачный). Валиден между thread_create и
|
||||
// завершением thread_join.
|
||||
using ThreadId = void*;
|
||||
// Создаёт поток с именем name (для отладки) и стеком stack_bytes (0 — по
|
||||
// умолчанию платформы; для ESP-IDF задавать явно, >= 4096).
|
||||
// out_id может быть nullptr (тогда присоединиться нельзя).
|
||||
bool thread_create(void (*fn)(void*), void* ctx, const char* name,
|
||||
uint32_t stack_bytes, ThreadId* out_id);
|
||||
// Блокирующе ждёт завершения потока. Гарантирует, что код потока больше не
|
||||
// исполняется (объекты потока можно освобождать после возврата).
|
||||
void thread_join(ThreadId id);
|
||||
// Мягкая блокирующая задержка текущего потока.
|
||||
void sleep_ms(uint32_t ms);
|
||||
|
||||
// ---- TCP (клиент и сервер) ----
|
||||
// Создаёт слушающий сокет; возвращает fd или -1. port==0 — любой свободный
|
||||
// (узнать выбранный: tcp_local_port).
|
||||
int tcp_listen(uint16_t port);
|
||||
// Локальный порт сокета (для ephemeral-слушателем).
|
||||
uint16_t tcp_local_port(int fd);
|
||||
// Принимает соединение (блокирующе); возвращает fd клиента или -1.
|
||||
// peer_ip (host byte order) / peer_port могут быть nullptr.
|
||||
int tcp_accept(int listen_fd, uint32_t* peer_ip, uint16_t* peer_port);
|
||||
// Подключается к host:port (host — DNS-имя или dotted-quad) с таймаутом.
|
||||
int tcp_connect(const char* host, uint16_t port, uint32_t timeout_ms);
|
||||
long tcp_send(int fd, const void* buf, size_t len); // >0 / -1
|
||||
// Блокирующее чтение; 0 — EOF, -1 — ошибка/таймаут.
|
||||
long tcp_recv(int fd, void* buf, size_t len);
|
||||
// Таймауты на последующие recv/send (0 — без таймаута).
|
||||
bool tcp_set_timeout(int fd, uint32_t rx_ms, uint32_t tx_ms);
|
||||
// Ожидание читаемости fd (для выходa из блокирующего accept). Таймаут в мс.
|
||||
bool tcp_poll_readable(int fd, uint32_t timeout_ms);
|
||||
// Прерывает блокированные recv/send на сокете (для остановки сервера).
|
||||
bool tcp_shutdown(int fd);
|
||||
void tcp_close(int fd);
|
||||
|
||||
} // namespace fgl::plat
|
||||
216
src/ayla/platform/posix/platform.cpp
Normal file
216
src/ayla/platform/posix/platform.cpp
Normal 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
|
||||
Reference in New Issue
Block a user