fix(remote): negotiate ACK-driven output state
- commit
- bfba9f26587871c37982592ccfc7041c490a485a
- author
- brice · · 2026-07-09 18:27 UTC
- parent
- 5d4ffc4
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
10 files · +69 -23
M internal/adapters/dgram/proxy_test.go +27 -0
| @@ -78,6 +78,33 @@ func TestProxyRuntimeCopiesFramesAndDaemonEOFIsTerminal(t *testing.T) { | |||
| 78 | 78 | } | |
| 79 | 79 | } | |
| 80 | 80 | ||
| 81 | + | func TestProxyRuntimeForwardsOutputAckCapabilityInHello(t *testing.T) { | |
| 82 | + | client := newFakeTransport() | |
| 83 | + | daemon := newFakeTransport() | |
| 84 | + | errCh := make(chan error, 1) | |
| 85 | + | go func() { errCh <- ProxyRuntime{Client: client, Daemon: daemon, IdleTTL: time.Hour}.Run(t.Context()) }() | |
| 86 | + | hello := ports.Hello{Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: "work", AckOutput: true} | |
| 87 | + | client.recv <- recvResult{frame: ports.Frame{Type: ports.MsgHello, Payload: ports.MarshalHello(hello)}} | |
| 88 | + | ||
| 89 | + | select { | |
| 90 | + | case frame := <-daemon.sent: | |
| 91 | + | got, err := ports.UnmarshalHello(frame.Payload) | |
| 92 | + | if err != nil { | |
| 93 | + | t.Fatal(err) | |
| 94 | + | } | |
| 95 | + | if !got.AckOutput { | |
| 96 | + | t.Fatal("proxied Hello lost AckOutput capability") | |
| 97 | + | } | |
| 98 | + | case <-time.After(time.Second): | |
| 99 | + | t.Fatal("timeout waiting for proxied Hello") | |
| 100 | + | } | |
| 101 | + | ||
| 102 | + | daemon.recv <- recvResult{err: io.EOF} | |
| 103 | + | if err := <-errCh; err != nil { | |
| 104 | + | t.Fatalf("Run err=%v, want nil", err) | |
| 105 | + | } | |
| 106 | + | } | |
| 107 | + | ||
| 81 | 108 | func TestProxyRuntimeRetriesRecoverableClientSendWithoutDroppingFrame(t *testing.T) { | |
| 82 | 109 | tests := []struct { | |
| 83 | 110 | name string | |
| @@ -78,6 +78,33 @@ func TestProxyRuntimeCopiesFramesAndDaemonEOFIsTerminal(t *testing.T) { | |||||
| 78 | } | 78 | } | ||
| 79 | } | 79 | } | ||
| 80 | 80 | ||||
| 81 | + | func TestProxyRuntimeForwardsOutputAckCapabilityInHello(t *testing.T) { | |||
| 82 | + | client := newFakeTransport() | |||
| 83 | + | daemon := newFakeTransport() | |||
| 84 | + | errCh := make(chan error, 1) | |||
| 85 | + | go func() { errCh <- ProxyRuntime{Client: client, Daemon: daemon, IdleTTL: time.Hour}.Run(t.Context()) }() | |||
| 86 | + | hello := ports.Hello{Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: "work", AckOutput: true} | |||
| 87 | + | client.recv <- recvResult{frame: ports.Frame{Type: ports.MsgHello, Payload: ports.MarshalHello(hello)}} | |||
| 88 | + | ||||
| 89 | + | select { | |||
| 90 | + | case frame := <-daemon.sent: | |||
| 91 | + | got, err := ports.UnmarshalHello(frame.Payload) | |||
| 92 | + | if err != nil { | |||
| 93 | + | t.Fatal(err) | |||
| 94 | + | } | |||
| 95 | + | if !got.AckOutput { | |||
| 96 | + | t.Fatal("proxied Hello lost AckOutput capability") | |||
| 97 | + | } | |||
| 98 | + | case <-time.After(time.Second): | |||
| 99 | + | t.Fatal("timeout waiting for proxied Hello") | |||
| 100 | + | } | |||
| 101 | + | ||||
| 102 | + | daemon.recv <- recvResult{err: io.EOF} | |||
| 103 | + | if err := <-errCh; err != nil { | |||
| 104 | + | t.Fatalf("Run err=%v, want nil", err) | |||
| 105 | + | } | |||
| 106 | + | } | |||
| 107 | + | ||||
| 81 | func TestProxyRuntimeRetriesRecoverableClientSendWithoutDroppingFrame(t *testing.T) { | 108 | func TestProxyRuntimeRetriesRecoverableClientSendWithoutDroppingFrame(t *testing.T) { | ||
| 82 | tests := []struct { | 109 | tests := []struct { | ||
| 83 | name string | 110 | name string | ||
M internal/ports/frame.go +1 -1
| @@ -1,7 +1,7 @@ | |||
| 1 | 1 | package ports | |
| 2 | 2 | ||
| 3 | 3 | // ProtocolVersion is the current vev IPC wire protocol version. | |
| 4 | - | const ProtocolVersion uint16 = 10 | |
| 4 | + | const ProtocolVersion uint16 = 11 | |
| 5 | 5 | ||
| 6 | 6 | // MsgType identifies the kind of payload carried by a Frame. | |
| 7 | 7 | type MsgType uint8 | |
| @@ -1,7 +1,7 @@ | |||||
| 1 | package ports | 1 | package ports | ||
| 2 | 2 | ||||
| 3 | // ProtocolVersion is the current vev IPC wire protocol version. | 3 | // ProtocolVersion is the current vev IPC wire protocol version. | ||
| 4 | - | const ProtocolVersion uint16 = 10 | 4 | + | const ProtocolVersion uint16 = 11 |
| 5 | 5 | ||||
| 6 | // MsgType identifies the kind of payload carried by a Frame. | 6 | // MsgType identifies the kind of payload carried by a Frame. | ||
| 7 | type MsgType uint8 | 7 | type MsgType uint8 | ||
M internal/ports/frame_test.go +2 -2
| @@ -3,8 +3,8 @@ package ports | |||
| 3 | 3 | import "testing" | |
| 4 | 4 | ||
| 5 | 5 | func TestProtocolVersion(t *testing.T) { | |
| 6 | - | if ProtocolVersion != 10 { | |
| 7 | - | t.Fatalf("ProtocolVersion = %d, want 10", ProtocolVersion) | |
| 6 | + | if ProtocolVersion != 11 { | |
| 7 | + | t.Fatalf("ProtocolVersion = %d, want 11", ProtocolVersion) | |
| 8 | 8 | } | |
| 9 | 9 | } | |
| 10 | 10 | ||
| @@ -3,8 +3,8 @@ package ports | |||||
| 3 | import "testing" | 3 | import "testing" | ||
| 4 | 4 | ||||
| 5 | func TestProtocolVersion(t *testing.T) { | 5 | func TestProtocolVersion(t *testing.T) { | ||
| 6 | - | if ProtocolVersion != 10 { | 6 | + | if ProtocolVersion != 11 { |
| 7 | - | t.Fatalf("ProtocolVersion = %d, want 10", ProtocolVersion) | 7 | + | t.Fatalf("ProtocolVersion = %d, want 11", ProtocolVersion) |
| 8 | } | 8 | } | ||
| 9 | } | 9 | } | ||
| 10 | 10 | ||||
M internal/ports/wire.go +11 -0
| @@ -66,6 +66,7 @@ type Hello struct { | |||
| 66 | 66 | TermEnv string | |
| 67 | 67 | Cwd string | |
| 68 | 68 | TrueColor bool | |
| 69 | + | AckOutput bool | |
| 69 | 70 | } | |
| 70 | 71 | ||
| 71 | 72 | // Input carries raw bytes typed/pasted by the client, destined for the PTY. | |
| @@ -309,6 +310,11 @@ func MarshalHello(h Hello) []byte { | |||
| 309 | 310 | } else { | |
| 310 | 311 | w.putUint8(0) | |
| 311 | 312 | } | |
| 313 | + | if h.AckOutput { | |
| 314 | + | w.putUint8(1) | |
| 315 | + | } else { | |
| 316 | + | w.putUint8(0) | |
| 317 | + | } | |
| 312 | 318 | return w.b | |
| 313 | 319 | } | |
| 314 | 320 | ||
| @@ -355,6 +361,11 @@ func UnmarshalHello(b []byte) (Hello, error) { | |||
| 355 | 361 | return Hello{}, err | |
| 356 | 362 | } | |
| 357 | 363 | h.TrueColor = trueColor != 0 | |
| 364 | + | ackOutput, err := r.getUint8() | |
| 365 | + | if err != nil { | |
| 366 | + | return Hello{}, err | |
| 367 | + | } | |
| 368 | + | h.AckOutput = ackOutput != 0 | |
| 358 | 369 | if err := r.done(); err != nil { | |
| 359 | 370 | return Hello{}, err | |
| 360 | 371 | } | |
| @@ -66,6 +66,7 @@ type Hello struct { | |||||
| 66 | TermEnv string | 66 | TermEnv string | ||
| 67 | Cwd string | 67 | Cwd string | ||
| 68 | TrueColor bool | 68 | TrueColor bool | ||
| 69 | + | AckOutput bool | |||
| 69 | } | 70 | } | ||
| 70 | 71 | ||||
| 71 | // Input carries raw bytes typed/pasted by the client, destined for the PTY. | 72 | // Input carries raw bytes typed/pasted by the client, destined for the PTY. | ||
| @@ -309,6 +310,11 @@ func MarshalHello(h Hello) []byte { | |||||
| 309 | } else { | 310 | } else { | ||
| 310 | w.putUint8(0) | 311 | w.putUint8(0) | ||
| 311 | } | 312 | } | ||
| 313 | + | if h.AckOutput { | |||
| 314 | + | w.putUint8(1) | |||
| 315 | + | } else { | |||
| 316 | + | w.putUint8(0) | |||
| 317 | + | } | |||
| 312 | return w.b | 318 | return w.b | ||
| 313 | } | 319 | } | ||
| 314 | 320 | ||||
| @@ -355,6 +361,11 @@ func UnmarshalHello(b []byte) (Hello, error) { | |||||
| 355 | return Hello{}, err | 361 | return Hello{}, err | ||
| 356 | } | 362 | } | ||
| 357 | h.TrueColor = trueColor != 0 | 363 | h.TrueColor = trueColor != 0 | ||
| 364 | + | ackOutput, err := r.getUint8() | |||
| 365 | + | if err != nil { | |||
| 366 | + | return Hello{}, err | |||
| 367 | + | } | |||
| 368 | + | h.AckOutput = ackOutput != 0 | |||
| 358 | if err := r.done(); err != nil { | 369 | if err := r.done(); err != nil { | ||
| 359 | return Hello{}, err | 370 | return Hello{}, err | ||
| 360 | } | 371 | } | ||
M internal/ports/wire_test.go +3 -2
| @@ -69,8 +69,9 @@ func TestHelloGoldenAndRoundTrip(t *testing.T) { | |||
| 69 | 69 | TermEnv: "xterm-256color", | |
| 70 | 70 | Cwd: "/tmp/project", | |
| 71 | 71 | TrueColor: true, | |
| 72 | + | AckOutput: true, | |
| 72 | 73 | }, | |
| 73 | - | want: []byte{0x00, 0x01, 0x01, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x00, 0x02, 0x77, 0x30, 0x00, 0x50, 0x00, 0x18, 0x00, 0x0e, 0x78, 0x74, 0x65, 0x72, 0x6d, 0x2d, 0x32, 0x35, 0x36, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x00, 0x0c, 0x2f, 0x74, 0x6d, 0x70, 0x2f, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x01}, | |
| 74 | + | want: []byte{0x00, 0x01, 0x01, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x00, 0x02, 0x77, 0x30, 0x00, 0x50, 0x00, 0x18, 0x00, 0x0e, 0x78, 0x74, 0x65, 0x72, 0x6d, 0x2d, 0x32, 0x35, 0x36, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x00, 0x0c, 0x2f, 0x74, 0x6d, 0x70, 0x2f, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x01, 0x01}, | |
| 74 | 75 | }, | |
| 75 | 76 | { | |
| 76 | 77 | name: "empty strings", | |
| @@ -83,7 +84,7 @@ func TestHelloGoldenAndRoundTrip(t *testing.T) { | |||
| 83 | 84 | Cwd: "", | |
| 84 | 85 | TrueColor: false, | |
| 85 | 86 | }, | |
| 86 | - | want: []byte{0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, | |
| 87 | + | want: []byte{0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, | |
| 87 | 88 | }, | |
| 88 | 89 | } | |
| 89 | 90 | ||
| @@ -69,8 +69,9 @@ func TestHelloGoldenAndRoundTrip(t *testing.T) { | |||||
| 69 | TermEnv: "xterm-256color", | 69 | TermEnv: "xterm-256color", | ||
| 70 | Cwd: "/tmp/project", | 70 | Cwd: "/tmp/project", | ||
| 71 | TrueColor: true, | 71 | TrueColor: true, | ||
| 72 | + | AckOutput: true, | |||
| 72 | }, | 73 | }, | ||
| 73 | - | want: []byte{0x00, 0x01, 0x01, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x00, 0x02, 0x77, 0x30, 0x00, 0x50, 0x00, 0x18, 0x00, 0x0e, 0x78, 0x74, 0x65, 0x72, 0x6d, 0x2d, 0x32, 0x35, 0x36, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x00, 0x0c, 0x2f, 0x74, 0x6d, 0x70, 0x2f, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x01}, | 74 | + | want: []byte{0x00, 0x01, 0x01, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x00, 0x02, 0x77, 0x30, 0x00, 0x50, 0x00, 0x18, 0x00, 0x0e, 0x78, 0x74, 0x65, 0x72, 0x6d, 0x2d, 0x32, 0x35, 0x36, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x00, 0x0c, 0x2f, 0x74, 0x6d, 0x70, 0x2f, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x01, 0x01}, |
| 74 | }, | 75 | }, | ||
| 75 | { | 76 | { | ||
| 76 | name: "empty strings", | 77 | name: "empty strings", | ||
| @@ -83,7 +84,7 @@ func TestHelloGoldenAndRoundTrip(t *testing.T) { | |||||
| 83 | Cwd: "", | 84 | Cwd: "", | ||
| 84 | TrueColor: false, | 85 | TrueColor: false, | ||
| 85 | }, | 86 | }, | ||
| 86 | - | want: []byte{0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, | 87 | + | want: []byte{0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, |
| 87 | }, | 88 | }, | ||
| 88 | } | 89 | } | ||
| 89 | 90 | ||||
M internal/usecase/client/client.go +2 -0
| @@ -352,6 +352,7 @@ func attachOnce(ctx context.Context, transport ports.Transport, term ports.Termi | |||
| 352 | 352 | termEnv := os.Getenv("TERM") | |
| 353 | 353 | colorTerm := os.Getenv("COLORTERM") | |
| 354 | 354 | trueColor := DetectTrueColor(termEnv, colorTerm) | |
| 355 | + | _, ackOutput := transport.(ports.DatagramTransport) | |
| 355 | 356 | hello := ports.Hello{ | |
| 356 | 357 | Version: ports.ProtocolVersion, | |
| 357 | 358 | Intent: intent, | |
| @@ -362,6 +363,7 @@ func attachOnce(ctx context.Context, transport ports.Transport, term ports.Termi | |||
| 362 | 363 | TermEnv: termEnv, | |
| 363 | 364 | Cwd: cwd, | |
| 364 | 365 | TrueColor: trueColor, | |
| 366 | + | AckOutput: ackOutput, | |
| 365 | 367 | } | |
| 366 | 368 | if err := transport.Send(ports.Frame{Type: ports.MsgHello, Payload: ports.MarshalHello(hello)}); err != nil { | |
| 367 | 369 | return attachResult{err: fmt.Errorf("vev: sending hello: %w", err)} | |
| @@ -352,6 +352,7 @@ func attachOnce(ctx context.Context, transport ports.Transport, term ports.Termi | |||||
| 352 | termEnv := os.Getenv("TERM") | 352 | termEnv := os.Getenv("TERM") | ||
| 353 | colorTerm := os.Getenv("COLORTERM") | 353 | colorTerm := os.Getenv("COLORTERM") | ||
| 354 | trueColor := DetectTrueColor(termEnv, colorTerm) | 354 | trueColor := DetectTrueColor(termEnv, colorTerm) | ||
| 355 | + | _, ackOutput := transport.(ports.DatagramTransport) | |||
| 355 | hello := ports.Hello{ | 356 | hello := ports.Hello{ | ||
| 356 | Version: ports.ProtocolVersion, | 357 | Version: ports.ProtocolVersion, | ||
| 357 | Intent: intent, | 358 | Intent: intent, | ||
| @@ -362,6 +363,7 @@ func attachOnce(ctx context.Context, transport ports.Transport, term ports.Termi | |||||
| 362 | TermEnv: termEnv, | 363 | TermEnv: termEnv, | ||
| 363 | Cwd: cwd, | 364 | Cwd: cwd, | ||
| 364 | TrueColor: trueColor, | 365 | TrueColor: trueColor, | ||
| 366 | + | AckOutput: ackOutput, | |||
| 365 | } | 367 | } | ||
| 366 | if err := transport.Send(ports.Frame{Type: ports.MsgHello, Payload: ports.MarshalHello(hello)}); err != nil { | 368 | if err := transport.Send(ports.Frame{Type: ports.MsgHello, Payload: ports.MarshalHello(hello)}); err != nil { | ||
| 367 | return attachResult{err: fmt.Errorf("vev: sending hello: %w", err)} | 369 | return attachResult{err: fmt.Errorf("vev: sending hello: %w", err)} | ||
M internal/usecase/client/client_test.go +6 -1
| @@ -151,6 +151,10 @@ func isType(typ ports.MsgType) any { | |||
| 151 | 151 | return mock.MatchedBy(func(f ports.Frame) bool { return f.Type == typ }) | |
| 152 | 152 | } | |
| 153 | 153 | ||
| 154 | + | type datagramMarkedTransport struct{ ports.Transport } | |
| 155 | + | ||
| 156 | + | func (datagramMarkedTransport) DatagramTransport() {} | |
| 157 | + | ||
| 154 | 158 | func TestAttachHelloIncludesTrueColor(t *testing.T) { | |
| 155 | 159 | t.Setenv("TERM", "xterm-256color") | |
| 156 | 160 | t.Setenv("COLORTERM", "truecolor") | |
| @@ -176,13 +180,14 @@ func TestAttachHelloIncludesTrueColor(t *testing.T) { | |||
| 176 | 180 | defer unblock() | |
| 177 | 181 | tr.EXPECT().Close().Return(nil).Once() | |
| 178 | 182 | ||
| 179 | - | err := client.Attach(context.Background(), tr, tm, realClock{}, ports.IntentEphemeral, "") | |
| 183 | + | err := client.Attach(context.Background(), datagramMarkedTransport{Transport: tr}, tm, realClock{}, ports.IntentEphemeral, "") | |
| 180 | 184 | require.NoError(t, err) | |
| 181 | 185 | ||
| 182 | 186 | select { | |
| 183 | 187 | case hello := <-gotHello: | |
| 184 | 188 | require.Equal(t, "xterm-256color", hello.TermEnv) | |
| 185 | 189 | require.True(t, hello.TrueColor) | |
| 190 | + | require.True(t, hello.AckOutput) | |
| 186 | 191 | case <-time.After(2 * time.Second): | |
| 187 | 192 | t.Fatal("hello frame was not sent") | |
| 188 | 193 | } | |
| @@ -151,6 +151,10 @@ func isType(typ ports.MsgType) any { | |||||
| 151 | return mock.MatchedBy(func(f ports.Frame) bool { return f.Type == typ }) | 151 | return mock.MatchedBy(func(f ports.Frame) bool { return f.Type == typ }) | ||
| 152 | } | 152 | } | ||
| 153 | 153 | ||||
| 154 | + | type datagramMarkedTransport struct{ ports.Transport } | |||
| 155 | + | ||||
| 156 | + | func (datagramMarkedTransport) DatagramTransport() {} | |||
| 157 | + | ||||
| 154 | func TestAttachHelloIncludesTrueColor(t *testing.T) { | 158 | func TestAttachHelloIncludesTrueColor(t *testing.T) { | ||
| 155 | t.Setenv("TERM", "xterm-256color") | 159 | t.Setenv("TERM", "xterm-256color") | ||
| 156 | t.Setenv("COLORTERM", "truecolor") | 160 | t.Setenv("COLORTERM", "truecolor") | ||
| @@ -176,13 +180,14 @@ func TestAttachHelloIncludesTrueColor(t *testing.T) { | |||||
| 176 | defer unblock() | 180 | defer unblock() | ||
| 177 | tr.EXPECT().Close().Return(nil).Once() | 181 | tr.EXPECT().Close().Return(nil).Once() | ||
| 178 | 182 | ||||
| 179 | - | err := client.Attach(context.Background(), tr, tm, realClock{}, ports.IntentEphemeral, "") | 183 | + | err := client.Attach(context.Background(), datagramMarkedTransport{Transport: tr}, tm, realClock{}, ports.IntentEphemeral, "") |
| 180 | require.NoError(t, err) | 184 | require.NoError(t, err) | ||
| 181 | 185 | ||||
| 182 | select { | 186 | select { | ||
| 183 | case hello := <-gotHello: | 187 | case hello := <-gotHello: | ||
| 184 | require.Equal(t, "xterm-256color", hello.TermEnv) | 188 | require.Equal(t, "xterm-256color", hello.TermEnv) | ||
| 185 | require.True(t, hello.TrueColor) | 189 | require.True(t, hello.TrueColor) | ||
| 190 | + | require.True(t, hello.AckOutput) | |||
| 186 | case <-time.After(2 * time.Second): | 191 | case <-time.After(2 * time.Second): | ||
| 187 | t.Fatal("hello frame was not sent") | 192 | t.Fatal("hello frame was not sent") | ||
| 188 | } | 193 | } | ||
M internal/usecase/daemon/client.go +2 -7
| @@ -425,12 +425,7 @@ func (d *Daemon) notifiesSnapshot() []chan struct{} { | |||
| 425 | 425 | type attachClientOptions struct { | |
| 426 | 426 | clientID [16]byte | |
| 427 | 427 | resumeCapable bool | |
| 428 | - | datagram bool | |
| 429 | - | } | |
| 430 | - | ||
| 431 | - | func isDatagramTransport(tr ports.Transport) bool { | |
| 432 | - | _, ok := tr.(ports.DatagramTransport) | |
| 433 | - | return ok | |
| 428 | + | ackOutput bool | |
| 434 | 429 | } | |
| 435 | 430 | ||
| 436 | 431 | func (d *Daemon) attachClient(sess *session, tr ports.Transport, sz domain.Size, opts attachClientOptions) (*attachedClient, *attachedClient) { | |
| @@ -439,7 +434,7 @@ func (d *Daemon) attachClient(sess *session, tr ports.Transport, sz domain.Size, | |||
| 439 | 434 | resumeToken = d.nextResumeTokenLocked() | |
| 440 | 435 | } | |
| 441 | 436 | caps := renderer.Capabilities{} | |
| 442 | - | advanceOutputOnAck := opts.datagram || isDatagramTransport(tr) | |
| 437 | + | advanceOutputOnAck := opts.ackOutput | |
| 443 | 438 | if advanceOutputOnAck { | |
| 444 | 439 | caps.AdvancePolicy = renderer.AdvanceOnAck | |
| 445 | 440 | } | |
| @@ -425,12 +425,7 @@ func (d *Daemon) notifiesSnapshot() []chan struct{} { | |||||
| 425 | type attachClientOptions struct { | 425 | type attachClientOptions struct { | ||
| 426 | clientID [16]byte | 426 | clientID [16]byte | ||
| 427 | resumeCapable bool | 427 | resumeCapable bool | ||
| 428 | - | datagram bool | 428 | + | ackOutput bool |
| 429 | - | } | |||
| 430 | - | ||||
| 431 | - | func isDatagramTransport(tr ports.Transport) bool { | |||
| 432 | - | _, ok := tr.(ports.DatagramTransport) | |||
| 433 | - | return ok | |||
| 434 | } | 429 | } | ||
| 435 | 430 | ||||
| 436 | func (d *Daemon) attachClient(sess *session, tr ports.Transport, sz domain.Size, opts attachClientOptions) (*attachedClient, *attachedClient) { | 431 | func (d *Daemon) attachClient(sess *session, tr ports.Transport, sz domain.Size, opts attachClientOptions) (*attachedClient, *attachedClient) { | ||
| @@ -439,7 +434,7 @@ func (d *Daemon) attachClient(sess *session, tr ports.Transport, sz domain.Size, | |||||
| 439 | resumeToken = d.nextResumeTokenLocked() | 434 | resumeToken = d.nextResumeTokenLocked() | ||
| 440 | } | 435 | } | ||
| 441 | caps := renderer.Capabilities{} | 436 | caps := renderer.Capabilities{} | ||
| 442 | - | advanceOutputOnAck := opts.datagram || isDatagramTransport(tr) | 437 | + | advanceOutputOnAck := opts.ackOutput |
| 443 | if advanceOutputOnAck { | 438 | if advanceOutputOnAck { | ||
| 444 | caps.AdvancePolicy = renderer.AdvanceOnAck | 439 | caps.AdvancePolicy = renderer.AdvanceOnAck | ||
| 445 | } | 440 | } | ||
M internal/usecase/daemon/daemon.go +4 -4
| @@ -654,7 +654,7 @@ func (d *Daemon) route(h ports.Hello, tr ports.Transport) (*session, *attachedCl | |||
| 654 | 654 | } | |
| 655 | 655 | } | |
| 656 | 656 | d.purgeParkedForSessionLocked(sess) | |
| 657 | - | ac, old := d.attachClient(sess, tr, sz, attachClientOptions{clientID: h.ClientID, resumeCapable: true}) | |
| 657 | + | ac, old := d.attachClient(sess, tr, sz, attachClientOptions{clientID: h.ClientID, resumeCapable: true, ackOutput: h.AckOutput}) | |
| 658 | 658 | sess.mu.Lock() | |
| 659 | 659 | sess.terminal = term | |
| 660 | 660 | sess.mu.Unlock() | |
| @@ -670,7 +670,7 @@ func (d *Daemon) route(h ports.Hello, tr ports.Transport) (*session, *attachedCl | |||
| 670 | 670 | return nil, nil, err | |
| 671 | 671 | } | |
| 672 | 672 | d.purgeParkedForSessionLocked(sess) | |
| 673 | - | ac, old := d.attachClient(sess, tr, sz, attachClientOptions{clientID: h.ClientID, resumeCapable: true}) | |
| 673 | + | ac, old := d.attachClient(sess, tr, sz, attachClientOptions{clientID: h.ClientID, resumeCapable: true, ackOutput: h.AckOutput}) | |
| 674 | 674 | sess.mu.Lock() | |
| 675 | 675 | sess.terminal = term | |
| 676 | 676 | sess.mu.Unlock() | |
| @@ -697,7 +697,7 @@ func (d *Daemon) route(h ports.Hello, tr ports.Transport) (*session, *attachedCl | |||
| 697 | 697 | return nil, nil, err | |
| 698 | 698 | } | |
| 699 | 699 | d.purgeParkedForSessionLocked(sess) | |
| 700 | - | ac, old := d.attachClient(sess, tr, sz, attachClientOptions{clientID: h.ClientID, resumeCapable: true}) | |
| 700 | + | ac, old := d.attachClient(sess, tr, sz, attachClientOptions{clientID: h.ClientID, resumeCapable: true, ackOutput: h.AckOutput}) | |
| 701 | 701 | sess.mu.Lock() | |
| 702 | 702 | sess.terminal = term | |
| 703 | 703 | sess.mu.Unlock() | |
| @@ -722,7 +722,7 @@ func (d *Daemon) route(h ports.Hello, tr ports.Transport) (*session, *attachedCl | |||
| 722 | 722 | } | |
| 723 | 723 | } | |
| 724 | 724 | d.purgeParkedForSessionLocked(sess) | |
| 725 | - | ac, old := d.attachClient(sess, tr, sz, attachClientOptions{clientID: h.ClientID, resumeCapable: true}) | |
| 725 | + | ac, old := d.attachClient(sess, tr, sz, attachClientOptions{clientID: h.ClientID, resumeCapable: true, ackOutput: h.AckOutput}) | |
| 726 | 726 | sess.mu.Lock() | |
| 727 | 727 | sess.terminal = term | |
| 728 | 728 | sess.mu.Unlock() | |
| @@ -654,7 +654,7 @@ func (d *Daemon) route(h ports.Hello, tr ports.Transport) (*session, *attachedCl | |||||
| 654 | } | 654 | } | ||
| 655 | } | 655 | } | ||
| 656 | d.purgeParkedForSessionLocked(sess) | 656 | d.purgeParkedForSessionLocked(sess) | ||
| 657 | - | ac, old := d.attachClient(sess, tr, sz, attachClientOptions{clientID: h.ClientID, resumeCapable: true}) | 657 | + | ac, old := d.attachClient(sess, tr, sz, attachClientOptions{clientID: h.ClientID, resumeCapable: true, ackOutput: h.AckOutput}) |
| 658 | sess.mu.Lock() | 658 | sess.mu.Lock() | ||
| 659 | sess.terminal = term | 659 | sess.terminal = term | ||
| 660 | sess.mu.Unlock() | 660 | sess.mu.Unlock() | ||
| @@ -670,7 +670,7 @@ func (d *Daemon) route(h ports.Hello, tr ports.Transport) (*session, *attachedCl | |||||
| 670 | return nil, nil, err | 670 | return nil, nil, err | ||
| 671 | } | 671 | } | ||
| 672 | d.purgeParkedForSessionLocked(sess) | 672 | d.purgeParkedForSessionLocked(sess) | ||
| 673 | - | ac, old := d.attachClient(sess, tr, sz, attachClientOptions{clientID: h.ClientID, resumeCapable: true}) | 673 | + | ac, old := d.attachClient(sess, tr, sz, attachClientOptions{clientID: h.ClientID, resumeCapable: true, ackOutput: h.AckOutput}) |
| 674 | sess.mu.Lock() | 674 | sess.mu.Lock() | ||
| 675 | sess.terminal = term | 675 | sess.terminal = term | ||
| 676 | sess.mu.Unlock() | 676 | sess.mu.Unlock() | ||
| @@ -697,7 +697,7 @@ func (d *Daemon) route(h ports.Hello, tr ports.Transport) (*session, *attachedCl | |||||
| 697 | return nil, nil, err | 697 | return nil, nil, err | ||
| 698 | } | 698 | } | ||
| 699 | d.purgeParkedForSessionLocked(sess) | 699 | d.purgeParkedForSessionLocked(sess) | ||
| 700 | - | ac, old := d.attachClient(sess, tr, sz, attachClientOptions{clientID: h.ClientID, resumeCapable: true}) | 700 | + | ac, old := d.attachClient(sess, tr, sz, attachClientOptions{clientID: h.ClientID, resumeCapable: true, ackOutput: h.AckOutput}) |
| 701 | sess.mu.Lock() | 701 | sess.mu.Lock() | ||
| 702 | sess.terminal = term | 702 | sess.terminal = term | ||
| 703 | sess.mu.Unlock() | 703 | sess.mu.Unlock() | ||
| @@ -722,7 +722,7 @@ func (d *Daemon) route(h ports.Hello, tr ports.Transport) (*session, *attachedCl | |||||
| 722 | } | 722 | } | ||
| 723 | } | 723 | } | ||
| 724 | d.purgeParkedForSessionLocked(sess) | 724 | d.purgeParkedForSessionLocked(sess) | ||
| 725 | - | ac, old := d.attachClient(sess, tr, sz, attachClientOptions{clientID: h.ClientID, resumeCapable: true}) | 725 | + | ac, old := d.attachClient(sess, tr, sz, attachClientOptions{clientID: h.ClientID, resumeCapable: true, ackOutput: h.AckOutput}) |
| 726 | sess.mu.Lock() | 726 | sess.mu.Lock() | ||
| 727 | sess.terminal = term | 727 | sess.terminal = term | ||
| 728 | sess.mu.Unlock() | 728 | sess.mu.Unlock() | ||
M internal/usecase/daemon/output_state_sync_test.go +11 -6
| @@ -28,17 +28,22 @@ func (t *datagramTestTransport) Recv() (ports.Frame, error) { | |||
| 28 | 28 | } | |
| 29 | 29 | return f, nil | |
| 30 | 30 | } | |
| 31 | - | func (t *datagramTestTransport) Close() error { t.once.Do(func() { close(t.recv) }); return nil } | |
| 32 | - | func (t *datagramTestTransport) DatagramTransport() {} | |
| 31 | + | func (t *datagramTestTransport) Close() error { t.once.Do(func() { close(t.recv) }); return nil } | |
| 33 | 32 | ||
| 34 | 33 | func TestDatagramAttachAdvancesRendererOnlyOnAck(t *testing.T) { | |
| 35 | 34 | p, releasePTY := newBlockingPTY(t) | |
| 36 | 35 | defer releasePTY() | |
| 37 | - | d, sess, old, _ := newManualSessionWithPTYs(t, p) | |
| 36 | + | d, sess, _, _ := newManualSessionWithPTYs(t, p) | |
| 38 | 37 | tr := newDatagramTestTransport() | |
| 39 | - | ac, replaced := d.attachClient(sess, tr, domain.Size{Cols: 80, Rows: 24}, attachClientOptions{datagram: true}) | |
| 40 | - | require.Same(t, old, replaced) | |
| 41 | - | defer d.detachReplacedClient(replaced) | |
| 38 | + | routed, ac, err := d.route(ports.Hello{ | |
| 39 | + | Version: ports.ProtocolVersion, | |
| 40 | + | Intent: ports.IntentAttach, | |
| 41 | + | Name: sess.name, | |
| 42 | + | Size: domain.Size{Cols: 80, Rows: 24}, | |
| 43 | + | AckOutput: true, | |
| 44 | + | }, tr) | |
| 45 | + | require.NoError(t, err) | |
| 46 | + | require.Same(t, sess, routed) | |
| 42 | 47 | ||
| 43 | 48 | sess.tabs[0].focusedPane().screen.Write([]byte("A")) | |
| 44 | 49 | d.paint(sess, ac, false) | |
| @@ -28,17 +28,22 @@ func (t *datagramTestTransport) Recv() (ports.Frame, error) { | |||||
| 28 | } | 28 | } | ||
| 29 | return f, nil | 29 | return f, nil | ||
| 30 | } | 30 | } | ||
| 31 | - | func (t *datagramTestTransport) Close() error { t.once.Do(func() { close(t.recv) }); return nil } | 31 | + | func (t *datagramTestTransport) Close() error { t.once.Do(func() { close(t.recv) }); return nil } |
| 32 | - | func (t *datagramTestTransport) DatagramTransport() {} | |||
| 33 | 32 | ||||
| 34 | func TestDatagramAttachAdvancesRendererOnlyOnAck(t *testing.T) { | 33 | func TestDatagramAttachAdvancesRendererOnlyOnAck(t *testing.T) { | ||
| 35 | p, releasePTY := newBlockingPTY(t) | 34 | p, releasePTY := newBlockingPTY(t) | ||
| 36 | defer releasePTY() | 35 | defer releasePTY() | ||
| 37 | - | d, sess, old, _ := newManualSessionWithPTYs(t, p) | 36 | + | d, sess, _, _ := newManualSessionWithPTYs(t, p) |
| 38 | tr := newDatagramTestTransport() | 37 | tr := newDatagramTestTransport() | ||
| 39 | - | ac, replaced := d.attachClient(sess, tr, domain.Size{Cols: 80, Rows: 24}, attachClientOptions{datagram: true}) | 38 | + | routed, ac, err := d.route(ports.Hello{ |
| 40 | - | require.Same(t, old, replaced) | 39 | + | Version: ports.ProtocolVersion, |
| 41 | - | defer d.detachReplacedClient(replaced) | 40 | + | Intent: ports.IntentAttach, |
| 41 | + | Name: sess.name, | |||
| 42 | + | Size: domain.Size{Cols: 80, Rows: 24}, | |||
| 43 | + | AckOutput: true, | |||
| 44 | + | }, tr) | |||
| 45 | + | require.NoError(t, err) | |||
| 46 | + | require.Same(t, sess, routed) | |||
| 42 | 47 | ||||
| 43 | sess.tabs[0].focusedPane().screen.Write([]byte("A")) | 48 | sess.tabs[0].focusedPane().screen.Write([]byte("A")) | ||
| 44 | d.paint(sess, ac, false) | 49 | d.paint(sess, ac, false) | ||