waveterm/pkg/streamclient/streamreader.go
Mike Sawka ae3e9f05b7
new job manager / framework for creating persistent remove sessions (#2779)
lots of stuff here.

introduces a streaming framework for the RPC system with flow control.
new authentication primitives for the RPC system. this is used to create
a persistent "job manager" process (via wsh) that can survive
disconnects. and then a jobcontroller in the main server that can
create, reconnect, and manage these new persistent jobs.

code is currently not actively hooked up to anything minus some new
debugging wsh commands, and a switch in the term block that lets me test
viewing the output.

after PRing this change the next steps are more testing and then
integrating this functionality into the product.
2026-01-21 16:54:18 -08:00

214 lines
4.1 KiB
Go

package streamclient
import (
"encoding/base64"
"fmt"
"io"
"sort"
"sync"
"github.com/wavetermdev/waveterm/pkg/wshrpc"
)
type AckSender interface {
SendAck(ackPk wshrpc.CommandStreamAckData)
}
type Reader struct {
lock sync.Mutex
cond *sync.Cond
id string
ackSender AckSender
readWindow int64
nextSeq int64
buffer []byte
eof bool
err error
closed bool
lastRwndSent int64
oooPackets []wshrpc.CommandStreamData // out-of-order packets awaiting delivery
}
func NewReader(id string, readWindow int64, ackSender AckSender) *Reader {
return NewReaderWithSeq(id, readWindow, 0, ackSender)
}
func NewReaderWithSeq(id string, readWindow int64, startSeq int64, ackSender AckSender) *Reader {
r := &Reader{
id: id,
readWindow: readWindow,
ackSender: ackSender,
nextSeq: startSeq,
lastRwndSent: readWindow,
}
r.cond = sync.NewCond(&r.lock)
return r
}
func (r *Reader) RecvData(dataPk wshrpc.CommandStreamData) {
r.lock.Lock()
defer r.lock.Unlock()
if r.closed || r.eof || r.err != nil {
return
}
if dataPk.Id != r.id {
return
}
// error packets can be sent without a valid Seq, so check for errors before validating sequence
if dataPk.Error != "" {
r.err = fmt.Errorf("stream error: %s", dataPk.Error)
r.cond.Broadcast()
r.sendAckLocked(true, false, "")
return
}
if dataPk.Seq < r.nextSeq {
return
}
if dataPk.Seq > r.nextSeq {
r.addOOOPacketLocked(dataPk)
return
}
r.recvDataOrderedLocked(dataPk)
r.processOOOPacketsLocked()
r.cond.Broadcast()
r.sendAckLocked(r.eof, false, "")
}
func (r *Reader) recvDataOrderedLocked(dataPk wshrpc.CommandStreamData) {
if dataPk.Data64 != "" {
data, err := base64.StdEncoding.DecodeString(dataPk.Data64)
if err != nil {
r.err = err
r.sendAckLocked(false, true, "base64 decode error")
return
}
r.buffer = append(r.buffer, data...)
r.nextSeq += int64(len(data))
}
if dataPk.Eof {
r.eof = true
}
}
func (r *Reader) addOOOPacketLocked(dataPk wshrpc.CommandStreamData) {
for _, pkt := range r.oooPackets {
if pkt.Seq == dataPk.Seq {
// this handles duplicates
return
}
}
r.oooPackets = append(r.oooPackets, dataPk)
}
func (r *Reader) processOOOPacketsLocked() {
if len(r.oooPackets) == 0 {
return
}
sort.Slice(r.oooPackets, func(i, j int) bool {
return r.oooPackets[i].Seq < r.oooPackets[j].Seq
})
consumed := 0
for _, pkt := range r.oooPackets {
if r.eof || r.err != nil {
// we're done, so we can clear any pending ooo packets
r.oooPackets = nil
return
}
if pkt.Seq != r.nextSeq {
break
}
r.recvDataOrderedLocked(pkt)
consumed++
}
r.oooPackets = r.oooPackets[consumed:]
}
func (r *Reader) sendAckLocked(fin bool, cancel bool, errStr string) {
rwnd := r.readWindow - int64(len(r.buffer))
if rwnd < 0 {
rwnd = 0
}
ack := wshrpc.CommandStreamAckData{
Id: r.id,
Seq: r.nextSeq,
Fin: fin,
Cancel: cancel,
RWnd: rwnd,
Error: errStr,
}
r.ackSender.SendAck(ack)
r.lastRwndSent = rwnd
}
func (r *Reader) Read(p []byte) (int, error) {
r.lock.Lock()
defer r.lock.Unlock()
for len(r.buffer) == 0 && !r.eof && r.err == nil && !r.closed {
r.cond.Wait()
}
if r.closed {
return 0, io.ErrClosedPipe
}
if r.err != nil {
return 0, r.err
}
if len(r.buffer) == 0 && r.eof {
return 0, io.EOF
}
n := copy(p, r.buffer)
r.buffer = r.buffer[n:]
if n > 0 {
currentRwnd := r.readWindow - int64(len(r.buffer))
if currentRwnd < 0 {
currentRwnd = 0
}
threshold := r.readWindow / 5
rwndDiff := currentRwnd - r.lastRwndSent
if len(r.buffer) == 0 || rwndDiff >= threshold {
r.sendAckLocked(false, false, "")
}
}
return n, nil
}
func (r *Reader) UpdateNextSeq(newSeq int64) {
r.lock.Lock()
defer r.lock.Unlock()
r.nextSeq = newSeq
}
func (r *Reader) Close() error {
r.lock.Lock()
defer r.lock.Unlock()
if r.closed {
if r.err != nil {
return r.err
}
return io.ErrClosedPipe
}
r.closed = true
if r.err == nil {
r.err = io.ErrClosedPipe
}
r.cond.Broadcast()
r.sendAckLocked(false, true, "")
return r.err
}