fix(phase5): apply PR 176 review follow-ups
- commit
- 64916bf22a19b856720846345aa41a0180159b0b
- author
- brice · · 2026-08-04 14:26 UTC
- parent
- ee9b61d
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
31 files · +218 -137
M internal/app/final_acceptance_test.go +29 -30
| @@ -17,48 +17,45 @@ func sendAcceptanceInput(t *testing.T, tr ports.Transport, data string) { | |||
| 17 | 17 | require.NoError(t, tr.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(ports.Input{Data: []byte(data)})})) | |
| 18 | 18 | } | |
| 19 | 19 | ||
| 20 | - | func awaitAcceptanceCommand(t *testing.T, p *pump, requestID uint64) ports.CommandResult { | |
| 20 | + | func awaitAcceptanceFrame(t *testing.T, p *pump, typ ports.MsgType, predicate func(ports.Frame) bool) ports.Frame { | |
| 21 | 21 | t.Helper() | |
| 22 | 22 | deadline := time.NewTimer(5 * time.Second) | |
| 23 | 23 | defer deadline.Stop() | |
| 24 | 24 | for { | |
| 25 | 25 | select { | |
| 26 | 26 | case frame, ok := <-p.ch: | |
| 27 | - | require.True(t, ok, "connection closed before command result") | |
| 28 | - | if frame.Type != ports.MsgCommandResult { | |
| 29 | - | continue | |
| 30 | - | } | |
| 31 | - | result, err := ports.UnmarshalCommandResult(frame.Payload) | |
| 32 | - | require.NoError(t, err) | |
| 33 | - | if result.RequestID == requestID { | |
| 34 | - | return result | |
| 27 | + | require.True(t, ok, "connection closed before frame") | |
| 28 | + | if frame.Type == typ && predicate(frame) { | |
| 29 | + | return frame | |
| 35 | 30 | } | |
| 36 | 31 | case <-deadline.C: | |
| 37 | - | t.Fatal("timed out waiting for command result") | |
| 32 | + | t.Fatal("timed out waiting for acceptance frame") | |
| 38 | 33 | } | |
| 39 | 34 | } | |
| 40 | 35 | } | |
| 41 | 36 | ||
| 37 | + | func awaitAcceptanceCommand(t *testing.T, p *pump, requestID uint64) ports.CommandResult { | |
| 38 | + | t.Helper() | |
| 39 | + | frame := awaitAcceptanceFrame(t, p, ports.MsgCommandResult, func(frame ports.Frame) bool { | |
| 40 | + | result, err := ports.UnmarshalCommandResult(frame.Payload) | |
| 41 | + | require.NoError(t, err) | |
| 42 | + | return result.RequestID == requestID | |
| 43 | + | }) | |
| 44 | + | result, err := ports.UnmarshalCommandResult(frame.Payload) | |
| 45 | + | require.NoError(t, err) | |
| 46 | + | return result | |
| 47 | + | } | |
| 48 | + | ||
| 42 | 49 | func awaitAcceptanceOutput(t *testing.T, p *pump, predicate func(ports.Output) bool) ports.Output { | |
| 43 | 50 | t.Helper() | |
| 44 | - | deadline := time.NewTimer(5 * time.Second) | |
| 45 | - | defer deadline.Stop() | |
| 46 | - | for { | |
| 47 | - | select { | |
| 48 | - | case frame, ok := <-p.ch: | |
| 49 | - | require.True(t, ok, "connection closed before output") | |
| 50 | - | if frame.Type != ports.MsgOutput { | |
| 51 | - | continue | |
| 52 | - | } | |
| 53 | - | output, err := ports.UnmarshalOutput(frame.Payload) | |
| 54 | - | require.NoError(t, err) | |
| 55 | - | if predicate(output) { | |
| 56 | - | return output | |
| 57 | - | } | |
| 58 | - | case <-deadline.C: | |
| 59 | - | t.Fatal("timed out waiting for output") | |
| 60 | - | } | |
| 61 | - | } | |
| 51 | + | frame := awaitAcceptanceFrame(t, p, ports.MsgOutput, func(frame ports.Frame) bool { | |
| 52 | + | output, err := ports.UnmarshalOutput(frame.Payload) | |
| 53 | + | require.NoError(t, err) | |
| 54 | + | return predicate(output) | |
| 55 | + | }) | |
| 56 | + | output, err := ports.UnmarshalOutput(frame.Payload) | |
| 57 | + | require.NoError(t, err) | |
| 58 | + | return output | |
| 62 | 59 | } | |
| 63 | 60 | ||
| 64 | 61 | func awaitAcceptanceCommandResult(t *testing.T, tr ports.Transport, p *pump, requestID uint64, slug string) ports.CommandResult { | |
| @@ -89,7 +86,7 @@ func TestAcceptanceTwoLocalAttachmentsKeepViewsOverTransports(t *testing.T) { | |||
| 89 | 86 | awaitText(t, firstPump, size, "Commands") | |
| 90 | 87 | sendAcceptanceInput(t, first, "CNT\r") | |
| 91 | 88 | awaitText(t, firstPump, size, " 1 (cat) 2 (cat) ") | |
| 92 | - | assertNoTextAfterInput(t, secondPump, size, "FIRST_PANE_INPUT") | |
| 89 | + | assertNoTextAfterInput(t, secondPump, size, " 1 (cat) 2 (cat) ") | |
| 93 | 90 | ||
| 94 | 91 | sendAcceptanceInput(t, first, "\x1b ") | |
| 95 | 92 | awaitText(t, firstPump, size, "Commands") | |
| @@ -115,7 +112,9 @@ func TestAcceptanceTwoLocalAttachmentsKeepViewsOverTransports(t *testing.T) { | |||
| 115 | 112 | ||
| 116 | 113 | // A resize and an output reset are scoped to one attachment output stream; | |
| 117 | 114 | // the shared tab's PTY/content geometry and the peer stream remain intact. | |
| 118 | - | require.NoError(t, first.Send(ports.Frame{Type: ports.MsgResize, Payload: ports.MarshalResize(ports.Resize{Size: domain.Size{Cols: 100, Rows: 30}})})) | |
| 115 | + | resizePayload, err := ports.MarshalResize(ports.Resize{Size: domain.Size{Cols: 100, Rows: 30}}) | |
| 116 | + | require.NoError(t, err) | |
| 117 | + | require.NoError(t, first.Send(ports.Frame{Type: ports.MsgResize, Payload: resizePayload})) | |
| 119 | 118 | resized := awaitAcceptanceOutput(t, firstPump, func(output ports.Output) bool { return output.Size == (domain.Size{Cols: 100, Rows: 30}) }) | |
| 120 | 119 | require.Equal(t, domain.Size{Cols: 100, Rows: 30}, resized.Size) | |
| 121 | 120 | assertNoTextAfterInput(t, secondPump, size, "FIRST_PANE_INPUT") | |
| @@ -17,48 +17,45 @@ func sendAcceptanceInput(t *testing.T, tr ports.Transport, data string) { | |||||
| 17 | require.NoError(t, tr.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(ports.Input{Data: []byte(data)})})) | 17 | require.NoError(t, tr.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(ports.Input{Data: []byte(data)})})) | ||
| 18 | } | 18 | } | ||
| 19 | 19 | ||||
| 20 | - | func awaitAcceptanceCommand(t *testing.T, p *pump, requestID uint64) ports.CommandResult { | 20 | + | func awaitAcceptanceFrame(t *testing.T, p *pump, typ ports.MsgType, predicate func(ports.Frame) bool) ports.Frame { |
| 21 | t.Helper() | 21 | t.Helper() | ||
| 22 | deadline := time.NewTimer(5 * time.Second) | 22 | deadline := time.NewTimer(5 * time.Second) | ||
| 23 | defer deadline.Stop() | 23 | defer deadline.Stop() | ||
| 24 | for { | 24 | for { | ||
| 25 | select { | 25 | select { | ||
| 26 | case frame, ok := <-p.ch: | 26 | case frame, ok := <-p.ch: | ||
| 27 | - | require.True(t, ok, "connection closed before command result") | 27 | + | require.True(t, ok, "connection closed before frame") |
| 28 | - | if frame.Type != ports.MsgCommandResult { | 28 | + | if frame.Type == typ && predicate(frame) { |
| 29 | - | continue | 29 | + | return frame |
| 30 | - | } | |||
| 31 | - | result, err := ports.UnmarshalCommandResult(frame.Payload) | |||
| 32 | - | require.NoError(t, err) | |||
| 33 | - | if result.RequestID == requestID { | |||
| 34 | - | return result | |||
| 35 | } | 30 | } | ||
| 36 | case <-deadline.C: | 31 | case <-deadline.C: | ||
| 37 | - | t.Fatal("timed out waiting for command result") | 32 | + | t.Fatal("timed out waiting for acceptance frame") |
| 38 | } | 33 | } | ||
| 39 | } | 34 | } | ||
| 40 | } | 35 | } | ||
| 41 | 36 | ||||
| 37 | + | func awaitAcceptanceCommand(t *testing.T, p *pump, requestID uint64) ports.CommandResult { | |||
| 38 | + | t.Helper() | |||
| 39 | + | frame := awaitAcceptanceFrame(t, p, ports.MsgCommandResult, func(frame ports.Frame) bool { | |||
| 40 | + | result, err := ports.UnmarshalCommandResult(frame.Payload) | |||
| 41 | + | require.NoError(t, err) | |||
| 42 | + | return result.RequestID == requestID | |||
| 43 | + | }) | |||
| 44 | + | result, err := ports.UnmarshalCommandResult(frame.Payload) | |||
| 45 | + | require.NoError(t, err) | |||
| 46 | + | return result | |||
| 47 | + | } | |||
| 48 | + | ||||
| 42 | func awaitAcceptanceOutput(t *testing.T, p *pump, predicate func(ports.Output) bool) ports.Output { | 49 | func awaitAcceptanceOutput(t *testing.T, p *pump, predicate func(ports.Output) bool) ports.Output { | ||
| 43 | t.Helper() | 50 | t.Helper() | ||
| 44 | - | deadline := time.NewTimer(5 * time.Second) | 51 | + | frame := awaitAcceptanceFrame(t, p, ports.MsgOutput, func(frame ports.Frame) bool { |
| 45 | - | defer deadline.Stop() | 52 | + | output, err := ports.UnmarshalOutput(frame.Payload) |
| 46 | - | for { | 53 | + | require.NoError(t, err) |
| 47 | - | select { | 54 | + | return predicate(output) |
| 48 | - | case frame, ok := <-p.ch: | 55 | + | }) |
| 49 | - | require.True(t, ok, "connection closed before output") | 56 | + | output, err := ports.UnmarshalOutput(frame.Payload) |
| 50 | - | if frame.Type != ports.MsgOutput { | 57 | + | require.NoError(t, err) |
| 51 | - | continue | 58 | + | return output |
| 52 | - | } | |||
| 53 | - | output, err := ports.UnmarshalOutput(frame.Payload) | |||
| 54 | - | require.NoError(t, err) | |||
| 55 | - | if predicate(output) { | |||
| 56 | - | return output | |||
| 57 | - | } | |||
| 58 | - | case <-deadline.C: | |||
| 59 | - | t.Fatal("timed out waiting for output") | |||
| 60 | - | } | |||
| 61 | - | } | |||
| 62 | } | 59 | } | ||
| 63 | 60 | ||||
| 64 | func awaitAcceptanceCommandResult(t *testing.T, tr ports.Transport, p *pump, requestID uint64, slug string) ports.CommandResult { | 61 | func awaitAcceptanceCommandResult(t *testing.T, tr ports.Transport, p *pump, requestID uint64, slug string) ports.CommandResult { | ||
| @@ -89,7 +86,7 @@ func TestAcceptanceTwoLocalAttachmentsKeepViewsOverTransports(t *testing.T) { | |||||
| 89 | awaitText(t, firstPump, size, "Commands") | 86 | awaitText(t, firstPump, size, "Commands") | ||
| 90 | sendAcceptanceInput(t, first, "CNT\r") | 87 | sendAcceptanceInput(t, first, "CNT\r") | ||
| 91 | awaitText(t, firstPump, size, " 1 (cat) 2 (cat) ") | 88 | awaitText(t, firstPump, size, " 1 (cat) 2 (cat) ") | ||
| 92 | - | assertNoTextAfterInput(t, secondPump, size, "FIRST_PANE_INPUT") | 89 | + | assertNoTextAfterInput(t, secondPump, size, " 1 (cat) 2 (cat) ") |
| 93 | 90 | ||||
| 94 | sendAcceptanceInput(t, first, "\x1b ") | 91 | sendAcceptanceInput(t, first, "\x1b ") | ||
| 95 | awaitText(t, firstPump, size, "Commands") | 92 | awaitText(t, firstPump, size, "Commands") | ||
| @@ -115,7 +112,9 @@ func TestAcceptanceTwoLocalAttachmentsKeepViewsOverTransports(t *testing.T) { | |||||
| 115 | 112 | ||||
| 116 | // A resize and an output reset are scoped to one attachment output stream; | 113 | // A resize and an output reset are scoped to one attachment output stream; | ||
| 117 | // the shared tab's PTY/content geometry and the peer stream remain intact. | 114 | // the shared tab's PTY/content geometry and the peer stream remain intact. | ||
| 118 | - | require.NoError(t, first.Send(ports.Frame{Type: ports.MsgResize, Payload: ports.MarshalResize(ports.Resize{Size: domain.Size{Cols: 100, Rows: 30}})})) | 115 | + | resizePayload, err := ports.MarshalResize(ports.Resize{Size: domain.Size{Cols: 100, Rows: 30}}) |
| 116 | + | require.NoError(t, err) | |||
| 117 | + | require.NoError(t, first.Send(ports.Frame{Type: ports.MsgResize, Payload: resizePayload})) | |||
| 119 | resized := awaitAcceptanceOutput(t, firstPump, func(output ports.Output) bool { return output.Size == (domain.Size{Cols: 100, Rows: 30}) }) | 118 | resized := awaitAcceptanceOutput(t, firstPump, func(output ports.Output) bool { return output.Size == (domain.Size{Cols: 100, Rows: 30}) }) | ||
| 120 | require.Equal(t, domain.Size{Cols: 100, Rows: 30}, resized.Size) | 119 | require.Equal(t, domain.Size{Cols: 100, Rows: 30}, resized.Size) | ||
| 121 | assertNoTextAfterInput(t, secondPump, size, "FIRST_PANE_INPUT") | 120 | assertNoTextAfterInput(t, secondPump, size, "FIRST_PANE_INPUT") | ||
M internal/app/remote_final_acceptance_test.go +7 -2
| @@ -77,9 +77,13 @@ func serveAcceptanceRemote(tr ports.Transport, target string, handoff ports.Atta | |||
| 77 | 77 | if outputs != nil { | |
| 78 | 78 | outputs <- string(data) | |
| 79 | 79 | } | |
| 80 | - | if err := tr.Send(ports.Frame{Type: ports.MsgOutput, Payload: ports.MarshalOutput(ports.Output{ | |
| 80 | + | payload, err := ports.MarshalOutput(ports.Output{ | |
| 81 | 81 | Epoch: 1, New: 1, Full: true, Size: domain.Size{Cols: 80, Rows: 24}, Data: data, | |
| 82 | - | })}); err != nil { | |
| 82 | + | }) | |
| 83 | + | if err != nil { | |
| 84 | + | return | |
| 85 | + | } | |
| 86 | + | if err := tr.Send(ports.Frame{Type: ports.MsgOutput, Payload: payload}); err != nil { | |
| 83 | 87 | return | |
| 84 | 88 | } | |
| 85 | 89 | _ = tr.Send(ports.Frame{Type: ports.MsgDetached, Payload: ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach})}) | |
| @@ -127,6 +131,7 @@ func TestAcceptanceRemoteDirectAndPickerUseOnlyRemoteTransports(t *testing.T) { | |||
| 127 | 131 | deps.Remote = true | |
| 128 | 132 | result := client.NewRunner(deps).Run(ctx, request) | |
| 129 | 133 | if request.SessionName == "work" { | |
| 134 | + | require.NoError(t, result, "the client handoff completes before the test injects the composition-root handoff") | |
| 130 | 135 | return &client.AttachTargetError{Target: ports.AttachTarget{Endpoint: "picked.example", Session: "picked", Intent: ports.IntentAttach}} | |
| 131 | 136 | } | |
| 132 | 137 | return result | |
| @@ -77,9 +77,13 @@ func serveAcceptanceRemote(tr ports.Transport, target string, handoff ports.Atta | |||||
| 77 | if outputs != nil { | 77 | if outputs != nil { | ||
| 78 | outputs <- string(data) | 78 | outputs <- string(data) | ||
| 79 | } | 79 | } | ||
| 80 | - | if err := tr.Send(ports.Frame{Type: ports.MsgOutput, Payload: ports.MarshalOutput(ports.Output{ | 80 | + | payload, err := ports.MarshalOutput(ports.Output{ |
| 81 | Epoch: 1, New: 1, Full: true, Size: domain.Size{Cols: 80, Rows: 24}, Data: data, | 81 | Epoch: 1, New: 1, Full: true, Size: domain.Size{Cols: 80, Rows: 24}, Data: data, | ||
| 82 | - | })}); err != nil { | 82 | + | }) |
| 83 | + | if err != nil { | |||
| 84 | + | return | |||
| 85 | + | } | |||
| 86 | + | if err := tr.Send(ports.Frame{Type: ports.MsgOutput, Payload: payload}); err != nil { | |||
| 83 | return | 87 | return | ||
| 84 | } | 88 | } | ||
| 85 | _ = tr.Send(ports.Frame{Type: ports.MsgDetached, Payload: ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach})}) | 89 | _ = tr.Send(ports.Frame{Type: ports.MsgDetached, Payload: ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach})}) | ||
| @@ -127,6 +131,7 @@ func TestAcceptanceRemoteDirectAndPickerUseOnlyRemoteTransports(t *testing.T) { | |||||
| 127 | deps.Remote = true | 131 | deps.Remote = true | ||
| 128 | result := client.NewRunner(deps).Run(ctx, request) | 132 | result := client.NewRunner(deps).Run(ctx, request) | ||
| 129 | if request.SessionName == "work" { | 133 | if request.SessionName == "work" { | ||
| 134 | + | require.NoError(t, result, "the client handoff completes before the test injects the composition-root handoff") | |||
| 130 | return &client.AttachTargetError{Target: ports.AttachTarget{Endpoint: "picked.example", Session: "picked", Intent: ports.IntentAttach}} | 135 | return &client.AttachTargetError{Target: ports.AttachTarget{Endpoint: "picked.example", Session: "picked", Intent: ports.IntentAttach}} | ||
| 131 | } | 136 | } | ||
| 132 | return result | 137 | return result | ||
M internal/ports/wire_final_test.go +11 -2
| @@ -235,10 +235,19 @@ func TestFinalClosedWireValuesRejectUnknownEnumsAndBooleans(t *testing.T) { | |||
| 235 | 235 | decode: func(b []byte) error { _, err := UnmarshalKill(b); return err }, | |
| 236 | 236 | }, | |
| 237 | 237 | { | |
| 238 | - | name: "sessions booleans", | |
| 238 | + | name: "sessions ephemeral boolean", | |
| 239 | 239 | payload: func() []byte { | |
| 240 | 240 | b := MarshalSessions(Sessions{Sessions: []SessionInfo{{State: SessionRunning}}}) | |
| 241 | - | b[4], b[7] = 2, 2 | |
| 241 | + | b[6] = 2 | |
| 242 | + | return b | |
| 243 | + | }(), | |
| 244 | + | decode: func(b []byte) error { _, err := UnmarshalSessions(b); return err }, | |
| 245 | + | }, | |
| 246 | + | { | |
| 247 | + | name: "sessions attached boolean", | |
| 248 | + | payload: func() []byte { | |
| 249 | + | b := MarshalSessions(Sessions{Sessions: []SessionInfo{{State: SessionRunning}}}) | |
| 250 | + | b[9] = 2 | |
| 242 | 251 | return b | |
| 243 | 252 | }(), | |
| 244 | 253 | decode: func(b []byte) error { _, err := UnmarshalSessions(b); return err }, | |
| @@ -235,10 +235,19 @@ func TestFinalClosedWireValuesRejectUnknownEnumsAndBooleans(t *testing.T) { | |||||
| 235 | decode: func(b []byte) error { _, err := UnmarshalKill(b); return err }, | 235 | decode: func(b []byte) error { _, err := UnmarshalKill(b); return err }, | ||
| 236 | }, | 236 | }, | ||
| 237 | { | 237 | { | ||
| 238 | - | name: "sessions booleans", | 238 | + | name: "sessions ephemeral boolean", |
| 239 | payload: func() []byte { | 239 | payload: func() []byte { | ||
| 240 | b := MarshalSessions(Sessions{Sessions: []SessionInfo{{State: SessionRunning}}}) | 240 | b := MarshalSessions(Sessions{Sessions: []SessionInfo{{State: SessionRunning}}}) | ||
| 241 | - | b[4], b[7] = 2, 2 | 241 | + | b[6] = 2 |
| 242 | + | return b | |||
| 243 | + | }(), | |||
| 244 | + | decode: func(b []byte) error { _, err := UnmarshalSessions(b); return err }, | |||
| 245 | + | }, | |||
| 246 | + | { | |||
| 247 | + | name: "sessions attached boolean", | |||
| 248 | + | payload: func() []byte { | |||
| 249 | + | b := MarshalSessions(Sessions{Sessions: []SessionInfo{{State: SessionRunning}}}) | |||
| 250 | + | b[9] = 2 | |||
| 242 | return b | 251 | return b | ||
| 243 | }(), | 252 | }(), | ||
| 244 | decode: func(b []byte) error { _, err := UnmarshalSessions(b); return err }, | 253 | decode: func(b []byte) error { _, err := UnmarshalSessions(b); return err }, | ||
M internal/usecase/daemon/attachment_effect_gate_test.go +18 -18
| @@ -165,7 +165,7 @@ func TestBeginCurrentAttachmentEffectFailsClosedOnStableCapabilityMismatch(t *te | |||
| 165 | 165 | ||
| 166 | 166 | func TestBeginAttachmentLeaseEffectRejectsForeignLease(t *testing.T) { | |
| 167 | 167 | d, sess, ac, _ := newManualSessionWithPTYs(t, newQuietPTY()) | |
| 168 | - | rc := d.attachCoordinator(sess, ac, true) | |
| 168 | + | rc := d.attachCoordinator(sess, nil, ac, true) | |
| 169 | 169 | foreign := &attachmentLease{attachment: &attachedClient{}} | |
| 170 | 170 | _, admitted := beginAttachmentLeaseEffect(sess, ac, foreign) | |
| 171 | 171 | require.False(t, admitted) | |
| @@ -196,7 +196,7 @@ func TestKillSessionAcquisitionTimeoutDoesNotPublishPartialInvalidation(t *testi | |||
| 196 | 196 | sess.registerAttachmentLocked(later) | |
| 197 | 197 | sess.mu.Unlock() | |
| 198 | 198 | ||
| 199 | - | rc := d.attachCoordinator(sess, first, true) | |
| 199 | + | rc := d.attachCoordinator(sess, nil, first, true) | |
| 200 | 200 | firstToken := sess.attachmentToken(first, firstTransport) | |
| 201 | 201 | firstToken.lease = rc.attachmentLease(first) | |
| 202 | 202 | first.publishAttachmentCapability(firstToken) | |
| @@ -302,7 +302,7 @@ func TestKillSessionInterruptsOnlyExactParticipantBlockedSends(t *testing.T) { | |||
| 302 | 302 | d, sess, snatched, _ := newManualSessionWithPTYs(t, &transactionalResizePTY{}) | |
| 303 | 303 | snatchedTransport := newTeardownBlockingTransport() | |
| 304 | 304 | snatched.replaceTransport(snatchedTransport) | |
| 305 | - | d.attachCoordinator(sess, snatched, true) | |
| 305 | + | d.attachCoordinator(sess, nil, snatched, true) | |
| 306 | 306 | snatched.publishAttachmentCapability(sess.attachmentToken(snatched, snatchedTransport)) | |
| 307 | 307 | ||
| 308 | 308 | activeTransport := newTeardownBlockingTransport() | |
| @@ -322,7 +322,7 @@ func TestKillSessionInterruptsOnlyExactParticipantBlockedSends(t *testing.T) { | |||
| 322 | 322 | d.mu.Lock() | |
| 323 | 323 | d.sessions[unrelated.id] = unrelated | |
| 324 | 324 | d.mu.Unlock() | |
| 325 | - | d.attachCoordinator(unrelated, unrelatedClient, true) | |
| 325 | + | d.attachCoordinator(unrelated, nil, unrelatedClient, true) | |
| 326 | 326 | ||
| 327 | 327 | snatchedToken := sess.attachmentToken(snatched, snatchedTransport) | |
| 328 | 328 | activeToken := sess.attachmentToken(active, activeTransport) | |
| @@ -358,7 +358,7 @@ func TestKillSessionInterruptsCapturedSendWithoutClosingNewerIncarnation(t *test | |||
| 358 | 358 | d, sess, ac, _ := newManualSessionWithPTYs(t, &transactionalResizePTY{}) | |
| 359 | 359 | stale := newTeardownBlockingTransport() | |
| 360 | 360 | ac.replaceTransport(stale) | |
| 361 | - | d.attachCoordinator(sess, ac, true) | |
| 361 | + | d.attachCoordinator(sess, nil, ac, true) | |
| 362 | 362 | ac.publishAttachmentCapability(sess.attachmentToken(ac, stale)) | |
| 363 | 363 | ||
| 364 | 364 | token := sess.attachmentToken(ac, stale) | |
| @@ -387,7 +387,7 @@ func TestShutdownSignalsServeWhenParticipantGateAcquisitionTimesOut(t *testing.T | |||
| 387 | 387 | d, sess, active, _ := newManualSessionWithPTYs(t, &transactionalResizePTY{}) | |
| 388 | 388 | transport := &closeTrackingTransport{} | |
| 389 | 389 | active.replaceTransport(transport) | |
| 390 | - | d.attachCoordinator(sess, active, true) | |
| 390 | + | d.attachCoordinator(sess, nil, active, true) | |
| 391 | 391 | active.publishAttachmentCapability(sess.attachmentToken(active, transport)) | |
| 392 | 392 | ||
| 393 | 393 | owner := freezeAttachmentEffectGates(active) | |
| @@ -418,7 +418,7 @@ func TestShutdownInterruptsBlockedParticipantSend(t *testing.T) { | |||
| 418 | 418 | d, sess, active, _ := newManualSessionWithPTYs(t, &transactionalResizePTY{}) | |
| 419 | 419 | blocked := newTeardownBlockingTransport() | |
| 420 | 420 | active.replaceTransport(blocked) | |
| 421 | - | d.attachCoordinator(sess, active, true) | |
| 421 | + | d.attachCoordinator(sess, nil, active, true) | |
| 422 | 422 | active.publishAttachmentCapability(sess.attachmentToken(active, blocked)) | |
| 423 | 423 | ||
| 424 | 424 | token := sess.attachmentToken(active, blocked) | |
| @@ -444,7 +444,7 @@ func TestRenderSendFailureCleanupIsDeadlineBoundedWhileAttachmentEffectGateIsBus | |||
| 444 | 444 | d, sess, ac, _ := newManualSessionWithPTYs(t, &transactionalResizePTY{}) | |
| 445 | 445 | transport := &closeTrackingTransport{} | |
| 446 | 446 | ac.replaceTransport(transport) | |
| 447 | - | rc := d.attachCoordinator(sess, ac, true) | |
| 447 | + | rc := d.attachCoordinator(sess, nil, ac, true) | |
| 448 | 448 | token := sess.attachmentToken(ac, transport) | |
| 449 | 449 | token.lease = rc.attachmentLease(ac) | |
| 450 | 450 | ac.publishAttachmentCapability(token) | |
| @@ -474,7 +474,7 @@ func TestAttachmentEffectGateReplacementInterruptsBlockedOldRenderBeforePublicat | |||
| 474 | 474 | d, sess, old, _ := newManualSessionWithPTYs(t, &transactionalResizePTY{}) | |
| 475 | 475 | oldTransport := newBlockedRenderReplacementTransport(d, sess) | |
| 476 | 476 | old.replaceTransport(oldTransport) | |
| 477 | - | rc := d.attachCoordinator(sess, old, true) | |
| 477 | + | rc := d.attachCoordinator(sess, nil, old, true) | |
| 478 | 478 | lease := rc.attachmentLease(old) | |
| 479 | 479 | token := sess.attachmentToken(old, oldTransport) | |
| 480 | 480 | token.lease = lease | |
| @@ -529,7 +529,7 @@ func TestAttachmentEffectGateAdmittedEffectCompletesBeforeConflictingTransition( | |||
| 529 | 529 | d, sess, old, _ := newManualSessionWithPTYs(t, &transactionalResizePTY{}) | |
| 530 | 530 | oldTransport := newDatagramTestTransport() | |
| 531 | 531 | old.replaceTransport(oldTransport) | |
| 532 | - | rc := d.attachCoordinator(sess, old, true) | |
| 532 | + | rc := d.attachCoordinator(sess, nil, old, true) | |
| 533 | 533 | token := sess.attachmentToken(old, oldTransport) | |
| 534 | 534 | token.lease = rc.attachmentLease(old) | |
| 535 | 535 | old.publishAttachmentCapability(token) | |
| @@ -630,7 +630,7 @@ func TestAttachmentEffectGateAdmittedActiveEffectsFinishBeforeReplacement(t *tes | |||
| 630 | 630 | oldTransport := newDatagramTestTransport() | |
| 631 | 631 | old.replaceTransport(oldTransport) | |
| 632 | 632 | old.keys = keys.NewRouter(d.clock, daemonKeyHandler{d: d, ac: old}, &d.bindings) | |
| 633 | - | rc := d.attachCoordinator(sess, old, true) | |
| 633 | + | rc := d.attachCoordinator(sess, nil, old, true) | |
| 634 | 634 | token := sess.attachmentToken(old, oldTransport) | |
| 635 | 635 | token.lease = rc.attachmentLease(old) | |
| 636 | 636 | if tt.setup != nil { | |
| @@ -683,7 +683,7 @@ func TestJumpAttentionAdmittedHandoffCrossesSessions(t *testing.T) { | |||
| 683 | 683 | target.tabs[1].attentionAt = time.Unix(1, 0) | |
| 684 | 684 | d.sessions[target.id] = target | |
| 685 | 685 | ||
| 686 | - | rc := d.attachCoordinator(source, ac, true) | |
| 686 | + | rc := d.attachCoordinator(source, nil, ac, true) | |
| 687 | 687 | token := source.attachmentToken(ac, ac.transport()) | |
| 688 | 688 | token.lease = rc.attachmentLease(ac) | |
| 689 | 689 | ac.publishAttachmentCapability(token) | |
| @@ -701,7 +701,7 @@ func TestPickerDeleteDoesNotDeleteSourceAfterInitiatorReplacement(t *testing.T) | |||
| 701 | 701 | d.enterPicker(sess, old) | |
| 702 | 702 | ||
| 703 | 703 | oldTransport := old.transport() | |
| 704 | - | rc := d.attachCoordinator(sess, old, true) | |
| 704 | + | rc := d.attachCoordinator(sess, nil, old, true) | |
| 705 | 705 | token := sess.attachmentToken(old, oldTransport) | |
| 706 | 706 | token.lease = rc.attachmentLease(old) | |
| 707 | 707 | old.publishAttachmentCapability(token) | |
| @@ -745,7 +745,7 @@ func TestPickerDeleteSourceForCurrentInitiatorDoesNotDeadlock(t *testing.T) { | |||
| 745 | 745 | defer release() | |
| 746 | 746 | d, sess, ac, _ := newManualSessionWithPTYs(t, p) | |
| 747 | 747 | d.enterPicker(sess, ac) | |
| 748 | - | rc := d.attachCoordinator(sess, ac, true) | |
| 748 | + | rc := d.attachCoordinator(sess, nil, ac, true) | |
| 749 | 749 | token := sess.attachmentToken(ac, ac.transport()) | |
| 750 | 750 | token.lease = rc.attachmentLease(ac) | |
| 751 | 751 | ac.publishAttachmentCapability(token) | |
| @@ -762,7 +762,7 @@ func TestAttachmentEffectGateAdmittedFirstPaintFinishesBeforeReplacement(t *test | |||
| 762 | 762 | d, sess, old, _ := newManualSessionWithPTYs(t, &transactionalResizePTY{}) | |
| 763 | 763 | oldTransport := newDatagramTestTransport() | |
| 764 | 764 | old.replaceTransport(oldTransport) | |
| 765 | - | rc := d.attachCoordinator(sess, old, true) | |
| 765 | + | rc := d.attachCoordinator(sess, nil, old, true) | |
| 766 | 766 | token := sess.attachmentToken(old, oldTransport) | |
| 767 | 767 | token.lease = rc.attachmentLease(old) | |
| 768 | 768 | ||
| @@ -796,7 +796,7 @@ func TestAttachmentEffectGateReversedConcurrentTransitionsDoNotDeadlock(t *testi | |||
| 796 | 796 | d, first, a, _ := newManualSessionWithPTYs(t, &transactionalResizePTY{}) | |
| 797 | 797 | aTransport := newDatagramTestTransport() | |
| 798 | 798 | a.replaceTransport(aTransport) | |
| 799 | - | d.attachCoordinator(first, a, true) | |
| 799 | + | d.attachCoordinator(first, nil, a, true) | |
| 800 | 800 | first.attachmentToken(a, aTransport) | |
| 801 | 801 | ||
| 802 | 802 | bTransport := newDatagramTestTransport() | |
| @@ -807,7 +807,7 @@ func TestAttachmentEffectGateReversedConcurrentTransitionsDoNotDeadlock(t *testi | |||
| 807 | 807 | d.mu.Lock() | |
| 808 | 808 | d.sessions[second.id] = second | |
| 809 | 809 | d.mu.Unlock() | |
| 810 | - | d.attachCoordinator(second, b, true) | |
| 810 | + | d.attachCoordinator(second, nil, b, true) | |
| 811 | 811 | second.attachmentToken(b, bTransport) | |
| 812 | 812 | ||
| 813 | 813 | start := make(chan struct{}) | |
| @@ -854,7 +854,7 @@ func TestAttachmentEffectGateFrozenConnectionRejectsLateEffect(t *testing.T) { | |||
| 854 | 854 | d, sess, ac, _ := newManualSessionWithPTYs(t, &transactionalResizePTY{}) | |
| 855 | 855 | transport := newDatagramTestTransport() | |
| 856 | 856 | ac.replaceTransport(transport) | |
| 857 | - | rc := d.attachCoordinator(sess, ac, true) | |
| 857 | + | rc := d.attachCoordinator(sess, nil, ac, true) | |
| 858 | 858 | token := sess.attachmentToken(ac, transport) | |
| 859 | 859 | token.lease = rc.attachmentLease(ac) | |
| 860 | 860 | ac.publishAttachmentCapability(token) | |
| @@ -165,7 +165,7 @@ func TestBeginCurrentAttachmentEffectFailsClosedOnStableCapabilityMismatch(t *te | |||||
| 165 | 165 | ||||
| 166 | func TestBeginAttachmentLeaseEffectRejectsForeignLease(t *testing.T) { | 166 | func TestBeginAttachmentLeaseEffectRejectsForeignLease(t *testing.T) { | ||
| 167 | d, sess, ac, _ := newManualSessionWithPTYs(t, newQuietPTY()) | 167 | d, sess, ac, _ := newManualSessionWithPTYs(t, newQuietPTY()) | ||
| 168 | - | rc := d.attachCoordinator(sess, ac, true) | 168 | + | rc := d.attachCoordinator(sess, nil, ac, true) |
| 169 | foreign := &attachmentLease{attachment: &attachedClient{}} | 169 | foreign := &attachmentLease{attachment: &attachedClient{}} | ||
| 170 | _, admitted := beginAttachmentLeaseEffect(sess, ac, foreign) | 170 | _, admitted := beginAttachmentLeaseEffect(sess, ac, foreign) | ||
| 171 | require.False(t, admitted) | 171 | require.False(t, admitted) | ||
| @@ -196,7 +196,7 @@ func TestKillSessionAcquisitionTimeoutDoesNotPublishPartialInvalidation(t *testi | |||||
| 196 | sess.registerAttachmentLocked(later) | 196 | sess.registerAttachmentLocked(later) | ||
| 197 | sess.mu.Unlock() | 197 | sess.mu.Unlock() | ||
| 198 | 198 | ||||
| 199 | - | rc := d.attachCoordinator(sess, first, true) | 199 | + | rc := d.attachCoordinator(sess, nil, first, true) |
| 200 | firstToken := sess.attachmentToken(first, firstTransport) | 200 | firstToken := sess.attachmentToken(first, firstTransport) | ||
| 201 | firstToken.lease = rc.attachmentLease(first) | 201 | firstToken.lease = rc.attachmentLease(first) | ||
| 202 | first.publishAttachmentCapability(firstToken) | 202 | first.publishAttachmentCapability(firstToken) | ||
| @@ -302,7 +302,7 @@ func TestKillSessionInterruptsOnlyExactParticipantBlockedSends(t *testing.T) { | |||||
| 302 | d, sess, snatched, _ := newManualSessionWithPTYs(t, &transactionalResizePTY{}) | 302 | d, sess, snatched, _ := newManualSessionWithPTYs(t, &transactionalResizePTY{}) | ||
| 303 | snatchedTransport := newTeardownBlockingTransport() | 303 | snatchedTransport := newTeardownBlockingTransport() | ||
| 304 | snatched.replaceTransport(snatchedTransport) | 304 | snatched.replaceTransport(snatchedTransport) | ||
| 305 | - | d.attachCoordinator(sess, snatched, true) | 305 | + | d.attachCoordinator(sess, nil, snatched, true) |
| 306 | snatched.publishAttachmentCapability(sess.attachmentToken(snatched, snatchedTransport)) | 306 | snatched.publishAttachmentCapability(sess.attachmentToken(snatched, snatchedTransport)) | ||
| 307 | 307 | ||||
| 308 | activeTransport := newTeardownBlockingTransport() | 308 | activeTransport := newTeardownBlockingTransport() | ||
| @@ -322,7 +322,7 @@ func TestKillSessionInterruptsOnlyExactParticipantBlockedSends(t *testing.T) { | |||||
| 322 | d.mu.Lock() | 322 | d.mu.Lock() | ||
| 323 | d.sessions[unrelated.id] = unrelated | 323 | d.sessions[unrelated.id] = unrelated | ||
| 324 | d.mu.Unlock() | 324 | d.mu.Unlock() | ||
| 325 | - | d.attachCoordinator(unrelated, unrelatedClient, true) | 325 | + | d.attachCoordinator(unrelated, nil, unrelatedClient, true) |
| 326 | 326 | ||||
| 327 | snatchedToken := sess.attachmentToken(snatched, snatchedTransport) | 327 | snatchedToken := sess.attachmentToken(snatched, snatchedTransport) | ||
| 328 | activeToken := sess.attachmentToken(active, activeTransport) | 328 | activeToken := sess.attachmentToken(active, activeTransport) | ||
| @@ -358,7 +358,7 @@ func TestKillSessionInterruptsCapturedSendWithoutClosingNewerIncarnation(t *test | |||||
| 358 | d, sess, ac, _ := newManualSessionWithPTYs(t, &transactionalResizePTY{}) | 358 | d, sess, ac, _ := newManualSessionWithPTYs(t, &transactionalResizePTY{}) | ||
| 359 | stale := newTeardownBlockingTransport() | 359 | stale := newTeardownBlockingTransport() | ||
| 360 | ac.replaceTransport(stale) | 360 | ac.replaceTransport(stale) | ||
| 361 | - | d.attachCoordinator(sess, ac, true) | 361 | + | d.attachCoordinator(sess, nil, ac, true) |
| 362 | ac.publishAttachmentCapability(sess.attachmentToken(ac, stale)) | 362 | ac.publishAttachmentCapability(sess.attachmentToken(ac, stale)) | ||
| 363 | 363 | ||||
| 364 | token := sess.attachmentToken(ac, stale) | 364 | token := sess.attachmentToken(ac, stale) | ||
| @@ -387,7 +387,7 @@ func TestShutdownSignalsServeWhenParticipantGateAcquisitionTimesOut(t *testing.T | |||||
| 387 | d, sess, active, _ := newManualSessionWithPTYs(t, &transactionalResizePTY{}) | 387 | d, sess, active, _ := newManualSessionWithPTYs(t, &transactionalResizePTY{}) | ||
| 388 | transport := &closeTrackingTransport{} | 388 | transport := &closeTrackingTransport{} | ||
| 389 | active.replaceTransport(transport) | 389 | active.replaceTransport(transport) | ||
| 390 | - | d.attachCoordinator(sess, active, true) | 390 | + | d.attachCoordinator(sess, nil, active, true) |
| 391 | active.publishAttachmentCapability(sess.attachmentToken(active, transport)) | 391 | active.publishAttachmentCapability(sess.attachmentToken(active, transport)) | ||
| 392 | 392 | ||||
| 393 | owner := freezeAttachmentEffectGates(active) | 393 | owner := freezeAttachmentEffectGates(active) | ||
| @@ -418,7 +418,7 @@ func TestShutdownInterruptsBlockedParticipantSend(t *testing.T) { | |||||
| 418 | d, sess, active, _ := newManualSessionWithPTYs(t, &transactionalResizePTY{}) | 418 | d, sess, active, _ := newManualSessionWithPTYs(t, &transactionalResizePTY{}) | ||
| 419 | blocked := newTeardownBlockingTransport() | 419 | blocked := newTeardownBlockingTransport() | ||
| 420 | active.replaceTransport(blocked) | 420 | active.replaceTransport(blocked) | ||
| 421 | - | d.attachCoordinator(sess, active, true) | 421 | + | d.attachCoordinator(sess, nil, active, true) |
| 422 | active.publishAttachmentCapability(sess.attachmentToken(active, blocked)) | 422 | active.publishAttachmentCapability(sess.attachmentToken(active, blocked)) | ||
| 423 | 423 | ||||
| 424 | token := sess.attachmentToken(active, blocked) | 424 | token := sess.attachmentToken(active, blocked) | ||
| @@ -444,7 +444,7 @@ func TestRenderSendFailureCleanupIsDeadlineBoundedWhileAttachmentEffectGateIsBus | |||||
| 444 | d, sess, ac, _ := newManualSessionWithPTYs(t, &transactionalResizePTY{}) | 444 | d, sess, ac, _ := newManualSessionWithPTYs(t, &transactionalResizePTY{}) | ||
| 445 | transport := &closeTrackingTransport{} | 445 | transport := &closeTrackingTransport{} | ||
| 446 | ac.replaceTransport(transport) | 446 | ac.replaceTransport(transport) | ||
| 447 | - | rc := d.attachCoordinator(sess, ac, true) | 447 | + | rc := d.attachCoordinator(sess, nil, ac, true) |
| 448 | token := sess.attachmentToken(ac, transport) | 448 | token := sess.attachmentToken(ac, transport) | ||
| 449 | token.lease = rc.attachmentLease(ac) | 449 | token.lease = rc.attachmentLease(ac) | ||
| 450 | ac.publishAttachmentCapability(token) | 450 | ac.publishAttachmentCapability(token) | ||
| @@ -474,7 +474,7 @@ func TestAttachmentEffectGateReplacementInterruptsBlockedOldRenderBeforePublicat | |||||
| 474 | d, sess, old, _ := newManualSessionWithPTYs(t, &transactionalResizePTY{}) | 474 | d, sess, old, _ := newManualSessionWithPTYs(t, &transactionalResizePTY{}) | ||
| 475 | oldTransport := newBlockedRenderReplacementTransport(d, sess) | 475 | oldTransport := newBlockedRenderReplacementTransport(d, sess) | ||
| 476 | old.replaceTransport(oldTransport) | 476 | old.replaceTransport(oldTransport) | ||
| 477 | - | rc := d.attachCoordinator(sess, old, true) | 477 | + | rc := d.attachCoordinator(sess, nil, old, true) |
| 478 | lease := rc.attachmentLease(old) | 478 | lease := rc.attachmentLease(old) | ||
| 479 | token := sess.attachmentToken(old, oldTransport) | 479 | token := sess.attachmentToken(old, oldTransport) | ||
| 480 | token.lease = lease | 480 | token.lease = lease | ||
| @@ -529,7 +529,7 @@ func TestAttachmentEffectGateAdmittedEffectCompletesBeforeConflictingTransition( | |||||
| 529 | d, sess, old, _ := newManualSessionWithPTYs(t, &transactionalResizePTY{}) | 529 | d, sess, old, _ := newManualSessionWithPTYs(t, &transactionalResizePTY{}) | ||
| 530 | oldTransport := newDatagramTestTransport() | 530 | oldTransport := newDatagramTestTransport() | ||
| 531 | old.replaceTransport(oldTransport) | 531 | old.replaceTransport(oldTransport) | ||
| 532 | - | rc := d.attachCoordinator(sess, old, true) | 532 | + | rc := d.attachCoordinator(sess, nil, old, true) |
| 533 | token := sess.attachmentToken(old, oldTransport) | 533 | token := sess.attachmentToken(old, oldTransport) | ||
| 534 | token.lease = rc.attachmentLease(old) | 534 | token.lease = rc.attachmentLease(old) | ||
| 535 | old.publishAttachmentCapability(token) | 535 | old.publishAttachmentCapability(token) | ||
| @@ -630,7 +630,7 @@ func TestAttachmentEffectGateAdmittedActiveEffectsFinishBeforeReplacement(t *tes | |||||
| 630 | oldTransport := newDatagramTestTransport() | 630 | oldTransport := newDatagramTestTransport() | ||
| 631 | old.replaceTransport(oldTransport) | 631 | old.replaceTransport(oldTransport) | ||
| 632 | old.keys = keys.NewRouter(d.clock, daemonKeyHandler{d: d, ac: old}, &d.bindings) | 632 | old.keys = keys.NewRouter(d.clock, daemonKeyHandler{d: d, ac: old}, &d.bindings) | ||
| 633 | - | rc := d.attachCoordinator(sess, old, true) | 633 | + | rc := d.attachCoordinator(sess, nil, old, true) |
| 634 | token := sess.attachmentToken(old, oldTransport) | 634 | token := sess.attachmentToken(old, oldTransport) | ||
| 635 | token.lease = rc.attachmentLease(old) | 635 | token.lease = rc.attachmentLease(old) | ||
| 636 | if tt.setup != nil { | 636 | if tt.setup != nil { | ||
| @@ -683,7 +683,7 @@ func TestJumpAttentionAdmittedHandoffCrossesSessions(t *testing.T) { | |||||
| 683 | target.tabs[1].attentionAt = time.Unix(1, 0) | 683 | target.tabs[1].attentionAt = time.Unix(1, 0) | ||
| 684 | d.sessions[target.id] = target | 684 | d.sessions[target.id] = target | ||
| 685 | 685 | ||||
| 686 | - | rc := d.attachCoordinator(source, ac, true) | 686 | + | rc := d.attachCoordinator(source, nil, ac, true) |
| 687 | token := source.attachmentToken(ac, ac.transport()) | 687 | token := source.attachmentToken(ac, ac.transport()) | ||
| 688 | token.lease = rc.attachmentLease(ac) | 688 | token.lease = rc.attachmentLease(ac) | ||
| 689 | ac.publishAttachmentCapability(token) | 689 | ac.publishAttachmentCapability(token) | ||
| @@ -701,7 +701,7 @@ func TestPickerDeleteDoesNotDeleteSourceAfterInitiatorReplacement(t *testing.T) | |||||
| 701 | d.enterPicker(sess, old) | 701 | d.enterPicker(sess, old) | ||
| 702 | 702 | ||||
| 703 | oldTransport := old.transport() | 703 | oldTransport := old.transport() | ||
| 704 | - | rc := d.attachCoordinator(sess, old, true) | 704 | + | rc := d.attachCoordinator(sess, nil, old, true) |
| 705 | token := sess.attachmentToken(old, oldTransport) | 705 | token := sess.attachmentToken(old, oldTransport) | ||
| 706 | token.lease = rc.attachmentLease(old) | 706 | token.lease = rc.attachmentLease(old) | ||
| 707 | old.publishAttachmentCapability(token) | 707 | old.publishAttachmentCapability(token) | ||
| @@ -745,7 +745,7 @@ func TestPickerDeleteSourceForCurrentInitiatorDoesNotDeadlock(t *testing.T) { | |||||
| 745 | defer release() | 745 | defer release() | ||
| 746 | d, sess, ac, _ := newManualSessionWithPTYs(t, p) | 746 | d, sess, ac, _ := newManualSessionWithPTYs(t, p) | ||
| 747 | d.enterPicker(sess, ac) | 747 | d.enterPicker(sess, ac) | ||
| 748 | - | rc := d.attachCoordinator(sess, ac, true) | 748 | + | rc := d.attachCoordinator(sess, nil, ac, true) |
| 749 | token := sess.attachmentToken(ac, ac.transport()) | 749 | token := sess.attachmentToken(ac, ac.transport()) | ||
| 750 | token.lease = rc.attachmentLease(ac) | 750 | token.lease = rc.attachmentLease(ac) | ||
| 751 | ac.publishAttachmentCapability(token) | 751 | ac.publishAttachmentCapability(token) | ||
| @@ -762,7 +762,7 @@ func TestAttachmentEffectGateAdmittedFirstPaintFinishesBeforeReplacement(t *test | |||||
| 762 | d, sess, old, _ := newManualSessionWithPTYs(t, &transactionalResizePTY{}) | 762 | d, sess, old, _ := newManualSessionWithPTYs(t, &transactionalResizePTY{}) | ||
| 763 | oldTransport := newDatagramTestTransport() | 763 | oldTransport := newDatagramTestTransport() | ||
| 764 | old.replaceTransport(oldTransport) | 764 | old.replaceTransport(oldTransport) | ||
| 765 | - | rc := d.attachCoordinator(sess, old, true) | 765 | + | rc := d.attachCoordinator(sess, nil, old, true) |
| 766 | token := sess.attachmentToken(old, oldTransport) | 766 | token := sess.attachmentToken(old, oldTransport) | ||
| 767 | token.lease = rc.attachmentLease(old) | 767 | token.lease = rc.attachmentLease(old) | ||
| 768 | 768 | ||||
| @@ -796,7 +796,7 @@ func TestAttachmentEffectGateReversedConcurrentTransitionsDoNotDeadlock(t *testi | |||||
| 796 | d, first, a, _ := newManualSessionWithPTYs(t, &transactionalResizePTY{}) | 796 | d, first, a, _ := newManualSessionWithPTYs(t, &transactionalResizePTY{}) | ||
| 797 | aTransport := newDatagramTestTransport() | 797 | aTransport := newDatagramTestTransport() | ||
| 798 | a.replaceTransport(aTransport) | 798 | a.replaceTransport(aTransport) | ||
| 799 | - | d.attachCoordinator(first, a, true) | 799 | + | d.attachCoordinator(first, nil, a, true) |
| 800 | first.attachmentToken(a, aTransport) | 800 | first.attachmentToken(a, aTransport) | ||
| 801 | 801 | ||||
| 802 | bTransport := newDatagramTestTransport() | 802 | bTransport := newDatagramTestTransport() | ||
| @@ -807,7 +807,7 @@ func TestAttachmentEffectGateReversedConcurrentTransitionsDoNotDeadlock(t *testi | |||||
| 807 | d.mu.Lock() | 807 | d.mu.Lock() | ||
| 808 | d.sessions[second.id] = second | 808 | d.sessions[second.id] = second | ||
| 809 | d.mu.Unlock() | 809 | d.mu.Unlock() | ||
| 810 | - | d.attachCoordinator(second, b, true) | 810 | + | d.attachCoordinator(second, nil, b, true) |
| 811 | second.attachmentToken(b, bTransport) | 811 | second.attachmentToken(b, bTransport) | ||
| 812 | 812 | ||||
| 813 | start := make(chan struct{}) | 813 | start := make(chan struct{}) | ||
| @@ -854,7 +854,7 @@ func TestAttachmentEffectGateFrozenConnectionRejectsLateEffect(t *testing.T) { | |||||
| 854 | d, sess, ac, _ := newManualSessionWithPTYs(t, &transactionalResizePTY{}) | 854 | d, sess, ac, _ := newManualSessionWithPTYs(t, &transactionalResizePTY{}) | ||
| 855 | transport := newDatagramTestTransport() | 855 | transport := newDatagramTestTransport() | ||
| 856 | ac.replaceTransport(transport) | 856 | ac.replaceTransport(transport) | ||
| 857 | - | rc := d.attachCoordinator(sess, ac, true) | 857 | + | rc := d.attachCoordinator(sess, nil, ac, true) |
| 858 | token := sess.attachmentToken(ac, transport) | 858 | token := sess.attachmentToken(ac, transport) | ||
| 859 | token.lease = rc.attachmentLease(ac) | 859 | token.lease = rc.attachmentLease(ac) | ||
| 860 | ac.publishAttachmentCapability(token) | 860 | ac.publishAttachmentCapability(token) | ||
M internal/usecase/daemon/attachment_registry.go +51 -26
| @@ -39,11 +39,7 @@ func (ac *attachedClient) viewSnapshot() attachmentView { | |||
| 39 | 39 | return ac.view | |
| 40 | 40 | } | |
| 41 | 41 | ||
| 42 | - | func (ac *attachedClient) publishView(view attachmentView) { | |
| 43 | - | if ac == nil { | |
| 44 | - | return | |
| 45 | - | } | |
| 46 | - | ac.viewMu.Lock() | |
| 42 | + | func (ac *attachedClient) publishViewLocked(view attachmentView) { | |
| 47 | 43 | // Rebase while viewMu is held, before the new revision becomes visible. | |
| 48 | 44 | // Output fences take the same lock, so no side effect can pair the new | |
| 49 | 45 | // ViewRevision with the previous output epoch. Test/headless fixtures may | |
| @@ -58,7 +54,28 @@ func (ac *attachedClient) publishView(view attachmentView) { | |||
| 58 | 54 | } | |
| 59 | 55 | } | |
| 60 | 56 | ac.view = view | |
| 61 | - | ac.viewMu.Unlock() | |
| 57 | + | } | |
| 58 | + | ||
| 59 | + | func (ac *attachedClient) publishView(view attachmentView) { | |
| 60 | + | if ac == nil { | |
| 61 | + | return | |
| 62 | + | } | |
| 63 | + | ac.viewMu.Lock() | |
| 64 | + | defer ac.viewMu.Unlock() | |
| 65 | + | ac.publishViewLocked(view) | |
| 66 | + | } | |
| 67 | + | ||
| 68 | + | func (ac *attachedClient) publishViewIfCurrent(before, view attachmentView) bool { | |
| 69 | + | if ac == nil { | |
| 70 | + | return false | |
| 71 | + | } | |
| 72 | + | ac.viewMu.Lock() | |
| 73 | + | defer ac.viewMu.Unlock() | |
| 74 | + | if ac.view != before { | |
| 75 | + | return false | |
| 76 | + | } | |
| 77 | + | ac.publishViewLocked(view) | |
| 78 | + | return true | |
| 62 | 79 | } | |
| 63 | 80 | ||
| 64 | 81 | // registerAttachmentLocked admits ac exactly once. Caller holds s.mu and must | |
| @@ -303,19 +320,20 @@ func (s *session) repairAttachmentView(ac *attachedClient) bool { | |||
| 303 | 320 | if s == nil || ac == nil { | |
| 304 | 321 | return false | |
| 305 | 322 | } | |
| 323 | + | before := ac.viewSnapshot() | |
| 306 | 324 | s.mu.Lock() | |
| 307 | - | defer s.mu.Unlock() | |
| 308 | 325 | if _, ok := s.attachments[ac]; !ok { | |
| 326 | + | s.mu.Unlock() | |
| 309 | 327 | return false | |
| 310 | 328 | } | |
| 311 | - | before := ac.viewSnapshot() | |
| 312 | 329 | after := s.repairAttachmentViewLocked(ac, before) | |
| 313 | 330 | if before.tabID == after.tabID && before.paneID == after.paneID && before.windowTop == after.windowTop && before.windowRows == after.windowRows && before.windowSet == after.windowSet && before.bookmark == after.bookmark && before.liveBottom == after.liveBottom { | |
| 331 | + | s.mu.Unlock() | |
| 314 | 332 | return false | |
| 315 | 333 | } | |
| 316 | 334 | after.revision++ | |
| 317 | - | ac.publishView(after) | |
| 318 | - | return true | |
| 335 | + | s.mu.Unlock() | |
| 336 | + | return ac.publishViewIfCurrent(before, after) | |
| 319 | 337 | } | |
| 320 | 338 | ||
| 321 | 339 | // prepareAttachmentViewsForRemovedTabLocked selects the nearest surviving tab | |
| @@ -376,24 +394,31 @@ func (s *session) tabForAttachment(ac *attachedClient) *tab { | |||
| 376 | 394 | if s == nil || ac == nil { | |
| 377 | 395 | return nil | |
| 378 | 396 | } | |
| 379 | - | s.repairAttachmentView(ac) | |
| 380 | - | s.mu.Lock() | |
| 381 | - | defer s.mu.Unlock() | |
| 382 | - | if _, ok := s.attachments[ac]; !ok { | |
| 383 | - | return nil | |
| 384 | - | } | |
| 385 | - | view := ac.viewSnapshot() | |
| 386 | - | repaired := s.repairAttachmentViewLocked(ac, view) | |
| 387 | - | if repaired.tabID != view.tabID || repaired.paneID != view.paneID || repaired.windowTop != view.windowTop || repaired.windowRows != view.windowRows || repaired.windowSet != view.windowSet || repaired.bookmark != view.bookmark || repaired.liveBottom != view.liveBottom { | |
| 388 | - | repaired.revision++ | |
| 389 | - | ac.publishView(repaired) | |
| 390 | - | } | |
| 391 | - | for _, tb := range s.tabs { | |
| 392 | - | if tb != nil && domain.TabStableID(tb.stableID) == repaired.tabID { | |
| 393 | - | return tb | |
| 397 | + | for { | |
| 398 | + | view := ac.viewSnapshot() | |
| 399 | + | s.mu.Lock() | |
| 400 | + | if _, ok := s.attachments[ac]; !ok { | |
| 401 | + | s.mu.Unlock() | |
| 402 | + | return nil | |
| 403 | + | } | |
| 404 | + | repaired := s.repairAttachmentViewLocked(ac, view) | |
| 405 | + | var target *tab | |
| 406 | + | for _, tb := range s.tabs { | |
| 407 | + | if tb != nil && domain.TabStableID(tb.stableID) == repaired.tabID { | |
| 408 | + | target = tb | |
| 409 | + | break | |
| 410 | + | } | |
| 411 | + | } | |
| 412 | + | s.mu.Unlock() | |
| 413 | + | ||
| 414 | + | if repaired.tabID != view.tabID || repaired.paneID != view.paneID || repaired.windowTop != view.windowTop || repaired.windowRows != view.windowRows || repaired.windowSet != view.windowSet || repaired.bookmark != view.bookmark || repaired.liveBottom != view.liveBottom { | |
| 415 | + | repaired.revision++ | |
| 416 | + | } | |
| 417 | + | if !ac.publishViewIfCurrent(view, repaired) { | |
| 418 | + | continue | |
| 394 | 419 | } | |
| 420 | + | return target | |
| 395 | 421 | } | |
| 396 | - | return nil | |
| 397 | 422 | } | |
| 398 | 423 | ||
| 399 | 424 | // paneForAttachment resolves both stable IDs and repairs a removed pane before | |
| @@ -39,11 +39,7 @@ func (ac *attachedClient) viewSnapshot() attachmentView { | |||||
| 39 | return ac.view | 39 | return ac.view | ||
| 40 | } | 40 | } | ||
| 41 | 41 | ||||
| 42 | - | func (ac *attachedClient) publishView(view attachmentView) { | 42 | + | func (ac *attachedClient) publishViewLocked(view attachmentView) { |
| 43 | - | if ac == nil { | |||
| 44 | - | return | |||
| 45 | - | } | |||
| 46 | - | ac.viewMu.Lock() | |||
| 47 | // Rebase while viewMu is held, before the new revision becomes visible. | 43 | // Rebase while viewMu is held, before the new revision becomes visible. | ||
| 48 | // Output fences take the same lock, so no side effect can pair the new | 44 | // Output fences take the same lock, so no side effect can pair the new | ||
| 49 | // ViewRevision with the previous output epoch. Test/headless fixtures may | 45 | // ViewRevision with the previous output epoch. Test/headless fixtures may | ||
| @@ -58,7 +54,28 @@ func (ac *attachedClient) publishView(view attachmentView) { | |||||
| 58 | } | 54 | } | ||
| 59 | } | 55 | } | ||
| 60 | ac.view = view | 56 | ac.view = view | ||
| 61 | - | ac.viewMu.Unlock() | 57 | + | } |
| 58 | + | ||||
| 59 | + | func (ac *attachedClient) publishView(view attachmentView) { | |||
| 60 | + | if ac == nil { | |||
| 61 | + | return | |||
| 62 | + | } | |||
| 63 | + | ac.viewMu.Lock() | |||
| 64 | + | defer ac.viewMu.Unlock() | |||
| 65 | + | ac.publishViewLocked(view) | |||
| 66 | + | } | |||
| 67 | + | ||||
| 68 | + | func (ac *attachedClient) publishViewIfCurrent(before, view attachmentView) bool { | |||
| 69 | + | if ac == nil { | |||
| 70 | + | return false | |||
| 71 | + | } | |||
| 72 | + | ac.viewMu.Lock() | |||
| 73 | + | defer ac.viewMu.Unlock() | |||
| 74 | + | if ac.view != before { | |||
| 75 | + | return false | |||
| 76 | + | } | |||
| 77 | + | ac.publishViewLocked(view) | |||
| 78 | + | return true | |||
| 62 | } | 79 | } | ||
| 63 | 80 | ||||
| 64 | // registerAttachmentLocked admits ac exactly once. Caller holds s.mu and must | 81 | // registerAttachmentLocked admits ac exactly once. Caller holds s.mu and must | ||
| @@ -303,19 +320,20 @@ func (s *session) repairAttachmentView(ac *attachedClient) bool { | |||||
| 303 | if s == nil || ac == nil { | 320 | if s == nil || ac == nil { | ||
| 304 | return false | 321 | return false | ||
| 305 | } | 322 | } | ||
| 323 | + | before := ac.viewSnapshot() | |||
| 306 | s.mu.Lock() | 324 | s.mu.Lock() | ||
| 307 | - | defer s.mu.Unlock() | |||
| 308 | if _, ok := s.attachments[ac]; !ok { | 325 | if _, ok := s.attachments[ac]; !ok { | ||
| 326 | + | s.mu.Unlock() | |||
| 309 | return false | 327 | return false | ||
| 310 | } | 328 | } | ||
| 311 | - | before := ac.viewSnapshot() | |||
| 312 | after := s.repairAttachmentViewLocked(ac, before) | 329 | after := s.repairAttachmentViewLocked(ac, before) | ||
| 313 | if before.tabID == after.tabID && before.paneID == after.paneID && before.windowTop == after.windowTop && before.windowRows == after.windowRows && before.windowSet == after.windowSet && before.bookmark == after.bookmark && before.liveBottom == after.liveBottom { | 330 | if before.tabID == after.tabID && before.paneID == after.paneID && before.windowTop == after.windowTop && before.windowRows == after.windowRows && before.windowSet == after.windowSet && before.bookmark == after.bookmark && before.liveBottom == after.liveBottom { | ||
| 331 | + | s.mu.Unlock() | |||
| 314 | return false | 332 | return false | ||
| 315 | } | 333 | } | ||
| 316 | after.revision++ | 334 | after.revision++ | ||
| 317 | - | ac.publishView(after) | 335 | + | s.mu.Unlock() |
| 318 | - | return true | 336 | + | return ac.publishViewIfCurrent(before, after) |
| 319 | } | 337 | } | ||
| 320 | 338 | ||||
| 321 | // prepareAttachmentViewsForRemovedTabLocked selects the nearest surviving tab | 339 | // prepareAttachmentViewsForRemovedTabLocked selects the nearest surviving tab | ||
| @@ -376,24 +394,31 @@ func (s *session) tabForAttachment(ac *attachedClient) *tab { | |||||
| 376 | if s == nil || ac == nil { | 394 | if s == nil || ac == nil { | ||
| 377 | return nil | 395 | return nil | ||
| 378 | } | 396 | } | ||
| 379 | - | s.repairAttachmentView(ac) | 397 | + | for { |
| 380 | - | s.mu.Lock() | 398 | + | view := ac.viewSnapshot() |
| 381 | - | defer s.mu.Unlock() | 399 | + | s.mu.Lock() |
| 382 | - | if _, ok := s.attachments[ac]; !ok { | 400 | + | if _, ok := s.attachments[ac]; !ok { |
| 383 | - | return nil | 401 | + | s.mu.Unlock() |
| 384 | - | } | 402 | + | return nil |
| 385 | - | view := ac.viewSnapshot() | 403 | + | } |
| 386 | - | repaired := s.repairAttachmentViewLocked(ac, view) | 404 | + | repaired := s.repairAttachmentViewLocked(ac, view) |
| 387 | - | if repaired.tabID != view.tabID || repaired.paneID != view.paneID || repaired.windowTop != view.windowTop || repaired.windowRows != view.windowRows || repaired.windowSet != view.windowSet || repaired.bookmark != view.bookmark || repaired.liveBottom != view.liveBottom { | 405 | + | var target *tab |
| 388 | - | repaired.revision++ | 406 | + | for _, tb := range s.tabs { |
| 389 | - | ac.publishView(repaired) | 407 | + | if tb != nil && domain.TabStableID(tb.stableID) == repaired.tabID { |
| 390 | - | } | 408 | + | target = tb |
| 391 | - | for _, tb := range s.tabs { | 409 | + | break |
| 392 | - | if tb != nil && domain.TabStableID(tb.stableID) == repaired.tabID { | 410 | + | } |
| 393 | - | return tb | 411 | + | } |
| 412 | + | s.mu.Unlock() | |||
| 413 | + | ||||
| 414 | + | if repaired.tabID != view.tabID || repaired.paneID != view.paneID || repaired.windowTop != view.windowTop || repaired.windowRows != view.windowRows || repaired.windowSet != view.windowSet || repaired.bookmark != view.bookmark || repaired.liveBottom != view.liveBottom { | |||
| 415 | + | repaired.revision++ | |||
| 416 | + | } | |||
| 417 | + | if !ac.publishViewIfCurrent(view, repaired) { | |||
| 418 | + | continue | |||
| 394 | } | 419 | } | ||
| 420 | + | return target | |||
| 395 | } | 421 | } | ||
| 396 | - | return nil | |||
| 397 | } | 422 | } | ||
| 398 | 423 | ||||
| 399 | // paneForAttachment resolves both stable IDs and repairs a removed pane before | 424 | // paneForAttachment resolves both stable IDs and repairs a removed pane before | ||
M internal/usecase/daemon/attachment_registry_test.go +3 -3
| @@ -186,15 +186,15 @@ func TestAttachmentRegistryConcurrentRegistrationSnapshotIsStable(t *testing.T) | |||
| 186 | 186 | t.Fatalf("snapshot[%d] id = %d, want %d", i, got[i].clientID[0], want) | |
| 187 | 187 | } | |
| 188 | 188 | } | |
| 189 | - | ordered := sess.repairAttachmentViewsLockedForTest() | |
| 189 | + | ordered := sess.repairAttachmentViewsForTest() | |
| 190 | 190 | for i, want := range []byte{1, 2, 3} { | |
| 191 | 191 | if ordered[i].clientID[0] != want { | |
| 192 | - | t.Fatalf("invalidation[%d] id = %d, want %d", i, ordered[i].clientID[0], want) | |
| 192 | + | t.Fatalf("ordered attachment[%d] id = %d, want %d", i, ordered[i].clientID[0], want) | |
| 193 | 193 | } | |
| 194 | 194 | } | |
| 195 | 195 | } | |
| 196 | 196 | ||
| 197 | - | func (s *session) repairAttachmentViewsLockedForTest() []*attachedClient { | |
| 197 | + | func (s *session) repairAttachmentViewsForTest() []*attachedClient { | |
| 198 | 198 | s.mu.Lock() | |
| 199 | 199 | defer s.mu.Unlock() | |
| 200 | 200 | s.repairAttachmentViewsLocked() | |
| @@ -186,15 +186,15 @@ func TestAttachmentRegistryConcurrentRegistrationSnapshotIsStable(t *testing.T) | |||||
| 186 | t.Fatalf("snapshot[%d] id = %d, want %d", i, got[i].clientID[0], want) | 186 | t.Fatalf("snapshot[%d] id = %d, want %d", i, got[i].clientID[0], want) | ||
| 187 | } | 187 | } | ||
| 188 | } | 188 | } | ||
| 189 | - | ordered := sess.repairAttachmentViewsLockedForTest() | 189 | + | ordered := sess.repairAttachmentViewsForTest() |
| 190 | for i, want := range []byte{1, 2, 3} { | 190 | for i, want := range []byte{1, 2, 3} { | ||
| 191 | if ordered[i].clientID[0] != want { | 191 | if ordered[i].clientID[0] != want { | ||
| 192 | - | t.Fatalf("invalidation[%d] id = %d, want %d", i, ordered[i].clientID[0], want) | 192 | + | t.Fatalf("ordered attachment[%d] id = %d, want %d", i, ordered[i].clientID[0], want) |
| 193 | } | 193 | } | ||
| 194 | } | 194 | } | ||
| 195 | } | 195 | } | ||
| 196 | 196 | ||||
| 197 | - | func (s *session) repairAttachmentViewsLockedForTest() []*attachedClient { | 197 | + | func (s *session) repairAttachmentViewsForTest() []*attachedClient { |
| 198 | s.mu.Lock() | 198 | s.mu.Lock() | ||
| 199 | defer s.mu.Unlock() | 199 | defer s.mu.Unlock() | ||
| 200 | s.repairAttachmentViewsLocked() | 200 | s.repairAttachmentViewsLocked() | ||
M internal/usecase/daemon/attachment_render_test.go +11 -3
| @@ -34,6 +34,7 @@ func (*blockedAttachmentTransport) Close() error { return nil } | |||
| 34 | 34 | func TestStaleCapacityReadinessRequeuesWithoutAnotherInvalidation(t *testing.T) { | |
| 35 | 35 | d, sess, ac, sends := newManualSessionWithPTYs(t, nil) | |
| 36 | 36 | rc := d.attachCoordinator(sess, nil, ac, true) | |
| 37 | + | require.True(t, sess.repairAttachmentView(ac)) | |
| 37 | 38 | rc.opts.clock = nil // drive fire explicitly; the interleaving is the test. | |
| 38 | 39 | ||
| 39 | 40 | firstObserved := make(chan struct{}) | |
| @@ -144,7 +145,8 @@ func TestAttachmentResizeKeepsSessionContentAndPeersFixed(t *testing.T) { | |||
| 144 | 145 | func TestAttachmentFirstPaintDoesNotWaitForBlockedPeer(t *testing.T) { | |
| 145 | 146 | pty, releasePTY := newBlockingPTY(t) | |
| 146 | 147 | defer releasePTY() | |
| 147 | - | d := newTestDaemon(t, newFactory(t, pty), stubClock{}) | |
| 148 | + | clock := &signalClock{timers: make(chan *signalTimer, 16)} | |
| 149 | + | d := newTestDaemon(t, newFactory(t, pty), clock) | |
| 148 | 150 | oldTransport := &blockedAttachmentTransport{entered: make(chan struct{}), release: make(chan struct{}), sends: make(chan ports.Frame, 8)} | |
| 149 | 151 | sess, old, err := d.route(ports.Hello{Version: ports.ProtocolVersion, Intent: ports.IntentNew, Name: "work", Size: domain.Size{Cols: 80, Rows: 24}, ClientID: [16]byte{1}}, oldTransport) | |
| 150 | 152 | require.NoError(t, err) | |
| @@ -157,7 +159,10 @@ func TestAttachmentFirstPaintDoesNotWaitForBlockedPeer(t *testing.T) { | |||
| 157 | 159 | freshLease := rc.attachmentLease(fresh) | |
| 158 | 160 | require.True(t, rc.markAttachmentReady(oldLease)) | |
| 159 | 161 | oldTransport.blocked.Store(true) | |
| 160 | - | sess.tabs[0].focusedPane().screen.Write([]byte("slow peer")) | |
| 162 | + | pane := sess.tabs[0].focusedPane() | |
| 163 | + | pane.mu.Lock() | |
| 164 | + | pane.screen.Write([]byte("slow peer")) | |
| 165 | + | pane.mu.Unlock() | |
| 161 | 166 | require.True(t, rc.invalidate(renderInvalidation{class: invalidateUrgent, reset: true, producer: "test"})) | |
| 162 | 167 | rc.fireCurrent(false) | |
| 163 | 168 | awaitTestCompletion(t, oldTransport.entered, "slow attachment did not begin its blocked output") | |
| @@ -185,7 +190,10 @@ func TestMultiAttachmentHandshakeFirstPaintNotGatedByBlockedPeer(t *testing.T) { | |||
| 185 | 190 | require.True(t, rc.markAttachmentReady(rc.attachmentLease(old))) | |
| 186 | 191 | ||
| 187 | 192 | oldTransport.blocked.Store(true) | |
| 188 | - | sess.tabs[0].focusedPane().screen.Write([]byte("slow peer")) | |
| 193 | + | pane := sess.tabs[0].focusedPane() | |
| 194 | + | pane.mu.Lock() | |
| 195 | + | pane.screen.Write([]byte("slow peer")) | |
| 196 | + | pane.mu.Unlock() | |
| 189 | 197 | require.True(t, rc.invalidate(renderInvalidation{class: invalidateUrgent, reset: true, producer: "test"})) | |
| 190 | 198 | fireDone := make(chan struct{}) | |
| 191 | 199 | go func() { | |
| @@ -34,6 +34,7 @@ func (*blockedAttachmentTransport) Close() error { return nil } | |||||
| 34 | func TestStaleCapacityReadinessRequeuesWithoutAnotherInvalidation(t *testing.T) { | 34 | func TestStaleCapacityReadinessRequeuesWithoutAnotherInvalidation(t *testing.T) { | ||
| 35 | d, sess, ac, sends := newManualSessionWithPTYs(t, nil) | 35 | d, sess, ac, sends := newManualSessionWithPTYs(t, nil) | ||
| 36 | rc := d.attachCoordinator(sess, nil, ac, true) | 36 | rc := d.attachCoordinator(sess, nil, ac, true) | ||
| 37 | + | require.True(t, sess.repairAttachmentView(ac)) | |||
| 37 | rc.opts.clock = nil // drive fire explicitly; the interleaving is the test. | 38 | rc.opts.clock = nil // drive fire explicitly; the interleaving is the test. | ||
| 38 | 39 | ||||
| 39 | firstObserved := make(chan struct{}) | 40 | firstObserved := make(chan struct{}) | ||
| @@ -144,7 +145,8 @@ func TestAttachmentResizeKeepsSessionContentAndPeersFixed(t *testing.T) { | |||||
| 144 | func TestAttachmentFirstPaintDoesNotWaitForBlockedPeer(t *testing.T) { | 145 | func TestAttachmentFirstPaintDoesNotWaitForBlockedPeer(t *testing.T) { | ||
| 145 | pty, releasePTY := newBlockingPTY(t) | 146 | pty, releasePTY := newBlockingPTY(t) | ||
| 146 | defer releasePTY() | 147 | defer releasePTY() | ||
| 147 | - | d := newTestDaemon(t, newFactory(t, pty), stubClock{}) | 148 | + | clock := &signalClock{timers: make(chan *signalTimer, 16)} |
| 149 | + | d := newTestDaemon(t, newFactory(t, pty), clock) | |||
| 148 | oldTransport := &blockedAttachmentTransport{entered: make(chan struct{}), release: make(chan struct{}), sends: make(chan ports.Frame, 8)} | 150 | oldTransport := &blockedAttachmentTransport{entered: make(chan struct{}), release: make(chan struct{}), sends: make(chan ports.Frame, 8)} | ||
| 149 | sess, old, err := d.route(ports.Hello{Version: ports.ProtocolVersion, Intent: ports.IntentNew, Name: "work", Size: domain.Size{Cols: 80, Rows: 24}, ClientID: [16]byte{1}}, oldTransport) | 151 | sess, old, err := d.route(ports.Hello{Version: ports.ProtocolVersion, Intent: ports.IntentNew, Name: "work", Size: domain.Size{Cols: 80, Rows: 24}, ClientID: [16]byte{1}}, oldTransport) | ||
| 150 | require.NoError(t, err) | 152 | require.NoError(t, err) | ||
| @@ -157,7 +159,10 @@ func TestAttachmentFirstPaintDoesNotWaitForBlockedPeer(t *testing.T) { | |||||
| 157 | freshLease := rc.attachmentLease(fresh) | 159 | freshLease := rc.attachmentLease(fresh) | ||
| 158 | require.True(t, rc.markAttachmentReady(oldLease)) | 160 | require.True(t, rc.markAttachmentReady(oldLease)) | ||
| 159 | oldTransport.blocked.Store(true) | 161 | oldTransport.blocked.Store(true) | ||
| 160 | - | sess.tabs[0].focusedPane().screen.Write([]byte("slow peer")) | 162 | + | pane := sess.tabs[0].focusedPane() |
| 163 | + | pane.mu.Lock() | |||
| 164 | + | pane.screen.Write([]byte("slow peer")) | |||
| 165 | + | pane.mu.Unlock() | |||
| 161 | require.True(t, rc.invalidate(renderInvalidation{class: invalidateUrgent, reset: true, producer: "test"})) | 166 | require.True(t, rc.invalidate(renderInvalidation{class: invalidateUrgent, reset: true, producer: "test"})) | ||
| 162 | rc.fireCurrent(false) | 167 | rc.fireCurrent(false) | ||
| 163 | awaitTestCompletion(t, oldTransport.entered, "slow attachment did not begin its blocked output") | 168 | awaitTestCompletion(t, oldTransport.entered, "slow attachment did not begin its blocked output") | ||
| @@ -185,7 +190,10 @@ func TestMultiAttachmentHandshakeFirstPaintNotGatedByBlockedPeer(t *testing.T) { | |||||
| 185 | require.True(t, rc.markAttachmentReady(rc.attachmentLease(old))) | 190 | require.True(t, rc.markAttachmentReady(rc.attachmentLease(old))) | ||
| 186 | 191 | ||||
| 187 | oldTransport.blocked.Store(true) | 192 | oldTransport.blocked.Store(true) | ||
| 188 | - | sess.tabs[0].focusedPane().screen.Write([]byte("slow peer")) | 193 | + | pane := sess.tabs[0].focusedPane() |
| 194 | + | pane.mu.Lock() | |||
| 195 | + | pane.screen.Write([]byte("slow peer")) | |||
| 196 | + | pane.mu.Unlock() | |||
| 189 | require.True(t, rc.invalidate(renderInvalidation{class: invalidateUrgent, reset: true, producer: "test"})) | 197 | require.True(t, rc.invalidate(renderInvalidation{class: invalidateUrgent, reset: true, producer: "test"})) | ||
| 190 | fireDone := make(chan struct{}) | 198 | fireDone := make(chan struct{}) | ||
| 191 | go func() { | 199 | go func() { | ||
M internal/usecase/daemon/client.go +2 -2
| @@ -803,8 +803,8 @@ func (d *Daemon) resizeAttachmentForLease(token attachmentConnectionToken, size | |||
| 803 | 803 | ac.pipelineScratch = composeCacheInput{} | |
| 804 | 804 | ac.sendMu.Unlock() | |
| 805 | 805 | ||
| 806 | - | sess, ok := localSession(token.sess) | |
| 807 | - | if !ok { | |
| 806 | + | sess := token.sess | |
| 807 | + | if sess == nil { | |
| 808 | 808 | return false | |
| 809 | 809 | } | |
| 810 | 810 | rc := sess.renderCoordinator() | |
| @@ -803,8 +803,8 @@ func (d *Daemon) resizeAttachmentForLease(token attachmentConnectionToken, size | |||||
| 803 | ac.pipelineScratch = composeCacheInput{} | 803 | ac.pipelineScratch = composeCacheInput{} | ||
| 804 | ac.sendMu.Unlock() | 804 | ac.sendMu.Unlock() | ||
| 805 | 805 | ||||
| 806 | - | sess, ok := localSession(token.sess) | 806 | + | sess := token.sess |
| 807 | - | if !ok { | 807 | + | if sess == nil { |
| 808 | return false | 808 | return false | ||
| 809 | } | 809 | } | ||
| 810 | rc := sess.renderCoordinator() | 810 | rc := sess.renderCoordinator() | ||
M internal/usecase/daemon/clipboard_test.go +1 -1
| @@ -203,7 +203,7 @@ func chunkReadPTY(t *testing.T, chunks ...[]byte) *portsmocks.MockPTY { | |||
| 203 | 203 | } | |
| 204 | 204 | ||
| 205 | 205 | func publishActiveClipboardCapability(d *Daemon, sess *session, ac *attachedClient, tr ports.Transport) { | |
| 206 | - | rc := d.attachCoordinator(sess, ac, true) | |
| 206 | + | rc := d.attachCoordinator(sess, nil, ac, true) | |
| 207 | 207 | token := sess.attachmentToken(ac, tr) | |
| 208 | 208 | token.lease = rc.attachmentLease(ac) | |
| 209 | 209 | ac.publishAttachmentCapability(token) | |
| @@ -203,7 +203,7 @@ func chunkReadPTY(t *testing.T, chunks ...[]byte) *portsmocks.MockPTY { | |||||
| 203 | } | 203 | } | ||
| 204 | 204 | ||||
| 205 | func publishActiveClipboardCapability(d *Daemon, sess *session, ac *attachedClient, tr ports.Transport) { | 205 | func publishActiveClipboardCapability(d *Daemon, sess *session, ac *attachedClient, tr ports.Transport) { | ||
| 206 | - | rc := d.attachCoordinator(sess, ac, true) | 206 | + | rc := d.attachCoordinator(sess, nil, ac, true) |
| 207 | token := sess.attachmentToken(ac, tr) | 207 | token := sess.attachmentToken(ac, tr) | ||
| 208 | token.lease = rc.attachmentLease(ac) | 208 | token.lease = rc.attachmentLease(ac) | ||
| 209 | ac.publishAttachmentCapability(token) | 209 | ac.publishAttachmentCapability(token) | ||
M internal/usecase/daemon/move_cleanup_test.go +2 -2
| @@ -35,7 +35,7 @@ func TestMovePaneReleasesResizeFencesBeforeAttachmentCleanup(t *testing.T) { | |||
| 35 | 35 | defer releaseDestination() | |
| 36 | 36 | ||
| 37 | 37 | d, source, client, _ := newManualSessionWithPTYs(t, movedPTY, remainingPTY) | |
| 38 | - | require.NotNil(t, d.attachCoordinator(source, client, true)) | |
| 38 | + | require.NotNil(t, d.attachCoordinator(source, nil, client, true)) | |
| 39 | 39 | sourceTab := source.tabs[0] | |
| 40 | 40 | sourceTab.stableID = "source-tab" | |
| 41 | 41 | moved := sourceTab.focusedPane() | |
| @@ -117,7 +117,7 @@ func TestMovePaneCleanupUsesCommitPointSourceAttachmentToken(t *testing.T) { | |||
| 117 | 117 | sourceTab.stableID = "source-tab" | |
| 118 | 118 | remainingTab.stableID = "remaining-tab" | |
| 119 | 119 | moved := sourceTab.focusedPane() | |
| 120 | - | require.NotNil(t, d.attachCoordinator(source, displaced, true)) | |
| 120 | + | require.NotNil(t, d.attachCoordinator(source, nil, displaced, true)) | |
| 121 | 121 | ||
| 122 | 122 | destination := &session{sessionCore: sessionCore{id: "destination", name: "destination", ephemeral: true}, tabs: []*tab{newTabWithStableID("destination-tab", "destination-pane", destinationPTY, domain.Size{Cols: 80, Rows: 23})}} | |
| 123 | 123 | destinationTab := destination.tabs[0] | |
| @@ -35,7 +35,7 @@ func TestMovePaneReleasesResizeFencesBeforeAttachmentCleanup(t *testing.T) { | |||||
| 35 | defer releaseDestination() | 35 | defer releaseDestination() | ||
| 36 | 36 | ||||
| 37 | d, source, client, _ := newManualSessionWithPTYs(t, movedPTY, remainingPTY) | 37 | d, source, client, _ := newManualSessionWithPTYs(t, movedPTY, remainingPTY) | ||
| 38 | - | require.NotNil(t, d.attachCoordinator(source, client, true)) | 38 | + | require.NotNil(t, d.attachCoordinator(source, nil, client, true)) |
| 39 | sourceTab := source.tabs[0] | 39 | sourceTab := source.tabs[0] | ||
| 40 | sourceTab.stableID = "source-tab" | 40 | sourceTab.stableID = "source-tab" | ||
| 41 | moved := sourceTab.focusedPane() | 41 | moved := sourceTab.focusedPane() | ||
| @@ -117,7 +117,7 @@ func TestMovePaneCleanupUsesCommitPointSourceAttachmentToken(t *testing.T) { | |||||
| 117 | sourceTab.stableID = "source-tab" | 117 | sourceTab.stableID = "source-tab" | ||
| 118 | remainingTab.stableID = "remaining-tab" | 118 | remainingTab.stableID = "remaining-tab" | ||
| 119 | moved := sourceTab.focusedPane() | 119 | moved := sourceTab.focusedPane() | ||
| 120 | - | require.NotNil(t, d.attachCoordinator(source, displaced, true)) | 120 | + | require.NotNil(t, d.attachCoordinator(source, nil, displaced, true)) |
| 121 | 121 | ||||
| 122 | destination := &session{sessionCore: sessionCore{id: "destination", name: "destination", ephemeral: true}, tabs: []*tab{newTabWithStableID("destination-tab", "destination-pane", destinationPTY, domain.Size{Cols: 80, Rows: 23})}} | 122 | destination := &session{sessionCore: sessionCore{id: "destination", name: "destination", ephemeral: true}, tabs: []*tab{newTabWithStableID("destination-tab", "destination-pane", destinationPTY, domain.Size{Cols: 80, Rows: 23})}} | ||
| 123 | destinationTab := destination.tabs[0] | 123 | destinationTab := destination.tabs[0] | ||
M internal/usecase/daemon/move_gate_race_test.go +3 -3
| @@ -19,7 +19,7 @@ func newMoveGateRaceFixture(t *testing.T) (*Daemon, *session, *tab, *pane, *sess | |||
| 19 | 19 | sourceTab := source.tabs[0] | |
| 20 | 20 | sourceTab.stableID = "source-tab" | |
| 21 | 21 | moved := sourceTab.focusedPane() | |
| 22 | - | require.NotNil(t, d.attachCoordinator(source, client, true)) | |
| 22 | + | require.NotNil(t, d.attachCoordinator(source, nil, client, true)) | |
| 23 | 23 | ||
| 24 | 24 | destination := &session{sessionCore: sessionCore{id: "destination", name: "destination", ephemeral: true}, tabs: []*tab{newTabWithStableID("destination-tab", "destination-pane", destinationPTY, domain.Size{Cols: 80, Rows: 23})}} | |
| 25 | 25 | destinationTab := destination.tabs[0] | |
| @@ -184,7 +184,7 @@ func TestFinalCloseDoesNotDeadlockWithMoveDispatchAdmission(t *testing.T) { | |||
| 184 | 184 | ||
| 185 | 185 | func TestPaletteFinalCloseEndsCurrentEffectBeforeDrainingPeers(t *testing.T) { | |
| 186 | 186 | d, sess, current, _ := newManualSessionWithPTYs(t, newQuietPTY()) | |
| 187 | - | d.attachCoordinator(sess, current, true) | |
| 187 | + | d.attachCoordinator(sess, nil, current, true) | |
| 188 | 188 | otherTransport := &closeTrackingTransport{} | |
| 189 | 189 | other := &attachedClient{tr: otherTransport, output: newOutputStateStream(), size: current.size} | |
| 190 | 190 | other.initOverlays() | |
| @@ -192,7 +192,7 @@ func TestPaletteFinalCloseEndsCurrentEffectBeforeDrainingPeers(t *testing.T) { | |||
| 192 | 192 | sess.mu.Lock() | |
| 193 | 193 | require.True(t, sess.registerAttachmentLocked(other)) | |
| 194 | 194 | sess.mu.Unlock() | |
| 195 | - | d.attachCoordinator(sess, other, true) | |
| 195 | + | d.attachCoordinator(sess, nil, other, true) | |
| 196 | 196 | ||
| 197 | 197 | currentToken := sess.attachmentToken(current, current.transport()) | |
| 198 | 198 | currentEffect, admitted := current.beginAttachmentEffect(currentToken) | |
| @@ -19,7 +19,7 @@ func newMoveGateRaceFixture(t *testing.T) (*Daemon, *session, *tab, *pane, *sess | |||||
| 19 | sourceTab := source.tabs[0] | 19 | sourceTab := source.tabs[0] | ||
| 20 | sourceTab.stableID = "source-tab" | 20 | sourceTab.stableID = "source-tab" | ||
| 21 | moved := sourceTab.focusedPane() | 21 | moved := sourceTab.focusedPane() | ||
| 22 | - | require.NotNil(t, d.attachCoordinator(source, client, true)) | 22 | + | require.NotNil(t, d.attachCoordinator(source, nil, client, true)) |
| 23 | 23 | ||||
| 24 | destination := &session{sessionCore: sessionCore{id: "destination", name: "destination", ephemeral: true}, tabs: []*tab{newTabWithStableID("destination-tab", "destination-pane", destinationPTY, domain.Size{Cols: 80, Rows: 23})}} | 24 | destination := &session{sessionCore: sessionCore{id: "destination", name: "destination", ephemeral: true}, tabs: []*tab{newTabWithStableID("destination-tab", "destination-pane", destinationPTY, domain.Size{Cols: 80, Rows: 23})}} | ||
| 25 | destinationTab := destination.tabs[0] | 25 | destinationTab := destination.tabs[0] | ||
| @@ -184,7 +184,7 @@ func TestFinalCloseDoesNotDeadlockWithMoveDispatchAdmission(t *testing.T) { | |||||
| 184 | 184 | ||||
| 185 | func TestPaletteFinalCloseEndsCurrentEffectBeforeDrainingPeers(t *testing.T) { | 185 | func TestPaletteFinalCloseEndsCurrentEffectBeforeDrainingPeers(t *testing.T) { | ||
| 186 | d, sess, current, _ := newManualSessionWithPTYs(t, newQuietPTY()) | 186 | d, sess, current, _ := newManualSessionWithPTYs(t, newQuietPTY()) | ||
| 187 | - | d.attachCoordinator(sess, current, true) | 187 | + | d.attachCoordinator(sess, nil, current, true) |
| 188 | otherTransport := &closeTrackingTransport{} | 188 | otherTransport := &closeTrackingTransport{} | ||
| 189 | other := &attachedClient{tr: otherTransport, output: newOutputStateStream(), size: current.size} | 189 | other := &attachedClient{tr: otherTransport, output: newOutputStateStream(), size: current.size} | ||
| 190 | other.initOverlays() | 190 | other.initOverlays() | ||
| @@ -192,7 +192,7 @@ func TestPaletteFinalCloseEndsCurrentEffectBeforeDrainingPeers(t *testing.T) { | |||||
| 192 | sess.mu.Lock() | 192 | sess.mu.Lock() | ||
| 193 | require.True(t, sess.registerAttachmentLocked(other)) | 193 | require.True(t, sess.registerAttachmentLocked(other)) | ||
| 194 | sess.mu.Unlock() | 194 | sess.mu.Unlock() | ||
| 195 | - | d.attachCoordinator(sess, other, true) | 195 | + | d.attachCoordinator(sess, nil, other, true) |
| 196 | 196 | ||||
| 197 | currentToken := sess.attachmentToken(current, current.transport()) | 197 | currentToken := sess.attachmentToken(current, current.transport()) | ||
| 198 | currentEffect, admitted := current.beginAttachmentEffect(currentToken) | 198 | currentEffect, admitted := current.beginAttachmentEffect(currentToken) | ||
M internal/usecase/daemon/move_tab_test.go +2 -2
| @@ -152,7 +152,7 @@ func TestMoveTabFinalSourceVisibleFloatingRejectsStaleSlotTransitions(t *testing | |||
| 152 | 152 | d, source, client, _ := newManualSessionWithPTYs(t, newQuietPTY()) | |
| 153 | 153 | moved := source.tabs[0] | |
| 154 | 154 | moved.stableID = "moved-tab" | |
| 155 | - | require.NotNil(t, d.attachCoordinator(source, client, true)) | |
| 155 | + | require.NotNil(t, d.attachCoordinator(source, nil, client, true)) | |
| 156 | 156 | destination := addMoveTabTestSession(d, "destination", "destination-tab") | |
| 157 | 157 | floating := newPaneWithStableID("floating", "floating-stable", newQuietPTY(), domain.Size{Cols: 20, Rows: 8}) | |
| 158 | 158 | moved.mu.Lock() | |
| @@ -453,7 +453,7 @@ func (r *moveTabPurgeRepository) DeleteIncarnation(_ context.Context, id domain. | |||
| 453 | 453 | ||
| 454 | 454 | func TestMoveTabFinalNamedSourcePurgesAfterDestinationAdmissionAndFailureDoesNotRollback(t *testing.T) { | |
| 455 | 455 | d, source, client, _ := newManualSessionWithPTYs(t, newQuietPTY()) | |
| 456 | - | require.NotNil(t, d.attachCoordinator(source, client, true)) | |
| 456 | + | require.NotNil(t, d.attachCoordinator(source, nil, client, true)) | |
| 457 | 457 | source.incarnation = domain.IncarnationID{4} | |
| 458 | 458 | moved := source.tabs[0] | |
| 459 | 459 | moved.stableID = "moved-tab" | |
| @@ -152,7 +152,7 @@ func TestMoveTabFinalSourceVisibleFloatingRejectsStaleSlotTransitions(t *testing | |||||
| 152 | d, source, client, _ := newManualSessionWithPTYs(t, newQuietPTY()) | 152 | d, source, client, _ := newManualSessionWithPTYs(t, newQuietPTY()) | ||
| 153 | moved := source.tabs[0] | 153 | moved := source.tabs[0] | ||
| 154 | moved.stableID = "moved-tab" | 154 | moved.stableID = "moved-tab" | ||
| 155 | - | require.NotNil(t, d.attachCoordinator(source, client, true)) | 155 | + | require.NotNil(t, d.attachCoordinator(source, nil, client, true)) |
| 156 | destination := addMoveTabTestSession(d, "destination", "destination-tab") | 156 | destination := addMoveTabTestSession(d, "destination", "destination-tab") | ||
| 157 | floating := newPaneWithStableID("floating", "floating-stable", newQuietPTY(), domain.Size{Cols: 20, Rows: 8}) | 157 | floating := newPaneWithStableID("floating", "floating-stable", newQuietPTY(), domain.Size{Cols: 20, Rows: 8}) | ||
| 158 | moved.mu.Lock() | 158 | moved.mu.Lock() | ||
| @@ -453,7 +453,7 @@ func (r *moveTabPurgeRepository) DeleteIncarnation(_ context.Context, id domain. | |||||
| 453 | 453 | ||||
| 454 | func TestMoveTabFinalNamedSourcePurgesAfterDestinationAdmissionAndFailureDoesNotRollback(t *testing.T) { | 454 | func TestMoveTabFinalNamedSourcePurgesAfterDestinationAdmissionAndFailureDoesNotRollback(t *testing.T) { | ||
| 455 | d, source, client, _ := newManualSessionWithPTYs(t, newQuietPTY()) | 455 | d, source, client, _ := newManualSessionWithPTYs(t, newQuietPTY()) | ||
| 456 | - | require.NotNil(t, d.attachCoordinator(source, client, true)) | 456 | + | require.NotNil(t, d.attachCoordinator(source, nil, client, true)) |
| 457 | source.incarnation = domain.IncarnationID{4} | 457 | source.incarnation = domain.IncarnationID{4} | ||
| 458 | moved := source.tabs[0] | 458 | moved := source.tabs[0] | ||
| 459 | moved.stableID = "moved-tab" | 459 | moved.stableID = "moved-tab" | ||
M internal/usecase/daemon/navigation_attachment_handoff_test.go +2 -2
| @@ -97,7 +97,7 @@ func TestNavigationHandoffsDropReplacedInitiatorWithoutMutation(t *testing.T) { | |||
| 97 | 97 | d.sessions[target.id] = target | |
| 98 | 98 | d.mu.Unlock() | |
| 99 | 99 | ||
| 100 | - | rc := d.attachCoordinator(source, old, true) | |
| 100 | + | rc := d.attachCoordinator(source, nil, old, true) | |
| 101 | 101 | token := source.attachmentToken(old, old.transport()) | |
| 102 | 102 | token.lease = rc.attachmentLease(old) | |
| 103 | 103 | old.publishAttachmentCapability(token) | |
| @@ -152,7 +152,7 @@ func TestStoppedSessionHandoffDoesNotResumeAfterInitiatorReplacement(t *testing. | |||
| 152 | 152 | d.stopped["stopped"] = stoppedSession{name: "stopped", cwd: "/tmp", createdAt: 7, state: ports.SessionStopped} | |
| 153 | 153 | d.mu.Unlock() | |
| 154 | 154 | ||
| 155 | - | rc := d.attachCoordinator(source, old, true) | |
| 155 | + | rc := d.attachCoordinator(source, nil, old, true) | |
| 156 | 156 | token := source.attachmentToken(old, old.transport()) | |
| 157 | 157 | token.lease = rc.attachmentLease(old) | |
| 158 | 158 | old.publishAttachmentCapability(token) | |
| @@ -97,7 +97,7 @@ func TestNavigationHandoffsDropReplacedInitiatorWithoutMutation(t *testing.T) { | |||||
| 97 | d.sessions[target.id] = target | 97 | d.sessions[target.id] = target | ||
| 98 | d.mu.Unlock() | 98 | d.mu.Unlock() | ||
| 99 | 99 | ||||
| 100 | - | rc := d.attachCoordinator(source, old, true) | 100 | + | rc := d.attachCoordinator(source, nil, old, true) |
| 101 | token := source.attachmentToken(old, old.transport()) | 101 | token := source.attachmentToken(old, old.transport()) | ||
| 102 | token.lease = rc.attachmentLease(old) | 102 | token.lease = rc.attachmentLease(old) | ||
| 103 | old.publishAttachmentCapability(token) | 103 | old.publishAttachmentCapability(token) | ||
| @@ -152,7 +152,7 @@ func TestStoppedSessionHandoffDoesNotResumeAfterInitiatorReplacement(t *testing. | |||||
| 152 | d.stopped["stopped"] = stoppedSession{name: "stopped", cwd: "/tmp", createdAt: 7, state: ports.SessionStopped} | 152 | d.stopped["stopped"] = stoppedSession{name: "stopped", cwd: "/tmp", createdAt: 7, state: ports.SessionStopped} | ||
| 153 | d.mu.Unlock() | 153 | d.mu.Unlock() | ||
| 154 | 154 | ||||
| 155 | - | rc := d.attachCoordinator(source, old, true) | 155 | + | rc := d.attachCoordinator(source, nil, old, true) |
| 156 | token := source.attachmentToken(old, old.transport()) | 156 | token := source.attachmentToken(old, old.transport()) | ||
| 157 | token.lease = rc.attachmentLease(old) | 157 | token.lease = rc.attachmentLease(old) | ||
| 158 | old.publishAttachmentCapability(token) | 158 | old.publishAttachmentCapability(token) | ||
M internal/usecase/daemon/navigation_attachment_incarnation_test.go +1 -1
| @@ -47,7 +47,7 @@ func TestNavigationHandoffRejectsResumedOrReboundInitiatorIncarnation(t *testing | |||
| 47 | 47 | d.sessions[target.id] = target | |
| 48 | 48 | d.mu.Unlock() | |
| 49 | 49 | ||
| 50 | - | rc := d.attachCoordinator(source, ac, true) | |
| 50 | + | rc := d.attachCoordinator(source, nil, ac, true) | |
| 51 | 51 | token := source.attachmentToken(ac, ac.transport()) | |
| 52 | 52 | token.lease = rc.attachmentLease(ac) | |
| 53 | 53 | ac.publishAttachmentCapability(token) | |
| @@ -47,7 +47,7 @@ func TestNavigationHandoffRejectsResumedOrReboundInitiatorIncarnation(t *testing | |||||
| 47 | d.sessions[target.id] = target | 47 | d.sessions[target.id] = target | ||
| 48 | d.mu.Unlock() | 48 | d.mu.Unlock() | ||
| 49 | 49 | ||||
| 50 | - | rc := d.attachCoordinator(source, ac, true) | 50 | + | rc := d.attachCoordinator(source, nil, ac, true) |
| 51 | token := source.attachmentToken(ac, ac.transport()) | 51 | token := source.attachmentToken(ac, ac.transport()) | ||
| 52 | token.lease = rc.attachmentLease(ac) | 52 | token.lease = rc.attachmentLease(ac) | ||
| 53 | ac.publishAttachmentCapability(token) | 53 | ac.publishAttachmentCapability(token) | ||
M internal/usecase/daemon/notify.go +3 -0
| @@ -310,6 +310,9 @@ func (d *Daemon) deliverGlobal(n domain.Notification) { | |||
| 310 | 310 | } | |
| 311 | 311 | targets := make([]*attachedClient, 0, len(sessions)) | |
| 312 | 312 | for _, s := range sessions { | |
| 313 | + | if s == nil { | |
| 314 | + | continue | |
| 315 | + | } | |
| 313 | 316 | for _, ac := range s.snapshotAttachments() { | |
| 314 | 317 | if !d.publishToast(ac, n) { | |
| 315 | 318 | continue | |
| @@ -310,6 +310,9 @@ func (d *Daemon) deliverGlobal(n domain.Notification) { | |||||
| 310 | } | 310 | } | ||
| 311 | targets := make([]*attachedClient, 0, len(sessions)) | 311 | targets := make([]*attachedClient, 0, len(sessions)) | ||
| 312 | for _, s := range sessions { | 312 | for _, s := range sessions { | ||
| 313 | + | if s == nil { | |||
| 314 | + | continue | |||
| 315 | + | } | |||
| 313 | for _, ac := range s.snapshotAttachments() { | 316 | for _, ac := range s.snapshotAttachments() { | ||
| 314 | if !d.publishToast(ac, n) { | 317 | if !d.publishToast(ac, n) { | ||
| 315 | continue | 318 | continue | ||
M internal/usecase/daemon/notify_test.go +1 -1
| @@ -52,7 +52,7 @@ func TestClientNoticeMapsFixedActionsAndDismissesOnlyConnectionToast(t *testing. | |||
| 52 | 52 | ||
| 53 | 53 | func TestMalformedClientNoticeIsIgnored(t *testing.T) { | |
| 54 | 54 | d, sess, ac, _ := newNoticeFixture(t, ¬iceClock{}) | |
| 55 | - | d.attachCoordinator(sess, ac, true) | |
| 55 | + | d.attachCoordinator(sess, nil, ac, true) | |
| 56 | 56 | tr, ok := ac.tr.(*portsmocks.MockTransport) | |
| 57 | 57 | require.True(t, ok, "attached client transport must be a MockTransport") | |
| 58 | 58 | frames := []ports.Frame{ | |
| @@ -52,7 +52,7 @@ func TestClientNoticeMapsFixedActionsAndDismissesOnlyConnectionToast(t *testing. | |||||
| 52 | 52 | ||||
| 53 | func TestMalformedClientNoticeIsIgnored(t *testing.T) { | 53 | func TestMalformedClientNoticeIsIgnored(t *testing.T) { | ||
| 54 | d, sess, ac, _ := newNoticeFixture(t, ¬iceClock{}) | 54 | d, sess, ac, _ := newNoticeFixture(t, ¬iceClock{}) | ||
| 55 | - | d.attachCoordinator(sess, ac, true) | 55 | + | d.attachCoordinator(sess, nil, ac, true) |
| 56 | tr, ok := ac.tr.(*portsmocks.MockTransport) | 56 | tr, ok := ac.tr.(*portsmocks.MockTransport) | ||
| 57 | require.True(t, ok, "attached client transport must be a MockTransport") | 57 | require.True(t, ok, "attached client transport must be a MockTransport") | ||
| 58 | frames := []ports.Frame{ | 58 | frames := []ports.Frame{ | ||
M internal/usecase/daemon/performance_benchmark_test.go +1 -1
| @@ -1083,7 +1083,7 @@ func newPerformanceFixtureWithCleanup(t testing.TB, config performanceConfig, re | |||
| 1083 | 1083 | compose: func() { fixture.renderCompositions++ }, | |
| 1084 | 1084 | emit: func() { fixture.renderEmissions++ }, | |
| 1085 | 1085 | } | |
| 1086 | - | d.attachCoordinator(sess, ac, true) | |
| 1086 | + | d.attachCoordinator(sess, nil, ac, true) | |
| 1087 | 1087 | ||
| 1088 | 1088 | // Prime the real renderer shadow before measurements. Subsequent paints use | |
| 1089 | 1089 | // actual production diffs rather than the initial full frame. | |
| @@ -1083,7 +1083,7 @@ func newPerformanceFixtureWithCleanup(t testing.TB, config performanceConfig, re | |||||
| 1083 | compose: func() { fixture.renderCompositions++ }, | 1083 | compose: func() { fixture.renderCompositions++ }, | ||
| 1084 | emit: func() { fixture.renderEmissions++ }, | 1084 | emit: func() { fixture.renderEmissions++ }, | ||
| 1085 | } | 1085 | } | ||
| 1086 | - | d.attachCoordinator(sess, ac, true) | 1086 | + | d.attachCoordinator(sess, nil, ac, true) |
| 1087 | 1087 | ||||
| 1088 | // Prime the real renderer shadow before measurements. Subsequent paints use | 1088 | // Prime the real renderer shadow before measurements. Subsequent paints use | ||
| 1089 | // actual production diffs rather than the initial full frame. | 1089 | // actual production diffs rather than the initial full frame. | ||
M internal/usecase/daemon/picker.go +4 -1
| @@ -87,6 +87,9 @@ func (d *Daemon) pickerViews(cur *session, ac *attachedClient) ([]picker.Session | |||
| 87 | 87 | d.mu.Lock() | |
| 88 | 88 | sessions := make([]*session, 0, len(d.sessions)) | |
| 89 | 89 | for _, entry := range d.sessions { | |
| 90 | + | if entry == nil { | |
| 91 | + | continue | |
| 92 | + | } | |
| 90 | 93 | sessions = append(sessions, entry) | |
| 91 | 94 | } | |
| 92 | 95 | stopped := make([]stoppedSession, 0, len(d.stopped)) | |
| @@ -405,7 +408,7 @@ func (d *Daemon) registerPreviewForSelection(ac *attachedClient) { | |||
| 405 | 408 | if targetSess == ac.currentAttachmentSession() { | |
| 406 | 409 | return | |
| 407 | 410 | } | |
| 408 | - | rc := d.attachCoordinator(targetSess, nil, false) | |
| 411 | + | rc := d.attachCoordinator(targetSess, nil, nil, false) | |
| 409 | 412 | rc.subscribePreviewFor(ac, generation, func(renderWake) { | |
| 410 | 413 | if pickerPreviewCurrent(ac, targetSess, next, generation) { | |
| 411 | 414 | if owner := ac.currentAttachmentSession(); owner != nil { | |
| @@ -87,6 +87,9 @@ func (d *Daemon) pickerViews(cur *session, ac *attachedClient) ([]picker.Session | |||||
| 87 | d.mu.Lock() | 87 | d.mu.Lock() | ||
| 88 | sessions := make([]*session, 0, len(d.sessions)) | 88 | sessions := make([]*session, 0, len(d.sessions)) | ||
| 89 | for _, entry := range d.sessions { | 89 | for _, entry := range d.sessions { | ||
| 90 | + | if entry == nil { | |||
| 91 | + | continue | |||
| 92 | + | } | |||
| 90 | sessions = append(sessions, entry) | 93 | sessions = append(sessions, entry) | ||
| 91 | } | 94 | } | ||
| 92 | stopped := make([]stoppedSession, 0, len(d.stopped)) | 95 | stopped := make([]stoppedSession, 0, len(d.stopped)) | ||
| @@ -405,7 +408,7 @@ func (d *Daemon) registerPreviewForSelection(ac *attachedClient) { | |||||
| 405 | if targetSess == ac.currentAttachmentSession() { | 408 | if targetSess == ac.currentAttachmentSession() { | ||
| 406 | return | 409 | return | ||
| 407 | } | 410 | } | ||
| 408 | - | rc := d.attachCoordinator(targetSess, nil, false) | 411 | + | rc := d.attachCoordinator(targetSess, nil, nil, false) |
| 409 | rc.subscribePreviewFor(ac, generation, func(renderWake) { | 412 | rc.subscribePreviewFor(ac, generation, func(renderWake) { | ||
| 410 | if pickerPreviewCurrent(ac, targetSess, next, generation) { | 413 | if pickerPreviewCurrent(ac, targetSess, next, generation) { | ||
| 411 | if owner := ac.currentAttachmentSession(); owner != nil { | 414 | if owner := ac.currentAttachmentSession(); owner != nil { | ||
M internal/usecase/daemon/picker_test.go +1 -1
| @@ -1550,7 +1550,7 @@ func TestPickerAttachmentEffectDeleteRemovesSelectedSessionAndRefreshes(t *testi | |||
| 1550 | 1550 | current.mu.Lock() | |
| 1551 | 1551 | current.registerAttachmentLocked(ac) | |
| 1552 | 1552 | current.mu.Unlock() | |
| 1553 | - | require.NotNil(t, d.attachCoordinator(current, ac, true)) | |
| 1553 | + | require.NotNil(t, d.attachCoordinator(current, nil, ac, true)) | |
| 1554 | 1554 | ||
| 1555 | 1555 | d.enterPicker(current, ac) | |
| 1556 | 1556 | d.handlePickerInput(ac, []byte("k")) | |
| @@ -1550,7 +1550,7 @@ func TestPickerAttachmentEffectDeleteRemovesSelectedSessionAndRefreshes(t *testi | |||||
| 1550 | current.mu.Lock() | 1550 | current.mu.Lock() | ||
| 1551 | current.registerAttachmentLocked(ac) | 1551 | current.registerAttachmentLocked(ac) | ||
| 1552 | current.mu.Unlock() | 1552 | current.mu.Unlock() | ||
| 1553 | - | require.NotNil(t, d.attachCoordinator(current, ac, true)) | 1553 | + | require.NotNil(t, d.attachCoordinator(current, nil, ac, true)) |
| 1554 | 1554 | ||||
| 1555 | d.enterPicker(current, ac) | 1555 | d.enterPicker(current, ac) | ||
| 1556 | d.handlePickerInput(ac, []byte("k")) | 1556 | d.handlePickerInput(ac, []byte("k")) | ||
M internal/usecase/daemon/recent_sessions.go +1 -1
| @@ -24,7 +24,7 @@ func (d *Daemon) recentSessions(current *session) []recentSession { | |||
| 24 | 24 | d.mu.Lock() | |
| 25 | 25 | recent := make([]recentSession, 0, len(d.sessions)) | |
| 26 | 26 | for _, sess := range d.sessions { | |
| 27 | - | if sess == current { | |
| 27 | + | if sess == nil || sess == current { | |
| 28 | 28 | continue | |
| 29 | 29 | } | |
| 30 | 30 | snap := sess.snapshotView(viewOptions{}) | |
| @@ -24,7 +24,7 @@ func (d *Daemon) recentSessions(current *session) []recentSession { | |||||
| 24 | d.mu.Lock() | 24 | d.mu.Lock() | ||
| 25 | recent := make([]recentSession, 0, len(d.sessions)) | 25 | recent := make([]recentSession, 0, len(d.sessions)) | ||
| 26 | for _, sess := range d.sessions { | 26 | for _, sess := range d.sessions { | ||
| 27 | - | if sess == current { | 27 | + | if sess == nil || sess == current { |
| 28 | continue | 28 | continue | ||
| 29 | } | 29 | } | ||
| 30 | snap := sess.snapshotView(viewOptions{}) | 30 | snap := sess.snapshotView(viewOptions{}) | ||
M internal/usecase/daemon/render_coordinator.go +8 -8
| @@ -370,11 +370,11 @@ func (c *renderCoordinator) retryCapacity(w renderWake, lease *attachmentLease) | |||
| 370 | 370 | generation := c.pendingGeneration | |
| 371 | 371 | queueCorrelation := c.queueCorrelation | |
| 372 | 372 | observer := c.opts.observer | |
| 373 | + | c.supervisor.startTaskLocked(func() { c.retryPendingGeneration(generation) }) | |
| 373 | 374 | c.mu.Unlock() | |
| 374 | 375 | if queueStart && observer != nil { | |
| 375 | 376 | observer.ObserveRuntime(ports.NewRuntimeMarkWithCorrelation("daemon", queueCorrelation, ports.RuntimeQueueEnqueued, 0, true)) | |
| 376 | 377 | } | |
| 377 | - | go c.retryPendingGeneration(generation) | |
| 378 | 378 | } | |
| 379 | 379 | ||
| 380 | 380 | func (c *renderCoordinator) retryPendingGeneration(generation uint64) { | |
| @@ -1095,7 +1095,7 @@ func (c *renderCoordinator) fireWithTimerTokenAndLease(token *timerToken, gen ui | |||
| 1095 | 1095 | w.lease = syncLease | |
| 1096 | 1096 | } | |
| 1097 | 1097 | } | |
| 1098 | - | preview, previews := c.takePendingPreviewsLocked() | |
| 1098 | + | previews := c.takePendingPreviewsLocked() | |
| 1099 | 1099 | worker := c.detachNormalTimerLocked() | |
| 1100 | 1100 | c.armed = false | |
| 1101 | 1101 | ||
| @@ -1114,7 +1114,7 @@ func (c *renderCoordinator) fireWithTimerTokenAndLease(token *timerToken, gen ui | |||
| 1114 | 1114 | if ackStart != nil { | |
| 1115 | 1115 | ackStart.publishStart() | |
| 1116 | 1116 | } | |
| 1117 | - | c.notifyPreviews(w, preview, previews) | |
| 1117 | + | c.notifyPreviews(w, previews) | |
| 1118 | 1118 | return | |
| 1119 | 1119 | } | |
| 1120 | 1120 | ||
| @@ -1184,7 +1184,7 @@ func (c *renderCoordinator) fireWithTimerTokenAndLease(token *timerToken, gen ui | |||
| 1184 | 1184 | if outputReady && wake != nil { | |
| 1185 | 1185 | wake(w) | |
| 1186 | 1186 | } | |
| 1187 | - | c.notifyPreviews(w, preview, previews) | |
| 1187 | + | c.notifyPreviews(w, previews) | |
| 1188 | 1188 | } | |
| 1189 | 1189 | ||
| 1190 | 1190 | func (c *renderCoordinator) fireValidLocked(token *timerToken, gen uint64, watchdog bool) bool { | |
| @@ -1228,19 +1228,19 @@ func (c *renderCoordinator) syncGateOpenLocked() bool { | |||
| 1228 | 1228 | ||
| 1229 | 1229 | // takePendingPreviewsLocked snapshots and consumes picker preview delivery | |
| 1230 | 1230 | // for this target generation. Preview callbacks are never ACK-gated. | |
| 1231 | - | func (c *renderCoordinator) takePendingPreviewsLocked() (func(renderWake), []func(renderWake)) { | |
| 1231 | + | func (c *renderCoordinator) takePendingPreviewsLocked() []func(renderWake) { | |
| 1232 | 1232 | if !c.pendingPreview { | |
| 1233 | - | return nil, nil | |
| 1233 | + | return nil | |
| 1234 | 1234 | } | |
| 1235 | 1235 | c.pendingPreview = false | |
| 1236 | 1236 | previews := make([]func(renderWake), 0, len(c.previewWakes)) | |
| 1237 | 1237 | for _, subscription := range c.previewWakes { | |
| 1238 | 1238 | previews = append(previews, subscription.fn) | |
| 1239 | 1239 | } | |
| 1240 | - | return nil, previews | |
| 1240 | + | return previews | |
| 1241 | 1241 | } | |
| 1242 | 1242 | ||
| 1243 | - | func (c *renderCoordinator) notifyPreviews(w renderWake, _ func(renderWake), previews []func(renderWake)) { | |
| 1243 | + | func (c *renderCoordinator) notifyPreviews(w renderWake, previews []func(renderWake)) { | |
| 1244 | 1244 | for _, fn := range previews { | |
| 1245 | 1245 | fn(w) | |
| 1246 | 1246 | } | |
| @@ -370,11 +370,11 @@ func (c *renderCoordinator) retryCapacity(w renderWake, lease *attachmentLease) | |||||
| 370 | generation := c.pendingGeneration | 370 | generation := c.pendingGeneration | ||
| 371 | queueCorrelation := c.queueCorrelation | 371 | queueCorrelation := c.queueCorrelation | ||
| 372 | observer := c.opts.observer | 372 | observer := c.opts.observer | ||
| 373 | + | c.supervisor.startTaskLocked(func() { c.retryPendingGeneration(generation) }) | |||
| 373 | c.mu.Unlock() | 374 | c.mu.Unlock() | ||
| 374 | if queueStart && observer != nil { | 375 | if queueStart && observer != nil { | ||
| 375 | observer.ObserveRuntime(ports.NewRuntimeMarkWithCorrelation("daemon", queueCorrelation, ports.RuntimeQueueEnqueued, 0, true)) | 376 | observer.ObserveRuntime(ports.NewRuntimeMarkWithCorrelation("daemon", queueCorrelation, ports.RuntimeQueueEnqueued, 0, true)) | ||
| 376 | } | 377 | } | ||
| 377 | - | go c.retryPendingGeneration(generation) | |||
| 378 | } | 378 | } | ||
| 379 | 379 | ||||
| 380 | func (c *renderCoordinator) retryPendingGeneration(generation uint64) { | 380 | func (c *renderCoordinator) retryPendingGeneration(generation uint64) { | ||
| @@ -1095,7 +1095,7 @@ func (c *renderCoordinator) fireWithTimerTokenAndLease(token *timerToken, gen ui | |||||
| 1095 | w.lease = syncLease | 1095 | w.lease = syncLease | ||
| 1096 | } | 1096 | } | ||
| 1097 | } | 1097 | } | ||
| 1098 | - | preview, previews := c.takePendingPreviewsLocked() | 1098 | + | previews := c.takePendingPreviewsLocked() |
| 1099 | worker := c.detachNormalTimerLocked() | 1099 | worker := c.detachNormalTimerLocked() | ||
| 1100 | c.armed = false | 1100 | c.armed = false | ||
| 1101 | 1101 | ||||
| @@ -1114,7 +1114,7 @@ func (c *renderCoordinator) fireWithTimerTokenAndLease(token *timerToken, gen ui | |||||
| 1114 | if ackStart != nil { | 1114 | if ackStart != nil { | ||
| 1115 | ackStart.publishStart() | 1115 | ackStart.publishStart() | ||
| 1116 | } | 1116 | } | ||
| 1117 | - | c.notifyPreviews(w, preview, previews) | 1117 | + | c.notifyPreviews(w, previews) |
| 1118 | return | 1118 | return | ||
| 1119 | } | 1119 | } | ||
| 1120 | 1120 | ||||
| @@ -1184,7 +1184,7 @@ func (c *renderCoordinator) fireWithTimerTokenAndLease(token *timerToken, gen ui | |||||
| 1184 | if outputReady && wake != nil { | 1184 | if outputReady && wake != nil { | ||
| 1185 | wake(w) | 1185 | wake(w) | ||
| 1186 | } | 1186 | } | ||
| 1187 | - | c.notifyPreviews(w, preview, previews) | 1187 | + | c.notifyPreviews(w, previews) |
| 1188 | } | 1188 | } | ||
| 1189 | 1189 | ||||
| 1190 | func (c *renderCoordinator) fireValidLocked(token *timerToken, gen uint64, watchdog bool) bool { | 1190 | func (c *renderCoordinator) fireValidLocked(token *timerToken, gen uint64, watchdog bool) bool { | ||
| @@ -1228,19 +1228,19 @@ func (c *renderCoordinator) syncGateOpenLocked() bool { | |||||
| 1228 | 1228 | ||||
| 1229 | // takePendingPreviewsLocked snapshots and consumes picker preview delivery | 1229 | // takePendingPreviewsLocked snapshots and consumes picker preview delivery | ||
| 1230 | // for this target generation. Preview callbacks are never ACK-gated. | 1230 | // for this target generation. Preview callbacks are never ACK-gated. | ||
| 1231 | - | func (c *renderCoordinator) takePendingPreviewsLocked() (func(renderWake), []func(renderWake)) { | 1231 | + | func (c *renderCoordinator) takePendingPreviewsLocked() []func(renderWake) { |
| 1232 | if !c.pendingPreview { | 1232 | if !c.pendingPreview { | ||
| 1233 | - | return nil, nil | 1233 | + | return nil |
| 1234 | } | 1234 | } | ||
| 1235 | c.pendingPreview = false | 1235 | c.pendingPreview = false | ||
| 1236 | previews := make([]func(renderWake), 0, len(c.previewWakes)) | 1236 | previews := make([]func(renderWake), 0, len(c.previewWakes)) | ||
| 1237 | for _, subscription := range c.previewWakes { | 1237 | for _, subscription := range c.previewWakes { | ||
| 1238 | previews = append(previews, subscription.fn) | 1238 | previews = append(previews, subscription.fn) | ||
| 1239 | } | 1239 | } | ||
| 1240 | - | return nil, previews | 1240 | + | return previews |
| 1241 | } | 1241 | } | ||
| 1242 | 1242 | ||||
| 1243 | - | func (c *renderCoordinator) notifyPreviews(w renderWake, _ func(renderWake), previews []func(renderWake)) { | 1243 | + | func (c *renderCoordinator) notifyPreviews(w renderWake, previews []func(renderWake)) { |
| 1244 | for _, fn := range previews { | 1244 | for _, fn := range previews { | ||
| 1245 | fn(w) | 1245 | fn(w) | ||
| 1246 | } | 1246 | } | ||
M internal/usecase/daemon/render_coordinator_test.go +4 -6
| @@ -404,10 +404,10 @@ func TestRenderCoordinatorUrgentDeadlineCannotBeExtended(t *testing.T) { | |||
| 404 | 404 | ||
| 405 | 405 | func TestRenderCoordinatorAckReadinessIsAttachmentScoped(t *testing.T) { | |
| 406 | 406 | slow, healthy := &attachedClient{}, &attachedClient{} | |
| 407 | - | slowReady := false | |
| 407 | + | var slowReady atomic.Bool | |
| 408 | 408 | wakes := make(chan renderWake, 2) | |
| 409 | 409 | rc := newRenderCoordinator(renderCoordinatorOptions{ | |
| 410 | - | ackReadyFor: func(ac *attachedClient) bool { return ac == healthy || slowReady }, | |
| 410 | + | ackReadyFor: func(ac *attachedClient) bool { return ac == healthy || slowReady.Load() }, | |
| 411 | 411 | wake: func(w renderWake) { wakes <- w }, | |
| 412 | 412 | }) | |
| 413 | 413 | rc.attach(slow) | |
| @@ -423,7 +423,7 @@ func TestRenderCoordinatorAckReadinessIsAttachmentScoped(t *testing.T) { | |||
| 423 | 423 | require.True(t, rc.pending, "slow attachment must retain the shared mutation until its ACK window opens") | |
| 424 | 424 | rc.mu.Unlock() | |
| 425 | 425 | ||
| 426 | - | slowReady = true | |
| 426 | + | slowReady.Store(true) | |
| 427 | 427 | rc.notifyAckForLease(rc.attachmentLease(slow)) | |
| 428 | 428 | wake = awaitTestValue(t, wakes, "slow attachment did not receive its deferred wake") | |
| 429 | 429 | require.Contains(t, wake.attachmentLeases, slow) | |
| @@ -977,7 +977,6 @@ func TestRenderCoordinatorLifecycleDropsStaleWakes(t *testing.T) { | |||
| 977 | 977 | name string | |
| 978 | 978 | teardown func(rc *renderCoordinator, owner *attachedClient) | |
| 979 | 979 | }{ | |
| 980 | - | {"detach", func(rc *renderCoordinator, owner *attachedClient) { rc.noteDetach(owner) }}, | |
| 981 | 980 | {"detach", func(rc *renderCoordinator, owner *attachedClient) { rc.noteDetach(owner) }}, | |
| 982 | 981 | {"session teardown", func(rc *renderCoordinator, _ *attachedClient) { rc.beginSessionTeardown().finish() }}, | |
| 983 | 982 | } | |
| @@ -1404,13 +1403,12 @@ func TestRenderCoordinatorInertTimerFiresSynchronouslyWithoutWorker(t *testing.T | |||
| 1404 | 1403 | } | |
| 1405 | 1404 | ||
| 1406 | 1405 | func TestRenderCoordinatorSyncBatchSurvivesAttachmentLifecycle(t *testing.T) { | |
| 1407 | - | t.Run("detach and park retain a gated headless preview until complete", func(t *testing.T) { | |
| 1406 | + | t.Run("detach retains a gated headless preview until complete", func(t *testing.T) { | |
| 1408 | 1407 | for _, transition := range []struct { | |
| 1409 | 1408 | name string | |
| 1410 | 1409 | run func(*renderCoordinator, *attachedClient) | |
| 1411 | 1410 | }{ | |
| 1412 | 1411 | {"detach", func(rc *renderCoordinator, ac *attachedClient) { rc.noteDetach(ac) }}, | |
| 1413 | - | {"detach", func(rc *renderCoordinator, ac *attachedClient) { rc.noteDetach(ac) }}, | |
| 1414 | 1412 | } { | |
| 1415 | 1413 | t.Run(transition.name, func(t *testing.T) { | |
| 1416 | 1414 | h := newCoordinatorHarness(t) | |
| @@ -404,10 +404,10 @@ func TestRenderCoordinatorUrgentDeadlineCannotBeExtended(t *testing.T) { | |||||
| 404 | 404 | ||||
| 405 | func TestRenderCoordinatorAckReadinessIsAttachmentScoped(t *testing.T) { | 405 | func TestRenderCoordinatorAckReadinessIsAttachmentScoped(t *testing.T) { | ||
| 406 | slow, healthy := &attachedClient{}, &attachedClient{} | 406 | slow, healthy := &attachedClient{}, &attachedClient{} | ||
| 407 | - | slowReady := false | 407 | + | var slowReady atomic.Bool |
| 408 | wakes := make(chan renderWake, 2) | 408 | wakes := make(chan renderWake, 2) | ||
| 409 | rc := newRenderCoordinator(renderCoordinatorOptions{ | 409 | rc := newRenderCoordinator(renderCoordinatorOptions{ | ||
| 410 | - | ackReadyFor: func(ac *attachedClient) bool { return ac == healthy || slowReady }, | 410 | + | ackReadyFor: func(ac *attachedClient) bool { return ac == healthy || slowReady.Load() }, |
| 411 | wake: func(w renderWake) { wakes <- w }, | 411 | wake: func(w renderWake) { wakes <- w }, | ||
| 412 | }) | 412 | }) | ||
| 413 | rc.attach(slow) | 413 | rc.attach(slow) | ||
| @@ -423,7 +423,7 @@ func TestRenderCoordinatorAckReadinessIsAttachmentScoped(t *testing.T) { | |||||
| 423 | require.True(t, rc.pending, "slow attachment must retain the shared mutation until its ACK window opens") | 423 | require.True(t, rc.pending, "slow attachment must retain the shared mutation until its ACK window opens") | ||
| 424 | rc.mu.Unlock() | 424 | rc.mu.Unlock() | ||
| 425 | 425 | ||||
| 426 | - | slowReady = true | 426 | + | slowReady.Store(true) |
| 427 | rc.notifyAckForLease(rc.attachmentLease(slow)) | 427 | rc.notifyAckForLease(rc.attachmentLease(slow)) | ||
| 428 | wake = awaitTestValue(t, wakes, "slow attachment did not receive its deferred wake") | 428 | wake = awaitTestValue(t, wakes, "slow attachment did not receive its deferred wake") | ||
| 429 | require.Contains(t, wake.attachmentLeases, slow) | 429 | require.Contains(t, wake.attachmentLeases, slow) | ||
| @@ -977,7 +977,6 @@ func TestRenderCoordinatorLifecycleDropsStaleWakes(t *testing.T) { | |||||
| 977 | name string | 977 | name string | ||
| 978 | teardown func(rc *renderCoordinator, owner *attachedClient) | 978 | teardown func(rc *renderCoordinator, owner *attachedClient) | ||
| 979 | }{ | 979 | }{ | ||
| 980 | - | {"detach", func(rc *renderCoordinator, owner *attachedClient) { rc.noteDetach(owner) }}, | |||
| 981 | {"detach", func(rc *renderCoordinator, owner *attachedClient) { rc.noteDetach(owner) }}, | 980 | {"detach", func(rc *renderCoordinator, owner *attachedClient) { rc.noteDetach(owner) }}, | ||
| 982 | {"session teardown", func(rc *renderCoordinator, _ *attachedClient) { rc.beginSessionTeardown().finish() }}, | 981 | {"session teardown", func(rc *renderCoordinator, _ *attachedClient) { rc.beginSessionTeardown().finish() }}, | ||
| 983 | } | 982 | } | ||
| @@ -1404,13 +1403,12 @@ func TestRenderCoordinatorInertTimerFiresSynchronouslyWithoutWorker(t *testing.T | |||||
| 1404 | } | 1403 | } | ||
| 1405 | 1404 | ||||
| 1406 | func TestRenderCoordinatorSyncBatchSurvivesAttachmentLifecycle(t *testing.T) { | 1405 | func TestRenderCoordinatorSyncBatchSurvivesAttachmentLifecycle(t *testing.T) { | ||
| 1407 | - | t.Run("detach and park retain a gated headless preview until complete", func(t *testing.T) { | 1406 | + | t.Run("detach retains a gated headless preview until complete", func(t *testing.T) { |
| 1408 | for _, transition := range []struct { | 1407 | for _, transition := range []struct { | ||
| 1409 | name string | 1408 | name string | ||
| 1410 | run func(*renderCoordinator, *attachedClient) | 1409 | run func(*renderCoordinator, *attachedClient) | ||
| 1411 | }{ | 1410 | }{ | ||
| 1412 | {"detach", func(rc *renderCoordinator, ac *attachedClient) { rc.noteDetach(ac) }}, | 1411 | {"detach", func(rc *renderCoordinator, ac *attachedClient) { rc.noteDetach(ac) }}, | ||
| 1413 | - | {"detach", func(rc *renderCoordinator, ac *attachedClient) { rc.noteDetach(ac) }}, | |||
| 1414 | } { | 1412 | } { | ||
| 1415 | t.Run(transition.name, func(t *testing.T) { | 1413 | t.Run(transition.name, func(t *testing.T) { | ||
| 1416 | h := newCoordinatorHarness(t) | 1414 | h := newCoordinatorHarness(t) | ||
M internal/usecase/daemon/render_damage_receipt_test.go +2 -2
| @@ -134,7 +134,7 @@ func TestRenderDamageReceiptRetainsRealVTDamageAcrossFailedEmission(t *testing.T | |||
| 134 | 134 | // Prepare failures now schedule recovery through the coordinator; | |
| 135 | 135 | // retain the failed transaction here until this test explicitly retries. | |
| 136 | 136 | d.clock = newNoticeClock() | |
| 137 | - | d.attachCoordinator(sess, ac, true) | |
| 137 | + | d.attachCoordinator(sess, nil, ac, true) | |
| 138 | 138 | healthy := ac.transport() | |
| 139 | 139 | p := sess.tabs[0].focusedPane() | |
| 140 | 140 | ||
| @@ -170,7 +170,7 @@ func TestRenderDamageReceiptRetainsRealVTDamageAcrossFailedEmission(t *testing.T | |||
| 170 | 170 | func TestPrepareFailureNotifiesAndSchedulesRecovery(t *testing.T) { | |
| 171 | 171 | clock := newNoticeClock() | |
| 172 | 172 | d, sess, ac, _ := newNoticeFixture(t, clock) | |
| 173 | - | rc := d.attachCoordinator(sess, ac, true) | |
| 173 | + | rc := d.attachCoordinator(sess, nil, ac, true) | |
| 174 | 174 | var producers []string | |
| 175 | 175 | rc.opts.onInvalidate = func(inv renderInvalidation) { | |
| 176 | 176 | // Both the notice repaint and the explicit recovery must re-enter only | |
| @@ -134,7 +134,7 @@ func TestRenderDamageReceiptRetainsRealVTDamageAcrossFailedEmission(t *testing.T | |||||
| 134 | // Prepare failures now schedule recovery through the coordinator; | 134 | // Prepare failures now schedule recovery through the coordinator; | ||
| 135 | // retain the failed transaction here until this test explicitly retries. | 135 | // retain the failed transaction here until this test explicitly retries. | ||
| 136 | d.clock = newNoticeClock() | 136 | d.clock = newNoticeClock() | ||
| 137 | - | d.attachCoordinator(sess, ac, true) | 137 | + | d.attachCoordinator(sess, nil, ac, true) |
| 138 | healthy := ac.transport() | 138 | healthy := ac.transport() | ||
| 139 | p := sess.tabs[0].focusedPane() | 139 | p := sess.tabs[0].focusedPane() | ||
| 140 | 140 | ||||
| @@ -170,7 +170,7 @@ func TestRenderDamageReceiptRetainsRealVTDamageAcrossFailedEmission(t *testing.T | |||||
| 170 | func TestPrepareFailureNotifiesAndSchedulesRecovery(t *testing.T) { | 170 | func TestPrepareFailureNotifiesAndSchedulesRecovery(t *testing.T) { | ||
| 171 | clock := newNoticeClock() | 171 | clock := newNoticeClock() | ||
| 172 | d, sess, ac, _ := newNoticeFixture(t, clock) | 172 | d, sess, ac, _ := newNoticeFixture(t, clock) | ||
| 173 | - | rc := d.attachCoordinator(sess, ac, true) | 173 | + | rc := d.attachCoordinator(sess, nil, ac, true) |
| 174 | var producers []string | 174 | var producers []string | ||
| 175 | rc.opts.onInvalidate = func(inv renderInvalidation) { | 175 | rc.opts.onInvalidate = func(inv renderInvalidation) { | ||
| 176 | // Both the notice repaint and the explicit recovery must re-enter only | 176 | // Both the notice repaint and the explicit recovery must re-enter only | ||
M internal/usecase/daemon/render_test.go +5 -5
| @@ -313,7 +313,7 @@ func TestPTYReaderSyncVisibilityTransitions(t *testing.T) { | |||
| 313 | 313 | d, sess, ac, sends := newManualSessionWithPTYs(t, activePTY, inactivePTY) | |
| 314 | 314 | clock := newCoordinatorMockClock(t, 8) | |
| 315 | 315 | d.clock = clock.clock | |
| 316 | - | rc := d.attachCoordinator(sess, ac, true) | |
| 316 | + | rc := d.attachCoordinator(sess, nil, ac, true) | |
| 317 | 317 | ||
| 318 | 318 | d.sessWg.Add(1) | |
| 319 | 319 | go d.ptyReader(sess, sess.tabs[1], sess.tabs[1].focusedPane()) | |
| @@ -349,7 +349,7 @@ func TestPTYReaderSyncVisibilityTransitions(t *testing.T) { | |||
| 349 | 349 | d, sess, ac, sends := newManualSessionWithPTYs(t, oldPTY, newPTY, parkedPTY) | |
| 350 | 350 | clock := newCoordinatorMockClock(t, 8) | |
| 351 | 351 | d.clock = clock.clock | |
| 352 | - | rc := d.attachCoordinator(sess, ac, true) | |
| 352 | + | rc := d.attachCoordinator(sess, nil, ac, true) | |
| 353 | 353 | ||
| 354 | 354 | d.sessWg.Add(2) | |
| 355 | 355 | go d.ptyReader(sess, sess.tabs[0], sess.tabs[0].focusedPane()) | |
| @@ -385,7 +385,7 @@ func TestPTYReaderRepublishesSynchronizedCompletionAfterAttachmentLifecycle(t *t | |||
| 385 | 385 | d, target, targetClient, _ := newManualSessionWithPTYs(t, pty) | |
| 386 | 386 | clock := newCoordinatorMockClock(t, 8) | |
| 387 | 387 | d.clock = clock.clock | |
| 388 | - | rc := d.attachCoordinator(target, targetClient, true) | |
| 388 | + | rc := d.attachCoordinator(target, nil, targetClient, true) | |
| 389 | 389 | ||
| 390 | 390 | // A viewer in a different session is previewing the headless target. | |
| 391 | 391 | viewer := &attachedClient{} | |
| @@ -429,7 +429,7 @@ func TestPTYReaderRepublishesSynchronizedCompletionAfterAttachmentLifecycle(t *t | |||
| 429 | 429 | d, target, detached, _ := newManualSessionWithPTYs(t, pty) | |
| 430 | 430 | clock := newCoordinatorMockClock(t, 8) | |
| 431 | 431 | d.clock = clock.clock | |
| 432 | - | rc := d.attachCoordinator(target, detached, true) | |
| 432 | + | rc := d.attachCoordinator(target, nil, detached, true) | |
| 433 | 433 | ||
| 434 | 434 | secondTransport, secondSends := newCapturingTransport(t) | |
| 435 | 435 | remaining := &attachedClient{tr: secondTransport, output: newOutputStateStream(), size: detached.size} | |
| @@ -470,7 +470,7 @@ func TestPTYReaderRepublishesSynchronizedCompletionAfterAttachmentLifecycle(t *t | |||
| 470 | 470 | d, target, client, _ := newManualSessionWithPTYs(t, pty) | |
| 471 | 471 | clock := newCoordinatorMockClock(t, 8) | |
| 472 | 472 | d.clock = clock.clock | |
| 473 | - | rc := d.attachCoordinator(target, client, true) | |
| 473 | + | rc := d.attachCoordinator(target, nil, client, true) | |
| 474 | 474 | wakes := make(chan renderWake, 1) | |
| 475 | 475 | rc.opts.wake = func(w renderWake) { wakes <- w } | |
| 476 | 476 | target.mu.Lock() | |
| @@ -313,7 +313,7 @@ func TestPTYReaderSyncVisibilityTransitions(t *testing.T) { | |||||
| 313 | d, sess, ac, sends := newManualSessionWithPTYs(t, activePTY, inactivePTY) | 313 | d, sess, ac, sends := newManualSessionWithPTYs(t, activePTY, inactivePTY) | ||
| 314 | clock := newCoordinatorMockClock(t, 8) | 314 | clock := newCoordinatorMockClock(t, 8) | ||
| 315 | d.clock = clock.clock | 315 | d.clock = clock.clock | ||
| 316 | - | rc := d.attachCoordinator(sess, ac, true) | 316 | + | rc := d.attachCoordinator(sess, nil, ac, true) |
| 317 | 317 | ||||
| 318 | d.sessWg.Add(1) | 318 | d.sessWg.Add(1) | ||
| 319 | go d.ptyReader(sess, sess.tabs[1], sess.tabs[1].focusedPane()) | 319 | go d.ptyReader(sess, sess.tabs[1], sess.tabs[1].focusedPane()) | ||
| @@ -349,7 +349,7 @@ func TestPTYReaderSyncVisibilityTransitions(t *testing.T) { | |||||
| 349 | d, sess, ac, sends := newManualSessionWithPTYs(t, oldPTY, newPTY, parkedPTY) | 349 | d, sess, ac, sends := newManualSessionWithPTYs(t, oldPTY, newPTY, parkedPTY) | ||
| 350 | clock := newCoordinatorMockClock(t, 8) | 350 | clock := newCoordinatorMockClock(t, 8) | ||
| 351 | d.clock = clock.clock | 351 | d.clock = clock.clock | ||
| 352 | - | rc := d.attachCoordinator(sess, ac, true) | 352 | + | rc := d.attachCoordinator(sess, nil, ac, true) |
| 353 | 353 | ||||
| 354 | d.sessWg.Add(2) | 354 | d.sessWg.Add(2) | ||
| 355 | go d.ptyReader(sess, sess.tabs[0], sess.tabs[0].focusedPane()) | 355 | go d.ptyReader(sess, sess.tabs[0], sess.tabs[0].focusedPane()) | ||
| @@ -385,7 +385,7 @@ func TestPTYReaderRepublishesSynchronizedCompletionAfterAttachmentLifecycle(t *t | |||||
| 385 | d, target, targetClient, _ := newManualSessionWithPTYs(t, pty) | 385 | d, target, targetClient, _ := newManualSessionWithPTYs(t, pty) | ||
| 386 | clock := newCoordinatorMockClock(t, 8) | 386 | clock := newCoordinatorMockClock(t, 8) | ||
| 387 | d.clock = clock.clock | 387 | d.clock = clock.clock | ||
| 388 | - | rc := d.attachCoordinator(target, targetClient, true) | 388 | + | rc := d.attachCoordinator(target, nil, targetClient, true) |
| 389 | 389 | ||||
| 390 | // A viewer in a different session is previewing the headless target. | 390 | // A viewer in a different session is previewing the headless target. | ||
| 391 | viewer := &attachedClient{} | 391 | viewer := &attachedClient{} | ||
| @@ -429,7 +429,7 @@ func TestPTYReaderRepublishesSynchronizedCompletionAfterAttachmentLifecycle(t *t | |||||
| 429 | d, target, detached, _ := newManualSessionWithPTYs(t, pty) | 429 | d, target, detached, _ := newManualSessionWithPTYs(t, pty) | ||
| 430 | clock := newCoordinatorMockClock(t, 8) | 430 | clock := newCoordinatorMockClock(t, 8) | ||
| 431 | d.clock = clock.clock | 431 | d.clock = clock.clock | ||
| 432 | - | rc := d.attachCoordinator(target, detached, true) | 432 | + | rc := d.attachCoordinator(target, nil, detached, true) |
| 433 | 433 | ||||
| 434 | secondTransport, secondSends := newCapturingTransport(t) | 434 | secondTransport, secondSends := newCapturingTransport(t) | ||
| 435 | remaining := &attachedClient{tr: secondTransport, output: newOutputStateStream(), size: detached.size} | 435 | remaining := &attachedClient{tr: secondTransport, output: newOutputStateStream(), size: detached.size} | ||
| @@ -470,7 +470,7 @@ func TestPTYReaderRepublishesSynchronizedCompletionAfterAttachmentLifecycle(t *t | |||||
| 470 | d, target, client, _ := newManualSessionWithPTYs(t, pty) | 470 | d, target, client, _ := newManualSessionWithPTYs(t, pty) | ||
| 471 | clock := newCoordinatorMockClock(t, 8) | 471 | clock := newCoordinatorMockClock(t, 8) | ||
| 472 | d.clock = clock.clock | 472 | d.clock = clock.clock | ||
| 473 | - | rc := d.attachCoordinator(target, client, true) | 473 | + | rc := d.attachCoordinator(target, nil, client, true) |
| 474 | wakes := make(chan renderWake, 1) | 474 | wakes := make(chan renderWake, 1) | ||
| 475 | rc.opts.wake = func(w renderWake) { wakes <- w } | 475 | rc.opts.wake = func(w renderWake) { wakes <- w } | ||
| 476 | target.mu.Lock() | 476 | target.mu.Lock() | ||
M internal/usecase/daemon/render_timer.go +12 -3
| @@ -66,15 +66,24 @@ func (l *timerLane) clearLocked(token *timerToken) bool { | |||
| 66 | 66 | type timerSupervisor struct{ workers sync.WaitGroup } | |
| 67 | 67 | ||
| 68 | 68 | func (s *timerSupervisor) startLocked(token *timerToken, timerC <-chan time.Time, fire func()) { | |
| 69 | - | s.workers.Add(1) | |
| 70 | - | go func() { | |
| 71 | - | defer s.workers.Done() | |
| 69 | + | s.startTaskLocked(func() { | |
| 72 | 70 | defer close(token.done) | |
| 73 | 71 | select { | |
| 74 | 72 | case <-timerC: | |
| 75 | 73 | fire() | |
| 76 | 74 | case <-token.cancel: | |
| 77 | 75 | } | |
| 76 | + | }) | |
| 77 | + | } | |
| 78 | + | ||
| 79 | + | // startTaskLocked accounts for coordinator work that is not backed by a timer | |
| 80 | + | // token. The caller must hold the coordinator lock so terminal teardown cannot | |
| 81 | + | // begin waiting between registration and launch. | |
| 82 | + | func (s *timerSupervisor) startTaskLocked(task func()) { | |
| 83 | + | s.workers.Add(1) | |
| 84 | + | go func() { | |
| 85 | + | defer s.workers.Done() | |
| 86 | + | task() | |
| 78 | 87 | }() | |
| 79 | 88 | } | |
| 80 | 89 | ||
| @@ -66,15 +66,24 @@ func (l *timerLane) clearLocked(token *timerToken) bool { | |||||
| 66 | type timerSupervisor struct{ workers sync.WaitGroup } | 66 | type timerSupervisor struct{ workers sync.WaitGroup } | ||
| 67 | 67 | ||||
| 68 | func (s *timerSupervisor) startLocked(token *timerToken, timerC <-chan time.Time, fire func()) { | 68 | func (s *timerSupervisor) startLocked(token *timerToken, timerC <-chan time.Time, fire func()) { | ||
| 69 | - | s.workers.Add(1) | 69 | + | s.startTaskLocked(func() { |
| 70 | - | go func() { | |||
| 71 | - | defer s.workers.Done() | |||
| 72 | defer close(token.done) | 70 | defer close(token.done) | ||
| 73 | select { | 71 | select { | ||
| 74 | case <-timerC: | 72 | case <-timerC: | ||
| 75 | fire() | 73 | fire() | ||
| 76 | case <-token.cancel: | 74 | case <-token.cancel: | ||
| 77 | } | 75 | } | ||
| 76 | + | }) | |||
| 77 | + | } | |||
| 78 | + | ||||
| 79 | + | // startTaskLocked accounts for coordinator work that is not backed by a timer | |||
| 80 | + | // token. The caller must hold the coordinator lock so terminal teardown cannot | |||
| 81 | + | // begin waiting between registration and launch. | |||
| 82 | + | func (s *timerSupervisor) startTaskLocked(task func()) { | |||
| 83 | + | s.workers.Add(1) | |||
| 84 | + | go func() { | |||
| 85 | + | defer s.workers.Done() | |||
| 86 | + | task() | |||
| 78 | }() | 87 | }() | ||
| 79 | } | 88 | } | ||
| 80 | 89 | ||||
M internal/usecase/daemon/resume.go +26 -3
| @@ -252,15 +252,22 @@ func (d *Daemon) parkAttachment(sess *session, ac *attachedClient) bool { | |||
| 252 | 252 | oldSame.closeDone() | |
| 253 | 253 | } | |
| 254 | 254 | d.log.Info("client parked for resume", "session", sess.name, "grace", grace) | |
| 255 | + | d.watchParkedTimer(token, parked) | |
| 256 | + | return true | |
| 257 | + | } | |
| 255 | 258 | ||
| 256 | - | go func(token uint64, parked *parkedAttachment) { | |
| 259 | + | func (d *Daemon) watchParkedTimer(token uint64, parked *parkedAttachment) { | |
| 260 | + | if parked == nil || parked.timer == nil { | |
| 261 | + | return | |
| 262 | + | } | |
| 263 | + | timer := parked.timer | |
| 264 | + | go func() { | |
| 257 | 265 | select { | |
| 258 | 266 | case <-timer.C(): | |
| 259 | 267 | d.expireParked(token, parked) | |
| 260 | 268 | case <-parked.done: | |
| 261 | 269 | } | |
| 262 | - | }(token, parked) | |
| 263 | - | return true | |
| 270 | + | }() | |
| 264 | 271 | } | |
| 265 | 272 | ||
| 266 | 273 | func (d *Daemon) expireParked(token uint64, parked *parkedAttachment) { | |
| @@ -348,9 +355,25 @@ func (d *Daemon) abortResumeClaim(ac *attachedClient) bool { | |||
| 348 | 355 | sess.unregisterAttachmentLocked(ac) | |
| 349 | 356 | sess.mu.Unlock() | |
| 350 | 357 | } | |
| 358 | + | // Recreate the parked entry and its watcher instead of restoring a timer | |
| 359 | + | // that may already have fired while the claim held the old entry. This also | |
| 360 | + | // makes a failed handshake's credential expiry deterministic under fake clocks. | |
| 361 | + | if parked.timer != nil { | |
| 362 | + | parked.timer.Stop() | |
| 363 | + | } | |
| 364 | + | parked.closeDone() | |
| 365 | + | rearmed := &parkedAttachment{ | |
| 366 | + | sess: parked.sess, | |
| 367 | + | ac: ac, | |
| 368 | + | pickerGeneration: parked.pickerGeneration, | |
| 369 | + | timer: d.clock.NewTimer(d.resumeParkGrace), | |
| 370 | + | done: make(chan struct{}), | |
| 371 | + | } | |
| 372 | + | d.parked[token] = rearmed | |
| 351 | 373 | captured := ac.transportSnapshot().transport | |
| 352 | 374 | ac.setSession(nil) | |
| 353 | 375 | d.mu.Unlock() | |
| 376 | + | d.watchParkedTimer(token, rearmed) | |
| 354 | 377 | if captured != nil { | |
| 355 | 378 | _ = ac.closeCapturedTransport(ac.revokeTransport(captured)) | |
| 356 | 379 | } | |
| @@ -252,15 +252,22 @@ func (d *Daemon) parkAttachment(sess *session, ac *attachedClient) bool { | |||||
| 252 | oldSame.closeDone() | 252 | oldSame.closeDone() | ||
| 253 | } | 253 | } | ||
| 254 | d.log.Info("client parked for resume", "session", sess.name, "grace", grace) | 254 | d.log.Info("client parked for resume", "session", sess.name, "grace", grace) | ||
| 255 | + | d.watchParkedTimer(token, parked) | |||
| 256 | + | return true | |||
| 257 | + | } | |||
| 255 | 258 | ||||
| 256 | - | go func(token uint64, parked *parkedAttachment) { | 259 | + | func (d *Daemon) watchParkedTimer(token uint64, parked *parkedAttachment) { |
| 260 | + | if parked == nil || parked.timer == nil { | |||
| 261 | + | return | |||
| 262 | + | } | |||
| 263 | + | timer := parked.timer | |||
| 264 | + | go func() { | |||
| 257 | select { | 265 | select { | ||
| 258 | case <-timer.C(): | 266 | case <-timer.C(): | ||
| 259 | d.expireParked(token, parked) | 267 | d.expireParked(token, parked) | ||
| 260 | case <-parked.done: | 268 | case <-parked.done: | ||
| 261 | } | 269 | } | ||
| 262 | - | }(token, parked) | 270 | + | }() |
| 263 | - | return true | |||
| 264 | } | 271 | } | ||
| 265 | 272 | ||||
| 266 | func (d *Daemon) expireParked(token uint64, parked *parkedAttachment) { | 273 | func (d *Daemon) expireParked(token uint64, parked *parkedAttachment) { | ||
| @@ -348,9 +355,25 @@ func (d *Daemon) abortResumeClaim(ac *attachedClient) bool { | |||||
| 348 | sess.unregisterAttachmentLocked(ac) | 355 | sess.unregisterAttachmentLocked(ac) | ||
| 349 | sess.mu.Unlock() | 356 | sess.mu.Unlock() | ||
| 350 | } | 357 | } | ||
| 358 | + | // Recreate the parked entry and its watcher instead of restoring a timer | |||
| 359 | + | // that may already have fired while the claim held the old entry. This also | |||
| 360 | + | // makes a failed handshake's credential expiry deterministic under fake clocks. | |||
| 361 | + | if parked.timer != nil { | |||
| 362 | + | parked.timer.Stop() | |||
| 363 | + | } | |||
| 364 | + | parked.closeDone() | |||
| 365 | + | rearmed := &parkedAttachment{ | |||
| 366 | + | sess: parked.sess, | |||
| 367 | + | ac: ac, | |||
| 368 | + | pickerGeneration: parked.pickerGeneration, | |||
| 369 | + | timer: d.clock.NewTimer(d.resumeParkGrace), | |||
| 370 | + | done: make(chan struct{}), | |||
| 371 | + | } | |||
| 372 | + | d.parked[token] = rearmed | |||
| 351 | captured := ac.transportSnapshot().transport | 373 | captured := ac.transportSnapshot().transport | ||
| 352 | ac.setSession(nil) | 374 | ac.setSession(nil) | ||
| 353 | d.mu.Unlock() | 375 | d.mu.Unlock() | ||
| 376 | + | d.watchParkedTimer(token, rearmed) | |||
| 354 | if captured != nil { | 377 | if captured != nil { | ||
| 355 | _ = ac.closeCapturedTransport(ac.revokeTransport(captured)) | 378 | _ = ac.closeCapturedTransport(ac.revokeTransport(captured)) | ||
| 356 | } | 379 | } | ||
M internal/usecase/daemon/resume_wake_incarnation_test.go +1 -1
| @@ -69,7 +69,7 @@ func TestParkedResumeRejectsDispatchedWakeFromPriorAttachment(t *testing.T) { | |||
| 69 | 69 | ||
| 70 | 70 | func TestSameAttachmentRebindRejectsQueuedWakeAndFansOutFreshGeneration(t *testing.T) { | |
| 71 | 71 | d, sess, ac, sends := newManualSessionWithPTYs(t, newQuietPTY()) | |
| 72 | - | rc := d.attachCoordinator(sess, ac, true) | |
| 72 | + | rc := d.attachCoordinator(sess, nil, ac, true) | |
| 73 | 73 | oldLease := rc.attachmentLease(ac) | |
| 74 | 74 | require.NotNil(t, oldLease) | |
| 75 | 75 | ||
| @@ -69,7 +69,7 @@ func TestParkedResumeRejectsDispatchedWakeFromPriorAttachment(t *testing.T) { | |||||
| 69 | 69 | ||||
| 70 | func TestSameAttachmentRebindRejectsQueuedWakeAndFansOutFreshGeneration(t *testing.T) { | 70 | func TestSameAttachmentRebindRejectsQueuedWakeAndFansOutFreshGeneration(t *testing.T) { | ||
| 71 | d, sess, ac, sends := newManualSessionWithPTYs(t, newQuietPTY()) | 71 | d, sess, ac, sends := newManualSessionWithPTYs(t, newQuietPTY()) | ||
| 72 | - | rc := d.attachCoordinator(sess, ac, true) | 72 | + | rc := d.attachCoordinator(sess, nil, ac, true) |
| 73 | oldLease := rc.attachmentLease(ac) | 73 | oldLease := rc.attachmentLease(ac) | ||
| 74 | require.NotNil(t, oldLease) | 74 | require.NotNil(t, oldLease) | ||
| 75 | 75 | ||||
M internal/usecase/daemon/session_core_test.go +0 -1
| @@ -30,7 +30,6 @@ func TestSessionCorePreservesLocalIdentityAndPromotedMutex(t *testing.T) { | |||
| 30 | 30 | ||
| 31 | 31 | core := sess.core() | |
| 32 | 32 | require.Same(t, &sess.sessionCore, core) | |
| 33 | - | require.Same(t, sess, sess) | |
| 34 | 33 | require.Equal(t, sessionCapabilities{}, core.caps, "zero-value local sessions must retain all local capabilities") | |
| 35 | 34 | ||
| 36 | 35 | core.mu.Lock() | |
| @@ -30,7 +30,6 @@ func TestSessionCorePreservesLocalIdentityAndPromotedMutex(t *testing.T) { | |||||
| 30 | 30 | ||||
| 31 | core := sess.core() | 31 | core := sess.core() | ||
| 32 | require.Same(t, &sess.sessionCore, core) | 32 | require.Same(t, &sess.sessionCore, core) | ||
| 33 | - | require.Same(t, sess, sess) | |||
| 34 | require.Equal(t, sessionCapabilities{}, core.caps, "zero-value local sessions must retain all local capabilities") | 33 | require.Equal(t, sessionCapabilities{}, core.caps, "zero-value local sessions must retain all local capabilities") | ||
| 35 | 34 | ||||
| 36 | core.mu.Lock() | 35 | core.mu.Lock() | ||
M internal/usecase/daemon/transactional_resize.go +1 -1
| @@ -848,7 +848,7 @@ func (d *Daemon) requestTransactionalResize(sess *session, ac *attachedClient, s | |||
| 848 | 848 | if ac == nil { | |
| 849 | 849 | return d.requestTransactionalResizeForLease(sess, nil, nil, size, immediate) | |
| 850 | 850 | } | |
| 851 | - | rc := d.attachCoordinator(sess, ac, true) | |
| 851 | + | rc := d.attachCoordinator(sess, nil, ac, true) | |
| 852 | 852 | return d.requestTransactionalResizeForLease(sess, ac, rc.attachmentLease(ac), size, immediate) | |
| 853 | 853 | } | |
| 854 | 854 | ||
| @@ -848,7 +848,7 @@ func (d *Daemon) requestTransactionalResize(sess *session, ac *attachedClient, s | |||||
| 848 | if ac == nil { | 848 | if ac == nil { | ||
| 849 | return d.requestTransactionalResizeForLease(sess, nil, nil, size, immediate) | 849 | return d.requestTransactionalResizeForLease(sess, nil, nil, size, immediate) | ||
| 850 | } | 850 | } | ||
| 851 | - | rc := d.attachCoordinator(sess, ac, true) | 851 | + | rc := d.attachCoordinator(sess, nil, ac, true) |
| 852 | return d.requestTransactionalResizeForLease(sess, ac, rc.attachmentLease(ac), size, immediate) | 852 | return d.requestTransactionalResizeForLease(sess, ac, rc.attachmentLease(ac), size, immediate) | ||
| 853 | } | 853 | } | ||
| 854 | 854 | ||||
M internal/usecase/daemon/transactional_resize_test.go +4 -4
| @@ -410,7 +410,7 @@ func TestTransactionalResizeRejectsNewerEpochBeforeSessionPublication(t *testing | |||
| 410 | 410 | d, sess, ac, _ := newManualSessionWithPTYs(t, first, second) | |
| 411 | 411 | observer := &daemonRuntimeObserver{} | |
| 412 | 412 | d.runtimeObserver = observer | |
| 413 | - | rc := d.attachCoordinator(sess, ac, true) | |
| 413 | + | rc := d.attachCoordinator(sess, nil, ac, true) | |
| 414 | 414 | lease := rc.attachmentLease(ac) | |
| 415 | 415 | var newer uint64 | |
| 416 | 416 | d.beforeSessionResizePublication = func() { | |
| @@ -471,7 +471,7 @@ func TestTransactionalResizeRechecksLeaseAtAttachmentPublication(t *testing.T) { | |||
| 471 | 471 | t.Run(tc.name, func(t *testing.T) { | |
| 472 | 472 | pty := &transactionalResizePTY{} | |
| 473 | 473 | d, sess, ac, _ := newManualSessionWithPTYs(t, pty) | |
| 474 | - | rc := d.attachCoordinator(sess, ac, true) | |
| 474 | + | rc := d.attachCoordinator(sess, nil, ac, true) | |
| 475 | 475 | lease := rc.attachmentLease(ac) | |
| 476 | 476 | epoch := rc.recordResizeRequestForLease(domain.Size{Cols: 100, Rows: 30}, ac, lease) | |
| 477 | 477 | require.NotZero(t, epoch) | |
| @@ -555,7 +555,7 @@ func TestRetryOwnerCannotPublishFloatingGeometryAfterMove(t *testing.T) { | |||
| 555 | 555 | tb.mu.Unlock() | |
| 556 | 556 | owner := publishPaneOwner(popup, source, tb, 7) | |
| 557 | 557 | ||
| 558 | - | rc := d.attachCoordinator(source, ac, true) | |
| 558 | + | rc := d.attachCoordinator(source, nil, ac, true) | |
| 559 | 559 | lease := rc.attachmentLease(ac) | |
| 560 | 560 | epoch := rc.recordResizeRequestForLease(domain.Size{Cols: 80, Rows: 24}, ac, lease) | |
| 561 | 561 | require.True(t, d.runResizeTransaction(source, ac, lease, epoch)) | |
| @@ -780,7 +780,7 @@ func TestTransactionalResizeRetriesAcceptedFloatingSlotByIdentity(t *testing.T) | |||
| 780 | 780 | tb.floating = floatingSlot{state: floatingVisible, pane: popup, generation: 7} | |
| 781 | 781 | tb.mu.Unlock() | |
| 782 | 782 | ||
| 783 | - | rc := d.attachCoordinator(sess, ac, true) | |
| 783 | + | rc := d.attachCoordinator(sess, nil, ac, true) | |
| 784 | 784 | lease := rc.attachmentLease(ac) | |
| 785 | 785 | epoch := rc.recordResizeRequestForLease(domain.Size{Cols: 80, Rows: 24}, ac, lease) | |
| 786 | 786 | require.NotZero(t, epoch) | |
| @@ -410,7 +410,7 @@ func TestTransactionalResizeRejectsNewerEpochBeforeSessionPublication(t *testing | |||||
| 410 | d, sess, ac, _ := newManualSessionWithPTYs(t, first, second) | 410 | d, sess, ac, _ := newManualSessionWithPTYs(t, first, second) | ||
| 411 | observer := &daemonRuntimeObserver{} | 411 | observer := &daemonRuntimeObserver{} | ||
| 412 | d.runtimeObserver = observer | 412 | d.runtimeObserver = observer | ||
| 413 | - | rc := d.attachCoordinator(sess, ac, true) | 413 | + | rc := d.attachCoordinator(sess, nil, ac, true) |
| 414 | lease := rc.attachmentLease(ac) | 414 | lease := rc.attachmentLease(ac) | ||
| 415 | var newer uint64 | 415 | var newer uint64 | ||
| 416 | d.beforeSessionResizePublication = func() { | 416 | d.beforeSessionResizePublication = func() { | ||
| @@ -471,7 +471,7 @@ func TestTransactionalResizeRechecksLeaseAtAttachmentPublication(t *testing.T) { | |||||
| 471 | t.Run(tc.name, func(t *testing.T) { | 471 | t.Run(tc.name, func(t *testing.T) { | ||
| 472 | pty := &transactionalResizePTY{} | 472 | pty := &transactionalResizePTY{} | ||
| 473 | d, sess, ac, _ := newManualSessionWithPTYs(t, pty) | 473 | d, sess, ac, _ := newManualSessionWithPTYs(t, pty) | ||
| 474 | - | rc := d.attachCoordinator(sess, ac, true) | 474 | + | rc := d.attachCoordinator(sess, nil, ac, true) |
| 475 | lease := rc.attachmentLease(ac) | 475 | lease := rc.attachmentLease(ac) | ||
| 476 | epoch := rc.recordResizeRequestForLease(domain.Size{Cols: 100, Rows: 30}, ac, lease) | 476 | epoch := rc.recordResizeRequestForLease(domain.Size{Cols: 100, Rows: 30}, ac, lease) | ||
| 477 | require.NotZero(t, epoch) | 477 | require.NotZero(t, epoch) | ||
| @@ -555,7 +555,7 @@ func TestRetryOwnerCannotPublishFloatingGeometryAfterMove(t *testing.T) { | |||||
| 555 | tb.mu.Unlock() | 555 | tb.mu.Unlock() | ||
| 556 | owner := publishPaneOwner(popup, source, tb, 7) | 556 | owner := publishPaneOwner(popup, source, tb, 7) | ||
| 557 | 557 | ||||
| 558 | - | rc := d.attachCoordinator(source, ac, true) | 558 | + | rc := d.attachCoordinator(source, nil, ac, true) |
| 559 | lease := rc.attachmentLease(ac) | 559 | lease := rc.attachmentLease(ac) | ||
| 560 | epoch := rc.recordResizeRequestForLease(domain.Size{Cols: 80, Rows: 24}, ac, lease) | 560 | epoch := rc.recordResizeRequestForLease(domain.Size{Cols: 80, Rows: 24}, ac, lease) | ||
| 561 | require.True(t, d.runResizeTransaction(source, ac, lease, epoch)) | 561 | require.True(t, d.runResizeTransaction(source, ac, lease, epoch)) | ||
| @@ -780,7 +780,7 @@ func TestTransactionalResizeRetriesAcceptedFloatingSlotByIdentity(t *testing.T) | |||||
| 780 | tb.floating = floatingSlot{state: floatingVisible, pane: popup, generation: 7} | 780 | tb.floating = floatingSlot{state: floatingVisible, pane: popup, generation: 7} | ||
| 781 | tb.mu.Unlock() | 781 | tb.mu.Unlock() | ||
| 782 | 782 | ||||
| 783 | - | rc := d.attachCoordinator(sess, ac, true) | 783 | + | rc := d.attachCoordinator(sess, nil, ac, true) |
| 784 | lease := rc.attachmentLease(ac) | 784 | lease := rc.attachmentLease(ac) | ||
| 785 | epoch := rc.recordResizeRequestForLease(domain.Size{Cols: 80, Rows: 24}, ac, lease) | 785 | epoch := rc.recordResizeRequestForLease(domain.Size{Cols: 80, Rows: 24}, ac, lease) | ||
| 786 | require.NotZero(t, epoch) | 786 | require.NotZero(t, epoch) | ||
M internal/usecase/daemon/transport_observability_test.go +1 -1
| @@ -20,7 +20,7 @@ func TestTransportObservabilityDaemonBoundaries(t *testing.T) { | |||
| 20 | 20 | // One in-flight state makes the second public resize take the ACK-blocked | |
| 21 | 21 | // path. The real coordinator owns both renders and the ACK wake. | |
| 22 | 22 | ac.output = newOutputStateStream(1) | |
| 23 | - | rc := d.attachCoordinator(sess, ac, true) | |
| 23 | + | rc := d.attachCoordinator(sess, nil, ac, true) | |
| 24 | 24 | ||
| 25 | 25 | if !d.resizeForFirstPaint(sess, ac, domain.Size{Cols: 90, Rows: 25}) { | |
| 26 | 26 | t.Fatal("first resize was not accepted") | |
| @@ -20,7 +20,7 @@ func TestTransportObservabilityDaemonBoundaries(t *testing.T) { | |||||
| 20 | // One in-flight state makes the second public resize take the ACK-blocked | 20 | // One in-flight state makes the second public resize take the ACK-blocked | ||
| 21 | // path. The real coordinator owns both renders and the ACK wake. | 21 | // path. The real coordinator owns both renders and the ACK wake. | ||
| 22 | ac.output = newOutputStateStream(1) | 22 | ac.output = newOutputStateStream(1) | ||
| 23 | - | rc := d.attachCoordinator(sess, ac, true) | 23 | + | rc := d.attachCoordinator(sess, nil, ac, true) |
| 24 | 24 | ||||
| 25 | if !d.resizeForFirstPaint(sess, ac, domain.Size{Cols: 90, Rows: 25}) { | 25 | if !d.resizeForFirstPaint(sess, ac, domain.Size{Cols: 90, Rows: 25}) { | ||
| 26 | t.Fatal("first resize was not accepted") | 26 | t.Fatal("first resize was not accepted") | ||