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