detached c130909

Merge pull request #229 from bnema/refactor/server-session-channel

refactor(daemon): consume typed session messages
commit
c13090951f1203530c9b307de34cfd370883092f
author
bnema · · 2026-08-28 14:23 UTC
committer
GitHub · · 2026-08-28 14:23 UTC
merge of
f791584 + 3217e93 · diff against the first parent
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
51 files · +3061 -491
A internal/adapters/sessionwire/server.go +373 -0
@@ -0,0 +1,373 @@
1+// Package sessionwire adapts typed session messages to strict binary frames.
2+package sessionwire
3+
4+import (
5+ "encoding/binary"
6+ "errors"
7+
8+ "github.com/bnema/vev/internal/ports"
9+ "github.com/bnema/vev/internal/protocol"
10+ "github.com/bnema/vev/internal/protocol/wire"
11+)
12+
13+var (
14+ ErrUnknownMessageType = errors.New("sessionwire: unknown message type")
15+ ErrWrongDirection = errors.New("sessionwire: wrong message direction")
16+ ErrInvalidMessage = errors.New("sessionwire: invalid message")
17+ ErrUnsupportedSend = errors.New("sessionwire: send mode is unsupported")
18+)
19+
20+type serverConnection struct{ raw ports.Transport }
21+
22+var _ ports.ServerConnection = (*serverConnection)(nil)
23+
24+// NewServerConnection wraps one raw connection incarnation exactly once.
25+func NewServerConnection(raw ports.Transport) ports.ServerConnection {
26+ if raw == nil {
27+ return nil
28+ }
29+ return &serverConnection{raw: raw}
30+}
31+
32+func (c *serverConnection) ReceiveClient() (protocol.ClientMessage, error) {
33+ frame, err := c.raw.Recv()
34+ if err != nil {
35+ return nil, err
36+ }
37+ message, err := decodeClient(frame)
38+ if err == nil {
39+ return message, nil
40+ }
41+ failure := &protocol.DecodeFailure{Category: protocol.DecodeMalformed, Type: uint8(frame.Type), Err: err}
42+ if errors.Is(err, ErrUnknownMessageType) {
43+ failure.Category = protocol.DecodeUnknownType
44+ } else if errors.Is(err, ErrWrongDirection) {
45+ failure.Category = protocol.DecodeWrongDirection
46+ }
47+ switch frame.Type {
48+ case wire.MsgHello:
49+ failure.Kind = protocol.DecodeMessageHello
50+ failure.Version, _ = wire.PeekHelloVersion(frame.Payload)
51+ case wire.MsgCommand:
52+ failure.Kind = protocol.DecodeMessageCommand
53+ failure.Version, _ = wire.PeekCommandVersion(frame.Payload)
54+ if len(frame.Payload) >= 10 {
55+ failure.RequestID = binary.BigEndian.Uint64(frame.Payload[2:10])
56+ }
57+ case wire.MsgKill:
58+ failure.Kind = protocol.DecodeMessageKill
59+ case wire.MsgRemotePreviewRequest:
60+ failure.Kind = protocol.DecodeMessageRemotePreview
61+ }
62+ return nil, failure
63+}
64+
65+func (c *serverConnection) SendServer(message protocol.ServerMessage) error {
66+ frame, err := encodeServer(message)
67+ if err != nil {
68+ return err
69+ }
70+ return c.raw.Send(frame)
71+}
72+
73+func (c *serverConnection) SendServerAsync(message protocol.ServerMessage) error {
74+ transport, ok := c.raw.(ports.AsyncTransport)
75+ if !ok {
76+ return ErrUnsupportedSend
77+ }
78+ frame, err := encodeServer(message)
79+ if err != nil {
80+ return err
81+ }
82+ return transport.SendAsync(frame)
83+}
84+
85+func (c *serverConnection) SendServerSynchronous(message protocol.ServerMessage) error {
86+ transport, ok := c.raw.(ports.OwnedSynchronousTransport)
87+ if !ok {
88+ return ErrUnsupportedSend
89+ }
90+ frame, err := encodeServer(message)
91+ if err != nil {
92+ return err
93+ }
94+ return transport.SendSynchronous(frame)
95+}
96+
97+func (c *serverConnection) SendOutput(output protocol.Output) error {
98+ frame, err := encodeOutput(output)
99+ if err != nil {
100+ return err
101+ }
102+ return c.raw.Send(frame)
103+}
104+
105+func (c *serverConnection) SendOutputAsync(output protocol.Output) error {
106+ transport, ok := c.raw.(ports.AsyncTransport)
107+ if !ok {
108+ return ErrUnsupportedSend
109+ }
110+ frame, err := encodeOutput(output)
111+ if err != nil {
112+ return err
113+ }
114+ return transport.SendAsync(frame)
115+}
116+
117+func (c *serverConnection) SendOutputSynchronous(output protocol.Output) error {
118+ transport, ok := c.raw.(ports.OwnedSynchronousTransport)
119+ if !ok {
120+ return ErrUnsupportedSend
121+ }
122+ frame, err := encodeOutput(output)
123+ if err != nil {
124+ return err
125+ }
126+ return transport.SendSynchronous(frame)
127+}
128+
129+func encodeOutput(output protocol.Output) (wire.Frame, error) {
130+ payload, err := wire.MarshalOutput(output)
131+ return wire.Frame{Type: wire.MsgOutput, Payload: payload}, err
132+}
133+
134+func (c *serverConnection) Capabilities() protocol.ConnectionCapabilities {
135+ _, datagram := c.raw.(ports.DatagramTransport)
136+ _, async := c.raw.(ports.AsyncTransport)
137+ _, synchronous := c.raw.(ports.OwnedSynchronousTransport)
138+ _, linkState := c.raw.(ports.LinkStateReporter)
139+ window := uint8(protocol.MaxOutputWindow)
140+ if datagram {
141+ window = 1
142+ }
143+ return protocol.ConnectionCapabilities{
144+ OutputDataLimit: protocol.MaxOutputDataLen,
145+ PreferredOutputWindow: window,
146+ AsyncSend: async,
147+ OwnedSynchronousSend: synchronous,
148+ LinkState: linkState,
149+ }
150+}
151+
152+func (c *serverConnection) LinkState() ports.LinkState {
153+ if reporter, ok := c.raw.(ports.LinkStateReporter); ok {
154+ return reporter.LinkState()
155+ }
156+ return ports.LinkStateConnected
157+}
158+
159+func (c *serverConnection) LinkEvents() <-chan ports.LinkEvent {
160+ if reporter, ok := c.raw.(ports.LinkStateReporter); ok {
161+ return reporter.LinkEvents()
162+ }
163+ return nil
164+}
165+
166+func (c *serverConnection) Close() error { return c.raw.Close() }
167+
168+type serverListener struct{ raw ports.Listener }
169+
170+var _ ports.ServerListener = (*serverListener)(nil)
171+
172+// NewServerListener wraps every accepted raw connection in a stable typed adapter.
173+func NewServerListener(raw ports.Listener) ports.ServerListener {
174+ if raw == nil {
175+ return nil
176+ }
177+ return &serverListener{raw: raw}
178+}
179+
180+func (l *serverListener) Accept() (ports.ServerConnection, error) {
181+ raw, err := l.raw.Accept()
182+ if err != nil {
183+ return nil, err
184+ }
185+ return NewServerConnection(raw), nil
186+}
187+
188+func (l *serverListener) Close() error { return l.raw.Close() }
189+func (l *serverListener) Addr() string { return l.raw.Addr() }
190+
191+func decodeClient(frame wire.Frame) (protocol.ClientMessage, error) {
192+ switch frame.Type {
193+ case wire.MsgHello:
194+ return wire.UnmarshalHello(frame.Payload)
195+ case wire.MsgInput:
196+ return wire.UnmarshalInput(frame.Payload)
197+ case wire.MsgResize:
198+ return wire.UnmarshalResize(frame.Payload)
199+ case wire.MsgDetach:
200+ return protocol.Detach{}, nil
201+ case wire.MsgPing:
202+ return protocol.Ping{}, nil
203+ case wire.MsgList:
204+ return protocol.List{}, nil
205+ case wire.MsgKill:
206+ return wire.UnmarshalKill(frame.Payload)
207+ case wire.MsgTheme:
208+ return wire.UnmarshalTheme(frame.Payload)
209+ case wire.MsgAck:
210+ return wire.UnmarshalAck(frame.Payload)
211+ case wire.MsgImagePush:
212+ return wire.UnmarshalImagePush(frame.Payload)
213+ case wire.MsgClientNotice:
214+ return wire.UnmarshalClientNotice(frame.Payload)
215+ case wire.MsgCommand:
216+ return wire.UnmarshalCommandRequest(frame.Payload)
217+ case wire.MsgOutputResetRequest:
218+ return wire.UnmarshalOutputResetRequest(frame.Payload)
219+ case wire.MsgRemotePreviewRequest:
220+ return wire.UnmarshalRemotePreviewRequest(frame.Payload)
221+ case wire.MsgRouteAttentionSubscription:
222+ return wire.UnmarshalRouteAttentionSubscription(frame.Payload)
223+ case wire.MsgSamePeerSwitchRequest:
224+ return wire.UnmarshalSamePeerSwitchRequest(frame.Payload)
225+ case wire.MsgParkedRouteRequest:
226+ return wire.UnmarshalParkedRouteRequest(frame.Payload)
227+ case wire.MsgRecentRouteSnapshot:
228+ return wire.UnmarshalRecentRouteSnapshot(frame.Payload)
229+ case wire.MsgRouteNavigationFailure:
230+ return wire.UnmarshalRouteNavigationFailure(frame.Payload)
231+ case wire.MsgWelcome, wire.MsgError, wire.MsgOutput, wire.MsgDetached, wire.MsgPong,
232+ wire.MsgSessions, wire.MsgCommandResult, wire.MsgNavigationAction,
233+ wire.MsgAttachTarget, wire.MsgRemotePreviewResponse,
234+ wire.MsgCommittedRouteIdentity, wire.MsgNavigateRecentRoute,
235+ wire.MsgRoutePosition, wire.MsgSamePeerSwitchFailure, wire.MsgParkedRouteResponse:
236+ return nil, ErrWrongDirection
237+ default:
238+ return nil, ErrUnknownMessageType
239+ }
240+}
241+
242+func encodeServer(message protocol.ServerMessage) (wire.Frame, error) {
243+ switch m := message.(type) {
244+ case protocol.Welcome:
245+ payload := wire.MarshalWelcome(m)
246+ if payload == nil {
247+ return wire.Frame{}, ErrInvalidMessage
248+ }
249+ return wire.Frame{Type: wire.MsgWelcome, Payload: payload}, nil
250+ case protocol.ErrorMsg:
251+ return wire.Frame{Type: wire.MsgError, Payload: wire.MarshalErrorMsg(m)}, nil
252+ case protocol.Output:
253+ payload, err := wire.MarshalOutput(m)
254+ return wire.Frame{Type: wire.MsgOutput, Payload: payload}, err
255+ case protocol.Detached:
256+ if err := protocol.ValidateDetached(m); err != nil {
257+ return wire.Frame{}, err
258+ }
259+ return wire.Frame{Type: wire.MsgDetached, Payload: wire.MarshalDetached(m)}, nil
260+ case protocol.Pong:
261+ return wire.Frame{Type: wire.MsgPong, Payload: wire.MarshalPong(m)}, nil
262+ case protocol.Sessions:
263+ return wire.Frame{Type: wire.MsgSessions, Payload: wire.MarshalSessions(m)}, nil
264+ case protocol.CommandResult:
265+ return wire.Frame{Type: wire.MsgCommandResult, Payload: wire.MarshalCommandResult(m)}, nil
266+ case protocol.NavigationDirective:
267+ payload := wire.MarshalNavigationDirective(m)
268+ if payload == nil {
269+ return wire.Frame{}, ErrInvalidMessage
270+ }
271+ return wire.Frame{Type: wire.MsgNavigationAction, Payload: payload}, nil
272+ case protocol.AttachTarget:
273+ payload := wire.MarshalAttachTarget(m)
274+ if payload == nil {
275+ return wire.Frame{}, ErrInvalidMessage
276+ }
277+ return wire.Frame{Type: wire.MsgAttachTarget, Payload: payload}, nil
278+ case protocol.RemotePreview:
279+ payload := wire.MarshalRemotePreview(m)
280+ if payload == nil {
281+ return wire.Frame{}, ErrInvalidMessage
282+ }
283+ return wire.Frame{Type: wire.MsgRemotePreviewResponse, Payload: payload}, nil
284+ case protocol.CommittedRouteIdentity:
285+ payload, err := wire.MarshalCommittedRouteIdentity(m)
286+ return wire.Frame{Type: wire.MsgCommittedRouteIdentity, Payload: payload}, err
287+ case protocol.RouteNavigationAction:
288+ payload, err := wire.MarshalRouteNavigationAction(m)
289+ return wire.Frame{Type: wire.MsgNavigateRecentRoute, Payload: payload}, err
290+ case protocol.RouteNavigationFailure:
291+ payload, err := wire.MarshalRouteNavigationFailure(m)
292+ return wire.Frame{Type: wire.MsgRouteNavigationFailure, Payload: payload}, err
293+ case protocol.RoutePosition:
294+ payload, err := wire.MarshalRoutePosition(m)
295+ return wire.Frame{Type: wire.MsgRoutePosition, Payload: payload}, err
296+ case protocol.SamePeerSwitchFailure:
297+ payload, err := wire.MarshalSamePeerSwitchFailure(m)
298+ return wire.Frame{Type: wire.MsgSamePeerSwitchFailure, Payload: payload}, err
299+ case protocol.ParkedRouteResponse:
300+ payload := wire.MarshalParkedRouteResponse(m)
301+ if payload == nil {
302+ return wire.Frame{}, ErrInvalidMessage
303+ }
304+ return wire.Frame{Type: wire.MsgParkedRouteResponse, Payload: payload}, nil
305+ case *protocol.Welcome:
306+ if m != nil {
307+ return encodeServer(*m)
308+ }
309+ case *protocol.ErrorMsg:
310+ if m != nil {
311+ return encodeServer(*m)
312+ }
313+ case *protocol.Output:
314+ if m != nil {
315+ return encodeServer(*m)
316+ }
317+ case *protocol.Detached:
318+ if m != nil {
319+ return encodeServer(*m)
320+ }
321+ case *protocol.Pong:
322+ if m != nil {
323+ return encodeServer(*m)
324+ }
325+ case *protocol.Sessions:
326+ if m != nil {
327+ return encodeServer(*m)
328+ }
329+ case *protocol.CommandResult:
330+ if m != nil {
331+ return encodeServer(*m)
332+ }
333+ case *protocol.NavigationDirective:
334+ if m != nil {
335+ return encodeServer(*m)
336+ }
337+ case *protocol.AttachTarget:
338+ if m != nil {
339+ return encodeServer(*m)
340+ }
341+ case *protocol.RemotePreview:
342+ if m != nil {
343+ return encodeServer(*m)
344+ }
345+ case *protocol.CommittedRouteIdentity:
346+ if m != nil {
347+ return encodeServer(*m)
348+ }
349+ case *protocol.RouteNavigationAction:
350+ if m != nil {
351+ return encodeServer(*m)
352+ }
353+ case *protocol.RouteNavigationFailure:
354+ if m != nil {
355+ return encodeServer(*m)
356+ }
357+ case *protocol.RoutePosition:
358+ if m != nil {
359+ return encodeServer(*m)
360+ }
361+ case *protocol.SamePeerSwitchFailure:
362+ if m != nil {
363+ return encodeServer(*m)
364+ }
365+ case *protocol.ParkedRouteResponse:
366+ if m != nil {
367+ return encodeServer(*m)
368+ }
369+ default:
370+ return wire.Frame{}, ErrWrongDirection
371+ }
372+ return wire.Frame{}, ErrInvalidMessage
373+}
A internal/adapters/sessionwire/server_test.go +286 -0
@@ -0,0 +1,286 @@
1+package sessionwire
2+
3+import (
4+ "errors"
5+ "io"
6+ "testing"
7+
8+ "github.com/bnema/vev/internal/domain"
9+ "github.com/bnema/vev/internal/ports"
10+ "github.com/bnema/vev/internal/protocol"
11+ "github.com/bnema/vev/internal/protocol/wire"
12+ "github.com/stretchr/testify/require"
13+)
14+
15+type scriptedTransport struct {
16+ recv wire.Frame
17+ recvErr error
18+ sent wire.Frame
19+ sendErr error
20+ closeErr error
21+ closeCall int
22+}
23+
24+func (t *scriptedTransport) Send(frame wire.Frame) error { t.sent = frame; return t.sendErr }
25+func (t *scriptedTransport) Recv() (wire.Frame, error) { return t.recv, t.recvErr }
26+func (t *scriptedTransport) Close() error { t.closeCall++; return t.closeErr }
27+
28+type capableTransport struct {
29+ scriptedTransport
30+ async wire.Frame
31+ synchronous wire.Frame
32+ events chan ports.LinkEvent
33+}
34+
35+func (*capableTransport) DatagramTransport() {}
36+func (t *capableTransport) SendAsync(frame wire.Frame) error {
37+ t.async = frame
38+ return t.sendErr
39+}
40+func (t *capableTransport) SendSynchronous(frame wire.Frame) error {
41+ t.synchronous = frame
42+ return t.sendErr
43+}
44+func (*capableTransport) LinkState() ports.LinkState { return ports.LinkStateDegraded }
45+func (t *capableTransport) LinkEvents() <-chan ports.LinkEvent { return t.events }
46+
47+func testTarget() domain.RemoteSessionTarget {
48+ return domain.RemoteSessionTarget{
49+ Endpoint: "remote", DisplayOrigin: "remote", LifecycleID: domain.SessionLifecycleID{1},
50+ SessionName: "work", LiveTabID: "tab-1",
51+ }
52+}
53+
54+func TestServerConnectionDecodesEveryClientMessage(t *testing.T) {
55+ target := testTarget()
56+ exact := protocol.ExactSessionTarget{LifecycleID: target.LifecycleID, SessionName: target.SessionName}
57+ lease := protocol.ParkedRouteLeaseID{1}
58+ commandPayload, err := wire.MarshalCommandRequest(protocol.CommandRequest{Version: protocol.Version, RequestID: 7, Slug: "list-sessions"})
59+ require.NoError(t, err)
60+ resizePayload, err := wire.MarshalResize(protocol.Resize{Size: domain.Size{Cols: 80, Rows: 24}})
61+ require.NoError(t, err)
62+ ackPayload, err := wire.MarshalAck(protocol.Ack{Epoch: 1, State: 2})
63+ require.NoError(t, err)
64+ attentionPayload, err := wire.MarshalRouteAttentionSubscription(protocol.RouteAttentionSubscription{})
65+ require.NoError(t, err)
66+ switchPayload, err := wire.MarshalSamePeerSwitchRequest(protocol.SamePeerSwitchRequest{RequestID: 3, Target: exact})
67+ require.NoError(t, err)
68+ snapshotPayload, err := wire.MarshalRecentRouteSnapshot(protocol.RecentRouteSnapshot{})
69+ require.NoError(t, err)
70+ failurePayload, err := wire.MarshalRouteNavigationFailure(protocol.RouteNavigationFailure{Key: 1, Generation: 1, Code: protocol.RouteFailureUnavailable})
71+ require.NoError(t, err)
72+
73+ tests := []struct {
74+ name string
75+ frame wire.Frame
76+ want protocol.ClientMessage
77+ }{
78+ {name: "hello", frame: wire.Frame{Type: wire.MsgHello, Payload: wire.MarshalHello(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}})}, want: protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}}},
79+ {name: "input", frame: wire.Frame{Type: wire.MsgInput, Payload: wire.MarshalInput(protocol.Input{InputSeq: 1, Data: []byte("x")})}, want: protocol.Input{InputSeq: 1, Data: []byte("x")}},
80+ {name: "resize", frame: wire.Frame{Type: wire.MsgResize, Payload: resizePayload}, want: protocol.Resize{Size: domain.Size{Cols: 80, Rows: 24}}},
81+ {name: "detach", frame: wire.Frame{Type: wire.MsgDetach}, want: protocol.Detach{}},
82+ {name: "ping", frame: wire.Frame{Type: wire.MsgPing}, want: protocol.Ping{}},
83+ {name: "list", frame: wire.Frame{Type: wire.MsgList}, want: protocol.List{}},
84+ {name: "kill", frame: wire.Frame{Type: wire.MsgKill, Payload: wire.MarshalKill(protocol.Kill{Name: "work"})}, want: protocol.Kill{Name: "work"}},
85+ {name: "theme", frame: wire.Frame{Type: wire.MsgTheme, Payload: wire.MarshalTheme(protocol.Theme{TrueColor: true})}, want: protocol.Theme{TrueColor: true}},
86+ {name: "ack", frame: wire.Frame{Type: wire.MsgAck, Payload: ackPayload}, want: protocol.Ack{Epoch: 1, State: 2}},
87+ {name: "image", frame: wire.Frame{Type: wire.MsgImagePush, Payload: wire.MarshalImagePush(protocol.ImagePush{InputSeq: 2, Mime: "image/png", Data: []byte{1}})}, want: protocol.ImagePush{InputSeq: 2, Mime: "image/png", Data: []byte{1}}},
88+ {name: "notice", frame: wire.Frame{Type: wire.MsgClientNotice, Payload: wire.MarshalClientNotice(protocol.ClientNotice{Action: protocol.ClientNoticeLinkConnected})}, want: protocol.ClientNotice{Action: protocol.ClientNoticeLinkConnected}},
89+ {name: "command", frame: wire.Frame{Type: wire.MsgCommand, Payload: commandPayload}, want: protocol.CommandRequest{Version: protocol.Version, RequestID: 7, Slug: "list-sessions"}},
90+ {name: "reset", frame: wire.Frame{Type: wire.MsgOutputResetRequest}, want: protocol.OutputResetRequest{}},
91+ {name: "preview", frame: wire.Frame{Type: wire.MsgRemotePreviewRequest, Payload: wire.MarshalRemotePreviewRequest(protocol.RemotePreviewRequest{Version: protocol.RemotePreviewSchemaVersion, Target: target, Width: 1, Height: 1})}, want: protocol.RemotePreviewRequest{Version: protocol.RemotePreviewSchemaVersion, Target: target, Width: 1, Height: 1}},
92+ {name: "attention", frame: wire.Frame{Type: wire.MsgRouteAttentionSubscription, Payload: attentionPayload}, want: protocol.RouteAttentionSubscription{Targets: []protocol.RouteAttentionTarget{}}},
93+ {name: "same peer", frame: wire.Frame{Type: wire.MsgSamePeerSwitchRequest, Payload: switchPayload}, want: protocol.SamePeerSwitchRequest{RequestID: 3, Target: exact}},
94+ {name: "parked", frame: wire.Frame{Type: wire.MsgParkedRouteRequest, Payload: wire.MarshalParkedRouteRequest(protocol.ParkedRouteRequest{RequestID: 4, LeaseID: lease, Action: protocol.ParkedRoutePrepare})}, want: protocol.ParkedRouteRequest{RequestID: 4, LeaseID: lease, Action: protocol.ParkedRoutePrepare}},
95+ {name: "snapshot", frame: wire.Frame{Type: wire.MsgRecentRouteSnapshot, Payload: snapshotPayload}, want: protocol.RecentRouteSnapshot{}},
96+ {name: "route failure", frame: wire.Frame{Type: wire.MsgRouteNavigationFailure, Payload: failurePayload}, want: protocol.RouteNavigationFailure{Key: 1, Generation: 1, Code: protocol.RouteFailureUnavailable}},
97+ }
98+ for _, tt := range tests {
99+ t.Run(tt.name, func(t *testing.T) {
100+ raw := &scriptedTransport{recv: tt.frame}
101+ got, err := NewServerConnection(raw).ReceiveClient()
102+ require.NoError(t, err)
103+ require.Equal(t, tt.want, got)
104+ })
105+ }
106+}
107+
108+func TestServerConnectionEncodesEveryServerMessage(t *testing.T) {
109+ exact := protocol.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{1}, SessionName: "work"}
110+ lease := protocol.ParkedRouteLeaseID{1}
111+ welcome := protocol.Welcome{SessionID: "session"}
112+ errorMessage := protocol.ErrorMsg{Code: protocol.ErrInternal, Text: "error"}
113+ output := protocol.Output{Epoch: 1, New: 1, Full: true, Size: domain.Size{Cols: 80, Rows: 24}}
114+ detached := protocol.Detached{Reason: protocol.ReasonDetach}
115+ pong := protocol.Pong{}
116+ sessions := protocol.Sessions{}
117+ commandResult := protocol.CommandResult{RequestID: 1, OK: true}
118+ navigation := protocol.NavigationDirective{Action: protocol.NavigationOpenHomePicker, LeaseID: lease}
119+ attachTarget := protocol.AttachTarget{Session: "work", Intent: protocol.IntentAttach}
120+ preview := protocol.RemotePreview{Version: protocol.RemotePreviewSchemaVersion, Status: protocol.RemotePreviewUnavailable}
121+ identity := protocol.CommittedRouteIdentity{Target: exact}
122+ routeAction := protocol.RouteNavigationAction{SnapshotGeneration: 1, Key: 1, Generation: 1}
123+ routeFailure := protocol.RouteNavigationFailure{Key: 1, Generation: 1, Code: protocol.RouteFailureUnavailable}
124+ routePosition := protocol.RoutePosition{Target: exact, ActiveTabID: "tab-1"}
125+ switchFailure := protocol.SamePeerSwitchFailure{RequestID: 1, Code: protocol.SamePeerSwitchUnavailable}
126+ parkedResponse := protocol.ParkedRouteResponse{RequestID: 1, Status: protocol.ParkedRouteReady}
127+
128+ outputPayload, err := wire.MarshalOutput(output)
129+ require.NoError(t, err)
130+ identityPayload, err := wire.MarshalCommittedRouteIdentity(identity)
131+ require.NoError(t, err)
132+ routeActionPayload, err := wire.MarshalRouteNavigationAction(routeAction)
133+ require.NoError(t, err)
134+ routeFailurePayload, err := wire.MarshalRouteNavigationFailure(routeFailure)
135+ require.NoError(t, err)
136+ routePositionPayload, err := wire.MarshalRoutePosition(routePosition)
137+ require.NoError(t, err)
138+ switchFailurePayload, err := wire.MarshalSamePeerSwitchFailure(switchFailure)
139+ require.NoError(t, err)
140+
141+ tests := []struct {
142+ name string
143+ message protocol.ServerMessage
144+ typeID wire.MsgType
145+ payload []byte
146+ }{
147+ {name: "welcome", message: welcome, typeID: wire.MsgWelcome, payload: wire.MarshalWelcome(welcome)},
148+ {name: "error", message: errorMessage, typeID: wire.MsgError, payload: wire.MarshalErrorMsg(errorMessage)},
149+ {name: "output", message: output, typeID: wire.MsgOutput, payload: outputPayload},
150+ {name: "detached", message: detached, typeID: wire.MsgDetached, payload: wire.MarshalDetached(detached)},
151+ {name: "pong", message: pong, typeID: wire.MsgPong, payload: wire.MarshalPong(pong)},
152+ {name: "sessions", message: sessions, typeID: wire.MsgSessions, payload: wire.MarshalSessions(sessions)},
153+ {name: "command result", message: commandResult, typeID: wire.MsgCommandResult, payload: wire.MarshalCommandResult(commandResult)},
154+ {name: "navigation", message: navigation, typeID: wire.MsgNavigationAction, payload: wire.MarshalNavigationDirective(navigation)},
155+ {name: "attach target", message: attachTarget, typeID: wire.MsgAttachTarget, payload: wire.MarshalAttachTarget(attachTarget)},
156+ {name: "preview", message: preview, typeID: wire.MsgRemotePreviewResponse, payload: wire.MarshalRemotePreview(preview)},
157+ {name: "identity", message: identity, typeID: wire.MsgCommittedRouteIdentity, payload: identityPayload},
158+ {name: "route action", message: routeAction, typeID: wire.MsgNavigateRecentRoute, payload: routeActionPayload},
159+ {name: "route failure", message: routeFailure, typeID: wire.MsgRouteNavigationFailure, payload: routeFailurePayload},
160+ {name: "route position", message: routePosition, typeID: wire.MsgRoutePosition, payload: routePositionPayload},
161+ {name: "switch failure", message: switchFailure, typeID: wire.MsgSamePeerSwitchFailure, payload: switchFailurePayload},
162+ {name: "parked response", message: parkedResponse, typeID: wire.MsgParkedRouteResponse, payload: wire.MarshalParkedRouteResponse(parkedResponse)},
163+ }
164+ for _, tt := range tests {
165+ t.Run(tt.name, func(t *testing.T) {
166+ raw := &scriptedTransport{}
167+ require.NoError(t, NewServerConnection(raw).SendServer(tt.message))
168+ require.Equal(t, tt.typeID, raw.sent.Type)
169+ require.Equal(t, tt.payload, raw.sent.Payload)
170+ })
171+ }
172+}
173+
174+func TestServerConnectionPreservesPayloadAgnosticClientControls(t *testing.T) {
175+ for _, tt := range []struct {
176+ name string
177+ frame wire.Frame
178+ want protocol.ClientMessage
179+ }{
180+ {name: "list", frame: wire.Frame{Type: wire.MsgList, Payload: []byte{1}}, want: protocol.List{}},
181+ {name: "detach", frame: wire.Frame{Type: wire.MsgDetach, Payload: []byte{1}}, want: protocol.Detach{}},
182+ {name: "ping", frame: wire.Frame{Type: wire.MsgPing, Payload: []byte{1}}, want: protocol.Ping{}},
183+ } {
184+ t.Run(tt.name, func(t *testing.T) {
185+ message, err := NewServerConnection(&scriptedTransport{recv: tt.frame}).ReceiveClient()
186+ require.NoError(t, err)
187+ require.Equal(t, tt.want, message)
188+ })
189+ }
190+}
191+
192+func TestServerConnectionClassifiesDecodeFailures(t *testing.T) {
193+ tests := []struct {
194+ name string
195+ frame wire.Frame
196+ category protocol.DecodeCategory
197+ version uint16
198+ request uint64
199+ }{
200+ {name: "malformed hello", frame: wire.Frame{Type: wire.MsgHello, Payload: []byte{0, 38}}, category: protocol.DecodeMalformed, version: 38},
201+ {name: "malformed command", frame: wire.Frame{Type: wire.MsgCommand, Payload: []byte{0, 37, 0, 0, 0, 0, 0, 0, 0, 9}}, category: protocol.DecodeMalformed, version: 37, request: 9},
202+ {name: "wrong direction", frame: wire.Frame{Type: wire.MsgOutput}, category: protocol.DecodeWrongDirection},
203+ {name: "unknown", frame: wire.Frame{Type: 255}, category: protocol.DecodeUnknownType},
204+ }
205+ for _, tt := range tests {
206+ t.Run(tt.name, func(t *testing.T) {
207+ _, err := NewServerConnection(&scriptedTransport{recv: tt.frame}).ReceiveClient()
208+ var failure *protocol.DecodeFailure
209+ require.ErrorAs(t, err, &failure)
210+ require.Equal(t, tt.category, failure.Category)
211+ require.Equal(t, tt.version, failure.Version)
212+ require.Equal(t, tt.request, failure.RequestID)
213+ })
214+ }
215+}
216+
217+func TestServerConnectionPreservesSendModesCapabilitiesAndErrors(t *testing.T) {
218+ sendErr := errors.New("send failed")
219+ events := make(chan ports.LinkEvent, 1)
220+ raw := &capableTransport{scriptedTransport: scriptedTransport{sendErr: sendErr}, events: events}
221+ connection := NewServerConnection(raw)
222+
223+ caps := connection.Capabilities()
224+ require.Equal(t, uint8(1), caps.PreferredOutputWindow)
225+ require.Equal(t, protocol.MaxOutputDataLen, caps.OutputDataLimit)
226+ require.True(t, caps.AsyncSend)
227+ require.True(t, caps.OwnedSynchronousSend)
228+ require.True(t, caps.LinkState)
229+ require.Equal(t, ports.LinkStateDegraded, connection.LinkState())
230+ require.Equal(t, (<-chan ports.LinkEvent)(events), connection.LinkEvents())
231+
232+ message := protocol.Pong{}
233+ require.ErrorIs(t, connection.SendServer(message), sendErr)
234+ require.Equal(t, wire.MsgPong, raw.sent.Type)
235+ require.ErrorIs(t, connection.SendServerAsync(message), sendErr)
236+ require.Equal(t, wire.MsgPong, raw.async.Type)
237+ require.ErrorIs(t, connection.SendServerSynchronous(message), sendErr)
238+ require.Equal(t, wire.MsgPong, raw.synchronous.Type)
239+
240+ output := protocol.Output{Epoch: 1, New: 1, Full: true, Size: domain.Size{Cols: 80, Rows: 24}}
241+ require.ErrorIs(t, connection.SendOutput(output), sendErr)
242+ require.Equal(t, wire.MsgOutput, raw.sent.Type)
243+ require.ErrorIs(t, connection.SendOutputAsync(output), sendErr)
244+ require.Equal(t, wire.MsgOutput, raw.async.Type)
245+ require.ErrorIs(t, connection.SendOutputSynchronous(output), sendErr)
246+ require.Equal(t, wire.MsgOutput, raw.synchronous.Type)
247+
248+ plain := NewServerConnection(&scriptedTransport{})
249+ require.ErrorIs(t, plain.SendServerAsync(message), ErrUnsupportedSend)
250+ require.ErrorIs(t, plain.SendServerSynchronous(message), ErrUnsupportedSend)
251+ require.ErrorIs(t, plain.SendOutputAsync(output), ErrUnsupportedSend)
252+ require.ErrorIs(t, plain.SendOutputSynchronous(output), ErrUnsupportedSend)
253+ require.Equal(t, uint8(protocol.MaxOutputWindow), plain.Capabilities().PreferredOutputWindow)
254+ require.Equal(t, ports.LinkStateConnected, plain.LinkState())
255+ require.Nil(t, plain.LinkEvents())
256+}
257+
258+func TestServerConnectionForwardsReceiveAndCloseErrors(t *testing.T) {
259+ recvErr := errors.Join(io.EOF, errors.New("closed"))
260+ closeErr := errors.New("close failed")
261+ raw := &scriptedTransport{recvErr: recvErr, closeErr: closeErr}
262+ connection := NewServerConnection(raw)
263+ _, err := connection.ReceiveClient()
264+ require.ErrorIs(t, err, io.EOF)
265+ require.ErrorIs(t, connection.Close(), closeErr)
266+ require.Equal(t, 1, raw.closeCall)
267+}
268+
269+func TestServerListenerWrapsEachAcceptedIncarnation(t *testing.T) {
270+ raw := &scriptedTransport{recv: wire.Frame{Type: wire.MsgPing}}
271+ listener := &scriptedListener{transport: raw}
272+ wrapped := NewServerListener(listener)
273+ first, err := wrapped.Accept()
274+ require.NoError(t, err)
275+ message, err := first.ReceiveClient()
276+ require.NoError(t, err)
277+ require.Equal(t, protocol.Ping{}, message)
278+ require.Equal(t, "test", wrapped.Addr())
279+ require.NoError(t, wrapped.Close())
280+}
281+
282+type scriptedListener struct{ transport ports.Transport }
283+
284+func (l *scriptedListener) Accept() (ports.Transport, error) { return l.transport, nil }
285+func (*scriptedListener) Close() error { return nil }
286+func (*scriptedListener) Addr() string { return "test" }
M internal/app/daemon_integration_test.go +57 -6
@@ -5,6 +5,7 @@ package app
5 import (5 import (
6 "context"6 "context"
7 "crypto/rand"7 "crypto/rand"
8+ "encoding/binary"
8 "errors"9 "errors"
9 "io"10 "io"
10 "log/slog"11 "log/slog"
@@ -24,6 +25,7 @@ import (
24 "github.com/bnema/vev/internal/adapters/ipc"25 "github.com/bnema/vev/internal/adapters/ipc"
25 "github.com/bnema/vev/internal/adapters/lifecycle"26 "github.com/bnema/vev/internal/adapters/lifecycle"
26 "github.com/bnema/vev/internal/adapters/pty"27 "github.com/bnema/vev/internal/adapters/pty"
28+ "github.com/bnema/vev/internal/adapters/sessionwire"
27 "github.com/bnema/vev/internal/adapters/snapshot"29 "github.com/bnema/vev/internal/adapters/snapshot"
28 "github.com/bnema/vev/internal/domain"30 "github.com/bnema/vev/internal/domain"
29 "github.com/bnema/vev/internal/persist"31 "github.com/bnema/vev/internal/persist"
@@ -58,7 +60,7 @@ func startDaemonInDir(t *testing.T, dir string, opts ...daemon.Option) (string,
58 d := daemon.New(pty.NewFactory(), clock.New(), discardLog(), opts...)60 d := daemon.New(pty.NewFactory(), clock.New(), discardLog(), opts...)
59 ctx, cancel := context.WithCancel(context.Background())61 ctx, cancel := context.WithCancel(context.Background())
60 ch := make(chan error, 1)62 ch := make(chan error, 1)
61- go func() { ch <- d.Serve(ctx, ln) }()63+ go func() { ch <- d.Serve(ctx, sessionwire.NewServerListener(ln)) }()
62 t.Cleanup(cancel)64 t.Cleanup(cancel)
63 return dir, ch65 return dir, ch
64 }66 }
@@ -135,6 +137,55 @@ func killAll(dir string) error {
135 return err137 return err
136 }138 }
137 139
140+func TestIntegration_MalformedCommandPreservesVersionAndRequestID(t *testing.T) {
141+ incompatibleVersion := make([]byte, 10)
142+ binary.BigEndian.PutUint16(incompatibleVersion[:2], protocol.Version+1)
143+ binary.BigEndian.PutUint64(incompatibleVersion[2:], 42)
144+ valid, err := wire.MarshalCommandRequest(protocol.CommandRequest{
145+ Version: protocol.Version, RequestID: 43, Slug: "list-sessions",
146+ })
147+ require.NoError(t, err)
148+
149+ tests := []struct {
150+ name string
151+ payload []byte
152+ want protocol.CommandResult
153+ }{
154+ {
155+ name: "incompatible version",
156+ payload: incompatibleVersion,
157+ want: protocol.CommandResult{RequestID: 42, Code: protocol.ErrVersionMismatch, Text: "protocol version mismatch"},
158+ },
159+ {
160+ name: "truncated version prefix",
161+ payload: []byte{0},
162+ want: protocol.CommandResult{Code: protocol.ErrInternal, Text: "malformed command request"},
163+ },
164+ {
165+ name: "trailing garbage",
166+ payload: append(append([]byte(nil), valid...), 0xff),
167+ want: protocol.CommandResult{RequestID: 43, Code: protocol.ErrInternal, Text: "malformed command request"},
168+ },
169+ }
170+ for _, tt := range tests {
171+ t.Run(tt.name, func(t *testing.T) {
172+ dir, _ := startDaemon(t)
173+ tr, err := ipc.DialContext(context.Background(), dir)
174+ require.NoError(t, err)
175+ defer func() { _ = tr.Close() }()
176+
177+ require.NoError(t, tr.Send(wire.Frame{Type: wire.MsgCommand, Payload: tt.payload}))
178+ frame, err := tr.Recv()
179+ require.NoError(t, err)
180+ require.Equal(t, wire.MsgCommandResult, frame.Type)
181+ require.Equal(t, wire.MarshalCommandResult(tt.want), frame.Payload)
182+ result, err := wire.UnmarshalCommandResult(frame.Payload)
183+ require.NoError(t, err)
184+ require.Equal(t, tt.want, result)
185+ })
186+ }
187+}
188+
138 // awaitText decodes MsgOutput frames into a fresh VT screen and returns once189 // awaitText decodes MsgOutput frames into a fresh VT screen and returns once
139 // the reconstructed grid contains want.190 // the reconstructed grid contains want.
140 func awaitText(t *testing.T, p *pump, sz domain.Size, want string) {191 func awaitText(t *testing.T, p *pump, sz domain.Size, want string) {
@@ -611,7 +662,7 @@ func TestLifecycleOwnershipOutlivesMaintenanceWriter(t *testing.T) {
611 if err != nil {662 if err != nil {
612 return err663 return err
613 }664 }
614- return d.Serve(ctx, listener)665+ return d.Serve(ctx, sessionwire.NewServerListener(listener))
615 })666 })
616 }()667 }()
617 668
@@ -746,7 +797,7 @@ func TestLifecycleOwnershipOutlivesRestorationWriter(t *testing.T) {
746 daemon.WithCatalogue(opened.Catalogue, opened.Records),797 daemon.WithCatalogue(opened.Catalogue, opened.Records),
747 daemon.WithSnapshotRepository(blocking),798 daemon.WithSnapshotRepository(blocking),
748 )799 )
749- return d.Serve(ctx, listener)800+ return d.Serve(ctx, sessionwire.NewServerListener(listener))
750 })801 })
751 }()802 }()
752 803
@@ -800,7 +851,7 @@ func publishRestorableCheckpoint(t *testing.T, stateDir string, repository *snap
800 daemon.WithRecoveryCoordinator(coordinator),851 daemon.WithRecoveryCoordinator(coordinator),
801 )852 )
802 served := make(chan error, 1)853 served := make(chan error, 1)
803- go func() { served <- d.Serve(ctx, listener) }()854+ go func() { served <- d.Serve(ctx, sessionwire.NewServerListener(listener)) }()
804 855
805 tr, _ := attach(t, runtimeDir, protocol.IntentNew, name, domain.Size{Cols: 80, Rows: 24})856 tr, _ := attach(t, runtimeDir, protocol.IntentNew, name, domain.Size{Cols: 80, Rows: 24})
806 require.NoError(t, tr.Send(wire.Frame{Type: wire.MsgInput, Payload: wire.MarshalInput(protocol.Input{Data: []byte("checkpoint me\n")})}))857 require.NoError(t, tr.Send(wire.Frame{Type: wire.MsgInput, Payload: wire.MarshalInput(protocol.Input{Data: []byte("checkpoint me\n")})}))
@@ -883,7 +934,7 @@ func runBlockedSnapshotWriterShutdown(t *testing.T) blockedSnapshotWriterShutdow
883 daemon.WithShell("/bin/cat", nil),934 daemon.WithShell("/bin/cat", nil),
884 daemon.WithSnapshotRepository(repository),935 daemon.WithSnapshotRepository(repository),
885 )936 )
886- return d.Serve(ctx, observed)937+ return d.Serve(ctx, sessionwire.NewServerListener(observed))
887 })938 })
888 }()939 }()
889 940
@@ -1074,7 +1125,7 @@ func TestLifecycleSocketCloseCatalogueRace(t *testing.T) {
1074 oldCtx, stopOld := context.WithCancel(context.Background())1125 oldCtx, stopOld := context.WithCancel(context.Background())
1075 oldDone := make(chan error, 1)1126 oldDone := make(chan error, 1)
1076 go func() {1127 go func() {
1077- serveErr := oldDaemon.Serve(oldCtx, controlledListener)1128+ serveErr := oldDaemon.Serve(oldCtx, sessionwire.NewServerListener(controlledListener))
1078 teardownEvents <- "owner-release"1129 teardownEvents <- "owner-release"
1079 close(ownerReleaseEntered)1130 close(ownerReleaseEntered)
1080 <-allowOwnerRelease1131 <-allowOwnerRelease
M internal/app/run.go +2 -1
@@ -37,6 +37,7 @@ import (
37 "github.com/bnema/vev/internal/adapters/observability"37 "github.com/bnema/vev/internal/adapters/observability"
38 "github.com/bnema/vev/internal/adapters/pty"38 "github.com/bnema/vev/internal/adapters/pty"
39 remoteadapter "github.com/bnema/vev/internal/adapters/remote"39 remoteadapter "github.com/bnema/vev/internal/adapters/remote"
40+ "github.com/bnema/vev/internal/adapters/sessionwire"
40 "github.com/bnema/vev/internal/adapters/shellcmd"41 "github.com/bnema/vev/internal/adapters/shellcmd"
41 snapshotadapter "github.com/bnema/vev/internal/adapters/snapshot"42 snapshotadapter "github.com/bnema/vev/internal/adapters/snapshot"
42 "github.com/bnema/vev/internal/adapters/sshstdio"43 "github.com/bnema/vev/internal/adapters/sshstdio"
@@ -742,7 +743,7 @@ func runDaemonOwnedWithLogger(ctx context.Context, log *slog.Logger) (retErr err
742 log.Warn("config watcher stopped", "path", configPath, "err", err)743 log.Warn("config watcher stopped", "path", configPath, "err", err)
743 }744 }
744 }()745 }()
745- if err := d.Serve(ctx, ln); err != nil {746+ if err := d.Serve(ctx, sessionwire.NewServerListener(ln)); err != nil {
746 log.Error("daemon exited", "err", err)747 log.Error("daemon exited", "err", err)
747 return err748 return err
748 }749 }
A internal/ports/mocks/session_mock.go +747 -0
@@ -0,0 +1,747 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package portsmocks
6+
7+import (
8+ "github.com/bnema/vev/internal/ports"
9+ "github.com/bnema/vev/internal/protocol"
10+ mock "github.com/stretchr/testify/mock"
11+)
12+
13+// NewMockServerConnection creates a new instance of MockServerConnection. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
14+// The first argument is typically a *testing.T value.
15+func NewMockServerConnection(t interface {
16+ mock.TestingT
17+ Cleanup(func())
18+}) *MockServerConnection {
19+ mock := &MockServerConnection{}
20+ mock.Mock.Test(t)
21+
22+ t.Cleanup(func() { mock.AssertExpectations(t) })
23+
24+ return mock
25+}
26+
27+// MockServerConnection is an autogenerated mock type for the ServerConnection type
28+type MockServerConnection struct {
29+ mock.Mock
30+}
31+
32+type MockServerConnection_Expecter struct {
33+ mock *mock.Mock
34+}
35+
36+func (_m *MockServerConnection) EXPECT() *MockServerConnection_Expecter {
37+ return &MockServerConnection_Expecter{mock: &_m.Mock}
38+}
39+
40+// Capabilities provides a mock function for the type MockServerConnection
41+func (_mock *MockServerConnection) Capabilities() protocol.ConnectionCapabilities {
42+ ret := _mock.Called()
43+
44+ if len(ret) == 0 {
45+ panic("no return value specified for Capabilities")
46+ }
47+
48+ var r0 protocol.ConnectionCapabilities
49+ if returnFunc, ok := ret.Get(0).(func() protocol.ConnectionCapabilities); ok {
50+ r0 = returnFunc()
51+ } else {
52+ r0 = ret.Get(0).(protocol.ConnectionCapabilities)
53+ }
54+ return r0
55+}
56+
57+// MockServerConnection_Capabilities_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Capabilities'
58+type MockServerConnection_Capabilities_Call struct {
59+ *mock.Call
60+}
61+
62+// Capabilities is a helper method to define mock.On call
63+func (_e *MockServerConnection_Expecter) Capabilities() *MockServerConnection_Capabilities_Call {
64+ return &MockServerConnection_Capabilities_Call{Call: _e.mock.On("Capabilities")}
65+}
66+
67+func (_c *MockServerConnection_Capabilities_Call) Run(run func()) *MockServerConnection_Capabilities_Call {
68+ _c.Call.Run(func(args mock.Arguments) {
69+ run()
70+ })
71+ return _c
72+}
73+
74+func (_c *MockServerConnection_Capabilities_Call) Return(connectionCapabilities protocol.ConnectionCapabilities) *MockServerConnection_Capabilities_Call {
75+ _c.Call.Return(connectionCapabilities)
76+ return _c
77+}
78+
79+func (_c *MockServerConnection_Capabilities_Call) RunAndReturn(run func() protocol.ConnectionCapabilities) *MockServerConnection_Capabilities_Call {
80+ _c.Call.Return(run)
81+ return _c
82+}
83+
84+// Close provides a mock function for the type MockServerConnection
85+func (_mock *MockServerConnection) Close() error {
86+ ret := _mock.Called()
87+
88+ if len(ret) == 0 {
89+ panic("no return value specified for Close")
90+ }
91+
92+ var r0 error
93+ if returnFunc, ok := ret.Get(0).(func() error); ok {
94+ r0 = returnFunc()
95+ } else {
96+ r0 = ret.Error(0)
97+ }
98+ return r0
99+}
100+
101+// MockServerConnection_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close'
102+type MockServerConnection_Close_Call struct {
103+ *mock.Call
104+}
105+
106+// Close is a helper method to define mock.On call
107+func (_e *MockServerConnection_Expecter) Close() *MockServerConnection_Close_Call {
108+ return &MockServerConnection_Close_Call{Call: _e.mock.On("Close")}
109+}
110+
111+func (_c *MockServerConnection_Close_Call) Run(run func()) *MockServerConnection_Close_Call {
112+ _c.Call.Run(func(args mock.Arguments) {
113+ run()
114+ })
115+ return _c
116+}
117+
118+func (_c *MockServerConnection_Close_Call) Return(err error) *MockServerConnection_Close_Call {
119+ _c.Call.Return(err)
120+ return _c
121+}
122+
123+func (_c *MockServerConnection_Close_Call) RunAndReturn(run func() error) *MockServerConnection_Close_Call {
124+ _c.Call.Return(run)
125+ return _c
126+}
127+
128+// LinkEvents provides a mock function for the type MockServerConnection
129+func (_mock *MockServerConnection) LinkEvents() <-chan ports.LinkEvent {
130+ ret := _mock.Called()
131+
132+ if len(ret) == 0 {
133+ panic("no return value specified for LinkEvents")
134+ }
135+
136+ var r0 <-chan ports.LinkEvent
137+ if returnFunc, ok := ret.Get(0).(func() <-chan ports.LinkEvent); ok {
138+ r0 = returnFunc()
139+ } else {
140+ if ret.Get(0) != nil {
141+ r0 = ret.Get(0).(<-chan ports.LinkEvent)
142+ }
143+ }
144+ return r0
145+}
146+
147+// MockServerConnection_LinkEvents_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LinkEvents'
148+type MockServerConnection_LinkEvents_Call struct {
149+ *mock.Call
150+}
151+
152+// LinkEvents is a helper method to define mock.On call
153+func (_e *MockServerConnection_Expecter) LinkEvents() *MockServerConnection_LinkEvents_Call {
154+ return &MockServerConnection_LinkEvents_Call{Call: _e.mock.On("LinkEvents")}
155+}
156+
157+func (_c *MockServerConnection_LinkEvents_Call) Run(run func()) *MockServerConnection_LinkEvents_Call {
158+ _c.Call.Run(func(args mock.Arguments) {
159+ run()
160+ })
161+ return _c
162+}
163+
164+func (_c *MockServerConnection_LinkEvents_Call) Return(linkEventCh <-chan ports.LinkEvent) *MockServerConnection_LinkEvents_Call {
165+ _c.Call.Return(linkEventCh)
166+ return _c
167+}
168+
169+func (_c *MockServerConnection_LinkEvents_Call) RunAndReturn(run func() <-chan ports.LinkEvent) *MockServerConnection_LinkEvents_Call {
170+ _c.Call.Return(run)
171+ return _c
172+}
173+
174+// LinkState provides a mock function for the type MockServerConnection
175+func (_mock *MockServerConnection) LinkState() ports.LinkState {
176+ ret := _mock.Called()
177+
178+ if len(ret) == 0 {
179+ panic("no return value specified for LinkState")
180+ }
181+
182+ var r0 ports.LinkState
183+ if returnFunc, ok := ret.Get(0).(func() ports.LinkState); ok {
184+ r0 = returnFunc()
185+ } else {
186+ r0 = ret.Get(0).(ports.LinkState)
187+ }
188+ return r0
189+}
190+
191+// MockServerConnection_LinkState_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LinkState'
192+type MockServerConnection_LinkState_Call struct {
193+ *mock.Call
194+}
195+
196+// LinkState is a helper method to define mock.On call
197+func (_e *MockServerConnection_Expecter) LinkState() *MockServerConnection_LinkState_Call {
198+ return &MockServerConnection_LinkState_Call{Call: _e.mock.On("LinkState")}
199+}
200+
201+func (_c *MockServerConnection_LinkState_Call) Run(run func()) *MockServerConnection_LinkState_Call {
202+ _c.Call.Run(func(args mock.Arguments) {
203+ run()
204+ })
205+ return _c
206+}
207+
208+func (_c *MockServerConnection_LinkState_Call) Return(linkState ports.LinkState) *MockServerConnection_LinkState_Call {
209+ _c.Call.Return(linkState)
210+ return _c
211+}
212+
213+func (_c *MockServerConnection_LinkState_Call) RunAndReturn(run func() ports.LinkState) *MockServerConnection_LinkState_Call {
214+ _c.Call.Return(run)
215+ return _c
216+}
217+
218+// ReceiveClient provides a mock function for the type MockServerConnection
219+func (_mock *MockServerConnection) ReceiveClient() (protocol.ClientMessage, error) {
220+ ret := _mock.Called()
221+
222+ if len(ret) == 0 {
223+ panic("no return value specified for ReceiveClient")
224+ }
225+
226+ var r0 protocol.ClientMessage
227+ var r1 error
228+ if returnFunc, ok := ret.Get(0).(func() (protocol.ClientMessage, error)); ok {
229+ return returnFunc()
230+ }
231+ if returnFunc, ok := ret.Get(0).(func() protocol.ClientMessage); ok {
232+ r0 = returnFunc()
233+ } else {
234+ if ret.Get(0) != nil {
235+ r0 = ret.Get(0).(protocol.ClientMessage)
236+ }
237+ }
238+ if returnFunc, ok := ret.Get(1).(func() error); ok {
239+ r1 = returnFunc()
240+ } else {
241+ r1 = ret.Error(1)
242+ }
243+ return r0, r1
244+}
245+
246+// MockServerConnection_ReceiveClient_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ReceiveClient'
247+type MockServerConnection_ReceiveClient_Call struct {
248+ *mock.Call
249+}
250+
251+// ReceiveClient is a helper method to define mock.On call
252+func (_e *MockServerConnection_Expecter) ReceiveClient() *MockServerConnection_ReceiveClient_Call {
253+ return &MockServerConnection_ReceiveClient_Call{Call: _e.mock.On("ReceiveClient")}
254+}
255+
256+func (_c *MockServerConnection_ReceiveClient_Call) Run(run func()) *MockServerConnection_ReceiveClient_Call {
257+ _c.Call.Run(func(args mock.Arguments) {
258+ run()
259+ })
260+ return _c
261+}
262+
263+func (_c *MockServerConnection_ReceiveClient_Call) Return(clientMessage protocol.ClientMessage, err error) *MockServerConnection_ReceiveClient_Call {
264+ _c.Call.Return(clientMessage, err)
265+ return _c
266+}
267+
268+func (_c *MockServerConnection_ReceiveClient_Call) RunAndReturn(run func() (protocol.ClientMessage, error)) *MockServerConnection_ReceiveClient_Call {
269+ _c.Call.Return(run)
270+ return _c
271+}
272+
273+// SendOutput provides a mock function for the type MockServerConnection
274+func (_mock *MockServerConnection) SendOutput(output protocol.Output) error {
275+ ret := _mock.Called(output)
276+
277+ if len(ret) == 0 {
278+ panic("no return value specified for SendOutput")
279+ }
280+
281+ var r0 error
282+ if returnFunc, ok := ret.Get(0).(func(protocol.Output) error); ok {
283+ r0 = returnFunc(output)
284+ } else {
285+ r0 = ret.Error(0)
286+ }
287+ return r0
288+}
289+
290+// MockServerConnection_SendOutput_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendOutput'
291+type MockServerConnection_SendOutput_Call struct {
292+ *mock.Call
293+}
294+
295+// SendOutput is a helper method to define mock.On call
296+// - output protocol.Output
297+func (_e *MockServerConnection_Expecter) SendOutput(output any) *MockServerConnection_SendOutput_Call {
298+ return &MockServerConnection_SendOutput_Call{Call: _e.mock.On("SendOutput", output)}
299+}
300+
301+func (_c *MockServerConnection_SendOutput_Call) Run(run func(output protocol.Output)) *MockServerConnection_SendOutput_Call {
302+ _c.Call.Run(func(args mock.Arguments) {
303+ var arg0 protocol.Output
304+ if args[0] != nil {
305+ arg0 = args[0].(protocol.Output)
306+ }
307+ run(
308+ arg0,
309+ )
310+ })
311+ return _c
312+}
313+
314+func (_c *MockServerConnection_SendOutput_Call) Return(err error) *MockServerConnection_SendOutput_Call {
315+ _c.Call.Return(err)
316+ return _c
317+}
318+
319+func (_c *MockServerConnection_SendOutput_Call) RunAndReturn(run func(output protocol.Output) error) *MockServerConnection_SendOutput_Call {
320+ _c.Call.Return(run)
321+ return _c
322+}
323+
324+// SendOutputAsync provides a mock function for the type MockServerConnection
325+func (_mock *MockServerConnection) SendOutputAsync(output protocol.Output) error {
326+ ret := _mock.Called(output)
327+
328+ if len(ret) == 0 {
329+ panic("no return value specified for SendOutputAsync")
330+ }
331+
332+ var r0 error
333+ if returnFunc, ok := ret.Get(0).(func(protocol.Output) error); ok {
334+ r0 = returnFunc(output)
335+ } else {
336+ r0 = ret.Error(0)
337+ }
338+ return r0
339+}
340+
341+// MockServerConnection_SendOutputAsync_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendOutputAsync'
342+type MockServerConnection_SendOutputAsync_Call struct {
343+ *mock.Call
344+}
345+
346+// SendOutputAsync is a helper method to define mock.On call
347+// - output protocol.Output
348+func (_e *MockServerConnection_Expecter) SendOutputAsync(output any) *MockServerConnection_SendOutputAsync_Call {
349+ return &MockServerConnection_SendOutputAsync_Call{Call: _e.mock.On("SendOutputAsync", output)}
350+}
351+
352+func (_c *MockServerConnection_SendOutputAsync_Call) Run(run func(output protocol.Output)) *MockServerConnection_SendOutputAsync_Call {
353+ _c.Call.Run(func(args mock.Arguments) {
354+ var arg0 protocol.Output
355+ if args[0] != nil {
356+ arg0 = args[0].(protocol.Output)
357+ }
358+ run(
359+ arg0,
360+ )
361+ })
362+ return _c
363+}
364+
365+func (_c *MockServerConnection_SendOutputAsync_Call) Return(err error) *MockServerConnection_SendOutputAsync_Call {
366+ _c.Call.Return(err)
367+ return _c
368+}
369+
370+func (_c *MockServerConnection_SendOutputAsync_Call) RunAndReturn(run func(output protocol.Output) error) *MockServerConnection_SendOutputAsync_Call {
371+ _c.Call.Return(run)
372+ return _c
373+}
374+
375+// SendOutputSynchronous provides a mock function for the type MockServerConnection
376+func (_mock *MockServerConnection) SendOutputSynchronous(output protocol.Output) error {
377+ ret := _mock.Called(output)
378+
379+ if len(ret) == 0 {
380+ panic("no return value specified for SendOutputSynchronous")
381+ }
382+
383+ var r0 error
384+ if returnFunc, ok := ret.Get(0).(func(protocol.Output) error); ok {
385+ r0 = returnFunc(output)
386+ } else {
387+ r0 = ret.Error(0)
388+ }
389+ return r0
390+}
391+
392+// MockServerConnection_SendOutputSynchronous_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendOutputSynchronous'
393+type MockServerConnection_SendOutputSynchronous_Call struct {
394+ *mock.Call
395+}
396+
397+// SendOutputSynchronous is a helper method to define mock.On call
398+// - output protocol.Output
399+func (_e *MockServerConnection_Expecter) SendOutputSynchronous(output any) *MockServerConnection_SendOutputSynchronous_Call {
400+ return &MockServerConnection_SendOutputSynchronous_Call{Call: _e.mock.On("SendOutputSynchronous", output)}
401+}
402+
403+func (_c *MockServerConnection_SendOutputSynchronous_Call) Run(run func(output protocol.Output)) *MockServerConnection_SendOutputSynchronous_Call {
404+ _c.Call.Run(func(args mock.Arguments) {
405+ var arg0 protocol.Output
406+ if args[0] != nil {
407+ arg0 = args[0].(protocol.Output)
408+ }
409+ run(
410+ arg0,
411+ )
412+ })
413+ return _c
414+}
415+
416+func (_c *MockServerConnection_SendOutputSynchronous_Call) Return(err error) *MockServerConnection_SendOutputSynchronous_Call {
417+ _c.Call.Return(err)
418+ return _c
419+}
420+
421+func (_c *MockServerConnection_SendOutputSynchronous_Call) RunAndReturn(run func(output protocol.Output) error) *MockServerConnection_SendOutputSynchronous_Call {
422+ _c.Call.Return(run)
423+ return _c
424+}
425+
426+// SendServer provides a mock function for the type MockServerConnection
427+func (_mock *MockServerConnection) SendServer(serverMessage protocol.ServerMessage) error {
428+ ret := _mock.Called(serverMessage)
429+
430+ if len(ret) == 0 {
431+ panic("no return value specified for SendServer")
432+ }
433+
434+ var r0 error
435+ if returnFunc, ok := ret.Get(0).(func(protocol.ServerMessage) error); ok {
436+ r0 = returnFunc(serverMessage)
437+ } else {
438+ r0 = ret.Error(0)
439+ }
440+ return r0
441+}
442+
443+// MockServerConnection_SendServer_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendServer'
444+type MockServerConnection_SendServer_Call struct {
445+ *mock.Call
446+}
447+
448+// SendServer is a helper method to define mock.On call
449+// - serverMessage protocol.ServerMessage
450+func (_e *MockServerConnection_Expecter) SendServer(serverMessage any) *MockServerConnection_SendServer_Call {
451+ return &MockServerConnection_SendServer_Call{Call: _e.mock.On("SendServer", serverMessage)}
452+}
453+
454+func (_c *MockServerConnection_SendServer_Call) Run(run func(serverMessage protocol.ServerMessage)) *MockServerConnection_SendServer_Call {
455+ _c.Call.Run(func(args mock.Arguments) {
456+ var arg0 protocol.ServerMessage
457+ if args[0] != nil {
458+ arg0 = args[0].(protocol.ServerMessage)
459+ }
460+ run(
461+ arg0,
462+ )
463+ })
464+ return _c
465+}
466+
467+func (_c *MockServerConnection_SendServer_Call) Return(err error) *MockServerConnection_SendServer_Call {
468+ _c.Call.Return(err)
469+ return _c
470+}
471+
472+func (_c *MockServerConnection_SendServer_Call) RunAndReturn(run func(serverMessage protocol.ServerMessage) error) *MockServerConnection_SendServer_Call {
473+ _c.Call.Return(run)
474+ return _c
475+}
476+
477+// SendServerAsync provides a mock function for the type MockServerConnection
478+func (_mock *MockServerConnection) SendServerAsync(serverMessage protocol.ServerMessage) error {
479+ ret := _mock.Called(serverMessage)
480+
481+ if len(ret) == 0 {
482+ panic("no return value specified for SendServerAsync")
483+ }
484+
485+ var r0 error
486+ if returnFunc, ok := ret.Get(0).(func(protocol.ServerMessage) error); ok {
487+ r0 = returnFunc(serverMessage)
488+ } else {
489+ r0 = ret.Error(0)
490+ }
491+ return r0
492+}
493+
494+// MockServerConnection_SendServerAsync_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendServerAsync'
495+type MockServerConnection_SendServerAsync_Call struct {
496+ *mock.Call
497+}
498+
499+// SendServerAsync is a helper method to define mock.On call
500+// - serverMessage protocol.ServerMessage
501+func (_e *MockServerConnection_Expecter) SendServerAsync(serverMessage any) *MockServerConnection_SendServerAsync_Call {
502+ return &MockServerConnection_SendServerAsync_Call{Call: _e.mock.On("SendServerAsync", serverMessage)}
503+}
504+
505+func (_c *MockServerConnection_SendServerAsync_Call) Run(run func(serverMessage protocol.ServerMessage)) *MockServerConnection_SendServerAsync_Call {
506+ _c.Call.Run(func(args mock.Arguments) {
507+ var arg0 protocol.ServerMessage
508+ if args[0] != nil {
509+ arg0 = args[0].(protocol.ServerMessage)
510+ }
511+ run(
512+ arg0,
513+ )
514+ })
515+ return _c
516+}
517+
518+func (_c *MockServerConnection_SendServerAsync_Call) Return(err error) *MockServerConnection_SendServerAsync_Call {
519+ _c.Call.Return(err)
520+ return _c
521+}
522+
523+func (_c *MockServerConnection_SendServerAsync_Call) RunAndReturn(run func(serverMessage protocol.ServerMessage) error) *MockServerConnection_SendServerAsync_Call {
524+ _c.Call.Return(run)
525+ return _c
526+}
527+
528+// SendServerSynchronous provides a mock function for the type MockServerConnection
529+func (_mock *MockServerConnection) SendServerSynchronous(serverMessage protocol.ServerMessage) error {
530+ ret := _mock.Called(serverMessage)
531+
532+ if len(ret) == 0 {
533+ panic("no return value specified for SendServerSynchronous")
534+ }
535+
536+ var r0 error
537+ if returnFunc, ok := ret.Get(0).(func(protocol.ServerMessage) error); ok {
538+ r0 = returnFunc(serverMessage)
539+ } else {
540+ r0 = ret.Error(0)
541+ }
542+ return r0
543+}
544+
545+// MockServerConnection_SendServerSynchronous_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendServerSynchronous'
546+type MockServerConnection_SendServerSynchronous_Call struct {
547+ *mock.Call
548+}
549+
550+// SendServerSynchronous is a helper method to define mock.On call
551+// - serverMessage protocol.ServerMessage
552+func (_e *MockServerConnection_Expecter) SendServerSynchronous(serverMessage any) *MockServerConnection_SendServerSynchronous_Call {
553+ return &MockServerConnection_SendServerSynchronous_Call{Call: _e.mock.On("SendServerSynchronous", serverMessage)}
554+}
555+
556+func (_c *MockServerConnection_SendServerSynchronous_Call) Run(run func(serverMessage protocol.ServerMessage)) *MockServerConnection_SendServerSynchronous_Call {
557+ _c.Call.Run(func(args mock.Arguments) {
558+ var arg0 protocol.ServerMessage
559+ if args[0] != nil {
560+ arg0 = args[0].(protocol.ServerMessage)
561+ }
562+ run(
563+ arg0,
564+ )
565+ })
566+ return _c
567+}
568+
569+func (_c *MockServerConnection_SendServerSynchronous_Call) Return(err error) *MockServerConnection_SendServerSynchronous_Call {
570+ _c.Call.Return(err)
571+ return _c
572+}
573+
574+func (_c *MockServerConnection_SendServerSynchronous_Call) RunAndReturn(run func(serverMessage protocol.ServerMessage) error) *MockServerConnection_SendServerSynchronous_Call {
575+ _c.Call.Return(run)
576+ return _c
577+}
578+
579+// NewMockServerListener creates a new instance of MockServerListener. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
580+// The first argument is typically a *testing.T value.
581+func NewMockServerListener(t interface {
582+ mock.TestingT
583+ Cleanup(func())
584+}) *MockServerListener {
585+ mock := &MockServerListener{}
586+ mock.Mock.Test(t)
587+
588+ t.Cleanup(func() { mock.AssertExpectations(t) })
589+
590+ return mock
591+}
592+
593+// MockServerListener is an autogenerated mock type for the ServerListener type
594+type MockServerListener struct {
595+ mock.Mock
596+}
597+
598+type MockServerListener_Expecter struct {
599+ mock *mock.Mock
600+}
601+
602+func (_m *MockServerListener) EXPECT() *MockServerListener_Expecter {
603+ return &MockServerListener_Expecter{mock: &_m.Mock}
604+}
605+
606+// Accept provides a mock function for the type MockServerListener
607+func (_mock *MockServerListener) Accept() (ports.ServerConnection, error) {
608+ ret := _mock.Called()
609+
610+ if len(ret) == 0 {
611+ panic("no return value specified for Accept")
612+ }
613+
614+ var r0 ports.ServerConnection
615+ var r1 error
616+ if returnFunc, ok := ret.Get(0).(func() (ports.ServerConnection, error)); ok {
617+ return returnFunc()
618+ }
619+ if returnFunc, ok := ret.Get(0).(func() ports.ServerConnection); ok {
620+ r0 = returnFunc()
621+ } else {
622+ if ret.Get(0) != nil {
623+ r0 = ret.Get(0).(ports.ServerConnection)
624+ }
625+ }
626+ if returnFunc, ok := ret.Get(1).(func() error); ok {
627+ r1 = returnFunc()
628+ } else {
629+ r1 = ret.Error(1)
630+ }
631+ return r0, r1
632+}
633+
634+// MockServerListener_Accept_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Accept'
635+type MockServerListener_Accept_Call struct {
636+ *mock.Call
637+}
638+
639+// Accept is a helper method to define mock.On call
640+func (_e *MockServerListener_Expecter) Accept() *MockServerListener_Accept_Call {
641+ return &MockServerListener_Accept_Call{Call: _e.mock.On("Accept")}
642+}
643+
644+func (_c *MockServerListener_Accept_Call) Run(run func()) *MockServerListener_Accept_Call {
645+ _c.Call.Run(func(args mock.Arguments) {
646+ run()
647+ })
648+ return _c
649+}
650+
651+func (_c *MockServerListener_Accept_Call) Return(serverConnection ports.ServerConnection, err error) *MockServerListener_Accept_Call {
652+ _c.Call.Return(serverConnection, err)
653+ return _c
654+}
655+
656+func (_c *MockServerListener_Accept_Call) RunAndReturn(run func() (ports.ServerConnection, error)) *MockServerListener_Accept_Call {
657+ _c.Call.Return(run)
658+ return _c
659+}
660+
661+// Addr provides a mock function for the type MockServerListener
662+func (_mock *MockServerListener) Addr() string {
663+ ret := _mock.Called()
664+
665+ if len(ret) == 0 {
666+ panic("no return value specified for Addr")
667+ }
668+
669+ var r0 string
670+ if returnFunc, ok := ret.Get(0).(func() string); ok {
671+ r0 = returnFunc()
672+ } else {
673+ r0 = ret.Get(0).(string)
674+ }
675+ return r0
676+}
677+
678+// MockServerListener_Addr_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Addr'
679+type MockServerListener_Addr_Call struct {
680+ *mock.Call
681+}
682+
683+// Addr is a helper method to define mock.On call
684+func (_e *MockServerListener_Expecter) Addr() *MockServerListener_Addr_Call {
685+ return &MockServerListener_Addr_Call{Call: _e.mock.On("Addr")}
686+}
687+
688+func (_c *MockServerListener_Addr_Call) Run(run func()) *MockServerListener_Addr_Call {
689+ _c.Call.Run(func(args mock.Arguments) {
690+ run()
691+ })
692+ return _c
693+}
694+
695+func (_c *MockServerListener_Addr_Call) Return(s string) *MockServerListener_Addr_Call {
696+ _c.Call.Return(s)
697+ return _c
698+}
699+
700+func (_c *MockServerListener_Addr_Call) RunAndReturn(run func() string) *MockServerListener_Addr_Call {
701+ _c.Call.Return(run)
702+ return _c
703+}
704+
705+// Close provides a mock function for the type MockServerListener
706+func (_mock *MockServerListener) Close() error {
707+ ret := _mock.Called()
708+
709+ if len(ret) == 0 {
710+ panic("no return value specified for Close")
711+ }
712+
713+ var r0 error
714+ if returnFunc, ok := ret.Get(0).(func() error); ok {
715+ r0 = returnFunc()
716+ } else {
717+ r0 = ret.Error(0)
718+ }
719+ return r0
720+}
721+
722+// MockServerListener_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close'
723+type MockServerListener_Close_Call struct {
724+ *mock.Call
725+}
726+
727+// Close is a helper method to define mock.On call
728+func (_e *MockServerListener_Expecter) Close() *MockServerListener_Close_Call {
729+ return &MockServerListener_Close_Call{Call: _e.mock.On("Close")}
730+}
731+
732+func (_c *MockServerListener_Close_Call) Run(run func()) *MockServerListener_Close_Call {
733+ _c.Call.Run(func(args mock.Arguments) {
734+ run()
735+ })
736+ return _c
737+}
738+
739+func (_c *MockServerListener_Close_Call) Return(err error) *MockServerListener_Close_Call {
740+ _c.Call.Return(err)
741+ return _c
742+}
743+
744+func (_c *MockServerListener_Close_Call) RunAndReturn(run func() error) *MockServerListener_Close_Call {
745+ _c.Call.Return(run)
746+ return _c
747+}
A internal/ports/session.go +26 -0
@@ -0,0 +1,26 @@
1+package ports
2+
3+import "github.com/bnema/vev/internal/protocol"
4+
5+// ServerConnection is one typed client/daemon session channel. Close must be
6+// concurrent-safe and unblock ReceiveClient and every send mode.
7+type ServerConnection interface {
8+ ReceiveClient() (protocol.ClientMessage, error)
9+ SendServer(protocol.ServerMessage) error
10+ SendServerAsync(protocol.ServerMessage) error
11+ SendServerSynchronous(protocol.ServerMessage) error
12+ SendOutput(protocol.Output) error
13+ SendOutputAsync(protocol.Output) error
14+ SendOutputSynchronous(protocol.Output) error
15+ Capabilities() protocol.ConnectionCapabilities
16+ LinkState() LinkState
17+ LinkEvents() <-chan LinkEvent
18+ Close() error
19+}
20+
21+// ServerListener accepts typed server-side session connections.
22+type ServerListener interface {
23+ Accept() (ServerConnection, error)
24+ Close() error
25+ Addr() string
26+}
A internal/protocol/decode.go +48 -0
@@ -0,0 +1,48 @@
1+package protocol
2+
3+import "fmt"
4+
5+// DecodeCategory classifies encoded input rejected before it reaches a use case.
6+type DecodeCategory uint8
7+
8+const (
9+ DecodeMalformed DecodeCategory = iota + 1
10+ DecodeUnknownType
11+ DecodeWrongDirection
12+)
13+
14+// DecodeMessageKind identifies message families whose malformed prefixes need
15+// compatibility responses before a typed message can be produced.
16+type DecodeMessageKind uint8
17+
18+const (
19+ DecodeMessageUnknown DecodeMessageKind = iota
20+ DecodeMessageHello
21+ DecodeMessageCommand
22+ DecodeMessageKill
23+ DecodeMessageRemotePreview
24+)
25+
26+// DecodeFailure describes rejected encoded input without retaining payload bytes.
27+type DecodeFailure struct {
28+ Category DecodeCategory
29+ Kind DecodeMessageKind
30+ Type uint8
31+ Version uint16
32+ RequestID uint64
33+ Err error
34+}
35+
36+func (e *DecodeFailure) Error() string {
37+ if e == nil {
38+ return "protocol: decode failure"
39+ }
40+ return fmt.Sprintf("protocol: decode message type %d: %v", e.Type, e.Err)
41+}
42+
43+func (e *DecodeFailure) Unwrap() error {
44+ if e == nil {
45+ return nil
46+ }
47+ return e.Err
48+}
A internal/protocol/messages.go +44 -0
@@ -0,0 +1,44 @@
1+package protocol
2+
3+// ClientMessage is a closed set of messages accepted from a session client.
4+type ClientMessage interface{ clientMessage() }
5+
6+// ServerMessage is a closed set of messages emitted by a session daemon.
7+type ServerMessage interface{ serverMessage() }
8+
9+func (Hello) clientMessage() {}
10+func (Input) clientMessage() {}
11+func (Resize) clientMessage() {}
12+func (Detach) clientMessage() {}
13+func (Ping) clientMessage() {}
14+func (List) clientMessage() {}
15+func (Kill) clientMessage() {}
16+func (Theme) clientMessage() {}
17+func (Ack) clientMessage() {}
18+func (ImagePush) clientMessage() {}
19+func (ClientNotice) clientMessage() {}
20+func (CommandRequest) clientMessage() {}
21+func (OutputResetRequest) clientMessage() {}
22+func (RemotePreviewRequest) clientMessage() {}
23+func (RouteAttentionSubscription) clientMessage() {}
24+func (SamePeerSwitchRequest) clientMessage() {}
25+func (ParkedRouteRequest) clientMessage() {}
26+func (RecentRouteSnapshot) clientMessage() {}
27+func (RouteNavigationFailure) clientMessage() {}
28+
29+func (Welcome) serverMessage() {}
30+func (ErrorMsg) serverMessage() {}
31+func (Output) serverMessage() {}
32+func (Detached) serverMessage() {}
33+func (Pong) serverMessage() {}
34+func (Sessions) serverMessage() {}
35+func (CommandResult) serverMessage() {}
36+func (NavigationDirective) serverMessage() {}
37+func (AttachTarget) serverMessage() {}
38+func (RemotePreview) serverMessage() {}
39+func (CommittedRouteIdentity) serverMessage() {}
40+func (RouteNavigationAction) serverMessage() {}
41+func (RouteNavigationFailure) serverMessage() {}
42+func (RoutePosition) serverMessage() {}
43+func (SamePeerSwitchFailure) serverMessage() {}
44+func (ParkedRouteResponse) serverMessage() {}
M internal/protocol/session.go +3 -0
@@ -23,6 +23,9 @@ const HandshakeTimeout = 15 * time.Second
23 // output publication in protocol version 37.23 // output publication in protocol version 37.
24 const MaxOutputDataLen = (16 << 20) - 5524 const MaxOutputDataLen = (16 << 20) - 55
25 25
26+// MaxOutputWindow caps output states sent before client acknowledgement.
27+const MaxOutputWindow = 8
28+
26 // ConnectionCapabilities describes transport behavior relevant to session29 // ConnectionCapabilities describes transport behavior relevant to session
27 // flow without naming a concrete carriage.30 // flow without naming a concrete carriage.
28 type ConnectionCapabilities struct {31 type ConnectionCapabilities struct {
M internal/usecase/daemon/attachment_connection.go +2 -2
@@ -16,7 +16,7 @@ type attachmentCapability struct {
16 rebase bool16 rebase bool
17 }17 }
18 18
19-func captureAttachmentCapability(entry *session, ac *attachedClient, tr ports.Transport) attachmentCapability {19+func captureAttachmentCapability(entry *session, ac *attachedClient, tr ports.ServerConnection) attachmentCapability {
20 if entry == nil || entry.core() == nil || ac == nil || tr == nil {20 if entry == nil || entry.core() == nil || ac == nil || tr == nil {
21 return attachmentCapability{}21 return attachmentCapability{}
22 }22 }
@@ -54,7 +54,7 @@ func captureAttachmentCapability(entry *session, ac *attachedClient, tr ports.Tr
54 return token54 return token
55 }55 }
56 56
57-func (s *session) captureAttachmentCapability(ac *attachedClient, tr ports.Transport) attachmentCapability {57+func (s *session) captureAttachmentCapability(ac *attachedClient, tr ports.ServerConnection) attachmentCapability {
58 return captureAttachmentCapability(s, ac, tr)58 return captureAttachmentCapability(s, ac, tr)
59 }59 }
60 60
M internal/usecase/daemon/attachment_effect_gate.go +5 -5
@@ -6,7 +6,7 @@ import (
6 "sync/atomic"6 "sync/atomic"
7 7
8 "github.com/bnema/vev/internal/ports"8 "github.com/bnema/vev/internal/ports"
9- "github.com/bnema/vev/internal/protocol/wire"9+ "github.com/bnema/vev/internal/protocol"
10 )10 )
11 11
12 type attachmentEffectPhase uint812 type attachmentEffectPhase uint8
@@ -189,7 +189,7 @@ func (t *attachmentEffect) endTransportSend() {
189 g.mu.Unlock()189 g.mu.Unlock()
190 }190 }
191 191
192-func (t *attachmentEffect) sendControl(frame wire.Frame) error {192+func (t *attachmentEffect) sendControl(message protocol.ServerMessage) error {
193 if !t.current() || t.ac == nil || t.transport.transport == nil {193 if !t.current() || t.ac == nil || t.transport.transport == nil {
194 return errAttachmentTransition194 return errAttachmentTransition
195 }195 }
@@ -198,7 +198,7 @@ func (t *attachmentEffect) sendControl(frame wire.Frame) error {
198 if !t.current() || !t.ac.transportSnapshotCurrent(t.transport) || !t.beginTransportSend(t.transport) {198 if !t.current() || !t.ac.transportSnapshotCurrent(t.transport) || !t.beginTransportSend(t.transport) {
199 return errAttachmentTransition199 return errAttachmentTransition
200 }200 }
201- err := t.transport.transport.Send(frame)201+ err := t.transport.transport.SendServer(message)
202 if err != nil {202 if err != nil {
203 t.reportTransportFailure(t.transport)203 t.reportTransportFailure(t.transport)
204 }204 }
@@ -229,12 +229,12 @@ func (ac *attachedClient) beginAttachmentEffect(token attachmentCapability) (*at
229 // admits the capability derived from the session registry. Handshakes use it229 // admits the capability derived from the session registry. Handshakes use it
230 // after Welcome has completed so a replacement blocked behind that send can230 // after Welcome has completed so a replacement blocked behind that send can
231 // publish before readiness or first paint.231 // publish before readiness or first paint.
232-func (ac *attachedClient) beginCurrentAttachmentEffect(sess *session, tr ports.Transport) (attachmentCapability, *attachmentEffect, bool) {232+func (ac *attachedClient) beginCurrentAttachmentEffect(sess *session, tr ports.ServerConnection) (attachmentCapability, *attachmentEffect, bool) {
233 //nolint:contextcheck // This compatibility wrapper deliberately delegates with a fresh context; cancellable callers use the context variant below.233 //nolint:contextcheck // This compatibility wrapper deliberately delegates with a fresh context; cancellable callers use the context variant below.
234 return ac.beginCurrentAttachmentEffectContext(context.Background(), sess, tr)234 return ac.beginCurrentAttachmentEffectContext(context.Background(), sess, tr)
235 }235 }
236 236
237-func (ac *attachedClient) beginCurrentAttachmentEffectContext(ctx context.Context, sess *session, tr ports.Transport) (attachmentCapability, *attachmentEffect, bool) {237+func (ac *attachedClient) beginCurrentAttachmentEffectContext(ctx context.Context, sess *session, tr ports.ServerConnection) (attachmentCapability, *attachmentEffect, bool) {
238 if ctx == nil || ac == nil || sess == nil || tr == nil {238 if ctx == nil || ac == nil || sess == nil || tr == nil {
239 return attachmentCapability{}, nil, false239 return attachmentCapability{}, nil, false
240 }240 }
M internal/usecase/daemon/attachment_effect_gate_test.go +1 -1
@@ -127,7 +127,7 @@ func startBlockedAttachmentSend(t *testing.T, token attachmentCapability) <-chan
127 go func() {127 go func() {
128 defer close(done)128 defer close(done)
129 defer ticket.End()129 defer ticket.End()
130- _ = token.ac.sendExpectedTransportForAttachment(token.transport, framePong(), ticket)130+ _ = token.ac.sendExpectedTransportForAttachment(token.transport, serverPong(), ticket)
131 }()131 }()
132 return done132 return done
133 }133 }
M internal/usecase/daemon/attachment_output.go +20 -21
@@ -10,7 +10,6 @@ import (
10 "github.com/bnema/vev/internal/domain"10 "github.com/bnema/vev/internal/domain"
11 "github.com/bnema/vev/internal/ports"11 "github.com/bnema/vev/internal/ports"
12 "github.com/bnema/vev/internal/protocol"12 "github.com/bnema/vev/internal/protocol"
13- "github.com/bnema/vev/internal/protocol/wire"
14 )13 )
15 14
16 // attachmentOutput owns the terminal-output dependency chain and emitted15 // attachmentOutput owns the terminal-output dependency chain and emitted
@@ -154,18 +153,18 @@ func (o *attachmentOutput) prepareFrame(d *Daemon, state *capturedRenderState, f
154 return &preparedAttachmentOutput{output: o, ansi: ansi, graphics: preparedGraphics, cursor: cursor, data: data}, nil153 return &preparedAttachmentOutput{output: o, ansi: ansi, graphics: preparedGraphics, cursor: cursor, data: data}, nil
155 }154 }
156 155
157-func (p *preparedAttachmentOutput) send(echoAck uint64, send func(wire.Frame) error) error {156+func (p *preparedAttachmentOutput) send(echoAck uint64, send func(protocol.Output) error) error {
158 if p == nil || p.ansi == nil {157 if p == nil || p.ansi == nil {
159 return nil158 return nil
160 }159 }
161- sendFrame := send160+ sendOutput := send
162 if p.graphics != nil {161 if p.graphics != nil {
163- sendFrame = func(frame wire.Frame) error {162+ sendOutput = func(output protocol.Output) error {
164 p.graphics.markSendAttempted()163 p.graphics.markSendAttempted()
165- return send(frame)164+ return send(output)
166 }165 }
167 }166 }
168- if err := p.ansi.send(p.data, echoAck, sendFrame); err != nil {167+ if err := p.ansi.send(p.data, echoAck, sendOutput); err != nil {
169 p.abortGraphics()168 p.abortGraphics()
170 return err169 return err
171 }170 }
@@ -286,7 +285,7 @@ func (s *attachmentOutput) prepare(frame renderer.Frame, damage []renderer.Damag
286 }, nil285 }, nil
287 }286 }
288 287
289-func (p *preparedOutput) send(data []byte, echoAck uint64, send func(wire.Frame) error) error {288+func (p *preparedOutput) send(data []byte, echoAck uint64, send func(protocol.Output) error) error {
290 if p == nil || p.stream == nil || p.attempted {289 if p == nil || p.stream == nil || p.attempted {
291 return nil290 return nil
292 }291 }
@@ -380,30 +379,30 @@ func (p *preparedOutput) currentLocked() bool {
380 (p.stream.attachment == nil || p.stream.attachment.lifecycle.generationValue() == p.connectionGeneration)379 (p.stream.attachment == nil || p.stream.attachment.lifecycle.generationValue() == p.connectionGeneration)
381 }380 }
382 381
383-func marshalOutputState(data []byte, epoch, base, next, echoAck, viewRevision uint64, size domain.Size, full bool) (wire.Frame, error) {382+func marshalOutputState(data []byte, epoch, base, next, echoAck, viewRevision uint64, size domain.Size, full bool) (protocol.Output, error) {
384- payload, err := wire.MarshalOutput(protocol.Output{383+ output := protocol.Output{
385 Epoch: epoch, Base: base, New: next, Echo: echoAck, ViewRevision: viewRevision,384 Epoch: epoch, Base: base, New: next, Echo: echoAck, ViewRevision: viewRevision,
386 Size: size, Full: full, Data: data,385 Size: size, Full: full, Data: data,
387- })
388- if err != nil {
389- return wire.Frame{}, err
390 }386 }
391- return wire.Frame{Type: wire.MsgOutput, Payload: payload}, nil387+ if err := protocol.ValidateOutput(output); err != nil {
388+ return protocol.Output{}, err
389+ }
390+ return output, nil
392 }391 }
393 392
394-// sideEffect builds an output frame without advancing the state stream. When393+// sideEffect builds output without advancing the state stream. When an
395-// an attachment is present, callers must hold its sendMu; attachment-bound394+// attachment is present, callers must hold its sendMu; attachment-bound sends
396-// sends use sideEffectLocked after validating their transport incarnation.395+// use sideEffectLocked after validating their connection incarnation.
397-func (s *attachmentOutput) sideEffect(data []byte, echoAck uint64) (wire.Frame, error) {396+func (s *attachmentOutput) sideEffect(data []byte, echoAck uint64) (protocol.Output, error) {
398 s.lockView()397 s.lockView()
399 defer s.unlockView()398 defer s.unlockView()
400 return s.sideEffectLocked(data, echoAck)399 return s.sideEffectLocked(data, echoAck)
401 }400 }
402 401
403-// sideEffectLocked requires the attachment view lock for both frame creation402+// sideEffectLocked requires the attachment view lock for both output creation
404-// and transport emission. Keeping that lock across the send prevents a view403+// and connection emission. Keeping that lock across the send prevents a view
405-// publication from overtaking a frame that was fenced just before it.404+// publication from overtaking output fenced just before it.
406-func (s *attachmentOutput) sideEffectLocked(data []byte, echoAck uint64) (wire.Frame, error) {405+func (s *attachmentOutput) sideEffectLocked(data []byte, echoAck uint64) (protocol.Output, error) {
407 _, epoch, viewRevision := s.fenceLocked()406 _, epoch, viewRevision := s.fenceLocked()
408 size := domain.Size{Cols: 1, Rows: 1}407 size := domain.Size{Cols: 1, Rows: 1}
409 if s != nil && s.attachment != nil {408 if s != nil && s.attachment != nil {
M internal/usecase/daemon/attachment_output_test.go +29 -27
@@ -92,11 +92,11 @@ func TestAttachmentOutputCapacityProbeDoesNotRaceWithSend(t *testing.T) {
92 release := make(chan struct{})92 release := make(chan struct{})
93 sendDone := make(chan error, 1)93 sendDone := make(chan error, 1)
94 go func() {94 go func() {
95- sendDone <- prepared.send(prepared.data, 0, func(wire.Frame) error {95+ sendDone <- prepared.send(prepared.data, 0, outputFrameSender(func(wire.Frame) error {
96 close(entered)96 close(entered)
97 <-release97 <-release
98 return nil98 return nil
99- })99+ }))
100 }()100 }()
101 <-entered101 <-entered
102 102
@@ -134,10 +134,10 @@ func TestAttachmentOutputEpochsAreAttachmentLocalAndPreparedFramesAreFenced(t *t
134 firstPrepared, err := first.prepare(frame, nil, true)134 firstPrepared, err := first.prepare(frame, nil, true)
135 require.NoError(t, err)135 require.NoError(t, err)
136 var firstFrame wire.Frame136 var firstFrame wire.Frame
137- require.NoError(t, firstPrepared.send(firstPrepared.data, 0, func(frame wire.Frame) error {137+ require.NoError(t, firstPrepared.send(firstPrepared.data, 0, outputFrameSender(func(frame wire.Frame) error {
138 firstFrame = frame138 firstFrame = frame
139 return nil139 return nil
140- }))140+ })))
141 firstOutput, err := wire.UnmarshalOutput(firstFrame.Payload)141 firstOutput, err := wire.UnmarshalOutput(firstFrame.Payload)
142 require.NoError(t, err)142 require.NoError(t, err)
143 require.Equal(t, uint64(1), firstOutput.Epoch)143 require.Equal(t, uint64(1), firstOutput.Epoch)
@@ -147,7 +147,7 @@ func TestAttachmentOutputEpochsAreAttachmentLocalAndPreparedFramesAreFenced(t *t
147 147
148 secondPrepared, err := second.prepare(frame, nil, true)148 secondPrepared, err := second.prepare(frame, nil, true)
149 require.NoError(t, err)149 require.NoError(t, err)
150- require.NoError(t, secondPrepared.send(secondPrepared.data, 0, func(wire.Frame) error { return nil }))150+ require.NoError(t, secondPrepared.send(secondPrepared.data, 0, outputFrameSender(func(wire.Frame) error { return nil })))
151 require.Equal(t, uint64(1), second.epoch)151 require.Equal(t, uint64(1), second.epoch)
152 require.Equal(t, uint64(1), second.next)152 require.Equal(t, uint64(1), second.next)
153 require.Equal(t, uint64(1), first.next)153 require.Equal(t, uint64(1), first.next)
@@ -161,10 +161,10 @@ func TestAttachmentOutputEpochsAreAttachmentLocalAndPreparedFramesAreFenced(t *t
161 require.Zero(t, first.next)161 require.Zero(t, first.next)
162 require.Zero(t, first.acked)162 require.Zero(t, first.acked)
163 var staleSent bool163 var staleSent bool
164- require.NoError(t, pending.send(pending.data, 0, func(wire.Frame) error {164+ require.NoError(t, pending.send(pending.data, 0, outputFrameSender(func(wire.Frame) error {
165 staleSent = true165 staleSent = true
166 return nil166 return nil
167- }))167+ })))
168 require.False(t, staleSent)168 require.False(t, staleSent)
169 require.Zero(t, first.next)169 require.Zero(t, first.next)
170 first.ack(1, 1)170 first.ack(1, 1)
@@ -173,10 +173,10 @@ func TestAttachmentOutputEpochsAreAttachmentLocalAndPreparedFramesAreFenced(t *t
173 reset, err := first.prepare(changed, []renderer.Damage{{Kind: renderer.DamageText, Width: 1, Height: 1}}, false)173 reset, err := first.prepare(changed, []renderer.Damage{{Kind: renderer.DamageText, Width: 1, Height: 1}}, false)
174 require.NoError(t, err)174 require.NoError(t, err)
175 var resetFrame wire.Frame175 var resetFrame wire.Frame
176- require.NoError(t, reset.send(reset.data, 0, func(frame wire.Frame) error {176+ require.NoError(t, reset.send(reset.data, 0, outputFrameSender(func(frame wire.Frame) error {
177 resetFrame = frame177 resetFrame = frame
178 return nil178 return nil
179- }))179+ })))
180 resetOutput, err := wire.UnmarshalOutput(resetFrame.Payload)180 resetOutput, err := wire.UnmarshalOutput(resetFrame.Payload)
181 require.NoError(t, err)181 require.NoError(t, err)
182 require.Equal(t, uint64(2), resetOutput.Epoch)182 require.Equal(t, uint64(2), resetOutput.Epoch)
@@ -205,23 +205,23 @@ func TestPreparedOutputDropsReplacedConnectionAndView(t *testing.T) {
205 fillOutputStateRows(frame, []string{"abc"})205 fillOutputStateRows(frame, []string{"abc"})
206 initial, err := stream.prepare(frame, nil, true)206 initial, err := stream.prepare(frame, nil, true)
207 require.NoError(t, err)207 require.NoError(t, err)
208- require.NoError(t, initial.send(initial.data, 0, func(wire.Frame) error { return nil }))208+ require.NoError(t, initial.send(initial.data, 0, outputFrameSender(func(wire.Frame) error { return nil })))
209 changed := frame.Clone()209 changed := frame.Clone()
210 changed.Set(0, 0, renderer.Cell{Rune: 'x', Style: renderer.DefaultStyle()})210 changed.Set(0, 0, renderer.Cell{Rune: 'x', Style: renderer.DefaultStyle()})
211 pending, err := stream.prepare(changed, []renderer.Damage{{Kind: renderer.DamageText, Width: 1, Height: 1}}, false)211 pending, err := stream.prepare(changed, []renderer.Damage{{Kind: renderer.DamageText, Width: 1, Height: 1}}, false)
212 require.NoError(t, err)212 require.NoError(t, err)
213 tt.change(ac)213 tt.change(ac)
214 var sent bool214 var sent bool
215- require.NoError(t, pending.send(pending.data, 0, func(wire.Frame) error {215+ require.NoError(t, pending.send(pending.data, 0, outputFrameSender(func(wire.Frame) error {
216 sent = true216 sent = true
217 return nil217 return nil
218- }))218+ })))
219 require.False(t, sent)219 require.False(t, sent)
220 require.Equal(t, uint64(1), stream.next)220 require.Equal(t, uint64(1), stream.next)
221 221
222 retry, err := stream.prepare(changed, []renderer.Damage{{Kind: renderer.DamageText, Width: 1, Height: 1}}, false)222 retry, err := stream.prepare(changed, []renderer.Damage{{Kind: renderer.DamageText, Width: 1, Height: 1}}, false)
223 require.NoError(t, err)223 require.NoError(t, err)
224- require.NoError(t, retry.send(retry.data, 0, func(wire.Frame) error { return nil }))224+ require.NoError(t, retry.send(retry.data, 0, outputFrameSender(func(wire.Frame) error { return nil })))
225 require.Equal(t, uint64(2), stream.next)225 require.Equal(t, uint64(2), stream.next)
226 })226 })
227 }227 }
@@ -242,9 +242,7 @@ func TestOutputStateSideEffectsDoNotAdvanceEpochStateOrACK(t *testing.T) {
242 stream := newOutputStateStream()242 stream := newOutputStateStream()
243 stream.ack(1, 0)243 stream.ack(1, 0)
244 beforeEpoch, beforeNext, beforeAck := stream.epoch, stream.next, stream.acked244 beforeEpoch, beforeNext, beforeAck := stream.epoch, stream.next, stream.acked
245- frame, err := stream.sideEffect([]byte("pty"), 9)245+ out, err := stream.sideEffect([]byte("pty"), 9)
246- require.NoError(t, err)
247- out, err := wire.UnmarshalOutput(frame.Payload)
248 require.NoError(t, err)246 require.NoError(t, err)
249 require.Equal(t, beforeEpoch, out.Epoch)247 require.Equal(t, beforeEpoch, out.Epoch)
250 require.Zero(t, out.Base)248 require.Zero(t, out.Base)
@@ -261,7 +259,7 @@ func TestAttachmentOutputFailedSendRetriesSnapshotWithoutAdvancing(t *testing.T)
261 fillOutputStateRows(initial, []string{"abc"})259 fillOutputStateRows(initial, []string{"abc"})
262 first, err := stream.prepare(initial, nil, true)260 first, err := stream.prepare(initial, nil, true)
263 require.NoError(t, err)261 require.NoError(t, err)
264- require.NoError(t, first.send(first.data, 0, func(wire.Frame) error { return nil }))262+ require.NoError(t, first.send(first.data, 0, outputFrameSender(func(wire.Frame) error { return nil })))
265 require.Equal(t, uint64(1), stream.next)263 require.Equal(t, uint64(1), stream.next)
266 264
267 changed := initial.Clone()265 changed := initial.Clone()
@@ -273,7 +271,7 @@ func TestAttachmentOutputFailedSendRetriesSnapshotWithoutAdvancing(t *testing.T)
273 require.Empty(t, probe.Bytes(), "preparation must not advance the renderer shadow")271 require.Empty(t, probe.Bytes(), "preparation must not advance the renderer shadow")
274 272
275 sendErr := errors.New("send failed")273 sendErr := errors.New("send failed")
276- require.ErrorIs(t, pending.send(pending.data, 0, func(wire.Frame) error { return sendErr }), sendErr)274+ require.ErrorIs(t, pending.send(pending.data, 0, outputFrameSender(func(wire.Frame) error { return sendErr })), sendErr)
277 require.Equal(t, uint64(1), stream.next, "failed send must not advance the state chain")275 require.Equal(t, uint64(1), stream.next, "failed send must not advance the state chain")
278 pending.commitNoSend()276 pending.commitNoSend()
279 probe, err = stream.renderer.Prepare(initial, nil, false)277 probe, err = stream.renderer.Prepare(initial, nil, false)
@@ -283,19 +281,19 @@ func TestAttachmentOutputFailedSendRetriesSnapshotWithoutAdvancing(t *testing.T)
283 retry, err := stream.prepare(changed, []renderer.Damage{{Kind: renderer.DamageText, Width: 1, Height: 1}}, false)281 retry, err := stream.prepare(changed, []renderer.Damage{{Kind: renderer.DamageText, Width: 1, Height: 1}}, false)
284 require.NoError(t, err)282 require.NoError(t, err)
285 var sent wire.Frame283 var sent wire.Frame
286- require.NoError(t, retry.send(retry.data, 0, func(frame wire.Frame) error {284+ require.NoError(t, retry.send(retry.data, 0, outputFrameSender(func(frame wire.Frame) error {
287 sent = frame285 sent = frame
288 return nil286 return nil
289- }))287+ })))
290 out, err := wire.UnmarshalOutput(sent.Payload)288 out, err := wire.UnmarshalOutput(sent.Payload)
291 require.NoError(t, err)289 require.NoError(t, err)
292 require.Zero(t, out.Base, "retry after ambiguous send failure must be dependency-free")290 require.Zero(t, out.Base, "retry after ambiguous send failure must be dependency-free")
293 require.Equal(t, uint64(2), out.New, "state numbers remain monotonic across rebases")291 require.Equal(t, uint64(2), out.New, "state numbers remain monotonic across rebases")
294 require.Equal(t, uint64(2), stream.next, "successful retry advances the chain exactly once")292 require.Equal(t, uint64(2), stream.next, "successful retry advances the chain exactly once")
295- require.NoError(t, retry.send(retry.data, 0, func(wire.Frame) error {293+ require.NoError(t, retry.send(retry.data, 0, outputFrameSender(func(wire.Frame) error {
296 t.Fatal("completed output sent twice")294 t.Fatal("completed output sent twice")
297 return nil295 return nil
298- }))296+ })))
299 require.Equal(t, uint64(2), stream.next)297 require.Equal(t, uint64(2), stream.next)
300 probe, err = stream.renderer.Prepare(changed, nil, false)298 probe, err = stream.renderer.Prepare(changed, nil, false)
301 require.NoError(t, err)299 require.NoError(t, err)
@@ -308,7 +306,7 @@ func TestAttachmentOutputNoByteCommitAdvancesShadowWithoutState(t *testing.T) {
308 fillOutputStateRows(frame, []string{"abc"})306 fillOutputStateRows(frame, []string{"abc"})
309 initial, err := stream.prepare(frame, nil, true)307 initial, err := stream.prepare(frame, nil, true)
310 require.NoError(t, err)308 require.NoError(t, err)
311- require.NoError(t, initial.send(initial.data, 0, func(wire.Frame) error { return nil }))309+ require.NoError(t, initial.send(initial.data, 0, outputFrameSender(func(wire.Frame) error { return nil })))
312 310
313 noOp, err := stream.prepare(frame, nil, false)311 noOp, err := stream.prepare(frame, nil, false)
314 require.NoError(t, err)312 require.NoError(t, err)
@@ -378,7 +376,11 @@ func outputStateFrame(stream *attachmentOutput, data []byte, reset bool, echoAck
378 if reset {376 if reset {
379 base = 0377 base = 0
380 }378 }
381- frame, err := frameOutputState(data, base, stream.next, echoAck)379+ output, err := serverOutputState(data, base, stream.next, echoAck)
380+ if err != nil {
381+ panic(err)
382+ }
383+ frame, err := testServerFrame(output)
382 if err != nil {384 if err != nil {
383 panic(err)385 panic(err)
384 }386 }
@@ -395,10 +397,10 @@ func drawOutputState(t *testing.T, stream *attachmentOutput, frame renderer.Fram
395 return wire.Frame{}, false, err397 return wire.Frame{}, false, err
396 }398 }
397 var output wire.Frame399 var output wire.Frame
398- err = prepared.send(prepared.data, echoAck, func(frame wire.Frame) error {400+ err = prepared.send(prepared.data, echoAck, outputFrameSender(func(frame wire.Frame) error {
399 output = frame401 output = frame
400 return nil402 return nil
401- })403+ }))
402 if err != nil {404 if err != nil {
403 return wire.Frame{}, false, err405 return wire.Frame{}, false, err
404 }406 }
@@ -456,7 +458,7 @@ func TestAttachmentOutputFailedSendKeepsTextCursorAndGraphicsSpeculative(t *test
456 require.NoError(t, err)458 require.NoError(t, err)
457 459
458 sendErr := errors.New("send failed")460 sendErr := errors.New("send failed")
459- require.ErrorIs(t, prepared.send(0, func(wire.Frame) error { return sendErr }), sendErr)461+ require.ErrorIs(t, prepared.send(0, outputFrameSender(func(wire.Frame) error { return sendErr })), sendErr)
460 462
461 require.Equal(t, cursorOut{valid: true, row: 1, col: 1}, output.lastCursor)463 require.Equal(t, cursorOut{valid: true, row: 1, col: 1}, output.lastCursor)
462 require.Empty(t, graphicsState.assets)464 require.Empty(t, graphicsState.assets)
M internal/usecase/daemon/attachment_parking_test.go +2 -2
@@ -142,7 +142,7 @@ func TestSuccessfulResumeRejectsOldCredentialAfterWelcome(t *testing.T) {
142 d.clientGone(sess, ac, oldTransport, false)142 d.clientGone(sess, ac, oldTransport, false)
143 143
144 resumedTransport := &closeTrackingTransport{}144 resumedTransport := &closeTrackingTransport{}
145- d.handleHello(resumedTransport, wire.Frame{Type: wire.MsgHello, Payload: wire.MarshalHello(helloResumeCapable(protocol.IntentResume, "work", oldToken))})145+ d.handleHello(resumedTransport, helloResumeCapable(protocol.IntentResume, "work", oldToken))
146 newToken := ac.resumeToken146 newToken := ac.resumeToken
147 require.NotEqual(t, oldToken, newToken)147 require.NotEqual(t, oldToken, newToken)
148 d.mu.Lock()148 d.mu.Lock()
@@ -166,7 +166,7 @@ func TestFailedResumeHandshakeKeepsParkedCredential(t *testing.T) {
166 d.clientGone(sess, ac, oldTransport, false)166 d.clientGone(sess, ac, oldTransport, false)
167 167
168 failed := &resumeWelcomeFailureTransport{}168 failed := &resumeWelcomeFailureTransport{}
169- d.handleHello(failed, wire.Frame{Type: wire.MsgHello, Payload: wire.MarshalHello(helloResumeCapable(protocol.IntentResume, "work", token))})169+ d.handleHello(failed, helloResumeCapable(protocol.IntentResume, "work", token))
170 require.Nil(t, ac.transport())170 require.Nil(t, ac.transport())
171 d.mu.Lock()171 d.mu.Lock()
172 parked := d.parked[token]172 parked := d.parked[token]
M internal/usecase/daemon/attachment_registry_test.go +2 -7
@@ -5,7 +5,6 @@ import (
5 "testing"5 "testing"
6 6
7 "github.com/bnema/vev/internal/domain"7 "github.com/bnema/vev/internal/domain"
8- "github.com/bnema/vev/internal/protocol/wire"
9 "github.com/stretchr/testify/require"8 "github.com/stretchr/testify/require"
10 )9 )
11 10
@@ -97,9 +96,7 @@ func TestTabForAttachmentRepairRebasesOutputBeforePublishingRevision(t *testing.
97 after := ac.viewSnapshot()96 after := ac.viewSnapshot()
98 require.Greater(t, after.revision, before.revision)97 require.Greater(t, after.revision, before.revision)
99 require.Greater(t, ac.output.currentEpoch(), beforeEpoch)98 require.Greater(t, ac.output.currentEpoch(), beforeEpoch)
100- frame, err := ac.output.sideEffect([]byte("x"), 0)99+ output, err := ac.output.sideEffect([]byte("x"), 0)
101- require.NoError(t, err)
102- output, err := wire.UnmarshalOutput(frame.Payload)
103 require.NoError(t, err)100 require.NoError(t, err)
104 require.Equal(t, after.revision, output.ViewRevision)101 require.Equal(t, after.revision, output.ViewRevision)
105 require.Equal(t, ac.output.currentEpoch(), output.Epoch)102 require.Equal(t, ac.output.currentEpoch(), output.Epoch)
@@ -127,9 +124,7 @@ func TestPrepareRemovedTabViewRebasesOutputBeforeSideEffect(t *testing.T) {
127 require.Equal(t, before.revision+1, after.revision)124 require.Equal(t, before.revision+1, after.revision)
128 require.Greater(t, ac.output.currentEpoch(), beforeEpoch)125 require.Greater(t, ac.output.currentEpoch(), beforeEpoch)
129 126
130- frame, err := ac.output.sideEffect([]byte("replacement"), 0)127+ output, err := ac.output.sideEffect([]byte("replacement"), 0)
131- require.NoError(t, err)
132- output, err := wire.UnmarshalOutput(frame.Payload)
133 require.NoError(t, err)128 require.NoError(t, err)
134 require.Equal(t, after.revision, output.ViewRevision)129 require.Equal(t, after.revision, output.ViewRevision)
135 require.NotEqual(t, beforeEpoch, output.Epoch)130 require.NotEqual(t, beforeEpoch, output.Epoch)
M internal/usecase/daemon/attachment_render_test.go +1 -1
@@ -283,7 +283,7 @@ func TestAttachmentResizeUsesLatestClaimedSessionGeometry(t *testing.T) {
283 secondLease := rc.attachmentLease(second)283 secondLease := rc.attachmentLease(second)
284 require.True(t, rc.markAttachmentReady(firstLease))284 require.True(t, rc.markAttachmentReady(firstLease))
285 require.True(t, rc.markAttachmentReady(secondLease))285 require.True(t, rc.markAttachmentReady(secondLease))
286- resize := func(ac *attachedClient, tr ports.Transport, size domain.Size) {286+ resize := func(ac *attachedClient, tr ports.ServerConnection, size domain.Size) {
287 token := sess.captureAttachmentCapability(ac, tr)287 token := sess.captureAttachmentCapability(ac, tr)
288 d.handleAttachmentClientFrame(token, wire.Frame{288 d.handleAttachmentClientFrame(token, wire.Frame{
289 Type: wire.MsgResize,289 Type: wire.MsgResize,
M internal/usecase/daemon/attention_test.go +1 -2
@@ -13,7 +13,6 @@ import (
13 renderer "github.com/bnema/vev-vt"13 renderer "github.com/bnema/vev-vt"
14 "github.com/bnema/vev/internal/domain"14 "github.com/bnema/vev/internal/domain"
15 "github.com/bnema/vev/internal/ports"15 "github.com/bnema/vev/internal/ports"
16- portsmocks "github.com/bnema/vev/internal/ports/mocks"
17 "github.com/bnema/vev/internal/protocol/wire"16 "github.com/bnema/vev/internal/protocol/wire"
18 "github.com/bnema/vev/internal/usecase/keys"17 "github.com/bnema/vev/internal/usecase/keys"
19 "github.com/bnema/vev/internal/usecase/layout"18 "github.com/bnema/vev/internal/usecase/layout"
@@ -164,7 +163,7 @@ func TestNoteAttentionDoesNotBlockOnWedgedOtherClient(t *testing.T) {
164 defer releases[1]()163 defer releases[1]()
165 selectTestAttachmentTab(sess, 0)164 selectTestAttachmentTab(sess, 0)
166 165
167- trW := portsmocks.NewMockTransport(t)166+ trW := newMockServerConnection(t)
168 block := make(chan struct{})167 block := make(chan struct{})
169 t.Cleanup(func() { close(block) })168 t.Cleanup(func() { close(block) })
170 trW.EXPECT().Send(mock.Anything).RunAndReturn(func(wire.Frame) error {169 trW.EXPECT().Send(mock.Anything).RunAndReturn(func(wire.Frame) error {
M internal/usecase/daemon/client.go +27 -28
@@ -14,14 +14,13 @@ import (
14 "github.com/bnema/vev/internal/domain"14 "github.com/bnema/vev/internal/domain"
15 "github.com/bnema/vev/internal/ports"15 "github.com/bnema/vev/internal/ports"
16 "github.com/bnema/vev/internal/protocol"16 "github.com/bnema/vev/internal/protocol"
17- "github.com/bnema/vev/internal/protocol/wire"
18 "github.com/bnema/vev/internal/usecase/keys"17 "github.com/bnema/vev/internal/usecase/keys"
19 "github.com/bnema/vev/internal/usecase/mouse"18 "github.com/bnema/vev/internal/usecase/mouse"
20 themeui "github.com/bnema/vev/internal/usecase/theme"19 themeui "github.com/bnema/vev/internal/usecase/theme"
21 )20 )
22 21
23 type transportSnapshot struct {22 type transportSnapshot struct {
24- transport ports.Transport23+ transport ports.ServerConnection
25 incarnation uint6424 incarnation uint64
26 }25 }
27 26
@@ -35,7 +34,7 @@ type appliedTheme struct {
35 }34 }
36 35
37 type attachedClient struct {36 type attachedClient struct {
38- tr ports.Transport37+ tr ports.ServerConnection
39 transportIncarnation uint6438 transportIncarnation uint64
40 output *attachmentOutput39 output *attachmentOutput
41 overlays *overlayRuntime40 overlays *overlayRuntime
@@ -274,13 +273,13 @@ func (ac *attachedClient) setClientTheme(t themeui.Theme) {
274 ac.themeMu.Unlock()273 ac.themeMu.Unlock()
275 }274 }
276 275
277-func (ac *attachedClient) transport() ports.Transport {276+func (ac *attachedClient) transport() ports.ServerConnection {
278 ac.linkMu.Lock()277 ac.linkMu.Lock()
279 defer ac.linkMu.Unlock()278 defer ac.linkMu.Unlock()
280 return ac.tr279 return ac.tr
281 }280 }
282 281
283-func (ac *attachedClient) replaceTransport(tr ports.Transport) {282+func (ac *attachedClient) replaceTransport(tr ports.ServerConnection) {
284 ac.linkMu.Lock()283 ac.linkMu.Lock()
285 ac.tr = tr284 ac.tr = tr
286 ac.transportIncarnation++285 ac.transportIncarnation++
@@ -290,7 +289,7 @@ func (ac *attachedClient) replaceTransport(tr ports.Transport) {
290 // revokeTransport removes tr only if it is still this attachment's current289 // revokeTransport removes tr only if it is still this attachment's current
291 // link. It returns the revoked transport, so asynchronous retirement can290 // link. It returns the revoked transport, so asynchronous retirement can
292 // close exactly that link without touching a later connection incarnation.291 // close exactly that link without touching a later connection incarnation.
293-func (ac *attachedClient) revokeTransport(tr ports.Transport) ports.Transport {292+func (ac *attachedClient) revokeTransport(tr ports.ServerConnection) ports.ServerConnection {
294 if tr == nil {293 if tr == nil {
295 return nil294 return nil
296 }295 }
@@ -319,20 +318,20 @@ func (ac *attachedClient) transportSnapshotCurrent(expected transportSnapshot) b
319 return ac.tr == expected.transport && ac.transportIncarnation == expected.incarnation318 return ac.tr == expected.transport && ac.transportIncarnation == expected.incarnation
320 }319 }
321 320
322-func (ac *attachedClient) closeCapturedTransport(tr ports.Transport) error {321+func (ac *attachedClient) closeCapturedTransport(tr ports.ServerConnection) error {
323 if tr == nil {322 if tr == nil {
324 return nil323 return nil
325 }324 }
326 return tr.Close()325 return tr.Close()
327 }326 }
328 327
329-func (ac *attachedClient) transportIs(tr ports.Transport) bool {328+func (ac *attachedClient) transportIs(tr ports.ServerConnection) bool {
330 ac.linkMu.Lock()329 ac.linkMu.Lock()
331 defer ac.linkMu.Unlock()330 defer ac.linkMu.Unlock()
332 return ac.tr == tr331 return ac.tr == tr
333 }332 }
334 333
335-func (ac *attachedClient) currentTransportIs(tr ports.Transport) bool {334+func (ac *attachedClient) currentTransportIs(tr ports.ServerConnection) bool {
336 return tr != nil && ac.transportIs(tr)335 return tr != nil && ac.transportIs(tr)
337 }336 }
338 337
@@ -410,7 +409,7 @@ var errTransportReplaced = errors.New("client transport was replaced")
410 409
411 // sendExpectedTransport writes only when expected is still the attachment's410 // sendExpectedTransport writes only when expected is still the attachment's
412 // current transport incarnation. It preserves sendMu -> linkMu lock ordering.411 // current transport incarnation. It preserves sendMu -> linkMu lock ordering.
413-func (ac *attachedClient) sendExpectedTransport(expected transportSnapshot, f wire.Frame) error {412+func (ac *attachedClient) sendExpectedTransport(expected transportSnapshot, message protocol.ServerMessage) error {
414 ac.sendMu.Lock()413 ac.sendMu.Lock()
415 defer ac.sendMu.Unlock()414 defer ac.sendMu.Unlock()
416 if !ac.transportSnapshotCurrent(expected) {415 if !ac.transportSnapshotCurrent(expected) {
@@ -419,7 +418,7 @@ func (ac *attachedClient) sendExpectedTransport(expected transportSnapshot, f wi
419 if expected.transport == nil {418 if expected.transport == nil {
420 return errors.New("client transport is nil")419 return errors.New("client transport is nil")
421 }420 }
422- return expected.transport.Send(f)421+ return expected.transport.SendServer(message)
423 }422 }
424 423
425 // beginExpectedTransportSendLocked validates that expected is still the424 // beginExpectedTransportSendLocked validates that expected is still the
@@ -436,7 +435,7 @@ func (ac *attachedClient) beginExpectedTransportSendLocked(expected transportSna
436 return nil435 return nil
437 }436 }
438 437
439-func (ac *attachedClient) sendExpectedTransportForAttachment(expected transportSnapshot, f wire.Frame, ticket *attachmentEffect) error {438+func (ac *attachedClient) sendExpectedTransportForAttachment(expected transportSnapshot, message protocol.ServerMessage, ticket *attachmentEffect) error {
440 ac.sendMu.Lock()439 ac.sendMu.Lock()
441 defer ac.sendMu.Unlock()440 defer ac.sendMu.Unlock()
442 // A attachment-bound send requires a live ticket, and reports every rejection,441 // A attachment-bound send requires a live ticket, and reports every rejection,
@@ -447,7 +446,7 @@ func (ac *attachedClient) sendExpectedTransportForAttachment(expected transportS
447 if err := ac.beginExpectedTransportSendLocked(expected, ticket); err != nil {446 if err := ac.beginExpectedTransportSendLocked(expected, ticket); err != nil {
448 return errAttachmentTransition447 return errAttachmentTransition
449 }448 }
450- err := expected.transport.Send(f)449+ err := expected.transport.SendServer(message)
451 if err != nil {450 if err != nil {
452 ticket.reportTransportFailure(expected)451 ticket.reportTransportFailure(expected)
453 }452 }
@@ -460,17 +459,17 @@ func (ac *attachedClient) sendExpectedTransportForAttachment(expected transportS
460 // detachNotifyTimeout, the transport is force-closed, failing the in-flight459 // detachNotifyTimeout, the transport is force-closed, failing the in-flight
461 // write. Detach/kill/shutdown paths use this so they are never gated on a460 // write. Detach/kill/shutdown paths use this so they are never gated on a
462 // client that has stopped draining its socket.461 // client that has stopped draining its socket.
463-func (d *Daemon) boundedSend(ac *attachedClient, f wire.Frame) {462+func (d *Daemon) boundedSend(ac *attachedClient, message protocol.ServerMessage) {
464- _ = d.boundedSendErr(ac, f)463+ _ = d.boundedSendErr(ac, message)
465 }464 }
466 465
467-func (d *Daemon) boundedSendErr(ac *attachedClient, f wire.Frame) error {466+func (d *Daemon) boundedSendErr(ac *attachedClient, message protocol.ServerMessage) error {
468 expected := ac.transportSnapshot()467 expected := ac.transportSnapshot()
469 if expected.transport == nil {468 if expected.transport == nil {
470 return errors.New("client transport is nil")469 return errors.New("client transport is nil")
471 }470 }
472 tr, err := d.boundedSendWith(expected.transport, func() error {471 tr, err := d.boundedSendWith(expected.transport, func() error {
473- return ac.sendExpectedTransport(expected, f)472+ return ac.sendExpectedTransport(expected, message)
474 })473 })
475 if err != nil && errors.Is(err, errSendTimedOut) {474 if err != nil && errors.Is(err, errSendTimedOut) {
476 _ = ac.closeCapturedTransport(tr)475 _ = ac.closeCapturedTransport(tr)
@@ -483,12 +482,12 @@ func (d *Daemon) boundedSendOutputErr(ac *attachedClient, b []byte) error {
483 return err482 return err
484 }483 }
485 484
486-func (d *Daemon) boundedSendOutputErrTransport(ac *attachedClient, b []byte) (ports.Transport, error) {485+func (d *Daemon) boundedSendOutputErrTransport(ac *attachedClient, b []byte) (ports.ServerConnection, error) {
487 expected := ac.transportSnapshot()486 expected := ac.transportSnapshot()
488 if expected.transport == nil {487 if expected.transport == nil {
489 return nil, errors.New("client transport is nil")488 return nil, errors.New("client transport is nil")
490 }489 }
491- if owned, ok := expected.transport.(ports.OwnedSynchronousTransport); ok {490+ if expected.transport.Capabilities().OwnedSynchronousSend {
492 ac.sendMu.Lock()491 ac.sendMu.Lock()
493 defer ac.sendMu.Unlock()492 defer ac.sendMu.Unlock()
494 if !ac.transportSnapshotCurrent(expected) {493 if !ac.transportSnapshotCurrent(expected) {
@@ -499,9 +498,9 @@ func (d *Daemon) boundedSendOutputErrTransport(ac *attachedClient, b []byte) (po
499 }498 }
500 ac.output.lockView()499 ac.output.lockView()
501 defer ac.output.unlockView()500 defer ac.output.unlockView()
502- frame, err := ac.output.sideEffectLocked(b, ac.echoAck.Load())501+ output, err := ac.output.sideEffectLocked(b, ac.echoAck.Load())
503 if err == nil {502 if err == nil {
504- err = owned.SendSynchronous(frame)503+ err = expected.transport.SendOutputSynchronous(output)
505 }504 }
506 return expected.transport, err505 return expected.transport, err
507 }506 }
@@ -516,21 +515,21 @@ func (d *Daemon) boundedSendOutputErrTransport(ac *attachedClient, b []byte) (po
516 }515 }
517 ac.output.lockView()516 ac.output.lockView()
518 defer ac.output.unlockView()517 defer ac.output.unlockView()
519- frame, err := ac.output.sideEffectLocked(b, ac.echoAck.Load())518+ output, err := ac.output.sideEffectLocked(b, ac.echoAck.Load())
520 if err != nil {519 if err != nil {
521 return err520 return err
522 }521 }
523- return expected.transport.Send(frame)522+ return expected.transport.SendOutput(output)
524 })523 })
525 }524 }
526 525
527 var errSendTimedOut = errors.New("send timed out")526 var errSendTimedOut = errors.New("send timed out")
528 527
529-func (d *Daemon) boundedSendWith(tr ports.Transport, send func() error) (ports.Transport, error) {528+func (d *Daemon) boundedSendWith(tr ports.ServerConnection, send func() error) (ports.ServerConnection, error) {
530 return d.boundedSendWithTimeout(detachNotifyTimeout, tr, send)529 return d.boundedSendWithTimeout(detachNotifyTimeout, tr, send)
531 }530 }
532 531
533-func (d *Daemon) boundedSendWithTimeout(timeout time.Duration, tr ports.Transport, send func() error) (ports.Transport, error) {532+func (d *Daemon) boundedSendWithTimeout(timeout time.Duration, tr ports.ServerConnection, send func() error) (ports.ServerConnection, error) {
534 timer := d.clock.NewTimer(timeout)533 timer := d.clock.NewTimer(timeout)
535 result := make(chan error, 1)534 result := make(chan error, 1)
536 go func() {535 go func() {
@@ -581,7 +580,7 @@ func (d *Daemon) notifyDetachedSnapshotAsync(snapshot detachedAttachmentSnapshot
581 go func() {580 go func() {
582 defer close(done)581 defer close(done)
583 _, _ = d.boundedSendWith(snapshot.transport.transport, func() error {582 _, _ = d.boundedSendWith(snapshot.transport.transport, func() error {
584- return snapshot.ac.sendExpectedTransport(snapshot.transport, frameDetached(reason))583+ return snapshot.ac.sendExpectedTransport(snapshot.transport, serverDetached(reason))
585 })584 })
586 revoked := snapshot.ac.revokeTransport(snapshot.transport.transport)585 revoked := snapshot.ac.revokeTransport(snapshot.transport.transport)
587 if revoked == nil {586 if revoked == nil {
@@ -619,7 +618,7 @@ type attachClientOptions struct {
619 startupOverlay protocol.StartupOverlay618 startupOverlay protocol.StartupOverlay
620 }619 }
621 620
622-func (d *Daemon) attachClient(sess *session, tr ports.Transport, sz domain.Size, opts attachClientOptions) (*attachedClient, error) {621+func (d *Daemon) attachClient(sess *session, tr ports.ServerConnection, sz domain.Size, opts attachClientOptions) (*attachedClient, error) {
623 d.mu.Lock()622 d.mu.Lock()
624 ac := d.prepareAttachedClientLocked(sess, tr, domain.Geometry{Size: sz}, opts)623 ac := d.prepareAttachedClientLocked(sess, tr, domain.Geometry{Size: sz}, opts)
625 d.mu.Unlock()624 d.mu.Unlock()
@@ -664,7 +663,7 @@ func (d *Daemon) finishAttachedClient(sess *session, ac *attachedClient, opts at
664 // prepareAttachedClientLocked allocates one detached attachment. Caller holds663 // prepareAttachedClientLocked allocates one detached attachment. Caller holds
665 // d.mu only to allocate its resume token; attachment publication happens later after664 // d.mu only to allocate its resume token; attachment publication happens later after
666 // the caller releases every architecture lock.665 // the caller releases every architecture lock.
667-func (d *Daemon) prepareAttachedClientLocked(sess *session, tr ports.Transport, geometry domain.Geometry, opts attachClientOptions) *attachedClient {666+func (d *Daemon) prepareAttachedClientLocked(sess *session, tr ports.ServerConnection, geometry domain.Geometry, opts attachClientOptions) *attachedClient {
668 resumeToken := uint64(0)667 resumeToken := uint64(0)
669 if opts.resumeCapable {668 if opts.resumeCapable {
670 resumeToken = d.nextResumeTokenLocked()669 resumeToken = d.nextResumeTokenLocked()
M internal/usecase/daemon/client_frame_routing.go +55 -91
@@ -7,7 +7,6 @@ import (
7 "github.com/bnema/vev/internal/domain"7 "github.com/bnema/vev/internal/domain"
8 "github.com/bnema/vev/internal/ports"8 "github.com/bnema/vev/internal/ports"
9 "github.com/bnema/vev/internal/protocol"9 "github.com/bnema/vev/internal/protocol"
10- "github.com/bnema/vev/internal/protocol/wire"
11 "github.com/bnema/vev/internal/usecase/command"10 "github.com/bnema/vev/internal/usecase/command"
12 )11 )
13 12
@@ -16,7 +15,7 @@ const connectionSnapshotAttempts = 8
16 // currentAttachmentConnection retries the lock-free session-to-role snapshot when a15 // currentAttachmentConnection retries the lock-free session-to-role snapshot when a
17 // handoff lands between those reads. The attachment gate still performs the final16 // handoff lands between those reads. The attachment gate still performs the final
18 // mutation admission after this function returns.17 // mutation admission after this function returns.
19-func (d *Daemon) currentAttachmentConnection(ac *attachedClient, tr ports.Transport) (*session, attachmentCapability, bool) {18+func (d *Daemon) currentAttachmentConnection(ac *attachedClient, tr ports.ServerConnection) (*session, attachmentCapability, bool) {
20 for range connectionSnapshotAttempts {19 for range connectionSnapshotAttempts {
21 if !ac.currentTransportIs(tr) {20 if !ac.currentTransportIs(tr) {
22 break21 break
@@ -50,8 +49,19 @@ func (d *Daemon) runConnLoop(ac *attachedClient) {
50 if !ac.currentTransportIs(tr) || ac.currentAttachmentSession() == nil {49 if !ac.currentTransportIs(tr) || ac.currentAttachmentSession() == nil {
51 return50 return
52 }51 }
53- f, err := tr.Recv()52+ message, err := tr.ReceiveClient()
54 if err != nil {53 if err != nil {
54+ var failure *protocol.DecodeFailure
55+ if errors.As(err, &failure) {
56+ d.log.Warn("rejected client message",
57+ "category", failure.Category,
58+ "kind", failure.Kind,
59+ "type", failure.Type,
60+ "version", failure.Version,
61+ "request", failure.RequestID,
62+ )
63+ continue
64+ }
55 for range connectionSnapshotAttempts {65 for range connectionSnapshotAttempts {
56 sess, _, ok := d.currentAttachmentConnection(ac, tr)66 sess, _, ok := d.currentAttachmentConnection(ac, tr)
57 if !ok {67 if !ok {
@@ -69,14 +79,14 @@ func (d *Daemon) runConnLoop(ac *attachedClient) {
69 if !ok {79 if !ok {
70 return80 return
71 }81 }
72- if d.handleAttachmentClientFrame(token, f) {82+ if d.handleAttachmentClientMessage(token, message) {
73 return83 return
74 }84 }
75 }85 }
76 }86 }
77 87
78-// handleAttachmentClientFrame owns the attached-client protocol.88+// handleAttachmentClientMessage owns the attached-client protocol.
79-func (d *Daemon) handleAttachmentClientFrame(capability attachmentCapability, f wire.Frame) bool {89+func (d *Daemon) handleAttachmentClientMessage(capability attachmentCapability, message protocol.ClientMessage) bool {
80 if !capability.current() {90 if !capability.current() {
81 return false91 return false
82 }92 }
@@ -91,63 +101,34 @@ func (d *Daemon) handleAttachmentClientFrame(capability attachmentCapability, f
91 if d.afterAttachmentEffectAdmitted != nil {101 if d.afterAttachmentEffectAdmitted != nil {
92 d.afterAttachmentEffectAdmitted(effect.capability())102 d.afterAttachmentEffectAdmitted(effect.capability())
93 }103 }
94- switch f.Type {104+ switch message := message.(type) {
95- case wire.MsgInput:105+ case protocol.Input:
96- if in, derr := wire.UnmarshalInput(f.Payload); derr == nil {106+ d.handleSequencedInputForAttachment(effect, message.InputSeq, message.Data)
97- d.handleSequencedInputForAttachment(effect, in.InputSeq, in.Data)107+ case protocol.Resize:
98- }108+ if effect.current() {
99- case wire.MsgResize:109+ d.resizeAttachmentGeometryForLease(effect, message.Geometry())
100- if rz, derr := wire.UnmarshalResize(f.Payload); derr == nil && effect.current() {
101- d.resizeAttachmentGeometryForLease(effect, rz.Geometry())
102- }
103- case wire.MsgTheme:
104- if th, derr := wire.UnmarshalTheme(f.Payload); derr == nil {
105- d.applyThemeForAttachment(effect, th)
106- }
107- case wire.MsgImagePush:
108- if ip, derr := wire.UnmarshalImagePush(f.Payload); derr == nil {
109- d.handleSequencedImagePushForAttachment(effect, ip.InputSeq, ip)
110- }
111- case wire.MsgClientNotice:
112- if notice, derr := wire.UnmarshalClientNotice(f.Payload); derr == nil {
113- d.handleClientNoticeForAttachment(effect, notice)
114- } else {
115- d.log.Warn("malformed client notice", "err", derr)
116 }110 }
117- case wire.MsgDetach:111+ case protocol.Theme:
112+ d.applyThemeForAttachment(effect, message)
113+ case protocol.ImagePush:
114+ d.handleSequencedImagePushForAttachment(effect, message.InputSeq, message)
115+ case protocol.ClientNotice:
116+ d.handleClientNoticeForAttachment(effect, message)
117+ case protocol.Detach:
118 if effect.current() {118 if effect.current() {
119 d.clientGoneForAttachment(effect, true)119 d.clientGoneForAttachment(effect, true)
120 return true120 return true
121 }121 }
122- case wire.MsgAck:122+ case protocol.Ack:
123- if ack, derr := wire.UnmarshalAck(f.Payload); derr == nil {123+ d.ackOutput(effect, message.Epoch, message.State)
124- d.ackOutput(effect, ack.Epoch, ack.State)124+ case protocol.OutputResetRequest:
125- }125+ d.resetOutput(effect)
126- case wire.MsgOutputResetRequest:126+ case protocol.SamePeerSwitchRequest:
127- if _, derr := wire.UnmarshalOutputResetRequest(f.Payload); derr == nil {127+ d.switchSamePeerForAttachment(effect, message)
128- d.resetOutput(effect)128+ case protocol.ParkedRouteRequest:
129- }129+ d.handleParkedRouteRequest(effect, message)
130- case wire.MsgSamePeerSwitchRequest:130+ case protocol.RecentRouteSnapshot:
131- request, derr := wire.UnmarshalSamePeerSwitchRequest(f.Payload)131+ replayIdentity := effect.ac.setRouteSnapshot(message)
132- if derr != nil {
133- d.log.Warn("malformed same-peer switch request", "err", derr)
134- break
135- }
136- d.switchSamePeerForAttachment(effect, request)
137- case wire.MsgParkedRouteRequest:
138- request, derr := wire.UnmarshalParkedRouteRequest(f.Payload)
139- if derr != nil {
140- d.log.Warn("malformed parked-route request", "err", derr)
141- break
142- }
143- d.handleParkedRouteRequest(effect, request)
144- case wire.MsgRecentRouteSnapshot:
145- snapshot, derr := wire.UnmarshalRecentRouteSnapshot(f.Payload)
146- if derr != nil {
147- d.log.Warn("malformed recent route snapshot", "err", derr)
148- break
149- }
150- replayIdentity := effect.ac.setRouteSnapshot(snapshot)
151 d.invalidateRender(effect.sess, effect.ac, false, "client_frame_routing.go:route-snapshot")132 d.invalidateRender(effect.sess, effect.ac, false, "client_frame_routing.go:route-snapshot")
152 if !replayIdentity {133 if !replayIdentity {
153 break134 break
@@ -155,61 +136,44 @@ func (d *Daemon) handleAttachmentClientFrame(capability attachmentCapability, f
155 if err := d.sendCommittedRouteIdentityForAttachment(effect); err != nil {136 if err := d.sendCommittedRouteIdentityForAttachment(effect); err != nil {
156 d.detachOnAttachmentSendError(effect.capability(), effect.transport.transport)137 d.detachOnAttachmentSendError(effect.capability(), effect.transport.transport)
157 }138 }
158- case wire.MsgRouteAttentionSubscription:139+ case protocol.RouteAttentionSubscription:
159- subscription, derr := wire.UnmarshalRouteAttentionSubscription(f.Payload)140+ effect.ac.setRouteAttentionSubscription(message)
160- if derr != nil {
161- d.log.Warn("malformed route attention subscription", "err", derr)
162- break
163- }
164- effect.ac.setRouteAttentionSubscription(subscription)
165 d.invalidateRender(effect.sess, effect.ac, false, "client_frame_routing.go:route-attention")141 d.invalidateRender(effect.sess, effect.ac, false, "client_frame_routing.go:route-attention")
166- case wire.MsgRouteNavigationFailure:142+ case protocol.RouteNavigationFailure:
167- failure, derr := wire.UnmarshalRouteNavigationFailure(f.Payload)143+ notice := "route navigation failed"
168- if derr != nil {144+ switch message.Code {
169- d.log.Warn("malformed route navigation failure", "err", derr)
170- break
171- }
172- message := "route navigation failed"
173- switch failure.Code {
174 case protocol.RouteFailureStaleSelection:145 case protocol.RouteFailureStaleSelection:
175- message = "that recent route is no longer available"146+ notice = "that recent route is no longer available"
176 case protocol.RouteFailureTargetChanged:147 case protocol.RouteFailureTargetChanged:
177- message = "that recent route changed before attach"148+ notice = "that recent route changed before attach"
178 case protocol.RouteFailureOriginUnavailable:149 case protocol.RouteFailureOriginUnavailable:
179- message = "the original route is no longer available"150+ notice = "the original route is no longer available"
180 case protocol.RouteFailureUnavailable:151 case protocol.RouteFailureUnavailable:
181- message = "the selected route is unavailable"152+ notice = "the selected route is unavailable"
182 case protocol.RouteFailureNoSuchRoute:153 case protocol.RouteFailureNoSuchRoute:
183- message = "that recent route no longer exists"154+ notice = "that recent route no longer exists"
184- }
185- d.notify(effect.sess, domain.NoticeWarn, domain.NoticeSessionUnavailable, message, nil)
186- case wire.MsgCommand:
187- request, derr := wire.UnmarshalCommandRequest(f.Payload)
188- if derr != nil {
189- return false
190 }155 }
191- result := d.executeAttachedCommand(effect, request)156+ d.notify(effect.sess, domain.NoticeWarn, domain.NoticeSessionUnavailable, notice, nil)
157+ case protocol.CommandRequest:
158+ result := d.executeAttachedCommand(effect, message)
192 resultEffect, ok := d.commandResultEffect(effect)159 resultEffect, ok := d.commandResultEffect(effect)
193 if !ok {160 if !ok {
194 // A remote handoff or explicit detach deliberately retired the old161 // A remote handoff or explicit detach deliberately retired the old
195 // attachment; it must not be treated as a stale-result send error.162 // attachment; it must not be treated as a stale-result send error.
196 return true163 return true
197 }164 }
198- if err := resultEffect.sendControl(frameCommandResult(result)); err != nil {165+ if err := resultEffect.sendControl(serverCommandResult(result)); err != nil {
199 resultEffect.End()166 resultEffect.End()
200 d.detachOnAttachmentSendError(resultEffect.capability(), resultEffect.transport.transport)167 d.detachOnAttachmentSendError(resultEffect.capability(), resultEffect.transport.transport)
201 return true168 return true
202 }169 }
203 resultEffect.End()170 resultEffect.End()
204- case wire.MsgPing:171+ case protocol.Ping:
205- if err := effect.sendControl(framePong()); err != nil {172+ if err := effect.sendControl(serverPong()); err != nil {
206 effect.End()173 effect.End()
207 d.detachOnAttachmentSendError(effect.capability(), effect.transport.transport)174 d.detachOnAttachmentSendError(effect.capability(), effect.transport.transport)
208 return true175 return true
209 }176 }
210- default:
211- // Unknown/out-of-band client messages are ignored so a newer
212- // client can add message types without breaking an older daemon.
213 }177 }
214 return false178 return false
215 }179 }
M internal/usecase/daemon/client_transport_errors.go +10 -10
@@ -7,15 +7,15 @@ import (
7 7
8 // clientGone detaches ac if it is still the session's current client. The8 // clientGone detaches ac if it is still the session's current client. The
9 // session remains registered and headless after the client is gone.9 // session remains registered and headless after the client is gone.
10-func (d *Daemon) clientGone(sess *session, ac *attachedClient, failed ports.Transport, explicit bool) {10+func (d *Daemon) clientGone(sess *session, ac *attachedClient, failed ports.ServerConnection, explicit bool) {
11 d.clientGoneWithNotice(sess, ac, failed, explicit, true)11 d.clientGoneWithNotice(sess, ac, failed, explicit, true)
12 }12 }
13 13
14-func (d *Daemon) clientGoneWithoutNotice(sess *session, ac *attachedClient, failed ports.Transport, explicit bool) {14+func (d *Daemon) clientGoneWithoutNotice(sess *session, ac *attachedClient, failed ports.ServerConnection, explicit bool) {
15 d.clientGoneWithNotice(sess, ac, failed, explicit, false)15 d.clientGoneWithNotice(sess, ac, failed, explicit, false)
16 }16 }
17 17
18-func (d *Daemon) clientGoneWithNotice(sess *session, ac *attachedClient, failed ports.Transport, explicit, notice bool) {18+func (d *Daemon) clientGoneWithNotice(sess *session, ac *attachedClient, failed ports.ServerConnection, explicit, notice bool) {
19 if sess == nil || ac == nil {19 if sess == nil || ac == nil {
20 return20 return
21 }21 }
@@ -65,7 +65,7 @@ func (d *Daemon) clientGoneForAttachment(effect *attachmentEffect, explicit bool
65 return true65 return true
66 }66 }
67 67
68-func (d *Daemon) finishClientGone(sess *session, ac *attachedClient, failed ports.Transport, explicit, notice bool) {68+func (d *Daemon) finishClientGone(sess *session, ac *attachedClient, failed ports.ServerConnection, explicit, notice bool) {
69 if sess == nil || ac == nil {69 if sess == nil || ac == nil {
70 return70 return
71 }71 }
@@ -111,7 +111,7 @@ func (d *Daemon) finishClientGone(sess *session, ac *attachedClient, failed port
111 // (the client is actively awaiting it), but deadline-bounded so a111 // (the client is actively awaiting it), but deadline-bounded so a
112 // wedged client cannot pin this conn handler and hang Serve's112 // wedged client cannot pin this conn handler and hang Serve's
113 // connWg.Wait.113 // connWg.Wait.
114- d.boundedSend(ac, frameDetached(protocol.ReasonDetach))114+ d.boundedSend(ac, serverDetached(protocol.ReasonDetach))
115 }115 }
116 _ = ac.closeCapturedTransport(oldTr)116 _ = ac.closeCapturedTransport(oldTr)
117 d.log.Info("client detached", "session", name, "explicit", explicit)117 d.log.Info("client detached", "session", name, "explicit", explicit)
@@ -119,7 +119,7 @@ func (d *Daemon) finishClientGone(sess *session, ac *attachedClient, failed port
119 119
120 // detachOnSendError drops a client whose transport failed, leaving the session120 // detachOnSendError drops a client whose transport failed, leaving the session
121 // registered and headless.121 // registered and headless.
122-func (d *Daemon) detachOnSendError(sess *session, ac *attachedClient, failed ports.Transport) {122+func (d *Daemon) detachOnSendError(sess *session, ac *attachedClient, failed ports.ServerConnection) {
123 expected := transportSnapshot{}123 expected := transportSnapshot{}
124 if failed != nil {124 if failed != nil {
125 expected = ac.transportSnapshot()125 expected = ac.transportSnapshot()
@@ -135,11 +135,11 @@ func (d *Daemon) detachOnSendError(sess *session, ac *attachedClient, failed por
135 d.clearParkingInFlightIfAbandoned(sess, ac, parkingToken)135 d.clearParkingInFlightIfAbandoned(sess, ac, parkingToken)
136 }136 }
137 137
138-func (d *Daemon) detachOnAttachmentSendError(token attachmentCapability, failed ports.Transport) {138+func (d *Daemon) detachOnAttachmentSendError(token attachmentCapability, failed ports.ServerConnection) {
139 d.detachOnAttachmentSendErrorUntil(token, failed, nil)139 d.detachOnAttachmentSendErrorUntil(token, failed, nil)
140 }140 }
141 141
142-func (d *Daemon) detachOnAttachmentSendErrorUntil(token attachmentCapability, failed ports.Transport, done func() <-chan struct{}) {142+func (d *Daemon) detachOnAttachmentSendErrorUntil(token attachmentCapability, failed ports.ServerConnection, done func() <-chan struct{}) {
143 // A delayed sender may report after the client has rebound. Only the exact143 // A delayed sender may report after the client has rebound. Only the exact
144 // transport captured by this attachment is allowed to detach either lifecycle.144 // transport captured by this attachment is allowed to detach either lifecycle.
145 if failed == nil || failed != token.transport.transport {145 if failed == nil || failed != token.transport.transport {
@@ -160,7 +160,7 @@ func (d *Daemon) detachOnAttachmentSendErrorUntil(token attachmentCapability, fa
160 // reserveAttachmentSendErrorCleanup accounts for cleanup before End releases the160 // reserveAttachmentSendErrorCleanup accounts for cleanup before End releases the
161 // attachment gate. This closes the WaitGroup Add/Wait race with terminal teardown;161 // attachment gate. This closes the WaitGroup Add/Wait race with terminal teardown;
162 // the returned launch function must be invoked immediately after ticket End.162 // the returned launch function must be invoked immediately after ticket End.
163-func (d *Daemon) reserveAttachmentSendErrorCleanup(token attachmentCapability, failed ports.Transport) func() {163+func (d *Daemon) reserveAttachmentSendErrorCleanup(token attachmentCapability, failed ports.ServerConnection) func() {
164 d.attachmentCleanupWg.Add(1)164 d.attachmentCleanupWg.Add(1)
165 return func() {165 return func() {
166 go func() {166 go func() {
@@ -178,7 +178,7 @@ func (d *Daemon) reserveAttachmentSendErrorCleanup(token attachmentCapability, f
178 }178 }
179 }179 }
180 180
181-func (d *Daemon) finishSendErrorDetach(sess *session, ac *attachedClient, failed ports.Transport) {181+func (d *Daemon) finishSendErrorDetach(sess *session, ac *attachedClient, failed ports.ServerConnection) {
182 ac.clearSamePeerOffer()182 ac.clearSamePeerOffer()
183 ac.clearParkedRoute()183 ac.clearParkedRoute()
184 name := sess.nameSnapshot()184 name := sess.nameSnapshot()
M internal/usecase/daemon/clipboard.go +5 -5
@@ -255,13 +255,13 @@ func (d *Daemon) forwardClipboardAsync(owner paneEffectLease, b64 string) {
255 sess.mu.Unlock()255 sess.mu.Unlock()
256 }256 }
257 257
258-func (d *Daemon) boundedSendClipboardForward(item clipboardForward, ticket *attachmentEffect) (ports.Transport, error) {258+func (d *Daemon) boundedSendClipboardForward(item clipboardForward, ticket *attachmentEffect) (ports.ServerConnection, error) {
259 token := item.token259 token := item.token
260 if token.ac == nil || ticket == nil || ticket.ended.Load() || token.transport.transport == nil {260 if token.ac == nil || ticket == nil || ticket.ended.Load() || token.transport.transport == nil {
261 return token.transport.transport, errAttachmentTransition261 return token.transport.transport, errAttachmentTransition
262 }262 }
263 expected := token.transport263 expected := token.transport
264- ownedTransport, owned := expected.transport.(ports.OwnedSynchronousTransport)264+ owned := expected.transport.Capabilities().OwnedSynchronousSend
265 send := func() error {265 send := func() error {
266 ac := token.ac266 ac := token.ac
267 ac.sendMu.Lock()267 ac.sendMu.Lock()
@@ -277,12 +277,12 @@ func (d *Daemon) boundedSendClipboardForward(item clipboardForward, ticket *atta
277 }277 }
278 ac.output.lockView()278 ac.output.lockView()
279 defer ac.output.unlockView()279 defer ac.output.unlockView()
280- frame, err := ac.output.sideEffectLocked(item.seq, ac.echoAck.Load())280+ output, err := ac.output.sideEffectLocked(item.seq, ac.echoAck.Load())
281 if err == nil {281 if err == nil {
282 if owned {282 if owned {
283- err = ownedTransport.SendSynchronous(frame)283+ err = expected.transport.SendOutputSynchronous(output)
284 } else {284 } else {
285- err = expected.transport.Send(frame)285+ err = expected.transport.SendOutput(output)
286 }286 }
287 }287 }
288 if err != nil {288 if err != nil {
M internal/usecase/daemon/clipboard_test.go +1 -1
@@ -204,7 +204,7 @@ func chunkReadPTY(t *testing.T, chunks ...[]byte) *portsmocks.MockPTY {
204 return p204 return p
205 }205 }
206 206
207-func publishActiveClipboardCapability(d *Daemon, sess *session, ac *attachedClient, tr ports.Transport) {207+func publishActiveClipboardCapability(d *Daemon, sess *session, ac *attachedClient, tr ports.ServerConnection) {
208 rc := d.attachCoordinator(sess, nil, ac, true)208 rc := d.attachCoordinator(sess, nil, ac, true)
209 token := sess.captureAttachmentCapability(ac, tr)209 token := sess.captureAttachmentCapability(ac, tr)
210 token.lease = rc.attachmentLease(ac)210 token.lease = rc.attachmentLease(ac)
M internal/usecase/daemon/control.go +9 -17
@@ -13,22 +13,16 @@ import (
13 "github.com/bnema/vev/internal/domain"13 "github.com/bnema/vev/internal/domain"
14 "github.com/bnema/vev/internal/ports"14 "github.com/bnema/vev/internal/ports"
15 "github.com/bnema/vev/internal/protocol"15 "github.com/bnema/vev/internal/protocol"
16- "github.com/bnema/vev/internal/protocol/wire"
17 "github.com/bnema/vev/internal/usecase/command"16 "github.com/bnema/vev/internal/usecase/command"
18 "github.com/bnema/vev/internal/usecase/layout"17 "github.com/bnema/vev/internal/usecase/layout"
19 )18 )
20 19
21-// handleCommand serves one one-shot control request. The leading version is20+// handleCommand serves one typed one-shot control request.
22-// checked before decoding the versioned body so newer layouts fail cleanly.21+func (d *Daemon) handleCommand(tr ports.ServerConnection, request protocol.CommandRequest) error {
23-func (d *Daemon) handleCommand(tr ports.Transport, f wire.Frame) error {
24 defer func() { _ = tr.Close() }()22 defer func() { _ = tr.Close() }()
25 23
26- if version, ok := wire.PeekCommandVersion(f.Payload); !ok || version != protocol.Version {24+ if request.Version != protocol.Version {
27- return d.sendCommandResult(tr, protocol.CommandResult{Code: protocol.ErrVersionMismatch, Text: "protocol version mismatch"})25+ return d.sendCommandResult(tr, protocol.CommandResult{RequestID: request.RequestID, Code: protocol.ErrVersionMismatch, Text: "protocol version mismatch"})
28- }
29- request, err := wire.UnmarshalCommandRequest(f.Payload)
30- if err != nil {
31- return d.sendCommandResult(tr, protocol.CommandResult{Code: protocol.ErrInternal, Text: "malformed command request"})
32 }26 }
33 if request.Attached {27 if request.Attached {
34 return d.sendCommandResult(tr, protocol.CommandResult{28 return d.sendCommandResult(tr, protocol.CommandResult{
@@ -69,8 +63,8 @@ func (d *Daemon) handleCommand(tr ports.Transport, f wire.Frame) error {
69 63
70 // sendCommandResult gives one-shot control responses the same observable64 // sendCommandResult gives one-shot control responses the same observable
71 // transport-failure behavior regardless of which validation path produced it.65 // transport-failure behavior regardless of which validation path produced it.
72-func (d *Daemon) sendCommandResult(tr ports.Transport, result protocol.CommandResult) error {66+func (d *Daemon) sendCommandResult(tr ports.ServerConnection, result protocol.CommandResult) error {
73- if err := d.boundedControlSend(tr, frameCommandResult(result)); err != nil {67+ if err := d.boundedControlSend(tr, serverCommandResult(result)); err != nil {
74 d.log.Warn("command response send failed", "err", err)68 d.log.Warn("command response send failed", "err", err)
75 return err69 return err
76 }70 }
@@ -80,17 +74,15 @@ func (d *Daemon) sendCommandResult(tr ports.Transport, result protocol.CommandRe
80 // boundedControlSend keeps one-shot control handlers from waiting forever on a74 // boundedControlSend keeps one-shot control handlers from waiting forever on a
81 // client that stopped reading. A timeout closes the exact transport so a75 // client that stopped reading. A timeout closes the exact transport so a
82 // blocked Send can unwind before the handler returns.76 // blocked Send can unwind before the handler returns.
83-func (d *Daemon) boundedControlSend(tr ports.Transport, frame wire.Frame) error {77+func (d *Daemon) boundedControlSend(tr ports.ServerConnection, message protocol.ServerMessage) error {
84- _, err := d.boundedSendWith(tr, func() error { return tr.Send(frame) })78+ _, err := d.boundedSendWith(tr, func() error { return tr.SendServer(message) })
85 if errors.Is(err, errSendTimedOut) {79 if errors.Is(err, errSendTimedOut) {
86 _ = tr.Close()80 _ = tr.Close()
87 }81 }
88 return err82 return err
89 }83 }
90 84
91-func frameCommandResult(result protocol.CommandResult) wire.Frame {85+func serverCommandResult(result protocol.CommandResult) protocol.CommandResult { return result }
92- return wire.Frame{Type: wire.MsgCommandResult, Payload: wire.MarshalCommandResult(result)}
93-}
94 86
95 func (d *Daemon) dispatchCommand(ctx context.Context, request protocol.CommandRequest) protocol.CommandResult {87 func (d *Daemon) dispatchCommand(ctx context.Context, request protocol.CommandRequest) protocol.CommandResult {
96 cmd, ok := command.BySlug(request.Slug)88 cmd, ok := command.BySlug(request.Slug)
M internal/usecase/daemon/control_test.go +8 -22
@@ -139,7 +139,7 @@ func TestHandleCommandTimesOutWithRequestGeneration(t *testing.T) {
139 defer releaseConn()139 defer releaseConn()
140 done := make(chan error, 1)140 done := make(chan error, 1)
141 go func() {141 go func() {
142- done <- d.handleCommand(tr, frame)142+ done <- d.handleCommandFrame(tr, frame)
143 }()143 }()
144 <-factory.entered144 <-factory.entered
145 timer := <-clock.timers145 timer := <-clock.timers
@@ -159,7 +159,7 @@ func TestHandleCommandResponseSendTimeoutClosesTransport(t *testing.T) {
159 tr := newBlockingControlSendTransport()159 tr := newBlockingControlSendTransport()
160 frame := commandFrame(t, protocol.CommandRequest{RequestID: 41, Attached: true})160 frame := commandFrame(t, protocol.CommandRequest{RequestID: 41, Attached: true})
161 done := make(chan error, 1)161 done := make(chan error, 1)
162- go func() { done <- d.handleCommand(tr, frame) }()162+ go func() { done <- d.handleCommandFrame(tr, frame) }()
163 163
164 <-tr.started164 <-tr.started
165 timer := <-clock.timers165 timer := <-clock.timers
@@ -184,7 +184,7 @@ func TestHandleCommandAttachedRejectionReturnsSendFailure(t *testing.T) {
184 sendErr := errors.New("command result send failed")184 sendErr := errors.New("command result send failed")
185 tr := &commandSendErrorTransport{err: sendErr}185 tr := &commandSendErrorTransport{err: sendErr}
186 186
187- err := d.handleCommand(tr, commandFrame(t, protocol.CommandRequest{187+ err := d.handleCommandFrame(tr, commandFrame(t, protocol.CommandRequest{
188 RequestID: 41,188 RequestID: 41,
189 Attached: true,189 Attached: true,
190 }))190 }))
@@ -194,20 +194,6 @@ func TestHandleCommandAttachedRejectionReturnsSendFailure(t *testing.T) {
194 require.True(t, tr.closed, "the failed one-shot transport must still close")194 require.True(t, tr.closed, "the failed one-shot transport must still close")
195 }195 }
196 196
197-func TestHandleCommandRejectsVersionBeforeDecodeOrDispatch(t *testing.T) {
198- // The version prefix is valid and mismatched, while the remainder is not a
199- // decodable request. The daemon must still return ErrVersionMismatch.
200- frame := wire.Frame{Type: wire.MsgCommand, Payload: []byte{0, byte(protocol.Version + 1)}}
201- tr, sends, _ := newConn(t, frame)
202-
203- d := newTestDaemon(t, nil, stubClock{})
204- require.NoError(t, d.handleCommand(tr, frame))
205-
206- result := awaitCommandResult(t, sends)
207- require.False(t, result.OK)
208- require.Equal(t, protocol.ErrVersionMismatch, result.Code)
209-}
210-
211 func TestHandleConnRoutesCommand(t *testing.T) {197 func TestHandleConnRoutesCommand(t *testing.T) {
212 d := newTestDaemon(t, nil, stubClock{})198 d := newTestDaemon(t, nil, stubClock{})
213 request := protocol.CommandRequest{Version: protocol.Version, Slug: "list-sessions"}199 request := protocol.CommandRequest{Version: protocol.Version, Slug: "list-sessions"}
@@ -887,7 +873,7 @@ func TestHandleCommandSerializesSelfTargetOnNonActiveTab(t *testing.T) {
887 firstTransport, firstSends, _ := newConn(t, firstFrame)873 firstTransport, firstSends, _ := newConn(t, firstFrame)
888 firstDone := make(chan struct{})874 firstDone := make(chan struct{})
889 go func() {875 go func() {
890- if err := d.handleCommand(firstTransport, firstFrame); err != nil {876+ if err := d.handleCommandFrame(firstTransport, firstFrame); err != nil {
891 t.Errorf("handle first command: %v", err)877 t.Errorf("handle first command: %v", err)
892 }878 }
893 close(firstDone)879 close(firstDone)
@@ -905,7 +891,7 @@ func TestHandleCommandSerializesSelfTargetOnNonActiveTab(t *testing.T) {
905 secondTransport, secondSends, _ := newConn(t, secondFrame)891 secondTransport, secondSends, _ := newConn(t, secondFrame)
906 secondDone := make(chan struct{})892 secondDone := make(chan struct{})
907 go func() {893 go func() {
908- if err := d.handleCommand(secondTransport, secondFrame); err != nil {894+ if err := d.handleCommandFrame(secondTransport, secondFrame); err != nil {
909 t.Errorf("handle second command: %v", err)895 t.Errorf("handle second command: %v", err)
910 }896 }
911 close(secondDone)897 close(secondDone)
@@ -1287,14 +1273,14 @@ func TestHandleCommandOppositeMoveCommandsDoNotDeadlock(t *testing.T) {
1287 done := make(chan struct{}, 2)1273 done := make(chan struct{}, 2)
1288 go func() {1274 go func() {
1289 <-start1275 <-start
1290- if err := d.handleCommand(leftTransport, leftFrame); err != nil {1276+ if err := d.handleCommandFrame(leftTransport, leftFrame); err != nil {
1291 t.Errorf("handle left command: %v", err)1277 t.Errorf("handle left command: %v", err)
1292 }1278 }
1293 done <- struct{}{}1279 done <- struct{}{}
1294 }()1280 }()
1295 go func() {1281 go func() {
1296 <-start1282 <-start
1297- if err := d.handleCommand(rightTransport, rightFrame); err != nil {1283+ if err := d.handleCommandFrame(rightTransport, rightFrame); err != nil {
1298 t.Errorf("handle right command: %v", err)1284 t.Errorf("handle right command: %v", err)
1299 }1285 }
1300 done <- struct{}{}1286 done <- struct{}{}
@@ -1339,7 +1325,7 @@ func sendCommand(t *testing.T, d *Daemon, request protocol.CommandRequest) proto
1339 t.Helper()1325 t.Helper()
1340 frame := commandFrame(t, request)1326 frame := commandFrame(t, request)
1341 tr, sends, _ := newConn(t, frame)1327 tr, sends, _ := newConn(t, frame)
1342- require.NoError(t, d.handleCommand(tr, frame))1328+ require.NoError(t, d.handleCommandFrame(tr, frame))
1343 return awaitCommandResult(t, sends)1329 return awaitCommandResult(t, sends)
1344 }1330 }
1345 1331
M internal/usecase/daemon/copymode_test.go +2 -2
@@ -68,7 +68,7 @@ func TestOwnedSynchronousSendReturnsCapturedTransportAcrossReplacement(t *testin
68 68
69 type swapErrorTransport struct {69 type swapErrorTransport struct {
70 ac *attachedClient70 ac *attachedClient
71- replacement ports.Transport71+ replacement ports.ServerConnection
72 err error72 err error
73 }73 }
74 74
@@ -82,7 +82,7 @@ func (t *swapErrorTransport) Close() error { return nil }
82 82
83 type ownedSwapErrorTransport struct {83 type ownedSwapErrorTransport struct {
84 ac *attachedClient84 ac *attachedClient
85- replacement ports.Transport85+ replacement ports.ServerConnection
86 err error86 err error
87 sent chan wire.Frame87 sent chan wire.Frame
88 }88 }
M internal/usecase/daemon/daemon.go +79 -67
@@ -18,7 +18,6 @@ import (
18 "github.com/bnema/vev/internal/domain"18 "github.com/bnema/vev/internal/domain"
19 "github.com/bnema/vev/internal/ports"19 "github.com/bnema/vev/internal/ports"
20 "github.com/bnema/vev/internal/protocol"20 "github.com/bnema/vev/internal/protocol"
21- "github.com/bnema/vev/internal/protocol/wire"
22 "github.com/bnema/vev/internal/usecase/keys"21 "github.com/bnema/vev/internal/usecase/keys"
23 recoveryusecase "github.com/bnema/vev/internal/usecase/recovery"22 recoveryusecase "github.com/bnema/vev/internal/usecase/recovery"
24 )23 )
@@ -55,7 +54,7 @@ const detachNotifyTimeout = time.Second
55 // heavy output degrades to lower fps on a slow link instead of overflowing the54 // heavy output degrades to lower fps on a slow link instead of overflowing the
56 // transport. It must stay well under the datagram carriage's 32-frame client55 // transport. It must stay well under the datagram carriage's 32-frame client
57 // window so its reliable queue never fills from painting alone.56 // window so its reliable queue never fills from painting alone.
58-const maxUnackedOutputStates = 857+const maxUnackedOutputStates = protocol.MaxOutputWindow
59 58
60 // normalizeOutputWindow bounds the untrusted Hello value. Zero deliberately59 // normalizeOutputWindow bounds the untrusted Hello value. Zero deliberately
61 // means the legacy/default window, so malformed or absent values remain safe.60 // means the legacy/default window, so malformed or absent values remain safe.
@@ -739,7 +738,7 @@ func New(ptys ports.PTYFactory, clock ports.Clock, log *slog.Logger, opts ...Opt
739 // Serve runs the accept loop over l, owning it for the loop's lifetime. It738 // Serve runs the accept loop over l, owning it for the loop's lifetime. It
740 // returns when the last session is removed or ctx is cancelled; on the latter739 // returns when the last session is removed or ctx is cancelled; on the latter
741 // path attached clients are detached with ReasonServerShutdown.740 // path attached clients are detached with ReasonServerShutdown.
742-func (d *Daemon) Serve(ctx context.Context, l ports.Listener) error {741+func (d *Daemon) Serve(ctx context.Context, l ports.ServerListener) error {
743 d.serveCtx, d.serveCancel = context.WithCancel(ctx)742 d.serveCtx, d.serveCancel = context.WithCancel(ctx)
744 defer d.serveCancel()743 defer d.serveCancel()
745 d.hardCtx, d.hardCancel = context.WithCancel(context.Background())744 d.hardCtx, d.hardCancel = context.WithCancel(context.Background())
@@ -1012,54 +1011,58 @@ func (d *Daemon) sessionsSnapshotLocked() []*session {
1012 return sessionsSnapshot(d.sessions)1011 return sessionsSnapshot(d.sessions)
1013 }1012 }
1014 1013
1015-// handleConn reads the first frame off a fresh connection and routes it. A1014+// handleConn reads the first typed message off a fresh connection and routes it.
1016-// context watcher closes the transport on serve-context cancel or handshake1015+// A context watcher closes the connection on cancellation or handshake timeout.
1017-// timeout so blocked transport operations unwind.1016+func (d *Daemon) handleConn(tr ports.ServerConnection) {
1018-func (d *Daemon) handleConn(tr ports.Transport) {
1019 handshakeCtx, timedOut, finishHandshake := d.newHandshakeContext(d.hardCtx)1017 handshakeCtx, timedOut, finishHandshake := d.newHandshakeContext(d.hardCtx)
1020 stopTransport := watchHandshakeTransport(handshakeCtx, tr)1018 stopTransport := watchHandshakeTransport(handshakeCtx, tr)
1021 defer finishHandshake()1019 defer finishHandshake()
1022 defer stopTransport()1020 defer stopTransport()
1023 1021
1024- var first wire.Frame1022+ var first protocol.ClientMessage
1025 err := boundedHandshakeOperation(handshakeCtx, tr, func() error {1023 err := boundedHandshakeOperation(handshakeCtx, tr, func() error {
1026- var recvErr error1024+ var receiveErr error
1027- first, recvErr = tr.Recv()1025+ first, receiveErr = tr.ReceiveClient()
1028- return recvErr1026+ return receiveErr
1029 })1027 })
1030 if err != nil {1028 if err != nil {
1031- err = handshakeContextError(d.hardCtx, timedOut, err)1029+ var failure *protocol.DecodeFailure
1032- d.log.Warn("connection closed before hello", "err", err)1030+ if errors.As(err, &failure) {
1031+ d.handleInitialDecodeFailure(handshakeCtx, tr, failure)
1032+ } else {
1033+ err = handshakeContextError(d.hardCtx, timedOut, err)
1034+ d.log.Warn("connection closed before hello", "err", err)
1035+ }
1033 _ = tr.Close()1036 _ = tr.Close()
1034 return1037 return
1035 }1038 }
1036- switch first.Type {1039+ switch message := first.(type) {
1037- case wire.MsgList:1040+ case protocol.List:
1038 stopTransport()1041 stopTransport()
1039 finishHandshake()1042 finishHandshake()
1040 d.handleList(tr)1043 d.handleList(tr)
1041- case wire.MsgCommand:1044+ case protocol.CommandRequest:
1042 stopTransport()1045 stopTransport()
1043 finishHandshake()1046 finishHandshake()
1044- if err := d.handleCommand(tr, first); err != nil {1047+ if err := d.handleCommand(tr, message); err != nil {
1045 d.log.Warn("command handler failed", "err", err)1048 d.log.Warn("command handler failed", "err", err)
1046 }1049 }
1047- case wire.MsgRemotePreviewRequest:1050+ case protocol.RemotePreviewRequest:
1048 stopTransport()1051 stopTransport()
1049 finishHandshake()1052 finishHandshake()
1050- if err := d.handleRemotePreview(tr, first); err != nil {1053+ if err := d.handleRemotePreview(tr, message); err != nil {
1051 d.log.Warn("remote preview handler failed", "err", err)1054 d.log.Warn("remote preview handler failed", "err", err)
1052 }1055 }
1053- case wire.MsgKill:1056+ case protocol.Kill:
1054 stopTransport()1057 stopTransport()
1055 finishHandshake()1058 finishHandshake()
1056- d.handleKill(tr, first)1059+ d.handleKill(tr, message)
1057- case wire.MsgHello:1060+ case protocol.Hello:
1058- d.handleHelloWithContext(handshakeCtx, timedOut, stopTransport, finishHandshake, tr, first)1061+ d.handleHelloWithContext(handshakeCtx, timedOut, stopTransport, finishHandshake, tr, message)
1059 default:1062 default:
1060- d.log.Warn("hello rejected", "err", "expected hello", "type", first.Type)1063+ d.log.Warn("hello rejected", "err", "expected hello")
1061 _ = boundedHandshakeOperation(handshakeCtx, tr, func() error {1064 _ = boundedHandshakeOperation(handshakeCtx, tr, func() error {
1062- return tr.Send(frameError(protocol.ErrInternal, "expected hello"))1065+ return tr.SendServer(serverError(protocol.ErrInternal, "expected hello"))
1063 })1066 })
1064 stopTransport()1067 stopTransport()
1065 finishHandshake()1068 finishHandshake()
@@ -1067,9 +1070,35 @@ func (d *Daemon) handleConn(tr ports.Transport) {
1067 }1070 }
1068 }1071 }
1069 1072
1073+func (d *Daemon) handleInitialDecodeFailure(ctx context.Context, tr ports.ServerConnection, failure *protocol.DecodeFailure) {
1074+ send := func(message protocol.ServerMessage) {
1075+ _ = boundedHandshakeOperation(ctx, tr, func() error { return tr.SendServer(message) })
1076+ }
1077+ switch failure.Kind {
1078+ case protocol.DecodeMessageHello:
1079+ if failure.Version != 0 && failure.Version != protocol.Version {
1080+ send(serverError(protocol.ErrVersionMismatch, "protocol version mismatch"))
1081+ return
1082+ }
1083+ send(serverError(protocol.ErrInternal, "malformed hello"))
1084+ case protocol.DecodeMessageCommand:
1085+ code, text := protocol.ErrInternal, "malformed command request"
1086+ if failure.Version != 0 && failure.Version != protocol.Version {
1087+ code, text = protocol.ErrVersionMismatch, "protocol version mismatch"
1088+ }
1089+ send(protocol.CommandResult{RequestID: failure.RequestID, Code: code, Text: text})
1090+ case protocol.DecodeMessageKill:
1091+ send(serverError(protocol.ErrInternal, "malformed kill request"))
1092+ case protocol.DecodeMessageRemotePreview:
1093+ send(protocol.RemotePreview{Version: protocol.RemotePreviewSchemaVersion, Status: protocol.RemotePreviewMalformed})
1094+ default:
1095+ send(serverError(protocol.ErrInternal, "expected hello"))
1096+ }
1097+}
1098+
1070 // handleList replies with the current session listing and closes the (one-shot1099 // handleList replies with the current session listing and closes the (one-shot
1071 // control) connection.1100 // control) connection.
1072-func (d *Daemon) handleList(tr ports.Transport) {1101+func (d *Daemon) handleList(tr ports.ServerConnection) {
1073 defer func() { _ = tr.Close() }()1102 defer func() { _ = tr.Close() }()
1074 1103
1075 d.mu.Lock()1104 d.mu.Lock()
@@ -1105,35 +1134,30 @@ func (d *Daemon) handleList(tr ports.Transport) {
1105 d.mu.Unlock()1134 d.mu.Unlock()
1106 1135
1107 sort.Slice(infos, func(i, j int) bool { return infos[i].Name < infos[j].Name })1136 sort.Slice(infos, func(i, j int) bool { return infos[i].Name < infos[j].Name })
1108- _ = d.boundedControlSend(tr, frameSessions(infos))1137+ _ = d.boundedControlSend(tr, serverSessions(infos))
1109 }1138 }
1110 1139
1111 // handleKill terminates the requested live session or stopped named session,1140 // handleKill terminates the requested live session or stopped named session,
1112 // or all sessions, and closes the control connection; the resulting EOF is the1141 // or all sessions, and closes the control connection; the resulting EOF is the
1113 // client's success signal.1142 // client's success signal.
1114-func (d *Daemon) handleKill(tr ports.Transport, f wire.Frame) {1143+func (d *Daemon) handleKill(tr ports.ServerConnection, request protocol.Kill) {
1115 defer func() { _ = tr.Close() }()1144 defer func() { _ = tr.Close() }()
1116 1145
1117- k, err := wire.UnmarshalKill(f.Payload)1146+ if request.All {
1118- if err != nil {
1119- _ = d.boundedControlSend(tr, frameError(protocol.ErrInternal, "malformed kill request"))
1120- return
1121- }
1122- if k.All {
1123 d.shutdownAll(protocol.ReasonServerShutdown)1147 d.shutdownAll(protocol.ReasonServerShutdown)
1124 return1148 return
1125 }1149 }
1126 1150
1127 d.mu.Lock()1151 d.mu.Lock()
1128- target := d.findByNameLocked(k.Name)1152+ target := d.findByNameLocked(request.Name)
1129 if target == nil {1153 if target == nil {
1130- if _, ok := d.inactive[k.Name]; ok {1154+ if _, ok := d.inactive[request.Name]; ok {
1131 d.mu.Unlock()1155 d.mu.Unlock()
1132 // Stopped sessions use the same catalogue-first, incarnation-second1156 // Stopped sessions use the same catalogue-first, incarnation-second
1133 // deletion order as live and offline purges.1157 // deletion order as live and offline purges.
1134- if err := d.retryStoppedPurge(k.Name); err != nil {1158+ if err := d.retryStoppedPurge(request.Name); err != nil {
1135- d.log.Warn("deleting stopped session failed", "err", err, "session", k.Name)1159+ d.log.Warn("deleting stopped session failed", "err", err, "session", request.Name)
1136- _ = d.boundedControlSend(tr, frameError(protocol.ErrInternal, "deleting stopped session failed"))1160+ _ = d.boundedControlSend(tr, serverError(protocol.ErrInternal, "deleting stopped session failed"))
1137 }1161 }
1138 return1162 return
1139 }1163 }
@@ -1141,54 +1165,42 @@ func (d *Daemon) handleKill(tr ports.Transport, f wire.Frame) {
1141 d.mu.Unlock()1165 d.mu.Unlock()
1142 1166
1143 if target == nil {1167 if target == nil {
1144- _ = d.boundedControlSend(tr, frameError(protocol.ErrNoSuchSession, "no such session: "+k.Name))1168+ _ = d.boundedControlSend(tr, serverError(protocol.ErrNoSuchSession, "no such session: "+request.Name))
1145 return1169 return
1146 }1170 }
1147 if err := d.killSession(target, protocol.ReasonSessionKilled, true); err != nil {1171 if err := d.killSession(target, protocol.ReasonSessionKilled, true); err != nil {
1148- _ = d.boundedControlSend(tr, frameError(protocol.ErrInternal, "deleting persisted session failed"))1172+ _ = d.boundedControlSend(tr, serverError(protocol.ErrInternal, "deleting persisted session failed"))
1149 }1173 }
1150 }1174 }
1151 1175
1152 // handleHello runs the attach handshake for direct package callers. Accepted1176 // handleHello runs the attach handshake for direct package callers. Accepted
1153 // connections use handleHelloWithContext so the deadline also covers the first1177 // connections use handleHelloWithContext so the deadline also covers the first
1154 // frame read in handleConn.1178 // frame read in handleConn.
1155-func (d *Daemon) handleHello(tr ports.Transport, f wire.Frame) {1179+func (d *Daemon) handleHello(tr ports.ServerConnection, hello protocol.Hello) {
1156 handshakeCtx, timedOut, finishHandshake := d.newHandshakeContext(context.Background())1180 handshakeCtx, timedOut, finishHandshake := d.newHandshakeContext(context.Background())
1157 stopTransport := watchHandshakeTransport(handshakeCtx, tr)1181 stopTransport := watchHandshakeTransport(handshakeCtx, tr)
1158 defer finishHandshake()1182 defer finishHandshake()
1159 defer stopTransport()1183 defer stopTransport()
1160- d.handleHelloWithContext(handshakeCtx, timedOut, stopTransport, finishHandshake, tr, f)1184+ d.handleHelloWithContext(handshakeCtx, timedOut, stopTransport, finishHandshake, tr, hello)
1161 }1185 }
1162 1186
1163 // handleHelloWithContext runs the attach handshake: version check, intent1187 // handleHelloWithContext runs the attach handshake: version check, intent
1164 // routing, Welcome, guaranteed first paint, then the per-connection input loop.1188 // routing, Welcome, guaranteed first paint, then the per-connection input loop.
1165-func (d *Daemon) handleHelloWithContext(handshakeCtx context.Context, timedOut <-chan struct{}, stopHandshakeTransport, finishHandshake func(), tr ports.Transport, f wire.Frame) {1189+func (d *Daemon) handleHelloWithContext(handshakeCtx context.Context, timedOut <-chan struct{}, stopHandshakeTransport, finishHandshake func(), tr ports.ServerConnection, h protocol.Hello) {
1166- if err := handshakeContextError(context.Background(), timedOut, handshakeCtx.Err()); err != nil {1190+ if err := handshakeContextError(d.hardCtx, timedOut, handshakeCtx.Err()); err != nil {
1167 _ = tr.Close()1191 _ = tr.Close()
1168 return1192 return
1169 }1193 }
1170- sendHandshake := func(frame wire.Frame) error {1194+ sendHandshake := func(message protocol.ServerMessage) error {
1171- err := boundedHandshakeOperation(handshakeCtx, tr, func() error { return tr.Send(frame) })1195+ err := boundedHandshakeOperation(handshakeCtx, tr, func() error { return tr.SendServer(message) })
1172 if err != nil {1196 if err != nil {
1173- return handshakeContextError(context.Background(), timedOut, err)1197+ return handshakeContextError(d.hardCtx, timedOut, err)
1174 }1198 }
1175 return nil1199 return nil
1176 }1200 }
1177- h, err := wire.UnmarshalHello(f.Payload)
1178- if err != nil {
1179- if version, ok := wire.PeekHelloVersion(f.Payload); ok && version != protocol.Version {
1180- d.log.Warn("hello rejected", "err", "protocol version mismatch", "version", version, "expected", protocol.Version)
1181- _ = sendHandshake(frameError(protocol.ErrVersionMismatch, "protocol version mismatch"))
1182- } else {
1183- d.log.Warn("hello rejected", "err", err)
1184- _ = sendHandshake(frameError(protocol.ErrInternal, "malformed hello"))
1185- }
1186- _ = tr.Close()
1187- return
1188- }
1189 if h.Version != protocol.Version {1201 if h.Version != protocol.Version {
1190 d.log.Warn("hello rejected", "err", "protocol version mismatch", "version", h.Version, "expected", protocol.Version, "intent", h.Intent, "session", h.Name)1202 d.log.Warn("hello rejected", "err", "protocol version mismatch", "version", h.Version, "expected", protocol.Version, "intent", h.Intent, "session", h.Name)
1191- _ = sendHandshake(frameError(protocol.ErrVersionMismatch, "protocol version mismatch"))1203+ _ = sendHandshake(serverError(protocol.ErrVersionMismatch, "protocol version mismatch"))
1192 _ = tr.Close()1204 _ = tr.Close()
1193 return1205 return
1194 }1206 }
@@ -1197,9 +1209,9 @@ func (d *Daemon) handleHelloWithContext(handshakeCtx context.Context, timedOut <
1197 if rerr != nil {1209 if rerr != nil {
1198 d.log.Warn("hello rejected", "err", rerr, "intent", h.Intent, "session", h.Name)1210 d.log.Warn("hello rejected", "err", rerr, "intent", h.Intent, "session", h.Name)
1199 if pe, ok := errors.AsType[*protoErr](rerr); ok {1211 if pe, ok := errors.AsType[*protoErr](rerr); ok {
1200- _ = sendHandshake(frameError(pe.code, pe.text))1212+ _ = sendHandshake(serverError(pe.code, pe.text))
1201 } else {1213 } else {
1202- _ = sendHandshake(frameError(protocol.ErrInternal, rerr.Error()))1214+ _ = sendHandshake(serverError(protocol.ErrInternal, rerr.Error()))
1203 }1215 }
1204 _ = tr.Close()1216 _ = tr.Close()
1205 return1217 return
@@ -1230,7 +1242,7 @@ func (d *Daemon) handleHelloWithContext(handshakeCtx context.Context, timedOut <
1230 return1242 return
1231 }1243 }
1232 welcomeDone, welcomeErr := boundedHandshakeOperationTracked(handshakeCtx, tr, func() error {1244 welcomeDone, welcomeErr := boundedHandshakeOperationTracked(handshakeCtx, tr, func() error {
1233- frame, err := frameWelcome(sess, d.resumeTokenSnapshot(ac))1245+ frame, err := serverWelcome(sess, d.resumeTokenSnapshot(ac))
1234 if err != nil {1246 if err != nil {
1235 return err1247 return err
1236 }1248 }
@@ -1311,7 +1323,7 @@ func (e *protoErr) Error() string { return e.text }
1311 // unlocking d.mu before returning, including every error path. It publishes1323 // unlocking d.mu before returning, including every error path. It publishes
1312 // terminal and role state before releasing d.mu, then defers coordinator1324 // terminal and role state before releasing d.mu, then defers coordinator
1313 // cleanup so obsolete workers never delay the new handshake.1325 // cleanup so obsolete workers never delay the new handshake.
1314-func (d *Daemon) finishAttach(sess *session, tr ports.Transport, sz domain.Size, h protocol.Hello) (*attachedClient, error) {1326+func (d *Daemon) finishAttach(sess *session, tr ports.ServerConnection, sz domain.Size, h protocol.Hello) (*attachedClient, error) {
1315 initialTabIndex := -11327 initialTabIndex := -1
1316 if h.RemoteTarget != nil {1328 if h.RemoteTarget != nil {
1317 var ok bool1329 var ok bool
@@ -1478,7 +1490,7 @@ func (d *Daemon) validateExactSessionTarget(ctx context.Context, target protocol
1478 return d.validateExactSessionTargetLocked(target)1490 return d.validateExactSessionTargetLocked(target)
1479 }1491 }
1480 1492
1481-func (d *Daemon) route(h protocol.Hello, tr ports.Transport) (*session, *attachedClient, error) {1493+func (d *Daemon) route(h protocol.Hello, tr ports.ServerConnection) (*session, *attachedClient, error) {
1482 ctx := d.serveCtx1494 ctx := d.serveCtx
1483 if ctx == nil {1495 if ctx == nil {
1484 ctx = context.Background()1496 ctx = context.Background()
@@ -1486,7 +1498,7 @@ func (d *Daemon) route(h protocol.Hello, tr ports.Transport) (*session, *attache
1486 return d.routeWithContext(ctx, h, tr)1498 return d.routeWithContext(ctx, h, tr)
1487 }1499 }
1488 1500
1489-func (d *Daemon) routeWithContext(ctx context.Context, h protocol.Hello, tr ports.Transport) (*session, *attachedClient, error) {1501+func (d *Daemon) routeWithContext(ctx context.Context, h protocol.Hello, tr ports.ServerConnection) (*session, *attachedClient, error) {
1490 if ctx == nil {1502 if ctx == nil {
1491 ctx = context.Background()1503 ctx = context.Background()
1492 }1504 }
M internal/usecase/daemon/daemon_test.go +7 -7
@@ -237,9 +237,9 @@ func newFactorySeq(t *testing.T, ptys ...ports.PTY) *portsmocks.MockPTYFactory {
237 // newConn scripts a MockTransport: Recv yields first then more (in order),237 // newConn scripts a MockTransport: Recv yields first then more (in order),
238 // then blocks until the connection is released or Closed (returning io.EOF).238 // then blocks until the connection is released or Closed (returning io.EOF).
239 // Every Send is captured on the returned channel.239 // Every Send is captured on the returned channel.
240-func newConn(t *testing.T, first wire.Frame, more ...wire.Frame) (*portsmocks.MockTransport, chan wire.Frame, func()) {240+func newConn(t *testing.T, first wire.Frame, more ...wire.Frame) (*mockServerConnection, chan wire.Frame, func()) {
241 t.Helper()241 t.Helper()
242- tr := portsmocks.NewMockTransport(t)242+ tr := newMockServerConnection(t)
243 sends := make(chan wire.Frame, 64)243 sends := make(chan wire.Frame, 64)
244 recvCh := make(chan wire.Frame, 1+len(more))244 recvCh := make(chan wire.Frame, 1+len(more))
245 recvCh <- first245 recvCh <- first
@@ -331,7 +331,7 @@ func listSessions(t *testing.T, d *Daemon) protocol.Sessions {
331 // attachment publication and Welcome acceptance. It uses the generated331 // attachment publication and Welcome acceptance. It uses the generated
332 // transport mock while channels make both sides of the wire deterministic.332 // transport mock while channels make both sides of the wire deterministic.
333 type welcomeBlockingTransport struct {333 type welcomeBlockingTransport struct {
334- tr *portsmocks.MockTransport334+ tr *mockServerConnection
335 sends chan wire.Frame335 sends chan wire.Frame
336 welcomeEntered chan struct{}336 welcomeEntered chan struct{}
337 releaseWelcome chan struct{}337 releaseWelcome chan struct{}
@@ -348,7 +348,7 @@ func newWelcomeBlockingTransport(t *testing.T) *welcomeBlockingTransport {
348 releaseWelcome: make(chan struct{}),348 releaseWelcome: make(chan struct{}),
349 recvDone: make(chan struct{}),349 recvDone: make(chan struct{}),
350 }350 }
351- b.tr = portsmocks.NewMockTransport(t)351+ b.tr = newMockServerConnection(t)
352 b.tr.EXPECT().Send(mock.Anything).RunAndReturn(func(f wire.Frame) error {352 b.tr.EXPECT().Send(mock.Anything).RunAndReturn(func(f wire.Frame) error {
353 b.sends <- f353 b.sends <- f
354 if f.Type == wire.MsgWelcome {354 if f.Type == wire.MsgWelcome {
@@ -375,9 +375,9 @@ func (b *welcomeBlockingTransport) finish() {
375 b.closeOnce.Do(func() { close(b.recvDone) })375 b.closeOnce.Do(func() { close(b.recvDone) })
376 }376 }
377 377
378-func newCapturingTransport(t testing.TB) (*portsmocks.MockTransport, chan wire.Frame) {378+func newCapturingTransport(t testing.TB) (*mockServerConnection, chan wire.Frame) {
379 t.Helper()379 t.Helper()
380- tr := portsmocks.NewMockTransport(t)380+ tr := newMockServerConnection(t)
381 sends := make(chan wire.Frame, 64)381 sends := make(chan wire.Frame, 64)
382 tr.EXPECT().Send(mock.Anything).RunAndReturn(func(f wire.Frame) error {382 tr.EXPECT().Send(mock.Anything).RunAndReturn(func(f wire.Frame) error {
383 sends <- f383 sends <- f
@@ -666,7 +666,7 @@ func TestHandleHelloDefersFreshOutputUntilWelcome(t *testing.T) {
666 tr := newWelcomeBlockingTransport(t)666 tr := newWelcomeBlockingTransport(t)
667 done := make(chan struct{})667 done := make(chan struct{})
668 go func() {668 go func() {
669- d.handleHello(tr.tr, mustHello(protocol.IntentNew, "welcome-gate", domain.Size{Cols: 80, Rows: 24}))669+ d.handleHelloFrame(tr.tr, mustHello(protocol.IntentNew, "welcome-gate", domain.Size{Cols: 80, Rows: 24}))
670 close(done)670 close(done)
671 }()671 }()
672 672
M internal/usecase/daemon/graphics_output.go +6 -7
@@ -15,7 +15,6 @@ import (
15 renderer "github.com/bnema/vev-vt"15 renderer "github.com/bnema/vev-vt"
16 "github.com/bnema/vev-vt/graphics"16 "github.com/bnema/vev-vt/graphics"
17 "github.com/bnema/vev/internal/domain"17 "github.com/bnema/vev/internal/domain"
18- "github.com/bnema/vev/internal/ports"
19 "github.com/bnema/vev/internal/protocol"18 "github.com/bnema/vev/internal/protocol"
20 "github.com/bnema/vev/internal/usecase/layout"19 "github.com/bnema/vev/internal/usecase/layout"
21 themeui "github.com/bnema/vev/internal/usecase/theme"20 themeui "github.com/bnema/vev/internal/usecase/theme"
@@ -1008,23 +1007,23 @@ func (d *Daemon) cleanupAttachmentOutput(ac *attachedClient) error {
1008 return errors.New("client transport is nil")1007 return errors.New("client transport is nil")
1009 }1008 }
1010 ac.output.lockView()1009 ac.output.lockView()
1011- frame, frameErr := ac.output.sideEffectLocked(prepared.data, ac.echoAck.Load())1010+ output, outputErr := ac.output.sideEffectLocked(prepared.data, ac.echoAck.Load())
1012- if frameErr != nil {1011+ if outputErr != nil {
1013 ac.output.unlockView()1012 ac.output.unlockView()
1014 prepared.abort()1013 prepared.abort()
1015 d.retireGraphicsOutput(ac, state)1014 d.retireGraphicsOutput(ac, state)
1016- return frameErr1015+ return outputErr
1017 }1016 }
1018 prepared.markSendAttempted()1017 prepared.markSendAttempted()
1019 var sendErr error1018 var sendErr error
1020- if owned, ok := expected.transport.(ports.OwnedSynchronousTransport); ok {1019+ if expected.transport.Capabilities().OwnedSynchronousSend {
1021- sendErr = owned.SendSynchronous(frame)1020+ sendErr = expected.transport.SendOutputSynchronous(output)
1022 } else {1021 } else {
1023 _, sendErr = d.boundedSendWith(expected.transport, func() error {1022 _, sendErr = d.boundedSendWith(expected.transport, func() error {
1024 if !ac.transportSnapshotCurrent(expected) {1023 if !ac.transportSnapshotCurrent(expected) {
1025 return errTransportReplaced1024 return errTransportReplaced
1026 }1025 }
1027- return expected.transport.Send(frame)1026+ return expected.transport.SendOutput(output)
1028 })1027 })
1029 if errors.Is(sendErr, errSendTimedOut) {1028 if errors.Is(sendErr, errSendTimedOut) {
1030 _ = ac.closeCapturedTransport(expected.transport)1029 _ = ac.closeCapturedTransport(expected.transport)
M internal/usecase/daemon/handshake.go +5 -5
@@ -52,7 +52,7 @@ func (d *Daemon) newHandshakeContext(parent context.Context) (context.Context, <
52 52
53 // watchHandshakeTransport closes a transport when the handshake context ends.53 // watchHandshakeTransport closes a transport when the handshake context ends.
54 // Transport.Close is required to interrupt blocked Send and Recv operations.54 // Transport.Close is required to interrupt blocked Send and Recv operations.
55-func watchHandshakeTransport(ctx context.Context, transport ports.Transport) func() {55+func watchHandshakeTransport(ctx context.Context, transport ports.ServerConnection) func() {
56 stop := make(chan struct{})56 stop := make(chan struct{})
57 done := make(chan struct{})57 done := make(chan struct{})
58 go func() {58 go func() {
@@ -74,8 +74,8 @@ func watchHandshakeTransport(ctx context.Context, transport ports.Transport) fun
74 74
75 // boundedHandshakeOperation waits for one transport operation while the shared75 // boundedHandshakeOperation waits for one transport operation while the shared
76 // handshake context remains live. Closing the transport is the interruption76 // handshake context remains live. Closing the transport is the interruption
77-// mechanism because ports.Transport methods do not accept contexts.77+// mechanism because ServerConnection methods do not accept contexts.
78-func boundedHandshakeOperation(ctx context.Context, transport ports.Transport, operation func() error) error {78+func boundedHandshakeOperation(ctx context.Context, transport ports.ServerConnection, operation func() error) error {
79 _, err := boundedHandshakeOperationTracked(ctx, transport, operation)79 _, err := boundedHandshakeOperationTracked(ctx, transport, operation)
80 return err80 return err
81 }81 }
@@ -84,7 +84,7 @@ func boundedHandshakeOperation(ctx context.Context, transport ports.Transport, o
84 // operation worker. Callers that own an effect gate must wait for this signal84 // operation worker. Callers that own an effect gate must wait for this signal
85 // before releasing the gate after cancellation; the transport operation itself85 // before releasing the gate after cancellation; the transport operation itself
86 // may still be unwinding after Close interrupts it.86 // may still be unwinding after Close interrupts it.
87-func boundedHandshakeOperationTracked(ctx context.Context, transport ports.Transport, operation func() error) (<-chan struct{}, error) {87+func boundedHandshakeOperationTracked(ctx context.Context, transport ports.ServerConnection, operation func() error) (<-chan struct{}, error) {
88 done := make(chan struct{})88 done := make(chan struct{})
89 if err := ctx.Err(); err != nil {89 if err := ctx.Err(); err != nil {
90 close(done)90 close(done)
@@ -129,7 +129,7 @@ func handshakeContextError(parent context.Context, timedOut <-chan struct{}, fal
129 // failHandshakeAttachment synchronously retires the exact connection admitted129 // failHandshakeAttachment synchronously retires the exact connection admitted
130 // by route. A fresh route-owned session is purged only after its attachment is130 // by route. A fresh route-owned session is purged only after its attachment is
131 // removed; existing sessions and their unrelated attachments are preserved.131 // removed; existing sessions and their unrelated attachments are preserved.
132-func (d *Daemon) failHandshakeAttachment(sess *session, ac *attachedClient, tr ports.Transport, welcomed bool) {132+func (d *Daemon) failHandshakeAttachment(sess *session, ac *attachedClient, tr ports.ServerConnection, welcomed bool) {
133 if sess == nil || ac == nil {133 if sess == nil || ac == nil {
134 if tr != nil {134 if tr != nil {
135 _ = tr.Close()135 _ = tr.Close()
M internal/usecase/daemon/handshake_test.go +6 -6
@@ -128,7 +128,7 @@ func TestHandshakeFirstPaintCancellationDoesNotRacePaintResult(t *testing.T) {
128 tr := newHandshakeBlockingTransport(false)128 tr := newHandshakeBlockingTransport(false)
129 handshakeDone := make(chan struct{})129 handshakeDone := make(chan struct{})
130 go func() {130 go func() {
131- d.handleHelloWithContext(ctx, make(chan struct{}), func() {}, func() {}, tr,131+ d.handleHelloFrameWithContext(ctx, make(chan struct{}), func() {}, func() {}, tr,
132 mustHello(protocol.IntentNew, "paint-cancel", domain.Size{Cols: 80, Rows: 24}))132 mustHello(protocol.IntentNew, "paint-cancel", domain.Size{Cols: 80, Rows: 24}))
133 close(handshakeDone)133 close(handshakeDone)
134 }()134 }()
@@ -183,7 +183,7 @@ func TestFailedResumeHandshakeRestoresParkedCredential(t *testing.T) {
183 resumeTransport := newHandshakeBlockingTransport(true)183 resumeTransport := newHandshakeBlockingTransport(true)
184 done := make(chan struct{})184 done := make(chan struct{})
185 go func() {185 go func() {
186- d.handleHello(resumeTransport, wire.Frame{Type: wire.MsgHello, Payload: wire.MarshalHello(helloResumeCapable(protocol.IntentResume, sess.name, token))})186+ d.handleHelloFrame(resumeTransport, wire.Frame{Type: wire.MsgHello, Payload: wire.MarshalHello(helloResumeCapable(protocol.IntentResume, sess.name, token))})
187 close(done)187 close(done)
188 }()188 }()
189 189
@@ -211,7 +211,7 @@ func TestHandshakeTimeoutClosesBlockedWelcomeSend(t *testing.T) {
211 tr := newHandshakeBlockingTransport(true)211 tr := newHandshakeBlockingTransport(true)
212 done := make(chan struct{})212 done := make(chan struct{})
213 go func() {213 go func() {
214- d.handleHello(tr, mustHello(protocol.IntentNew, "timeout-send", domain.Size{Cols: 80, Rows: 24}))214+ d.handleHelloFrame(tr, mustHello(protocol.IntentNew, "timeout-send", domain.Size{Cols: 80, Rows: 24}))
215 close(done)215 close(done)
216 }()216 }()
217 217
@@ -247,7 +247,7 @@ func TestHandshakeTimeoutCancelsRouteRestoreWait(t *testing.T) {
247 tr := newHandshakeBlockingTransport(false)247 tr := newHandshakeBlockingTransport(false)
248 done := make(chan struct{})248 done := make(chan struct{})
249 go func() {249 go func() {
250- d.handleHello(tr, mustHello(protocol.IntentAttach, "restoring", domain.Size{Cols: 80, Rows: 24}))250+ d.handleHelloFrame(tr, mustHello(protocol.IntentAttach, "restoring", domain.Size{Cols: 80, Rows: 24}))
251 close(done)251 close(done)
252 }()252 }()
253 253
@@ -275,7 +275,7 @@ func TestHandshakeTimeoutRemovesRestoredEmptySession(t *testing.T) {
275 tr := newHandshakeBlockingTransport(true)275 tr := newHandshakeBlockingTransport(true)
276 done := make(chan struct{})276 done := make(chan struct{})
277 go func() {277 go func() {
278- d.handleHello(tr, mustHello(protocol.IntentAttach, "restored", domain.Size{Cols: 80, Rows: 24}))278+ d.handleHelloFrame(tr, mustHello(protocol.IntentAttach, "restored", domain.Size{Cols: 80, Rows: 24}))
279 close(done)279 close(done)
280 }()280 }()
281 281
@@ -305,7 +305,7 @@ func TestHandshakeTimeoutPreservesUnrelatedAttachment(t *testing.T) {
305 tr := newHandshakeBlockingTransport(true)305 tr := newHandshakeBlockingTransport(true)
306 done := make(chan struct{})306 done := make(chan struct{})
307 go func() {307 go func() {
308- d.handleHello(tr, mustHello(protocol.IntentAttach, "shared", domain.Size{Cols: 80, Rows: 24}))308+ d.handleHelloFrame(tr, mustHello(protocol.IntentAttach, "shared", domain.Size{Cols: 80, Rows: 24}))
309 close(done)309 close(done)
310 }()310 }()
311 311
M internal/usecase/daemon/notify_test.go +8 -3
@@ -1,6 +1,7 @@
1 package daemon1 package daemon
2 2
3 import (3 import (
4+ "bytes"
4 "context"5 "context"
5 "errors"6 "errors"
6 "fmt"7 "fmt"
@@ -15,7 +16,6 @@ import (
15 vt "github.com/bnema/vev-vt"16 vt "github.com/bnema/vev-vt"
16 "github.com/bnema/vev/internal/domain"17 "github.com/bnema/vev/internal/domain"
17 "github.com/bnema/vev/internal/ports"18 "github.com/bnema/vev/internal/ports"
18- portsmocks "github.com/bnema/vev/internal/ports/mocks"
19 "github.com/bnema/vev/internal/protocol"19 "github.com/bnema/vev/internal/protocol"
20 "github.com/bnema/vev/internal/protocol/wire"20 "github.com/bnema/vev/internal/protocol/wire"
21 )21 )
@@ -54,9 +54,11 @@ func TestClientNoticeMapsFixedActionsAndDismissesOnlyConnectionToast(t *testing.
54 54
55 func TestMalformedClientNoticeIsIgnored(t *testing.T) {55 func TestMalformedClientNoticeIsIgnored(t *testing.T) {
56 d, sess, ac, _ := newNoticeFixture(t, &noticeClock{})56 d, sess, ac, _ := newNoticeFixture(t, &noticeClock{})
57+ var logs bytes.Buffer
58+ d.log = slog.New(slog.NewTextHandler(&logs, nil))
57 d.attachCoordinator(sess, nil, ac, true)59 d.attachCoordinator(sess, nil, ac, true)
58- tr, ok := ac.tr.(*portsmocks.MockTransport)60+ tr, ok := ac.tr.(*mockServerConnection)
59- require.True(t, ok, "attached client transport must be a MockTransport")61+ require.True(t, ok, "attached client connection must use the test wrapper")
60 frames := []wire.Frame{62 frames := []wire.Frame{
61 {Type: wire.MsgClientNotice, Payload: []byte{0xff}},63 {Type: wire.MsgClientNotice, Payload: []byte{0xff}},
62 {Type: wire.MsgDetach, Payload: wire.MarshalDetach(protocol.Detach{})},64 {Type: wire.MsgDetach, Payload: wire.MarshalDetach(protocol.Detach{})},
@@ -70,6 +72,9 @@ func TestMalformedClientNoticeIsIgnored(t *testing.T) {
70 toasts, _ := visibleToasts(ac)72 toasts, _ := visibleToasts(ac)
71 require.Empty(t, toasts)73 require.Empty(t, toasts)
72 require.Empty(t, d.notices.history())74 require.Empty(t, d.notices.history())
75+ require.Contains(t, logs.String(), "rejected client message")
76+ require.Contains(t, logs.String(), "category=1")
77+ require.Contains(t, logs.String(), "type=11")
73 }78 }
74 79
75 func stripNoticeTimes(notices []domain.Notification) []domain.Notification {80 func stripNoticeTimes(notices []domain.Notification) []domain.Notification {
M internal/usecase/daemon/parked_route.go +2 -19
@@ -9,7 +9,6 @@ import (
9 "github.com/bnema/vev/internal/domain"9 "github.com/bnema/vev/internal/domain"
10 "github.com/bnema/vev/internal/ports"10 "github.com/bnema/vev/internal/ports"
11 "github.com/bnema/vev/internal/protocol"11 "github.com/bnema/vev/internal/protocol"
12- "github.com/bnema/vev/internal/protocol/wire"
13 )12 )
14 13
15 const parkedRouteLeaseTTL = 15 * time.Minute14 const parkedRouteLeaseTTL = 15 * time.Minute
@@ -238,33 +237,17 @@ func (ac *attachedClient) consumePublishedParkedRoute(previous, published attach
238 return 0237 return 0
239 }238 }
240 239
241-func parkedRouteResponseFrame(response protocol.ParkedRouteResponse) (wire.Frame, error) {
242- payload := wire.MarshalParkedRouteResponse(response)
243- if payload == nil {
244- return wire.Frame{}, errAttachmentTransition
245- }
246- return wire.Frame{Type: wire.MsgParkedRouteResponse, Payload: payload}, nil
247-}
248-
249 func (d *Daemon) sendParkedRouteResponse(effect *attachmentEffect, response protocol.ParkedRouteResponse) error {240 func (d *Daemon) sendParkedRouteResponse(effect *attachmentEffect, response protocol.ParkedRouteResponse) error {
250- frame, err := parkedRouteResponseFrame(response)241+ return effect.sendControl(response)
251- if err != nil {
252- return err
253- }
254- return effect.sendControl(frame)
255 }242 }
256 243
257 // sendParkedRouteResponseLocked preserves response -> full-output ordering.244 // sendParkedRouteResponseLocked preserves response -> full-output ordering.
258 // Caller holds ac.sendMu and keeps parkedRouteOutput set through this send.245 // Caller holds ac.sendMu and keeps parkedRouteOutput set through this send.
259 func (d *Daemon) sendParkedRouteResponseLocked(ac *attachedClient, expected transportSnapshot, response protocol.ParkedRouteResponse) error {246 func (d *Daemon) sendParkedRouteResponseLocked(ac *attachedClient, expected transportSnapshot, response protocol.ParkedRouteResponse) error {
260- frame, err := parkedRouteResponseFrame(response)
261- if err != nil {
262- return err
263- }
264 if ac == nil || !ac.transportSnapshotCurrent(expected) || expected.transport == nil {247 if ac == nil || !ac.transportSnapshotCurrent(expected) || expected.transport == nil {
265 return errAttachmentTransition248 return errAttachmentTransition
266 }249 }
267- return expected.transport.Send(frame)250+ return expected.transport.SendServer(response)
268 }251 }
269 252
270 func (ac *attachedClient) releaseParkedOutputLocked() {253 func (ac *attachedClient) releaseParkedOutputLocked() {
M internal/usecase/daemon/picker.go +9 -8
@@ -11,7 +11,6 @@ import (
11 "github.com/bnema/vev/internal/domain"11 "github.com/bnema/vev/internal/domain"
12 "github.com/bnema/vev/internal/ports"12 "github.com/bnema/vev/internal/ports"
13 "github.com/bnema/vev/internal/protocol"13 "github.com/bnema/vev/internal/protocol"
14- "github.com/bnema/vev/internal/protocol/wire"
15 "github.com/bnema/vev/internal/usecase/layout"14 "github.com/bnema/vev/internal/usecase/layout"
16 "github.com/bnema/vev/internal/usecase/picker"15 "github.com/bnema/vev/internal/usecase/picker"
17 "github.com/bnema/vev/internal/usecase/ui"16 "github.com/bnema/vev/internal/usecase/ui"
@@ -924,14 +923,17 @@ func (d *Daemon) sendLocalAttachTargetForAttachment(effect *attachmentEffect, ta
924 // Explicit tab rows already take the direct transition path above, while923 // Explicit tab rows already take the direct transition path above, while
925 // stopped sessions have no active target session and retain the fallback.924 // stopped sessions have no active target session and retain the fallback.
926 samePeerEligible := guard.allowSamePeer && targetSess != nil && target.TabIndex <= 0925 samePeerEligible := guard.allowSamePeer && targetSess != nil && target.TabIndex <= 0
927- payload := wire.MarshalAttachTarget(protocol.AttachTarget{926+ if exactTarget == nil {
927+ return errAttachmentTransition
928+ }
929+ handoff := protocol.AttachTarget{
928 Session: sessionName,930 Session: sessionName,
929 Intent: protocol.IntentAttach,931 Intent: protocol.IntentAttach,
930 ExactTarget: exactTarget,932 ExactTarget: exactTarget,
931 EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,933 EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
932 SamePeer: samePeerEligible,934 SamePeer: samePeerEligible,
933- })935+ }
934- if payload == nil || exactTarget == nil {936+ if protocol.ValidateAttachTarget(handoff) != nil {
935 return errAttachmentTransition937 return errAttachmentTransition
936 }938 }
937 // A close-and-dial handoff leaves this daemon's Kitty namespace. A939 // A close-and-dial handoff leaves this daemon's Kitty namespace. A
@@ -944,7 +946,7 @@ func (d *Daemon) sendLocalAttachTargetForAttachment(effect *attachmentEffect, ta
944 } else {946 } else {
945 effect.ac.offerSamePeerTarget(*exactTarget)947 effect.ac.offerSamePeerTarget(*exactTarget)
946 }948 }
947- if err := effect.sendControl(wire.Frame{Type: wire.MsgAttachTarget, Payload: payload}); err != nil {949+ if err := effect.sendControl(handoff); err != nil {
948 if samePeerEligible {950 if samePeerEligible {
949 effect.ac.clearSamePeerOffer()951 effect.ac.clearSamePeerOffer()
950 }952 }
@@ -990,11 +992,10 @@ func (d *Daemon) sendRemoteAttachTargetForAttachment(effect *attachmentEffect, t
990 RemoteTarget: &remoteTarget,992 RemoteTarget: &remoteTarget,
991 EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,993 EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
992 }994 }
993- payload := wire.MarshalAttachTarget(handoff)995+ if protocol.ValidateAttachTarget(handoff) != nil {
994- if payload == nil {
995 return failUnavailable()996 return failUnavailable()
996 }997 }
997- if err := effect.sendControl(wire.Frame{Type: wire.MsgAttachTarget, Payload: payload}); err != nil {998+ if err := effect.sendControl(handoff); err != nil {
998 return domain.UserErr(domain.NoticeSessionUnavailable, "couldn't attach to remote session", err)999 return domain.UserErr(domain.NoticeSessionUnavailable, "couldn't attach to remote session", err)
999 }1000 }
1000 if guard.closePicker {1001 if guard.closePicker {
M internal/usecase/daemon/picker_test.go +13 -4
@@ -1011,8 +1011,12 @@ func TestPickerSessionSwitchPublishesBeforeInFlightPaintSendCompletes(t *testing
1011 d := newTestDaemon(t, nil, stubClock{})1011 d := newTestDaemon(t, nil, stubClock{})
1012 enteredSend := make(chan struct{})1012 enteredSend := make(chan struct{})
1013 releaseSend := make(chan struct{})1013 releaseSend := make(chan struct{})
1014- tr := portsmocks.NewMockTransport(t)1014+ tr := portsmocks.NewMockServerConnection(t)
1015- tr.EXPECT().Send(mock.Anything).RunAndReturn(func(wire.Frame) error {1015+ tr.EXPECT().Capabilities().Return(protocol.ConnectionCapabilities{
1016+ OutputDataLimit: protocol.MaxOutputDataLen,
1017+ PreferredOutputWindow: protocol.MaxOutputWindow,
1018+ }).Maybe()
1019+ tr.EXPECT().SendOutput(mock.Anything).RunAndReturn(func(protocol.Output) error {
1016 close(enteredSend)1020 close(enteredSend)
1017 <-releaseSend1021 <-releaseSend
1018 return nil1022 return nil
@@ -1408,8 +1412,13 @@ func TestResumeStoppedAndSwitchInheritsTerminalEnv(t *testing.T) {
1408 })).Return(floating, nil).Once()1412 })).Return(floating, nil).Once()
1409 d := newTestDaemon(t, f, stubClock{})1413 d := newTestDaemon(t, f, stubClock{})
1410 d.inactive["old"] = inactiveSession{name: "old", cwd: t.TempDir(), createdAt: 1, state: protocol.SessionDown}1414 d.inactive["old"] = inactiveSession{name: "old", cwd: t.TempDir(), createdAt: 1, state: protocol.SessionDown}
1411- tr := portsmocks.NewMockTransport(t)1415+ tr := portsmocks.NewMockServerConnection(t)
1412- tr.EXPECT().Send(mock.Anything).Return(nil).Maybe()1416+ tr.EXPECT().Capabilities().Return(protocol.ConnectionCapabilities{
1417+ OutputDataLimit: protocol.MaxOutputDataLen,
1418+ PreferredOutputWindow: protocol.MaxOutputWindow,
1419+ }).Maybe()
1420+ tr.EXPECT().SendOutput(mock.Anything).Return(nil).Maybe()
1421+ tr.EXPECT().SendServer(mock.Anything).Return(nil).Maybe()
1413 tr.EXPECT().Close().Return(nil).Maybe()1422 tr.EXPECT().Close().Return(nil).Maybe()
1414 sess, ac, err := d.route(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentNew, Name: "work", Size: sz, TrueColor: true}, tr)1423 sess, ac, err := d.route(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentNew, Name: "work", Size: sz, TrueColor: true}, tr)
1415 require.NoError(t, err)1424 require.NoError(t, err)
M internal/usecase/daemon/remote_attach.go +7 -15
@@ -7,14 +7,13 @@ import (
7 "github.com/bnema/vev/internal/domain"7 "github.com/bnema/vev/internal/domain"
8 "github.com/bnema/vev/internal/ports"8 "github.com/bnema/vev/internal/ports"
9 "github.com/bnema/vev/internal/protocol"9 "github.com/bnema/vev/internal/protocol"
10- "github.com/bnema/vev/internal/protocol/wire"
11 )10 )
12 11
13 // routeRemoteTargetWithContext is the exact-target branch for picker handoffs.12 // routeRemoteTargetWithContext is the exact-target branch for picker handoffs.
14 // It is deliberately separate from legacy name routing: a lifecycle mismatch,13 // It is deliberately separate from legacy name routing: a lifecycle mismatch,
15 // missing tab, broken record, or stopped-record race is a no-such-target error,14 // missing tab, broken record, or stopped-record race is a no-such-target error,
16 // never an invitation to attach a same-name replacement.15 // never an invitation to attach a same-name replacement.
17-func (d *Daemon) routeRemoteTargetWithContext(ctx context.Context, h protocol.Hello, tr ports.Transport) (*session, *attachedClient, error) {16+func (d *Daemon) routeRemoteTargetWithContext(ctx context.Context, h protocol.Hello, tr ports.ServerConnection) (*session, *attachedClient, error) {
18 if h.RemoteTarget == nil {17 if h.RemoteTarget == nil {
19 return nil, nil, errors.New("daemon: missing remote target")18 return nil, nil, errors.New("daemon: missing remote target")
20 }19 }
@@ -120,12 +119,7 @@ func (d *Daemon) sendNavigationActionForAttachment(effect *attachmentEffect, act
120 effect.ac.clearParkedRoute()119 effect.ac.clearParkedRoute()
121 }120 }
122 }121 }
123- payload := wire.MarshalNavigationDirective(directive)122+ if err := effect.sendControl(directive); err != nil {
124- if payload == nil {
125- rollback()
126- return errAttachmentTransition
127- }
128- if err := effect.sendControl(wire.Frame{Type: wire.MsgNavigationAction, Payload: payload}); err != nil {
129 rollback()123 rollback()
130 return err124 return err
131 }125 }
@@ -133,11 +127,10 @@ func (d *Daemon) sendNavigationActionForAttachment(effect *attachmentEffect, act
133 }127 }
134 128
135 func (d *Daemon) sendRecentRouteNavigationActionForAttachment(effect *attachmentEffect, action protocol.RouteNavigationAction) error {129 func (d *Daemon) sendRecentRouteNavigationActionForAttachment(effect *attachmentEffect, action protocol.RouteNavigationAction) error {
136- payload, err := wire.MarshalRouteNavigationAction(action)130+ if action.SnapshotGeneration == 0 || action.Key == 0 || action.Generation == 0 {
137- if err != nil {
138 return errAttachmentTransition131 return errAttachmentTransition
139 }132 }
140- return effect.sendControl(wire.Frame{Type: wire.MsgNavigateRecentRoute, Payload: payload})133+ return effect.sendControl(action)
141 }134 }
142 135
143 func (d *Daemon) sendCommittedRouteIdentityForAttachment(effect *attachmentEffect) error {136 func (d *Daemon) sendCommittedRouteIdentityForAttachment(effect *attachmentEffect) error {
@@ -150,14 +143,13 @@ func (d *Daemon) sendCommittedRouteIdentityForAttachment(effect *attachmentEffec
150 Ephemeral: effect.sess.ephemeral,143 Ephemeral: effect.sess.ephemeral,
151 }144 }
152 effect.sess.mu.Unlock()145 effect.sess.mu.Unlock()
153- payload, err := wire.MarshalCommittedRouteIdentity(identity)146+ if err := identity.Validate(); err != nil {
154- if err != nil {
155 return errAttachmentTransition147 return errAttachmentTransition
156 }148 }
157- return effect.sendControl(wire.Frame{Type: wire.MsgCommittedRouteIdentity, Payload: payload})149+ return effect.sendControl(identity)
158 }150 }
159 151
160-func (d *Daemon) finishRouteAttach(sess *session, tr ports.Transport, sz domain.Size, h protocol.Hello, routeCreated, purge bool) (*attachedClient, error) {152+func (d *Daemon) finishRouteAttach(sess *session, tr ports.ServerConnection, sz domain.Size, h protocol.Hello, routeCreated, purge bool) (*attachedClient, error) {
161 ac, err := d.finishAttach(sess, tr, sz, h)153 ac, err := d.finishAttach(sess, tr, sz, h)
162 if err != nil && routeCreated {154 if err != nil && routeCreated {
163 if cleanupErr := d.killSessionIfEmpty(sess, protocol.ReasonSessionKilled, purge); cleanupErr != nil {155 if cleanupErr := d.killSessionIfEmpty(sess, protocol.ReasonSessionKilled, purge); cleanupErr != nil {
M internal/usecase/daemon/remote_picker_test.go +4 -4
@@ -890,8 +890,8 @@ func TestRemotePickerHandoffSendFailureKeepsPickerOpen(t *testing.T) {
890 d.remoteCatalog.status["arch"] = remoteHostFresh890 d.remoteCatalog.status["arch"] = remoteHostFresh
891 d.remoteCatalog.mu.Unlock()891 d.remoteCatalog.mu.Unlock()
892 cause := errors.New("remote attach send failed")892 cause := errors.New("remote attach send failed")
893- tr := portsmocks.NewMockTransport(t)893+ tr := portsmocks.NewMockServerConnection(t)
894- tr.EXPECT().Send(mock.Anything).Return(cause)894+ tr.EXPECT().SendServer(mock.Anything).Return(cause)
895 sess, ac, _ := addRemoteRefreshPickerOwner(t, d, "local", tr)895 sess, ac, _ := addRemoteRefreshPickerOwner(t, d, "local", tr)
896 ac.overlays.pickerMu.Lock()896 ac.overlays.pickerMu.Lock()
897 ac.overlays.picker = picker.New(nil, picker.SelectionConfig{})897 ac.overlays.picker = picker.New(nil, picker.SelectionConfig{})
@@ -921,9 +921,9 @@ func TestRemotePickerHandoffSendFailureKeepsPickerOpen(t *testing.T) {
921 require.Same(t, sess, ac.currentAttachmentSession())921 require.Same(t, sess, ac.currentAttachmentSession())
922 }922 }
923 923
924-func addRemoteRefreshPickerOwner(t *testing.T, d *Daemon, id domain.SessionID, transports ...ports.Transport) (*session, *attachedClient, chan wire.Frame) {924+func addRemoteRefreshPickerOwner(t *testing.T, d *Daemon, id domain.SessionID, transports ...ports.ServerConnection) (*session, *attachedClient, chan wire.Frame) {
925 t.Helper()925 t.Helper()
926- var tr ports.Transport926+ var tr ports.ServerConnection
927 var sends chan wire.Frame927 var sends chan wire.Frame
928 if len(transports) != 0 {928 if len(transports) != 0 {
929 tr = transports[0]929 tr = transports[0]
M internal/usecase/daemon/remote_preview.go +5 -11
@@ -7,15 +7,10 @@ import (
7 "github.com/bnema/vev/internal/domain"7 "github.com/bnema/vev/internal/domain"
8 "github.com/bnema/vev/internal/ports"8 "github.com/bnema/vev/internal/ports"
9 "github.com/bnema/vev/internal/protocol"9 "github.com/bnema/vev/internal/protocol"
10- "github.com/bnema/vev/internal/protocol/wire"
11 )10 )
12 11
13-func (d *Daemon) handleRemotePreview(tr ports.Transport, frame wire.Frame) error {12+func (d *Daemon) handleRemotePreview(tr ports.ServerConnection, request protocol.RemotePreviewRequest) error {
14 defer func() { _ = tr.Close() }()13 defer func() { _ = tr.Close() }()
15- request, err := wire.UnmarshalRemotePreviewRequest(frame.Payload)
16- if err != nil {
17- return d.sendRemotePreview(tr, protocol.RemotePreview{Version: protocol.RemotePreviewSchemaVersion, Status: protocol.RemotePreviewMalformed})
18- }
19 preview, err := d.captureRemotePreview(request)14 preview, err := d.captureRemotePreview(request)
20 if err != nil {15 if err != nil {
21 status := protocol.RemotePreviewUnavailable16 status := protocol.RemotePreviewUnavailable
@@ -27,12 +22,11 @@ func (d *Daemon) handleRemotePreview(tr ports.Transport, frame wire.Frame) error
27 return d.sendRemotePreview(tr, preview)22 return d.sendRemotePreview(tr, preview)
28 }23 }
29 24
30-func (d *Daemon) sendRemotePreview(tr ports.Transport, preview protocol.RemotePreview) error {25+func (d *Daemon) sendRemotePreview(tr ports.ServerConnection, preview protocol.RemotePreview) error {
31- payload := wire.MarshalRemotePreview(preview)26+ if protocol.ValidateRemotePreview(preview) != nil {
32- if payload == nil {27+ preview = protocol.RemotePreview{Version: protocol.RemotePreviewSchemaVersion, Status: protocol.RemotePreviewMalformed}
33- payload = wire.MarshalRemotePreview(protocol.RemotePreview{Version: protocol.RemotePreviewSchemaVersion, Status: protocol.RemotePreviewMalformed})
34 }28 }
35- return d.boundedControlSend(tr, wire.Frame{Type: wire.MsgRemotePreviewResponse, Payload: payload})29+ return d.boundedControlSend(tr, preview)
36 }30 }
37 31
38 var errRemotePreviewNoSuchTarget = errors.New("daemon: remote preview target does not exist")32 var errRemotePreviewNoSuchTarget = errors.New("daemon: remote preview target does not exist")
M internal/usecase/daemon/remote_route_test.go +1 -1
@@ -183,7 +183,7 @@ func TestFailedHandshakeCleanupPreservesConcurrentAttachment(t *testing.T) {
183 d.clientGone(sess, winner(), winnerTransport, false)183 d.clientGone(sess, winner(), winnerTransport, false)
184 }184 }
185 185
186-func attachWhenRouteCleanupSnapshots(t *testing.T, d *Daemon, sess *session) (func() *attachedClient, ports.Transport) {186+func attachWhenRouteCleanupSnapshots(t *testing.T, d *Daemon, sess *session) (func() *attachedClient, ports.ServerConnection) {
187 t.Helper()187 t.Helper()
188 winnerTransport, _ := newCapturingTransport(t)188 winnerTransport, _ := newCapturingTransport(t)
189 var winner *attachedClient189 var winner *attachedClient
M internal/usecase/daemon/render_damage_receipt_test.go +3 -3
@@ -108,7 +108,7 @@ func TestRenderDamageReceiptRetainsRealVTDamageAcrossFailedEmission(t *testing.T
108 for _, tt := range []struct {108 for _, tt := range []struct {
109 name string109 name string
110 fail func(*composedRenderFrame, *attachedClient)110 fail func(*composedRenderFrame, *attachedClient)
111- restore func(*session, *attachedClient, ports.Transport)111+ restore func(*session, *attachedClient, ports.ServerConnection)
112 }{112 }{
113 {113 {
114 name: "prepare failure",114 name: "prepare failure",
@@ -116,12 +116,12 @@ func TestRenderDamageReceiptRetainsRealVTDamageAcrossFailedEmission(t *testing.T
116 // An invalid frame makes the real output.prepare reject after capture.116 // An invalid frame makes the real output.prepare reject after capture.
117 c.frame = renderer.Frame{Width: 1}117 c.frame = renderer.Frame{Width: 1}
118 },118 },
119- restore: func(_ *session, _ *attachedClient, _ ports.Transport) {},119+ restore: func(_ *session, _ *attachedClient, _ ports.ServerConnection) {},
120 },120 },
121 {121 {
122 name: "send failure",122 name: "send failure",
123 fail: func(_ *composedRenderFrame, ac *attachedClient) { ac.replaceTransport(cacheFailTransport{}) },123 fail: func(_ *composedRenderFrame, ac *attachedClient) { ac.replaceTransport(cacheFailTransport{}) },
124- restore: func(sess *session, ac *attachedClient, healthy ports.Transport) {124+ restore: func(sess *session, ac *attachedClient, healthy ports.ServerConnection) {
125 sess.mu.Lock()125 sess.mu.Lock()
126 sess.registerAttachmentLocked(ac)126 sess.registerAttachmentLocked(ac)
127 sess.mu.Unlock()127 sess.mu.Unlock()
M internal/usecase/daemon/render_pipeline.go +9 -13
@@ -7,7 +7,6 @@ import (
7 "github.com/bnema/vev/internal/domain"7 "github.com/bnema/vev/internal/domain"
8 "github.com/bnema/vev/internal/ports"8 "github.com/bnema/vev/internal/ports"
9 "github.com/bnema/vev/internal/protocol"9 "github.com/bnema/vev/internal/protocol"
10- "github.com/bnema/vev/internal/protocol/wire"
11 "github.com/bnema/vev/internal/usecase/layout"10 "github.com/bnema/vev/internal/usecase/layout"
12 "github.com/bnema/vev/internal/usecase/notices"11 "github.com/bnema/vev/internal/usecase/notices"
13 "github.com/bnema/vev/internal/usecase/palette"12 "github.com/bnema/vev/internal/usecase/palette"
@@ -475,7 +474,7 @@ func (d *Daemon) emitFrame(entry *session, ac *attachedClient, state *capturedRe
475 d.invalidateRender(entry, ac, true, "render_pipeline.go:prepare-failed")474 d.invalidateRender(entry, ac, true, "render_pipeline.go:prepare-failed")
476 return true475 return true
477 }476 }
478- var sendTr ports.Transport477+ var sendTr ports.ServerConnection
479 var sendErr error478 var sendErr error
480 suppressedGraphics := state.suppressedGraphics && !ac.terminalCapabilities.SupportsKittyGraphics()479 suppressedGraphics := state.suppressedGraphics && !ac.terminalCapabilities.SupportsKittyGraphics()
481 if len(data) > 0 {480 if len(data) > 0 {
@@ -485,9 +484,9 @@ func (d *Daemon) emitFrame(entry *session, ac *attachedClient, state *capturedRe
485 if sendTr == nil {484 if sendTr == nil {
486 sendErr = errors.New("client transport is nil")485 sendErr = errors.New("client transport is nil")
487 } else {486 } else {
488- send := sendTr.Send487+ send := sendTr.SendOutput
489- if async, ok := sendTr.(ports.AsyncTransport); ok {488+ if sendTr.Capabilities().AsyncSend {
490- send = async.SendAsync489+ send = sendTr.SendOutputAsync
491 }490 }
492 interruptible := false491 interruptible := false
493 if marks.attachmentEffect != nil {492 if marks.attachmentEffect != nil {
@@ -540,9 +539,8 @@ func (d *Daemon) emitFrame(entry *session, ac *attachedClient, state *capturedRe
540 }539 }
541 540
542 if position.ActiveTabID != "" && position != ac.output.lastRoutePosition {541 if position.ActiveTabID != "" && position != ac.output.lastRoutePosition {
543- payload, positionMarshalErr := wire.MarshalRoutePosition(position)542+ if positionValidateErr := position.Validate(); positionValidateErr != nil {
544- if positionMarshalErr != nil {543+ d.log.Error("validate route position", "err", positionValidateErr)
545- d.log.Error("marshal route position", "err", positionMarshalErr)
546 } else {544 } else {
547 sendTransport := ac.transportSnapshot()545 sendTransport := ac.transportSnapshot()
548 sendTr = sendTransport.transport546 sendTr = sendTransport.transport
@@ -556,12 +554,10 @@ func (d *Daemon) emitFrame(entry *session, ac *attachedClient, state *capturedRe
556 if sendErr == nil {554 if sendErr == nil {
557 if sendTr == nil {555 if sendTr == nil {
558 sendErr = errors.New("client transport is nil")556 sendErr = errors.New("client transport is nil")
557+ } else if sendTr.Capabilities().AsyncSend {
558+ sendErr = sendTr.SendServerAsync(position)
559 } else {559 } else {
560- send := sendTr.Send560+ sendErr = sendTr.SendServer(position)
561- if async, ok := sendTr.(ports.AsyncTransport); ok {
562- send = async.SendAsync
563- }
564- sendErr = send(wire.Frame{Type: wire.MsgRoutePosition, Payload: payload})
565 }561 }
566 }562 }
567 if marks.attachmentEffect != nil && interruptible {563 if marks.attachmentEffect != nil && interruptible {
M internal/usecase/daemon/render_pipeline_test.go +2 -2
@@ -333,10 +333,10 @@ func TestComposeEmitExactReplayTiledFloatingBarsOverlayAndCursor(t *testing.T) {
333 prepared, err := stream.prepareFrame(nil, &state, composed.frame, composed.damage, composed.reset, composed.cursor)333 prepared, err := stream.prepareFrame(nil, &state, composed.frame, composed.damage, composed.reset, composed.cursor)
334 require.NoError(t, err)334 require.NoError(t, err)
335 var outputFrame wire.Frame335 var outputFrame wire.Frame
336- require.NoError(t, prepared.send(0, func(frame wire.Frame) error {336+ require.NoError(t, prepared.send(0, outputFrameSender(func(frame wire.Frame) error {
337 outputFrame = frame337 outputFrame = frame
338 return nil338 return nil
339- }))339+ })))
340 output, err := wire.UnmarshalOutput(outputFrame.Payload)340 output, err := wire.UnmarshalOutput(outputFrame.Payload)
341 require.NoError(t, err)341 require.NoError(t, err)
342 terminalBytes := output.Data342 terminalBytes := output.Data
M internal/usecase/daemon/render_test.go +3 -3
@@ -676,7 +676,7 @@ func TestResizePreservesLiveContentAndEvictsScrollback(t *testing.T) {
676 }676 }
677 win.focusedPane().screen.Row = 3677 win.focusedPane().screen.Row = 3
678 678
679- tr := portsmocks.NewMockTransport(t)679+ tr := newMockServerConnection(t)
680 tr.EXPECT().Close().Return(nil).Maybe()680 tr.EXPECT().Close().Return(nil).Maybe()
681 tr.EXPECT().Send(mock.Anything).Return(nil).Maybe()681 tr.EXPECT().Send(mock.Anything).Return(nil).Maybe()
682 682
@@ -931,7 +931,7 @@ func TestResizeOrdersPTYBeforeScreen(t *testing.T) {
931 win.focusedPane().pty = p931 win.focusedPane().pty = p
932 932
933 var gotOutput atomic.Bool933 var gotOutput atomic.Bool
934- tr := portsmocks.NewMockTransport(t)934+ tr := newMockServerConnection(t)
935 tr.EXPECT().Close().Return(nil).Maybe()935 tr.EXPECT().Close().Return(nil).Maybe()
936 tr.EXPECT().Send(mock.Anything).RunAndReturn(func(f wire.Frame) error {936 tr.EXPECT().Send(mock.Anything).RunAndReturn(func(f wire.Frame) error {
937 if f.Type == wire.MsgOutput {937 if f.Type == wire.MsgOutput {
@@ -963,7 +963,7 @@ func TestSendErrorKeepsEphemeralHeadless(t *testing.T) {
963 p := portsmocks.NewMockPTY(t)963 p := portsmocks.NewMockPTY(t)
964 p.EXPECT().Close().Return(nil).Maybe()964 p.EXPECT().Close().Return(nil).Maybe()
965 965
966- tr := portsmocks.NewMockTransport(t)966+ tr := newMockServerConnection(t)
967 tr.EXPECT().Send(mock.Anything).Return(io.ErrClosedPipe).Maybe()967 tr.EXPECT().Send(mock.Anything).Return(io.ErrClosedPipe).Maybe()
968 tr.EXPECT().Close().Return(nil).Maybe()968 tr.EXPECT().Close().Return(nil).Maybe()
969 969
M internal/usecase/daemon/resume.go +3 -3
@@ -480,7 +480,7 @@ func (d *Daemon) finishParkedAttachmentRetirements(retirements []parkedAttachmen
480 // to the named session's active attachment because transport teardown has not480 // to the named session's active attachment because transport teardown has not
481 // parked it yet. Only the exact live owner (same session, token, and client ID)481 // parked it yet. Only the exact live owner (same session, token, and client ID)
482 // is accepted; arbitrary unknown tokens stay fail-closed.482 // is accepted; arbitrary unknown tokens stay fail-closed.
483-func (d *Daemon) resumeLiveAttachment(h protocol.Hello, tr ports.Transport, sz domain.Size) (*session, *attachedClient, bool, error) {483+func (d *Daemon) resumeLiveAttachment(h protocol.Hello, tr ports.ServerConnection, sz domain.Size) (*session, *attachedClient, bool, error) {
484 d.mu.Lock()484 d.mu.Lock()
485 if d.closing {485 if d.closing {
486 d.mu.Unlock()486 d.mu.Unlock()
@@ -583,7 +583,7 @@ func (d *Daemon) resumeLiveAttachment(h protocol.Hello, tr ports.Transport, sz d
583 // registry lock, preserving the global sendMu > Daemon.mu ordering. The parked583 // registry lock, preserving the global sendMu > Daemon.mu ordering. The parked
584 // entry is revalidated after both locks are held because it may expire between584 // entry is revalidated after both locks are held because it may expire between
585 // the initial lookup and lock acquisition.585 // the initial lookup and lock acquisition.
586-func (d *Daemon) resumeParked(h protocol.Hello, tr ports.Transport, sz domain.Size) (*session, *attachedClient, bool, error) {586+func (d *Daemon) resumeParked(h protocol.Hello, tr ports.ServerConnection, sz domain.Size) (*session, *attachedClient, bool, error) {
587 d.mu.Lock()587 d.mu.Lock()
588 parked := d.parked[h.ResumeToken]588 parked := d.parked[h.ResumeToken]
589 d.mu.Unlock()589 d.mu.Unlock()
@@ -632,7 +632,7 @@ func (d *Daemon) resumeParked(h protocol.Hello, tr ports.Transport, sz domain.Si
632 632
633 // resumeParkedLocked completes a validated resume. Caller holds both ac.sendMu633 // resumeParkedLocked completes a validated resume. Caller holds both ac.sendMu
634 // and d.mu in that order.634 // and d.mu in that order.
635-func (d *Daemon) resumeParkedLocked(h protocol.Hello, tr ports.Transport, sz domain.Size) (*session, *attachedClient, bool, error) {635+func (d *Daemon) resumeParkedLocked(h protocol.Hello, tr ports.ServerConnection, sz domain.Size) (*session, *attachedClient, bool, error) {
636 parked := d.parked[h.ResumeToken]636 parked := d.parked[h.ResumeToken]
637 if parked == nil || h.ResumeToken == 0 {637 if parked == nil || h.ResumeToken == 0 {
638 return nil, nil, false, nil638 return nil, nil, false, nil
M internal/usecase/daemon/resume_test.go +3 -3
@@ -254,14 +254,14 @@ func TestBoundedSendTimeoutCannotTargetResumedTransport(t *testing.T) {
254 expected := ac.transportSnapshot()254 expected := ac.transportSnapshot()
255 orphanDone := make(chan struct{})255 orphanDone := make(chan struct{})
256 type boundedResult struct {256 type boundedResult struct {
257- transport ports.Transport257+ transport ports.ServerConnection
258 err error258 err error
259 }259 }
260 result := make(chan boundedResult, 1)260 result := make(chan boundedResult, 1)
261 go func() {261 go func() {
262 transport, sendErr := d.boundedSendWithTimeout(time.Second, expected.transport, func() error {262 transport, sendErr := d.boundedSendWithTimeout(time.Second, expected.transport, func() error {
263 defer close(orphanDone)263 defer close(orphanDone)
264- return ac.sendExpectedTransport(expected, frameDetached(protocol.ReasonDetach))264+ return ac.sendExpectedTransport(expected, serverDetached(protocol.ReasonDetach))
265 })265 })
266 result <- boundedResult{transport: transport, err: sendErr}266 result <- boundedResult{transport: transport, err: sendErr}
267 }()267 }()
@@ -327,7 +327,7 @@ func TestHandleHelloResumeDefersFreshOutputUntilWelcome(t *testing.T) {
327 done := make(chan struct{})327 done := make(chan struct{})
328 resumeHello := helloResumeCapable(protocol.IntentResume, sess.name, token)328 resumeHello := helloResumeCapable(protocol.IntentResume, sess.name, token)
329 go func() {329 go func() {
330- d.handleHello(tr.tr, wire.Frame{Type: wire.MsgHello, Payload: wire.MarshalHello(resumeHello)})330+ d.handleHelloFrame(tr.tr, wire.Frame{Type: wire.MsgHello, Payload: wire.MarshalHello(resumeHello)})
331 close(done)331 close(done)
332 }()332 }()
333 333
M internal/usecase/daemon/same_peer_switch.go +3 -4
@@ -3,7 +3,6 @@ package daemon
3 import (3 import (
4 "github.com/bnema/vev/internal/domain"4 "github.com/bnema/vev/internal/domain"
5 "github.com/bnema/vev/internal/protocol"5 "github.com/bnema/vev/internal/protocol"
6- "github.com/bnema/vev/internal/protocol/wire"
7 )6 )
8 7
9 func (ac *attachedClient) offerSamePeerTarget(target protocol.ExactSessionTarget) {8 func (ac *attachedClient) offerSamePeerTarget(target protocol.ExactSessionTarget) {
@@ -122,11 +121,11 @@ func (d *Daemon) sendSamePeerSwitchFailure(effect *attachmentEffect, requestID u
122 }121 }
123 defer sender.End()122 defer sender.End()
124 }123 }
125- payload, err := wire.MarshalSamePeerSwitchFailure(protocol.SamePeerSwitchFailure{RequestID: requestID, Code: code})124+ failure := protocol.SamePeerSwitchFailure{RequestID: requestID, Code: code}
126- if err != nil {125+ if failure.Validate() != nil {
127 return126 return
128 }127 }
129- if err := sender.sendControl(wire.Frame{Type: wire.MsgSamePeerSwitchFailure, Payload: payload}); err != nil {128+ if err := sender.sendControl(failure); err != nil {
130 d.detachOnAttachmentSendError(sender.capability(), sender.transport.transport)129 d.detachOnAttachmentSendError(sender.capability(), sender.transport.transport)
131 }130 }
132 }131 }
M internal/usecase/daemon/session_test.go +15 -15
@@ -398,7 +398,7 @@ func TestServeReturnsWhenLastSessionExits(t *testing.T) {
398 p, releasePTY := newBlockingPTY(t)398 p, releasePTY := newBlockingPTY(t)
399 tr, sends, _ := newConn(t, mustHello(protocol.IntentEphemeral, "", domain.Size{Cols: 80, Rows: 24}))399 tr, sends, _ := newConn(t, mustHello(protocol.IntentEphemeral, "", domain.Size{Cols: 80, Rows: 24}))
400 400
401- l := portsmocks.NewMockListener(t)401+ l := newMockServerListener(t)
402 connCh := make(chan ports.Transport, 1)402 connCh := make(chan ports.Transport, 1)
403 connCh <- tr403 connCh <- tr
404 closed := make(chan struct{})404 closed := make(chan struct{})
@@ -438,7 +438,7 @@ func TestServeGracefulShutdownOnContextCancel(t *testing.T) {
438 defer releasePTY()438 defer releasePTY()
439 tr, sends, _ := newConn(t, mustHello(protocol.IntentNew, "long", domain.Size{Cols: 80, Rows: 24}))439 tr, sends, _ := newConn(t, mustHello(protocol.IntentNew, "long", domain.Size{Cols: 80, Rows: 24}))
440 440
441- l := portsmocks.NewMockListener(t)441+ l := newMockServerListener(t)
442 connCh := make(chan ports.Transport, 1)442 connCh := make(chan ports.Transport, 1)
443 connCh <- tr443 connCh <- tr
444 closed := make(chan struct{})444 closed := make(chan struct{})
@@ -573,7 +573,7 @@ func TestHelloRacingShutdownIsRejected(t *testing.T) {
573 func TestServeReturnsDespiteWedgedClientOnShutdown(t *testing.T) {573 func TestServeReturnsDespiteWedgedClientOnShutdown(t *testing.T) {
574 p, _ := newBlockingPTY(t) // Close unblocks the parked reader574 p, _ := newBlockingPTY(t) // Close unblocks the parked reader
575 575
576- tr := portsmocks.NewMockTransport(t)576+ tr := newMockServerConnection(t)
577 sends := make(chan wire.Frame, 64)577 sends := make(chan wire.Frame, 64)
578 recvCh := make(chan wire.Frame, 1)578 recvCh := make(chan wire.Frame, 1)
579 recvCh <- mustHello(protocol.IntentNew, "wedge", domain.Size{Cols: 80, Rows: 24})579 recvCh <- mustHello(protocol.IntentNew, "wedge", domain.Size{Cols: 80, Rows: 24})
@@ -600,7 +600,7 @@ func TestServeReturnsDespiteWedgedClientOnShutdown(t *testing.T) {
600 }).Maybe()600 }).Maybe()
601 tr.EXPECT().Close().RunAndReturn(func() error { closeConn(); return nil }).Maybe()601 tr.EXPECT().Close().RunAndReturn(func() error { closeConn(); return nil }).Maybe()
602 602
603- l := portsmocks.NewMockListener(t)603+ l := newMockServerListener(t)
604 connCh := make(chan ports.Transport, 1)604 connCh := make(chan ports.Transport, 1)
605 connCh <- tr605 connCh <- tr
606 lnClosed := make(chan struct{})606 lnClosed := make(chan struct{})
@@ -1021,7 +1021,7 @@ func TestAttachRestoresPersistedTabNames(t *testing.T) {
1021 require.NoError(t, err)1021 require.NoError(t, err)
1022 require.True(t, ok)1022 require.True(t, ok)
1023 d.inactive["work"] = inactiveSessionFromRecord(record, protocol.SessionDown, nil)1023 d.inactive["work"] = inactiveSessionFromRecord(record, protocol.SessionDown, nil)
1024- tr := portsmocks.NewMockTransport(t)1024+ tr := newMockServerConnection(t)
1025 tr.EXPECT().Send(mock.Anything).Return(nil).Maybe()1025 tr.EXPECT().Send(mock.Anything).Return(nil).Maybe()
1026 tr.EXPECT().Close().Return(nil).Maybe()1026 tr.EXPECT().Close().Return(nil).Maybe()
1027 1027
@@ -1195,7 +1195,7 @@ func TestAttachResumesStoppedSessionFromStoredCwd(t *testing.T) {
1195 WithStore(t, store)(d)1195 WithStore(t, store)(d)
1196 cwd := t.TempDir()1196 cwd := t.TempDir()
1197 d.inactive["work"] = inactiveSession{name: "work", cwd: cwd, createdAt: 1, state: protocol.SessionDown}1197 d.inactive["work"] = inactiveSession{name: "work", cwd: cwd, createdAt: 1, state: protocol.SessionDown}
1198- tr := portsmocks.NewMockTransport(t)1198+ tr := newMockServerConnection(t)
1199 tr.EXPECT().Send(mock.Anything).Return(nil).Maybe()1199 tr.EXPECT().Send(mock.Anything).Return(nil).Maybe()
1200 tr.EXPECT().Close().Return(nil).Maybe()1200 tr.EXPECT().Close().Return(nil).Maybe()
1201 1201
@@ -1218,7 +1218,7 @@ func TestAttachStoppedMissingCwdFallsBackToHome(t *testing.T) {
1218 d := newTestDaemon(t, newFactory(t, p), stubClock{})1218 d := newTestDaemon(t, newFactory(t, p), stubClock{})
1219 WithStore(t, store)(d)1219 WithStore(t, store)(d)
1220 d.inactive["work"] = inactiveSession{name: "work", cwd: "/definitely/missing/vev", createdAt: 1, state: protocol.SessionDown}1220 d.inactive["work"] = inactiveSession{name: "work", cwd: "/definitely/missing/vev", createdAt: 1, state: protocol.SessionDown}
1221- tr := portsmocks.NewMockTransport(t)1221+ tr := newMockServerConnection(t)
1222 tr.EXPECT().Send(mock.Anything).Return(nil).Maybe()1222 tr.EXPECT().Send(mock.Anything).Return(nil).Maybe()
1223 tr.EXPECT().Close().Return(nil).Maybe()1223 tr.EXPECT().Close().Return(nil).Maybe()
1224 1224
@@ -1299,14 +1299,14 @@ func TestNewSessionAssignsStableIDsAndChildEnv(t *testing.T) {
1299 func TestIntentNewStoppedNameRejected(t *testing.T) {1299 func TestIntentNewStoppedNameRejected(t *testing.T) {
1300 d := newTestDaemon(t, portsmocks.NewMockPTYFactory(t), stubClock{})1300 d := newTestDaemon(t, portsmocks.NewMockPTYFactory(t), stubClock{})
1301 d.inactive["taken"] = inactiveSession{name: "taken", cwd: "/tmp", createdAt: 1}1301 d.inactive["taken"] = inactiveSession{name: "taken", cwd: "/tmp", createdAt: 1}
1302- tr := portsmocks.NewMockTransport(t)1302+ tr := newMockServerConnection(t)
1303 _, _, err := d.route(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentNew, Name: "taken", Size: domain.Size{Cols: 80, Rows: 24}}, tr)1303 _, _, err := d.route(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentNew, Name: "taken", Size: domain.Size{Cols: 80, Rows: 24}}, tr)
1304 require.ErrorContains(t, err, "name already in use")1304 require.ErrorContains(t, err, "name already in use")
1305 }1305 }
1306 1306
1307 func TestIntentNewUnsafeNameRejected(t *testing.T) {1307 func TestIntentNewUnsafeNameRejected(t *testing.T) {
1308 d := newTestDaemon(t, portsmocks.NewMockPTYFactory(t), stubClock{})1308 d := newTestDaemon(t, portsmocks.NewMockPTYFactory(t), stubClock{})
1309- tr := portsmocks.NewMockTransport(t)1309+ tr := newMockServerConnection(t)
1310 _, _, err := d.route(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentNew, Name: "my work", Size: domain.Size{Cols: 80, Rows: 24}}, tr)1310 _, _, err := d.route(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentNew, Name: "my work", Size: domain.Size{Cols: 80, Rows: 24}}, tr)
1311 require.ErrorContains(t, err, domain.ErrInvalidSessionName.Error())1311 require.ErrorContains(t, err, domain.ErrInvalidSessionName.Error())
1312 }1312 }
@@ -1446,7 +1446,7 @@ func TestAttachUpdatesFutureChildEnvTrueColor(t *testing.T) {
1446 1446
1447 d := newTestDaemon(t, f, stubClock{})1447 d := newTestDaemon(t, f, stubClock{})
1448 d.baseEnv = []string{"KEEP=1", "COLORTERM=old"}1448 d.baseEnv = []string{"KEEP=1", "COLORTERM=old"}
1449- tr := portsmocks.NewMockTransport(t)1449+ tr := newMockServerConnection(t)
1450 tr.EXPECT().Send(mock.Anything).Return(nil).Maybe()1450 tr.EXPECT().Send(mock.Anything).Return(nil).Maybe()
1451 tr.EXPECT().Close().Return(nil).Maybe()1451 tr.EXPECT().Close().Return(nil).Maybe()
1452 sess, ac, err := d.route(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentNew, Name: "work", Size: sz, TrueColor: true}, tr)1452 sess, ac, err := d.route(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentNew, Name: "work", Size: sz, TrueColor: true}, tr)
@@ -1488,10 +1488,10 @@ func TestLiveAttachUpdatesFutureChildEnvTrueColor(t *testing.T) {
1488 expectFloatingPrewarmOpen(f, normalSize, floating)1488 expectFloatingPrewarmOpen(f, normalSize, floating)
1489 1489
1490 d := newTestDaemon(t, f, stubClock{})1490 d := newTestDaemon(t, f, stubClock{})
1491- tr1 := portsmocks.NewMockTransport(t)1491+ tr1 := newMockServerConnection(t)
1492 tr1.EXPECT().Send(mock.Anything).Return(nil).Maybe()1492 tr1.EXPECT().Send(mock.Anything).Return(nil).Maybe()
1493 tr1.EXPECT().Close().Return(nil).Maybe()1493 tr1.EXPECT().Close().Return(nil).Maybe()
1494- tr2 := portsmocks.NewMockTransport(t)1494+ tr2 := newMockServerConnection(t)
1495 tr2.EXPECT().Send(mock.Anything).Return(nil).Maybe()1495 tr2.EXPECT().Send(mock.Anything).Return(nil).Maybe()
1496 tr2.EXPECT().Close().Return(nil).Maybe()1496 tr2.EXPECT().Close().Return(nil).Maybe()
1497 sess, _, err := d.route(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentNew, Name: "work", Size: sz, TrueColor: false}, tr1)1497 sess, _, err := d.route(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentNew, Name: "work", Size: sz, TrueColor: false}, tr1)
@@ -1532,7 +1532,7 @@ func TestCreateSessionAndSwitchInheritsTerminalEnv(t *testing.T) {
1532 floating := newQuietPTY()1532 floating := newQuietPTY()
1533 expectFloatingPrewarmOpen(f, normalSize, floating)1533 expectFloatingPrewarmOpen(f, normalSize, floating)
1534 d := newTestDaemon(t, f, stubClock{})1534 d := newTestDaemon(t, f, stubClock{})
1535- tr := portsmocks.NewMockTransport(t)1535+ tr := newMockServerConnection(t)
1536 tr.EXPECT().Send(mock.Anything).Return(nil).Maybe()1536 tr.EXPECT().Send(mock.Anything).Return(nil).Maybe()
1537 tr.EXPECT().Close().Return(nil).Maybe()1537 tr.EXPECT().Close().Return(nil).Maybe()
1538 sess, ac, err := d.route(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentNew, Name: "work", Size: sz, TrueColor: true}, tr)1538 sess, ac, err := d.route(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentNew, Name: "work", Size: sz, TrueColor: true}, tr)
@@ -1601,10 +1601,10 @@ func TestAttachEnvironmentReplacesFuturePTYInputs(t *testing.T) {
1601 floating := newQuietPTY()1601 floating := newQuietPTY()
1602 expectFloatingPrewarmOpen(f, normalSize, floating)1602 expectFloatingPrewarmOpen(f, normalSize, floating)
1603 d := newTestDaemon(t, f, stubClock{})1603 d := newTestDaemon(t, f, stubClock{})
1604- tr1 := portsmocks.NewMockTransport(t)1604+ tr1 := newMockServerConnection(t)
1605 tr1.EXPECT().Send(mock.Anything).Return(nil).Maybe()1605 tr1.EXPECT().Send(mock.Anything).Return(nil).Maybe()
1606 tr1.EXPECT().Close().Return(nil).Maybe()1606 tr1.EXPECT().Close().Return(nil).Maybe()
1607- tr2 := portsmocks.NewMockTransport(t)1607+ tr2 := newMockServerConnection(t)
1608 tr2.EXPECT().Send(mock.Anything).Return(nil).Maybe()1608 tr2.EXPECT().Send(mock.Anything).Return(nil).Maybe()
1609 tr2.EXPECT().Close().Return(nil).Maybe()1609 tr2.EXPECT().Close().Return(nil).Maybe()
1610 1610
M internal/usecase/daemon/snapshot_scheduler_test.go +2 -2
@@ -613,9 +613,9 @@ func (c *serveShutdownClock) nextFinalTimer(t *testing.T) *snapshotDeadlineTimer
613 }613 }
614 }614 }
615 615
616-func serveSnapshotListener(t *testing.T, tr ports.Transport) *portsmocks.MockListener {616+func serveSnapshotListener(t *testing.T, tr ports.Transport) *mockServerListener {
617 t.Helper()617 t.Helper()
618- l := portsmocks.NewMockListener(t)618+ l := newMockServerListener(t)
619 connections := make(chan ports.Transport, 1)619 connections := make(chan ports.Transport, 1)
620 connections <- tr620 connections <- tr
621 closed := make(chan struct{})621 closed := make(chan struct{})
A internal/usecase/daemon/typed_connection_test.go +1077 -0
@@ -0,0 +1,1077 @@
1+package daemon
2+
3+import (
4+ "context"
5+ "errors"
6+
7+ "github.com/stretchr/testify/mock"
8+
9+ "github.com/bnema/vev/internal/ports"
10+ portsmocks "github.com/bnema/vev/internal/ports/mocks"
11+ "github.com/bnema/vev/internal/protocol"
12+ "github.com/bnema/vev/internal/protocol/wire"
13+)
14+
15+type mockTestingT interface {
16+ mock.TestingT
17+ Cleanup(func())
18+}
19+
20+type mockServerConnection struct{ *portsmocks.MockTransport }
21+
22+func newMockServerConnection(t mockTestingT) *mockServerConnection {
23+ return &mockServerConnection{MockTransport: portsmocks.NewMockTransport(t)}
24+}
25+
26+func (c *mockServerConnection) ReceiveClient() (protocol.ClientMessage, error) {
27+ return testReceiveClient(c.MockTransport)
28+}
29+func (c *mockServerConnection) SendServer(message protocol.ServerMessage) error {
30+ return testSendServer(c.MockTransport, message)
31+}
32+func (c *mockServerConnection) SendServerAsync(message protocol.ServerMessage) error {
33+ return testSendServerAsync(c.MockTransport, message)
34+}
35+func (c *mockServerConnection) SendServerSynchronous(message protocol.ServerMessage) error {
36+ return testSendServerSynchronous(c.MockTransport, message)
37+}
38+func (c *mockServerConnection) SendOutput(output protocol.Output) error {
39+ return testSendOutput(c.MockTransport, output)
40+}
41+func (c *mockServerConnection) SendOutputAsync(output protocol.Output) error {
42+ return testSendOutputAsync(c.MockTransport, output)
43+}
44+func (c *mockServerConnection) SendOutputSynchronous(output protocol.Output) error {
45+ return testSendOutputSynchronous(c.MockTransport, output)
46+}
47+func (c *mockServerConnection) Capabilities() protocol.ConnectionCapabilities {
48+ return testServerCapabilities(c.MockTransport)
49+}
50+func (c *mockServerConnection) LinkState() ports.LinkState { return testLinkState(c.MockTransport) }
51+func (c *mockServerConnection) LinkEvents() <-chan ports.LinkEvent {
52+ return testLinkEvents(c.MockTransport)
53+}
54+
55+type rawServerConnection struct{ raw ports.Transport }
56+
57+func (c *rawServerConnection) ReceiveClient() (protocol.ClientMessage, error) {
58+ return testReceiveClient(c.raw)
59+}
60+func (c *rawServerConnection) SendServer(message protocol.ServerMessage) error {
61+ return testSendServer(c.raw, message)
62+}
63+func (c *rawServerConnection) SendServerAsync(message protocol.ServerMessage) error {
64+ return testSendServerAsync(c.raw, message)
65+}
66+func (c *rawServerConnection) SendServerSynchronous(message protocol.ServerMessage) error {
67+ return testSendServerSynchronous(c.raw, message)
68+}
69+func (c *rawServerConnection) SendOutput(output protocol.Output) error {
70+ return testSendOutput(c.raw, output)
71+}
72+func (c *rawServerConnection) SendOutputAsync(output protocol.Output) error {
73+ return testSendOutputAsync(c.raw, output)
74+}
75+func (c *rawServerConnection) SendOutputSynchronous(output protocol.Output) error {
76+ return testSendOutputSynchronous(c.raw, output)
77+}
78+func (c *rawServerConnection) Capabilities() protocol.ConnectionCapabilities {
79+ return testServerCapabilities(c.raw)
80+}
81+func (c *rawServerConnection) LinkState() ports.LinkState { return testLinkState(c.raw) }
82+func (c *rawServerConnection) LinkEvents() <-chan ports.LinkEvent { return testLinkEvents(c.raw) }
83+func (c *rawServerConnection) Close() error { return c.raw.Close() }
84+
85+type mockServerListener struct{ *portsmocks.MockListener }
86+
87+func newMockServerListener(t mockTestingT) *mockServerListener {
88+ return &mockServerListener{MockListener: portsmocks.NewMockListener(t)}
89+}
90+
91+func (l *mockServerListener) Accept() (ports.ServerConnection, error) {
92+ raw, err := l.MockListener.Accept()
93+ if err != nil {
94+ return nil, err
95+ }
96+ return &rawServerConnection{raw: raw}, nil
97+}
98+
99+func testReceiveClient(raw ports.Transport) (protocol.ClientMessage, error) {
100+ frame, err := raw.Recv()
101+ if err != nil {
102+ return nil, err
103+ }
104+ message, err := testDecodeClientFrame(frame)
105+ if err == nil {
106+ return message, nil
107+ }
108+ failure := &protocol.DecodeFailure{Category: protocol.DecodeMalformed, Type: uint8(frame.Type), Err: err}
109+ switch frame.Type {
110+ case wire.MsgHello:
111+ failure.Kind = protocol.DecodeMessageHello
112+ failure.Version, _ = wire.PeekHelloVersion(frame.Payload)
113+ case wire.MsgCommand:
114+ failure.Kind = protocol.DecodeMessageCommand
115+ failure.Version, _ = wire.PeekCommandVersion(frame.Payload)
116+ case wire.MsgKill:
117+ failure.Kind = protocol.DecodeMessageKill
118+ case wire.MsgRemotePreviewRequest:
119+ failure.Kind = protocol.DecodeMessageRemotePreview
120+ }
121+ return nil, failure
122+}
123+
124+func testDecodeClientFrame(frame wire.Frame) (protocol.ClientMessage, error) {
125+ switch frame.Type {
126+ case wire.MsgHello:
127+ return wire.UnmarshalHello(frame.Payload)
128+ case wire.MsgInput:
129+ return wire.UnmarshalInput(frame.Payload)
130+ case wire.MsgResize:
131+ return wire.UnmarshalResize(frame.Payload)
132+ case wire.MsgDetach:
133+ return protocol.Detach{}, nil
134+ case wire.MsgPing:
135+ return protocol.Ping{}, nil
136+ case wire.MsgList:
137+ return protocol.List{}, nil
138+ case wire.MsgKill:
139+ return wire.UnmarshalKill(frame.Payload)
140+ case wire.MsgTheme:
141+ return wire.UnmarshalTheme(frame.Payload)
142+ case wire.MsgAck:
143+ return wire.UnmarshalAck(frame.Payload)
144+ case wire.MsgImagePush:
145+ return wire.UnmarshalImagePush(frame.Payload)
146+ case wire.MsgClientNotice:
147+ return wire.UnmarshalClientNotice(frame.Payload)
148+ case wire.MsgCommand:
149+ return wire.UnmarshalCommandRequest(frame.Payload)
150+ case wire.MsgOutputResetRequest:
151+ return wire.UnmarshalOutputResetRequest(frame.Payload)
152+ case wire.MsgRemotePreviewRequest:
153+ return wire.UnmarshalRemotePreviewRequest(frame.Payload)
154+ case wire.MsgRouteAttentionSubscription:
155+ return wire.UnmarshalRouteAttentionSubscription(frame.Payload)
156+ case wire.MsgSamePeerSwitchRequest:
157+ return wire.UnmarshalSamePeerSwitchRequest(frame.Payload)
158+ case wire.MsgParkedRouteRequest:
159+ return wire.UnmarshalParkedRouteRequest(frame.Payload)
160+ case wire.MsgRecentRouteSnapshot:
161+ return wire.UnmarshalRecentRouteSnapshot(frame.Payload)
162+ case wire.MsgRouteNavigationFailure:
163+ return wire.UnmarshalRouteNavigationFailure(frame.Payload)
164+ default:
165+ return nil, errors.New("test server connection: unsupported client frame")
166+ }
167+}
168+
169+func testSendServer(raw ports.Transport, message protocol.ServerMessage) error {
170+ frame, err := testServerFrame(message)
171+ if err != nil {
172+ return err
173+ }
174+ return raw.Send(frame)
175+}
176+
177+func testSendServerAsync(raw ports.Transport, message protocol.ServerMessage) error {
178+ transport, ok := raw.(ports.AsyncTransport)
179+ if !ok {
180+ return errors.New("test server connection: async unsupported")
181+ }
182+ frame, err := testServerFrame(message)
183+ if err != nil {
184+ return err
185+ }
186+ return transport.SendAsync(frame)
187+}
188+
189+func testSendServerSynchronous(raw ports.Transport, message protocol.ServerMessage) error {
190+ transport, ok := raw.(ports.OwnedSynchronousTransport)
191+ if !ok {
192+ return errors.New("test server connection: synchronous unsupported")
193+ }
194+ frame, err := testServerFrame(message)
195+ if err != nil {
196+ return err
197+ }
198+ return transport.SendSynchronous(frame)
199+}
200+
201+func testSendOutput(raw ports.Transport, output protocol.Output) error {
202+ frame, err := testServerFrame(output)
203+ if err != nil {
204+ return err
205+ }
206+ return raw.Send(frame)
207+}
208+
209+func testSendOutputAsync(raw ports.Transport, output protocol.Output) error {
210+ transport, ok := raw.(ports.AsyncTransport)
211+ if !ok {
212+ return errors.New("test server connection: async unsupported")
213+ }
214+ frame, err := testServerFrame(output)
215+ if err != nil {
216+ return err
217+ }
218+ return transport.SendAsync(frame)
219+}
220+
221+func testSendOutputSynchronous(raw ports.Transport, output protocol.Output) error {
222+ transport, ok := raw.(ports.OwnedSynchronousTransport)
223+ if !ok {
224+ return errors.New("test server connection: synchronous unsupported")
225+ }
226+ frame, err := testServerFrame(output)
227+ if err != nil {
228+ return err
229+ }
230+ return transport.SendSynchronous(frame)
231+}
232+
233+func testServerCapabilities(raw ports.Transport) protocol.ConnectionCapabilities {
234+ _, datagram := raw.(ports.DatagramTransport)
235+ _, async := raw.(ports.AsyncTransport)
236+ _, synchronous := raw.(ports.OwnedSynchronousTransport)
237+ _, link := raw.(ports.LinkStateReporter)
238+ window := uint8(protocol.MaxOutputWindow)
239+ if datagram {
240+ window = 1
241+ }
242+ return protocol.ConnectionCapabilities{OutputDataLimit: protocol.MaxOutputDataLen, PreferredOutputWindow: window, AsyncSend: async, OwnedSynchronousSend: synchronous, LinkState: link}
243+}
244+
245+func testLinkState(raw ports.Transport) ports.LinkState {
246+ if reporter, ok := raw.(ports.LinkStateReporter); ok {
247+ return reporter.LinkState()
248+ }
249+ return ports.LinkStateConnected
250+}
251+
252+func testLinkEvents(raw ports.Transport) <-chan ports.LinkEvent {
253+ if reporter, ok := raw.(ports.LinkStateReporter); ok {
254+ return reporter.LinkEvents()
255+ }
256+ return nil
257+}
258+
259+func outputFrameSender(send func(wire.Frame) error) func(protocol.Output) error {
260+ return func(output protocol.Output) error {
261+ frame, err := testServerFrame(output)
262+ if err != nil {
263+ return err
264+ }
265+ return send(frame)
266+ }
267+}
268+
269+func testServerFrame(message protocol.ServerMessage) (wire.Frame, error) {
270+ switch m := message.(type) {
271+ case protocol.Welcome:
272+ return wire.Frame{Type: wire.MsgWelcome, Payload: wire.MarshalWelcome(m)}, nil
273+ case protocol.ErrorMsg:
274+ return wire.Frame{Type: wire.MsgError, Payload: wire.MarshalErrorMsg(m)}, nil
275+ case protocol.Output:
276+ payload, err := wire.MarshalOutput(m)
277+ return wire.Frame{Type: wire.MsgOutput, Payload: payload}, err
278+ case protocol.Detached:
279+ return wire.Frame{Type: wire.MsgDetached, Payload: wire.MarshalDetached(m)}, nil
280+ case protocol.Pong:
281+ return wire.Frame{Type: wire.MsgPong, Payload: wire.MarshalPong(m)}, nil
282+ case protocol.Sessions:
283+ return wire.Frame{Type: wire.MsgSessions, Payload: wire.MarshalSessions(m)}, nil
284+ case protocol.CommandResult:
285+ return wire.Frame{Type: wire.MsgCommandResult, Payload: wire.MarshalCommandResult(m)}, nil
286+ case protocol.NavigationDirective:
287+ return wire.Frame{Type: wire.MsgNavigationAction, Payload: wire.MarshalNavigationDirective(m)}, nil
288+ case protocol.AttachTarget:
289+ return wire.Frame{Type: wire.MsgAttachTarget, Payload: wire.MarshalAttachTarget(m)}, nil
290+ case protocol.RemotePreview:
291+ return wire.Frame{Type: wire.MsgRemotePreviewResponse, Payload: wire.MarshalRemotePreview(m)}, nil
292+ case protocol.CommittedRouteIdentity:
293+ payload, err := wire.MarshalCommittedRouteIdentity(m)
294+ return wire.Frame{Type: wire.MsgCommittedRouteIdentity, Payload: payload}, err
295+ case protocol.RouteNavigationAction:
296+ payload, err := wire.MarshalRouteNavigationAction(m)
297+ return wire.Frame{Type: wire.MsgNavigateRecentRoute, Payload: payload}, err
298+ case protocol.RouteNavigationFailure:
299+ payload, err := wire.MarshalRouteNavigationFailure(m)
300+ return wire.Frame{Type: wire.MsgRouteNavigationFailure, Payload: payload}, err
301+ case protocol.RoutePosition:
302+ payload, err := wire.MarshalRoutePosition(m)
303+ return wire.Frame{Type: wire.MsgRoutePosition, Payload: payload}, err
304+ case protocol.SamePeerSwitchFailure:
305+ payload, err := wire.MarshalSamePeerSwitchFailure(m)
306+ return wire.Frame{Type: wire.MsgSamePeerSwitchFailure, Payload: payload}, err
307+ case protocol.ParkedRouteResponse:
308+ return wire.Frame{Type: wire.MsgParkedRouteResponse, Payload: wire.MarshalParkedRouteResponse(m)}, nil
309+ default:
310+ return wire.Frame{}, errors.New("test server connection: unsupported server message")
311+ }
312+}
313+
314+func (d *Daemon) handleCommandFrame(connection ports.ServerConnection, frame wire.Frame) error {
315+ request, err := wire.UnmarshalCommandRequest(frame.Payload)
316+ if err != nil {
317+ code := protocol.ErrInternal
318+ if version, ok := wire.PeekCommandVersion(frame.Payload); ok && version != protocol.Version {
319+ code = protocol.ErrVersionMismatch
320+ }
321+ return d.sendCommandResult(connection, protocol.CommandResult{Code: code, Text: err.Error()})
322+ }
323+ return d.handleCommand(connection, request)
324+}
325+
326+func (d *Daemon) handleHelloFrame(connection ports.ServerConnection, frame wire.Frame) {
327+ hello, err := wire.UnmarshalHello(frame.Payload)
328+ if err != nil {
329+ return
330+ }
331+ d.handleHello(connection, hello)
332+}
333+
334+func (d *Daemon) handleHelloFrameWithContext(ctx context.Context, timedOut <-chan struct{}, stop, finish func(), connection ports.ServerConnection, frame wire.Frame) {
335+ hello, err := wire.UnmarshalHello(frame.Payload)
336+ if err != nil {
337+ return
338+ }
339+ d.handleHelloWithContext(ctx, timedOut, stop, finish, connection, hello)
340+}
341+
342+func (d *Daemon) handleAttachmentClientFrame(capability attachmentCapability, frame wire.Frame) bool {
343+ message, err := testReceiveClient(&singleFrameTransport{frame: frame})
344+ if err != nil {
345+ return false
346+ }
347+ return d.handleAttachmentClientMessage(capability, message)
348+}
349+
350+type singleFrameTransport struct{ frame wire.Frame }
351+
352+func (*singleFrameTransport) Send(wire.Frame) error { return nil }
353+func (t *singleFrameTransport) Recv() (wire.Frame, error) { return t.frame, nil }
354+func (*singleFrameTransport) Close() error { return nil }
355+
356+// Methods below let the existing raw transport fixtures exercise the typed
357+// daemon boundary while preserving their byte-level assertions.
358+
359+func (t *blockedRenderReplacementTransport) ReceiveClient() (protocol.ClientMessage, error) {
360+ return testReceiveClient(t)
361+}
362+func (t *blockedRenderReplacementTransport) SendServer(m protocol.ServerMessage) error {
363+ return testSendServer(t, m)
364+}
365+func (t *blockedRenderReplacementTransport) SendServerAsync(m protocol.ServerMessage) error {
366+ return testSendServerAsync(t, m)
367+}
368+func (t *blockedRenderReplacementTransport) SendServerSynchronous(m protocol.ServerMessage) error {
369+ return testSendServerSynchronous(t, m)
370+}
371+func (t *blockedRenderReplacementTransport) Capabilities() protocol.ConnectionCapabilities {
372+ return testServerCapabilities(t)
373+}
374+func (t *blockedRenderReplacementTransport) LinkState() ports.LinkState { return testLinkState(t) }
375+func (t *blockedRenderReplacementTransport) LinkEvents() <-chan ports.LinkEvent {
376+ return testLinkEvents(t)
377+}
378+
379+func (t *teardownBlockingTransport) ReceiveClient() (protocol.ClientMessage, error) {
380+ return testReceiveClient(t)
381+}
382+func (t *teardownBlockingTransport) SendServer(m protocol.ServerMessage) error {
383+ return testSendServer(t, m)
384+}
385+func (t *teardownBlockingTransport) SendServerAsync(m protocol.ServerMessage) error {
386+ return testSendServerAsync(t, m)
387+}
388+func (t *teardownBlockingTransport) SendServerSynchronous(m protocol.ServerMessage) error {
389+ return testSendServerSynchronous(t, m)
390+}
391+func (t *teardownBlockingTransport) Capabilities() protocol.ConnectionCapabilities {
392+ return testServerCapabilities(t)
393+}
394+func (t *teardownBlockingTransport) LinkState() ports.LinkState { return testLinkState(t) }
395+func (t *teardownBlockingTransport) LinkEvents() <-chan ports.LinkEvent { return testLinkEvents(t) }
396+
397+func (t *closeTrackingTransport) ReceiveClient() (protocol.ClientMessage, error) {
398+ return testReceiveClient(t)
399+}
400+func (t *closeTrackingTransport) SendServer(m protocol.ServerMessage) error {
401+ return testSendServer(t, m)
402+}
403+func (t *closeTrackingTransport) SendServerAsync(m protocol.ServerMessage) error {
404+ return testSendServerAsync(t, m)
405+}
406+func (t *closeTrackingTransport) SendServerSynchronous(m protocol.ServerMessage) error {
407+ return testSendServerSynchronous(t, m)
408+}
409+func (t *closeTrackingTransport) Capabilities() protocol.ConnectionCapabilities {
410+ return testServerCapabilities(t)
411+}
412+func (t *closeTrackingTransport) LinkState() ports.LinkState { return testLinkState(t) }
413+func (t *closeTrackingTransport) LinkEvents() <-chan ports.LinkEvent { return testLinkEvents(t) }
414+
415+func (t *closeCountingBlockedTransport) ReceiveClient() (protocol.ClientMessage, error) {
416+ return testReceiveClient(t)
417+}
418+func (t *closeCountingBlockedTransport) SendServer(m protocol.ServerMessage) error {
419+ return testSendServer(t, m)
420+}
421+func (t *closeCountingBlockedTransport) SendServerAsync(m protocol.ServerMessage) error {
422+ return testSendServerAsync(t, m)
423+}
424+func (t *closeCountingBlockedTransport) SendServerSynchronous(m protocol.ServerMessage) error {
425+ return testSendServerSynchronous(t, m)
426+}
427+func (t *closeCountingBlockedTransport) Capabilities() protocol.ConnectionCapabilities {
428+ return testServerCapabilities(t)
429+}
430+func (t *closeCountingBlockedTransport) LinkState() ports.LinkState { return testLinkState(t) }
431+func (t *closeCountingBlockedTransport) LinkEvents() <-chan ports.LinkEvent { return testLinkEvents(t) }
432+
433+func (t *handshakeBlockingTransport) ReceiveClient() (protocol.ClientMessage, error) {
434+ return testReceiveClient(t)
435+}
436+func (t *handshakeBlockingTransport) SendServer(m protocol.ServerMessage) error {
437+ return testSendServer(t, m)
438+}
439+func (t *handshakeBlockingTransport) SendServerAsync(m protocol.ServerMessage) error {
440+ return testSendServerAsync(t, m)
441+}
442+func (t *handshakeBlockingTransport) SendServerSynchronous(m protocol.ServerMessage) error {
443+ return testSendServerSynchronous(t, m)
444+}
445+func (t *handshakeBlockingTransport) Capabilities() protocol.ConnectionCapabilities {
446+ return testServerCapabilities(t)
447+}
448+func (t *handshakeBlockingTransport) LinkState() ports.LinkState { return testLinkState(t) }
449+func (t *handshakeBlockingTransport) LinkEvents() <-chan ports.LinkEvent { return testLinkEvents(t) }
450+
451+func (t *lateGraphicsCleanupTransport) ReceiveClient() (protocol.ClientMessage, error) {
452+ return testReceiveClient(t)
453+}
454+func (t *lateGraphicsCleanupTransport) SendServer(m protocol.ServerMessage) error {
455+ return testSendServer(t, m)
456+}
457+func (t *lateGraphicsCleanupTransport) SendServerAsync(m protocol.ServerMessage) error {
458+ return testSendServerAsync(t, m)
459+}
460+func (t *lateGraphicsCleanupTransport) SendServerSynchronous(m protocol.ServerMessage) error {
461+ return testSendServerSynchronous(t, m)
462+}
463+func (t *lateGraphicsCleanupTransport) Capabilities() protocol.ConnectionCapabilities {
464+ return testServerCapabilities(t)
465+}
466+func (t *lateGraphicsCleanupTransport) LinkState() ports.LinkState { return testLinkState(t) }
467+func (t *lateGraphicsCleanupTransport) LinkEvents() <-chan ports.LinkEvent { return testLinkEvents(t) }
468+
469+func (t *datagramTestTransport) ReceiveClient() (protocol.ClientMessage, error) {
470+ return testReceiveClient(t)
471+}
472+func (t *datagramTestTransport) SendServer(m protocol.ServerMessage) error {
473+ return testSendServer(t, m)
474+}
475+func (t *datagramTestTransport) SendServerAsync(m protocol.ServerMessage) error {
476+ return testSendServerAsync(t, m)
477+}
478+func (t *datagramTestTransport) SendServerSynchronous(m protocol.ServerMessage) error {
479+ return testSendServerSynchronous(t, m)
480+}
481+func (t *datagramTestTransport) Capabilities() protocol.ConnectionCapabilities {
482+ return testServerCapabilities(t)
483+}
484+func (t *datagramTestTransport) LinkState() ports.LinkState { return testLinkState(t) }
485+func (t *datagramTestTransport) LinkEvents() <-chan ports.LinkEvent { return testLinkEvents(t) }
486+
487+func (t *asyncPaintTransport) ReceiveClient() (protocol.ClientMessage, error) {
488+ return testReceiveClient(t)
489+}
490+func (t *asyncPaintTransport) SendServer(m protocol.ServerMessage) error { return testSendServer(t, m) }
491+func (t *asyncPaintTransport) SendServerAsync(m protocol.ServerMessage) error {
492+ return testSendServerAsync(t, m)
493+}
494+func (t *asyncPaintTransport) SendServerSynchronous(m protocol.ServerMessage) error {
495+ return testSendServerSynchronous(t, m)
496+}
497+func (t *asyncPaintTransport) Capabilities() protocol.ConnectionCapabilities {
498+ return testServerCapabilities(t)
499+}
500+func (t *asyncPaintTransport) LinkState() ports.LinkState { return testLinkState(t) }
501+func (t *asyncPaintTransport) LinkEvents() <-chan ports.LinkEvent { return testLinkEvents(t) }
502+
503+func (t *timedSideEffectTransport) ReceiveClient() (protocol.ClientMessage, error) {
504+ return testReceiveClient(t)
505+}
506+func (t *timedSideEffectTransport) SendServer(m protocol.ServerMessage) error {
507+ return testSendServer(t, m)
508+}
509+func (t *timedSideEffectTransport) SendServerAsync(m protocol.ServerMessage) error {
510+ return testSendServerAsync(t, m)
511+}
512+func (t *timedSideEffectTransport) SendServerSynchronous(m protocol.ServerMessage) error {
513+ return testSendServerSynchronous(t, m)
514+}
515+func (t *timedSideEffectTransport) Capabilities() protocol.ConnectionCapabilities {
516+ return testServerCapabilities(t)
517+}
518+func (t *timedSideEffectTransport) LinkState() ports.LinkState { return testLinkState(t) }
519+func (t *timedSideEffectTransport) LinkEvents() <-chan ports.LinkEvent { return testLinkEvents(t) }
520+
521+func (t *scriptedReplayTransport) ReceiveClient() (protocol.ClientMessage, error) {
522+ return testReceiveClient(t)
523+}
524+func (t *scriptedReplayTransport) SendServer(m protocol.ServerMessage) error {
525+ return testSendServer(t, m)
526+}
527+func (t *scriptedReplayTransport) SendServerAsync(m protocol.ServerMessage) error {
528+ return testSendServerAsync(t, m)
529+}
530+func (t *scriptedReplayTransport) SendServerSynchronous(m protocol.ServerMessage) error {
531+ return testSendServerSynchronous(t, m)
532+}
533+func (t *scriptedReplayTransport) Capabilities() protocol.ConnectionCapabilities {
534+ return testServerCapabilities(t)
535+}
536+func (t *scriptedReplayTransport) LinkState() ports.LinkState { return testLinkState(t) }
537+func (t *scriptedReplayTransport) LinkEvents() <-chan ports.LinkEvent { return testLinkEvents(t) }
538+
539+func (t *swapErrorTransport) ReceiveClient() (protocol.ClientMessage, error) {
540+ return testReceiveClient(t)
541+}
542+func (t *swapErrorTransport) SendServer(m protocol.ServerMessage) error { return testSendServer(t, m) }
543+func (t *swapErrorTransport) SendServerAsync(m protocol.ServerMessage) error {
544+ return testSendServerAsync(t, m)
545+}
546+func (t *swapErrorTransport) SendServerSynchronous(m protocol.ServerMessage) error {
547+ return testSendServerSynchronous(t, m)
548+}
549+func (t *swapErrorTransport) Capabilities() protocol.ConnectionCapabilities {
550+ return testServerCapabilities(t)
551+}
552+func (t *swapErrorTransport) LinkState() ports.LinkState { return testLinkState(t) }
553+func (t *swapErrorTransport) LinkEvents() <-chan ports.LinkEvent { return testLinkEvents(t) }
554+
555+func (t *ownedSwapErrorTransport) ReceiveClient() (protocol.ClientMessage, error) {
556+ return testReceiveClient(t)
557+}
558+func (t *ownedSwapErrorTransport) SendServer(m protocol.ServerMessage) error {
559+ return testSendServer(t, m)
560+}
561+func (t *ownedSwapErrorTransport) SendServerAsync(m protocol.ServerMessage) error {
562+ return testSendServerAsync(t, m)
563+}
564+func (t *ownedSwapErrorTransport) SendServerSynchronous(m protocol.ServerMessage) error {
565+ return testSendServerSynchronous(t, m)
566+}
567+func (t *ownedSwapErrorTransport) Capabilities() protocol.ConnectionCapabilities {
568+ return testServerCapabilities(t)
569+}
570+func (t *ownedSwapErrorTransport) LinkState() ports.LinkState { return testLinkState(t) }
571+func (t *ownedSwapErrorTransport) LinkEvents() <-chan ports.LinkEvent { return testLinkEvents(t) }
572+
573+func (t *blockingControlSendTransport) ReceiveClient() (protocol.ClientMessage, error) {
574+ return testReceiveClient(t)
575+}
576+func (t *blockingControlSendTransport) SendServer(m protocol.ServerMessage) error {
577+ return testSendServer(t, m)
578+}
579+func (t *blockingControlSendTransport) SendServerAsync(m protocol.ServerMessage) error {
580+ return testSendServerAsync(t, m)
581+}
582+func (t *blockingControlSendTransport) SendServerSynchronous(m protocol.ServerMessage) error {
583+ return testSendServerSynchronous(t, m)
584+}
585+func (t *blockingControlSendTransport) Capabilities() protocol.ConnectionCapabilities {
586+ return testServerCapabilities(t)
587+}
588+func (t *blockingControlSendTransport) LinkState() ports.LinkState { return testLinkState(t) }
589+func (t *blockingControlSendTransport) LinkEvents() <-chan ports.LinkEvent { return testLinkEvents(t) }
590+
591+func (t *commandSendErrorTransport) ReceiveClient() (protocol.ClientMessage, error) {
592+ return testReceiveClient(t)
593+}
594+func (t *commandSendErrorTransport) SendServer(m protocol.ServerMessage) error {
595+ return testSendServer(t, m)
596+}
597+func (t *commandSendErrorTransport) SendServerAsync(m protocol.ServerMessage) error {
598+ return testSendServerAsync(t, m)
599+}
600+func (t *commandSendErrorTransport) SendServerSynchronous(m protocol.ServerMessage) error {
601+ return testSendServerSynchronous(t, m)
602+}
603+func (t *commandSendErrorTransport) Capabilities() protocol.ConnectionCapabilities {
604+ return testServerCapabilities(t)
605+}
606+func (t *commandSendErrorTransport) LinkState() ports.LinkState { return testLinkState(t) }
607+func (t *commandSendErrorTransport) LinkEvents() <-chan ports.LinkEvent { return testLinkEvents(t) }
608+
609+func (t *fanoutBlockingTransport) ReceiveClient() (protocol.ClientMessage, error) {
610+ return testReceiveClient(t)
611+}
612+func (t *fanoutBlockingTransport) SendServer(m protocol.ServerMessage) error {
613+ return testSendServer(t, m)
614+}
615+func (t *fanoutBlockingTransport) SendServerAsync(m protocol.ServerMessage) error {
616+ return testSendServerAsync(t, m)
617+}
618+func (t *fanoutBlockingTransport) SendServerSynchronous(m protocol.ServerMessage) error {
619+ return testSendServerSynchronous(t, m)
620+}
621+func (t *fanoutBlockingTransport) Capabilities() protocol.ConnectionCapabilities {
622+ return testServerCapabilities(t)
623+}
624+func (t *fanoutBlockingTransport) LinkState() ports.LinkState { return testLinkState(t) }
625+func (t *fanoutBlockingTransport) LinkEvents() <-chan ports.LinkEvent { return testLinkEvents(t) }
626+
627+func (t *staleClipboardErrorTransport) ReceiveClient() (protocol.ClientMessage, error) {
628+ return testReceiveClient(t)
629+}
630+func (t *staleClipboardErrorTransport) SendServer(m protocol.ServerMessage) error {
631+ return testSendServer(t, m)
632+}
633+func (t *staleClipboardErrorTransport) SendServerAsync(m protocol.ServerMessage) error {
634+ return testSendServerAsync(t, m)
635+}
636+func (t *staleClipboardErrorTransport) SendServerSynchronous(m protocol.ServerMessage) error {
637+ return testSendServerSynchronous(t, m)
638+}
639+func (t *staleClipboardErrorTransport) Capabilities() protocol.ConnectionCapabilities {
640+ return testServerCapabilities(t)
641+}
642+func (t *staleClipboardErrorTransport) LinkState() ports.LinkState { return testLinkState(t) }
643+func (t *staleClipboardErrorTransport) LinkEvents() <-chan ports.LinkEvent { return testLinkEvents(t) }
644+
645+func (t *movingClipboardErrorTransport) ReceiveClient() (protocol.ClientMessage, error) {
646+ return testReceiveClient(t)
647+}
648+func (t *movingClipboardErrorTransport) SendServer(m protocol.ServerMessage) error {
649+ return testSendServer(t, m)
650+}
651+func (t *movingClipboardErrorTransport) SendServerAsync(m protocol.ServerMessage) error {
652+ return testSendServerAsync(t, m)
653+}
654+func (t *movingClipboardErrorTransport) SendServerSynchronous(m protocol.ServerMessage) error {
655+ return testSendServerSynchronous(t, m)
656+}
657+func (t *movingClipboardErrorTransport) Capabilities() protocol.ConnectionCapabilities {
658+ return testServerCapabilities(t)
659+}
660+func (t *movingClipboardErrorTransport) LinkState() ports.LinkState { return testLinkState(t) }
661+func (t *movingClipboardErrorTransport) LinkEvents() <-chan ports.LinkEvent { return testLinkEvents(t) }
662+
663+func (t *blockingClipboardTransport) ReceiveClient() (protocol.ClientMessage, error) {
664+ return testReceiveClient(t)
665+}
666+func (t *blockingClipboardTransport) SendServer(m protocol.ServerMessage) error {
667+ return testSendServer(t, m)
668+}
669+func (t *blockingClipboardTransport) SendServerAsync(m protocol.ServerMessage) error {
670+ return testSendServerAsync(t, m)
671+}
672+func (t *blockingClipboardTransport) SendServerSynchronous(m protocol.ServerMessage) error {
673+ return testSendServerSynchronous(t, m)
674+}
675+func (t *blockingClipboardTransport) Capabilities() protocol.ConnectionCapabilities {
676+ return testServerCapabilities(t)
677+}
678+func (t *blockingClipboardTransport) LinkState() ports.LinkState { return testLinkState(t) }
679+func (t *blockingClipboardTransport) LinkEvents() <-chan ports.LinkEvent { return testLinkEvents(t) }
680+
681+func (t *blockedAttachmentTransport) ReceiveClient() (protocol.ClientMessage, error) {
682+ return testReceiveClient(t)
683+}
684+func (t *blockedAttachmentTransport) SendServer(m protocol.ServerMessage) error {
685+ return testSendServer(t, m)
686+}
687+func (t *blockedAttachmentTransport) SendServerAsync(m protocol.ServerMessage) error {
688+ return testSendServerAsync(t, m)
689+}
690+func (t *blockedAttachmentTransport) SendServerSynchronous(m protocol.ServerMessage) error {
691+ return testSendServerSynchronous(t, m)
692+}
693+func (t *blockedAttachmentTransport) Capabilities() protocol.ConnectionCapabilities {
694+ return testServerCapabilities(t)
695+}
696+func (t *blockedAttachmentTransport) LinkState() ports.LinkState { return testLinkState(t) }
697+func (t *blockedAttachmentTransport) LinkEvents() <-chan ports.LinkEvent { return testLinkEvents(t) }
698+
699+func (t *resumeWelcomeFailureTransport) ReceiveClient() (protocol.ClientMessage, error) {
700+ return testReceiveClient(t)
701+}
702+func (t *resumeWelcomeFailureTransport) SendServer(m protocol.ServerMessage) error {
703+ return testSendServer(t, m)
704+}
705+func (t *resumeWelcomeFailureTransport) SendServerAsync(m protocol.ServerMessage) error {
706+ return testSendServerAsync(t, m)
707+}
708+func (t *resumeWelcomeFailureTransport) SendServerSynchronous(m protocol.ServerMessage) error {
709+ return testSendServerSynchronous(t, m)
710+}
711+func (t *resumeWelcomeFailureTransport) Capabilities() protocol.ConnectionCapabilities {
712+ return testServerCapabilities(t)
713+}
714+func (t *resumeWelcomeFailureTransport) LinkState() ports.LinkState { return testLinkState(t) }
715+func (t *resumeWelcomeFailureTransport) LinkEvents() <-chan ports.LinkEvent { return testLinkEvents(t) }
716+
717+func (t *committedIdentityErrorTransport) ReceiveClient() (protocol.ClientMessage, error) {
718+ return testReceiveClient(t)
719+}
720+func (t *committedIdentityErrorTransport) SendServer(m protocol.ServerMessage) error {
721+ return testSendServer(t, m)
722+}
723+func (t *committedIdentityErrorTransport) SendServerAsync(m protocol.ServerMessage) error {
724+ return testSendServerAsync(t, m)
725+}
726+func (t *committedIdentityErrorTransport) SendServerSynchronous(m protocol.ServerMessage) error {
727+ return testSendServerSynchronous(t, m)
728+}
729+func (t *committedIdentityErrorTransport) Capabilities() protocol.ConnectionCapabilities {
730+ return testServerCapabilities(t)
731+}
732+func (t *committedIdentityErrorTransport) LinkState() ports.LinkState { return testLinkState(t) }
733+func (t *committedIdentityErrorTransport) LinkEvents() <-chan ports.LinkEvent {
734+ return testLinkEvents(t)
735+}
736+
737+func (t *countingOutputTransport) ReceiveClient() (protocol.ClientMessage, error) {
738+ return testReceiveClient(t)
739+}
740+func (t *countingOutputTransport) SendServer(m protocol.ServerMessage) error {
741+ return testSendServer(t, m)
742+}
743+func (t *countingOutputTransport) SendServerAsync(m protocol.ServerMessage) error {
744+ return testSendServerAsync(t, m)
745+}
746+func (t *countingOutputTransport) SendServerSynchronous(m protocol.ServerMessage) error {
747+ return testSendServerSynchronous(t, m)
748+}
749+func (t *countingOutputTransport) Capabilities() protocol.ConnectionCapabilities {
750+ return testServerCapabilities(t)
751+}
752+func (t *countingOutputTransport) LinkState() ports.LinkState { return testLinkState(t) }
753+func (t *countingOutputTransport) LinkEvents() <-chan ports.LinkEvent { return testLinkEvents(t) }
754+
755+func (t *parkedRouteExpiryTransport) ReceiveClient() (protocol.ClientMessage, error) {
756+ return testReceiveClient(t)
757+}
758+func (t *parkedRouteExpiryTransport) SendServer(m protocol.ServerMessage) error {
759+ return testSendServer(t, m)
760+}
761+func (t *parkedRouteExpiryTransport) SendServerAsync(m protocol.ServerMessage) error {
762+ return testSendServerAsync(t, m)
763+}
764+func (t *parkedRouteExpiryTransport) SendServerSynchronous(m protocol.ServerMessage) error {
765+ return testSendServerSynchronous(t, m)
766+}
767+func (t *parkedRouteExpiryTransport) Capabilities() protocol.ConnectionCapabilities {
768+ return testServerCapabilities(t)
769+}
770+func (t *parkedRouteExpiryTransport) LinkState() ports.LinkState { return testLinkState(t) }
771+func (t *parkedRouteExpiryTransport) LinkEvents() <-chan ports.LinkEvent { return testLinkEvents(t) }
772+
773+func (t *remotePickerSendErrorTransport) ReceiveClient() (protocol.ClientMessage, error) {
774+ return testReceiveClient(t)
775+}
776+func (t *remotePickerSendErrorTransport) SendServer(m protocol.ServerMessage) error {
777+ return testSendServer(t, m)
778+}
779+func (t *remotePickerSendErrorTransport) SendServerAsync(m protocol.ServerMessage) error {
780+ return testSendServerAsync(t, m)
781+}
782+func (t *remotePickerSendErrorTransport) SendServerSynchronous(m protocol.ServerMessage) error {
783+ return testSendServerSynchronous(t, m)
784+}
785+func (t *remotePickerSendErrorTransport) Capabilities() protocol.ConnectionCapabilities {
786+ return testServerCapabilities(t)
787+}
788+func (t *remotePickerSendErrorTransport) LinkState() ports.LinkState { return testLinkState(t) }
789+func (t *remotePickerSendErrorTransport) LinkEvents() <-chan ports.LinkEvent {
790+ return testLinkEvents(t)
791+}
792+
793+func (t failingOutputTransport) ReceiveClient() (protocol.ClientMessage, error) {
794+ return testReceiveClient(t)
795+}
796+func (t failingOutputTransport) SendServer(m protocol.ServerMessage) error {
797+ return testSendServer(t, m)
798+}
799+func (t failingOutputTransport) SendServerAsync(m protocol.ServerMessage) error {
800+ return testSendServerAsync(t, m)
801+}
802+func (t failingOutputTransport) SendServerSynchronous(m protocol.ServerMessage) error {
803+ return testSendServerSynchronous(t, m)
804+}
805+func (t failingOutputTransport) Capabilities() protocol.ConnectionCapabilities {
806+ return testServerCapabilities(t)
807+}
808+func (t failingOutputTransport) LinkState() ports.LinkState { return testLinkState(t) }
809+func (t failingOutputTransport) LinkEvents() <-chan ports.LinkEvent { return testLinkEvents(t) }
810+
811+func (t cacheFailTransport) ReceiveClient() (protocol.ClientMessage, error) {
812+ return testReceiveClient(t)
813+}
814+func (t cacheFailTransport) SendServer(m protocol.ServerMessage) error { return testSendServer(t, m) }
815+func (t cacheFailTransport) SendServerAsync(m protocol.ServerMessage) error {
816+ return testSendServerAsync(t, m)
817+}
818+func (t cacheFailTransport) SendServerSynchronous(m protocol.ServerMessage) error {
819+ return testSendServerSynchronous(t, m)
820+}
821+func (t cacheFailTransport) Capabilities() protocol.ConnectionCapabilities {
822+ return testServerCapabilities(t)
823+}
824+func (t cacheFailTransport) LinkState() ports.LinkState { return testLinkState(t) }
825+func (t cacheFailTransport) LinkEvents() <-chan ports.LinkEvent { return testLinkEvents(t) }
826+
827+func (t parkedRouteFailTransport) ReceiveClient() (protocol.ClientMessage, error) {
828+ return testReceiveClient(t)
829+}
830+func (t parkedRouteFailTransport) SendServer(m protocol.ServerMessage) error {
831+ return testSendServer(t, m)
832+}
833+func (t parkedRouteFailTransport) SendServerAsync(m protocol.ServerMessage) error {
834+ return testSendServerAsync(t, m)
835+}
836+func (t parkedRouteFailTransport) SendServerSynchronous(m protocol.ServerMessage) error {
837+ return testSendServerSynchronous(t, m)
838+}
839+func (t parkedRouteFailTransport) Capabilities() protocol.ConnectionCapabilities {
840+ return testServerCapabilities(t)
841+}
842+func (t parkedRouteFailTransport) LinkState() ports.LinkState { return testLinkState(t) }
843+func (t parkedRouteFailTransport) LinkEvents() <-chan ports.LinkEvent { return testLinkEvents(t) }
844+
845+func (t *blockedRenderReplacementTransport) SendOutput(o protocol.Output) error {
846+ return testSendOutput(t, o)
847+}
848+func (t *blockedRenderReplacementTransport) SendOutputAsync(o protocol.Output) error {
849+ return testSendOutputAsync(t, o)
850+}
851+func (t *blockedRenderReplacementTransport) SendOutputSynchronous(o protocol.Output) error {
852+ return testSendOutputSynchronous(t, o)
853+}
854+
855+func (t *teardownBlockingTransport) SendOutput(o protocol.Output) error { return testSendOutput(t, o) }
856+func (t *teardownBlockingTransport) SendOutputAsync(o protocol.Output) error {
857+ return testSendOutputAsync(t, o)
858+}
859+func (t *teardownBlockingTransport) SendOutputSynchronous(o protocol.Output) error {
860+ return testSendOutputSynchronous(t, o)
861+}
862+
863+func (t *closeTrackingTransport) SendOutput(o protocol.Output) error { return testSendOutput(t, o) }
864+func (t *closeTrackingTransport) SendOutputAsync(o protocol.Output) error {
865+ return testSendOutputAsync(t, o)
866+}
867+func (t *closeTrackingTransport) SendOutputSynchronous(o protocol.Output) error {
868+ return testSendOutputSynchronous(t, o)
869+}
870+
871+func (t *closeCountingBlockedTransport) SendOutput(o protocol.Output) error {
872+ return testSendOutput(t, o)
873+}
874+func (t *closeCountingBlockedTransport) SendOutputAsync(o protocol.Output) error {
875+ return testSendOutputAsync(t, o)
876+}
877+func (t *closeCountingBlockedTransport) SendOutputSynchronous(o protocol.Output) error {
878+ return testSendOutputSynchronous(t, o)
879+}
880+
881+func (t *handshakeBlockingTransport) SendOutput(o protocol.Output) error { return testSendOutput(t, o) }
882+func (t *handshakeBlockingTransport) SendOutputAsync(o protocol.Output) error {
883+ return testSendOutputAsync(t, o)
884+}
885+func (t *handshakeBlockingTransport) SendOutputSynchronous(o protocol.Output) error {
886+ return testSendOutputSynchronous(t, o)
887+}
888+
889+func (t *lateGraphicsCleanupTransport) SendOutput(o protocol.Output) error {
890+ return testSendOutput(t, o)
891+}
892+func (t *lateGraphicsCleanupTransport) SendOutputAsync(o protocol.Output) error {
893+ return testSendOutputAsync(t, o)
894+}
895+func (t *lateGraphicsCleanupTransport) SendOutputSynchronous(o protocol.Output) error {
896+ return testSendOutputSynchronous(t, o)
897+}
898+
899+func (t *datagramTestTransport) SendOutput(o protocol.Output) error { return testSendOutput(t, o) }
900+func (t *datagramTestTransport) SendOutputAsync(o protocol.Output) error {
901+ return testSendOutputAsync(t, o)
902+}
903+func (t *datagramTestTransport) SendOutputSynchronous(o protocol.Output) error {
904+ return testSendOutputSynchronous(t, o)
905+}
906+
907+func (t *asyncPaintTransport) SendOutput(o protocol.Output) error { return testSendOutput(t, o) }
908+func (t *asyncPaintTransport) SendOutputAsync(o protocol.Output) error {
909+ return testSendOutputAsync(t, o)
910+}
911+func (t *asyncPaintTransport) SendOutputSynchronous(o protocol.Output) error {
912+ return testSendOutputSynchronous(t, o)
913+}
914+
915+func (t *timedSideEffectTransport) SendOutput(o protocol.Output) error { return testSendOutput(t, o) }
916+func (t *timedSideEffectTransport) SendOutputAsync(o protocol.Output) error {
917+ return testSendOutputAsync(t, o)
918+}
919+func (t *timedSideEffectTransport) SendOutputSynchronous(o protocol.Output) error {
920+ return testSendOutputSynchronous(t, o)
921+}
922+
923+func (t *scriptedReplayTransport) SendOutput(o protocol.Output) error { return testSendOutput(t, o) }
924+func (t *scriptedReplayTransport) SendOutputAsync(o protocol.Output) error {
925+ return testSendOutputAsync(t, o)
926+}
927+func (t *scriptedReplayTransport) SendOutputSynchronous(o protocol.Output) error {
928+ return testSendOutputSynchronous(t, o)
929+}
930+
931+func (t *swapErrorTransport) SendOutput(o protocol.Output) error { return testSendOutput(t, o) }
932+func (t *swapErrorTransport) SendOutputAsync(o protocol.Output) error {
933+ return testSendOutputAsync(t, o)
934+}
935+func (t *swapErrorTransport) SendOutputSynchronous(o protocol.Output) error {
936+ return testSendOutputSynchronous(t, o)
937+}
938+
939+func (t *ownedSwapErrorTransport) SendOutput(o protocol.Output) error { return testSendOutput(t, o) }
940+func (t *ownedSwapErrorTransport) SendOutputAsync(o protocol.Output) error {
941+ return testSendOutputAsync(t, o)
942+}
943+func (t *ownedSwapErrorTransport) SendOutputSynchronous(o protocol.Output) error {
944+ return testSendOutputSynchronous(t, o)
945+}
946+
947+func (t *blockingControlSendTransport) SendOutput(o protocol.Output) error {
948+ return testSendOutput(t, o)
949+}
950+func (t *blockingControlSendTransport) SendOutputAsync(o protocol.Output) error {
951+ return testSendOutputAsync(t, o)
952+}
953+func (t *blockingControlSendTransport) SendOutputSynchronous(o protocol.Output) error {
954+ return testSendOutputSynchronous(t, o)
955+}
956+
957+func (t *commandSendErrorTransport) SendOutput(o protocol.Output) error { return testSendOutput(t, o) }
958+func (t *commandSendErrorTransport) SendOutputAsync(o protocol.Output) error {
959+ return testSendOutputAsync(t, o)
960+}
961+func (t *commandSendErrorTransport) SendOutputSynchronous(o protocol.Output) error {
962+ return testSendOutputSynchronous(t, o)
963+}
964+
965+func (t *fanoutBlockingTransport) SendOutput(o protocol.Output) error { return testSendOutput(t, o) }
966+func (t *fanoutBlockingTransport) SendOutputAsync(o protocol.Output) error {
967+ return testSendOutputAsync(t, o)
968+}
969+func (t *fanoutBlockingTransport) SendOutputSynchronous(o protocol.Output) error {
970+ return testSendOutputSynchronous(t, o)
971+}
972+
973+func (t *staleClipboardErrorTransport) SendOutput(o protocol.Output) error {
974+ return testSendOutput(t, o)
975+}
976+func (t *staleClipboardErrorTransport) SendOutputAsync(o protocol.Output) error {
977+ return testSendOutputAsync(t, o)
978+}
979+func (t *staleClipboardErrorTransport) SendOutputSynchronous(o protocol.Output) error {
980+ return testSendOutputSynchronous(t, o)
981+}
982+
983+func (t *movingClipboardErrorTransport) SendOutput(o protocol.Output) error {
984+ return testSendOutput(t, o)
985+}
986+func (t *movingClipboardErrorTransport) SendOutputAsync(o protocol.Output) error {
987+ return testSendOutputAsync(t, o)
988+}
989+func (t *movingClipboardErrorTransport) SendOutputSynchronous(o protocol.Output) error {
990+ return testSendOutputSynchronous(t, o)
991+}
992+
993+func (t *blockingClipboardTransport) SendOutput(o protocol.Output) error { return testSendOutput(t, o) }
994+func (t *blockingClipboardTransport) SendOutputAsync(o protocol.Output) error {
995+ return testSendOutputAsync(t, o)
996+}
997+func (t *blockingClipboardTransport) SendOutputSynchronous(o protocol.Output) error {
998+ return testSendOutputSynchronous(t, o)
999+}
1000+
1001+func (t *blockedAttachmentTransport) SendOutput(o protocol.Output) error { return testSendOutput(t, o) }
1002+func (t *blockedAttachmentTransport) SendOutputAsync(o protocol.Output) error {
1003+ return testSendOutputAsync(t, o)
1004+}
1005+func (t *blockedAttachmentTransport) SendOutputSynchronous(o protocol.Output) error {
1006+ return testSendOutputSynchronous(t, o)
1007+}
1008+
1009+func (t *resumeWelcomeFailureTransport) SendOutput(o protocol.Output) error {
1010+ return testSendOutput(t, o)
1011+}
1012+func (t *resumeWelcomeFailureTransport) SendOutputAsync(o protocol.Output) error {
1013+ return testSendOutputAsync(t, o)
1014+}
1015+func (t *resumeWelcomeFailureTransport) SendOutputSynchronous(o protocol.Output) error {
1016+ return testSendOutputSynchronous(t, o)
1017+}
1018+
1019+func (t *committedIdentityErrorTransport) SendOutput(o protocol.Output) error {
1020+ return testSendOutput(t, o)
1021+}
1022+func (t *committedIdentityErrorTransport) SendOutputAsync(o protocol.Output) error {
1023+ return testSendOutputAsync(t, o)
1024+}
1025+func (t *committedIdentityErrorTransport) SendOutputSynchronous(o protocol.Output) error {
1026+ return testSendOutputSynchronous(t, o)
1027+}
1028+
1029+func (t *countingOutputTransport) SendOutput(o protocol.Output) error { return testSendOutput(t, o) }
1030+func (t *countingOutputTransport) SendOutputAsync(o protocol.Output) error {
1031+ return testSendOutputAsync(t, o)
1032+}
1033+func (t *countingOutputTransport) SendOutputSynchronous(o protocol.Output) error {
1034+ return testSendOutputSynchronous(t, o)
1035+}
1036+
1037+func (t *parkedRouteExpiryTransport) SendOutput(o protocol.Output) error { return testSendOutput(t, o) }
1038+func (t *parkedRouteExpiryTransport) SendOutputAsync(o protocol.Output) error {
1039+ return testSendOutputAsync(t, o)
1040+}
1041+func (t *parkedRouteExpiryTransport) SendOutputSynchronous(o protocol.Output) error {
1042+ return testSendOutputSynchronous(t, o)
1043+}
1044+
1045+func (t *remotePickerSendErrorTransport) SendOutput(o protocol.Output) error {
1046+ return testSendOutput(t, o)
1047+}
1048+func (t *remotePickerSendErrorTransport) SendOutputAsync(o protocol.Output) error {
1049+ return testSendOutputAsync(t, o)
1050+}
1051+func (t *remotePickerSendErrorTransport) SendOutputSynchronous(o protocol.Output) error {
1052+ return testSendOutputSynchronous(t, o)
1053+}
1054+
1055+func (t failingOutputTransport) SendOutput(o protocol.Output) error { return testSendOutput(t, o) }
1056+func (t failingOutputTransport) SendOutputAsync(o protocol.Output) error {
1057+ return testSendOutputAsync(t, o)
1058+}
1059+func (t failingOutputTransport) SendOutputSynchronous(o protocol.Output) error {
1060+ return testSendOutputSynchronous(t, o)
1061+}
1062+
1063+func (t cacheFailTransport) SendOutput(o protocol.Output) error { return testSendOutput(t, o) }
1064+func (t cacheFailTransport) SendOutputAsync(o protocol.Output) error {
1065+ return testSendOutputAsync(t, o)
1066+}
1067+func (t cacheFailTransport) SendOutputSynchronous(o protocol.Output) error {
1068+ return testSendOutputSynchronous(t, o)
1069+}
1070+
1071+func (t parkedRouteFailTransport) SendOutput(o protocol.Output) error { return testSendOutput(t, o) }
1072+func (t parkedRouteFailTransport) SendOutputAsync(o protocol.Output) error {
1073+ return testSendOutputAsync(t, o)
1074+}
1075+func (t parkedRouteFailTransport) SendOutputSynchronous(o protocol.Output) error {
1076+ return testSendOutputSynchronous(t, o)
1077+}
M internal/usecase/daemon/wire.go +21 -27
@@ -4,10 +4,9 @@ package daemon
4 import (4 import (
5 "github.com/bnema/vev/internal/domain"5 "github.com/bnema/vev/internal/domain"
6 "github.com/bnema/vev/internal/protocol"6 "github.com/bnema/vev/internal/protocol"
7- "github.com/bnema/vev/internal/protocol/wire"
8 )7 )
9 8
10-func frameWelcome(s *session, resumeToken uint64) (wire.Frame, error) {9+func serverWelcome(s *session, resumeToken uint64) (protocol.Welcome, error) {
11 s.mu.Lock()10 s.mu.Lock()
12 var identity *protocol.CommittedRouteIdentity11 var identity *protocol.CommittedRouteIdentity
13 if s.incarnation != (domain.SessionLifecycleID{}) {12 if s.incarnation != (domain.SessionLifecycleID{}) {
@@ -16,7 +15,7 @@ func frameWelcome(s *session, resumeToken uint64) (wire.Frame, error) {
16 Ephemeral: s.ephemeral,15 Ephemeral: s.ephemeral,
17 }16 }
18 }17 }
19- w := protocol.Welcome{18+ message := protocol.Welcome{
20 SessionID: string(s.id),19 SessionID: string(s.id),
21 SessionName: s.name,20 SessionName: s.name,
22 Ephemeral: s.ephemeral,21 Ephemeral: s.ephemeral,
@@ -24,37 +23,32 @@ func frameWelcome(s *session, resumeToken uint64) (wire.Frame, error) {
24 CommittedIdentity: identity,23 CommittedIdentity: identity,
25 }24 }
26 s.mu.Unlock()25 s.mu.Unlock()
27- w.ResumeToken = resumeToken26+ message.ResumeToken = resumeToken
28- payload := wire.MarshalWelcome(w)27+ if identity != nil {
29- if payload == nil {28+ if err := identity.Validate(); err != nil {
30- return wire.Frame{}, protocol.ErrInvalidRouteWire29+ return protocol.Welcome{}, err
30+ }
31 }31 }
32- return wire.Frame{Type: wire.MsgWelcome, Payload: payload}, nil32+ return message, nil
33 }33 }
34 34
35-func frameError(code uint16, text string) wire.Frame {35+func serverError(code uint16, text string) protocol.ErrorMsg {
36- return wire.Frame{Type: wire.MsgError, Payload: wire.MarshalErrorMsg(protocol.ErrorMsg{Code: code, Text: text})}36+ return protocol.ErrorMsg{Code: code, Text: text}
37 }37 }
38 38
39-func frameOutputState(b []byte, baseState uint64, state uint64, echoAck uint64) (wire.Frame, error) {39+func serverOutputState(data []byte, baseState, state, echoAck uint64) (protocol.Output, error) {
40- payload, err := wire.MarshalOutput(protocol.Output{40+ message := protocol.Output{
41 Epoch: 1, Base: baseState, New: state, Echo: echoAck,41 Epoch: 1, Base: baseState, New: state, Echo: echoAck,
42- Size: domain.Size{Cols: 1, Rows: 1}, Full: state != 0 && baseState == 0, Data: b,42+ Size: domain.Size{Cols: 1, Rows: 1}, Full: state != 0 && baseState == 0, Data: data,
43- })
44- if err != nil {
45- return wire.Frame{}, err
46 }43 }
47- return wire.Frame{Type: wire.MsgOutput, Payload: payload}, nil44+ if err := protocol.ValidateOutput(message); err != nil {
48-}45+ return protocol.Output{}, err
49-46+ }
50-func frameDetached(reason uint8) wire.Frame {47+ return message, nil
51- return wire.Frame{Type: wire.MsgDetached, Payload: wire.MarshalDetached(protocol.Detached{Reason: reason})}
52-}
53-
54-func framePong() wire.Frame {
55- return wire.Frame{Type: wire.MsgPong, Payload: wire.MarshalPong(protocol.Pong{})}
56 }48 }
57 49
58-func frameSessions(infos []protocol.SessionInfo) wire.Frame {50+func serverDetached(reason uint8) protocol.Detached { return protocol.Detached{Reason: reason} }
59- return wire.Frame{Type: wire.MsgSessions, Payload: wire.MarshalSessions(protocol.Sessions{Sessions: infos})}51+func serverPong() protocol.Pong { return protocol.Pong{} }
52+func serverSessions(infos []protocol.SessionInfo) protocol.Sessions {
53+ return protocol.Sessions{Sessions: infos}
60 }54 }
M internal/usecase/daemon/wire_metadata_test.go +2 -8
@@ -4,8 +4,6 @@ import (
4 "fmt"4 "fmt"
5 "sync"5 "sync"
6 "testing"6 "testing"
7-
8- "github.com/bnema/vev/internal/protocol/wire"
9 )7 )
10 8
11 func TestFrameWelcomeSnapshotsSessionMetadataUnderLock(t *testing.T) {9 func TestFrameWelcomeSnapshotsSessionMetadataUnderLock(t *testing.T) {
@@ -23,13 +21,9 @@ func TestFrameWelcomeSnapshotsSessionMetadataUnderLock(t *testing.T) {
23 })21 })
24 22
25 for range 1000 {23 for range 1000 {
26- frame, frameErr := frameWelcome(sess, ac.resumeToken)24+ welcome, err := serverWelcome(sess, ac.resumeToken)
27- if frameErr != nil {
28- t.Fatalf("frameWelcome() error = %v", frameErr)
29- }
30- welcome, err := wire.UnmarshalWelcome(frame.Payload)
31 if err != nil {25 if err != nil {
32- t.Fatalf("UnmarshalWelcome() error = %v", err)26+ t.Fatalf("serverWelcome() error = %v", err)
33 }27 }
34 if welcome.SessionID != "session" || welcome.SessionName == "" {28 if welcome.SessionID != "session" || welcome.SessionName == "" {
35 t.Fatalf("welcome metadata = %+v", welcome)29 t.Fatalf("welcome metadata = %+v", welcome)

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close