#pragma once #include #include #include #include #include #include #include #include #include #include #include #include #include #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 slots; // length == capacity (typ. 25) std::unique_ptr 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 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; using SlotMapChangeHandler = std::function; using AccessChangeHandler = std::function; CarrierStore() = default; CarrierStore(const CarrierStore&) = delete; CarrierStore& operator=(const CarrierStore&) = delete; CarrierStore(CarrierStore&&) = delete; CarrierStore& operator=(CarrierStore&&) = delete; 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); } 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(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 ids() const { std::vector 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> 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 slots; CarrierIDStatus id_state; SlotMapStatus sm_state; CarrierAccessStatus acc_state; }; // Carrier record layout, big-endian throughout: // [u8 magic = 0xC4][u8 version = 1] // [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] static constexpr uint8_t kMagic = 0xC4; static constexpr uint8_t kVersion = 0x01; void install_(const std::string& carrierid, uint8_t port_id, std::vector slots, std::optional id_state, std::optional sm_state, std::optional acc_state) { auto fsm = std::make_unique(); 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(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(c->fsm->id_status()), static_cast(c->fsm->slot_map_status()), static_cast(c->fsm->access_status()), c->port_id}; out.write(reinterpret_cast(header), sizeof(header)); auto write_be16 = [&](uint16_t v) { uint8_t b[2] = {static_cast(v >> 8), static_cast(v)}; out.write(reinterpret_cast(b), 2); }; write_be16(static_cast(c->slots.size())); for (const auto& s : c->slots) { out.put(static_cast(s.state)); } write_be16(static_cast(c->carrierid.size())); out.write(c->carrierid.data(), static_cast(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 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(header), sizeof(header)); if (!in || header[0] != kMagic || header[1] != kVersion) return std::nullopt; Record r; r.id_state = static_cast(header[2]); r.sm_state = static_cast(header[3]); r.acc_state = static_cast(header[4]); r.port_id = header[5]; auto read_be16 = [&](uint16_t& v) { uint8_t b[2]; in.read(reinterpret_cast(b), 2); if (!in) return false; v = static_cast((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::eof()) return std::nullopt; s.state = static_cast(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 carriers_; IDChangeHandler on_id_; SlotMapChangeHandler on_sm_; AccessChangeHandler on_acc_; bool persistent_ = false; std::filesystem::path journal_dir_; uint64_t next_seq_ = 0; }; class LoadPortStore { public: using TransferChangeHandler = std::function; using ReservationChangeHandler = std::function; using AssociationChangeHandler = std::function; 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 ids() const { std::vector out; out.reserve(ports_.size()); for (const auto& kv : ports_) out.push_back(kv.first); return out; } // ---- Persistence ---------------------------------------------------- // One file per port: `.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: // [u8 magic = 0xC5][u8 version = 1] // [u8 port_id][u8 tx_state][u8 rs_state][u8 as_state] // [u16 cid_len][cid_len × u8 carrierid] static constexpr uint8_t kMagic = 0xC5; static constexpr uint8_t kVersion = 0x01; void install_(uint8_t port_id, std::string carrierid, std::optional tx, std::optional rs, std::optional as) { auto fsm = std::make_unique(); 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(lp->fsm->transfer_state()), static_cast(lp->fsm->reservation_state()), static_cast(lp->fsm->association_state())}; out.write(reinterpret_cast(header), sizeof(header)); uint16_t cid_len = static_cast(lp->carrierid.size()); uint8_t lb[2] = {static_cast(cid_len >> 8), static_cast(cid_len)}; out.write(reinterpret_cast(lb), 2); out.write(lp->carrierid.data(), static_cast(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 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(header), sizeof(header)); if (!in || header[0] != kMagic || header[1] != kVersion) return std::nullopt; Record r; r.port_id = header[2]; r.tx_state = static_cast(header[3]); r.rs_state = static_cast(header[4]); r.as_state = static_cast(header[5]); uint8_t lb[2]; in.read(reinterpret_cast(lb), 2); if (!in) return std::nullopt; uint16_t cid_len = static_cast((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 ports_; TransferChangeHandler on_tx_; ReservationChangeHandler on_rs_; AssociationChangeHandler on_as_; bool persistent_ = false; std::filesystem::path journal_dir_; }; } // namespace secsgem::gem