diff --git a/ASFW.xcodeproj/project.pbxproj b/ASFW.xcodeproj/project.pbxproj index bae1f9e3..0e59e49a 100644 --- a/ASFW.xcodeproj/project.pbxproj +++ b/ASFW.xcodeproj/project.pbxproj @@ -1440,6 +1440,7 @@ B6092FC4D28C8CBE1F474D4B /* AVCHandler.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = AVCHandler.cpp; sourceTree = ""; }; B67D7B85C0AE7742A87A96D8 /* DiceLockWatch.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = DiceLockWatch.hpp; sourceTree = ""; }; B68AF53D0E4DFFFEFFB6C111 /* AVCUnit.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = AVCUnit.cpp; sourceTree = ""; }; + B6A28B6568BD16B3A7F638BD /* LoginStateMachine.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = LoginStateMachine.hpp; sourceTree = ""; }; B6CB2AFAEB80414349FBF935 /* net.asfw.driver.dext */ = {isa = PBXFileReference; explicitFileType = "wrapper.driver-extension"; includeInIndex = 0; path = net.asfw.driver.dext; sourceTree = BUILT_PRODUCTS_DIR; }; B6EABCE6198C5B339B48E9CA /* DiceDiagnosticsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiceDiagnosticsTests.swift; sourceTree = ""; }; B6FC8F89D537351E9BA9D735 /* ControllerConfig.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = ControllerConfig.cpp; sourceTree = ""; }; @@ -3190,6 +3191,7 @@ 651E702CFD124214444CEFCC /* ISessionScheduler.hpp */, 50A11F405110ACE28D722447 /* LoginSession.cpp */, E7BA855FF45BC36F50C20752 /* LoginSession.hpp */, + B6A28B6568BD16B3A7F638BD /* LoginStateMachine.hpp */, 36A97E74ED2EB18E06BCF26F /* SessionRegistry.cpp */, FA21FB623DC8076A7D6A6ACE /* SessionRegistry.hpp */, ); diff --git a/ASFWDriver/Protocols/SBP2/SBP2WireFormats.hpp b/ASFWDriver/Protocols/SBP2/SBP2WireFormats.hpp index f3e3e842..495cc7ea 100644 --- a/ASFWDriver/Protocols/SBP2/SBP2WireFormats.hpp +++ b/ASFWDriver/Protocols/SBP2/SBP2WireFormats.hpp @@ -174,6 +174,7 @@ struct StatusBlock { uint32_t status[6]{}; // Up to 24 additional bytes of status static constexpr uint32_t kMaxSize = 32; // header (8) + max status (24) + static constexpr uint8_t kRespRequestComplete = 0; // Apple kFWSBP2RequestComplete // Bit positions cross-validated with Linux sbp2.c STATUS_GET_* macros // (quadlet bits 31:30 src, 29:28 resp, 27 dead, 26:24 len → byte 0 above). diff --git a/ASFWDriver/Protocols/SBP2/Session/CommandExecutor.cpp b/ASFWDriver/Protocols/SBP2/Session/CommandExecutor.cpp index 78252118..c05f3774 100644 --- a/ASFWDriver/Protocols/SBP2/Session/CommandExecutor.cpp +++ b/ASFWDriver/Protocols/SBP2/Session/CommandExecutor.cpp @@ -416,13 +416,14 @@ bool CommandExecutor::SubmitTaskManagement(SBP2ManagementORB::Function function, if (!IsSupportedTaskManagementFunction(function)) { return false; } - if (session_.State() != LoginState::LoggedIn || managementORB_) { + const LoginSnapshot snap = session_.Snapshot(); + if (snap.state != LoginState::LoggedIn || !snap.loginID || managementORB_) { return false; } auto orb = std::make_unique(bus_, busInfo_, addrSpaceMgr_, owner_); orb->SetFunction(function); - orb->SetLoginID(session_.LoginID()); + orb->SetLoginID(*snap.loginID); orb->SetManagementAgentOffset(session_.TargetInfo().managementAgentOffset); // Grace beyond the advertised management timeout: a target can post status // a hair past its own Unit_Characteristics timeout (observed on the LS-9000, @@ -432,7 +433,7 @@ bool CommandExecutor::SubmitTaskManagement(SBP2ManagementORB::Function function, // headroom instead. Not a model-specific value. orb->SetTimeout(session_.TargetInfo().managementTimeoutMs + kManagementTimeoutGraceMs); orb->SetScheduler(&scheduler_); - orb->SetTargetNode(session_.Generation(), session_.TargetInfo().targetNodeId); + orb->SetTargetNode(snap.generation, session_.TargetInfo().targetNodeId); const SBP2ManagementORB* submittedORB = orb.get(); const std::weak_ptr weak = lifetimeToken_; diff --git a/ASFWDriver/Protocols/SBP2/Session/LoginSession.cpp b/ASFWDriver/Protocols/SBP2/Session/LoginSession.cpp index c323b592..a3cde5df 100644 --- a/ASFWDriver/Protocols/SBP2/Session/LoginSession.cpp +++ b/ASFWDriver/Protocols/SBP2/Session/LoginSession.cpp @@ -1,4 +1,5 @@ -// LoginSession — SBP-2 login/reconnect/logout state machine. See LoginSession.hpp. +// LoginSession — SBP-2 login/reconnect/logout. See LoginSession.hpp; the +// transitions are LoginFsm (LoginStateMachine.hpp). // // Ported from PR #19's SBP2LoginSession, decomposed for DICE: the command plane // moved to FetchAgent (composed here), timers go through ISessionScheduler (one @@ -16,11 +17,24 @@ namespace ASFW::Protocols::SBP2 { using namespace ASFW::Protocols::SBP2::Wire; +namespace Fsm = LoginFsm; // DICE's logging has no dedicated SBP-2 category; the session/command layer logs // under the Async subsystem category (matching FetchAgent). #define ASFW_LOG_SBP2(fmt, ...) ASFW_LOG(Async, fmt, ##__VA_ARGS__) +namespace { + +uint64_t PackSnapshot(const Fsm::State& state) noexcept { + const auto* held = Fsm::Held(state); + return static_cast(Fsm::Reported(state)) | + (static_cast(Fsm::GenerationOf(state)) << 8) | + (static_cast(held ? held->id : 0) << 24) | + (static_cast(held != nullptr) << 40); +} + +} // namespace + // --------------------------------------------------------------------------- // Construction / Destruction // --------------------------------------------------------------------------- @@ -33,10 +47,12 @@ LoginSession::LoginSession(Async::IFireWireBus& bus, , busInfo_(busInfo) , addrSpaceMgr_(addrSpaceMgr) , scheduler_(scheduler) - , fetchAgent_(bus, busInfo, scheduler) {} + , fetchAgent_(bus, busInfo, scheduler) + , executorLock_(IOLockAlloc()) {} LoginSession::~LoginSession() { CancelManagementTimer(); + CancelTimerSlot(reconnectWindow_); if (loginWriteHandle_) { bus_.Cancel(loginWriteHandle_); loginWriteHandle_ = {}; @@ -59,6 +75,9 @@ LoginSession::~LoginSession() { } fetchAgent_.Unbind(); DeallocateResources(); + if (executorLock_) { + IOLockFree(executorLock_); + } } // --------------------------------------------------------------------------- @@ -77,7 +96,7 @@ void LoginSession::Configure(const SBP2TargetInfo& info) noexcept { } // --------------------------------------------------------------------------- -// Login +// Session operations // --------------------------------------------------------------------------- bool LoginSession::Login() noexcept { @@ -85,208 +104,284 @@ bool LoginSession::Login() noexcept { ASFW_LOG_SBP2( "LoginSession::Login: not configured"); return false; } - if (state_ == LoginState::LoggingIn || state_ == LoginState::LoggedIn) { - ASFW_LOG_SBP2( "LoginSession::Login: state=%{public}s, ignoring", ToString(state_)); - return false; - } - - if (!AllocateResources()) { - ASFW_LOG_SBP2( "LoginSession::Login: resource allocation failed"); - SetState(LoginState::Failed); - return false; - } - - SetState(LoginState::LoggingIn); - loginGeneration_ = static_cast(busInfo_.GetGeneration().value); - loginNodeID_ = targetInfo_.targetNodeId; - - BuildLoginORB(); + return Request(Fsm::Ev::Login{}, "Login"); +} - const FW::Generation gen{loginGeneration_}; - const FW::NodeId node{static_cast(TargetNodeShort())}; - const Async::FWAddress mgmtAddr{Async::FWAddress::QualifiedAddressParts{ - .addressHi = 0xFFFF, - .addressLo = ManagementAgentAddressLo(targetInfo_.managementAgentOffset), - .nodeID = loginNodeID_}}; - const FW::FwSpeed speed = busInfo_.GetSpeed(node); +bool LoginSession::Logout() noexcept { + return Request(Fsm::Ev::Logout{}, "Logout"); +} - const std::weak_ptr weakSelf = weak_from_this(); - loginWriteHandle_ = bus_.WriteBlock( - gen, node, mgmtAddr, - std::span{loginORBAddressBE_.data(), loginORBAddressBE_.size()}, - speed, - [weakSelf, requestGeneration = loginGeneration_.load()](Async::AsyncStatus status, - std::span) { - if (auto self = weakSelf.lock()) { - self->OnLoginWriteComplete(requestGeneration, status); - } - }); +bool LoginSession::Resume() noexcept { + return Request(Fsm::Ev::Resume{}, "Resume"); +} - if (!loginWriteHandle_) { - ASFW_LOG_SBP2( "LoginSession::Login: WriteBlock failed immediately"); - SetState(LoginState::Failed); - return false; - } +void LoginSession::HandleBusReset(uint16_t newGeneration) noexcept { + Serialized([this, newGeneration] { + ASFW_LOG_SBP2( "LoginSession::HandleBusReset: state=%{public}s newGen=%u gen=%u", + Fsm::Name(fsm_), newGeneration, Fsm::GenerationOf(fsm_)); + (void)Apply(Fsm::Ev::BusReset{newGeneration}); + }); +} - StartLoginTimer(); - return true; +void LoginSession::AbandonSuspended() noexcept { + Serialized([this] { + if (Fsm::Reported(fsm_) != LoginState::Suspended) { + return; + } + ASFW_LOG_SBP2("LoginSession: device gone while suspended — session lost"); + (void)Apply(Fsm::Ev::DeviceGone{}); + }); } // --------------------------------------------------------------------------- -// Logout +// State machine // --------------------------------------------------------------------------- -bool LoginSession::Logout() noexcept { - if (state_ != LoginState::LoggedIn && state_ != LoginState::Suspended) { - ASFW_LOG_SBP2( "LoginSession::Logout: state=%{public}s, ignoring", ToString(state_)); - return false; +void LoginSession::Serialized(std::function work) noexcept { + IOLockLock(executorLock_); + pendingWork_.push_back(std::move(work)); + if (executing_) { + IOLockUnlock(executorLock_); + return; } + executing_ = true; + IOLockUnlock(executorLock_); - SetState(LoginState::LoggingOut); - fetchAgent_.Unbind(); - BuildLogoutORB(); + // A callback may drop the registry's reference mid-transition. + const auto keepAlive = weak_from_this().lock(); + for (;;) { + IOLockLock(executorLock_); + if (pendingWork_.empty()) { + executing_ = false; + IOLockUnlock(executorLock_); + return; + } + auto next = std::move(pendingWork_.front()); + pendingWork_.pop_front(); + IOLockUnlock(executorLock_); + next(); + } +} - const FW::Generation gen{loginGeneration_}; - const FW::NodeId node{static_cast(TargetNodeShort())}; - const Async::FWAddress mgmtAddr{Async::FWAddress::QualifiedAddressParts{ - .addressHi = 0xFFFF, - .addressLo = ManagementAgentAddressLo(targetInfo_.managementAgentOffset), - .nodeID = loginNodeID_}}; - const FW::FwSpeed speed = busInfo_.GetSpeed(node); +void LoginSession::Dispatch(Fsm::Event event) noexcept { + Serialized([this, event = std::move(event)] { (void)Apply(event); }); +} - const std::weak_ptr weakSelf = weak_from_this(); - logoutWriteHandle_ = bus_.WriteBlock( - gen, node, mgmtAddr, - std::span{logoutORBAddressBE_.data(), logoutORBAddressBE_.size()}, - speed, - [weakSelf, requestGeneration = loginGeneration_.load()](Async::AsyncStatus status, - std::span) { - if (auto self = weakSelf.lock()) { - self->OnLogoutWriteComplete(requestGeneration, status); - } - }); +bool LoginSession::Request(Fsm::Event event, const char* what) noexcept { + // Shared: if another thread is running work, ours runs after we return. + const auto accepted = std::make_shared>(); + Serialized([this, accepted, what, event = std::move(event)] { + *accepted = Apply(event); + if (!**accepted) { + ASFW_LOG_SBP2( "LoginSession::%{public}s: state=%{public}s, ignoring", what, + Fsm::Name(fsm_)); + } + }); + if (!accepted->has_value()) { + return true; + } + // A write refused on the spot is queued behind the request and has run by + // now; it leaves the session Failed. + return **accepted && Snapshot().state != LoginState::Failed; +} - if (!logoutWriteHandle_) { - ASFW_LOG_SBP2( "LoginSession::Logout: WriteBlock failed"); - SetState(LoginState::Failed); +bool LoginSession::Apply(const Fsm::Event& event) noexcept { + auto step = Fsm::Transition(fsm_, event, Now()); + if (!step) { return false; } + const Fsm::Effects& fx = step->fx; - StartLogoutTimer(); + if (fx.cancelTimer) { + CancelManagementTimer(); + } + if (fx.cancelWindow) { + CancelTimerSlot(reconnectWindow_); + } + if (fx.unbind) { + fetchAgent_.Unbind(); + } + if (fx.release) { + DeallocateResources(); + } + // Down-edge for the old login before the fallback login's state is visible. + if (fx.sessionLostFirst) { + NotifySessionLost(fx.generation); + } + Commit(std::move(step->next)); + if (fx.write) { + IssueWrite(*fx.write); + } + if (fx.arm) { + ArmTimer(*fx.arm); + } + if (fx.armWindow) { + ArmTimerSlot(&LoginSession::reconnectWindow_, targetInfo_.managementTimeoutMs + 1000, + Fsm::Ev::ReconnectWindowClosed{}); + } + if (fx.bind) { + BindCommandPlane(*fx.bind); + } + Notify(fx); return true; } -// --------------------------------------------------------------------------- -// Reconnect -// --------------------------------------------------------------------------- - -bool LoginSession::Reconnect() noexcept { - if (state_ != LoginState::Suspended && state_ != LoginState::LoggedIn) { - ASFW_LOG_SBP2( "LoginSession::Reconnect: state=%{public}s, ignoring", ToString(state_)); - return false; +void LoginSession::Commit(Fsm::State next) noexcept { + const char* from = Fsm::Name(fsm_); + const char* to = Fsm::Name(next); + if (std::strcmp(from, to) != 0) { + ASFW_LOG_SBP2( "LoginSession: state %{public}s -> %{public}s", from, to); } + fsm_ = std::move(next); + snapshot_.store(PackSnapshot(fsm_), std::memory_order_release); +} - SetState(LoginState::Reconnecting); - loginGeneration_ = static_cast(busInfo_.GetGeneration().value); - loginNodeID_ = targetInfo_.targetNodeId; +Fsm::Route LoginSession::Now() const noexcept { + return Fsm::Route{static_cast(busInfo_.GetGeneration().value), + targetInfo_.targetNodeId}; +} +LoginSnapshot LoginSession::Snapshot() const noexcept { + const uint64_t word = snapshot_.load(std::memory_order_acquire); + LoginSnapshot snap{}; + snap.state = static_cast(word & 0xFFu); + snap.generation = static_cast(word >> 8); + if ((word >> 40) & 1u) { + snap.loginID = static_cast(word >> 24); + } + return snap; +} + +void LoginSession::IssueWrite(Fsm::Op op) noexcept { + const auto route = Fsm::RouteOf(fsm_); + if (!route) { + return; + } + // Every management ORB needs the address spaces; a reset released them. if (!AllocateResources()) { - ASFW_LOG_SBP2( "LoginSession::Reconnect: resource allocation failed"); - SetState(LoginState::Failed); - return false; + ASFW_LOG_SBP2( "LoginSession: resource allocation failed"); + Dispatch(Fsm::Ev::ResourcesUnavailable{}); + return; } - BuildReconnectORB(); + const auto* held = Fsm::Held(fsm_); + Async::AsyncHandle* handle = nullptr; + switch (op) { + case Fsm::Op::Login: + BuildLoginORB(); + handle = &loginWriteHandle_; + break; + case Fsm::Op::Reconnect: + BuildReconnectORB(held ? held->id : 0); + handle = &reconnectWriteHandle_; + break; + case Fsm::Op::Logout: + BuildLogoutORB(held ? held->id : 0); + handle = &logoutWriteHandle_; + break; + } - const FW::Generation gen{loginGeneration_}; - const FW::NodeId node{static_cast(TargetNodeShort())}; + const auto& orbAddress = OrbAddressFor(op); + const FW::NodeId node = ShortNode(route->node); const Async::FWAddress mgmtAddr{Async::FWAddress::QualifiedAddressParts{ .addressHi = 0xFFFF, .addressLo = ManagementAgentAddressLo(targetInfo_.managementAgentOffset), - .nodeID = loginNodeID_}}; - const FW::FwSpeed speed = busInfo_.GetSpeed(node); + .nodeID = route->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_.load()](Async::AsyncStatus status, - std::span) { + *handle = bus_.WriteBlock( + FW::Generation{route->generation}, node, mgmtAddr, + std::span{orbAddress.data(), orbAddress.size()}, + busInfo_.GetSpeed(node), + [weakSelf, op, generation = route->generation](Async::AsyncStatus status, + std::span) { if (auto self = weakSelf.lock()) { - self->OnReconnectWriteComplete(requestGeneration, status); + if (status != Async::AsyncStatus::kSuccess) { + ASFW_LOG_SBP2( "LoginSession: management write status=%{public}s", + Async::ToString(status)); + } + self->Dispatch(Fsm::Ev::WriteDone{ + op, generation, status == Async::AsyncStatus::kSuccess}); } }); - if (!reconnectWriteHandle_) { - ASFW_LOG_SBP2( "LoginSession::Reconnect: WriteBlock failed, will retry"); - ArmManagementTimer(kLoginRetryDelayMs, [this]() { OnReconnectTimeout(); }); - return true; // will retry + if (!*handle) { + ASFW_LOG_SBP2( "LoginSession: management WriteBlock failed immediately"); + Dispatch(Fsm::Ev::WriteRefused{op}); } - - return true; } -// --------------------------------------------------------------------------- -// Bus Reset Handling -// --------------------------------------------------------------------------- - -void LoginSession::HandleBusReset(uint16_t newGeneration) noexcept { - ASFW_LOG_SBP2( "LoginSession::HandleBusReset: state=%{public}s newGen=%u loginGen=%u", - ToString(state_), newGeneration, loginGeneration_.load()); - - switch (state_) { - case LoginState::LoggingIn: - CancelManagementTimer(); - loginRetryCount_ = 0; - loginGeneration_ = newGeneration; - fetchAgent_.Unbind(); - DeallocateResources(); - loginID_ = 0; - SetState(LoginState::Suspended); +void LoginSession::ArmTimer(Fsm::Timer timer) noexcept { + const uint64_t mgmtMs = targetInfo_.managementTimeoutMs; + const auto arm = [this](uint64_t delayMs, Fsm::Event event) { + ArmTimerSlot(&LoginSession::managementTimer_, delayMs, std::move(event)); + }; + switch (timer) { + case Fsm::Timer::LoginTimeout: + arm(mgmtMs, Fsm::Ev::Timeout{Fsm::Op::Login}); break; - - case LoginState::LoggedIn: - CancelManagementTimer(); - fetchAgent_.Unbind(); - DeallocateResources(); - SetState(LoginState::Suspended); - loginGeneration_ = newGeneration; + case Fsm::Timer::LoginRetry: + arm(kRetryDelayMs, Fsm::Ev::LoginRetryDue{}); break; - - case LoginState::Reconnecting: - CancelManagementTimer(); - fetchAgent_.Unbind(); - DeallocateResources(); - loginGeneration_ = newGeneration; - SetState(LoginState::Suspended); + case Fsm::Timer::ReconnectRetry: + arm(kRetryDelayMs, Fsm::Ev::Timeout{Fsm::Op::Reconnect}); break; - - case LoginState::LoggingOut: - CancelManagementTimer(); - fetchAgent_.Unbind(); - DeallocateResources(); - SetState(LoginState::Idle); + case Fsm::Timer::ReconnectResend: + arm(kReconnectResendDelayMs, Fsm::Ev::ReconnectRetryDue{}); break; - - case LoginState::Failed: - DeallocateResources(); + case Fsm::Timer::LogoutTimeout: + arm(mgmtMs, Fsm::Ev::Timeout{Fsm::Op::Logout}); break; + } +} - default: - break; +void LoginSession::Notify(const Fsm::Effects& fx) noexcept { + switch (fx.notify) { + case Fsm::Notify::None: + return; + case Fsm::Notify::SessionLost: + NotifySessionLost(fx.generation); + return; + case Fsm::Notify::LoggedIn: + case Fsm::Notify::LoginFailed: + if (loginCallback_) { + LoginCompleteParams params{}; + params.status = fx.status; + if (const auto* held = Fsm::Held(fsm_)) { + params.loginResponse = held->response; + } + params.generation = fx.generation; + loginCallback_(params); + } + return; + case Fsm::Notify::LogoutDone: + if (logoutCallback_) { + LogoutCompleteParams params{}; + params.status = fx.status; + params.generation = fx.generation; + logoutCallback_(params); + } + return; } } -void LoginSession::AbandonSuspended() noexcept { - if (state_ != LoginState::Suspended) { +// A rejected or unanswered reconnect means the target dropped the old login — +// typically a device power-cycle, which also reverts its command state +// (initialization such as MODE SELECT is gone). Emit the terminal down-edge +// for the old session BEFORE the fallback login: observers tear down state +// keyed to it (the HBA destroys SCSI target 0), and the fresh login's up-edge +// rebuilds it, forcing a full SAM re-probe/re-init against the rebooted device +// instead of reusing its stale identity. Matches Apple's model — IOFireWireSBP2 +// reports a failed reconnect as a lost session to its clients rather than +// silently re-logging-in underneath them. +void LoginSession::NotifySessionLost(uint16_t generation) noexcept { + if (!loginCallback_) { return; } - ASFW_LOG_SBP2("LoginSession: device gone while suspended — session lost"); - CancelManagementTimer(); - loginID_ = 0; - SetState(LoginState::Failed); - NotifySessionLost(); + LoginCompleteParams params{}; + params.status = Fsm::kStatusSessionLost; + params.generation = generation; + loginCallback_(params); } // --------------------------------------------------------------------------- @@ -294,7 +389,20 @@ void LoginSession::AbandonSuspended() noexcept { // --------------------------------------------------------------------------- uint32_t LoginSession::ReconnectHoldSeconds() const noexcept { - return reconnectHold_ > 0 ? (1u << reconnectHold_) : 0; + const auto* held = Fsm::Held(fsm_); + return held && held->reconnectHold > 0 ? (1u << held->reconnectHold) : 0; +} + +Async::FWAddress LoginSession::CommandBlockAgent() const noexcept { + const auto* held = Fsm::Held(fsm_); + if (!held) { + return {}; + } + const auto route = Fsm::RouteOf(fsm_); + return Async::FWAddress{Async::FWAddress::QualifiedAddressParts{ + .addressHi = held->commandBlockAgentHi, + .addressLo = held->commandBlockAgentLo, + .nodeID = route ? route->node : targetInfo_.targetNodeId}}; } // --------------------------------------------------------------------------- @@ -452,6 +560,21 @@ bool LoginSession::AllocateLogoutORBAddressSpace() noexcept { // ORB Construction // --------------------------------------------------------------------------- +namespace { + +// Management agent write payload: [nodeID(2)][addressHi(2)][addressLo(4)] BE. +void EncodeOrbAddress(std::array& out, uint16_t localNode, + const AddressSpaceManager::AddressRangeMeta& meta) noexcept { + out[0] = static_cast(localNode >> 8); + out[1] = static_cast(localNode & 0xFF); + out[2] = static_cast(meta.addressHi >> 8); + out[3] = static_cast(meta.addressHi & 0xFF); + const uint32_t addrLoBE = OSSwapHostToBigInt32(meta.addressLo); + std::memcpy(&out[4], &addrLoBE, sizeof(uint32_t)); +} + +} // namespace + void LoginSession::BuildLoginORB() noexcept { std::memset(&loginORBBuffer_, 0, sizeof(loginORBBuffer_)); @@ -475,23 +598,17 @@ void LoginSession::BuildLoginORB() noexcept { std::span{reinterpret_cast(&loginORBBuffer_), sizeof(loginORBBuffer_)}); - // Management agent write payload: [nodeID(2)][addressHi(2)][addressLo(4)] BE. - loginORBAddressBE_[0] = static_cast(localNode >> 8); - loginORBAddressBE_[1] = static_cast(localNode & 0xFF); - loginORBAddressBE_[2] = static_cast(loginORBMeta_.addressHi >> 8); - loginORBAddressBE_[3] = static_cast(loginORBMeta_.addressHi & 0xFF); - const uint32_t orbAddrLoBE = OSSwapHostToBigInt32(loginORBMeta_.addressLo); - std::memcpy(&loginORBAddressBE_[4], &orbAddrLoBE, sizeof(uint32_t)); + EncodeOrbAddress(loginORBAddressBE_, localNode, loginORBMeta_); } -void LoginSession::BuildReconnectORB() noexcept { +void LoginSession::BuildReconnectORB(uint16_t loginID) noexcept { std::memset(&reconnectORBBuffer_, 0, sizeof(reconnectORBBuffer_)); const uint16_t localNode = NormalizeBusNodeID(static_cast(busInfo_.GetLocalNodeID().value)); reconnectORBBuffer_.options = Options::kReconnectNotify; - reconnectORBBuffer_.loginID = OSSwapHostToBigInt16(loginID_); + reconnectORBBuffer_.loginID = OSSwapHostToBigInt16(loginID); reconnectORBBuffer_.statusFIFOAddressHi = OSSwapHostToBigInt32(ComposeBusAddressHi(localNode, statusBlockMeta_.addressHi)); reconnectORBBuffer_.statusFIFOAddressLo = OSSwapHostToBigInt32(statusBlockMeta_.addressLo); @@ -501,22 +618,17 @@ void LoginSession::BuildReconnectORB() noexcept { std::span{reinterpret_cast(&reconnectORBBuffer_), sizeof(reconnectORBBuffer_)}); - reconnectORBAddressBE_[0] = static_cast(localNode >> 8); - reconnectORBAddressBE_[1] = static_cast(localNode & 0xFF); - reconnectORBAddressBE_[2] = static_cast(reconnectORBMeta_.addressHi >> 8); - reconnectORBAddressBE_[3] = static_cast(reconnectORBMeta_.addressHi & 0xFF); - const uint32_t addrLoBE = OSSwapHostToBigInt32(reconnectORBMeta_.addressLo); - std::memcpy(&reconnectORBAddressBE_[4], &addrLoBE, sizeof(uint32_t)); + EncodeOrbAddress(reconnectORBAddressBE_, localNode, reconnectORBMeta_); } -void LoginSession::BuildLogoutORB() noexcept { +void LoginSession::BuildLogoutORB(uint16_t loginID) noexcept { std::memset(&logoutORBBuffer_, 0, sizeof(logoutORBBuffer_)); const uint16_t localNode = NormalizeBusNodeID(static_cast(busInfo_.GetLocalNodeID().value)); logoutORBBuffer_.options = Options::kLogoutNotify; - logoutORBBuffer_.loginID = OSSwapHostToBigInt16(loginID_); + logoutORBBuffer_.loginID = OSSwapHostToBigInt16(loginID); logoutORBBuffer_.statusFIFOAddressHi = OSSwapHostToBigInt32(ComposeBusAddressHi(localNode, statusBlockMeta_.addressHi)); logoutORBBuffer_.statusFIFOAddressLo = OSSwapHostToBigInt32(statusBlockMeta_.addressLo); @@ -526,192 +638,69 @@ void LoginSession::BuildLogoutORB() noexcept { std::span{reinterpret_cast(&logoutORBBuffer_), sizeof(logoutORBBuffer_)}); - logoutORBAddressBE_[0] = static_cast(localNode >> 8); - logoutORBAddressBE_[1] = static_cast(localNode & 0xFF); - logoutORBAddressBE_[2] = static_cast(logoutORBMeta_.addressHi >> 8); - logoutORBAddressBE_[3] = static_cast(logoutORBMeta_.addressHi & 0xFF); - const uint32_t addrLoBE = OSSwapHostToBigInt32(logoutORBMeta_.addressLo); - std::memcpy(&logoutORBAddressBE_[4], &addrLoBE, sizeof(uint32_t)); -} - -// --------------------------------------------------------------------------- -// Command-plane binding -// --------------------------------------------------------------------------- - -void LoginSession::BindFetchAgent() noexcept { - const auto cbaAddr = [&](uint32_t offset) { - return Async::FWAddress{Async::FWAddress::QualifiedAddressParts{ - .addressHi = commandBlockAgent_.addressHi, - .addressLo = commandBlockAgent_.addressLo + offset, - .nodeID = loginNodeID_}}; - }; - - FetchAgent::Binding binding{}; - binding.generation = loginGeneration_; - binding.nodeID = loginNodeID_; - binding.fetchAgentAddress = cbaAddr(Wire::CommandBlockAgentOffsets::kFetchAgent); - binding.agentResetAddress = cbaAddr(Wire::CommandBlockAgentOffsets::kAgentReset); - binding.maxPayloadSize = maxPayloadSize_; - fetchAgent_.Bind(binding); - - RefreshUnsolicitedStatusAddress(); + EncodeOrbAddress(logoutORBAddressBE_, localNode, logoutORBMeta_); } -void LoginSession::RefreshUnsolicitedStatusAddress() noexcept { - unsolicitedStatusAddress_ = Async::FWAddress{Async::FWAddress::QualifiedAddressParts{ - .addressHi = commandBlockAgent_.addressHi, - .addressLo = commandBlockAgent_.addressLo + Wire::CommandBlockAgentOffsets::kUnsolicitedStatusEnable, - .nodeID = loginNodeID_}}; +const std::array& LoginSession::OrbAddressFor(Fsm::Op op) const noexcept { + switch (op) { + case Fsm::Op::Login: return loginORBAddressBE_; + case Fsm::Op::Reconnect: return reconnectORBAddressBE_; + case Fsm::Op::Logout: return logoutORBAddressBE_; + } + return loginORBAddressBE_; } // --------------------------------------------------------------------------- -// Management Write Completions +// Command-plane binding // --------------------------------------------------------------------------- -void LoginSession::OnLoginWriteComplete(uint16_t expectedGeneration, - Async::AsyncStatus status) noexcept { - if (expectedGeneration != loginGeneration_ || state_ != LoginState::LoggingIn) { - return; - } - - CancelManagementTimer(); - - if (status != Async::AsyncStatus::kSuccess) { - if (loginRetryCount_ < kLoginRetryMax) { - loginRetryCount_++; - ArmManagementTimer(kLoginRetryDelayMs, [this]() { - loginGeneration_ = static_cast(busInfo_.GetGeneration().value); - loginNodeID_ = targetInfo_.targetNodeId; - SetState(LoginState::Idle); - (void)Login(); - }); - return; - } - - ASFW_LOG_SBP2( "LoginSession: login retries exhausted"); - SetState(LoginState::Failed); - if (loginCallback_) { - LoginCompleteParams params{}; - params.status = -1; - params.generation = loginGeneration_; - loginCallback_(params); - } +void LoginSession::BindCommandPlane(Fsm::Op op) noexcept { + const auto* held = Fsm::Held(fsm_); + const auto route = Fsm::RouteOf(fsm_); + if (!held || !route) { return; } + BindFetchAgent(*held, *route); - // Management agent write ACK'd. Wait for the device to write the status block; - // restart the timer for the device processing window. - StartLoginTimer(); -} - -void LoginSession::OnLoginTimeout() noexcept { - if (state_ != LoginState::LoggingIn) { - return; - } - - if (loginRetryCount_ < kLoginRetryMax) { - loginRetryCount_++; - SetState(LoginState::Idle); - (void)Login(); - } else { - SetState(LoginState::Failed); - if (loginCallback_) { - LoginCompleteParams params{}; - params.status = -2; // timeout - params.generation = loginGeneration_; - loginCallback_(params); + // Login enables unsolicited status before the busy-timeout write (PR #19's + // ordering); a reconnect replays the busy timeout first. + if (op == Fsm::Op::Login) { + if (unsolicitedStatusRequested_) { + unsolicitedStatusRequested_ = false; + EnableUnsolicitedStatusNow(); } - } -} - -void LoginSession::OnReconnectWriteComplete(uint16_t expectedGeneration, - Async::AsyncStatus status) noexcept { - if (expectedGeneration != loginGeneration_ || state_ != LoginState::Reconnecting) { - return; - } - - if (status != Async::AsyncStatus::kSuccess) { - // The old retry called Reconnect(), which ignores anything but - // Suspended/LoggedIn — so a failed write left the session Reconnecting - // with no timer, forever (e.g. device unplugged mid-reconnect). A failed - // reconnect write is a failed reconnect: Apple reports the session lost - // and its client logs in afresh (IOFireWireSBP2Login::doReconnect, - // IOFireWireSBP2Login.cpp:2285). Same fallback as a synchronous - // WriteBlock failure in Reconnect(). - ASFW_LOG_SBP2( "LoginSession::OnReconnectWriteComplete: status=%{public}s, session lost", - Async::ToString(status)); - ArmManagementTimer(kLoginRetryDelayMs, [this]() { OnReconnectTimeout(); }); + ASFW_LOG_SBP2( "LoginSession: login successful — loginID=%u CBA=%04x:%08x reconnectHold=2^%u", + held->id, held->commandBlockAgentHi, held->commandBlockAgentLo, + held->reconnectHold); + WriteBusyTimeout(); return; } - // Reconnect ORB write ACK'd. Wait for the status block from the device. - StartReconnectTimer(); -} - -void LoginSession::OnReconnectTimeout() noexcept { - if (state_ != LoginState::Reconnecting) { - return; - } - ASFW_LOG_SBP2( "LoginSession: reconnect timeout, falling back to full login"); - NotifySessionLost(); - SetState(LoginState::Idle); - (void)Login(); -} - -// A rejected or unanswered reconnect means the target dropped the old login — -// typically a device power-cycle, which also reverts its command state -// (initialization such as MODE SELECT is gone). Emit the terminal down-edge -// for the old session BEFORE the fallback login: observers tear down state -// keyed to it (the HBA destroys SCSI target 0), and the fresh login's up-edge -// rebuilds it, forcing a full SAM re-probe/re-init against the rebooted device -// instead of reusing its stale identity. Matches Apple's model — IOFireWireSBP2 -// reports a failed reconnect as a lost session to its clients rather than -// silently re-logging-in underneath them. -void LoginSession::NotifySessionLost() noexcept { - if (!loginCallback_) { - return; + ASFW_LOG_SBP2( "LoginSession: reconnect successful — loginID=%u", held->id); + WriteBusyTimeout(); + if (unsolicitedStatusRequested_) { + unsolicitedStatusRequested_ = false; + EnableUnsolicitedStatusNow(); } - LoginCompleteParams params{}; - params.status = kStatusSessionLost; - params.generation = loginGeneration_; - loginCallback_(params); } -void LoginSession::OnLogoutWriteComplete(uint16_t expectedGeneration, - Async::AsyncStatus status) noexcept { - if (expectedGeneration != loginGeneration_ || state_ != LoginState::LoggingOut) { - return; - } - - if (status != Async::AsyncStatus::kSuccess) { - CancelManagementTimer(); - loginID_ = 0; - SetState(LoginState::Idle); - if (logoutCallback_) { - LogoutCompleteParams params{}; - params.status = -1; - params.generation = loginGeneration_; - logoutCallback_(params); - } - return; - } +void LoginSession::BindFetchAgent(const Fsm::HeldLogin& login, const Fsm::Route& route) noexcept { + const auto cbaAddr = [&](uint32_t offset) { + return Async::FWAddress{Async::FWAddress::QualifiedAddressParts{ + .addressHi = login.commandBlockAgentHi, + .addressLo = login.commandBlockAgentLo + offset, + .nodeID = route.node}}; + }; - StartLogoutTimer(); -} + FetchAgent::Binding binding{}; + binding.generation = route.generation; + binding.nodeID = route.node; + binding.fetchAgentAddress = cbaAddr(Wire::CommandBlockAgentOffsets::kFetchAgent); + binding.agentResetAddress = cbaAddr(Wire::CommandBlockAgentOffsets::kAgentReset); + binding.maxPayloadSize = maxPayloadSize_; + fetchAgent_.Bind(binding); -void LoginSession::OnLogoutTimeout() noexcept { - if (state_ != LoginState::LoggingOut) { - return; - } - ASFW_LOG_SBP2( "LoginSession: logout timeout, transitioning to Idle anyway"); - loginID_ = 0; - SetState(LoginState::Idle); - if (logoutCallback_) { - LogoutCompleteParams params{}; - params.status = -2; - params.generation = loginGeneration_; - logoutCallback_(params); - } + unsolicitedStatusAddress_ = cbaAddr(Wire::CommandBlockAgentOffsets::kUnsolicitedStatusEnable); } // --------------------------------------------------------------------------- @@ -731,55 +720,43 @@ void LoginSession::OnStatusBlockRemoteWrite(uint32_t offset, } std::memcpy(&block, payload.data(), len); - ASFW_LOG_SBP2( "LoginSession::OnStatusBlockRemoteWrite: state=%{public}s offset=%u len=%u sbpStatus=%u", - ToString(state_), offset, len, block.sbpStatus); + Serialized([this, block, offset, len] { + ASFW_LOG_SBP2( "LoginSession::OnStatusBlockRemoteWrite: state=%{public}s offset=%u len=%u sbpStatus=%u", + Fsm::Name(fsm_), offset, len, block.sbpStatus); + + switch (Fsm::Reported(fsm_)) { + case LoginState::LoggingIn: + CompleteLoginFromStatusBlock(block); + break; + case LoginState::Reconnecting: + CompleteReconnectFromStatusBlock(block); + break; + case LoginState::LoggingOut: + ASFW_LOG_SBP2( "LoginSession: logout complete (was loginID=%u)", + Fsm::Held(fsm_) ? Fsm::Held(fsm_)->id : 0); + (void)Apply(Fsm::Ev::LogoutDone{}); + break; + case LoginState::LoggedIn: + ProcessStatusBlock(block, len); + break; + default: + ASFW_LOG_SBP2( "LoginSession: unexpected status block in state %{public}s", + Fsm::Name(fsm_)); + break; + } + }); +} - switch (state_) { - case LoginState::LoggingIn: - CancelManagementTimer(); - CompleteLoginFromStatusBlock(block, len); - break; - case LoginState::Reconnecting: - CancelManagementTimer(); - CompleteReconnectFromStatusBlock(block, len); - break; - case LoginState::LoggingOut: - CancelManagementTimer(); - CompleteLogoutFromStatusBlock(block, len); - break; - case LoginState::LoggedIn: - ProcessStatusBlock(block, len); - break; - default: - ASFW_LOG_SBP2( "LoginSession: unexpected status block in state %{public}s", ToString(state_)); - break; +void LoginSession::CompleteLoginFromStatusBlock(const Wire::StatusBlock& block) noexcept { + const auto* loggingIn = std::get_if(&fsm_); + if (!loggingIn) { + return; } -} -void LoginSession::CompleteLoginFromStatusBlock(const Wire::StatusBlock& block, - uint32_t length) noexcept { if (block.sbpStatus != Wire::SBPStatus::kNoAdditionalInfo) { ASFW_LOG_SBP2( "LoginSession: login failed — sbpStatus=%u, retrying (%u/%u)", - block.sbpStatus, loginRetryCount_ + 1, kLoginRetryMax); - if (loginRetryCount_ < kLoginRetryMax) { - loginRetryCount_++; - ArmManagementTimer(kLoginRetryDelayMs, [this]() { - loginGeneration_ = static_cast(busInfo_.GetGeneration().value); - loginNodeID_ = targetInfo_.targetNodeId; - SetState(LoginState::Idle); - (void)Login(); - }); - return; - } - SetState(LoginState::Failed); - if (loginCallback_) { - LoginCompleteParams params{}; - params.status = -1; - params.statusBlock = block; - params.statusBlockLength = length; - params.generation = loginGeneration_; - loginCallback_(params); - } + block.sbpStatus, loggingIn->retries + 1, Fsm::kLoginRetryMax); + (void)Apply(Fsm::Ev::LoginRejected{}); return; } @@ -791,141 +768,60 @@ void LoginSession::CompleteLoginFromStatusBlock(const Wire::StatusBlock& block, if (kr != kIOReturnSuccess || responseData.size() < sizeof(Wire::LoginResponse)) { ASFW_LOG_SBP2( "LoginSession: failed to read login response (kr=0x%08x, len=%zu)", kr, responseData.size()); - if (loginRetryCount_ < kLoginRetryMax) { - loginRetryCount_++; - ArmManagementTimer(kLoginRetryDelayMs, [this]() { - loginGeneration_ = static_cast(busInfo_.GetGeneration().value); - loginNodeID_ = targetInfo_.targetNodeId; - SetState(LoginState::Idle); - (void)Login(); - }); - return; - } - SetState(LoginState::Failed); - if (loginCallback_) { - LoginCompleteParams params{}; - params.status = -1; - params.statusBlock = block; - params.statusBlockLength = length; - params.generation = loginGeneration_; - loginCallback_(params); - } + (void)Apply(Fsm::Ev::LoginRejected{}); return; } Wire::LoginResponse resp{}; std::memcpy(&resp, responseData.data(), sizeof(resp)); - loginID_ = OSSwapBigToHostInt16(resp.loginID); - reconnectHold_ = OSSwapBigToHostInt16(resp.reconnectHold); - loginResponse_ = resp; - - const uint32_t cbaHi = OSSwapBigToHostInt32(resp.commandBlockAgentAddressHi); - const uint32_t cbaLo = OSSwapBigToHostInt32(resp.commandBlockAgentAddressLo); - commandBlockAgent_ = Async::FWAddress{Async::FWAddress::QualifiedAddressParts{ - .addressHi = static_cast(cbaHi & 0xFFFFu), - .addressLo = cbaLo, - .nodeID = loginNodeID_}}; - - loginRetryCount_ = 0; - if (!EnablePhysicalDma()) { - return; - } - SetState(LoginState::LoggedIn); - BindFetchAgent(); - - // If unsolicited status was requested before login, enable it now (before the - // busy-timeout write, matching PR #19's ordering). - if (unsolicitedStatusRequested_) { - unsolicitedStatusRequested_ = false; - EnableUnsolicitedStatus(); - } - - ASFW_LOG_SBP2( "LoginSession: login successful — loginID=%u CBA=%04x:%08x reconnectHold=2^%u", - loginID_.load(), commandBlockAgent_.addressHi, commandBlockAgent_.addressLo, reconnectHold_); - - WriteBusyTimeout(); + Fsm::HeldLogin login{}; + login.id = OSSwapBigToHostInt16(resp.loginID); + login.reconnectHold = OSSwapBigToHostInt16(resp.reconnectHold); + login.commandBlockAgentHi = + static_cast(OSSwapBigToHostInt32(resp.commandBlockAgentAddressHi) & 0xFFFFu); + login.commandBlockAgentLo = OSSwapBigToHostInt32(resp.commandBlockAgentAddressLo); + login.response = resp; - if (loginCallback_) { - LoginCompleteParams params{}; - params.status = 0; - params.loginResponse = loginResponse_; - params.statusBlock = block; - params.statusBlockLength = length; - params.generation = loginGeneration_; - loginCallback_(params); - } + const bool physicalDmaOk = EnablePhysicalDma(loggingIn->route); + (void)Apply(Fsm::Ev::LoginAccepted{login, physicalDmaOk}); } -void LoginSession::CompleteReconnectFromStatusBlock(const Wire::StatusBlock& block, - uint32_t length) noexcept { - if (block.sbpStatus != Wire::SBPStatus::kNoAdditionalInfo) { - ASFW_LOG_SBP2( "LoginSession: reconnect failed — sbpStatus=%u, falling back to full login", - block.sbpStatus); - NotifySessionLost(); - SetState(LoginState::Idle); - (void)Login(); +void LoginSession::CompleteReconnectFromStatusBlock(const Wire::StatusBlock& block) noexcept { + const auto* reconnecting = std::get_if(&fsm_); + if (!reconnecting) { return; } - - if (!EnablePhysicalDma()) { + if (block.sbpStatus != Wire::SBPStatus::kNoAdditionalInfo) { + const bool retry = block.Response() == Wire::StatusBlock::kRespRequestComplete && + block.sbpStatus == Wire::SBPStatus::kUnspecifiedError; + ASFW_LOG_SBP2( "LoginSession: reconnect failed — sbpStatus=%u resp=%u, %{public}s", + block.sbpStatus, block.Response(), + retry ? "retrying" : "falling back to full login"); + (void)Apply(Fsm::Ev::ReconnectRejected{retry}); return; } - SetState(LoginState::LoggedIn); - BindFetchAgent(); - ASFW_LOG_SBP2( "LoginSession: reconnect successful — loginID=%u", loginID_.load()); - - WriteBusyTimeout(); - - if (unsolicitedStatusRequested_) { - unsolicitedStatusRequested_ = false; - EnableUnsolicitedStatus(); - } - - if (loginCallback_) { - LoginCompleteParams params{}; - params.status = 0; - params.loginResponse = loginResponse_; - params.statusBlock = block; - params.statusBlockLength = length; - params.generation = loginGeneration_; - loginCallback_(params); - } + const bool physicalDmaOk = EnablePhysicalDma(reconnecting->route); + (void)Apply(Fsm::Ev::ReconnectAccepted{physicalDmaOk}); } // Command data moves by physical DMA, so a login is usable only once the OHCI // admits the target's node for this generation; Linux enables at login // (sbp2.c:1146) and reconnect (sbp2.c:1198) the same way. A stale generation // means a reset is already in flight: wait for rediscovery like any reset. -bool LoginSession::EnablePhysicalDma() noexcept { +bool LoginSession::EnablePhysicalDma(const Fsm::Route& route) noexcept { if (!physicalDma_) { return true; } - const auto enabled = physicalDma_(loginNodeID_, loginGeneration_.load()); + const auto enabled = physicalDma_(route.node, route.generation); if (enabled) { return true; } ASFW_LOG_SBP2("LoginSession: physical DMA not enabled for node=0x%04x gen=%u (err=%u), suspending", - loginNodeID_, loginGeneration_.load(), static_cast(enabled.error())); - SetState(LoginState::Suspended); + route.node, route.generation, static_cast(enabled.error())); return false; } -void LoginSession::CompleteLogoutFromStatusBlock(const Wire::StatusBlock& /*block*/, - uint32_t /*length*/) noexcept { - const uint16_t oldLoginID = loginID_; - loginID_ = 0; - SetState(LoginState::Idle); - ASFW_LOG_SBP2( "LoginSession: logout complete (was loginID=%u)", oldLoginID); - - if (logoutCallback_) { - LogoutCompleteParams params{}; - params.status = 0; - params.generation = loginGeneration_; - logoutCallback_(params); - } -} - void LoginSession::ProcessStatusBlock(const Wire::StatusBlock& block, uint32_t length) noexcept { // Unsolicited: (details & 0xC0) == 0x80 (source bit set, resp == 0). const bool isUnsolicited = (block.details & 0xC0) == 0x80; @@ -935,7 +831,7 @@ void LoginSession::ProcessStatusBlock(const Wire::StatusBlock& block, uint32_t l } if (isUnsolicited) { - EnableUnsolicitedStatus(); // re-arm so the device can send more + EnableUnsolicitedStatusNow(); // re-arm so the device can send more return; } @@ -947,8 +843,9 @@ void LoginSession::ProcessStatusBlock(const Wire::StatusBlock& block, uint32_t l // --------------------------------------------------------------------------- bool LoginSession::SubmitORB(SBP2CommandORB* orb) noexcept { - if (state_ != LoginState::LoggedIn) { - ASFW_LOG_SBP2( "LoginSession::SubmitORB: state=%{public}s, rejecting", ToString(state_)); + const LoginState state = Snapshot().state; + if (state != LoginState::LoggedIn) { + ASFW_LOG_SBP2( "LoginSession::SubmitORB: state=%{public}s, rejecting", ToString(state)); return false; } return fetchAgent_.Submit(orb); @@ -963,29 +860,36 @@ void LoginSession::ResetFetchAgent(std::function callback) noexcept { // --------------------------------------------------------------------------- void LoginSession::EnableUnsolicitedStatus() noexcept { - if (state_ != LoginState::LoggedIn) { + Serialized([this] { EnableUnsolicitedStatusNow(); }); +} + +void LoginSession::EnableUnsolicitedStatusNow() noexcept { + const auto* loggedIn = std::get_if(&fsm_); + if (!loggedIn) { unsolicitedStatusRequested_ = true; return; } - const FW::Generation gen{loginGeneration_}; - const FW::NodeId node{static_cast(TargetNodeShort())}; - const FW::FwSpeed speed = busInfo_.GetSpeed(node); - + const auto route = loggedIn->route; + const FW::NodeId node = ShortNode(route.node); const std::weak_ptr weakSelf = weak_from_this(); unsolicitedStatusWriteHandle_ = bus_.WriteQuad( - gen, node, unsolicitedStatusAddress_, 0, speed, - [weakSelf, requestGeneration = loginGeneration_.load()](Async::AsyncStatus status, + FW::Generation{route.generation}, node, unsolicitedStatusAddress_, 0, + busInfo_.GetSpeed(node), + [weakSelf, requestGeneration = route.generation](Async::AsyncStatus status, std::span) { if (auto self = weakSelf.lock()) { - self->OnUnsolicitedStatusEnableComplete(requestGeneration, status); + self->Serialized([self, requestGeneration, status] { + self->OnUnsolicitedStatusEnableComplete(requestGeneration, status); + }); } }); } void LoginSession::OnUnsolicitedStatusEnableComplete(uint16_t expectedGeneration, Async::AsyncStatus status) noexcept { - if (expectedGeneration != loginGeneration_ || state_ != LoginState::LoggedIn) { + const auto* loggedIn = std::get_if(&fsm_); + if (!loggedIn || expectedGeneration != loggedIn->route.generation) { return; } if (status != Async::AsyncStatus::kSuccess) { @@ -1003,25 +907,29 @@ void LoginSession::WriteBusyTimeout() noexcept { bus_.Cancel(busyTimeoutWriteHandle_); busyTimeoutInProgress_ = false; } + const auto route = Fsm::RouteOf(fsm_); + if (!route) { + return; + } - const FW::Generation gen{loginGeneration_}; - const FW::NodeId node{static_cast(TargetNodeShort())}; + const FW::NodeId node = ShortNode(route->node); const Async::FWAddress busyAddr{Async::FWAddress::QualifiedAddressParts{ .addressHi = kCSRBusAddressHi, .addressLo = kBusyTimeoutAddressLo, - .nodeID = loginNodeID_}}; - const FW::FwSpeed speed = busInfo_.GetSpeed(node); + .nodeID = route->node}}; busyTimeoutInProgress_ = true; const std::weak_ptr weakSelf = weak_from_this(); busyTimeoutWriteHandle_ = bus_.WriteBlock( - gen, node, busyAddr, + FW::Generation{route->generation}, node, busyAddr, std::span{reinterpret_cast(&busyTimeoutBuffer_), 4}, - speed, - [weakSelf, requestGeneration = loginGeneration_.load()](Async::AsyncStatus status, - std::span) { + busInfo_.GetSpeed(node), + [weakSelf, requestGeneration = route->generation](Async::AsyncStatus status, + std::span) { if (auto self = weakSelf.lock()) { - self->OnBusyTimeoutComplete(requestGeneration, status); + self->Serialized([self, requestGeneration, status] { + self->OnBusyTimeoutComplete(requestGeneration, status); + }); } }); @@ -1033,7 +941,7 @@ void LoginSession::WriteBusyTimeout() noexcept { void LoginSession::OnBusyTimeoutComplete(uint16_t expectedGeneration, Async::AsyncStatus status) noexcept { - if (expectedGeneration != loginGeneration_) { + if (expectedGeneration != Fsm::GenerationOf(fsm_)) { return; } busyTimeoutInProgress_ = false; @@ -1046,41 +954,31 @@ void LoginSession::OnBusyTimeoutComplete(uint16_t expectedGeneration, // Helpers // --------------------------------------------------------------------------- -void LoginSession::SetState(LoginState newState) noexcept { - if (state_ != newState) { - ASFW_LOG_SBP2( "LoginSession: state %{public}s -> %{public}s", ToString(state_), ToString(newState)); - state_ = newState; - } -} - -void LoginSession::StartLoginTimer() noexcept { - ArmManagementTimer(targetInfo_.managementTimeoutMs, [this]() { OnLoginTimeout(); }); -} - -void LoginSession::StartReconnectTimer() noexcept { - ArmManagementTimer(targetInfo_.managementTimeoutMs + 1000, [this]() { OnReconnectTimeout(); }); -} - -void LoginSession::StartLogoutTimer() noexcept { - ArmManagementTimer(targetInfo_.managementTimeoutMs, [this]() { OnLogoutTimeout(); }); -} - -void LoginSession::ArmManagementTimer(uint64_t delayMs, std::function fn) noexcept { - CancelManagementTimer(); +void LoginSession::ArmTimerSlot(TimerSlot LoginSession::*slot, uint64_t delayMs, + Fsm::Event event) noexcept { + CancelTimerSlot(this->*slot); + const uint64_t arm = (this->*slot).arm; const std::weak_ptr weakSelf = weak_from_this(); - managementTimerToken_ = scheduler_.ScheduleAfter( - delayMs * 1'000'000ULL, [weakSelf, fn = std::move(fn)]() mutable { + (this->*slot).token = scheduler_.ScheduleAfter( + delayMs * 1'000'000ULL, [weakSelf, slot, arm, event = std::move(event)]() mutable { if (auto self = weakSelf.lock()) { - self->managementTimerToken_ = kInvalidSchedulerToken; - fn(); + self->Serialized([self, slot, arm, event = std::move(event)] { + TimerSlot& fired = (*self).*slot; + if (arm != fired.arm) { + return; + } + fired.token = kInvalidSchedulerToken; + (void)self->Apply(event); + }); } }); } -void LoginSession::CancelManagementTimer() noexcept { - if (managementTimerToken_ != kInvalidSchedulerToken) { - scheduler_.Cancel(managementTimerToken_); - managementTimerToken_ = kInvalidSchedulerToken; +void LoginSession::CancelTimerSlot(TimerSlot& slot) noexcept { + ++slot.arm; + if (slot.token != kInvalidSchedulerToken) { + scheduler_.Cancel(slot.token); + slot.token = kInvalidSchedulerToken; } } diff --git a/ASFWDriver/Protocols/SBP2/Session/LoginSession.hpp b/ASFWDriver/Protocols/SBP2/Session/LoginSession.hpp index ff026bb8..6c4a5fd7 100644 --- a/ASFWDriver/Protocols/SBP2/Session/LoginSession.hpp +++ b/ASFWDriver/Protocols/SBP2/Session/LoginSession.hpp @@ -12,9 +12,13 @@ // 3. On the status block write, parse the login response, derive the // command-block-agent addresses, Bind() the FetchAgent, and write the // busy-timeout CSR. -// 4. On bus reset, Suspend then Reconnect() with the stored loginID. +// 4. On bus reset, suspend; Resume() after rediscovery reconnects a held +// login or logs in again if the login never completed. // 5. Logout() terminates the session. // +// The states and their transitions are LoginFsm (LoginStateMachine.hpp); this +// class feeds it events and carries out the effects. +// // DICE adaptations vs #19: // * Timers go through the injected ISessionScheduler (one cancelable management // timer at a time), never the IOSleep-on-queue path. The two-queue model @@ -25,6 +29,7 @@ // solicited blocks to FetchAgent::OnStatusBlock. #include "ISessionScheduler.hpp" +#include "LoginStateMachine.hpp" #include "FetchAgent.hpp" #include "../SBP2WireFormats.hpp" #include "../SBP2CommandORB.hpp" @@ -35,10 +40,13 @@ #include #include #include +#include #include #include #include +#include #include +#include namespace ASFW::Async { class IFireWireBus; @@ -76,8 +84,6 @@ struct SBP2TargetInfo { 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}; }; @@ -86,33 +92,14 @@ struct LogoutCompleteParams { 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 +// What other queues read: one atomic word, so the state, login ID and +// generation are always from the same transition. +struct LoginSnapshot { + LoginState state{LoginState::Idle}; + uint16_t generation{0}; + std::optional loginID; // set only while the target holds our login }; -[[nodiscard]] inline constexpr const char* ToString(LoginState s) noexcept { - switch (s) { - case LoginState::Idle: return "Idle"; - case LoginState::LoggingIn: return "LoggingIn"; - case LoginState::LoggedIn: return "LoggedIn"; - case LoginState::Reconnecting: return "Reconnecting"; - case LoginState::LoggingOut: return "LoggingOut"; - case LoginState::Suspended: return "Suspended"; - case LoginState::Failed: return "Failed"; - } - return "Unknown"; -} - // --------------------------------------------------------------------------- // LoginSession // --------------------------------------------------------------------------- @@ -153,7 +140,9 @@ class LoginSession : public std::enable_shared_from_this { [[nodiscard]] bool Login() noexcept; [[nodiscard]] bool Logout() noexcept; - [[nodiscard]] bool Reconnect() noexcept; + // Rediscovery supplied a fresh route (Configure() first): reconnect a held + // login, or log in if the reset interrupted the login. + [[nodiscard]] bool Resume() noexcept; void HandleBusReset(uint16_t newGeneration) noexcept; // The suspended login's device is gone for good (unit terminated, or back // as a new incarnation that cannot hold the old login). No reconnect is @@ -164,11 +153,13 @@ class LoginSession : public std::enable_shared_from_this { // Accessors // ----------------------------------------------------------------------- - [[nodiscard]] LoginState State() const noexcept { return state_; } - [[nodiscard]] uint16_t LoginID() const noexcept { return loginID_; } - [[nodiscard]] uint16_t Generation() const noexcept { return loginGeneration_; } + // Safe from any queue. + [[nodiscard]] LoginSnapshot Snapshot() const noexcept; + [[nodiscard]] LoginState State() const noexcept { return Snapshot().state; } + [[nodiscard]] const SBP2TargetInfo& TargetInfo() const noexcept { return targetInfo_; } - [[nodiscard]] Async::FWAddress CommandBlockAgent() const noexcept { return commandBlockAgent_; } + // Read the state unserialized: host tests and diagnostics only. + [[nodiscard]] Async::FWAddress CommandBlockAgent() const noexcept; [[nodiscard]] uint32_t ReconnectHoldSeconds() const noexcept; [[nodiscard]] uint16_t MaxPayloadSize() const noexcept { return maxPayloadSize_; } void SetMaxPayloadSize(uint16_t bytes) noexcept { maxPayloadSize_ = bytes; } @@ -216,50 +207,64 @@ class LoginSession : public std::enable_shared_from_this { bool AllocateLogoutORBAddressSpace() noexcept; void RegisterStatusBlockCallback() noexcept; + // State machine ---------------------------------------------------------- + // Every access to fsm_ runs through Serialized(), one piece of work at a + // time whatever the calling queue: timers, completions and RX arrive on the + // driver queue, the probe user client calls in from its own. The lock + // covers only the queue, so work may run callbacks that take the registry's + // lock. Work raised while other work runs (an inline write completion, a + // callback calling back in) is queued behind it: run-to-completion. + void Serialized(std::function work) noexcept; + void Dispatch(LoginFsm::Event event) noexcept; + // A public request: false if the state refused it, or its write was refused + // on the spot. Queued behind another thread's work, it reports true. + [[nodiscard]] bool Request(LoginFsm::Event event, const char* what) noexcept; + bool Apply(const LoginFsm::Event& event) noexcept; + void Commit(LoginFsm::State next) noexcept; + [[nodiscard]] LoginFsm::Route Now() const noexcept; + void IssueWrite(LoginFsm::Op op) noexcept; + void ArmTimer(LoginFsm::Timer timer) noexcept; + void Notify(const LoginFsm::Effects& fx) noexcept; + // ORB construction -------------------------------------------------------- void BuildLoginORB() noexcept; - void BuildReconnectORB() noexcept; - void BuildLogoutORB() noexcept; - - // Bind / unbind the command plane ---------------------------------------- - void BindFetchAgent() noexcept; - void RefreshUnsolicitedStatusAddress() noexcept; - - // Management write completions ------------------------------------------- - void OnLoginWriteComplete(uint16_t expectedGeneration, Async::AsyncStatus status) noexcept; - void OnLoginTimeout() noexcept; - void OnReconnectWriteComplete(uint16_t expectedGeneration, Async::AsyncStatus status) noexcept; - void OnReconnectTimeout() noexcept; - void OnLogoutWriteComplete(uint16_t expectedGeneration, Async::AsyncStatus status) noexcept; - void OnLogoutTimeout() noexcept; - void NotifySessionLost() noexcept; + void BuildReconnectORB(uint16_t loginID) noexcept; + void BuildLogoutORB(uint16_t loginID) noexcept; + [[nodiscard]] const std::array& OrbAddressFor(LoginFsm::Op op) const noexcept; + + // Bind the command plane after a login or reconnect ----------------------- + void BindCommandPlane(LoginFsm::Op op) noexcept; + void BindFetchAgent(const LoginFsm::HeldLogin& login, const LoginFsm::Route& route) noexcept; + + void NotifySessionLost(uint16_t generation) noexcept; // Status block handling --------------------------------------------------- void OnStatusBlockRemoteWrite(uint32_t offset, std::span payload) noexcept; void ProcessStatusBlock(const Wire::StatusBlock& block, uint32_t length) noexcept; - void CompleteLoginFromStatusBlock(const Wire::StatusBlock& block, uint32_t length) noexcept; - [[nodiscard]] bool EnablePhysicalDma() noexcept; - void CompleteReconnectFromStatusBlock(const Wire::StatusBlock& block, uint32_t length) noexcept; - void CompleteLogoutFromStatusBlock(const Wire::StatusBlock& block, uint32_t length) noexcept; + void CompleteLoginFromStatusBlock(const Wire::StatusBlock& block) noexcept; + [[nodiscard]] bool EnablePhysicalDma(const LoginFsm::Route& route) noexcept; + void CompleteReconnectFromStatusBlock(const Wire::StatusBlock& block) noexcept; // Unsolicited status + busy timeout writes ------------------------------- + void EnableUnsolicitedStatusNow() noexcept; void OnUnsolicitedStatusEnableComplete(uint16_t expectedGeneration, Async::AsyncStatus status) noexcept; void WriteBusyTimeout() noexcept; void OnBusyTimeoutComplete(uint16_t expectedGeneration, Async::AsyncStatus status) noexcept; - // Helpers ----------------------------------------------------------------- - void SetState(LoginState newState) noexcept; - void StartLoginTimer() noexcept; - void StartReconnectTimer() noexcept; - void StartLogoutTimer() noexcept; - void CancelLoginTimer() noexcept { CancelManagementTimer(); } - void ArmManagementTimer(uint64_t delayMs, std::function fn) noexcept; - void CancelManagementTimer() noexcept; - void ScheduleManagementRetry(uint64_t delayMs, void (LoginSession::*op)()) noexcept; - - [[nodiscard]] uint16_t TargetNodeShort() const noexcept { - return static_cast(loginNodeID_ & 0x3Fu); + // Timers ------------------------------------------------------------------ + // A timer that fires after it was cancelled or re-armed (its work queued + // behind the transition that did so) is dropped by its arm count. + struct TimerSlot { + SchedulerToken token{kInvalidSchedulerToken}; + uint64_t arm{0}; + }; + void ArmTimerSlot(TimerSlot LoginSession::*slot, uint64_t delayMs, LoginFsm::Event event) noexcept; + void CancelTimerSlot(TimerSlot& slot) noexcept; + void CancelManagementTimer() noexcept { CancelTimerSlot(managementTimer_); } + + [[nodiscard]] static FW::NodeId ShortNode(uint16_t node) noexcept { + return FW::NodeId{static_cast(node & 0x3Fu)}; } // Members ----------------------------------------------------------------- @@ -275,28 +280,16 @@ class LoginSession : public std::enable_shared_from_this { bool configured_{false}; uint16_t maxPayloadSize_{4096}; // default, clipped by login response - // Session state - // Written on the driver work queue; read cross-queue by the HBA - // (SBP2TargetBridge::IsReady → SessionRegistry::GetSessionState from the - // SCSI controller's queues). The registry lock does not cover the writers - // (RX, timer and write-completion paths), so these must be atomic. - std::atomic state_{LoginState::Idle}; - std::atomic loginID_{0}; - std::atomic 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; - // LoginCompleteParams.status for a lost session (reconnect rejected/timed - // out): terminal for the OLD login; a fresh fallback login follows. - static constexpr int kStatusSessionLost = -3; + // Session state. fsm_ lives on the driver work queue; other queues (the + // HBA via SessionRegistry::GetSessionState, the user client) read snapshot_. + LoginFsm::State fsm_{LoginFsm::St::Idle{}}; + std::atomic snapshot_{0}; + IOLock* executorLock_{nullptr}; + std::deque> pendingWork_; + bool executing_{false}; + + static constexpr uint64_t kRetryDelayMs = 1000; + static constexpr uint64_t kReconnectResendDelayMs = 100; // Address space handles --------------------------------------------------- uint64_t loginORBHandle_{0}; @@ -345,8 +338,10 @@ class LoginSession : public std::enable_shared_from_this { LogoutCallback logoutCallback_; StatusCallback statusCallback_; - // Single cancelable management-plane timer (login/reconnect/logout/retry). - SchedulerToken managementTimerToken_{kInvalidSchedulerToken}; + // One management-plane timer (login/reconnect/logout timeouts and retries) + // and the reconnect window. + TimerSlot managementTimer_; + TimerSlot reconnectWindow_; }; } // namespace ASFW::Protocols::SBP2 diff --git a/ASFWDriver/Protocols/SBP2/Session/LoginStateMachine.hpp b/ASFWDriver/Protocols/SBP2/Session/LoginStateMachine.hpp new file mode 100644 index 00000000..e397275f --- /dev/null +++ b/ASFWDriver/Protocols/SBP2/Session/LoginStateMachine.hpp @@ -0,0 +1,424 @@ +#pragma once + +// LoginStateMachine — the SBP-2 management-plane states as data. +// +// Each state carries only what is true in it. A login ID exists only while the +// target holds our login, so "do we hold a login?" is the state's type, never a +// sentinel value: SBP-2 does not reserve any login_ID, and the H4D hands out 0. +// Neither reference uses one either (Apple decides by fLoginState, +// IOFireWireSBP2Login.cpp:2067; Linux by its work function, sbp2.c:849,937). +// +// Transition() is pure: LoginSession feeds it events and carries out the +// returned effects (bus writes, timers, callbacks). The table, and how it +// compares with Apple and Linux, is in documentation/sbp2-login-fsm.md. + +#include "../SBP2WireFormats.hpp" + +#include +#include +#include +#include +#include + +namespace ASFW::Protocols::SBP2 { + +enum class LoginState : uint8_t { + Idle, + LoggingIn, + LoggedIn, + Reconnecting, + LoggingOut, + Suspended, // Lost after bus reset, waiting for rediscovery + 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"; +} + +namespace LoginFsm { + +enum class Op : uint8_t { Login, Reconnect, Logout }; + +struct Route { + uint16_t generation{0}; + uint16_t node{0xFFFF}; +}; + +// A login the target holds for us, from its login response. +struct HeldLogin { + uint16_t id{0}; + uint16_t commandBlockAgentHi{0}; + uint32_t commandBlockAgentLo{0}; + uint16_t reconnectHold{0}; + Wire::LoginResponse response{}; +}; + +namespace St { +struct Idle { uint16_t generation{0}; }; +struct LoggingIn { Route route; uint32_t retries{0}; }; +struct LoggedIn { HeldLogin login; Route route; }; +struct Reconnecting { HeldLogin login; Route route; }; +// Bus reset with a login held: reconnect once rediscovery supplies the route. +struct SuspendedHeld { HeldLogin login; uint16_t generation{0}; }; +// Bus reset before the login completed: log in once rediscovery supplies the route. +struct SuspendedNoLogin { uint16_t generation{0}; }; +struct LoggingOut { HeldLogin login; Route route; }; +struct Failed { uint16_t generation{0}; }; +} // namespace St + +using State = std::variant; + +namespace Ev { +struct Login {}; +struct Logout {}; +struct Resume {}; // rediscovered; `now` is the fresh route +struct BusReset { uint16_t generation{0}; }; +struct DeviceGone {}; +struct ResourcesUnavailable {}; +struct WriteRefused { Op op; }; // the bus refused the write synchronously +struct WriteDone { Op op; uint16_t generation{0}; bool ok{false}; }; +struct Timeout { Op op; }; +struct LoginRetryDue {}; +struct ReconnectRetryDue {}; +struct ReconnectWindowClosed {}; +struct LoginAccepted { HeldLogin login; bool physicalDmaOk{false}; }; +struct LoginRejected {}; // error status, or the response is unreadable +struct ReconnectAccepted { bool physicalDmaOk{false}; }; +// retry: request complete with sbp_status "unspecified error", which Apple +// treats as transient (IOFireWireSBP2Login.cpp:2498-2503). +struct ReconnectRejected { bool retry{false}; }; +struct LogoutDone {}; +} // namespace Ev + +using Event = std::variant; + +enum class Timer : uint8_t { + LoginTimeout, // management timeout → Timeout{Login} + LoginRetry, // 1 s → LoginRetryDue + ReconnectRetry, // 1 s → Timeout{Reconnect} + ReconnectResend, // 100 ms → ReconnectRetryDue (Apple :589) + LogoutTimeout, // management timeout → Timeout{Logout} +}; + +enum class Notify : uint8_t { None, LoggedIn, LoginFailed, SessionLost, LogoutDone }; + +// LoginCompleteParams.status for a lost session (reconnect rejected/timed out, +// device gone): terminal for the OLD login. +inline constexpr int kStatusSessionLost = -3; + +// Carried out by LoginSession in this order: cancelTimer/cancelWindow, unbind, +// release, sessionLostFirst, (state committed), write, arm/armWindow, bind, notify. +// +// The reconnect window is a second timer, beside the management one: it runs +// from the bus reset (management timeout + 1 s → ReconnectWindowClosed) and a +// held login that has not reconnected when it closes is lost. Apple starts it +// at the reset and restarts it on every further reset +// (IOFireWireSBP2Login::suspendedNotify → startReconnectTimer, :2067-2077, +// :2202-2235). +struct Effects { + bool cancelTimer{false}; + bool cancelWindow{false}; + bool unbind{false}; // detach the fetch agent + bool release{false}; // free the management address spaces + bool sessionLostFirst{false}; // down-edge for the old login before what follows + std::optional write; + std::optional arm; + bool armWindow{false}; + std::optional bind; // bind the command plane after a login or reconnect + Notify notify{Notify::None}; + int status{0}; + uint16_t generation{0}; +}; + +struct Step { + State next; + Effects fx; +}; + +inline constexpr uint32_t kLoginRetryMax = 32; // Apple default (IOFireWireSBP2Login.cpp:142) + +[[nodiscard]] inline LoginState Reported(const State& s) noexcept { + return std::visit([](const auto& st) -> LoginState { + using T = std::decay_t; + if constexpr (std::is_same_v) return LoginState::Idle; + else if constexpr (std::is_same_v) return LoginState::LoggingIn; + else if constexpr (std::is_same_v) return LoginState::LoggedIn; + else if constexpr (std::is_same_v) return LoginState::Reconnecting; + else if constexpr (std::is_same_v) return LoginState::LoggingOut; + else if constexpr (std::is_same_v) return LoginState::Failed; + else return LoginState::Suspended; + }, s); +} + +[[nodiscard]] inline const char* Name(const State& s) noexcept { + if (std::holds_alternative(s)) return "SuspendedHeld"; + if (std::holds_alternative(s)) return "SuspendedNoLogin"; + return ToString(Reported(s)); +} + +[[nodiscard]] inline const HeldLogin* Held(const State& s) noexcept { + return std::visit([](const auto& st) -> const HeldLogin* { + if constexpr (requires { st.login; }) return &st.login; + else return nullptr; + }, s); +} + +[[nodiscard]] inline std::optional RouteOf(const State& s) noexcept { + return std::visit([](const auto& st) -> std::optional { + if constexpr (requires { st.route; }) return st.route; + else return std::nullopt; + }, s); +} + +[[nodiscard]] inline uint16_t GenerationOf(const State& s) noexcept { + return std::visit([](const auto& st) -> uint16_t { + if constexpr (requires { st.route; }) return st.route.generation; + else return st.generation; + }, s); +} + +namespace detail { + +inline Step StartLogin(const Route& now, uint32_t retries) { + return {St::LoggingIn{now, retries}, {.write = Op::Login, .arm = Timer::LoginTimeout}}; +} + +inline Step FailLogin(const Route& route, int status) { + return {St::Failed{route.generation}, + {.cancelTimer = true, .notify = Notify::LoginFailed, .status = status, + .generation = route.generation}}; +} + +// A failed attempt backs off 1 s and logs in again, up to kLoginRetryMax +// (Apple: completeLogin arms a 1 s retry, IOFireWireSBP2Login.cpp:1703-1710). +inline Step RetryLogin(const St::LoggingIn& s) { + if (s.retries >= kLoginRetryMax) { + return FailLogin(s.route, -1); + } + return {St::LoggingIn{s.route, s.retries + 1}, {.arm = Timer::LoginRetry}}; +} + +// The target dropped the old login: report it lost, then log in afresh +// (Apple: ReconnectFailed to the client, which logs in again, +// IOFireWireSBP2Login.cpp:2565-2583, IOFireWireSerialBusProtocolTransport.cpp:794-806). +inline Step FallBackToLogin(const St::Reconnecting& s, const Route& now) { + Step step = StartLogin(now, 0); + step.fx.cancelTimer = true; + step.fx.cancelWindow = true; + step.fx.sessionLostFirst = true; + step.fx.generation = s.route.generation; + return step; +} + +inline Step Bound(const HeldLogin& login, const Route& route, Op op) { + return {St::LoggedIn{login, route}, + {.cancelTimer = true, .cancelWindow = true, .bind = op, .notify = Notify::LoggedIn, + .generation = route.generation}}; +} + +inline Step Suspend(State next) { + return {std::move(next), {.cancelTimer = true, .unbind = true, .release = true}}; +} + +// A reset with a login held (re)starts the reconnect window. +inline Step SuspendHeld(const HeldLogin& login, uint16_t generation) { + Step step = Suspend(St::SuspendedHeld{login, generation}); + step.fx.armWindow = true; + return step; +} + +// Physical DMA was refused: a reset is in flight. Hold the login meanwhile. +inline Step HoldWithoutDma(const HeldLogin& login, uint16_t generation) { + return {St::SuspendedHeld{login, generation}, {.cancelTimer = true, .armWindow = true}}; +} + +inline Step Lost(uint16_t generation) { + return {St::Failed{generation}, + {.cancelTimer = true, .cancelWindow = true, .notify = Notify::SessionLost, + .status = kStatusSessionLost, .generation = generation}}; +} + +inline Step LoggedOut(const Route& route, int status) { + return {St::Idle{route.generation}, + {.cancelTimer = true, .notify = Notify::LogoutDone, .status = status, + .generation = route.generation}}; +} + +inline bool Current(const Ev::WriteDone& e, Op op, const Route& route) { + return e.op == op && e.generation == route.generation; +} + +template struct Overload : F... { using F::operator()...; }; + +} // namespace detail + +// nullopt: the event means nothing in this state (stale or out of order) and is dropped. +[[nodiscard]] inline std::optional Transition(const State& state, const Event& event, + const Route& now) { + using namespace detail; + using R = std::optional; + + return std::visit(Overload{ + // Idle / Failed ------------------------------------------------------ + [&](const St::Idle&, const Ev::Login&) -> R { return StartLogin(now, 0); }, + [&](const St::Failed&, const Ev::Login&) -> R { return StartLogin(now, 0); }, + [&](const St::Failed&, const Ev::BusReset& e) -> R { + return Step{St::Failed{e.generation}, {.release = true}}; + }, + + // LoggingIn ---------------------------------------------------------- + [&](const St::LoggingIn& s, const Ev::ResourcesUnavailable&) -> R { + return Step{St::Failed{s.route.generation}, {.cancelTimer = true}}; + }, + [&](const St::LoggingIn& s, const Ev::WriteRefused& e) -> R { + if (e.op != Op::Login) return std::nullopt; + return Step{St::Failed{s.route.generation}, {.cancelTimer = true}}; + }, + [&](const St::LoggingIn& s, const Ev::WriteDone& e) -> R { + if (!Current(e, Op::Login, s.route)) return std::nullopt; + if (e.ok) return Step{s, {.arm = Timer::LoginTimeout}}; + return RetryLogin(s); + }, + [&](const St::LoggingIn& s, const Ev::Timeout& e) -> R { + if (e.op != Op::Login) return std::nullopt; + if (s.retries >= kLoginRetryMax) return FailLogin(s.route, -2); + return StartLogin(now, s.retries + 1); + }, + [&](const St::LoggingIn& s, const Ev::LoginRetryDue&) -> R { + return StartLogin(now, s.retries); + }, + [&](const St::LoggingIn& s, const Ev::LoginAccepted& e) -> R { + if (!e.physicalDmaOk) return HoldWithoutDma(e.login, s.route.generation); + return Bound(e.login, s.route, Op::Login); + }, + [&](const St::LoggingIn& s, const Ev::LoginRejected&) -> R { return RetryLogin(s); }, + [&](const St::LoggingIn&, const Ev::BusReset& e) -> R { + return Suspend(St::SuspendedNoLogin{e.generation}); + }, + + // LoggedIn ----------------------------------------------------------- + [&](const St::LoggedIn& s, const Ev::BusReset& e) -> R { + return SuspendHeld(s.login, e.generation); + }, + [&](const St::LoggedIn& s, const Ev::Logout&) -> R { + return Step{St::LoggingOut{s.login, s.route}, + {.unbind = true, .write = Op::Logout, .arm = Timer::LogoutTimeout}}; + }, + + // Reconnecting ------------------------------------------------------- + [&](const St::Reconnecting& s, const Ev::ResourcesUnavailable&) -> R { + return Step{St::Failed{s.route.generation}, {.cancelTimer = true}}; + }, + // Apple reports a failed reconnect write as a lost session + // (IOFireWireSBP2Login::doReconnect, IOFireWireSBP2Login.cpp:2285). + [&](const St::Reconnecting& s, const Ev::WriteRefused& e) -> R { + if (e.op != Op::Reconnect) return std::nullopt; + return Step{s, {.arm = Timer::ReconnectRetry}}; + }, + // ACKed: the status is due before the reconnect window closes. + [&](const St::Reconnecting& s, const Ev::WriteDone& e) -> R { + if (!Current(e, Op::Reconnect, s.route)) return std::nullopt; + if (e.ok) return Step{s, {}}; + return Step{s, {.arm = Timer::ReconnectRetry}}; + }, + [&](const St::Reconnecting& s, const Ev::ReconnectWindowClosed&) -> R { + return FallBackToLogin(s, now); + }, + [&](const St::Reconnecting& s, const Ev::Timeout& e) -> R { + if (e.op != Op::Reconnect) return std::nullopt; + return FallBackToLogin(s, now); + }, + [&](const St::Reconnecting& s, const Ev::ReconnectAccepted& e) -> R { + if (!e.physicalDmaOk) return HoldWithoutDma(s.login, s.route.generation); + return Bound(s.login, s.route, Op::Reconnect); + }, + // Unspecified error: send the reconnect again until the window closes. + [&](const St::Reconnecting& s, const Ev::ReconnectRejected& e) -> R { + if (e.retry) return Step{s, {.arm = Timer::ReconnectResend}}; + return FallBackToLogin(s, now); + }, + [&](const St::Reconnecting& s, const Ev::ReconnectRetryDue&) -> R { + return Step{St::Reconnecting{s.login, now}, {.write = Op::Reconnect}}; + }, + [&](const St::Reconnecting& s, const Ev::BusReset& e) -> R { + return SuspendHeld(s.login, e.generation); + }, + + // SuspendedHeld ------------------------------------------------------ + [&](const St::SuspendedHeld& s, const Ev::Resume&) -> R { + return Step{St::Reconnecting{s.login, now}, {.write = Op::Reconnect}}; + }, + [&](const St::SuspendedHeld& s, const Ev::Logout&) -> R { + return Step{St::LoggingOut{s.login, now}, + {.cancelWindow = true, .unbind = true, .write = Op::Logout, + .arm = Timer::LogoutTimeout}}; + }, + [&](const St::SuspendedHeld& s, const Ev::DeviceGone&) -> R { return Lost(s.generation); }, + [&](const St::SuspendedHeld& s, const Ev::BusReset& e) -> R { + return Step{St::SuspendedHeld{s.login, e.generation}, {.armWindow = true}}; + }, + // Not rediscovered in time: the target has dropped the login. Report + // it lost; rediscovery then logs in afresh (Apple: reconnectTimeout → + // ReconnectFailed, :2395-2413, and the client logs in again). + [&](const St::SuspendedHeld& s, const Ev::ReconnectWindowClosed&) -> R { + return Step{St::SuspendedNoLogin{s.generation}, + {.notify = Notify::SessionLost, .status = kStatusSessionLost, + .generation = s.generation}}; + }, + + // SuspendedNoLogin --------------------------------------------------- + [&](const St::SuspendedNoLogin&, const Ev::Resume&) -> R { return StartLogin(now, 0); }, + [&](const St::SuspendedNoLogin&, const Ev::Login&) -> R { return StartLogin(now, 0); }, + // Nothing to log out of: the login never completed. + [&](const St::SuspendedNoLogin& s, const Ev::Logout&) -> R { + return LoggedOut(Route{s.generation, now.node}, 0); + }, + [&](const St::SuspendedNoLogin& s, const Ev::DeviceGone&) -> R { return Lost(s.generation); }, + [&](const St::SuspendedNoLogin&, const Ev::BusReset& e) -> R { + return Step{St::SuspendedNoLogin{e.generation}, {}}; + }, + + // LoggingOut --------------------------------------------------------- + [&](const St::LoggingOut& s, const Ev::ResourcesUnavailable&) -> R { + return Step{St::Failed{s.route.generation}, {.cancelTimer = true}}; + }, + [&](const St::LoggingOut& s, const Ev::WriteRefused& e) -> R { + if (e.op != Op::Logout) return std::nullopt; + return Step{St::Failed{s.route.generation}, {.cancelTimer = true}}; + }, + [&](const St::LoggingOut& s, const Ev::WriteDone& e) -> R { + if (!Current(e, Op::Logout, s.route)) return std::nullopt; + if (e.ok) return Step{s, {.arm = Timer::LogoutTimeout}}; + return LoggedOut(s.route, -1); + }, + [&](const St::LoggingOut& s, const Ev::Timeout& e) -> R { + if (e.op != Op::Logout) return std::nullopt; + return LoggedOut(s.route, -2); + }, + [&](const St::LoggingOut& s, const Ev::LogoutDone&) -> R { return LoggedOut(s.route, 0); }, + // A bus reset changes nothing here: the logout write of the old + // generation fails, or the timeout ends it, and both report the logout + // (Apple: suspendedNotify ignores LoggingOut, IOFireWireSBP2Login.cpp:2088-2092). + + [](const auto&, const auto&) -> R { return std::nullopt; }, + }, state, event); +} + +} // namespace LoginFsm +} // namespace ASFW::Protocols::SBP2 diff --git a/ASFWDriver/Protocols/SBP2/Session/SessionRegistry.cpp b/ASFWDriver/Protocols/SBP2/Session/SessionRegistry.cpp index 4897264b..f91545f2 100644 --- a/ASFWDriver/Protocols/SBP2/Session/SessionRegistry.cpp +++ b/ASFWDriver/Protocols/SBP2/Session/SessionRegistry.cpp @@ -224,9 +224,10 @@ std::optional SessionRegistry::GetSessionState(void* owner, } SBP2SessionState state{}; - state.loginState = record->session->State(); - state.loginID = record->session->LoginID(); - state.generation = record->session->Generation(); + const LoginSnapshot snap = record->session->Snapshot(); + state.loginState = snap.state; + state.loginID = snap.loginID.value_or(0); + state.generation = snap.generation; state.lastError = record->lastError; state.reconnectPending = (state.loginState == LoginState::Suspended); return state; @@ -499,7 +500,7 @@ void SessionRegistry::RefreshTargets(Discovery::Generation gen) { record.session->Configure(BuildTargetInfoFromUnit(*unit)); record.route = *route; - ASFW_LOG(Async, "SessionRegistry: reconnecting session handle=%llu gen=%u", + ASFW_LOG(Async, "SessionRegistry: resuming session handle=%llu gen=%u", handle, gen.value); toReconnect.push_back(record.session); } @@ -511,8 +512,7 @@ void SessionRegistry::RefreshTargets(Discovery::Generation gen) { session->AbandonSuspended(); } for (auto& session : toReconnect) { - if (session->LoginID() == 0) (void)session->Login(); - else (void)session->Reconnect(); + (void)session->Resume(); } } diff --git a/documentation/sbp2-login-fsm.md b/documentation/sbp2-login-fsm.md new file mode 100644 index 00000000..51570064 --- /dev/null +++ b/documentation/sbp2-login-fsm.md @@ -0,0 +1,142 @@ +# SBP-2 login state machine + +`LoginSession` drives the management plane (login, reconnect, logout) through +`LoginFsm` in `ASFWDriver/Protocols/SBP2/Session/LoginStateMachine.hpp`. Each +state is a type that carries only the data that is valid in it, and one pure +function, `Transition(state, event, now)`, returns the next state plus the +effects (`Effects`) the session carries out: bus writes, timers, binding the +command plane, callbacks. `now` is the current bus generation and the target's +node from the last `Configure()`. + +A login ID exists only in the states where the target holds our login. Whether +a reset is followed by a reconnect or a fresh login is decided by the state's +type, not by a sentinel value. SBP-2 reserves no login_ID, and the Hasselblad +H4D assigns 0. Neither reference uses a sentinel for this: Apple decides by +`fLoginState` (`IOFireWireSBP2Login.cpp:2067`), Linux by its work function +(`sbp2.c:849`, `937`; its `INVALID_LOGIN_ID` at `sbp2.c:184` only gates the +logout in remove). + +## States + +| State | Data | Reported as | +|---|---|---| +| `Idle` | generation | Idle | +| `LoggingIn` | route, retries used | LoggingIn | +| `LoggedIn` | login, route | LoggedIn | +| `Reconnecting` | login, route | Reconnecting | +| `SuspendedHeld` | login, generation | Suspended | +| `SuspendedNoLogin` | generation | Suspended | +| `LoggingOut` | login, route | LoggingOut | +| `Failed` | generation | Failed | + +`login` is the target's login response: login_ID, command block agent, +reconnect_hold. `route` is the generation and node the management writes go to. +`LoginState` stays the reported enum, so the registry, the HBA bridge and the +user client are unchanged. + +## Transitions + +Write completions carry the operation and the generation they were issued in; +one from an older generation, or for another operation, is dropped. Any pair +not listed is dropped. + +Two timers run independently. The management timer covers login, reconnect +and logout timeouts and retries. The reconnect window (management timeout + +1 s) starts at a reset with a login held and restarts on every further reset, +as Apple's `startReconnectTimer` does from `suspendedNotify` (A:2067-2077, +A:2202-2235). A held login that has not reconnected when it closes is lost. + +| From | Event | To | Effects | Reference | +|---|---|---|---|---| +| Idle, Failed | Login | LoggingIn(now) | write login, login timeout | Apple `submitLogin` A:1463-1533 | +| Failed | bus reset | Failed | release | | +| LoggingIn | write refused, resources unavailable | Failed | cancel timer | | +| LoggingIn | write ACKed | LoggingIn | login timeout | | +| LoggingIn | write failed, status error, unreadable response | LoggingIn(retries+1) | 1 s retry; Failed + report at 32 | Apple `completeLogin` A:1703-1710 | +| LoggingIn | login timeout | LoggingIn(now, retries+1) | write login; Failed + report at 32 | | +| LoggingIn | retry due | LoggingIn(now) | write login | | +| LoggingIn | accepted, physical DMA on | LoggedIn | bind, busy timeout, report up | Apple A:1965-2033; Linux `sbp2_login` L:833-886 | +| LoggingIn | accepted, physical DMA refused | SuspendedHeld | open window | Linux enables at login L:1146 | +| LoggingIn | bus reset | SuspendedNoLogin | cancel timer, unbind, release | | +| LoggedIn | bus reset | SuspendedHeld | cancel timer, unbind, release, open window | Apple `suspendedNotify` A:2067 | +| LoggedIn | Logout | LoggingOut(route) | unbind, write logout, timeout | Apple `executeLogout` A:2664-2682 | +| SuspendedHeld | Resume | Reconnecting(now) | write reconnect | Apple `resumeNotify` A:2121-2125, 2278; Linux `sbp2_reconnect` L:917-924 | +| SuspendedNoLogin | Resume, Login | LoggingIn(now) | write login | | +| SuspendedHeld | Logout | LoggingOut(now) | close window, unbind, write logout, timeout | see *Where the references disagree* | +| SuspendedHeld | window closed | SuspendedNoLogin | report session lost | Apple `reconnectTimeout` A:2395-2413 | +| SuspendedNoLogin | Logout | Idle | report logout done, nothing on the wire | | +| Suspended* | device gone | Failed | close window, report session lost | Apple drops the login on terminate A:2158-2196 | +| SuspendedHeld | bus reset | SuspendedHeld | track generation, restart window | Apple A:2072-2077 | +| SuspendedNoLogin | bus reset | SuspendedNoLogin | track generation | | +| Reconnecting | resources unavailable | Failed | cancel timer | | +| Reconnecting | write refused or failed | Reconnecting | 1 s, then as a timeout | Apple A:2285 | +| Reconnecting | write ACKed | Reconnecting | (status due before the window closes) | Apple A:2349-2362 | +| Reconnecting | accepted, physical DMA on | LoggedIn | close window, bind, busy timeout, report up | Apple A:2458-2496; Linux L:944-954 | +| Reconnecting | accepted, physical DMA refused | SuspendedHeld | open window | Linux enables at reconnect L:1198 | +| Reconnecting | rejected: request complete, unspecified error | Reconnecting | resend after 100 ms | Apple A:2498-2503, 589 | +| Reconnecting | resend due | Reconnecting(now) | write reconnect | Apple `reconnectRetryTimeout` → `doReconnect` | +| Reconnecting | rejected otherwise, retry timeout, window closed | LoggingIn(now) | close window, report session lost, then write login | Apple ReconnectFailed A:2504-2519, 2395-2413 → client logs in T:794-806 | +| Reconnecting | bus reset | SuspendedHeld | cancel timer, unbind, release, restart window | Apple A:2072-2077 | +| LoggingOut | write refused, resources unavailable | Failed | cancel timer | | +| LoggingOut | write ACKed | LoggingOut | logout timeout | | +| LoggingOut | status, write failed, timeout | Idle | report logout done (0 / -1 / -2) | Apple A:1916-1944, 2730-2757 | +| LoggingOut | bus reset | (ignored; the old-generation write fails or times out) | | Apple `suspendedNotify` A:2088-2092 | + +A = `references/IOFireWireSBP2/Source/IOFireWireSBP2/IOFireWireSBP2Login.cpp`, +T = `IOFireWireSerialBusProtocolTransport.cpp`, +L = `references/linux-ohci-firewire-low-level-stack/sbp2.c`. + +## Threading + +Timers, write completions, status blocks and bus resets arrive on the driver +work queue, and the HBA pumps its commands there too. The probe user client +calls `StartLogin`/`ReleaseSession` from its own queue (no `QUEUENAME`, no hop). +So every access to the state goes through `LoginSession::Serialized()`: a queue +of work run one piece at a time by whichever thread finds it idle. Its lock +covers only the queue; the work itself runs unlocked, so a callback may take +the registry's lock without an AB-BA against `SessionRegistry::OnBusReset`, +which calls in under that lock. Work raised while other work runs (an inline +write completion, a callback that calls back in, a call from another queue) is +queued behind it: run-to-completion. A request queued behind another thread's +work returns true before it has run. + +Readers on other queues (the HBA through `SessionRegistry::GetSessionState`, +the user client at `SBP2Handler.hpp:227`, `SubmitORB`) use +`LoginSession::Snapshot()`: state, generation and login ID packed in one atomic +word, so a reader never sees the ID of one transition with the state of +another. + +The user-client path still drives `CommandExecutor` and `FetchAgent` from its +own queue, as before this change; only the login state is serialized here. + +## Behaviour changes in this refactor + +- After a reset, a login with login_ID 0 reconnects instead of logging in again. +- A logout after a reset that interrupted the login completes without a write. + It used to send a logout ORB with login_ID 0. +- A logout from Suspended allocates the management address spaces again. They + were released by the reset, so the ORB pointer used to address nothing. +- Login is accepted only from Idle, Failed and SuspendedNoLogin. The registry + already started logins only from Idle. +- A write that completes inline no longer has its retry timer overwritten by + the timeout armed after it. +- A timer that fires after it was cancelled or re-armed is dropped. +- `Resume()` from LoggedIn is dropped; the old `Reconnect()` accepted it. There + was no caller. +- The reported generation follows every reset: in Suspended, Failed, and in + Idle after a reset during logout. It used to keep the first reset's value, + so a logout from Suspended after two resets went out at a stale generation. +- The reported login ID is 0 outside the states that hold a login. It used to + keep a stale ID, for example in Failed after a fallback login failed. +- `LoginCompleteParams` no longer carries the status block. Nothing read it. + +## Where the references disagree + +**Logout while suspended** follows Linux. Linux sends the logout at once, to +the device's current node (`sbp2_remove`, L:1221-1243). Apple sets +`fLogoutPending` instead (A:2639-2655), but in the Reconnect state that path +only sets the flag again, and the reconnect timeout calls `executeLogout` +with the flag still set, which returns busy (A:2409-2410, 2634). No logout +goes out and the client is never answered; +the target drops the login when its reconnect hold expires. Until rediscovery +the node we send to is the one from the last `Configure()`. diff --git a/tests/protocols/CMakeLists.txt b/tests/protocols/CMakeLists.txt index 3e0b1262..33340f44 100644 --- a/tests/protocols/CMakeLists.txt +++ b/tests/protocols/CMakeLists.txt @@ -321,6 +321,11 @@ add_protocols_test(FetchAgentTests "${ASFW_DRIVER_DIR}/Protocols/SBP2/SBP2CommandORB.cpp" ) +# SBP2 LoginStateMachine Tests (pure transition table) +add_protocols_test(LoginStateMachineTests + LoginStateMachineTests.cpp +) + # SBP2 LoginSession Tests (login/reconnect/logout state machine + status routing) add_protocols_test(LoginSessionTests LoginSessionTests.cpp diff --git a/tests/protocols/LoginSessionTests.cpp b/tests/protocols/LoginSessionTests.cpp index 1a3d060c..eb895b45 100644 --- a/tests/protocols/LoginSessionTests.cpp +++ b/tests/protocols/LoginSessionTests.cpp @@ -5,6 +5,11 @@ #include "tests/mocks/DeferredFireWireBus.hpp" #include "FakeSessionScheduler.hpp" +#include +#include +#include +#include + #include #include #include @@ -270,7 +275,7 @@ TEST(LoginSessionTests, ReconnectReplaysBusyTimeoutRegister) { rig.bus.SetGeneration(ASFW::FW::Generation{2}); rig.session.HandleBusReset(2); ASSERT_EQ(LoginState::Suspended, rig.session.State()); - ASSERT_TRUE(rig.session.Reconnect()); + ASSERT_TRUE(rig.session.Resume()); ASSERT_EQ(LoginState::Reconnecting, rig.session.State()); const size_t reconnectWriteIndex = rig.bus.WriteCount() - 1; @@ -300,7 +305,7 @@ TEST(LoginSessionTests, ReconnectAckStartsStatusTimeoutAndFallsBackToLogin) { rig.bus.SetGeneration(ASFW::FW::Generation{2}); rig.session.HandleBusReset(2); ASSERT_EQ(LoginState::Suspended, rig.session.State()); - ASSERT_TRUE(rig.session.Reconnect()); + ASSERT_TRUE(rig.session.Resume()); ASSERT_EQ(LoginState::Reconnecting, rig.session.State()); const size_t reconnectWriteIndex = rig.bus.WriteCount() - 1; @@ -335,7 +340,7 @@ TEST(LoginSessionTests, FailedReconnectWriteEmitsSessionLostThenFallbackLogin) { rig.bus.SetGeneration(ASFW::FW::Generation{2}); rig.session.HandleBusReset(2); - ASSERT_TRUE(rig.session.Reconnect()); + ASSERT_TRUE(rig.session.Resume()); const size_t reconnectWriteIndex = rig.bus.WriteCount() - 1; ASSERT_TRUE(rig.bus.CompleteWrite(rig.bus.WriteAt(reconnectWriteIndex).handle, ASFW::Async::AsyncStatus::kTimeout)); @@ -359,7 +364,7 @@ TEST(LoginSessionTests, ReconnectRejectionEmitsSessionLostThenFreshLoginUpEdge) rig.bus.SetGeneration(ASFW::FW::Generation{2}); rig.session.HandleBusReset(2); - ASSERT_TRUE(rig.session.Reconnect()); + ASSERT_TRUE(rig.session.Resume()); const size_t reconnectWriteIndex = rig.bus.WriteCount() - 1; const uint64_t reconnectOrbAddress = @@ -421,6 +426,52 @@ TEST(LoginSessionTests, ReconnectRejectionEmitsSessionLostThenFreshLoginUpEdge) EXPECT_EQ(LoginState::LoggedIn, rig.session.State()); } +// Apple treats "request complete, unspecified error" as transient and sends the +// reconnect again after 100 ms (IOFireWireSBP2Login.cpp:2498-2503); the login +// is not lost. +TEST(LoginSessionTests, ReconnectUnspecifiedErrorResendsInsteadOfLoggingIn) { + SessionRig rig; + rig.LoginSuccessfully(); + std::vector statuses; + rig.session.SetLoginCallback( + [&statuses](const ASFW::Protocols::SBP2::LoginCompleteParams& params) { + statuses.push_back(params.status); + }); + rig.bus.SetGeneration(ASFW::FW::Generation{2}); + rig.session.HandleBusReset(2); + ASSERT_TRUE(rig.session.Resume()); + + const size_t reconnectWriteIndex = rig.bus.WriteCount() - 1; + const uint64_t reconnectStatusAddress = ReadORBAddress( + rig.addressManager, DecodeOrbAddressFromPayload(rig.bus.WriteAt(reconnectWriteIndex).data), + offsetof(ReconnectORB, statusFIFOAddressHi), offsetof(ReconnectORB, statusFIFOAddressLo)); + ASSERT_TRUE(rig.bus.CompleteWrite(rig.bus.WriteAt(reconnectWriteIndex).handle, + ASFW::Async::AsyncStatus::kSuccess)); + StatusBlock unspecified{}; + unspecified.sbpStatus = SBPStatus::kUnspecifiedError; + rig.addressManager.ApplyRemoteWrite( + reconnectStatusAddress, + std::span{reinterpret_cast(&unspecified), sizeof(unspecified)}); + + EXPECT_EQ(LoginState::Reconnecting, rig.session.State()); + rig.AdvanceMs(99); + EXPECT_EQ(reconnectWriteIndex + 1, rig.bus.WriteCount()); + rig.AdvanceMs(1); + ASSERT_EQ(reconnectWriteIndex + 2, rig.bus.WriteCount()); + const auto& resend = rig.bus.WriteAt(reconnectWriteIndex + 1); + ASSERT_TRUE(rig.bus.CompleteWrite(resend.handle, ASFW::Async::AsyncStatus::kSuccess)); + + StatusBlock good{}; + good.sbpStatus = SBPStatus::kNoAdditionalInfo; + rig.addressManager.ApplyRemoteWrite( + reconnectStatusAddress, + std::span{reinterpret_cast(&good), sizeof(good)}); + + EXPECT_EQ(LoginState::LoggedIn, rig.session.State()); + ASSERT_EQ(1u, statuses.size()); + EXPECT_EQ(0, statuses[0]); +} + TEST(LoginSessionTests, ReconnectTimeoutEmitsSessionLostBeforeFallbackLogin) { SessionRig rig; rig.LoginSuccessfully(); @@ -433,7 +484,7 @@ TEST(LoginSessionTests, ReconnectTimeoutEmitsSessionLostBeforeFallbackLogin) { rig.bus.SetGeneration(ASFW::FW::Generation{2}); rig.session.HandleBusReset(2); - ASSERT_TRUE(rig.session.Reconnect()); + ASSERT_TRUE(rig.session.Resume()); const size_t reconnectWriteIndex = rig.bus.WriteCount() - 1; ASSERT_TRUE(rig.bus.CompleteWrite(rig.bus.WriteAt(reconnectWriteIndex).handle, ASFW::Async::AsyncStatus::kSuccess)); @@ -445,6 +496,50 @@ TEST(LoginSessionTests, ReconnectTimeoutEmitsSessionLostBeforeFallbackLogin) { EXPECT_EQ(LoginState::LoggingIn, rig.session.State()); } +// The reconnect window runs from the reset (Apple suspendedNotify → +// startReconnectTimer): a login not rediscovered in time is reported lost, and +// the rediscovery that follows logs in afresh. +TEST(LoginSessionTests, ReconnectWindowClosingBeforeRediscoveryLosesTheLogin) { + SessionRig rig; + rig.LoginSuccessfully(); + std::vector statuses; + rig.session.SetLoginCallback( + [&statuses](const ASFW::Protocols::SBP2::LoginCompleteParams& params) { + statuses.push_back(params.status); + }); + rig.bus.SetGeneration(ASFW::FW::Generation{2}); + rig.session.HandleBusReset(2); + const size_t writesBefore = rig.bus.WriteCount(); + + rig.AdvanceMs(1009); + EXPECT_TRUE(statuses.empty()); + rig.AdvanceMs(1); + ASSERT_EQ(1u, statuses.size()); + EXPECT_EQ(-3, statuses[0]); + EXPECT_EQ(LoginState::Suspended, rig.session.State()); + EXPECT_FALSE(rig.session.Snapshot().loginID.has_value()); + EXPECT_EQ(writesBefore, rig.bus.WriteCount()); + + ASSERT_TRUE(rig.session.Resume()); + EXPECT_EQ(LoginState::LoggingIn, rig.session.State()); + EXPECT_EQ(writesBefore + 1, rig.bus.WriteCount()); +} + +// A second reset restarts the window rather than shortening it. +TEST(LoginSessionTests, AnotherResetRestartsTheReconnectWindow) { + SessionRig rig; + rig.LoginSuccessfully(); + rig.bus.SetGeneration(ASFW::FW::Generation{2}); + rig.session.HandleBusReset(2); + rig.AdvanceMs(800); + rig.bus.SetGeneration(ASFW::FW::Generation{3}); + rig.session.HandleBusReset(3); + rig.AdvanceMs(800); + + ASSERT_TRUE(rig.session.Resume()); + EXPECT_EQ(LoginState::Reconnecting, rig.session.State()); +} + TEST(LoginSessionTests, BusyTimeoutReplayCancelsInFlightWrite) { SessionRig rig; @@ -453,7 +548,7 @@ TEST(LoginSessionTests, BusyTimeoutReplayCancelsInFlightWrite) { rig.bus.SetGeneration(ASFW::FW::Generation{2}); rig.session.HandleBusReset(2); - ASSERT_TRUE(rig.session.Reconnect()); + ASSERT_TRUE(rig.session.Resume()); const size_t reconnectWriteIndex = rig.bus.WriteCount() - 1; const uint64_t reconnectOrbAddress = @@ -491,7 +586,7 @@ TEST(LoginSessionTests, BusResetWhileLoggingInWaitsForDiscoveryBeforeRetry) { ASSERT_TRUE(rig.session.Login()); EXPECT_EQ(2u, rig.bus.WriteCount()); EXPECT_EQ(3u, rig.bus.WriteAt(1).nodeId.value); - EXPECT_EQ(2u, rig.session.Generation()); + EXPECT_EQ(2u, rig.session.Snapshot().generation); } TEST(LoginSessionTests, BusResetWhileReconnectingWaitsForFreshNodeBeforeRetry) { @@ -499,7 +594,7 @@ TEST(LoginSessionTests, BusResetWhileReconnectingWaitsForFreshNodeBeforeRetry) { rig.LoginSuccessfully(); rig.bus.SetGeneration(ASFW::FW::Generation{2}); rig.session.HandleBusReset(2); - ASSERT_TRUE(rig.session.Reconnect()); + ASSERT_TRUE(rig.session.Resume()); rig.bus.SetGeneration(ASFW::FW::Generation{3}); rig.session.HandleBusReset(3); EXPECT_EQ(LoginState::Suspended, rig.session.State()); @@ -508,10 +603,132 @@ TEST(LoginSessionTests, BusResetWhileReconnectingWaitsForFreshNodeBeforeRetry) { auto info = rig.session.TargetInfo(); info.targetNodeId = 3; rig.session.Configure(info); - ASSERT_TRUE(rig.session.Reconnect()); + ASSERT_TRUE(rig.session.Resume()); EXPECT_EQ(4u, rig.bus.WriteCount()); EXPECT_EQ(3u, rig.bus.WriteAt(3).nodeId.value); - EXPECT_EQ(3u, rig.session.Generation()); + EXPECT_EQ(3u, rig.session.Snapshot().generation); +} + +// A reset before the login completed leaves no login to log out of: logout +// completes at once and puts nothing on the wire (it used to send login_ID 0). +TEST(LoginSessionTests, LogoutAfterInterruptedLoginSendsNothing) { + SessionRig rig; + std::vector logoutStatuses; + rig.session.SetLogoutCallback( + [&](const ASFW::Protocols::SBP2::LogoutCompleteParams& p) { logoutStatuses.push_back(p.status); }); + ASSERT_TRUE(rig.session.Login()); + rig.session.HandleBusReset(2); + const size_t writesBefore = rig.bus.WriteCount(); + + ASSERT_TRUE(rig.session.Logout()); + + EXPECT_EQ(writesBefore, rig.bus.WriteCount()); + EXPECT_EQ(LoginState::Idle, rig.session.State()); + ASSERT_EQ(1u, logoutStatuses.size()); + EXPECT_EQ(0, logoutStatuses[0]); +} + +// The reset released the management address spaces; a logout from Suspended +// must allocate them again, or its ORB pointer addresses nothing. +TEST(LoginSessionTests, LogoutWhileSuspendedPointsAtAnAllocatedORB) { + SessionRig rig; + rig.LoginSuccessfully(); + rig.bus.SetGeneration(ASFW::FW::Generation{2}); + rig.session.HandleBusReset(2); + + ASSERT_TRUE(rig.session.Logout()); + ASSERT_EQ(LoginState::LoggingOut, rig.session.State()); + + const uint64_t logoutOrbAddress = + DecodeOrbAddressFromPayload(rig.bus.WriteAt(rig.bus.WriteCount() - 1).data); + EXPECT_NE(0u, logoutOrbAddress & 0xFFFF'FFFF'FFFFull); + uint32_t quadlet = 0; + EXPECT_EQ(ASFW::Async::ResponseCode::Complete, + rig.addressManager.ReadQuadlet(logoutOrbAddress, &quadlet)); +} + +void DeliverLogoutStatus(SessionRig& rig) { + StatusBlock status{}; + status.sbpStatus = SBPStatus::kNoAdditionalInfo; + rig.addressManager.ApplyRemoteWrite( + rig.sessionStatusAddress, + std::span{reinterpret_cast(&status), sizeof(status)}); +} + +// A callback that calls back in runs after the transition that raised it. +TEST(LoginSessionTests, LoginFromLogoutCallbackRunsAfterTheLogoutCompletes) { + SessionRig rig; + rig.LoginSuccessfully(); + std::optional loginAccepted; + LoginState stateInCallback = LoginState::Failed; + rig.session.SetLogoutCallback([&](const ASFW::Protocols::SBP2::LogoutCompleteParams&) { + stateInCallback = rig.session.State(); + loginAccepted = rig.session.Login(); + }); + ASSERT_TRUE(rig.session.Logout()); + const size_t writesBefore = rig.bus.WriteCount(); + + DeliverLogoutStatus(rig); + + EXPECT_EQ(LoginState::Idle, stateInCallback); + ASSERT_TRUE(loginAccepted.has_value()); + EXPECT_TRUE(*loginAccepted); + EXPECT_EQ(LoginState::LoggingIn, rig.session.State()); + EXPECT_EQ(writesBefore + 1, rig.bus.WriteCount()); +} + +// The probe user client calls in from its own queue. A request made while the +// driver queue is inside a transition is queued, not run alongside it. +TEST(LoginSessionTests, RequestFromAnotherThreadQueuesBehindRunningWork) { + SessionRig rig; + rig.LoginSuccessfully(); + std::promise inCallback; + std::promise release; + auto released = release.get_future().share(); + rig.session.SetLogoutCallback([&](const ASFW::Protocols::SBP2::LogoutCompleteParams&) { + inCallback.set_value(); + released.wait(); + }); + ASSERT_TRUE(rig.session.Logout()); + const size_t writesBefore = rig.bus.WriteCount(); + + std::thread driverQueue([&] { DeliverLogoutStatus(rig); }); + ASSERT_EQ(std::future_status::ready, + inCallback.get_future().wait_for(std::chrono::seconds(5))); + + auto login = std::async(std::launch::async, [&] { return rig.session.Login(); }); + const auto queued = login.wait_for(std::chrono::seconds(5)); + const size_t writesWhileHeld = rig.bus.WriteCount(); + release.set_value(); + driverQueue.join(); + + ASSERT_EQ(std::future_status::ready, queued); + EXPECT_TRUE(login.get()); + EXPECT_EQ(writesBefore, writesWhileHeld); + EXPECT_EQ(writesBefore + 1, rig.bus.WriteCount()); + EXPECT_EQ(LoginState::LoggingIn, rig.session.State()); +} + +// A reset during logout leaves the logout to finish on its own: the write of +// the old generation fails and reports it (Apple ignores the suspend there). +TEST(LoginSessionTests, BusResetWhileLoggingOutStillReportsTheLogout) { + SessionRig rig; + rig.LoginSuccessfully(); + std::vector logoutStatuses; + rig.session.SetLogoutCallback( + [&](const ASFW::Protocols::SBP2::LogoutCompleteParams& p) { logoutStatuses.push_back(p.status); }); + ASSERT_TRUE(rig.session.Logout()); + const auto logoutWrite = rig.bus.WriteAt(rig.bus.WriteCount() - 1).handle; + + rig.bus.SetGeneration(ASFW::FW::Generation{2}); + rig.session.HandleBusReset(2); + EXPECT_EQ(LoginState::LoggingOut, rig.session.State()); + EXPECT_TRUE(logoutStatuses.empty()); + + ASSERT_TRUE(rig.bus.CompleteWrite(logoutWrite, ASFW::Async::AsyncStatus::kTimeout)); + EXPECT_EQ(LoginState::Idle, rig.session.State()); + ASSERT_EQ(1u, logoutStatuses.size()); + EXPECT_EQ(-1, logoutStatuses[0]); } TEST(LoginSessionTests, ImmediateORBRetryStaysBoundToOriginalORBAndQueuesNextImmediate) { @@ -724,7 +941,7 @@ TEST(LoginSessionTests, PhysicalDmaIsEnabledOnLoginAndReconnect) { auto refreshed = rig.session.TargetInfo(); refreshed.targetNodeId = kTargetNodeID + 1; rig.session.Configure(refreshed); - ASSERT_TRUE(rig.session.Reconnect()); + ASSERT_TRUE(rig.session.Resume()); const size_t reconnectWriteIndex = rig.bus.WriteCount() - 1; const uint64_t reconnectOrbAddress = DecodeOrbAddressFromPayload(rig.bus.WriteAt(reconnectWriteIndex).data); diff --git a/tests/protocols/LoginStateMachineTests.cpp b/tests/protocols/LoginStateMachineTests.cpp new file mode 100644 index 00000000..bc064804 --- /dev/null +++ b/tests/protocols/LoginStateMachineTests.cpp @@ -0,0 +1,407 @@ +// LoginStateMachine — one test per transition in documentation/sbp2-login-fsm.md. +// Transition() is pure, so these need no bus, scheduler or address space. + +#include + +#include "ASFWDriver/Protocols/SBP2/Session/LoginStateMachine.hpp" + +namespace { + +using namespace ASFW::Protocols::SBP2; +using namespace ASFW::Protocols::SBP2::LoginFsm; + +constexpr Route kOld{1, 0xFFC2}; +constexpr Route kNow{2, 0xFFC3}; + +HeldLogin MakeLogin(uint16_t id) { + HeldLogin login{}; + login.id = id; + login.commandBlockAgentHi = 0xFFFF; + login.commandBlockAgentLo = 0xF0010000; + return login; +} + +Step Expect(const State& state, const Event& event, const Route& now = kNow) { + auto step = Transition(state, event, now); + EXPECT_TRUE(step.has_value()); + return step.value_or(Step{state, {}}); +} + +void ExpectIgnored(const State& state, const Event& event) { + EXPECT_FALSE(Transition(state, event, kNow).has_value()); +} + +template const S& As(const Step& step) { + EXPECT_TRUE(std::holds_alternative(step.next)); + static const S fallback{}; + const auto* s = std::get_if(&step.next); + return s ? *s : fallback; +} + +void ExpectRoute(const Route& actual, const Route& expected) { + EXPECT_EQ(expected.generation, actual.generation); + EXPECT_EQ(expected.node, actual.node); +} + +// Idle / Failed -------------------------------------------------------------- + +TEST(LoginStateMachineTests, IdleLoginStartsLoginAtCurrentRoute) { + const Step step = Expect(St::Idle{}, Ev::Login{}); + ExpectRoute(As(step).route, kNow); + EXPECT_EQ(Op::Login, step.fx.write); + EXPECT_EQ(Timer::LoginTimeout, step.fx.arm); +} + +TEST(LoginStateMachineTests, FailedLoginStartsWithFreshRetryBudget) { + const Step step = Expect(St::Failed{}, Ev::Login{}); + EXPECT_EQ(0u, As(step).retries); + EXPECT_EQ(Op::Login, step.fx.write); +} + +TEST(LoginStateMachineTests, FailedBusResetReleasesResources) { + const Step step = Expect(St::Failed{1}, Ev::BusReset{2}); + EXPECT_EQ(2u, As(step).generation); + EXPECT_TRUE(step.fx.release); +} + +TEST(LoginStateMachineTests, IdleIgnoresEverythingButLogin) { + ExpectIgnored(St::Idle{}, Ev::Logout{}); + ExpectIgnored(St::Idle{}, Ev::Resume{}); + ExpectIgnored(St::Idle{}, Ev::BusReset{2}); + ExpectIgnored(St::Idle{}, Ev::DeviceGone{}); +} + +// LoggingIn ------------------------------------------------------------------ + +TEST(LoginStateMachineTests, LoggingInResourcesUnavailableFails) { + const Step step = Expect(St::LoggingIn{kOld}, Ev::ResourcesUnavailable{}); + As(step); + EXPECT_EQ(Notify::None, step.fx.notify); +} + +TEST(LoginStateMachineTests, LoggingInRefusedWriteFails) { + const Step step = Expect(St::LoggingIn{kOld}, Ev::WriteRefused{Op::Login}); + As(step); + EXPECT_TRUE(step.fx.cancelTimer); +} + +TEST(LoginStateMachineTests, LoggingInAckedWriteWaitsForStatus) { + const Step step = Expect(St::LoggingIn{kOld}, Ev::WriteDone{Op::Login, kOld.generation, true}); + ExpectRoute(As(step).route, kOld); + EXPECT_EQ(Timer::LoginTimeout, step.fx.arm); + EXPECT_FALSE(step.fx.write.has_value()); +} + +TEST(LoginStateMachineTests, LoggingInFailedWriteBacksOff) { + const Step step = Expect(St::LoggingIn{kOld, 3}, Ev::WriteDone{Op::Login, kOld.generation, false}); + EXPECT_EQ(4u, As(step).retries); + EXPECT_EQ(Timer::LoginRetry, step.fx.arm); +} + +TEST(LoginStateMachineTests, LoggingInFailedWriteWithNoRetriesLeftFails) { + const Step step = Expect(St::LoggingIn{kOld, kLoginRetryMax}, + Ev::WriteDone{Op::Login, kOld.generation, false}); + As(step); + EXPECT_EQ(Notify::LoginFailed, step.fx.notify); + EXPECT_EQ(-1, step.fx.status); +} + +TEST(LoginStateMachineTests, LoggingInDropsStaleOrForeignWriteCompletions) { + ExpectIgnored(St::LoggingIn{kOld}, Ev::WriteDone{Op::Login, 7, false}); + ExpectIgnored(St::LoggingIn{kOld}, Ev::WriteDone{Op::Logout, kOld.generation, false}); + ExpectIgnored(St::LoggingIn{kOld}, Ev::Timeout{Op::Reconnect}); +} + +TEST(LoginStateMachineTests, LoggingInTimeoutLogsInAgainAtCurrentRoute) { + const Step step = Expect(St::LoggingIn{kOld, 2}, Ev::Timeout{Op::Login}); + const auto& next = As(step); + ExpectRoute(next.route, kNow); + EXPECT_EQ(3u, next.retries); + EXPECT_EQ(Op::Login, step.fx.write); +} + +TEST(LoginStateMachineTests, LoggingInTimeoutWithNoRetriesLeftFails) { + const Step step = Expect(St::LoggingIn{kOld, kLoginRetryMax}, Ev::Timeout{Op::Login}); + As(step); + EXPECT_EQ(-2, step.fx.status); +} + +TEST(LoginStateMachineTests, LoggingInRetryDueLogsInAtCurrentRoute) { + const Step step = Expect(St::LoggingIn{kOld, 4}, Ev::LoginRetryDue{}); + const auto& next = As(step); + ExpectRoute(next.route, kNow); + EXPECT_EQ(4u, next.retries); + EXPECT_EQ(Op::Login, step.fx.write); +} + +TEST(LoginStateMachineTests, LoggingInAcceptedBindsAndReportsUp) { + const Step step = Expect(St::LoggingIn{kOld}, Ev::LoginAccepted{MakeLogin(0x42), true}); + const auto& next = As(step); + EXPECT_EQ(0x42u, next.login.id); + ExpectRoute(next.route, kOld); + EXPECT_EQ(Op::Login, step.fx.bind); + EXPECT_EQ(Notify::LoggedIn, step.fx.notify); + EXPECT_EQ(0, step.fx.status); + EXPECT_TRUE(step.fx.cancelWindow); +} + +TEST(LoginStateMachineTests, LoggingInAcceptedWithoutPhysicalDmaHoldsLoginSuspended) { + const Step step = Expect(St::LoggingIn{kOld}, Ev::LoginAccepted{MakeLogin(0x42), false}); + EXPECT_EQ(0x42u, As(step).login.id); + EXPECT_EQ(Notify::None, step.fx.notify); +} + +TEST(LoginStateMachineTests, LoggingInRejectedBacksOffThenFails) { + EXPECT_EQ(Timer::LoginRetry, Expect(St::LoggingIn{kOld}, Ev::LoginRejected{}).fx.arm); + const Step last = Expect(St::LoggingIn{kOld, kLoginRetryMax}, Ev::LoginRejected{}); + As(last); + EXPECT_EQ(-1, last.fx.status); +} + +TEST(LoginStateMachineTests, LoggingInBusResetSuspendsWithoutLogin) { + const Step step = Expect(St::LoggingIn{kOld, 5}, Ev::BusReset{2}); + EXPECT_EQ(2u, As(step).generation); + EXPECT_TRUE(step.fx.release); + EXPECT_FALSE(step.fx.write.has_value()); +} + +TEST(LoginStateMachineTests, LoggingInIgnoresLogout) { + ExpectIgnored(St::LoggingIn{kOld}, Ev::Logout{}); +} + +// LoggedIn ------------------------------------------------------------------- + +TEST(LoginStateMachineTests, LoggedInBusResetHoldsLogin) { + const Step step = Expect(St::LoggedIn{MakeLogin(0x42), kOld}, Ev::BusReset{2}); + const auto& next = As(step); + EXPECT_EQ(0x42u, next.login.id); + EXPECT_EQ(2u, next.generation); + EXPECT_TRUE(step.fx.unbind); + EXPECT_TRUE(step.fx.release); + EXPECT_TRUE(step.fx.armWindow); +} + +TEST(LoginStateMachineTests, LoggedInLogoutWritesLogout) { + const Step step = Expect(St::LoggedIn{MakeLogin(0x42), kOld}, Ev::Logout{}); + ExpectRoute(As(step).route, kOld); + EXPECT_TRUE(step.fx.unbind); + EXPECT_EQ(Op::Logout, step.fx.write); + EXPECT_EQ(Timer::LogoutTimeout, step.fx.arm); +} + +TEST(LoginStateMachineTests, LoggedInIgnoresLoginAndResume) { + ExpectIgnored(St::LoggedIn{MakeLogin(1), kOld}, Ev::Login{}); + ExpectIgnored(St::LoggedIn{MakeLogin(1), kOld}, Ev::Resume{}); +} + +// Reconnecting --------------------------------------------------------------- + +TEST(LoginStateMachineTests, ReconnectingResourcesUnavailableFails) { + As(Expect(St::Reconnecting{MakeLogin(1), kNow}, Ev::ResourcesUnavailable{})); +} + +TEST(LoginStateMachineTests, ReconnectingRefusedWriteRetriesThroughTimeout) { + const Step step = Expect(St::Reconnecting{MakeLogin(1), kNow}, Ev::WriteRefused{Op::Reconnect}); + As(step); + EXPECT_EQ(Timer::ReconnectRetry, step.fx.arm); +} + +// The window from the reset bounds the wait for status; an ACK arms nothing. +TEST(LoginStateMachineTests, ReconnectingWriteCompletion) { + const St::Reconnecting s{MakeLogin(1), kNow}; + const Step acked = Expect(s, Ev::WriteDone{Op::Reconnect, kNow.generation, true}); + EXPECT_FALSE(acked.fx.arm.has_value()); + EXPECT_FALSE(acked.fx.armWindow); + EXPECT_EQ(Timer::ReconnectRetry, + Expect(s, Ev::WriteDone{Op::Reconnect, kNow.generation, false}).fx.arm); + ExpectIgnored(s, Ev::WriteDone{Op::Reconnect, kOld.generation, true}); +} + +TEST(LoginStateMachineTests, ReconnectingWindowClosedReportsLostThenLogsIn) { + const Step step = Expect(St::Reconnecting{MakeLogin(1), kOld}, Ev::ReconnectWindowClosed{}); + ExpectRoute(As(step).route, kNow); + EXPECT_TRUE(step.fx.sessionLostFirst); + EXPECT_EQ(Op::Login, step.fx.write); +} + +TEST(LoginStateMachineTests, ReconnectingTimeoutReportsLostThenLogsIn) { + const Step step = Expect(St::Reconnecting{MakeLogin(1), kOld}, Ev::Timeout{Op::Reconnect}); + ExpectRoute(As(step).route, kNow); + EXPECT_TRUE(step.fx.sessionLostFirst); + EXPECT_EQ(kOld.generation, step.fx.generation); + EXPECT_EQ(Op::Login, step.fx.write); +} + +TEST(LoginStateMachineTests, ReconnectingAcceptedBindsAndReportsUp) { + const Step step = Expect(St::Reconnecting{MakeLogin(0x42), kNow}, Ev::ReconnectAccepted{true}); + const auto& next = As(step); + EXPECT_EQ(0x42u, next.login.id); + ExpectRoute(next.route, kNow); + EXPECT_EQ(Op::Reconnect, step.fx.bind); + EXPECT_EQ(Notify::LoggedIn, step.fx.notify); + EXPECT_TRUE(step.fx.cancelWindow); +} + +TEST(LoginStateMachineTests, ReconnectingAcceptedWithoutPhysicalDmaStaysSuspended) { + const Step step = Expect(St::Reconnecting{MakeLogin(0x42), kNow}, Ev::ReconnectAccepted{false}); + EXPECT_EQ(0x42u, As(step).login.id); +} + +TEST(LoginStateMachineTests, ReconnectingRejectedReportsLostThenLogsIn) { + const Step step = Expect(St::Reconnecting{MakeLogin(1), kOld}, Ev::ReconnectRejected{}); + As(step); + EXPECT_TRUE(step.fx.sessionLostFirst); + EXPECT_EQ(Op::Login, step.fx.write); +} + +TEST(LoginStateMachineTests, ReconnectingUnspecifiedErrorResendsAfterABackoff) { + const Step step = Expect(St::Reconnecting{MakeLogin(0x42), kOld}, Ev::ReconnectRejected{true}); + ExpectRoute(As(step).route, kOld); + EXPECT_EQ(Timer::ReconnectResend, step.fx.arm); + EXPECT_FALSE(step.fx.sessionLostFirst); + EXPECT_FALSE(step.fx.cancelWindow); +} + +TEST(LoginStateMachineTests, ReconnectingResendGoesToTheCurrentRoute) { + const Step step = Expect(St::Reconnecting{MakeLogin(0x42), kOld}, Ev::ReconnectRetryDue{}); + const auto& next = As(step); + EXPECT_EQ(0x42u, next.login.id); + ExpectRoute(next.route, kNow); + EXPECT_EQ(Op::Reconnect, step.fx.write); +} + +TEST(LoginStateMachineTests, ReconnectingBusResetKeepsLoginHeld) { + const Step step = Expect(St::Reconnecting{MakeLogin(0x42), kNow}, Ev::BusReset{3}); + EXPECT_EQ(0x42u, As(step).login.id); + EXPECT_TRUE(step.fx.release); + EXPECT_TRUE(step.fx.armWindow); +} + +// SuspendedHeld -------------------------------------------------------------- + +// The H4D assigns login_ID 0; a held login reconnects whatever its ID. +TEST(LoginStateMachineTests, SuspendedHeldResumeReconnectsEvenWithLoginIdZero) { + const Step step = Expect(St::SuspendedHeld{MakeLogin(0), 2}, Ev::Resume{}); + const auto& next = As(step); + EXPECT_EQ(0u, next.login.id); + ExpectRoute(next.route, kNow); + EXPECT_EQ(Op::Reconnect, step.fx.write); + EXPECT_FALSE(step.fx.armWindow); // the window runs from the reset, not the write +} + +TEST(LoginStateMachineTests, SuspendedHeldLogoutWritesLogoutAtCurrentRoute) { + const Step step = Expect(St::SuspendedHeld{MakeLogin(0x42), 2}, Ev::Logout{}); + ExpectRoute(As(step).route, kNow); + EXPECT_EQ(Op::Logout, step.fx.write); +} + +TEST(LoginStateMachineTests, SuspendedHeldDeviceGoneReportsLost) { + const Step step = Expect(St::SuspendedHeld{MakeLogin(1), 2}, Ev::DeviceGone{}); + As(step); + EXPECT_EQ(Notify::SessionLost, step.fx.notify); + EXPECT_EQ(kStatusSessionLost, step.fx.status); +} + +TEST(LoginStateMachineTests, SuspendedHeldBusResetRestartsTheWindow) { + const Step step = Expect(St::SuspendedHeld{MakeLogin(0x42), 2}, Ev::BusReset{3}); + EXPECT_EQ(3u, As(step).generation); + EXPECT_FALSE(step.fx.write.has_value()); + EXPECT_TRUE(step.fx.armWindow); +} + +TEST(LoginStateMachineTests, SuspendedHeldWindowClosedReportsLostAndWaitsToLogIn) { + const Step step = Expect(St::SuspendedHeld{MakeLogin(0x42), 2}, Ev::ReconnectWindowClosed{}); + EXPECT_EQ(2u, As(step).generation); + EXPECT_EQ(Notify::SessionLost, step.fx.notify); + EXPECT_EQ(kStatusSessionLost, step.fx.status); + EXPECT_FALSE(step.fx.write.has_value()); +} + +TEST(LoginStateMachineTests, WindowClosingOutsideTheReconnectIsDropped) { + ExpectIgnored(St::LoggedIn{MakeLogin(1), kNow}, Ev::ReconnectWindowClosed{}); + ExpectIgnored(St::SuspendedNoLogin{2}, Ev::ReconnectWindowClosed{}); + ExpectIgnored(St::LoggingIn{kNow}, Ev::ReconnectWindowClosed{}); +} + +TEST(LoginStateMachineTests, SuspendedHeldIgnoresLogin) { + ExpectIgnored(St::SuspendedHeld{MakeLogin(1), 2}, Ev::Login{}); +} + +// SuspendedNoLogin ----------------------------------------------------------- + +TEST(LoginStateMachineTests, SuspendedNoLoginResumeLogsIn) { + const Step step = Expect(St::SuspendedNoLogin{2}, Ev::Resume{}); + ExpectRoute(As(step).route, kNow); + EXPECT_EQ(Op::Login, step.fx.write); +} + +TEST(LoginStateMachineTests, SuspendedNoLoginLoginLogsIn) { + EXPECT_EQ(Op::Login, Expect(St::SuspendedNoLogin{2}, Ev::Login{}).fx.write); +} + +// No login was granted, so there is nothing to log out of on the wire. +TEST(LoginStateMachineTests, SuspendedNoLoginLogoutCompletesWithoutWrite) { + const Step step = Expect(St::SuspendedNoLogin{2}, Ev::Logout{}); + As(step); + EXPECT_FALSE(step.fx.write.has_value()); + EXPECT_EQ(Notify::LogoutDone, step.fx.notify); + EXPECT_EQ(0, step.fx.status); +} + +TEST(LoginStateMachineTests, SuspendedNoLoginDeviceGoneReportsLost) { + EXPECT_EQ(Notify::SessionLost, Expect(St::SuspendedNoLogin{2}, Ev::DeviceGone{}).fx.notify); +} + +TEST(LoginStateMachineTests, SuspendedNoLoginBusResetTracksGeneration) { + EXPECT_EQ(3u, As(Expect(St::SuspendedNoLogin{2}, Ev::BusReset{3})).generation); +} + +// LoggingOut ----------------------------------------------------------------- + +TEST(LoginStateMachineTests, LoggingOutRefusedOrUnallocatedFails) { + const St::LoggingOut s{MakeLogin(1), kOld}; + As(Expect(s, Ev::WriteRefused{Op::Logout})); + As(Expect(s, Ev::ResourcesUnavailable{})); +} + +TEST(LoginStateMachineTests, LoggingOutWriteCompletion) { + const St::LoggingOut s{MakeLogin(1), kOld}; + EXPECT_EQ(Timer::LogoutTimeout, Expect(s, Ev::WriteDone{Op::Logout, kOld.generation, true}).fx.arm); + const Step failed = Expect(s, Ev::WriteDone{Op::Logout, kOld.generation, false}); + As(failed); + EXPECT_EQ(-1, failed.fx.status); + ExpectIgnored(s, Ev::WriteDone{Op::Logout, kNow.generation, true}); +} + +TEST(LoginStateMachineTests, LoggingOutEndsInIdle) { + const St::LoggingOut s{MakeLogin(1), kOld}; + const Step timeout = Expect(s, Ev::Timeout{Op::Logout}); + As(timeout); + EXPECT_EQ(-2, timeout.fx.status); + const Step done = Expect(s, Ev::LogoutDone{}); + EXPECT_EQ(Notify::LogoutDone, done.fx.notify); + EXPECT_EQ(0, done.fx.status); +} + +TEST(LoginStateMachineTests, LoggingOutIgnoresBusReset) { + ExpectIgnored(St::LoggingOut{MakeLogin(1), kOld}, Ev::BusReset{2}); +} + +// Reporting ------------------------------------------------------------------- + +TEST(LoginStateMachineTests, BothSuspendedStatesReportSuspended) { + EXPECT_EQ(LoginState::Suspended, Reported(St::SuspendedHeld{})); + EXPECT_EQ(LoginState::Suspended, Reported(St::SuspendedNoLogin{})); + EXPECT_STREQ("SuspendedHeld", Name(St::SuspendedHeld{})); + EXPECT_STREQ("SuspendedNoLogin", Name(St::SuspendedNoLogin{})); +} + +TEST(LoginStateMachineTests, OnlyStatesWithALoginExposeOne) { + EXPECT_EQ(nullptr, Held(St::Idle{})); + EXPECT_EQ(nullptr, Held(St::LoggingIn{})); + EXPECT_EQ(nullptr, Held(St::SuspendedNoLogin{})); + ASSERT_NE(nullptr, Held(St::SuspendedHeld{MakeLogin(0), 1})); + EXPECT_EQ(0u, Held(St::SuspendedHeld{MakeLogin(0), 1})->id); +} + +} // namespace diff --git a/tests/protocols/SessionRegistryTests.cpp b/tests/protocols/SessionRegistryTests.cpp index 3b2ec3bf..12dc4d91 100644 --- a/tests/protocols/SessionRegistryTests.cpp +++ b/tests/protocols/SessionRegistryTests.cpp @@ -1335,6 +1335,32 @@ TEST(SessionRegistryTests, BusResetDuringLoginLogsInAgainAfterRediscovery) { EXPECT_EQ(ASFW::Protocols::SBP2::LoginState::LoggingIn, state->loginState); } +// SBP-2 reserves no login_ID and the H4D assigns 0: a held login reconnects +// whatever its ID (Apple decides by login state, IOFireWireSBP2Login.cpp:2067). +// Treating 0 as "no login" sent a Login the target rejects while it holds ours. +TEST(SessionRegistryTests, LoginIdZeroReconnectsAfterRediscovery) { + SessionRegistryRig rig; + const uint64_t handle = rig.CreateSession(); + rig.LoginSuccessfully(handle, 0); + while (rig.bus.PendingWriteCount() > 0) { + ASSERT_TRUE(rig.bus.CompleteNextWrite(ASFW::Async::AsyncStatus::kSuccess)); + } + + rig.registry.OnBusReset(2); + rig.deviceManager.SuspendAllForBusReset(); + rig.bus.SetGeneration(ASFW::FW::Generation{2}); + rig.UpsertDevice(Generation{2}, 0x33); + const size_t writesBefore = rig.bus.WriteCount(); + rig.registry.RefreshTargets(Generation{2}); + + ASSERT_EQ(writesBefore + 1, rig.bus.WriteCount()); + EXPECT_EQ(0x33u, rig.bus.WriteAt(writesBefore).nodeId.value); + const auto state = rig.registry.GetSessionState(SessionRegistryRig::Owner(), handle); + ASSERT_TRUE(state.has_value()); + EXPECT_EQ(ASFW::Protocols::SBP2::LoginState::Reconnecting, state->loginState); + EXPECT_EQ(0u, state->loginID); +} + class ReleaseParksCommandTest : public ::testing::TestWithParam {}; // Quitting a client mid-transfer completes the task at once, but the target