#include "ayla/session.hpp" #include #include #include #include #include #include "ayla/envelope.hpp" #include "ayla/httpc.hpp" #include "ayla/json.hpp" #include "ayla/log.hpp" #include "ayla/platform/platform.hpp" namespace fgl::ayla { namespace { constexpr uint32_t kLoopTickMs = 20; constexpr size_t kMaxName = 40; constexpr size_t kMaxQueue = 32; struct Command { uint8_t type; // 1=GET, 2=SET, 3=DELETE char name[kMaxName]; int64_t value; char base_type[10]; int cmd_id; }; bool gen_random_token(char* out, size_t len) { static const char kAlpha[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; uint8_t rnd[32]; size_t need = len <= sizeof(rnd) ? len : sizeof(rnd); if (!plat::random(rnd, need)) return false; for (size_t i = 0; i < len; i++) { out[i] = kAlpha[rnd[i % need] % 62]; } out[len] = '\0'; return true; } // "cmd_id=5&status=200" -> значения; отсутствующие остаются прежними. void parse_query(const char* query, int* cmd_id, int* status) { const char* p = query; while (*p != '\0') { const char* eq = strchr(p, '='); const char* amp = strchr(p, '&'); const char* end = (amp != nullptr) ? amp : p + strlen(p); if (eq != nullptr && eq < end) { size_t klen = static_cast(eq - p); int v = atoi(eq + 1); if (klen == 6 && strncmp(p, "cmd_id", 6) == 0) *cmd_id = v; if (klen == 6 && strncmp(p, "status", 6) == 0) *status = v; } if (amp == nullptr) break; p = amp + 1; } } } // namespace struct Session::Impl { SessionConfig cfg{}; SessionTimings timings{}; SessionCallbacks cbs{}; char host[64] = {}; char dsn[40] = {}; char lanip_key[64] = {}; // ---- httpd-поток (используется только из httpd-потока) ---- HttpServer httpd; SessionCrypto crypto; bool crypto_ready = false; int64_t out_seq = 0; char ke_random2[17] = {}; // Рабочие буферы datapoint-пути (httpd однопоточен; вынесены из стека — // экономия ~7КБ стека потока httpd). char dp_body[kHttpdMaxBody + 1]; char dp_enc_b64[kEnvelopeMaxB64]; char dp_sign_b64[96]; char dp_plain[kEnvelopeMaxPlain]; char dp_data[kEnvelopeMaxPlain]; // ---- разделяемое ---- std::mutex queue_mu; Command queue[kMaxQueue] = {}; uint8_t queue_len = 0; int next_cmd_id = 1; bool batch_open = false; Command batch[kMaxQueue] = {}; uint8_t batch_len = 0; std::atomic state{static_cast(SessionState::kIdle)}; std::atomic last_error{static_cast(SessionError::kNone)}; std::atomic ke_time_ms{0}; // время ответа на KE (0 — не было) std::atomic last_local_reg_ms{0}; std::atomic quiet_until_ms{0}; // пауза local_reg (восстановление) std::atomic retry_at_ms{0}; std::atomic had_poll_since_ke{false}; std::atomic ever_active{false}; // был online хотя бы раз std::atomic want_notify{false}; // после batch commit / перехода online std::atomic delete_pending{false}; std::atomic delete_served{false}; std::atomic decrypt_failed{false}; std::atomic running{false}; std::atomic rekeys{0}; std::atomic pushes_ok{0}; std::atomic pushes_bad{0}; std::atomic cmds_served{0}; plat::ThreadId thread = nullptr; uint8_t backoff_attempts = 0; uint32_t backoff_ms = 0; bool reg_ok = false; // последний local_reg принят (202/200) uint16_t listen_port_actual = 0; // ---------- helpers (вызывается из обоих потоков) ---------- void set_state(SessionState st, SessionError err) { uint8_t prev = state.exchange(static_cast(st), std::memory_order_acq_rel); last_error.store(static_cast(err), std::memory_order_release); if (prev != static_cast(st) && cbs.on_state != nullptr) { cbs.on_state(cbs.ctx, st, err); // без дублирования одинаковых состояний } } // ---------- очередь (mutex) ---------- bool enqueue_locked(const Command& cmd) { // coalescing: SET замещает незабранный SET того же свойства; // GET-дубликат отбрасывается; DELETE — единственный. if (cmd.type == 3) { for (uint8_t i = 0; i < queue_len; i++) { if (queue[i].type == 3) return true; } } else { for (uint8_t i = 0; i < queue_len; i++) { if (queue[i].type == cmd.type && strncmp(queue[i].name, cmd.name, kMaxName) == 0) { if (cmd.type == 2) { queue[i].value = cmd.value; // замещаем return true; } if (cmd.type == 1) return true; // дубликат GET } } } if (queue_len >= cfg.max_queue || queue_len >= kMaxQueue) return false; queue[queue_len++] = cmd; return true; } bool submit(Command cmd) { std::lock_guard lk(queue_mu); bool ok; if (batch_open && cmd.type != 3) { if (batch_len >= kMaxQueue) return false; // coalescing внутри batch for (uint8_t i = 0; i < batch_len; i++) { if (batch[i].type == cmd.type && strncmp(batch[i].name, cmd.name, kMaxName) == 0) { if (cmd.type == 2) { batch[i].value = cmd.value; return true; } if (cmd.type == 1) return true; } } batch[batch_len++] = cmd; ok = true; } else { ok = enqueue_locked(cmd); } return ok; } // ---------- httpd-обработчики (httpd-поток) ---------- static bool http_handler(const HttpRequest& req, HttpResponse& resp, void* ctx); void handle_key_exchange(const HttpRequest& req, HttpResponse& resp); void handle_commands(HttpResponse& resp); void handle_datapoint(const HttpRequest& req, HttpResponse& resp); void build_get_payload(char* out, size_t out_cap, const Command& c, int* seq_out); void build_set_payload(char* out, size_t out_cap, const Command& c); // ---------- session-поток ---------- static void session_thread_trampoline(void* ctx) { static_cast(ctx)->session_loop(); } void session_loop(); bool send_local_reg(bool notify, bool first); }; // --------------------------------------------------------------------------- // httpd // --------------------------------------------------------------------------- bool Session::Impl::http_handler(const HttpRequest& req, HttpResponse& resp, void* ctx) { auto* impl = static_cast(ctx); if (strcmp(req.method, "POST") == 0) { if (strcmp(req.target, "/local_lan/key_exchange.json") == 0) { impl->handle_key_exchange(req, resp); return true; } if (strcmp(req.target, "/local_lan/property/datapoint.json") == 0) { impl->handle_datapoint(req, resp); return true; } if (strcmp(req.target, "/local_lan/property/datapoint/ack.json") == 0 || strcmp(req.target, "/local_lan/node/property/datapoint.json") == 0 || strcmp(req.target, "/local_lan/node/property/datapoint/ack.json") == 0) { FGL_LOGD("session: ack/node datapoint (пусто ok)"); resp.status = 200; return true; } } else if (strcmp(req.method, "GET") == 0) { if (strcmp(req.target, "/local_lan/commands.json") == 0) { if (!impl->crypto_ready) { resp.status = 401; return true; } impl->handle_commands(resp); return true; } } resp.status = 404; return true; } void Session::Impl::handle_key_exchange(const HttpRequest& req, HttpResponse& resp) { // Тело: {"key_exchange":{"ver":1,"proto":1,"key_id":N,"random_1":..,"time_1":N,"sec":""}} char body[kHttpdMaxBody + 1]; size_t n = req.body_len < kHttpdMaxBody ? req.body_len : kHttpdMaxBody; memcpy(body, req.body, n); body[n] = '\0'; json::Doc doc; if (!doc.parse(body)) { resp.status = 400; return; } int64_t ver = 0, proto = 0, key_id = 0, time_1 = 0; char random_1[32] = {}, sec[8] = {}; bool have_r1 = doc.get_string("random_1", random_1, sizeof(random_1)); bool have_t1 = doc.get_int("time_1", &time_1); bool have_sec = doc.get_string("sec", sec, sizeof(sec)); bool have_ver = doc.get_int("ver", &ver); bool have_proto = doc.get_int("proto", &proto); bool have_kid = doc.get_int("key_id", &key_id); if (!have_r1 || !have_t1 || !have_ver || !have_proto || !have_kid) { FGL_LOGW("session: key_exchange неполный"); resp.status = 400; return; } if (ver != 1 || proto != 1 || (have_sec && sec[0] != '\0')) { FGL_LOGW("session: key_exchange ver/proto/sec не поддержаны"); set_state(SessionState::kKeyError, SessionError::kBadKeyExchange); resp.status = 426; return; } { // sec длиннее буфера -> get_string=false, но поле есть: setup-режим // не поддерживаем — тоже 426 (PROTOCOL §3.1). const char* sec_start = nullptr; size_t sec_len = 0; jsmntype_t sec_type; if (doc.find("sec", &sec_start, &sec_len, &sec_type) && sec_type == JSMN_STRING && sec_len >= sizeof(sec)) { set_state(SessionState::kKeyError, SessionError::kBadKeyExchange); resp.status = 426; return; } } if (static_cast(key_id) != cfg.lanip_key_id) { FGL_LOGE("session: key_id %lld != %u — ротация ключа?", static_cast(key_id), static_cast(cfg.lanip_key_id)); set_state(SessionState::kKeyError, SessionError::kKeyMismatch); resp.status = 412; return; } if (!gen_random_token(ke_random2, 16)) { resp.status = 500; return; } int64_t time_2 = static_cast(plat::now_ms()) * 1000000ll; crypto_ready = crypto.init(lanip_key, random_1, ke_random2, time_1, time_2); // цепочки сбрасываются тут же if (!crypto_ready) { resp.status = 500; return; } had_poll_since_ke.store(false, std::memory_order_release); ke_time_ms.store(plat::now_ms(), std::memory_order_release); bool rekey = ever_active.load(std::memory_order_acquire); rekeys.fetch_add(1, std::memory_order_relaxed); if (!rekey && state.load(std::memory_order_acquire) != static_cast(SessionState::kRegistering)) { set_state(SessionState::kRegistering, SessionError::kNone); } FGL_LOGI("session: key exchange #%u (rekey=%d)", static_cast(rekeys.load(std::memory_order_relaxed)), rekey); static thread_local char out[128]; json::Writer w(out, sizeof(out)); w.begin_object(); w.key("random_2"); w.string(ke_random2); w.key("time_2"); w.integer(time_2); w.end_object(); resp.status = 200; resp.body = reinterpret_cast(out); resp.body_len = strlen(out); // буфер out живёт до конца ответа (send_response в handle_connection // выполняется синхронно в том же кадре стека httpd-потока). } void Session::Impl::build_get_payload(char* out, size_t out_cap, const Command& c, int* seq_out) { (void)seq_out; json::Writer w(out, out_cap); w.begin_object(); w.key("cmds"); w.begin_array(); w.begin_object(); w.key("cmd"); w.begin_object(); w.key("cmd_id"); w.integer(c.cmd_id); w.key("method"); w.string("GET"); w.key("resource"); char res[80]; snprintf(res, sizeof(res), "property.json?name=%s", c.name); w.string(res); w.key("data"); w.string(""); w.key("uri"); w.string("/local_lan/property/datapoint.json"); w.end_object(); w.end_object(); w.end_array(); w.end_object(); } void Session::Impl::build_set_payload(char* out, size_t out_cap, const Command& c) { json::Writer w(out, out_cap); w.begin_object(); w.key("properties"); w.begin_array(); w.begin_object(); w.key("property"); w.begin_object(); w.key("base_type"); w.string(c.base_type); w.key("name"); w.string(c.name); w.key("value"); if (strcmp(c.base_type, "boolean") == 0) { w.boolean(c.value != 0); } else { w.integer(c.value); } w.key("id"); char id[9]; gen_random_token(id, 8); w.string(id); w.end_object(); w.end_object(); w.end_array(); w.end_object(); } void Session::Impl::handle_commands(HttpResponse& resp) { Command head{}; bool have = false; uint8_t remaining = 0; { std::lock_guard lk(queue_mu); if (queue_len > 0) { head = queue[0]; have = true; queue_len--; memmove(queue, queue + 1, queue_len * sizeof(Command)); remaining = queue_len; } } char payload[512]; if (!have) { payload[0] = '{'; payload[1] = '}'; payload[2] = '\0'; } else if (head.type == 1) { build_get_payload(payload, sizeof(payload), head, nullptr); } else if (head.type == 2) { build_set_payload(payload, sizeof(payload), head); } else { // DELETE session json::Writer w(payload, sizeof(payload)); w.begin_object(); w.key("cmds"); w.begin_array(); w.begin_object(); w.key("cmd"); w.begin_object(); w.key("cmd_id"); w.integer(0); w.key("method"); w.string("DELETE"); w.key("resource"); w.string("local_reg.json"); w.key("data"); w.string("delete_session"); w.key("uri"); w.string("/local_lan"); w.end_object(); w.end_object(); w.end_array(); w.end_object(); delete_served.store(true, std::memory_order_release); delete_pending.store(false, std::memory_order_release); FGL_LOGI("session: delete_session выдан модулю"); } static thread_local char envelope[kEnvelopeMaxB64]; int64_t seq = out_seq++; if (!envelope_pack(crypto.app, seq, payload, envelope, sizeof(envelope))) { resp.status = 500; return; } cmds_served.fetch_add(1, std::memory_order_relaxed); had_poll_since_ke.store(true, std::memory_order_release); uint8_t st8 = state.load(std::memory_order_acquire); if (st8 == static_cast(SessionState::kRegistering) || st8 == static_cast(SessionState::kRecovering) || st8 == static_cast(SessionState::kOffline)) { // Опрос команд = сессия жива (в т.ч. после re-key при recovering/offline). ever_active.store(true, std::memory_order_release); decrypt_failed.store(false, std::memory_order_release); set_state(SessionState::kOnline, SessionError::kNone); want_notify.store(true, std::memory_order_release); } resp.status = remaining > 0 ? 206 : 200; resp.body = reinterpret_cast(envelope); resp.body_len = strlen(envelope); } void Session::Impl::handle_datapoint(const HttpRequest& req, HttpResponse& resp) { size_t n = req.body_len < kHttpdMaxBody ? req.body_len : kHttpdMaxBody; memcpy(dp_body, req.body, n); dp_body[n] = '\0'; json::Doc wrap; if (!wrap.parse(dp_body)) { resp.status = 400; return; } if (!wrap.get_string("enc", dp_enc_b64, sizeof(dp_enc_b64)) || !wrap.get_string("sign", dp_sign_b64, sizeof(dp_sign_b64))) { resp.status = 400; return; } int64_t seq_no = -1; if (!envelope_unpack(crypto.dev, dp_enc_b64, dp_sign_b64, dp_plain, sizeof(dp_plain), &seq_no)) { pushes_bad.fetch_add(1, std::memory_order_relaxed); FGL_LOGW("session: push не расшифрован/подпись (401); пауза и re-key"); if (state.load(std::memory_order_acquire) == static_cast(SessionState::kOnline)) { decrypt_failed.store(true, std::memory_order_release); set_state(SessionState::kRecovering, SessionError::kDecryptFailed); // Стратегия восстановления (PROTOCOL §4.4): замолчать на > порога // «возврата» (~44-50с) — следующий local_reg заставит модуль re-key. quiet_until_ms.store(plat::now_ms() + timings.recovering_quiet_ms, std::memory_order_release); } resp.status = 401; return; } // Восстановление после 401 с живой цепочкой (бракованная подпись): // сообщение расшифровано — отменяем тишину и возвращаем online. if (decrypt_failed.exchange(false, std::memory_order_acq_rel)) { quiet_until_ms.store(0, std::memory_order_release); set_state(SessionState::kOnline, SessionError::kNone); FGL_LOGI("session: цепочка восстановлена (успешный push после 401)"); } // {"seq_no":N,"data":{"name":..,"value":..}} json::Doc top; if (!top.parse(dp_plain)) { pushes_ok.fetch_add(1, std::memory_order_relaxed); resp.status = 200; return; } const char* data_start = nullptr; size_t data_len = 0; jsmntype_t data_type; PropertyEvent ev{}; ev.seq_no = seq_no; if (top.find("data", &data_start, &data_len, &data_type) && data_type == JSMN_OBJECT && data_len < sizeof(dp_data)) { memcpy(dp_data, data_start, data_len); dp_data[data_len] = '\0'; { json::Doc data_doc; if (data_doc.parse(dp_data)) { char name[kMaxName]; if (data_doc.get_string("name", name, sizeof(name))) { snprintf(ev.name, sizeof(ev.name), "%s", name); int64_t iv = 0; bool bv = false; const char* sv = nullptr; size_t sv_len = 0; jsmntype_t vt; if (data_doc.get_int("value", &iv)) { ev.is_int = true; ev.int_value = iv; } else if (data_doc.get_bool("value", &bv)) { ev.is_bool = true; ev.bool_value = bv; } else if (data_doc.find("value", &sv, &sv_len, &vt) && vt == JSMN_STRING && sv_len + 1 <= sizeof(ev.str_value)) { memcpy(ev.str_value, sv, sv_len); ev.str_value[sv_len] = '\0'; } int cmd_id = -1, status = 0; parse_query(req.query, &cmd_id, &status); ev.cmd_id = cmd_id; ev.status = status; if (cbs.on_property != nullptr) { cbs.on_property(cbs.ctx, ev); } } } } } pushes_ok.fetch_add(1, std::memory_order_relaxed); resp.status = 200; } // --------------------------------------------------------------------------- // session-поток: local_reg / keep-alive / backoff / таймауты // --------------------------------------------------------------------------- bool Session::Impl::send_local_reg(bool notify, bool first) { char path[96]; if (first) { snprintf(path, sizeof(path), "/local_reg.json?dsn=%s", dsn); } else { snprintf(path, sizeof(path), "/local_reg.json"); } char local_ip[24]; if (!plat::local_ip_for(host, local_ip, sizeof(local_ip))) { FGL_LOGW("session: local_ip_for(%s) failed", host); return false; } char body[160]; json::Writer w(body, sizeof(body)); w.begin_object(); w.key("local_reg"); w.begin_object(); w.key("ip"); w.string(local_ip); w.key("notify"); w.boolean(notify); w.key("port"); w.integer(listen_port_actual); w.key("uri"); w.string("/local_lan"); w.end_object(); w.end_object(); if (!w.ok()) return false; HttpcRequest req; req.method = first ? "POST" : "PUT"; req.host = host; req.port = cfg.device_port; req.path = path; req.body = reinterpret_cast(body); req.body_len = strlen(body); req.timeout_ms = 5000; HttpcResponse resp; if (!httpc_perform(req, &resp)) { return false; } last_local_reg_ms.store(plat::now_ms(), std::memory_order_release); if (resp.status == 503) { set_state(SessionState::kOffline, SessionError::kNoSlot); retry_at_ms.store(plat::now_ms() + timings.no_slot_retry_ms, std::memory_order_release); FGL_LOGW("session: local_reg -> 503 (нет слотов)"); backoff_attempts = 0; backoff_ms = 0; reg_ok = false; // сессия не активировалась — следующий local_reg POST?dsn return true; // транспорт ок — это протокольный ответ } if (resp.status != 202 && resp.status != 200) { FGL_LOGW("session: local_reg -> %d", resp.status); return false; } if (state.load(std::memory_order_acquire) == static_cast(SessionState::kIdle)) { set_state(SessionState::kRegistering, SessionError::kNone); } backoff_attempts = 0; backoff_ms = 0; reg_ok = true; return true; } void Session::Impl::session_loop() { bool first_reg = true; while (running.load(std::memory_order_acquire)) { uint64_t now = plat::now_ms(); SessionState st = static_cast(state.load(std::memory_order_acquire)); // kKeyError — устойчивая ошибка: конфиг менять вручную, не дёргаем модуль. if (st == SessionState::kKeyError) { plat::sleep_ms(kLoopTickMs); continue; } // Тишина (восстановление после «KE без poll»). if (now < quiet_until_ms.load(std::memory_order_acquire)) { plat::sleep_ms(kLoopTickMs); continue; } // Активация: KE отвечен, но опроса нет. uint64_t ke_time = ke_time_ms.load(std::memory_order_acquire); if (st == SessionState::kRegistering && ke_time != 0 && !had_poll_since_ke.load(std::memory_order_acquire) && now - ke_time > timings.activation_timeout_ms) { FGL_LOGW("session: активация не наступила (KE без poll) — пауза %ums", static_cast(timings.recovering_quiet_ms)); ke_time_ms.store(0, std::memory_order_release); quiet_until_ms.store(now + timings.recovering_quiet_ms, std::memory_order_release); set_state(SessionState::kRecovering, SessionError::kActivationTimeout); continue; } // Backoff / отложенный повтор. if (now < retry_at_ms.load(std::memory_order_acquire)) { plat::sleep_ms(kLoopTickMs); continue; } // local_reg: по keep-alive, по notify (batch/online) или первичный. bool queue_nonempty; { std::lock_guard lk(queue_mu); queue_nonempty = queue_len > 0; } bool notify = queue_nonempty || want_notify.exchange(false, std::memory_order_acq_rel); uint64_t last_reg = last_local_reg_ms.load(std::memory_order_acquire); bool due = notify || last_reg == 0 || now - last_reg >= timings.keepalive_ms; if (!due) { plat::sleep_ms(kLoopTickMs); continue; } bool first = first_reg || !reg_ok; if (!send_local_reg(queue_nonempty, first)) { // Транспортная ошибка: backoff. backoff_ms = backoff_ms == 0 ? timings.backoff_base_ms : (backoff_ms * 8) / 5; // x1.6 if (backoff_ms > timings.backoff_max_ms) { backoff_ms = timings.backoff_max_ms; } backoff_attempts++; if (backoff_attempts >= timings.backoff_attempts) { set_state(SessionState::kOffline, SessionError::kUnreachable); retry_at_ms.store(now + timings.backoff_max_ms, std::memory_order_release); backoff_attempts = 0; backoff_ms = 0; } else { retry_at_ms.store(now + backoff_ms, std::memory_order_release); } continue; } if (reg_ok) first_reg = false; } } // --------------------------------------------------------------------------- // Session (публичный класс) // --------------------------------------------------------------------------- Session* Session::create(const SessionConfig& cfg, const SessionCallbacks& cbs) { if (cfg.host == nullptr || cfg.dsn == nullptr || cfg.lanip_key == nullptr) { return nullptr; } if (strlen(cfg.lanip_key) >= sizeof(Impl::lanip_key) || strlen(cfg.dsn) >= sizeof(Impl::dsn) || strlen(cfg.host) >= sizeof(Impl::host)) { return nullptr; // не помещается во внутренние копии } auto* impl = new (std::nothrow) Impl(); if (impl == nullptr) return nullptr; impl->cfg = cfg; impl->cbs = cbs; if (impl->cfg.max_queue == 0) impl->cfg.max_queue = 16; if (impl->cfg.keepalive_ms == 0) impl->cfg.keepalive_ms = 15000; impl->timings.keepalive_ms = impl->cfg.keepalive_ms; snprintf(impl->host, sizeof(impl->host), "%s", cfg.host); snprintf(impl->dsn, sizeof(impl->dsn), "%s", cfg.dsn); snprintf(impl->lanip_key, sizeof(impl->lanip_key), "%s", cfg.lanip_key); auto* s = new (std::nothrow) Session(cfg, cbs); if (s == nullptr) { delete impl; return nullptr; } s->impl_ = impl; return s; } Session::Session(const SessionConfig&, const SessionCallbacks&) : impl_(nullptr) {} SessionError Session::last_error() const { if (impl_ == nullptr) return SessionError::kNone; return static_cast(impl_->last_error.load(std::memory_order_acquire)); } SessionState Session::state() const { if (impl_ == nullptr) return SessionState::kIdle; return static_cast(impl_->state.load(std::memory_order_acquire)); } uint16_t Session::listen_port() const { return impl_ != nullptr ? impl_->listen_port_actual : 0; } uint32_t Session::rekey_count() const { return impl_ != nullptr ? impl_->rekeys.load(std::memory_order_relaxed) : 0; } uint32_t Session::pushes_ok() const { return impl_ != nullptr ? impl_->pushes_ok.load(std::memory_order_relaxed) : 0; } uint32_t Session::pushes_bad() const { return impl_ != nullptr ? impl_->pushes_bad.load(std::memory_order_relaxed) : 0; } uint32_t Session::commands_served() const { return impl_ != nullptr ? impl_->cmds_served.load(std::memory_order_relaxed) : 0; } bool Session::had_activity() const { return impl_ != nullptr && impl_->had_poll_since_ke.load(std::memory_order_acquire); } Session::~Session() { stop(); delete impl_; } bool Session::start() { if (impl_ == nullptr || impl_->running.load(std::memory_order_acquire)) { return false; } if (!impl_->httpd.start(impl_->cfg.listen_port, Impl::http_handler, impl_, "fgl_session")) { return false; } impl_->listen_port_actual = impl_->httpd.port(); impl_->running.store(true, std::memory_order_release); if (!plat::thread_create(Impl::session_thread_trampoline, impl_, "fgl_sess", 8192, &impl_->thread)) { impl_->running.store(false, std::memory_order_release); impl_->httpd.stop(); return false; } return true; } void Session::stop() { if (impl_ == nullptr || !impl_->running.load(std::memory_order_acquire)) { return; } // Штатное завершение: DELETE-команда + notify local_reg. Сессионный поток // ещё работает и доставит notify; ждём выдачи команды модулю. if (impl_->state.load(std::memory_order_acquire) != static_cast(SessionState::kKeyError)) { delete_session(); uint64_t deadline = plat::now_ms() + impl_->timings.delete_wait_ms; while (plat::now_ms() < deadline && !impl_->delete_served.load(std::memory_order_acquire)) { plat::sleep_ms(10); } } impl_->running.store(false, std::memory_order_release); if (impl_->thread != nullptr) { plat::thread_join(impl_->thread); impl_->thread = nullptr; } impl_->httpd.stop(); // Сброс для возможного рестарта. impl_->delete_served.store(false, std::memory_order_release); impl_->delete_pending.store(false, std::memory_order_release); impl_->quiet_until_ms.store(0, std::memory_order_release); impl_->retry_at_ms.store(0, std::memory_order_release); impl_->ke_time_ms.store(0, std::memory_order_release); impl_->set_state(SessionState::kIdle, SessionError::kNone); } bool Session::get_property(const char* name) { if (impl_ == nullptr || name == nullptr || strlen(name) >= kMaxName) { return false; } Command cmd{}; cmd.type = 1; snprintf(cmd.name, sizeof(cmd.name), "%s", name); { std::lock_guard lk(impl_->queue_mu); cmd.cmd_id = impl_->next_cmd_id++; } if (!impl_->submit(cmd)) return false; impl_->want_notify.store(true, std::memory_order_release); return true; } bool Session::set_property(const char* name, int64_t value, const char* base_type) { if (impl_ == nullptr || name == nullptr || strlen(name) >= kMaxName) { return false; } Command cmd{}; cmd.type = 2; snprintf(cmd.name, sizeof(cmd.name), "%s", name); cmd.value = value; snprintf(cmd.base_type, sizeof(cmd.base_type), "%s", base_type); { std::lock_guard lk(impl_->queue_mu); cmd.cmd_id = impl_->next_cmd_id++; } if (!impl_->submit(cmd)) return false; impl_->want_notify.store(true, std::memory_order_release); return true; } void Session::set_timings_for_test(const SessionTimings& t) { if (impl_ == nullptr) return; SessionTimings tmp = t; if (tmp.keepalive_ms < 100) tmp.keepalive_ms = 100; // анти-спам impl_->timings = tmp; } bool Session::begin_batch() { std::lock_guard lk(impl_->queue_mu); if (impl_->batch_open) return false; impl_->batch_open = true; impl_->batch_len = 0; return true; } bool Session::commit_batch() { { std::lock_guard lk(impl_->queue_mu); if (!impl_->batch_open) return false; bool ok = true; for (uint8_t i = 0; i < impl_->batch_len; i++) { if (!impl_->enqueue_locked(impl_->batch[i])) ok = false; } impl_->batch_open = false; impl_->batch_len = 0; if (!ok) return false; } impl_->want_notify.store(true, std::memory_order_release); return true; } bool Session::abort_batch() { std::lock_guard lk(impl_->queue_mu); if (!impl_->batch_open) return false; impl_->batch_open = false; impl_->batch_len = 0; return true; } bool Session::delete_session() { if (impl_ == nullptr) return false; Command cmd{}; cmd.type = 3; snprintf(cmd.name, sizeof(cmd.name), "local_reg.json"); { std::lock_guard lk(impl_->queue_mu); if (!impl_->enqueue_locked(cmd)) return false; } impl_->delete_pending.store(true, std::memory_order_release); impl_->want_notify.store(true, std::memory_order_release); return true; } } // namespace fgl::ayla