fix(remote): harden navigation and attachment concurrency
- commit
- 310aed63ff6f66e25d74bb66be1f851594ce8c2c
- author
- brice · · 2026-08-08 17:33 UTC
- parent
- d63c59a
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
12 files · +74 -37
M internal/ports/navigation_test.go +2 -2
| @@ -17,8 +17,8 @@ func TestNavigationActionWireTable(t *testing.T) { | |||
| 17 | 17 | }{ | |
| 18 | 18 | {name: "open home", action: NavigationOpenHomePicker, payload: []byte{1}, valid: true}, | |
| 19 | 19 | {name: "back", action: NavigationBack, payload: []byte{2}, valid: true}, | |
| 20 | - | {name: "zero", action: 0, valid: false}, | |
| 21 | - | {name: "unknown", action: 3, valid: false}, | |
| 20 | + | {name: "zero", action: 0, payload: []byte{0}, valid: false}, | |
| 21 | + | {name: "unknown", action: 3, payload: []byte{3}, valid: false}, | |
| 22 | 22 | } | |
| 23 | 23 | ||
| 24 | 24 | for _, tt := range tests { | |
| @@ -17,8 +17,8 @@ func TestNavigationActionWireTable(t *testing.T) { | |||||
| 17 | }{ | 17 | }{ | ||
| 18 | {name: "open home", action: NavigationOpenHomePicker, payload: []byte{1}, valid: true}, | 18 | {name: "open home", action: NavigationOpenHomePicker, payload: []byte{1}, valid: true}, | ||
| 19 | {name: "back", action: NavigationBack, payload: []byte{2}, valid: true}, | 19 | {name: "back", action: NavigationBack, payload: []byte{2}, valid: true}, | ||
| 20 | - | {name: "zero", action: 0, valid: false}, | 20 | + | {name: "zero", action: 0, payload: []byte{0}, valid: false}, |
| 21 | - | {name: "unknown", action: 3, valid: false}, | 21 | + | {name: "unknown", action: 3, payload: []byte{3}, valid: false}, |
| 22 | } | 22 | } | ||
| 23 | 23 | ||||
| 24 | for _, tt := range tests { | 24 | for _, tt := range tests { | ||
M internal/ports/remote_target_wire_test.go +14 -10
| @@ -2,6 +2,7 @@ package ports | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "bytes" | |
| 5 | + | "errors" | |
| 5 | 6 | "testing" | |
| 6 | 7 | ||
| 7 | 8 | "github.com/bnema/vev/internal/domain" | |
| @@ -82,11 +83,12 @@ func TestRemoteTargetWireRoundTripPreservesExactSelector(t *testing.T) { | |||
| 82 | 83 | func TestRemoteTargetWireRejectsClientOwnedEnvironmentPolicy(t *testing.T) { | |
| 83 | 84 | target := testRemoteTarget(false) | |
| 84 | 85 | tests := []struct { | |
| 85 | - | name string | |
| 86 | - | validate func() error | |
| 87 | - | marshalBad func() []byte | |
| 88 | - | marshalGood func() []byte | |
| 89 | - | unmarshal func([]byte) error | |
| 86 | + | name string | |
| 87 | + | validate func() error | |
| 88 | + | marshalBad func() []byte | |
| 89 | + | marshalGood func() []byte | |
| 90 | + | unmarshal func([]byte) error | |
| 91 | + | policyOffset int | |
| 90 | 92 | }{ | |
| 91 | 93 | { | |
| 92 | 94 | name: "hello", | |
| @@ -111,7 +113,8 @@ func TestRemoteTargetWireRejectsClientOwnedEnvironmentPolicy(t *testing.T) { | |||
| 111 | 113 | RemoteTarget: target, EnvironmentPolicy: EnvironmentPolicyDaemonOwned, | |
| 112 | 114 | }) | |
| 113 | 115 | }, | |
| 114 | - | unmarshal: func(payload []byte) error { _, err := UnmarshalHello(payload); return err }, | |
| 116 | + | unmarshal: func(payload []byte) error { _, err := UnmarshalHello(payload); return err }, | |
| 117 | + | policyOffset: 3, | |
| 115 | 118 | }, | |
| 116 | 119 | { | |
| 117 | 120 | name: "attach target", | |
| @@ -133,7 +136,8 @@ func TestRemoteTargetWireRejectsClientOwnedEnvironmentPolicy(t *testing.T) { | |||
| 133 | 136 | RemoteTarget: target, EnvironmentPolicy: EnvironmentPolicyDaemonOwned, | |
| 134 | 137 | }) | |
| 135 | 138 | }, | |
| 136 | - | unmarshal: func(payload []byte) error { _, err := UnmarshalAttachTarget(payload); return err }, | |
| 139 | + | unmarshal: func(payload []byte) error { _, err := UnmarshalAttachTarget(payload); return err }, | |
| 140 | + | policyOffset: 1, | |
| 137 | 141 | }, | |
| 138 | 142 | } | |
| 139 | 143 | ||
| @@ -158,7 +162,7 @@ func TestRemoteTargetWireRejectsClientOwnedEnvironmentPolicy(t *testing.T) { | |||
| 158 | 162 | if err := tt.unmarshal(trailing); err == nil { | |
| 159 | 163 | t.Fatal("trailing garbage unexpectedly decoded") | |
| 160 | 164 | } | |
| 161 | - | payload[len(payload)-3] = byte(EnvironmentPolicyClientOwned) | |
| 165 | + | payload[len(payload)-tt.policyOffset] = byte(EnvironmentPolicyClientOwned) | |
| 162 | 166 | if err := tt.unmarshal(payload); err == nil { | |
| 163 | 167 | t.Fatal("unmarshal accepted client-owned policy for remote target") | |
| 164 | 168 | } | |
| @@ -166,7 +170,7 @@ func TestRemoteTargetWireRejectsClientOwnedEnvironmentPolicy(t *testing.T) { | |||
| 166 | 170 | } | |
| 167 | 171 | } | |
| 168 | 172 | ||
| 169 | - | func TestAttachTargetRejectsTokenlessResume(t *testing.T) { | |
| 173 | + | func TestAttachTargetRejectsResumeIntent(t *testing.T) { | |
| 170 | 174 | tests := []AttachTarget{ | |
| 171 | 175 | {Endpoint: "host", Session: "work", Intent: IntentResume}, | |
| 172 | 176 | { | |
| @@ -178,7 +182,7 @@ func TestAttachTargetRejectsTokenlessResume(t *testing.T) { | |||
| 178 | 182 | }, | |
| 179 | 183 | } | |
| 180 | 184 | for _, target := range tests { | |
| 181 | - | if err := ValidateAttachTarget(target); err != ErrInvalidAttachTarget { | |
| 185 | + | if err := ValidateAttachTarget(target); !errors.Is(err, ErrInvalidAttachTarget) { | |
| 182 | 186 | t.Fatalf("ValidateAttachTarget(%#v) error = %v, want %v", target, err, ErrInvalidAttachTarget) | |
| 183 | 187 | } | |
| 184 | 188 | if got := MarshalAttachTarget(target); got != nil { | |
| @@ -2,6 +2,7 @@ package ports | |||||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | "bytes" | 4 | "bytes" | ||
| 5 | + | "errors" | |||
| 5 | "testing" | 6 | "testing" | ||
| 6 | 7 | ||||
| 7 | "github.com/bnema/vev/internal/domain" | 8 | "github.com/bnema/vev/internal/domain" | ||
| @@ -82,11 +83,12 @@ func TestRemoteTargetWireRoundTripPreservesExactSelector(t *testing.T) { | |||||
| 82 | func TestRemoteTargetWireRejectsClientOwnedEnvironmentPolicy(t *testing.T) { | 83 | func TestRemoteTargetWireRejectsClientOwnedEnvironmentPolicy(t *testing.T) { | ||
| 83 | target := testRemoteTarget(false) | 84 | target := testRemoteTarget(false) | ||
| 84 | tests := []struct { | 85 | tests := []struct { | ||
| 85 | - | name string | 86 | + | name string |
| 86 | - | validate func() error | 87 | + | validate func() error |
| 87 | - | marshalBad func() []byte | 88 | + | marshalBad func() []byte |
| 88 | - | marshalGood func() []byte | 89 | + | marshalGood func() []byte |
| 89 | - | unmarshal func([]byte) error | 90 | + | unmarshal func([]byte) error |
| 91 | + | policyOffset int | |||
| 90 | }{ | 92 | }{ | ||
| 91 | { | 93 | { | ||
| 92 | name: "hello", | 94 | name: "hello", | ||
| @@ -111,7 +113,8 @@ func TestRemoteTargetWireRejectsClientOwnedEnvironmentPolicy(t *testing.T) { | |||||
| 111 | RemoteTarget: target, EnvironmentPolicy: EnvironmentPolicyDaemonOwned, | 113 | RemoteTarget: target, EnvironmentPolicy: EnvironmentPolicyDaemonOwned, | ||
| 112 | }) | 114 | }) | ||
| 113 | }, | 115 | }, | ||
| 114 | - | unmarshal: func(payload []byte) error { _, err := UnmarshalHello(payload); return err }, | 116 | + | unmarshal: func(payload []byte) error { _, err := UnmarshalHello(payload); return err }, |
| 117 | + | policyOffset: 3, | |||
| 115 | }, | 118 | }, | ||
| 116 | { | 119 | { | ||
| 117 | name: "attach target", | 120 | name: "attach target", | ||
| @@ -133,7 +136,8 @@ func TestRemoteTargetWireRejectsClientOwnedEnvironmentPolicy(t *testing.T) { | |||||
| 133 | RemoteTarget: target, EnvironmentPolicy: EnvironmentPolicyDaemonOwned, | 136 | RemoteTarget: target, EnvironmentPolicy: EnvironmentPolicyDaemonOwned, | ||
| 134 | }) | 137 | }) | ||
| 135 | }, | 138 | }, | ||
| 136 | - | unmarshal: func(payload []byte) error { _, err := UnmarshalAttachTarget(payload); return err }, | 139 | + | unmarshal: func(payload []byte) error { _, err := UnmarshalAttachTarget(payload); return err }, |
| 140 | + | policyOffset: 1, | |||
| 137 | }, | 141 | }, | ||
| 138 | } | 142 | } | ||
| 139 | 143 | ||||
| @@ -158,7 +162,7 @@ func TestRemoteTargetWireRejectsClientOwnedEnvironmentPolicy(t *testing.T) { | |||||
| 158 | if err := tt.unmarshal(trailing); err == nil { | 162 | if err := tt.unmarshal(trailing); err == nil { | ||
| 159 | t.Fatal("trailing garbage unexpectedly decoded") | 163 | t.Fatal("trailing garbage unexpectedly decoded") | ||
| 160 | } | 164 | } | ||
| 161 | - | payload[len(payload)-3] = byte(EnvironmentPolicyClientOwned) | 165 | + | payload[len(payload)-tt.policyOffset] = byte(EnvironmentPolicyClientOwned) |
| 162 | if err := tt.unmarshal(payload); err == nil { | 166 | if err := tt.unmarshal(payload); err == nil { | ||
| 163 | t.Fatal("unmarshal accepted client-owned policy for remote target") | 167 | t.Fatal("unmarshal accepted client-owned policy for remote target") | ||
| 164 | } | 168 | } | ||
| @@ -166,7 +170,7 @@ func TestRemoteTargetWireRejectsClientOwnedEnvironmentPolicy(t *testing.T) { | |||||
| 166 | } | 170 | } | ||
| 167 | } | 171 | } | ||
| 168 | 172 | ||||
| 169 | - | func TestAttachTargetRejectsTokenlessResume(t *testing.T) { | 173 | + | func TestAttachTargetRejectsResumeIntent(t *testing.T) { |
| 170 | tests := []AttachTarget{ | 174 | tests := []AttachTarget{ | ||
| 171 | {Endpoint: "host", Session: "work", Intent: IntentResume}, | 175 | {Endpoint: "host", Session: "work", Intent: IntentResume}, | ||
| 172 | { | 176 | { | ||
| @@ -178,7 +182,7 @@ func TestAttachTargetRejectsTokenlessResume(t *testing.T) { | |||||
| 178 | }, | 182 | }, | ||
| 179 | } | 183 | } | ||
| 180 | for _, target := range tests { | 184 | for _, target := range tests { | ||
| 181 | - | if err := ValidateAttachTarget(target); err != ErrInvalidAttachTarget { | 185 | + | if err := ValidateAttachTarget(target); !errors.Is(err, ErrInvalidAttachTarget) { |
| 182 | t.Fatalf("ValidateAttachTarget(%#v) error = %v, want %v", target, err, ErrInvalidAttachTarget) | 186 | t.Fatalf("ValidateAttachTarget(%#v) error = %v, want %v", target, err, ErrInvalidAttachTarget) | ||
| 183 | } | 187 | } | ||
| 184 | if got := MarshalAttachTarget(target); got != nil { | 188 | if got := MarshalAttachTarget(target); got != nil { | ||
M internal/ports/wire.go +1 -1
| @@ -708,7 +708,7 @@ func ValidateHello(h Hello) error { | |||
| 708 | 708 | return ErrInvalidHello | |
| 709 | 709 | } | |
| 710 | 710 | if err := validateHelloNavigation(h); err != nil { | |
| 711 | - | return fmt.Errorf("%w: navigation", ErrInvalidHello) | |
| 711 | + | return fmt.Errorf("%w: navigation: %w", ErrInvalidHello, err) | |
| 712 | 712 | } | |
| 713 | 713 | if len(h.Name) > math.MaxUint16 || len(h.TermEnv) > math.MaxUint16 || len(h.Cwd) > math.MaxUint16 || len(h.Env) > math.MaxUint32 { | |
| 714 | 714 | return ErrInvalidHello | |
| @@ -708,7 +708,7 @@ func ValidateHello(h Hello) error { | |||||
| 708 | return ErrInvalidHello | 708 | return ErrInvalidHello | ||
| 709 | } | 709 | } | ||
| 710 | if err := validateHelloNavigation(h); err != nil { | 710 | if err := validateHelloNavigation(h); err != nil { | ||
| 711 | - | return fmt.Errorf("%w: navigation", ErrInvalidHello) | 711 | + | return fmt.Errorf("%w: navigation: %w", ErrInvalidHello, err) |
| 712 | } | 712 | } | ||
| 713 | if len(h.Name) > math.MaxUint16 || len(h.TermEnv) > math.MaxUint16 || len(h.Cwd) > math.MaxUint16 || len(h.Env) > math.MaxUint32 { | 713 | if len(h.Name) > math.MaxUint16 || len(h.TermEnv) > math.MaxUint16 || len(h.Cwd) > math.MaxUint16 || len(h.Env) > math.MaxUint32 { | ||
| 714 | return ErrInvalidHello | 714 | return ErrInvalidHello | ||
M internal/ports/wire_test.go +1 -1
| @@ -95,7 +95,7 @@ func TestHelloGoldenAndRoundTrip(t *testing.T) { | |||
| 95 | 95 | want := append(append([]byte(nil), tt.want...), 0, 0) | |
| 96 | 96 | got := MarshalHello(tt.msg) | |
| 97 | 97 | if !bytes.Equal(got, want) { | |
| 98 | - | t.Fatalf("MarshalHello() = %#v, want %#v", got, tt.want) | |
| 98 | + | t.Fatalf("MarshalHello() = %#v, want %#v", got, want) | |
| 99 | 99 | } | |
| 100 | 100 | back, err := UnmarshalHello(got) | |
| 101 | 101 | if err != nil { | |
| @@ -95,7 +95,7 @@ func TestHelloGoldenAndRoundTrip(t *testing.T) { | |||||
| 95 | want := append(append([]byte(nil), tt.want...), 0, 0) | 95 | want := append(append([]byte(nil), tt.want...), 0, 0) | ||
| 96 | got := MarshalHello(tt.msg) | 96 | got := MarshalHello(tt.msg) | ||
| 97 | if !bytes.Equal(got, want) { | 97 | if !bytes.Equal(got, want) { | ||
| 98 | - | t.Fatalf("MarshalHello() = %#v, want %#v", got, tt.want) | 98 | + | t.Fatalf("MarshalHello() = %#v, want %#v", got, want) |
| 99 | } | 99 | } | ||
| 100 | back, err := UnmarshalHello(got) | 100 | back, err := UnmarshalHello(got) | ||
| 101 | if err != nil { | 101 | if err != nil { | ||
M internal/usecase/client/client.go +5 -1
| @@ -253,7 +253,7 @@ func validateAttachRequest(request AttachRequest) error { | |||
| 253 | 253 | return fmt.Errorf("vev: invalid session name: %w", err) | |
| 254 | 254 | } | |
| 255 | 255 | } | |
| 256 | - | if err := ports.ValidateNavigation(request.NavigationCapabilities, request.StartupOverlay, request.Remote || request.RemoteTarget != nil); err != nil { | |
| 256 | + | if err := ports.ValidateNavigation(request.NavigationCapabilities, request.StartupOverlay, request.RemoteTarget != nil || request.EnvironmentPolicy == ports.EnvironmentPolicyDaemonOwned); err != nil { | |
| 257 | 257 | return fmt.Errorf("vev: invalid navigation route: %w", err) | |
| 258 | 258 | } | |
| 259 | 259 | if request.RemoteTarget == nil { | |
| @@ -514,6 +514,9 @@ func (r *Runner) Run(ctx context.Context, request AttachRequest) (retErr error) | |||
| 514 | 514 | if result.target.RemoteTarget != nil || result.target.Endpoint != "" { | |
| 515 | 515 | if homeRoute == nil && attemptRequest.RemoteTarget == nil { | |
| 516 | 516 | routeRequest := attemptRequest | |
| 517 | + | if result.sessionName != "" { | |
| 518 | + | routeRequest.SessionName = result.sessionName | |
| 519 | + | } | |
| 517 | 520 | routeRequest.Intent = ports.IntentAttach | |
| 518 | 521 | routeRequest.NavigationCapabilities = 0 | |
| 519 | 522 | routeRequest.StartupOverlay = ports.StartupOverlayNone | |
| @@ -589,6 +592,7 @@ func (r *Runner) Run(ctx context.Context, request AttachRequest) (retErr error) | |||
| 589 | 592 | if returnNavigationPending && !returnResumeFallback && homeRoute != nil { | |
| 590 | 593 | returnRoute = nil | |
| 591 | 594 | returnNavigationPending = false | |
| 595 | + | dialer = homeRoute.dialer | |
| 592 | 596 | attemptRequest = homeRoute.request | |
| 593 | 597 | attemptRequest.Intent = ports.IntentAttach | |
| 594 | 598 | attemptRequest.NavigationCapabilities = ports.NavigationCapabilityBack | |
| @@ -253,7 +253,7 @@ func validateAttachRequest(request AttachRequest) error { | |||||
| 253 | return fmt.Errorf("vev: invalid session name: %w", err) | 253 | return fmt.Errorf("vev: invalid session name: %w", err) | ||
| 254 | } | 254 | } | ||
| 255 | } | 255 | } | ||
| 256 | - | if err := ports.ValidateNavigation(request.NavigationCapabilities, request.StartupOverlay, request.Remote || request.RemoteTarget != nil); err != nil { | 256 | + | if err := ports.ValidateNavigation(request.NavigationCapabilities, request.StartupOverlay, request.RemoteTarget != nil || request.EnvironmentPolicy == ports.EnvironmentPolicyDaemonOwned); err != nil { |
| 257 | return fmt.Errorf("vev: invalid navigation route: %w", err) | 257 | return fmt.Errorf("vev: invalid navigation route: %w", err) | ||
| 258 | } | 258 | } | ||
| 259 | if request.RemoteTarget == nil { | 259 | if request.RemoteTarget == nil { | ||
| @@ -514,6 +514,9 @@ func (r *Runner) Run(ctx context.Context, request AttachRequest) (retErr error) | |||||
| 514 | if result.target.RemoteTarget != nil || result.target.Endpoint != "" { | 514 | if result.target.RemoteTarget != nil || result.target.Endpoint != "" { | ||
| 515 | if homeRoute == nil && attemptRequest.RemoteTarget == nil { | 515 | if homeRoute == nil && attemptRequest.RemoteTarget == nil { | ||
| 516 | routeRequest := attemptRequest | 516 | routeRequest := attemptRequest | ||
| 517 | + | if result.sessionName != "" { | |||
| 518 | + | routeRequest.SessionName = result.sessionName | |||
| 519 | + | } | |||
| 517 | routeRequest.Intent = ports.IntentAttach | 520 | routeRequest.Intent = ports.IntentAttach | ||
| 518 | routeRequest.NavigationCapabilities = 0 | 521 | routeRequest.NavigationCapabilities = 0 | ||
| 519 | routeRequest.StartupOverlay = ports.StartupOverlayNone | 522 | routeRequest.StartupOverlay = ports.StartupOverlayNone | ||
| @@ -589,6 +592,7 @@ func (r *Runner) Run(ctx context.Context, request AttachRequest) (retErr error) | |||||
| 589 | if returnNavigationPending && !returnResumeFallback && homeRoute != nil { | 592 | if returnNavigationPending && !returnResumeFallback && homeRoute != nil { | ||
| 590 | returnRoute = nil | 593 | returnRoute = nil | ||
| 591 | returnNavigationPending = false | 594 | returnNavigationPending = false | ||
| 595 | + | dialer = homeRoute.dialer | |||
| 592 | attemptRequest = homeRoute.request | 596 | attemptRequest = homeRoute.request | ||
| 593 | attemptRequest.Intent = ports.IntentAttach | 597 | attemptRequest.Intent = ports.IntentAttach | ||
| 594 | attemptRequest.NavigationCapabilities = ports.NavigationCapabilityBack | 598 | attemptRequest.NavigationCapabilities = ports.NavigationCapabilityBack | ||
M internal/usecase/client/client_test.go +1 -0
| @@ -90,6 +90,7 @@ func (t *reconnectTestTimer) Reset(d time.Duration) bool { | |||
| 90 | 90 | t.mu.Lock() | |
| 91 | 91 | t.duration = d | |
| 92 | 92 | t.fired = false | |
| 93 | + | t.stopped = false | |
| 93 | 94 | t.mu.Unlock() | |
| 94 | 95 | return true | |
| 95 | 96 | } | |
| @@ -90,6 +90,7 @@ func (t *reconnectTestTimer) Reset(d time.Duration) bool { | |||||
| 90 | t.mu.Lock() | 90 | t.mu.Lock() | ||
| 91 | t.duration = d | 91 | t.duration = d | ||
| 92 | t.fired = false | 92 | t.fired = false | ||
| 93 | + | t.stopped = false | |||
| 93 | t.mu.Unlock() | 94 | t.mu.Unlock() | ||
| 94 | return true | 95 | return true | ||
| 95 | } | 96 | } | ||
M internal/usecase/daemon/client.go +7 -2
| @@ -211,7 +211,12 @@ func (ac *attachedClient) clearGeometryClaim() { | |||
| 211 | 211 | ac.sizeMu.Unlock() | |
| 212 | 212 | } | |
| 213 | 213 | ||
| 214 | - | func (ac *attachedClient) currentAttachmentSession() *session { return ac.sess.Get() } | |
| 214 | + | func (ac *attachedClient) currentAttachmentSession() *session { | |
| 215 | + | if ac == nil { | |
| 216 | + | return nil | |
| 217 | + | } | |
| 218 | + | return ac.sess.Get() | |
| 219 | + | } | |
| 215 | 220 | ||
| 216 | 221 | // currentSession narrows the attachment to its owning local session. | |
| 217 | 222 | func (ac *attachedClient) currentSession() *session { | |
| @@ -847,7 +852,7 @@ func (d *Daemon) applyThemeForAttachment(token attachmentConnectionToken, msg po | |||
| 847 | 852 | if !token.attachmentEffectCurrent() { | |
| 848 | 853 | return | |
| 849 | 854 | } | |
| 850 | - | if rc := token.sess.core().coordinator.Load(); rc != nil { | |
| 855 | + | if rc := sess.renderCoordinator(); rc != nil { | |
| 851 | 856 | rc.invalidateForLease(token.ac, token.lease, renderInvalidation{class: invalidateUrgent, reset: true, producer: "client.go"}) | |
| 852 | 857 | } | |
| 853 | 858 | } | |
| @@ -211,7 +211,12 @@ func (ac *attachedClient) clearGeometryClaim() { | |||||
| 211 | ac.sizeMu.Unlock() | 211 | ac.sizeMu.Unlock() | ||
| 212 | } | 212 | } | ||
| 213 | 213 | ||||
| 214 | - | func (ac *attachedClient) currentAttachmentSession() *session { return ac.sess.Get() } | 214 | + | func (ac *attachedClient) currentAttachmentSession() *session { |
| 215 | + | if ac == nil { | |||
| 216 | + | return nil | |||
| 217 | + | } | |||
| 218 | + | return ac.sess.Get() | |||
| 219 | + | } | |||
| 215 | 220 | ||||
| 216 | // currentSession narrows the attachment to its owning local session. | 221 | // currentSession narrows the attachment to its owning local session. | ||
| 217 | func (ac *attachedClient) currentSession() *session { | 222 | func (ac *attachedClient) currentSession() *session { | ||
| @@ -847,7 +852,7 @@ func (d *Daemon) applyThemeForAttachment(token attachmentConnectionToken, msg po | |||||
| 847 | if !token.attachmentEffectCurrent() { | 852 | if !token.attachmentEffectCurrent() { | ||
| 848 | return | 853 | return | ||
| 849 | } | 854 | } | ||
| 850 | - | if rc := token.sess.core().coordinator.Load(); rc != nil { | 855 | + | if rc := sess.renderCoordinator(); rc != nil { |
| 851 | rc.invalidateForLease(token.ac, token.lease, renderInvalidation{class: invalidateUrgent, reset: true, producer: "client.go"}) | 856 | rc.invalidateForLease(token.ac, token.lease, renderInvalidation{class: invalidateUrgent, reset: true, producer: "client.go"}) | ||
| 852 | } | 857 | } | ||
| 853 | } | 858 | } | ||
M internal/usecase/daemon/client_frame_routing.go +6 -4
| @@ -278,10 +278,12 @@ func (d *Daemon) ackOutput(token attachmentConnectionToken, epoch, state uint64) | |||
| 278 | 278 | return false | |
| 279 | 279 | } | |
| 280 | 280 | ac.sendMu.Lock() | |
| 281 | - | ac.output.ack(epoch, state) | |
| 281 | + | acknowledged := ac.output.ack(epoch, state) | |
| 282 | 282 | ac.sendMu.Unlock() | |
| 283 | - | if rc := token.sess.core().coordinator.Load(); rc != nil { | |
| 284 | - | rc.notifyAckForLease(token.lease) | |
| 283 | + | if acknowledged { | |
| 284 | + | if rc := token.sess.renderCoordinator(); rc != nil { | |
| 285 | + | rc.notifyAckForLease(token.lease) | |
| 286 | + | } | |
| 285 | 287 | } | |
| 286 | - | return true | |
| 288 | + | return acknowledged | |
| 287 | 289 | } | |
| @@ -278,10 +278,12 @@ func (d *Daemon) ackOutput(token attachmentConnectionToken, epoch, state uint64) | |||||
| 278 | return false | 278 | return false | ||
| 279 | } | 279 | } | ||
| 280 | ac.sendMu.Lock() | 280 | ac.sendMu.Lock() | ||
| 281 | - | ac.output.ack(epoch, state) | 281 | + | acknowledged := ac.output.ack(epoch, state) |
| 282 | ac.sendMu.Unlock() | 282 | ac.sendMu.Unlock() | ||
| 283 | - | if rc := token.sess.core().coordinator.Load(); rc != nil { | 283 | + | if acknowledged { |
| 284 | - | rc.notifyAckForLease(token.lease) | 284 | + | if rc := token.sess.renderCoordinator(); rc != nil { |
| 285 | + | rc.notifyAckForLease(token.lease) | |||
| 286 | + | } | |||
| 285 | } | 287 | } | ||
| 286 | - | return true | 288 | + | return acknowledged |
| 287 | } | 289 | } | ||
M internal/usecase/daemon/output_state_stream.go +4 -3
| @@ -250,17 +250,18 @@ func (s *outputStateStream) sideEffectLocked(data []byte, echoAck uint64) (ports | |||
| 250 | 250 | return marshalOutputState(data, epoch, 0, 0, echoAck, viewRevision, size, false) | |
| 251 | 251 | } | |
| 252 | 252 | ||
| 253 | - | func (s *outputStateStream) ack(epoch, state uint64) { | |
| 253 | + | func (s *outputStateStream) ack(epoch, state uint64) bool { | |
| 254 | 254 | if s == nil { | |
| 255 | - | return | |
| 255 | + | return false | |
| 256 | 256 | } | |
| 257 | 257 | s.lockView() | |
| 258 | 258 | defer s.unlockView() | |
| 259 | 259 | if epoch != s.currentEpochLocked() || state > s.next || state <= s.acked { | |
| 260 | - | return | |
| 260 | + | return false | |
| 261 | 261 | } | |
| 262 | 262 | s.acked = state | |
| 263 | 263 | s.publishOutstanding() | |
| 264 | + | return true | |
| 264 | 265 | } | |
| 265 | 266 | ||
| 266 | 267 | func (s *outputStateStream) rebase() { | |
| @@ -250,17 +250,18 @@ func (s *outputStateStream) sideEffectLocked(data []byte, echoAck uint64) (ports | |||||
| 250 | return marshalOutputState(data, epoch, 0, 0, echoAck, viewRevision, size, false) | 250 | return marshalOutputState(data, epoch, 0, 0, echoAck, viewRevision, size, false) | ||
| 251 | } | 251 | } | ||
| 252 | 252 | ||||
| 253 | - | func (s *outputStateStream) ack(epoch, state uint64) { | 253 | + | func (s *outputStateStream) ack(epoch, state uint64) bool { |
| 254 | if s == nil { | 254 | if s == nil { | ||
| 255 | - | return | 255 | + | return false |
| 256 | } | 256 | } | ||
| 257 | s.lockView() | 257 | s.lockView() | ||
| 258 | defer s.unlockView() | 258 | defer s.unlockView() | ||
| 259 | if epoch != s.currentEpochLocked() || state > s.next || state <= s.acked { | 259 | if epoch != s.currentEpochLocked() || state > s.next || state <= s.acked { | ||
| 260 | - | return | 260 | + | return false |
| 261 | } | 261 | } | ||
| 262 | s.acked = state | 262 | s.acked = state | ||
| 263 | s.publishOutstanding() | 263 | s.publishOutstanding() | ||
| 264 | + | return true | |||
| 264 | } | 265 | } | ||
| 265 | 266 | ||||
| 266 | func (s *outputStateStream) rebase() { | 267 | func (s *outputStateStream) rebase() { | ||
M internal/usecase/daemon/remote_picker_test.go +14 -3
| @@ -376,11 +376,22 @@ type fixedRemoteRefreshClock struct{ now time.Time } | |||
| 376 | 376 | func (c fixedRemoteRefreshClock) Now() time.Time { return c.now } | |
| 377 | 377 | func (fixedRemoteRefreshClock) NewTimer(time.Duration) ports.Timer { return stubTimer{} } | |
| 378 | 378 | ||
| 379 | - | type remotePickerClock struct{ now time.Time } | |
| 379 | + | type remotePickerClock struct { | |
| 380 | + | mu sync.Mutex | |
| 381 | + | now time.Time | |
| 382 | + | } | |
| 380 | 383 | ||
| 381 | - | func (c *remotePickerClock) Now() time.Time { return c.now } | |
| 384 | + | func (c *remotePickerClock) Now() time.Time { | |
| 385 | + | c.mu.Lock() | |
| 386 | + | defer c.mu.Unlock() | |
| 387 | + | return c.now | |
| 388 | + | } | |
| 382 | 389 | func (*remotePickerClock) NewTimer(time.Duration) ports.Timer { return stubTimer{} } | |
| 383 | - | func (c *remotePickerClock) Advance(d time.Duration) { c.now = c.now.Add(d) } | |
| 390 | + | func (c *remotePickerClock) Advance(d time.Duration) { | |
| 391 | + | c.mu.Lock() | |
| 392 | + | c.now = c.now.Add(d) | |
| 393 | + | c.mu.Unlock() | |
| 394 | + | } | |
| 384 | 395 | ||
| 385 | 396 | func requireNoRemoteLockViolations(t *testing.T, catalog *channelRemoteCatalog, cache *recordingRemoteCache) { | |
| 386 | 397 | t.Helper() | |
| @@ -376,11 +376,22 @@ type fixedRemoteRefreshClock struct{ now time.Time } | |||||
| 376 | func (c fixedRemoteRefreshClock) Now() time.Time { return c.now } | 376 | func (c fixedRemoteRefreshClock) Now() time.Time { return c.now } | ||
| 377 | func (fixedRemoteRefreshClock) NewTimer(time.Duration) ports.Timer { return stubTimer{} } | 377 | func (fixedRemoteRefreshClock) NewTimer(time.Duration) ports.Timer { return stubTimer{} } | ||
| 378 | 378 | ||||
| 379 | - | type remotePickerClock struct{ now time.Time } | 379 | + | type remotePickerClock struct { |
| 380 | + | mu sync.Mutex | |||
| 381 | + | now time.Time | |||
| 382 | + | } | |||
| 380 | 383 | ||||
| 381 | - | func (c *remotePickerClock) Now() time.Time { return c.now } | 384 | + | func (c *remotePickerClock) Now() time.Time { |
| 385 | + | c.mu.Lock() | |||
| 386 | + | defer c.mu.Unlock() | |||
| 387 | + | return c.now | |||
| 388 | + | } | |||
| 382 | func (*remotePickerClock) NewTimer(time.Duration) ports.Timer { return stubTimer{} } | 389 | func (*remotePickerClock) NewTimer(time.Duration) ports.Timer { return stubTimer{} } | ||
| 383 | - | func (c *remotePickerClock) Advance(d time.Duration) { c.now = c.now.Add(d) } | 390 | + | func (c *remotePickerClock) Advance(d time.Duration) { |
| 391 | + | c.mu.Lock() | |||
| 392 | + | c.now = c.now.Add(d) | |||
| 393 | + | c.mu.Unlock() | |||
| 394 | + | } | |||
| 384 | 395 | ||||
| 385 | func requireNoRemoteLockViolations(t *testing.T, catalog *channelRemoteCatalog, cache *recordingRemoteCache) { | 396 | func requireNoRemoteLockViolations(t *testing.T, catalog *channelRemoteCatalog, cache *recordingRemoteCache) { | ||
| 386 | t.Helper() | 397 | t.Helper() | ||
M internal/usecase/daemon/resume.go +10 -10
| @@ -70,7 +70,7 @@ func (d *Daemon) markParkingInFlight(sess *session, ac *attachedClient) uint64 { | |||
| 70 | 70 | ac.resumeToken = d.nextResumeTokenLocked() | |
| 71 | 71 | } | |
| 72 | 72 | d.ensureParkingInFlightLocked(sess, ac) | |
| 73 | - | d.log.Info("parking in flight", "session", sess.name) | |
| 73 | + | d.log.Info("parking in flight", "session", sess.nameSnapshot()) | |
| 74 | 74 | return ac.resumeToken | |
| 75 | 75 | } | |
| 76 | 76 | ||
| @@ -178,7 +178,7 @@ func (d *Daemon) waitParkingInFlight(h ports.Hello) bool { | |||
| 178 | 178 | d.mu.Unlock() | |
| 179 | 179 | return false | |
| 180 | 180 | } | |
| 181 | - | if h.Name != "" && pending.sess != nil && pending.sess.name != h.Name { | |
| 181 | + | if h.Name != "" && pending.sess != nil && pending.sess.nameSnapshot() != h.Name { | |
| 182 | 182 | d.mu.Unlock() | |
| 183 | 183 | return false | |
| 184 | 184 | } | |
| @@ -208,7 +208,7 @@ func (d *Daemon) prepareParkAttachment(sess *session, ac *attachedClient) bool { | |||
| 208 | 208 | // terminal session removal and can strand IntentResume waiters. Callers | |
| 209 | 209 | // that need the detach→park gap advertised must markParkingInFlight while | |
| 210 | 210 | // still the exact live owner. Direct parkAttachment still parks below. | |
| 211 | - | d.log.Info("parking client prepared", "session", sess.name) | |
| 211 | + | d.log.Info("parking client prepared", "session", sess.nameSnapshot()) | |
| 212 | 212 | return true | |
| 213 | 213 | } | |
| 214 | 214 | ||
| @@ -262,7 +262,7 @@ func (d *Daemon) parkAttachment(sess *session, ac *attachedClient) bool { | |||
| 262 | 262 | } | |
| 263 | 263 | oldSame.closeDone() | |
| 264 | 264 | } | |
| 265 | - | d.log.Info("client parked for resume", "session", sess.name, "grace", grace) | |
| 265 | + | d.log.Info("client parked for resume", "session", sess.nameSnapshot(), "grace", grace) | |
| 266 | 266 | d.watchParkedTimer(token, parked) | |
| 267 | 267 | return true | |
| 268 | 268 | } | |
| @@ -291,7 +291,7 @@ func (d *Daemon) expireParked(token uint64, parked *parkedAttachment) { | |||
| 291 | 291 | d.removeParkedLocked(token, parked) | |
| 292 | 292 | d.mu.Unlock() | |
| 293 | 293 | d.closePickerIfCurrent(parked.ac, nil, parked.pickerGeneration) | |
| 294 | - | d.log.Warn("parked client expired", "session", parked.sess.name) | |
| 294 | + | d.log.Warn("parked client expired", "session", parked.sess.nameSnapshot()) | |
| 295 | 295 | return | |
| 296 | 296 | } | |
| 297 | 297 | d.mu.Unlock() | |
| @@ -501,7 +501,7 @@ func (d *Daemon) resumeLiveAttachment(h ports.Hello, tr ports.Transport, sz doma | |||
| 501 | 501 | ||
| 502 | 502 | if clientMismatch { | |
| 503 | 503 | reason := "client id mismatch" | |
| 504 | - | d.log.Warn("resume rejected", "session", sess.name, "err", reason) | |
| 504 | + | d.log.Warn("resume rejected", "session", sess.nameSnapshot(), "err", reason) | |
| 505 | 505 | return nil, nil, false, &protoErr{ports.ErrNoSuchSession, "resume token is no longer valid"} | |
| 506 | 506 | } | |
| 507 | 507 | if !credentialMatch { | |
| @@ -549,7 +549,7 @@ func (d *Daemon) resumeLiveAttachment(h ports.Hello, tr ports.Transport, sz doma | |||
| 549 | 549 | return nil, nil, false, &protoErr{ports.ErrNoSuchSession, "resume token is no longer valid"} | |
| 550 | 550 | } | |
| 551 | 551 | _ = ac.closeCapturedTransport(oldSnap.transport) | |
| 552 | - | d.log.Info("live resume credential parked for reconnect", "session", sess.name) | |
| 552 | + | d.log.Info("live resume credential parked for reconnect", "session", sess.nameSnapshot()) | |
| 553 | 553 | return d.resumeParked(h, tr, sz) | |
| 554 | 554 | } | |
| 555 | 555 | ||
| @@ -606,7 +606,7 @@ func (d *Daemon) resumeParkedLocked(h ports.Hello, tr ports.Transport, sz domain | |||
| 606 | 606 | registered := d.sessions[sess.id] == sess | |
| 607 | 607 | if !registered { | |
| 608 | 608 | d.removeParkedLocked(h.ResumeToken, parked) | |
| 609 | - | d.log.Warn("resume rejected", "session", sess.name, "registered", false) | |
| 609 | + | d.log.Warn("resume rejected", "session", sess.nameSnapshot(), "registered", false) | |
| 610 | 610 | return nil, nil, false, &protoErr{ports.ErrNoSuchSession, "resume token is no longer valid"} | |
| 611 | 611 | } | |
| 612 | 612 | ac := parked.ac | |
| @@ -615,7 +615,7 @@ func (d *Daemon) resumeParkedLocked(h ports.Hello, tr ports.Transport, sz domain | |||
| 615 | 615 | } | |
| 616 | 616 | if ac.clientID != h.ClientID { | |
| 617 | 617 | reason := "client id mismatch" | |
| 618 | - | d.log.Warn("resume rejected", "session", sess.name, "err", reason) | |
| 618 | + | d.log.Warn("resume rejected", "session", sess.nameSnapshot(), "err", reason) | |
| 619 | 619 | return nil, nil, false, &protoErr{ports.ErrNoSuchSession, "resume token is no longer valid"} | |
| 620 | 620 | } | |
| 621 | 621 | if h.RemoteTarget != nil && !d.remoteTargetMatchesSessionLocked(sess, *h.RemoteTarget) { | |
| @@ -663,6 +663,6 @@ func (d *Daemon) resumeParkedLocked(h ports.Hello, tr ports.Transport, sz domain | |||
| 663 | 663 | } | |
| 664 | 664 | d.deferAttachmentTransitionCleanups(transition) | |
| 665 | 665 | d.touchMRU(sess) | |
| 666 | - | d.log.Info("client resumed", "session", sess.name) | |
| 666 | + | d.log.Info("client resumed", "session", sess.nameSnapshot()) | |
| 667 | 667 | return sess, ac, true, nil | |
| 668 | 668 | } | |
| @@ -70,7 +70,7 @@ func (d *Daemon) markParkingInFlight(sess *session, ac *attachedClient) uint64 { | |||||
| 70 | ac.resumeToken = d.nextResumeTokenLocked() | 70 | ac.resumeToken = d.nextResumeTokenLocked() | ||
| 71 | } | 71 | } | ||
| 72 | d.ensureParkingInFlightLocked(sess, ac) | 72 | d.ensureParkingInFlightLocked(sess, ac) | ||
| 73 | - | d.log.Info("parking in flight", "session", sess.name) | 73 | + | d.log.Info("parking in flight", "session", sess.nameSnapshot()) |
| 74 | return ac.resumeToken | 74 | return ac.resumeToken | ||
| 75 | } | 75 | } | ||
| 76 | 76 | ||||
| @@ -178,7 +178,7 @@ func (d *Daemon) waitParkingInFlight(h ports.Hello) bool { | |||||
| 178 | d.mu.Unlock() | 178 | d.mu.Unlock() | ||
| 179 | return false | 179 | return false | ||
| 180 | } | 180 | } | ||
| 181 | - | if h.Name != "" && pending.sess != nil && pending.sess.name != h.Name { | 181 | + | if h.Name != "" && pending.sess != nil && pending.sess.nameSnapshot() != h.Name { |
| 182 | d.mu.Unlock() | 182 | d.mu.Unlock() | ||
| 183 | return false | 183 | return false | ||
| 184 | } | 184 | } | ||
| @@ -208,7 +208,7 @@ func (d *Daemon) prepareParkAttachment(sess *session, ac *attachedClient) bool { | |||||
| 208 | // terminal session removal and can strand IntentResume waiters. Callers | 208 | // terminal session removal and can strand IntentResume waiters. Callers | ||
| 209 | // that need the detach→park gap advertised must markParkingInFlight while | 209 | // that need the detach→park gap advertised must markParkingInFlight while | ||
| 210 | // still the exact live owner. Direct parkAttachment still parks below. | 210 | // still the exact live owner. Direct parkAttachment still parks below. | ||
| 211 | - | d.log.Info("parking client prepared", "session", sess.name) | 211 | + | d.log.Info("parking client prepared", "session", sess.nameSnapshot()) |
| 212 | return true | 212 | return true | ||
| 213 | } | 213 | } | ||
| 214 | 214 | ||||
| @@ -262,7 +262,7 @@ func (d *Daemon) parkAttachment(sess *session, ac *attachedClient) bool { | |||||
| 262 | } | 262 | } | ||
| 263 | oldSame.closeDone() | 263 | oldSame.closeDone() | ||
| 264 | } | 264 | } | ||
| 265 | - | d.log.Info("client parked for resume", "session", sess.name, "grace", grace) | 265 | + | d.log.Info("client parked for resume", "session", sess.nameSnapshot(), "grace", grace) |
| 266 | d.watchParkedTimer(token, parked) | 266 | d.watchParkedTimer(token, parked) | ||
| 267 | return true | 267 | return true | ||
| 268 | } | 268 | } | ||
| @@ -291,7 +291,7 @@ func (d *Daemon) expireParked(token uint64, parked *parkedAttachment) { | |||||
| 291 | d.removeParkedLocked(token, parked) | 291 | d.removeParkedLocked(token, parked) | ||
| 292 | d.mu.Unlock() | 292 | d.mu.Unlock() | ||
| 293 | d.closePickerIfCurrent(parked.ac, nil, parked.pickerGeneration) | 293 | d.closePickerIfCurrent(parked.ac, nil, parked.pickerGeneration) | ||
| 294 | - | d.log.Warn("parked client expired", "session", parked.sess.name) | 294 | + | d.log.Warn("parked client expired", "session", parked.sess.nameSnapshot()) |
| 295 | return | 295 | return | ||
| 296 | } | 296 | } | ||
| 297 | d.mu.Unlock() | 297 | d.mu.Unlock() | ||
| @@ -501,7 +501,7 @@ func (d *Daemon) resumeLiveAttachment(h ports.Hello, tr ports.Transport, sz doma | |||||
| 501 | 501 | ||||
| 502 | if clientMismatch { | 502 | if clientMismatch { | ||
| 503 | reason := "client id mismatch" | 503 | reason := "client id mismatch" | ||
| 504 | - | d.log.Warn("resume rejected", "session", sess.name, "err", reason) | 504 | + | d.log.Warn("resume rejected", "session", sess.nameSnapshot(), "err", reason) |
| 505 | return nil, nil, false, &protoErr{ports.ErrNoSuchSession, "resume token is no longer valid"} | 505 | return nil, nil, false, &protoErr{ports.ErrNoSuchSession, "resume token is no longer valid"} | ||
| 506 | } | 506 | } | ||
| 507 | if !credentialMatch { | 507 | if !credentialMatch { | ||
| @@ -549,7 +549,7 @@ func (d *Daemon) resumeLiveAttachment(h ports.Hello, tr ports.Transport, sz doma | |||||
| 549 | return nil, nil, false, &protoErr{ports.ErrNoSuchSession, "resume token is no longer valid"} | 549 | return nil, nil, false, &protoErr{ports.ErrNoSuchSession, "resume token is no longer valid"} | ||
| 550 | } | 550 | } | ||
| 551 | _ = ac.closeCapturedTransport(oldSnap.transport) | 551 | _ = ac.closeCapturedTransport(oldSnap.transport) | ||
| 552 | - | d.log.Info("live resume credential parked for reconnect", "session", sess.name) | 552 | + | d.log.Info("live resume credential parked for reconnect", "session", sess.nameSnapshot()) |
| 553 | return d.resumeParked(h, tr, sz) | 553 | return d.resumeParked(h, tr, sz) | ||
| 554 | } | 554 | } | ||
| 555 | 555 | ||||
| @@ -606,7 +606,7 @@ func (d *Daemon) resumeParkedLocked(h ports.Hello, tr ports.Transport, sz domain | |||||
| 606 | registered := d.sessions[sess.id] == sess | 606 | registered := d.sessions[sess.id] == sess | ||
| 607 | if !registered { | 607 | if !registered { | ||
| 608 | d.removeParkedLocked(h.ResumeToken, parked) | 608 | d.removeParkedLocked(h.ResumeToken, parked) | ||
| 609 | - | d.log.Warn("resume rejected", "session", sess.name, "registered", false) | 609 | + | d.log.Warn("resume rejected", "session", sess.nameSnapshot(), "registered", false) |
| 610 | return nil, nil, false, &protoErr{ports.ErrNoSuchSession, "resume token is no longer valid"} | 610 | return nil, nil, false, &protoErr{ports.ErrNoSuchSession, "resume token is no longer valid"} | ||
| 611 | } | 611 | } | ||
| 612 | ac := parked.ac | 612 | ac := parked.ac | ||
| @@ -615,7 +615,7 @@ func (d *Daemon) resumeParkedLocked(h ports.Hello, tr ports.Transport, sz domain | |||||
| 615 | } | 615 | } | ||
| 616 | if ac.clientID != h.ClientID { | 616 | if ac.clientID != h.ClientID { | ||
| 617 | reason := "client id mismatch" | 617 | reason := "client id mismatch" | ||
| 618 | - | d.log.Warn("resume rejected", "session", sess.name, "err", reason) | 618 | + | d.log.Warn("resume rejected", "session", sess.nameSnapshot(), "err", reason) |
| 619 | return nil, nil, false, &protoErr{ports.ErrNoSuchSession, "resume token is no longer valid"} | 619 | return nil, nil, false, &protoErr{ports.ErrNoSuchSession, "resume token is no longer valid"} | ||
| 620 | } | 620 | } | ||
| 621 | if h.RemoteTarget != nil && !d.remoteTargetMatchesSessionLocked(sess, *h.RemoteTarget) { | 621 | if h.RemoteTarget != nil && !d.remoteTargetMatchesSessionLocked(sess, *h.RemoteTarget) { | ||
| @@ -663,6 +663,6 @@ func (d *Daemon) resumeParkedLocked(h ports.Hello, tr ports.Transport, sz domain | |||||
| 663 | } | 663 | } | ||
| 664 | d.deferAttachmentTransitionCleanups(transition) | 664 | d.deferAttachmentTransitionCleanups(transition) | ||
| 665 | d.touchMRU(sess) | 665 | d.touchMRU(sess) | ||
| 666 | - | d.log.Info("client resumed", "session", sess.name) | 666 | + | d.log.Info("client resumed", "session", sess.nameSnapshot()) |
| 667 | return sess, ac, true, nil | 667 | return sess, ac, true, nil | ||
| 668 | } | 668 | } | ||
M internal/usecase/daemon/session.go +9 -0
| @@ -118,6 +118,15 @@ type session struct { | |||
| 118 | 118 | floatingLaunches map[*floatingLaunch]struct{} | |
| 119 | 119 | } | |
| 120 | 120 | ||
| 121 | + | func (s *session) nameSnapshot() string { | |
| 122 | + | if s == nil { | |
| 123 | + | return "" | |
| 124 | + | } | |
| 125 | + | s.mu.Lock() | |
| 126 | + | defer s.mu.Unlock() | |
| 127 | + | return s.name | |
| 128 | + | } | |
| 129 | + | ||
| 121 | 130 | // tab is a pane layout container; pane owns PTY/screen/scrollback/render scheduling state. | |
| 122 | 131 | type tab struct { | |
| 123 | 132 | mu sync.Mutex // guards tree, panes, floating, nextPaneID, size, and pane map membership | |
| @@ -118,6 +118,15 @@ type session struct { | |||||
| 118 | floatingLaunches map[*floatingLaunch]struct{} | 118 | floatingLaunches map[*floatingLaunch]struct{} | ||
| 119 | } | 119 | } | ||
| 120 | 120 | ||||
| 121 | + | func (s *session) nameSnapshot() string { | |||
| 122 | + | if s == nil { | |||
| 123 | + | return "" | |||
| 124 | + | } | |||
| 125 | + | s.mu.Lock() | |||
| 126 | + | defer s.mu.Unlock() | |||
| 127 | + | return s.name | |||
| 128 | + | } | |||
| 129 | + | ||||
| 121 | // tab is a pane layout container; pane owns PTY/screen/scrollback/render scheduling state. | 130 | // tab is a pane layout container; pane owns PTY/screen/scrollback/render scheduling state. | ||
| 122 | type tab struct { | 131 | type tab struct { | ||
| 123 | mu sync.Mutex // guards tree, panes, floating, nextPaneID, size, and pane map membership | 132 | mu sync.Mutex // guards tree, panes, floating, nextPaneID, size, and pane map membership | ||