fix(routes): preserve navigation across session switches
- commit
- eac59c20e1fdee9e83ff80f5cad770e8ca700e49
- author
- brice · · 2026-08-09 18:25 UTC
- parent
- 129ef59
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
4 files · +164 -2
M internal/usecase/client/client.go +6 -0
| @@ -546,8 +546,14 @@ func (r *Runner) Run(ctx context.Context, request AttachRequest) (retErr error) | |||
| 546 | 546 | } else { | |
| 547 | 547 | attemptRequest.Intent = ports.IntentAttach | |
| 548 | 548 | } | |
| 549 | + | // Back is transient to the picker overlay. Re-derive the home-picker | |
| 550 | + | // capability from the selected route instead of trusting request | |
| 551 | + | // metadata, which may have been cleared after a daemon-side switch. | |
| 549 | 552 | attemptRequest.StartupOverlay = ports.StartupOverlayNone | |
| 550 | 553 | attemptRequest.NavigationCapabilities = 0 | |
| 554 | + | if homeRoute != nil && selection.selected.presentation.kind == ports.RouteKindRemote { | |
| 555 | + | attemptRequest.NavigationCapabilities = ports.NavigationCapabilityHomePicker | |
| 556 | + | } | |
| 551 | 557 | resumeToken = selection.selected.resumeToken | |
| 552 | 558 | routeNavigationPending = true | |
| 553 | 559 | routeNavigationResumeFallback = selection.selected.resumeToken != 0 | |
| @@ -546,8 +546,14 @@ func (r *Runner) Run(ctx context.Context, request AttachRequest) (retErr error) | |||||
| 546 | } else { | 546 | } else { | ||
| 547 | attemptRequest.Intent = ports.IntentAttach | 547 | attemptRequest.Intent = ports.IntentAttach | ||
| 548 | } | 548 | } | ||
| 549 | + | // Back is transient to the picker overlay. Re-derive the home-picker | |||
| 550 | + | // capability from the selected route instead of trusting request | |||
| 551 | + | // metadata, which may have been cleared after a daemon-side switch. | |||
| 549 | attemptRequest.StartupOverlay = ports.StartupOverlayNone | 552 | attemptRequest.StartupOverlay = ports.StartupOverlayNone | ||
| 550 | attemptRequest.NavigationCapabilities = 0 | 553 | attemptRequest.NavigationCapabilities = 0 | ||
| 554 | + | if homeRoute != nil && selection.selected.presentation.kind == ports.RouteKindRemote { | |||
| 555 | + | attemptRequest.NavigationCapabilities = ports.NavigationCapabilityHomePicker | |||
| 556 | + | } | |||
| 551 | resumeToken = selection.selected.resumeToken | 557 | resumeToken = selection.selected.resumeToken | ||
| 552 | routeNavigationPending = true | 558 | routeNavigationPending = true | ||
| 553 | routeNavigationResumeFallback = selection.selected.resumeToken != 0 | 559 | routeNavigationResumeFallback = selection.selected.resumeToken != 0 | ||
M internal/usecase/client/client_test.go +91 -0
| @@ -483,6 +483,97 @@ func TestAttachHelloPreservesCompleteAttachRequest(t *testing.T) { | |||
| 483 | 483 | require.Equal(t, request.StartupOverlay, hello.StartupOverlay) | |
| 484 | 484 | } | |
| 485 | 485 | ||
| 486 | + | func TestRouteNavigationPreservesRemoteHomePickerAcrossLocalReturn(t *testing.T) { | |
| 487 | + | term := newRunTerminal() | |
| 488 | + | defer term.in.unblock() | |
| 489 | + | ||
| 490 | + | localLifecycle := domain.SessionLifecycleID{1, 1, 1} | |
| 491 | + | remoteLifecycle := domain.SessionLifecycleID{2, 2, 2} | |
| 492 | + | newLifecycle := domain.SessionLifecycleID{3, 3, 3} | |
| 493 | + | welcome := func(name string, lifecycle domain.SessionLifecycleID, token uint64) ports.Frame { | |
| 494 | + | return frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{ | |
| 495 | + | SessionID: name + "-id", | |
| 496 | + | SessionName: name, | |
| 497 | + | ResumeToken: token, | |
| 498 | + | Capabilities: ports.CapabilityResume, | |
| 499 | + | CommittedIdentity: &ports.CommittedRouteIdentity{ | |
| 500 | + | Target: ports.ExactSessionTarget{LifecycleID: lifecycle, SessionName: name}, | |
| 501 | + | }, | |
| 502 | + | })) | |
| 503 | + | } | |
| 504 | + | remoteTarget := domain.RemoteSessionTarget{ | |
| 505 | + | Endpoint: "remote", | |
| 506 | + | DisplayOrigin: "remote", | |
| 507 | + | LifecycleID: remoteLifecycle, | |
| 508 | + | SessionName: "remote-manual", | |
| 509 | + | LiveTabID: "tab-1", | |
| 510 | + | } | |
| 511 | + | remoteHandoff := ports.AttachTarget{ | |
| 512 | + | Endpoint: "remote", Session: "remote-manual", Intent: ports.IntentAttach, | |
| 513 | + | RemoteTarget: &remoteTarget, EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned, | |
| 514 | + | } | |
| 515 | + | ||
| 516 | + | local1 := &recordingTransport{recvs: []recvItem{ | |
| 517 | + | {f: welcome("test", localLifecycle, 11)}, | |
| 518 | + | {f: frameOf(ports.MsgAttachTarget, ports.MarshalAttachTarget(remoteHandoff))}, | |
| 519 | + | }} | |
| 520 | + | local2 := &recordingTransport{recvs: []recvItem{ | |
| 521 | + | {f: welcome("test", localLifecycle, 11)}, | |
| 522 | + | {f: frameOf(ports.MsgCommittedRouteIdentity, mustMarshalCommittedIdentity(ports.CommittedRouteIdentity{ | |
| 523 | + | Target: ports.ExactSessionTarget{LifecycleID: newLifecycle, SessionName: "new"}, | |
| 524 | + | }))}, | |
| 525 | + | {f: frameOf(ports.MsgNavigateRecentRoute, mustMarshalRouteAction(ports.RouteNavigationAction{SnapshotGeneration: 4, Key: 2, Generation: 2}))}, | |
| 526 | + | }} | |
| 527 | + | local3 := &recordingTransport{recvs: []recvItem{ | |
| 528 | + | {f: welcome("test", localLifecycle, 11)}, | |
| 529 | + | {f: frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))}, | |
| 530 | + | }} | |
| 531 | + | remote1 := &recordingTransport{recvs: []recvItem{ | |
| 532 | + | {f: welcome("remote-manual", remoteLifecycle, 22)}, | |
| 533 | + | {f: frameOf(ports.MsgNavigateRecentRoute, mustMarshalRouteAction(ports.RouteNavigationAction{SnapshotGeneration: 2, Key: 1, Generation: 1}))}, | |
| 534 | + | }} | |
| 535 | + | remote2 := &recordingTransport{recvs: []recvItem{ | |
| 536 | + | {f: welcome("remote-manual", remoteLifecycle, 22)}, | |
| 537 | + | {f: frameOf(ports.MsgNavigationAction, ports.MarshalNavigationAction(ports.NavigationOpenHomePicker))}, | |
| 538 | + | }} | |
| 539 | + | localDialer := &sequenceDialer{trs: []ports.Transport{local1, local2, local3}} | |
| 540 | + | remoteDialer := &sequenceDialer{trs: []ports.Transport{remote1, remote2}} | |
| 541 | + | ||
| 542 | + | deps := testDependencies(localDialer, term, realClock{}, nil, nil) | |
| 543 | + | deps.AttachHandoff = func(target ports.AttachTarget) (ports.Dialer, client.AttachRequest, error) { | |
| 544 | + | require.Equal(t, remoteHandoff, target) | |
| 545 | + | return remoteDialer, client.AttachRequest{ | |
| 546 | + | Intent: ports.IntentAttach, SessionName: target.Session, Remote: true, | |
| 547 | + | Origin: ports.RouteOriginDiscovery, OriginKey: target.Endpoint, | |
| 548 | + | RemoteTarget: &remoteTarget, EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned, | |
| 549 | + | }, nil | |
| 550 | + | } | |
| 551 | + | ||
| 552 | + | err := runTestClient(context.Background(), deps, client.AttachRequest{ | |
| 553 | + | Intent: ports.IntentAttach, SessionName: "test", Origin: ports.RouteOriginLocal, OriginKey: "local", | |
| 554 | + | }) | |
| 555 | + | remoteHello := helloFromSend(t, remote2) | |
| 556 | + | require.NoError(t, err) | |
| 557 | + | ||
| 558 | + | require.Equal(t, ports.NavigationCapabilityHomePicker, remoteHello.NavigationCapabilities) | |
| 559 | + | } | |
| 560 | + | ||
| 561 | + | func mustMarshalCommittedIdentity(identity ports.CommittedRouteIdentity) []byte { | |
| 562 | + | payload, err := ports.MarshalCommittedRouteIdentity(identity) | |
| 563 | + | if err != nil { | |
| 564 | + | panic(err) | |
| 565 | + | } | |
| 566 | + | return payload | |
| 567 | + | } | |
| 568 | + | ||
| 569 | + | func mustMarshalRouteAction(action ports.RouteNavigationAction) []byte { | |
| 570 | + | payload, err := ports.MarshalRouteNavigationAction(action) | |
| 571 | + | if err != nil { | |
| 572 | + | panic(err) | |
| 573 | + | } | |
| 574 | + | return payload | |
| 575 | + | } | |
| 576 | + | ||
| 486 | 577 | func TestAttachTargetHandoffReturnsValidatedTargetAndClosesTransport(t *testing.T) { | |
| 487 | 578 | var out bytes.Buffer | |
| 488 | 579 | var restoreCount atomic.Int32 | |
| @@ -483,6 +483,97 @@ func TestAttachHelloPreservesCompleteAttachRequest(t *testing.T) { | |||||
| 483 | require.Equal(t, request.StartupOverlay, hello.StartupOverlay) | 483 | require.Equal(t, request.StartupOverlay, hello.StartupOverlay) | ||
| 484 | } | 484 | } | ||
| 485 | 485 | ||||
| 486 | + | func TestRouteNavigationPreservesRemoteHomePickerAcrossLocalReturn(t *testing.T) { | |||
| 487 | + | term := newRunTerminal() | |||
| 488 | + | defer term.in.unblock() | |||
| 489 | + | ||||
| 490 | + | localLifecycle := domain.SessionLifecycleID{1, 1, 1} | |||
| 491 | + | remoteLifecycle := domain.SessionLifecycleID{2, 2, 2} | |||
| 492 | + | newLifecycle := domain.SessionLifecycleID{3, 3, 3} | |||
| 493 | + | welcome := func(name string, lifecycle domain.SessionLifecycleID, token uint64) ports.Frame { | |||
| 494 | + | return frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{ | |||
| 495 | + | SessionID: name + "-id", | |||
| 496 | + | SessionName: name, | |||
| 497 | + | ResumeToken: token, | |||
| 498 | + | Capabilities: ports.CapabilityResume, | |||
| 499 | + | CommittedIdentity: &ports.CommittedRouteIdentity{ | |||
| 500 | + | Target: ports.ExactSessionTarget{LifecycleID: lifecycle, SessionName: name}, | |||
| 501 | + | }, | |||
| 502 | + | })) | |||
| 503 | + | } | |||
| 504 | + | remoteTarget := domain.RemoteSessionTarget{ | |||
| 505 | + | Endpoint: "remote", | |||
| 506 | + | DisplayOrigin: "remote", | |||
| 507 | + | LifecycleID: remoteLifecycle, | |||
| 508 | + | SessionName: "remote-manual", | |||
| 509 | + | LiveTabID: "tab-1", | |||
| 510 | + | } | |||
| 511 | + | remoteHandoff := ports.AttachTarget{ | |||
| 512 | + | Endpoint: "remote", Session: "remote-manual", Intent: ports.IntentAttach, | |||
| 513 | + | RemoteTarget: &remoteTarget, EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned, | |||
| 514 | + | } | |||
| 515 | + | ||||
| 516 | + | local1 := &recordingTransport{recvs: []recvItem{ | |||
| 517 | + | {f: welcome("test", localLifecycle, 11)}, | |||
| 518 | + | {f: frameOf(ports.MsgAttachTarget, ports.MarshalAttachTarget(remoteHandoff))}, | |||
| 519 | + | }} | |||
| 520 | + | local2 := &recordingTransport{recvs: []recvItem{ | |||
| 521 | + | {f: welcome("test", localLifecycle, 11)}, | |||
| 522 | + | {f: frameOf(ports.MsgCommittedRouteIdentity, mustMarshalCommittedIdentity(ports.CommittedRouteIdentity{ | |||
| 523 | + | Target: ports.ExactSessionTarget{LifecycleID: newLifecycle, SessionName: "new"}, | |||
| 524 | + | }))}, | |||
| 525 | + | {f: frameOf(ports.MsgNavigateRecentRoute, mustMarshalRouteAction(ports.RouteNavigationAction{SnapshotGeneration: 4, Key: 2, Generation: 2}))}, | |||
| 526 | + | }} | |||
| 527 | + | local3 := &recordingTransport{recvs: []recvItem{ | |||
| 528 | + | {f: welcome("test", localLifecycle, 11)}, | |||
| 529 | + | {f: frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))}, | |||
| 530 | + | }} | |||
| 531 | + | remote1 := &recordingTransport{recvs: []recvItem{ | |||
| 532 | + | {f: welcome("remote-manual", remoteLifecycle, 22)}, | |||
| 533 | + | {f: frameOf(ports.MsgNavigateRecentRoute, mustMarshalRouteAction(ports.RouteNavigationAction{SnapshotGeneration: 2, Key: 1, Generation: 1}))}, | |||
| 534 | + | }} | |||
| 535 | + | remote2 := &recordingTransport{recvs: []recvItem{ | |||
| 536 | + | {f: welcome("remote-manual", remoteLifecycle, 22)}, | |||
| 537 | + | {f: frameOf(ports.MsgNavigationAction, ports.MarshalNavigationAction(ports.NavigationOpenHomePicker))}, | |||
| 538 | + | }} | |||
| 539 | + | localDialer := &sequenceDialer{trs: []ports.Transport{local1, local2, local3}} | |||
| 540 | + | remoteDialer := &sequenceDialer{trs: []ports.Transport{remote1, remote2}} | |||
| 541 | + | ||||
| 542 | + | deps := testDependencies(localDialer, term, realClock{}, nil, nil) | |||
| 543 | + | deps.AttachHandoff = func(target ports.AttachTarget) (ports.Dialer, client.AttachRequest, error) { | |||
| 544 | + | require.Equal(t, remoteHandoff, target) | |||
| 545 | + | return remoteDialer, client.AttachRequest{ | |||
| 546 | + | Intent: ports.IntentAttach, SessionName: target.Session, Remote: true, | |||
| 547 | + | Origin: ports.RouteOriginDiscovery, OriginKey: target.Endpoint, | |||
| 548 | + | RemoteTarget: &remoteTarget, EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned, | |||
| 549 | + | }, nil | |||
| 550 | + | } | |||
| 551 | + | ||||
| 552 | + | err := runTestClient(context.Background(), deps, client.AttachRequest{ | |||
| 553 | + | Intent: ports.IntentAttach, SessionName: "test", Origin: ports.RouteOriginLocal, OriginKey: "local", | |||
| 554 | + | }) | |||
| 555 | + | remoteHello := helloFromSend(t, remote2) | |||
| 556 | + | require.NoError(t, err) | |||
| 557 | + | ||||
| 558 | + | require.Equal(t, ports.NavigationCapabilityHomePicker, remoteHello.NavigationCapabilities) | |||
| 559 | + | } | |||
| 560 | + | ||||
| 561 | + | func mustMarshalCommittedIdentity(identity ports.CommittedRouteIdentity) []byte { | |||
| 562 | + | payload, err := ports.MarshalCommittedRouteIdentity(identity) | |||
| 563 | + | if err != nil { | |||
| 564 | + | panic(err) | |||
| 565 | + | } | |||
| 566 | + | return payload | |||
| 567 | + | } | |||
| 568 | + | ||||
| 569 | + | func mustMarshalRouteAction(action ports.RouteNavigationAction) []byte { | |||
| 570 | + | payload, err := ports.MarshalRouteNavigationAction(action) | |||
| 571 | + | if err != nil { | |||
| 572 | + | panic(err) | |||
| 573 | + | } | |||
| 574 | + | return payload | |||
| 575 | + | } | |||
| 576 | + | ||||
| 486 | func TestAttachTargetHandoffReturnsValidatedTargetAndClosesTransport(t *testing.T) { | 577 | func TestAttachTargetHandoffReturnsValidatedTargetAndClosesTransport(t *testing.T) { | ||
| 487 | var out bytes.Buffer | 578 | var out bytes.Buffer | ||
| 488 | var restoreCount atomic.Int32 | 579 | var restoreCount atomic.Int32 | ||
A internal/usecase/daemon/attachment_route_identity_test.go +59 -0
| @@ -0,0 +1,59 @@ | |||
| 1 | + | package daemon | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "testing" | |
| 5 | + | ||
| 6 | + | "github.com/bnema/vev/internal/domain" | |
| 7 | + | "github.com/bnema/vev/internal/ports" | |
| 8 | + | "github.com/stretchr/testify/require" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | func TestAttachmentTransitionPublishesCommittedRouteIdentity(t *testing.T) { | |
| 12 | + | d, source, ac, _ := newManualSessionWithPTYs(t, nil) | |
| 13 | + | transport := &closeTrackingTransport{} | |
| 14 | + | ac.replaceTransport(transport) | |
| 15 | + | rc := d.attachCoordinator(source, nil, ac, true) | |
| 16 | + | token := source.attachmentToken(ac, transport) | |
| 17 | + | token.lease = rc.attachmentLease(ac) | |
| 18 | + | ac.publishAttachmentCapability(token) | |
| 19 | + | ac.setRouteSnapshot(ports.RecentRouteSnapshot{Generation: 1}) | |
| 20 | + | ||
| 21 | + | target := &session{ | |
| 22 | + | sessionCore: sessionCore{ | |
| 23 | + | id: "target", | |
| 24 | + | name: "target", | |
| 25 | + | incarnation: domain.IncarnationID{2}, | |
| 26 | + | }, | |
| 27 | + | ctx: source.ctx, | |
| 28 | + | cancel: func() {}, | |
| 29 | + | tabs: []*tab{newTab(nil, domain.Size{Cols: 80, Rows: 23})}, | |
| 30 | + | } | |
| 31 | + | d.mu.Lock() | |
| 32 | + | d.sessions[target.id] = target | |
| 33 | + | d.mu.Unlock() | |
| 34 | + | ||
| 35 | + | transition, err := d.transitionAttachment(attachmentTransitionRequest{ | |
| 36 | + | source: source, | |
| 37 | + | target: target, | |
| 38 | + | next: ac, | |
| 39 | + | expectedTransport: transportSnapshot{transport: transport, incarnation: ac.transportSnapshot().incarnation}, | |
| 40 | + | ready: true, | |
| 41 | + | }) | |
| 42 | + | require.NoError(t, err) | |
| 43 | + | d.deferAttachmentTransitionCleanups(transition) | |
| 44 | + | ||
| 45 | + | var identity ports.CommittedRouteIdentity | |
| 46 | + | found := false | |
| 47 | + | for _, frame := range transport.Sends() { | |
| 48 | + | if frame.Type != ports.MsgCommittedRouteIdentity { | |
| 49 | + | continue | |
| 50 | + | } | |
| 51 | + | var decodeErr error | |
| 52 | + | identity, decodeErr = ports.UnmarshalCommittedRouteIdentity(frame.Payload) | |
| 53 | + | require.NoError(t, decodeErr) | |
| 54 | + | found = true | |
| 55 | + | } | |
| 56 | + | require.True(t, found, "attachment transitions must publish their committed route identity") | |
| 57 | + | require.Equal(t, "target", identity.Target.SessionName) | |
| 58 | + | require.Equal(t, domain.IncarnationID{2}, identity.Target.LifecycleID) | |
| 59 | + | } | |
| @@ -0,0 +1,59 @@ | |||||
| 1 | + | package daemon | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "testing" | |||
| 5 | + | ||||
| 6 | + | "github.com/bnema/vev/internal/domain" | |||
| 7 | + | "github.com/bnema/vev/internal/ports" | |||
| 8 | + | "github.com/stretchr/testify/require" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | func TestAttachmentTransitionPublishesCommittedRouteIdentity(t *testing.T) { | |||
| 12 | + | d, source, ac, _ := newManualSessionWithPTYs(t, nil) | |||
| 13 | + | transport := &closeTrackingTransport{} | |||
| 14 | + | ac.replaceTransport(transport) | |||
| 15 | + | rc := d.attachCoordinator(source, nil, ac, true) | |||
| 16 | + | token := source.attachmentToken(ac, transport) | |||
| 17 | + | token.lease = rc.attachmentLease(ac) | |||
| 18 | + | ac.publishAttachmentCapability(token) | |||
| 19 | + | ac.setRouteSnapshot(ports.RecentRouteSnapshot{Generation: 1}) | |||
| 20 | + | ||||
| 21 | + | target := &session{ | |||
| 22 | + | sessionCore: sessionCore{ | |||
| 23 | + | id: "target", | |||
| 24 | + | name: "target", | |||
| 25 | + | incarnation: domain.IncarnationID{2}, | |||
| 26 | + | }, | |||
| 27 | + | ctx: source.ctx, | |||
| 28 | + | cancel: func() {}, | |||
| 29 | + | tabs: []*tab{newTab(nil, domain.Size{Cols: 80, Rows: 23})}, | |||
| 30 | + | } | |||
| 31 | + | d.mu.Lock() | |||
| 32 | + | d.sessions[target.id] = target | |||
| 33 | + | d.mu.Unlock() | |||
| 34 | + | ||||
| 35 | + | transition, err := d.transitionAttachment(attachmentTransitionRequest{ | |||
| 36 | + | source: source, | |||
| 37 | + | target: target, | |||
| 38 | + | next: ac, | |||
| 39 | + | expectedTransport: transportSnapshot{transport: transport, incarnation: ac.transportSnapshot().incarnation}, | |||
| 40 | + | ready: true, | |||
| 41 | + | }) | |||
| 42 | + | require.NoError(t, err) | |||
| 43 | + | d.deferAttachmentTransitionCleanups(transition) | |||
| 44 | + | ||||
| 45 | + | var identity ports.CommittedRouteIdentity | |||
| 46 | + | found := false | |||
| 47 | + | for _, frame := range transport.Sends() { | |||
| 48 | + | if frame.Type != ports.MsgCommittedRouteIdentity { | |||
| 49 | + | continue | |||
| 50 | + | } | |||
| 51 | + | var decodeErr error | |||
| 52 | + | identity, decodeErr = ports.UnmarshalCommittedRouteIdentity(frame.Payload) | |||
| 53 | + | require.NoError(t, decodeErr) | |||
| 54 | + | found = true | |||
| 55 | + | } | |||
| 56 | + | require.True(t, found, "attachment transitions must publish their committed route identity") | |||
| 57 | + | require.Equal(t, "target", identity.Target.SessionName) | |||
| 58 | + | require.Equal(t, domain.IncarnationID{2}, identity.Target.LifecycleID) | |||
| 59 | + | } | |||
M internal/usecase/daemon/attachment_transition.go +8 -2
| @@ -167,17 +167,23 @@ func (d *Daemon) transitionAttachment(req attachmentTransitionRequest) (attachme | |||
| 167 | 167 | } | |
| 168 | 168 | ||
| 169 | 169 | frozen, err := d.freezeAttachmentTransition(req, participants) | |
| 170 | - | defer frozen.unfreeze() | |
| 171 | 170 | if err != nil { | |
| 171 | + | frozen.unfreeze() | |
| 172 | 172 | return attachmentTransitionResult{}, err | |
| 173 | 173 | } | |
| 174 | 174 | ||
| 175 | 175 | result, err := d.publishAttachmentTransition(req) | |
| 176 | + | frozen.unfreeze() | |
| 176 | 177 | if err != nil { | |
| 177 | 178 | return result, err | |
| 178 | 179 | } | |
| 179 | 180 | if result.published.ac != nil && result.published.ac.routeSnapshotCopy().Generation != 0 { | |
| 180 | - | if identityErr := d.sendCommittedRouteIdentityForAttachment(result.published); identityErr != nil { | |
| 181 | + | identityErr := errAttachmentTransition | |
| 182 | + | if effect, admitted := result.published.ac.beginAttachmentEffect(result.published); admitted { | |
| 183 | + | identityErr = d.sendCommittedRouteIdentityForAttachment(effect.connectionToken()) | |
| 184 | + | effect.End() | |
| 185 | + | } | |
| 186 | + | if identityErr != nil { | |
| 181 | 187 | d.log.Warn("publishing committed route identity failed", "err", identityErr, "action", req.action) | |
| 182 | 188 | } | |
| 183 | 189 | } | |
| @@ -167,17 +167,23 @@ func (d *Daemon) transitionAttachment(req attachmentTransitionRequest) (attachme | |||||
| 167 | } | 167 | } | ||
| 168 | 168 | ||||
| 169 | frozen, err := d.freezeAttachmentTransition(req, participants) | 169 | frozen, err := d.freezeAttachmentTransition(req, participants) | ||
| 170 | - | defer frozen.unfreeze() | |||
| 171 | if err != nil { | 170 | if err != nil { | ||
| 171 | + | frozen.unfreeze() | |||
| 172 | return attachmentTransitionResult{}, err | 172 | return attachmentTransitionResult{}, err | ||
| 173 | } | 173 | } | ||
| 174 | 174 | ||||
| 175 | result, err := d.publishAttachmentTransition(req) | 175 | result, err := d.publishAttachmentTransition(req) | ||
| 176 | + | frozen.unfreeze() | |||
| 176 | if err != nil { | 177 | if err != nil { | ||
| 177 | return result, err | 178 | return result, err | ||
| 178 | } | 179 | } | ||
| 179 | if result.published.ac != nil && result.published.ac.routeSnapshotCopy().Generation != 0 { | 180 | if result.published.ac != nil && result.published.ac.routeSnapshotCopy().Generation != 0 { | ||
| 180 | - | if identityErr := d.sendCommittedRouteIdentityForAttachment(result.published); identityErr != nil { | 181 | + | identityErr := errAttachmentTransition |
| 182 | + | if effect, admitted := result.published.ac.beginAttachmentEffect(result.published); admitted { | |||
| 183 | + | identityErr = d.sendCommittedRouteIdentityForAttachment(effect.connectionToken()) | |||
| 184 | + | effect.End() | |||
| 185 | + | } | |||
| 186 | + | if identityErr != nil { | |||
| 181 | d.log.Warn("publishing committed route identity failed", "err", identityErr, "action", req.action) | 187 | d.log.Warn("publishing committed route identity failed", "err", identityErr, "action", req.action) | ||
| 182 | } | 188 | } | ||
| 183 | } | 189 | } | ||