session: say why a detached owner failed to start
97 unmodified lines9899100101102103104105106107108109110111112113114115116117118112119120121122123124125126127128129130131132133134135136137119138139140141142143144145146147148149150151152153154155156157158159160161162163164165166492 unmodified linesExpand all97 unmodified lines if conn, err := net.DialTimeout("unix", sockPath, 30*time.Millisecond); err == nil { _ = conn.Close() return nil } if _, err := exec.LookPath(argv[0]); err != nil { return err } self, _ := os.Executable() if self == "" { self = os.Args[0] } cmd := exec.Command(self, argv...) cmd.Env = append(os.Environ(), "TOBI_OWNER=1", "TOBI_ID="+id) cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true} // the owner is detached, so its stderr is the only way it can explain itself var stderr syncBuffer cmd.Stderr = &stderr if err := cmd.Start(); err != nil { return fmt.Errorf("start owner: %w", err) } exited := make(chan struct{}) go func() { _ = cmd.Wait() }() go func() { _ = cmd.Wait(); close(exited) }()
fail := func(reason string) error { if msg := stderr.lastLine(); msg != "" { return fmt.Errorf("owner for session %q %s: %s", id, reason, msg) } return fmt.Errorf("owner for session %q %s", id, reason) } for deadline := time.Now().Add(2 * time.Second); time.Now().Before(deadline); time.Sleep(10 * time.Millisecond) { if conn, err := net.DialTimeout("unix", sockPath, 20*time.Millisecond); err == nil { _ = conn.Close() return nil } select { case <-exited: return fail("exited before it was ready") default: } } return fmt.Errorf("owner for session %q did not start", id) return fail("did not start")}
// read while the child may still be writing, so the copier must not be racedtype syncBuffer struct { mu sync.Mutex buf bytes.Buffer}
func (b *syncBuffer) Write(p []byte) (int, error) { b.mu.Lock() defer b.mu.Unlock() if b.buf.Len() > 8<<10 { return len(p), nil } return b.buf.Write(p)}
func (b *syncBuffer) lastLine() string { b.mu.Lock() defer b.mu.Unlock() lines := strings.Split(strings.TrimSpace(b.buf.String()), "\n") return strings.TrimSpace(lines[len(lines)-1])}
func ListLocal() []Session { dir := RuntimeDir() entries, _ := os.ReadDir(dir) var list []Session492 unmodified linesExpand all9 unmodified lines10111213141516171819202122232425262747 unmodified lines757677786979808182831 unmodified line8586878879899091929312 unmodified lines10610710810910011011111211311411 unmodified lines1261271281291201301311321331343 unmodified lines1381391401411421431441451461471481491501511529 unmodified lines
"tobi/internal/protocol")
// t.TempDir() blows past the ~104 byte cap on a unix socket path on macosfunc shortTempDir(t *testing.T) string { dir, err := os.MkdirTemp("", "tb") if err != nil { t.Fatal(err) } t.Cleanup(func() { _ = os.RemoveAll(dir) }) return dir}
func startOwner(t *testing.T, cmd ...string) string { id, sp := GenerateID(), "" sp = SocketPath(id) go func() { _ = RunOwner(id, cmd) }()47 unmodified lines }}
func TestStaleSocketCleanup(t *testing.T) { t.Setenv("XDG_RUNTIME_DIR", t.TempDir()) t.Setenv("XDG_RUNTIME_DIR", shortTempDir(t)) sp := filepath.Join(RuntimeDir(), "stale123456.sock") _ = os.WriteFile(sp, []byte("stale"), 0600) _ = ListLocal() if _, err := os.Stat(sp); !os.IsNotExist(err) {1 unmodified line }}
func TestOwnerMultiViewerHopAndOSC(t *testing.T) { t.Setenv("XDG_RUNTIME_DIR", t.TempDir()) t.Setenv("XDG_RUNTIME_DIR", shortTempDir(t)) sp := startOwner(t, "sh") v1, v2 := attach(t, sp), attach(t, sp) defer v1.Close() defer v2.Close()12 unmodified lines }}
func TestSlowViewerDoesNotBlockOwner(t *testing.T) { t.Setenv("XDG_RUNTIME_DIR", t.TempDir()) t.Setenv("XDG_RUNTIME_DIR", shortTempDir(t)) sp := startOwner(t, "sh", "-c", "dd if=/dev/zero bs=1024 count=2000; sleep 10") slow, _ := net.Dial("unix", sp) defer slow.Close() _ = protocol.WriteFrame(slow, protocol.TypeAttach, protocol.EncodeSize(80, 24))11 unmodified lines }}
func TestDetachedIdleTTL(t *testing.T) { t.Setenv("XDG_RUNTIME_DIR", t.TempDir()) t.Setenv("XDG_RUNTIME_DIR", shortTempDir(t)) orig := detachedTTL detachedTTL = 80 * time.Millisecond defer func() { detachedTTL = orig }() sp := startOwner(t, "sh", "-c", "sleep 10")3 unmodified lines if _, err := os.Stat(sp); !os.IsNotExist(err) { t.Fatalf("socket not reaped: %v", err) }}
func TestEnsureOwnerNamesAMissingCommand(t *testing.T) { t.Setenv("XDG_RUNTIME_DIR", shortTempDir(t)) err := EnsureOwner(GenerateID(), []string{"tobi-no-such-command-xyz"}) if err == nil || !strings.Contains(err.Error(), "tobi-no-such-command-xyz") { t.Fatalf("EnsureOwner = %v, want the missing command named", err) } if strings.Contains(err.Error(), "did not start") { t.Fatalf("EnsureOwner = %v, want the real reason rather than a timeout", err) }}