[client] Push status snapshot on every state.Set and classify SSO errors

Two related daemon-side status-stream fixes that together keep the UI's
status in sync with the daemon's contextState:

* state.Set previously only mutated the in-memory enum — transitions
  that weren't accompanied by a Mark{Management,Signal,...} call (e.g.
  StatusNeedsLogin after a PermissionDenied login, StatusLoginFailed
  after OAuth init failure, StatusIdle in the Login defer) left the
  UI stuck on the previous snapshot until an unrelated peer event
  happened to fire notifyStateChange. Add a callback on contextState
  fired from Set (outside the mutex, to avoid lock-order issues with
  the recorder's stateChangeMux), and wire it in Server.Start to the
  recorder's new public NotifyStateChange. Every state.Set callsite
  now pushes automatically; new ones don't need to opt in.

* WaitSSOLogin's WaitToken error branch lumped every failure into
  StatusLoginFailed, including context.Canceled aborts from a parallel
  profile switch (actCancel/waitCancel). That spurious LoginFailed
  then wedged the new profile's Up RPC with "up already in progress:
  current status LoginFailed". Split the branch by error type:
  context.Canceled lets the top-level defer pick StatusIdle,
  context.DeadlineExceeded sets StatusNeedsLogin (retryable; OAuth
  device-code window just expired), other errors keep LoginFailed
  (real auth/IO failures). Document the full state-transition table
  in the function godoc.
This commit is contained in:
Zoltan Papp
2026-05-14 14:51:51 +02:00
parent d33b841a33
commit d841a6aa07
3 changed files with 87 additions and 8 deletions

View File

@@ -1281,6 +1281,18 @@ func (d *Status) notifyStateChange() {
}
}
// NotifyStateChange is the public wake-the-subscribers entry point used by
// callers that mutate state outside the peer recorder — most importantly
// the connect-state machine, which writes StatusNeedsLogin into the
// shared contextState (client/internal/state.go) without touching any
// recorder field. Without this push the SubscribeStatus stream stays on
// the previous snapshot until an unrelated peer/management/signal
// change happens to fire notifyStateChange, leaving the UI's status
// out of sync with the daemon.
func (d *Status) NotifyStateChange() {
d.notifyStateChange()
}
func (d *Status) SetWgIface(wgInterface WGIfaceStatus) {
d.mux.Lock()
defer d.mux.Unlock()