session: answer color queries with the viewers' agreed colors
310 unmodified linesExpand all31131231331431531631731831932031531631731831932032132232332432517 unmodified lines34334434534634634734834935035135235334734834935035135235335435535635735835936036136239 unmodified lines4024034044054014024064074084094104112 unmodified lines4144154164174184194204214224234243 unmodified lines428429430431432433434435436437438159 unmodified linesExpand all59859960060160260360460560659260760860961061179 unmodified lines69169269369468068168268368468569569669769869970070170270370470514 unmodified lines72072172272370870971071172472572672772872973073173273373473573673773873911 unmodified lines310 unmodified linesExpand all p []byte}
type viewerClient struct { conn net.Conn ch chan outMsg done chan struct{} once sync.Once cols int rows int conn net.Conn ch chan outMsg done chan struct{} once sync.Once cols int rows int colors map[int]termcolor.Color}
func (vc *viewerClient) Close() { vc.once.Do(func() {17 unmodified lines }}
type Owner struct { sess Session ptyFile *os.File vt *xterm.Terminal addon *xterm.SerializeAddon cmd *exec.Cmd listener net.Listener mu sync.Mutex viewers map[*viewerClient]struct{} sess Session ptyFile *os.File vt *xterm.Terminal addon *xterm.SerializeAddon cmd *exec.Cmd listener net.Listener mu sync.Mutex viewers map[*viewerClient]struct{} // the osc color handler reads colorSets while mu is held for vt writes, // so it gets its own lock colorMu sync.Mutex colorSets map[*viewerClient]map[int]termcolor.Color idleTimer *time.Timer closed bool}
39 unmodified lines } }}
func (o *Owner) addViewer(conn net.Conn, cols, rows int) (*viewerClient, []byte) { vc := &viewerClient{conn: conn, ch: make(chan outMsg, 128), done: make(chan struct{}), cols: cols, rows: rows}func (o *Owner) addViewer(conn net.Conn, cols, rows int, colors map[int]termcolor.Color) (*viewerClient, []byte) { vc := &viewerClient{conn: conn, ch: make(chan outMsg, 128), done: make(chan struct{}), cols: cols, rows: rows, colors: colors} o.mu.Lock() defer o.mu.Unlock() if o.closed { return nil, nil2 unmodified lines o.idleTimer.Stop() o.idleTimer = nil } o.viewers[vc] = struct{}{} o.colorMu.Lock() o.colorSets[vc] = colors o.colorMu.Unlock() o.applySizeLocked() return vc, append(o.addon.Serialize(nil), mouseEncodings[o.vt.DecPrivateModes().MouseEncoding]...)}
3 unmodified linesfunc (o *Owner) removeViewer(vc *viewerClient) { o.mu.Lock() defer o.mu.Unlock() delete(o.viewers, vc) o.colorMu.Lock() delete(o.colorSets, vc) o.colorMu.Unlock() if len(o.viewers) == 0 && !o.closed { o.idleTimer = time.AfterFunc(detachedTTL, o.Close) } o.applySizeLocked()159 unmodified linesExpand all } if rows < 1 { rows = 24 } var colors map[int]termcolor.Color if len(payload) > 4 { colors, _ = termcolor.DecodeColors(payload[4:]) }
vc, snap := o.addViewer(conn, cols, rows) vc, snap := o.addViewer(conn, cols, rows, colors) if vc == nil { _ = conn.Close() return }79 unmodified lines Command: strings.Join(command, " "), Cwd: cwd, StartedAt: time.Now(), }, ptyFile: ptyFile, vt: vt, addon: xterm.NewSerializeAddon(vt), cmd: cmd, listener: listener, viewers: map[*viewerClient]struct{}{}, ptyFile: ptyFile, vt: vt, addon: xterm.NewSerializeAddon(vt), cmd: cmd, listener: listener, viewers: map[*viewerClient]struct{}{}, colorSets: map[*viewerClient]map[int]termcolor.Color{}, }
vt.OnTitleChange(func(title string) { if title != "" {14 unmodified lines if ev.Type != xterm.ColorRequestReport { continue } i := ev.Index if i >= 0 && i < int(xterm.SpecialColorForeground) { fmt.Fprintf(owner.ptyFile, "\x1b]4;%d;rgb:0000/0000/0000\x1b\\", i) } else if i <= int(xterm.SpecialColorCursor) { fmt.Fprintf(owner.ptyFile, "\x1b]%d;rgb:0000/0000/0000\x1b\\", 10+i-int(xterm.SpecialColorForeground)) if i < 0 || i > int(xterm.SpecialColorCursor) { continue } owner.colorMu.Lock() sets := make([]map[int]termcolor.Color, 0, len(owner.colorSets)) for _, set := range owner.colorSets { sets = append(sets, set) } owner.colorMu.Unlock() if c, ok := termcolor.Agreed(i, sets); ok { fmt.Fprint(owner.ptyFile, termcolor.Reply(i, c)) } } }) owner.idleTimer = time.AfterFunc(detachedTTL, owner.Close)
11 unmodified lines123456781 unmodified line101112131415161718200 unmodified linesExpand all219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294package session
import ( "fmt" "net" "os" "os/exec" "path/filepath"1 unmodified line "testing" "time"
"tobi/internal/protocol" "tobi/internal/termcolor")
// t.TempDir() blows past the ~104 byte cap on a unix socket path on macosfunc shortTempDir(t *testing.T) string {200 unmodified linesExpand all if strings.Contains(err.Error(), "did not start") { t.Fatalf("EnsureOwner = %v, want the real reason rather than a timeout", err) }}func attachColors(t *testing.T, sp string, colors map[int]termcolor.Color) net.Conn { c, err := net.Dial("unix", sp) if err != nil { t.Fatal(err) } _ = protocol.WriteFrame(c, protocol.TypeAttach, append(protocol.EncodeSize(80, 24), termcolor.EncodeColors(colors)...)) if ft, _, err := protocol.ReadFrame(c); err != nil || ft != protocol.TypeSnapshot { t.Fatalf("snapshot: %v", err) } return c}
func expectNoOutput(t *testing.T, c net.Conn, contains string) { t.Helper() _ = c.SetReadDeadline(time.Now().Add(300 * time.Millisecond)) defer c.SetReadDeadline(time.Time{}) for { ft, p, err := protocol.ReadFrame(c) if err != nil { if ne, ok := err.(net.Error); ok && ne.Timeout() { return } t.Fatalf("frame %q: %v", contains, err) } if ft == protocol.TypeOutput && strings.Contains(string(p), contains) { t.Fatalf("unexpected %q", contains) } }}
func TestOwnerAnswersAgreedColorsOnly(t *testing.T) { t.Setenv("XDG_RUNTIME_DIR", shortTempDir(t)) sp := startOwner(t, "sh", "-c", "stty raw -echo; cat") ask := func(v net.Conn, i int) { _ = protocol.WriteFrame(v, protocol.TypeInput, []byte(fmt.Sprintf("\x1b]%d;?\x1b\\", i))) }
fg := termcolor.Color{R: 0xffff, G: 0xffff, B: 0xffff} v1 := attachColors(t, sp, map[int]termcolor.Color{termcolor.Foreground: fg, termcolor.Background: {R: 0, G: 0, B: 0}}) defer v1.Close() v2 := attachColors(t, sp, map[int]termcolor.Color{termcolor.Foreground: fg, termcolor.Background: {R: 0, G: 0, B: 0xffff}}) defer v2.Close()
ask(v1, 10) expectFrame(t, v1, protocol.TypeOutput, "\x1b]10;rgb:ffff/ffff/ffff") ask(v1, 11) expectNoOutput(t, v1, "\x1b]11;rgb:")
// a viewer without colors silences even agreed colors v3 := attach(t, sp) defer v3.Close() ask(v1, 10) expectNoOutput(t, v1, "\x1b]10;rgb:")
// once the disagreeing and unknown viewers detach the answer appears v2.Close() v3.Close() time.Sleep(100 * time.Millisecond) ask(v1, 11) expectFrame(t, v1, protocol.TypeOutput, "\x1b]11;rgb:0000/0000/0000")}func TestAttachPayloadKeepsLegacySize(t *testing.T) { colors := map[int]termcolor.Color{termcolor.Foreground: {R: 1, G: 2, B: 3}} payload := append(protocol.EncodeSize(120, 40), termcolor.EncodeColors(colors)...) c, r, err := protocol.DecodeSize(payload) if err != nil || c != 120 || r != 40 { t.Fatalf("DecodeSize on a longer payload = %d %d %v, want the size bytes only", c, r, err) } if colors2, ok := termcolor.DecodeColors(payload[4:]); !ok || colors2[termcolor.Foreground] != (termcolor.Color{R: 1, G: 2, B: 3}) { t.Fatalf("DecodeColors on the tail = %v %v", colors2, ok) }}