Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0284bd652a | ||
|
|
c82251169f | ||
|
|
f065464e2c | ||
|
|
534e7da40d | ||
|
|
96ce8cef8d | ||
|
|
052945f9f8 | ||
|
|
aff8e0572a | ||
|
|
f70f18fc3f | ||
|
|
8d9bc2eba0 | ||
|
|
4a40090a5f |
@@ -106,6 +106,48 @@ func DialConnManual(conn Conn, ocb *OCB) (*Client, error) {
|
|||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DialConnRaw creates a mosh client with no internal goroutines.
|
||||||
|
// The caller must call Tick() for sending AND RecvRaw() for receiving.
|
||||||
|
// Use this when the caller needs raw transport diffs (e.g., for
|
||||||
|
// framebuffer state tracking in the WASM client).
|
||||||
|
func DialConnRaw(conn Conn, ocb *OCB) (*Client, error) {
|
||||||
|
c := &Client{
|
||||||
|
conn: conn,
|
||||||
|
transport: NewTransport(ocb, false),
|
||||||
|
ocb: ocb,
|
||||||
|
outputC: make(chan struct{}, 1),
|
||||||
|
done: make(chan struct{}),
|
||||||
|
sentActionCounts: make(map[uint64]int),
|
||||||
|
}
|
||||||
|
|
||||||
|
c.transport.ForceNextSend()
|
||||||
|
c.tick()
|
||||||
|
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecvRaw reads one datagram from the connection and processes it
|
||||||
|
// through the transport. Returns the raw diff payload, or nil if
|
||||||
|
// no complete message was received (timeout, fragment, replay).
|
||||||
|
// The caller can use Transport().LastRecvOldNum()/LastRecvNewNum()
|
||||||
|
// to get the state numbers for this diff.
|
||||||
|
func (c *Client) RecvRaw(timeout time.Duration) []byte {
|
||||||
|
buf := make([]byte, maxPayload+64)
|
||||||
|
|
||||||
|
c.conn.SetReadDeadline(time.Now().Add(timeout))
|
||||||
|
n, err := c.conn.Read(buf)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if n < minDatagram {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
data := make([]byte, n)
|
||||||
|
copy(data, buf[:n])
|
||||||
|
return c.transport.Recv(data)
|
||||||
|
}
|
||||||
|
|
||||||
// Send sends keystrokes to the server.
|
// Send sends keystrokes to the server.
|
||||||
func (c *Client) Send(keys []byte) {
|
func (c *Client) Send(keys []byte) {
|
||||||
c.actionsMu.Lock()
|
c.actionsMu.Lock()
|
||||||
|
|||||||
+52
-19
@@ -7,6 +7,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
"syscall/js"
|
"syscall/js"
|
||||||
|
"time"
|
||||||
|
|
||||||
mosh "github.com/unixshells/mosh-go"
|
mosh "github.com/unixshells/mosh-go"
|
||||||
)
|
)
|
||||||
@@ -16,23 +17,23 @@ func main() {
|
|||||||
select {} // keep alive
|
select {} // keep alive
|
||||||
}
|
}
|
||||||
|
|
||||||
// moshConnect(url, key) → Promise<MoshSession>
|
// moshConnect(url, key, cols, rows) → Promise<MoshSession>
|
||||||
// url: WebTransport URL (e.g., "https://relay.example.com/mosh/user/device")
|
|
||||||
// key: base64-encoded mosh key
|
|
||||||
func moshConnect(this js.Value, args []js.Value) interface{} {
|
func moshConnect(this js.Value, args []js.Value) interface{} {
|
||||||
if len(args) < 2 {
|
if len(args) < 4 {
|
||||||
return reject("moshConnect requires (url, key)")
|
return reject("moshConnect requires (url, key, cols, rows)")
|
||||||
}
|
}
|
||||||
|
|
||||||
url := args[0].String()
|
url := args[0].String()
|
||||||
key := args[1].String()
|
key := args[1].String()
|
||||||
|
cols := args[2].Int()
|
||||||
|
rows := args[3].Int()
|
||||||
|
|
||||||
handler := js.FuncOf(func(this js.Value, pargs []js.Value) interface{} {
|
handler := js.FuncOf(func(this js.Value, pargs []js.Value) interface{} {
|
||||||
resolve := pargs[0]
|
resolve := pargs[0]
|
||||||
rejectFn := pargs[1]
|
rejectFn := pargs[1]
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
session, err := newSession(url, key)
|
session, err := newSession(url, key, cols, rows)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
rejectFn.Invoke(err.Error())
|
rejectFn.Invoke(err.Error())
|
||||||
return
|
return
|
||||||
@@ -47,13 +48,14 @@ func moshConnect(this js.Value, args []js.Value) interface{} {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type session struct {
|
type session struct {
|
||||||
client *mosh.Client
|
client *mosh.Client
|
||||||
conn *wtConn
|
conn *wtConn
|
||||||
mu sync.Mutex
|
tracker *stateTracker
|
||||||
closed bool
|
mu sync.Mutex
|
||||||
|
closed bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func newSession(url, key string) (*session, error) {
|
func newSession(url, key string, cols, rows int) (*session, error) {
|
||||||
for len(key)%4 != 0 {
|
for len(key)%4 != 0 {
|
||||||
key += "="
|
key += "="
|
||||||
}
|
}
|
||||||
@@ -71,14 +73,22 @@ func newSession(url, key string) (*session, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use DialConnManual — no internal sendLoop. JS drives Tick() via setInterval.
|
client, err := mosh.DialConnRaw(conn, ocb)
|
||||||
client, err := mosh.DialConnManual(conn, ocb)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
conn.Close()
|
conn.Close()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &session{client: client, conn: conn}, nil
|
s := &session{
|
||||||
|
client: client,
|
||||||
|
conn: conn,
|
||||||
|
tracker: newStateTracker(cols, rows),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Background goroutine: read from mosh client, feed to state tracker.
|
||||||
|
go s.recvLoop()
|
||||||
|
|
||||||
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *session) jsObject() js.Value {
|
func (s *session) jsObject() js.Value {
|
||||||
@@ -97,7 +107,10 @@ func (s *session) jsObject() js.Value {
|
|||||||
if len(args) < 2 {
|
if len(args) < 2 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
s.client.Resize(uint16(args[0].Int()), uint16(args[1].Int()))
|
cols := args[0].Int()
|
||||||
|
rows := args[1].Int()
|
||||||
|
s.client.Resize(uint16(cols), uint16(rows))
|
||||||
|
s.tracker.resize(cols, rows)
|
||||||
s.client.Tick()
|
s.client.Tick()
|
||||||
return nil
|
return nil
|
||||||
}))
|
}))
|
||||||
@@ -108,11 +121,9 @@ func (s *session) jsObject() js.Value {
|
|||||||
return nil
|
return nil
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// poll() — check for output. Called by JS setInterval.
|
// poll() — returns pre-diffed ANSI output from the state tracker.
|
||||||
// Returns string if output available, null otherwise.
|
|
||||||
// Using JS-driven polling avoids Go goroutine scheduler starvation.
|
|
||||||
obj.Set("poll", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
obj.Set("poll", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
||||||
out := s.client.Recv(0)
|
out := s.tracker.poll()
|
||||||
if out == nil {
|
if out == nil {
|
||||||
return js.Null()
|
return js.Null()
|
||||||
}
|
}
|
||||||
@@ -130,6 +141,28 @@ func (s *session) jsObject() js.Value {
|
|||||||
return obj
|
return obj
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// recvLoop reads raw transport diffs and feeds them to the state
|
||||||
|
// tracker for framebuffer-based processing.
|
||||||
|
func (s *session) recvLoop() {
|
||||||
|
for {
|
||||||
|
diff := s.client.RecvRaw(1 * time.Second)
|
||||||
|
if diff == nil {
|
||||||
|
s.mu.Lock()
|
||||||
|
closed := s.closed
|
||||||
|
s.mu.Unlock()
|
||||||
|
if closed {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
t := s.client.Transport()
|
||||||
|
oldNum := t.LastRecvOldNum()
|
||||||
|
newNum := t.LastRecvNewNum()
|
||||||
|
throwawayNum := t.ThrowawayNum()
|
||||||
|
s.tracker.applyDiff(diff, oldNum, newNum, throwawayNum)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func reject(msg string) js.Value {
|
func reject(msg string) js.Value {
|
||||||
return js.Global().Get("Promise").Call("reject", msg)
|
return js.Global().Get("Promise").Call("reject", msg)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
//go:build js && wasm
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
mosh "github.com/unixshells/mosh-go"
|
||||||
|
vt "github.com/unixshells/vt-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
// stateTracker implements Dart-style received state management.
|
||||||
|
// Each incoming diff is applied to a copy of the base state (looked up
|
||||||
|
// by oldNum), producing a new state stored by newNum. The display
|
||||||
|
// always shows the latest received state, diffed against what's
|
||||||
|
// currently on screen. This eliminates garbled output from missed
|
||||||
|
// diffs and overlapping state transitions.
|
||||||
|
type stateTracker struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
|
||||||
|
shadow *vt.Emulator
|
||||||
|
shadowState uint64
|
||||||
|
cols, rows int
|
||||||
|
|
||||||
|
// Received states: stateNum → framebuffer snapshot.
|
||||||
|
states map[uint64]*mosh.Framebuffer
|
||||||
|
latestState uint64
|
||||||
|
displayedFB *mosh.Framebuffer
|
||||||
|
|
||||||
|
// Output buffer: pre-diffed ANSI ready for the terminal.
|
||||||
|
output []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func newStateTracker(cols, rows int) *stateTracker {
|
||||||
|
return &stateTracker{
|
||||||
|
shadow: vt.NewEmulator(cols, rows),
|
||||||
|
cols: cols,
|
||||||
|
rows: rows,
|
||||||
|
states: make(map[uint64]*mosh.Framebuffer),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyDiff processes an incoming server diff using state tracking.
|
||||||
|
// throwawayNum is the server's indication of which states it no longer references.
|
||||||
|
func (st *stateTracker) applyDiff(diff []byte, oldNum, newNum, throwawayNum uint64) {
|
||||||
|
st.mu.Lock()
|
||||||
|
defer st.mu.Unlock()
|
||||||
|
|
||||||
|
// Already have this state — skip.
|
||||||
|
if _, ok := st.states[newNum]; ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Snapshot current shadow as oldNum if we don't have it.
|
||||||
|
if oldNum == st.shadowState {
|
||||||
|
if _, ok := st.states[oldNum]; !ok {
|
||||||
|
st.states[oldNum] = mosh.SnapshotEmulator(st.shadow, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Need the base state to apply the diff.
|
||||||
|
base, ok := st.states[oldNum]
|
||||||
|
if !ok {
|
||||||
|
// After a resize we have no states. Start fresh with a
|
||||||
|
// clean emulator so the first post-resize diff works.
|
||||||
|
st.shadow = vt.NewEmulator(st.cols, st.rows)
|
||||||
|
st.shadowState = oldNum
|
||||||
|
base = mosh.SnapshotEmulator(st.shadow, true)
|
||||||
|
st.states[oldNum] = base
|
||||||
|
} else if st.shadowState != oldNum {
|
||||||
|
st.shadow = vt.NewEmulator(st.cols, st.rows)
|
||||||
|
// Write the base framebuffer as ANSI to restore state.
|
||||||
|
if base != nil {
|
||||||
|
st.shadow.Write(base.Diff(nil))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply diff: feed hoststrings to shadow.
|
||||||
|
instrs, err := mosh.UnmarshalHostMessage(diff)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, hi := range instrs {
|
||||||
|
if len(hi.Hoststring) > 0 {
|
||||||
|
st.shadow.Write(hi.Hoststring)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
st.shadowState = newNum
|
||||||
|
|
||||||
|
// Snapshot result.
|
||||||
|
snap := mosh.SnapshotEmulator(st.shadow, true)
|
||||||
|
st.states[newNum] = snap
|
||||||
|
if newNum > st.latestState {
|
||||||
|
st.latestState = newNum
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prune states the server no longer references.
|
||||||
|
if throwawayNum > 0 {
|
||||||
|
for n := range st.states {
|
||||||
|
if n < throwawayNum {
|
||||||
|
delete(st.states, n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display diff: compute ANSI difference between displayed and latest.
|
||||||
|
latest := st.states[st.latestState]
|
||||||
|
if latest != nil {
|
||||||
|
ansi := latest.Diff(st.displayedFB)
|
||||||
|
if len(ansi) > 0 {
|
||||||
|
st.output = append(st.output, ansi...)
|
||||||
|
}
|
||||||
|
st.displayedFB = latest
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// poll returns accumulated ANSI output, or nil.
|
||||||
|
func (st *stateTracker) poll() []byte {
|
||||||
|
st.mu.Lock()
|
||||||
|
defer st.mu.Unlock()
|
||||||
|
if len(st.output) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := st.output
|
||||||
|
st.output = nil
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// resize resets the state tracker for new dimensions.
|
||||||
|
func (st *stateTracker) resize(cols, rows int) {
|
||||||
|
st.mu.Lock()
|
||||||
|
defer st.mu.Unlock()
|
||||||
|
st.cols = cols
|
||||||
|
st.rows = rows
|
||||||
|
st.shadow = vt.NewEmulator(cols, rows)
|
||||||
|
st.shadowState = 0
|
||||||
|
st.states = make(map[uint64]*mosh.Framebuffer)
|
||||||
|
st.displayedFB = nil
|
||||||
|
}
|
||||||
|
|
||||||
+22
-13
@@ -1,5 +1,3 @@
|
|||||||
//go:build !js
|
|
||||||
|
|
||||||
package mosh
|
package mosh
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -7,9 +5,20 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
uv "github.com/charmbracelet/ultraviolet"
|
|
||||||
"github.com/charmbracelet/x/ansi"
|
"github.com/charmbracelet/x/ansi"
|
||||||
"github.com/charmbracelet/x/vt"
|
"github.com/unixshells/vt-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Cell attribute bitmasks (matching charmbracelet/ultraviolet values).
|
||||||
|
const (
|
||||||
|
attrBold = 1 << 0
|
||||||
|
attrFaint = 1 << 1
|
||||||
|
attrItalic = 1 << 2
|
||||||
|
attrBlink = 1 << 3
|
||||||
|
attrRapidBlink = 1 << 4
|
||||||
|
attrReverse = 1 << 5
|
||||||
|
attrConceal = 1 << 6
|
||||||
|
attrStrikethrough = 1 << 7
|
||||||
)
|
)
|
||||||
|
|
||||||
// Framebuffer represents a terminal screen state for mosh SSP.
|
// Framebuffer represents a terminal screen state for mosh SSP.
|
||||||
@@ -165,11 +174,11 @@ func (fb *Framebuffer) fullRedraw() []byte {
|
|||||||
buf = append(buf, "\r\n"...)
|
buf = append(buf, "\r\n"...)
|
||||||
}
|
}
|
||||||
rowOff := y * fb.W
|
rowOff := y * fb.W
|
||||||
// Track trailing spaces to avoid sending them.
|
// Track trailing default-attr spaces to avoid sending them.
|
||||||
lastNonSpace := -1
|
lastNonSpace := -1
|
||||||
for x := fb.W - 1; x >= 0; x-- {
|
for x := fb.W - 1; x >= 0; x-- {
|
||||||
c := &fb.Cells[rowOff+x]
|
c := &fb.Cells[rowOff+x]
|
||||||
if c.Rune != ' ' || c.Rune == 0 || c.Attr != (Attr{}) {
|
if (c.Rune != ' ' && c.Rune != 0) || c.Attr != (Attr{}) {
|
||||||
lastNonSpace = x
|
lastNonSpace = x
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -403,13 +412,13 @@ func SnapshotEmulator(emu *vt.Emulator, cursorVisible bool) *Framebuffer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Attributes.
|
// Attributes.
|
||||||
cell.Attr.Bold = c.Style.Attrs&uv.AttrBold != 0
|
cell.Attr.Bold = c.Style.Attrs&attrBold != 0
|
||||||
cell.Attr.Dim = c.Style.Attrs&uv.AttrFaint != 0
|
cell.Attr.Dim = c.Style.Attrs&attrFaint != 0
|
||||||
cell.Attr.Italic = c.Style.Attrs&uv.AttrItalic != 0
|
cell.Attr.Italic = c.Style.Attrs&attrItalic != 0
|
||||||
cell.Attr.Blink = c.Style.Attrs&uv.AttrBlink != 0
|
cell.Attr.Blink = c.Style.Attrs&attrBlink != 0
|
||||||
cell.Attr.Reverse = c.Style.Attrs&uv.AttrReverse != 0
|
cell.Attr.Reverse = c.Style.Attrs&attrReverse != 0
|
||||||
cell.Attr.Strike = c.Style.Attrs&uv.AttrStrikethrough != 0
|
cell.Attr.Strike = c.Style.Attrs&attrStrikethrough != 0
|
||||||
cell.Attr.Under = c.Style.Underline != uv.UnderlineNone
|
cell.Attr.Under = c.Style.Underline != ansi.UnderlineNone
|
||||||
|
|
||||||
cell.Attr.FG = convertColor(c.Style.Fg)
|
cell.Attr.FG = convertColor(c.Style.Fg)
|
||||||
cell.Attr.BG = convertColor(c.Style.Bg)
|
cell.Attr.BG = convertColor(c.Style.Bg)
|
||||||
|
|||||||
@@ -3,16 +3,16 @@ module github.com/unixshells/mosh-go
|
|||||||
go 1.26.0
|
go 1.26.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/charmbracelet/ultraviolet v0.0.0-20260303162955-0b88c25f3fff
|
|
||||||
github.com/charmbracelet/x/ansi v0.11.6
|
github.com/charmbracelet/x/ansi v0.11.6
|
||||||
github.com/charmbracelet/x/vt v0.0.0-20260305213658-fe36e8c10185
|
|
||||||
github.com/creack/pty v1.1.24
|
github.com/creack/pty v1.1.24
|
||||||
|
github.com/unixshells/vt-go v0.1.0
|
||||||
golang.org/x/crypto v0.49.0
|
golang.org/x/crypto v0.49.0
|
||||||
golang.org/x/term v0.41.0
|
golang.org/x/term v0.41.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/charmbracelet/colorprofile v0.4.2 // indirect
|
github.com/charmbracelet/colorprofile v0.4.2 // indirect
|
||||||
|
github.com/charmbracelet/ultraviolet v0.0.0-20260303162955-0b88c25f3fff // indirect
|
||||||
github.com/charmbracelet/x/exp/ordered v0.1.0 // indirect
|
github.com/charmbracelet/x/exp/ordered v0.1.0 // indirect
|
||||||
github.com/charmbracelet/x/term v0.2.2 // indirect
|
github.com/charmbracelet/x/term v0.2.2 // indirect
|
||||||
github.com/charmbracelet/x/termios v0.1.1 // indirect
|
github.com/charmbracelet/x/termios v0.1.1 // indirect
|
||||||
|
|||||||
@@ -10,8 +10,6 @@ github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSg
|
|||||||
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
||||||
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
|
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
|
||||||
github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
|
github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
|
||||||
github.com/charmbracelet/x/vt v0.0.0-20260305213658-fe36e8c10185 h1:KYS9lWBROkdkuyc0bdQuS8C86Me3OAgjUAKcB4zArlg=
|
|
||||||
github.com/charmbracelet/x/vt v0.0.0-20260305213658-fe36e8c10185/go.mod h1:2djWW5EeP5QlpztEYYbbyyzUyyxVD71mWVQguNISHcM=
|
|
||||||
github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM=
|
github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM=
|
||||||
github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k=
|
github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k=
|
||||||
github.com/clipperhouse/displaywidth v0.9.0 h1:Qb4KOhYwRiN3viMv1v/3cTBlz3AcAZX3+y9OLhMtAtA=
|
github.com/clipperhouse/displaywidth v0.9.0 h1:Qb4KOhYwRiN3viMv1v/3cTBlz3AcAZX3+y9OLhMtAtA=
|
||||||
@@ -30,6 +28,8 @@ github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELU
|
|||||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
|
github.com/unixshells/vt-go v0.1.0 h1:HWILcExV8MHc6o2YqIRMIM/KrjH31kPzZrlvLS9BG6Y=
|
||||||
|
github.com/unixshells/vt-go v0.1.0/go.mod h1:dZpOXxVyO7LG1uxkVMixX7HwUf2ZbUkEKhtHHJTTIBI=
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||||
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import (
|
|||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/charmbracelet/x/vt"
|
"github.com/unixshells/vt-go"
|
||||||
"github.com/creack/pty"
|
"github.com/creack/pty"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+32
-4
@@ -34,10 +34,12 @@ type Transport struct {
|
|||||||
pendingDataAck bool // true = send ack ASAP (received data, not just ack)
|
pendingDataAck bool // true = send ack ASAP (received data, not just ack)
|
||||||
|
|
||||||
// Incoming state — list of received state nums for old_num validation.
|
// Incoming state — list of received state nums for old_num validation.
|
||||||
receivedNums []uint64 // ordered list of state nums we have
|
receivedNums []uint64 // ordered list of state nums we have
|
||||||
ackNum uint64 // latest received state num
|
ackNum uint64 // latest received state num
|
||||||
sentAckNum uint64 // last ackNum we actually sent on wire
|
sentAckNum uint64 // last ackNum we actually sent on wire
|
||||||
throwawayNum uint64 // oldest state we still hold
|
throwawayNum uint64 // oldest state we still hold
|
||||||
|
lastRecvOldNum uint64 // oldNum from most recently received diff
|
||||||
|
lastRecvNewNum uint64 // newNum from most recently received diff
|
||||||
|
|
||||||
// Sequence counter for the crypto layer (independent of SSP state numbering).
|
// Sequence counter for the crypto layer (independent of SSP state numbering).
|
||||||
seqOut uint64
|
seqOut uint64
|
||||||
@@ -326,6 +328,10 @@ func (t *Transport) Recv(wire []byte) []byte {
|
|||||||
t.receivedNums = filtered
|
t.receivedNums = filtered
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Track oldNum/newNum for state management.
|
||||||
|
t.lastRecvOldNum = ti.OldNum
|
||||||
|
t.lastRecvNewNum = ti.NewNum
|
||||||
|
|
||||||
// Add new state.
|
// Add new state.
|
||||||
t.receivedNums = append(t.receivedNums, ti.NewNum)
|
t.receivedNums = append(t.receivedNums, ti.NewNum)
|
||||||
|
|
||||||
@@ -359,6 +365,28 @@ func (t *Transport) SentNum() uint64 {
|
|||||||
return t.sentNum
|
return t.sentNum
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LastRecvOldNum returns the oldNum from the most recently received diff.
|
||||||
|
func (t *Transport) LastRecvOldNum() uint64 {
|
||||||
|
t.mu.Lock()
|
||||||
|
defer t.mu.Unlock()
|
||||||
|
return t.lastRecvOldNum
|
||||||
|
}
|
||||||
|
|
||||||
|
// LastRecvNewNum returns the newNum from the most recently received diff.
|
||||||
|
func (t *Transport) LastRecvNewNum() uint64 {
|
||||||
|
t.mu.Lock()
|
||||||
|
defer t.mu.Unlock()
|
||||||
|
return t.lastRecvNewNum
|
||||||
|
}
|
||||||
|
|
||||||
|
// ThrowawayNum returns the server's throwaway number — states below this
|
||||||
|
// are no longer referenced by the server and can be safely pruned.
|
||||||
|
func (t *Transport) ThrowawayNum() uint64 {
|
||||||
|
t.mu.Lock()
|
||||||
|
defer t.mu.Unlock()
|
||||||
|
return t.throwawayNum
|
||||||
|
}
|
||||||
|
|
||||||
// LastRecv returns the time of the last received datagram.
|
// LastRecv returns the time of the last received datagram.
|
||||||
func (t *Transport) LastRecv() time.Time {
|
func (t *Transport) LastRecv() time.Time {
|
||||||
t.mu.Lock()
|
t.mu.Lock()
|
||||||
|
|||||||
Reference in New Issue
Block a user