Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b71a9c5824 | ||
|
|
2939a82eca | ||
|
|
c582e52c15 | ||
|
|
ad1792735e | ||
|
|
1694f8c993 | ||
|
|
ecc46b8df2 | ||
|
|
83b9ce0de5 | ||
|
|
37159d31bd | ||
|
|
30eb6a82a5 | ||
|
|
cc1f22485d | ||
|
|
d8d48e4269 | ||
|
|
5f8cade139 | ||
|
|
bbe416444b |
@@ -12,7 +12,7 @@ import (
|
|||||||
// Client is a mosh client. It connects to a mosh server over UDP,
|
// Client is a mosh client. It connects to a mosh server over UDP,
|
||||||
// handles the SSP transport, and provides send/recv for terminal I/O.
|
// handles the SSP transport, and provides send/recv for terminal I/O.
|
||||||
type Client struct {
|
type Client struct {
|
||||||
conn *net.UDPConn
|
conn Conn
|
||||||
transport *Transport
|
transport *Transport
|
||||||
ocb *OCB
|
ocb *OCB
|
||||||
|
|
||||||
@@ -32,8 +32,8 @@ type Client struct {
|
|||||||
wg sync.WaitGroup
|
wg sync.WaitGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dial connects to a mosh server. The key is the base64-encoded mosh key
|
// Dial connects to a mosh server over UDP. The key is the base64-encoded
|
||||||
// (with or without padding).
|
// mosh key (with or without padding).
|
||||||
func Dial(host string, port int, key string) (*Client, error) {
|
func Dial(host string, port int, key string) (*Client, error) {
|
||||||
// Pad key for base64 if needed.
|
// Pad key for base64 if needed.
|
||||||
for len(key)%4 != 0 {
|
for len(key)%4 != 0 {
|
||||||
@@ -57,6 +57,12 @@ func Dial(host string, port int, key string) (*Client, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return DialConn(conn, ocb)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DialConn creates a mosh client over an existing datagram connection.
|
||||||
|
// Use this with WebTransport or other non-UDP transports.
|
||||||
|
func DialConn(conn Conn, ocb *OCB) (*Client, error) {
|
||||||
c := &Client{
|
c := &Client{
|
||||||
conn: conn,
|
conn: conn,
|
||||||
transport: NewTransport(ocb, false),
|
transport: NewTransport(ocb, false),
|
||||||
@@ -77,6 +83,29 @@ func Dial(host string, port int, key string) (*Client, error) {
|
|||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DialConnManual creates a mosh client without an internal sendLoop.
|
||||||
|
// The caller must call Tick() periodically (e.g., every 8-16ms) to flush
|
||||||
|
// outgoing datagrams. Use this in WASM where Go timers are unreliable.
|
||||||
|
func DialConnManual(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.wg.Add(1)
|
||||||
|
go c.recvLoop()
|
||||||
|
|
||||||
|
// Send initial keepalive to associate with the server.
|
||||||
|
c.transport.ForceNextSend()
|
||||||
|
c.tick()
|
||||||
|
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
// 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()
|
||||||
@@ -141,10 +170,23 @@ func (c *Client) Transport() *Transport {
|
|||||||
return c.transport
|
return c.transport
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tick flushes pending actions as mosh datagrams. Call this periodically
|
||||||
|
// from an external timer (e.g., JS setInterval in WASM) when the internal
|
||||||
|
// sendLoop is not used.
|
||||||
|
func (c *Client) Tick() {
|
||||||
|
c.tick()
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Client) tick() {
|
func (c *Client) tick() {
|
||||||
// Batch: compute diff from accumulated actions.
|
// Only create a new state when the server has acked the previous one.
|
||||||
|
// This matches the Dart mosh client's behavior: at most one unacked
|
||||||
|
// state in flight. Keystrokes accumulate until the ack arrives, then
|
||||||
|
// the next tick sends them all as a single new state.
|
||||||
|
// Without this check, SetPending resets diffSent, causing Tick() to
|
||||||
|
// increment sentNum for the same cumulative payload — the server
|
||||||
|
// applies duplicate keystrokes.
|
||||||
c.actionsMu.Lock()
|
c.actionsMu.Lock()
|
||||||
if c.dirty {
|
if c.dirty && c.transport.AckedByRemote() >= c.transport.SentNum() {
|
||||||
c.dirty = false
|
c.dirty = false
|
||||||
c.processAcksLocked()
|
c.processAcksLocked()
|
||||||
newActions := c.actions[c.ackedActionCount:]
|
newActions := c.actions[c.ackedActionCount:]
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
//go:build js && wasm
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"syscall/js"
|
||||||
|
|
||||||
|
mosh "github.com/unixshells/mosh-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
js.Global().Set("moshConnect", js.FuncOf(moshConnect))
|
||||||
|
select {} // keep alive
|
||||||
|
}
|
||||||
|
|
||||||
|
// moshConnect(url, key) → 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{} {
|
||||||
|
if len(args) < 2 {
|
||||||
|
return reject("moshConnect requires (url, key)")
|
||||||
|
}
|
||||||
|
|
||||||
|
url := args[0].String()
|
||||||
|
key := args[1].String()
|
||||||
|
|
||||||
|
handler := js.FuncOf(func(this js.Value, pargs []js.Value) interface{} {
|
||||||
|
resolve := pargs[0]
|
||||||
|
rejectFn := pargs[1]
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
session, err := newSession(url, key)
|
||||||
|
if err != nil {
|
||||||
|
rejectFn.Invoke(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resolve.Invoke(session.jsObject())
|
||||||
|
}()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return js.Global().Get("Promise").New(handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
type session struct {
|
||||||
|
client *mosh.Client
|
||||||
|
conn *wtConn
|
||||||
|
mu sync.Mutex
|
||||||
|
closed bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSession(url, key string) (*session, error) {
|
||||||
|
for len(key)%4 != 0 {
|
||||||
|
key += "="
|
||||||
|
}
|
||||||
|
rawKey, err := base64.StdEncoding.DecodeString(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("bad key: %w", err)
|
||||||
|
}
|
||||||
|
ocb, err := mosh.NewOCB(rawKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := dialWebTransport(url)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use DialConnManual — no internal sendLoop. JS drives Tick() via setInterval.
|
||||||
|
client, err := mosh.DialConnManual(conn, ocb)
|
||||||
|
if err != nil {
|
||||||
|
conn.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &session{client: client, conn: conn}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *session) jsObject() js.Value {
|
||||||
|
obj := js.Global().Get("Object").New()
|
||||||
|
|
||||||
|
obj.Set("send", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
||||||
|
if len(args) < 1 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
s.client.Send([]byte(args[0].String()))
|
||||||
|
s.client.Tick()
|
||||||
|
return nil
|
||||||
|
}))
|
||||||
|
|
||||||
|
obj.Set("resize", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
||||||
|
if len(args) < 2 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
s.client.Resize(uint16(args[0].Int()), uint16(args[1].Int()))
|
||||||
|
s.client.Tick()
|
||||||
|
return nil
|
||||||
|
}))
|
||||||
|
|
||||||
|
// tick() — flush pending keystrokes + keepalive/retransmit.
|
||||||
|
obj.Set("tick", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
||||||
|
s.client.Tick()
|
||||||
|
return nil
|
||||||
|
}))
|
||||||
|
|
||||||
|
// poll() — check for output. Called by JS setInterval.
|
||||||
|
// 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{} {
|
||||||
|
out := s.client.Recv(0)
|
||||||
|
if out == nil {
|
||||||
|
return js.Null()
|
||||||
|
}
|
||||||
|
return string(out)
|
||||||
|
}))
|
||||||
|
|
||||||
|
obj.Set("close", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
||||||
|
s.mu.Lock()
|
||||||
|
s.closed = true
|
||||||
|
s.mu.Unlock()
|
||||||
|
s.client.Close()
|
||||||
|
return nil
|
||||||
|
}))
|
||||||
|
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
|
||||||
|
func reject(msg string) js.Value {
|
||||||
|
return js.Global().Get("Promise").Call("reject", msg)
|
||||||
|
}
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
//go:build js && wasm
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"sync"
|
||||||
|
"syscall/js"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// wtConn wraps a browser WebTransport connection as a mosh.Conn.
|
||||||
|
type wtConn struct {
|
||||||
|
transport js.Value
|
||||||
|
writer js.Value // persistent writable stream writer
|
||||||
|
incoming chan []byte
|
||||||
|
done chan struct{}
|
||||||
|
once sync.Once
|
||||||
|
|
||||||
|
mu sync.Mutex
|
||||||
|
deadline time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func dialWebTransport(url string) (*wtConn, error) {
|
||||||
|
wt := js.Global().Get("WebTransport").New(url)
|
||||||
|
|
||||||
|
readyCh := make(chan error, 1)
|
||||||
|
onReady := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
||||||
|
readyCh <- nil
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
onFail := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
||||||
|
msg := "WebTransport connection failed"
|
||||||
|
if len(args) > 0 && !args[0].IsUndefined() && !args[0].IsNull() {
|
||||||
|
if m := args[0].Get("message"); !m.IsUndefined() {
|
||||||
|
msg = m.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
readyCh <- errors.New(msg)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
wt.Get("ready").Call("then", onReady, onFail)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err := <-readyCh:
|
||||||
|
if err != nil {
|
||||||
|
onReady.Release()
|
||||||
|
onFail.Release()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
case <-time.After(10 * time.Second):
|
||||||
|
onReady.Release()
|
||||||
|
onFail.Release()
|
||||||
|
wt.Call("close")
|
||||||
|
return nil, errors.New("WebTransport connect timeout")
|
||||||
|
}
|
||||||
|
onReady.Release()
|
||||||
|
onFail.Release()
|
||||||
|
|
||||||
|
writer := wt.Get("datagrams").Get("writable").Call("getWriter")
|
||||||
|
|
||||||
|
c := &wtConn{
|
||||||
|
transport: wt,
|
||||||
|
writer: writer,
|
||||||
|
incoming: make(chan []byte, 256),
|
||||||
|
done: make(chan struct{}),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start reading datagrams via a persistent JS callback.
|
||||||
|
c.startReader()
|
||||||
|
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// startReader sets up a self-chaining read loop using persistent callbacks.
|
||||||
|
func (c *wtConn) startReader() {
|
||||||
|
reader := c.transport.Get("datagrams").Get("readable").Call("getReader")
|
||||||
|
|
||||||
|
var onData, onErr js.Func
|
||||||
|
var readNext js.Func
|
||||||
|
|
||||||
|
readNext = js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
||||||
|
reader.Call("read").Call("then", onData, onErr)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
onData = js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
||||||
|
result := args[0]
|
||||||
|
if result.Get("done").Bool() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
value := result.Get("value")
|
||||||
|
buf := make([]byte, value.Get("byteLength").Int())
|
||||||
|
js.CopyBytesToGo(buf, js.Global().Get("Uint8Array").New(value))
|
||||||
|
select {
|
||||||
|
case c.incoming <- buf:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
// Defer next read to next event loop tick so Go goroutines can process.
|
||||||
|
js.Global().Call("setTimeout", readNext, 0)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
onErr = js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
||||||
|
// Stream error or closed — stop reading.
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
// Start first read.
|
||||||
|
readNext.Invoke()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *wtConn) Read(b []byte) (int, error) {
|
||||||
|
c.mu.Lock()
|
||||||
|
deadline := c.deadline
|
||||||
|
c.mu.Unlock()
|
||||||
|
|
||||||
|
var timer <-chan time.Time
|
||||||
|
if !deadline.IsZero() {
|
||||||
|
d := time.Until(deadline)
|
||||||
|
if d <= 0 {
|
||||||
|
return 0, errors.New("i/o timeout")
|
||||||
|
}
|
||||||
|
timer = time.After(d)
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-c.done:
|
||||||
|
return 0, errors.New("connection closed")
|
||||||
|
case data := <-c.incoming:
|
||||||
|
n := copy(b, data)
|
||||||
|
return n, nil
|
||||||
|
case <-timer:
|
||||||
|
return 0, errors.New("i/o timeout")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *wtConn) Write(b []byte) (int, error) {
|
||||||
|
arr := js.Global().Get("Uint8Array").New(len(b))
|
||||||
|
js.CopyBytesToJS(arr, b)
|
||||||
|
c.writer.Call("write", arr)
|
||||||
|
return len(b), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *wtConn) SetReadDeadline(t time.Time) error {
|
||||||
|
c.mu.Lock()
|
||||||
|
c.deadline = t
|
||||||
|
c.mu.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *wtConn) Close() error {
|
||||||
|
c.once.Do(func() {
|
||||||
|
close(c.done)
|
||||||
|
c.transport.Call("close")
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
|
mosh "github.com/unixshells/mosh-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if len(os.Args) != 4 {
|
||||||
|
fmt.Fprintf(os.Stderr, "Usage: %s <host> <port> <base64key>\n", os.Args[0])
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
host := os.Args[1]
|
||||||
|
port, _ := strconv.Atoi(os.Args[2])
|
||||||
|
key, err := base64.StdEncoding.DecodeString(os.Args[3])
|
||||||
|
if err != nil {
|
||||||
|
// Try raw base64 without padding.
|
||||||
|
key, err = base64.RawStdEncoding.DecodeString(os.Args[2+1])
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "bad key: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ocb, err := mosh.NewOCB(key)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "ocb: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
transport := mosh.NewTransport(ocb, false) // client mode
|
||||||
|
|
||||||
|
// Resolve host.
|
||||||
|
addrs, err := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", host, port))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "resolve: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := net.DialUDP("udp", nil, addrs)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "dial: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
flush := func() {
|
||||||
|
datagrams := transport.Tick()
|
||||||
|
for _, dg := range datagrams {
|
||||||
|
conn.Write(dg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read incoming in background.
|
||||||
|
go func() {
|
||||||
|
buf := make([]byte, 65536)
|
||||||
|
for {
|
||||||
|
n, err := conn.Read(buf)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
diff := transport.Recv(buf[:n])
|
||||||
|
if diff == nil || len(diff) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
instrs, err := mosh.UnmarshalHostMessage(diff)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("unmarshal error: %v\n", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, hi := range instrs {
|
||||||
|
if len(hi.Hoststring) > 0 {
|
||||||
|
text := string(hi.Hoststring)
|
||||||
|
if !utf8.ValidString(text) {
|
||||||
|
text = fmt.Sprintf("(%d raw bytes)", len(hi.Hoststring))
|
||||||
|
}
|
||||||
|
fmt.Printf("=== hoststring (%d bytes) ===\n%s\n=== end ===\n", len(hi.Hoststring), text)
|
||||||
|
}
|
||||||
|
if hi.Width > 0 || hi.Height > 0 {
|
||||||
|
fmt.Printf("=== resize: %dx%d ===\n", hi.Width, hi.Height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Tick timer.
|
||||||
|
ticker := time.NewTicker(20 * time.Millisecond)
|
||||||
|
go func() {
|
||||||
|
for range ticker.C {
|
||||||
|
flush()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Initial keepalive.
|
||||||
|
fmt.Println("Sending initial keepalive...")
|
||||||
|
transport.ForceNextSend()
|
||||||
|
flush()
|
||||||
|
|
||||||
|
// Resize.
|
||||||
|
fmt.Println("Sending resize 80x24...")
|
||||||
|
resize := mosh.MarshalUserMessage([]mosh.UserInstruction{
|
||||||
|
{Width: 80, Height: 24},
|
||||||
|
})
|
||||||
|
transport.SetPending(resize)
|
||||||
|
flush()
|
||||||
|
|
||||||
|
// Wait for initial output.
|
||||||
|
fmt.Println("Waiting 5 seconds for initial output...")
|
||||||
|
time.Sleep(5 * time.Second)
|
||||||
|
|
||||||
|
// Send ls + Enter.
|
||||||
|
fmt.Println("\nSending ls + Enter...")
|
||||||
|
keystroke := mosh.MarshalUserMessage([]mosh.UserInstruction{
|
||||||
|
{Keys: []byte("ls\r")},
|
||||||
|
})
|
||||||
|
transport.SetPending(keystroke)
|
||||||
|
flush()
|
||||||
|
|
||||||
|
// Wait for response.
|
||||||
|
time.Sleep(3 * time.Second)
|
||||||
|
|
||||||
|
fmt.Println("\nDone.")
|
||||||
|
ticker.Stop()
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package mosh
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// Conn is a datagram-oriented connection used by the mosh transport.
|
||||||
|
// UDP, WebTransport, and other datagram transports implement this interface.
|
||||||
|
type Conn interface {
|
||||||
|
// Read reads a datagram. Returns the number of bytes read.
|
||||||
|
// Must respect deadlines set by SetReadDeadline.
|
||||||
|
Read(b []byte) (int, error)
|
||||||
|
|
||||||
|
// Write writes a datagram.
|
||||||
|
Write(b []byte) (int, error)
|
||||||
|
|
||||||
|
// SetReadDeadline sets the deadline for Read calls.
|
||||||
|
SetReadDeadline(t time.Time) error
|
||||||
|
|
||||||
|
// Close closes the connection.
|
||||||
|
Close() error
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Direction bits in the 64-bit nonce header.
|
||||||
|
dirToServer = uint64(0) // client → server
|
||||||
|
dirToClient = uint64(1) << 63 // server → client
|
||||||
|
seqMask = ^(uint64(1) << 63) // lower 63 bits
|
||||||
|
|
||||||
|
// Minimum wire datagram: 8 (nonce) + 16 (tag) = 24 bytes.
|
||||||
|
minDatagram = 24
|
||||||
|
|
||||||
|
// Maximum payload per UDP datagram.
|
||||||
|
maxPayload = 16384
|
||||||
|
|
||||||
|
// Tick interval matches mosh's SEND_MINDELAY (8ms) for keystroke batching.
|
||||||
|
tickInterval = 8 * time.Millisecond
|
||||||
|
)
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//go:build !js
|
||||||
|
|
||||||
package mosh
|
package mosh
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
Executable
BIN
Binary file not shown.
@@ -1,3 +1,5 @@
|
|||||||
|
//go:build !js
|
||||||
|
|
||||||
package mosh
|
package mosh
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -18,20 +20,6 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// Direction bits in the 64-bit nonce header.
|
|
||||||
dirToServer = uint64(0) // client → server
|
|
||||||
dirToClient = uint64(1) << 63 // server → client
|
|
||||||
seqMask = ^(uint64(1) << 63) // lower 63 bits
|
|
||||||
|
|
||||||
// Minimum wire datagram: 8 (nonce) + 16 (tag) = 24 bytes.
|
|
||||||
minDatagram = 24
|
|
||||||
|
|
||||||
// Maximum payload per UDP datagram.
|
|
||||||
maxPayload = 16384
|
|
||||||
|
|
||||||
// Tick interval matches mosh's SEND_MINDELAY (8ms) for keystroke batching.
|
|
||||||
tickInterval = 8 * time.Millisecond
|
|
||||||
|
|
||||||
// Server gives up waiting for client after this.
|
// Server gives up waiting for client after this.
|
||||||
associationTimeout = 60 * time.Second
|
associationTimeout = 60 * time.Second
|
||||||
|
|
||||||
@@ -270,6 +258,9 @@ func (s *Server) readIO(r io.Reader, out chan<- []byte) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) mainLoopRW(rw io.Writer, resize func(cols, rows uint16), ioOutput <-chan []byte, userInput <-chan UserInstruction) {
|
func (s *Server) mainLoopRW(rw io.Writer, resize func(cols, rows uint16), ioOutput <-chan []byte, userInput <-chan UserInstruction) {
|
||||||
|
// Wait for the first client datagram. If no client connects within
|
||||||
|
// 60 seconds, this was a failed setup — shut down to release resources.
|
||||||
|
deadline := time.After(associationTimeout)
|
||||||
for {
|
for {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
addr := s.clientAddr
|
addr := s.clientAddr
|
||||||
@@ -280,6 +271,9 @@ func (s *Server) mainLoopRW(rw io.Writer, resize func(cols, rows uint16), ioOutp
|
|||||||
select {
|
select {
|
||||||
case <-s.done:
|
case <-s.done:
|
||||||
return
|
return
|
||||||
|
case <-deadline:
|
||||||
|
close(s.done)
|
||||||
|
return
|
||||||
case <-time.After(100 * time.Millisecond):
|
case <-time.After(100 * time.Millisecond):
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -369,7 +363,9 @@ func (s *Server) Close() {
|
|||||||
// snapshots the framebuffer, diffs against the last-acked state, and
|
// snapshots the framebuffer, diffs against the last-acked state, and
|
||||||
// sends CUP-based updates via the transport.
|
// sends CUP-based updates via the transport.
|
||||||
func (s *Server) mainLoop(ptyOutput <-chan []byte, userInput <-chan UserInstruction) {
|
func (s *Server) mainLoop(ptyOutput <-chan []byte, userInput <-chan UserInstruction) {
|
||||||
// Wait for client to connect before processing.
|
// Wait for the first client datagram. If no client connects within
|
||||||
|
// 60 seconds, this was a failed setup — shut down to release resources.
|
||||||
|
deadline := time.After(associationTimeout)
|
||||||
for {
|
for {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
addr := s.clientAddr
|
addr := s.clientAddr
|
||||||
@@ -380,6 +376,9 @@ func (s *Server) mainLoop(ptyOutput <-chan []byte, userInput <-chan UserInstruct
|
|||||||
select {
|
select {
|
||||||
case <-s.done:
|
case <-s.done:
|
||||||
return
|
return
|
||||||
|
case <-deadline:
|
||||||
|
close(s.done)
|
||||||
|
return
|
||||||
case <-time.After(100 * time.Millisecond):
|
case <-time.After(100 * time.Millisecond):
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//go:build !js
|
||||||
|
|
||||||
package mosh
|
package mosh
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
Reference in New Issue
Block a user