refactor(remote): centralize recovery routes
- commit
- b0018c5f23c1318ea67a46b69e592425ba72b7fb
- author
- brice · · 2026-08-08 19:16 UTC
- parent
- 655f64a
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
5 files · +60 -58
M internal/ports/navigation_test.go +12 -8
| @@ -44,15 +44,19 @@ func TestNavigationActionWireTable(t *testing.T) { | |||
| 44 | 44 | func TestHelloNavigationValidationTable(t *testing.T) { | |
| 45 | 45 | base := Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}} | |
| 46 | 46 | tests := []struct { | |
| 47 | - | name string | |
| 48 | - | hello Hello | |
| 49 | - | valid bool | |
| 50 | - | capability NavigationCapabilities | |
| 51 | - | overlay StartupOverlay | |
| 47 | + | name string | |
| 48 | + | hello Hello | |
| 49 | + | valid bool | |
| 50 | + | capability NavigationCapabilities | |
| 51 | + | overlay StartupOverlay | |
| 52 | + | wantPayload string | |
| 52 | 53 | }{ | |
| 53 | 54 | {name: "ordinary", hello: base, valid: true}, | |
| 54 | 55 | {name: "resume route", hello: Hello{Version: ProtocolVersion, Intent: IntentResume, Size: domain.Size{Cols: 80, Rows: 24}}, valid: true}, | |
| 55 | - | {name: "startup picker requires back", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: NavigationCapabilityBack, StartupOverlay: StartupOverlaySessionPicker}, valid: true, capability: NavigationCapabilityBack, overlay: StartupOverlaySessionPicker}, | |
| 56 | + | {name: "daemon-owned home picker", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: NavigationCapabilityHomePicker, EnvironmentPolicy: EnvironmentPolicyDaemonOwned}, valid: true, capability: NavigationCapabilityHomePicker}, | |
| 57 | + | {name: "client-owned home picker rejected", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: NavigationCapabilityHomePicker}, valid: false}, | |
| 58 | + | {name: "daemon-owned back picker rejected", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: NavigationCapabilityBack, StartupOverlay: StartupOverlaySessionPicker, EnvironmentPolicy: EnvironmentPolicyDaemonOwned}, valid: false}, | |
| 59 | + | {name: "startup picker requires back", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: NavigationCapabilityBack, StartupOverlay: StartupOverlaySessionPicker}, valid: true, capability: NavigationCapabilityBack, overlay: StartupOverlaySessionPicker, wantPayload: "001a02" + "00000000000000000000000000000000" + "0000000000000000" + "0000" + "00500018" + "0000" + "0000" + "00" + "00" + "00000000" + "00" + "00" + "0201"}, | |
| 56 | 60 | {name: "new rejects navigation", hello: Hello{Version: ProtocolVersion, Intent: IntentNew, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: NavigationCapabilityBack, StartupOverlay: StartupOverlaySessionPicker}, valid: false}, | |
| 57 | 61 | {name: "unknown capability", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: 4}, valid: false}, | |
| 58 | 62 | {name: "back without picker", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: NavigationCapabilityBack}, valid: false}, | |
| @@ -66,8 +70,8 @@ func TestHelloNavigationValidationTable(t *testing.T) { | |||
| 66 | 70 | require.NoError(t, err) | |
| 67 | 71 | payload := MarshalHello(tt.hello) | |
| 68 | 72 | require.NotNil(t, payload) | |
| 69 | - | if tt.name == "startup picker requires back" { | |
| 70 | - | expected, decodeErr := hex.DecodeString("001a02" + "00000000000000000000000000000000" + "0000000000000000" + "0000" + "00500018" + "0000" + "0000" + "00" + "00" + "00000000" + "00" + "00" + "0201") | |
| 73 | + | if tt.wantPayload != "" { | |
| 74 | + | expected, decodeErr := hex.DecodeString(tt.wantPayload) | |
| 71 | 75 | require.NoError(t, decodeErr) | |
| 72 | 76 | require.Equal(t, expected, payload) | |
| 73 | 77 | assertAllPrefixesFail(t, expected, UnmarshalHello) | |
| @@ -44,15 +44,19 @@ func TestNavigationActionWireTable(t *testing.T) { | |||||
| 44 | func TestHelloNavigationValidationTable(t *testing.T) { | 44 | func TestHelloNavigationValidationTable(t *testing.T) { | ||
| 45 | base := Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}} | 45 | base := Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}} | ||
| 46 | tests := []struct { | 46 | tests := []struct { | ||
| 47 | - | name string | 47 | + | name string |
| 48 | - | hello Hello | 48 | + | hello Hello |
| 49 | - | valid bool | 49 | + | valid bool |
| 50 | - | capability NavigationCapabilities | 50 | + | capability NavigationCapabilities |
| 51 | - | overlay StartupOverlay | 51 | + | overlay StartupOverlay |
| 52 | + | wantPayload string | |||
| 52 | }{ | 53 | }{ | ||
| 53 | {name: "ordinary", hello: base, valid: true}, | 54 | {name: "ordinary", hello: base, valid: true}, | ||
| 54 | {name: "resume route", hello: Hello{Version: ProtocolVersion, Intent: IntentResume, Size: domain.Size{Cols: 80, Rows: 24}}, valid: true}, | 55 | {name: "resume route", hello: Hello{Version: ProtocolVersion, Intent: IntentResume, Size: domain.Size{Cols: 80, Rows: 24}}, valid: true}, | ||
| 55 | - | {name: "startup picker requires back", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: NavigationCapabilityBack, StartupOverlay: StartupOverlaySessionPicker}, valid: true, capability: NavigationCapabilityBack, overlay: StartupOverlaySessionPicker}, | 56 | + | {name: "daemon-owned home picker", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: NavigationCapabilityHomePicker, EnvironmentPolicy: EnvironmentPolicyDaemonOwned}, valid: true, capability: NavigationCapabilityHomePicker}, |
| 57 | + | {name: "client-owned home picker rejected", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: NavigationCapabilityHomePicker}, valid: false}, | |||
| 58 | + | {name: "daemon-owned back picker rejected", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: NavigationCapabilityBack, StartupOverlay: StartupOverlaySessionPicker, EnvironmentPolicy: EnvironmentPolicyDaemonOwned}, valid: false}, | |||
| 59 | + | {name: "startup picker requires back", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: NavigationCapabilityBack, StartupOverlay: StartupOverlaySessionPicker}, valid: true, capability: NavigationCapabilityBack, overlay: StartupOverlaySessionPicker, wantPayload: "001a02" + "00000000000000000000000000000000" + "0000000000000000" + "0000" + "00500018" + "0000" + "0000" + "00" + "00" + "00000000" + "00" + "00" + "0201"}, | |||
| 56 | {name: "new rejects navigation", hello: Hello{Version: ProtocolVersion, Intent: IntentNew, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: NavigationCapabilityBack, StartupOverlay: StartupOverlaySessionPicker}, valid: false}, | 60 | {name: "new rejects navigation", hello: Hello{Version: ProtocolVersion, Intent: IntentNew, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: NavigationCapabilityBack, StartupOverlay: StartupOverlaySessionPicker}, valid: false}, | ||
| 57 | {name: "unknown capability", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: 4}, valid: false}, | 61 | {name: "unknown capability", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: 4}, valid: false}, | ||
| 58 | {name: "back without picker", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: NavigationCapabilityBack}, valid: false}, | 62 | {name: "back without picker", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: NavigationCapabilityBack}, valid: false}, | ||
| @@ -66,8 +70,8 @@ func TestHelloNavigationValidationTable(t *testing.T) { | |||||
| 66 | require.NoError(t, err) | 70 | require.NoError(t, err) | ||
| 67 | payload := MarshalHello(tt.hello) | 71 | payload := MarshalHello(tt.hello) | ||
| 68 | require.NotNil(t, payload) | 72 | require.NotNil(t, payload) | ||
| 69 | - | if tt.name == "startup picker requires back" { | 73 | + | if tt.wantPayload != "" { |
| 70 | - | expected, decodeErr := hex.DecodeString("001a02" + "00000000000000000000000000000000" + "0000000000000000" + "0000" + "00500018" + "0000" + "0000" + "00" + "00" + "00000000" + "00" + "00" + "0201") | 74 | + | expected, decodeErr := hex.DecodeString(tt.wantPayload) |
| 71 | require.NoError(t, decodeErr) | 75 | require.NoError(t, decodeErr) | ||
| 72 | require.Equal(t, expected, payload) | 76 | require.Equal(t, expected, payload) | ||
| 73 | assertAllPrefixesFail(t, expected, UnmarshalHello) | 77 | assertAllPrefixesFail(t, expected, UnmarshalHello) | ||
M internal/usecase/client/client.go +32 -46
| @@ -355,6 +355,34 @@ func (r *Runner) Run(ctx context.Context, request AttachRequest) (retErr error) | |||
| 355 | 355 | rawEntered: &rawEntered, | |
| 356 | 356 | stage: reconnectStageOfflineRetrying, | |
| 357 | 357 | } | |
| 358 | + | restoreReturnRoute := func(route attachRoute) { | |
| 359 | + | returnNavigationPending = true | |
| 360 | + | dialer = route.dialer | |
| 361 | + | attemptRequest = route.request | |
| 362 | + | if route.resumeToken != 0 { | |
| 363 | + | attemptRequest.Intent = ports.IntentResume | |
| 364 | + | } else { | |
| 365 | + | attemptRequest.Intent = ports.IntentAttach | |
| 366 | + | } | |
| 367 | + | attemptRequest.StartupOverlay = ports.StartupOverlayNone | |
| 368 | + | resumeToken = route.resumeToken | |
| 369 | + | returnResumeFallback = route.resumeToken != 0 | |
| 370 | + | remote = syncReconnectRemote(reconnect, attemptRequest.Remote || r.remote) | |
| 371 | + | backoff = defaultReconnectBackoff.initial | |
| 372 | + | } | |
| 373 | + | enterHomePicker := func() { | |
| 374 | + | dialer = homeRoute.dialer | |
| 375 | + | attemptRequest = homeRoute.request | |
| 376 | + | attemptRequest.Intent = ports.IntentAttach | |
| 377 | + | attemptRequest.NavigationCapabilities = ports.NavigationCapabilityBack | |
| 378 | + | attemptRequest.StartupOverlay = ports.StartupOverlaySessionPicker | |
| 379 | + | attemptRequest.RemoteTarget = nil | |
| 380 | + | attemptRequest.EnvironmentPolicy = ports.EnvironmentPolicyClientOwned | |
| 381 | + | attemptRequest.Remote = homeRoute.request.Remote || r.remote | |
| 382 | + | resumeToken = 0 | |
| 383 | + | remote = syncReconnectRemote(reconnect, homeRoute.request.Remote || r.remote) | |
| 384 | + | backoff = defaultReconnectBackoff.initial | |
| 385 | + | } | |
| 358 | 386 | var input *terminalInputPump | |
| 359 | 387 | defer func() { | |
| 360 | 388 | if input != nil { | |
| @@ -467,17 +495,7 @@ func (r *Runner) Run(ctx context.Context, request AttachRequest) (retErr error) | |||
| 467 | 495 | } | |
| 468 | 496 | returnRoute = &attachRoute{dialer: dialer, request: returnRequest, resumeToken: result.resumeToken} | |
| 469 | 497 | homeNavigationPending = true | |
| 470 | - | dialer = homeRoute.dialer | |
| 471 | - | attemptRequest = homeRoute.request | |
| 472 | - | attemptRequest.Remote = homeRoute.request.Remote || r.remote | |
| 473 | - | attemptRequest.Intent = ports.IntentAttach | |
| 474 | - | attemptRequest.NavigationCapabilities = ports.NavigationCapabilityBack | |
| 475 | - | attemptRequest.StartupOverlay = ports.StartupOverlaySessionPicker | |
| 476 | - | attemptRequest.RemoteTarget = nil | |
| 477 | - | attemptRequest.EnvironmentPolicy = ports.EnvironmentPolicyClientOwned | |
| 478 | - | remote = syncReconnectRemote(reconnect, homeRoute.request.Remote || r.remote) | |
| 479 | - | resumeToken = 0 | |
| 480 | - | backoff = defaultReconnectBackoff.initial | |
| 498 | + | enterHomePicker() | |
| 481 | 499 | continue | |
| 482 | 500 | case ports.NavigationBack: | |
| 483 | 501 | if attemptRequest.StartupOverlay != ports.StartupOverlaySessionPicker || returnRoute == nil || attemptRequest.NavigationCapabilities&ports.NavigationCapabilityBack == 0 { | |
| @@ -487,18 +505,7 @@ func (r *Runner) Run(ctx context.Context, request AttachRequest) (retErr error) | |||
| 487 | 505 | returnRoute = nil | |
| 488 | 506 | homeNavigationPending = false | |
| 489 | 507 | returnNavigationPending = true | |
| 490 | - | dialer = route.dialer | |
| 491 | - | attemptRequest = route.request | |
| 492 | - | if route.resumeToken != 0 { | |
| 493 | - | attemptRequest.Intent = ports.IntentResume | |
| 494 | - | } else { | |
| 495 | - | attemptRequest.Intent = ports.IntentAttach | |
| 496 | - | } | |
| 497 | - | attemptRequest.StartupOverlay = ports.StartupOverlayNone | |
| 498 | - | resumeToken = route.resumeToken | |
| 499 | - | returnResumeFallback = route.resumeToken != 0 | |
| 500 | - | remote = syncReconnectRemote(reconnect, attemptRequest.Remote || r.remote) | |
| 501 | - | backoff = defaultReconnectBackoff.initial | |
| 508 | + | restoreReturnRoute(route) | |
| 502 | 509 | continue | |
| 503 | 510 | default: | |
| 504 | 511 | return errors.New("vev: unsupported navigation action") | |
| @@ -570,18 +577,7 @@ func (r *Runner) Run(ctx context.Context, request AttachRequest) (retErr error) | |||
| 570 | 577 | returnRoute = nil | |
| 571 | 578 | homeNavigationPending = false | |
| 572 | 579 | returnNavigationPending = true | |
| 573 | - | dialer = route.dialer | |
| 574 | - | attemptRequest = route.request | |
| 575 | - | if route.resumeToken != 0 { | |
| 576 | - | attemptRequest.Intent = ports.IntentResume | |
| 577 | - | } else { | |
| 578 | - | attemptRequest.Intent = ports.IntentAttach | |
| 579 | - | } | |
| 580 | - | attemptRequest.StartupOverlay = ports.StartupOverlayNone | |
| 581 | - | resumeToken = route.resumeToken | |
| 582 | - | returnResumeFallback = route.resumeToken != 0 | |
| 583 | - | remote = syncReconnectRemote(reconnect, attemptRequest.Remote || r.remote) | |
| 584 | - | backoff = defaultReconnectBackoff.initial | |
| 580 | + | restoreReturnRoute(route) | |
| 585 | 581 | continue | |
| 586 | 582 | } | |
| 587 | 583 | if returnResumeFallback { | |
| @@ -596,17 +592,7 @@ func (r *Runner) Run(ctx context.Context, request AttachRequest) (retErr error) | |||
| 596 | 592 | if returnNavigationPending && !returnResumeFallback && homeRoute != nil { | |
| 597 | 593 | returnRoute = nil | |
| 598 | 594 | returnNavigationPending = false | |
| 599 | - | dialer = homeRoute.dialer | |
| 600 | - | attemptRequest = homeRoute.request | |
| 601 | - | attemptRequest.Intent = ports.IntentAttach | |
| 602 | - | attemptRequest.NavigationCapabilities = ports.NavigationCapabilityBack | |
| 603 | - | attemptRequest.StartupOverlay = ports.StartupOverlaySessionPicker | |
| 604 | - | attemptRequest.RemoteTarget = nil | |
| 605 | - | attemptRequest.EnvironmentPolicy = ports.EnvironmentPolicyClientOwned | |
| 606 | - | attemptRequest.Remote = homeRoute.request.Remote || r.remote | |
| 607 | - | resumeToken = 0 | |
| 608 | - | remote = syncReconnectRemote(reconnect, homeRoute.request.Remote || r.remote) | |
| 609 | - | backoff = defaultReconnectBackoff.initial | |
| 595 | + | enterHomePicker() | |
| 610 | 596 | continue | |
| 611 | 597 | } | |
| 612 | 598 | if !shouldReconnect(result.err) || resumeToken == 0 || ctx.Err() != nil { | |
| @@ -355,6 +355,34 @@ func (r *Runner) Run(ctx context.Context, request AttachRequest) (retErr error) | |||||
| 355 | rawEntered: &rawEntered, | 355 | rawEntered: &rawEntered, | ||
| 356 | stage: reconnectStageOfflineRetrying, | 356 | stage: reconnectStageOfflineRetrying, | ||
| 357 | } | 357 | } | ||
| 358 | + | restoreReturnRoute := func(route attachRoute) { | |||
| 359 | + | returnNavigationPending = true | |||
| 360 | + | dialer = route.dialer | |||
| 361 | + | attemptRequest = route.request | |||
| 362 | + | if route.resumeToken != 0 { | |||
| 363 | + | attemptRequest.Intent = ports.IntentResume | |||
| 364 | + | } else { | |||
| 365 | + | attemptRequest.Intent = ports.IntentAttach | |||
| 366 | + | } | |||
| 367 | + | attemptRequest.StartupOverlay = ports.StartupOverlayNone | |||
| 368 | + | resumeToken = route.resumeToken | |||
| 369 | + | returnResumeFallback = route.resumeToken != 0 | |||
| 370 | + | remote = syncReconnectRemote(reconnect, attemptRequest.Remote || r.remote) | |||
| 371 | + | backoff = defaultReconnectBackoff.initial | |||
| 372 | + | } | |||
| 373 | + | enterHomePicker := func() { | |||
| 374 | + | dialer = homeRoute.dialer | |||
| 375 | + | attemptRequest = homeRoute.request | |||
| 376 | + | attemptRequest.Intent = ports.IntentAttach | |||
| 377 | + | attemptRequest.NavigationCapabilities = ports.NavigationCapabilityBack | |||
| 378 | + | attemptRequest.StartupOverlay = ports.StartupOverlaySessionPicker | |||
| 379 | + | attemptRequest.RemoteTarget = nil | |||
| 380 | + | attemptRequest.EnvironmentPolicy = ports.EnvironmentPolicyClientOwned | |||
| 381 | + | attemptRequest.Remote = homeRoute.request.Remote || r.remote | |||
| 382 | + | resumeToken = 0 | |||
| 383 | + | remote = syncReconnectRemote(reconnect, homeRoute.request.Remote || r.remote) | |||
| 384 | + | backoff = defaultReconnectBackoff.initial | |||
| 385 | + | } | |||
| 358 | var input *terminalInputPump | 386 | var input *terminalInputPump | ||
| 359 | defer func() { | 387 | defer func() { | ||
| 360 | if input != nil { | 388 | if input != nil { | ||
| @@ -467,17 +495,7 @@ func (r *Runner) Run(ctx context.Context, request AttachRequest) (retErr error) | |||||
| 467 | } | 495 | } | ||
| 468 | returnRoute = &attachRoute{dialer: dialer, request: returnRequest, resumeToken: result.resumeToken} | 496 | returnRoute = &attachRoute{dialer: dialer, request: returnRequest, resumeToken: result.resumeToken} | ||
| 469 | homeNavigationPending = true | 497 | homeNavigationPending = true | ||
| 470 | - | dialer = homeRoute.dialer | 498 | + | enterHomePicker() |
| 471 | - | attemptRequest = homeRoute.request | |||
| 472 | - | attemptRequest.Remote = homeRoute.request.Remote || r.remote | |||
| 473 | - | attemptRequest.Intent = ports.IntentAttach | |||
| 474 | - | attemptRequest.NavigationCapabilities = ports.NavigationCapabilityBack | |||
| 475 | - | attemptRequest.StartupOverlay = ports.StartupOverlaySessionPicker | |||
| 476 | - | attemptRequest.RemoteTarget = nil | |||
| 477 | - | attemptRequest.EnvironmentPolicy = ports.EnvironmentPolicyClientOwned | |||
| 478 | - | remote = syncReconnectRemote(reconnect, homeRoute.request.Remote || r.remote) | |||
| 479 | - | resumeToken = 0 | |||
| 480 | - | backoff = defaultReconnectBackoff.initial | |||
| 481 | continue | 499 | continue | ||
| 482 | case ports.NavigationBack: | 500 | case ports.NavigationBack: | ||
| 483 | if attemptRequest.StartupOverlay != ports.StartupOverlaySessionPicker || returnRoute == nil || attemptRequest.NavigationCapabilities&ports.NavigationCapabilityBack == 0 { | 501 | if attemptRequest.StartupOverlay != ports.StartupOverlaySessionPicker || returnRoute == nil || attemptRequest.NavigationCapabilities&ports.NavigationCapabilityBack == 0 { | ||
| @@ -487,18 +505,7 @@ func (r *Runner) Run(ctx context.Context, request AttachRequest) (retErr error) | |||||
| 487 | returnRoute = nil | 505 | returnRoute = nil | ||
| 488 | homeNavigationPending = false | 506 | homeNavigationPending = false | ||
| 489 | returnNavigationPending = true | 507 | returnNavigationPending = true | ||
| 490 | - | dialer = route.dialer | 508 | + | restoreReturnRoute(route) |
| 491 | - | attemptRequest = route.request | |||
| 492 | - | if route.resumeToken != 0 { | |||
| 493 | - | attemptRequest.Intent = ports.IntentResume | |||
| 494 | - | } else { | |||
| 495 | - | attemptRequest.Intent = ports.IntentAttach | |||
| 496 | - | } | |||
| 497 | - | attemptRequest.StartupOverlay = ports.StartupOverlayNone | |||
| 498 | - | resumeToken = route.resumeToken | |||
| 499 | - | returnResumeFallback = route.resumeToken != 0 | |||
| 500 | - | remote = syncReconnectRemote(reconnect, attemptRequest.Remote || r.remote) | |||
| 501 | - | backoff = defaultReconnectBackoff.initial | |||
| 502 | continue | 509 | continue | ||
| 503 | default: | 510 | default: | ||
| 504 | return errors.New("vev: unsupported navigation action") | 511 | return errors.New("vev: unsupported navigation action") | ||
| @@ -570,18 +577,7 @@ func (r *Runner) Run(ctx context.Context, request AttachRequest) (retErr error) | |||||
| 570 | returnRoute = nil | 577 | returnRoute = nil | ||
| 571 | homeNavigationPending = false | 578 | homeNavigationPending = false | ||
| 572 | returnNavigationPending = true | 579 | returnNavigationPending = true | ||
| 573 | - | dialer = route.dialer | 580 | + | restoreReturnRoute(route) |
| 574 | - | attemptRequest = route.request | |||
| 575 | - | if route.resumeToken != 0 { | |||
| 576 | - | attemptRequest.Intent = ports.IntentResume | |||
| 577 | - | } else { | |||
| 578 | - | attemptRequest.Intent = ports.IntentAttach | |||
| 579 | - | } | |||
| 580 | - | attemptRequest.StartupOverlay = ports.StartupOverlayNone | |||
| 581 | - | resumeToken = route.resumeToken | |||
| 582 | - | returnResumeFallback = route.resumeToken != 0 | |||
| 583 | - | remote = syncReconnectRemote(reconnect, attemptRequest.Remote || r.remote) | |||
| 584 | - | backoff = defaultReconnectBackoff.initial | |||
| 585 | continue | 581 | continue | ||
| 586 | } | 582 | } | ||
| 587 | if returnResumeFallback { | 583 | if returnResumeFallback { | ||
| @@ -596,17 +592,7 @@ func (r *Runner) Run(ctx context.Context, request AttachRequest) (retErr error) | |||||
| 596 | if returnNavigationPending && !returnResumeFallback && homeRoute != nil { | 592 | if returnNavigationPending && !returnResumeFallback && homeRoute != nil { | ||
| 597 | returnRoute = nil | 593 | returnRoute = nil | ||
| 598 | returnNavigationPending = false | 594 | returnNavigationPending = false | ||
| 599 | - | dialer = homeRoute.dialer | 595 | + | enterHomePicker() |
| 600 | - | attemptRequest = homeRoute.request | |||
| 601 | - | attemptRequest.Intent = ports.IntentAttach | |||
| 602 | - | attemptRequest.NavigationCapabilities = ports.NavigationCapabilityBack | |||
| 603 | - | attemptRequest.StartupOverlay = ports.StartupOverlaySessionPicker | |||
| 604 | - | attemptRequest.RemoteTarget = nil | |||
| 605 | - | attemptRequest.EnvironmentPolicy = ports.EnvironmentPolicyClientOwned | |||
| 606 | - | attemptRequest.Remote = homeRoute.request.Remote || r.remote | |||
| 607 | - | resumeToken = 0 | |||
| 608 | - | remote = syncReconnectRemote(reconnect, homeRoute.request.Remote || r.remote) | |||
| 609 | - | backoff = defaultReconnectBackoff.initial | |||
| 610 | continue | 596 | continue | ||
| 611 | } | 597 | } | ||
| 612 | if !shouldReconnect(result.err) || resumeToken == 0 || ctx.Err() != nil { | 598 | if !shouldReconnect(result.err) || resumeToken == 0 || ctx.Err() != nil { | ||
M internal/usecase/daemon/attachment_effect_gate.go +14 -2
| @@ -1,6 +1,7 @@ | |||
| 1 | 1 | package daemon | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "context" | |
| 4 | 5 | "sync" | |
| 5 | 6 | "sync/atomic" | |
| 6 | 7 | ||
| @@ -219,6 +220,13 @@ func (ac *attachedClient) beginAttachmentEffect(token attachmentConnectionToken) | |||
| 219 | 220 | // after Welcome has completed so a replacement blocked behind that send can | |
| 220 | 221 | // publish before readiness or first paint. | |
| 221 | 222 | func (ac *attachedClient) beginCurrentAttachmentEffect(sess *session, tr ports.Transport) (attachmentConnectionToken, *attachmentEffectTicket, bool) { | |
| 223 | + | return ac.beginCurrentAttachmentEffectContext(context.Background(), sess, tr) | |
| 224 | + | } | |
| 225 | + | ||
| 226 | + | func (ac *attachedClient) beginCurrentAttachmentEffectContext(ctx context.Context, sess *session, tr ports.Transport) (attachmentConnectionToken, *attachmentEffectTicket, bool) { | |
| 227 | + | if ctx == nil { | |
| 228 | + | ctx = context.Background() | |
| 229 | + | } | |
| 222 | 230 | if ac == nil || sess == nil || tr == nil { | |
| 223 | 231 | return attachmentConnectionToken{}, nil, false | |
| 224 | 232 | } | |
| @@ -237,8 +245,12 @@ func (ac *attachedClient) beginCurrentAttachmentEffect(sess *session, tr ports.T | |||
| 237 | 245 | if g.phase == attachmentEffectsFrozen { | |
| 238 | 246 | changed := g.changed | |
| 239 | 247 | g.mu.Unlock() | |
| 240 | - | <-changed | |
| 241 | - | continue | |
| 248 | + | select { | |
| 249 | + | case <-changed: | |
| 250 | + | continue | |
| 251 | + | case <-ctx.Done(): | |
| 252 | + | return token, nil, false | |
| 253 | + | } | |
| 242 | 254 | } | |
| 243 | 255 | g.mu.Unlock() | |
| 244 | 256 | // A stable mismatch is only recoverable when a publication raced token | |
| @@ -1,6 +1,7 @@ | |||||
| 1 | package daemon | 1 | package daemon | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | + | "context" | |||
| 4 | "sync" | 5 | "sync" | ||
| 5 | "sync/atomic" | 6 | "sync/atomic" | ||
| 6 | 7 | ||||
| @@ -219,6 +220,13 @@ func (ac *attachedClient) beginAttachmentEffect(token attachmentConnectionToken) | |||||
| 219 | // after Welcome has completed so a replacement blocked behind that send can | 220 | // after Welcome has completed so a replacement blocked behind that send can | ||
| 220 | // publish before readiness or first paint. | 221 | // publish before readiness or first paint. | ||
| 221 | func (ac *attachedClient) beginCurrentAttachmentEffect(sess *session, tr ports.Transport) (attachmentConnectionToken, *attachmentEffectTicket, bool) { | 222 | func (ac *attachedClient) beginCurrentAttachmentEffect(sess *session, tr ports.Transport) (attachmentConnectionToken, *attachmentEffectTicket, bool) { | ||
| 223 | + | return ac.beginCurrentAttachmentEffectContext(context.Background(), sess, tr) | |||
| 224 | + | } | |||
| 225 | + | ||||
| 226 | + | func (ac *attachedClient) beginCurrentAttachmentEffectContext(ctx context.Context, sess *session, tr ports.Transport) (attachmentConnectionToken, *attachmentEffectTicket, bool) { | |||
| 227 | + | if ctx == nil { | |||
| 228 | + | ctx = context.Background() | |||
| 229 | + | } | |||
| 222 | if ac == nil || sess == nil || tr == nil { | 230 | if ac == nil || sess == nil || tr == nil { | ||
| 223 | return attachmentConnectionToken{}, nil, false | 231 | return attachmentConnectionToken{}, nil, false | ||
| 224 | } | 232 | } | ||
| @@ -237,8 +245,12 @@ func (ac *attachedClient) beginCurrentAttachmentEffect(sess *session, tr ports.T | |||||
| 237 | if g.phase == attachmentEffectsFrozen { | 245 | if g.phase == attachmentEffectsFrozen { | ||
| 238 | changed := g.changed | 246 | changed := g.changed | ||
| 239 | g.mu.Unlock() | 247 | g.mu.Unlock() | ||
| 240 | - | <-changed | 248 | + | select { |
| 241 | - | continue | 249 | + | case <-changed: |
| 250 | + | continue | |||
| 251 | + | case <-ctx.Done(): | |||
| 252 | + | return token, nil, false | |||
| 253 | + | } | |||
| 242 | } | 254 | } | ||
| 243 | g.mu.Unlock() | 255 | g.mu.Unlock() | ||
| 244 | // A stable mismatch is only recoverable when a publication raced token | 256 | // A stable mismatch is only recoverable when a publication raced token | ||
M internal/usecase/daemon/daemon.go +1 -1
| @@ -1183,7 +1183,7 @@ func (d *Daemon) handleHelloWithContext(handshakeCtx context.Context, timedOut < | |||
| 1183 | 1183 | // Release Welcome's effect before discovering post-handshake authority so a | |
| 1184 | 1184 | // replacement blocked behind the send can publish its generation and lease. | |
| 1185 | 1185 | welcomeTicket.End() | |
| 1186 | - | postWelcomeToken, postWelcomeTicket, admitted := ac.beginCurrentAttachmentEffect(sess, tr) | |
| 1186 | + | postWelcomeToken, postWelcomeTicket, admitted := ac.beginCurrentAttachmentEffectContext(handshakeCtx, sess, tr) | |
| 1187 | 1187 | if !admitted { | |
| 1188 | 1188 | failAttachment() | |
| 1189 | 1189 | return | |
| @@ -1183,7 +1183,7 @@ func (d *Daemon) handleHelloWithContext(handshakeCtx context.Context, timedOut < | |||||
| 1183 | // Release Welcome's effect before discovering post-handshake authority so a | 1183 | // Release Welcome's effect before discovering post-handshake authority so a | ||
| 1184 | // replacement blocked behind the send can publish its generation and lease. | 1184 | // replacement blocked behind the send can publish its generation and lease. | ||
| 1185 | welcomeTicket.End() | 1185 | welcomeTicket.End() | ||
| 1186 | - | postWelcomeToken, postWelcomeTicket, admitted := ac.beginCurrentAttachmentEffect(sess, tr) | 1186 | + | postWelcomeToken, postWelcomeTicket, admitted := ac.beginCurrentAttachmentEffectContext(handshakeCtx, sess, tr) |
| 1187 | if !admitted { | 1187 | if !admitted { | ||
| 1188 | failAttachment() | 1188 | failAttachment() | ||
| 1189 | return | 1189 | return | ||
M internal/usecase/daemon/picker.go +1 -1
| @@ -879,7 +879,7 @@ func (d *Daemon) switchToTargetForAttachment(token attachmentConnectionToken, ta | |||
| 879 | 879 | } | |
| 880 | 880 | token.effect.bindActionEnd(d, action) | |
| 881 | 881 | token.effect.End() | |
| 882 | - | if token.sess == nil { | |
| 882 | + | if !token.attachmentCurrent() { | |
| 883 | 883 | return errAttachmentTransition | |
| 884 | 884 | } | |
| 885 | 885 | return d.switchToTargetGuardedForAttachment(token.sess, token.ac, target, guard, &token, action) | |
| @@ -879,7 +879,7 @@ func (d *Daemon) switchToTargetForAttachment(token attachmentConnectionToken, ta | |||||
| 879 | } | 879 | } | ||
| 880 | token.effect.bindActionEnd(d, action) | 880 | token.effect.bindActionEnd(d, action) | ||
| 881 | token.effect.End() | 881 | token.effect.End() | ||
| 882 | - | if token.sess == nil { | 882 | + | if !token.attachmentCurrent() { |
| 883 | return errAttachmentTransition | 883 | return errAttachmentTransition | ||
| 884 | } | 884 | } | ||
| 885 | return d.switchToTargetGuardedForAttachment(token.sess, token.ac, target, guard, &token, action) | 885 | return d.switchToTargetGuardedForAttachment(token.sess, token.ac, target, guard, &token, action) | ||