- session.{hpp,cpp}: state machine (idle/registering/online/recovering/
offline/key_error); httpd-обработчики key_exchange (200/426/412, re-key
прозрачно), commands (одна команда, 206/200, envelope, глобальный seq_no),
datapoint (unpack -> PropertyEvent / 401+тишина 50с для re-key-восстановления);
сессионный поток: local_reg POST?dsn/PUT (local_ip_for), keep-alive, backoff
x1.6->60с, 503->offline/NoSlot, activation-timeout->recovering, delete_session
с ожиданием выдачи; очередь с coalescing + batch; телеметрия; колбэки из
двух потоков с задокументированным контрактом; буферы datapoint-пути в Impl.
- platform: local_ip_for (UDP-connect) posix+esp-idf; стек httpd 24576
(переполнение 16КБ поймано gdb на Release).
- mock_ac.py: мок-модуль, stdlib-only чистый python AES-256 (свёрстан с
pycryptodome); сценарии: 503, no-poll, rekey-every, stale-gap (эмуляция
'вернувшегося' приложения), fail-pushes (битая подпись), garbage-pushes
(обрыв блока), break-outbound (исходящий десинк -> модуль ре-кает на
local_reg, как probe1-3), push-every, fail-first-ke.
- session_runner + test_session_mock.py: 9 сценариев через ctest, включая
самосинхронизацию CBC и восстановление после исходящего десинка.
- Прибор AP-WC1E: активация <=1с; re-key семантика ИСПРАВЛЕНА по живым
тестам: re-key при зазоре local_reg >= ~44-50с (не по возрасту сессии!);
при честном keep-alive 15с сессия стабильна без re-key; PROTOCOL/LEGACY/
PLAN обновлены; восстановление = тишина >порога + возврат.
- CI: 7/7 x3 (gcc-Rel, gcc-ASan/UBSan, clang); ESP-IDF esp32 build complete.
Ревью под-агентом: 2 круга (стек httpd, залипание состояний, dangling cfg,
физика десинка) — APPROVED.
251 lines
6.9 KiB
C++
251 lines
6.9 KiB
C++
// 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) {
|
|
if (pthread_attr_setstacksize(&attr, stack_bytes) != 0) {
|
|
// glibc отвергает < PTHREAD_STACK_MIN; остаётся дефолт (больше — не меньше)
|
|
// логируем только: ядро запрашивает >= PTHREAD_STACK_MIN.
|
|
}
|
|
}
|
|
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;
|
|
}
|
|
|
|
bool local_ip_for(const char* host, char* out, size_t out_cap) {
|
|
struct addrinfo hints {};
|
|
hints.ai_family = AF_INET;
|
|
hints.ai_socktype = SOCK_DGRAM;
|
|
struct addrinfo* list = nullptr;
|
|
if (::getaddrinfo(host, "80", &hints, &list) != 0 || list == nullptr) {
|
|
return false;
|
|
}
|
|
int fd = ::socket(AF_INET, SOCK_DGRAM, 0);
|
|
if (fd < 0) {
|
|
::freeaddrinfo(list);
|
|
return false;
|
|
}
|
|
bool ok = ::connect(fd, list->ai_addr, list->ai_addrlen) == 0;
|
|
struct sockaddr_in local {};
|
|
socklen_t slen = sizeof(local);
|
|
if (ok && ::getsockname(fd, reinterpret_cast<struct sockaddr*>(&local),
|
|
&slen) == 0) {
|
|
const char* s = inet_ntop(AF_INET, &local.sin_addr, out,
|
|
static_cast<socklen_t>(out_cap));
|
|
ok = s != nullptr;
|
|
} else {
|
|
ok = false;
|
|
}
|
|
::freeaddrinfo(list);
|
|
::close(fd);
|
|
return ok;
|
|
}
|
|
|
|
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
|