diff --git a/ASFWDriver/ASFWDriver.cpp b/ASFWDriver/ASFWDriver.cpp index fa945244..96f7b91d 100644 --- a/ASFWDriver/ASFWDriver.cpp +++ b/ASFWDriver/ASFWDriver.cpp @@ -64,6 +64,7 @@ #include "Protocols/AVC/AVCDiscovery.hpp" #include "Protocols/AVC/CMP/CMPClient.hpp" #include "Protocols/AVC/FCPResponseRouter.hpp" +#include "Protocols/SBP2/Session/DriverKitSessionScheduler.hpp" #include "Scheduling/Scheduler.hpp" #include "Service/DriverContext.hpp" #include "Service/LocalRequestWiring.hpp" @@ -306,7 +307,11 @@ kern_return_t IMPL(ASFWDriver, Start) { // Construct the SBP-2 manager dependency, then assemble the single inbound // request dispatch (CSR / FCP / DICE / SBP-2) in one place. - DriverWiring::EnsureSbp2Deps(ctx); + kr = DriverWiring::EnsureSbp2Deps(*this, ctx); + if (kr != kIOReturnSuccess) { + DriverWiring::CleanupStartFailure(ctx); + return kr; + } ASFW::Service::WireLocalRequestDispatch(ctx); EnsureRomScanner(ctx); @@ -593,6 +598,21 @@ void ASFWDriver::AsyncWatchdogTimerFired_Impl(ASFWDriver_AsyncWatchdogTimerFired ScheduleAsyncWatchdog(kAsyncWatchdogPeriodUsec); } +void ASFWDriver::SBP2SessionTimerFired_Impl(ASFWDriver_SBP2SessionTimerFired_Args) { + (void)action; + (void)time; + + if (!ivars || !ivars->context) { + return; + } + auto& ctx = *ivars->context; + if (ctx.stopping.load(std::memory_order_acquire) || !ctx.deps.sbp2SessionScheduler) { + return; + } + + ctx.deps.sbp2SessionScheduler->HandleTimerFired(); +} + void ASFWDriver::ProviderNotificationReady_Impl(ASFWDriver_ProviderNotificationReady_Args) { (void)action; diff --git a/ASFWDriver/ASFWDriver.iig b/ASFWDriver/ASFWDriver.iig index af94954b..e0cd9bd4 100644 --- a/ASFWDriver/ASFWDriver.iig +++ b/ASFWDriver/ASFWDriver.iig @@ -51,6 +51,10 @@ public: uint64_t time) TYPE(IOTimerDispatchSource::TimerOccurred); + virtual void SBP2SessionTimerFired(OSAction* action, + uint64_t time) + TYPE(IOTimerDispatchSource::TimerOccurred); + virtual void ProviderNotificationReady(OSAction* action) TYPE(IOServiceNotificationDispatchSource::ServiceNotificationReady); diff --git a/ASFWDriver/Controller/ControllerCore.hpp b/ASFWDriver/Controller/ControllerCore.hpp index 49d248d3..311cc2fa 100644 --- a/ASFWDriver/Controller/ControllerCore.hpp +++ b/ASFWDriver/Controller/ControllerCore.hpp @@ -86,6 +86,8 @@ class FCPResponseRouter; namespace ASFW::Protocols::SBP2 { class AddressSpaceManager; +class DriverKitSessionScheduler; +class SessionRegistry; } namespace ASFW::IRM { @@ -143,6 +145,8 @@ class ControllerCore final : private Role::IPhyConfigReset, std::shared_ptr avcDiscovery; std::shared_ptr fcpResponseRouter; std::shared_ptr sbp2AddressSpaceManager; + std::shared_ptr sbp2SessionScheduler; + std::shared_ptr sbp2SessionRegistry; // FW-19: local software CSR responder (STATE_SET/CLEAR, BROADCAST_CHANNEL, // TOPOLOGY_MAP) plus its hardware adapters for root status / cycle master. @@ -212,6 +216,9 @@ class ControllerCore final : private Role::IPhyConfigReset, Protocols::SBP2::AddressSpaceManager* GetSbp2AddressSpaceManager() const; void SetSbp2AddressSpaceManager( std::shared_ptr sbp2AddressSpaceManager); + Protocols::SBP2::SessionRegistry* GetSbp2SessionRegistry() const; + void SetSbp2SessionRegistry( + std::shared_ptr sbp2SessionRegistry); IRM::IRMClient* GetIRMClient() const; void SetIRMClient(std::shared_ptr client); diff --git a/ASFWDriver/Controller/ControllerCoreDiscovery.cpp b/ASFWDriver/Controller/ControllerCoreDiscovery.cpp index 63afc49d..98873370 100644 --- a/ASFWDriver/Controller/ControllerCoreDiscovery.cpp +++ b/ASFWDriver/Controller/ControllerCoreDiscovery.cpp @@ -41,6 +41,7 @@ #include "../Hardware/RegisterMap.hpp" #include "../Bus/IRM/IRMClient.hpp" #include "../Protocols/AVC/AVCDiscovery.hpp" +#include "../Protocols/SBP2/Session/SessionRegistry.hpp" #include "../Protocols/AVC/CMP/CMPClient.hpp" #include "../Audio/Protocols/DeviceProtocolFactory.hpp" #include "../Scheduling/Scheduler.hpp" @@ -626,6 +627,10 @@ void ControllerCore::OnDiscoveryScanComplete(Discovery::Generation gen, gen.value); } + if (deps_.sbp2SessionRegistry) { + deps_.sbp2SessionRegistry->RefreshTargets(gen); + } + ASFW_LOG(Discovery, "Discovery complete: %zu devices processed in gen=%u", roms.size(), gen.value); ASFW_LOG(Discovery, "═══════════════════════════════════════════════════════"); diff --git a/ASFWDriver/Controller/ControllerCoreFacades.cpp b/ASFWDriver/Controller/ControllerCoreFacades.cpp index 8c076b71..901a27e6 100644 --- a/ASFWDriver/Controller/ControllerCoreFacades.cpp +++ b/ASFWDriver/Controller/ControllerCoreFacades.cpp @@ -32,6 +32,7 @@ #include "../Bus/IRM/IRMClient.hpp" #include "../Protocols/AVC/AVCDiscovery.hpp" #include "../Protocols/AVC/CMP/CMPClient.hpp" +#include "../Protocols/SBP2/Session/SessionRegistry.hpp" #include "../Audio/Protocols/DeviceProtocolFactory.hpp" #include "../Scheduling/Scheduler.hpp" #include "../Version/DriverVersion.hpp" @@ -105,6 +106,15 @@ void ControllerCore::SetSbp2AddressSpaceManager( deps_.sbp2AddressSpaceManager = std::move(sbp2AddressSpaceManager); } +Protocols::SBP2::SessionRegistry* ControllerCore::GetSbp2SessionRegistry() const { + return deps_.sbp2SessionRegistry.get(); +} + +void ControllerCore::SetSbp2SessionRegistry( + std::shared_ptr sbp2SessionRegistry) { + deps_.sbp2SessionRegistry = std::move(sbp2SessionRegistry); +} + IRM::IRMClient* ControllerCore::GetIRMClient() const { return deps_.irmClient.get(); } void ControllerCore::SetIRMClient(std::shared_ptr client) { diff --git a/ASFWDriver/Controller/ControllerCoreInterrupts.cpp b/ASFWDriver/Controller/ControllerCoreInterrupts.cpp index e229c1ae..d159bf8d 100644 --- a/ASFWDriver/Controller/ControllerCoreInterrupts.cpp +++ b/ASFWDriver/Controller/ControllerCoreInterrupts.cpp @@ -31,6 +31,7 @@ #include "../Hardware/RegisterMap.hpp" #include "../Bus/IRM/IRMClient.hpp" #include "../Protocols/AVC/AVCDiscovery.hpp" +#include "../Protocols/SBP2/Session/SessionRegistry.hpp" #include "../Protocols/AVC/CMP/CMPClient.hpp" #include "../Audio/Protocols/DeviceProtocolFactory.hpp" #include "../Scheduling/Scheduler.hpp" @@ -82,6 +83,9 @@ void ControllerCore::HandleInterrupt(const InterruptSnapshot& snapshot) { if (powerLinkPolicy_) { powerLinkPolicy_->OnBusResetStarted(generation); } + if (deps_.sbp2SessionRegistry) { + deps_.sbp2SessionRegistry->OnBusReset(static_cast(generation)); + } } DispatchAsyncInterrupts(events); LogBusResetCompletionEvents(events, snapshot.timestamp); diff --git a/ASFWDriver/Protocols/SBP2/AddressSpaceManager.hpp b/ASFWDriver/Protocols/SBP2/AddressSpaceManager.hpp index 90801cbd..9f80e069 100644 --- a/ASFWDriver/Protocols/SBP2/AddressSpaceManager.hpp +++ b/ASFWDriver/Protocols/SBP2/AddressSpaceManager.hpp @@ -1,8 +1,8 @@ #pragma once #include +#include #include -#include #include #include #include @@ -392,6 +392,21 @@ class AddressSpaceManager { IOLockUnlock(lock_); } + // Attach a human-readable label to a range for diagnostic logging only. + // No-op if the handle is unknown. Truncated to fit the fixed buffer. + void SetDebugLabel(uint64_t handle, const char* label) { + if (!lock_ || handle == 0) { + return; + } + + IOLockLock(lock_); + auto it = ranges_.find(handle); + if (it != ranges_.end()) { + CopyDebugLabel(it->second, label); + } + IOLockUnlock(lock_); + } + void ClearAll() { if (!lock_) { return; @@ -411,11 +426,14 @@ class AddressSpaceManager { static constexpr uint32_t kAutoAddressWindowEndLo = 0x0FFF'FFFFu; static constexpr uint64_t kAutoAddressAlignment = 8ULL; + static constexpr std::size_t kDebugLabelCapacity = 32; + struct AddressRange { AddressRangeMeta meta{}; void* owner{nullptr}; std::vector buffer; RemoteWriteCallback onRemoteWrite; + std::array debugLabel{}; OSSharedPtr descriptor{}; OSSharedPtr dmaCommand{}; @@ -425,6 +443,18 @@ class AddressSpaceManager { bool hasBacking{false}; }; + static void CopyDebugLabel(AddressRange& range, const char* label) { + range.debugLabel.fill('\0'); + if (!label) { + return; + } + std::strncpy(range.debugLabel.data(), label, range.debugLabel.size() - 1); + } + + [[maybe_unused]] static const char* DebugLabelCString(const AddressRange& range) { + return range.debugLabel[0] != '\0' ? range.debugLabel.data() : "unlabeled"; + } + static uint64_t ComposeAddress(uint16_t hi, uint32_t lo) { return (static_cast(hi) << 32) | static_cast(lo); } @@ -485,8 +515,9 @@ class AddressSpaceManager { for (const auto& entry : ranges_) { const auto& range = entry.second; ASFW_ADDRSPACE_LOG( - "AddressSpaceManager[%p] range handle=0x%llx owner=%p addr=0x%012llx len=%u backing=%u dma=0x%08x", + "AddressSpaceManager[%p] range label=%s handle=0x%llx owner=%p addr=0x%012llx len=%u backing=%u dma=0x%08x", this, + DebugLabelCString(range), static_cast(range.meta.handle), range.owner, static_cast(range.meta.address), diff --git a/ASFWDriver/Protocols/SBP2/SBP2CommandORB.cpp b/ASFWDriver/Protocols/SBP2/SBP2CommandORB.cpp index 67ed416c..578f8f74 100644 --- a/ASFWDriver/Protocols/SBP2/SBP2CommandORB.cpp +++ b/ASFWDriver/Protocols/SBP2/SBP2CommandORB.cpp @@ -64,10 +64,15 @@ void SBP2CommandORB::DeallocateResources() noexcept { // Command block (CDB) // --------------------------------------------------------------------------- -void SBP2CommandORB::SetCommandBlock(std::span cdb) noexcept { - const uint32_t copyLen = static_cast( - std::min(cdb.size(), static_cast(maxCommandBlockSize_))); +bool SBP2CommandORB::SetCommandBlock(std::span cdb) noexcept { + // Reject (do not silently truncate) a CDB that won't fit the command block. + if (cdb.size() > static_cast(maxCommandBlockSize_)) { + ASFW_LOG(Async, "SBP2CommandORB: CDB size %zu exceeds max command block %u", + cdb.size(), maxCommandBlockSize_); + return false; + } + const uint32_t copyLen = static_cast(cdb.size()); if (copyLen > 0) { std::memcpy(orbStorage_.data() + Wire::NormalORB::kHeaderSize, cdb.data(), copyLen); @@ -78,15 +83,20 @@ void SBP2CommandORB::SetCommandBlock(std::span cdb) noexcept { std::memset(orbStorage_.data() + Wire::NormalORB::kHeaderSize + copyLen, 0, maxCommandBlockSize_ - copyLen); } + return true; } // --------------------------------------------------------------------------- // Prepare for execution (fills in dynamic fields) // --------------------------------------------------------------------------- -void SBP2CommandORB::PrepareForExecution(uint16_t localNodeID, - FW::FwSpeed speed, - uint16_t maxPayloadLog) noexcept { +kern_return_t SBP2CommandORB::PrepareForExecution(uint16_t localNodeID, + FW::FwSpeed speed, + uint16_t maxPayloadLog) noexcept { + if (!IsValid()) { + return kIOReturnNotReady; + } + auto* orb = reinterpret_cast(orbStorage_.data()); const uint16_t busNodeID = Wire::NormalizeBusNodeID(localNodeID); @@ -152,20 +162,21 @@ void SBP2CommandORB::PrepareForExecution(uint16_t localNodeID, orb->dataSize = dataDescriptor_.dataSize; // Flush ORB to address space - WriteORBToAddressSpace(); + return WriteORBToAddressSpace(); } // --------------------------------------------------------------------------- // Write ORB buffer to DMA-backed address space // --------------------------------------------------------------------------- -void SBP2CommandORB::WriteORBToAddressSpace() noexcept { +kern_return_t SBP2CommandORB::WriteORBToAddressSpace() noexcept { const auto span = std::span(orbStorage_.data(), orbStorage_.size()); const kern_return_t kr = addrMgr_.WriteLocalData( owner_, orbHandle_, 0, span); if (kr != kIOReturnSuccess) { ASFW_LOG(Async, "SBP2CommandORB: failed to write ORB to address space: 0x%08x", kr); } + return kr; } // --------------------------------------------------------------------------- @@ -180,20 +191,26 @@ Async::FWAddress SBP2CommandORB::GetORBAddress() const noexcept { return Async::FWAddress(parts); } -void SBP2CommandORB::SetNextORBAddress(uint32_t hi, uint32_t lo) noexcept { +kern_return_t SBP2CommandORB::SetNextORBAddress(uint32_t hi, uint32_t lo) noexcept { + if (!IsValid()) { + return kIOReturnNotReady; + } auto* orb = reinterpret_cast(orbStorage_.data()); orb->nextORBAddressHi = hi; orb->nextORBAddressLo = lo; - WriteORBToAddressSpace(); + return WriteORBToAddressSpace(); } -void SBP2CommandORB::SetToDummy() noexcept { +kern_return_t SBP2CommandORB::SetToDummy() noexcept { + if (!IsValid()) { + return kIOReturnNotReady; + } // Set rq_fmt=3 (bits [13:12] = 11) to make device skip this ORB auto* orb = reinterpret_cast(orbStorage_.data()); uint16_t hostOptions = OSSwapBigToHostInt16(orb->options); hostOptions = (hostOptions & ~0x3000u) | 0x6000u; orb->options = OSSwapHostToBigInt16(hostOptions); - WriteORBToAddressSpace(); + return WriteORBToAddressSpace(); } // --------------------------------------------------------------------------- diff --git a/ASFWDriver/Protocols/SBP2/SBP2CommandORB.hpp b/ASFWDriver/Protocols/SBP2/SBP2CommandORB.hpp index cd88d810..bdd406e8 100644 --- a/ASFWDriver/Protocols/SBP2/SBP2CommandORB.hpp +++ b/ASFWDriver/Protocols/SBP2/SBP2CommandORB.hpp @@ -49,10 +49,12 @@ class SBP2CommandORB { SBP2CommandORB& operator=(const SBP2CommandORB&) = delete; // Configuration (call before submit) - void SetCommandBlock(std::span cdb) noexcept; + // Returns false if the CDB exceeds maxCommandBlockSize_ (rejected, not truncated). + [[nodiscard]] bool SetCommandBlock(std::span cdb) noexcept; void SetFlags(uint32_t flags) noexcept { flags_ = flags; } void SetMaxPayloadSize(uint16_t bytes) noexcept { maxPayloadSize_ = bytes; } void SetTimeout(uint32_t ms) noexcept { timeoutDuration_ = ms; } + [[nodiscard]] uint32_t GetTimeout() const noexcept { return timeoutDuration_; } void SetCompletionCallback(CompletionCallback cb) noexcept { completionCallback_ = std::move(cb); } // Bind page table result from SBP2PageTable::Build. @@ -60,24 +62,29 @@ class SBP2CommandORB { dataDescriptor_ = ptResult; } - // Internal: called by the session layer before submission. - void PrepareForExecution(uint16_t localNodeID, FW::FwSpeed speed, - uint16_t maxPayloadLog) noexcept; + // Internal: called by the session layer before submission. Flushes the ORB to + // address space; returns the write status (kIOReturnNotReady if not allocated). + [[nodiscard]] kern_return_t PrepareForExecution(uint16_t localNodeID, FW::FwSpeed speed, + uint16_t maxPayloadLog) noexcept; // Internal: ORB address for fetch agent / chaining. [[nodiscard]] Async::FWAddress GetORBAddress() const noexcept; - // Internal: set the next ORB pointer (big-endian values). - void SetNextORBAddress(uint32_t hi, uint32_t lo) noexcept; + // Internal: set the next ORB pointer (big-endian values). Re-flushes the ORB. + [[nodiscard]] kern_return_t SetNextORBAddress(uint32_t hi, uint32_t lo) noexcept; // Set rq_fmt=3 (NOP dummy) so device skips this ORB if already fetched. - void SetToDummy() noexcept; + [[nodiscard]] kern_return_t SetToDummy() noexcept; // Internal: timer management. void StartTimer(IODispatchQueue* queue) noexcept; void CancelTimer() noexcept; // State tracking. + // True once the ORB's address-space backing was allocated successfully. + // The constructor swallows an allocation failure, so callers must check this + // before submitting a freshly-constructed ORB. + [[nodiscard]] bool IsValid() const noexcept { return orbHandle_ != 0; } [[nodiscard]] bool IsAppended() const noexcept { return isAppended_; } void SetAppended(bool state) noexcept { isAppended_ = state; } @@ -90,7 +97,7 @@ class SBP2CommandORB { private: bool AllocateResources() noexcept; void DeallocateResources() noexcept; - void WriteORBToAddressSpace() noexcept; + [[nodiscard]] kern_return_t WriteORBToAddressSpace() noexcept; AddressSpaceManager& addrMgr_; void* owner_; diff --git a/ASFWDriver/Protocols/SBP2/SBP2_SESSION_PORT.md b/ASFWDriver/Protocols/SBP2/SBP2_SESSION_PORT.md new file mode 100644 index 00000000..7ae0adb4 --- /dev/null +++ b/ASFWDriver/Protocols/SBP2/SBP2_SESSION_PORT.md @@ -0,0 +1,201 @@ +# SBP-2 Session/Command Port — Component Breakdown & DICE-API Adaptation Map + +**Linear:** FW-54 (epic) → FW-56 (this step). **Branch:** `sbp2-session-port`. +**Source of truth for behavior:** PR #19 (`SBP2LoginSession.cpp` 1847 lines, `SBP2SessionRegistry.cpp` 799 lines) + its four test files. +**Source of truth for idiom:** DICE (`Protocols/SBP2/*` foundation, already hardened — see FW-55). + +This is a **re-implementation**, not a merge. #19's monoliths are decomposed the way DICE split `ConfigROM` (Builder/Stager/Reader/Scanner/Parser/Store). The session layer was written against #19's ORB API, which differs from DICE's — §3 maps every call. + +**Object model (decided):** components are plain modern-C++ classes (POCO), matching DICE's existing POCO foundation (`SBP2CommandORB`/`SBP2ManagementORB`/`AddressSpaceManager` are all plain classes, host-tested under `ASFW_HOST_TEST`). They are **not** IIG/`IOService` objects — that would break the gtest conformance oracle and diverge from the foundation. The only DriverKit-native surface is a single thin driver-level `IOTimerDispatchSource`+`OSAction` owner, injected into the POCO components as `ISessionScheduler` (§7a). "Modernize" is honored via C++ idioms + native primitives *behind* the interfaces, not by IOService-ifying every object. + +--- + +## 1. `SBP2LoginSession.cpp` (1847 lines, 60 methods) → 2 components + +New directory: `Protocols/SBP2/Session/`. + +> **Decomposition correction (from reading the code):** an earlier draft proposed +> 4 components (separate `LoginOrbExchange` + `UnsolicitedStatusSink`). +> `OnStatusBlockRemoteWrite` switches on `LoginState` and routes directly into +> login/reconnect/logout completion — all sharing `state_`, `loginGeneration_`, +> and the timers. Splitting those out scatters shared mutable state across class +> boundaries (worse, not better). The one genuinely clean seam is the post-login +> **command plane** (`FetchAgent`). So the login side is **2 cohesive components**. + +### 1a. `LoginSession` — login lifecycle state machine (management plane) +Owns `LoginState`, login/reconnect IDs, target info, the login/reconnect/logout/ +status-FIFO address ranges, and status routing. Cohesive: every item below +reads/writes `state_` + `loginGeneration_` + the timers. + +| Methods folded in | +|---| +| `Configure`, `Login`, `Logout`, `Reconnect`, `HandleBusReset`, `SetState` | +| ranges: `Allocate{Login,LoginResponse,StatusBlock,Reconnect,Logout}…`, `Allocate`/`DeallocateResources` | +| `BuildLoginORB`, `BuildReconnectORB`, `BuildLogoutORB`, `RefreshCommandBlockAgentAddresses` | +| `On{Login,Reconnect,Logout}WriteComplete`, `On{Login,Reconnect,Logout}Timeout` | +| `OnStatusBlockRemoteWrite`, `ProcessStatusBlock`, `Complete{Login,Reconnect,Logout}FromStatusBlock` | +| `EnableUnsolicitedStatus` + `OnUnsolicitedStatusEnableComplete`, `WriteBusyTimeout` + `OnBusyTimeoutComplete` | +| accessors: `CommandBlockAgent`, `ReconnectHoldSeconds`, `TargetInfo`, `MaxPayloadSize`, `State`, `LoginID`, `Generation` | +| timers via injected `ISessionScheduler` (§7a); **`SetTimeoutQueue` + owned-queue machinery deleted** (§4) | + +Owns the status-FIFO range (read by both login completion *and* unsolicited/command +status — both are `LoginSession` transitions). Its remote-write callback captures +`weak_from_this()`. Backing is an `IOBufferMemoryDescriptor` via +`AddressSpaceManager`, never a raw buffer (§7). + +### 1b. `FetchAgent` — command plane (post-login ORB submission) +Doorbell, ORB chaining, fetch-agent reset. The engine `CommandExecutor` (§2b) +drives. Self-contained: depends only on `SBP2CommandORB` (✓ step 1), the bus, and +the scheduler — `LoginSession` hands it the command-block-agent address + +node/generation once login succeeds. + +| Methods folded in | +|---| +| `SubmitORB`, `AppendORB`, `AppendORBImmediate`, `RingDoorbell` | +| `MakeORBKey` (×2), `ClearORBTracking`, `outstandingORBs_` / `chainTailORB_` state | +| `StartSubmittedORBTimer`, `FailSubmittedORB`, `FailPendingImmediateORBs` | +| `OnFetchAgentWriteComplete`, `OnDoorbellComplete`, `ResetFetchAgent`, `OnAgentResetComplete` | +| `SubmitManagementORB` (drives an `SBP2ManagementORB` for task-mgmt) | + +--- + +## 2. `SBP2SessionRegistry.cpp` (799 lines, 30 methods) → registry + executor + slim record + +### 2a. `SessionRegistry` — identity & lifecycle only (no command state) +| Methods | +|---| +| `CreateSession`, `StartLogin`, `GetSessionState`, `ReleaseSession`, `ReleaseOwner` | +| `OnBusReset`, `RefreshTargets` | +| `FindByHandle` (×2), `FindByHandleForOwner` (×2), `ResolveUnit` | +| `HasSessionForTargetLocked` (dup-target reject, `afcbd9f`), `RetireSessionLocked`, `EraseRetiredSessionLocked`, `SetReleaseLogoutCallbackLocked` | +| testing seams: `GetSessionForTesting`, `GetSessionWeakForTesting` | + +Hardening to preserve: owner validation (`8b64806`), release order = sessions **before** address ranges (`9ca0d8e`). + +### 2b. `CommandExecutor` — command plane (lifts the god-object out of the record) +Owns everything command-related currently bloating `SBP2SessionRecord`. + +| Methods | Owned state (moved out of record) | +|---|---| +| `SubmitInquiry`, `GetInquiryResult` | `commandORB` (`unique_ptr`) | +| `SubmitCommand`, `GetCommandResult` | `commandPageTable` (`unique_ptr`) | +| `SubmitTaskManagement`, `IsSupportedTaskManagementFunction` | `managementORB` (`unique_ptr`) | +| `FailActiveCommandLocked` (`f8b0403`, `45a5609`) | `activeCommandRequest`, `pendingCommandResult`, `commandReady`, `commandInFlight`, `commandBufferHandle` | +| `CleanupCommandResources`, `CleanupManagementResources`, `BuildCommandFlags` | | + +### 2c. `SessionRecord` — slim value type +`{ handle, owner, guid, romOffset, shared_ptr session, SBP2SessionState state }`. Command guts → `CommandExecutor` (keyed by handle). + +--- + +## 3. DICE-API adaptation map (every #19 ORB call → DICE) + +### `SBP2CommandORB` +| #19 call (in session layer) | #19 API | DICE API | Action | +|---|---|---|---| +| `orb->SetCommandBlock(cdb)` *(return checked)* | `[[nodiscard]] bool` (rejects oversized CDB) | `void` | **Add `[[nodiscard]] bool SetCommandBlock`** to DICE — real hardening (`1d2ac01`), bounds-check CDB ≤ `maxCommandBlockSize_`. | +| `orb->IsValid()` | `bool` (`orbHandle_!=0`) | **absent** | **Add `IsValid()`** to DICE. DICE ctor calls `AllocateResources()` but swallows the bool → failed alloc is currently undetectable. Required. | +| `orb->PrepareForExecution(node, speed, payloadLog)` *(return checked)* | `[[nodiscard]] kern_return_t` | `void` (same 3 args) | **Change DICE return to `kern_return_t`**; propagate alloc/write failure (`1d2ac01`). Args already match. | +| `chainTailORB_->SetNextORBAddress(hi, lo)` *(return checked)* | `[[nodiscard]] kern_return_t` | `void` | **Change DICE return to `kern_return_t`** (fails if ORB not allocated). | +| `orb->SetToDummy()` | `[[nodiscard]] kern_return_t` | `void` | Change DICE return to `kern_return_t` for symmetry (used in fetch-agent chain teardown). | +| `orb->StartTimer(workQueue_, timeoutQueue)` | 2 queues | `StartTimer(queue)` 1 queue | **Adapt call site** → `StartTimer(workQueue_)`. Single-queue model (§4). | +| `orb->SetMaxPayloadSize(...)` | dropped in #19 | present in DICE | keep DICE; executor may set it. No change. | +| `SetFlags`, `SetTimeout`, `SetCompletionCallback`, `SetDataDescriptor`, `GetORBAddress`, `IsAppended`, `GetFlags`, fetch-agent retry getters | identical | identical | no change | + +### `SBP2ManagementORB` +| #19 call | DICE | Action | +|---|---|---| +| `orb->SetTimeoutQueue(q)` | **absent** | **Drop the call.** DICE arms timeout on `workQueue_` after the write ACK (already correct, see FW-55). | +| `SetWorkQueue`, `SetTimeout`, `SetCompletionCallback`, `SetFunction`, `SetLoginID`, `SetTargetORBAddress`, `SetManagementAgentOffset`, `SetTargetNode`, `Execute`, `GetFunction`, `InProgress` | present | no change | + +### `LoginSession` internal (our new component — we define the API) +| #19 surface | Action | +|---|---| +| `SetTimeoutQueue`, `EnsureTimeoutQueue`, `ReleaseOwnedTimeoutQueue`, `EffectiveTimeoutQueue` | **Delete.** Single-queue model collapses owned-timeout-queue machinery (§4). | +| `SubmitDelayedCallback(delayMs, …)` | Re-express on `SBP2DelayedDispatch::DispatchAfterCompat` (what DICE's ManagementORB already uses). | + +--- + +## 4. Single-Default-queue simplification + +Per the dext's single-`Default`-queue confinement, #19's separate **timeout queue** abstraction is dead weight. DICE's `SBP2ManagementORB` already proves the pattern: arm a delayed callback on `workQueue_` guarded by a `timerGeneration_` counter + `lifetimeToken_` weak_ptr. The whole `*TimeoutQueue*` family (`SetTimeoutQueue`/`EnsureTimeoutQueue`/`ReleaseOwnedTimeoutQueue`/`EffectiveTimeoutQueue` + the owned `IODispatchQueue`) is **removed**, not ported. ~80 lines of #19 evaporate. + +Lifetime safety for async callbacks: each component is `enable_shared_from_this`; callbacks capture `weak_from_this()` and bail on `expired()` (the `2b0ddca` guard), matching DICE's `lifetimeToken_` idiom. + +--- + +## 5. Foundation additions FW-56 needs (small, in DICE's CommandORB) + +These are the only foundation touches; they are genuine hardening, not signature taste: +1. `[[nodiscard]] bool IsValid() const noexcept { return orbHandle_ != 0; }` +2. `[[nodiscard]] bool SetCommandBlock(std::span)` — bounds-check vs `maxCommandBlockSize_`. +3. `PrepareForExecution` / `SetNextORBAddress` / `SetToDummy` → return `kern_return_t`. + +Each gets a unit test in `tests/protocols/SBP2ORBTests.cpp` (which already exists on DICE). + +--- + +## 6. Build/test order within FW-56 (each green before next) + +Revised to a dependency-correct **bottom-up** order (the registry constructs a +`LoginSession`, so it cannot precede it): + +0. ✅ `ISessionScheduler` interface + virtual-clock fake (§7a). Production timer wiring (IOTimerDispatchSource+OSAction) lands in FW-58. +1. ✅ CommandORB foundation additions (§5) → `SBP2ORBTests` green. +2. `FetchAgent` (command plane; needs CommandORB ✓ + scheduler ✓ + bus) + focused tests. +3. `LoginSession` (state machine + status routing; uses scheduler; drives FetchAgent) → port `SBP2LoginSessionTests`. +4. `SessionRecord` slim type + `SessionRegistry` (identity/lifecycle; constructs `LoginSession`) → port registry half of `SBP2SessionRegistryTests`. +5. `CommandExecutor` (drives FetchAgent; owns command ORB/page-table/inflight) → command half of `SBP2SessionRegistryTests`. +6. `tests/CMakeLists` targets registered; full host suite green; checkpoint commit. + +> Tests come from #19 but were written against #19's API — adapt assertions to the decomposed components and DICE call shapes as each piece lands. + +--- + +## 7. DriverKit primitives — use native, not hand-rolled + +Guidance (approved): lean on DriverKit's own facilities rather than reinventing them. + +### 7a. Timers → `IOTimerDispatchSource` + `OSAction` (not the IOSleep hack) + +The current `SBP2DelayedDispatch::DispatchAfterCompat` schedules a timeout as +`queue->DispatchAsync(^{ IOSleep(delayMs); work(); })`. That **blocks the queue +thread for the whole delay** — on the dext's single `Default` queue, every armed +timeout stalls the queue. Do **not** propagate this into the four new components. + +The dext already has the correct idiom: `Scheduling/WatchdogCoordinator` uses +`IOTimerDispatchSource::Create(queue, &timer)` driven by an `OSAction` +(`AsyncWatchdogTimerFired`, `TYPE()`-declared in `ASFWDriver.iig`). Follow it. + +**Constraint:** an `OSAction` target must be an IIG `TYPE()`-declared method on a +DriverKit class. The SBP-2 session components are plain C++ (POCO), not +`IOService`s, so they cannot host `OSAction` callbacks directly. Therefore: + +- Components depend on an **injected scheduler interface** — e.g. + `ISessionScheduler { CancelableToken ScheduleAfter(uint64_t ns, fn); void Cancel(token); }` + — never on `DispatchAfterCompat` directly. +- **Production** backing: an `IOTimerDispatchSource`+`OSAction` owned at the + driver/controller level, routing fires back into the component (wired in FW-58). + The generation-counter + `weak_from_this()` guards still apply per fire. +- **Host tests**: a fake scheduler with a manually-advanced virtual clock — makes + the timeout/reconnect/busy-replay paths deterministically testable (no real time). + +This keeps the POCO components testable *and* gets real, non-blocking DriverKit +timers in production. The `SBP2DelayedDispatch` shim stays only as the host-test +fallback path if convenient; production goes through the scheduler interface. + +### 7b. Memory → `IOBufferMemoryDescriptor` via `AddressSpaceManager` + +Every SBP-2 address range (login/response/status-FIFO/reconnect/logout ORBs, +command ORB, page table) is allocated through `AddressSpaceManager`, which already +backs each range with `OSSharedPtr` + +`OSSharedPtr` + `CreateMapping`. Components hold **handles**, not +buffers; they never allocate device-visible memory by hand. `OSSharedPtr` / +`OSAction` lifetimes are RAII-managed. + +### 7c. Async transactions → existing bus async API + +Login/reconnect/logout writes, fetch-agent writes, and doorbell rings go through +the established `Async::IFireWireBus` async write API (as #19 does), whose +completions are already `OSAction`-driven inside `AsyncSubsystem`. No new +transport primitive is introduced at the SBP-2 layer. diff --git a/ASFWDriver/Protocols/SBP2/Session/CommandExecutor.cpp b/ASFWDriver/Protocols/SBP2/Session/CommandExecutor.cpp new file mode 100644 index 00000000..c393a128 --- /dev/null +++ b/ASFWDriver/Protocols/SBP2/Session/CommandExecutor.cpp @@ -0,0 +1,341 @@ +// CommandExecutor — SBP-2 command plane for one session. See CommandExecutor.hpp. +// +// Ported from PR #19's SBP2SessionRegistry command methods (§2b). The registry +// owns one CommandExecutor per SessionRecord and holds its lock around the +// synchronous entry points; async ORB/management completions run on the single +// Default queue and are guarded by the weak lifetime token. + +#include "CommandExecutor.hpp" + +#include "../../../Async/Interfaces/IFireWireBus.hpp" +#include "../../../Async/Interfaces/IFireWireBusInfo.hpp" +#include "../../../Common/FWCommon.hpp" + +#include +#include +#include + +namespace ASFW::Protocols::SBP2 { + +namespace { + +constexpr uint8_t kInquiryOpcode = 0x12; + +uint32_t BuildCommandFlags(SCSI::DataDirection direction) { + uint32_t flags = SBP2CommandORB::kNotify | SBP2CommandORB::kImmediate | + SBP2CommandORB::kNormalORB; + if (direction == SCSI::DataDirection::FromTarget) { + flags |= SBP2CommandORB::kDataFromTarget; + } + return flags; +} + +} // namespace + +CommandExecutor::CommandExecutor(Async::IFireWireBus& bus, + Async::IFireWireBusInfo& busInfo, + AddressSpaceManager& addrSpaceMgr, + LoginSession& session, + void* owner, + int32_t& lastError, + IODispatchQueue* workQueue) noexcept + : bus_(bus) + , busInfo_(busInfo) + , addrSpaceMgr_(addrSpaceMgr) + , session_(session) + , owner_(owner) + , lastError_(lastError) + , workQueue_(workQueue) {} + +CommandExecutor::~CommandExecutor() { + lifetimeToken_.reset(); + CleanupCommandResources(); + CleanupManagementResources(); +} + +bool CommandExecutor::IsSupportedTaskManagementFunction( + SBP2ManagementORB::Function function) noexcept { + switch (function) { + case SBP2ManagementORB::Function::AbortTaskSet: + case SBP2ManagementORB::Function::LogicalUnitReset: + case SBP2ManagementORB::Function::TargetReset: + return true; + default: + return false; + } +} + +bool CommandExecutor::SubmitInquiry(uint8_t allocationLength) { + return SubmitCommand(SCSI::BuildInquiryRequest(allocationLength)); +} + +bool CommandExecutor::SubmitCommand(const SCSI::CommandRequest& request) { + if (request.cdb.empty()) { + return false; + } + if (session_.State() != LoginState::LoggedIn || commandInFlight_) { + return false; + } + if (request.transferLength > 0 && request.direction == SCSI::DataDirection::None) { + return false; + } + if (request.direction == SCSI::DataDirection::FromTarget && !request.outgoingPayload.empty()) { + return false; + } + if (request.direction == SCSI::DataDirection::ToTarget && + request.outgoingPayload.size() != request.transferLength) { + return false; + } + + const uint16_t maxCDB = session_.TargetInfo().maxCommandBlockSize; + if (maxCDB < request.cdb.size()) { + return false; + } + + uint64_t bufferHandle = 0; + AddressSpaceManager::AddressRangeMeta bufferMeta{}; + if (request.transferLength > 0) { + const kern_return_t kr = addrSpaceMgr_.AllocateAddressRangeAuto( + owner_, 0xFFFF, request.transferLength, &bufferHandle, &bufferMeta); + if (kr != kIOReturnSuccess) { + ASFW_LOG(Async, "CommandExecutor: failed to allocate command buffer: 0x%08x", kr); + return false; + } + } + + std::unique_ptr pageTable; + if (request.transferLength > 0) { + if (request.direction == SCSI::DataDirection::ToTarget) { + const kern_return_t writeKr = addrSpaceMgr_.WriteLocalData( + owner_, bufferHandle, 0, + std::span{request.outgoingPayload.data(), + request.outgoingPayload.size()}); + if (writeKr != kIOReturnSuccess) { + addrSpaceMgr_.DeallocateAddressRange(owner_, bufferHandle); + return false; + } + } + + pageTable = std::make_unique(addrSpaceMgr_, owner_); + SBP2PageTable::Segment segment{bufferMeta.address, request.transferLength}; + if (!pageTable->Build(std::span(&segment, 1), + busInfo_.GetLocalNodeID().value)) { + addrSpaceMgr_.DeallocateAddressRange(owner_, bufferHandle); + return false; + } + } + + auto orb = std::make_unique(addrSpaceMgr_, owner_, maxCDB); + if (!orb->SetCommandBlock(std::span{request.cdb.data(), request.cdb.size()})) { + if (bufferHandle != 0) { + addrSpaceMgr_.DeallocateAddressRange(owner_, bufferHandle); + } + return false; + } + orb->SetFlags(BuildCommandFlags(request.direction)); + orb->SetTimeout(request.timeoutMs > 0 ? request.timeoutMs + : session_.TargetInfo().managementTimeoutMs); + if (pageTable) { + orb->SetDataDescriptor(pageTable->GetResult()); + } + + const SBP2CommandORB* submittedORB = orb.get(); + const std::weak_ptr weak = lifetimeToken_; + orb->SetCompletionCallback([this, weak, submittedORB](int transportStatus, uint8_t sbpStatus) { + if (weak.expired()) { + return; + } + if (!commandInFlight_ || commandORB_.get() != submittedORB) { + return; + } + + commandInFlight_ = false; + commandReady_ = true; + lastCompletedCommandOpcode_ = activeCommandOpcode_; + activeCommandOpcode_.reset(); + + SCSI::CommandResult result{}; + result.transportStatus = transportStatus; + result.sbpStatus = sbpStatus; + + if (transportStatus == 0 && sbpStatus == Wire::SBPStatus::kNoAdditionalInfo && + activeCommandRequest_.has_value() && + activeCommandRequest_->direction == SCSI::DataDirection::FromTarget && + activeCommandRequest_->transferLength > 0 && commandBufferHandle_ != 0) { + std::vector payload; + const kern_return_t readKr = addrSpaceMgr_.ReadIncomingData( + owner_, commandBufferHandle_, 0, + activeCommandRequest_->transferLength, &payload); + if (readKr == kIOReturnSuccess) { + result.payload = std::move(payload); + } else { + result.transportStatus = static_cast(readKr); + } + } + + if (activeCommandRequest_.has_value() && activeCommandRequest_->captureSenseData) { + result.senseData = result.payload; + } + + lastError_ = (result.transportStatus == 0 && + result.sbpStatus == Wire::SBPStatus::kNoAdditionalInfo) + ? 0 + : static_cast(result.transportStatus); + pendingCommandResult_ = std::move(result); + activeCommandRequest_.reset(); + CleanupCommandResources(); + }); + + commandInFlight_ = true; + commandReady_ = false; + pendingCommandResult_.reset(); + activeCommandRequest_ = request; + activeCommandOpcode_ = request.cdb.front(); + commandBufferHandle_ = bufferHandle; + commandORB_ = std::move(orb); + commandPageTable_ = std::move(pageTable); + + if (session_.SubmitORB(commandORB_.get())) { + return true; + } + + // Submission rejected synchronously — roll back. + if (commandORB_.get() == submittedORB) { + commandInFlight_ = false; + commandReady_ = false; + pendingCommandResult_.reset(); + activeCommandRequest_.reset(); + activeCommandOpcode_.reset(); + CleanupCommandResources(); + } + return false; +} + +std::optional CommandExecutor::GetCommandResult() { + if (!commandReady_ || !pendingCommandResult_.has_value()) { + return std::nullopt; + } + SCSI::CommandResult result = std::move(*pendingCommandResult_); + pendingCommandResult_.reset(); + lastCompletedCommandOpcode_.reset(); + commandReady_ = false; + return result; +} + +std::optional CommandExecutor::GetInquiryResult() { + if (!commandReady_ || !pendingCommandResult_.has_value() || + lastCompletedCommandOpcode_ != kInquiryOpcode) { + return std::nullopt; + } + SCSI::CommandResult result = std::move(*pendingCommandResult_); + pendingCommandResult_.reset(); + lastCompletedCommandOpcode_.reset(); + commandReady_ = false; + return result; +} + +bool CommandExecutor::SubmitTaskManagement(SBP2ManagementORB::Function function) { + if (!IsSupportedTaskManagementFunction(function)) { + return false; + } + if (session_.State() != LoginState::LoggedIn || managementORB_) { + return false; + } + + auto orb = std::make_unique(bus_, busInfo_, addrSpaceMgr_, owner_); + orb->SetFunction(function); + orb->SetLoginID(session_.LoginID()); + orb->SetManagementAgentOffset(session_.TargetInfo().managementAgentOffset); + orb->SetTimeout(session_.TargetInfo().managementTimeoutMs); + orb->SetWorkQueue(workQueue_); + orb->SetTargetNode(session_.Generation(), session_.TargetInfo().targetNodeId); + + const SBP2ManagementORB* submittedORB = orb.get(); + const std::weak_ptr weak = lifetimeToken_; + orb->SetCompletionCallback([this, weak, submittedORB, function](int status) { + if (weak.expired()) { + return; + } + if (managementORB_.get() != submittedORB) { + return; + } + lastError_ = static_cast(status); + if (status == 0 && IsSupportedTaskManagementFunction(function)) { + CleanupCommandResources(); + pendingCommandResult_.reset(); + activeCommandRequest_.reset(); + activeCommandOpcode_.reset(); + lastCompletedCommandOpcode_.reset(); + commandReady_ = false; + } + managementORB_.reset(); + }); + + managementORB_ = std::move(orb); + if (!managementORB_->Execute()) { + managementORB_.reset(); + return false; + } + return true; +} + +void CommandExecutor::OnBusReset() { + CleanupManagementResources(); + if (commandInFlight_ || commandORB_) { + FailActiveCommand(static_cast(kIOReturnAborted), Wire::SBPStatus::kRequestAborted); + } +} + +void CommandExecutor::Cleanup() { + CleanupCommandResources(); + CleanupManagementResources(); +} + +void CommandExecutor::FailActiveCommand(int transportStatus, uint8_t sbpStatus) noexcept { + if (!commandInFlight_) { + return; + } + + commandInFlight_ = false; + commandReady_ = true; + lastCompletedCommandOpcode_ = activeCommandOpcode_; + activeCommandOpcode_.reset(); + + SCSI::CommandResult result{}; + result.transportStatus = transportStatus; + result.sbpStatus = sbpStatus; + lastError_ = (transportStatus == 0 && sbpStatus == Wire::SBPStatus::kNoAdditionalInfo) + ? 0 + : static_cast(result.transportStatus); + pendingCommandResult_ = std::move(result); + activeCommandRequest_.reset(); + commandPageTable_.reset(); + if (commandBufferHandle_ != 0) { + addrSpaceMgr_.DeallocateAddressRange(owner_, commandBufferHandle_); + commandBufferHandle_ = 0; + } + if (commandORB_) { + commandORB_->SetAppended(false); + } + + CleanupCommandResources(); +} + +void CommandExecutor::CleanupCommandResources() { + // Mirror #19: wipe the session's fetch-agent ORB tracking (cancels any + // in-flight fetch-agent/doorbell write) before dropping our ORB resources. + session_.ClearCommandTracking(); + if (commandBufferHandle_ != 0) { + addrSpaceMgr_.DeallocateAddressRange(owner_, commandBufferHandle_); + commandBufferHandle_ = 0; + } + commandORB_.reset(); + commandPageTable_.reset(); + commandInFlight_ = false; +} + +void CommandExecutor::CleanupManagementResources() { + managementORB_.reset(); +} + +} // namespace ASFW::Protocols::SBP2 diff --git a/ASFWDriver/Protocols/SBP2/Session/CommandExecutor.hpp b/ASFWDriver/Protocols/SBP2/Session/CommandExecutor.hpp new file mode 100644 index 00000000..128e017c --- /dev/null +++ b/ASFWDriver/Protocols/SBP2/Session/CommandExecutor.hpp @@ -0,0 +1,113 @@ +#pragma once + +// CommandExecutor — SBP-2 command plane for one logged-in session. +// +// Decomposed from PR #19's SBP2SessionRegistry (§2b of SBP2_SESSION_PORT.md): the +// command god-object that bloated SBP2SessionRecord (command ORB, page table, +// management ORB, in-flight tracking, pending result) is lifted out into this +// type, one instance per SessionRecord. It drives the session's FetchAgent by +// submitting Normal Command ORBs through LoginSession::SubmitORB and matches +// completions back via the ORB completion callback. +// +// Lifetime: owned by the SessionRecord (created once the LoginSession exists). It +// holds a reference to its LoginSession and writes the session's lastError slot. +// Async completions capture a weak lifetime token and bail if the executor has +// been destroyed (session release / registry teardown). + +#include "LoginSession.hpp" +#include "../SBP2CommandORB.hpp" +#include "../SBP2ManagementORB.hpp" +#include "../SBP2PageTable.hpp" +#include "../SCSICommandSet.hpp" +#include "../AddressSpaceManager.hpp" +#include "../../../Async/AsyncTypes.hpp" + +#include +#ifdef ASFW_HOST_TEST +#include "../../../Testing/HostDriverKitStubs.hpp" +#else +#include +#endif + +#include +#include +#include + +namespace ASFW::Async { +class IFireWireBus; +class IFireWireBusInfo; +} + +namespace ASFW::Protocols::SBP2 { + +class CommandExecutor { +public: + CommandExecutor(Async::IFireWireBus& bus, + Async::IFireWireBusInfo& busInfo, + AddressSpaceManager& addrSpaceMgr, + LoginSession& session, + void* owner, + int32_t& lastError, + IODispatchQueue* workQueue) noexcept; + ~CommandExecutor(); + + CommandExecutor(const CommandExecutor&) = delete; + CommandExecutor& operator=(const CommandExecutor&) = delete; + + // Submit a generic SCSI command. Returns false if not logged in, another + // command is active, the request is malformed, or the ORB cannot be built. + [[nodiscard]] bool SubmitCommand(const SCSI::CommandRequest& request); + + // Submit a SCSI INQUIRY (convenience wrapper over SubmitCommand). + [[nodiscard]] bool SubmitInquiry(uint8_t allocationLength); + + // Destructive reads of the pending result. GetInquiryResult only returns a + // result whose completed opcode was INQUIRY. + [[nodiscard]] std::optional GetCommandResult(); + [[nodiscard]] std::optional GetInquiryResult(); + + // Submit a task-management recovery ORB (abort task set / LU reset / target + // reset). Returns false if not logged in, one is already in flight, or the + // function is unsupported. + [[nodiscard]] bool SubmitTaskManagement(SBP2ManagementORB::Function function); + + // Bus reset: fail the active command (synthetic aborted result) and drop the + // management ORB. The FetchAgent is unbound by LoginSession separately. + void OnBusReset(); + + // Release-time cleanup: drop command + management resources. + void Cleanup(); + + [[nodiscard]] static bool IsSupportedTaskManagementFunction( + SBP2ManagementORB::Function function) noexcept; + +private: + void FailActiveCommand(int transportStatus, uint8_t sbpStatus) noexcept; + void CleanupCommandResources(); + void CleanupManagementResources(); + + Async::IFireWireBus& bus_; + Async::IFireWireBusInfo& busInfo_; + AddressSpaceManager& addrSpaceMgr_; + LoginSession& session_; + void* owner_; + int32_t& lastError_; + IODispatchQueue* workQueue_{nullptr}; + + // Command god-object state (moved out of #19's SBP2SessionRecord). + std::optional activeCommandRequest_; + std::optional pendingCommandResult_; + std::optional activeCommandOpcode_; + std::optional lastCompletedCommandOpcode_; + bool commandReady_{false}; + bool commandInFlight_{false}; + std::unique_ptr commandORB_; + std::unique_ptr commandPageTable_; + uint64_t commandBufferHandle_{0}; + + std::unique_ptr managementORB_; + + std::shared_ptr lifetimeToken_{std::make_shared(0)}; +}; + +} // namespace ASFW::Protocols::SBP2 diff --git a/ASFWDriver/Protocols/SBP2/Session/DriverKitSessionScheduler.cpp b/ASFWDriver/Protocols/SBP2/Session/DriverKitSessionScheduler.cpp new file mode 100644 index 00000000..fee0e51c --- /dev/null +++ b/ASFWDriver/Protocols/SBP2/Session/DriverKitSessionScheduler.cpp @@ -0,0 +1,211 @@ +#include "DriverKitSessionScheduler.hpp" + +#include "../../../Common/TimingUtils.hpp" +#include "../../../Logging/Logging.hpp" + +#ifndef ASFW_HOST_TEST +#include +#endif + +#include +#include + +namespace ASFW::Protocols::SBP2 { + +namespace { + +class IOLockGuard { +public: + explicit IOLockGuard(IOLock* lock) : lock_(lock) { + if (lock_) { + IOLockLock(lock_); + } + } + + ~IOLockGuard() { + if (lock_) { + IOLockUnlock(lock_); + } + } + + IOLockGuard(const IOLockGuard&) = delete; + IOLockGuard& operator=(const IOLockGuard&) = delete; + +private: + IOLock* lock_{nullptr}; +}; + +} // namespace + +DriverKitSessionScheduler::DriverKitSessionScheduler() { + lock_ = IOLockAlloc(); +} + +DriverKitSessionScheduler::~DriverKitSessionScheduler() { + Reset(); + if (lock_) { + IOLockFree(lock_); + lock_ = nullptr; + } +} + +kern_return_t DriverKitSessionScheduler::Prepare(::ASFWDriver& service, + OSSharedPtr workQueue) { + if (!workQueue) { + return kIOReturnNotReady; + } + + Reset(); + workQueue_ = std::move(workQueue); + +#ifdef ASFW_HOST_TEST + (void)service; + return kIOReturnSuccess; +#else + IOTimerDispatchSource* rawTimer = nullptr; + auto kr = IOTimerDispatchSource::Create(workQueue_.get(), &rawTimer); + if (kr != kIOReturnSuccess || rawTimer == nullptr) { + workQueue_.reset(); + return kr != kIOReturnSuccess ? kr : kIOReturnNoResources; + } + timer_ = OSSharedPtr(rawTimer, OSNoRetain); + + OSAction* rawAction = nullptr; + kr = service.CreateActionSBP2SessionTimerFired(0, &rawAction); + if (kr != kIOReturnSuccess || rawAction == nullptr) { + timer_.reset(); + workQueue_.reset(); + return kr != kIOReturnSuccess ? kr : kIOReturnError; + } + action_ = OSSharedPtr(rawAction, OSNoRetain); + + kr = timer_->SetHandler(action_.get()); + if (kr != kIOReturnSuccess) { + Reset(); + return kr; + } + + kr = timer_->SetEnableWithCompletion(true, nullptr); + if (kr != kIOReturnSuccess) { + Reset(); + return kr; + } + + (void)ASFW::Timing::initializeHostTimebase(); + return kIOReturnSuccess; +#endif +} + +void DriverKitSessionScheduler::Reset() noexcept { + { + IOLockGuard guard(lock_); + pending_.clear(); + } + + if (timer_) { + (void)timer_->SetEnableWithCompletion(false, nullptr); + } + action_.reset(); + timer_.reset(); + workQueue_.reset(); +} + +SchedulerToken DriverKitSessionScheduler::ScheduleAfter(uint64_t delayNs, + std::function fn) { + if (!fn) { + return kInvalidSchedulerToken; + } + +#ifdef ASFW_HOST_TEST + if (!workQueue_) { + fn(); + return kInvalidSchedulerToken; + } + const SchedulerToken token = nextToken_++; + workQueue_->DispatchAsyncAfter(delayNs, std::move(fn)); + return token; +#else + if (!timer_) { + return kInvalidSchedulerToken; + } + + IOLockGuard guard(lock_); + SchedulerToken token = nextToken_++; + if (token == kInvalidSchedulerToken) { + token = nextToken_++; + } + pending_.emplace(token, PendingCallback{ + .deadlineTicks = DeadlineTicksFromNow(delayNs), + .fn = std::move(fn), + }); + ArmNextLocked(); + return token; +#endif +} + +void DriverKitSessionScheduler::Cancel(SchedulerToken token) { + if (token == kInvalidSchedulerToken) { + return; + } + + IOLockGuard guard(lock_); + if (pending_.erase(token) > 0) { + ArmNextLocked(); + } +} + +void DriverKitSessionScheduler::HandleTimerFired() noexcept { + std::vector> due; + + { + IOLockGuard guard(lock_); + const uint64_t now = mach_absolute_time(); + for (auto it = pending_.begin(); it != pending_.end();) { + if (it->second.deadlineTicks <= now) { + due.push_back(std::move(it->second.fn)); + it = pending_.erase(it); + } else { + ++it; + } + } + ArmNextLocked(); + } + + for (auto& fn : due) { + if (fn) { + fn(); + } + } +} + +void DriverKitSessionScheduler::ArmNextLocked() noexcept { +#ifdef ASFW_HOST_TEST + return; +#else + if (!timer_ || pending_.empty()) { + return; + } + + const auto next = std::min_element( + pending_.begin(), pending_.end(), + [](const auto& a, const auto& b) { + return a.second.deadlineTicks < b.second.deadlineTicks; + }); + if (next == pending_.end()) { + return; + } + + (void)timer_->WakeAtTime(kIOTimerClockMachAbsoluteTime, next->second.deadlineTicks, 0); +#endif +} + +uint64_t DriverKitSessionScheduler::DeadlineTicksFromNow(uint64_t delayNs) const noexcept { + (void)ASFW::Timing::initializeHostTimebase(); + uint64_t deltaTicks = ASFW::Timing::nanosToHostTicks(delayNs); + if (deltaTicks == 0) { + deltaTicks = 1; + } + return mach_absolute_time() + deltaTicks; +} + +} // namespace ASFW::Protocols::SBP2 diff --git a/ASFWDriver/Protocols/SBP2/Session/DriverKitSessionScheduler.hpp b/ASFWDriver/Protocols/SBP2/Session/DriverKitSessionScheduler.hpp new file mode 100644 index 00000000..be4c34fd --- /dev/null +++ b/ASFWDriver/Protocols/SBP2/Session/DriverKitSessionScheduler.hpp @@ -0,0 +1,61 @@ +#pragma once + +#include "ISessionScheduler.hpp" + +#ifdef ASFW_HOST_TEST +#include "../../../Testing/HostDriverKitStubs.hpp" +#else +#include +#include +#include +#include +#include +#endif + +#include +#include +#include + +class ASFWDriver; + +namespace ASFW::Protocols::SBP2 { + +// Production implementation of ISessionScheduler. A single DriverKit timer +// source wakes the next due SBP-2 session callback; callbacks then run on the +// driver's Default queue. +class DriverKitSessionScheduler final : public ISessionScheduler { +public: + DriverKitSessionScheduler(); + ~DriverKitSessionScheduler() override; + + DriverKitSessionScheduler(const DriverKitSessionScheduler&) = delete; + DriverKitSessionScheduler& operator=(const DriverKitSessionScheduler&) = delete; + + [[nodiscard]] kern_return_t Prepare(::ASFWDriver& service, + OSSharedPtr workQueue); + void Reset() noexcept; + + [[nodiscard]] SchedulerToken ScheduleAfter(uint64_t delayNs, + std::function fn) override; + void Cancel(SchedulerToken token) override; + + void HandleTimerFired() noexcept; + +private: + struct PendingCallback { + uint64_t deadlineTicks{0}; + std::function fn; + }; + + void ArmNextLocked() noexcept; + [[nodiscard]] uint64_t DeadlineTicksFromNow(uint64_t delayNs) const noexcept; + + IOLock* lock_{nullptr}; + OSSharedPtr workQueue_{}; + OSSharedPtr timer_{}; + OSSharedPtr action_{}; + std::map pending_; + SchedulerToken nextToken_{1}; +}; + +} // namespace ASFW::Protocols::SBP2 diff --git a/ASFWDriver/Protocols/SBP2/Session/FetchAgent.cpp b/ASFWDriver/Protocols/SBP2/Session/FetchAgent.cpp new file mode 100644 index 00000000..5cc1a938 --- /dev/null +++ b/ASFWDriver/Protocols/SBP2/Session/FetchAgent.cpp @@ -0,0 +1,500 @@ +// FetchAgent — SBP-2 command-plane engine. See FetchAgent.hpp. +// Ported from SBP2LoginSession's fetch-agent methods (PR #19), adapted to DICE: +// driven by an explicit Binding instead of login state, ORB timeouts via the +// injected ISessionScheduler (no IOSleep-on-queue), CommandORB kern_return_t. + +#include "FetchAgent.hpp" + +#include "../../../Common/FWCommon.hpp" + +#include +#include + +namespace ASFW::Protocols::SBP2 { + +using namespace ASFW::Protocols::SBP2::Wire; + +namespace { +// Fetch-agent write retry backoff, matching PR #19 (1000 ms between attempts). +constexpr uint64_t kFetchAgentWriteRetryDelayNs = 1'000'000'000ULL; +} + +FetchAgent::FetchAgent(Async::IFireWireBus& bus, + Async::IFireWireBusInfo& busInfo, + ISessionScheduler& scheduler) noexcept + : bus_(bus) + , busInfo_(busInfo) + , scheduler_(scheduler) {} + +FetchAgent::~FetchAgent() { + lifetimeToken_.reset(); + Clear(true); +} + +// --------------------------------------------------------------------------- +// Binding lifecycle +// --------------------------------------------------------------------------- + +void FetchAgent::Bind(const Binding& binding) noexcept { + Clear(true); + binding_ = binding; + bound_ = true; +} + +void FetchAgent::Unbind() noexcept { + Clear(true); + bound_ = false; + binding_ = {}; +} + +void FetchAgent::Clear(bool cancelTimers) noexcept { + const bool cancelFetchWrite = + cancelTimers && fetchAgentWriteInUse_ && static_cast(fetchAgentWriteHandle_); + const Async::AsyncHandle fetchWrite = fetchAgentWriteHandle_; + const bool cancelDoorbell = + cancelTimers && doorbellInProgress_ && static_cast(doorbellWriteHandle_); + const Async::AsyncHandle doorbell = doorbellWriteHandle_; + + if (cancelTimers) { + for (auto& [key, entry] : outstandingORBs_) { + if (entry.timeoutToken != kInvalidSchedulerToken) { + scheduler_.Cancel(entry.timeoutToken); + } + if (entry.orb != nullptr) { + entry.orb->SetAppended(false); + } + } + } + + outstandingORBs_.clear(); + pendingImmediateORBs_.clear(); + chainTailORB_ = nullptr; + activeFetchAgentORB_ = nullptr; + fetchAgentWriteHandle_ = {}; + fetchAgentWriteInUse_ = false; + doorbellWriteHandle_ = {}; + doorbellInProgress_ = false; + doorbellRingAgain_ = false; + + if (cancelFetchWrite) { + (void)bus_.Cancel(fetchWrite); + } + if (cancelDoorbell) { + (void)bus_.Cancel(doorbell); + } +} + +// --------------------------------------------------------------------------- +// Submission +// --------------------------------------------------------------------------- + +bool FetchAgent::Submit(SBP2CommandORB* orb) noexcept { + if (!bound_) { + ASFW_LOG(Async, "FetchAgent::Submit: not bound, rejecting"); + return false; + } + if (orb == nullptr || !orb->IsValid() || orb->IsAppended()) { + ASFW_LOG(Async, "FetchAgent::Submit: invalid ORB (null=%d valid=%d appended=%d)", + orb == nullptr, + orb != nullptr && orb->IsValid(), + orb != nullptr && orb->IsAppended()); + return false; + } + + const kern_return_t prepareKr = + orb->PrepareForExecution(LocalBusNodeID(), TargetSpeed(), MaxPayloadLog()); + if (prepareKr != kIOReturnSuccess) { + ASFW_LOG(Async, "FetchAgent::Submit: PrepareForExecution failed: 0x%08x", prepareKr); + return false; + } + + orb->SetFetchAgentWriteRetries(defaultWriteRetries_); + orb->SetAppended(true); + outstandingORBs_[MakeORBKey(orb->GetORBAddress())] = Outstanding{orb, kInvalidSchedulerToken}; + + const bool isImmediate = (orb->GetFlags() & SBP2CommandORB::kImmediate) != 0; + if (isImmediate) { + chainTailORB_ = orb; + if (fetchAgentWriteInUse_) { + pendingImmediateORBs_.push_back(orb); + return true; + } + return AppendImmediate(orb); + } + + if (!AppendChained(orb)) { + return false; + } + RingDoorbell(); + if (activeFetchAgentORB_ != orb) { + StartORBTimeout(orb); + } + return true; +} + +bool FetchAgent::AppendImmediate(SBP2CommandORB* orb) noexcept { + if (orb == nullptr || fetchAgentWriteInUse_) { + return false; + } + + const uint16_t localNode = LocalBusNodeID(); + const Async::FWAddress orbAddr = orb->GetORBAddress(); + fetchAgentWriteData_[0] = static_cast(localNode >> 8); + fetchAgentWriteData_[1] = static_cast(localNode & 0xFF); + fetchAgentWriteData_[2] = static_cast(orbAddr.addressHi >> 8); + fetchAgentWriteData_[3] = static_cast(orbAddr.addressHi & 0xFF); + const uint32_t addrLoBE = OSSwapHostToBigInt32(orbAddr.addressLo); + std::memcpy(&fetchAgentWriteData_[4], &addrLoBE, sizeof(uint32_t)); + + activeFetchAgentORB_ = orb; + fetchAgentWriteInUse_ = true; + + const std::weak_ptr weak = lifetimeToken_; + const uint16_t requestGeneration = binding_.generation; + fetchAgentWriteHandle_ = bus_.WriteBlock( + FW::Generation{binding_.generation}, + FW::NodeId{static_cast(binding_.nodeID & 0x3Fu)}, + binding_.fetchAgentAddress, + std::span{fetchAgentWriteData_.data(), fetchAgentWriteData_.size()}, + TargetSpeed(), + [this, weak, requestGeneration](Async::AsyncStatus status, std::span) { + if (weak.expired()) { + return; + } + OnFetchAgentWriteComplete(requestGeneration, status); + }); + + if (!fetchAgentWriteHandle_) { + ASFW_LOG(Async, "FetchAgent::AppendImmediate: WriteBlock failed"); + fetchAgentWriteInUse_ = false; + activeFetchAgentORB_ = nullptr; + FailORB(orb, -1, Wire::SBPStatus::kUnspecifiedError); + return false; + } + return true; +} + +bool FetchAgent::AppendChained(SBP2CommandORB* orb) noexcept { + if (chainTailORB_ == nullptr) { + chainTailORB_ = orb; + return AppendImmediate(orb); + } + + if (chainTailORB_ != orb) { + const Async::FWAddress orbAddr = orb->GetORBAddress(); + const uint16_t localNode = LocalBusNodeID(); + const uint32_t nextHi = OSSwapHostToBigInt32(ComposeBusAddressHi(localNode, orbAddr.addressHi)); + const uint32_t nextLo = OSSwapHostToBigInt32(orbAddr.addressLo); + const kern_return_t linkKr = chainTailORB_->SetNextORBAddress(nextHi, nextLo); + if (linkKr != kIOReturnSuccess) { + ASFW_LOG(Async, "FetchAgent::AppendChained: SetNextORBAddress failed: 0x%08x", linkKr); + FailORB(orb, -1, Wire::SBPStatus::kUnspecifiedError); + return false; + } + chainTailORB_ = orb; + } + return true; +} + +void FetchAgent::RingDoorbell() noexcept { + if (doorbellInProgress_) { + doorbellRingAgain_ = true; + return; + } + doorbellInProgress_ = true; + + const std::weak_ptr weak = lifetimeToken_; + const uint16_t requestGeneration = binding_.generation; + doorbellWriteHandle_ = bus_.WriteQuad( + FW::Generation{binding_.generation}, + FW::NodeId{static_cast(binding_.nodeID & 0x3Fu)}, + binding_.doorbellAddress, + 0, + TargetSpeed(), + [this, weak, requestGeneration](Async::AsyncStatus status, std::span) { + if (weak.expired()) { + return; + } + OnDoorbellComplete(requestGeneration, status); + }); + + if (!doorbellWriteHandle_) { + ASFW_LOG(Async, "FetchAgent::RingDoorbell: WriteQuad failed"); + doorbellInProgress_ = false; + } +} + +// --------------------------------------------------------------------------- +// Completions +// --------------------------------------------------------------------------- + +void FetchAgent::OnFetchAgentWriteComplete(uint16_t expectedGeneration, + Async::AsyncStatus status) noexcept { + if (!bound_ || expectedGeneration != binding_.generation) { + return; + } + + fetchAgentWriteInUse_ = false; + fetchAgentWriteHandle_ = {}; + + if (status != Async::AsyncStatus::kSuccess) { + if (activeFetchAgentORB_ != nullptr) { + uint32_t retries = activeFetchAgentORB_->GetFetchAgentWriteRetries(); + if (retries > 0) { + activeFetchAgentORB_->SetFetchAgentWriteRetries(retries - 1); + SBP2CommandORB* retryORB = activeFetchAgentORB_; + const std::weak_ptr weak = lifetimeToken_; + (void)scheduler_.ScheduleAfter( + kFetchAgentWriteRetryDelayNs, [this, weak, retryORB]() { + if (weak.expired()) { + return; + } + if (activeFetchAgentORB_ == retryORB) { + (void)AppendImmediate(retryORB); + } + }); + return; + } + + SBP2CommandORB* failedORB = activeFetchAgentORB_; + FailORB(failedORB, -1, Wire::SBPStatus::kUnspecifiedError); + FailPendingImmediate(-1, Wire::SBPStatus::kUnspecifiedError); + Clear(true); + Reset(nullptr); + } + return; + } + + // Write succeeded — the target may now fetch the ORB. Arm its timeout. + StartORBTimeout(activeFetchAgentORB_); + activeFetchAgentORB_ = nullptr; + + if (!pendingImmediateORBs_.empty()) { + SBP2CommandORB* next = pendingImmediateORBs_.front(); + pendingImmediateORBs_.pop_front(); + (void)AppendImmediate(next); + } +} + +void FetchAgent::OnDoorbellComplete(uint16_t expectedGeneration, + Async::AsyncStatus status) noexcept { + if (!bound_ || expectedGeneration != binding_.generation) { + return; + } + doorbellInProgress_ = false; + doorbellWriteHandle_ = {}; + (void)status; + + if (doorbellRingAgain_) { + doorbellRingAgain_ = false; + RingDoorbell(); + } +} + +// --------------------------------------------------------------------------- +// Status block → ORB matching +// --------------------------------------------------------------------------- + +bool FetchAgent::OnStatusBlock(const Wire::StatusBlock& block, uint32_t /*length*/) noexcept { + const uint64_t orbKey = MakeORBKey(OSSwapBigToHostInt16(block.orbOffsetHi), OSSwapBigToHostInt32(block.orbOffsetLo)); + const auto it = outstandingORBs_.find(orbKey); + if (it == outstandingORBs_.end()) { + ASFW_LOG(Async, "FetchAgent::OnStatusBlock: unmatched ORB hi=%04x lo=%08x", + OSSwapBigToHostInt16(block.orbOffsetHi), OSSwapBigToHostInt32(block.orbOffsetLo)); + return false; + } + + Outstanding entry = it->second; + outstandingORBs_.erase(it); + if (entry.timeoutToken != kInvalidSchedulerToken) { + scheduler_.Cancel(entry.timeoutToken); + } + if (entry.orb != nullptr) { + if (chainTailORB_ == entry.orb) { + chainTailORB_ = nullptr; + } + entry.orb->SetAppended(false); + auto cb = entry.orb->GetCompletionCallback(); + if (cb) { + cb(0, block.sbpStatus); + } + } + return true; +} + +// --------------------------------------------------------------------------- +// Fetch-agent reset +// --------------------------------------------------------------------------- + +void FetchAgent::Reset(std::function callback) noexcept { + if (!bound_ || agentResetInProgress_) { + if (callback) { + callback(-1); + } + return; + } + + agentResetInProgress_ = true; + agentResetCallback_ = std::move(callback); + + const std::weak_ptr weak = lifetimeToken_; + const uint16_t requestGeneration = binding_.generation; + agentResetWriteHandle_ = bus_.WriteQuad( + FW::Generation{binding_.generation}, + FW::NodeId{static_cast(binding_.nodeID & 0x3Fu)}, + binding_.agentResetAddress, + 0, + TargetSpeed(), + [this, weak, requestGeneration](Async::AsyncStatus status, std::span) { + if (weak.expired()) { + return; + } + OnAgentResetComplete(requestGeneration, status); + }); + + if (!agentResetWriteHandle_) { + ASFW_LOG(Async, "FetchAgent::Reset: WriteQuad failed"); + agentResetInProgress_ = false; + if (agentResetCallback_) { + auto cb = std::move(agentResetCallback_); + agentResetCallback_ = nullptr; + cb(-1); + } + } +} + +void FetchAgent::OnAgentResetComplete(uint16_t expectedGeneration, + Async::AsyncStatus status) noexcept { + if (expectedGeneration != binding_.generation) { + return; + } + agentResetInProgress_ = false; + Clear(true); + + if (agentResetCallback_) { + const int result = (status == Async::AsyncStatus::kSuccess) ? 0 : -1; + auto cb = std::move(agentResetCallback_); + agentResetCallback_ = nullptr; + cb(result); + } +} + +// --------------------------------------------------------------------------- +// ORB timeout + failure +// --------------------------------------------------------------------------- + +void FetchAgent::StartORBTimeout(SBP2CommandORB* orb) noexcept { + if (orb == nullptr) { + return; + } + const uint32_t timeoutMs = orb->GetTimeout(); + if (timeoutMs == 0) { + return; + } + + const auto it = outstandingORBs_.find(MakeORBKey(orb->GetORBAddress())); + if (it == outstandingORBs_.end()) { + return; + } + if (it->second.timeoutToken != kInvalidSchedulerToken) { + scheduler_.Cancel(it->second.timeoutToken); + } + + const std::weak_ptr weak = lifetimeToken_; + const uint64_t key = it->first; + it->second.timeoutToken = scheduler_.ScheduleAfter( + static_cast(timeoutMs) * 1'000'000ULL, [this, weak, key]() { + if (weak.expired()) { + return; + } + const auto entryIt = outstandingORBs_.find(key); + if (entryIt == outstandingORBs_.end()) { + return; + } + SBP2CommandORB* timedOut = entryIt->second.orb; + FailORB(timedOut, -1, Wire::SBPStatus::kUnspecifiedError); + }); +} + +void FetchAgent::FailORB(SBP2CommandORB* orb, int transportStatus, uint8_t sbpStatus) noexcept { + if (orb == nullptr) { + return; + } + const auto it = outstandingORBs_.find(MakeORBKey(orb->GetORBAddress())); + if (it != outstandingORBs_.end()) { + if (it->second.timeoutToken != kInvalidSchedulerToken) { + scheduler_.Cancel(it->second.timeoutToken); + } + outstandingORBs_.erase(it); + } + pendingImmediateORBs_.erase( + std::remove(pendingImmediateORBs_.begin(), pendingImmediateORBs_.end(), orb), + pendingImmediateORBs_.end()); + if (activeFetchAgentORB_ == orb) { + activeFetchAgentORB_ = nullptr; + } + if (chainTailORB_ == orb) { + chainTailORB_ = nullptr; + } + orb->SetAppended(false); + + auto cb = orb->GetCompletionCallback(); + if (cb) { + cb(transportStatus, sbpStatus); + } +} + +void FetchAgent::FailPendingImmediate(int transportStatus, uint8_t sbpStatus) noexcept { + auto pending = pendingImmediateORBs_; + for (auto* orb : pending) { + FailORB(orb, transportStatus, sbpStatus); + } +} + +void FetchAgent::CompleteORB(SBP2CommandORB* orb, int transportStatus, uint8_t sbpStatus) noexcept { + if (orb == nullptr) { + return; + } + auto cb = orb->GetCompletionCallback(); + if (cb) { + cb(transportStatus, sbpStatus); + } +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +uint16_t FetchAgent::LocalBusNodeID() const noexcept { + return NormalizeBusNodeID(static_cast(busInfo_.GetLocalNodeID().value)); +} + +FW::FwSpeed FetchAgent::TargetSpeed() const noexcept { + return busInfo_.GetSpeed(FW::NodeId{static_cast(binding_.nodeID & 0x3Fu)}); +} + +uint64_t FetchAgent::MakeORBKey(uint16_t addressHi, uint32_t addressLo) noexcept { + return (static_cast(addressHi) << 32) | static_cast(addressLo); +} + +uint64_t FetchAgent::MakeORBKey(const Async::FWAddress& address) noexcept { + return MakeORBKey(address.addressHi, address.addressLo); +} + +uint16_t FetchAgent::MaxPayloadLog() const noexcept { + uint16_t payloadBytes = binding_.maxPayloadSize; + if (payloadBytes > 4096) { + payloadBytes = 4096; + } + const uint16_t quadlets = payloadBytes / 4; + if (quadlets == 0) { + return 0; + } + uint16_t log = 0; + while ((1u << log) < quadlets && log < 15) { + ++log; + } + return log; +} + +} // namespace ASFW::Protocols::SBP2 diff --git a/ASFWDriver/Protocols/SBP2/Session/FetchAgent.hpp b/ASFWDriver/Protocols/SBP2/Session/FetchAgent.hpp new file mode 100644 index 00000000..5da8c6fa --- /dev/null +++ b/ASFWDriver/Protocols/SBP2/Session/FetchAgent.hpp @@ -0,0 +1,141 @@ +#pragma once + +// FetchAgent — SBP-2 command-plane engine. +// +// Submits Normal Command ORBs to a logged-in target: writes the first immediate +// ORB to the fetch-agent register, chains subsequent ORBs via their next-ORB +// pointer, rings the doorbell, retries failed fetch-agent writes, tracks +// outstanding ORBs, times them out, and matches incoming status blocks back to +// the ORB that produced them. +// +// Owned by SBP2LoginSession (composition). The login *state* (LoginState, +// loginID, generation) stays in LoginSession; FetchAgent holds only the ORB +// mechanics and is driven through an explicit Binding the session supplies once +// a login or reconnect succeeds. CommandExecutor submits ORBs via the session. +// +// Timers go through the injected ISessionScheduler (never the IOSleep-on-queue +// path). Async bus callbacks are guarded by a weak lifetime token. + +#include "ISessionScheduler.hpp" +#include "../SBP2CommandORB.hpp" +#include "../SBP2WireFormats.hpp" +#include "../../../Async/AsyncTypes.hpp" +#include "../../../Async/Interfaces/IFireWireBus.hpp" +#include "../../../Logging/Logging.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace ASFW::Protocols::SBP2 { + +class FetchAgent { +public: + // Supplied by LoginSession when a login/reconnect succeeds. Holds the target + // node + generation and the three command-block-agent register addresses. + struct Binding { + uint16_t generation{0}; + uint16_t nodeID{0xFFFF}; + Async::FWAddress fetchAgentAddress{}; + Async::FWAddress doorbellAddress{}; + Async::FWAddress agentResetAddress{}; + uint16_t maxPayloadSize{4096}; + }; + + FetchAgent(Async::IFireWireBus& bus, + Async::IFireWireBusInfo& busInfo, + ISessionScheduler& scheduler) noexcept; + ~FetchAgent(); + + FetchAgent(const FetchAgent&) = delete; + FetchAgent& operator=(const FetchAgent&) = delete; + + // Bind to a freshly logged-in target. Re-binding (reconnect) updates the + // generation/addresses; outstanding ORBs are cleared first. + void Bind(const Binding& binding) noexcept; + + // Detach (logout / lost). Cancels and drops all outstanding ORBs. + void Unbind() noexcept; + + [[nodiscard]] bool IsBound() const noexcept { return bound_; } + + // Submit a Normal Command ORB. Returns false if not bound, or the ORB is + // null / not allocated (IsValid) / already appended. + [[nodiscard]] bool Submit(SBP2CommandORB* orb) noexcept; + + // Feed a status block arriving on the session's status FIFO. Returns true if + // it matched and completed an outstanding ORB; false if unsolicited. + [[nodiscard]] bool OnStatusBlock(const Wire::StatusBlock& block, + uint32_t length) noexcept; + + // Issue a fetch-agent reset. callback(0)=success, callback(-1)=failure. The + // ORB chain is cleared once the reset completes. + void Reset(std::function callback) noexcept; + + // Cancel timers and drop all ORB tracking (bus reset / logout). + void Clear(bool cancelTimers) noexcept; + + // Test seam: override the per-ORB fetch-agent-write retry budget. + void SetWriteRetriesForTesting(uint32_t retries) noexcept { defaultWriteRetries_ = retries; } + +private: + struct Outstanding { + SBP2CommandORB* orb{nullptr}; + SchedulerToken timeoutToken{kInvalidSchedulerToken}; + }; + + [[nodiscard]] bool AppendImmediate(SBP2CommandORB* orb) noexcept; + [[nodiscard]] bool AppendChained(SBP2CommandORB* orb) noexcept; + void RingDoorbell() noexcept; + + void OnFetchAgentWriteComplete(uint16_t expectedGeneration, + Async::AsyncStatus status) noexcept; + void OnDoorbellComplete(uint16_t expectedGeneration, + Async::AsyncStatus status) noexcept; + void OnAgentResetComplete(uint16_t expectedGeneration, + Async::AsyncStatus status) noexcept; + + void StartORBTimeout(SBP2CommandORB* orb) noexcept; + void FailORB(SBP2CommandORB* orb, int transportStatus, uint8_t sbpStatus) noexcept; + void FailPendingImmediate(int transportStatus, uint8_t sbpStatus) noexcept; + void CompleteORB(SBP2CommandORB* orb, int transportStatus, uint8_t sbpStatus) noexcept; + + [[nodiscard]] uint16_t LocalBusNodeID() const noexcept; + [[nodiscard]] FW::FwSpeed TargetSpeed() const noexcept; + [[nodiscard]] static uint64_t MakeORBKey(uint16_t addressHi, uint32_t addressLo) noexcept; + [[nodiscard]] static uint64_t MakeORBKey(const Async::FWAddress& address) noexcept; + [[nodiscard]] uint16_t MaxPayloadLog() const noexcept; + + Async::IFireWireBus& bus_; + Async::IFireWireBusInfo& busInfo_; + ISessionScheduler& scheduler_; + + bool bound_{false}; + Binding binding_{}; + + std::unordered_map outstandingORBs_; + std::deque pendingImmediateORBs_; + SBP2CommandORB* chainTailORB_{nullptr}; + SBP2CommandORB* activeFetchAgentORB_{nullptr}; + + std::array fetchAgentWriteData_{}; + Async::AsyncHandle fetchAgentWriteHandle_{}; + bool fetchAgentWriteInUse_{false}; + + Async::AsyncHandle doorbellWriteHandle_{}; + bool doorbellInProgress_{false}; + bool doorbellRingAgain_{false}; + + Async::AsyncHandle agentResetWriteHandle_{}; + bool agentResetInProgress_{false}; + std::function agentResetCallback_; + + uint32_t defaultWriteRetries_{20}; + std::shared_ptr lifetimeToken_{std::make_shared(0)}; +}; + +} // namespace ASFW::Protocols::SBP2 diff --git a/ASFWDriver/Protocols/SBP2/Session/ISessionScheduler.hpp b/ASFWDriver/Protocols/SBP2/Session/ISessionScheduler.hpp new file mode 100644 index 00000000..aad8b270 --- /dev/null +++ b/ASFWDriver/Protocols/SBP2/Session/ISessionScheduler.hpp @@ -0,0 +1,45 @@ +#pragma once + +// ISessionScheduler — injected one-shot timer abstraction for the SBP-2 session +// components (login/reconnect/logout/busy timeouts, command timeouts). +// +// The session components are plain C++ (POCO) so they can be host-tested under +// ASFW_HOST_TEST; they cannot host an OSAction directly (an OSAction target must +// be an IIG TYPE()-declared method on a DriverKit class). They therefore depend +// on this interface instead of scheduling timers themselves. +// +// - Production: backed by a single driver-level IOTimerDispatchSource + OSAction +// owner (the WatchdogCoordinator precedent), wired in FW-58. This avoids the +// SBP2DelayedDispatch IOSleep-on-queue pattern, which blocks the dext's single +// Default queue thread for the whole delay. +// - Host tests: a deterministic virtual-clock fake (FakeSessionScheduler). +// +// Threading: callbacks fire on the owning dispatch queue (the single Default +// queue), so components need no extra locking beyond their existing generation + +// weak-ownership guards. + +#include +#include + +namespace ASFW::Protocols::SBP2 { + +// Cancellation token for a scheduled callback. kInvalidSchedulerToken (0) is +// never returned by ScheduleAfter and is always a safe no-op for Cancel(). +using SchedulerToken = uint64_t; +inline constexpr SchedulerToken kInvalidSchedulerToken = 0; + +class ISessionScheduler { +public: + virtual ~ISessionScheduler() = default; + + // Schedule `fn` to run once, `delayNs` from now. Returns a token usable with + // Cancel(). A delay of 0 still defers (the callback never runs inline). + [[nodiscard]] virtual SchedulerToken ScheduleAfter(uint64_t delayNs, + std::function fn) = 0; + + // Cancel a pending callback. No-op if it already fired, was already canceled, + // or the token is invalid. After Cancel() returns, the callback will not run. + virtual void Cancel(SchedulerToken token) = 0; +}; + +} // namespace ASFW::Protocols::SBP2 diff --git a/ASFWDriver/Protocols/SBP2/Session/LoginSession.cpp b/ASFWDriver/Protocols/SBP2/Session/LoginSession.cpp new file mode 100644 index 00000000..8e9d0465 --- /dev/null +++ b/ASFWDriver/Protocols/SBP2/Session/LoginSession.cpp @@ -0,0 +1,1026 @@ +// LoginSession — SBP-2 login/reconnect/logout state machine. See LoginSession.hpp. +// +// Ported from PR #19's SBP2LoginSession, decomposed for DICE: the command plane +// moved to FetchAgent (composed here), timers go through ISessionScheduler (one +// cancelable management timer at a time) instead of the two-queue IOSleep model, +// and management async writes/the status-FIFO callback capture weak_from_this. + +#include "LoginSession.hpp" + +#include "../../../Async/Interfaces/IFireWireBus.hpp" +#include "../../../Async/Interfaces/IFireWireBusInfo.hpp" +#include "../../../Common/FWCommon.hpp" + +#include + +namespace ASFW::Protocols::SBP2 { + +using namespace ASFW::Protocols::SBP2::Wire; + +// DICE's logging has no dedicated SBP-2 category; the session/command layer logs +// under the Async subsystem category (matching FetchAgent). +#define ASFW_LOG_SBP2(fmt, ...) ASFW_LOG(Async, fmt, ##__VA_ARGS__) + +// --------------------------------------------------------------------------- +// Construction / Destruction +// --------------------------------------------------------------------------- + +LoginSession::LoginSession(Async::IFireWireBus& bus, + Async::IFireWireBusInfo& busInfo, + AddressSpaceManager& addrSpaceMgr, + ISessionScheduler& scheduler) + : bus_(bus) + , busInfo_(busInfo) + , addrSpaceMgr_(addrSpaceMgr) + , scheduler_(scheduler) + , fetchAgent_(bus, busInfo, scheduler) {} + +LoginSession::~LoginSession() { + CancelManagementTimer(); + if (loginWriteHandle_) { + bus_.Cancel(loginWriteHandle_); + loginWriteHandle_ = {}; + } + if (reconnectWriteHandle_) { + bus_.Cancel(reconnectWriteHandle_); + reconnectWriteHandle_ = {}; + } + if (logoutWriteHandle_) { + bus_.Cancel(logoutWriteHandle_); + logoutWriteHandle_ = {}; + } + if (unsolicitedStatusWriteHandle_) { + bus_.Cancel(unsolicitedStatusWriteHandle_); + unsolicitedStatusWriteHandle_ = {}; + } + if (busyTimeoutWriteHandle_) { + bus_.Cancel(busyTimeoutWriteHandle_); + busyTimeoutWriteHandle_ = {}; + } + fetchAgent_.Unbind(); + DeallocateResources(); +} + +// --------------------------------------------------------------------------- +// Configuration +// --------------------------------------------------------------------------- + +void LoginSession::Configure(const SBP2TargetInfo& info) noexcept { + targetInfo_ = info; + configured_ = true; + + ASFW_LOG_SBP2( + "LoginSession: configured target node=0x%04x mgmt_offset=%u LUN=%u " + "mgmt_timeout=%ums max_orb=%u max_cmd_block=%u", + info.targetNodeId, info.managementAgentOffset, info.lun, + info.managementTimeoutMs, info.maxORBSize, info.maxCommandBlockSize); +} + +// --------------------------------------------------------------------------- +// Login +// --------------------------------------------------------------------------- + +bool LoginSession::Login() noexcept { + if (!configured_) { + ASFW_LOG_SBP2( "LoginSession::Login: not configured"); + return false; + } + if (state_ == LoginState::LoggingIn || state_ == LoginState::LoggedIn) { + ASFW_LOG_SBP2( "LoginSession::Login: state=%s, ignoring", ToString(state_)); + return false; + } + + if (!AllocateResources()) { + ASFW_LOG_SBP2( "LoginSession::Login: resource allocation failed"); + SetState(LoginState::Failed); + return false; + } + + SetState(LoginState::LoggingIn); + loginGeneration_ = static_cast(busInfo_.GetGeneration().value); + loginNodeID_ = targetInfo_.targetNodeId; + + BuildLoginORB(); + + const FW::Generation gen{loginGeneration_}; + const FW::NodeId node{static_cast(TargetNodeShort())}; + const Async::FWAddress mgmtAddr{Async::FWAddress::QualifiedAddressParts{ + .addressHi = 0xFFFF, + .addressLo = ManagementAgentAddressLo(targetInfo_.managementAgentOffset), + .nodeID = loginNodeID_}}; + const FW::FwSpeed speed = busInfo_.GetSpeed(node); + + const std::weak_ptr weakSelf = weak_from_this(); + loginWriteHandle_ = bus_.WriteBlock( + gen, node, mgmtAddr, + std::span{loginORBAddressBE_.data(), loginORBAddressBE_.size()}, + speed, + [weakSelf, requestGeneration = loginGeneration_](Async::AsyncStatus status, + std::span) { + if (auto self = weakSelf.lock()) { + self->OnLoginWriteComplete(requestGeneration, status); + } + }); + + if (!loginWriteHandle_) { + ASFW_LOG_SBP2( "LoginSession::Login: WriteBlock failed immediately"); + SetState(LoginState::Failed); + return false; + } + + StartLoginTimer(); + return true; +} + +// --------------------------------------------------------------------------- +// Logout +// --------------------------------------------------------------------------- + +bool LoginSession::Logout() noexcept { + if (state_ != LoginState::LoggedIn && state_ != LoginState::Suspended) { + ASFW_LOG_SBP2( "LoginSession::Logout: state=%s, ignoring", ToString(state_)); + return false; + } + + SetState(LoginState::LoggingOut); + fetchAgent_.Unbind(); + BuildLogoutORB(); + + const FW::Generation gen{loginGeneration_}; + const FW::NodeId node{static_cast(TargetNodeShort())}; + const Async::FWAddress mgmtAddr{Async::FWAddress::QualifiedAddressParts{ + .addressHi = 0xFFFF, + .addressLo = ManagementAgentAddressLo(targetInfo_.managementAgentOffset), + .nodeID = loginNodeID_}}; + const FW::FwSpeed speed = busInfo_.GetSpeed(node); + + const std::weak_ptr weakSelf = weak_from_this(); + logoutWriteHandle_ = bus_.WriteBlock( + gen, node, mgmtAddr, + std::span{logoutORBAddressBE_.data(), logoutORBAddressBE_.size()}, + speed, + [weakSelf, requestGeneration = loginGeneration_](Async::AsyncStatus status, + std::span) { + if (auto self = weakSelf.lock()) { + self->OnLogoutWriteComplete(requestGeneration, status); + } + }); + + if (!logoutWriteHandle_) { + ASFW_LOG_SBP2( "LoginSession::Logout: WriteBlock failed"); + SetState(LoginState::Failed); + return false; + } + + StartLogoutTimer(); + return true; +} + +// --------------------------------------------------------------------------- +// Reconnect +// --------------------------------------------------------------------------- + +bool LoginSession::Reconnect() noexcept { + if (state_ != LoginState::Suspended && state_ != LoginState::LoggedIn) { + ASFW_LOG_SBP2( "LoginSession::Reconnect: state=%s, ignoring", ToString(state_)); + return false; + } + + SetState(LoginState::Reconnecting); + loginGeneration_ = static_cast(busInfo_.GetGeneration().value); + loginNodeID_ = targetInfo_.targetNodeId; + + if (!AllocateResources()) { + ASFW_LOG_SBP2( "LoginSession::Reconnect: resource allocation failed"); + SetState(LoginState::Failed); + return false; + } + + BuildReconnectORB(); + + const FW::Generation gen{loginGeneration_}; + const FW::NodeId node{static_cast(TargetNodeShort())}; + const Async::FWAddress mgmtAddr{Async::FWAddress::QualifiedAddressParts{ + .addressHi = 0xFFFF, + .addressLo = ManagementAgentAddressLo(targetInfo_.managementAgentOffset), + .nodeID = loginNodeID_}}; + const FW::FwSpeed speed = busInfo_.GetSpeed(node); + + const std::weak_ptr weakSelf = weak_from_this(); + reconnectWriteHandle_ = bus_.WriteBlock( + gen, node, mgmtAddr, + std::span{reconnectORBAddressBE_.data(), reconnectORBAddressBE_.size()}, + speed, + [weakSelf, requestGeneration = loginGeneration_](Async::AsyncStatus status, + std::span) { + if (auto self = weakSelf.lock()) { + self->OnReconnectWriteComplete(requestGeneration, status); + } + }); + + if (!reconnectWriteHandle_) { + ASFW_LOG_SBP2( "LoginSession::Reconnect: WriteBlock failed, will retry"); + ArmManagementTimer(kLoginRetryDelayMs, [this]() { OnReconnectTimeout(); }); + return true; // will retry + } + + return true; +} + +// --------------------------------------------------------------------------- +// Bus Reset Handling +// --------------------------------------------------------------------------- + +void LoginSession::HandleBusReset(uint16_t newGeneration) noexcept { + ASFW_LOG_SBP2( "LoginSession::HandleBusReset: state=%s newGen=%u loginGen=%u", + ToString(state_), newGeneration, loginGeneration_); + + switch (state_) { + case LoginState::LoggingIn: + CancelManagementTimer(); + loginRetryCount_ = 0; + loginGeneration_ = newGeneration; + fetchAgent_.Unbind(); + DeallocateResources(); + SetState(LoginState::Idle); + ArmManagementTimer(100, [this]() { (void)Login(); }); + break; + + case LoginState::LoggedIn: + CancelManagementTimer(); + fetchAgent_.Unbind(); + DeallocateResources(); + SetState(LoginState::Suspended); + loginGeneration_ = newGeneration; + break; + + case LoginState::Reconnecting: + CancelManagementTimer(); + fetchAgent_.Unbind(); + DeallocateResources(); + loginGeneration_ = newGeneration; + SetState(LoginState::Suspended); + ArmManagementTimer(100, [this]() { (void)Reconnect(); }); + break; + + case LoginState::LoggingOut: + CancelManagementTimer(); + fetchAgent_.Unbind(); + DeallocateResources(); + SetState(LoginState::Idle); + break; + + case LoginState::Failed: + DeallocateResources(); + break; + + default: + break; + } +} + +// --------------------------------------------------------------------------- +// Accessors +// --------------------------------------------------------------------------- + +uint32_t LoginSession::ReconnectHoldSeconds() const noexcept { + return reconnectHold_ > 0 ? (1u << reconnectHold_) : 0; +} + +// --------------------------------------------------------------------------- +// Resource Allocation +// --------------------------------------------------------------------------- + +bool LoginSession::AllocateResources() noexcept { + const bool allResourcesAllocated = loginORBHandle_ != 0 && loginResponseHandle_ != 0 && + statusBlockHandle_ != 0 && reconnectORBHandle_ != 0 && + logoutORBHandle_ != 0; + const bool anyResourceAllocated = loginORBHandle_ != 0 || loginResponseHandle_ != 0 || + statusBlockHandle_ != 0 || reconnectORBHandle_ != 0 || + logoutORBHandle_ != 0; + + if (allResourcesAllocated) { + // Re-register the callback in case a reset path cleared it. + RegisterStatusBlockCallback(); + return true; + } + + if (anyResourceAllocated) { + ASFW_LOG_SBP2( "LoginSession: inconsistent resource state before allocation"); + DeallocateResources(); + } + + if (!AllocateLoginORBAddressSpace()) { + return false; + } + if (!AllocateLoginResponseAddressSpace()) { + DeallocateResources(); + return false; + } + if (!AllocateStatusBlockAddressSpace()) { + DeallocateResources(); + return false; + } + if (!AllocateReconnectORBAddressSpace()) { + DeallocateResources(); + return false; + } + if (!AllocateLogoutORBAddressSpace()) { + DeallocateResources(); + return false; + } + + RegisterStatusBlockCallback(); + ASFW_LOG_SBP2( "LoginSession: all address spaces allocated"); + return true; +} + +void LoginSession::RegisterStatusBlockCallback() noexcept { + if (statusBlockHandle_ == 0) { + return; + } + // AddressSpaceManager dispatches outside its lock, so never capture raw this. + const std::weak_ptr weakSelf = weak_from_this(); + addrSpaceMgr_.SetRemoteWriteCallback( + statusBlockHandle_, + [weakSelf](uint64_t /*handle*/, uint32_t offset, std::span payload) { + if (auto self = weakSelf.lock()) { + self->OnStatusBlockRemoteWrite(offset, payload); + } + }); +} + +void LoginSession::DeallocateResources() noexcept { + if (loginORBHandle_) { + addrSpaceMgr_.DeallocateAddressRange(this, loginORBHandle_); + loginORBHandle_ = 0; + loginORBMeta_ = {}; + } + if (loginResponseHandle_) { + addrSpaceMgr_.DeallocateAddressRange(this, loginResponseHandle_); + loginResponseHandle_ = 0; + loginResponseMeta_ = {}; + } + if (statusBlockHandle_) { + addrSpaceMgr_.SetRemoteWriteCallback(statusBlockHandle_, {}); + addrSpaceMgr_.DeallocateAddressRange(this, statusBlockHandle_); + statusBlockHandle_ = 0; + statusBlockMeta_ = {}; + } + if (reconnectORBHandle_) { + addrSpaceMgr_.DeallocateAddressRange(this, reconnectORBHandle_); + reconnectORBHandle_ = 0; + reconnectORBMeta_ = {}; + } + if (logoutORBHandle_) { + addrSpaceMgr_.DeallocateAddressRange(this, logoutORBHandle_); + logoutORBHandle_ = 0; + logoutORBMeta_ = {}; + } + + loginORBAddressBE_ = {}; + reconnectORBAddressBE_ = {}; + logoutORBAddressBE_ = {}; +} + +bool LoginSession::AllocateLoginORBAddressSpace() noexcept { + const auto kr = addrSpaceMgr_.AllocateAddressRangeAuto( + this, 0xFFFF, Wire::LoginORB::kSize, &loginORBHandle_, &loginORBMeta_); + if (kr != kIOReturnSuccess) { + ASFW_LOG_SBP2( "LoginSession: failed to allocate login ORB address space: 0x%08x", kr); + return false; + } + addrSpaceMgr_.SetDebugLabel(loginORBHandle_, "sbp2-login-orb"); + return true; +} + +bool LoginSession::AllocateLoginResponseAddressSpace() noexcept { + const auto kr = addrSpaceMgr_.AllocateAddressRangeAuto( + this, 0xFFFF, Wire::LoginResponse::kSize, &loginResponseHandle_, &loginResponseMeta_); + if (kr != kIOReturnSuccess) { + ASFW_LOG_SBP2( "LoginSession: failed to allocate login response address space: 0x%08x", kr); + return false; + } + addrSpaceMgr_.SetDebugLabel(loginResponseHandle_, "sbp2-login-response"); + return true; +} + +bool LoginSession::AllocateStatusBlockAddressSpace() noexcept { + const auto kr = addrSpaceMgr_.AllocateAddressRangeAuto( + this, 0xFFFF, Wire::StatusBlock::kMaxSize, &statusBlockHandle_, &statusBlockMeta_); + if (kr != kIOReturnSuccess) { + ASFW_LOG_SBP2( "LoginSession: failed to allocate status block address space: 0x%08x", kr); + return false; + } + addrSpaceMgr_.SetDebugLabel(statusBlockHandle_, "sbp2-status-fifo"); + return true; +} + +bool LoginSession::AllocateReconnectORBAddressSpace() noexcept { + const auto kr = addrSpaceMgr_.AllocateAddressRangeAuto( + this, 0xFFFF, Wire::ReconnectORB::kSize, &reconnectORBHandle_, &reconnectORBMeta_); + if (kr != kIOReturnSuccess) { + ASFW_LOG_SBP2( "LoginSession: failed to allocate reconnect ORB address space: 0x%08x", kr); + return false; + } + addrSpaceMgr_.SetDebugLabel(reconnectORBHandle_, "sbp2-reconnect-orb"); + return true; +} + +bool LoginSession::AllocateLogoutORBAddressSpace() noexcept { + const auto kr = addrSpaceMgr_.AllocateAddressRangeAuto( + this, 0xFFFF, Wire::LogoutORB::kSize, &logoutORBHandle_, &logoutORBMeta_); + if (kr != kIOReturnSuccess) { + ASFW_LOG_SBP2( "LoginSession: failed to allocate logout ORB address space: 0x%08x", kr); + return false; + } + addrSpaceMgr_.SetDebugLabel(logoutORBHandle_, "sbp2-logout-orb"); + return true; +} + +// --------------------------------------------------------------------------- +// ORB Construction +// --------------------------------------------------------------------------- + +void LoginSession::BuildLoginORB() noexcept { + std::memset(&loginORBBuffer_, 0, sizeof(loginORBBuffer_)); + + const uint16_t localNode = + NormalizeBusNodeID(static_cast(busInfo_.GetLocalNodeID().value)); + + loginORBBuffer_.loginResponseAddressHi = + OSSwapHostToBigInt32(ComposeBusAddressHi(localNode, loginResponseMeta_.addressHi)); + loginORBBuffer_.loginResponseAddressLo = OSSwapHostToBigInt32(loginResponseMeta_.addressLo); + loginORBBuffer_.options = + static_cast(Options::kLoginNotify | Options::kExclusiveLogin); + loginORBBuffer_.lun = OSSwapHostToBigInt16(targetInfo_.lun); + loginORBBuffer_.passwordLength = 0; + loginORBBuffer_.loginResponseLength = OSSwapHostToBigInt16(sizeof(Wire::LoginResponse)); + loginORBBuffer_.statusFIFOAddressHi = + OSSwapHostToBigInt32(ComposeBusAddressHi(localNode, statusBlockMeta_.addressHi)); + loginORBBuffer_.statusFIFOAddressLo = OSSwapHostToBigInt32(statusBlockMeta_.addressLo); + + addrSpaceMgr_.WriteLocalData( + this, loginORBHandle_, 0, + std::span{reinterpret_cast(&loginORBBuffer_), + sizeof(loginORBBuffer_)}); + + // Management agent write payload: [nodeID(2)][addressHi(2)][addressLo(4)] BE. + loginORBAddressBE_[0] = static_cast(localNode >> 8); + loginORBAddressBE_[1] = static_cast(localNode & 0xFF); + loginORBAddressBE_[2] = static_cast(loginORBMeta_.addressHi >> 8); + loginORBAddressBE_[3] = static_cast(loginORBMeta_.addressHi & 0xFF); + const uint32_t orbAddrLoBE = OSSwapHostToBigInt32(loginORBMeta_.addressLo); + std::memcpy(&loginORBAddressBE_[4], &orbAddrLoBE, sizeof(uint32_t)); +} + +void LoginSession::BuildReconnectORB() noexcept { + std::memset(&reconnectORBBuffer_, 0, sizeof(reconnectORBBuffer_)); + + const uint16_t localNode = + NormalizeBusNodeID(static_cast(busInfo_.GetLocalNodeID().value)); + + reconnectORBBuffer_.options = Options::kReconnectNotify; + reconnectORBBuffer_.loginID = OSSwapHostToBigInt16(loginID_); + reconnectORBBuffer_.statusFIFOAddressHi = + OSSwapHostToBigInt32(ComposeBusAddressHi(localNode, statusBlockMeta_.addressHi)); + reconnectORBBuffer_.statusFIFOAddressLo = OSSwapHostToBigInt32(statusBlockMeta_.addressLo); + + addrSpaceMgr_.WriteLocalData( + this, reconnectORBHandle_, 0, + std::span{reinterpret_cast(&reconnectORBBuffer_), + sizeof(reconnectORBBuffer_)}); + + reconnectORBAddressBE_[0] = static_cast(localNode >> 8); + reconnectORBAddressBE_[1] = static_cast(localNode & 0xFF); + reconnectORBAddressBE_[2] = static_cast(reconnectORBMeta_.addressHi >> 8); + reconnectORBAddressBE_[3] = static_cast(reconnectORBMeta_.addressHi & 0xFF); + const uint32_t addrLoBE = OSSwapHostToBigInt32(reconnectORBMeta_.addressLo); + std::memcpy(&reconnectORBAddressBE_[4], &addrLoBE, sizeof(uint32_t)); +} + +void LoginSession::BuildLogoutORB() noexcept { + std::memset(&logoutORBBuffer_, 0, sizeof(logoutORBBuffer_)); + + const uint16_t localNode = + NormalizeBusNodeID(static_cast(busInfo_.GetLocalNodeID().value)); + + logoutORBBuffer_.options = Options::kLogoutNotify; + logoutORBBuffer_.loginID = OSSwapHostToBigInt16(loginID_); + logoutORBBuffer_.statusFIFOAddressHi = + OSSwapHostToBigInt32(ComposeBusAddressHi(localNode, statusBlockMeta_.addressHi)); + logoutORBBuffer_.statusFIFOAddressLo = OSSwapHostToBigInt32(statusBlockMeta_.addressLo); + + addrSpaceMgr_.WriteLocalData( + this, logoutORBHandle_, 0, + std::span{reinterpret_cast(&logoutORBBuffer_), + sizeof(logoutORBBuffer_)}); + + logoutORBAddressBE_[0] = static_cast(localNode >> 8); + logoutORBAddressBE_[1] = static_cast(localNode & 0xFF); + logoutORBAddressBE_[2] = static_cast(logoutORBMeta_.addressHi >> 8); + logoutORBAddressBE_[3] = static_cast(logoutORBMeta_.addressHi & 0xFF); + const uint32_t addrLoBE = OSSwapHostToBigInt32(logoutORBMeta_.addressLo); + std::memcpy(&logoutORBAddressBE_[4], &addrLoBE, sizeof(uint32_t)); +} + +// --------------------------------------------------------------------------- +// Command-plane binding +// --------------------------------------------------------------------------- + +void LoginSession::BindFetchAgent() noexcept { + const auto cbaAddr = [&](uint32_t offset) { + return Async::FWAddress{Async::FWAddress::QualifiedAddressParts{ + .addressHi = commandBlockAgent_.addressHi, + .addressLo = commandBlockAgent_.addressLo + offset, + .nodeID = loginNodeID_}}; + }; + + FetchAgent::Binding binding{}; + binding.generation = loginGeneration_; + binding.nodeID = loginNodeID_; + binding.fetchAgentAddress = cbaAddr(Wire::CommandBlockAgentOffsets::kFetchAgent); + binding.doorbellAddress = cbaAddr(Wire::CommandBlockAgentOffsets::kDoorbell); + binding.agentResetAddress = cbaAddr(Wire::CommandBlockAgentOffsets::kAgentReset); + binding.maxPayloadSize = maxPayloadSize_; + fetchAgent_.Bind(binding); + + RefreshUnsolicitedStatusAddress(); +} + +void LoginSession::RefreshUnsolicitedStatusAddress() noexcept { + unsolicitedStatusAddress_ = Async::FWAddress{Async::FWAddress::QualifiedAddressParts{ + .addressHi = commandBlockAgent_.addressHi, + .addressLo = commandBlockAgent_.addressLo + Wire::CommandBlockAgentOffsets::kUnsolicitedStatusEnable, + .nodeID = loginNodeID_}}; +} + +// --------------------------------------------------------------------------- +// Management Write Completions +// --------------------------------------------------------------------------- + +void LoginSession::OnLoginWriteComplete(uint16_t expectedGeneration, + Async::AsyncStatus status) noexcept { + if (expectedGeneration != loginGeneration_ || state_ != LoginState::LoggingIn) { + return; + } + + CancelManagementTimer(); + + if (status != Async::AsyncStatus::kSuccess) { + if (loginRetryCount_ < kLoginRetryMax) { + loginRetryCount_++; + ArmManagementTimer(kLoginRetryDelayMs, [this]() { + loginGeneration_ = static_cast(busInfo_.GetGeneration().value); + loginNodeID_ = targetInfo_.targetNodeId; + SetState(LoginState::Idle); + (void)Login(); + }); + return; + } + + ASFW_LOG_SBP2( "LoginSession: login retries exhausted"); + SetState(LoginState::Failed); + if (loginCallback_) { + LoginCompleteParams params{}; + params.status = -1; + params.generation = loginGeneration_; + loginCallback_(params); + } + return; + } + + // Management agent write ACK'd. Wait for the device to write the status block; + // restart the timer for the device processing window. + StartLoginTimer(); +} + +void LoginSession::OnLoginTimeout() noexcept { + if (state_ != LoginState::LoggingIn) { + return; + } + + if (loginRetryCount_ < kLoginRetryMax) { + loginRetryCount_++; + SetState(LoginState::Idle); + (void)Login(); + } else { + SetState(LoginState::Failed); + if (loginCallback_) { + LoginCompleteParams params{}; + params.status = -2; // timeout + params.generation = loginGeneration_; + loginCallback_(params); + } + } +} + +void LoginSession::OnReconnectWriteComplete(uint16_t expectedGeneration, + Async::AsyncStatus status) noexcept { + if (expectedGeneration != loginGeneration_ || state_ != LoginState::Reconnecting) { + return; + } + + if (status != Async::AsyncStatus::kSuccess) { + ASFW_LOG_SBP2( "LoginSession::OnReconnectWriteComplete: status=%s, retrying", + Async::ToString(status)); + ArmManagementTimer(100, [this]() { (void)Reconnect(); }); + return; + } + + // Reconnect ORB write ACK'd. Wait for the status block from the device. + StartReconnectTimer(); +} + +void LoginSession::OnReconnectTimeout() noexcept { + if (state_ != LoginState::Reconnecting) { + return; + } + ASFW_LOG_SBP2( "LoginSession: reconnect timeout, falling back to full login"); + SetState(LoginState::Idle); + (void)Login(); +} + +void LoginSession::OnLogoutWriteComplete(uint16_t expectedGeneration, + Async::AsyncStatus status) noexcept { + if (expectedGeneration != loginGeneration_ || state_ != LoginState::LoggingOut) { + return; + } + + if (status != Async::AsyncStatus::kSuccess) { + CancelManagementTimer(); + loginID_ = 0; + SetState(LoginState::Idle); + if (logoutCallback_) { + LogoutCompleteParams params{}; + params.status = -1; + params.generation = loginGeneration_; + logoutCallback_(params); + } + return; + } + + StartLogoutTimer(); +} + +void LoginSession::OnLogoutTimeout() noexcept { + if (state_ != LoginState::LoggingOut) { + return; + } + ASFW_LOG_SBP2( "LoginSession: logout timeout, transitioning to Idle anyway"); + loginID_ = 0; + SetState(LoginState::Idle); + if (logoutCallback_) { + LogoutCompleteParams params{}; + params.status = -2; + params.generation = loginGeneration_; + logoutCallback_(params); + } +} + +// --------------------------------------------------------------------------- +// Status Block Handling +// --------------------------------------------------------------------------- + +void LoginSession::OnStatusBlockRemoteWrite(uint32_t offset, + std::span payload) noexcept { + if (payload.empty()) { + return; + } + + Wire::StatusBlock block{}; + uint32_t len = static_cast(payload.size()); + if (len > sizeof(block)) { + len = sizeof(block); + } + std::memcpy(&block, payload.data(), len); + + ASFW_LOG_SBP2( "LoginSession::OnStatusBlockRemoteWrite: state=%s offset=%u len=%u sbpStatus=%u", + ToString(state_), offset, len, block.sbpStatus); + + switch (state_) { + case LoginState::LoggingIn: + CancelManagementTimer(); + CompleteLoginFromStatusBlock(block, len); + break; + case LoginState::Reconnecting: + CancelManagementTimer(); + CompleteReconnectFromStatusBlock(block, len); + break; + case LoginState::LoggingOut: + CancelManagementTimer(); + CompleteLogoutFromStatusBlock(block, len); + break; + case LoginState::LoggedIn: + ProcessStatusBlock(block, len); + break; + default: + ASFW_LOG_SBP2( "LoginSession: unexpected status block in state %s", ToString(state_)); + break; + } +} + +void LoginSession::CompleteLoginFromStatusBlock(const Wire::StatusBlock& block, + uint32_t length) noexcept { + if (block.sbpStatus != Wire::SBPStatus::kNoAdditionalInfo) { + ASFW_LOG_SBP2( "LoginSession: login failed — sbpStatus=%u, retrying (%u/%u)", + block.sbpStatus, loginRetryCount_ + 1, kLoginRetryMax); + if (loginRetryCount_ < kLoginRetryMax) { + loginRetryCount_++; + ArmManagementTimer(kLoginRetryDelayMs, [this]() { + loginGeneration_ = static_cast(busInfo_.GetGeneration().value); + loginNodeID_ = targetInfo_.targetNodeId; + SetState(LoginState::Idle); + (void)Login(); + }); + return; + } + SetState(LoginState::Failed); + if (loginCallback_) { + LoginCompleteParams params{}; + params.status = -1; + params.statusBlock = block; + params.statusBlockLength = length; + params.generation = loginGeneration_; + loginCallback_(params); + } + return; + } + + // Login succeeded — read the login response the device wrote to our space. + std::vector responseData; + const auto kr = addrSpaceMgr_.ReadIncomingData( + this, loginResponseHandle_, 0, sizeof(Wire::LoginResponse), &responseData); + + if (kr != kIOReturnSuccess || responseData.size() < sizeof(Wire::LoginResponse)) { + ASFW_LOG_SBP2( "LoginSession: failed to read login response (kr=0x%08x, len=%zu)", + kr, responseData.size()); + if (loginRetryCount_ < kLoginRetryMax) { + loginRetryCount_++; + ArmManagementTimer(kLoginRetryDelayMs, [this]() { + loginGeneration_ = static_cast(busInfo_.GetGeneration().value); + loginNodeID_ = targetInfo_.targetNodeId; + SetState(LoginState::Idle); + (void)Login(); + }); + return; + } + SetState(LoginState::Failed); + if (loginCallback_) { + LoginCompleteParams params{}; + params.status = -1; + params.statusBlock = block; + params.statusBlockLength = length; + params.generation = loginGeneration_; + loginCallback_(params); + } + return; + } + + Wire::LoginResponse resp{}; + std::memcpy(&resp, responseData.data(), sizeof(resp)); + + loginID_ = OSSwapBigToHostInt16(resp.loginID); + reconnectHold_ = OSSwapBigToHostInt16(resp.reconnectHold); + loginResponse_ = resp; + + const uint32_t cbaHi = OSSwapBigToHostInt32(resp.commandBlockAgentAddressHi); + const uint32_t cbaLo = OSSwapBigToHostInt32(resp.commandBlockAgentAddressLo); + commandBlockAgent_ = Async::FWAddress{Async::FWAddress::QualifiedAddressParts{ + .addressHi = static_cast(cbaHi & 0xFFFFu), + .addressLo = cbaLo, + .nodeID = loginNodeID_}}; + + loginRetryCount_ = 0; + SetState(LoginState::LoggedIn); + BindFetchAgent(); + + // If unsolicited status was requested before login, enable it now (before the + // busy-timeout write, matching PR #19's ordering). + if (unsolicitedStatusRequested_) { + unsolicitedStatusRequested_ = false; + EnableUnsolicitedStatus(); + } + + ASFW_LOG_SBP2( "LoginSession: login successful — loginID=%u CBA=%04x:%08x reconnectHold=2^%u", + loginID_, commandBlockAgent_.addressHi, commandBlockAgent_.addressLo, reconnectHold_); + + WriteBusyTimeout(); + + if (loginCallback_) { + LoginCompleteParams params{}; + params.status = 0; + params.loginResponse = loginResponse_; + params.statusBlock = block; + params.statusBlockLength = length; + params.generation = loginGeneration_; + loginCallback_(params); + } +} + +void LoginSession::CompleteReconnectFromStatusBlock(const Wire::StatusBlock& block, + uint32_t length) noexcept { + if (block.sbpStatus != Wire::SBPStatus::kNoAdditionalInfo) { + ASFW_LOG_SBP2( "LoginSession: reconnect failed — sbpStatus=%u, falling back to full login", + block.sbpStatus); + SetState(LoginState::Idle); + (void)Login(); + return; + } + + SetState(LoginState::LoggedIn); + BindFetchAgent(); + ASFW_LOG_SBP2( "LoginSession: reconnect successful — loginID=%u", loginID_); + + WriteBusyTimeout(); + + if (unsolicitedStatusRequested_) { + unsolicitedStatusRequested_ = false; + EnableUnsolicitedStatus(); + } + + if (loginCallback_) { + LoginCompleteParams params{}; + params.status = 0; + params.loginResponse = loginResponse_; + params.statusBlock = block; + params.statusBlockLength = length; + params.generation = loginGeneration_; + loginCallback_(params); + } +} + +void LoginSession::CompleteLogoutFromStatusBlock(const Wire::StatusBlock& /*block*/, + uint32_t /*length*/) noexcept { + const uint16_t oldLoginID = loginID_; + loginID_ = 0; + SetState(LoginState::Idle); + ASFW_LOG_SBP2( "LoginSession: logout complete (was loginID=%u)", oldLoginID); + + if (logoutCallback_) { + LogoutCompleteParams params{}; + params.status = 0; + params.generation = loginGeneration_; + logoutCallback_(params); + } +} + +void LoginSession::ProcessStatusBlock(const Wire::StatusBlock& block, uint32_t length) noexcept { + // Unsolicited: (details & 0xC0) == 0x80 (source bit set, resp == 0). + const bool isUnsolicited = (block.details & 0xC0) == 0x80; + + if (statusCallback_) { + statusCallback_(block, length); + } + + if (isUnsolicited) { + EnableUnsolicitedStatus(); // re-arm so the device can send more + return; + } + + (void)fetchAgent_.OnStatusBlock(block, length); +} + +// --------------------------------------------------------------------------- +// Command plane (delegates to FetchAgent) +// --------------------------------------------------------------------------- + +bool LoginSession::SubmitORB(SBP2CommandORB* orb) noexcept { + if (state_ != LoginState::LoggedIn) { + ASFW_LOG_SBP2( "LoginSession::SubmitORB: state=%s, rejecting", ToString(state_)); + return false; + } + return fetchAgent_.Submit(orb); +} + +void LoginSession::ResetFetchAgent(std::function callback) noexcept { + fetchAgent_.Reset(std::move(callback)); +} + +// --------------------------------------------------------------------------- +// Unsolicited Status Enable +// --------------------------------------------------------------------------- + +void LoginSession::EnableUnsolicitedStatus() noexcept { + if (state_ != LoginState::LoggedIn) { + unsolicitedStatusRequested_ = true; + return; + } + + const FW::Generation gen{loginGeneration_}; + const FW::NodeId node{static_cast(TargetNodeShort())}; + const FW::FwSpeed speed = busInfo_.GetSpeed(node); + + const std::weak_ptr weakSelf = weak_from_this(); + unsolicitedStatusWriteHandle_ = bus_.WriteQuad( + gen, node, unsolicitedStatusAddress_, 0, speed, + [weakSelf, requestGeneration = loginGeneration_](Async::AsyncStatus status, + std::span) { + if (auto self = weakSelf.lock()) { + self->OnUnsolicitedStatusEnableComplete(requestGeneration, status); + } + }); +} + +void LoginSession::OnUnsolicitedStatusEnableComplete(uint16_t expectedGeneration, + Async::AsyncStatus status) noexcept { + if (expectedGeneration != loginGeneration_ || state_ != LoginState::LoggedIn) { + return; + } + if (status != Async::AsyncStatus::kSuccess) { + ASFW_LOG_SBP2( "LoginSession::OnUnsolicitedStatusEnableComplete: status=%s", + Async::ToString(status)); + } +} + +// --------------------------------------------------------------------------- +// Busy Timeout +// --------------------------------------------------------------------------- + +void LoginSession::WriteBusyTimeout() noexcept { + if (busyTimeoutInProgress_) { + bus_.Cancel(busyTimeoutWriteHandle_); + busyTimeoutInProgress_ = false; + } + + const FW::Generation gen{loginGeneration_}; + const FW::NodeId node{static_cast(TargetNodeShort())}; + const Async::FWAddress busyAddr{Async::FWAddress::QualifiedAddressParts{ + .addressHi = kCSRBusAddressHi, + .addressLo = kBusyTimeoutAddressLo, + .nodeID = loginNodeID_}}; + const FW::FwSpeed speed = busInfo_.GetSpeed(node); + + busyTimeoutInProgress_ = true; + const std::weak_ptr weakSelf = weak_from_this(); + busyTimeoutWriteHandle_ = bus_.WriteBlock( + gen, node, busyAddr, + std::span{reinterpret_cast(&busyTimeoutBuffer_), 4}, + speed, + [weakSelf, requestGeneration = loginGeneration_](Async::AsyncStatus status, + std::span) { + if (auto self = weakSelf.lock()) { + self->OnBusyTimeoutComplete(requestGeneration, status); + } + }); + + if (!busyTimeoutWriteHandle_) { + ASFW_LOG_SBP2( "LoginSession::WriteBusyTimeout: WriteBlock failed"); + busyTimeoutInProgress_ = false; + } +} + +void LoginSession::OnBusyTimeoutComplete(uint16_t expectedGeneration, + Async::AsyncStatus status) noexcept { + if (expectedGeneration != loginGeneration_) { + return; + } + busyTimeoutInProgress_ = false; + if (status != Async::AsyncStatus::kSuccess && status != Async::AsyncStatus::kAborted) { + ASFW_LOG_SBP2( "LoginSession::OnBusyTimeoutComplete: status=%s", Async::ToString(status)); + } +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +void LoginSession::SetState(LoginState newState) noexcept { + if (state_ != newState) { + ASFW_LOG_SBP2( "LoginSession: state %s -> %s", ToString(state_), ToString(newState)); + state_ = newState; + } +} + +void LoginSession::StartLoginTimer() noexcept { + ArmManagementTimer(targetInfo_.managementTimeoutMs, [this]() { OnLoginTimeout(); }); +} + +void LoginSession::StartReconnectTimer() noexcept { + ArmManagementTimer(targetInfo_.managementTimeoutMs + 1000, [this]() { OnReconnectTimeout(); }); +} + +void LoginSession::StartLogoutTimer() noexcept { + ArmManagementTimer(targetInfo_.managementTimeoutMs, [this]() { OnLogoutTimeout(); }); +} + +void LoginSession::ArmManagementTimer(uint64_t delayMs, std::function fn) noexcept { + CancelManagementTimer(); + const std::weak_ptr weakSelf = weak_from_this(); + managementTimerToken_ = scheduler_.ScheduleAfter( + delayMs * 1'000'000ULL, [weakSelf, fn = std::move(fn)]() mutable { + if (auto self = weakSelf.lock()) { + self->managementTimerToken_ = kInvalidSchedulerToken; + fn(); + } + }); +} + +void LoginSession::CancelManagementTimer() noexcept { + if (managementTimerToken_ != kInvalidSchedulerToken) { + scheduler_.Cancel(managementTimerToken_); + managementTimerToken_ = kInvalidSchedulerToken; + } +} + +} // namespace ASFW::Protocols::SBP2 diff --git a/ASFWDriver/Protocols/SBP2/Session/LoginSession.hpp b/ASFWDriver/Protocols/SBP2/Session/LoginSession.hpp new file mode 100644 index 00000000..598732ca --- /dev/null +++ b/ASFWDriver/Protocols/SBP2/Session/LoginSession.hpp @@ -0,0 +1,326 @@ +#pragma once + +// LoginSession — SBP-2 login/reconnect/logout state machine (management plane). +// +// Decomposed from PR #19's SBP2LoginSession (1847 lines). The post-login command +// plane (fetch-agent ORB submission, doorbell, status→ORB matching, agent reset) +// lives in the composed FetchAgent (§1b of SBP2_SESSION_PORT.md); this class owns +// only the management plane: +// +// 1. Configure() with ROM-derived target parameters. +// 2. Login() — write the Login ORB address to the device's management agent. +// 3. On the status block write, parse the login response, derive the +// command-block-agent addresses, Bind() the FetchAgent, and write the +// busy-timeout CSR. +// 4. On bus reset, Suspend then Reconnect() with the stored loginID. +// 5. Logout() terminates the session. +// +// DICE adaptations vs #19: +// * Timers go through the injected ISessionScheduler (one cancelable management +// timer at a time), never the IOSleep-on-queue path. The two-queue model +// (work + owned timeout queue) is removed (§4). +// * Device-visible memory is allocated through AddressSpaceManager (handles, not +// raw buffers). The status-FIFO remote-write callback captures weak_from_this. +// * Command submission delegates to the FetchAgent; status routing forwards +// solicited blocks to FetchAgent::OnStatusBlock. + +#include "ISessionScheduler.hpp" +#include "FetchAgent.hpp" +#include "../SBP2WireFormats.hpp" +#include "../SBP2CommandORB.hpp" +#include "../AddressSpaceManager.hpp" +#include "../../../Async/AsyncTypes.hpp" +#include "../../../Logging/Logging.hpp" + +#include +#include +#include +#include +#include + +namespace ASFW::Async { +class IFireWireBus; +class IFireWireBusInfo; +} + +namespace ASFW::Protocols::SBP2 { + +// --------------------------------------------------------------------------- +// Configuration parameters (from Config ROM Unit_Directory parsing) +// --------------------------------------------------------------------------- + +struct SBP2TargetInfo { + uint32_t managementAgentOffset{0}; // From Management_Agent_Offset key + uint16_t lun{0}; // Logical unit number + + // From Unit_Characteristics key (if present) + uint32_t managementTimeoutMs{2000}; // Unit_Characteristics[15:8] * 500 ms + uint16_t maxORBSize{32}; // Unit_Characteristics[7:0] * 4, min 32 + uint16_t maxCommandBlockSize{0}; // maxORBSize - sizeof(NormalORB header) + + // From Fast_Start key (optional) + bool fastStartSupported{false}; + uint8_t fastStartOffset{0}; + uint8_t fastStartMaxPayload{0}; + + // Target node (from discovery) + uint16_t targetNodeId{0xFFFF}; +}; + +// --------------------------------------------------------------------------- +// Completion callback parameters +// --------------------------------------------------------------------------- + +struct LoginCompleteParams { + int status{0}; // 0 = success, negative = errno-style error + Wire::LoginResponse loginResponse{}; + Wire::StatusBlock statusBlock{}; + uint32_t statusBlockLength{0}; + uint16_t generation{0}; +}; + +struct LogoutCompleteParams { + int status{0}; + uint16_t generation{0}; +}; + +// --------------------------------------------------------------------------- +// Login session states +// --------------------------------------------------------------------------- + +enum class LoginState : uint8_t { + Idle, + LoggingIn, + LoggedIn, + Reconnecting, + LoggingOut, + Suspended, // Lost after bus reset, waiting for reconnect + Failed +}; + +[[nodiscard]] inline constexpr const char* ToString(LoginState s) noexcept { + switch (s) { + case LoginState::Idle: return "Idle"; + case LoginState::LoggingIn: return "LoggingIn"; + case LoginState::LoggedIn: return "LoggedIn"; + case LoginState::Reconnecting: return "Reconnecting"; + case LoginState::LoggingOut: return "LoggingOut"; + case LoginState::Suspended: return "Suspended"; + case LoginState::Failed: return "Failed"; + } + return "Unknown"; +} + +// --------------------------------------------------------------------------- +// LoginSession +// --------------------------------------------------------------------------- + +class LoginSession : public std::enable_shared_from_this { + friend class SessionRegistry; + +public: + using LoginCallback = std::function; + using LogoutCallback = std::function; + using StatusCallback = std::function; + + LoginSession(Async::IFireWireBus& bus, + Async::IFireWireBusInfo& busInfo, + AddressSpaceManager& addrSpaceMgr, + ISessionScheduler& scheduler); + ~LoginSession(); + + LoginSession(const LoginSession&) = delete; + LoginSession& operator=(const LoginSession&) = delete; + + // ----------------------------------------------------------------------- + // Configuration (call once before Login) + // ----------------------------------------------------------------------- + + void Configure(const SBP2TargetInfo& info) noexcept; + void SetLoginCallback(LoginCallback cb) noexcept { loginCallback_ = std::move(cb); } + void SetLogoutCallback(LogoutCallback cb) noexcept { logoutCallback_ = std::move(cb); } + void SetStatusCallback(StatusCallback cb) noexcept { statusCallback_ = std::move(cb); } + + // ----------------------------------------------------------------------- + // Session operations + // ----------------------------------------------------------------------- + + [[nodiscard]] bool Login() noexcept; + [[nodiscard]] bool Logout() noexcept; + [[nodiscard]] bool Reconnect() noexcept; + void HandleBusReset(uint16_t newGeneration) noexcept; + + // ----------------------------------------------------------------------- + // Accessors + // ----------------------------------------------------------------------- + + [[nodiscard]] LoginState State() const noexcept { return state_; } + [[nodiscard]] uint16_t LoginID() const noexcept { return loginID_; } + [[nodiscard]] uint16_t Generation() const noexcept { return loginGeneration_; } + [[nodiscard]] const SBP2TargetInfo& TargetInfo() const noexcept { return targetInfo_; } + [[nodiscard]] Async::FWAddress CommandBlockAgent() const noexcept { return commandBlockAgent_; } + [[nodiscard]] uint32_t ReconnectHoldSeconds() const noexcept; + [[nodiscard]] uint16_t MaxPayloadSize() const noexcept { return maxPayloadSize_; } + void SetMaxPayloadSize(uint16_t bytes) noexcept { maxPayloadSize_ = bytes; } + + // ----------------------------------------------------------------------- + // Command plane (delegates to FetchAgent) + // ----------------------------------------------------------------------- + + /// Submit a Normal Command ORB. Requires LoggedIn. Returns false if not + /// logged in or the FetchAgent rejects the ORB. + [[nodiscard]] bool SubmitORB(SBP2CommandORB* orb) noexcept; + + /// Reset the fetch agent (clears the ORB chain). Completion via callback. + void ResetFetchAgent(std::function callback) noexcept; + + /// Drop all outstanding command-ORB tracking and cancel any in-flight + /// fetch-agent / doorbell write. Used by the command plane (CommandExecutor) + /// when a command completes, fails, or is aborted — mirrors #19's + /// ClearORBTracking, which CleanupCommandResources invoked on the session. + void ClearCommandTracking() noexcept { fetchAgent_.Clear(true); } + + /// Re-enable unsolicited status after the device sends one. If called before + /// login completes, it is deferred until LoggedIn. + void EnableUnsolicitedStatus() noexcept; + + // Test seam: override the FetchAgent's fetch-agent-write retry budget. + void SetFetchAgentWriteRetriesForTesting(uint32_t retries) noexcept { + fetchAgent_.SetWriteRetriesForTesting(retries); + } + +private: + // Resource allocation ----------------------------------------------------- + bool AllocateResources() noexcept; + void DeallocateResources() noexcept; + bool AllocateLoginORBAddressSpace() noexcept; + bool AllocateLoginResponseAddressSpace() noexcept; + bool AllocateStatusBlockAddressSpace() noexcept; + bool AllocateReconnectORBAddressSpace() noexcept; + bool AllocateLogoutORBAddressSpace() noexcept; + void RegisterStatusBlockCallback() noexcept; + + // ORB construction -------------------------------------------------------- + void BuildLoginORB() noexcept; + void BuildReconnectORB() noexcept; + void BuildLogoutORB() noexcept; + + // Bind / unbind the command plane ---------------------------------------- + void BindFetchAgent() noexcept; + void RefreshUnsolicitedStatusAddress() noexcept; + + // Management write completions ------------------------------------------- + void OnLoginWriteComplete(uint16_t expectedGeneration, Async::AsyncStatus status) noexcept; + void OnLoginTimeout() noexcept; + void OnReconnectWriteComplete(uint16_t expectedGeneration, Async::AsyncStatus status) noexcept; + void OnReconnectTimeout() noexcept; + void OnLogoutWriteComplete(uint16_t expectedGeneration, Async::AsyncStatus status) noexcept; + void OnLogoutTimeout() noexcept; + + // Status block handling --------------------------------------------------- + void OnStatusBlockRemoteWrite(uint32_t offset, std::span payload) noexcept; + void ProcessStatusBlock(const Wire::StatusBlock& block, uint32_t length) noexcept; + void CompleteLoginFromStatusBlock(const Wire::StatusBlock& block, uint32_t length) noexcept; + void CompleteReconnectFromStatusBlock(const Wire::StatusBlock& block, uint32_t length) noexcept; + void CompleteLogoutFromStatusBlock(const Wire::StatusBlock& block, uint32_t length) noexcept; + + // Unsolicited status + busy timeout writes ------------------------------- + void OnUnsolicitedStatusEnableComplete(uint16_t expectedGeneration, + Async::AsyncStatus status) noexcept; + void WriteBusyTimeout() noexcept; + void OnBusyTimeoutComplete(uint16_t expectedGeneration, Async::AsyncStatus status) noexcept; + + // Helpers ----------------------------------------------------------------- + void SetState(LoginState newState) noexcept; + void StartLoginTimer() noexcept; + void StartReconnectTimer() noexcept; + void StartLogoutTimer() noexcept; + void CancelLoginTimer() noexcept { CancelManagementTimer(); } + void ArmManagementTimer(uint64_t delayMs, std::function fn) noexcept; + void CancelManagementTimer() noexcept; + void ScheduleManagementRetry(uint64_t delayMs, void (LoginSession::*op)()) noexcept; + + [[nodiscard]] uint16_t TargetNodeShort() const noexcept { + return static_cast(loginNodeID_ & 0x3Fu); + } + + // Members ----------------------------------------------------------------- + Async::IFireWireBus& bus_; + Async::IFireWireBusInfo& busInfo_; + AddressSpaceManager& addrSpaceMgr_; + ISessionScheduler& scheduler_; + FetchAgent fetchAgent_; + + // Configuration + SBP2TargetInfo targetInfo_{}; + bool configured_{false}; + uint16_t maxPayloadSize_{4096}; // default, clipped by login response + + // Session state + LoginState state_{LoginState::Idle}; + uint16_t loginID_{0}; + uint16_t loginGeneration_{0}; + uint16_t loginNodeID_{0xFFFF}; + + // Login response data + Wire::LoginResponse loginResponse_{}; + Async::FWAddress commandBlockAgent_{}; + uint16_t reconnectHold_{0}; + + // Login retry state + uint32_t loginRetryCount_{0}; + static constexpr uint32_t kLoginRetryMax = 32; + static constexpr uint64_t kLoginRetryDelayMs = 1000; + + // Address space handles --------------------------------------------------- + uint64_t loginORBHandle_{0}; + AddressSpaceManager::AddressRangeMeta loginORBMeta_{}; + Wire::LoginORB loginORBBuffer_{}; + + uint64_t loginResponseHandle_{0}; + AddressSpaceManager::AddressRangeMeta loginResponseMeta_{}; + + uint64_t statusBlockHandle_{0}; + AddressSpaceManager::AddressRangeMeta statusBlockMeta_{}; + + uint64_t reconnectORBHandle_{0}; + AddressSpaceManager::AddressRangeMeta reconnectORBMeta_{}; + Wire::ReconnectORB reconnectORBBuffer_{}; + + uint64_t logoutORBHandle_{0}; + AddressSpaceManager::AddressRangeMeta logoutORBMeta_{}; + Wire::LogoutORB logoutORBBuffer_{}; + + // 8-byte big-endian ORB addresses for the management agent write + std::array loginORBAddressBE_{}; + std::array reconnectORBAddressBE_{}; + std::array logoutORBAddressBE_{}; + + // In-flight management writes + Async::AsyncHandle loginWriteHandle_{}; + Async::AsyncHandle reconnectWriteHandle_{}; + Async::AsyncHandle logoutWriteHandle_{}; + + // Unsolicited status enable + Async::FWAddress unsolicitedStatusAddress_{}; + Async::AsyncHandle unsolicitedStatusWriteHandle_{}; + bool unsolicitedStatusRequested_{false}; + + // Busy timeout CSR write + static constexpr uint32_t kCSRBusAddressHi = 0x0000FFFFu; + static constexpr uint32_t kBusyTimeoutAddressLo = 0xF0000210u; + static constexpr uint32_t kBusyTimeoutValue = 0x0000000Fu; + Async::AsyncHandle busyTimeoutWriteHandle_{}; + bool busyTimeoutInProgress_{false}; + uint32_t busyTimeoutBuffer_{OSSwapHostToBigInt32(kBusyTimeoutValue)}; + + // Callbacks + LoginCallback loginCallback_; + LogoutCallback logoutCallback_; + StatusCallback statusCallback_; + + // Single cancelable management-plane timer (login/reconnect/logout/retry). + SchedulerToken managementTimerToken_{kInvalidSchedulerToken}; +}; + +} // namespace ASFW::Protocols::SBP2 diff --git a/ASFWDriver/Protocols/SBP2/Session/SessionRegistry.cpp b/ASFWDriver/Protocols/SBP2/Session/SessionRegistry.cpp new file mode 100644 index 00000000..84ea5235 --- /dev/null +++ b/ASFWDriver/Protocols/SBP2/Session/SessionRegistry.cpp @@ -0,0 +1,475 @@ +// SessionRegistry — identity & lifecycle for SBP-2 sessions. See SessionRegistry.hpp. +// +// Ported from PR #19's SBP2SessionRegistry (§2a). The command plane is delegated +// to each record's CommandExecutor (§2b). LoginSession timers run on an injected +// ISessionScheduler; ReleaseSession is async (no IOSleep wait-loop). + +#include "SessionRegistry.hpp" + +#include "../../../Async/Interfaces/IFireWireBusInfo.hpp" +#include "../../../Discovery/FWDevice.hpp" +#include "../../../Discovery/FWUnit.hpp" + +#include +#include + +namespace ASFW::Protocols::SBP2 { + +namespace { + +class IOLockGuard { +public: + explicit IOLockGuard(IOLock* lock) : lock_(lock) { + if (lock_ != nullptr) { + IOLockLock(lock_); + } + } + ~IOLockGuard() { + if (lock_ != nullptr) { + IOLockUnlock(lock_); + } + } + IOLockGuard(const IOLockGuard&) = delete; + IOLockGuard& operator=(const IOLockGuard&) = delete; + +private: + IOLock* lock_{nullptr}; +}; + +SBP2TargetInfo BuildTargetInfoFromUnit(const Discovery::FWUnit& unit) { + SBP2TargetInfo info{}; + + info.managementAgentOffset = unit.GetManagementAgentOffset().value_or(0); + info.lun = static_cast(unit.GetLUN().value_or(0) & 0xFFFF); + + if (auto uc = unit.GetUnitCharacteristics(); uc.has_value()) { + const uint32_t value = *uc; + const uint8_t orbSizeUnits = static_cast(value & 0xFF); + const uint8_t timeoutUnits = static_cast((value >> 8) & 0xFF); + info.managementTimeoutMs = static_cast(timeoutUnits) * 500; + info.maxORBSize = std::max(static_cast(orbSizeUnits) * 4, 32); + } + info.maxCommandBlockSize = info.maxORBSize > Wire::NormalORB::kHeaderSize + ? static_cast(info.maxORBSize - Wire::NormalORB::kHeaderSize) + : 0; + + if (auto fastStart = unit.GetFastStart(); fastStart.has_value()) { + const uint32_t value = *fastStart; + info.fastStartSupported = true; + info.fastStartOffset = static_cast((value >> 8) & 0xFF); + info.fastStartMaxPayload = static_cast(value & 0xFF); + } + + if (auto device = unit.GetDevice(); device) { + info.targetNodeId = device->GetNodeID(); + } + + return info; +} + +} // namespace + +SessionRegistry::SessionRegistry(Async::IFireWireBus& bus, + Async::IFireWireBusInfo& busInfo, + AddressSpaceManager& addrSpaceMgr, + Discovery::IDeviceManager& deviceManager, + ISessionScheduler& scheduler, + IODispatchQueue* workQueue) + : bus_(bus) + , busInfo_(busInfo) + , addrSpaceMgr_(addrSpaceMgr) + , deviceManager_(deviceManager) + , scheduler_(scheduler) + , workQueue_(workQueue) { + lock_ = IOLockAlloc(); +} + +SessionRegistry::~SessionRegistry() { + { + IOLockGuard lock(lock_); + for (auto& [handle, record] : sessions_) { + if (record.session && record.session->State() == LoginState::LoggedIn) { + (void)record.session->Logout(); + } + if (record.executor) { + record.executor->Cleanup(); + } + } + sessions_.clear(); + } + retiringSessions_.clear(); + + if (lock_ != nullptr) { + IOLockFree(lock_); + lock_ = nullptr; + } +} + +std::expected SessionRegistry::CreateSession(void* owner, + uint64_t guid, + uint32_t romOffset) { + auto unit = ResolveUnit(guid, romOffset); + if (!unit) { + ASFW_LOG(Async, "SessionRegistry: no unit found for guid=0x%016llx romOffset=%u", + guid, romOffset); + return std::unexpected(kIOReturnNotFound); + } + + if (!unit->Matches(kSBP2UnitSpecId, kSBP2UnitSwVersion)) { + ASFW_LOG(Async, "SessionRegistry: unit identity spec=0x%06x sw=0x%06x is not SBP-2", + unit->GetUnitSpecID(), unit->GetUnitSwVersion()); + return std::unexpected(kIOReturnUnsupported); + } + + const auto mgmtOffset = unit->GetManagementAgentOffset(); + if (!mgmtOffset.has_value() || *mgmtOffset == 0) { + ASFW_LOG(Async, "SessionRegistry: unit has no Management_Agent_Offset"); + return std::unexpected(kIOReturnUnsupported); + } + + auto targetInfo = BuildTargetInfoFromUnit(*unit); + if (targetInfo.managementAgentOffset == 0) { + return std::unexpected(kIOReturnUnsupported); + } + + IOLockGuard lock(lock_); + if (HasSessionForTargetLocked(guid, romOffset)) { + ASFW_LOG(Async, "SessionRegistry: duplicate session for guid=0x%016llx romOffset=%u", + guid, romOffset); + return std::unexpected(kIOReturnExclusiveAccess); + } + + const uint64_t handle = nextHandle_++; + + auto [it, inserted] = sessions_.try_emplace(handle); + if (!inserted) { + return std::unexpected(kIOReturnNoMemory); + } + + SessionRecord& record = it->second; + record.handle = handle; + record.owner = owner; + record.guid = guid; + record.romOffset = romOffset; + record.session = std::make_shared(bus_, busInfo_, addrSpaceMgr_, scheduler_); + record.session->Configure(targetInfo); + // record.lastError lives at a stable address (map node), so the executor may + // bind a reference to it. + record.executor = std::make_unique( + bus_, busInfo_, addrSpaceMgr_, *record.session, owner, record.lastError, workQueue_); + + ASFW_LOG(Async, "SessionRegistry: created session handle=%llu guid=0x%016llx romOffset=%u", + handle, guid, romOffset); + return handle; +} + +bool SessionRegistry::StartLogin(void* owner, uint64_t handle) { + IOLockGuard lock(lock_); + auto* record = FindByHandleForOwner(owner, handle); + if (!record || !record->session) { + return false; + } + if (record->session->State() != LoginState::Idle) { + return false; + } + + record->session->SetLoginCallback([this, handle](const LoginCompleteParams& params) { + IOLockGuard cbLock(lock_); + auto* rec = FindByHandle(handle); + if (rec == nullptr) { + return; + } + rec->lastError = params.status; + }); + + return record->session->Login(); +} + +std::optional SessionRegistry::GetSessionState(void* owner, + uint64_t handle) const { + IOLockGuard lock(lock_); + const auto* record = FindByHandleForOwner(owner, handle); + if (!record || !record->session) { + return std::nullopt; + } + + SBP2SessionState state{}; + state.loginState = record->session->State(); + state.loginID = record->session->LoginID(); + state.generation = record->session->Generation(); + state.lastError = record->lastError; + state.reconnectPending = (state.loginState == LoginState::Suspended); + return state; +} + +bool SessionRegistry::SubmitInquiry(void* owner, uint64_t handle, uint8_t allocationLength) { + IOLockGuard lock(lock_); + auto* record = FindByHandleForOwner(owner, handle); + if (!record || !record->executor) { + return false; + } + return record->executor->SubmitInquiry(allocationLength); +} + +std::optional SessionRegistry::GetInquiryResult(void* owner, uint64_t handle) { + IOLockGuard lock(lock_); + auto* record = FindByHandleForOwner(owner, handle); + if (!record || !record->executor) { + return std::nullopt; + } + return record->executor->GetInquiryResult(); +} + +bool SessionRegistry::SubmitCommand(void* owner, uint64_t handle, + const SCSI::CommandRequest& request) { + IOLockGuard lock(lock_); + auto* record = FindByHandleForOwner(owner, handle); + if (!record || !record->executor) { + return false; + } + return record->executor->SubmitCommand(request); +} + +std::optional SessionRegistry::GetCommandResult(void* owner, uint64_t handle) { + IOLockGuard lock(lock_); + auto* record = FindByHandleForOwner(owner, handle); + if (!record || !record->executor) { + return std::nullopt; + } + return record->executor->GetCommandResult(); +} + +bool SessionRegistry::SubmitTaskManagement(void* owner, uint64_t handle, + SBP2ManagementORB::Function function) { + IOLockGuard lock(lock_); + auto* record = FindByHandleForOwner(owner, handle); + if (!record || !record->executor) { + return false; + } + return record->executor->SubmitTaskManagement(function); +} + +bool SessionRegistry::ReleaseSession(void* owner, uint64_t handle) { + IOLockGuard lock(lock_); + auto it = sessions_.find(handle); + if (it == sessions_.end() || it->second.owner != owner) { + return false; + } + + auto& record = it->second; + if (record.executor) { + record.executor->Cleanup(); + } + + const LoginState state = record.session ? record.session->State() : LoginState::Idle; + if (record.session && + (state == LoginState::LoggedIn || state == LoginState::Suspended)) { + SetReleaseLogoutCallbackLocked(handle, record.session); + if (record.session->Logout()) { + RetireSessionLocked(record); + } + sessions_.erase(it); + return true; + } + + if (record.session && state == LoginState::LoggingOut) { + SetReleaseLogoutCallbackLocked(handle, record.session); + RetireSessionLocked(record); + sessions_.erase(it); + return true; + } + + sessions_.erase(it); + return true; +} + +void SessionRegistry::ReleaseOwner(void* owner) { + IOLockGuard lock(lock_); + for (auto it = sessions_.begin(); it != sessions_.end();) { + if (it->second.owner != owner) { + ++it; + continue; + } + + auto& record = it->second; + const LoginState state = record.session ? record.session->State() : LoginState::Idle; + + if (record.session && + (state == LoginState::LoggedIn || state == LoginState::Suspended)) { + SetReleaseLogoutCallbackLocked(record.handle, record.session); + if (record.session->Logout()) { + if (record.executor) { + record.executor->Cleanup(); + } + RetireSessionLocked(record); + it = sessions_.erase(it); + continue; + } + } else if (record.session && state == LoginState::LoggingOut) { + SetReleaseLogoutCallbackLocked(record.handle, record.session); + if (record.executor) { + record.executor->Cleanup(); + } + RetireSessionLocked(record); + it = sessions_.erase(it); + continue; + } + + if (record.executor) { + record.executor->Cleanup(); + } + it = sessions_.erase(it); + } +} + +void SessionRegistry::OnBusReset(uint16_t newGeneration) { + IOLockGuard lock(lock_); + for (auto& [handle, record] : sessions_) { + if (record.session) { + record.session->HandleBusReset(newGeneration); + } + if (record.executor) { + record.executor->OnBusReset(); + } + } +} + +void SessionRegistry::RefreshTargets(Discovery::Generation gen) { + IOLockGuard lock(lock_); + for (auto& [handle, record] : sessions_) { + if (!record.session || record.session->State() != LoginState::Suspended) { + continue; + } + + auto unit = ResolveUnit(record.guid, record.romOffset); + if (!unit) { + ASFW_LOG(Async, "SessionRegistry: RefreshTargets: unit not found for handle=%llu", + handle); + continue; + } + + record.session->Configure(BuildTargetInfoFromUnit(*unit)); + ASFW_LOG(Async, "SessionRegistry: reconnecting session handle=%llu gen=%u", + handle, gen.value); + (void)record.session->Reconnect(); + } +} + +#ifdef ASFW_HOST_TEST +LoginSession* SessionRegistry::GetSessionForTesting(uint64_t handle) { + IOLockGuard lock(lock_); + auto* record = FindByHandle(handle); + return record ? record->session.get() : nullptr; +} + +std::weak_ptr SessionRegistry::GetSessionWeakForTesting(uint64_t handle) { + IOLockGuard lock(lock_); + auto* record = FindByHandle(handle); + if (!record) { + return {}; + } + return record->session; +} +#endif + +SessionRecord* SessionRegistry::FindByHandle(uint64_t handle) { + auto it = sessions_.find(handle); + return it != sessions_.end() ? &it->second : nullptr; +} + +const SessionRecord* SessionRegistry::FindByHandle(uint64_t handle) const { + auto it = sessions_.find(handle); + return it != sessions_.end() ? &it->second : nullptr; +} + +SessionRecord* SessionRegistry::FindByHandleForOwner(void* owner, uint64_t handle) { + auto* record = FindByHandle(handle); + return (record != nullptr && record->owner == owner) ? record : nullptr; +} + +const SessionRecord* SessionRegistry::FindByHandleForOwner(void* owner, uint64_t handle) const { + const auto* record = FindByHandle(handle); + return (record != nullptr && record->owner == owner) ? record : nullptr; +} + +std::shared_ptr SessionRegistry::ResolveUnit(uint64_t guid, + uint32_t romOffset) const { + const auto devices = deviceManager_.GetAllDevices(); + for (const auto& device : devices) { + if (!device || !device->IsReady() || device->GetGUID() != guid) { + continue; + } + for (const auto& unit : device->GetUnits()) { + if (unit && unit->GetDirectoryOffset() == romOffset) { + return unit; + } + } + } + return nullptr; +} + +bool SessionRegistry::HasSessionForTargetLocked(uint64_t guid, uint32_t romOffset) const { + for (const auto& [handle, record] : sessions_) { + if (record.guid == guid && record.romOffset == romOffset && record.session != nullptr) { + return true; + } + } + return std::any_of(retiringSessions_.begin(), retiringSessions_.end(), + [guid, romOffset](const RetiringSession& retired) { + return retired.guid == guid && retired.romOffset == romOffset && + retired.session != nullptr; + }); +} + +void SessionRegistry::RetireSessionLocked(const SessionRecord& record) { + if (record.session == nullptr) { + return; + } + const auto it = std::find_if( + retiringSessions_.begin(), retiringSessions_.end(), + [&record](const RetiringSession& retired) { return retired.session == record.session; }); + if (it == retiringSessions_.end()) { + retiringSessions_.push_back(RetiringSession{ + .guid = record.guid, + .romOffset = record.romOffset, + .session = record.session, + }); + } +} + +void SessionRegistry::EraseRetiredSessionLocked(const std::shared_ptr& session) { + if (session == nullptr) { + return; + } + retiringSessions_.erase( + std::remove_if(retiringSessions_.begin(), retiringSessions_.end(), + [&session](const RetiringSession& retired) { + return retired.session == session; + }), + retiringSessions_.end()); +} + +void SessionRegistry::SetReleaseLogoutCallbackLocked( + uint64_t handle, const std::shared_ptr& session) { + if (session == nullptr) { + return; + } + std::weak_ptr weakSession = session; + session->SetLogoutCallback([this, handle, weakSession](const LogoutCompleteParams&) { + IOLockGuard cbLock(lock_); + std::shared_ptr completedSession = weakSession.lock(); + + auto it = sessions_.find(handle); + if (it != sessions_.end() && + (completedSession == nullptr || it->second.session == completedSession)) { + if (it->second.executor) { + it->second.executor->Cleanup(); + } + sessions_.erase(it); + } + + EraseRetiredSessionLocked(completedSession); + }); +} + +} // namespace ASFW::Protocols::SBP2 diff --git a/ASFWDriver/Protocols/SBP2/Session/SessionRegistry.hpp b/ASFWDriver/Protocols/SBP2/Session/SessionRegistry.hpp new file mode 100644 index 00000000..a3cc6b7a --- /dev/null +++ b/ASFWDriver/Protocols/SBP2/Session/SessionRegistry.hpp @@ -0,0 +1,140 @@ +#pragma once + +// SessionRegistry — bridges discovery metadata to LoginSession instances. +// +// Decomposed from PR #19's SBP2SessionRegistry (§2a of SBP2_SESSION_PORT.md): +// identity & lifecycle only. Sessions are created for an owner and target +// (guid, romOffset); public operations use opaque handles plus owner validation, +// and the registry rejects duplicate live targets by (guid, romOffset). The +// command plane lives in a per-record CommandExecutor (§2b); command/inquiry/ +// task-management calls validate here and delegate to it. +// +// DICE adaptations vs #19: +// * LoginSession timers run on an injected ISessionScheduler (constructor +// argument), not the two-queue model. +// * ReleaseSession no longer blocks the queue with an IOSleep wait-loop; like +// ReleaseOwner it starts logout, retires the session, and lets the async +// logout completion (or its scheduler timeout) erase it. + +#include "LoginSession.hpp" +#include "CommandExecutor.hpp" +#include "ISessionScheduler.hpp" +#include "../SBP2ManagementORB.hpp" +#include "../SCSICommandSet.hpp" +#include "../AddressSpaceManager.hpp" +#include "../../../Discovery/IDeviceManager.hpp" +#include "../../../Discovery/DiscoveryTypes.hpp" +#include "../../../Logging/Logging.hpp" + +#include +#include +#include +#include +#include +#include + +namespace ASFW::Protocols::SBP2 { + +// SBP-2 Unit_Directory identity used by Nikon/modernscan and visible in raw ROM. +inline constexpr uint32_t kSBP2UnitSpecId = 0x00609E; +inline constexpr uint32_t kSBP2UnitSwVersion = 0x010483; + +// Per-session state exposed to UserClient. +struct SBP2SessionState { + LoginState loginState{LoginState::Idle}; + uint16_t loginID{0}; + uint16_t generation{0}; + int32_t lastError{0}; + bool reconnectPending{false}; +}; + +// Slim per-session record (§2c). The command god-object that bloated #19's record +// now lives in `executor`. +struct SessionRecord { + uint64_t handle{0}; + void* owner{nullptr}; + uint64_t guid{0}; + uint32_t romOffset{0}; + std::shared_ptr session; + int32_t lastError{0}; + // Declared last so it is destroyed first (it references `session`/`lastError`). + std::unique_ptr executor; +}; + +class SessionRegistry { +public: + SessionRegistry(Async::IFireWireBus& bus, + Async::IFireWireBusInfo& busInfo, + AddressSpaceManager& addrSpaceMgr, + Discovery::IDeviceManager& deviceManager, + ISessionScheduler& scheduler, + IODispatchQueue* workQueue = nullptr); + ~SessionRegistry(); + + SessionRegistry(const SessionRegistry&) = delete; + SessionRegistry& operator=(const SessionRegistry&) = delete; + + [[nodiscard]] std::expected CreateSession(void* owner, + uint64_t guid, + uint32_t romOffset); + + [[nodiscard]] bool StartLogin(void* owner, uint64_t handle); + + [[nodiscard]] std::optional GetSessionState(void* owner, + uint64_t handle) const; + + [[nodiscard]] bool SubmitInquiry(void* owner, uint64_t handle, uint8_t allocationLength = 96); + [[nodiscard]] std::optional GetInquiryResult(void* owner, uint64_t handle); + + [[nodiscard]] bool SubmitCommand(void* owner, uint64_t handle, + const SCSI::CommandRequest& request); + [[nodiscard]] std::optional GetCommandResult(void* owner, uint64_t handle); + + [[nodiscard]] bool SubmitTaskManagement(void* owner, uint64_t handle, + SBP2ManagementORB::Function function); + + [[nodiscard]] bool ReleaseSession(void* owner, uint64_t handle); + void ReleaseOwner(void* owner); + + void OnBusReset(uint16_t newGeneration); + void RefreshTargets(Discovery::Generation gen); + +#ifdef ASFW_HOST_TEST + LoginSession* GetSessionForTesting(uint64_t handle); + std::weak_ptr GetSessionWeakForTesting(uint64_t handle); +#endif + +private: + SessionRecord* FindByHandle(uint64_t handle); + const SessionRecord* FindByHandle(uint64_t handle) const; + SessionRecord* FindByHandleForOwner(void* owner, uint64_t handle); + const SessionRecord* FindByHandleForOwner(void* owner, uint64_t handle) const; + + std::shared_ptr ResolveUnit(uint64_t guid, uint32_t romOffset) const; + + [[nodiscard]] bool HasSessionForTargetLocked(uint64_t guid, uint32_t romOffset) const; + void RetireSessionLocked(const SessionRecord& record); + void EraseRetiredSessionLocked(const std::shared_ptr& session); + void SetReleaseLogoutCallbackLocked(uint64_t handle, + const std::shared_ptr& session); + + Async::IFireWireBus& bus_; + Async::IFireWireBusInfo& busInfo_; + AddressSpaceManager& addrSpaceMgr_; + Discovery::IDeviceManager& deviceManager_; + ISessionScheduler& scheduler_; + IODispatchQueue* workQueue_{nullptr}; + + IOLock* lock_{nullptr}; + std::map sessions_; + struct RetiringSession { + uint64_t guid{0}; + uint32_t romOffset{0}; + std::shared_ptr session; + }; + // Hidden from registry clients, but retained until async logout finishes/times out. + std::vector retiringSessions_; + uint64_t nextHandle_{1}; +}; + +} // namespace ASFW::Protocols::SBP2 diff --git a/ASFWDriver/Service/DriverContext.cpp b/ASFWDriver/Service/DriverContext.cpp index c71834c5..fec0ff88 100644 --- a/ASFWDriver/Service/DriverContext.cpp +++ b/ASFWDriver/Service/DriverContext.cpp @@ -31,6 +31,8 @@ #include "../Logging/Logging.hpp" #include "../Protocols/AVC/FCPResponseRouter.hpp" #include "../Protocols/SBP2/AddressSpaceManager.hpp" +#include "../Protocols/SBP2/Session/DriverKitSessionScheduler.hpp" +#include "../Protocols/SBP2/Session/SessionRegistry.hpp" #include "../Scheduling/Scheduler.hpp" void ServiceContext::DisarmProviderNotifications() { @@ -72,6 +74,8 @@ void ServiceContext::Reset() { deps.topologyMapService.reset(); deps.busManagerElectionDriver.reset(); deps.fcpResponseRouter.reset(); // Clean up FCP router + deps.sbp2SessionRegistry.reset(); + deps.sbp2SessionScheduler.reset(); deps.sbp2AddressSpaceManager.reset(); deps.avcDiscovery.reset(); // Clean up AV/C discovery deps.irmClient.reset(); // Clean up IRM client @@ -180,7 +184,7 @@ void DriverWiring::EnsureDeps(ASFWDriver* driver, ::ServiceContext& ctx) { // depend only on IFireWireBus ports (ControllerCore::Bus()). } -void DriverWiring::EnsureSbp2Deps(::ServiceContext& ctx) { +kern_return_t DriverWiring::EnsureSbp2Deps(ASFWDriver& service, ::ServiceContext& ctx) { auto& d = ctx.deps; if (!d.sbp2AddressSpaceManager && d.hardware) { @@ -189,14 +193,35 @@ void DriverWiring::EnsureSbp2Deps(::ServiceContext& ctx) { ASFW_LOG(Controller, "[Controller] SBP2 AddressSpaceManager initialized"); } + if (!d.sbp2SessionScheduler) { + d.sbp2SessionScheduler = + std::make_shared(); + const auto kr = d.sbp2SessionScheduler->Prepare(service, ctx.workQueue); + if (kr != kIOReturnSuccess) { + d.sbp2SessionScheduler.reset(); + return kr; + } + ASFW_LOG(Controller, "[Controller] SBP2 session scheduler initialized"); + } + + if (!d.sbp2SessionRegistry && ctx.controller && d.sbp2AddressSpaceManager && + d.deviceManager && d.sbp2SessionScheduler) { + auto& bus = ctx.controller->Bus(); + d.sbp2SessionRegistry = std::make_shared( + bus, bus, *d.sbp2AddressSpaceManager, *d.deviceManager, *d.sbp2SessionScheduler, + ctx.workQueue.get()); + ASFW_LOG(Controller, "[Controller] SBP2 SessionRegistry initialized"); + } + if (ctx.controller) { ctx.controller->SetSbp2AddressSpaceManager(d.sbp2AddressSpaceManager); + ctx.controller->SetSbp2SessionRegistry(d.sbp2SessionRegistry); } - // Inbound request routing (tCodes 0x0/0x1/0x4/0x5) is owned centrally by - // LocalRequestDispatch (see WireLocalRequestDispatch), which registers the - // SBP-2 / CSR / FCP / DICE address handlers in one place. This function only - // constructs the SBP-2 manager dependency. + // Inbound local-request routing remains owned centrally by LocalRequestDispatch + // (see WireLocalRequestDispatch). This helper owns the higher-level SBP-2 + // session dependencies that sit above the address-space manager. + return kIOReturnSuccess; } kern_return_t DriverWiring::PrepareQueue(ASFWDriver& service, ::ServiceContext& ctx) { diff --git a/ASFWDriver/Service/DriverContext.hpp b/ASFWDriver/Service/DriverContext.hpp index 589356cb..322fe975 100644 --- a/ASFWDriver/Service/DriverContext.hpp +++ b/ASFWDriver/Service/DriverContext.hpp @@ -55,7 +55,7 @@ namespace ASFW::Driver { class DriverWiring { public: static void EnsureDeps(ASFWDriver* driver, ::ServiceContext& ctx); - static void EnsureSbp2Deps(::ServiceContext& ctx); + static kern_return_t EnsureSbp2Deps(ASFWDriver& service, ::ServiceContext& ctx); static kern_return_t PrepareQueue(ASFWDriver& service, ::ServiceContext& ctx); static kern_return_t PrepareInterrupts(ASFWDriver& service, IOService* provider, ::ServiceContext& ctx); static kern_return_t PrepareWatchdog(ASFWDriver& service, ::ServiceContext& ctx); diff --git a/ASFWDriver/UserClient/Core/ASFWDriverUserClient.cpp b/ASFWDriver/UserClient/Core/ASFWDriverUserClient.cpp index 533b17bb..f2ff1ccc 100644 --- a/ASFWDriver/UserClient/Core/ASFWDriverUserClient.cpp +++ b/ASFWDriver/UserClient/Core/ASFWDriverUserClient.cpp @@ -61,6 +61,15 @@ enum { kMethodDeallocateAddressRange = 47, kMethodReadIncomingData = 48, kMethodWriteLocalData = 49, + kMethodCreateSBP2Session = 52, + kMethodStartSBP2Login = 53, + kMethodGetSBP2SessionState = 54, + kMethodSubmitSBP2Inquiry = 55, + kMethodGetSBP2InquiryResult = 56, + kMethodSubmitSBP2Command = 57, + kMethodGetSBP2CommandResult = 58, + kMethodSubmitSBP2TaskManagement = 59, + kMethodReleaseSBP2Session = 60, // TODO(ASFW-IRM): Remove temporary IRM test method after dedicated validation tooling exists. kMethodTestIRMAllocation = 26, kMethodTestIRMRelease = 27, @@ -599,6 +608,24 @@ kern_return_t ASFWDriverUserClient::ExternalMethod(uint64_t selector, return runtimeState->SBP2().ReadIncomingData(arguments, this); case kMethodWriteLocalData: return runtimeState->SBP2().WriteLocalData(arguments, this); + case kMethodCreateSBP2Session: + return runtimeState->SBP2().CreateSBP2Session(arguments, this); + case kMethodStartSBP2Login: + return runtimeState->SBP2().StartSBP2Login(arguments, this); + case kMethodGetSBP2SessionState: + return runtimeState->SBP2().GetSBP2SessionState(arguments, this); + case kMethodSubmitSBP2Inquiry: + return runtimeState->SBP2().SubmitSBP2Inquiry(arguments, this); + case kMethodGetSBP2InquiryResult: + return runtimeState->SBP2().GetSBP2InquiryResult(arguments, this); + case kMethodSubmitSBP2Command: + return runtimeState->SBP2().SubmitSBP2Command(arguments, this); + case kMethodGetSBP2CommandResult: + return runtimeState->SBP2().GetSBP2CommandResult(arguments, this); + case kMethodSubmitSBP2TaskManagement: + return runtimeState->SBP2().SubmitSBP2TaskManagement(arguments, this); + case kMethodReleaseSBP2Session: + return runtimeState->SBP2().ReleaseSBP2Session(arguments, this); default: break; } diff --git a/ASFWDriver/UserClient/Core/ASFWDriverUserClient.iig b/ASFWDriver/UserClient/Core/ASFWDriverUserClient.iig index fd8c2d4b..41cef979 100644 --- a/ASFWDriver/UserClient/Core/ASFWDriverUserClient.iig +++ b/ASFWDriver/UserClient/Core/ASFWDriverUserClient.iig @@ -59,6 +59,15 @@ public: kMethodDeallocateAddressRange = 47, kMethodReadIncomingData = 48, kMethodWriteLocalData = 49, + kMethodCreateSBP2Session = 52, + kMethodStartSBP2Login = 53, + kMethodGetSBP2SessionState = 54, + kMethodSubmitSBP2Inquiry = 55, + kMethodGetSBP2InquiryResult = 56, + kMethodSubmitSBP2Command = 57, + kMethodGetSBP2CommandResult = 58, + kMethodSubmitSBP2TaskManagement = 59, + kMethodReleaseSBP2Session = 60, // TODO(ASFW-IRM): Remove temporary IRM test method after dedicated validation tooling exists. kMethodTestIRMAllocation = 26, kMethodTestIRMRelease = 27, diff --git a/ASFWDriver/UserClient/Core/UserClientRuntimeState.hpp b/ASFWDriver/UserClient/Core/UserClientRuntimeState.hpp index 1063ed8a..d88559b1 100644 --- a/ASFWDriver/UserClient/Core/UserClientRuntimeState.hpp +++ b/ASFWDriver/UserClient/Core/UserClientRuntimeState.hpp @@ -52,9 +52,10 @@ class UserClientRuntimeState final { auto* controllerCore = GetControllerCorePtr(driver); auto* avcDiscovery = controllerCore ? controllerCore->GetAVCDiscovery() : nullptr; auto* sbp2Manager = controllerCore ? controllerCore->GetSbp2AddressSpaceManager() : nullptr; + auto* sbp2Registry = controllerCore ? controllerCore->GetSbp2SessionRegistry() : nullptr; avcHandler_ = std::make_unique(avcDiscovery); isochHandler_ = std::make_unique(driver); - sbp2Handler_ = std::make_unique(sbp2Manager); + sbp2Handler_ = std::make_unique(sbp2Manager, sbp2Registry); diagnosticsHandler_ = std::make_unique(driver); return HandlersReady(); diff --git a/ASFWDriver/UserClient/Handlers/SBP2Handler.hpp b/ASFWDriver/UserClient/Handlers/SBP2Handler.hpp index 78e424a5..9023cf88 100644 --- a/ASFWDriver/UserClient/Handlers/SBP2Handler.hpp +++ b/ASFWDriver/UserClient/Handlers/SBP2Handler.hpp @@ -1,6 +1,20 @@ #pragma once +// SBP2Handler — user-client boundary for the SBP-2 address-space + session/command +// layer. Foundation methods (address-range alloc/dealloc/read/write) operate on the +// AddressSpaceManager; session methods (FW-56's SessionRegistry) expose create/login/ +// state/inquiry/command/task-management/release across the DriverKit selector ABI. +// +// Ported from PR #19 (re-threaded onto DICE's decomposed SessionRegistry). The +// owner-validation contract is load-bearing: every session call passes the opaque +// (void* owner, uint64_t handle) pair straight through to the registry, which +// rejects cross-owner access (8b64806). `registry` defaults to null so the existing +// address-space-only construction keeps working until the registry is wired into the +// driver lifecycle (FW-58). + +#include #include +#include #include #include @@ -8,19 +22,27 @@ #include "../../Logging/Logging.hpp" #include "../../Protocols/SBP2/AddressSpaceManager.hpp" +#include "../../Protocols/SBP2/SCSICommandSet.hpp" +#include "../../Protocols/SBP2/Session/SessionRegistry.hpp" +#include "../WireFormats/SBP2CommandWireFormats.hpp" namespace ASFW::UserClient { class SBP2Handler { public: - explicit SBP2Handler(ASFW::Protocols::SBP2::AddressSpaceManager* manager) - : manager_(manager) {} + explicit SBP2Handler(ASFW::Protocols::SBP2::AddressSpaceManager* manager, + ASFW::Protocols::SBP2::SessionRegistry* registry = nullptr) + : manager_(manager), registry_(registry) {} ~SBP2Handler() = default; SBP2Handler(const SBP2Handler&) = delete; SBP2Handler& operator=(const SBP2Handler&) = delete; + // ----------------------------------------------------------------------- + // Address space management + // ----------------------------------------------------------------------- + kern_return_t AllocateAddressRange(IOUserClientMethodArguments* args, void* owner) { if (!manager_) { return kIOReturnNotReady; @@ -36,12 +58,7 @@ class SBP2Handler { uint64_t handle = 0; const kern_return_t kr = manager_->AllocateAddressRange( - owner, - addressHi, - addressLo, - length, - &handle, - nullptr); + owner, addressHi, addressLo, length, &handle, nullptr); if (kr != kIOReturnSuccess) { return kr; } @@ -118,20 +135,295 @@ class SBP2Handler { } return manager_->WriteLocalData( - owner, - handle, - offset, - std::span(bytes, length)); + owner, handle, offset, std::span(bytes, length)); } + // Release every address range and session owned by this client. void ReleaseOwner(void* owner) { + if (registry_) { + registry_->ReleaseOwner(owner); // sessions before address ranges (9ca0d8e) + } if (manager_) { manager_->ReleaseOwner(owner); } } + // ----------------------------------------------------------------------- + // Session / command management + // ----------------------------------------------------------------------- + + kern_return_t CreateSBP2Session(IOUserClientMethodArguments* args, void* owner) { + if (!registry_) { + return kIOReturnNotReady; + } + if (!args || !args->scalarInput || args->scalarInputCount < 3 || + !args->scalarOutput || args->scalarOutputCount < 1) { + return kIOReturnBadArgument; + } + + const uint32_t guidHi = static_cast(args->scalarInput[0] & 0xFFFF'FFFFu); + const uint32_t guidLo = static_cast(args->scalarInput[1] & 0xFFFF'FFFFu); + const uint32_t romOffset = static_cast(args->scalarInput[2] & 0xFFFF'FFFFu); + const uint64_t guid = (static_cast(guidHi) << 32) | guidLo; + + auto result = registry_->CreateSession(owner, guid, romOffset); + if (!result.has_value()) { + return result.error(); + } + + args->scalarOutput[0] = *result; + args->scalarOutputCount = 1; + return kIOReturnSuccess; + } + + kern_return_t StartSBP2Login(IOUserClientMethodArguments* args, void* owner) { + if (!registry_) { + return kIOReturnNotReady; + } + if (!args || !args->scalarInput || args->scalarInputCount < 1) { + return kIOReturnBadArgument; + } + + const uint64_t handle = args->scalarInput[0]; + return registry_->StartLogin(owner, handle) ? kIOReturnSuccess : kIOReturnError; + } + + kern_return_t GetSBP2SessionState(IOUserClientMethodArguments* args, void* owner) { + if (!registry_) { + return kIOReturnNotReady; + } + if (!args || !args->scalarInput || args->scalarInputCount < 1 || + !args->scalarOutput || args->scalarOutputCount < 5) { + return kIOReturnBadArgument; + } + + const uint64_t handle = args->scalarInput[0]; + auto state = registry_->GetSessionState(owner, handle); + if (!state.has_value()) { + return kIOReturnNotFound; + } + + // Return as scalars: loginState, loginID, generation, lastError, reconnectPending. + args->scalarOutput[0] = static_cast(state->loginState); + args->scalarOutput[1] = static_cast(state->loginID); + args->scalarOutput[2] = static_cast(state->generation); + args->scalarOutput[3] = static_cast(static_cast(state->lastError)); + args->scalarOutput[4] = state->reconnectPending ? 1 : 0; + args->scalarOutputCount = 5; + return kIOReturnSuccess; + } + + kern_return_t SubmitSBP2Inquiry(IOUserClientMethodArguments* args, void* owner) { + if (!registry_) { + return kIOReturnNotReady; + } + if (!args || !args->scalarInput || args->scalarInputCount < 2) { + return kIOReturnBadArgument; + } + + const uint64_t handle = args->scalarInput[0]; + const uint8_t allocationLength = static_cast(args->scalarInput[1] & 0xFFu); + return registry_->SubmitInquiry(owner, handle, allocationLength) ? kIOReturnSuccess + : kIOReturnError; + } + + kern_return_t GetSBP2InquiryResult(IOUserClientMethodArguments* args, void* owner) { + if (!registry_) { + return kIOReturnNotReady; + } + if (!args || !args->scalarInput || args->scalarInputCount < 1) { + return kIOReturnBadArgument; + } + + const uint64_t handle = args->scalarInput[0]; + auto result = registry_->GetInquiryResult(owner, handle); + if (!result.has_value()) { + return kIOReturnNotFound; + } + if (result->transportStatus != 0) { + return result->transportStatus > 0 + ? static_cast(result->transportStatus) + : kIOReturnError; + } + if (result->sbpStatus != ASFW::Protocols::SBP2::Wire::SBPStatus::kNoAdditionalInfo) { + return kIOReturnError; + } + + OSData* output = OSData::withBytes(result->payload.data(), + static_cast(result->payload.size())); + if (!output) { + return kIOReturnNoMemory; + } + + args->structureOutput = output; + args->structureOutputDescriptor = nullptr; + return kIOReturnSuccess; + } + + kern_return_t SubmitSBP2Command(IOUserClientMethodArguments* args, void* owner) { + if (!registry_) { + return kIOReturnNotReady; + } + if (!args || !args->scalarInput || args->scalarInputCount < 1 || !args->structureInput) { + return kIOReturnBadArgument; + } + + OSData* input = OSDynamicCast(OSData, args->structureInput); + if (!input) { + return kIOReturnBadArgument; + } + + const auto* bytes = static_cast(input->getBytesNoCopy()); + const size_t inputLength = input->getLength(); + if (!bytes || inputLength < sizeof(Wire::SBP2CommandRequestWire)) { + return kIOReturnBadArgument; + } + + const auto* header = reinterpret_cast(bytes); + if (header->cdbLength == 0 || header->cdbLength > Wire::kSBP2CommandMaxCDBLength || + header->transferLength > Wire::kSBP2CommandMaxTransferLength || + header->captureSenseData > 1 || header->_reserved[0] != 0 || + header->_reserved[1] != 0) { + return kIOReturnBadArgument; + } + + const size_t expectedLength = sizeof(Wire::SBP2CommandRequestWire) + + static_cast(header->cdbLength) + + static_cast(header->outgoingLength); + if (inputLength != expectedLength) { + return kIOReturnBadArgument; + } + + Protocols::SBP2::SCSI::DataDirection direction{}; + switch (header->direction) { + case 0: + direction = Protocols::SBP2::SCSI::DataDirection::None; + break; + case 1: + direction = Protocols::SBP2::SCSI::DataDirection::FromTarget; + break; + case 2: + direction = Protocols::SBP2::SCSI::DataDirection::ToTarget; + break; + default: + return kIOReturnBadArgument; + } + + if (direction == Protocols::SBP2::SCSI::DataDirection::ToTarget) { + if (header->outgoingLength != header->transferLength) { + return kIOReturnBadArgument; + } + } else if (header->outgoingLength != 0) { + return kIOReturnBadArgument; + } + if (direction == Protocols::SBP2::SCSI::DataDirection::None && header->transferLength != 0) { + return kIOReturnBadArgument; + } + + Protocols::SBP2::SCSI::CommandRequest request{}; + request.direction = direction; + request.transferLength = header->transferLength; + request.timeoutMs = header->timeoutMs; + request.captureSenseData = header->captureSenseData != 0; + + const uint8_t* cursor = bytes + sizeof(Wire::SBP2CommandRequestWire); + request.cdb.assign(cursor, cursor + header->cdbLength); + cursor += header->cdbLength; + request.outgoingPayload.assign(cursor, cursor + header->outgoingLength); + + const uint64_t handle = args->scalarInput[0]; + return registry_->SubmitCommand(owner, handle, request) ? kIOReturnSuccess + : kIOReturnError; + } + + kern_return_t GetSBP2CommandResult(IOUserClientMethodArguments* args, void* owner) { + if (!registry_) { + return kIOReturnNotReady; + } + if (!args || !args->scalarInput || args->scalarInputCount < 1) { + return kIOReturnBadArgument; + } + + const uint64_t handle = args->scalarInput[0]; + auto result = registry_->GetCommandResult(owner, handle); + if (!result.has_value()) { + return kIOReturnNotFound; + } + + Wire::SBP2CommandResultWire header{}; + header.transportStatus = result->transportStatus; + header.sbpStatus = result->sbpStatus; + header.payloadLength = static_cast(result->payload.size()); + header.senseLength = static_cast(result->senseData.size()); + + std::vector serialized(sizeof(Wire::SBP2CommandResultWire) + + result->payload.size() + result->senseData.size()); + std::memcpy(serialized.data(), &header, sizeof(header)); + + size_t offset = sizeof(Wire::SBP2CommandResultWire); + if (!result->payload.empty()) { + std::memcpy(serialized.data() + offset, result->payload.data(), result->payload.size()); + offset += result->payload.size(); + } + if (!result->senseData.empty()) { + std::memcpy(serialized.data() + offset, result->senseData.data(), + result->senseData.size()); + } + + OSData* output = OSData::withBytes(serialized.data(), + static_cast(serialized.size())); + if (!output) { + return kIOReturnNoMemory; + } + + args->structureOutput = output; + args->structureOutputDescriptor = nullptr; + return kIOReturnSuccess; + } + + kern_return_t SubmitSBP2TaskManagement(IOUserClientMethodArguments* args, void* owner) { + if (!registry_) { + return kIOReturnNotReady; + } + if (!args || !args->scalarInput || args->scalarInputCount < 2) { + return kIOReturnBadArgument; + } + + Protocols::SBP2::SBP2ManagementORB::Function function{}; + switch (args->scalarInput[1]) { + case 0x0C: + function = Protocols::SBP2::SBP2ManagementORB::Function::AbortTaskSet; + break; + case 0x0E: + function = Protocols::SBP2::SBP2ManagementORB::Function::LogicalUnitReset; + break; + case 0x0F: + function = Protocols::SBP2::SBP2ManagementORB::Function::TargetReset; + break; + default: + return kIOReturnBadArgument; + } + + const uint64_t handle = args->scalarInput[0]; + return registry_->SubmitTaskManagement(owner, handle, function) ? kIOReturnSuccess + : kIOReturnError; + } + + kern_return_t ReleaseSBP2Session(IOUserClientMethodArguments* args, void* owner) { + if (!registry_) { + return kIOReturnNotReady; + } + if (!args || !args->scalarInput || args->scalarInputCount < 1) { + return kIOReturnBadArgument; + } + + const uint64_t handle = args->scalarInput[0]; + return registry_->ReleaseSession(owner, handle) ? kIOReturnSuccess : kIOReturnNotFound; + } + private: ASFW::Protocols::SBP2::AddressSpaceManager* manager_{nullptr}; + ASFW::Protocols::SBP2::SessionRegistry* registry_{nullptr}; }; } // namespace ASFW::UserClient diff --git a/ASFWDriver/UserClient/WireFormats/SBP2CommandWireFormats.hpp b/ASFWDriver/UserClient/WireFormats/SBP2CommandWireFormats.hpp new file mode 100644 index 00000000..a29e286c --- /dev/null +++ b/ASFWDriver/UserClient/WireFormats/SBP2CommandWireFormats.hpp @@ -0,0 +1,49 @@ +#pragma once + +#include +#include + +namespace ASFW::UserClient::Wire { + +inline constexpr uint32_t kSBP2CommandMaxCDBLength = 16; +inline constexpr uint32_t kSBP2CommandMaxTransferLength = 16u * 1024u * 1024u; + +// DriverKit user-client ABI records. These are native-endian host records, +// not SBP-2 big-endian bus wire structures. +struct __attribute__((packed)) SBP2CommandRequestWire { + uint32_t cdbLength; + uint32_t transferLength; + uint32_t outgoingLength; + uint32_t timeoutMs; + uint8_t direction; + uint8_t captureSenseData; + uint8_t _reserved[2]; + // Followed by: CDB bytes, then outgoing payload bytes. +}; + +struct __attribute__((packed)) SBP2CommandResultWire { + int32_t transportStatus; + uint8_t sbpStatus; + uint8_t _reserved[3]; + uint32_t payloadLength; + uint32_t senseLength; + // Followed by: payload bytes, then sense bytes. +}; + +static_assert(sizeof(SBP2CommandRequestWire) == 20); +static_assert(offsetof(SBP2CommandRequestWire, cdbLength) == 0); +static_assert(offsetof(SBP2CommandRequestWire, transferLength) == 4); +static_assert(offsetof(SBP2CommandRequestWire, outgoingLength) == 8); +static_assert(offsetof(SBP2CommandRequestWire, timeoutMs) == 12); +static_assert(offsetof(SBP2CommandRequestWire, direction) == 16); +static_assert(offsetof(SBP2CommandRequestWire, captureSenseData) == 17); +static_assert(offsetof(SBP2CommandRequestWire, _reserved) == 18); + +static_assert(sizeof(SBP2CommandResultWire) == 16); +static_assert(offsetof(SBP2CommandResultWire, transportStatus) == 0); +static_assert(offsetof(SBP2CommandResultWire, sbpStatus) == 4); +static_assert(offsetof(SBP2CommandResultWire, _reserved) == 5); +static_assert(offsetof(SBP2CommandResultWire, payloadLength) == 8); +static_assert(offsetof(SBP2CommandResultWire, senseLength) == 12); + +} // namespace ASFW::UserClient::Wire diff --git a/tests/common/AddressSpaceManagerTests.cpp b/tests/common/AddressSpaceManagerTests.cpp index 681a8944..114b22c9 100644 --- a/tests/common/AddressSpaceManagerTests.cpp +++ b/tests/common/AddressSpaceManagerTests.cpp @@ -272,3 +272,45 @@ TEST(AddressSpaceManagerTests, AutoAllocationRejectsRequestLargerThanWindow) { &handle, nullptr)); } + +// SetDebugLabel is a diagnostics-only annotation; its contract is that it never +// disturbs range state and silently tolerates unknown/zero handles and null +// labels. Used by the SBP-2 session layer to tag login/status/ORB ranges. +TEST(AddressSpaceManagerTests, SetDebugLabelToleratesBadInputAndPreservesRange) { + ASFW::Protocols::SBP2::AddressSpaceManager manager(nullptr); + + // No-ops on a fresh manager: zero handle, unknown handle. + manager.SetDebugLabel(0, "ignored"); + manager.SetDebugLabel(0xDEAD'BEEF, "unknown"); + + uint64_t handle = 0; + ASSERT_EQ(kIOReturnSuccess, + manager.AllocateAddressRange(reinterpret_cast(0x1), + 0xFFFF, + 0x0010'0000, + 16, + &handle, + nullptr)); + + manager.SetDebugLabel(handle, nullptr); // null label tolerated + manager.SetDebugLabel(handle, "sbp2-login-orb"); // normal labelling + + // Labelling must not perturb the range: a write/read round trip still works. + const std::array payload{0xDE, 0xAD, 0xBE, 0xEF}; + EXPECT_EQ(kIOReturnSuccess, + manager.WriteLocalData(reinterpret_cast(0x1), + handle, + 0, + std::span(payload.data(), payload.size()))); + + std::vector readback; + ASSERT_EQ(kIOReturnSuccess, + manager.ReadIncomingData(reinterpret_cast(0x1), + handle, + 0, + 4, + &readback)); + ASSERT_EQ(4u, readback.size()); + EXPECT_EQ(0xDE, readback[0]); + EXPECT_EQ(0xEF, readback[3]); +} diff --git a/tests/mocks/FakeSessionScheduler.hpp b/tests/mocks/FakeSessionScheduler.hpp new file mode 100644 index 00000000..2c58a391 --- /dev/null +++ b/tests/mocks/FakeSessionScheduler.hpp @@ -0,0 +1,97 @@ +#pragma once + +// FakeSessionScheduler — deterministic virtual-clock backing for ISessionScheduler. +// Host-test only: no real time. Tests drive timeouts by calling Advance(). + +#include "ASFWDriver/Protocols/SBP2/Session/ISessionScheduler.hpp" + +#include +#include +#include +#include +#include + +namespace ASFW::Testing { + +class FakeSessionScheduler final : public ASFW::Protocols::SBP2::ISessionScheduler { +public: + using Token = ASFW::Protocols::SBP2::SchedulerToken; + + [[nodiscard]] Token ScheduleAfter(uint64_t delayNs, + std::function fn) override { + const Token token = ++nextToken_; + entries_.push_back(Entry{token, nowNs_ + delayNs, std::move(fn), false, false}); + return token; + } + + void Cancel(Token token) override { + if (token == ASFW::Protocols::SBP2::kInvalidSchedulerToken) { + return; + } + for (auto& e : entries_) { + if (e.token == token && !e.fired) { + e.canceled = true; + } + } + } + + // Advance the virtual clock by deltaNs, firing every due, non-canceled callback + // in ascending deadline order (ties broken by insertion order). The clock is + // stepped to each callback's own deadline *before* it runs, so a callback that + // schedules follow-up work with a relative delay (e.g. reconnect, busy-timeout + // replay) computes its deadline from its own fire time — matching a real timer, + // not from the end of the Advance() window. Newly-due callbacks fire within the + // same Advance(). The callback is moved out before invocation so a callback that + // mutates the entry list cannot invalidate the function being run. + void Advance(uint64_t deltaNs) { + const uint64_t target = nowNs_ + deltaNs; + for (;;) { + std::size_t bestIdx = entries_.size(); + for (std::size_t i = 0; i < entries_.size(); ++i) { + Entry& e = entries_[i]; + if (e.canceled || e.fired || e.deadlineNs > target) { + continue; + } + if (bestIdx == entries_.size() || + e.deadlineNs < entries_[bestIdx].deadlineNs) { + bestIdx = i; + } + } + if (bestIdx == entries_.size()) { + break; + } + nowNs_ = entries_[bestIdx].deadlineNs; // clock == deadline during callback + entries_[bestIdx].fired = true; + auto fn = std::move(entries_[bestIdx].fn); + fn(); + } + nowNs_ = target; + } + + [[nodiscard]] uint64_t NowNs() const noexcept { return nowNs_; } + + [[nodiscard]] std::size_t PendingCount() const noexcept { + std::size_t n = 0; + for (const auto& e : entries_) { + if (!e.canceled && !e.fired) { + ++n; + } + } + return n; + } + +private: + struct Entry { + Token token; + uint64_t deadlineNs; + std::function fn; + bool canceled; + bool fired; + }; + + Token nextToken_{ASFW::Protocols::SBP2::kInvalidSchedulerToken}; + uint64_t nowNs_{0}; + std::vector entries_; +}; + +} // namespace ASFW::Testing diff --git a/tests/protocols/CMakeLists.txt b/tests/protocols/CMakeLists.txt index ed0dcf56..707ac3f0 100644 --- a/tests/protocols/CMakeLists.txt +++ b/tests/protocols/CMakeLists.txt @@ -185,3 +185,55 @@ add_protocols_test(SBP2ORBTests "${ASFW_DRIVER_DIR}/Protocols/SBP2/SBP2CommandORB.cpp" "${ASFW_DRIVER_DIR}/Protocols/SBP2/SBP2ManagementORB.cpp" ) + +# SBP2 Session Scheduler Tests (ISessionScheduler interface + virtual-clock fake) +add_protocols_test(SessionSchedulerTests + SessionSchedulerTests.cpp +) + +# SBP2 FetchAgent Tests (command-plane ORB submission/chaining/completion) +add_protocols_test(FetchAgentTests + FetchAgentTests.cpp + "${ASFW_DRIVER_DIR}/Protocols/SBP2/Session/FetchAgent.cpp" + "${ASFW_DRIVER_DIR}/Protocols/SBP2/SBP2CommandORB.cpp" +) + +# SBP2 LoginSession Tests (login/reconnect/logout state machine + status routing) +add_protocols_test(LoginSessionTests + LoginSessionTests.cpp + "${ASFW_DRIVER_DIR}/Protocols/SBP2/Session/LoginSession.cpp" + "${ASFW_DRIVER_DIR}/Protocols/SBP2/Session/FetchAgent.cpp" + "${ASFW_DRIVER_DIR}/Protocols/SBP2/SBP2CommandORB.cpp" +) + +# SBP2 SessionRegistry Tests (identity/lifecycle + per-record CommandExecutor) +add_protocols_test(SessionRegistryTests + SessionRegistryTests.cpp + "${ASFW_DRIVER_DIR}/Protocols/SBP2/Session/SessionRegistry.cpp" + "${ASFW_DRIVER_DIR}/Protocols/SBP2/Session/CommandExecutor.cpp" + "${ASFW_DRIVER_DIR}/Protocols/SBP2/Session/LoginSession.cpp" + "${ASFW_DRIVER_DIR}/Protocols/SBP2/Session/FetchAgent.cpp" + "${ASFW_DRIVER_DIR}/Protocols/SBP2/SBP2CommandORB.cpp" + "${ASFW_DRIVER_DIR}/Protocols/SBP2/SBP2ManagementORB.cpp" + "${ASFW_DRIVER_DIR}/Discovery/DeviceManager.cpp" + "${ASFW_DRIVER_DIR}/Discovery/DeviceRegistry.cpp" + "${ASFW_DRIVER_DIR}/Discovery/FWDevice.cpp" + "${ASFW_DRIVER_DIR}/Discovery/FWUnit.cpp" + "${ASFW_DRIVER_DIR}/Discovery/SpeedPolicy.cpp" +) + +# SBP2Handler Tests (user-client boundary: session state + command ABI hardening) +add_protocols_test(SBP2HandlerTests + SBP2HandlerTests.cpp + "${ASFW_DRIVER_DIR}/Protocols/SBP2/Session/SessionRegistry.cpp" + "${ASFW_DRIVER_DIR}/Protocols/SBP2/Session/CommandExecutor.cpp" + "${ASFW_DRIVER_DIR}/Protocols/SBP2/Session/LoginSession.cpp" + "${ASFW_DRIVER_DIR}/Protocols/SBP2/Session/FetchAgent.cpp" + "${ASFW_DRIVER_DIR}/Protocols/SBP2/SBP2CommandORB.cpp" + "${ASFW_DRIVER_DIR}/Protocols/SBP2/SBP2ManagementORB.cpp" + "${ASFW_DRIVER_DIR}/Discovery/DeviceManager.cpp" + "${ASFW_DRIVER_DIR}/Discovery/DeviceRegistry.cpp" + "${ASFW_DRIVER_DIR}/Discovery/FWDevice.cpp" + "${ASFW_DRIVER_DIR}/Discovery/FWUnit.cpp" + "${ASFW_DRIVER_DIR}/Discovery/SpeedPolicy.cpp" +) diff --git a/tests/protocols/FetchAgentTests.cpp b/tests/protocols/FetchAgentTests.cpp new file mode 100644 index 00000000..e42a2c4f --- /dev/null +++ b/tests/protocols/FetchAgentTests.cpp @@ -0,0 +1,153 @@ +#include + +#include "ASFWDriver/Protocols/SBP2/Session/FetchAgent.hpp" +#include "ASFWDriver/Protocols/SBP2/SBP2CommandORB.hpp" +#include "ASFWDriver/Protocols/SBP2/AddressSpaceManager.hpp" +#include "ASFWDriver/Protocols/SBP2/SBP2WireFormats.hpp" +#include "ASFWDriver/Testing/HostDriverKitStubs.hpp" +#include "tests/mocks/DeferredFireWireBus.hpp" +#include "FakeSessionScheduler.hpp" + +#include +#include +#include + +namespace { + +using ASFW::Protocols::SBP2::AddressSpaceManager; +using ASFW::Protocols::SBP2::FetchAgent; +using ASFW::Protocols::SBP2::SBP2CommandORB; +using ASFW::Async::Testing::DeferredFireWireBus; +using ASFW::Testing::FakeSessionScheduler; +using ASFW::Async::AsyncStatus; +namespace Wire = ASFW::Protocols::SBP2::Wire; + +constexpr uint64_t kMs = 1'000'000ULL; + +ASFW::Async::FWAddress MakeAddr(uint16_t hi, uint32_t lo, uint16_t node) { + return ASFW::Async::FWAddress( + ASFW::Async::FWAddress::QualifiedAddressParts{hi, lo, node}); +} + +struct Rig { + DeferredFireWireBus bus; + FakeSessionScheduler scheduler; + AddressSpaceManager addressManager{nullptr}; + + Rig() { + bus.SetGeneration(ASFW::FW::Generation{1}); + bus.SetLocalNodeID(ASFW::FW::NodeId{0x21}); + bus.SetDefaultSpeed(ASFW::FW::FwSpeed::S400); + } + + FetchAgent::Binding Binding() const { + return FetchAgent::Binding{ + .generation = 1, + .nodeID = 0x02, + .fetchAgentAddress = MakeAddr(0xFFFF, 0x0000'0050u, 0x02), + .doorbellAddress = MakeAddr(0xFFFF, 0x0000'0054u, 0x02), + .agentResetAddress = MakeAddr(0xFFFF, 0x0000'0058u, 0x02), + .maxPayloadSize = 2048, + }; + } +}; + +// Build a solicited status block targeting the given ORB address. +Wire::StatusBlock StatusFor(const ASFW::Async::FWAddress& orbAddr, uint8_t sbpStatus) { + Wire::StatusBlock block{}; + block.details = 0x00; // solicited (source bits not the 0x80 unsolicited pattern) + block.sbpStatus = sbpStatus; + block.orbOffsetHi = OSSwapHostToBigInt16(orbAddr.addressHi); + block.orbOffsetLo = OSSwapHostToBigInt32(orbAddr.addressLo); + return block; +} + +TEST(FetchAgentTests, SubmitRejectedWhenUnbound) { + Rig rig; + FetchAgent agent(rig.bus, rig.bus, rig.scheduler); + SBP2CommandORB orb(rig.addressManager, reinterpret_cast(0x1), 16); + EXPECT_FALSE(agent.Submit(&orb)); +} + +TEST(FetchAgentTests, ImmediateSubmitWritesToFetchAgentAndArmsTimeoutOnSuccess) { + Rig rig; + FetchAgent agent(rig.bus, rig.bus, rig.scheduler); + agent.Bind(rig.Binding()); + + SBP2CommandORB orb(rig.addressManager, reinterpret_cast(0x1), 16); + orb.SetFlags(SBP2CommandORB::kImmediate | SBP2CommandORB::kNotify | SBP2CommandORB::kNormalORB); + orb.SetTimeout(500); + int completions = 0; + int lastSbp = -1; + orb.SetCompletionCallback([&](int /*transport*/, uint8_t sbp) { + ++completions; + lastSbp = sbp; + }); + + ASSERT_TRUE(agent.Submit(&orb)); + ASSERT_EQ(1u, rig.bus.PendingWriteCount()); // fetch-agent write issued + EXPECT_EQ(0u, rig.scheduler.PendingCount()); // timeout not armed until write ACKs + + ASSERT_TRUE(rig.bus.CompleteNextWrite(AsyncStatus::kSuccess)); + EXPECT_EQ(1u, rig.scheduler.PendingCount()); // timeout armed after success + EXPECT_EQ(0, completions); + + // Solicited status completes the ORB and cancels the timeout. + ASSERT_TRUE(agent.OnStatusBlock(StatusFor(orb.GetORBAddress(), 0x00), Wire::StatusBlock::kMaxSize)); + EXPECT_EQ(1, completions); + EXPECT_EQ(0x00, lastSbp); + EXPECT_EQ(0u, rig.scheduler.PendingCount()); // timeout canceled on completion +} + +TEST(FetchAgentTests, OrbTimeoutFailsCommandWhenNoStatusArrives) { + Rig rig; + FetchAgent agent(rig.bus, rig.bus, rig.scheduler); + agent.Bind(rig.Binding()); + + SBP2CommandORB orb(rig.addressManager, reinterpret_cast(0x2), 16); + orb.SetFlags(SBP2CommandORB::kImmediate); + orb.SetTimeout(500); + int completions = 0; + int lastTransport = 0; + orb.SetCompletionCallback([&](int transport, uint8_t /*sbp*/) { + ++completions; + lastTransport = transport; + }); + + ASSERT_TRUE(agent.Submit(&orb)); + ASSERT_TRUE(rig.bus.CompleteNextWrite(AsyncStatus::kSuccess)); + ASSERT_EQ(1u, rig.scheduler.PendingCount()); + + rig.scheduler.Advance(499 * kMs); + EXPECT_EQ(0, completions); + rig.scheduler.Advance(2 * kMs); // cross the 500 ms deadline + EXPECT_EQ(1, completions); + EXPECT_EQ(-1, lastTransport); +} + +TEST(FetchAgentTests, WriteRetryExhaustionFailsOrbAndResetsAgent) { + Rig rig; + FetchAgent agent(rig.bus, rig.bus, rig.scheduler); + agent.Bind(rig.Binding()); + agent.SetWriteRetriesForTesting(0); // no retries → first failure is terminal + + SBP2CommandORB orb(rig.addressManager, reinterpret_cast(0x3), 16); + orb.SetFlags(SBP2CommandORB::kImmediate); + orb.SetTimeout(500); + int completions = 0; + int lastTransport = 0; + orb.SetCompletionCallback([&](int transport, uint8_t /*sbp*/) { + ++completions; + lastTransport = transport; + }); + + ASSERT_TRUE(agent.Submit(&orb)); + ASSERT_EQ(1u, rig.bus.PendingWriteCount()); + ASSERT_TRUE(rig.bus.CompleteNextWrite(AsyncStatus::kHardwareError)); // fetch-agent write fails + + EXPECT_EQ(1, completions); + EXPECT_EQ(-1, lastTransport); + EXPECT_EQ(1u, rig.bus.PendingWriteCount()); // agent-reset write issued +} + +} // namespace diff --git a/tests/protocols/LoginSessionTests.cpp b/tests/protocols/LoginSessionTests.cpp new file mode 100644 index 00000000..c29e24aa --- /dev/null +++ b/tests/protocols/LoginSessionTests.cpp @@ -0,0 +1,600 @@ +#include + +#include "ASFWDriver/Protocols/SBP2/Session/LoginSession.hpp" +#include "ASFWDriver/Testing/HostDriverKitStubs.hpp" +#include "tests/mocks/DeferredFireWireBus.hpp" +#include "FakeSessionScheduler.hpp" + +#include +#include +#include +#include + +// LoginSession host tests — ported from PR #19's SBP2LoginSessionTests, adapted to +// the decomposed DICE component: the class is `LoginSession`, timers run on an +// injected ISessionScheduler (FakeSessionScheduler virtual clock) instead of the +// two-queue IOSleep model, and command-plane ORB submission/retry/status-matching +// is delegated to the composed FetchAgent. #19's two-queue-specific test +// (LoginRetryDelayUsesTimeoutQueueInsteadOfWorkQueue) is dropped — that machinery +// is removed in DICE (SBP2_SESSION_PORT.md §4). + +namespace { + +constexpr uint16_t kTargetNodeID = 0x05; + +using ASFW::Protocols::SBP2::AddressSpaceManager; +using ASFW::Protocols::SBP2::LoginState; +using ASFW::Protocols::SBP2::LoginSession; +using ASFW::Protocols::SBP2::SBP2CommandORB; +using ASFW::Protocols::SBP2::SBP2TargetInfo; +using ASFW::Testing::FakeSessionScheduler; +using ASFW::Protocols::SBP2::Wire::CommandBlockAgentOffsets; +using ASFW::Protocols::SBP2::Wire::LoginORB; +using ASFW::Protocols::SBP2::Wire::LoginResponse; +using ASFW::Protocols::SBP2::Wire::NormalizeBusNodeID; +using ASFW::Protocols::SBP2::Wire::ReconnectORB; +using ASFW::Protocols::SBP2::Wire::StatusBlock; +namespace SBPStatus = ASFW::Protocols::SBP2::Wire::SBPStatus; +// OSSwapHostToBigInt*/OSSwapBigToHostInt* are global byte-order intrinsics. + +uint64_t ComposeAddress(uint16_t hi, uint32_t lo) { + return (static_cast(hi) << 32) | lo; +} + +uint64_t DecodeOrbAddressFromPayload(std::span payload) { + const uint16_t addressHi = + static_cast((static_cast(payload[2]) << 8) | payload[3]); + const uint32_t addressLo = + (static_cast(payload[4]) << 24) | + (static_cast(payload[5]) << 16) | + (static_cast(payload[6]) << 8) | + static_cast(payload[7]); + return ComposeAddress(addressHi, addressLo); +} + +uint32_t DecodeBE32Payload(std::span payload) { + return (static_cast(payload[0]) << 24) | + (static_cast(payload[1]) << 16) | + (static_cast(payload[2]) << 8) | + static_cast(payload[3]); +} + +void ExpectBusyTimeoutWrite(const ASFW::Async::Testing::DeferredFireWireBus::WriteSummary& write, + uint16_t expectedNodeID) { + ASSERT_EQ(4u, write.data.size()); + EXPECT_EQ(0xFFFFu, write.address.addressHi); + EXPECT_EQ(0xF0000210u, write.address.addressLo); + EXPECT_EQ(expectedNodeID, write.address.nodeID); + EXPECT_EQ(static_cast(expectedNodeID & 0x3Fu), write.nodeId.value); + EXPECT_EQ(0x0000000Fu, DecodeBE32Payload(write.data)); +} + +uint32_t ReadQuadlet(AddressSpaceManager& manager, uint64_t address) { + uint32_t value = 0; + EXPECT_EQ(ASFW::Async::ResponseCode::Complete, manager.ReadQuadlet(address, &value)); + return value; +} + +uint64_t ReadORBAddress(AddressSpaceManager& manager, + uint64_t orbAddress, + size_t hiOffset, + size_t loOffset) { + const uint32_t hi = OSSwapBigToHostInt32(ReadQuadlet(manager, orbAddress + hiOffset)); + const uint32_t lo = OSSwapBigToHostInt32(ReadQuadlet(manager, orbAddress + loOffset)); + return ComposeAddress(static_cast(hi & 0xFFFFu), lo); +} + +class SessionRig { +public: + SessionRig() + : sessionOwner(std::make_shared(bus, bus, addressManager, scheduler)) + , session(*sessionOwner) { + bus.SetGeneration(ASFW::FW::Generation{1}); + bus.SetLocalNodeID(ASFW::FW::NodeId{0x2A}); + bus.SetDefaultSpeed(ASFW::FW::FwSpeed::S400); + + SBP2TargetInfo info{}; + info.managementAgentOffset = 0x100; + info.lun = 3; + info.managementTimeoutMs = 10; + info.maxORBSize = 32; + info.maxCommandBlockSize = 12; + info.targetNodeId = kTargetNodeID; + + session.Configure(info); + } + + // Bus completions fire inline via CompleteNextWrite; timeouts/retries run on + // the virtual-clock scheduler. There is no queue to drain. + void DrainReady() {} + + void AdvanceMs(uint64_t milliseconds) { + scheduler.Advance(milliseconds * 1'000'000ULL); + } + + void LoginSuccessfully(uint16_t loginId = 0x0042, + uint32_t commandBlockAgentLo = 0x0020'0000, + bool drainPendingWrites = true) { + ASSERT_TRUE(session.Login()); + ASSERT_EQ(1u, bus.PendingWriteCount()); + + const auto& loginWrite = bus.WriteAt(0); + const uint64_t loginOrbAddress = DecodeOrbAddressFromPayload(loginWrite.data); + const uint64_t loginResponseAddress = + ReadORBAddress(addressManager, loginOrbAddress, + offsetof(LoginORB, loginResponseAddressHi), + offsetof(LoginORB, loginResponseAddressLo)); + const uint64_t statusAddress = + ReadORBAddress(addressManager, loginOrbAddress, + offsetof(LoginORB, statusFIFOAddressHi), + offsetof(LoginORB, statusFIFOAddressLo)); + sessionStatusAddress = statusAddress; + + LoginResponse response{}; + response.length = OSSwapHostToBigInt16(LoginResponse::kSize); + response.loginID = OSSwapHostToBigInt16(loginId); + response.commandBlockAgentAddressHi = OSSwapHostToBigInt32(0x0000'FFFFu); + response.commandBlockAgentAddressLo = OSSwapHostToBigInt32(commandBlockAgentLo); + response.reconnectHold = OSSwapHostToBigInt16(1); + + ASSERT_TRUE(bus.CompleteNextWrite(ASFW::Async::AsyncStatus::kSuccess)); + addressManager.ApplyRemoteWrite( + loginResponseAddress, + std::span{reinterpret_cast(&response), sizeof(response)}); + + StatusBlock status{}; + status.details = 0; + status.sbpStatus = SBPStatus::kNoAdditionalInfo; + addressManager.ApplyRemoteWrite( + statusAddress, + std::span{reinterpret_cast(&status), sizeof(status)}); + + if (drainPendingWrites) { + while (bus.PendingWriteCount() > 0U) { + ASSERT_TRUE(bus.CompleteNextWrite(ASFW::Async::AsyncStatus::kSuccess)); + } + } + ASSERT_EQ(LoginState::LoggedIn, session.State()); + } + + ASFW::Async::Testing::DeferredFireWireBus bus; + FakeSessionScheduler scheduler; + AddressSpaceManager addressManager{nullptr}; + std::shared_ptr sessionOwner; + LoginSession& session; + uint64_t sessionStatusAddress{0}; +}; + +TEST(LoginSessionTests, LoginAckCancelsStaleTimeoutBeforeStatusArrives) { + SessionRig rig; + + ASSERT_TRUE(rig.session.Login()); + ASSERT_EQ(LoginState::LoggingIn, rig.session.State()); + ASSERT_EQ(1u, rig.bus.PendingWriteCount()); + + rig.AdvanceMs(5); + ASSERT_TRUE(rig.bus.CompleteNextWrite(ASFW::Async::AsyncStatus::kSuccess)); + + rig.AdvanceMs(5); + + EXPECT_EQ(LoginState::LoggingIn, rig.session.State()); + EXPECT_EQ(1u, rig.bus.WriteCount()); +} + +TEST(LoginSessionTests, LoginORBMatchesAppleWireLayout) { + SessionRig rig; + + ASSERT_TRUE(rig.session.Login()); + ASSERT_EQ(1u, rig.bus.PendingWriteCount()); + + const auto& loginWrite = rig.bus.WriteAt(0); + const uint64_t loginOrbAddress = DecodeOrbAddressFromPayload(loginWrite.data); + + const uint32_t quadlet4 = OSSwapBigToHostInt32( + ReadQuadlet(rig.addressManager, loginOrbAddress + offsetof(LoginORB, options))); + const uint32_t quadlet5 = OSSwapBigToHostInt32( + ReadQuadlet(rig.addressManager, loginOrbAddress + offsetof(LoginORB, passwordLength))); + + EXPECT_EQ(0x9000u, static_cast(quadlet4 >> 16)); + EXPECT_EQ(3u, static_cast(quadlet4 & 0xFFFFu)); + EXPECT_EQ(0u, static_cast(quadlet5 >> 16)); + EXPECT_EQ(LoginResponse::kSize, static_cast(quadlet5 & 0xFFFFu)); +} + +TEST(LoginSessionTests, LoginORBUsesFullBusNodeIdInEmbeddedAddresses) { + SessionRig rig; + + ASSERT_TRUE(rig.session.Login()); + ASSERT_EQ(1u, rig.bus.PendingWriteCount()); + + const auto& loginWrite = rig.bus.WriteAt(0); + ASSERT_EQ(8u, loginWrite.data.size()); + + const uint16_t payloadNode = + static_cast((static_cast(loginWrite.data[0]) << 8) | + loginWrite.data[1]); + const uint64_t loginOrbAddress = DecodeOrbAddressFromPayload(loginWrite.data); + const uint32_t responseHi = OSSwapBigToHostInt32( + ReadQuadlet(rig.addressManager, loginOrbAddress + offsetof(LoginORB, loginResponseAddressHi))); + const uint32_t statusHi = OSSwapBigToHostInt32( + ReadQuadlet(rig.addressManager, loginOrbAddress + offsetof(LoginORB, statusFIFOAddressHi))); + + const uint16_t expectedNode = NormalizeBusNodeID(0x2A); + EXPECT_EQ(expectedNode, payloadNode); + EXPECT_EQ((static_cast(expectedNode) << 16) | 0xFFFFu, responseHi); + EXPECT_EQ((static_cast(expectedNode) << 16) | 0xFFFFu, statusHi); +} + +TEST(LoginSessionTests, QueuedUnsolicitedStatusEnableUsesDerivedAddressAfterLogin) { + SessionRig rig; + + rig.session.EnableUnsolicitedStatus(); + constexpr uint32_t commandBlockAgentLo = 0x0030'0000; + rig.LoginSuccessfully(0x0042, commandBlockAgentLo); + + ASSERT_GE(rig.bus.WriteCount(), 3u); + const auto& unsolicitedWrite = rig.bus.WriteAt(1); + EXPECT_EQ(0xFFFFu, unsolicitedWrite.address.addressHi); + EXPECT_EQ(commandBlockAgentLo + CommandBlockAgentOffsets::kUnsolicitedStatusEnable, + unsolicitedWrite.address.addressLo); +} + +TEST(LoginSessionTests, LoginWritesBusyTimeoutRegister) { + SessionRig rig; + + rig.LoginSuccessfully(); + + ASSERT_GE(rig.bus.WriteCount(), 2u); + ExpectBusyTimeoutWrite(rig.bus.WriteAt(1), kTargetNodeID); +} + +TEST(LoginSessionTests, ReconnectReplaysBusyTimeoutRegister) { + SessionRig rig; + rig.LoginSuccessfully(); + + rig.bus.SetGeneration(ASFW::FW::Generation{2}); + rig.session.HandleBusReset(2); + ASSERT_EQ(LoginState::Suspended, rig.session.State()); + ASSERT_TRUE(rig.session.Reconnect()); + ASSERT_EQ(LoginState::Reconnecting, rig.session.State()); + + const size_t reconnectWriteIndex = rig.bus.WriteCount() - 1; + const uint64_t reconnectOrbAddress = + DecodeOrbAddressFromPayload(rig.bus.WriteAt(reconnectWriteIndex).data); + const uint64_t reconnectStatusAddress = + ReadORBAddress(rig.addressManager, reconnectOrbAddress, + offsetof(ReconnectORB, statusFIFOAddressHi), + offsetof(ReconnectORB, statusFIFOAddressLo)); + + ASSERT_TRUE(rig.bus.CompleteNextWrite(ASFW::Async::AsyncStatus::kSuccess)); + StatusBlock status{}; + status.details = 0; + status.sbpStatus = SBPStatus::kNoAdditionalInfo; + rig.addressManager.ApplyRemoteWrite( + reconnectStatusAddress, + std::span{reinterpret_cast(&status), sizeof(status)}); + + ASSERT_GE(rig.bus.WriteCount(), reconnectWriteIndex + 2); + ExpectBusyTimeoutWrite(rig.bus.WriteAt(reconnectWriteIndex + 1), kTargetNodeID); +} + +TEST(LoginSessionTests, ReconnectAckStartsStatusTimeoutAndFallsBackToLogin) { + SessionRig rig; + rig.LoginSuccessfully(); + + rig.bus.SetGeneration(ASFW::FW::Generation{2}); + rig.session.HandleBusReset(2); + ASSERT_EQ(LoginState::Suspended, rig.session.State()); + ASSERT_TRUE(rig.session.Reconnect()); + ASSERT_EQ(LoginState::Reconnecting, rig.session.State()); + + const size_t reconnectWriteIndex = rig.bus.WriteCount() - 1; + ASSERT_TRUE(rig.bus.CompleteWrite(rig.bus.WriteAt(reconnectWriteIndex).handle, + ASFW::Async::AsyncStatus::kSuccess)); + + rig.AdvanceMs(1009); + EXPECT_EQ(LoginState::Reconnecting, rig.session.State()); + EXPECT_EQ(reconnectWriteIndex + 1, rig.bus.WriteCount()); + + rig.AdvanceMs(1); + EXPECT_EQ(LoginState::LoggingIn, rig.session.State()); + EXPECT_EQ(reconnectWriteIndex + 2, rig.bus.WriteCount()); +} + +TEST(LoginSessionTests, BusyTimeoutReplayCancelsInFlightWrite) { + SessionRig rig; + + rig.LoginSuccessfully(0x0042, 0x0020'0000, false); + const auto firstBusyHandle = rig.bus.WriteAt(1).handle; + + rig.bus.SetGeneration(ASFW::FW::Generation{2}); + rig.session.HandleBusReset(2); + ASSERT_TRUE(rig.session.Reconnect()); + + const size_t reconnectWriteIndex = rig.bus.WriteCount() - 1; + const uint64_t reconnectOrbAddress = + DecodeOrbAddressFromPayload(rig.bus.WriteAt(reconnectWriteIndex).data); + const uint64_t reconnectStatusAddress = + ReadORBAddress(rig.addressManager, reconnectOrbAddress, + offsetof(ReconnectORB, statusFIFOAddressHi), + offsetof(ReconnectORB, statusFIFOAddressLo)); + + ASSERT_TRUE(rig.bus.CompleteWrite(rig.bus.WriteAt(reconnectWriteIndex).handle, + ASFW::Async::AsyncStatus::kSuccess)); + StatusBlock status{}; + status.details = 0; + status.sbpStatus = SBPStatus::kNoAdditionalInfo; + rig.addressManager.ApplyRemoteWrite( + reconnectStatusAddress, + std::span{reinterpret_cast(&status), sizeof(status)}); + + EXPECT_FALSE(rig.bus.CompleteWrite(firstBusyHandle, ASFW::Async::AsyncStatus::kSuccess)); + ASSERT_GE(rig.bus.WriteCount(), reconnectWriteIndex + 2); + ExpectBusyTimeoutWrite(rig.bus.WriteAt(reconnectWriteIndex + 1), kTargetNodeID); +} + +TEST(LoginSessionTests, BusResetWhileLoggingInRetriesLoginAfterDelay) { + SessionRig rig; + + ASSERT_TRUE(rig.session.Login()); + ASSERT_EQ(LoginState::LoggingIn, rig.session.State()); + ASSERT_EQ(1u, rig.bus.WriteCount()); + + rig.bus.SetGeneration(ASFW::FW::Generation{2}); + rig.session.HandleBusReset(2); + EXPECT_EQ(LoginState::Idle, rig.session.State()); + + rig.AdvanceMs(99); + EXPECT_EQ(1u, rig.bus.WriteCount()); + + rig.AdvanceMs(1); + EXPECT_EQ(LoginState::LoggingIn, rig.session.State()); + EXPECT_EQ(2u, rig.bus.WriteCount()); + EXPECT_EQ(2u, rig.session.Generation()); +} + +TEST(LoginSessionTests, BusResetWhileLoggingInDefersRetryOnScheduler) { + SessionRig rig; + + ASSERT_TRUE(rig.session.Login()); + rig.bus.SetGeneration(ASFW::FW::Generation{2}); + rig.session.HandleBusReset(2); + + // The retry is deferred on the scheduler, not run inline. + EXPECT_GT(rig.scheduler.PendingCount(), 0u); + EXPECT_EQ(LoginState::Idle, rig.session.State()); + EXPECT_EQ(1u, rig.bus.WriteCount()); +} + +TEST(LoginSessionTests, BusResetWhileReconnectingRetriesReconnectAfterDelay) { + SessionRig rig; + rig.LoginSuccessfully(); + + rig.bus.SetGeneration(ASFW::FW::Generation{2}); + rig.session.HandleBusReset(2); + ASSERT_EQ(LoginState::Suspended, rig.session.State()); + ASSERT_TRUE(rig.session.Reconnect()); + ASSERT_EQ(LoginState::Reconnecting, rig.session.State()); + ASSERT_EQ(2u, rig.session.Generation()); + ASSERT_EQ(3u, rig.bus.WriteCount()); + + rig.bus.SetGeneration(ASFW::FW::Generation{3}); + rig.session.HandleBusReset(3); + EXPECT_EQ(LoginState::Suspended, rig.session.State()); + + rig.AdvanceMs(99); + EXPECT_EQ(3u, rig.bus.WriteCount()); + + rig.AdvanceMs(1); + EXPECT_EQ(LoginState::Reconnecting, rig.session.State()); + EXPECT_EQ(4u, rig.bus.WriteCount()); + EXPECT_EQ(3u, rig.session.Generation()); +} + +TEST(LoginSessionTests, ImmediateORBRetryStaysBoundToOriginalORBAndQueuesNextImmediate) { + SessionRig rig; + rig.LoginSuccessfully(); + + SBP2CommandORB first(rig.addressManager, &rig.session, 16); + first.SetFlags(SBP2CommandORB::kImmediate); + first.SetTimeout(50); + + SBP2CommandORB second(rig.addressManager, &rig.session, 16); + second.SetFlags(SBP2CommandORB::kImmediate); + + ASSERT_TRUE(rig.session.SubmitORB(&first)); + ASSERT_EQ(1u, rig.bus.PendingWriteCount()); + const size_t firstFetchWriteIndex = rig.bus.WriteCount() - 1; + const uint32_t firstAddressLo = static_cast( + DecodeOrbAddressFromPayload(rig.bus.WriteAt(firstFetchWriteIndex).data)); + + ASSERT_TRUE(rig.session.SubmitORB(&second)); + EXPECT_EQ(3u, rig.bus.WriteCount()); + + ASSERT_TRUE(rig.bus.CompleteNextWrite(ASFW::Async::AsyncStatus::kTimeout)); + + rig.AdvanceMs(999); + EXPECT_EQ(3u, rig.bus.WriteCount()); + + rig.AdvanceMs(1); + ASSERT_EQ(4u, rig.bus.WriteCount()); + const uint32_t retryAddressLo = static_cast( + DecodeOrbAddressFromPayload(rig.bus.WriteAt(3).data)); + EXPECT_EQ(firstAddressLo, retryAddressLo); +} + +TEST(LoginSessionTests, SubmittedImmediateORBStartsTimeoutAfterFetchAgentWriteSucceeds) { + SessionRig rig; + rig.LoginSuccessfully(); + + SBP2CommandORB orb(rig.addressManager, &rig.session, 16); + orb.SetFlags(SBP2CommandORB::kImmediate); + orb.SetTimeout(25); + + int callbackStatus = 99; + int callbackCount = 0; + orb.SetCompletionCallback([&](int status, uint8_t) { + ++callbackCount; + callbackStatus = status; + }); + + const size_t pendingTimersBeforeSubmit = rig.scheduler.PendingCount(); + + ASSERT_TRUE(rig.session.SubmitORB(&orb)); + EXPECT_EQ(pendingTimersBeforeSubmit, rig.scheduler.PendingCount()); + + ASSERT_TRUE(rig.bus.CompleteNextWrite(ASFW::Async::AsyncStatus::kSuccess)); + EXPECT_EQ(pendingTimersBeforeSubmit + 1U, rig.scheduler.PendingCount()); + + rig.AdvanceMs(24); + EXPECT_EQ(0, callbackCount); + + rig.AdvanceMs(1); + EXPECT_EQ(1, callbackCount); + EXPECT_EQ(-1, callbackStatus); +} + +TEST(LoginSessionTests, SolicitedStatusCompletesORBMatchingByORBAddress) { + SessionRig rig; + rig.LoginSuccessfully(); + + ASSERT_NE(0u, rig.session.CommandBlockAgent().addressLo); + + SBP2CommandORB first(rig.addressManager, &rig.session, 16); + first.SetFlags(0); + int firstStatus = 99; + first.SetCompletionCallback([&firstStatus](int status, uint8_t) { firstStatus = status; }); + + SBP2CommandORB second(rig.addressManager, &rig.session, 16); + second.SetFlags(0); + int secondStatus = 99; + second.SetCompletionCallback([&secondStatus](int status, uint8_t) { secondStatus = status; }); + + ASSERT_TRUE(rig.session.SubmitORB(&first)); + ASSERT_TRUE(rig.bus.CompleteNextWrite(ASFW::Async::AsyncStatus::kSuccess)); + + ASSERT_TRUE(rig.session.SubmitORB(&second)); + ASSERT_TRUE(rig.bus.CompleteNextWrite(ASFW::Async::AsyncStatus::kSuccess)); + + StatusBlock status{}; + const auto firstAddress = first.GetORBAddress(); + status.details = 0; + status.sbpStatus = SBPStatus::kNoAdditionalInfo; + status.orbOffsetHi = OSSwapHostToBigInt16(firstAddress.addressHi); + status.orbOffsetLo = OSSwapHostToBigInt32(firstAddress.addressLo); + + rig.addressManager.ApplyRemoteWrite( + rig.sessionStatusAddress, + std::span{reinterpret_cast(&status), sizeof(status)}); + + EXPECT_EQ(0, firstStatus); + EXPECT_EQ(99, secondStatus); +} + +TEST(LoginSessionTests, ChainedORBLinkFailureReturnsFalseWithoutDoorbellWrite) { + SessionRig rig; + rig.LoginSuccessfully(); + + auto* firstOwner = reinterpret_cast(0x101); + auto* secondOwner = reinterpret_cast(0x102); + + SBP2CommandORB first(rig.addressManager, firstOwner, 16); + first.SetFlags(0); + ASSERT_TRUE(rig.session.SubmitORB(&first)); + ASSERT_TRUE(rig.bus.CompleteNextWrite(ASFW::Async::AsyncStatus::kSuccess)); + + rig.addressManager.ReleaseOwner(firstOwner); + + SBP2CommandORB second(rig.addressManager, secondOwner, 16); + second.SetFlags(0); + + int secondTransportStatus = 99; + second.SetCompletionCallback([&secondTransportStatus](int status, uint8_t) { + secondTransportStatus = status; + }); + + const size_t writeCountBeforeSubmit = rig.bus.WriteCount(); + EXPECT_FALSE(rig.session.SubmitORB(&second)); + EXPECT_EQ(writeCountBeforeSubmit, rig.bus.WriteCount()); + EXPECT_EQ(-1, secondTransportStatus); + EXPECT_FALSE(second.IsAppended()); +} + +TEST(LoginSessionTests, ImmediateORBWriteRetryExhaustionFailsActiveCommandAndResetsFetchAgent) { + SessionRig rig; + rig.LoginSuccessfully(); + + SBP2CommandORB orb(rig.addressManager, &rig.session, 16); + orb.SetFlags(SBP2CommandORB::kImmediate); + + int callbackStatus = 99; + int callbackCount = 0; + orb.SetCompletionCallback([&](int status, uint8_t) { + ++callbackCount; + callbackStatus = status; + }); + + const auto commandBlock = rig.session.CommandBlockAgent(); + const size_t writesBeforeSubmit = rig.bus.WriteCount(); + ASSERT_TRUE(rig.session.SubmitORB(&orb)); + orb.SetFetchAgentWriteRetries(0); + ASSERT_EQ(writesBeforeSubmit + 1, rig.bus.WriteCount()); + + ASSERT_TRUE(rig.bus.CompleteNextWrite(ASFW::Async::AsyncStatus::kTimeout)); + + ASSERT_EQ(1, callbackCount); + EXPECT_EQ(-1, callbackStatus); + EXPECT_FALSE(orb.IsAppended()); + EXPECT_GE(rig.bus.WriteCount(), writesBeforeSubmit + 2); + EXPECT_EQ(commandBlock.addressLo + CommandBlockAgentOffsets::kAgentReset, + rig.bus.WriteAt(rig.bus.WriteCount() - 1).address.addressLo); + + EXPECT_TRUE(rig.bus.CompleteNextWrite(ASFW::Async::AsyncStatus::kSuccess)); +} + +TEST(LoginSessionTests, + ImmediateORBWriteRetryExhaustionClearsPendingImmediateQueueAndFailsActiveCommandOnce) { + SessionRig rig; + rig.LoginSuccessfully(); + + SBP2CommandORB first(rig.addressManager, &rig.session, 16); + first.SetFlags(SBP2CommandORB::kImmediate); + int firstCallbackStatus = 99; + int firstCallbackCount = 0; + first.SetCompletionCallback([&](int status, uint8_t) { + ++firstCallbackCount; + firstCallbackStatus = status; + }); + + SBP2CommandORB second(rig.addressManager, &rig.session, 16); + second.SetFlags(SBP2CommandORB::kImmediate); + int secondCallbackStatus = 99; + int secondCallbackCount = 0; + second.SetCompletionCallback([&](int status, uint8_t) { + ++secondCallbackCount; + secondCallbackStatus = status; + }); + + const auto commandBlock = rig.session.CommandBlockAgent(); + const size_t writesBeforeSubmit = rig.bus.WriteCount(); + + ASSERT_TRUE(rig.session.SubmitORB(&first)); + ASSERT_TRUE(rig.session.SubmitORB(&second)); + first.SetFetchAgentWriteRetries(0); + EXPECT_EQ(writesBeforeSubmit + 1, rig.bus.WriteCount()); + + ASSERT_TRUE(rig.bus.CompleteNextWrite(ASFW::Async::AsyncStatus::kTimeout)); + + EXPECT_EQ(1, firstCallbackCount); + EXPECT_EQ(-1, firstCallbackStatus); + EXPECT_EQ(1, secondCallbackCount); + EXPECT_EQ(-1, secondCallbackStatus); + EXPECT_FALSE(first.IsAppended()); + EXPECT_FALSE(second.IsAppended()); + EXPECT_GE(rig.bus.WriteCount(), writesBeforeSubmit + 2); + EXPECT_EQ(commandBlock.addressLo + CommandBlockAgentOffsets::kAgentReset, + rig.bus.WriteAt(rig.bus.WriteCount() - 1).address.addressLo); + EXPECT_TRUE(rig.bus.CompleteNextWrite(ASFW::Async::AsyncStatus::kSuccess)); +} + +} // namespace diff --git a/tests/protocols/SBP2HandlerTests.cpp b/tests/protocols/SBP2HandlerTests.cpp new file mode 100644 index 00000000..94af00c3 --- /dev/null +++ b/tests/protocols/SBP2HandlerTests.cpp @@ -0,0 +1,253 @@ +#include + +#include "ASFWDriver/Discovery/DeviceManager.hpp" +#include "ASFWDriver/Protocols/SBP2/Session/SessionRegistry.hpp" +#include "ASFWDriver/Protocols/SBP2/SBP2WireFormats.hpp" +#include "ASFWDriver/Testing/HostDriverKitStubs.hpp" +#include "tests/mocks/DeferredFireWireBus.hpp" +#include "FakeSessionScheduler.hpp" + +// The host stub's IOUserClientMethodArguments::structureInput is a void*, which +// dynamic_cast (the host OSDynamicCast) cannot accept. Override to static_cast for +// the handler include — the real DriverKit build keeps the RTTI-based macro. +#undef OSDynamicCast +#define OSDynamicCast(type, object) static_cast(object) +#include "ASFWDriver/UserClient/Handlers/SBP2Handler.hpp" +#include "ASFWDriver/UserClient/WireFormats/SBP2CommandWireFormats.hpp" +#undef OSDynamicCast + +#include +#include +#include +#include +#include +#include + +// SBP2Handler host tests — the two user-client ABI tests deferred from PR #19's +// SBP2SessionRegistryTests (FW-56 handoff). They exercise the session-aware handler +// re-threaded onto DICE's SessionRegistry: scalar-output sizing for GetSBP2SessionState +// and the SubmitSBP2Command structure-input ABI hardening. + +namespace { + +using ASFW::Discovery::CfgKey; +using ASFW::Discovery::ConfigROM; +using ASFW::Discovery::DeviceKind; +using ASFW::Discovery::DeviceManager; +using ASFW::Discovery::DeviceRecord; +using ASFW::Discovery::Generation; +using ASFW::Discovery::LifeState; +using ASFW::Discovery::LinkPolicy; +using ASFW::Discovery::RomEntry; +using ASFW::Protocols::SBP2::AddressSpaceManager; +using ASFW::Protocols::SBP2::SessionRegistry; +using ASFW::Testing::FakeSessionScheduler; +using ASFW::Protocols::SBP2::Wire::LoginORB; +using ASFW::Protocols::SBP2::Wire::LoginResponse; +using ASFW::Protocols::SBP2::Wire::StatusBlock; +namespace SBPStatus = ASFW::Protocols::SBP2::Wire::SBPStatus; +namespace UCWire = ASFW::UserClient::Wire; + +constexpr uint32_t kSBP2UnitSpecId = 0x00609E; +constexpr uint32_t kSBP2UnitSwVersion = 0x010483; + +uint64_t ComposeAddress(uint16_t hi, uint32_t lo) { + return (static_cast(hi) << 32) | lo; +} + +uint64_t DecodeAddressFromWritePayload(std::span payload) { + const uint16_t addressHi = + static_cast((static_cast(payload[2]) << 8) | payload[3]); + const uint32_t addressLo = + (static_cast(payload[4]) << 24) | (static_cast(payload[5]) << 16) | + (static_cast(payload[6]) << 8) | static_cast(payload[7]); + return ComposeAddress(addressHi, addressLo); +} + +uint32_t ReadQuadlet(AddressSpaceManager& manager, uint64_t address) { + uint32_t value = 0; + EXPECT_EQ(ASFW::Async::ResponseCode::Complete, manager.ReadQuadlet(address, &value)); + return value; +} + +uint64_t ReadORBAddress(AddressSpaceManager& manager, uint64_t orbAddress, + size_t hiOffset, size_t loOffset) { + const uint32_t hi = OSSwapBigToHostInt32(ReadQuadlet(manager, orbAddress + hiOffset)); + const uint32_t lo = OSSwapBigToHostInt32(ReadQuadlet(manager, orbAddress + loOffset)); + return ComposeAddress(static_cast(hi & 0xFFFFu), lo); +} + +std::vector BuildCommandRequestWire(std::vector cdb, + uint32_t transferLength = 0, + uint8_t direction = 0, + std::vector outgoingPayload = {}, + uint8_t captureSenseData = 0, + uint8_t reserved0 = 0, + uint8_t reserved1 = 0) { + UCWire::SBP2CommandRequestWire header{}; + header.cdbLength = static_cast(cdb.size()); + header.transferLength = transferLength; + header.outgoingLength = static_cast(outgoingPayload.size()); + header.direction = direction; + header.captureSenseData = captureSenseData; + header._reserved[0] = reserved0; + header._reserved[1] = reserved1; + + std::vector serialized(sizeof(header) + cdb.size() + outgoingPayload.size()); + std::memcpy(serialized.data(), &header, sizeof(header)); + size_t offset = sizeof(header); + if (!cdb.empty()) { + std::memcpy(serialized.data() + offset, cdb.data(), cdb.size()); + offset += cdb.size(); + } + if (!outgoingPayload.empty()) { + std::memcpy(serialized.data() + offset, outgoingPayload.data(), outgoingPayload.size()); + } + return serialized; +} + +class HandlerRig { +public: + HandlerRig() : registry(bus, bus, addressManager, deviceManager, scheduler, &queue) { + queue.SetManualDispatchForTesting(true); + bus.SetGeneration(ASFW::FW::Generation{1}); + bus.SetLocalNodeID(ASFW::FW::NodeId{0x2A}); + bus.SetDefaultSpeed(ASFW::FW::FwSpeed::S400); + UpsertDevice(); + } + + void UpsertDevice() { + DeviceRecord record{}; + record.guid = kGuid; + record.vendorId = 0x001122; + record.modelId = 0x334455; + record.kind = DeviceKind::Unknown; + record.vendorName = "Scanner Vendor"; + record.modelName = "Scanner Model"; + record.gen = Generation{1}; + record.nodeId = 0x32; + record.link = LinkPolicy{}; + record.state = LifeState::Ready; + + ConfigROM rom{}; + rom.gen = Generation{1}; + rom.nodeId = record.nodeId; + rom.vendorName = record.vendorName; + rom.modelName = record.modelName; + rom.rootDirMinimal = { + RomEntry{CfgKey::Unit_Spec_Id, kSBP2UnitSpecId, 0, 0}, + RomEntry{CfgKey::Unit_Sw_Version, kSBP2UnitSwVersion, 0, 0}, + RomEntry{CfgKey::Logical_Unit_Number, 0x000002, 0, 0}, + RomEntry{CfgKey::Management_Agent_Offset, 0x000080, 1, 0}, + RomEntry{CfgKey::Unit_Characteristics, 0x080400, 0, 0}, + }; + ASSERT_NE(nullptr, deviceManager.UpsertDevice(record, rom)); + } + + uint64_t CreateSession() { + auto result = registry.CreateSession(Owner(), kGuid, 0); + EXPECT_TRUE(result.has_value()); + return result.value_or(0); + } + + void LoginSuccessfully(uint64_t handle) { + ASSERT_TRUE(registry.StartLogin(Owner(), handle)); + ASSERT_EQ(1u, bus.PendingWriteCount()); + + const auto& loginWrite = bus.WriteAt(0); + const uint64_t loginOrbAddress = DecodeAddressFromWritePayload(loginWrite.data); + const uint64_t loginResponseAddress = + ReadORBAddress(addressManager, loginOrbAddress, + offsetof(LoginORB, loginResponseAddressHi), + offsetof(LoginORB, loginResponseAddressLo)); + sessionStatusAddress = + ReadORBAddress(addressManager, loginOrbAddress, + offsetof(LoginORB, statusFIFOAddressHi), + offsetof(LoginORB, statusFIFOAddressLo)); + + LoginResponse response{}; + response.length = OSSwapHostToBigInt16(LoginResponse::kSize); + response.loginID = OSSwapHostToBigInt16(0x0042); + response.commandBlockAgentAddressHi = OSSwapHostToBigInt32(0x0000'FFFFu); + response.commandBlockAgentAddressLo = OSSwapHostToBigInt32(0x0020'0000u); + response.reconnectHold = OSSwapHostToBigInt16(1); + + ASSERT_TRUE(bus.CompleteNextWrite(ASFW::Async::AsyncStatus::kSuccess)); + addressManager.ApplyRemoteWrite( + loginResponseAddress, + std::span{reinterpret_cast(&response), sizeof(response)}); + + StatusBlock status{}; + status.details = 0; + status.sbpStatus = SBPStatus::kNoAdditionalInfo; + addressManager.ApplyRemoteWrite( + sessionStatusAddress, + std::span{reinterpret_cast(&status), sizeof(status)}); + } + + static constexpr uint64_t kGuid = 0x0003DB0001DDDDA1ULL; + static void* Owner() noexcept { return reinterpret_cast(0xCAFE); } + + ASFW::Async::Testing::DeferredFireWireBus bus; + FakeSessionScheduler scheduler; + AddressSpaceManager addressManager{nullptr}; + DeviceManager deviceManager; + IODispatchQueue queue; + SessionRegistry registry; + uint64_t sessionStatusAddress{0}; +}; + +TEST(SBP2HandlerTests, HandlerRejectsMissingOrShortSessionStateOutput) { + HandlerRig rig; + const uint64_t handle = rig.CreateSession(); + rig.LoginSuccessfully(handle); + + ASFW::UserClient::SBP2Handler handler(nullptr, &rig.registry); + uint64_t scalarInput[] = {handle}; + + IOUserClientMethodArguments args{}; + args.scalarInput = scalarInput; + args.scalarInputCount = 1; + EXPECT_EQ(kIOReturnBadArgument, handler.GetSBP2SessionState(&args, HandlerRig::Owner())); + + uint64_t shortOutput[4]{}; + args.scalarOutput = shortOutput; + args.scalarOutputCount = 4; + EXPECT_EQ(kIOReturnBadArgument, handler.GetSBP2SessionState(&args, HandlerRig::Owner())); + + uint64_t output[5]{}; + args.scalarOutput = output; + args.scalarOutputCount = 5; + EXPECT_EQ(kIOReturnSuccess, handler.GetSBP2SessionState(&args, HandlerRig::Owner())); + EXPECT_EQ(5u, args.scalarOutputCount); +} + +TEST(SBP2HandlerTests, HandlerHardensCommandABIInputsBeforeSubmission) { + HandlerRig rig; + const uint64_t handle = rig.CreateSession(); + rig.LoginSuccessfully(handle); + + ASFW::UserClient::SBP2Handler handler(nullptr, &rig.registry); + uint64_t scalarInput[] = {handle}; + + auto submit = [&](const std::vector& serialized) { + std::unique_ptr input( + OSData::withBytes(serialized.data(), static_cast(serialized.size()))); + IOUserClientMethodArguments args{}; + args.scalarInput = scalarInput; + args.scalarInputCount = 1; + args.structureInput = input.get(); + return handler.SubmitSBP2Command(&args, HandlerRig::Owner()); + }; + + EXPECT_EQ(kIOReturnBadArgument, submit(BuildCommandRequestWire(std::vector(17, 0x00)))); + EXPECT_EQ(kIOReturnBadArgument, submit(BuildCommandRequestWire({0x00}, 0, 0, {}, 2))); + EXPECT_EQ(kIOReturnBadArgument, submit(BuildCommandRequestWire({0x00}, 0, 0, {}, 0, 1))); + EXPECT_EQ(kIOReturnBadArgument, + submit(BuildCommandRequestWire({0x2A}, UCWire::kSBP2CommandMaxTransferLength + 1, 1))); + EXPECT_EQ(kIOReturnBadArgument, submit(BuildCommandRequestWire({0x2A}, 4, 2, {0x01, 0x02}))); + EXPECT_EQ(kIOReturnBadArgument, submit(BuildCommandRequestWire({0x2A}, 4, 1, {0x01}))); + EXPECT_EQ(kIOReturnSuccess, submit(BuildCommandRequestWire({0x00}))); +} + +} // namespace diff --git a/tests/protocols/SBP2ORBTests.cpp b/tests/protocols/SBP2ORBTests.cpp index 15fe7f19..9e5b6f43 100644 --- a/tests/protocols/SBP2ORBTests.cpp +++ b/tests/protocols/SBP2ORBTests.cpp @@ -253,7 +253,7 @@ TEST(SBP2ORBTests, CommandORBDirectDescriptorUsesFullBusNodeId) { descriptor.isDirect = true; orb.SetDataDescriptor(descriptor); - orb.PrepareForExecution(0x21, ASFW::FW::FwSpeed::S400, 6); + ASSERT_EQ(kIOReturnSuccess, orb.PrepareForExecution(0x21, ASFW::FW::FwSpeed::S400, 6)); const auto orbAddress = orb.GetORBAddress(); const uint64_t packedAddress = ComposeAddress(orbAddress.addressHi, orbAddress.addressLo); @@ -289,4 +289,37 @@ TEST(SBP2ORBTests, ManagementORBDestructionInvalidatesPendingTimeout) { EXPECT_EQ(0, completionCount); } +// --- CommandORB configuration hardening (FW-56 step 1) -------------------- + +TEST(SBP2ORBTests, CommandORBIsValidAfterSuccessfulConstruction) { + AddressSpaceManager manager{nullptr}; + SBP2CommandORB orb(manager, reinterpret_cast(0x70), 16); + EXPECT_TRUE(orb.IsValid()); +} + +TEST(SBP2ORBTests, SetCommandBlockRejectsOversizedCdb) { + AddressSpaceManager manager{nullptr}; + SBP2CommandORB orb(manager, reinterpret_cast(0x71), 16); + + const std::array exact{}; + EXPECT_TRUE(orb.SetCommandBlock(std::span(exact.data(), exact.size()))); + + const std::array shorter{}; + EXPECT_TRUE(orb.SetCommandBlock(std::span(shorter.data(), shorter.size()))); + + const std::array oversized{}; + EXPECT_FALSE(orb.SetCommandBlock(std::span(oversized.data(), oversized.size()))); +} + +TEST(SBP2ORBTests, ChainAndDummyOpsReturnSuccessOnValidORB) { + AddressSpaceManager manager{nullptr}; + SBP2CommandORB orb(manager, reinterpret_cast(0x72), 16); + ASSERT_TRUE(orb.IsValid()); + + EXPECT_EQ(kIOReturnSuccess, orb.SetNextORBAddress(0x8000'0000u, 0x0000'0000u)); + EXPECT_EQ(kIOReturnSuccess, orb.SetToDummy()); + EXPECT_EQ(kIOReturnSuccess, + orb.PrepareForExecution(0x21, ASFW::FW::FwSpeed::S400, 6)); +} + } // namespace diff --git a/tests/protocols/SessionRegistryTests.cpp b/tests/protocols/SessionRegistryTests.cpp new file mode 100644 index 00000000..21bce16b --- /dev/null +++ b/tests/protocols/SessionRegistryTests.cpp @@ -0,0 +1,819 @@ +#include + +#include "ASFWDriver/Discovery/DeviceManager.hpp" +#include "ASFWDriver/Protocols/SBP2/SBP2ManagementORB.hpp" +#include "ASFWDriver/Protocols/SBP2/Session/SessionRegistry.hpp" +#include "ASFWDriver/Protocols/SBP2/SCSICommandSet.hpp" +#include "ASFWDriver/Protocols/SBP2/SBP2WireFormats.hpp" +#include "ASFWDriver/Testing/HostDriverKitStubs.hpp" +#include "tests/mocks/DeferredFireWireBus.hpp" +#include "FakeSessionScheduler.hpp" + +#include +#include +#include +#include +#include + +// SessionRegistry host tests — ported from PR #19's SBP2SessionRegistryTests, +// adapted to the decomposed DICE component: `SessionRegistry` (identity/lifecycle) +// delegates the command plane to a per-record CommandExecutor, and LoginSession +// timers run on an injected ISessionScheduler. The two SBP2Handler-dependent tests +// from #19 move to FW-57 (the session-aware user-client handler does not exist on +// DICE yet). Byte-order uses the global OSSwap* intrinsics (DICE dropped the +// Wire::ToBE*/FromBE* helpers). + +namespace { + +constexpr uint32_t kSBP2UnitSpecId = 0x00609E; +constexpr uint32_t kSBP2UnitSwVersion = 0x010483; + +using ASFW::Discovery::CfgKey; +using ASFW::Discovery::ConfigROM; +using ASFW::Discovery::DeviceKind; +using ASFW::Discovery::DeviceManager; +using ASFW::Discovery::DeviceRecord; +using ASFW::Discovery::Generation; +using ASFW::Discovery::LifeState; +using ASFW::Discovery::LinkPolicy; +using ASFW::Discovery::RomEntry; +using ASFW::Protocols::SBP2::AddressSpaceManager; +using ASFW::Protocols::SBP2::LoginSession; +using ASFW::Protocols::SBP2::SBP2ManagementORB; +using ASFW::Protocols::SBP2::SessionRegistry; +using ASFW::Testing::FakeSessionScheduler; +namespace SCSI = ASFW::Protocols::SBP2::SCSI; +using ASFW::Protocols::SBP2::Wire::LoginORB; +using ASFW::Protocols::SBP2::Wire::LoginResponse; +using ASFW::Protocols::SBP2::Wire::NormalORB; +using ASFW::Protocols::SBP2::Wire::ReconnectORB; +using ASFW::Protocols::SBP2::Wire::StatusBlock; +using ASFW::Protocols::SBP2::Wire::TaskManagementORB; +namespace SBPStatus = ASFW::Protocols::SBP2::Wire::SBPStatus; +// OSSwapHostToBigInt*/OSSwapBigToHostInt* are global byte-order intrinsics. + +uint64_t ComposeAddress(uint16_t hi, uint32_t lo) { + return (static_cast(hi) << 32) | lo; +} + +uint64_t DecodeAddressFromWritePayload(std::span payload) { + const uint16_t addressHi = + static_cast((static_cast(payload[2]) << 8) | payload[3]); + const uint32_t addressLo = + (static_cast(payload[4]) << 24) | + (static_cast(payload[5]) << 16) | + (static_cast(payload[6]) << 8) | + static_cast(payload[7]); + return ComposeAddress(addressHi, addressLo); +} + +uint32_t ReadQuadlet(AddressSpaceManager& manager, uint64_t address) { + uint32_t value = 0; + EXPECT_EQ(ASFW::Async::ResponseCode::Complete, manager.ReadQuadlet(address, &value)); + return value; +} + +uint64_t ReadORBAddress(AddressSpaceManager& manager, + uint64_t orbAddress, + size_t hiOffset, + size_t loOffset) { + const uint32_t hi = OSSwapBigToHostInt32(ReadQuadlet(manager, orbAddress + hiOffset)); + const uint32_t lo = OSSwapBigToHostInt32(ReadQuadlet(manager, orbAddress + loOffset)); + return ComposeAddress(static_cast(hi & 0xFFFFu), lo); +} + +uint64_t ReadDataBufferAddress(AddressSpaceManager& manager, uint64_t orbAddress) { + const uint32_t hi = OSSwapBigToHostInt32( + ReadQuadlet(manager, orbAddress + offsetof(NormalORB, dataDescriptorHi))); + const uint32_t lo = OSSwapBigToHostInt32( + ReadQuadlet(manager, orbAddress + offsetof(NormalORB, dataDescriptorLo))); + return ComposeAddress(static_cast(hi & 0xFFFFu), lo); +} + +uint16_t ReadTaskManagementFunction(AddressSpaceManager& manager, uint64_t orbAddress) { + const uint32_t optionsAndLoginID = + OSSwapBigToHostInt32(ReadQuadlet(manager, orbAddress + offsetof(TaskManagementORB, options))); + return static_cast((optionsAndLoginID >> 16) & 0x000Fu); +} + +uint16_t ReadTaskManagementLoginID(AddressSpaceManager& manager, uint64_t orbAddress) { + const uint32_t optionsAndLoginID = + OSSwapBigToHostInt32(ReadQuadlet(manager, orbAddress + offsetof(TaskManagementORB, options))); + return static_cast(optionsAndLoginID & 0xFFFFu); +} + +uint64_t ReadTaskManagementStatusAddress(AddressSpaceManager& manager, uint64_t orbAddress) { + return ReadORBAddress(manager, orbAddress, + offsetof(TaskManagementORB, statusFIFOAddressHi), + offsetof(TaskManagementORB, statusFIFOAddressLo)); +} + +void CompleteTaskManagementStatus(AddressSpaceManager& manager, + uint64_t statusAddress, + uint64_t orbAddress, + uint8_t sbpStatus = SBPStatus::kNoAdditionalInfo) { + StatusBlock status{}; + status.details = 0; + status.sbpStatus = sbpStatus; + status.orbOffsetHi = OSSwapHostToBigInt16(static_cast((orbAddress >> 32) & 0xFFFFu)); + status.orbOffsetLo = OSSwapHostToBigInt32(static_cast(orbAddress & 0xFFFF'FFFFu)); + manager.ApplyRemoteWrite( + statusAddress, + std::span{reinterpret_cast(&status), sizeof(status)}); +} + +class SessionRegistryRig { +public: + explicit SessionRegistryRig(uint32_t unitCharacteristics = 0x080400) + : registry(bus, bus, addressManager, deviceManager, scheduler, &queue) { + queue.SetManualDispatchForTesting(true); + ASFW::Testing::SetHostMonotonicClockForTesting([this]() { return nowNs; }); + + bus.SetGeneration(ASFW::FW::Generation{1}); + bus.SetLocalNodeID(ASFW::FW::NodeId{0x2A}); + bus.SetDefaultSpeed(ASFW::FW::FwSpeed::S400); + + UpsertDevice(Generation{1}, 0x32, unitCharacteristics); + } + + ~SessionRegistryRig() { + ASFW::Testing::ResetHostMonotonicClockForTesting(); + } + + void UpsertDevice(Generation generation, uint8_t nodeId, uint32_t unitCharacteristics = 0x080400) { + DeviceRecord record{}; + record.guid = kGuid; + record.vendorId = 0x001122; + record.modelId = 0x334455; + record.kind = DeviceKind::Unknown; + record.vendorName = "Scanner Vendor"; + record.modelName = "Scanner Model"; + record.gen = generation; + record.nodeId = nodeId; + record.link = LinkPolicy{}; + record.state = LifeState::Ready; + + ConfigROM rom{}; + rom.gen = generation; + rom.nodeId = record.nodeId; + rom.vendorName = record.vendorName; + rom.modelName = record.modelName; + rom.rootDirMinimal = { + RomEntry{CfgKey::Unit_Spec_Id, kSBP2UnitSpecId, 0, 0}, + RomEntry{CfgKey::Unit_Sw_Version, kSBP2UnitSwVersion, 0, 0}, + RomEntry{CfgKey::Logical_Unit_Number, 0x000002, 0, 0}, + RomEntry{CfgKey::Management_Agent_Offset, 0x000080, 1, 0}, + RomEntry{CfgKey::Unit_Characteristics, unitCharacteristics, 0, 0}, + }; + + auto device = deviceManager.UpsertDevice(record, rom); + EXPECT_NE(nullptr, device); + if (!device) { + return; + } + EXPECT_FALSE(device->GetUnits().empty()); + } + + void AdvanceMs(uint64_t milliseconds) { + nowNs += milliseconds * 1'000'000ULL; + scheduler.Advance(milliseconds * 1'000'000ULL); + while (queue.DrainReadyForTesting() > 0U) { + } + } + + uint64_t CreateSession() { + auto result = registry.CreateSession(Owner(), kGuid, 0); + EXPECT_TRUE(result.has_value()); + return result.value_or(0); + } + + void LoginSuccessfully(uint64_t handle, + uint16_t loginId = 0x0042, + uint32_t commandBlockAgentLo = 0x0020'0000) { + ASSERT_TRUE(registry.StartLogin(Owner(), handle)); + ASSERT_EQ(1u, bus.PendingWriteCount()); + + const auto& loginWrite = bus.WriteAt(0); + const uint64_t loginOrbAddress = DecodeAddressFromWritePayload(loginWrite.data); + const uint64_t loginResponseAddress = + ReadORBAddress(addressManager, loginOrbAddress, + offsetof(LoginORB, loginResponseAddressHi), + offsetof(LoginORB, loginResponseAddressLo)); + sessionStatusAddress = + ReadORBAddress(addressManager, loginOrbAddress, + offsetof(LoginORB, statusFIFOAddressHi), + offsetof(LoginORB, statusFIFOAddressLo)); + + LoginResponse response{}; + response.length = OSSwapHostToBigInt16(LoginResponse::kSize); + response.loginID = OSSwapHostToBigInt16(loginId); + response.commandBlockAgentAddressHi = OSSwapHostToBigInt32(0x0000'FFFFu); + response.commandBlockAgentAddressLo = OSSwapHostToBigInt32(commandBlockAgentLo); + response.reconnectHold = OSSwapHostToBigInt16(1); + + ASSERT_TRUE(bus.CompleteNextWrite(ASFW::Async::AsyncStatus::kSuccess)); + addressManager.ApplyRemoteWrite( + loginResponseAddress, + std::span{reinterpret_cast(&response), sizeof(response)}); + + StatusBlock status{}; + status.details = 0; + status.sbpStatus = SBPStatus::kNoAdditionalInfo; + addressManager.ApplyRemoteWrite( + sessionStatusAddress, + std::span{reinterpret_cast(&status), sizeof(status)}); + } + + static constexpr uint64_t kGuid = 0x0003DB0001DDDDA1ULL; + static void* Owner() noexcept { return reinterpret_cast(0xCAFE); } + static void* OtherOwner() noexcept { return reinterpret_cast(0xBEEF); } + + ASFW::Async::Testing::DeferredFireWireBus bus; + FakeSessionScheduler scheduler; + AddressSpaceManager addressManager{nullptr}; + DeviceManager deviceManager; + IODispatchQueue queue; + SessionRegistry registry; + uint64_t nowNs{0}; + uint64_t sessionStatusAddress{0}; +}; + +TEST(SessionRegistryTests, BuildStandardCommandHelpersUseExpectedOpCodes) { + const auto inquiry = SCSI::BuildInquiryRequest(64); + ASSERT_EQ(6u, inquiry.cdb.size()); + EXPECT_EQ(0x12, inquiry.cdb[0]); + EXPECT_EQ(64u, inquiry.transferLength); + + const auto tur = SCSI::BuildTestUnitReadyRequest(); + ASSERT_EQ(6u, tur.cdb.size()); + EXPECT_EQ(0x00, tur.cdb[0]); + EXPECT_EQ(SCSI::DataDirection::None, tur.direction); + + const auto sense = SCSI::BuildRequestSenseRequest(18); + ASSERT_EQ(6u, sense.cdb.size()); + EXPECT_EQ(0x03, sense.cdb[0]); + EXPECT_EQ(SCSI::DataDirection::FromTarget, sense.direction); +} + +TEST(SessionRegistryTests, SubmitRequestSenseCapturesPayloadAndSenseData) { + SessionRegistryRig rig; + const uint64_t handle = rig.CreateSession(); + rig.LoginSuccessfully(handle); + + const auto request = SCSI::BuildRequestSenseRequest(18); + const size_t pendingBeforeSubmit = rig.bus.PendingWriteCount(); + ASSERT_TRUE(rig.registry.SubmitCommand(SessionRegistryRig::Owner(), handle, request)); + ASSERT_EQ(pendingBeforeSubmit + 1U, rig.bus.PendingWriteCount()); + + const auto& write = rig.bus.WriteAt(rig.bus.WriteCount() - 1); + const uint64_t commandOrbAddress = DecodeAddressFromWritePayload(write.data); + const uint64_t dataBufferAddress = ReadDataBufferAddress(rig.addressManager, commandOrbAddress); + + const std::vector sensePayload{ + 0x70, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x0A, + 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, + 0x3A, 0x01 + }; + rig.addressManager.ApplyRemoteWrite(dataBufferAddress, sensePayload); + + StatusBlock status{}; + status.details = 0; + status.sbpStatus = SBPStatus::kNoAdditionalInfo; + status.orbOffsetHi = OSSwapHostToBigInt16(static_cast((commandOrbAddress >> 32) & 0xFFFFu)); + status.orbOffsetLo = OSSwapHostToBigInt32(static_cast(commandOrbAddress & 0xFFFF'FFFFu)); + rig.addressManager.ApplyRemoteWrite( + rig.sessionStatusAddress, + std::span{reinterpret_cast(&status), sizeof(status)}); + + auto result = rig.registry.GetCommandResult(SessionRegistryRig::Owner(), handle); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(0, result->transportStatus); + EXPECT_EQ(SBPStatus::kNoAdditionalInfo, result->sbpStatus); + EXPECT_EQ(sensePayload, result->payload); + EXPECT_EQ(sensePayload, result->senseData); +} + +TEST(SessionRegistryTests, InquiryFailureResultPreservesSBPStatus) { + SessionRegistryRig rig; + const uint64_t handle = rig.CreateSession(); + rig.LoginSuccessfully(handle); + + ASSERT_TRUE(rig.registry.SubmitInquiry(SessionRegistryRig::Owner(), handle, 36)); + const auto& write = rig.bus.WriteAt(rig.bus.WriteCount() - 1); + const uint64_t commandOrbAddress = DecodeAddressFromWritePayload(write.data); + + StatusBlock status{}; + status.details = 0; + status.sbpStatus = SBPStatus::kRequestAborted; + status.orbOffsetHi = OSSwapHostToBigInt16(static_cast((commandOrbAddress >> 32) & 0xFFFFu)); + status.orbOffsetLo = OSSwapHostToBigInt32(static_cast(commandOrbAddress & 0xFFFF'FFFFu)); + rig.addressManager.ApplyRemoteWrite( + rig.sessionStatusAddress, + std::span{reinterpret_cast(&status), sizeof(status)}); + + auto result = rig.registry.GetInquiryResult(SessionRegistryRig::Owner(), handle); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(0, result->transportStatus); + EXPECT_EQ(SBPStatus::kRequestAborted, result->sbpStatus); + EXPECT_TRUE(result->payload.empty()); + EXPECT_FALSE(rig.registry.GetInquiryResult(SessionRegistryRig::Owner(), handle).has_value()); +} + +TEST(SessionRegistryTests, ActiveCommandFailsOnceAfterFetchAgentRetryExhaustion) { + SessionRegistryRig rig; + const uint64_t handle = rig.CreateSession(); + rig.LoginSuccessfully(handle); + + auto* session = rig.registry.GetSessionForTesting(handle); + ASSERT_NE(nullptr, session); + + session->SetFetchAgentWriteRetriesForTesting(0); + const auto request = SCSI::BuildTestUnitReadyRequest(); + ASSERT_TRUE(rig.registry.SubmitCommand(SessionRegistryRig::Owner(), handle, request)); + const auto fetchAgentWrite = rig.bus.WriteAt(rig.bus.WriteCount() - 1); + ASSERT_EQ(8u, fetchAgentWrite.data.size()); + const uint64_t commandOrbAddress = DecodeAddressFromWritePayload(fetchAgentWrite.data); + + ASSERT_TRUE(rig.bus.CompleteWrite(fetchAgentWrite.handle, ASFW::Async::AsyncStatus::kTimeout)); + rig.AdvanceMs(1000); + + const auto commandResult = rig.registry.GetCommandResult(SessionRegistryRig::Owner(), handle); + ASSERT_TRUE(commandResult.has_value()); + EXPECT_EQ(-1, commandResult->transportStatus); + EXPECT_EQ(SBPStatus::kUnspecifiedError, commandResult->sbpStatus); + + const auto stateAfterFailure = rig.registry.GetSessionState(SessionRegistryRig::Owner(), handle); + ASSERT_TRUE(stateAfterFailure.has_value()); + EXPECT_EQ(-1, stateAfterFailure->lastError); + + ASSERT_GT(rig.bus.WriteCount(), 0u); + const auto agentResetWrite = rig.bus.WriteAt(rig.bus.WriteCount() - 1); + EXPECT_EQ(4u, agentResetWrite.data.size()); + + ASSERT_TRUE(rig.bus.CompleteWrite(agentResetWrite.handle, ASFW::Async::AsyncStatus::kSuccess)); + + uint32_t ignored = 0; + EXPECT_EQ(ASFW::Async::ResponseCode::AddressError, + rig.addressManager.ReadQuadlet(commandOrbAddress, &ignored)); +} + +TEST(SessionRegistryTests, RejectsSessionOperationsFromNonOwningClient) { + SessionRegistryRig rig; + const uint64_t handle = rig.CreateSession(); + + EXPECT_FALSE(rig.registry.StartLogin(SessionRegistryRig::OtherOwner(), handle)); + EXPECT_EQ(0u, rig.bus.PendingWriteCount()); + + rig.LoginSuccessfully(handle); + + EXPECT_FALSE(rig.registry.GetSessionState(SessionRegistryRig::OtherOwner(), handle).has_value()); + EXPECT_FALSE(rig.registry.SubmitCommand(SessionRegistryRig::OtherOwner(), handle, + SCSI::BuildTestUnitReadyRequest())); + EXPECT_FALSE(rig.registry.SubmitInquiry(SessionRegistryRig::OtherOwner(), handle, 36)); + EXPECT_FALSE(rig.registry.SubmitTaskManagement( + SessionRegistryRig::OtherOwner(), handle, SBP2ManagementORB::Function::LogicalUnitReset)); + EXPECT_FALSE(rig.registry.ReleaseSession(SessionRegistryRig::OtherOwner(), handle)); + + ASSERT_TRUE(rig.registry.SubmitCommand(SessionRegistryRig::Owner(), handle, + SCSI::BuildTestUnitReadyRequest())); + const auto& commandWrite = rig.bus.WriteAt(rig.bus.WriteCount() - 1); + const uint64_t commandOrbAddress = DecodeAddressFromWritePayload(commandWrite.data); + StatusBlock commandStatus{}; + commandStatus.details = 0; + commandStatus.sbpStatus = SBPStatus::kNoAdditionalInfo; + commandStatus.orbOffsetHi = OSSwapHostToBigInt16(static_cast((commandOrbAddress >> 32) & 0xFFFFu)); + commandStatus.orbOffsetLo = OSSwapHostToBigInt32(static_cast(commandOrbAddress & 0xFFFF'FFFFu)); + rig.addressManager.ApplyRemoteWrite( + rig.sessionStatusAddress, + std::span{reinterpret_cast(&commandStatus), sizeof(commandStatus)}); + + EXPECT_FALSE(rig.registry.GetCommandResult(SessionRegistryRig::OtherOwner(), handle).has_value()); + EXPECT_TRUE(rig.registry.GetCommandResult(SessionRegistryRig::Owner(), handle).has_value()); + + ASSERT_TRUE(rig.registry.SubmitInquiry(SessionRegistryRig::Owner(), handle, 36)); + const auto& inquiryWrite = rig.bus.WriteAt(rig.bus.WriteCount() - 1); + const uint64_t inquiryOrbAddress = DecodeAddressFromWritePayload(inquiryWrite.data); + StatusBlock inquiryStatus{}; + inquiryStatus.details = 0; + inquiryStatus.sbpStatus = SBPStatus::kRequestAborted; + inquiryStatus.orbOffsetHi = OSSwapHostToBigInt16(static_cast((inquiryOrbAddress >> 32) & 0xFFFFu)); + inquiryStatus.orbOffsetLo = OSSwapHostToBigInt32(static_cast(inquiryOrbAddress & 0xFFFF'FFFFu)); + rig.addressManager.ApplyRemoteWrite( + rig.sessionStatusAddress, + std::span{reinterpret_cast(&inquiryStatus), sizeof(inquiryStatus)}); + + EXPECT_FALSE(rig.registry.GetInquiryResult(SessionRegistryRig::OtherOwner(), handle).has_value()); + EXPECT_TRUE(rig.registry.GetInquiryResult(SessionRegistryRig::Owner(), handle).has_value()); + EXPECT_TRUE(rig.registry.GetSessionState(SessionRegistryRig::Owner(), handle).has_value()); +} + +TEST(SessionRegistryTests, SubmitTaskManagementWritesLogicalUnitResetORB) { + SessionRegistryRig rig; + const uint64_t handle = rig.CreateSession(); + rig.LoginSuccessfully(handle); + + const size_t writesBeforeSubmit = rig.bus.WriteCount(); + ASSERT_TRUE(rig.registry.SubmitTaskManagement( + SessionRegistryRig::Owner(), handle, SBP2ManagementORB::Function::LogicalUnitReset)); + + ASSERT_EQ(writesBeforeSubmit + 1U, rig.bus.WriteCount()); + const auto& write = rig.bus.WriteAt(writesBeforeSubmit); + const uint64_t managementOrbAddress = DecodeAddressFromWritePayload(write.data); + + EXPECT_EQ(0x0Eu, ReadTaskManagementFunction(rig.addressManager, managementOrbAddress)); + EXPECT_EQ(0x0042u, ReadTaskManagementLoginID(rig.addressManager, managementOrbAddress)); +} + +TEST(SessionRegistryTests, TaskManagementSuccessClearsActiveCommandTracking) { + SessionRegistryRig rig; + const uint64_t handle = rig.CreateSession(); + rig.LoginSuccessfully(handle); + + const auto request = SCSI::BuildTestUnitReadyRequest(); + ASSERT_TRUE(rig.registry.SubmitCommand(SessionRegistryRig::Owner(), handle, request)); + ASSERT_FALSE(rig.registry.GetCommandResult(SessionRegistryRig::Owner(), handle).has_value()); + ASSERT_GT(rig.bus.WriteCount(), 0U); + const auto fetchAgentWrite = rig.bus.WriteAt(rig.bus.WriteCount() - 1); + ASSERT_EQ(8U, fetchAgentWrite.data.size()); + + const size_t writesBeforeTaskManagement = rig.bus.WriteCount(); + ASSERT_TRUE(rig.registry.SubmitTaskManagement( + SessionRegistryRig::Owner(), handle, SBP2ManagementORB::Function::AbortTaskSet)); + const auto& taskWrite = rig.bus.WriteAt(writesBeforeTaskManagement); + const uint64_t taskOrbAddress = DecodeAddressFromWritePayload(taskWrite.data); + const uint64_t taskStatusAddress = + ReadTaskManagementStatusAddress(rig.addressManager, taskOrbAddress); + + ASSERT_TRUE(rig.bus.CompleteWrite(taskWrite.handle, ASFW::Async::AsyncStatus::kSuccess)); + CompleteTaskManagementStatus(rig.addressManager, taskStatusAddress, taskOrbAddress); + + EXPECT_FALSE(rig.registry.GetCommandResult(SessionRegistryRig::Owner(), handle).has_value()); + EXPECT_FALSE(rig.bus.CompleteWrite(fetchAgentWrite.handle, ASFW::Async::AsyncStatus::kSuccess)); + EXPECT_TRUE(rig.registry.SubmitCommand(SessionRegistryRig::Owner(), handle, request)); +} + +TEST(SessionRegistryTests, SubmitTaskManagementRejectsInvalidFunction) { + SessionRegistryRig rig; + const uint64_t handle = rig.CreateSession(); + rig.LoginSuccessfully(handle); + + EXPECT_FALSE(rig.registry.SubmitTaskManagement( + SessionRegistryRig::Owner(), handle, SBP2ManagementORB::Function::QueryLogins)); +} + +TEST(SessionRegistryTests, MissingDiscoverySuspendsDeviceAndReconnectWaitsForResume) { + SessionRegistryRig rig; + const uint64_t handle = rig.CreateSession(); + rig.LoginSuccessfully(handle); + + rig.registry.OnBusReset(2); + const auto suspendedState = rig.registry.GetSessionState(SessionRegistryRig::Owner(), handle); + ASSERT_TRUE(suspendedState.has_value()); + EXPECT_EQ(ASFW::Protocols::SBP2::LoginState::Suspended, suspendedState->loginState); + + rig.deviceManager.MarkDeviceLost(SessionRegistryRig::kGuid); + const auto suspendedDevice = rig.deviceManager.GetDeviceByGUID(SessionRegistryRig::kGuid); + ASSERT_NE(nullptr, suspendedDevice); + EXPECT_TRUE(suspendedDevice->IsSuspended()); + + const size_t writesBeforeMissingRefresh = rig.bus.WriteCount(); + rig.registry.RefreshTargets(Generation{2}); + EXPECT_EQ(writesBeforeMissingRefresh, rig.bus.WriteCount()); + + rig.bus.SetGeneration(ASFW::FW::Generation{2}); + rig.UpsertDevice(Generation{2}, 0x33); + const auto resumedDevice = rig.deviceManager.GetDeviceByGUID(SessionRegistryRig::kGuid); + ASSERT_NE(nullptr, resumedDevice); + EXPECT_TRUE(resumedDevice->IsReady()); + EXPECT_EQ(0x33u, resumedDevice->GetNodeID()); + + rig.registry.RefreshTargets(Generation{2}); + ASSERT_EQ(writesBeforeMissingRefresh + 1U, rig.bus.WriteCount()); + const auto& reconnectWrite = rig.bus.WriteAt(writesBeforeMissingRefresh); + EXPECT_EQ(0x33u, reconnectWrite.nodeId.value); + EXPECT_EQ(8U, reconnectWrite.data.size()); + + const uint64_t reconnectOrbAddress = DecodeAddressFromWritePayload(reconnectWrite.data); + const uint64_t reconnectStatusAddress = + ReadORBAddress(rig.addressManager, reconnectOrbAddress, + offsetof(ReconnectORB, statusFIFOAddressHi), + offsetof(ReconnectORB, statusFIFOAddressLo)); + ASSERT_TRUE(rig.bus.CompleteWrite(reconnectWrite.handle, ASFW::Async::AsyncStatus::kSuccess)); + + StatusBlock reconnectStatus{}; + reconnectStatus.details = 0; + reconnectStatus.sbpStatus = SBPStatus::kNoAdditionalInfo; + rig.addressManager.ApplyRemoteWrite( + reconnectStatusAddress, + std::span{reinterpret_cast(&reconnectStatus), sizeof(reconnectStatus)}); + + ASSERT_GE(rig.bus.WriteCount(), writesBeforeMissingRefresh + 2U); + const auto& busyTimeoutWrite = rig.bus.WriteAt(writesBeforeMissingRefresh + 1U); + EXPECT_EQ(0x33u, busyTimeoutWrite.nodeId.value); + EXPECT_EQ(0x33u, busyTimeoutWrite.address.nodeID); + + const size_t writesBeforeCommand = rig.bus.WriteCount(); + ASSERT_TRUE(rig.registry.SubmitCommand(SessionRegistryRig::Owner(), handle, + SCSI::BuildTestUnitReadyRequest())); + ASSERT_EQ(writesBeforeCommand + 1U, rig.bus.WriteCount()); + const auto& fetchAgentWrite = rig.bus.WriteAt(writesBeforeCommand); + EXPECT_EQ(0x33u, fetchAgentWrite.nodeId.value); + EXPECT_EQ(0x33u, fetchAgentWrite.address.nodeID); +} + +TEST(SessionRegistryTests, RepeatedMissingDiscoveryTerminatesSuspendedSBP2Device) { + SessionRegistryRig rig; + const uint64_t handle = rig.CreateSession(); + rig.LoginSuccessfully(handle); + + rig.registry.OnBusReset(2); + rig.deviceManager.MarkDeviceLost(SessionRegistryRig::kGuid); + const auto suspendedDevice = rig.deviceManager.GetDeviceByGUID(SessionRegistryRig::kGuid); + ASSERT_NE(nullptr, suspendedDevice); + ASSERT_TRUE(suspendedDevice->IsSuspended()); + + rig.deviceManager.MarkDeviceLost(SessionRegistryRig::kGuid); + EXPECT_EQ(nullptr, rig.deviceManager.GetDeviceByGUID(SessionRegistryRig::kGuid)); + + const size_t writesBeforeRefresh = rig.bus.WriteCount(); + rig.registry.RefreshTargets(Generation{2}); + EXPECT_EQ(writesBeforeRefresh, rig.bus.WriteCount()); +} + +TEST(SessionRegistryTests, ReleaseOwnerRetainsSessionUntilLogoutStatusArrives) { + SessionRegistryRig rig; + const uint64_t handle = rig.CreateSession(); + rig.LoginSuccessfully(handle); + + std::weak_ptr weakSession = rig.registry.GetSessionWeakForTesting(handle); + ASSERT_FALSE(weakSession.expired()); + + const size_t writesBeforeRelease = rig.bus.WriteCount(); + rig.registry.ReleaseOwner(SessionRegistryRig::Owner()); + + EXPECT_FALSE(weakSession.expired()); + ASSERT_EQ(writesBeforeRelease + 1U, rig.bus.WriteCount()); + EXPECT_FALSE(rig.registry.GetSessionState(SessionRegistryRig::Owner(), handle).has_value()); + + const auto& logoutWrite = rig.bus.WriteAt(writesBeforeRelease); + EXPECT_TRUE(rig.bus.CompleteWrite(logoutWrite.handle, ASFW::Async::AsyncStatus::kSuccess)); + EXPECT_FALSE(weakSession.expired()); + + StatusBlock status{}; + status.details = 0; + status.sbpStatus = SBPStatus::kNoAdditionalInfo; + rig.addressManager.ApplyRemoteWrite( + rig.sessionStatusAddress, + std::span{reinterpret_cast(&status), sizeof(status)}); + + EXPECT_TRUE(weakSession.expired()); + EXPECT_FALSE(rig.registry.GetSessionState(SessionRegistryRig::Owner(), handle).has_value()); +} + +TEST(SessionRegistryTests, ReleaseOwnerRetainsSessionUntilLogoutTimeout) { + SessionRegistryRig rig; + const uint64_t handle = rig.CreateSession(); + rig.LoginSuccessfully(handle); + + std::weak_ptr weakSession = rig.registry.GetSessionWeakForTesting(handle); + ASSERT_FALSE(weakSession.expired()); + + const size_t writesBeforeRelease = rig.bus.WriteCount(); + rig.registry.ReleaseOwner(SessionRegistryRig::Owner()); + + EXPECT_FALSE(weakSession.expired()); + ASSERT_EQ(writesBeforeRelease + 1U, rig.bus.WriteCount()); + EXPECT_FALSE(rig.registry.GetSessionState(SessionRegistryRig::Owner(), handle).has_value()); + + const auto& logoutWrite = rig.bus.WriteAt(writesBeforeRelease); + EXPECT_TRUE(rig.bus.CompleteWrite(logoutWrite.handle, ASFW::Async::AsyncStatus::kSuccess)); + EXPECT_FALSE(weakSession.expired()); + + rig.AdvanceMs(2'000); + + EXPECT_TRUE(weakSession.expired()); + EXPECT_FALSE(rig.registry.GetSessionState(SessionRegistryRig::Owner(), handle).has_value()); +} + +TEST(SessionRegistryTests, ReleaseOwnerDuringPendingLoginCancelsWriteAndDropsSession) { + SessionRegistryRig rig; + const uint64_t handle = rig.CreateSession(); + + std::weak_ptr weakSession = rig.registry.GetSessionWeakForTesting(handle); + ASSERT_FALSE(weakSession.expired()); + + ASSERT_TRUE(rig.registry.StartLogin(SessionRegistryRig::Owner(), handle)); + ASSERT_EQ(1u, rig.bus.PendingWriteCount()); + const auto loginWrite = rig.bus.WriteAt(0); + + rig.registry.ReleaseOwner(SessionRegistryRig::Owner()); + + EXPECT_TRUE(weakSession.expired()); + EXPECT_FALSE(rig.registry.GetSessionState(SessionRegistryRig::Owner(), handle).has_value()); + EXPECT_EQ(0u, rig.bus.PendingWriteCount()); + EXPECT_FALSE(rig.bus.CompleteWrite(loginWrite.handle, ASFW::Async::AsyncStatus::kSuccess)); +} + +TEST(SessionRegistryTests, ReleaseSessionDuringPendingLogoutRetainsUntilStatusArrives) { + SessionRegistryRig rig; + const uint64_t handle = rig.CreateSession(); + rig.LoginSuccessfully(handle); + + auto* session = rig.registry.GetSessionForTesting(handle); + ASSERT_NE(nullptr, session); + + std::weak_ptr weakSession = rig.registry.GetSessionWeakForTesting(handle); + ASSERT_FALSE(weakSession.expired()); + + const size_t writesBeforeLogout = rig.bus.WriteCount(); + const size_t pendingBeforeLogout = rig.bus.PendingWriteCount(); + ASSERT_TRUE(session->Logout()); + ASSERT_EQ(writesBeforeLogout + 1U, rig.bus.WriteCount()); + ASSERT_EQ(pendingBeforeLogout + 1U, rig.bus.PendingWriteCount()); + + EXPECT_TRUE(rig.registry.ReleaseSession(SessionRegistryRig::Owner(), handle)); + EXPECT_FALSE(weakSession.expired()); + EXPECT_FALSE(rig.registry.GetSessionState(SessionRegistryRig::Owner(), handle).has_value()); + + const auto& logoutWrite = rig.bus.WriteAt(writesBeforeLogout); + EXPECT_TRUE(rig.bus.CompleteWrite(logoutWrite.handle, ASFW::Async::AsyncStatus::kSuccess)); + EXPECT_FALSE(weakSession.expired()); + + StatusBlock status{}; + status.details = 0; + status.sbpStatus = SBPStatus::kNoAdditionalInfo; + rig.addressManager.ApplyRemoteWrite( + rig.sessionStatusAddress, + std::span{reinterpret_cast(&status), sizeof(status)}); + + EXPECT_TRUE(weakSession.expired()); + EXPECT_FALSE(rig.registry.GetSessionState(SessionRegistryRig::Owner(), handle).has_value()); +} + +TEST(SessionRegistryTests, CreateSessionRejectsDuplicateTargetAcrossOwners) { + SessionRegistryRig rig; + const uint64_t handle = rig.CreateSession(); + ASSERT_NE(0u, handle); + + auto duplicate = rig.registry.CreateSession(SessionRegistryRig::OtherOwner(), + SessionRegistryRig::kGuid, 0); + ASSERT_FALSE(duplicate.has_value()); + EXPECT_EQ(kIOReturnExclusiveAccess, duplicate.error()); +} + +TEST(SessionRegistryTests, CreateSessionRejectsDuplicateTargetUntilLogoutCompletes) { + SessionRegistryRig rig; + const uint64_t handle = rig.CreateSession(); + rig.LoginSuccessfully(handle); + + const size_t writesBeforeRelease = rig.bus.WriteCount(); + rig.registry.ReleaseOwner(SessionRegistryRig::Owner()); + + auto duplicateWhileLoggingOut = rig.registry.CreateSession(SessionRegistryRig::OtherOwner(), + SessionRegistryRig::kGuid, 0); + ASSERT_FALSE(duplicateWhileLoggingOut.has_value()); + EXPECT_EQ(kIOReturnExclusiveAccess, duplicateWhileLoggingOut.error()); + + const auto& logoutWrite = rig.bus.WriteAt(writesBeforeRelease); + ASSERT_TRUE(rig.bus.CompleteWrite(logoutWrite.handle, ASFW::Async::AsyncStatus::kSuccess)); + + StatusBlock status{}; + status.details = 0; + status.sbpStatus = SBPStatus::kNoAdditionalInfo; + rig.addressManager.ApplyRemoteWrite( + rig.sessionStatusAddress, + std::span{reinterpret_cast(&status), sizeof(status)}); + + auto replacement = rig.registry.CreateSession(SessionRegistryRig::OtherOwner(), + SessionRegistryRig::kGuid, 0); + ASSERT_TRUE(replacement.has_value()); +} + +TEST(SessionRegistryTests, MissingDiscoveryStillTerminatesNonSBP2Device) { + DeviceManager deviceManager; + + DeviceRecord record{}; + record.guid = SessionRegistryRig::kGuid + 2U; + record.vendorId = 0x001122; + record.modelId = 0x334455; + record.kind = DeviceKind::Unknown; + record.vendorName = "Other Vendor"; + record.modelName = "Other Device"; + record.gen = Generation{1}; + record.nodeId = 0x10; + record.link = LinkPolicy{}; + record.state = LifeState::Ready; + + ConfigROM rom{}; + rom.gen = Generation{1}; + rom.nodeId = record.nodeId; + rom.rootDirMinimal = { + RomEntry{CfgKey::Unit_Spec_Id, 0x00A02D, 0, 0}, + RomEntry{CfgKey::Unit_Sw_Version, 0x010001, 0, 0}, + }; + + ASSERT_NE(nullptr, deviceManager.UpsertDevice(record, rom)); + deviceManager.MarkDeviceLost(record.guid); + EXPECT_EQ(nullptr, deviceManager.GetDeviceByGUID(record.guid)); +} + +TEST(SessionRegistryTests, SubmitCommandRejectsCDBLargerThanORBPayloadBudget) { + SessionRegistryRig rig; + const uint64_t handle = rig.CreateSession(); + rig.LoginSuccessfully(handle); + + // maxCommandBlockSize is maxORBSize(32) - NormalORB::kHeaderSize(16) = 16, so a + // 17-byte CDB exceeds the budget and must be rejected. + SCSI::CommandRequest request{}; + request.cdb = std::vector(17, 0x12); + request.direction = SCSI::DataDirection::None; + request.transferLength = 0; + request.timeoutMs = 100; + + EXPECT_FALSE(rig.registry.SubmitCommand(SessionRegistryRig::Owner(), handle, request)); +} + +TEST(SessionRegistryTests, CreateSessionAcceptsRealSBP2SpecAndVersion) { + SessionRegistryRig rig; + auto result = rig.registry.CreateSession(SessionRegistryRig::Owner(), + SessionRegistryRig::kGuid, 0); + ASSERT_TRUE(result.has_value()); +} + +TEST(SessionRegistryTests, UnitCharacteristicsDecodeMinimumORBSize) { + SessionRegistryRig rig(0x000408); + + const uint64_t handle = rig.CreateSession(); + auto* session = rig.registry.GetSessionForTesting(handle); + ASSERT_NE(nullptr, session); + + const auto& targetInfo = session->TargetInfo(); + EXPECT_EQ(2000u, targetInfo.managementTimeoutMs); + EXPECT_EQ(32u, targetInfo.maxORBSize); + // DICE's NormalORB header is 16 bytes (4 quadlets), vs PR #19's 20. + EXPECT_EQ(32u - NormalORB::kHeaderSize, targetInfo.maxCommandBlockSize); +} + +TEST(SessionRegistryTests, UnitCharacteristicsDecodeTimeoutAndORBSizeFromLowBytes) { + SessionRegistryRig rig(0x000410); + + const uint64_t handle = rig.CreateSession(); + auto* session = rig.registry.GetSessionForTesting(handle); + ASSERT_NE(nullptr, session); + + const auto& targetInfo = session->TargetInfo(); + EXPECT_EQ(2000u, targetInfo.managementTimeoutMs); + EXPECT_EQ(64u, targetInfo.maxORBSize); + EXPECT_EQ(64u - NormalORB::kHeaderSize, targetInfo.maxCommandBlockSize); +} + +TEST(SessionRegistryTests, DeviceDiscoveryParsesNikonStyleManagementAgentCSRKey) { + DeviceManager deviceManager; + + DeviceRecord record{}; + record.guid = SessionRegistryRig::kGuid + 1U; + record.vendorId = 0x0090B5; + record.modelId = 0x004001; + record.kind = DeviceKind::Unknown; + record.vendorName = "Nikon"; + record.modelName = "LS-4000 ED"; + record.gen = Generation{1}; + record.nodeId = 0x00; + record.link = LinkPolicy{}; + record.state = LifeState::Ready; + + ConfigROM rom{}; + rom.gen = Generation{1}; + rom.nodeId = record.nodeId; + rom.bib.busInfoLength = 4; + rom.rootDirMinimal = { + RomEntry{CfgKey::Unit_Directory, 0x000001, 3, 1}, + }; + rom.rawQuadlets = { + OSSwapHostToBigInt32(0x04045343u), + OSSwapHostToBigInt32(0x31333934u), + OSSwapHostToBigInt32(0x00FF5012u), + OSSwapHostToBigInt32(0x0090B540u), + OSSwapHostToBigInt32(0x01FFFFFFu), + OSSwapHostToBigInt32(0x0001B344u), + OSSwapHostToBigInt32(0x0004CAEEu), + OSSwapHostToBigInt32(0x1200609Eu), + OSSwapHostToBigInt32(0x13010483u), + OSSwapHostToBigInt32(0x5400C000u), + OSSwapHostToBigInt32(0x14060000u), + }; + + auto device = deviceManager.UpsertDevice(record, rom); + ASSERT_NE(device, nullptr); + ASSERT_EQ(device->GetUnits().size(), 1u); + + const auto& unit = device->GetUnits().front(); + ASSERT_NE(unit, nullptr); + EXPECT_TRUE(unit->Matches(kSBP2UnitSpecId, kSBP2UnitSwVersion)); + ASSERT_TRUE(unit->GetManagementAgentOffset().has_value()); + EXPECT_EQ(*unit->GetManagementAgentOffset(), 0x00C000u); + ASSERT_TRUE(unit->GetLUN().has_value()); + EXPECT_EQ(*unit->GetLUN(), 0x060000u); +} + +} // namespace diff --git a/tests/protocols/SessionSchedulerTests.cpp b/tests/protocols/SessionSchedulerTests.cpp new file mode 100644 index 00000000..2fd3c3f2 --- /dev/null +++ b/tests/protocols/SessionSchedulerTests.cpp @@ -0,0 +1,83 @@ +#include + +#include + +#include "FakeSessionScheduler.hpp" + +using ASFW::Testing::FakeSessionScheduler; +using ASFW::Protocols::SBP2::kInvalidSchedulerToken; + +TEST(SessionSchedulerTests, FiresOnlyAfterDeadlineCrossed) { + FakeSessionScheduler sched; + int fired = 0; + (void)sched.ScheduleAfter(100, [&fired] { ++fired; }); + + sched.Advance(50); + EXPECT_EQ(0, fired); // not yet due + EXPECT_EQ(1u, sched.PendingCount()); + + sched.Advance(50); // now == deadline + EXPECT_EQ(1, fired); // fires exactly at the deadline + EXPECT_EQ(0u, sched.PendingCount()); + + sched.Advance(1000); + EXPECT_EQ(1, fired); // one-shot: does not re-fire +} + +TEST(SessionSchedulerTests, FiresInDeadlineOrderRegardlessOfInsertion) { + FakeSessionScheduler sched; + std::vector order; + (void)sched.ScheduleAfter(300, [&order] { order.push_back(3); }); + (void)sched.ScheduleAfter(100, [&order] { order.push_back(1); }); + (void)sched.ScheduleAfter(200, [&order] { order.push_back(2); }); + + sched.Advance(300); + ASSERT_EQ(3u, order.size()); + EXPECT_EQ(1, order[0]); + EXPECT_EQ(2, order[1]); + EXPECT_EQ(3, order[2]); +} + +TEST(SessionSchedulerTests, CancelPreventsFire) { + FakeSessionScheduler sched; + int fired = 0; + const auto token = sched.ScheduleAfter(100, [&fired] { ++fired; }); + + sched.Cancel(token); + EXPECT_EQ(0u, sched.PendingCount()); + + sched.Advance(1000); + EXPECT_EQ(0, fired); + + sched.Cancel(token); // double-cancel is a no-op + sched.Cancel(kInvalidSchedulerToken); // invalid token is a no-op +} + +TEST(SessionSchedulerTests, ReentrantScheduleFiresWithinSameAdvanceWhenDue) { + FakeSessionScheduler sched; + std::vector order; + // First callback (at t=100) schedules a second at +50 (absolute t=150), + // which is still within the t=200 advance window and must fire too. + (void)sched.ScheduleAfter(100, [&] { + order.push_back(1); + (void)sched.ScheduleAfter(50, [&order] { order.push_back(2); }); + }); + + sched.Advance(200); + ASSERT_EQ(2u, order.size()); + EXPECT_EQ(1, order[0]); + EXPECT_EQ(2, order[1]); + EXPECT_EQ(0u, sched.PendingCount()); +} + +TEST(SessionSchedulerTests, CallbackThatCancelsAnotherIsHonored) { + FakeSessionScheduler sched; + int fired = 0; + ASFW::Protocols::SBP2::SchedulerToken victim = kInvalidSchedulerToken; + // Earlier callback cancels a later, not-yet-due one. + (void)sched.ScheduleAfter(100, [&] { sched.Cancel(victim); }); + victim = sched.ScheduleAfter(200, [&fired] { ++fired; }); + + sched.Advance(300); + EXPECT_EQ(0, fired); // victim was canceled before its deadline +}