9876dd9b5a
tests / build-and-test (push) Successful in 2m59s
tests / thread-sanitizer (push) Successful in 3m36s
tests / tshark-dissector (push) Successful in 2m25s
tests / secs4j-interop (push) Successful in 59s
tests / python-interop (push) Successful in 3m20s
tests / libfuzzer (push) Successful in 3m40s
Completes the daemon's GEM300 surface and adds two new test tiers. D10 — E87 carriers: CarrierStore gains the HandlerSlot observer pattern (add_id/slot_map/access_handler). The daemon's id-observer forwards host S3F17 decisions onto the Subscribe stream as CarrierAction (PROCEED on a Confirmed transition, CANCEL on CancelCarrier); ReportCarrier drives the flow tool-side: WAITING creates the carrier + records the slot map, IN_ACCESS/COMPLETE advance the access FSM (INVALID_OBJECT on unknown, CANNOT_DO_NOW on an illegal transition). E16 — operations RPCs: Describe (full name inventory: variables/events/ alarms/commands/constants + device header), FlushSpool (purge or drain), SendTerminalMessage (S10F1 tool->host, honest CANNOT_DO_NOW when no host and stream 10 isn't spoolable). Stream responsiveness: Subscribe/WatchHealth poll at 100ms (was 500ms) so a cancelled stream frees its sync-server worker thread promptly — this was found by the new stress test, which hung under Subscribe churn at 500ms. Tests: - A randomized concurrent RPC stress case: 4 threads x 250 seeded ops (set/get/fire/alarm/control-state/describe + Subscribe churn), asserts no failed RPC and a still-responsive engine afterward; prints its seed; a strong TSan target. - A virtual fab (interop/virtual_fab.py + the `fab` compose service / tools/spawn_fab.sh): N daemons, each with a secsgem-py host AND a secsgem_client tool, driven by seeded random traffic with end-to-end invariant checks (set/get round-trips, event->S6F11 and alarm->S5F1 delivery, command->tool->completion). Verified green at N=3 (~150 ops/eq, all commands round-tripped, 0 violations). Wired into run_interop.sh (now 13 steps). Also fixes the CI break from the previous commit: the Python-client lane's test_values.py step lacked PYTHONPATH=clients/python (now step-level env). Two bugs found and fixed while building this, both mine from this batch: 1. carrier test hung on a CancelCarrier of a still-NotConfirmed carrier — a self-transition the FSM doesn't signal, so the observer never fired and the stream Read blocked forever. Fixed to cancel a Confirmed carrier; the NotConfirmed edge is documented as a known E87 limitation. 2. the 500ms stream poll above. Daemon suite 7 cases / 214 assertions; core 475 / 3097; virtual fab green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
539 lines
20 KiB
C++
539 lines
20 KiB
C++
#pragma once
|
||
|
||
#include <algorithm>
|
||
#include <cstdint>
|
||
#include <cstdio>
|
||
#include <filesystem>
|
||
#include <fstream>
|
||
#include <functional>
|
||
#include <map>
|
||
#include <memory>
|
||
#include <optional>
|
||
#include <string>
|
||
#include <system_error>
|
||
#include <utility>
|
||
#include <vector>
|
||
|
||
#include "secsgem/gem/handler_slot.hpp"
|
||
#include "secsgem/gem/carrier_state.hpp"
|
||
#include "secsgem/gem/load_port_state.hpp"
|
||
|
||
namespace secsgem::gem {
|
||
|
||
// One slot in a carrier slot map. E87-0716 slot states: Empty (0),
|
||
// NotEmpty (1), Undefined (3) — we keep the wire byte directly rather
|
||
// than wrapping it in an enum, since the application may carry tool-
|
||
// specific extensions.
|
||
struct CarrierSlot {
|
||
uint8_t state = 3; // undefined by default
|
||
};
|
||
|
||
struct Carrier {
|
||
std::string carrierid;
|
||
uint8_t port_id = 0; // 0 = unbound; ports are 1-based
|
||
std::vector<CarrierSlot> slots; // length == capacity (typ. 25)
|
||
std::unique_ptr<CarrierStateMachine> fsm;
|
||
std::filesystem::path journal_path; // empty when persistence disabled
|
||
};
|
||
|
||
struct LoadPort {
|
||
uint8_t port_id;
|
||
std::string carrierid; // empty when nothing associated
|
||
std::unique_ptr<LoadPortStateMachine> fsm;
|
||
std::filesystem::path journal_path; // empty when persistence disabled
|
||
};
|
||
|
||
// Both stores are non-movable for the same reason ProcessJobStore is —
|
||
// per-row FSMs close over `this` via their change-handler lambdas.
|
||
class CarrierStore {
|
||
public:
|
||
using IDChangeHandler =
|
||
std::function<void(const std::string& carrierid, CarrierIDStatus from,
|
||
CarrierIDStatus to, CarrierIDEvent)>;
|
||
using SlotMapChangeHandler =
|
||
std::function<void(const std::string& carrierid, SlotMapStatus from,
|
||
SlotMapStatus to, SlotMapEvent)>;
|
||
using AccessChangeHandler =
|
||
std::function<void(const std::string& carrierid, CarrierAccessStatus from,
|
||
CarrierAccessStatus to, CarrierAccessEvent)>;
|
||
|
||
CarrierStore() = default;
|
||
CarrierStore(const CarrierStore&) = delete;
|
||
CarrierStore& operator=(const CarrierStore&) = delete;
|
||
CarrierStore(CarrierStore&&) = delete;
|
||
CarrierStore& operator=(CarrierStore&&) = delete;
|
||
|
||
// set_ replaces the primary handler (legacy semantics); add_ appends an
|
||
// observer that survives set_ calls (see handler_slot.hpp). The gRPC
|
||
// daemon observes via add_ to forward E87 actions onto its tool stream.
|
||
void set_id_handler(IDChangeHandler h) { on_id_ = std::move(h); }
|
||
void set_slot_map_handler(SlotMapChangeHandler h) { on_sm_ = std::move(h); }
|
||
void set_access_handler(AccessChangeHandler h) { on_acc_ = std::move(h); }
|
||
void add_id_handler(IDChangeHandler h) { on_id_.add(std::move(h)); }
|
||
void add_slot_map_handler(SlotMapChangeHandler h) { on_sm_.add(std::move(h)); }
|
||
void add_access_handler(AccessChangeHandler h) { on_acc_.add(std::move(h)); }
|
||
|
||
enum class CreateResult { Created, Denied_AlreadyExists };
|
||
|
||
CreateResult create(std::string carrierid, uint8_t port_id = 0,
|
||
std::size_t capacity = 25) {
|
||
if (carriers_.count(carrierid)) return CreateResult::Denied_AlreadyExists;
|
||
install_(carrierid, port_id, std::vector<CarrierSlot>(capacity),
|
||
/*restore_id=*/std::nullopt, std::nullopt, std::nullopt);
|
||
if (persistent_) write_record_(carrierid);
|
||
return CreateResult::Created;
|
||
}
|
||
|
||
bool has(const std::string& cid) const { return carriers_.count(cid) > 0; }
|
||
const Carrier* get(const std::string& cid) const {
|
||
auto it = carriers_.find(cid);
|
||
return it == carriers_.end() ? nullptr : &it->second;
|
||
}
|
||
Carrier* get(const std::string& cid) {
|
||
auto it = carriers_.find(cid);
|
||
return it == carriers_.end() ? nullptr : &it->second;
|
||
}
|
||
|
||
bool fire_id_event(const std::string& cid, CarrierIDEvent e) {
|
||
auto* c = get(cid);
|
||
if (!(c && c->fsm->on_id_event(e))) return false;
|
||
if (persistent_) write_record_(cid);
|
||
return true;
|
||
}
|
||
bool fire_slot_map_event(const std::string& cid, SlotMapEvent e) {
|
||
auto* c = get(cid);
|
||
if (!(c && c->fsm->on_slot_map_event(e))) return false;
|
||
if (persistent_) write_record_(cid);
|
||
return true;
|
||
}
|
||
bool fire_access_event(const std::string& cid, CarrierAccessEvent e) {
|
||
auto* c = get(cid);
|
||
if (!(c && c->fsm->on_access_event(e))) return false;
|
||
if (persistent_) write_record_(cid);
|
||
return true;
|
||
}
|
||
|
||
// Application sets slots[i].state directly; persistence sync is opt-in
|
||
// through this helper so the journal records the new slot map.
|
||
void set_slot_state(const std::string& cid, std::size_t slot_idx,
|
||
uint8_t state) {
|
||
auto* c = get(cid);
|
||
if (!c || slot_idx >= c->slots.size()) return;
|
||
c->slots[slot_idx].state = state;
|
||
if (persistent_) write_record_(cid);
|
||
}
|
||
|
||
bool remove(const std::string& cid) {
|
||
auto it = carriers_.find(cid);
|
||
if (it == carriers_.end()) return false;
|
||
if (persistent_ && !it->second.journal_path.empty()) {
|
||
std::error_code ec;
|
||
std::filesystem::remove(it->second.journal_path, ec);
|
||
}
|
||
carriers_.erase(it);
|
||
return true;
|
||
}
|
||
std::size_t size() const { return carriers_.size(); }
|
||
std::vector<std::string> ids() const {
|
||
std::vector<std::string> out;
|
||
out.reserve(carriers_.size());
|
||
for (const auto& kv : carriers_) out.push_back(kv.first);
|
||
return out;
|
||
}
|
||
|
||
// ---- Persistence ----------------------------------------------------
|
||
// Mirrors SpoolStore::enable_persistence: scan `dir` for *.car records,
|
||
// replay them into the in-memory map, then keep the directory in sync
|
||
// on subsequent create / fire_* / remove calls. Atomic writes via
|
||
// .tmp + rename. Records that fail magic/version are silently dropped
|
||
// so a single bad file can't poison the whole store.
|
||
void enable_persistence(std::filesystem::path dir) {
|
||
namespace fs = std::filesystem;
|
||
journal_dir_ = std::move(dir);
|
||
persistent_ = true;
|
||
std::error_code ec;
|
||
fs::create_directories(journal_dir_, ec);
|
||
|
||
std::vector<std::pair<uint64_t, fs::path>> existing;
|
||
for (auto& entry : fs::directory_iterator(journal_dir_, ec)) {
|
||
if (!entry.is_regular_file()) continue;
|
||
const auto& p = entry.path();
|
||
if (p.extension() != ".car") continue;
|
||
uint64_t seq = 0;
|
||
try { seq = std::stoull(p.stem().string()); }
|
||
catch (...) { continue; }
|
||
existing.emplace_back(seq, p);
|
||
}
|
||
std::sort(existing.begin(), existing.end(),
|
||
[](const auto& a, const auto& b) { return a.first < b.first; });
|
||
|
||
for (auto& [seq, path] : existing) {
|
||
auto rec = load_record_(path);
|
||
if (!rec) { fs::remove(path, ec); continue; }
|
||
install_(rec->carrierid, rec->port_id, std::move(rec->slots),
|
||
rec->id_state, rec->sm_state, rec->acc_state);
|
||
auto* c = get(rec->carrierid);
|
||
if (c) c->journal_path = path;
|
||
if (seq >= next_seq_) next_seq_ = seq + 1;
|
||
}
|
||
}
|
||
|
||
bool persistence_enabled() const { return persistent_; }
|
||
std::filesystem::path journal_dir() const { return journal_dir_; }
|
||
|
||
private:
|
||
struct Record {
|
||
std::string carrierid;
|
||
uint8_t port_id;
|
||
std::vector<CarrierSlot> slots;
|
||
CarrierIDStatus id_state;
|
||
SlotMapStatus sm_state;
|
||
CarrierAccessStatus acc_state;
|
||
};
|
||
|
||
// Carrier record (v1), big-endian throughout:
|
||
// [u8 magic = 0xC4][u8 version]
|
||
// [u8 id_state][u8 sm_state][u8 acc_state][u8 port_id]
|
||
// [u16 slot_count][slot_count × u8 slot.state]
|
||
// [u16 cid_len][cid_len × u8 carrierid]
|
||
//
|
||
// Upgrade discipline (mirrors PJ/Substrate): loader accepts any
|
||
// version in [1, kVersion]; future field additions go at the end
|
||
// and are gated behind `if (version >= N)` blocks.
|
||
static constexpr uint8_t kMagic = 0xC4;
|
||
static constexpr uint8_t kVersion = 0x01;
|
||
|
||
void install_(const std::string& carrierid, uint8_t port_id,
|
||
std::vector<CarrierSlot> slots,
|
||
std::optional<CarrierIDStatus> id_state,
|
||
std::optional<SlotMapStatus> sm_state,
|
||
std::optional<CarrierAccessStatus> acc_state) {
|
||
auto fsm = std::make_unique<CarrierStateMachine>();
|
||
if (id_state || sm_state || acc_state) {
|
||
fsm->restore_state(id_state.value_or(CarrierIDStatus::NotConfirmed),
|
||
sm_state.value_or(SlotMapStatus::NotRead),
|
||
acc_state.value_or(CarrierAccessStatus::NotAccessed));
|
||
}
|
||
const std::string id = carrierid;
|
||
fsm->set_id_handler([this, id](CarrierIDStatus f, CarrierIDStatus t,
|
||
CarrierIDEvent e) {
|
||
if (on_id_) on_id_(id, f, t, e);
|
||
});
|
||
fsm->set_slot_map_handler([this, id](SlotMapStatus f, SlotMapStatus t,
|
||
SlotMapEvent e) {
|
||
if (on_sm_) on_sm_(id, f, t, e);
|
||
});
|
||
fsm->set_access_handler([this, id](CarrierAccessStatus f, CarrierAccessStatus t,
|
||
CarrierAccessEvent e) {
|
||
if (on_acc_) on_acc_(id, f, t, e);
|
||
});
|
||
carriers_.emplace(carrierid, Carrier{carrierid, port_id, std::move(slots),
|
||
std::move(fsm), {}});
|
||
}
|
||
|
||
void write_record_(const std::string& carrierid) {
|
||
namespace fs = std::filesystem;
|
||
auto* c = get(carrierid);
|
||
if (!c) return;
|
||
fs::path target = c->journal_path;
|
||
if (target.empty()) {
|
||
char namebuf[32];
|
||
std::snprintf(namebuf, sizeof(namebuf), "%010llu.car",
|
||
static_cast<unsigned long long>(next_seq_++));
|
||
target = journal_dir_ / namebuf;
|
||
c->journal_path = target;
|
||
}
|
||
fs::path tmp = target;
|
||
tmp += ".tmp";
|
||
std::ofstream out(tmp, std::ios::binary | std::ios::trunc);
|
||
if (!out) return;
|
||
uint8_t header[6] = {
|
||
kMagic, kVersion,
|
||
static_cast<uint8_t>(c->fsm->id_status()),
|
||
static_cast<uint8_t>(c->fsm->slot_map_status()),
|
||
static_cast<uint8_t>(c->fsm->access_status()),
|
||
c->port_id};
|
||
out.write(reinterpret_cast<const char*>(header), sizeof(header));
|
||
auto write_be16 = [&](uint16_t v) {
|
||
uint8_t b[2] = {static_cast<uint8_t>(v >> 8), static_cast<uint8_t>(v)};
|
||
out.write(reinterpret_cast<const char*>(b), 2);
|
||
};
|
||
write_be16(static_cast<uint16_t>(c->slots.size()));
|
||
for (const auto& s : c->slots) {
|
||
out.put(static_cast<char>(s.state));
|
||
}
|
||
write_be16(static_cast<uint16_t>(c->carrierid.size()));
|
||
out.write(c->carrierid.data(),
|
||
static_cast<std::streamsize>(c->carrierid.size()));
|
||
out.flush();
|
||
out.close();
|
||
std::error_code ec;
|
||
fs::rename(tmp, target, ec);
|
||
if (ec) fs::remove(tmp, ec);
|
||
}
|
||
|
||
static std::optional<Record> load_record_(const std::filesystem::path& p) {
|
||
std::ifstream in(p, std::ios::binary);
|
||
if (!in) return std::nullopt;
|
||
uint8_t header[6];
|
||
in.read(reinterpret_cast<char*>(header), sizeof(header));
|
||
if (!in || header[0] != kMagic ||
|
||
header[1] < 1 || header[1] > kVersion) return std::nullopt;
|
||
Record r;
|
||
r.id_state = static_cast<CarrierIDStatus>(header[2]);
|
||
r.sm_state = static_cast<SlotMapStatus>(header[3]);
|
||
r.acc_state = static_cast<CarrierAccessStatus>(header[4]);
|
||
r.port_id = header[5];
|
||
auto read_be16 = [&](uint16_t& v) {
|
||
uint8_t b[2];
|
||
in.read(reinterpret_cast<char*>(b), 2);
|
||
if (!in) return false;
|
||
v = static_cast<uint16_t>((uint16_t(b[0]) << 8) | b[1]);
|
||
return true;
|
||
};
|
||
uint16_t slot_count = 0;
|
||
if (!read_be16(slot_count)) return std::nullopt;
|
||
r.slots.resize(slot_count);
|
||
for (auto& s : r.slots) {
|
||
int c = in.get();
|
||
if (c == std::char_traits<char>::eof()) return std::nullopt;
|
||
s.state = static_cast<uint8_t>(c);
|
||
}
|
||
uint16_t cid_len = 0;
|
||
if (!read_be16(cid_len)) return std::nullopt;
|
||
r.carrierid.resize(cid_len);
|
||
in.read(r.carrierid.data(), cid_len);
|
||
if (!in) return std::nullopt;
|
||
return r;
|
||
}
|
||
|
||
std::map<std::string, Carrier> carriers_;
|
||
HandlerSlot<const std::string&, CarrierIDStatus, CarrierIDStatus,
|
||
CarrierIDEvent> on_id_;
|
||
HandlerSlot<const std::string&, SlotMapStatus, SlotMapStatus,
|
||
SlotMapEvent> on_sm_;
|
||
HandlerSlot<const std::string&, CarrierAccessStatus, CarrierAccessStatus,
|
||
CarrierAccessEvent> on_acc_;
|
||
bool persistent_ = false;
|
||
std::filesystem::path journal_dir_;
|
||
uint64_t next_seq_ = 0;
|
||
};
|
||
|
||
class LoadPortStore {
|
||
public:
|
||
using TransferChangeHandler =
|
||
std::function<void(uint8_t portid, LoadPortTransferState from,
|
||
LoadPortTransferState to, LoadPortTransferEvent)>;
|
||
using ReservationChangeHandler =
|
||
std::function<void(uint8_t portid, LoadPortReservationStatus from,
|
||
LoadPortReservationStatus to, LoadPortReservationEvent)>;
|
||
using AssociationChangeHandler =
|
||
std::function<void(uint8_t portid, LoadPortAssociationStatus from,
|
||
LoadPortAssociationStatus to, LoadPortAssociationEvent)>;
|
||
|
||
LoadPortStore() = default;
|
||
LoadPortStore(const LoadPortStore&) = delete;
|
||
LoadPortStore& operator=(const LoadPortStore&) = delete;
|
||
LoadPortStore(LoadPortStore&&) = delete;
|
||
LoadPortStore& operator=(LoadPortStore&&) = delete;
|
||
|
||
void set_transfer_handler(TransferChangeHandler h) { on_tx_ = std::move(h); }
|
||
void set_reservation_handler(ReservationChangeHandler h) { on_rs_ = std::move(h); }
|
||
void set_association_handler(AssociationChangeHandler h) { on_as_ = std::move(h); }
|
||
|
||
// Idempotent: re-create a port_id is a no-op.
|
||
bool create(uint8_t port_id) {
|
||
if (ports_.count(port_id)) return false;
|
||
install_(port_id, "", std::nullopt, std::nullopt, std::nullopt);
|
||
if (persistent_) write_record_(port_id);
|
||
return true;
|
||
}
|
||
|
||
bool has(uint8_t pid) const { return ports_.count(pid) > 0; }
|
||
const LoadPort* get(uint8_t pid) const {
|
||
auto it = ports_.find(pid);
|
||
return it == ports_.end() ? nullptr : &it->second;
|
||
}
|
||
LoadPort* get(uint8_t pid) {
|
||
auto it = ports_.find(pid);
|
||
return it == ports_.end() ? nullptr : &it->second;
|
||
}
|
||
|
||
bool associate(uint8_t pid, std::string carrierid) {
|
||
auto* p = get(pid);
|
||
if (!p) return false;
|
||
p->carrierid = std::move(carrierid);
|
||
bool ok = p->fsm->on_association_event(LoadPortAssociationEvent::Associate);
|
||
if (persistent_) write_record_(pid);
|
||
return ok;
|
||
}
|
||
|
||
bool disassociate(uint8_t pid) {
|
||
auto* p = get(pid);
|
||
if (!p) return false;
|
||
p->carrierid.clear();
|
||
bool ok = p->fsm->on_association_event(LoadPortAssociationEvent::Disassociate);
|
||
if (persistent_) write_record_(pid);
|
||
return ok;
|
||
}
|
||
|
||
bool fire_transfer_event(uint8_t pid, LoadPortTransferEvent e) {
|
||
auto* p = get(pid);
|
||
if (!(p && p->fsm->on_transfer_event(e))) return false;
|
||
if (persistent_) write_record_(pid);
|
||
return true;
|
||
}
|
||
bool fire_reservation_event(uint8_t pid, LoadPortReservationEvent e) {
|
||
auto* p = get(pid);
|
||
if (!(p && p->fsm->on_reservation_event(e))) return false;
|
||
if (persistent_) write_record_(pid);
|
||
return true;
|
||
}
|
||
|
||
std::size_t size() const { return ports_.size(); }
|
||
std::vector<uint8_t> ids() const {
|
||
std::vector<uint8_t> out;
|
||
out.reserve(ports_.size());
|
||
for (const auto& kv : ports_) out.push_back(kv.first);
|
||
return out;
|
||
}
|
||
|
||
// ---- Persistence ----------------------------------------------------
|
||
// One file per port: `<port_id 0-padded>.lp`. Port IDs are 1–255 so
|
||
// we can use the port id directly as the filename stem.
|
||
void enable_persistence(std::filesystem::path dir) {
|
||
namespace fs = std::filesystem;
|
||
journal_dir_ = std::move(dir);
|
||
persistent_ = true;
|
||
std::error_code ec;
|
||
fs::create_directories(journal_dir_, ec);
|
||
|
||
for (auto& entry : fs::directory_iterator(journal_dir_, ec)) {
|
||
if (!entry.is_regular_file()) continue;
|
||
const auto& p = entry.path();
|
||
if (p.extension() != ".lp") continue;
|
||
auto rec = load_record_(p);
|
||
if (!rec) { fs::remove(p, ec); continue; }
|
||
install_(rec->port_id, rec->carrierid, rec->tx_state, rec->rs_state,
|
||
rec->as_state);
|
||
auto* lp = get(rec->port_id);
|
||
if (lp) lp->journal_path = p;
|
||
}
|
||
}
|
||
|
||
bool persistence_enabled() const { return persistent_; }
|
||
std::filesystem::path journal_dir() const { return journal_dir_; }
|
||
|
||
private:
|
||
struct Record {
|
||
uint8_t port_id;
|
||
std::string carrierid;
|
||
LoadPortTransferState tx_state;
|
||
LoadPortReservationStatus rs_state;
|
||
LoadPortAssociationStatus as_state;
|
||
};
|
||
|
||
// Load-port record (v1):
|
||
// [u8 magic = 0xC5][u8 version]
|
||
// [u8 port_id][u8 tx_state][u8 rs_state][u8 as_state]
|
||
// [u16 cid_len][cid_len × u8 carrierid]
|
||
//
|
||
// Upgrade discipline: loader accepts any version in [1, kVersion];
|
||
// future fields append at the end behind an `if (version >= N)` gate.
|
||
static constexpr uint8_t kMagic = 0xC5;
|
||
static constexpr uint8_t kVersion = 0x01;
|
||
|
||
void install_(uint8_t port_id, std::string carrierid,
|
||
std::optional<LoadPortTransferState> tx,
|
||
std::optional<LoadPortReservationStatus> rs,
|
||
std::optional<LoadPortAssociationStatus> as) {
|
||
auto fsm = std::make_unique<LoadPortStateMachine>();
|
||
if (tx || rs || as) {
|
||
fsm->restore_state(tx.value_or(LoadPortTransferState::InService),
|
||
rs.value_or(LoadPortReservationStatus::NotReserved),
|
||
as.value_or(LoadPortAssociationStatus::NotAssociated));
|
||
}
|
||
const uint8_t pid = port_id;
|
||
fsm->set_transfer_handler([this, pid](LoadPortTransferState f,
|
||
LoadPortTransferState t,
|
||
LoadPortTransferEvent e) {
|
||
if (on_tx_) on_tx_(pid, f, t, e);
|
||
});
|
||
fsm->set_reservation_handler([this, pid](LoadPortReservationStatus f,
|
||
LoadPortReservationStatus t,
|
||
LoadPortReservationEvent e) {
|
||
if (on_rs_) on_rs_(pid, f, t, e);
|
||
});
|
||
fsm->set_association_handler([this, pid](LoadPortAssociationStatus f,
|
||
LoadPortAssociationStatus t,
|
||
LoadPortAssociationEvent e) {
|
||
if (on_as_) on_as_(pid, f, t, e);
|
||
});
|
||
ports_.emplace(port_id, LoadPort{port_id, std::move(carrierid),
|
||
std::move(fsm), {}});
|
||
}
|
||
|
||
void write_record_(uint8_t pid) {
|
||
namespace fs = std::filesystem;
|
||
auto* lp = get(pid);
|
||
if (!lp) return;
|
||
if (lp->journal_path.empty()) {
|
||
char namebuf[16];
|
||
std::snprintf(namebuf, sizeof(namebuf), "%03u.lp", unsigned(pid));
|
||
lp->journal_path = journal_dir_ / namebuf;
|
||
}
|
||
fs::path tmp = lp->journal_path;
|
||
tmp += ".tmp";
|
||
std::ofstream out(tmp, std::ios::binary | std::ios::trunc);
|
||
if (!out) return;
|
||
uint8_t header[6] = {
|
||
kMagic, kVersion, pid,
|
||
static_cast<uint8_t>(lp->fsm->transfer_state()),
|
||
static_cast<uint8_t>(lp->fsm->reservation_state()),
|
||
static_cast<uint8_t>(lp->fsm->association_state())};
|
||
out.write(reinterpret_cast<const char*>(header), sizeof(header));
|
||
uint16_t cid_len = static_cast<uint16_t>(lp->carrierid.size());
|
||
uint8_t lb[2] = {static_cast<uint8_t>(cid_len >> 8),
|
||
static_cast<uint8_t>(cid_len)};
|
||
out.write(reinterpret_cast<const char*>(lb), 2);
|
||
out.write(lp->carrierid.data(),
|
||
static_cast<std::streamsize>(lp->carrierid.size()));
|
||
out.flush();
|
||
out.close();
|
||
std::error_code ec;
|
||
fs::rename(tmp, lp->journal_path, ec);
|
||
if (ec) fs::remove(tmp, ec);
|
||
}
|
||
|
||
static std::optional<Record> load_record_(const std::filesystem::path& p) {
|
||
std::ifstream in(p, std::ios::binary);
|
||
if (!in) return std::nullopt;
|
||
uint8_t header[6];
|
||
in.read(reinterpret_cast<char*>(header), sizeof(header));
|
||
if (!in || header[0] != kMagic ||
|
||
header[1] < 1 || header[1] > kVersion) return std::nullopt;
|
||
Record r;
|
||
r.port_id = header[2];
|
||
r.tx_state = static_cast<LoadPortTransferState>(header[3]);
|
||
r.rs_state = static_cast<LoadPortReservationStatus>(header[4]);
|
||
r.as_state = static_cast<LoadPortAssociationStatus>(header[5]);
|
||
uint8_t lb[2];
|
||
in.read(reinterpret_cast<char*>(lb), 2);
|
||
if (!in) return std::nullopt;
|
||
uint16_t cid_len = static_cast<uint16_t>((uint16_t(lb[0]) << 8) | lb[1]);
|
||
r.carrierid.resize(cid_len);
|
||
in.read(r.carrierid.data(), cid_len);
|
||
if (cid_len && !in) return std::nullopt;
|
||
return r;
|
||
}
|
||
|
||
std::map<uint8_t, LoadPort> ports_;
|
||
TransferChangeHandler on_tx_;
|
||
ReservationChangeHandler on_rs_;
|
||
AssociationChangeHandler on_as_;
|
||
bool persistent_ = false;
|
||
std::filesystem::path journal_dir_;
|
||
};
|
||
|
||
} // namespace secsgem::gem
|