fix: prevent Mosh character ghosting caused by overlapping diffs
- client.go: track last output oldNum, replace instead of append when same base - transport.go: reject diffs that do not chain from latest acknowledged state (oldNum != ackNum), preventing stale ANSI sequences from corrupting output
This commit is contained in:
@@ -260,6 +260,13 @@ func (c *Client) recvLoop() {
|
||||
defer c.wg.Done()
|
||||
buf := make([]byte, maxPayload+64)
|
||||
|
||||
// Track the base state (oldNum) of the most recently buffered output.
|
||||
// Mosh diffs are cumulative snapshots from a given base. When a new diff
|
||||
// has the same oldNum as the previous one, it supersedes the older diff
|
||||
// and we replace the output buffer instead of appending. This prevents
|
||||
// character ghosting caused by concatenating overlapping diffs.
|
||||
var lastOutputOldNum uint64
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-c.done:
|
||||
@@ -287,6 +294,9 @@ func (c *Client) recvLoop() {
|
||||
continue
|
||||
}
|
||||
|
||||
// Read oldNum AFTER transport.Recv() — it's set by the Recv call above.
|
||||
currentOldNum := c.transport.LastRecvOldNum()
|
||||
|
||||
instrs, err := UnmarshalHostMessage(diff)
|
||||
if err != nil || len(instrs) == 0 {
|
||||
continue
|
||||
@@ -301,7 +311,14 @@ func (c *Client) recvLoop() {
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
c.output = append(c.output, output...)
|
||||
// Same base → newer diff supersedes older one (replace).
|
||||
// Different base → new incremental content (append).
|
||||
if currentOldNum == lastOutputOldNum {
|
||||
c.output = output
|
||||
} else {
|
||||
c.output = append(c.output, output...)
|
||||
}
|
||||
lastOutputOldNum = currentOldNum
|
||||
c.mu.Unlock()
|
||||
|
||||
select {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
module github.com/unixshells/mosh-go
|
||||
module github.com/ys-ll/mosh-go
|
||||
|
||||
go 1.26.0
|
||||
|
||||
|
||||
+14
-27
@@ -63,10 +63,6 @@ type Transport struct {
|
||||
// Latch capability negotiation.
|
||||
localCaps []byte
|
||||
remoteCaps []byte
|
||||
|
||||
// Reusable zlib writer to avoid per-tick allocations.
|
||||
zlibBuf bytes.Buffer
|
||||
zlibW *zlib.Writer
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -187,7 +183,7 @@ func (t *Transport) Tick() [][]byte {
|
||||
|
||||
// Marshal → compress → fragment → encrypt.
|
||||
pbData := ti.Marshal()
|
||||
compressed := t.zlibCompress(pbData)
|
||||
compressed := zlibCompress(pbData)
|
||||
frags := Fragmentize(t.sentNum, compressed)
|
||||
|
||||
var datagrams [][]byte
|
||||
@@ -308,15 +304,12 @@ func (t *Transport) Recv(wire []byte) []byte {
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we have old_num (required to apply diff).
|
||||
hasOld := false
|
||||
for _, n := range t.receivedNums {
|
||||
if n == ti.OldNum {
|
||||
hasOld = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasOld {
|
||||
// Reject diffs that do not chain from our latest acknowledged state.
|
||||
// Mosh diffs MUST be sequential: each diff's oldNum must equal the
|
||||
// newNum of the most recently accepted diff. Accepting diffs from
|
||||
// stale bases (oldNum != ackNum) causes overlapping ANSI sequences
|
||||
// that produce character ghosting and garbled output.
|
||||
if ti.OldNum != t.ackNum {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -473,19 +466,13 @@ func (t *Transport) updateRTT(tsReply uint16) {
|
||||
}
|
||||
}
|
||||
|
||||
// zlibCompress compresses data with zlib, reusing the writer.
|
||||
func (t *Transport) zlibCompress(data []byte) []byte {
|
||||
t.zlibBuf.Reset()
|
||||
if t.zlibW == nil {
|
||||
t.zlibW = zlib.NewWriter(&t.zlibBuf)
|
||||
} else {
|
||||
t.zlibW.Reset(&t.zlibBuf)
|
||||
}
|
||||
t.zlibW.Write(data)
|
||||
t.zlibW.Close()
|
||||
out := make([]byte, t.zlibBuf.Len())
|
||||
copy(out, t.zlibBuf.Bytes())
|
||||
return out
|
||||
// zlibCompress compresses data with zlib (default level).
|
||||
func zlibCompress(data []byte) []byte {
|
||||
var buf bytes.Buffer
|
||||
w := zlib.NewWriter(&buf)
|
||||
w.Write(data)
|
||||
w.Close()
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
// zlibDecompress decompresses zlib data. Returns nil on error.
|
||||
|
||||
Reference in New Issue
Block a user