diff --git a/ASFWDriver/ASFWDriver.cpp b/ASFWDriver/ASFWDriver.cpp index 5d7e6df9..33ef7e1a 100644 --- a/ASFWDriver/ASFWDriver.cpp +++ b/ASFWDriver/ASFWDriver.cpp @@ -35,8 +35,6 @@ #include "Async/AsyncSubsystem.hpp" #include "Async/DMAMemoryImpl.hpp" #include "Async/Interfaces/IFireWireBus.hpp" -#include "Async/PacketHelpers.hpp" -#include "Async/ResponseCode.hpp" #include "Audio/AudioCoordinator.hpp" #include "Bus/SelfIDCapture.hpp" #include "Common/DriverKitOwnership.hpp" @@ -224,32 +222,6 @@ kern_return_t IMPL(ASFWDriver, Start) { ASFW_LOG(Controller, "✅ FCPResponseRouter initialized"); } - if (ctx.deps.fcpResponseRouter && ctx.deps.asyncSubsystem) { - if (auto* router = ctx.deps.asyncSubsystem->GetPacketRouter()) { - router->RegisterRequestHandler( - 0x1, // tCode for Block Write Request - [fcpRouter = - ctx.deps.fcpResponseRouter.get()](const ASFW::Async::ARPacketView& packet) { - if (fcpRouter) { - const ASFW::Protocols::Ports::BlockWriteRequestView request{ - .sourceID = packet.sourceID, - .destOffset = ASFW::Async::ExtractDestOffset(packet.header), - .payload = packet.payload, - }; - - const auto disposition = fcpRouter->RouteBlockWrite(request); - if (disposition == - ASFW::Protocols::Ports::BlockWriteDisposition::kAddressError) { - return ASFW::Async::ResponseCode::AddressError; - } - return ASFW::Async::ResponseCode::Complete; - } - return ASFW::Async::ResponseCode::NoResponse; - }); - ASFW_LOG(Controller, "✅ FCPResponseRouter wired to PacketRouter (tCode 0x1)"); - } - } - DriverWiring::EnsureSbp2Deps(ctx); if (ctx.deps.speedPolicy) { diff --git a/ASFWDriver/Async/AsyncSubsystemInterrupts.cpp b/ASFWDriver/Async/AsyncSubsystemInterrupts.cpp index afab073c..b3c78a52 100644 --- a/ASFWDriver/Async/AsyncSubsystemInterrupts.cpp +++ b/ASFWDriver/Async/AsyncSubsystemInterrupts.cpp @@ -2,6 +2,7 @@ #include "Contexts/ATRequestContext.hpp" #include "Contexts/ATResponseContext.hpp" +#include "Tx/ResponseSender.hpp" #include "../Logging/Logging.hpp" #include "../Shared/Memory/DMAMemoryManager.hpp" @@ -22,6 +23,9 @@ uint32_t AsyncSubsystem::DrainTxCompletions(const char* reason) { return; } while (auto completion = ctx->ScanCompletion()) { + if (completion->isResponseContext && responseSender_) { + responseSender_->OnTxCompletion(*completion); + } tracking_->OnTxCompletion(*completion); ++drained; } diff --git a/ASFWDriver/Async/AsyncSubsystemLifecycle.cpp b/ASFWDriver/Async/AsyncSubsystemLifecycle.cpp index 757c9433..234a5ae3 100644 --- a/ASFWDriver/Async/AsyncSubsystemLifecycle.cpp +++ b/ASFWDriver/Async/AsyncSubsystemLifecycle.cpp @@ -563,7 +563,10 @@ void AsyncSubsystem::Teardown(bool disableHardware) { txnMgr_.reset(); } - responseSender_.reset(); + if (responseSender_) { + responseSender_->ClearOutstandingResponses(); + responseSender_.reset(); + } descriptorBuilder_ = nullptr; descriptorBuilderResponse_ = nullptr; packetBuilder_.reset(); diff --git a/ASFWDriver/Async/Tx/ResponseSender.cpp b/ASFWDriver/Async/Tx/ResponseSender.cpp index 7d114b55..97f7f020 100644 --- a/ASFWDriver/Async/Tx/ResponseSender.cpp +++ b/ASFWDriver/Async/Tx/ResponseSender.cpp @@ -2,6 +2,7 @@ #include "../Engine/ContextManager.hpp" #include "../Contexts/ATResponseContext.hpp" +#include "../Track/TxCompletion.hpp" #include "../Tx/DescriptorBuilder.hpp" #include "../Tx/Submitter.hpp" #include "../../Bus/GenerationTracker.hpp" @@ -50,7 +51,8 @@ void ResponseSender::SendResponse(const ARPacketView& request, const uint32_t* header, std::size_t headerBytes, uint64_t payloadDeviceAddress, - std::size_t payloadLength) noexcept { + std::size_t payloadLength, + std::shared_ptr payloadLease) noexcept { // Per IEEE 1394, broadcast requests (destID=0xFFFF) do not get responses. if (request.destID == 0xFFFF) { ASFW_LOG_V3(Async, "ResponseSender: skip response for broadcast destID=0xFFFF"); @@ -78,8 +80,16 @@ void ResponseSender::SendResponse(const ARPacketView& request, return; } + const auto* leaseKey = chain.last; + if (payloadLease && leaseKey) { + responsePayloadLeases_[leaseKey] = std::move(payloadLease); + } + const auto submitRes = submitter_.submit_tx_chain(atRspCtx, std::move(chain)); if (submitRes.kr != kIOReturnSuccess) { + if (leaseKey) { + responsePayloadLeases_.erase(leaseKey); + } ASFW_LOG_ERROR( Async, "ResponseSender: submit_tx_chain failed (tCode=0x%x kr=0x%x)", @@ -147,7 +157,8 @@ void ResponseSender::SendReadQuadletResponse(const ARPacketView& request, void ResponseSender::SendReadBlockResponse(const ARPacketView& request, ResponseCode rcode, uint64_t payloadDeviceAddress, - uint32_t payloadLength) noexcept { + uint32_t payloadLength, + std::shared_ptr payloadLease) noexcept { if (request.tCode != 0x5) { ASFW_LOG_V3(Async, "ResponseSender: skip RdBlockResp for non-read-block tCode=0x%x", @@ -177,7 +188,19 @@ void ResponseSender::SendReadBlockResponse(const ARPacketView& request, header, sizeof(header), responsePayloadAddress, - responsePayloadLen); + responsePayloadLen, + std::move(payloadLease)); +} + +void ResponseSender::OnTxCompletion(const TxCompletion& completion) noexcept { + if (!completion.isResponseContext || !completion.descriptor) { + return; + } + responsePayloadLeases_.erase(completion.descriptor); +} + +void ResponseSender::ClearOutstandingResponses() noexcept { + responsePayloadLeases_.clear(); } } // namespace ASFW::Async diff --git a/ASFWDriver/Async/Tx/ResponseSender.hpp b/ASFWDriver/Async/Tx/ResponseSender.hpp index 5a1b80ea..a4e475d2 100644 --- a/ASFWDriver/Async/Tx/ResponseSender.hpp +++ b/ASFWDriver/Async/Tx/ResponseSender.hpp @@ -2,6 +2,8 @@ #include #include +#include +#include #include "../ResponseCode.hpp" #include "../Rx/PacketRouter.hpp" @@ -11,9 +13,11 @@ namespace ASFW::Async { class DescriptorBuilder; class ATResponseContext; class IFireWireBusInfo; +struct TxCompletion; namespace Bus { class GenerationTracker; } namespace Engine { class ContextManager; } namespace Tx { class Submitter; } +namespace HW { struct OHCIDescriptor; } /// Utility to build and send Write Response (WrResp) packets for incoming AR requests. class ResponseSender { @@ -36,7 +40,14 @@ class ResponseSender { void SendReadBlockResponse(const ARPacketView& request, ResponseCode rcode, uint64_t payloadDeviceAddress, - uint32_t payloadLength) noexcept; + uint32_t payloadLength, + std::shared_ptr payloadLease = {}) noexcept; + + /// Release response payload leases after AT response descriptor completion. + void OnTxCompletion(const TxCompletion& completion) noexcept; + + /// Drop any retained response payload leases during response subsystem teardown. + void ClearOutstandingResponses() noexcept; private: void SendResponse(const ARPacketView& request, @@ -45,12 +56,14 @@ class ResponseSender { const uint32_t* header, std::size_t headerBytes, uint64_t payloadDeviceAddress, - std::size_t payloadLength) noexcept; + std::size_t payloadLength, + std::shared_ptr payloadLease = {}) noexcept; DescriptorBuilder& builder_; Tx::Submitter& submitter_; Engine::ContextManager& ctxMgr_; Bus::GenerationTracker& generationTracker_; + std::unordered_map> responsePayloadLeases_; }; } // namespace ASFW::Async diff --git a/ASFWDriver/Controller/ControllerCore.hpp b/ASFWDriver/Controller/ControllerCore.hpp index b62d6ab3..194abffd 100644 --- a/ASFWDriver/Controller/ControllerCore.hpp +++ b/ASFWDriver/Controller/ControllerCore.hpp @@ -57,6 +57,7 @@ class FCPResponseRouter; namespace ASFW::Protocols::SBP2 { class AddressSpaceManager; +class SBP2SessionRegistry; } namespace ASFW::IRM { @@ -96,6 +97,7 @@ class ControllerCore { std::shared_ptr avcDiscovery; std::shared_ptr fcpResponseRouter; std::shared_ptr sbp2AddressSpaceManager; + std::shared_ptr sbp2SessionRegistry; std::shared_ptr irmClient; @@ -137,9 +139,13 @@ class ControllerCore { Protocols::AVC::IAVCDiscovery* GetAVCDiscovery() const; void SetAVCDiscovery(std::shared_ptr avcDiscovery); void SetFCPResponseRouter(std::shared_ptr fcpResponseRouter); + Protocols::SBP2::AddressSpaceManager* GetSbp2AddressSpaceManager() const; + Protocols::SBP2::SBP2SessionRegistry* GetSBP2SessionRegistry() const; void SetSbp2AddressSpaceManager( std::shared_ptr sbp2AddressSpaceManager); + 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 956db04c..4e7947fb 100644 --- a/ASFWDriver/Controller/ControllerCoreDiscovery.cpp +++ b/ASFWDriver/Controller/ControllerCoreDiscovery.cpp @@ -30,6 +30,7 @@ #include "../Protocols/AVC/AVCDiscovery.hpp" #include "../Protocols/AVC/CMP/CMPClient.hpp" #include "../Protocols/Audio/DeviceProtocolFactory.hpp" +#include "../Protocols/SBP2/SBP2SessionRegistry.hpp" #include "../Scheduling/Scheduler.hpp" #include "../Version/DriverVersion.hpp" #include "ControllerStateMachine.hpp" @@ -116,6 +117,18 @@ void ControllerCore::OnTopologyReady(const TopologySnapshot& snap) { // CMP (PCR) operations target a *specific device's* plug registers, not the IRM node. // Device-scoped CMP wiring is done at stream start time (IsochService). } + + if (deps_.sbp2SessionRegistry) { + deps_.sbp2SessionRegistry->OnBusReset(static_cast(snap.generation)); + } + + // NOTE: CSR STATE_SET CMSTR write removed. Apple IOFireWireFamily does NOT write + // CSR STATE_SET via async transactions — it uses the OHCI LinkControl register + // directly (kCycleMaster bit), which ASFWDriver already sets in kDefaultLinkControl + // during controller initialization. Async loopback to the local node does not work + // in ASFWDriver (always returns timeout), so the previous implementation was a no-op. + // OHCI hardware generates cycle-start packets automatically when the node is root + // and kCycleMaster is set in LinkControl. } // NOLINTNEXTLINE(readability-function-cognitive-complexity) @@ -218,7 +231,7 @@ void ControllerCore::OnDiscoveryScanComplete(Discovery::Generation gen, } else if (deps_.deviceManager && zeroRomScanInconclusive) { ASFW_LOG(Discovery, "ROM scan for gen=%u produced 0 ROMs but topology still has remote " - "link-active nodes; keeping existing devices until a conclusive scan", + "link-active nodes — keeping existing devices until a conclusive scan", gen.value); } @@ -226,6 +239,14 @@ void ControllerCore::OnDiscoveryScanComplete(Discovery::Generation gen, ASFW_LOG(Discovery, "Discovery complete: %zu devices processed in gen=%u", roms.size(), gen.value); ASFW_LOG(Discovery, "═══════════════════════════════════════════════════════"); + + if (deps_.sbp2SessionRegistry && !zeroRomScanInconclusive) { + deps_.sbp2SessionRegistry->RefreshTargets(gen); + } else if (deps_.sbp2SessionRegistry) { + ASFW_LOG(Discovery, + "Skipping SBP-2 target refresh for inconclusive zero-ROM scan gen=%u", + gen.value); + } } } // namespace ASFW::Driver diff --git a/ASFWDriver/Controller/ControllerCoreFacades.cpp b/ASFWDriver/Controller/ControllerCoreFacades.cpp index 894c095f..48f2f525 100644 --- a/ASFWDriver/Controller/ControllerCoreFacades.cpp +++ b/ASFWDriver/Controller/ControllerCoreFacades.cpp @@ -104,16 +104,25 @@ void ControllerCore::SetFCPResponseRouter( deps_.fcpResponseRouter = std::move(fcpResponseRouter); } +IRM::IRMClient* ControllerCore::GetIRMClient() const { return deps_.irmClient.get(); } + Protocols::SBP2::AddressSpaceManager* ControllerCore::GetSbp2AddressSpaceManager() const { return deps_.sbp2AddressSpaceManager.get(); } +Protocols::SBP2::SBP2SessionRegistry* ControllerCore::GetSBP2SessionRegistry() const { + return deps_.sbp2SessionRegistry.get(); +} + void ControllerCore::SetSbp2AddressSpaceManager( std::shared_ptr sbp2AddressSpaceManager) { deps_.sbp2AddressSpaceManager = std::move(sbp2AddressSpaceManager); } -IRM::IRMClient* ControllerCore::GetIRMClient() const { return deps_.irmClient.get(); } +void ControllerCore::SetSBP2SessionRegistry( + std::shared_ptr sbp2SessionRegistry) { + deps_.sbp2SessionRegistry = std::move(sbp2SessionRegistry); +} void ControllerCore::SetIRMClient(std::shared_ptr client) { deps_.irmClient = std::move(client); diff --git a/ASFWDriver/Logging/Logging.cpp b/ASFWDriver/Logging/Logging.cpp index 5932f8c6..1339886d 100644 --- a/ASFWDriver/Logging/Logging.cpp +++ b/ASFWDriver/Logging/Logging.cpp @@ -31,5 +31,6 @@ os_log_t AVC() { static os_log_t log = MakeCategory("avc"); return os_log_t Isoch() { static os_log_t log = MakeCategory("isoch"); return log; } os_log_t Audio() { static os_log_t log = MakeCategory("audio"); return log; } os_log_t DICE() { static os_log_t log = MakeCategory("dice"); return log; } +os_log_t SBP2() { static os_log_t log = MakeCategory("sbp2"); return log; } } // namespace ASFW::Driver::Logging diff --git a/ASFWDriver/Logging/Logging.hpp b/ASFWDriver/Logging/Logging.hpp index 3bd5585e..9a3c998b 100644 --- a/ASFWDriver/Logging/Logging.hpp +++ b/ASFWDriver/Logging/Logging.hpp @@ -98,6 +98,7 @@ os_log_t AVC(); os_log_t Isoch(); os_log_t Audio(); os_log_t DICE(); +os_log_t SBP2(); } // namespace ASFW::Driver::Logging // ----- time helpers (header-only, safe in DriverKit) ----- diff --git a/ASFWDriver/Protocols/SBP2/AddressSpaceManager.hpp b/ASFWDriver/Protocols/SBP2/AddressSpaceManager.hpp index 90801cbd..819dfc8e 100644 --- a/ASFWDriver/Protocols/SBP2/AddressSpaceManager.hpp +++ b/ASFWDriver/Protocols/SBP2/AddressSpaceManager.hpp @@ -1,11 +1,13 @@ #pragma once #include +#include #include -#include #include #include #include +#include +#include #include #include #include @@ -32,9 +34,20 @@ class AddressSpaceManager { public: // Callback invoked when a remote write arrives for a registered range. // Parameters: handle, offset within range, payload data. + // + // AddressSpaceManager invokes callbacks outside its lock. Callback targets + // must capture weak/shared lifetime state, not raw owning object pointers. using RemoteWriteCallback = std::function payload)>; + struct RemoteWriteCallbackSlot { + explicit RemoteWriteCallbackSlot(RemoteWriteCallback callbackIn) + : callback(std::move(callbackIn)) {} + + RemoteWriteCallback callback; + std::atomic enabled{true}; + }; + struct AddressRangeMeta { uint64_t handle{0}; uint64_t address{0}; @@ -46,6 +59,7 @@ class AddressSpaceManager { struct ReadSlice { uint64_t payloadDeviceAddress{0}; uint32_t payloadLength{0}; + std::shared_ptr backingLease{}; }; explicit AddressSpaceManager(ASFW::Driver::HardwareInterface* hardware) noexcept @@ -229,6 +243,9 @@ class AddressSpaceManager { if (!lock_) { return kIOReturnBadArgument; } + if (data.size() > std::numeric_limits::max()) { + return kIOReturnBadArgument; + } IOLockLock(lock_); auto it = ranges_.find(handle); @@ -256,8 +273,11 @@ class AddressSpaceManager { if (!lock_ || payload.empty()) { return Async::ResponseCode::AddressError; } + if (payload.size() > std::numeric_limits::max()) { + return Async::ResponseCode::DataError; + } - RemoteWriteCallback callback; + std::shared_ptr callbackSlot; uint64_t handle = 0; uint32_t offset = 0; @@ -272,9 +292,10 @@ class AddressSpaceManager { offset = static_cast(address - range->meta.address); ASFW_ADDRSPACE_LOG( - "AddressSpaceManager[%p] remote write addr=0x%012llx len=%zu src=%p " + "AddressSpaceManager[%p] remote write label=%s addr=0x%012llx len=%zu src=%p " "handle=0x%llx rangeAddr=0x%012llx off=%u buf=%p mapped=%p backing=%u", this, + DebugLabelCString(*range), static_cast(address), payload.size(), payload.data(), @@ -282,17 +303,18 @@ class AddressSpaceManager { static_cast(range->meta.address), offset, range->buffer.data(), - range->mappedBytes, - range->hasBacking ? 1u : 0u); + range->backing ? range->backing->mappedBytes : nullptr, + range->backing && range->backing->hasBacking ? 1u : 0u); WriteBytesLocked(*range, offset, payload); - callback = range->onRemoteWrite; + callbackSlot = range->remoteWriteSlot; handle = range->meta.handle; IOLockUnlock(lock_); } // Fire callback outside lock to avoid deadlock. - if (callback) { - callback(handle, offset, payload); + if (callbackSlot && callbackSlot->enabled.load(std::memory_order_acquire) && + callbackSlot->callback) { + callbackSlot->callback(handle, offset, payload); } return Async::ResponseCode::Complete; @@ -313,13 +335,14 @@ class AddressSpaceManager { return Async::ResponseCode::AddressError; } - if (!range->hasBacking || range->deviceAddress == 0) { + if (!range->backing || !range->backing->hasBacking || + range->backing->deviceAddress == 0) { IOLockUnlock(lock_); return Async::ResponseCode::DataError; } const uint64_t offset = address - range->meta.address; - const uint64_t payloadAddress = range->deviceAddress + offset; + const uint64_t payloadAddress = range->backing->deviceAddress + offset; if (payloadAddress > 0xFFFF'FFFFULL) { IOLockUnlock(lock_); return Async::ResponseCode::DataError; @@ -327,6 +350,19 @@ class AddressSpaceManager { outSlice->payloadDeviceAddress = payloadAddress; outSlice->payloadLength = length; + outSlice->backingLease = range->backing; + + ASFW_ADDRSPACE_LOG( + "AddressSpaceManager[%p] remote read-block label=%s addr=0x%012llx len=%u " + "handle=0x%llx rangeAddr=0x%012llx off=%llu dma=0x%08x", + this, + DebugLabelCString(*range), + static_cast(address), + length, + static_cast(range->meta.handle), + static_cast(range->meta.address), + static_cast(offset), + static_cast(payloadAddress)); IOLockUnlock(lock_); return Async::ResponseCode::Complete; @@ -349,6 +385,17 @@ class AddressSpaceManager { range->buffer.data() + static_cast(offset), sizeof(uint32_t)); + ASFW_ADDRSPACE_LOG( + "AddressSpaceManager[%p] remote read-quadlet label=%s addr=0x%012llx " + "handle=0x%llx rangeAddr=0x%012llx off=%u value=0x%08x", + this, + DebugLabelCString(*range), + static_cast(address), + static_cast(range->meta.handle), + static_cast(range->meta.address), + offset, + *outValue); + IOLockUnlock(lock_); return Async::ResponseCode::Complete; } @@ -379,6 +426,10 @@ class AddressSpaceManager { // Register a callback to fire when a remote write arrives for the given handle. // Must be called after AllocateAddressRange. Replaces any previous callback. + // + // Replacing or clearing the callback invalidates callbacks copied but not + // yet dispatched. Callback targets must still use weak/shared lifetime + // capture because callbacks run outside the manager lock. void SetRemoteWriteCallback(uint64_t handle, RemoteWriteCallback callback) { if (!lock_ || handle == 0) { return; @@ -387,7 +438,20 @@ class AddressSpaceManager { IOLockLock(lock_); auto it = ranges_.find(handle); if (it != ranges_.end()) { - it->second.onRemoteWrite = std::move(callback); + ReplaceRemoteWriteCallbackLocked(it->second, std::move(callback)); + } + IOLockUnlock(lock_); + } + + 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_); } @@ -410,12 +474,29 @@ class AddressSpaceManager { static constexpr uint32_t kAutoAddressWindowStartLo = 0x0010'0000u; static constexpr uint32_t kAutoAddressWindowEndLo = 0x0FFF'FFFFu; static constexpr uint64_t kAutoAddressAlignment = 8ULL; + static constexpr std::size_t kDebugLabelCapacity = 64; - struct AddressRange { - AddressRangeMeta meta{}; - void* owner{nullptr}; - std::vector buffer; - RemoteWriteCallback onRemoteWrite; + struct AddressRangeBacking { + ~AddressRangeBacking() { + if (mapping) { + mapping->release(); + mapping = nullptr; + } + + if (dmaCommand) { + dmaCommand->CompleteDMA(kIODMACommandCompleteDMANoOptions); + dmaCommand.reset(); + } + + descriptor.reset(); + mappedBytes = nullptr; + deviceAddress = 0; + hasBacking = false; + } + + AddressRangeBacking() = default; + AddressRangeBacking(const AddressRangeBacking&) = delete; + AddressRangeBacking& operator=(const AddressRangeBacking&) = delete; OSSharedPtr descriptor{}; OSSharedPtr dmaCommand{}; @@ -425,6 +506,15 @@ class AddressSpaceManager { bool hasBacking{false}; }; + struct AddressRange { + AddressRangeMeta meta{}; + void* owner{nullptr}; + std::vector buffer; + std::shared_ptr remoteWriteSlot; + std::array debugLabel{}; + std::shared_ptr backing{}; + }; + static uint64_t ComposeAddress(uint16_t hi, uint32_t lo) { return (static_cast(hi) << 32) | static_cast(lo); } @@ -453,6 +543,28 @@ class AddressSpaceManager { return end <= static_cast(range.meta.length); } + static void CopyDebugLabel(AddressRange& range, const char* label) { + const char* source = label != nullptr ? label : "unlabeled"; + range.debugLabel.fill('\0'); + std::strncpy(range.debugLabel.data(), source, range.debugLabel.size() - 1); + } + + static const char* DebugLabelCString(const AddressRange& range) { + return range.debugLabel[0] != '\0' ? range.debugLabel.data() : "unlabeled"; + } + + static void ReplaceRemoteWriteCallbackLocked(AddressRange& range, + RemoteWriteCallback callback) { + if (range.remoteWriteSlot) { + range.remoteWriteSlot->enabled.store(false, std::memory_order_release); + range.remoteWriteSlot.reset(); + } + if (callback) { + range.remoteWriteSlot = + std::make_shared(std::move(callback)); + } + } + AddressRange* FindRangeByAddressLocked(uint64_t address, uint32_t length) { const uint64_t end = address + static_cast(length); if (end < address) { @@ -485,14 +597,15 @@ 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), range.meta.length, - range.hasBacking ? 1u : 0u, - static_cast(range.deviceAddress)); + range.backing && range.backing->hasBacking ? 1u : 0u, + static_cast(range.backing ? range.backing->deviceAddress : 0)); } } @@ -518,6 +631,7 @@ class AddressSpaceManager { } AddressRange range{}; + CopyDebugLabel(range, nullptr); range.owner = owner; range.meta.handle = nextHandle_++; range.meta.address = start; @@ -549,6 +663,7 @@ class AddressSpaceManager { kern_return_t AllocateBacking(AddressRange& range) { const std::size_t size = static_cast(range.meta.length); + const std::size_t backingSize = static_cast(AlignUp(size, 16)); // IOBufferMemoryDescriptor::Create expects memory-direction options only. // Cache policy is set at CreateMapping time, not in allocation options. @@ -556,12 +671,12 @@ class AddressSpaceManager { std::optional dma; if (hardware_) { - dma = hardware_->AllocateDMA(size, options, 16); + dma = hardware_->AllocateDMA(backingSize, options, 16); } if (!dma.has_value()) { #ifdef ASFW_HOST_TEST - range.hasBacking = false; + range.backing.reset(); return kIOReturnSuccess; #else ASFW_LOG(UserClient, @@ -576,7 +691,7 @@ class AddressSpaceManager { kIOMemoryMapCacheModeInhibit, 0, 0, - size, + backingSize, 0, &mapping); if (kr != kIOReturnSuccess || !mapping) { @@ -597,44 +712,33 @@ class AddressSpaceManager { return kIOReturnNoMemory; } - std::memset(mapped, 0, size); + std::memset(mapped, 0, backingSize); OSSynchronizeIO(); - range.descriptor = std::move(dma->descriptor); - range.dmaCommand = std::move(dma->dmaCommand); - range.mapping = mapping; - range.mappedBytes = mapped; - range.deviceAddress = dma->deviceAddress; - range.hasBacking = true; + auto backing = std::make_shared(); + backing->descriptor = std::move(dma->descriptor); + backing->dmaCommand = std::move(dma->dmaCommand); + backing->mapping = mapping; + backing->mappedBytes = mapped; + backing->deviceAddress = dma->deviceAddress; + backing->hasBacking = true; + range.backing = std::move(backing); return kIOReturnSuccess; } static void CleanupBacking(AddressRange& range) { - if (range.mapping) { - range.mapping->release(); - range.mapping = nullptr; - } - - if (range.dmaCommand) { - range.dmaCommand->CompleteDMA(kIODMACommandCompleteDMANoOptions); - range.dmaCommand.reset(); - } - - range.descriptor.reset(); - range.mappedBytes = nullptr; - range.deviceAddress = 0; - range.hasBacking = false; + range.backing.reset(); } static void SyncRange(AddressRange& range, uint64_t offset, uint64_t length) { - if (!range.hasBacking) { + if (!range.backing || !range.backing->hasBacking) { return; } #if defined(IODMACommand_Synchronize_ID) - if (range.dmaCommand) { - const kern_return_t syncKr = range.dmaCommand->Synchronize( + if (range.backing->dmaCommand) { + const kern_return_t syncKr = range.backing->dmaCommand->Synchronize( 0, offset, length); @@ -682,14 +786,16 @@ class AddressSpaceManager { data.size(), data.data(), range.buffer.data(), - range.mappedBytes, + range.backing ? range.backing->mappedBytes : nullptr, static_cast(reinterpret_cast(data.data()) & 0x7ULL), static_cast(reinterpret_cast(range.buffer.data()) & 0x7ULL), - static_cast(reinterpret_cast(range.mappedBytes) & 0x7ULL)); + static_cast( + reinterpret_cast(range.backing ? range.backing->mappedBytes : nullptr) & + 0x7ULL)); CopyPayloadBytes(range.buffer.data() + static_cast(offset), data); - if (range.hasBacking && range.mappedBytes) { - CopyPayloadBytes(range.mappedBytes + static_cast(offset), data); + if (range.backing && range.backing->hasBacking && range.backing->mappedBytes) { + CopyPayloadBytes(range.backing->mappedBytes + static_cast(offset), data); std::atomic_thread_fence(std::memory_order_release); SyncRange(range, offset, data.size()); } diff --git a/ASFWDriver/Protocols/SBP2/SBP2CommandORB.cpp b/ASFWDriver/Protocols/SBP2/SBP2CommandORB.cpp index 6179b62e..26e25947 100644 --- a/ASFWDriver/Protocols/SBP2/SBP2CommandORB.cpp +++ b/ASFWDriver/Protocols/SBP2/SBP2CommandORB.cpp @@ -1,12 +1,11 @@ // SBP-2 Normal Command ORB implementation. +// Ported from Apple IOFireWireSBP2ORB. // Ref: SBP-2 §5.1.1 (Normal Command ORB format) #include "SBP2CommandORB.hpp" #include "SBP2DelayedDispatch.hpp" #include "../../Common/FWCommon.hpp" -#include - namespace ASFW::Protocols::SBP2 { // --------------------------------------------------------------------------- @@ -19,12 +18,11 @@ SBP2CommandORB::SBP2CommandORB(AddressSpaceManager& addrMgr, void* owner, , owner_(owner) , maxCommandBlockSize_(maxCommandBlockSize) { - AllocateResources(); + isValid_ = AllocateResources(); } SBP2CommandORB::~SBP2CommandORB() { CancelTimer(); - lifetimeToken_.reset(); DeallocateResources(); } @@ -33,6 +31,10 @@ SBP2CommandORB::~SBP2CommandORB() { // --------------------------------------------------------------------------- bool SBP2CommandORB::AllocateResources() noexcept { + if (orbHandle_ != 0) { + return true; + } + const uint32_t orbSize = Wire::NormalORB::kHeaderSize + maxCommandBlockSize_; orbStorage_.resize(orbSize, 0); @@ -41,11 +43,11 @@ bool SBP2CommandORB::AllocateResources() noexcept { owner_, 0xFFFF, orbSize, &orbHandle_, &orbMeta_); if (kr != kIOReturnSuccess) { - ASFW_LOG(Async, "SBP2CommandORB: failed to allocate ORB address space: 0x%08x", kr); + ASFW_LOG(SBP2, "SBP2CommandORB: failed to allocate ORB address space: 0x%08x", kr); return false; } - ASFW_LOG(Async, "SBP2CommandORB: allocated %u-byte ORB at %04x:%08x", + ASFW_LOG(SBP2, "SBP2CommandORB: allocated %u-byte ORB at %04x:%08x", orbSize, orbMeta_.addressHi, orbMeta_.addressLo); return true; } @@ -63,9 +65,16 @@ 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 { + if (!IsValid() || orbStorage_.size() < Wire::NormalORB::kHeaderSize) { + return false; + } + + if (cdb.size() > static_cast(maxCommandBlockSize_)) { + return false; + } + + const uint32_t copyLen = static_cast(cdb.size()); if (copyLen > 0) { std::memcpy(orbStorage_.data() + Wire::NormalORB::kHeaderSize, @@ -77,15 +86,24 @@ 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() || orbStorage_.size() < sizeof(Wire::NormalORB)) { + return kIOReturnNotReady; + } + if (maxPayloadLog > 15) { + return kIOReturnBadArgument; + } + auto* orb = reinterpret_cast(orbStorage_.data()); const uint16_t busNodeID = Wire::NormalizeBusNodeID(localNodeID); @@ -151,20 +169,25 @@ 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 { + if (!IsValid() || orbHandle_ == 0) { + return kIOReturnNotReady; + } + 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); + ASFW_LOG(SBP2, "SBP2CommandORB: failed to write ORB to address space: 0x%08x", kr); } + return kr; } // --------------------------------------------------------------------------- @@ -179,62 +202,77 @@ 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() || orbStorage_.size() < sizeof(Wire::NormalORB)) { + 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() || orbStorage_.size() < sizeof(Wire::NormalORB)) { + 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 = Wire::FromBE16(orb->options); hostOptions = (hostOptions & ~0x3000u) | 0x6000u; orb->options = Wire::ToBE16(hostOptions); - WriteORBToAddressSpace(); + return WriteORBToAddressSpace(); } // --------------------------------------------------------------------------- // Timer management // --------------------------------------------------------------------------- -void SBP2CommandORB::StartTimer(IODispatchQueue* queue) noexcept { +void SBP2CommandORB::StartTimer(IODispatchQueue* completionQueue, + IODispatchQueue* timeoutQueue) noexcept { CancelTimer(); - if (queue == nullptr || timeoutDuration_ == 0) { + if (completionQueue == nullptr || timeoutQueue == nullptr || timeoutDuration_ == 0) { return; } - timerQueue_ = queue; - inProgress_.store(true, std::memory_order_relaxed); + completionQueue_ = completionQueue; + timerQueue_ = timeoutQueue; + auto timerState = timerState_; + timerState->inProgress.store(true, std::memory_order_relaxed); const uint32_t timeout = timeoutDuration_; const uint64_t expectedGeneration = - timerGeneration_.fetch_add(1, std::memory_order_acq_rel) + 1ULL; - const std::weak_ptr weakLifetime = lifetimeToken_; + timerState->generation.fetch_add(1, std::memory_order_acq_rel) + 1ULL; const uint64_t delayNs = static_cast(timeout) * 1'000'000ULL; - DispatchAfterCompat(queue, delayNs, [this, weakLifetime, expectedGeneration, timeout]() { - if (weakLifetime.expired()) { - return; - } - if (timerGeneration_.load(std::memory_order_acquire) != expectedGeneration || - !inProgress_.load(std::memory_order_relaxed) || - !completionCallback_) { - return; - } - - ASFW_LOG(Async, "SBP2CommandORB: ORB timeout after %u ms", timeout); - inProgress_.store(false, std::memory_order_relaxed); - timerGeneration_.fetch_add(1, std::memory_order_acq_rel); - completionCallback_(-1, Wire::SBPStatus::kUnspecifiedError); + DispatchAfterCompat(timeoutQueue, delayNs, [timerState, + expectedGeneration, + timeout, + completionQueue]() { + DispatchAsyncCompat(completionQueue, [timerState, + expectedGeneration, + timeout]() { + if (timerState->generation.load(std::memory_order_acquire) != expectedGeneration || + !timerState->inProgress.load(std::memory_order_relaxed) || + !timerState->completionCallback) { + return; + } + + ASFW_LOG(SBP2, "SBP2CommandORB: ORB timeout after %u ms", timeout); + timerState->inProgress.store(false, std::memory_order_relaxed); + timerState->generation.fetch_add(1, std::memory_order_acq_rel); + timerState->completionCallback(-1, Wire::SBPStatus::kUnspecifiedError); + }); }); } void SBP2CommandORB::CancelTimer() noexcept { - inProgress_.store(false, std::memory_order_relaxed); + timerState_->inProgress.store(false, std::memory_order_relaxed); + completionQueue_ = nullptr; timerQueue_ = nullptr; - timerGeneration_.fetch_add(1, std::memory_order_acq_rel); + timerState_->generation.fetch_add(1, std::memory_order_acq_rel); } } // namespace ASFW::Protocols::SBP2 diff --git a/ASFWDriver/Protocols/SBP2/SBP2CommandORB.hpp b/ASFWDriver/Protocols/SBP2/SBP2CommandORB.hpp index cd88d810..1f637630 100644 --- a/ASFWDriver/Protocols/SBP2/SBP2CommandORB.hpp +++ b/ASFWDriver/Protocols/SBP2/SBP2CommandORB.hpp @@ -3,6 +3,7 @@ // SBP-2 Normal Command ORB. // Represents a single SCSI command submitted to the device after login. // +// Ported from Apple IOFireWireSBP2ORB. // Ref: SBP-2 §5.1.1 (Normal Command ORB format) #include "AddressSpaceManager.hpp" @@ -49,35 +50,39 @@ class SBP2CommandORB { SBP2CommandORB& operator=(const SBP2CommandORB&) = delete; // Configuration (call before submit) - void SetCommandBlock(std::span cdb) noexcept; + [[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; } - void SetCompletionCallback(CompletionCallback cb) noexcept { completionCallback_ = std::move(cb); } + void SetCompletionCallback(CompletionCallback cb) noexcept { + completionCallback_ = std::move(cb); + timerState_->completionCallback = completionCallback_; + } // Bind page table result from SBP2PageTable::Build. void SetDataDescriptor(const SBP2PageTable::Result& ptResult) noexcept { 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 SBP2LoginSession before submission. + [[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; + [[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 StartTimer(IODispatchQueue* completionQueue, IODispatchQueue* timeoutQueue) noexcept; void CancelTimer() noexcept; // State tracking. + [[nodiscard]] bool IsValid() const noexcept { return isValid_; } [[nodiscard]] bool IsAppended() const noexcept { return isAppended_; } void SetAppended(bool state) noexcept { isAppended_ = state; } @@ -88,16 +93,21 @@ class SBP2CommandORB { [[nodiscard]] CompletionCallback& GetCompletionCallback() noexcept { return completionCallback_; } private: + struct TimerState { + std::atomic inProgress{false}; + std::atomic generation{0}; + CompletionCallback completionCallback{}; + }; + bool AllocateResources() noexcept; void DeallocateResources() noexcept; - void WriteORBToAddressSpace() noexcept; + [[nodiscard]] kern_return_t WriteORBToAddressSpace() noexcept; AddressSpaceManager& addrMgr_; void* owner_; uint32_t maxCommandBlockSize_; uint32_t flags_{0}; - uint16_t maxPayloadSize_{0}; uint32_t timeoutDuration_{0}; CompletionCallback completionCallback_; @@ -110,14 +120,14 @@ class SBP2CommandORB { SBP2PageTable::Result dataDescriptor_{}; // State. + bool isValid_{false}; bool isAppended_{false}; - std::atomic inProgress_{false}; uint32_t fetchAgentWriteRetries_{20}; // Timer. + IODispatchQueue* completionQueue_{nullptr}; IODispatchQueue* timerQueue_{nullptr}; - std::atomic timerGeneration_{0}; - std::shared_ptr lifetimeToken_{std::make_shared(0)}; + std::shared_ptr timerState_{std::make_shared()}; }; } // namespace ASFW::Protocols::SBP2 diff --git a/ASFWDriver/Protocols/SBP2/SBP2DelayedDispatch.hpp b/ASFWDriver/Protocols/SBP2/SBP2DelayedDispatch.hpp index 43ebcf44..6ece74e8 100644 --- a/ASFWDriver/Protocols/SBP2/SBP2DelayedDispatch.hpp +++ b/ASFWDriver/Protocols/SBP2/SBP2DelayedDispatch.hpp @@ -12,6 +12,22 @@ namespace ASFW::Protocols::SBP2 { +inline void DispatchAsyncCompat(IODispatchQueue* queue, + std::function callback) noexcept { + if (queue == nullptr || !callback) { + return; + } + +#ifdef ASFW_HOST_TEST + queue->DispatchAsync(std::move(callback)); +#else + auto work = std::move(callback); + queue->DispatchAsync(^{ + work(); + }); +#endif +} + inline void DispatchAfterCompat(IODispatchQueue* queue, uint64_t delayNs, std::function callback) noexcept { diff --git a/ASFWDriver/Protocols/SBP2/SBP2LoginSession.cpp b/ASFWDriver/Protocols/SBP2/SBP2LoginSession.cpp new file mode 100644 index 00000000..ba48a601 --- /dev/null +++ b/ASFWDriver/Protocols/SBP2/SBP2LoginSession.cpp @@ -0,0 +1,1847 @@ +#include "SBP2LoginSession.hpp" +#include "SBP2DelayedDispatch.hpp" +#include "AddressSpaceManager.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; + +// --------------------------------------------------------------------------- +// Construction / Destruction +// --------------------------------------------------------------------------- + +SBP2LoginSession::SBP2LoginSession(Async::IFireWireBus& bus, + Async::IFireWireBusInfo& busInfo, + AddressSpaceManager& addrSpaceMgr) + : bus_(bus) + , busInfo_(busInfo) + , addrSpaceMgr_(addrSpaceMgr) {} + +SBP2LoginSession::~SBP2LoginSession() { + CancelPendingTimer(); + if (loginWriteHandle_) { + bus_.Cancel(loginWriteHandle_); + loginWriteHandle_ = {}; + } + if (reconnectWriteHandle_) { + bus_.Cancel(reconnectWriteHandle_); + reconnectWriteHandle_ = {}; + } + if (logoutWriteHandle_) { + bus_.Cancel(logoutWriteHandle_); + logoutWriteHandle_ = {}; + } + if (fetchAgentWriteHandle_) { + bus_.Cancel(fetchAgentWriteHandle_); + fetchAgentWriteHandle_ = {}; + } + if (doorbellWriteHandle_) { + bus_.Cancel(doorbellWriteHandle_); + doorbellWriteHandle_ = {}; + } + if (agentResetWriteHandle_) { + bus_.Cancel(agentResetWriteHandle_); + agentResetWriteHandle_ = {}; + } + if (unsolicitedStatusWriteHandle_) { + bus_.Cancel(unsolicitedStatusWriteHandle_); + unsolicitedStatusWriteHandle_ = {}; + } + if (busyTimeoutWriteHandle_) { + bus_.Cancel(busyTimeoutWriteHandle_); + busyTimeoutWriteHandle_ = {}; + } + ClearORBTracking(true); + lifetimeToken_.reset(); + ReleaseOwnedTimeoutQueue(); + DeallocateResources(); +} + +void SBP2LoginSession::SetWorkQueue(IODispatchQueue* queue) noexcept { + workQueue_ = queue; + if (timeoutQueue_ == nullptr) { + EnsureTimeoutQueue(); + } +} + +void SBP2LoginSession::SetTimeoutQueue(IODispatchQueue* queue) noexcept { + timeoutQueue_ = queue; + if (queue != nullptr) { + ReleaseOwnedTimeoutQueue(); + } else { + EnsureTimeoutQueue(); + } +} + +// --------------------------------------------------------------------------- +// Configuration +// --------------------------------------------------------------------------- + +void SBP2LoginSession::Configure(const SBP2TargetInfo& info) noexcept { + targetInfo_ = info; + configured_ = true; + + ASFW_LOG(SBP2, + "SBP2LoginSession: 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 SBP2LoginSession::Login() noexcept { + if (!configured_) { + ASFW_LOG(SBP2, "SBP2LoginSession::Login: not configured"); + return false; + } + + if (state_ == LoginState::LoggingIn || state_ == LoginState::LoggedIn) { + ASFW_LOG(SBP2, "SBP2LoginSession::Login: state=%s, ignoring", ToString(state_)); + return false; + } + + // Allocate address spaces for ORB/response/status on first login. + if (!AllocateResources()) { + ASFW_LOG(SBP2, "SBP2LoginSession::Login: resource allocation failed"); + SetState(LoginState::Failed); + return false; + } + + SetState(LoginState::LoggingIn); + loginGeneration_ = static_cast(busInfo_.GetGeneration().value); + loginNodeID_ = targetInfo_.targetNodeId; + + BuildLoginORB(); + + ASFW_LOG(SBP2, + "SBP2LoginSession::Login: sending login ORB to node 0x%04x gen=%u LUN=%u", + loginNodeID_, loginGeneration_, targetInfo_.lun); + + // Write the Login ORB address to the management agent. + const FW::Generation gen{loginGeneration_}; + const FW::NodeId node{static_cast(loginNodeID_ & 0x3Fu)}; + 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 response) { + if (auto self = weakSelf.lock()) { + self->OnLoginWriteComplete(requestGeneration, status, response); + } + }); + + if (!loginWriteHandle_) { + ASFW_LOG(SBP2, "SBP2LoginSession::Login: WriteBlock failed immediately"); + SetState(LoginState::Failed); + return false; + } + + // Start management timeout + StartLoginTimer(); + return true; +} + +// --------------------------------------------------------------------------- +// Logout +// --------------------------------------------------------------------------- + +bool SBP2LoginSession::Logout() noexcept { + if (state_ != LoginState::LoggedIn && state_ != LoginState::Suspended) { + ASFW_LOG(SBP2, "SBP2LoginSession::Logout: state=%s, ignoring", ToString(state_)); + return false; + } + + SetState(LoginState::LoggingOut); + BuildLogoutORB(); + + ASFW_LOG(SBP2, "SBP2LoginSession::Logout: sending logout ORB loginID=%u", loginID_); + + const FW::Generation gen{loginGeneration_}; + const FW::NodeId node{static_cast(loginNodeID_ & 0x3Fu)}; + 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 response) { + if (auto self = weakSelf.lock()) { + self->OnLogoutWriteComplete(requestGeneration, status, response); + } + }); + + if (!logoutWriteHandle_) { + ASFW_LOG(SBP2, "SBP2LoginSession::Logout: WriteBlock failed"); + SetState(LoginState::Failed); + return false; + } + + StartLogoutTimer(); + return true; +} + +// --------------------------------------------------------------------------- +// Reconnect +// --------------------------------------------------------------------------- + +bool SBP2LoginSession::Reconnect() noexcept { + if (state_ != LoginState::Suspended && state_ != LoginState::LoggedIn) { + ASFW_LOG(SBP2, "SBP2LoginSession::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, "SBP2LoginSession::Reconnect: resource allocation failed"); + SetState(LoginState::Failed); + return false; + } + + BuildReconnectORB(); + + ASFW_LOG(SBP2, + "SBP2LoginSession::Reconnect: sending reconnect ORB loginID=%u gen=%u", + loginID_, loginGeneration_); + + const FW::Generation gen{loginGeneration_}; + const FW::NodeId node{static_cast(loginNodeID_ & 0x3Fu)}; + 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 response) { + if (auto self = weakSelf.lock()) { + self->OnReconnectWriteComplete(requestGeneration, status, response); + } + }); + + if (!reconnectWriteHandle_) { + ASFW_LOG(SBP2, "SBP2LoginSession::Reconnect: WriteBlock failed, will retry"); + const std::weak_ptr weakSelf = weak_from_this(); + SubmitDelayedCallback(kLoginRetryDelayMs, [weakSelf]() { + if (auto self = weakSelf.lock()) { + self->OnReconnectTimeout(); + } + }); + reconnectTimerActive_ = true; + return true; // Will retry + } + + return true; +} + +// --------------------------------------------------------------------------- +// Bus Reset Handling +// --------------------------------------------------------------------------- + +void SBP2LoginSession::HandleBusReset(uint16_t newGeneration) noexcept { + ASFW_LOG(SBP2, + "SBP2LoginSession::HandleBusReset: state=%s newGen=%u loginGen=%u", + ToString(state_), newGeneration, loginGeneration_); + + switch (state_) { + case LoginState::LoggingIn: + // Login was in progress — cancel and retry after reset settles. + CancelLoginTimer(); + loginRetryCount_ = 0; + loginGeneration_ = newGeneration; + ClearORBTracking(true); + DeallocateResources(); + SetState(LoginState::Idle); + { + const std::weak_ptr weakSelf = weak_from_this(); + SubmitDelayedCallback(100, [weakSelf]() { + if (auto self = weakSelf.lock()) { + (void)self->Login(); + } + }); + } + break; + + case LoginState::LoggedIn: + // Transition to Suspended — wait for topology then reconnect. + CancelPendingTimer(); + ClearORBTracking(true); + DeallocateResources(); + SetState(LoginState::Suspended); + loginGeneration_ = newGeneration; + break; + + case LoginState::Reconnecting: + // Reconnect was in flight — retry. + reconnectTimerActive_ = false; + CancelPendingTimer(); + ClearORBTracking(true); + DeallocateResources(); + loginGeneration_ = newGeneration; + SetState(LoginState::Suspended); + { + const std::weak_ptr weakSelf = weak_from_this(); + SubmitDelayedCallback(100, [weakSelf]() { + if (auto self = weakSelf.lock()) { + (void)self->Reconnect(); + } + }); + } + break; + + case LoginState::LoggingOut: + // Logout in flight during bus reset — consider logged out. + CancelPendingTimer(); + DeallocateResources(); + ClearORBTracking(true); + SetState(LoginState::Idle); + break; + + case LoginState::Failed: + DeallocateResources(); + break; + + default: + break; + } +} + +// --------------------------------------------------------------------------- +// Accessors +// --------------------------------------------------------------------------- + +Async::FWAddress SBP2LoginSession::CommandBlockAgent() const noexcept { + return commandBlockAgent_; +} + +uint32_t SBP2LoginSession::ReconnectHoldSeconds() const noexcept { + return reconnectHold_ > 0 ? (1u << reconnectHold_) : 0; +} + +// --------------------------------------------------------------------------- +// Resource Allocation +// --------------------------------------------------------------------------- + +bool SBP2LoginSession::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 callback in case it was previously cleared by a reset path. + if (statusBlockHandle_ != 0) { + const std::weak_ptr weakSelf = weak_from_this(); + // AddressSpaceManager dispatches outside its lock, so never capture raw this. + addrSpaceMgr_.SetRemoteWriteCallback( + statusBlockHandle_, + [weakSelf](uint64_t /*handle*/, uint32_t offset, std::span payload) { + if (auto self = weakSelf.lock()) { + self->OnStatusBlockRemoteWrite(offset, payload); + } + }); + } + return true; // Already allocated + } + + if (anyResourceAllocated) { + ASFW_LOG(SBP2, + "SBP2LoginSession: inconsistent resource state before allocation " + "loginORB=0x%llu loginResp=0x%llu status=0x%llu reconnect=0x%llu logout=0x%llu", + static_cast(loginORBHandle_), + static_cast(loginResponseHandle_), + static_cast(statusBlockHandle_), + static_cast(reconnectORBHandle_), + static_cast(logoutORBHandle_)); + DeallocateResources(); + } + + if (!AllocateLoginORBAddressSpace()) { + return false; + } + if (!AllocateLoginResponseAddressSpace()) { + ASFW_LOG(SBP2, "SBP2LoginSession: rollback login resource allocation at login response stage"); + DeallocateResources(); + return false; + } + if (!AllocateStatusBlockAddressSpace()) { + ASFW_LOG(SBP2, "SBP2LoginSession: rollback login resource allocation at status block stage"); + DeallocateResources(); + return false; + } + if (!AllocateReconnectORBAddressSpace()) { + ASFW_LOG(SBP2, "SBP2LoginSession: rollback login resource allocation at reconnect ORB stage"); + DeallocateResources(); + return false; + } + if (!AllocateLogoutORBAddressSpace()) { + ASFW_LOG(SBP2, "SBP2LoginSession: rollback login resource allocation at logout ORB stage"); + DeallocateResources(); + return false; + } + + // Register a callback for status block writes — the device writes status + // here to signal login/reconnect/logout completion (and ORB completion + // in Step 2). + const std::weak_ptr weakSelf = weak_from_this(); + // AddressSpaceManager dispatches outside its lock, so never capture raw this. + addrSpaceMgr_.SetRemoteWriteCallback( + statusBlockHandle_, + [weakSelf](uint64_t /*handle*/, uint32_t offset, std::span payload) { + if (auto self = weakSelf.lock()) { + self->OnStatusBlockRemoteWrite(offset, payload); + } + }); + + ASFW_LOG(SBP2, "SBP2LoginSession: all address spaces allocated"); + return true; +} + +void SBP2LoginSession::DeallocateResources() noexcept { + ClearORBTracking(true); + + 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 SBP2LoginSession::AllocateLoginORBAddressSpace() noexcept { + // Login ORB is 32 bytes, readable by target device. + auto kr = addrSpaceMgr_.AllocateAddressRangeAuto( + this, 0xFFFF, Wire::LoginORB::kSize, + &loginORBHandle_, &loginORBMeta_); + if (kr != kIOReturnSuccess) { + ASFW_LOG(SBP2, "SBP2LoginSession: failed to allocate login ORB address space: 0x%08x", kr); + return false; + } + addrSpaceMgr_.SetDebugLabel(loginORBHandle_, "sbp2-login-orb"); + return true; +} + +bool SBP2LoginSession::AllocateLoginResponseAddressSpace() noexcept { + // Login response is 16 bytes, writable by target device. + auto kr = addrSpaceMgr_.AllocateAddressRangeAuto( + this, 0xFFFF, Wire::LoginResponse::kSize, + &loginResponseHandle_, &loginResponseMeta_); + if (kr != kIOReturnSuccess) { + ASFW_LOG(SBP2, "SBP2LoginSession: failed to allocate login response address space: 0x%08x", kr); + return false; + } + addrSpaceMgr_.SetDebugLabel(loginResponseHandle_, "sbp2-login-response"); + return true; +} + +bool SBP2LoginSession::AllocateStatusBlockAddressSpace() noexcept { + // Status block is up to 32 bytes, writable by target device. + auto kr = addrSpaceMgr_.AllocateAddressRangeAuto( + this, 0xFFFF, Wire::StatusBlock::kMaxSize, + &statusBlockHandle_, &statusBlockMeta_); + if (kr != kIOReturnSuccess) { + ASFW_LOG(SBP2, "SBP2LoginSession: failed to allocate status block address space: 0x%08x", kr); + return false; + } + addrSpaceMgr_.SetDebugLabel(statusBlockHandle_, "sbp2-status-fifo"); + return true; +} + +bool SBP2LoginSession::AllocateReconnectORBAddressSpace() noexcept { + auto kr = addrSpaceMgr_.AllocateAddressRangeAuto( + this, 0xFFFF, Wire::ReconnectORB::kSize, + &reconnectORBHandle_, &reconnectORBMeta_); + if (kr != kIOReturnSuccess) { + ASFW_LOG(SBP2, "SBP2LoginSession: failed to allocate reconnect ORB address space: 0x%08x", kr); + return false; + } + addrSpaceMgr_.SetDebugLabel(reconnectORBHandle_, "sbp2-reconnect-orb"); + return true; +} + +bool SBP2LoginSession::AllocateLogoutORBAddressSpace() noexcept { + auto kr = addrSpaceMgr_.AllocateAddressRangeAuto( + this, 0xFFFF, Wire::LogoutORB::kSize, + &logoutORBHandle_, &logoutORBMeta_); + if (kr != kIOReturnSuccess) { + ASFW_LOG(SBP2, "SBP2LoginSession: failed to allocate logout ORB address space: 0x%08x", kr); + return false; + } + addrSpaceMgr_.SetDebugLabel(logoutORBHandle_, "sbp2-logout-orb"); + return true; +} + +// --------------------------------------------------------------------------- +// ORB Construction +// --------------------------------------------------------------------------- + +void SBP2LoginSession::BuildLoginORB() noexcept { + std::memset(&loginORBBuffer_, 0, sizeof(loginORBBuffer_)); + + // Get local node ID for filling address fields. + const uint16_t localNode = + NormalizeBusNodeID(static_cast(busInfo_.GetLocalNodeID().value)); + + // Login response address: nodeID in upper 16 bits of addressHi. + const uint32_t responseAddrHi = ToBE32( + ComposeBusAddressHi(localNode, loginResponseMeta_.addressHi)); + const uint32_t responseAddrLo = ToBE32(loginResponseMeta_.addressLo); + + // Status FIFO address. + const uint32_t statusAddrHi = ToBE32( + ComposeBusAddressHi(localNode, statusBlockMeta_.addressHi)); + const uint32_t statusAddrLo = ToBE32(statusBlockMeta_.addressLo); + + // Fill login ORB fields. + loginORBBuffer_.loginResponseAddressHi = responseAddrHi; + loginORBBuffer_.loginResponseAddressLo = responseAddrLo; + loginORBBuffer_.options = static_cast( + Options::kLoginNotify | Options::kExclusiveLogin); + loginORBBuffer_.lun = ToBE16(targetInfo_.lun); + loginORBBuffer_.passwordLength = 0; + loginORBBuffer_.loginResponseLength = ToBE16(sizeof(Wire::LoginResponse)); + loginORBBuffer_.statusFIFOAddressHi = statusAddrHi; + loginORBBuffer_.statusFIFOAddressLo = statusAddrLo; + + // Write login ORB data to address space so device can read it. + addrSpaceMgr_.WriteLocalData( + this, loginORBHandle_, 0, + std::span{reinterpret_cast(&loginORBBuffer_), + sizeof(loginORBBuffer_)}); + + // Build the 8-byte management agent write payload: ORB address in big-endian. + // Format: [nodeID(2)][addressHi(2)][addressLo(4)] + 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 = ToBE32(loginORBMeta_.addressLo); + std::memcpy(&loginORBAddressBE_[4], &orbAddrLoBE, sizeof(uint32_t)); + + ASFW_LOG(SBP2, + "SBP2LoginSession::BuildLoginORB: mgmt=0x%08x payload=%02x%02x:%02x%02x:%02x%02x%02x%02x " + "ORB at %04x:%08x, response at %04x:%08x, status at %04x:%08x, LUN=%u", + ManagementAgentAddressLo(targetInfo_.managementAgentOffset), + loginORBAddressBE_[0], loginORBAddressBE_[1], + loginORBAddressBE_[2], loginORBAddressBE_[3], + loginORBAddressBE_[4], loginORBAddressBE_[5], + loginORBAddressBE_[6], loginORBAddressBE_[7], + localNode, loginORBMeta_.addressLo, + localNode, loginResponseMeta_.addressLo, + localNode, statusBlockMeta_.addressLo, + targetInfo_.lun); +} + +void SBP2LoginSession::BuildReconnectORB() noexcept { + std::memset(&reconnectORBBuffer_, 0, sizeof(reconnectORBBuffer_)); + + const uint16_t localNode = + NormalizeBusNodeID(static_cast(busInfo_.GetLocalNodeID().value)); + + // Reconnect ORB: options = reconnect (3) | notify + reconnectORBBuffer_.options = Options::kReconnectNotify; + reconnectORBBuffer_.loginID = ToBE16(loginID_); + + // Status FIFO address — reuse the dedicated status block address space. + const uint32_t statusAddrHi = ToBE32( + ComposeBusAddressHi(localNode, statusBlockMeta_.addressHi)); + const uint32_t statusAddrLo = ToBE32(statusBlockMeta_.addressLo); + reconnectORBBuffer_.statusFIFOAddressHi = statusAddrHi; + reconnectORBBuffer_.statusFIFOAddressLo = statusAddrLo; + + // Write reconnect ORB data. + addrSpaceMgr_.WriteLocalData( + this, reconnectORBHandle_, 0, + std::span{reinterpret_cast(&reconnectORBBuffer_), + sizeof(reconnectORBBuffer_)}); + + // Build management agent write payload. + 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 = ToBE32(reconnectORBMeta_.addressLo); + std::memcpy(&reconnectORBAddressBE_[4], &addrLoBE, sizeof(uint32_t)); +} + +void SBP2LoginSession::BuildLogoutORB() noexcept { + std::memset(&logoutORBBuffer_, 0, sizeof(logoutORBBuffer_)); + + const uint16_t localNode = + NormalizeBusNodeID(static_cast(busInfo_.GetLocalNodeID().value)); + + logoutORBBuffer_.options = Options::kLogoutNotify; + logoutORBBuffer_.loginID = ToBE16(loginID_); + + // Status FIFO address — reuse the dedicated status block address space. + const uint32_t statusAddrHi = ToBE32( + ComposeBusAddressHi(localNode, statusBlockMeta_.addressHi)); + const uint32_t statusAddrLo = ToBE32(statusBlockMeta_.addressLo); + logoutORBBuffer_.statusFIFOAddressHi = statusAddrHi; + logoutORBBuffer_.statusFIFOAddressLo = statusAddrLo; + + 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 = ToBE32(logoutORBMeta_.addressLo); + std::memcpy(&logoutORBAddressBE_[4], &addrLoBE, sizeof(uint32_t)); +} + +void SBP2LoginSession::RefreshCommandBlockAgentAddresses() noexcept { + commandBlockAgent_.nodeID = loginNodeID_; + fetchAgentAddress_ = Async::FWAddress{ + Async::FWAddress::QualifiedAddressParts{ + .addressHi = commandBlockAgent_.addressHi, + .addressLo = commandBlockAgent_.addressLo + Wire::CommandBlockAgentOffsets::kFetchAgent, + .nodeID = loginNodeID_ + } + }; + doorbellAddress_ = Async::FWAddress{ + Async::FWAddress::QualifiedAddressParts{ + .addressHi = commandBlockAgent_.addressHi, + .addressLo = commandBlockAgent_.addressLo + Wire::CommandBlockAgentOffsets::kDoorbell, + .nodeID = loginNodeID_ + } + }; + agentResetAddress_ = Async::FWAddress{ + Async::FWAddress::QualifiedAddressParts{ + .addressHi = commandBlockAgent_.addressHi, + .addressLo = commandBlockAgent_.addressLo + Wire::CommandBlockAgentOffsets::kAgentReset, + .nodeID = loginNodeID_ + } + }; + unsolicitedStatusAddress_ = Async::FWAddress{ + Async::FWAddress::QualifiedAddressParts{ + .addressHi = commandBlockAgent_.addressHi, + .addressLo = commandBlockAgent_.addressLo + Wire::CommandBlockAgentOffsets::kUnsolicitedStatusEnable, + .nodeID = loginNodeID_ + } + }; +} + +// --------------------------------------------------------------------------- +// Completion Handlers +// --------------------------------------------------------------------------- + +void SBP2LoginSession::OnLoginWriteComplete(uint16_t expectedGeneration, + Async::AsyncStatus status, + std::span response) noexcept { + if (expectedGeneration != loginGeneration_ || state_ != LoginState::LoggingIn) { + return; + } + + CancelLoginTimer(); + + if (status != Async::AsyncStatus::kSuccess) { + ASFW_LOG(SBP2, "SBP2LoginSession::OnLoginWriteComplete: status=%s, retrying (%u/%u)", + Async::ToString(status), loginRetryCount_ + 1, kLoginRetryMax); + + if (loginRetryCount_ < kLoginRetryMax) { + loginRetryCount_++; + const std::weak_ptr weakSelf = weak_from_this(); + SubmitDelayedCallback(kLoginRetryDelayMs, [weakSelf]() { + if (auto self = weakSelf.lock()) { + self->loginGeneration_ = + static_cast(self->busInfo_.GetGeneration().value); + self->loginNodeID_ = self->targetInfo_.targetNodeId; + self->SetState(LoginState::Idle); + (void)self->Login(); + } + }); + return; + } + + ASFW_LOG(SBP2, "SBP2LoginSession: login retries exhausted"); + SetState(LoginState::Failed); + + if (loginCallback_) { + LoginCompleteParams params{}; + params.status = -1; + params.generation = loginGeneration_; + loginCallback_(params); + } + return; + } + + // Management agent write ACK'd. The device will now: + // 1. Fetch the ORB via read from our address space + // 2. Process the login + // 3. Write login response to our address space + // 4. Write status block to our status FIFO + // + // We wait for the status block write callback (OnStatusBlockRemoteWrite) + // before reading the login response. Restart the timer for the device + // processing window. + ASFW_LOG(SBP2, "SBP2LoginSession: management agent write ACK'd, waiting for status block"); + StartLoginTimer(); +} + +void SBP2LoginSession::OnLoginTimeout() noexcept { + loginTimerActive_ = false; + + if (state_ != LoginState::LoggingIn) { + return; // Already handled + } + + ASFW_LOG(SBP2, + "SBP2LoginSession: login timeout (%u/%u) waiting for target node=0x%04x " + "to read label=sbp2-login-orb at %04x:%08x and write label=sbp2-status-fifo at %04x:%08x", + loginRetryCount_ + 1, + kLoginRetryMax, + loginNodeID_, + loginORBMeta_.addressHi, + loginORBMeta_.addressLo, + statusBlockMeta_.addressHi, + statusBlockMeta_.addressLo); + + 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 SBP2LoginSession::OnReconnectWriteComplete(uint16_t expectedGeneration, + Async::AsyncStatus status, + std::span response) noexcept { + if (expectedGeneration != loginGeneration_ || state_ != LoginState::Reconnecting) { + return; + } + + reconnectTimerActive_ = false; + + if (status != Async::AsyncStatus::kSuccess) { + ASFW_LOG(SBP2, "SBP2LoginSession::OnReconnectWriteComplete: status=%s, retrying", + Async::ToString(status)); + + const std::weak_ptr weakSelf = weak_from_this(); + SubmitDelayedCallback(100, [weakSelf]() { + if (auto self = weakSelf.lock()) { + (void)self->Reconnect(); + } + }); + return; + } + + // Reconnect ORB write ACK'd. Wait for status block from device. + ASFW_LOG(SBP2, "SBP2LoginSession: reconnect write ACK'd, waiting for status block"); + StartReconnectTimer(); +} + +void SBP2LoginSession::OnReconnectTimeout() noexcept { + reconnectTimerActive_ = false; + + if (state_ != LoginState::Reconnecting) { + return; + } + + ASFW_LOG(SBP2, "SBP2LoginSession: reconnect timeout, falling back to full login"); + SetState(LoginState::Idle); + (void)Login(); +} + +void SBP2LoginSession::OnLogoutWriteComplete(uint16_t expectedGeneration, + Async::AsyncStatus status, + std::span response) noexcept { + if (expectedGeneration != loginGeneration_ || state_ != LoginState::LoggingOut) { + return; + } + + logoutTimerActive_ = false; + + if (status != Async::AsyncStatus::kSuccess) { + ASFW_LOG(SBP2, "SBP2LoginSession::OnLogoutWriteComplete: status=%s", + Async::ToString(status)); + + CancelPendingTimer(); + logoutTimerActive_ = false; + const uint16_t oldLoginID = loginID_; + loginID_ = 0; + SetState(LoginState::Idle); + + ASFW_LOG(SBP2, "SBP2LoginSession: logout transport failed (was loginID=%u)", oldLoginID); + + if (logoutCallback_) { + LogoutCompleteParams params{}; + params.status = -1; + params.generation = loginGeneration_; + logoutCallback_(params); + } + return; + } + + ASFW_LOG(SBP2, "SBP2LoginSession: logout management agent write ACK'd, waiting for status block"); + StartLogoutTimer(); +} + +void SBP2LoginSession::OnLogoutTimeout() noexcept { + logoutTimerActive_ = false; + if (state_ != LoginState::LoggingOut) { + return; + } + + ASFW_LOG(SBP2, "SBP2LoginSession: 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 SBP2LoginSession::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, + "SBP2LoginSession::OnStatusBlockRemoteWrite: state=%s offset=%u len=%u " + "src=%u resp=%u dead=%u sbpStatus=%u", + ToString(state_), offset, len, + block.Source(), block.Response(), block.DeadBit(), block.sbpStatus); + + // Dispatch to state-specific handler. + switch (state_) { + case LoginState::LoggingIn: + CancelLoginTimer(); + CompleteLoginFromStatusBlock(block, len); + break; + + case LoginState::Reconnecting: + reconnectTimerActive_ = false; + CancelPendingTimer(); + CompleteReconnectFromStatusBlock(block, len); + break; + + case LoginState::LoggingOut: + logoutTimerActive_ = false; + CancelPendingTimer(); + CompleteLogoutFromStatusBlock(block, len); + break; + + case LoginState::LoggedIn: + // Unsolicited status or ORB completion — forward to callback. + ProcessStatusBlock(block, len); + break; + + default: + ASFW_LOG(SBP2, "SBP2LoginSession: unexpected status block in state %s", ToString(state_)); + break; + } +} + +void SBP2LoginSession::CompleteLoginFromStatusBlock(const Wire::StatusBlock& block, + uint32_t length) noexcept { + if (block.sbpStatus != Wire::SBPStatus::kNoAdditionalInfo) { + ASFW_LOG(SBP2, + "SBP2LoginSession: login failed — sbpStatus=%u, retrying (%u/%u)", + block.sbpStatus, loginRetryCount_ + 1, kLoginRetryMax); + + if (loginRetryCount_ < kLoginRetryMax) { + loginRetryCount_++; + const std::weak_ptr weakSelf = weak_from_this(); + SubmitDelayedCallback(kLoginRetryDelayMs, [weakSelf]() { + if (auto self = weakSelf.lock()) { + self->loginGeneration_ = + static_cast(self->busInfo_.GetGeneration().value); + self->loginNodeID_ = self->targetInfo_.targetNodeId; + self->SetState(LoginState::Idle); + (void)self->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 that the device wrote to + // our address space. + std::vector responseData; + auto kr = addrSpaceMgr_.ReadIncomingData( + this, loginResponseHandle_, 0, sizeof(Wire::LoginResponse), &responseData); + + if (kr != kIOReturnSuccess || responseData.size() < sizeof(Wire::LoginResponse)) { + ASFW_LOG(SBP2, + "SBP2LoginSession: failed to read login response (kr=0x%08x, len=%zu)", + kr, responseData.size()); + + if (loginRetryCount_ < kLoginRetryMax) { + loginRetryCount_++; + const std::weak_ptr weakSelf = weak_from_this(); + SubmitDelayedCallback(kLoginRetryDelayMs, [weakSelf]() { + if (auto self = weakSelf.lock()) { + self->loginGeneration_ = + static_cast(self->busInfo_.GetGeneration().value); + self->loginNodeID_ = self->targetInfo_.targetNodeId; + self->SetState(LoginState::Idle); + (void)self->Login(); + } + }); + return; + } + + SetState(LoginState::Failed); + if (loginCallback_) { + LoginCompleteParams params{}; + params.status = -1; + params.statusBlock = block; + params.statusBlockLength = length; + params.generation = loginGeneration_; + loginCallback_(params); + } + return; + } + + // Parse login response. + Wire::LoginResponse resp{}; + std::memcpy(&resp, responseData.data(), sizeof(resp)); + + loginID_ = FromBE16(resp.loginID); + reconnectHold_ = FromBE16(resp.reconnectHold); + loginResponse_ = resp; + + // Extract command block agent address. + const uint32_t cbaHi = FromBE32(resp.commandBlockAgentAddressHi); + const uint32_t cbaLo = FromBE32(resp.commandBlockAgentAddressLo); + commandBlockAgent_ = Async::FWAddress{ + Async::FWAddress::QualifiedAddressParts{ + .addressHi = static_cast(cbaHi & 0xFFFFu), + .addressLo = cbaLo, + .nodeID = loginNodeID_ + } + }; + + loginRetryCount_ = 0; + SetState(LoginState::LoggedIn); + + // Compute CBA-derived addresses for fetch agent, doorbell, agent reset, + // and unsolicited status enable. + RefreshCommandBlockAgentAddresses(); + + // If unsolicited status was requested while not logged in, enable it after + // all CBA-derived registers are available. + if (unsolicitedStatusRequested_) { + unsolicitedStatusRequested_ = false; + EnableUnsolicitedStatus(); + } + + ASFW_LOG(SBP2, + "SBP2LoginSession: login successful — loginID=%u, CBA=%04x:%08x, " + "reconnectHold=2^%u=%us", + loginID_, + commandBlockAgent_.addressHi, commandBlockAgent_.addressLo, + reconnectHold_, ReconnectHoldSeconds()); + + WriteBusyTimeout(); + + if (loginCallback_) { + LoginCompleteParams params{}; + params.status = 0; + params.loginResponse = loginResponse_; + params.statusBlock = block; + params.statusBlockLength = length; + params.generation = loginGeneration_; + loginCallback_(params); + } +} + +void SBP2LoginSession::CompleteReconnectFromStatusBlock(const Wire::StatusBlock& block, + uint32_t length) noexcept { + if (block.sbpStatus != Wire::SBPStatus::kNoAdditionalInfo) { + ASFW_LOG(SBP2, + "SBP2LoginSession: reconnect failed — sbpStatus=%u, falling back to full login", + block.sbpStatus); + + SetState(LoginState::Idle); + (void)Login(); + return; + } + + SetState(LoginState::LoggedIn); + RefreshCommandBlockAgentAddresses(); + ASFW_LOG(SBP2, "SBP2LoginSession: reconnect successful — loginID=%u", loginID_); + + WriteBusyTimeout(); + + // If unsolicited status was requested while not logged in, enable it now. + 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 SBP2LoginSession::CompleteLogoutFromStatusBlock(const Wire::StatusBlock& block, + uint32_t length) noexcept { + const uint16_t oldLoginID = loginID_; + loginID_ = 0; + SetState(LoginState::Idle); + + ASFW_LOG(SBP2, "SBP2LoginSession: logout complete (was loginID=%u)", oldLoginID); + + if (logoutCallback_) { + LogoutCompleteParams params{}; + params.status = 0; + params.generation = loginGeneration_; + logoutCallback_(params); + } +} + +void SBP2LoginSession::ProcessStatusBlock(const Wire::StatusBlock& block, + uint32_t length) noexcept { + // Distinguish unsolicited vs solicited status. + // Unsolicited: (details & 0xC0) == 0x80 (source bit set, resp == 0) + const bool isUnsolicited = (block.details & 0xC0) == 0x80; + + if (statusCallback_) { + statusCallback_(block, length); + } + + if (isUnsolicited) { + // Re-enable unsolicited status so device can send more + EnableUnsolicitedStatus(); + return; + } + + const uint64_t orbKey = MakeORBKey(FromBE16(block.orbOffsetHi), FromBE32(block.orbOffsetLo)); + const auto it = outstandingORBs_.find(orbKey); + if (it == outstandingORBs_.end()) { + ASFW_LOG(SBP2, + "SBP2LoginSession::ProcessStatusBlock: unmatched ORB status hi=%04x lo=%08x", + FromBE16(block.orbOffsetHi), + FromBE32(block.orbOffsetLo)); + return; + } + + SBP2CommandORB* orb = it->second; + outstandingORBs_.erase(it); + if (orb != nullptr) { + orb->CancelTimer(); + auto cb = orb->GetCompletionCallback(); + if (cb) { + cb(0, block.sbpStatus); + } + } +} + +// --------------------------------------------------------------------------- +// Internal Helpers +// --------------------------------------------------------------------------- + +void SBP2LoginSession::SetState(LoginState newState) noexcept { + if (state_ != newState) { + ASFW_LOG(SBP2, "SBP2LoginSession: state %s -> %s", ToString(state_), ToString(newState)); + state_ = newState; + } +} + +void SBP2LoginSession::StartLoginTimer() noexcept { + loginTimerActive_ = true; + const std::weak_ptr weakSelf = weak_from_this(); + SubmitDelayedCallback(targetInfo_.managementTimeoutMs, [weakSelf]() { + if (auto self = weakSelf.lock()) { + self->OnLoginTimeout(); + } + }); +} + +void SBP2LoginSession::StartReconnectTimer() noexcept { + reconnectTimerActive_ = true; + const std::weak_ptr weakSelf = weak_from_this(); + SubmitDelayedCallback(targetInfo_.managementTimeoutMs + 1000, [weakSelf]() { + if (auto self = weakSelf.lock()) { + self->OnReconnectTimeout(); + } + }); +} + +void SBP2LoginSession::StartLogoutTimer() noexcept { + logoutTimerActive_ = true; + const std::weak_ptr weakSelf = weak_from_this(); + SubmitDelayedCallback(targetInfo_.managementTimeoutMs, [weakSelf]() { + if (auto self = weakSelf.lock()) { + self->OnLogoutTimeout(); + } + }); +} + +void SBP2LoginSession::CancelLoginTimer() noexcept { + loginTimerActive_ = false; + CancelPendingTimer(); +} + +void SBP2LoginSession::CancelPendingTimer() noexcept { + delayedCallbackGeneration_.fetch_add(1, std::memory_order_acq_rel); +} + +void SBP2LoginSession::EnsureTimeoutQueue() noexcept { + if (timeoutQueue_ != nullptr || workQueue_ == nullptr) { + return; + } + +#ifdef ASFW_HOST_TEST + ownedTimeoutQueue_ = std::make_unique(); + if (ownedTimeoutQueue_ != nullptr && + workQueue_->UsesManualDispatchForTesting()) { + ownedTimeoutQueue_->SetManualDispatchForTesting(true); + } + timeoutQueue_ = ownedTimeoutQueue_.get(); +#else + IODispatchQueue* queue = nullptr; + const kern_return_t kr = IODispatchQueue::Create("com.asfw.sbp2.timeout", 0, 0, &queue); + if (kr != kIOReturnSuccess || queue == nullptr) { + ASFW_LOG(SBP2, + "SBP2LoginSession: failed to create timeout queue (kr=0x%08x), falling back to workQueue", + kr); + timeoutQueue_ = workQueue_; + return; + } + ownedTimeoutQueue_ = queue; + timeoutQueue_ = ownedTimeoutQueue_; +#endif +} + +void SBP2LoginSession::ReleaseOwnedTimeoutQueue() noexcept { +#ifdef ASFW_HOST_TEST + IODispatchQueue* ownedQueue = ownedTimeoutQueue_.get(); + ownedTimeoutQueue_.reset(); + if (timeoutQueue_ == ownedQueue) { + timeoutQueue_ = nullptr; + } +#else + IODispatchQueue* ownedQueue = ownedTimeoutQueue_; + if (ownedTimeoutQueue_ != nullptr) { + ownedTimeoutQueue_->release(); + ownedTimeoutQueue_ = nullptr; + } + if (timeoutQueue_ == ownedQueue) { + timeoutQueue_ = nullptr; + } +#endif +} + +IODispatchQueue* SBP2LoginSession::EffectiveTimeoutQueue() const noexcept { + if (timeoutQueue_ != nullptr) { + return timeoutQueue_; + } + return workQueue_; +} + +void SBP2LoginSession::SubmitDelayedCallback(uint64_t delayMs, + std::function callback) noexcept { + IODispatchQueue* delayQueue = timeoutQueue_ != nullptr ? timeoutQueue_ : workQueue_; + if (workQueue_ == nullptr || delayQueue == nullptr || !callback) { + return; + } + + const uint64_t expectedGeneration = + delayedCallbackGeneration_.fetch_add(1, std::memory_order_acq_rel) + 1ULL; + const std::weak_ptr weakLifetime = lifetimeToken_; + const std::weak_ptr weakSelf = weak_from_this(); + const uint64_t delayNs = delayMs * 1'000'000ULL; + IODispatchQueue* bounceQueue = workQueue_; + + DispatchAfterCompat(delayQueue, + delayNs, + [weakLifetime, + weakSelf, + expectedGeneration, + bounceQueue, + cb = std::move(callback)]() mutable { + if (weakLifetime.expired()) { + return; + } + DispatchAsyncCompat(bounceQueue, + [weakLifetime, + weakSelf, + expectedGeneration, + cb = std::move(cb)]() mutable { + if (weakLifetime.expired()) { + return; + } + auto self = weakSelf.lock(); + if (!self) { + return; + } + if (self->delayedCallbackGeneration_.load(std::memory_order_acquire) != expectedGeneration) { + return; + } + cb(); + }); + }); +} + +uint64_t SBP2LoginSession::MakeORBKey(uint16_t addressHi, uint32_t addressLo) noexcept { + return (static_cast(addressHi) << 32) | static_cast(addressLo); +} + +uint64_t SBP2LoginSession::MakeORBKey(const Async::FWAddress& address) noexcept { + return MakeORBKey(address.addressHi, address.addressLo); +} + +void SBP2LoginSession::ClearORBTracking(bool cancelTimers) noexcept { + const bool shouldCancelFetchAgentWrite = + cancelTimers && fetchAgentWriteInUse_ && static_cast(fetchAgentWriteHandle_); + const Async::AsyncHandle fetchAgentWriteHandle = fetchAgentWriteHandle_; + const bool shouldCancelDoorbellWrite = + cancelTimers && doorbellInProgress_ && static_cast(doorbellWriteHandle_); + const Async::AsyncHandle doorbellWriteHandle = doorbellWriteHandle_; + + if (cancelTimers) { + for (auto& [key, orb] : outstandingORBs_) { + if (orb != nullptr) { + orb->CancelTimer(); + orb->SetAppended(false); + } + } + if (activeFetchAgentORB_ != nullptr) { + activeFetchAgentORB_->CancelTimer(); + } + for (auto* orb : pendingImmediateORBs_) { + if (orb != nullptr) { + orb->CancelTimer(); + } + } + } + + outstandingORBs_.clear(); + pendingImmediateORBs_.clear(); + chainTailORB_ = nullptr; + activeFetchAgentORB_ = nullptr; + fetchAgentWriteHandle_ = {}; + fetchAgentWriteInUse_ = false; + doorbellWriteHandle_ = {}; + doorbellInProgress_ = false; + doorbellRingAgain_ = false; + + if (shouldCancelFetchAgentWrite) { + (void)bus_.Cancel(fetchAgentWriteHandle); + } + if (shouldCancelDoorbellWrite) { + (void)bus_.Cancel(doorbellWriteHandle); + } +} + +// --------------------------------------------------------------------------- +// ORB Submission +// --------------------------------------------------------------------------- + +bool SBP2LoginSession::SubmitORB(SBP2CommandORB* orb) noexcept { + if (state_ != LoginState::LoggedIn) { + ASFW_LOG(SBP2, "SBP2LoginSession::SubmitORB: state=%s, rejecting", ToString(state_)); + return false; + } + + if (orb == nullptr || !orb->IsValid() || orb->IsAppended()) { + ASFW_LOG(SBP2, "SBP2LoginSession::SubmitORB: invalid ORB (null=%d, valid=%d, appended=%d)", + orb == nullptr, + orb != nullptr && orb->IsValid(), + orb != nullptr && orb->IsAppended()); + return false; + } + + // Fetch agent and doorbell addresses are computed at login time. + + const uint16_t localNode = + NormalizeBusNodeID(static_cast(busInfo_.GetLocalNodeID().value)); + const FW::FwSpeed speed = busInfo_.GetSpeed( + FW::NodeId{static_cast(loginNodeID_ & 0x3Fu)}); + + // Max payload log: derive from maxPayloadSize_ (bytes → log2(quadlets)). + // Capped at 15 (max 2^15 = 32768 quadlets = 128KB). + uint16_t maxPayloadLog = 0; + { + uint16_t payloadBytes = maxPayloadSize_; + if (payloadBytes > 4096) payloadBytes = 4096; + uint16_t quadlets = payloadBytes / 4; + if (quadlets > 0) { + maxPayloadLog = 0; + while ((1u << maxPayloadLog) < quadlets && maxPayloadLog < 15) { + maxPayloadLog++; + } + } + } + + const kern_return_t prepareKr = orb->PrepareForExecution(localNode, speed, maxPayloadLog); + if (prepareKr != kIOReturnSuccess) { + ASFW_LOG(SBP2, "SBP2LoginSession::SubmitORB: PrepareForExecution failed: 0x%08x", + prepareKr); + return false; + } + + orb->SetFetchAgentWriteRetries(testFetchAgentWriteRetries_); + orb->SetAppended(true); + outstandingORBs_[MakeORBKey(orb->GetORBAddress())] = orb; + + const bool isImmediate = (orb->GetFlags() & SBP2CommandORB::kImmediate) != 0; + + if (isImmediate) { + chainTailORB_ = orb; + + if (fetchAgentWriteInUse_) { + pendingImmediateORBs_.push_back(orb); + ASFW_LOG(SBP2, "SBP2LoginSession::SubmitORB: fetch agent busy, deferring ORB"); + return true; + } + + return AppendORBImmediate(orb); + } else { + // Chained: link to last ORB, ring doorbell + if (!AppendORB(orb)) { + return false; + } + RingDoorbell(); + if (activeFetchAgentORB_ != orb) { + StartSubmittedORBTimer(orb); + } + } + + return true; +} + +bool SBP2LoginSession::AppendORBImmediate(SBP2CommandORB* orb) noexcept { + if (orb == nullptr || fetchAgentWriteInUse_) { + return false; + } + + // Build 8-byte ORB address in big-endian + // Format: [nodeID(2)][addressHi(2)][addressLo(4)] + const uint16_t localNode = + NormalizeBusNodeID(static_cast(busInfo_.GetLocalNodeID().value)); + 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 = ToBE32(orbAddr.addressLo); + std::memcpy(&fetchAgentWriteData_[4], &addrLoBE, sizeof(uint32_t)); + + activeFetchAgentORB_ = orb; + fetchAgentWriteInUse_ = true; + + const FW::Generation gen{loginGeneration_}; + const FW::NodeId node{static_cast(loginNodeID_ & 0x3Fu)}; + const FW::FwSpeed speed = busInfo_.GetSpeed(node); + + const std::weak_ptr weakSelf = weak_from_this(); + fetchAgentWriteHandle_ = bus_.WriteBlock( + gen, node, fetchAgentAddress_, + std::span{fetchAgentWriteData_.data(), fetchAgentWriteData_.size()}, + speed, + [weakSelf, requestGeneration = loginGeneration_](Async::AsyncStatus status, + std::span response) { + if (auto self = weakSelf.lock()) { + self->OnFetchAgentWriteComplete(requestGeneration, status, response); + } + }); + + if (!fetchAgentWriteHandle_) { + ASFW_LOG(SBP2, "SBP2LoginSession::AppendORBImmediate: WriteBlock failed"); + fetchAgentWriteInUse_ = false; + activeFetchAgentORB_ = nullptr; + FailSubmittedORB(orb, -1, Wire::SBPStatus::kUnspecifiedError); + return false; + } + + ASFW_LOG(SBP2, + "SBP2LoginSession::AppendORBImmediate: wrote ORB addr %04x:%08x to fetch agent", + localNode, orbAddr.addressLo); + return true; +} + +void SBP2LoginSession::StartSubmittedORBTimer(SBP2CommandORB* orb) noexcept { + if (orb == nullptr) { + return; + } + + IODispatchQueue* timeoutQueue = timeoutQueue_ != nullptr ? timeoutQueue_ : workQueue_; + orb->StartTimer(workQueue_, timeoutQueue); +} + +void SBP2LoginSession::FailSubmittedORB(SBP2CommandORB* orb, + int transportStatus, + uint8_t sbpStatus) noexcept { + if (orb == nullptr) { + return; + } + + const auto key = MakeORBKey(orb->GetORBAddress()); + outstandingORBs_.erase(key); + pendingImmediateORBs_.erase( + std::remove(pendingImmediateORBs_.begin(), pendingImmediateORBs_.end(), orb), + pendingImmediateORBs_.end()); + if (activeFetchAgentORB_ == orb) { + activeFetchAgentORB_ = nullptr; + } + if (chainTailORB_ == orb) { + chainTailORB_ = nullptr; + } + orb->CancelTimer(); + orb->SetAppended(false); + + auto cb = orb->GetCompletionCallback(); + if (cb) { + cb(transportStatus, sbpStatus); + } +} + +void SBP2LoginSession::FailPendingImmediateORBs(int transportStatus, + uint8_t sbpStatus) noexcept { + auto pending = pendingImmediateORBs_; + for (auto* orb : pending) { + FailSubmittedORB(orb, transportStatus, sbpStatus); + } +} + +bool SBP2LoginSession::AppendORB(SBP2CommandORB* orb) noexcept { + if (chainTailORB_ == nullptr) { + // First ORB — write directly to fetch agent instead of chaining + chainTailORB_ = orb; + return AppendORBImmediate(orb); + } + + if (chainTailORB_ != orb) { + const Async::FWAddress orbAddr = orb->GetORBAddress(); + + // Set the new ORB's address in big-endian into the last ORB's next pointer + const uint16_t localNode = + NormalizeBusNodeID(static_cast(busInfo_.GetLocalNodeID().value)); + const uint32_t nextHi = ToBE32(ComposeBusAddressHi(localNode, orbAddr.addressHi)); + const uint32_t nextLo = ToBE32(orbAddr.addressLo); + const kern_return_t linkKr = chainTailORB_->SetNextORBAddress(nextHi, nextLo); + if (linkKr != kIOReturnSuccess) { + ASFW_LOG(SBP2, "SBP2LoginSession::AppendORB: SetNextORBAddress failed: 0x%08x", + linkKr); + FailSubmittedORB(orb, -1, Wire::SBPStatus::kUnspecifiedError); + return false; + } + + chainTailORB_ = orb; + } + + return true; +} + +void SBP2LoginSession::RingDoorbell() noexcept { + if (doorbellInProgress_) { + doorbellRingAgain_ = true; + return; + } + + doorbellInProgress_ = true; + + const FW::Generation gen{loginGeneration_}; + const FW::NodeId node{static_cast(loginNodeID_ & 0x3Fu)}; + const FW::FwSpeed speed = busInfo_.GetSpeed(node); + + const std::weak_ptr weakSelf = weak_from_this(); + doorbellWriteHandle_ = bus_.WriteQuad( + gen, node, doorbellAddress_, 0, speed, + [weakSelf, requestGeneration = loginGeneration_](Async::AsyncStatus status, + std::span response) { + if (auto self = weakSelf.lock()) { + self->OnDoorbellComplete(requestGeneration, status, response); + } + }); + + if (!doorbellWriteHandle_) { + ASFW_LOG(SBP2, "SBP2LoginSession::RingDoorbell: WriteQuad failed"); + doorbellInProgress_ = false; + } +} + +void SBP2LoginSession::OnFetchAgentWriteComplete(uint16_t expectedGeneration, + Async::AsyncStatus status, + std::span response) noexcept { + if (expectedGeneration != loginGeneration_ || state_ != LoginState::LoggedIn) { + return; + } + + fetchAgentWriteInUse_ = false; + fetchAgentWriteHandle_ = {}; + + if (status != Async::AsyncStatus::kSuccess) { + ASFW_LOG(SBP2, + "SBP2LoginSession::OnFetchAgentWriteComplete: status=%s, retries=%u", + Async::ToString(status), + activeFetchAgentORB_ ? activeFetchAgentORB_->GetFetchAgentWriteRetries() : 0); + + if (activeFetchAgentORB_ != nullptr) { + uint32_t retries = activeFetchAgentORB_->GetFetchAgentWriteRetries(); + if (retries > 0) { + retries--; + activeFetchAgentORB_->SetFetchAgentWriteRetries(retries); + // Retry after a delay + SBP2CommandORB* retryORB = activeFetchAgentORB_; + const std::weak_ptr weakSelf = weak_from_this(); + SubmitDelayedCallback(1000, [weakSelf, retryORB]() { + if (auto self = weakSelf.lock()) { + if (self->activeFetchAgentORB_ == retryORB) { + self->AppendORBImmediate(retryORB); + } + } + }); + return; + } + + // Retries exhausted — report failure + SBP2CommandORB* failedORB = activeFetchAgentORB_; + if (failedORB != nullptr) { + FailSubmittedORB(failedORB, -1, Wire::SBPStatus::kUnspecifiedError); + } + FailPendingImmediateORBs(-1, Wire::SBPStatus::kUnspecifiedError); + ClearORBTracking(true); + ResetFetchAgent(nullptr); + } + return; + } + + // Fetch agent write succeeded. The target can now fetch this ORB. + StartSubmittedORBTimer(activeFetchAgentORB_); + activeFetchAgentORB_ = nullptr; + + if (!pendingImmediateORBs_.empty()) { + SBP2CommandORB* next = pendingImmediateORBs_.front(); + pendingImmediateORBs_.pop_front(); + ASFW_LOG(SBP2, "SBP2LoginSession: submitting deferred ORB"); + AppendORBImmediate(next); + } +} + +void SBP2LoginSession::OnDoorbellComplete(uint16_t expectedGeneration, + Async::AsyncStatus status, + std::span response) noexcept { + if (expectedGeneration != loginGeneration_ || state_ != LoginState::LoggedIn) { + return; + } + + doorbellInProgress_ = false; + doorbellWriteHandle_ = {}; + + if (status != Async::AsyncStatus::kSuccess) { + ASFW_LOG(SBP2, "SBP2LoginSession::OnDoorbellComplete: status=%s", + Async::ToString(status)); + } + + if (doorbellRingAgain_) { + doorbellRingAgain_ = false; + RingDoorbell(); + } +} + +// --------------------------------------------------------------------------- +// Management ORB Submission +// --------------------------------------------------------------------------- + +bool SBP2LoginSession::SubmitManagementORB(SBP2ManagementORB* orb) noexcept { + if (state_ != LoginState::LoggedIn) { + ASFW_LOG(SBP2, "SBP2LoginSession::SubmitManagementORB: state=%s, rejecting", + ToString(state_)); + return false; + } + + if (orb == nullptr) { + return false; + } + + EnsureTimeoutQueue(); + + // Configure the management ORB with current session parameters + orb->SetLoginID(loginID_); + orb->SetManagementAgentOffset(targetInfo_.managementAgentOffset); + orb->SetTargetNode(loginGeneration_, loginNodeID_); + orb->SetTimeout(targetInfo_.managementTimeoutMs); + orb->SetWorkQueue(workQueue_); + orb->SetTimeoutQueue(EffectiveTimeoutQueue()); + + ASFW_LOG(SBP2, "SBP2LoginSession::SubmitManagementORB: function=%u", + static_cast(orb->GetFunction())); + + return orb->Execute(); +} + +// --------------------------------------------------------------------------- +// Fetch Agent Reset +// --------------------------------------------------------------------------- + +void SBP2LoginSession::ResetFetchAgent(std::function callback) noexcept { + if (state_ != LoginState::LoggedIn) { + if (callback) callback(-1); + return; + } + + if (agentResetInProgress_) { + if (callback) callback(-1); + return; + } + + agentResetInProgress_ = true; + agentResetCallback_ = std::move(callback); + + const FW::Generation gen{loginGeneration_}; + const FW::NodeId node{static_cast(loginNodeID_ & 0x3Fu)}; + const FW::FwSpeed speed = busInfo_.GetSpeed(node); + + const std::weak_ptr weakSelf = weak_from_this(); + agentResetWriteHandle_ = bus_.WriteQuad( + gen, node, agentResetAddress_, 0, speed, + [weakSelf, requestGeneration = loginGeneration_](Async::AsyncStatus status, + std::span response) { + if (auto self = weakSelf.lock()) { + self->OnAgentResetComplete(requestGeneration, status, response); + } + }); + + if (!agentResetWriteHandle_) { + ASFW_LOG(SBP2, "SBP2LoginSession::ResetFetchAgent: WriteQuad failed"); + agentResetInProgress_ = false; + if (agentResetCallback_) { + agentResetCallback_(-1); + agentResetCallback_ = nullptr; + } + } +} + +void SBP2LoginSession::OnAgentResetComplete(uint16_t expectedGeneration, + Async::AsyncStatus status, + std::span response) noexcept { + if (expectedGeneration != loginGeneration_) { + return; + } + + agentResetInProgress_ = false; + + // Clear ORB chain after reset + ClearORBTracking(true); + + ASFW_LOG(SBP2, "SBP2LoginSession::OnAgentResetComplete: status=%s, ORB chain cleared", + Async::ToString(status)); + + if (agentResetCallback_) { + int result = (status == Async::AsyncStatus::kSuccess) ? 0 : -1; + auto cb = std::move(agentResetCallback_); + agentResetCallback_ = nullptr; + cb(result); + } +} + +// --------------------------------------------------------------------------- +// Unsolicited Status Enable +// --------------------------------------------------------------------------- + +void SBP2LoginSession::EnableUnsolicitedStatus() noexcept { + if (state_ != LoginState::LoggedIn) { + unsolicitedStatusRequested_ = true; + return; + } + + const FW::Generation gen{loginGeneration_}; + const FW::NodeId node{static_cast(loginNodeID_ & 0x3Fu)}; + 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 response) { + if (auto self = weakSelf.lock()) { + self->OnUnsolicitedStatusEnableComplete(requestGeneration, status, response); + } + }); +} + +void SBP2LoginSession::OnUnsolicitedStatusEnableComplete( + uint16_t expectedGeneration, + Async::AsyncStatus status, + std::span response) noexcept { + if (expectedGeneration != loginGeneration_ || state_ != LoginState::LoggedIn) { + return; + } + + if (status != Async::AsyncStatus::kSuccess) { + ASFW_LOG(SBP2, "SBP2LoginSession::OnUnsolicitedStatusEnableComplete: status=%s", + Async::ToString(status)); + } +} + +// --------------------------------------------------------------------------- +// Busy Timeout +// --------------------------------------------------------------------------- + +void SBP2LoginSession::WriteBusyTimeout() noexcept { + if (busyTimeoutInProgress_) { + bus_.Cancel(busyTimeoutWriteHandle_); + busyTimeoutInProgress_ = false; + } + + const FW::Generation gen{loginGeneration_}; + const FW::NodeId node{static_cast(loginNodeID_ & 0x3Fu)}; + 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 response) { + if (auto self = weakSelf.lock()) { + self->OnBusyTimeoutComplete(requestGeneration, status, response); + } + }); + + if (!busyTimeoutWriteHandle_) { + ASFW_LOG(SBP2, "SBP2LoginSession::WriteBusyTimeout: WriteBlock failed"); + busyTimeoutInProgress_ = false; + } +} + +void SBP2LoginSession::OnBusyTimeoutComplete(uint16_t expectedGeneration, + Async::AsyncStatus status, + std::span) noexcept { + if (expectedGeneration != loginGeneration_) { + return; + } + + busyTimeoutInProgress_ = false; + + if (status != Async::AsyncStatus::kSuccess && status != Async::AsyncStatus::kAborted) { + ASFW_LOG(SBP2, + "SBP2LoginSession::OnBusyTimeoutComplete: status=%s", + Async::ToString(status)); + } +} + +} // namespace ASFW::Protocols::SBP2 diff --git a/ASFWDriver/Protocols/SBP2/SBP2LoginSession.hpp b/ASFWDriver/Protocols/SBP2/SBP2LoginSession.hpp new file mode 100644 index 00000000..54e5a280 --- /dev/null +++ b/ASFWDriver/Protocols/SBP2/SBP2LoginSession.hpp @@ -0,0 +1,479 @@ +#pragma once + +// SBP-2 Login/Reconnect/Logout state machine for ASFW. +// Ported from Apple IOFireWireSBP2Login.cpp — simplified for DriverKit. +// +// Lifecycle: +// 1. Create SBP2LoginSession with bus + address-space deps +// 2. Call Configure() with ROM-derived parameters (management offset, LUN, etc.) +// 3. Call Login() — sends login ORB to device's management agent +// 4. On success, session is kLoggedIn — can submit ORBs via fetch agent +// 5. On bus reset, auto Reconnect() with stored loginID +// 6. Call Logout() to terminate session + +#include "SBP2WireFormats.hpp" +#include "SBP2CommandORB.hpp" +#include "SBP2ManagementORB.hpp" +#include "AddressSpaceManager.hpp" +#include "../../Async/AsyncTypes.hpp" +#include "../../Logging/Logging.hpp" + +#include +#ifdef ASFW_HOST_TEST +#include "../../Testing/HostDriverKitStubs.hpp" +#else +#include +#endif + +#include +#include +#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}; +}; + +// --------------------------------------------------------------------------- +// Login 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"; +} + +// --------------------------------------------------------------------------- +// SBP2LoginSession +// --------------------------------------------------------------------------- + +class SBP2LoginSession : public std::enable_shared_from_this { + friend class SBP2SessionRegistry; + +public: + using LoginCallback = std::function; + using LogoutCallback = std::function; + using StatusCallback = std::function; + + SBP2LoginSession(Async::IFireWireBus& bus, + Async::IFireWireBusInfo& busInfo, + AddressSpaceManager& addrSpaceMgr); + ~SBP2LoginSession(); + + SBP2LoginSession(const SBP2LoginSession&) = delete; + SBP2LoginSession& operator=(const SBP2LoginSession&) = delete; + + // ----------------------------------------------------------------------- + // Configuration (call once before Login) + // ----------------------------------------------------------------------- + + /// Configure target parameters from Config ROM. Must be called before Login(). + void Configure(const SBP2TargetInfo& info) noexcept; + + /// Set login completion callback. + void SetLoginCallback(LoginCallback cb) noexcept { loginCallback_ = std::move(cb); } + + /// Set logout completion callback. + void SetLogoutCallback(LogoutCallback cb) noexcept { logoutCallback_ = std::move(cb); } + + /// Set status block notification callback (receives solicited + unsolicited status). + void SetStatusCallback(StatusCallback cb) noexcept { statusCallback_ = std::move(cb); } + + /// Bind the IODispatchQueue used for delayed callbacks (timers). + /// Must be called before Login() for timeout/retry support. + void SetWorkQueue(IODispatchQueue* queue) noexcept; + void SetTimeoutQueue(IODispatchQueue* queue) noexcept; + + // ----------------------------------------------------------------------- + // Session operations + // ----------------------------------------------------------------------- + + /// Initiate login to device. Completion via loginCallback_. + /// Returns false if already logged in or configuration missing. + [[nodiscard]] bool Login() noexcept; + + /// Initiate logout. Completion via logoutCallback_. + [[nodiscard]] bool Logout() noexcept; + + /// Reconnect after bus reset. Called automatically or manually. + [[nodiscard]] bool Reconnect() noexcept; + + /// Handle bus reset notification — transitions to Suspended if logged in. + 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_; } + + /// Command Block Agent address (valid after successful login). + [[nodiscard]] Async::FWAddress CommandBlockAgent() const noexcept; + + /// Get negotiated reconnect hold time (seconds). + [[nodiscard]] uint32_t ReconnectHoldSeconds() const noexcept; + + /// Get max payload size for ORBs (bytes). + [[nodiscard]] uint16_t MaxPayloadSize() const noexcept { return maxPayloadSize_; } + + /// Set max payload size override (clipped by login response). + void SetMaxPayloadSize(uint16_t bytes) noexcept { maxPayloadSize_ = bytes; } + +#ifdef ASFW_HOST_TEST + /// Test hook: override fetch-agent retry budget for subsequently submitted ORBs. + void SetFetchAgentWriteRetriesForTesting(uint32_t retries) noexcept { + testFetchAgentWriteRetries_ = retries; + } + + /// Reset testing override for fetch-agent retries. + void ResetFetchAgentWriteRetriesForTesting() noexcept { + testFetchAgentWriteRetries_ = 20; + } +#endif + + // ----------------------------------------------------------------------- + // ORB submission + // ----------------------------------------------------------------------- + + /// Submit a Normal Command ORB to the device's fetch agent. + /// Requires LoggedIn state. ORB must be fully configured before calling. + [[nodiscard]] bool SubmitORB(SBP2CommandORB* orb) noexcept; + + /// Submit a management ORB (abort task, reset, etc). + /// Requires LoggedIn state. ORB must be fully configured before calling. + [[nodiscard]] bool SubmitManagementORB(SBP2ManagementORB* orb) noexcept; + + /// Reset the fetch agent. Clears ORB chain. Completion via callback. + void ResetFetchAgent(std::function callback) noexcept; + + /// Re-enable unsolicited status after device sends one. + void EnableUnsolicitedStatus() noexcept; + +private: + // ----------------------------------------------------------------------- + // Internal: resource allocation + // ----------------------------------------------------------------------- + + bool AllocateResources() noexcept; + void DeallocateResources() noexcept; + + bool AllocateLoginORBAddressSpace() noexcept; + bool AllocateLoginResponseAddressSpace() noexcept; + bool AllocateStatusBlockAddressSpace() noexcept; + bool AllocateReconnectORBAddressSpace() noexcept; + bool AllocateLogoutORBAddressSpace() noexcept; + + // ----------------------------------------------------------------------- + // Internal: ORB construction and submission + // ----------------------------------------------------------------------- + + void BuildLoginORB() noexcept; + void BuildReconnectORB() noexcept; + void BuildLogoutORB() noexcept; + void RefreshCommandBlockAgentAddresses() noexcept; + + // ----------------------------------------------------------------------- + // Internal: completion handlers + // ----------------------------------------------------------------------- + + void OnLoginWriteComplete(uint16_t expectedGeneration, + Async::AsyncStatus status, + std::span response) noexcept; + void OnLoginTimeout() noexcept; + void OnReconnectWriteComplete(uint16_t expectedGeneration, + Async::AsyncStatus status, + std::span response) noexcept; + void OnReconnectTimeout() noexcept; + void OnLogoutWriteComplete(uint16_t expectedGeneration, + Async::AsyncStatus status, + std::span response) noexcept; + void OnLogoutTimeout() noexcept; + + // ----------------------------------------------------------------------- + // Internal: status block handling + // ----------------------------------------------------------------------- + + /// Called by AddressSpaceManager remote-write callback when the device + /// writes a status block. Dispatches to the appropriate state handler. + void OnStatusBlockRemoteWrite(uint32_t offset, std::span payload) noexcept; + + /// Parse and dispatch a received status block. + void ProcessStatusBlock(const Wire::StatusBlock& block, uint32_t length) noexcept; + + // Internal: login/reconnect completion via status block + 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; + + // ----------------------------------------------------------------------- + // Internal: helpers + // ----------------------------------------------------------------------- + + void SetState(LoginState newState) noexcept; + void StartLoginTimer() noexcept; + void StartReconnectTimer() noexcept; + void StartLogoutTimer() noexcept; + void CancelLoginTimer() noexcept; + + /// Submit a delayed callback via IOTimerDispatchSource. + void SubmitDelayedCallback(uint64_t delayMs, + std::function callback) noexcept; + + /// Cancel any pending timer callback. + void CancelPendingTimer() noexcept; + void EnsureTimeoutQueue() noexcept; + void ReleaseOwnedTimeoutQueue() noexcept; + [[nodiscard]] IODispatchQueue* EffectiveTimeoutQueue() const noexcept; + void ClearORBTracking(bool cancelTimers) noexcept; + [[nodiscard]] static uint64_t MakeORBKey(uint16_t addressHi, uint32_t addressLo) noexcept; + [[nodiscard]] static uint64_t MakeORBKey(const Async::FWAddress& address) noexcept; + + // ----------------------------------------------------------------------- + // Members + // ----------------------------------------------------------------------- + + Async::IFireWireBus& bus_; + Async::IFireWireBusInfo& busInfo_; + AddressSpaceManager& addrSpaceMgr_; + + // Configuration + SBP2TargetInfo targetInfo_{}; + bool configured_{false}; + uint16_t maxPayloadSize_{4096}; // default, clipped by login + + // 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 (from AddressSpaceManager) + // ----------------------------------------------------------------------- + + 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_{}; + + // ORB addresses as big-endian for management agent write (8 bytes each) + std::array loginORBAddressBE_{}; + std::array reconnectORBAddressBE_{}; + std::array logoutORBAddressBE_{}; + + // ----------------------------------------------------------------------- + // Async handles for in-flight operations + // ----------------------------------------------------------------------- + + Async::AsyncHandle loginWriteHandle_{}; + Async::AsyncHandle reconnectWriteHandle_{}; + Async::AsyncHandle logoutWriteHandle_{}; + + bool loginTimerActive_{false}; + bool reconnectTimerActive_{false}; + bool logoutTimerActive_{false}; + + // ----------------------------------------------------------------------- + // Callbacks + // ----------------------------------------------------------------------- + + LoginCallback loginCallback_; + LogoutCallback logoutCallback_; + StatusCallback statusCallback_; + uint32_t testFetchAgentWriteRetries_{20}; + + // ----------------------------------------------------------------------- + // Timer infrastructure + // ----------------------------------------------------------------------- + + IODispatchQueue* workQueue_{nullptr}; + IODispatchQueue* timeoutQueue_{nullptr}; +#ifdef ASFW_HOST_TEST + std::unique_ptr ownedTimeoutQueue_{}; +#else + IODispatchQueue* ownedTimeoutQueue_{nullptr}; +#endif + std::atomic delayedCallbackGeneration_{0}; + std::shared_ptr lifetimeToken_{std::make_shared(0)}; + + // ----------------------------------------------------------------------- + // Constants + // ----------------------------------------------------------------------- + + static constexpr uint16_t kCSRBusAddressHi = 0x0000FFFFu; + static constexpr uint32_t kBusyTimeoutAddressLo = 0xF0000210u; + static constexpr uint32_t kBusyTimeoutValue = 0x0000000Fu; + + // ----------------------------------------------------------------------- + // Fetch Agent / Doorbell internals + // ----------------------------------------------------------------------- + + /// Write ORB address to fetch agent (CBA + kORBPointer). + bool AppendORBImmediate(SBP2CommandORB* orb) noexcept; + void StartSubmittedORBTimer(SBP2CommandORB* orb) noexcept; + void FailSubmittedORB(SBP2CommandORB* orb, + int transportStatus, + uint8_t sbpStatus) noexcept; + void FailPendingImmediateORBs(int transportStatus, uint8_t sbpStatus) noexcept; + + /// Chain ORB to last ORB's next pointer. + [[nodiscard]] bool AppendORB(SBP2CommandORB* orb) noexcept; + + /// Ring doorbell (write quadlet to CBA + kDoorbell). + void RingDoorbell() noexcept; + + /// Fetch agent write completion handler. + void OnFetchAgentWriteComplete(uint16_t expectedGeneration, + Async::AsyncStatus status, + std::span response) noexcept; + + /// Doorbell write completion handler. + void OnDoorbellComplete(uint16_t expectedGeneration, + Async::AsyncStatus status, + std::span response) noexcept; + + /// Fetch agent reset completion handler. + void OnAgentResetComplete(uint16_t expectedGeneration, + Async::AsyncStatus status, + std::span response) noexcept; + + /// Unsolicited status enable completion handler. + void OnUnsolicitedStatusEnableComplete(uint16_t expectedGeneration, + Async::AsyncStatus status, + std::span response) noexcept; + + /// Write the SBP-2 busy timeout CSR to the target. + void WriteBusyTimeout() noexcept; + + /// Busy timeout write completion handler. + void OnBusyTimeoutComplete(uint16_t expectedGeneration, + Async::AsyncStatus status, + std::span response) noexcept; + + // Fetch agent state + Async::FWAddress fetchAgentAddress_{}; + Async::FWAddress doorbellAddress_{}; + Async::AsyncHandle fetchAgentWriteHandle_{}; + bool fetchAgentWriteInUse_{false}; + + // ORB chain state + SBP2CommandORB* chainTailORB_{nullptr}; + SBP2CommandORB* activeFetchAgentORB_{nullptr}; + std::deque pendingImmediateORBs_; + std::unordered_map outstandingORBs_; + + // Doorbell state + Async::AsyncHandle doorbellWriteHandle_{}; + bool doorbellInProgress_{false}; + bool doorbellRingAgain_{false}; + + // Fetch agent write data (8-byte BE ORB address) + std::array fetchAgentWriteData_{}; + + // Agent reset state + Async::FWAddress agentResetAddress_{}; + Async::AsyncHandle agentResetWriteHandle_{}; + bool agentResetInProgress_{false}; + std::function agentResetCallback_; + + // Unsolicited status enable state + Async::FWAddress unsolicitedStatusAddress_{}; + Async::AsyncHandle unsolicitedStatusWriteHandle_{}; + bool unsolicitedStatusRequested_{false}; + + // Busy timeout state + Async::AsyncHandle busyTimeoutWriteHandle_{}; + bool busyTimeoutInProgress_{false}; + uint32_t busyTimeoutBuffer_{Wire::ToBE32(kBusyTimeoutValue)}; +}; + +} // namespace ASFW::Protocols::SBP2 diff --git a/ASFWDriver/Protocols/SBP2/SBP2ManagementORB.cpp b/ASFWDriver/Protocols/SBP2/SBP2ManagementORB.cpp index dd75eee3..d3d4b043 100644 --- a/ASFWDriver/Protocols/SBP2/SBP2ManagementORB.cpp +++ b/ASFWDriver/Protocols/SBP2/SBP2ManagementORB.cpp @@ -1,4 +1,5 @@ // SBP-2 Management ORB implementation. +// Ported from Apple IOFireWireSBP2ManagementORB. // Ref: SBP-2 §6 (Task Management) #include "SBP2ManagementORB.hpp" @@ -12,6 +13,155 @@ namespace ASFW::Protocols::SBP2 { using namespace ASFW::Protocols::SBP2::Wire; +namespace { + +constexpr int kManagementTransportFailure = -1; +constexpr int kManagementTimeout = -2; +constexpr int kManagementMalformedStatus = -3; +constexpr int kManagementDeviceFailure = -4; + +using ManagementAsyncState = SBP2ManagementORB::AsyncState; + +Async::AsyncHandle TakeWriteHandle(const std::shared_ptr& state) noexcept { + return Async::AsyncHandle{state->writeHandleValue.exchange(0, std::memory_order_acq_rel)}; +} + +void ClearStatusBlockCallback(const std::shared_ptr& state) noexcept { + const uint64_t handle = state->statusBlockHandle.load(std::memory_order_acquire); + if (handle != 0 && state->addrMgr != nullptr) { + state->addrMgr->SetRemoteWriteCallback(handle, {}); + } +} + +bool CompleteAsyncOperation(const std::shared_ptr& state, + int status) noexcept { + if (!state->inProgress.exchange(false, std::memory_order_acq_rel)) { + return false; + } + + state->timerActive.store(false, std::memory_order_relaxed); + state->timerGeneration.fetch_add(1, std::memory_order_acq_rel); + ClearStatusBlockCallback(state); + + const Async::AsyncHandle pendingWrite = TakeWriteHandle(state); + if (pendingWrite && state->bus != nullptr) { + (void)state->bus->Cancel(pendingWrite); + } + + if (!state->destroyed.load(std::memory_order_acquire) && + state->completionCallback) { + state->completionCallback(status); + } + return true; +} + +void HandleStatusBlockWrite(const std::shared_ptr& state, + uint32_t offset, + std::span payload) noexcept { + if (!state->inProgress.load(std::memory_order_relaxed)) { + return; + } + + ASFW_LOG(SBP2, "SBP2ManagementORB: received status block (offset=%u len=%zu)", + offset, payload.size()); + + if (offset != 0 || payload.size() < 8 || payload.size() > Wire::StatusBlock::kMaxSize) { + (void)CompleteAsyncOperation(state, kManagementMalformedStatus); + return; + } + + Wire::StatusBlock block{}; + std::memcpy(&block, payload.data(), payload.size()); + + const uint16_t orbOffsetHi = FromBE16(block.orbOffsetHi); + const uint32_t orbOffsetLo = FromBE32(block.orbOffsetLo); + if (orbOffsetHi != state->expectedORBAddressHi.load(std::memory_order_acquire) || + orbOffsetLo != state->expectedORBAddressLo.load(std::memory_order_acquire)) { + ASFW_LOG(SBP2, + "SBP2ManagementORB: status block ORB mismatch expected=%04x:%08x got=%04x:%08x", + state->expectedORBAddressHi.load(std::memory_order_relaxed), + state->expectedORBAddressLo.load(std::memory_order_relaxed), + orbOffsetHi, + orbOffsetLo); + (void)CompleteAsyncOperation(state, kManagementMalformedStatus); + return; + } + + if (block.Response() != 0 || block.DeadBit() != 0) { + ASFW_LOG(SBP2, + "SBP2ManagementORB: device rejected management ORB resp=%u dead=%u status=%u", + block.Response(), + block.DeadBit(), + block.sbpStatus); + (void)CompleteAsyncOperation(state, kManagementDeviceFailure); + return; + } + + if (block.sbpStatus != Wire::SBPStatus::kNoAdditionalInfo) { + ASFW_LOG(SBP2, + "SBP2ManagementORB: management ORB completed with sbpStatus=%u", + block.sbpStatus); + (void)CompleteAsyncOperation(state, kManagementDeviceFailure); + return; + } + + (void)CompleteAsyncOperation(state, 0); +} + +void HandleTimeout(const std::shared_ptr& state) noexcept { + if (!state->inProgress.load(std::memory_order_relaxed)) { + return; + } + + ASFW_LOG(SBP2, "SBP2ManagementORB: timeout"); + (void)CompleteAsyncOperation(state, kManagementTimeout); +} + +void HandleWriteComplete(const std::shared_ptr& state, + Async::AsyncStatus status, + std::span /*response*/, + uint32_t timeoutMs, + IODispatchQueue* workQueue, + IODispatchQueue* timeoutQueue) noexcept { + state->writeHandleValue.store(0, std::memory_order_release); + + if (!state->inProgress.load(std::memory_order_relaxed)) { + return; + } + + if (status != Async::AsyncStatus::kSuccess) { + ASFW_LOG(SBP2, "SBP2ManagementORB::OnWriteComplete: status=%s", + Async::ToString(status)); + (void)CompleteAsyncOperation(state, kManagementTransportFailure); + return; + } + + state->timerActive.store(true, std::memory_order_relaxed); + ASFW_LOG(SBP2, "SBP2ManagementORB: mgmt agent ACK'd, waiting for status block (timeout=%ums)", + timeoutMs); + + if (workQueue == nullptr || timeoutQueue == nullptr || timeoutMs == 0) { + return; + } + + const uint64_t expectedGeneration = + state->timerGeneration.fetch_add(1, std::memory_order_acq_rel) + 1ULL; + const uint64_t delayNs = static_cast(timeoutMs) * 1'000'000ULL; + + DispatchAfterCompat(timeoutQueue, delayNs, [state, expectedGeneration, workQueue]() { + DispatchAsyncCompat(workQueue, [state, expectedGeneration]() { + if (state->timerGeneration.load(std::memory_order_acquire) != expectedGeneration || + !state->timerActive.load(std::memory_order_relaxed) || + !state->inProgress.load(std::memory_order_relaxed)) { + return; + } + HandleTimeout(state); + }); + }); +} + +} // namespace + // --------------------------------------------------------------------------- // Construction / Destruction // --------------------------------------------------------------------------- @@ -22,11 +172,19 @@ SBP2ManagementORB::SBP2ManagementORB(Async::IFireWireBus& bus, : bus_(bus) , busInfo_(busInfo) , addrMgr_(addrMgr) - , owner_(owner) {} + , owner_(owner) + , asyncState_(std::make_shared(&bus_, &addrMgr_)) {} SBP2ManagementORB::~SBP2ManagementORB() { - timerGeneration_.fetch_add(1, std::memory_order_acq_rel); - lifetimeToken_.reset(); + asyncState_->destroyed.store(true, std::memory_order_release); + asyncState_->inProgress.store(false, std::memory_order_relaxed); + asyncState_->timerActive.store(false, std::memory_order_relaxed); + asyncState_->timerGeneration.fetch_add(1, std::memory_order_acq_rel); + ClearStatusBlockCallback(asyncState_); + const Async::AsyncHandle pendingHandle = TakeWriteHandle(asyncState_); + if (pendingHandle) { + (void)bus_.Cancel(pendingHandle); + } DeallocateResources(); } @@ -35,8 +193,21 @@ SBP2ManagementORB::~SBP2ManagementORB() { // --------------------------------------------------------------------------- bool SBP2ManagementORB::AllocateResources() noexcept { - if (orbHandle_ != 0) { - return true; // Already allocated + const auto state = asyncState_; + const auto registerStatusWriteCallback = [this, state]() { + // AddressSpaceManager dispatches outside its lock; the shared state token + // keeps callback lifetime independent from this ORB object. + addrMgr_.SetRemoteWriteCallback( + statusBlockHandle_, + [state](uint64_t /*handle*/, uint32_t offset, std::span payload) { + HandleStatusBlockWrite(state, offset, payload); + }); + state->statusBlockHandle.store(statusBlockHandle_, std::memory_order_release); + }; + + if (orbHandle_ != 0 && statusBlockHandle_ != 0) { + registerStatusWriteCallback(); + return true; } // Allocate ORB address space (32 bytes) @@ -44,7 +215,7 @@ bool SBP2ManagementORB::AllocateResources() noexcept { owner_, 0xFFFF, Wire::TaskManagementORB::kSize, &orbHandle_, &orbMeta_); if (kr != kIOReturnSuccess) { - ASFW_LOG(Async, "SBP2ManagementORB: failed to allocate ORB: 0x%08x", kr); + ASFW_LOG(SBP2, "SBP2ManagementORB: failed to allocate ORB: 0x%08x", kr); return false; } @@ -53,18 +224,14 @@ bool SBP2ManagementORB::AllocateResources() noexcept { owner_, 0xFFFF, Wire::StatusBlock::kMaxSize, &statusBlockHandle_, &statusBlockMeta_); if (kr != kIOReturnSuccess) { - ASFW_LOG(Async, "SBP2ManagementORB: failed to allocate status block: 0x%08x", kr); + ASFW_LOG(SBP2, "SBP2ManagementORB: failed to allocate status block: 0x%08x", kr); addrMgr_.DeallocateAddressRange(owner_, orbHandle_); orbHandle_ = 0; return false; } // Register remote-write callback for the per-ORB status block - addrMgr_.SetRemoteWriteCallback( - statusBlockHandle_, - [this](uint64_t /*handle*/, uint32_t offset, std::span payload) { - OnStatusBlockWrite(offset, payload); - }); + registerStatusWriteCallback(); return true; } @@ -86,7 +253,11 @@ void SBP2ManagementORB::DeallocateResources() noexcept { // ORB construction // --------------------------------------------------------------------------- -void SBP2ManagementORB::BuildManagementORB() noexcept { +kern_return_t SBP2ManagementORB::BuildManagementORB() noexcept { + if (orbHandle_ == 0 || statusBlockHandle_ == 0) { + return kIOReturnNotReady; + } + std::memset(&orbBuffer_, 0, sizeof(orbBuffer_)); const uint16_t localNode = @@ -109,10 +280,14 @@ void SBP2ManagementORB::BuildManagementORB() noexcept { orbBuffer_.statusFIFOAddressLo = ToBE32(statusBlockMeta_.addressLo); // Write ORB to address space - addrMgr_.WriteLocalData( + const kern_return_t writeKr = addrMgr_.WriteLocalData( owner_, orbHandle_, 0, std::span{reinterpret_cast(&orbBuffer_), sizeof(orbBuffer_)}); + if (writeKr != kIOReturnSuccess) { + ASFW_LOG(SBP2, "SBP2ManagementORB: failed to write management ORB: 0x%08x", writeKr); + return writeKr; + } // Build 8-byte management agent write payload: ORB address in BE orbAddressBE_[0] = static_cast(localNode >> 8); @@ -122,11 +297,12 @@ void SBP2ManagementORB::BuildManagementORB() noexcept { const uint32_t addrLoBE = ToBE32(orbMeta_.addressLo); std::memcpy(&orbAddressBE_[4], &addrLoBE, sizeof(uint32_t)); - ASFW_LOG(Async, + ASFW_LOG(SBP2, "SBP2ManagementORB: built function=%u loginID=%u ORB at %04x:%08x status at %04x:%08x", fn, loginID_, localNode, orbMeta_.addressLo, localNode, statusBlockMeta_.addressLo); + return kIOReturnSuccess; } // --------------------------------------------------------------------------- @@ -134,8 +310,8 @@ void SBP2ManagementORB::BuildManagementORB() noexcept { // --------------------------------------------------------------------------- bool SBP2ManagementORB::Execute() noexcept { - if (inProgress_.load(std::memory_order_relaxed)) { - ASFW_LOG(Async, "SBP2ManagementORB::Execute: already in progress"); + if (asyncState_->inProgress.load(std::memory_order_relaxed)) { + ASFW_LOG(SBP2, "SBP2ManagementORB::Execute: already in progress"); return false; } @@ -143,9 +319,17 @@ bool SBP2ManagementORB::Execute() noexcept { return false; } - BuildManagementORB(); + const kern_return_t buildKr = BuildManagementORB(); + if (buildKr != kIOReturnSuccess) { + return false; + } - inProgress_.store(true, std::memory_order_relaxed); + asyncState_->destroyed.store(false, std::memory_order_release); + asyncState_->completionCallback = completionCallback_; + asyncState_->expectedORBAddressHi.store(orbMeta_.addressHi, std::memory_order_release); + asyncState_->expectedORBAddressLo.store(orbMeta_.addressLo, std::memory_order_release); + asyncState_->timerActive.store(false, std::memory_order_relaxed); + asyncState_->inProgress.store(true, std::memory_order_relaxed); // Write ORB address to management agent const FW::Generation gen{generation_}; @@ -159,91 +343,28 @@ bool SBP2ManagementORB::Execute() noexcept { }; const FW::FwSpeed speed = busInfo_.GetSpeed(node); - writeHandle_ = bus_.WriteBlock( + const Async::AsyncHandle writeHandle = bus_.WriteBlock( gen, node, mgmtAddr, std::span{orbAddressBE_.data(), orbAddressBE_.size()}, speed, - [this](Async::AsyncStatus status, std::span response) { - OnWriteComplete(status, response); + [state = asyncState_, + timeoutMs = timeoutMs_, + workQueue = workQueue_, + effectiveTimeoutQueue = (timeoutQueue_ != nullptr ? timeoutQueue_ : workQueue_)]( + Async::AsyncStatus status, + std::span response) { + HandleWriteComplete(state, status, response, timeoutMs, workQueue, effectiveTimeoutQueue); }); - if (!writeHandle_) { - ASFW_LOG(Async, "SBP2ManagementORB::Execute: WriteBlock failed"); - inProgress_.store(false, std::memory_order_relaxed); + if (!writeHandle) { + ASFW_LOG(SBP2, "SBP2ManagementORB::Execute: WriteBlock failed"); + asyncState_->inProgress.store(false, std::memory_order_relaxed); return false; } + asyncState_->writeHandleValue.store(writeHandle.value, std::memory_order_release); - ASFW_LOG(Async, "SBP2ManagementORB::Execute: wrote management ORB to agent"); + ASFW_LOG(SBP2, "SBP2ManagementORB::Execute: wrote management ORB to agent"); return true; } -// --------------------------------------------------------------------------- -// Completion handlers -// --------------------------------------------------------------------------- - -void SBP2ManagementORB::OnWriteComplete(Async::AsyncStatus status, - std::span response) noexcept { - if (status != Async::AsyncStatus::kSuccess) { - ASFW_LOG(Async, "SBP2ManagementORB::OnWriteComplete: status=%s", - Async::ToString(status)); - Complete(-1); - return; - } - - // Management agent write ACK'd. Start timeout, wait for status block. - timerActive_.store(true, std::memory_order_relaxed); - ASFW_LOG(Async, "SBP2ManagementORB: mgmt agent ACK'd, waiting for status block (timeout=%ums)", - timeoutMs_); - - if (workQueue_ && timeoutMs_ > 0) { - const uint32_t timeout = timeoutMs_; - const uint64_t expectedGeneration = - timerGeneration_.fetch_add(1, std::memory_order_acq_rel) + 1ULL; - const std::weak_ptr weakLifetime = lifetimeToken_; - const uint64_t delayNs = static_cast(timeout) * 1'000'000ULL; - - DispatchAfterCompat(workQueue_, delayNs, [this, weakLifetime, expectedGeneration]() { - if (weakLifetime.expired()) { - return; - } - if (timerGeneration_.load(std::memory_order_acquire) != expectedGeneration || - !timerActive_.load(std::memory_order_relaxed) || - !inProgress_.load(std::memory_order_relaxed)) { - return; - } - OnTimeout(); - }); - } -} - -void SBP2ManagementORB::OnStatusBlockWrite(uint32_t offset, - std::span payload) noexcept { - if (!inProgress_.load(std::memory_order_relaxed)) { - return; - } - - ASFW_LOG(Async, "SBP2ManagementORB: received status block (offset=%u len=%zu)", - offset, payload.size()); - - Complete(0); -} - -void SBP2ManagementORB::OnTimeout() noexcept { - if (!inProgress_.load(std::memory_order_relaxed)) { - return; - } - ASFW_LOG(Async, "SBP2ManagementORB: timeout"); - Complete(-2); -} - -void SBP2ManagementORB::Complete(int status) noexcept { - inProgress_.store(false, std::memory_order_relaxed); - timerActive_.store(false, std::memory_order_relaxed); - timerGeneration_.fetch_add(1, std::memory_order_acq_rel); - - if (completionCallback_) { - completionCallback_(status); - } -} - } // namespace ASFW::Protocols::SBP2 diff --git a/ASFWDriver/Protocols/SBP2/SBP2ManagementORB.hpp b/ASFWDriver/Protocols/SBP2/SBP2ManagementORB.hpp index b7c01ee0..2d0dabe1 100644 --- a/ASFWDriver/Protocols/SBP2/SBP2ManagementORB.hpp +++ b/ASFWDriver/Protocols/SBP2/SBP2ManagementORB.hpp @@ -4,6 +4,7 @@ // Written to the management agent address (same as login/reconnect/logout). // Has its own per-ORB status FIFO address space. // +// Ported from Apple IOFireWireSBP2ManagementORB. // Ref: SBP-2 §6 (Task Management) #include "AddressSpaceManager.hpp" @@ -18,7 +19,6 @@ #include #endif -#include #include #include #include @@ -60,31 +60,51 @@ class SBP2ManagementORB { } void SetManagementAgentOffset(uint32_t offset) noexcept { managementAgentOffset_ = offset; } void SetTimeout(uint32_t ms) noexcept { timeoutMs_ = ms; } - void SetCompletionCallback(CompletionCallback cb) noexcept { completionCallback_ = std::move(cb); } + void SetCompletionCallback(CompletionCallback cb) noexcept { + completionCallback_ = std::move(cb); + asyncState_->completionCallback = completionCallback_; + } - // Set node targeting before Execute. + // Set node targeting (called by SBP2LoginSession before Execute) void SetTargetNode(uint16_t generation, uint16_t nodeID) noexcept { generation_ = generation; nodeID_ = nodeID; } void SetWorkQueue(IODispatchQueue* queue) noexcept { workQueue_ = queue; } + void SetTimeoutQueue(IODispatchQueue* queue) noexcept { timeoutQueue_ = queue; } // Lifecycle [[nodiscard]] bool Execute() noexcept; [[nodiscard]] Function GetFunction() const noexcept { return function_; } - [[nodiscard]] bool InProgress() const noexcept { return inProgress_.load(std::memory_order_relaxed); } + [[nodiscard]] bool InProgress() const noexcept { + return asyncState_->inProgress.load(std::memory_order_relaxed); + } + + // Shared by delayed callbacks after the ORB object itself may have been destroyed. + struct AsyncState { + AsyncState(Async::IFireWireBus* busIn, AddressSpaceManager* addrMgrIn) + : bus(busIn) + , addrMgr(addrMgrIn) {} + + Async::IFireWireBus* bus{nullptr}; + AddressSpaceManager* addrMgr{nullptr}; + std::atomic destroyed{false}; + std::atomic inProgress{false}; + std::atomic timerActive{false}; + std::atomic timerGeneration{0}; + std::atomic statusBlockHandle{0}; + std::atomic writeHandleValue{0}; + std::atomic expectedORBAddressHi{0}; + std::atomic expectedORBAddressLo{0}; + CompletionCallback completionCallback{}; + }; private: bool AllocateResources() noexcept; void DeallocateResources() noexcept; - void BuildManagementORB() noexcept; - - void OnWriteComplete(Async::AsyncStatus status, std::span response) noexcept; - void OnStatusBlockWrite(uint32_t offset, std::span payload) noexcept; - void OnTimeout() noexcept; - void Complete(int status) noexcept; + [[nodiscard]] kern_return_t BuildManagementORB() noexcept; // Dependencies Async::IFireWireBus& bus_; @@ -114,11 +134,8 @@ class SBP2ManagementORB { // Management agent write payload (8-byte BE ORB address) std::array orbAddressBE_{}; - Async::AsyncHandle writeHandle_{}; - // State - std::atomic inProgress_{false}; - std::atomic timerActive_{false}; + std::shared_ptr asyncState_; // Node targeting uint16_t generation_{0}; @@ -126,8 +143,7 @@ class SBP2ManagementORB { // Timer infrastructure IODispatchQueue* workQueue_{nullptr}; - std::atomic timerGeneration_{0}; - std::shared_ptr lifetimeToken_{std::make_shared(0)}; + IODispatchQueue* timeoutQueue_{nullptr}; }; } // namespace ASFW::Protocols::SBP2 diff --git a/ASFWDriver/Protocols/SBP2/SBP2PageTable.hpp b/ASFWDriver/Protocols/SBP2/SBP2PageTable.hpp index 9f16f490..ffa9463b 100644 --- a/ASFWDriver/Protocols/SBP2/SBP2PageTable.hpp +++ b/ASFWDriver/Protocols/SBP2/SBP2PageTable.hpp @@ -4,6 +4,7 @@ // Converts scatter-gather DMA segments into SBP-2 Page Table Entries (PTEs) // or a single direct-address descriptor when possible. // +// Ported from Apple IOFireWireSBP2ORB::setCommandBuffers. // Ref: SBP-2 §5.1.2 (Page Table Entry format) #include "AddressSpaceManager.hpp" @@ -34,6 +35,10 @@ class SBP2PageTable { explicit SBP2PageTable(AddressSpaceManager& addrMgr, void* owner) noexcept : addrMgr_(addrMgr), owner_(owner) {} + ~SBP2PageTable() noexcept { + Clear(); + } + SBP2PageTable(const SBP2PageTable&) = delete; SBP2PageTable& operator=(const SBP2PageTable&) = delete; @@ -111,7 +116,7 @@ class SBP2PageTable { owner_, 0xFFFF, ptSize, &pageTableHandle_, &pageTableMeta_); if (kr != kIOReturnSuccess) { - ASFW_LOG(Async, "SBP2PageTable: failed to allocate page table: 0x%08x", kr); + ASFW_LOG(SBP2, "SBP2PageTable: failed to allocate page table: 0x%08x", kr); return false; } @@ -120,7 +125,7 @@ class SBP2PageTable { reinterpret_cast(ptes.data()), ptSize); kr = addrMgr_.WriteLocalData(owner_, pageTableHandle_, 0, pteSpan); if (kr != kIOReturnSuccess) { - ASFW_LOG(Async, "SBP2PageTable: failed to write page table: 0x%08x", kr); + ASFW_LOG(SBP2, "SBP2PageTable: failed to write page table: 0x%08x", kr); Clear(); return false; } @@ -132,7 +137,7 @@ class SBP2PageTable { result_.options = Wire::Options::kPageTableUnrestricted; result_.isDirect = false; - ASFW_LOG(Async, "SBP2PageTable: built %u PTEs (%u bytes) at %04x:%08x", + ASFW_LOG(SBP2, "SBP2PageTable: built %u PTEs (%u bytes) at %04x:%08x", pteCount_, ptSize, pageTableMeta_.addressHi, pageTableMeta_.addressLo); return true; } diff --git a/ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.cpp b/ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.cpp new file mode 100644 index 00000000..d8201f06 --- /dev/null +++ b/ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.cpp @@ -0,0 +1,799 @@ +#include "SBP2SessionRegistry.hpp" + +#include "../../Async/Interfaces/IFireWireBusInfo.hpp" +#include "../../Discovery/FWDevice.hpp" +#include "../../Discovery/FWUnit.hpp" + +#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}; +}; + +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; +} + +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 + +SBP2SessionRegistry::SBP2SessionRegistry(Async::IFireWireBus& bus, + Async::IFireWireBusInfo& busInfo, + AddressSpaceManager& addrSpaceMgr, + Discovery::IDeviceManager& deviceManager, + IODispatchQueue* workQueue) + : bus_(bus) + , busInfo_(busInfo) + , addrSpaceMgr_(addrSpaceMgr) + , deviceManager_(deviceManager) + , workQueue_(workQueue) { + lock_ = IOLockAlloc(); +} + +SBP2SessionRegistry::~SBP2SessionRegistry() { + { + IOLockGuard lock(lock_); + for (auto& [handle, record] : sessions_) { + if (record.session && record.session->State() == LoginState::LoggedIn) { + (void)record.session->Logout(); + } + CleanupCommandResources(record); + CleanupManagementResources(record); + } + sessions_.clear(); + } + retiringSessions_.clear(); + + if (lock_ != nullptr) { + IOLockFree(lock_); + lock_ = nullptr; + } +} + +std::expected SBP2SessionRegistry::CreateSession(void* owner, + uint64_t guid, + uint32_t romOffset) { + auto unit = ResolveUnit(guid, romOffset); + if (!unit) { + ASFW_LOG(SBP2, "SBP2SessionRegistry: no unit found for guid=0x%016llx romOffset=%u", + guid, romOffset); + return std::unexpected(kIOReturnNotFound); + } + + if (!unit->Matches(kSBP2UnitSpecId, kSBP2UnitSwVersion)) { + ASFW_LOG(SBP2, + "SBP2SessionRegistry: 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(SBP2, "SBP2SessionRegistry: 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(SBP2, + "SBP2SessionRegistry: duplicate session for guid=0x%016llx romOffset=%u", + guid, romOffset); + return std::unexpected(kIOReturnExclusiveAccess); + } + + auto session = std::make_shared(bus_, busInfo_, addrSpaceMgr_); + session->Configure(targetInfo); + session->SetWorkQueue(workQueue_); + + const uint64_t handle = nextHandle_++; + + SBP2SessionRecord record{}; + record.handle = handle; + record.owner = owner; + record.guid = guid; + record.romOffset = romOffset; + record.session = std::move(session); + + auto [it, inserted] = sessions_.emplace(handle, std::move(record)); + if (!inserted) { + return std::unexpected(kIOReturnNoMemory); + } + + ASFW_LOG(SBP2, "SBP2SessionRegistry: created session handle=%llu guid=0x%016llx romOffset=%u", + handle, guid, romOffset); + return handle; +} + +bool SBP2SessionRegistry::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->state.lastError = params.status; + if (params.status == 0) { + rec->state.loginID = params.loginResponse.loginID; + rec->state.loginState = LoginState::LoggedIn; + rec->state.generation = params.generation; + } else { + rec->state.loginState = LoginState::Failed; + } + }); + + return record->session->Login(); +} + +std::optional SBP2SessionRegistry::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->state.lastError; + state.reconnectPending = (state.loginState == LoginState::Suspended); + return state; +} + +bool SBP2SessionRegistry::SubmitInquiry(void* owner, uint64_t handle, uint8_t allocationLength) { + return SubmitCommand(owner, handle, SCSI::BuildInquiryRequest(allocationLength)); +} + +std::optional SBP2SessionRegistry::GetInquiryResult(void* owner, + uint64_t handle) { + IOLockGuard lock(lock_); + auto* record = FindByHandleForOwner(owner, handle); + if (!record || !record->commandReady || !record->pendingCommandResult.has_value() || + record->lastCompletedCommandOpcode != kInquiryOpcode) { + return std::nullopt; + } + + SCSI::CommandResult result = std::move(*record->pendingCommandResult); + record->pendingCommandResult.reset(); + record->lastCompletedCommandOpcode.reset(); + record->commandReady = false; + return result; +} + +bool SBP2SessionRegistry::SubmitCommand(void* owner, + uint64_t handle, + const SCSI::CommandRequest& request) { + std::shared_ptr session; + SBP2CommandORB* submittedORB = nullptr; + + { + IOLockGuard lock(lock_); + auto* record = FindByHandleForOwner(owner, handle); + if (!record || !record->session || request.cdb.empty()) { + return false; + } + + if (record->session->State() != LoginState::LoggedIn || record->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 = record->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( + record->owner, 0xFFFF, request.transferLength, &bufferHandle, &bufferMeta); + if (kr != kIOReturnSuccess) { + ASFW_LOG(SBP2, "SBP2SessionRegistry: 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( + record->owner, + bufferHandle, + 0, + std::span{request.outgoingPayload.data(), request.outgoingPayload.size()}); + if (writeKr != kIOReturnSuccess) { + addrSpaceMgr_.DeallocateAddressRange(record->owner, bufferHandle); + return false; + } + } + + pageTable = std::make_unique(addrSpaceMgr_, record->owner); + SBP2PageTable::Segment segment{bufferMeta.address, request.transferLength}; + if (!pageTable->Build(std::span(&segment, 1), + busInfo_.GetLocalNodeID().value)) { + addrSpaceMgr_.DeallocateAddressRange(record->owner, bufferHandle); + return false; + } + } + + auto orb = std::make_unique(addrSpaceMgr_, record->owner, maxCDB); + if (!orb->SetCommandBlock(std::span{request.cdb.data(), request.cdb.size()})) { + if (bufferHandle != 0) { + addrSpaceMgr_.DeallocateAddressRange(record->owner, bufferHandle); + } + return false; + } + orb->SetFlags(BuildCommandFlags(request.direction)); + orb->SetTimeout(request.timeoutMs > 0 + ? request.timeoutMs + : record->session->TargetInfo().managementTimeoutMs); + if (pageTable) { + orb->SetDataDescriptor(pageTable->GetResult()); + } + + const uint64_t captureHandle = handle; + orb->SetCompletionCallback([this, captureHandle](int transportStatus, uint8_t sbpStatus) { + IOLockGuard cbLock(lock_); + auto* rec = FindByHandle(captureHandle); + if (rec == nullptr) { + return; + } + if (!rec->commandInFlight) { + return; + } + + rec->commandInFlight = false; + rec->commandReady = true; + rec->lastCompletedCommandOpcode = rec->activeCommandOpcode; + rec->activeCommandOpcode.reset(); + + SCSI::CommandResult result{}; + result.transportStatus = transportStatus; + result.sbpStatus = sbpStatus; + + if (transportStatus == 0 && + sbpStatus == Wire::SBPStatus::kNoAdditionalInfo && + rec->activeCommandRequest.has_value() && + rec->activeCommandRequest->direction == SCSI::DataDirection::FromTarget && + rec->activeCommandRequest->transferLength > 0 && + rec->commandBufferHandle != 0) { + std::vector payload; + const kern_return_t readKr = addrSpaceMgr_.ReadIncomingData( + rec->owner, + rec->commandBufferHandle, + 0, + rec->activeCommandRequest->transferLength, + &payload); + if (readKr == kIOReturnSuccess) { + result.payload = std::move(payload); + } else { + result.transportStatus = static_cast(readKr); + } + } + + if (rec->activeCommandRequest.has_value() && rec->activeCommandRequest->captureSenseData) { + result.senseData = result.payload; + } + + rec->state.lastError = static_cast(result.transportStatus); + if (result.transportStatus == 0 && result.sbpStatus == Wire::SBPStatus::kNoAdditionalInfo) { + rec->state.lastError = 0; + } + rec->pendingCommandResult = std::move(result); + rec->activeCommandRequest.reset(); + CleanupCommandResources(*rec); + }); + + record->commandInFlight = true; + record->commandReady = false; + record->pendingCommandResult.reset(); + record->activeCommandRequest = request; + record->activeCommandOpcode = request.cdb.front(); + record->commandBufferHandle = bufferHandle; + record->commandORB = std::move(orb); + record->commandPageTable = std::move(pageTable); + session = record->session; + submittedORB = record->commandORB.get(); + } + + if (session->SubmitORB(submittedORB)) { + return true; + } + + IOLockGuard lock(lock_); + auto* record = FindByHandleForOwner(owner, handle); + if (record != nullptr && record->commandORB.get() == submittedORB) { + record->commandInFlight = false; + record->commandReady = false; + record->pendingCommandResult.reset(); + record->activeCommandRequest.reset(); + record->activeCommandOpcode.reset(); + CleanupCommandResources(*record); + } + return false; +} + +std::optional SBP2SessionRegistry::GetCommandResult(void* owner, + uint64_t handle) { + IOLockGuard lock(lock_); + auto* record = FindByHandleForOwner(owner, handle); + if (!record || !record->commandReady || !record->pendingCommandResult.has_value()) { + return std::nullopt; + } + + SCSI::CommandResult result = std::move(*record->pendingCommandResult); + record->pendingCommandResult.reset(); + record->lastCompletedCommandOpcode.reset(); + record->commandReady = false; + return result; +} + +bool SBP2SessionRegistry::SubmitTaskManagement(void* owner, + uint64_t handle, + SBP2ManagementORB::Function function) { + IOLockGuard lock(lock_); + auto* record = FindByHandleForOwner(owner, handle); + if (!record || !record->session || !IsSupportedTaskManagementFunction(function)) { + return false; + } + + if (record->session->State() != LoginState::LoggedIn || record->managementORB) { + return false; + } + + auto orb = std::make_unique(bus_, busInfo_, addrSpaceMgr_, record->owner); + orb->SetFunction(function); + orb->SetLoginID(record->session->LoginID()); + orb->SetManagementAgentOffset(record->session->TargetInfo().managementAgentOffset); + orb->SetTimeout(record->session->TargetInfo().managementTimeoutMs); + orb->SetWorkQueue(workQueue_); + orb->SetTimeoutQueue(workQueue_); + orb->SetTargetNode(record->session->Generation(), record->session->TargetInfo().targetNodeId); + + const auto* submittedORB = orb.get(); + orb->SetCompletionCallback([this, handle, submittedORB, function](int status) { + IOLockGuard cbLock(lock_); + auto* rec = FindByHandle(handle); + if (rec == nullptr || rec->managementORB.get() != submittedORB) { + return; + } + + rec->state.lastError = static_cast(status); + if (status == 0 && IsSupportedTaskManagementFunction(function)) { + CleanupCommandResources(*rec); + rec->pendingCommandResult.reset(); + rec->activeCommandRequest.reset(); + rec->activeCommandOpcode.reset(); + rec->lastCompletedCommandOpcode.reset(); + rec->commandReady = false; + } + rec->managementORB.reset(); + }); + + record->managementORB = std::move(orb); + + if (!record->session->SubmitManagementORB(record->managementORB.get())) { + record->managementORB.reset(); + return false; + } + + return true; +} + +bool SBP2SessionRegistry::ReleaseSession(void* owner, uint64_t handle) { + uint32_t waitMs = 0; + + { + IOLockGuard lock(lock_); + auto it = sessions_.find(handle); + if (it == sessions_.end() || it->second.owner != owner) { + return false; + } + + auto& record = it->second; + CleanupCommandResources(record); + CleanupManagementResources(record); + + if (record.session && + (record.session->State() == LoginState::LoggedIn || + record.session->State() == LoginState::Suspended)) { + waitMs = std::max(record.session->TargetInfo().managementTimeoutMs + 500, 500); + SetReleaseLogoutCallbackLocked(handle, record.session); + if (!record.session->Logout()) { + sessions_.erase(it); + return true; + } + } else if (record.session && record.session->State() == LoginState::LoggingOut) { + SetReleaseLogoutCallbackLocked(handle, record.session); + RetireSessionLocked(record); + sessions_.erase(it); + return true; + } else { + sessions_.erase(it); + return true; + } + } + + for (uint32_t elapsed = 0; elapsed < waitMs; elapsed += 10) { + IOSleep(10); + + IOLockGuard lock(lock_); + auto it = sessions_.find(handle); + if (it == sessions_.end()) { + return true; + } + + auto& record = it->second; + const LoginState state = record.session ? record.session->State() : LoginState::Idle; + if (state != LoginState::LoggingOut) { + CleanupCommandResources(record); + sessions_.erase(it); + return true; + } + } + + IOLockGuard lock(lock_); + auto it = sessions_.find(handle); + if (it != sessions_.end()) { + CleanupCommandResources(it->second); + CleanupManagementResources(it->second); + if (it->second.session && it->second.session->State() == LoginState::LoggingOut) { + RetireSessionLocked(it->second); + } + sessions_.erase(it); + } + return true; +} + +void SBP2SessionRegistry::ReleaseOwner(void* owner) { + IOLockGuard lock(lock_); + for (auto it = sessions_.begin(); it != sessions_.end();) { + if (it->second.owner == owner) { + auto& record = it->second; + if (record.session && + (record.session->State() == LoginState::LoggedIn || + record.session->State() == LoginState::Suspended)) { + SetReleaseLogoutCallbackLocked(record.handle, record.session); + if (record.session->Logout()) { + CleanupCommandResources(record); + CleanupManagementResources(record); + RetireSessionLocked(record); + it = sessions_.erase(it); + continue; + } + } else if (record.session && record.session->State() == LoginState::LoggingOut) { + SetReleaseLogoutCallbackLocked(record.handle, record.session); + CleanupCommandResources(record); + CleanupManagementResources(record); + RetireSessionLocked(record); + it = sessions_.erase(it); + continue; + } + CleanupCommandResources(record); + CleanupManagementResources(record); + it = sessions_.erase(it); + } else { + ++it; + } + } +} + +void SBP2SessionRegistry::OnBusReset(uint16_t newGeneration) { + IOLockGuard lock(lock_); + for (auto& [handle, record] : sessions_) { + if (record.session) { + record.session->HandleBusReset(newGeneration); + } + CleanupManagementResources(record); + if (record.commandInFlight || record.commandORB) { + FailActiveCommandLocked(record, + static_cast(kIOReturnAborted), + Wire::SBPStatus::kRequestAborted); + } + } +} + +void SBP2SessionRegistry::FailActiveCommandLocked(SBP2SessionRecord& record, + int transportStatus, + uint8_t sbpStatus) noexcept { + if (!record.commandInFlight) { + return; + } + + record.commandInFlight = false; + record.commandReady = true; + record.lastCompletedCommandOpcode = record.activeCommandOpcode; + record.activeCommandOpcode.reset(); + + SCSI::CommandResult result{}; + result.transportStatus = transportStatus; + result.sbpStatus = sbpStatus; + if (transportStatus == 0 && sbpStatus == Wire::SBPStatus::kNoAdditionalInfo) { + record.state.lastError = 0; + } else { + record.state.lastError = static_cast(result.transportStatus); + } + record.pendingCommandResult = std::move(result); + record.activeCommandRequest.reset(); + record.commandPageTable.reset(); + if (record.commandBufferHandle != 0) { + addrSpaceMgr_.DeallocateAddressRange(record.owner, record.commandBufferHandle); + record.commandBufferHandle = 0; + } + if (record.commandORB) { + record.commandORB->SetAppended(false); + } + + CleanupCommandResources(record); +} + +void SBP2SessionRegistry::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(SBP2, "SBP2SessionRegistry: RefreshTargets: unit not found for handle=%llu", + handle); + continue; + } + + auto targetInfo = BuildTargetInfoFromUnit(*unit); + record.session->Configure(targetInfo); + + ASFW_LOG(SBP2, "SBP2SessionRegistry: reconnecting session handle=%llu gen=%u", + handle, gen.value); + (void)record.session->Reconnect(); + } +} + +#ifdef ASFW_HOST_TEST +SBP2LoginSession* SBP2SessionRegistry::GetSessionForTesting(uint64_t handle) { + IOLockGuard lock(lock_); + auto* record = FindByHandle(handle); + if (!record) { + return nullptr; + } + return record->session.get(); +} + +std::weak_ptr SBP2SessionRegistry::GetSessionWeakForTesting(uint64_t handle) { + IOLockGuard lock(lock_); + auto* record = FindByHandle(handle); + if (!record) { + return {}; + } + return record->session; +} +#endif + +SBP2SessionRecord* SBP2SessionRegistry::FindByHandle(uint64_t handle) { + auto it = sessions_.find(handle); + return it != sessions_.end() ? &it->second : nullptr; +} + +const SBP2SessionRecord* SBP2SessionRegistry::FindByHandle(uint64_t handle) const { + auto it = sessions_.find(handle); + return it != sessions_.end() ? &it->second : nullptr; +} + +SBP2SessionRecord* SBP2SessionRegistry::FindByHandleForOwner(void* owner, uint64_t handle) { + auto* record = FindByHandle(handle); + return (record != nullptr && record->owner == owner) ? record : nullptr; +} + +const SBP2SessionRecord* SBP2SessionRegistry::FindByHandleForOwner(void* owner, + uint64_t handle) const { + const auto* record = FindByHandle(handle); + return (record != nullptr && record->owner == owner) ? record : nullptr; +} + +std::shared_ptr SBP2SessionRegistry::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; +} + +void SBP2SessionRegistry::CleanupCommandResources(SBP2SessionRecord& record) { + if (record.session) { + record.session->ClearORBTracking(true); + } + if (record.commandBufferHandle != 0) { + addrSpaceMgr_.DeallocateAddressRange(record.owner, record.commandBufferHandle); + record.commandBufferHandle = 0; + } + record.commandORB.reset(); + record.commandPageTable.reset(); + record.commandInFlight = false; +} + +void SBP2SessionRegistry::CleanupManagementResources(SBP2SessionRecord& record) { + record.managementORB.reset(); +} + +bool SBP2SessionRegistry::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 SBP2SessionRegistry::RetireSessionLocked(const SBP2SessionRecord& 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 SBP2SessionRegistry::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 SBP2SessionRegistry::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)) { + CleanupCommandResources(it->second); + CleanupManagementResources(it->second); + sessions_.erase(it); + } + + EraseRetiredSessionLocked(completedSession); + }); +} + +bool SBP2SessionRegistry::IsSupportedTaskManagementFunction( + SBP2ManagementORB::Function function) noexcept { + switch (function) { + case SBP2ManagementORB::Function::AbortTaskSet: + case SBP2ManagementORB::Function::LogicalUnitReset: + case SBP2ManagementORB::Function::TargetReset: + return true; + default: + return false; + } +} + +} // namespace ASFW::Protocols::SBP2 diff --git a/ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.hpp b/ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.hpp new file mode 100644 index 00000000..56ada706 --- /dev/null +++ b/ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.hpp @@ -0,0 +1,167 @@ +#pragma once + +// SBP-2 Session Registry — bridges discovery metadata to SBP2LoginSession instances. +// Sessions are created for an owner and target (guid, romOffset). Public +// operations use opaque handles plus owner validation, while the registry also +// rejects duplicate live targets by (guid, romOffset). + +#include "SBP2LoginSession.hpp" +#include "SBP2CommandORB.hpp" +#include "SBP2ManagementORB.hpp" +#include "SBP2PageTable.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}; +}; + +struct SBP2SessionRecord { + uint64_t handle{0}; + void* owner{nullptr}; + uint64_t guid{0}; + uint32_t romOffset{0}; + std::shared_ptr session; + SBP2SessionState state{}; + + 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; +}; + +class SBP2SessionRegistry { +public: + SBP2SessionRegistry(Async::IFireWireBus& bus, + Async::IFireWireBusInfo& busInfo, + AddressSpaceManager& addrSpaceMgr, + Discovery::IDeviceManager& deviceManager, + IODispatchQueue* workQueue = nullptr); + + ~SBP2SessionRegistry(); + + SBP2SessionRegistry(const SBP2SessionRegistry&) = delete; + SBP2SessionRegistry& operator=(const SBP2SessionRegistry&) = delete; + + // Create an owner-bound session for target (guid, romOffset). + // Validates the unit is SBP-2, has Management_Agent_Offset, and is not + // already represented by another live or retiring session. + [[nodiscard]] std::expected CreateSession(void* owner, + uint64_t guid, + uint32_t romOffset); + + // Start login for an owner-bound handle. Returns false if not in Idle state. + [[nodiscard]] bool StartLogin(void* owner, uint64_t handle); + + // Get session state. Returns nullopt if owner/handle is not found. + [[nodiscard]] std::optional GetSessionState(void* owner, + uint64_t handle) const; + + // Submit SCSI INQUIRY. Returns false if not logged in or inquiry already in-flight. + [[nodiscard]] bool SubmitInquiry(void* owner, + uint64_t handle, + uint8_t allocationLength = 96); + + // Get inquiry result (destructive read). Returns nullopt if not ready. + [[nodiscard]] std::optional GetInquiryResult(void* owner, + uint64_t handle); + + // Submit a generic SCSI command. Returns false if not logged in or another command is active. + [[nodiscard]] bool SubmitCommand(void* owner, + uint64_t handle, + const SCSI::CommandRequest& request); + + // Get generic command result (destructive read). Returns nullopt if not ready. + [[nodiscard]] std::optional GetCommandResult(void* owner, + uint64_t handle); + + // Submit a task-management recovery ORB. + [[nodiscard]] bool SubmitTaskManagement(void* owner, + uint64_t handle, + SBP2ManagementORB::Function function); + + // Release a specific owner-bound session. + [[nodiscard]] bool ReleaseSession(void* owner, uint64_t handle); + + // Release all sessions for an owner (best-effort logout + cleanup). + void ReleaseOwner(void* owner); + + // Bus reset: suspend all active sessions. + void OnBusReset(uint16_t newGeneration); + + // After discovery completes: refresh target info and reconnect suspended sessions. + void RefreshTargets(Discovery::Generation gen); +#ifdef ASFW_HOST_TEST + // Testing hook: return the underlying login session for a handle. + SBP2LoginSession* GetSessionForTesting(uint64_t handle); + std::weak_ptr GetSessionWeakForTesting(uint64_t handle); +#endif + +private: + SBP2SessionRecord* FindByHandle(uint64_t handle); + const SBP2SessionRecord* FindByHandle(uint64_t handle) const; + SBP2SessionRecord* FindByHandleForOwner(void* owner, uint64_t handle); + const SBP2SessionRecord* FindByHandleForOwner(void* owner, uint64_t handle) const; + void FailActiveCommandLocked(SBP2SessionRecord& record, + int transportStatus, + uint8_t sbpStatus) noexcept; + + std::shared_ptr ResolveUnit(uint64_t guid, uint32_t romOffset) const; + + void CleanupCommandResources(SBP2SessionRecord& record); + void CleanupManagementResources(SBP2SessionRecord& record); + [[nodiscard]] bool HasSessionForTargetLocked(uint64_t guid, uint32_t romOffset) const; + void RetireSessionLocked(const SBP2SessionRecord& record); + void EraseRetiredSessionLocked(const std::shared_ptr& session); + void SetReleaseLogoutCallbackLocked(uint64_t handle, + const std::shared_ptr& session); + [[nodiscard]] static bool IsSupportedTaskManagementFunction( + SBP2ManagementORB::Function function) noexcept; + + Async::IFireWireBus& bus_; + Async::IFireWireBusInfo& busInfo_; + AddressSpaceManager& addrSpaceMgr_; + Discovery::IDeviceManager& deviceManager_; + 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 or times out. + std::vector retiringSessions_; + uint64_t nextHandle_{1}; +}; + +} // namespace ASFW::Protocols::SBP2 diff --git a/ASFWDriver/Protocols/SBP2/SBP2WireFormats.hpp b/ASFWDriver/Protocols/SBP2/SBP2WireFormats.hpp index 12fa5f16..c363d1ae 100644 --- a/ASFWDriver/Protocols/SBP2/SBP2WireFormats.hpp +++ b/ASFWDriver/Protocols/SBP2/SBP2WireFormats.hpp @@ -1,13 +1,15 @@ #pragma once // SBP-2 (Serial Bus Protocol 2) wire-format definitions. -// Based on ANSI INCITS 335-1999 (SBP-2). +// Based on ANSI INCITS 335-1999 (SBP-2) and Apple IOFireWireSBP2 structures. // All multi-byte fields are stored in **big-endian** (bus/wire) order. // Use ToBusOrder / FromBusOrder from Core/PhyPackets.hpp or std::byteswap for conversion. #include +#include #include #include +#include namespace ASFW::Protocols::SBP2::Wire { @@ -154,18 +156,25 @@ struct NormalORB { // Access via CommandBlock() helper. [[nodiscard]] uint32_t* CommandBlock() noexcept { - return reinterpret_cast(reinterpret_cast(this) + 16); + return reinterpret_cast(reinterpret_cast(this) + kHeaderSize); } [[nodiscard]] const uint32_t* CommandBlock() const noexcept { - return reinterpret_cast(reinterpret_cast(this) + 16); + return reinterpret_cast(reinterpret_cast(this) + kHeaderSize); } // Minimum ORB size (no command block) - static constexpr uint32_t kHeaderSize = 16; + static constexpr uint32_t kHeaderSize = 20; // Null next-ORB indicator (bit 31 set in hi address) static constexpr uint32_t kNextORBNull = 0x80000000u; }; +static_assert(sizeof(NormalORB) == NormalORB::kHeaderSize, "NormalORB header must be 20 bytes"); +static_assert(std::is_trivially_copyable_v, "NormalORB must stay trivially copyable"); +static_assert(offsetof(NormalORB, nextORBAddressHi) == 0, "NormalORB next hi offset changed"); +static_assert(offsetof(NormalORB, dataDescriptorHi) == 8, "NormalORB data descriptor hi offset changed"); +static_assert(offsetof(NormalORB, options) == 16, "NormalORB options offset changed"); +static_assert(offsetof(NormalORB, dataSize) == 18, "NormalORB data size offset changed"); + // Page Table Entry — maps data buffer for DMA. // Ref: SBP-2 §5.1.2 struct PageTableEntry { @@ -177,14 +186,18 @@ struct PageTableEntry { }; static_assert(sizeof(PageTableEntry) == PageTableEntry::kSize, "PTE must be 8 bytes"); +static_assert(std::is_trivially_copyable_v, + "PageTableEntry must stay trivially copyable"); +static_assert(offsetof(PageTableEntry, segmentLength) == 0, "PTE length offset changed"); +static_assert(offsetof(PageTableEntry, segmentBaseAddressLo) == 4, "PTE addressLo offset changed"); // --------------------------------------------------------------------------- // SBP-2 Status Block -// Ref: SBP-2 §5.2 +// Ref: SBP-2 §5.3 status blocks // --------------------------------------------------------------------------- struct StatusBlock { - uint8_t details{0}; // [7] Src, [6:4] Resp, [3:2] D, [1:0] Len + uint8_t details{0}; // [7:6] Src, [5:4] Resp, [3] D, [2:0] Len uint8_t sbpStatus{0}; // SBP-2 specific status code uint16_t orbOffsetHi{0}; uint32_t orbOffsetLo{0}; @@ -192,13 +205,18 @@ struct StatusBlock { static constexpr uint32_t kMaxSize = 32; // header (8) + max status (24) - [[nodiscard]] uint8_t Source() const noexcept { return (details >> 7) & 0x1; } - [[nodiscard]] uint8_t Response() const noexcept { return (details >> 4) & 0x7; } - [[nodiscard]] uint8_t DeadBit() const noexcept { return (details >> 2) & 0x1; } - [[nodiscard]] uint8_t Length() const noexcept { return details & 0x3; } + [[nodiscard]] uint8_t Source() const noexcept { return (details >> 6) & 0x3; } + [[nodiscard]] uint8_t Response() const noexcept { return (details >> 4) & 0x3; } + [[nodiscard]] uint8_t DeadBit() const noexcept { return (details >> 3) & 0x1; } + [[nodiscard]] uint8_t Length() const noexcept { return details & 0x7; } }; static_assert(sizeof(StatusBlock) == 32, "StatusBlock must be 32 bytes"); +static_assert(std::is_trivially_copyable_v, + "StatusBlock must stay trivially copyable"); +static_assert(offsetof(StatusBlock, details) == 0, "StatusBlock details offset changed"); +static_assert(offsetof(StatusBlock, orbOffsetHi) == 2, "StatusBlock orbOffsetHi offset changed"); +static_assert(offsetof(StatusBlock, orbOffsetLo) == 4, "StatusBlock orbOffsetLo offset changed"); // --------------------------------------------------------------------------- // SBP-2 Management ORB (Task Management) @@ -219,6 +237,14 @@ struct TaskManagementORB { }; static_assert(sizeof(TaskManagementORB) == TaskManagementORB::kSize); +static_assert(std::is_trivially_copyable_v, + "TaskManagementORB must stay trivially copyable"); +static_assert(offsetof(TaskManagementORB, orbOffsetHi) == 0, + "TaskManagementORB orbOffsetHi offset changed"); +static_assert(offsetof(TaskManagementORB, options) == 16, + "TaskManagementORB options offset changed"); +static_assert(offsetof(TaskManagementORB, statusFIFOAddressHi) == 24, + "TaskManagementORB status FIFO hi offset changed"); // --------------------------------------------------------------------------- // Management Agent address calculation @@ -231,8 +257,8 @@ static_assert(sizeof(TaskManagementORB) == TaskManagementORB::kSize); } // SBP-2 ORBs embed a full 16-bit node ID in bus addresses. ASFW's generic -// bus-info path may expose only the local 6-bit physical node number, so expand -// it to the local-bus form (0xffc0 | phyId) before writing ORBs. +// bus-info path exposes only the local 6-bit physical node number, so expand it +// to the local-bus form Apple uses (0xffc0 | phyId) before writing ORBs. [[nodiscard]] inline constexpr uint16_t NormalizeBusNodeID(uint16_t nodeID) noexcept { if ((nodeID & 0xFFC0u) == 0xFFC0u) { return nodeID; @@ -247,6 +273,7 @@ static_assert(sizeof(TaskManagementORB) == TaskManagementORB::kSize); } // Command Block Agent register offsets (relative to agent base from login response). +// Verified against Apple IOFireWireSBP2Login::clearAllTasksInSet / login response processing. struct CommandBlockAgentOffsets { static constexpr uint32_t kAgentReset = 0x04; // Fetch agent reset (quadlet write) static constexpr uint32_t kFetchAgent = 0x08; // ORB pointer write (fetch agent, non-fast-start) @@ -288,19 +315,21 @@ namespace Options { static constexpr uint32_t kFunctionTargetReset = 0xF; } -// SBP-2 status codes (from sbpStatus field) +// SBP-2 §5.3.1 request status codes (from sbpStatus field) namespace SBPStatus { - static constexpr uint8_t kNoAdditionalInfo = 0; - static constexpr uint8_t kReqTypeNotSupported = 1; - static constexpr uint8_t kSpeedNotSupported = 2; - static constexpr uint8_t kPageSizeNotSupported = 3; - static constexpr uint8_t kAccessDenied = 4; - static constexpr uint8_t kResourceUnavailable = 5; - static constexpr uint8_t kFunctionRejected = 6; - static constexpr uint8_t kLoginIDNotRecognized = 7; - static constexpr uint8_t kDummyORBCompleted = 8; - static constexpr uint8_t kRequestAborted = 0xB; - static constexpr uint8_t kUnspecifiedError = 0xFF; + static constexpr uint8_t kNoAdditionalInfo = 0; + static constexpr uint8_t kReqTypeNotSupported = 1; + static constexpr uint8_t kSpeedNotSupported = 2; + static constexpr uint8_t kPageSizeNotSupported = 3; + static constexpr uint8_t kAccessDenied = 4; + static constexpr uint8_t kLogicalUnitNotSupported = 5; + static constexpr uint8_t kMaxPayloadTooSmall = 6; + static constexpr uint8_t kResourcesUnavailable = 8; + static constexpr uint8_t kFunctionRejected = 9; + static constexpr uint8_t kLoginIDNotRecognized = 10; + static constexpr uint8_t kDummyORBCompleted = 11; + static constexpr uint8_t kRequestAborted = 12; + static constexpr uint8_t kUnspecifiedError = 0xFF; } // Busy timeout register (CSR address 0xFFFFF0000210) diff --git a/ASFWDriver/Service/DriverContext.cpp b/ASFWDriver/Service/DriverContext.cpp index cef59fa5..579d2642 100644 --- a/ASFWDriver/Service/DriverContext.cpp +++ b/ASFWDriver/Service/DriverContext.cpp @@ -4,10 +4,13 @@ #include +#include + #include "../Async/AsyncSubsystem.hpp" #include "../Async/Interfaces/IFireWireBus.hpp" #include "../Async/PacketHelpers.hpp" #include "../Async/ResponseCode.hpp" +#include "../Async/Rx/PacketRouter.hpp" #include "../Async/Tx/ResponseSender.hpp" #include "../Audio/AudioCoordinator.hpp" #include "../Bus/BusManager.hpp" @@ -25,12 +28,30 @@ #include "../Hardware/HardwareInterface.hpp" #include "../Hardware/InterruptManager.hpp" #include "../Logging/Logging.hpp" +#include "../Protocols/AVC/AVCDiscovery.hpp" #include "../Protocols/AVC/FCPResponseRouter.hpp" #include "../Protocols/SBP2/AddressSpaceManager.hpp" +#include "../Protocols/SBP2/SBP2SessionRegistry.hpp" #include "../Scheduling/Scheduler.hpp" +namespace { + +void StopAndClearAsyncRouting(ServiceContext& ctx) { + if (!ctx.deps.asyncSubsystem) { + return; + } + + ctx.deps.asyncSubsystem->Stop(); + if (auto* router = ctx.deps.asyncSubsystem->GetPacketRouter()) { + router->ClearAllHandlers(); + } +} + +} // namespace + void ServiceContext::Reset() { stopping.store(true, std::memory_order_release); + StopAndClearAsyncRouting(*this); controller.reset(); audioCoordinator.reset(); deps.hardware.reset(); @@ -44,12 +65,13 @@ void ServiceContext::Reset() { deps.configRomStager.reset(); deps.interrupts.reset(); deps.topology.reset(); - deps.fcpResponseRouter.reset(); // Clean up FCP router + deps.fcpResponseRouter.reset(); // Clean up FCP router deps.sbp2AddressSpaceManager.reset(); - deps.avcDiscovery.reset(); // Clean up AV/C discovery - deps.irmClient.reset(); // Clean up IRM client + deps.sbp2SessionRegistry.reset(); + deps.avcDiscovery.reset(); // Clean up AV/C discovery + deps.irmClient.reset(); // Clean up IRM client deps.asyncController.reset(); - deps.asyncSubsystem.reset(); // Stop and cleanup asyncSubsystem + deps.asyncSubsystem.reset(); // Cleanup asyncSubsystem after handlers are cleared. statusPublisher.Reset(); watchdog.Reset(); #ifndef ASFW_HOST_TEST @@ -137,116 +159,186 @@ void DriverWiring::EnsureDeps(ASFWDriver* driver, ::ServiceContext& ctx) { void DriverWiring::EnsureSbp2Deps(::ServiceContext& ctx) { auto& d = ctx.deps; - if (!d.sbp2AddressSpaceManager && d.hardware) { - d.sbp2AddressSpaceManager = - std::make_shared(d.hardware.get()); - ASFW_LOG(Controller, "[Controller] SBP2 AddressSpaceManager initialized"); + if (!d.fcpResponseRouter && d.avcDiscovery && ctx.controller) { + auto& bus = ctx.controller->Bus(); + d.fcpResponseRouter = std::make_shared( + *d.avcDiscovery, + bus + ); + ASFW_LOG(Controller, "[Controller] FCPResponseRouter initialized"); } - if (ctx.controller) { - ctx.controller->SetSbp2AddressSpaceManager(d.sbp2AddressSpaceManager); + if (!d.sbp2AddressSpaceManager && d.hardware) { + d.sbp2AddressSpaceManager = std::make_shared( + d.hardware.get()); + ASFW_LOG(Controller, "[Controller] SBP2 AddressSpaceManager initialized"); } - if (!d.asyncSubsystem) { - return; + if (!d.sbp2SessionRegistry && ctx.controller && d.deviceManager && d.sbp2AddressSpaceManager) { + auto& bus = ctx.controller->Bus(); + d.sbp2SessionRegistry = + std::make_shared( + bus, + bus, + *d.sbp2AddressSpaceManager, + *d.deviceManager, + ctx.workQueue.get()); + ASFW_LOG(Controller, "[Controller] SBP2 SessionRegistry initialized"); } - auto* router = d.asyncSubsystem->GetPacketRouter(); - if (!router) { - return; - } + if (ctx.controller) { + ctx.controller->SetSbp2AddressSpaceManager(d.sbp2AddressSpaceManager); + ctx.controller->SetSBP2SessionRegistry(d.sbp2SessionRegistry); + } + + if (d.asyncSubsystem) { + if (auto* router = d.asyncSubsystem->GetPacketRouter()) { + auto* sbp2Manager = d.sbp2AddressSpaceManager.get(); + auto* fcpRouter = d.fcpResponseRouter.get(); + auto* responder = router->GetResponseSender(); + + // Quadlet write requests (tCode 0x0): direct address-space writes. + router->RegisterRequestHandler( + 0x0, + [sbp2Manager](const ASFW::Async::ARPacketView& packet) { + uint64_t destOffset = 0; + ASFW::Async::ResponseCode result = ASFW::Async::ResponseCode::AddressError; + if (!sbp2Manager || packet.header.size() < 16) { + return ASFW::Async::ResponseCode::AddressError; + } + + destOffset = ASFW::Async::ExtractDestOffset(packet.header); + const auto quadletData = + std::span(packet.header.data() + 12, 4); + result = sbp2Manager->ApplyRemoteWrite(destOffset, quadletData); + ASFW_LOG_V2( + Async, + "SBP2 AR write-quadlet req: src=0x%04x offset=0x%012llx len=4 -> rcode=0x%x", + packet.sourceID, + static_cast(destOffset), + static_cast(result)); + return result; + }); + + // Block write requests (tCode 0x1): SBP-2 first, then FCP fallback. + router->RegisterRequestHandler( + 0x1, + [sbp2Manager, fcpRouter](const ASFW::Async::ARPacketView& packet) { + uint64_t destOffset = 0; + if (sbp2Manager && packet.header.size() >= 16 && !packet.payload.empty()) { + destOffset = ASFW::Async::ExtractDestOffset(packet.header); + const auto sbp2Result = + sbp2Manager->ApplyRemoteWrite(destOffset, packet.payload); + ASFW_LOG_V2( + Async, + "SBP2 AR write-block req: src=0x%04x offset=0x%012llx len=%zu -> rcode=0x%x", + packet.sourceID, + static_cast(destOffset), + packet.payload.size(), + static_cast(sbp2Result)); + if (sbp2Result != ASFW::Async::ResponseCode::AddressError) { + return sbp2Result; + } + } + + if (fcpRouter) { + const Protocols::Ports::BlockWriteRequestView request{ + .sourceID = packet.sourceID, + .destOffset = ASFW::Async::ExtractDestOffset(packet.header), + .payload = packet.payload, + }; + const auto disposition = fcpRouter->RouteBlockWrite(request); + if (disposition == + Protocols::Ports::BlockWriteDisposition::kAddressError) { + return ASFW::Async::ResponseCode::AddressError; + } + return ASFW::Async::ResponseCode::Complete; + } - auto* sbp2Manager = d.sbp2AddressSpaceManager.get(); - auto* fcpRouter = d.fcpResponseRouter.get(); - auto* responder = router->GetResponseSender(); - - router->RegisterRequestHandler( - 0x0, - [sbp2Manager](const ASFW::Async::ARPacketView& packet) { - if (!sbp2Manager || packet.header.size() < 16) { - return ASFW::Async::ResponseCode::AddressError; - } - - const uint64_t destOffset = ASFW::Async::ExtractDestOffset(packet.header); - const auto quadletData = std::span(packet.header.data() + 12, 4); - return sbp2Manager->ApplyRemoteWrite(destOffset, quadletData); - }); - - router->RegisterRequestHandler( - 0x1, - [sbp2Manager, fcpRouter](const ASFW::Async::ARPacketView& packet) { - if (sbp2Manager && packet.header.size() >= 16 && !packet.payload.empty()) { - const uint64_t destOffset = ASFW::Async::ExtractDestOffset(packet.header); - const auto sbp2Result = - sbp2Manager->ApplyRemoteWrite(destOffset, packet.payload); - if (sbp2Result != ASFW::Async::ResponseCode::AddressError) { - return sbp2Result; - } - } - - if (fcpRouter) { - const ASFW::Protocols::Ports::BlockWriteRequestView request{ - .sourceID = packet.sourceID, - .destOffset = ASFW::Async::ExtractDestOffset(packet.header), - .payload = packet.payload, - }; - const auto disposition = fcpRouter->RouteBlockWrite(request); - if (disposition == ASFW::Protocols::Ports::BlockWriteDisposition::kAddressError) { return ASFW::Async::ResponseCode::AddressError; - } - return ASFW::Async::ResponseCode::Complete; - } - - return ASFW::Async::ResponseCode::AddressError; - }); - - router->RegisterRequestHandler( - 0x4, - [sbp2Manager, responder](const ASFW::Async::ARPacketView& packet) { - uint32_t quadlet = 0; - auto result = ASFW::Async::ResponseCode::AddressError; - - if (sbp2Manager && packet.header.size() >= 12) { - const uint64_t destOffset = ASFW::Async::ExtractDestOffset(packet.header); - result = sbp2Manager->ReadQuadlet(destOffset, &quadlet); - } - - if (responder) { - responder->SendReadQuadletResponse(packet, result, quadlet); - } - return ASFW::Async::ResponseCode::NoResponse; - }); - - router->RegisterRequestHandler( - 0x5, - [sbp2Manager, responder](const ASFW::Async::ARPacketView& packet) { - auto result = ASFW::Async::ResponseCode::AddressError; - ASFW::Protocols::SBP2::AddressSpaceManager::ReadSlice slice{}; - - if (sbp2Manager && packet.header.size() >= 16) { - const uint64_t destOffset = ASFW::Async::ExtractDestOffset(packet.header); - const auto readLength = - static_cast(ASFW::Async::ExtractDataLength(packet.header)); - if (readLength > 0) { - result = sbp2Manager->ResolveReadSlice(destOffset, readLength, &slice); - } else { - result = ASFW::Async::ResponseCode::DataError; - } - } - - if (responder) { - if (result == ASFW::Async::ResponseCode::Complete) { - responder->SendReadBlockResponse( - packet, result, slice.payloadDeviceAddress, slice.payloadLength); - } else { - responder->SendReadBlockResponse(packet, result, 0, 0); - } - } - return ASFW::Async::ResponseCode::NoResponse; - }); - - ASFW_LOG(Controller, - "[Controller] PacketRouter wired for SBP2 address spaces (tCode 0x0/0x1/0x4/0x5)"); + }); + + // Read quadlet requests (tCode 0x4): active read response from address-space manager. + router->RegisterRequestHandler( + 0x4, + [sbp2Manager, responder](const ASFW::Async::ARPacketView& packet) { + uint32_t quadlet = 0; + ASFW::Async::ResponseCode result = ASFW::Async::ResponseCode::AddressError; + + if (sbp2Manager && packet.header.size() >= 12) { + const uint64_t destOffset = ASFW::Async::ExtractDestOffset(packet.header); + result = sbp2Manager->ReadQuadlet(destOffset, &quadlet); + } + + if (responder) { + responder->SendReadQuadletResponse(packet, result, quadlet); + } + return ASFW::Async::ResponseCode::NoResponse; + }); + + // Read block requests (tCode 0x5): active read response from address-space manager. + router->RegisterRequestHandler( + 0x5, + [sbp2Manager, responder](const ASFW::Async::ARPacketView& packet) { + ASFW::Async::ResponseCode result = ASFW::Async::ResponseCode::AddressError; + ASFW::Protocols::SBP2::AddressSpaceManager::ReadSlice slice{}; + uint64_t destOffset = 0; + uint32_t readLength = 0; + + if (sbp2Manager && packet.header.size() >= 16) { + destOffset = ASFW::Async::ExtractDestOffset(packet.header); + readLength = + static_cast(ASFW::Async::ExtractDataLength(packet.header)); + if (readLength > 0) { + result = sbp2Manager->ResolveReadSlice(destOffset, readLength, &slice); + } else { + result = ASFW::Async::ResponseCode::DataError; + } + } + + if (result == ASFW::Async::ResponseCode::Complete) { + ASFW_LOG_V2( + Async, + "SBP2 AR read-block req: src=0x%04x offset=0x%012llx len=%u -> " + "rcode=0x%x payload=0x%08x/%u", + packet.sourceID, + static_cast(destOffset), + readLength, + static_cast(result), + static_cast(slice.payloadDeviceAddress), + slice.payloadLength); + } else { + ASFW_LOG_V2( + Async, + "SBP2 AR read-block req: src=0x%04x offset=0x%012llx len=%u -> " + "rcode=0x%x", + packet.sourceID, + static_cast(destOffset), + readLength, + static_cast(result)); + } + + if (responder) { + if (result == ASFW::Async::ResponseCode::Complete) { + responder->SendReadBlockResponse( + packet, + result, + slice.payloadDeviceAddress, + slice.payloadLength, + std::move(slice.backingLease)); + } else { + responder->SendReadBlockResponse(packet, result, 0, 0); + } + } + + return ASFW::Async::ResponseCode::NoResponse; + }); + + ASFW_LOG( + Controller, + "[Controller] PacketRouter wired for SBP2/FCP handlers (tCode 0x0/0x1/0x4/0x5)"); + } + } } kern_return_t DriverWiring::PrepareQueue(ASFWDriver& service, ::ServiceContext& ctx) { @@ -310,11 +402,9 @@ void DriverWiring::CleanupStartFailure(::ServiceContext& ctx) { ctx.controller.reset(); } - // CRITICAL: Stop asyncSubsystem BEFORE cancelling watchdog - // This prevents the crash where watchdog fires after completion queue is deactivated - if (ctx.deps.asyncSubsystem) { - ctx.deps.asyncSubsystem->Stop(); - } + // CRITICAL: Stop async routing before releasing protocol dependencies. + // This prevents PacketRouter callbacks from outliving captured SBP2/FCP objects. + StopAndClearAsyncRouting(ctx); if (ctx.deps.interrupts) ctx.deps.interrupts->Disable(); diff --git a/ASFWDriver/UserClient/Core/ASFWDriverUserClient.cpp b/ASFWDriver/UserClient/Core/ASFWDriverUserClient.cpp index ec7f5b69..8b6e9c14 100644 --- a/ASFWDriver/UserClient/Core/ASFWDriverUserClient.cpp +++ b/ASFWDriver/UserClient/Core/ASFWDriverUserClient.cpp @@ -58,6 +58,16 @@ enum { kMethodDeallocateAddressRange = 47, kMethodReadIncomingData = 48, kMethodWriteLocalData = 49, + // SBP-2 session management + kMethodCreateSBP2Session = 53, + kMethodStartSBP2Login = 54, + kMethodGetSBP2SessionState = 55, + kMethodSubmitSBP2Inquiry = 56, + kMethodGetSBP2InquiryResult = 57, + kMethodReleaseSBP2Session = 58, + kMethodSubmitSBP2Command = 59, + kMethodGetSBP2CommandResult = 60, + kMethodSubmitSBP2TaskManagement = 61, // TODO(ASFW-IRM): Remove temporary IRM test method after dedicated validation tooling exists. kMethodTestIRMAllocation = 26, kMethodTestIRMRelease = 27, @@ -205,6 +215,8 @@ kern_return_t IMPL(ASFWDriverUserClient, Stop) { ivars->driver = nullptr; } if (auto* runtimeState = ASFW::UserClient::GetRuntimeState(this); runtimeState != nullptr) { + // Release owner-bound SBP-2 resources before handler teardown so + // abrupt client exit cannot strand address ranges inside the driver. runtimeState->ReleaseOwner(this); runtimeState->ResetHandlers(); } @@ -494,6 +506,34 @@ kern_return_t ASFWDriverUserClient::ExternalMethod(uint64_t selector, case kMethodStopIsochTransmit: return runtimeState->Isoch().StopIsochTransmit(arguments); + // SBP-2 session management (53-61) + 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 kMethodReleaseSBP2Session: + return runtimeState->SBP2().ReleaseSBP2Session(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); + default: return kIOReturnBadArgument; } diff --git a/ASFWDriver/UserClient/Core/ASFWDriverUserClient.iig b/ASFWDriver/UserClient/Core/ASFWDriverUserClient.iig index 599ca8e7..49502ff1 100644 --- a/ASFWDriver/UserClient/Core/ASFWDriverUserClient.iig +++ b/ASFWDriver/UserClient/Core/ASFWDriverUserClient.iig @@ -59,6 +59,16 @@ public: kMethodDeallocateAddressRange = 47, kMethodReadIncomingData = 48, kMethodWriteLocalData = 49, + // SBP-2 session management + kMethodCreateSBP2Session = 53, + kMethodStartSBP2Login = 54, + kMethodGetSBP2SessionState = 55, + kMethodSubmitSBP2Inquiry = 56, + kMethodGetSBP2InquiryResult = 57, + kMethodReleaseSBP2Session = 58, + kMethodSubmitSBP2Command = 59, + kMethodGetSBP2CommandResult = 60, + kMethodSubmitSBP2TaskManagement = 61, // 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 666ed035..b9f59ada 100644 --- a/ASFWDriver/UserClient/Core/UserClientRuntimeState.hpp +++ b/ASFWDriver/UserClient/Core/UserClientRuntimeState.hpp @@ -51,10 +51,11 @@ class UserClientRuntimeState final { auto* controllerCore = GetControllerCorePtr(driver); auto* avcDiscovery = controllerCore ? controllerCore->GetAVCDiscovery() : nullptr; - auto* sbp2Manager = controllerCore ? controllerCore->GetSbp2AddressSpaceManager() : nullptr; + auto* sbp2Mgr = 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(sbp2Mgr, sbp2Registry); return HandlersReady(); } @@ -72,7 +73,10 @@ class UserClientRuntimeState final { } void ReleaseOwner(void* owner) noexcept { - if (owner != nullptr && sbp2Handler_ != nullptr) { + if (owner == nullptr) { + return; + } + if (sbp2Handler_ != nullptr) { sbp2Handler_->ReleaseOwner(owner); } } diff --git a/ASFWDriver/UserClient/Handlers/SBP2Handler.hpp b/ASFWDriver/UserClient/Handlers/SBP2Handler.hpp index 78e424a5..d628473a 100644 --- a/ASFWDriver/UserClient/Handlers/SBP2Handler.hpp +++ b/ASFWDriver/UserClient/Handlers/SBP2Handler.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -8,19 +9,24 @@ #include "../../Logging/Logging.hpp" #include "../../Protocols/SBP2/AddressSpaceManager.hpp" +#include "../../Protocols/SBP2/SCSICommandSet.hpp" +#include "../../Protocols/SBP2/SBP2SessionRegistry.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::SBP2SessionRegistry* registry) + : manager_(manager), registry_(registry) {} ~SBP2Handler() = default; SBP2Handler(const SBP2Handler&) = delete; SBP2Handler& operator=(const SBP2Handler&) = delete; + // Address space management (selectors 46-49) kern_return_t AllocateAddressRange(IOUserClientMethodArguments* args, void* owner) { if (!manager_) { return kIOReturnNotReady; @@ -78,14 +84,34 @@ class SBP2Handler { std::vector data; const kern_return_t kr = manager_->ReadIncomingData(owner, handle, offset, length, &data); if (kr != kIOReturnSuccess) { + ASFW_LOG(UserClient, + "ReadIncomingData: owner=%p handle=0x%llx offset=%u len=%u -> kr=0x%x", + owner, + static_cast(handle), + offset, + length, + static_cast(kr)); return kr; } OSData* output = OSData::withBytes(data.data(), static_cast(data.size())); if (!output) { + ASFW_LOG(UserClient, + "ReadIncomingData: owner=%p handle=0x%llx offset=%u len=%u -> no memory", + owner, + static_cast(handle), + offset, + length); return kIOReturnNoMemory; } + ASFW_LOG(UserClient, + "ReadIncomingData: owner=%p handle=0x%llx offset=%u len=%u -> %u bytes", + owner, + static_cast(handle), + offset, + length, + static_cast(data.size())); args->structureOutput = output; args->structureOutputDescriptor = nullptr; return kIOReturnSuccess; @@ -125,13 +151,293 @@ class SBP2Handler { } void ReleaseOwner(void* owner) { + if (registry_) { + registry_->ReleaseOwner(owner); + } if (manager_) { manager_->ReleaseOwner(owner); } } + // Session management (selectors 53-58) + + 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()); + memcpy(serialized.data(), &header, sizeof(header)); + + size_t offset = sizeof(Wire::SBP2CommandResultWire); + if (!result->payload.empty()) { + memcpy(serialized.data() + offset, result->payload.data(), result->payload.size()); + offset += result->payload.size(); + } + if (!result->senseData.empty()) { + 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::SBP2SessionRegistry* 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/AddressSpaceManagerTests.cpp b/tests/AddressSpaceManagerTests.cpp index 681a8944..12109c87 100644 --- a/tests/AddressSpaceManagerTests.cpp +++ b/tests/AddressSpaceManagerTests.cpp @@ -1,5 +1,6 @@ #include #include +#include #include "ASFWDriver/Protocols/SBP2/AddressSpaceManager.hpp" @@ -261,6 +262,65 @@ TEST(AddressSpaceManagerTests, AutoAllocationReusesFreedGap) { EXPECT_EQ(firstMeta.addressLo, thirdMeta.addressLo); } +TEST(AddressSpaceManagerTests, ClearingRemoteWriteCallbackStopsFurtherNotifications) { + ASFW::Protocols::SBP2::AddressSpaceManager manager(nullptr); + + uint64_t handle = 0; + ASFW::Protocols::SBP2::AddressSpaceManager::AddressRangeMeta meta{}; + ASSERT_EQ(kIOReturnSuccess, + manager.AllocateAddressRange(reinterpret_cast(0xC), + 0xFFFF, + 0x0050'0000, + 8, + &handle, + &meta)); + + int callbackCount = 0; + manager.SetRemoteWriteCallback( + handle, + [&callbackCount](uint64_t, uint32_t, std::span) { + ++callbackCount; + }); + + const std::array payload{0xDE, 0xAD, 0xBE, 0xEF}; + EXPECT_EQ(ASFW::Async::ResponseCode::Complete, + manager.ApplyRemoteWrite( + ComposeAddress(meta.addressHi, meta.addressLo), + std::span(payload.data(), payload.size()))); + EXPECT_EQ(1, callbackCount); + + manager.SetRemoteWriteCallback(handle, {}); + EXPECT_EQ(ASFW::Async::ResponseCode::Complete, + manager.ApplyRemoteWrite( + ComposeAddress(meta.addressHi, meta.addressLo), + std::span(payload.data(), payload.size()))); + EXPECT_EQ(1, callbackCount); +} + +TEST(AddressSpaceManagerTests, WriteLocalDataRejectsSizeTooLargeForWireLength) { + ASFW::Protocols::SBP2::AddressSpaceManager manager(nullptr); + + constexpr std::size_t tooLarge = + static_cast(std::numeric_limits::max()) + 1u; + const auto hugePayload = + std::span(reinterpret_cast(0x1000), tooLarge); + + EXPECT_EQ(kIOReturnBadArgument, + manager.WriteLocalData(reinterpret_cast(0xE), 1, 0, hugePayload)); +} + +TEST(AddressSpaceManagerTests, ApplyRemoteWriteRejectsSizeTooLargeForWireLength) { + ASFW::Protocols::SBP2::AddressSpaceManager manager(nullptr); + + constexpr std::size_t tooLarge = + static_cast(std::numeric_limits::max()) + 1u; + const auto hugePayload = + std::span(reinterpret_cast(0x1000), tooLarge); + + EXPECT_EQ(ASFW::Async::ResponseCode::DataError, + manager.ApplyRemoteWrite(ComposeAddress(0xFFFF, 0x0050'0000), hugePayload)); +} + TEST(AddressSpaceManagerTests, AutoAllocationRejectsRequestLargerThanWindow) { ASFW::Protocols::SBP2::AddressSpaceManager manager(nullptr); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index aaee4000..b919e4d5 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1252,6 +1252,101 @@ target_include_directories(SimITEngineTests PRIVATE ${ASFW_COMMON_INCLUDES}) gtest_discover_tests(SimITEngineTests) +# SBP-2 Address Space Manager Tests +add_executable(AddressSpaceManagerTests + "${ASFW_TESTS_DIR}/AddressSpaceManagerTests.cpp" + "${ASFW_TESTS_DIR}/HardwareInterfaceStub.cpp" +) + +target_link_libraries(AddressSpaceManagerTests + PRIVATE + GTest::gtest_main +) + +target_compile_definitions(AddressSpaceManagerTests + PRIVATE + ASFW_HOST_TEST +) + +target_include_directories(AddressSpaceManagerTests PRIVATE ${ASFW_COMMON_INCLUDES}) + +gtest_discover_tests(AddressSpaceManagerTests) + +# SBP-2 Login Session Tests +add_executable(SBP2LoginSessionTests + "${ASFW_TESTS_DIR}/SBP2LoginSessionTests.cpp" + "${ASFW_DRIVER_DIR}/Protocols/SBP2/SBP2LoginSession.cpp" + "${ASFW_DRIVER_DIR}/Protocols/SBP2/SBP2CommandORB.cpp" + "${ASFW_DRIVER_DIR}/Protocols/SBP2/SBP2ManagementORB.cpp" + "${ASFW_TESTS_DIR}/HardwareInterfaceStub.cpp" + "${ASFW_TESTS_DIR}/LoggingStubs.cpp" +) + +target_link_libraries(SBP2LoginSessionTests + PRIVATE + GTest::gtest_main +) + +target_compile_definitions(SBP2LoginSessionTests + PRIVATE + ASFW_HOST_TEST +) + +target_include_directories(SBP2LoginSessionTests PRIVATE ${ASFW_COMMON_INCLUDES}) + +gtest_discover_tests(SBP2LoginSessionTests) + +# SBP-2 ORB timer tests +add_executable(SBP2ORBTests + "${ASFW_TESTS_DIR}/SBP2ORBTests.cpp" + "${ASFW_DRIVER_DIR}/Protocols/SBP2/SBP2CommandORB.cpp" + "${ASFW_DRIVER_DIR}/Protocols/SBP2/SBP2ManagementORB.cpp" + "${ASFW_TESTS_DIR}/HardwareInterfaceStub.cpp" + "${ASFW_TESTS_DIR}/LoggingStubs.cpp" +) + +target_link_libraries(SBP2ORBTests + PRIVATE + GTest::gtest_main +) + +target_compile_definitions(SBP2ORBTests + PRIVATE + ASFW_HOST_TEST +) + +target_include_directories(SBP2ORBTests PRIVATE ${ASFW_COMMON_INCLUDES}) + +gtest_discover_tests(SBP2ORBTests) + +# SBP-2 Session Registry / command tests +add_executable(SBP2SessionRegistryTests + "${ASFW_TESTS_DIR}/SBP2SessionRegistryTests.cpp" + "${ASFW_DRIVER_DIR}/Protocols/SBP2/SBP2SessionRegistry.cpp" + "${ASFW_DRIVER_DIR}/Protocols/SBP2/SBP2LoginSession.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/FWDevice.cpp" + "${ASFW_DRIVER_DIR}/Discovery/FWUnit.cpp" + "${ASFW_TESTS_DIR}/HardwareInterfaceStub.cpp" + "${ASFW_TESTS_DIR}/LoggingStubs.cpp" +) + +target_link_libraries(SBP2SessionRegistryTests + PRIVATE + GTest::gtest_main +) + +target_compile_definitions(SBP2SessionRegistryTests + PRIVATE + ASFW_HOST_TEST +) + +target_include_directories(SBP2SessionRegistryTests PRIVATE ${ASFW_COMMON_INCLUDES}) + +gtest_discover_tests(SBP2SessionRegistryTests) + # Device protocol factory routing tests add_executable(DeviceProtocolFactoryTests "${ASFW_TESTS_DIR}/DeviceProtocolFactoryTests.cpp" @@ -1481,44 +1576,3 @@ target_compile_definitions(ROMScanNodeStateMachineTests target_include_directories(ROMScanNodeStateMachineTests PRIVATE ${ASFW_COMMON_INCLUDES}) gtest_discover_tests(ROMScanNodeStateMachineTests) - -add_executable(AddressSpaceManagerTests - "${ASFW_TESTS_DIR}/AddressSpaceManagerTests.cpp" - "${ASFW_TESTS_DIR}/HardwareInterfaceStub.cpp" -) - -target_link_libraries(AddressSpaceManagerTests - PRIVATE - GTest::gtest_main -) - -target_compile_definitions(AddressSpaceManagerTests - PRIVATE - ASFW_HOST_TEST -) - -target_include_directories(AddressSpaceManagerTests PRIVATE ${ASFW_COMMON_INCLUDES}) - -gtest_discover_tests(AddressSpaceManagerTests) - -add_executable(SBP2ORBTests - "${ASFW_TESTS_DIR}/SBP2ORBTests.cpp" - "${ASFW_DRIVER_DIR}/Protocols/SBP2/SBP2CommandORB.cpp" - "${ASFW_DRIVER_DIR}/Protocols/SBP2/SBP2ManagementORB.cpp" - "${ASFW_TESTS_DIR}/HardwareInterfaceStub.cpp" - "${ASFW_TESTS_DIR}/LoggingStubs.cpp" -) - -target_link_libraries(SBP2ORBTests - PRIVATE - GTest::gtest_main -) - -target_compile_definitions(SBP2ORBTests - PRIVATE - ASFW_HOST_TEST -) - -target_include_directories(SBP2ORBTests PRIVATE ${ASFW_COMMON_INCLUDES}) - -gtest_discover_tests(SBP2ORBTests) diff --git a/tests/ResponseSenderStub.cpp b/tests/ResponseSenderStub.cpp index 452ca885..b848b2e0 100644 --- a/tests/ResponseSenderStub.cpp +++ b/tests/ResponseSenderStub.cpp @@ -28,20 +28,29 @@ void ResponseSender::SendReadQuadletResponse(const ARPacketView& request, void ResponseSender::SendReadBlockResponse(const ARPacketView& request, ResponseCode rcode, uint64_t payloadDeviceAddress, - uint32_t payloadLength) noexcept { + uint32_t payloadLength, + std::shared_ptr payloadLease) noexcept { (void)request; (void)rcode; (void)payloadDeviceAddress; (void)payloadLength; + (void)payloadLease; } +void ResponseSender::OnTxCompletion(const TxCompletion& completion) noexcept { + (void)completion; +} + +void ResponseSender::ClearOutstandingResponses() noexcept {} + void ResponseSender::SendResponse(const ARPacketView& request, ResponseCode rcode, uint8_t responseTCode, const uint32_t* header, std::size_t headerBytes, uint64_t payloadDeviceAddress, - std::size_t payloadLength) noexcept { + std::size_t payloadLength, + std::shared_ptr payloadLease) noexcept { (void)request; (void)rcode; (void)responseTCode; @@ -49,6 +58,7 @@ void ResponseSender::SendResponse(const ARPacketView& request, (void)headerBytes; (void)payloadDeviceAddress; (void)payloadLength; + (void)payloadLease; } } // namespace ASFW::Async diff --git a/tests/SBP2LoginSessionTests.cpp b/tests/SBP2LoginSessionTests.cpp new file mode 100644 index 00000000..637ddd19 --- /dev/null +++ b/tests/SBP2LoginSessionTests.cpp @@ -0,0 +1,628 @@ +#include + +#include "ASFWDriver/Protocols/SBP2/SBP2LoginSession.hpp" +#include "ASFWDriver/Testing/HostDriverKitStubs.hpp" +#include "tests/mocks/DeferredFireWireBus.hpp" + +#include +#include +#include +#include + +namespace { + +constexpr uint16_t kTargetNodeID = 0x05; + +using ASFW::Protocols::SBP2::AddressSpaceManager; +using ASFW::Protocols::SBP2::LoginState; +using ASFW::Protocols::SBP2::SBP2CommandORB; +using ASFW::Protocols::SBP2::SBP2LoginSession; +using ASFW::Protocols::SBP2::SBP2TargetInfo; +using ASFW::Protocols::SBP2::Wire::FromBE16; +using ASFW::Protocols::SBP2::Wire::CommandBlockAgentOffsets; +using ASFW::Protocols::SBP2::Wire::FromBE32; +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; +using ASFW::Protocols::SBP2::Wire::ToBE16; +using ASFW::Protocols::SBP2::Wire::ToBE32; +namespace SBPStatus = ASFW::Protocols::SBP2::Wire::SBPStatus; + +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 = FromBE32(ReadQuadlet(manager, orbAddress + hiOffset)); + const uint32_t lo = FromBE32(ReadQuadlet(manager, orbAddress + loOffset)); + return ComposeAddress(static_cast(hi & 0xFFFFu), lo); +} + +class SessionRig { +public: + SessionRig() + : sessionOwner(std::make_shared(bus, bus, addressManager)) + , session(*sessionOwner) { + workQueue.SetManualDispatchForTesting(true); + timeoutQueue.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); + + SBP2TargetInfo info{}; + info.managementAgentOffset = 0x100; + info.lun = 3; + info.managementTimeoutMs = 10; + info.maxORBSize = 32; + info.maxCommandBlockSize = 12; + info.targetNodeId = kTargetNodeID; + + session.SetWorkQueue(&workQueue); + session.SetTimeoutQueue(&timeoutQueue); + session.Configure(info); + } + + ~SessionRig() { + ASFW::Testing::ResetHostMonotonicClockForTesting(); + } + + void DrainReady() { + while (workQueue.DrainReadyForTesting() > 0U || + timeoutQueue.DrainReadyForTesting() > 0U) { + } + } + + void AdvanceMs(uint64_t milliseconds) { + nowNs += milliseconds * 1'000'000ULL; + DrainReady(); + } + + 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 = ToBE16(LoginResponse::kSize); + response.loginID = ToBE16(loginId); + response.commandBlockAgentAddressHi = ToBE32(0x0000'FFFFu); + response.commandBlockAgentAddressLo = ToBE32(commandBlockAgentLo); + response.reconnectHold = ToBE16(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)); + } + } + DrainReady(); + ASSERT_EQ(LoginState::LoggedIn, session.State()); + } + + ASFW::Async::Testing::DeferredFireWireBus bus; + AddressSpaceManager addressManager{nullptr}; + std::shared_ptr sessionOwner; + SBP2LoginSession& session; + IODispatchQueue workQueue; + IODispatchQueue timeoutQueue; + uint64_t nowNs{0}; + uint64_t sessionStatusAddress{0}; +}; + +TEST(SBP2LoginSessionTests, 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.DrainReady(); + + rig.AdvanceMs(5); + + EXPECT_EQ(LoginState::LoggingIn, rig.session.State()); + EXPECT_EQ(1u, rig.bus.WriteCount()); +} + +TEST(SBP2LoginSessionTests, 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 = FromBE32( + ReadQuadlet(rig.addressManager, loginOrbAddress + offsetof(LoginORB, options))); + const uint32_t quadlet5 = FromBE32( + 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(SBP2LoginSessionTests, 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 = FromBE32( + ReadQuadlet(rig.addressManager, loginOrbAddress + offsetof(LoginORB, loginResponseAddressHi))); + const uint32_t statusHi = FromBE32( + 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(SBP2LoginSessionTests, 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(SBP2LoginSessionTests, LoginWritesBusyTimeoutRegister) { + SessionRig rig; + + rig.LoginSuccessfully(); + + ASSERT_GE(rig.bus.WriteCount(), 2u); + ExpectBusyTimeoutWrite(rig.bus.WriteAt(1), kTargetNodeID); +} + +TEST(SBP2LoginSessionTests, 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(SBP2LoginSessionTests, 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.DrainReady(); + + 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(SBP2LoginSessionTests, 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(SBP2LoginSessionTests, 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(SBP2LoginSessionTests, LoginRetryDelayUsesTimeoutQueueInsteadOfWorkQueue) { + SessionRig rig; + + ASSERT_TRUE(rig.session.Login()); + rig.bus.SetGeneration(ASFW::FW::Generation{2}); + rig.session.HandleBusReset(2); + + EXPECT_EQ(0u, rig.workQueue.PendingTaskCountForTesting()); + EXPECT_GT(rig.timeoutQueue.PendingTaskCountForTesting(), 0u); + + int workExecuted = 0; + rig.workQueue.DispatchAsync([&workExecuted]() { ++workExecuted; }); + rig.DrainReady(); + + EXPECT_EQ(1, workExecuted); +} + +TEST(SBP2LoginSessionTests, 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(SBP2LoginSessionTests, 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.DrainReady(); + + 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(SBP2LoginSessionTests, 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.timeoutQueue.PendingTaskCountForTesting(); + + ASSERT_TRUE(rig.session.SubmitORB(&orb)); + EXPECT_EQ(pendingTimersBeforeSubmit, rig.timeoutQueue.PendingTaskCountForTesting()); + + ASSERT_TRUE(rig.bus.CompleteNextWrite(ASFW::Async::AsyncStatus::kSuccess)); + EXPECT_EQ(pendingTimersBeforeSubmit + 1U, rig.timeoutQueue.PendingTaskCountForTesting()); + + rig.AdvanceMs(24); + EXPECT_EQ(0, callbackCount); + + rig.AdvanceMs(1); + EXPECT_EQ(1, callbackCount); + EXPECT_EQ(-1, callbackStatus); +} + +TEST(SBP2LoginSessionTests, 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)); + rig.DrainReady(); + + ASSERT_TRUE(rig.session.SubmitORB(&second)); + ASSERT_TRUE(rig.bus.CompleteNextWrite(ASFW::Async::AsyncStatus::kSuccess)); + rig.DrainReady(); + + StatusBlock status{}; + const auto firstAddress = first.GetORBAddress(); + status.details = 0; + status.sbpStatus = SBPStatus::kNoAdditionalInfo; + status.orbOffsetHi = ToBE16(firstAddress.addressHi); + status.orbOffsetLo = ToBE32(firstAddress.addressLo); + + rig.addressManager.ApplyRemoteWrite( + rig.sessionStatusAddress, + std::span{reinterpret_cast(&status), sizeof(status)}); + + EXPECT_EQ(0, firstStatus); + EXPECT_EQ(99, secondStatus); +} + +TEST(SBP2LoginSessionTests, 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.DrainReady(); + + 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(SBP2LoginSessionTests, 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)); + rig.DrainReady(); + + 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)); + rig.DrainReady(); +} + +TEST( + SBP2LoginSessionTests, + 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)); + rig.DrainReady(); + + 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)); + rig.DrainReady(); +} + +} // namespace diff --git a/tests/SBP2ORBTests.cpp b/tests/SBP2ORBTests.cpp index 689494bd..48905545 100644 --- a/tests/SBP2ORBTests.cpp +++ b/tests/SBP2ORBTests.cpp @@ -2,7 +2,6 @@ #include "ASFWDriver/Protocols/SBP2/SBP2CommandORB.hpp" #include "ASFWDriver/Protocols/SBP2/SBP2ManagementORB.hpp" -#include "ASFWDriver/Protocols/SBP2/SBP2PageTable.hpp" #include "ASFWDriver/Protocols/SBP2/SBP2WireFormats.hpp" #include "ASFWDriver/Testing/HostDriverKitStubs.hpp" #include "tests/mocks/DeferredFireWireBus.hpp" @@ -10,6 +9,7 @@ #include #include #include +#include #include namespace { @@ -17,8 +17,6 @@ namespace { using ASFW::Protocols::SBP2::AddressSpaceManager; using ASFW::Protocols::SBP2::SBP2CommandORB; using ASFW::Protocols::SBP2::SBP2ManagementORB; -using ASFW::Protocols::SBP2::SBP2PageTable; -using ASFW::Protocols::SBP2::Wire::FromBE16; using ASFW::Protocols::SBP2::Wire::FromBE32; using ASFW::Protocols::SBP2::Wire::ManagementAgentAddressLo; using ASFW::Protocols::SBP2::Wire::NormalizeBusNodeID; @@ -61,7 +59,8 @@ uint64_t ReadStatusAddressFromManagementORB(AddressSpaceManager& manager, uint64 class ORBTimerRig { public: ORBTimerRig() { - queue.SetManualDispatchForTesting(true); + workQueue.SetManualDispatchForTesting(true); + timeoutQueue.SetManualDispatchForTesting(true); ASFW::Testing::SetHostMonotonicClockForTesting([this]() { return nowNs; }); bus.SetGeneration(ASFW::FW::Generation{1}); @@ -74,7 +73,8 @@ class ORBTimerRig { } void DrainReady() { - while (queue.DrainReadyForTesting() > 0U) { + while (workQueue.DrainReadyForTesting() > 0U || + timeoutQueue.DrainReadyForTesting() > 0U) { } } @@ -85,10 +85,37 @@ class ORBTimerRig { ASFW::Async::Testing::DeferredFireWireBus bus; AddressSpaceManager addressManager{nullptr}; - IODispatchQueue queue; + IODispatchQueue workQueue; + IODispatchQueue timeoutQueue; uint64_t nowNs{0}; }; +TEST(SBP2ORBTests, StatusBlockDetailsDecodeSBP2Layout) { + StatusBlock status{}; + status.details = 0b10011101; + + EXPECT_EQ(2, status.Source()); + EXPECT_EQ(1, status.Response()); + EXPECT_EQ(1, status.DeadBit()); + EXPECT_EQ(5, status.Length()); +} + +TEST(SBP2ORBTests, SBPStatusConstantsMatchSBP2RequestStatus) { + EXPECT_EQ(0, SBPStatus::kNoAdditionalInfo); + EXPECT_EQ(1, SBPStatus::kReqTypeNotSupported); + EXPECT_EQ(2, SBPStatus::kSpeedNotSupported); + EXPECT_EQ(3, SBPStatus::kPageSizeNotSupported); + EXPECT_EQ(4, SBPStatus::kAccessDenied); + EXPECT_EQ(5, SBPStatus::kLogicalUnitNotSupported); + EXPECT_EQ(6, SBPStatus::kMaxPayloadTooSmall); + EXPECT_EQ(8, SBPStatus::kResourcesUnavailable); + EXPECT_EQ(9, SBPStatus::kFunctionRejected); + EXPECT_EQ(10, SBPStatus::kLoginIDNotRecognized); + EXPECT_EQ(11, SBPStatus::kDummyORBCompleted); + EXPECT_EQ(12, SBPStatus::kRequestAborted); + EXPECT_EQ(0xFF, SBPStatus::kUnspecifiedError); +} + TEST(SBP2ORBTests, CommandORBTimerFiresOnHostQueue) { ORBTimerRig rig; @@ -97,12 +124,42 @@ TEST(SBP2ORBTests, CommandORBTimerFiresOnHostQueue) { orb.SetTimeout(5); orb.SetCompletionCallback([&completionStatus](int status, uint8_t) { completionStatus = status; }); - orb.StartTimer(&rig.queue); + orb.StartTimer(&rig.workQueue, &rig.timeoutQueue); rig.AdvanceMs(5); EXPECT_EQ(-1, completionStatus); } +TEST(SBP2ORBTests, CommandORBRejectsOversizedCDBWithoutOverwritingCommandBlock) { + ORBTimerRig rig; + + SBP2CommandORB orb(rig.addressManager, reinterpret_cast(0x8), 4); + const std::array originalCDB{0x12, 0x34, 0x56, 0x78}; + const std::array oversizedCDB{0xFF, 0xEE, 0xDD, 0xCC, 0xBB, 0xAA}; + + ASSERT_TRUE(orb.SetCommandBlock(originalCDB)); + EXPECT_FALSE(orb.SetCommandBlock(oversizedCDB)); + 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); + const uint32_t commandQuadlet = ReadQuadlet( + rig.addressManager, + packedAddress + ASFW::Protocols::SBP2::Wire::NormalORB::kHeaderSize); + + std::array writtenCDB{}; + std::memcpy(writtenCDB.data(), &commandQuadlet, writtenCDB.size()); + EXPECT_EQ(originalCDB, writtenCDB); +} + +TEST(SBP2ORBTests, CommandORBRejectsOutOfRangeMaxPayloadLog) { + ORBTimerRig rig; + + SBP2CommandORB orb(rig.addressManager, reinterpret_cast(0x9), 4); + + EXPECT_EQ(kIOReturnBadArgument, orb.PrepareForExecution(0x21, ASFW::FW::FwSpeed::S400, 16)); +} + TEST(SBP2ORBTests, CommandORBCancelSuppressesPendingTimeout) { ORBTimerRig rig; @@ -111,7 +168,7 @@ TEST(SBP2ORBTests, CommandORBCancelSuppressesPendingTimeout) { orb.SetTimeout(5); orb.SetCompletionCallback([&completionCount](int, uint8_t) { ++completionCount; }); - orb.StartTimer(&rig.queue); + orb.StartTimer(&rig.workQueue, &rig.timeoutQueue); orb.CancelTimer(); rig.AdvanceMs(5); @@ -127,65 +184,13 @@ TEST(SBP2ORBTests, CommandORBDestructionInvalidatesPendingTimeout) { rig.addressManager, reinterpret_cast(0x3), 16); orb->SetTimeout(5); orb->SetCompletionCallback([&completionCount](int, uint8_t) { ++completionCount; }); - orb->StartTimer(&rig.queue); + orb->StartTimer(&rig.workQueue, &rig.timeoutQueue); } rig.AdvanceMs(5); EXPECT_EQ(0, completionCount); } -TEST(SBP2ORBTests, PageTableUsesDirectDescriptorForSingleAlignedSegment) { - ORBTimerRig rig; - - SBP2PageTable pageTable(rig.addressManager, reinterpret_cast(0x40)); - const std::array segments{{ - {.address = 0x0001'2345'6000ULL, .length = 512}, - }}; - - ASSERT_TRUE(pageTable.Build(segments, 0x21)); - - const auto& result = pageTable.GetResult(); - EXPECT_TRUE(result.isDirect); - EXPECT_EQ(1u, pageTable.EntryCount()); - EXPECT_EQ(0x0001u, FromBE32(result.dataDescriptorHi) & 0xFFFFu); - EXPECT_EQ(0x2345'6000u, FromBE32(result.dataDescriptorLo)); - EXPECT_EQ(512u, FromBE16(result.dataSize)); - EXPECT_EQ(0u, result.options); -} - -TEST(SBP2ORBTests, PageTableSplitsSegmentsIntoPublishedEntries) { - ORBTimerRig rig; - - SBP2PageTable pageTable(rig.addressManager, reinterpret_cast(0x41)); - const std::array segments{{ - {.address = 0x0001'0000'1000ULL, .length = 0x30}, - }}; - - ASSERT_TRUE(pageTable.Build(segments, 0x21, 0x10)); - - const auto& result = pageTable.GetResult(); - ASSERT_FALSE(result.isDirect); - ASSERT_EQ(3u, pageTable.EntryCount()); - EXPECT_EQ(3u, FromBE16(result.dataSize)); - EXPECT_EQ(ASFW::Protocols::SBP2::Wire::Options::kPageTableUnrestricted, - result.options); - - const uint32_t descriptorHi = FromBE32(result.dataDescriptorHi); - const uint16_t expectedNode = NormalizeBusNodeID(0x21); - EXPECT_EQ(expectedNode, static_cast(descriptorHi >> 16)); - EXPECT_EQ(0xFFFFu, descriptorHi & 0xFFFFu); - - const uint64_t tableAddress = - ComposeAddress(static_cast(descriptorHi & 0xFFFFu), - FromBE32(result.dataDescriptorLo)); - const uint32_t firstEntryHeader = FromBE32(ReadQuadlet(rig.addressManager, tableAddress)); - const uint32_t firstEntryLo = FromBE32(ReadQuadlet(rig.addressManager, tableAddress + 4)); - - EXPECT_EQ(0x0010u, firstEntryHeader >> 16); - EXPECT_EQ(0x0001u, firstEntryHeader & 0xFFFFu); - EXPECT_EQ(0x0000'1000u, firstEntryLo); -} - TEST(SBP2ORBTests, ManagementORBStatusWriteCancelsTimeout) { ORBTimerRig rig; @@ -195,7 +200,8 @@ TEST(SBP2ORBTests, ManagementORBStatusWriteCancelsTimeout) { orb.SetManagementAgentOffset(0x80); orb.SetTargetNode(1, 0x3F); orb.SetTimeout(5); - orb.SetWorkQueue(&rig.queue); + orb.SetWorkQueue(&rig.workQueue); + orb.SetTimeoutQueue(&rig.timeoutQueue); int completionStatus = 99; orb.SetCompletionCallback([&completionStatus](int status) { completionStatus = status; }); @@ -213,6 +219,8 @@ TEST(SBP2ORBTests, ManagementORBStatusWriteCancelsTimeout) { StatusBlock status{}; status.details = 0; status.sbpStatus = SBPStatus::kNoAdditionalInfo; + status.orbOffsetHi = ToBE16(static_cast((orbAddress >> 32) & 0xFFFFu)); + status.orbOffsetLo = ToBE32(static_cast(orbAddress & 0xFFFF'FFFFu)); rig.addressManager.ApplyRemoteWrite( ReadStatusAddressFromManagementORB(rig.addressManager, orbAddress), std::span{reinterpret_cast(&status), sizeof(status)}); @@ -257,7 +265,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); @@ -269,6 +277,38 @@ TEST(SBP2ORBTests, CommandORBDirectDescriptorUsesFullBusNodeId) { EXPECT_EQ((static_cast(expectedNode) << 16) | 0xFFFFu, dataDescriptorHi); } +TEST(SBP2ORBTests, PageTableDestructorReleasesAddressRange) { + ORBTimerRig rig; + + uint64_t pageTableAddress = 0; + { + ASFW::Protocols::SBP2::SBP2PageTable pageTable( + rig.addressManager, reinterpret_cast(0x10)); + ASFW::Protocols::SBP2::SBP2PageTable::Segment segment{ + .address = 0x2000, + .length = 0xF004, + }; + + ASSERT_TRUE(pageTable.Build( + std::span(&segment, 1), + 0x21)); + ASSERT_FALSE(pageTable.GetResult().isDirect); + + const uint32_t descriptorHi = FromBE32(pageTable.GetResult().dataDescriptorHi); + const uint32_t descriptorLo = FromBE32(pageTable.GetResult().dataDescriptorLo); + pageTableAddress = ComposeAddress(static_cast(descriptorHi & 0xFFFFu), + descriptorLo); + + uint32_t pteQuadlet = 0; + EXPECT_EQ(ASFW::Async::ResponseCode::Complete, + rig.addressManager.ReadQuadlet(pageTableAddress, &pteQuadlet)); + } + + uint32_t pteQuadlet = 0; + EXPECT_EQ(ASFW::Async::ResponseCode::AddressError, + rig.addressManager.ReadQuadlet(pageTableAddress, &pteQuadlet)); +} + TEST(SBP2ORBTests, ManagementORBDestructionInvalidatesPendingTimeout) { ORBTimerRig rig; @@ -281,7 +321,8 @@ TEST(SBP2ORBTests, ManagementORBDestructionInvalidatesPendingTimeout) { orb->SetManagementAgentOffset(0x81); orb->SetTargetNode(1, 0x3F); orb->SetTimeout(5); - orb->SetWorkQueue(&rig.queue); + orb->SetWorkQueue(&rig.workQueue); + orb->SetTimeoutQueue(&rig.timeoutQueue); orb->SetCompletionCallback([&completionCount](int) { ++completionCount; }); ASSERT_TRUE(orb->Execute()); @@ -293,4 +334,140 @@ TEST(SBP2ORBTests, ManagementORBDestructionInvalidatesPendingTimeout) { EXPECT_EQ(0, completionCount); } +TEST(SBP2ORBTests, ManagementORBPropagatesDeviceStatusFailure) { + ORBTimerRig rig; + + SBP2ManagementORB orb(rig.bus, rig.bus, rig.addressManager, reinterpret_cast(0x8)); + orb.SetFunction(SBP2ManagementORB::Function::LogicalUnitReset); + orb.SetLoginID(0x44); + orb.SetManagementAgentOffset(0x90); + orb.SetTargetNode(1, 0x3F); + orb.SetTimeout(5); + orb.SetWorkQueue(&rig.workQueue); + orb.SetTimeoutQueue(&rig.timeoutQueue); + + int completionStatus = 99; + orb.SetCompletionCallback([&completionStatus](int status) { completionStatus = status; }); + + ASSERT_TRUE(orb.Execute()); + const auto& write = rig.bus.WriteAt(0); + const uint64_t orbAddress = DecodeOrbAddressFromPayload(write.data); + + ASSERT_TRUE(rig.bus.CompleteNextWrite(ASFW::Async::AsyncStatus::kSuccess)); + rig.DrainReady(); + + StatusBlock status{}; + status.details = 0; + status.sbpStatus = SBPStatus::kFunctionRejected; + status.orbOffsetHi = ToBE16(static_cast((orbAddress >> 32) & 0xFFFFu)); + status.orbOffsetLo = ToBE32(static_cast(orbAddress & 0xFFFF'FFFFu)); + rig.addressManager.ApplyRemoteWrite( + ReadStatusAddressFromManagementORB(rig.addressManager, orbAddress), + std::span{reinterpret_cast(&status), sizeof(status)}); + + rig.DrainReady(); + EXPECT_EQ(-4, completionStatus); +} + +TEST(SBP2ORBTests, ManagementORBRejectsMalformedStatusPayload) { + ORBTimerRig rig; + + SBP2ManagementORB orb(rig.bus, rig.bus, rig.addressManager, reinterpret_cast(0x9)); + orb.SetFunction(SBP2ManagementORB::Function::AbortTaskSet); + orb.SetLoginID(0x45); + orb.SetManagementAgentOffset(0x91); + orb.SetTargetNode(1, 0x3F); + orb.SetTimeout(5); + orb.SetWorkQueue(&rig.workQueue); + orb.SetTimeoutQueue(&rig.timeoutQueue); + + int completionStatus = 99; + orb.SetCompletionCallback([&completionStatus](int status) { completionStatus = status; }); + + ASSERT_TRUE(orb.Execute()); + const auto& write = rig.bus.WriteAt(0); + const uint64_t orbAddress = DecodeOrbAddressFromPayload(write.data); + + ASSERT_TRUE(rig.bus.CompleteNextWrite(ASFW::Async::AsyncStatus::kSuccess)); + rig.DrainReady(); + + const std::array shortPayload{0, 0, 0, 0}; + rig.addressManager.ApplyRemoteWrite( + ReadStatusAddressFromManagementORB(rig.addressManager, orbAddress), + std::span{shortPayload.data(), shortPayload.size()}); + + rig.DrainReady(); + EXPECT_EQ(-3, completionStatus); +} + +TEST(SBP2ORBTests, ManagementORBRejectsMismatchedStatusORBAddress) { + ORBTimerRig rig; + + SBP2ManagementORB orb(rig.bus, rig.bus, rig.addressManager, reinterpret_cast(0xA)); + orb.SetFunction(SBP2ManagementORB::Function::AbortTaskSet); + orb.SetLoginID(0x46); + orb.SetManagementAgentOffset(0x92); + orb.SetTargetNode(1, 0x3F); + orb.SetTimeout(5); + orb.SetWorkQueue(&rig.workQueue); + orb.SetTimeoutQueue(&rig.timeoutQueue); + + int completionStatus = 99; + orb.SetCompletionCallback([&completionStatus](int status) { completionStatus = status; }); + + ASSERT_TRUE(orb.Execute()); + const auto& write = rig.bus.WriteAt(0); + const uint64_t orbAddress = DecodeOrbAddressFromPayload(write.data); + + ASSERT_TRUE(rig.bus.CompleteNextWrite(ASFW::Async::AsyncStatus::kSuccess)); + rig.DrainReady(); + + StatusBlock status{}; + status.details = 0; + status.sbpStatus = SBPStatus::kNoAdditionalInfo; + status.orbOffsetHi = ToBE16(static_cast(((orbAddress + 8) >> 32) & 0xFFFFu)); + status.orbOffsetLo = ToBE32(static_cast((orbAddress + 8) & 0xFFFF'FFFFu)); + rig.addressManager.ApplyRemoteWrite( + ReadStatusAddressFromManagementORB(rig.addressManager, orbAddress), + std::span{reinterpret_cast(&status), sizeof(status)}); + + rig.DrainReady(); + EXPECT_EQ(-3, completionStatus); +} + +TEST(SBP2ORBTests, ManagementORBDestroyedAfterExecuteIgnoresPendingWriteAndStatus) { + ORBTimerRig rig; + + int completionCount = 0; + uint64_t statusAddress = 0; + { + auto orb = std::make_unique( + rig.bus, rig.bus, rig.addressManager, reinterpret_cast(0xB)); + orb->SetFunction(SBP2ManagementORB::Function::AbortTaskSet); + orb->SetLoginID(0x47); + orb->SetManagementAgentOffset(0x93); + orb->SetTargetNode(1, 0x3F); + orb->SetTimeout(5); + orb->SetWorkQueue(&rig.workQueue); + orb->SetTimeoutQueue(&rig.timeoutQueue); + orb->SetCompletionCallback([&completionCount](int) { ++completionCount; }); + + ASSERT_TRUE(orb->Execute()); + const auto& write = rig.bus.WriteAt(0); + const uint64_t orbAddress = DecodeOrbAddressFromPayload(write.data); + statusAddress = ReadStatusAddressFromManagementORB(rig.addressManager, orbAddress); + } + + EXPECT_EQ(0u, rig.bus.PendingWriteCount()); + StatusBlock status{}; + status.details = 0; + status.sbpStatus = SBPStatus::kNoAdditionalInfo; + rig.addressManager.ApplyRemoteWrite( + statusAddress, + std::span{reinterpret_cast(&status), sizeof(status)}); + rig.AdvanceMs(5); + + EXPECT_EQ(0, completionCount); +} + } // namespace diff --git a/tests/SBP2SessionRegistryTests.cpp b/tests/SBP2SessionRegistryTests.cpp new file mode 100644 index 00000000..a2bd199f --- /dev/null +++ b/tests/SBP2SessionRegistryTests.cpp @@ -0,0 +1,934 @@ +#include + +#include "ASFWDriver/Discovery/DeviceManager.hpp" +#include "ASFWDriver/Protocols/SBP2/SBP2ManagementORB.hpp" +#include "ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.hpp" +#include "ASFWDriver/Protocols/SBP2/SCSICommandSet.hpp" +#include "ASFWDriver/Protocols/SBP2/SBP2WireFormats.hpp" +#include "ASFWDriver/Testing/HostDriverKitStubs.hpp" +#ifndef OSDynamicCast +#define ASFW_TEST_DEFINED_OSDYNAMICCAST 1 +#define OSDynamicCast(type, object) static_cast(object) +#endif +#include "ASFWDriver/UserClient/Handlers/SBP2Handler.hpp" +#ifdef ASFW_TEST_DEFINED_OSDYNAMICCAST +#undef OSDynamicCast +#undef ASFW_TEST_DEFINED_OSDYNAMICCAST +#endif +#include "tests/mocks/DeferredFireWireBus.hpp" + +#include +#include +#include +#include + +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::SBP2ManagementORB; +using ASFW::Protocols::SBP2::SBP2SessionRegistry; +namespace SCSI = ASFW::Protocols::SBP2::SCSI; +using ASFW::Protocols::SBP2::Wire::FromBE16; +using ASFW::Protocols::SBP2::Wire::FromBE32; +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; +using ASFW::Protocols::SBP2::Wire::ToBE16; +using ASFW::Protocols::SBP2::Wire::ToBE32; +namespace SBPStatus = ASFW::Protocols::SBP2::Wire::SBPStatus; +namespace UCWire = ASFW::UserClient::Wire; + +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 = FromBE32(ReadQuadlet(manager, orbAddress + hiOffset)); + const uint32_t lo = FromBE32(ReadQuadlet(manager, orbAddress + loOffset)); + return ComposeAddress(static_cast(hi & 0xFFFFu), lo); +} + +uint64_t ReadDataBufferAddress(AddressSpaceManager& manager, uint64_t orbAddress) { + const uint32_t hi = FromBE32( + ReadQuadlet(manager, orbAddress + offsetof(NormalORB, dataDescriptorHi))); + const uint32_t lo = FromBE32( + 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 = + FromBE32(ReadQuadlet(manager, orbAddress + offsetof(TaskManagementORB, options))); + return static_cast((optionsAndLoginID >> 16) & 0x000Fu); +} + +uint16_t ReadTaskManagementLoginID(AddressSpaceManager& manager, uint64_t orbAddress) { + const uint32_t optionsAndLoginID = + FromBE32(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 = ToBE16(static_cast((orbAddress >> 32) & 0xFFFFu)); + status.orbOffsetLo = ToBE32(static_cast(orbAddress & 0xFFFF'FFFFu)); + manager.ApplyRemoteWrite( + statusAddress, + std::span{reinterpret_cast(&status), sizeof(status)}); +} + +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 SessionRegistryRig { +public: + explicit SessionRegistryRig(uint32_t unitCharacteristics = 0x080400) + : registry(bus, bus, addressManager, deviceManager, &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; + while (queue.DrainReadyForTesting() > 0U) { + } + } + + uint64_t CreateSession() { + auto result = registry.CreateSession(Owner(), kGuid, 0); + EXPECT_TRUE(result.has_value()); + const uint64_t handle = result.value_or(0); + if (auto* session = registry.GetSessionForTesting(handle)) { + session->SetTimeoutQueue(&queue); + } + return handle; + } + + 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 = ToBE16(LoginResponse::kSize); + response.loginID = ToBE16(loginId); + response.commandBlockAgentAddressHi = ToBE32(0x0000'FFFFu); + response.commandBlockAgentAddressLo = ToBE32(commandBlockAgentLo); + response.reconnectHold = ToBE16(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; + AddressSpaceManager addressManager{nullptr}; + DeviceManager deviceManager; + IODispatchQueue queue; + SBP2SessionRegistry registry; + uint64_t nowNs{0}; + uint64_t sessionStatusAddress{0}; +}; + +TEST(SBP2SessionRegistryTests, 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(SBP2SessionRegistryTests, 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 = ToBE16(static_cast((commandOrbAddress >> 32) & 0xFFFFu)); + status.orbOffsetLo = ToBE32(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(SBP2SessionRegistryTests, 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 = ToBE16(static_cast((commandOrbAddress >> 32) & 0xFFFFu)); + status.orbOffsetLo = ToBE32(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(SBP2SessionRegistryTests, 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); + while (rig.queue.DrainReadyForTesting() > 0U) { + } + + 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)); + while (rig.queue.DrainReadyForTesting() > 0U) { + } + + uint32_t ignored = 0; + EXPECT_EQ(ASFW::Async::ResponseCode::AddressError, + rig.addressManager.ReadQuadlet(commandOrbAddress, &ignored)); +} + +TEST(SBP2SessionRegistryTests, 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 = ToBE16(static_cast((commandOrbAddress >> 32) & 0xFFFFu)); + commandStatus.orbOffsetLo = ToBE32(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 = ToBE16(static_cast((inquiryOrbAddress >> 32) & 0xFFFFu)); + inquiryStatus.orbOffsetLo = ToBE32(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(SBP2SessionRegistryTests, HandlerRejectsMissingOrShortSessionStateOutput) { + SessionRegistryRig 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, SessionRegistryRig::Owner())); + + uint64_t shortOutput[4]{}; + args.scalarOutput = shortOutput; + args.scalarOutputCount = 4; + EXPECT_EQ(kIOReturnBadArgument, + handler.GetSBP2SessionState(&args, SessionRegistryRig::Owner())); + + uint64_t output[5]{}; + args.scalarOutput = output; + args.scalarOutputCount = 5; + EXPECT_EQ(kIOReturnSuccess, + handler.GetSBP2SessionState(&args, SessionRegistryRig::Owner())); + EXPECT_EQ(5u, args.scalarOutputCount); +} + +TEST(SBP2SessionRegistryTests, HandlerHardensCommandABIInputsBeforeSubmission) { + SessionRegistryRig 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, SessionRegistryRig::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}))); +} + +TEST(SBP2SessionRegistryTests, 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(SBP2SessionRegistryTests, 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(SBP2SessionRegistryTests, SubmitTaskManagementRejectsInvalidFunction) { + SessionRegistryRig rig; + const uint64_t handle = rig.CreateSession(); + rig.LoginSuccessfully(handle); + + EXPECT_FALSE(rig.registry.SubmitTaskManagement( + SessionRegistryRig::Owner(), handle, SBP2ManagementORB::Function::QueryLogins)); +} + +TEST(SBP2SessionRegistryTests, 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(SBP2SessionRegistryTests, 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(SBP2SessionRegistryTests, 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(SBP2SessionRegistryTests, 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(SBP2SessionRegistryTests, 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(SBP2SessionRegistryTests, 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(SBP2SessionRegistryTests, 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(SBP2SessionRegistryTests, 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(SBP2SessionRegistryTests, 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(SBP2SessionRegistryTests, SubmitCommandRejectsCDBLargerThanORBPayloadBudget) { + SessionRegistryRig rig; + const uint64_t handle = rig.CreateSession(); + rig.LoginSuccessfully(handle); + + SCSI::CommandRequest request{}; + request.cdb = std::vector(16, 0x12); + request.direction = SCSI::DataDirection::None; + request.transferLength = 0; + request.timeoutMs = 100; + + EXPECT_FALSE(rig.registry.SubmitCommand(SessionRegistryRig::Owner(), handle, request)); +} + +TEST(SBP2SessionRegistryTests, CreateSessionAcceptsRealSBP2SpecAndVersion) { + SessionRegistryRig rig; + auto result = rig.registry.CreateSession(SessionRegistryRig::Owner(), + SessionRegistryRig::kGuid, + 0); + ASSERT_TRUE(result.has_value()); +} + +TEST(SBP2SessionRegistryTests, 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); + EXPECT_EQ(12u, targetInfo.maxCommandBlockSize); +} + +TEST(SBP2SessionRegistryTests, 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(SBP2SessionRegistryTests, 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 = { + ToBE32(0x04045343u), + ToBE32(0x31333934u), + ToBE32(0x00FF5012u), + ToBE32(0x0090B540u), + ToBE32(0x01FFFFFFu), + ToBE32(0x0001B344u), + ToBE32(0x0004CAEEu), + ToBE32(0x1200609Eu), + ToBE32(0x13010483u), + ToBE32(0x5400C000u), + ToBE32(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