-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Expand file tree
/
Copy pathstreammanager.go
More file actions
448 lines (373 loc) · 9.81 KB
/
Copy pathstreammanager.go
File metadata and controls
448 lines (373 loc) · 9.81 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package jobmanager
import (
"encoding/base64"
"fmt"
"io"
"log"
"sync"
"github.com/wavetermdev/waveterm/pkg/wshrpc"
)
const (
CwndSize = 64 * 1024 // 64 KB window for connected mode
CirBufSize = 2 * 1024 * 1024 // 2 MB max buffer size
DisconnReadSz = 4 * 1024 // 4 KB read chunks when disconnected
MaxPacketSize = 4 * 1024 // 4 KB max data per packet
)
type DataSender interface {
SendData(dataPk wshrpc.CommandStreamData) error
}
type streamTerminalEvent struct {
isEof bool
err string
}
// StreamManager handles PTY output buffering with ACK-based flow control
type StreamManager struct {
lock sync.Mutex
drainCond *sync.Cond
streamId string
// this is the data read from the attached reader
buf *CirBuf
terminalEvent *streamTerminalEvent
eofPos int64 // fixed position when EOF/error occurs (-1 if not yet)
reader io.Reader
cwndSize int
rwndSize int
// invariant: if connected is true, dataSender is non-nil
connected bool
dataSender DataSender
// unacked state (reset on disconnect)
sentNotAcked int64
terminalEventSent bool
// track max acked to handle out-of-order ACKs (reset on disconnect)
maxAckedSeq int64
maxAckedRwnd int64
// terminal state - once true, stream is complete
terminalEventAcked bool
closed bool
// OnSendError, if set, is called (asynchronously) when a SendData call fails.
// Used to tear down the stale client connection so a fresh attach can proceed.
OnSendError func(err error)
}
func MakeStreamManager() *StreamManager {
return MakeStreamManagerWithSizes(CwndSize, CirBufSize)
}
func MakeStreamManagerWithSizes(cwndSize, cirbufSize int) *StreamManager {
sm := &StreamManager{
buf: MakeCirBuf(cirbufSize, true),
eofPos: -1,
cwndSize: cwndSize,
rwndSize: cwndSize,
}
sm.drainCond = sync.NewCond(&sm.lock)
go sm.senderLoop()
return sm
}
// AttachReader starts reading from the given reader
func (sm *StreamManager) AttachReader(r io.Reader) error {
sm.lock.Lock()
defer sm.lock.Unlock()
if sm.reader != nil {
return fmt.Errorf("reader already attached")
}
sm.reader = r
go sm.readLoop()
return nil
}
// ClientConnected transitions to CONNECTED mode
func (sm *StreamManager) ClientConnected(streamId string, dataSender DataSender, rwndSize int, clientSeq int64) (int64, error) {
sm.lock.Lock()
defer sm.lock.Unlock()
if sm.closed || sm.terminalEventAcked {
return 0, fmt.Errorf("stream is closed")
}
if sm.connected {
return 0, fmt.Errorf("client already connected")
}
if dataSender == nil {
return 0, fmt.Errorf("dataSender cannot be nil")
}
headPos := sm.buf.HeadPos()
if clientSeq > headPos {
bytesToConsume := int(clientSeq - headPos)
available := sm.buf.Size()
if bytesToConsume > available {
return 0, fmt.Errorf("client seq %d is beyond our stream end (head=%d, size=%d)", clientSeq, headPos, available)
}
if bytesToConsume > 0 {
if err := sm.buf.Consume(bytesToConsume); err != nil {
return 0, fmt.Errorf("failed to consume buffer: %w", err)
}
headPos = sm.buf.HeadPos()
}
}
sm.streamId = streamId
sm.dataSender = dataSender
sm.connected = true
sm.rwndSize = rwndSize
sm.sentNotAcked = 0
effectiveWindow := sm.cwndSize
if sm.rwndSize < effectiveWindow {
effectiveWindow = sm.rwndSize
}
sm.buf.SetEffectiveWindow(true, effectiveWindow)
sm.drainCond.Signal()
startSeq := headPos
if clientSeq > startSeq {
startSeq = clientSeq
}
return startSeq, nil
}
// GetStreamId returns the current stream ID (safe to call with lock held by caller)
func (sm *StreamManager) GetStreamId() string {
sm.lock.Lock()
defer sm.lock.Unlock()
return sm.streamId
}
// GetStreamDoneInfo returns whether the stream is done and the error if there was one.
// The error is only meaningful if done=true, as the error is delivered as part of the stream otherwise.
func (sm *StreamManager) GetStreamDoneInfo() (done bool, streamError string) {
sm.lock.Lock()
defer sm.lock.Unlock()
if !sm.terminalEventAcked {
return false, ""
}
if sm.terminalEvent != nil && !sm.terminalEvent.isEof {
return true, sm.terminalEvent.err
}
return true, ""
}
// ClientDisconnected transitions to DISCONNECTED mode
func (sm *StreamManager) ClientDisconnected() {
sm.lock.Lock()
defer sm.lock.Unlock()
if !sm.connected {
return
}
sm.connected = false
sm.dataSender = nil
sm.sentNotAcked = 0
sm.maxAckedSeq = 0
sm.maxAckedRwnd = 0
if !sm.terminalEventAcked {
sm.terminalEventSent = false
}
sm.buf.SetEffectiveWindow(false, CirBufSize)
sm.drainCond.Signal()
}
// RecvAck processes an ACK from the client
// must be connected, and streamid must match
func (sm *StreamManager) RecvAck(ackPk wshrpc.CommandStreamAckData) {
sm.lock.Lock()
defer sm.lock.Unlock()
if !sm.connected || ackPk.Id != sm.streamId {
return
}
if ackPk.Fin {
sm.terminalEventAcked = true
sm.drainCond.Signal()
return
}
seq := ackPk.Seq
rwnd := ackPk.RWnd
// Ignore stale ACKs using tuple comparison (seq, rwnd)
if seq < sm.maxAckedSeq || (seq == sm.maxAckedSeq && rwnd <= sm.maxAckedRwnd) {
// log.Printf("streammanager ignoring stale ACK: seq=%d rwnd=%d (max: seq=%d rwnd=%d)",
// seq, rwnd, sm.maxAckedSeq, sm.maxAckedRwnd)
return
}
// Update max acked tuple
sm.maxAckedSeq = seq
sm.maxAckedRwnd = rwnd
headPos := sm.buf.HeadPos()
if seq < headPos {
return
}
ackedBytes := seq - headPos
if ackedBytes > sm.sentNotAcked {
return
}
if ackedBytes > 0 {
if err := sm.buf.Consume(int(ackedBytes)); err != nil {
return
}
sm.sentNotAcked -= ackedBytes
}
prevRwnd := sm.rwndSize
sm.rwndSize = int(ackPk.RWnd)
effectiveWindow := sm.cwndSize
if sm.rwndSize < effectiveWindow {
effectiveWindow = sm.rwndSize
}
sm.buf.SetEffectiveWindow(true, effectiveWindow)
if sm.rwndSize > prevRwnd || ackedBytes > 0 {
sm.drainCond.Signal()
}
}
// SetRwndSize dynamically updates the receive window size
func (sm *StreamManager) SetRwndSize(rwndSize int) error {
sm.lock.Lock()
defer sm.lock.Unlock()
if rwndSize < 0 {
return fmt.Errorf("rwndSize cannot be negative")
}
if !sm.connected {
return fmt.Errorf("not connected")
}
sm.rwndSize = rwndSize
effectiveWindow := sm.cwndSize
if sm.rwndSize < effectiveWindow {
effectiveWindow = sm.rwndSize
}
sm.buf.SetEffectiveWindow(true, effectiveWindow)
sm.drainCond.Signal()
return nil
}
// Close shuts down the sender loop. The reader loop will exit on its next iteration
// or when the underlying reader is closed.
func (sm *StreamManager) Close() {
sm.lock.Lock()
defer sm.lock.Unlock()
sm.closed = true
sm.drainCond.Signal()
}
// readLoop is the main read goroutine
func (sm *StreamManager) readLoop() {
readBuf := make([]byte, MaxPacketSize)
for {
sm.lock.Lock()
closed := sm.closed
sm.lock.Unlock()
if closed {
return
}
n, err := sm.reader.Read(readBuf)
if n > 0 {
sm.handleReadData(readBuf[:n])
}
if err != nil {
if err == io.EOF {
sm.handleEOF()
} else {
sm.handleError(err)
}
return
}
}
}
func (sm *StreamManager) handleReadData(data []byte) {
offset := 0
for offset < len(data) {
n, waitCh := sm.buf.WriteAvailable(data[offset:])
offset += n
if n > 0 {
sm.lock.Lock()
sm.drainCond.Signal()
sm.lock.Unlock()
}
if waitCh != nil {
<-waitCh
}
}
}
func (sm *StreamManager) handleEOF() {
sm.lock.Lock()
defer sm.lock.Unlock()
log.Printf("handleEOF: PTY reached EOF, totalSize=%d", sm.buf.TotalSize())
sm.eofPos = sm.buf.TotalSize()
sm.terminalEvent = &streamTerminalEvent{isEof: true}
sm.drainCond.Signal()
}
func (sm *StreamManager) handleError(err error) {
sm.lock.Lock()
defer sm.lock.Unlock()
log.Printf("handleError: PTY error=%v, totalSize=%d", err, sm.buf.TotalSize())
sm.eofPos = sm.buf.TotalSize()
sm.terminalEvent = &streamTerminalEvent{err: err.Error()}
sm.drainCond.Signal()
}
func (sm *StreamManager) senderLoop() {
for {
done, pkt, sender := sm.prepareNextPacket()
if done {
return
}
if pkt == nil {
continue
}
err := sender.SendData(*pkt)
if err != nil {
log.Printf("senderLoop: send error (seq=%d): %v -- marking client disconnected\n", pkt.Seq, err)
sm.ClientDisconnected()
if sm.OnSendError != nil {
sm.OnSendError(err)
}
}
}
}
func (sm *StreamManager) prepareNextPacket() (done bool, pkt *wshrpc.CommandStreamData, sender DataSender) {
sm.lock.Lock()
defer sm.lock.Unlock()
available := sm.buf.Size()
if sm.closed || sm.terminalEventAcked {
return true, nil, nil
}
if !sm.connected {
sm.drainCond.Wait()
return false, nil, nil
}
if available == 0 {
if sm.terminalEvent != nil && !sm.terminalEventSent {
return false, sm.prepareTerminalPacket(), sm.dataSender
}
sm.drainCond.Wait()
return false, nil, nil
}
effectiveRwnd := sm.rwndSize
if sm.cwndSize < effectiveRwnd {
effectiveRwnd = sm.cwndSize
}
availableToSend := int64(effectiveRwnd) - sm.sentNotAcked
if availableToSend <= 0 {
sm.drainCond.Wait()
return false, nil, nil
}
peekSize := int(availableToSend)
if peekSize > MaxPacketSize {
peekSize = MaxPacketSize
}
if peekSize > available {
peekSize = available
}
data := make([]byte, peekSize)
n := sm.buf.PeekDataAt(int(sm.sentNotAcked), data)
if n == 0 {
sm.drainCond.Wait()
return false, nil, nil
}
data = data[:n]
seq := sm.buf.HeadPos() + sm.sentNotAcked
sm.sentNotAcked += int64(n)
return false, &wshrpc.CommandStreamData{
Id: sm.streamId,
Seq: seq,
Data64: base64.StdEncoding.EncodeToString(data),
}, sm.dataSender
}
func (sm *StreamManager) prepareTerminalPacket() *wshrpc.CommandStreamData {
if sm.terminalEventSent || sm.terminalEvent == nil {
return nil
}
pkt := &wshrpc.CommandStreamData{
Id: sm.streamId,
Seq: sm.eofPos,
}
if sm.terminalEvent.isEof {
pkt.Eof = true
} else {
pkt.Error = sm.terminalEvent.err
}
sm.terminalEventSent = true
return pkt
}