fix(remote): harden picker recovery routes
- commit
- 517c29eb45d83454d77758da3f25448c6094335f
- author
- brice · · 2026-08-08 18:29 UTC
- parent
- f9840fe
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
5 files · +16 -5
M internal/ports/frame.go +1 -1
| @@ -22,7 +22,7 @@ const MaxFrameLen = 16 << 20 | |||
| 22 | 22 | type MsgType uint8 | |
| 23 | 23 | ||
| 24 | 24 | // Frame message types. Client-originated messages occupy 1–13 and 15; | |
| 25 | - | // server-originated messages occupy 16–23 and 25. Values 14 and 24 remain | |
| 25 | + | // server-originated messages occupy 16–23 and 25–26. Values 14 and 24 remain | |
| 26 | 26 | // reserved for future extensions. | |
| 27 | 27 | const ( | |
| 28 | 28 | MsgHello MsgType = 1 | |
| @@ -22,7 +22,7 @@ const MaxFrameLen = 16 << 20 | |||||
| 22 | type MsgType uint8 | 22 | type MsgType uint8 | ||
| 23 | 23 | ||||
| 24 | // Frame message types. Client-originated messages occupy 1–13 and 15; | 24 | // Frame message types. Client-originated messages occupy 1–13 and 15; | ||
| 25 | - | // server-originated messages occupy 16–23 and 25. Values 14 and 24 remain | 25 | + | // server-originated messages occupy 16–23 and 25–26. Values 14 and 24 remain |
| 26 | // reserved for future extensions. | 26 | // reserved for future extensions. | ||
| 27 | const ( | 27 | const ( | ||
| 28 | MsgHello MsgType = 1 | 28 | MsgHello MsgType = 1 | ||
M internal/usecase/client/client.go +8 -1
| @@ -461,7 +461,11 @@ func (r *Runner) Run(ctx context.Context, request AttachRequest) (retErr error) | |||
| 461 | 461 | if attemptRequest.NavigationCapabilities&ports.NavigationCapabilityHomePicker == 0 || homeRoute == nil { | |
| 462 | 462 | return errors.New("vev: stale home navigation action") | |
| 463 | 463 | } | |
| 464 | - | returnRoute = &attachRoute{dialer: dialer, request: attemptRequest, resumeToken: result.resumeToken} | |
| 464 | + | returnRequest := attemptRequest | |
| 465 | + | if result.sessionName != "" { | |
| 466 | + | returnRequest.SessionName = result.sessionName | |
| 467 | + | } | |
| 468 | + | returnRoute = &attachRoute{dialer: dialer, request: returnRequest, resumeToken: result.resumeToken} | |
| 465 | 469 | homeNavigationPending = true | |
| 466 | 470 | dialer = homeRoute.dialer | |
| 467 | 471 | attemptRequest = homeRoute.request | |
| @@ -543,6 +547,9 @@ func (r *Runner) Run(ctx context.Context, request AttachRequest) (retErr error) | |||
| 543 | 547 | if result.sessionName != "" { | |
| 544 | 548 | attemptRequest.SessionName = result.sessionName | |
| 545 | 549 | } | |
| 550 | + | if result.welcomed { | |
| 551 | + | homeNavigationPending = false | |
| 552 | + | } | |
| 546 | 553 | if result.err == nil { | |
| 547 | 554 | if result.welcomed && returnNavigationPending { | |
| 548 | 555 | returnRoute = nil | |
| @@ -461,7 +461,11 @@ func (r *Runner) Run(ctx context.Context, request AttachRequest) (retErr error) | |||||
| 461 | if attemptRequest.NavigationCapabilities&ports.NavigationCapabilityHomePicker == 0 || homeRoute == nil { | 461 | if attemptRequest.NavigationCapabilities&ports.NavigationCapabilityHomePicker == 0 || homeRoute == nil { | ||
| 462 | return errors.New("vev: stale home navigation action") | 462 | return errors.New("vev: stale home navigation action") | ||
| 463 | } | 463 | } | ||
| 464 | - | returnRoute = &attachRoute{dialer: dialer, request: attemptRequest, resumeToken: result.resumeToken} | 464 | + | returnRequest := attemptRequest |
| 465 | + | if result.sessionName != "" { | |||
| 466 | + | returnRequest.SessionName = result.sessionName | |||
| 467 | + | } | |||
| 468 | + | returnRoute = &attachRoute{dialer: dialer, request: returnRequest, resumeToken: result.resumeToken} | |||
| 465 | homeNavigationPending = true | 469 | homeNavigationPending = true | ||
| 466 | dialer = homeRoute.dialer | 470 | dialer = homeRoute.dialer | ||
| 467 | attemptRequest = homeRoute.request | 471 | attemptRequest = homeRoute.request | ||
| @@ -543,6 +547,9 @@ func (r *Runner) Run(ctx context.Context, request AttachRequest) (retErr error) | |||||
| 543 | if result.sessionName != "" { | 547 | if result.sessionName != "" { | ||
| 544 | attemptRequest.SessionName = result.sessionName | 548 | attemptRequest.SessionName = result.sessionName | ||
| 545 | } | 549 | } | ||
| 550 | + | if result.welcomed { | |||
| 551 | + | homeNavigationPending = false | |||
| 552 | + | } | |||
| 546 | if result.err == nil { | 553 | if result.err == nil { | ||
| 547 | if result.welcomed && returnNavigationPending { | 554 | if result.welcomed && returnNavigationPending { | ||
| 548 | returnRoute = nil | 555 | returnRoute = nil | ||
M internal/usecase/client/client_test.go +2 -1
| @@ -61,10 +61,11 @@ func (c *reconnectTestClock) fireReconnect(t *testing.T) { | |||
| 61 | 61 | c.mu.Lock() | |
| 62 | 62 | defer c.mu.Unlock() | |
| 63 | 63 | for _, timer := range c.timers { | |
| 64 | + | timer.mu.Lock() | |
| 64 | 65 | if timer.duration != 100*time.Millisecond { | |
| 66 | + | timer.mu.Unlock() | |
| 65 | 67 | continue | |
| 66 | 68 | } | |
| 67 | - | timer.mu.Lock() | |
| 68 | 69 | if !timer.stopped && !timer.fired { | |
| 69 | 70 | timer.fired = true | |
| 70 | 71 | select { | |
| @@ -61,10 +61,11 @@ func (c *reconnectTestClock) fireReconnect(t *testing.T) { | |||||
| 61 | c.mu.Lock() | 61 | c.mu.Lock() | ||
| 62 | defer c.mu.Unlock() | 62 | defer c.mu.Unlock() | ||
| 63 | for _, timer := range c.timers { | 63 | for _, timer := range c.timers { | ||
| 64 | + | timer.mu.Lock() | |||
| 64 | if timer.duration != 100*time.Millisecond { | 65 | if timer.duration != 100*time.Millisecond { | ||
| 66 | + | timer.mu.Unlock() | |||
| 65 | continue | 67 | continue | ||
| 66 | } | 68 | } | ||
| 67 | - | timer.mu.Lock() | |||
| 68 | if !timer.stopped && !timer.fired { | 69 | if !timer.stopped && !timer.fired { | ||
| 69 | timer.fired = true | 70 | timer.fired = true | ||
| 70 | select { | 71 | select { | ||
M internal/usecase/daemon/picker.go +1 -1
| @@ -944,7 +944,7 @@ func (d *Daemon) remotePickerTargetReady(key domain.RemoteSessionKey) bool { | |||
| 944 | 944 | } | |
| 945 | 945 | for _, session := range entry.Sessions { | |
| 946 | 946 | if session.Name == key.Name { | |
| 947 | - | return !remoteSessionStateStopped(session.State) && session.State != "broken" | |
| 947 | + | return session.State == "up" | |
| 948 | 948 | } | |
| 949 | 949 | } | |
| 950 | 950 | return false | |
| @@ -944,7 +944,7 @@ func (d *Daemon) remotePickerTargetReady(key domain.RemoteSessionKey) bool { | |||||
| 944 | } | 944 | } | ||
| 945 | for _, session := range entry.Sessions { | 945 | for _, session := range entry.Sessions { | ||
| 946 | if session.Name == key.Name { | 946 | if session.Name == key.Name { | ||
| 947 | - | return !remoteSessionStateStopped(session.State) && session.State != "broken" | 947 | + | return session.State == "up" |
| 948 | } | 948 | } | ||
| 949 | } | 949 | } | ||
| 950 | return false | 950 | return false | ||
M internal/usecase/daemon/resume_test.go +4 -1
| @@ -104,7 +104,10 @@ func TestTokenlessResumeIsRejectedBeforeNameRouting(t *testing.T) { | |||
| 104 | 104 | var protocol *protoErr | |
| 105 | 105 | require.ErrorAs(t, err, &protocol) | |
| 106 | 106 | require.Equal(t, ports.ErrNoSuchSession, protocol.code) | |
| 107 | - | require.Empty(t, d.sessions) | |
| 107 | + | d.mu.Lock() | |
| 108 | + | sessionCount := len(d.sessions) | |
| 109 | + | d.mu.Unlock() | |
| 110 | + | require.Zero(t, sessionCount) | |
| 108 | 111 | } | |
| 109 | 112 | ||
| 110 | 113 | func TestResumeRejectsMismatchedRemoteTargetBeforeOwnershipMutation(t *testing.T) { | |
| @@ -104,7 +104,10 @@ func TestTokenlessResumeIsRejectedBeforeNameRouting(t *testing.T) { | |||||
| 104 | var protocol *protoErr | 104 | var protocol *protoErr | ||
| 105 | require.ErrorAs(t, err, &protocol) | 105 | require.ErrorAs(t, err, &protocol) | ||
| 106 | require.Equal(t, ports.ErrNoSuchSession, protocol.code) | 106 | require.Equal(t, ports.ErrNoSuchSession, protocol.code) | ||
| 107 | - | require.Empty(t, d.sessions) | 107 | + | d.mu.Lock() |
| 108 | + | sessionCount := len(d.sessions) | |||
| 109 | + | d.mu.Unlock() | |||
| 110 | + | require.Zero(t, sessionCount) | |||
| 108 | } | 111 | } | ||
| 109 | 112 | ||||
| 110 | func TestResumeRejectsMismatchedRemoteTargetBeforeOwnershipMutation(t *testing.T) { | 113 | func TestResumeRejectsMismatchedRemoteTargetBeforeOwnershipMutation(t *testing.T) { | ||