refactor some code and remove scaffolding tests
16 unmodified lines171819202121222324256 unmodified lines3233343536363738394015 unmodified lines16 unmodified lines version = "0.1.0"; src = ./.; vendorHash = "sha256-qbOGgVRsO0/F1dB/vIERm6EsUjgWGzT2z938pnKVbcI="; subPackages = [ "." ]; ldflags = [ "-s" "-w" "-X tobi/internal/version.Version=${rev}" ]; ldflags = [ "-s" "-w" "-X tobi/internal/remote.Version=${rev}" ]; nativeBuildInputs = [ pkgs.zstd ]; # cross-compile every carrier slice into a temp dir outside # internal/remote/payloads, so no slice can recursively embed the # slices of earlier loop iterations (same slices as6 unmodified lines os=''${target%%/*} arch=''${target##*/} echo "building carrier slice for $os/$arch" CGO_ENABLED=0 GOOS=$os GOARCH=$arch go build -trimpath \ -ldflags="-s -w -X tobi/internal/version.Version=${rev}" \ -ldflags="-s -w -X tobi/internal/remote.Version=${rev}" \ -o $slices/tobi-$os-$arch . zstd -19 -q --rm $slices/tobi-$os-$arch done mv $slices/*.zst internal/remote/payloads/15 unmodified lines19 unmodified lines20212223242524252728262728322930313233343835363738396 unmodified lines19 unmodified lines
func dispatch(args []string) error { switch { case os.Getenv("TOBI_COMPLETE") == "1": _ = app.Complete(strings.Join(args, " ")) return nil return app.Complete(strings.Join(args, " ")) case os.Getenv("TOBI_OWNER") == "1": id := os.Getenv("TOBI_ID") // the inner command must not re-enter owner, rpc, or completion mode for _, k := range []string{"TOBI_OWNER", "TOBI_ID", "TOBI_RPC", "TOBI_COMPLETE"} { os.Unsetenv(k) } return wrapErr("owner", session.RunOwner(id, args)) return wrapErr("owner", session.RunOwner(os.Getenv("TOBI_ID"), args)) case os.Getenv("TOBI_RPC") != "": op := os.Getenv("TOBI_RPC") return wrapErr("rpc "+op, remote.RunRPC(op, os.Getenv("TOBI_ID"))) } if i := slices.Index(args, "--"); i != -1 { return app.Spawn(strings.TrimSpace(strings.Join(args[:i], " ")), args[i+1:]) return app.Spawn(strings.Join(args[:i], " "), args[i+1:]) } return app.Interactive(strings.Join(args, " "))}
6 unmodified lines6 unmodified lines7891011111213141520 unmodified lines6 unmodified lines "darwin/arm64")
VERSION="$(git describe --always --dirty 2>/dev/null || echo dev)"LDFLAGS="-s -w -X tobi/internal/version.Version=${VERSION}"LDFLAGS="-s -w -X tobi/internal/remote.Version=${VERSION}"
tmp_dir="$(mktemp -d /tmp/tobi-slices-XXXXXX)"trap 'rm -rf "${tmp_dir}"' EXIT
20 unmodified lines19 unmodified lines2021222324242526252627282928303132313233343533343536373829 unmodified lines686970717372737476777879757677787984858687888990918081828384858687889394959689909192939499959697981 unmodified line1001011021031091041051111061071131081091101111171181121131141151 unmodified line11711811912012112212312412512612712812913012813113213313413518 unmodified lines1541551561571551581591601611591621631641651661 unmodified line1681691701711691701711721731721731741751751761771781791801 unmodified line1821831841851851861871881891901911921921931941951961971981992001971982012022032042052062078 unmodified lines19 unmodified lines "tobi/internal/target")
func Find(ctx context.Context, query string) []session.Session { raw := strings.TrimSpace(query) q := strings.ToLower(strings.TrimSpace(query)) self, peers := strings.ToLower(session.ShortHostname()), remote.DiscoverPeersFunc(ctx) t := target.Parse(q) q := strings.ToLower(raw) self, peers := strings.ToLower(session.ShortHostname()), remote.DiscoverPeers(ctx) t := target.Parse(raw) id, host := strings.ToLower(t.ID), strings.ToLower(t.Host) matchID := func(list []session.Session) []session.Session { return lo.Filter(list, func(s session.Session, _ int) bool { return strings.HasPrefix(strings.ToLower(s.ID), t.ID) }) return lo.Filter(list, func(s session.Session, _ int) bool { return strings.HasPrefix(strings.ToLower(s.ID), id) }) }
// an id or an addressed user makes the target exact: local, tailscale // peer, and remembered hosts resolve directly; other shapes fall // through to the global fuzzy lookup below if t.ID != "" || strings.Contains(q, "@") { if t.Host == self || t.Host == "localhost" { if t.ID != "" || t.User != "" { if host == self || host == "localhost" { return matchID(session.ListLocal()) } peer := lo.ContainsBy(peers, func(p remote.Peer) bool { return strings.EqualFold(p.Name, t.Host) }) || len(remote.KnownDests(t.Host)) > 1 if peer {29 unmodified lines all = append(all, <-lists...) }
byNewest(all) return rankSessions(all, q) return rankSessions(all, q, true)}
// rankSessions is the one matcher/ranker behind CLI resolution and the// picker filter: empty queries keep order, direct prefix hits win, fuzzy// matching falls back, canonical dedupe keeps the best rankfunc rankSessions(list []session.Session, query string) []session.Session {func rankSessions(list []session.Session, query string, directFirst bool) []session.Session { q := strings.ToLower(strings.TrimSpace(query)) if q == "" { return list } direct := lo.Filter(list, func(s session.Session, _ int) bool { return strings.HasPrefix(strings.ToLower(s.Canonical()), q) || strings.HasPrefix(strings.ToLower(s.ID), q) || strings.HasPrefix(strings.ToLower(s.Command), q) || strings.Contains(strings.ToLower(s.Cwd), q) }) if len(direct) > 0 { return lo.UniqBy(direct, (session.Session).Canonical) if directFirst { direct := lo.Filter(list, func(s session.Session, _ int) bool { return strings.HasPrefix(strings.ToLower(s.Canonical()), q) || strings.HasPrefix(strings.ToLower(s.ID), q) || strings.HasPrefix(strings.ToLower(s.Command), q) || strings.Contains(strings.ToLower(s.Cwd), q) }) if len(direct) > 0 { return lo.UniqBy(direct, (session.Session).Canonical) } } matches := fuzzy.Find(q, lo.Map(list, func(s session.Session, _ int) string { return s.MatchString() })) return lo.UniqBy(lo.Map(matches, func(m fuzzy.Match, _ int) session.Session { return list[m.Index] }), (session.Session).Canonical) matches := fuzzy.Find(q, lo.Map(list, func(s session.Session, _ int) string { return fmt.Sprintf("%s %s %s %s", s.Host, s.Canonical(), s.Command, s.Cwd) })) return lo.UniqBy(lo.Map(matches, func(m fuzzy.Match, _ int) session.Session { return list[m.Index] }), (session.Session).Canonical)}
// byNewest orders newest first with a stable id tiebreakfunc byNewest(s []session.Session) { slices.SortFunc(s, func(a, b session.Session) int { if c := b.StartedAt.Compare(a.StartedAt); c != 0 { return c1 unmodified line return strings.Compare(a.ID, b.ID) })}
func connectOrHop(target session.Session) error {func connectOrHop(dst session.Session) error { if cs := os.Getenv("TOBI_CONTROL"); cs != "" { return session.SendHop(cs, target.Canonical()) return session.SendHop(cs, dst.Canonical()) } return RunHopLoop(target) return RunHopLoop(dst)}
func Interactive(query string) error { // outside a tty, or dotted targets: attach directly; a plain query // seeds the picker filter (readme behavior) if q := strings.TrimSpace(query); !isTTY() || strings.Contains(q, ".") { if sessions := Find(context.Background(), q); len(sessions) > 0 { return connectOrHop(sessions[0]) }1 unmodified line } return runPicker(session.ListLocal(), query)}
func suggest(err error, q string) error { ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() alt := lo.Filter(Find(ctx, q), func(s session.Session, _ int) bool { return !strings.EqualFold(s.Canonical(), q) }) if len(alt) > 0 { return fmt.Errorf("%w (did you mean %s?)", err, alt[0].Canonical()) } return err}
func RunHopLoop(target session.Session) error {func RunHopLoop(dst session.Session) error { if rawState, err := term.MakeRaw(int(os.Stdin.Fd())); err == nil { defer term.Restore(int(os.Stdin.Fd()), rawState) }
18 unmodified lines } return remote.AttachRemote(s.Host, s.ID) }
current := target current := dst for { stream, err := dial(current) if err != nil { return fmt.Errorf("connect %s: %w", current.Canonical(), err) return suggest(fmt.Errorf("connect %s: %w", current.Canonical(), err), current.Canonical()) }
if err = session.Attach(stream, inputCh); err == nil || err == session.ErrDetached { return nil1 unmodified line hop, ok := err.(session.HopRequest) if !ok { return err } ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) next := Find(ctx, hop.Target) cancel() if len(next) == 0 { return fmt.Errorf("hop target not found: %s", hop.Target) t := target.Parse(hop.Target) if t.Host == "" || !session.ValidateID(t.ID) { return suggest(fmt.Errorf("invalid hop target: %s", hop.Target), hop.Target) } current = next[0] current = session.Session{Host: t.Dest(), ID: t.ID} }}
func Spawn(addr string, cmdArgs []string) error {1 unmodified line return errors.New("usage: tobi [host.[id]] -- <command> [args...]") }
self := strings.ToLower(session.ShortHostname()) t := target.Parse(strings.ToLower(strings.TrimSpace(addr))) t := target.Parse(strings.TrimSpace(addr)) host := lo.Ternary(t.Host != "", t.Host, self) id := lo.Ternary(t.ID != "", t.ID, session.GenerateID()) if !session.ValidateID(id) { return fmt.Errorf("invalid session id: %q (must be 1-64 alphanumeric, - or _)", id) }
if host == self || host == "localhost" { if t.User == "" && (strings.EqualFold(host, self) || strings.EqualFold(host, "localhost")) { if err := session.EnsureOwner(id, cmdArgs); err != nil { return err } host = session.ShortHostname() } else { t.Host = host host = t.Dest() } else if err := remote.SpawnRemote(host, id, cmdArgs); err != nil { return err if err := remote.SpawnRemote(host, id, cmdArgs); err != nil { return err } } return connectOrHop(session.Session{ID: id, Host: host})}
8 unmodified lines123456745678914151011121314151617241819262728293031323334353637383940414220212223242526272829303147484950515253545556575859606162636465666768697071727374757677787980package app
import ( "context" "os" "path/filepath" "reflect" "regexp" "strings" "testing" "time" "unicode/utf8"
"tobi/internal/remote"
"github.com/charmbracelet/bubbles/spinner" "github.com/charmbracelet/bubbles/textinput" "tobi/internal/session")
var ansi = regexp.MustCompile("\\x1b\\[[0-9;]*m")
func TestViewNeverExceedsWidth(t *testing.T) { for _, width := range []int{0, 30, 40, 60, 76, 80, 100, 160, 300} { for _, width := range []int{0, 30, 40, 60, 76, 100, 300} { f := textinput.New() f.Focus() m := pickerModel{ width: width, filter: f, spinner: spinner.New(spinner.WithSpinner(spinner.Dot)), rows: []hostRow{ {name: "selfish", local: true, sessions: []session.Session{{ID: "a", Command: "hx", Cwd: "/tmp", StartedAt: time.Now()}}}, {name: "caladon", sessions: []session.Session{{ID: "b", Command: "nu", Cwd: "/x", Attached: true, StartedAt: time.Now()}}}, {name: "pupos", unreachable: true}, {name: "homura", peerOffline: true}, }, } w := min(width, maxUIWidth) for ln, line := range strings.Split(m.View(), "\n") { plain := ansi.ReplaceAllString(line, "") if got := utf8.RuneCountInString(plain); got > w { t.Fatalf("width %d: line %d is %d > %d: %q", width, ln, got, w, plain) m := pickerModel{width: width, filter: f, spinner: spinner.New(spinner.WithSpinner(spinner.Dot)), rows: []hostRow{ {name: "selfish", local: true, sessions: []session.Session{{ID: "a", Command: "hx", Cwd: "/tmp", StartedAt: time.Now()}}}, {name: "caladon", sessions: []session.Session{{ID: "b", Command: "nu", Cwd: "/x", StartedAt: time.Now()}}}, {name: "pupos", unreachable: true}, {name: "homura", peerOffline: true}, }} for n, line := range strings.Split(m.View(), "\n") { if got, max := utf8.RuneCountInString(ansi.ReplaceAllString(line, "")), min(width, maxUIWidth); got > max { t.Fatalf("width %d line %d: %d > %d", width, n, got, max) } } }}
func TestFindDirectTargets(t *testing.T) { dir := t.TempDir() t.Setenv("XDG_CONFIG_HOME", dir) t.Setenv("XDG_RUNTIME_DIR", t.TempDir()) if err := os.MkdirAll(filepath.Join(dir, "tobi"), 0o755); err != nil { t.Fatal(err) } hosts := filepath.Join(dir, "tobi", "hosts.json") if err := os.WriteFile(hosts, []byte(`{"caladon":["dawn@caladon"]}`), 0o644); err != nil { t.Fatal(err) }
prev := remote.DiscoverPeersFunc t.Cleanup(func() { remote.DiscoverPeersFunc = prev }) remote.DiscoverPeersFunc = func(context.Context) []remote.Peer { return nil }
for q, want := range map[string][]session.Session{ "caladon.work": {{Host: "caladon", ID: "work"}}, // remembered host, valid id: direct "dawn@caladon.wo": {{Host: "dawn@caladon", ID: "wo"}}, // addressed user survives "unknownhost.work": nil, // neither peer nor remembered: no direct hit } { got := Find(context.Background(), q) if len(got) != len(want) { t.Errorf("Find(%q) = %v, want %v", q, got, want) continue } for i := range got { if !reflect.DeepEqual(got[i], want[i]) { t.Errorf("Find(%q)[%d] = %v, want %v", q, i, got[i], want[i]) } } }}19 unmodified lines20212223242425262716 unmodified lines44454647495048495354555657505152533 unmodified lines5758596069706162636493 unmodified lines1581591601611721621631641751761771781791801651661831841851861871671681691701714 unmodified lines1761771781792002012021801811822051831842082091851861871887 unmodified lines19619719819922522622720020123020220320420520620720823823920921021121219 unmodified lines2322332342352672362372382392 unmodified lines24224324424527827928024624724824928528625025125225325425525625729429525825926026126226326430330426526626726810 unmodified lines27928028128232328328428528616 unmodified lines30330430530634830730830931031115 unmodified lines32732832933037237333133233333437833533633733833934034134238738834334434534629 unmodified lines37637737837942642742838042943038143243343443543643738238338419 unmodified lines "tobi/internal/remote" "tobi/internal/session")
// catppuccin, latte for light terminals and mocha for dark onesvar ( textC = lipgloss.AdaptiveColor{Light: "#4c4f69", Dark: "#cdd6f4"} overlay = lipgloss.AdaptiveColor{Light: "#9ca0b0", Dark: "#6c7086"} lavender = lipgloss.AdaptiveColor{Light: "#7287fd", Dark: "#b4befe"}16 unmodified lines pickStyle = lipgloss.NewStyle().Background(surface) borderStyle = lipgloss.NewStyle().Foreground(overlay))
// maxUIWidth keeps right-aligned columns from drifting past ~76 columns on// very wide terminalsconst maxUIWidth = 76
// padTo appends spaces so the plain-text width of s reaches wfunc padTo(s string, w int) string { return s + strings.Repeat(" ", max(0, w-utf8.RuneCountInString(s)))}
type hostRow struct { name string local bool probing bool3 unmodified lines duration time.Duration sessions []session.Session}
// probeResult carries the row index it was scheduled for, so Update can// patch m.rows directly instead of re-finding the host by nametype probeResult struct { row int sessions []session.Session err error93 unmodified lines return m, nil}
func (m pickerModel) visible() []session.Session { return rankSessions(lo.FlatMap(m.rows, func(r hostRow, _ int) []session.Session { return r.sessions }), m.filter.Value()) return rankSessions(lo.FlatMap(m.rows, func(r hostRow, _ int) []session.Session { return r.sessions }), m.filter.Value(), false)}
func (m pickerModel) total() int { return lo.SumBy(m.rows, func(r hostRow) int { return len(r.sessions) })}
// kittyFace is indexed by gaze direction+1: left, center, right; % marks// the eye charactervar kittyFace = [...]string{"(^% % ^)", "(^ % % ^)", "(^ % %^)"}
const ( eyeDozing = "-" eyeOpen = "•" eyeLooking = ".")const eyeDozing, eyeOpen, eyeLooking = "-", "•", "."
func makeKitty(dir int, char string) string { return strings.ReplaceAll(kittyFace[min(2, max(0, dir+1))], "%", char)}4 unmodified lines return "" } var b strings.Builder
// header box: the kitty and name sit on the top border, counters dock // right; the filter field lives inside the box counts := fmt.Sprintf("%d host%s · %d active ", len(m.rows), plural(len(m.rows)), m.total()) counts := fmt.Sprintf("%d host%s · %d active ", len(m.rows), plural(len(m.rows)), lo.SumBy(m.rows, func(r hostRow) int { return len(r.sessions) })) kitty := makeKitty(m.lookDir, eyeDozing) if m.filter.Value() != "" { // pupils follow the cursor's column within the input field kitty = makeKitty(m.filter.Position()/10-1, eyeLooking) } else if m.blinking { // dozing; after a random 3-6s the eyes open toward the chosen // direction, pause, blink close-open-close, then hold that gaze if t := m.twitch - m.blinkAt; t < 10 || t >= 12 && t < 14 { // eyes open, straight kitty = makeKitty(m.lookDir, eyeOpen) } }7 unmodified lines filter := m.filter.View() b.WriteString(borderStyle.Render("│") + " " + filter + strings.Repeat(" ", max(1, w-4-lipgloss.Width(filter))) + borderStyle.Render(" │") + "\n") b.WriteString(borderStyle.Render("╰"+strings.Repeat("─", w-2)+"╯") + "\n")
// host table: [dot] [name] [badge] [sessions] [ping] // lipgloss measures every block; the glyph cell is pinned to a fixed // width so the one ambiguous glyph per row cannot skew alignment cursor := 0 if q := strings.TrimSpace(m.filter.Value()); q != "" { // search mode: one flat list across every host, best match first for _, s := range m.visible() { sessionRow(&b, w, cursor, m.cursor, s, s.Canonical(), " ", "› ") cursor++ } } else { cursor = m.drawHostTable(&b, w, cursor) } // footer: a thin rule, then the hints distributed with the last one // pinned flush right; hints drop off the end on narrow terminals b.WriteString(borderStyle.Render(strings.Repeat("─", w)) + "\n") hint := func(k, act string) string { return dimStyle.Render("[") + textStyle.Render(k) + dimStyle.Render("] "+act) }19 unmodified lines } return b.String()}
// footer hints are immutable; w is the plain rune width of "[k] act"var pickerHints = []struct { k, act string w int}{2 unmodified lines {"esc", "clear", 11}, {"ctrl+c", "quit", 13},}
// sessionRow renders one selectable session line, shared by search mode// (canonical label) and grouped mode (id label); pad and selPad are the// unselected and selected leading indentsfunc sessionRow(b *strings.Builder, w, cursor, sel int, s session.Session, label, pad, selPad string) { age := relTime(s.StartedAt) rest := label + " " + s.Command + " " + s.Cwd if cursor == sel { // one style for the whole highlighted row, no nested resets b.WriteString(pickStyle.Render(padTo(selPad+rest, w-1-utf8.RuneCountInString(age))+" "+age) + "\n") b.WriteString(pickStyle.Render(selPad+rest+strings.Repeat(" ", max(0, w-1-utf8.RuneCountInString(age)-utf8.RuneCountInString(selPad+rest)))+" "+age) + "\n") return } left := pad + mauveStyle.Render(label) + " " + lavenderStyle.Render(s.Command) + " " + yellowStyle.Render(s.Cwd) fill := max(0, w-utf8.RuneCountInString(age)-lipgloss.Width(left)) b.WriteString(left + strings.Repeat(" ", fill) + age + "\n")}
// drawHostTable renders the host table and its session sub-rows; cursor// counts selectable sessions and is returned so View can resume countingfunc (m pickerModel) drawHostTable(b *strings.Builder, w, cursor int) int { nameW := 0 for _, r := range m.rows { nameW = max(nameW, utf8.RuneCountInString(r.name)) } nameW = min(nameW, 22) for _, r := range m.rows { // one state derivation per row: glyph, status text, ping cell and // the three cell styles all switch on the same state glyph, sess := "○", "no sessions" glyphSt, sessSt, nameSt := dimStyle, dimStyle, textStyle ping := dimStyle.Render(fmt.Sprintf("%.1fs", r.duration.Seconds())) switch {10 unmodified lines case len(r.sessions) > 0: glyph, sess = "●", fmt.Sprintf("%d session%s", len(r.sessions), plural(len(r.sessions))) glyphSt, sessSt = mintStyle, mintStyle.Faint(true) } // the status cell yields space on very narrow terminals reserve := 5 if r.local { reserve += 8 // the local pill and its gap }16 unmodified lines}
func runPicker(local []session.Session, initial string) error { peersCtx, cancel := context.WithTimeout(context.Background(), 2*time.Second) peers := remote.DiscoverPeersFunc(peersCtx) peers := remote.DiscoverPeers(peersCtx) cancel()
self := strings.ToLower(session.ShortHostname()) probes := make(chan probeResult, len(peers))15 unmodified lines } m.filter.Width = m.width - 8 m.lookDir, m.nextBlink = []int{-1, 1}[rand.IntN(2)], 30+rand.IntN(31) online := lo.Filter(peers, func(p remote.Peer, _ int) bool { return p.Online && !strings.EqualFold(p.Name, self) }) // tailscale already knows these hosts are off the mesh: park them at the // bottom, never probed. hidden unless TOBI_OFFLINE=1 var offline []remote.Peer if os.Getenv("TOBI_OFFLINE") == "1" { offline = lo.Reject(peers, func(p remote.Peer, _ int) bool { return p.Online || strings.EqualFold(p.Name, self) }) } // one probing row per user; bare hosts probe only when no user is known for _, p := range online { for _, dest := range remote.ProbeDests(p.Name) { m.rows = append(m.rows, hostRow{name: dest, probing: true}) } } for _, p := range offline { m.rows = append(m.rows, hostRow{name: p.Name, peerOffline: true}) } // rows are final now: probes identify themselves by row index, never by // a pointer that a later append could invalidate for i := range m.rows { if !m.rows[i].probing { continue }29 unmodified lines } return fmt.Sprintf("%dd ago", int(d.Hours()/24))}
// probeClock caps the visible probe timer just under the 10s budget so a// maxed-out probe reads 9.9s instead of tripping into "10.0s"func probeClock(d time.Duration) time.Duration {func probeClock(d time.Duration) time.Duration { return min(d, 9900*time.Millisecond) } return min(d, 9900*time.Millisecond)}
func plural(n int) string { if n == 1 { return "" } return "s"}func plural(n int) string { return lo.Ternary(n == 1, "", "s") }
func isTTY() bool { return term.IsTerminal(int(os.Stdout.Fd())) }6 unmodified lines789101111121314161718192021151617181925202127282930313233222324352537262739402829304243444546473132334950515234355556575836373839404142434445464748606162636449666768697071726 unmodified lines "testing")
func withHostMap(t *testing.T, content string) { t.Helper() dir := t.TempDir() t.Setenv("XDG_CONFIG_HOME", dir) t.Setenv("HOME", dir) if content != "" { if err := os.MkdirAll(filepath.Join(dir, "tobi"), 0700); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(dir, "tobi", "hosts.json"), []byte(content), 0600); err != nil { t.Fatal(err) } _ = os.MkdirAll(filepath.Join(dir, "tobi"), 0700) _ = os.WriteFile(filepath.Join(dir, "tobi", "hosts.json"), []byte(content), 0600) }}
func TestRememberHost(t *testing.T) {func TestHostMemory(t *testing.T) { withHostMap(t, "") RememberHost("root@caladon") RememberHost("dawn@caladon") RememberHost("dawn@caladon") // idempotent RememberHost("caladon") // bare host: not remembered
if got, want := KnownDests("caladon"), []string{"caladon", "dawn@caladon", "root@caladon"}; !slices.Equal(got, want) { t.Fatalf("KnownDests = %v, want %v", got, want) for _, dest := range []string{"root@caladon", "dawn@caladon", "dawn@caladon", "caladon", "root@caladon"} { RememberHost(dest) } RememberHost("root@caladon") // moves to front among users if got, want := KnownDests("caladon"), []string{"caladon", "root@caladon", "dawn@caladon"}; !slices.Equal(got, want) { t.Fatalf("KnownDests after re-remember = %v, want %v", got, want) t.Fatalf("KnownDests = %v, want %v", got, want) } if d := hostDest("caladon"); d != "root@caladon" { t.Fatalf("hostDest = %q, want the most recent user", d) if hostDest("caladon") != "root@caladon" || !slices.Equal(ProbeDests("caladon"), []string{"root@caladon", "dawn@caladon"}) { t.Fatal("remembered destination ordering broken") }}
func TestLoadHostsBackCompat(t *testing.T) { withHostMap(t, `{"caladon":"old@caladon"}`) if got, want := KnownDests("caladon"), []string{"caladon", "old@caladon"}; !slices.Equal(got, want) { t.Fatalf("KnownDests old form = %v, want %v", got, want) if hostDest("unknown") != "unknown" || !slices.Equal(ProbeDests("unknown"), []string{"unknown"}) { t.Fatal("unknown host should stay bare") } withHostMap(t, `{"volta":["a@volta","b@volta"]}`) if got, want := KnownDests("volta"), []string{"volta", "a@volta", "b@volta"}; !slices.Equal(got, want) { t.Fatalf("KnownDests volta = %v, want %v", got, want) }}
func TestProbeDests(t *testing.T) { withHostMap(t, "") if got := ProbeDests("freshhost"); !slices.Equal(got, []string{"freshhost"}) { t.Fatalf("ProbeDests unknown = %v", got)func TestHostMemoryBackCompat(t *testing.T) { for _, tc := range []struct { data, host string want []string }{ {`{"caladon":"old@caladon"}`, "caladon", []string{"caladon", "old@caladon"}}, {`{"volta":["a@volta","b@volta"]}`, "volta", []string{"volta", "a@volta", "b@volta"}}, } { withHostMap(t, tc.data) if got := KnownDests(tc.host); !slices.Equal(got, tc.want) { t.Fatalf("KnownDests = %v, want %v", got, tc.want) } } RememberHost("dawn@caladon") RememberHost("root@caladon") if got, want := ProbeDests("caladon"), []string{"root@caladon", "dawn@caladon"}; !slices.Equal(got, want) { t.Fatalf("ProbeDests = %v, want %v", got, want) }}
func TestHostDestUnknownStaysBare(t *testing.T) { withHostMap(t, "") if d := hostDest("unknownhost"); d != "unknownhost" { t.Fatalf("hostDest = %q, want the host unchanged", d) }}21 unmodified lines2223242526262728293032333132333738343536374038394043444146474842434445464752535448495051525354555657635859606162637071726474656667686970817172737475767787887879808112 unmodified lines9495969710998991001011121131141021151161031041191201051221061071081091261271101111121131141151321331161351361171181191391401201421431441451211221231241481491251261271531281291301316 unmodified lines1381391401411681691421711721431441451461471481751761771781491501511521531541551561871881571581591911601611621631931941641961971981991651661671681691702031711721731741 unmodified line17617717817921321418018118218318422022118518618718822622722818919019119219319419519623723819719819920021 unmodified lines22222322422526826922622722822923027527623127823223323423523622 unmodified lines2592602612623093102632642652668 unmodified lines27527627727832732827928028128214 unmodified lines2972982993003513013023033047 unmodified lines31231331431536736831637037137237331731831932037632132232332432538238338432632732832919 unmodified lines3493503513524124133533543553561 unmodified line35835936036142336236336436536666 unmodified lines21 unmodified lines "github.com/klauspost/compress/zstd" "github.com/samber/lo" "tobi/internal/session" "tobi/internal/target" "tobi/internal/version")
//go:embed payloads/*var embeddedPayloads embed.FS
// hosts the user has addressed with an explicit user@ prefix are remembered,// so bare peer-name probes can authenticate without a User line in ssh_configvar hostMu sync.Mutex
func hostMapPath() string { if base, err := os.UserConfigDir(); err == nil && base != "" { return filepath.Join(base, "tobi", "hosts.json") base, _ := os.UserConfigDir() if base == "" { return "" } return "" return filepath.Join(base, "tobi", "hosts.json")}
// loadHosts reads the remembered destinations per bare host, accepting the// older all-strings single formfunc loadHosts() map[string][]string { m := map[string][]string{} data, err := os.ReadFile(hostMapPath()) if err != nil || json.Unmarshal(data, &m) == nil { data, _ := os.ReadFile(hostMapPath()) var m map[string][]string if json.Unmarshal(data, &m) == nil && m != nil { return m } var old map[string]string if json.Unmarshal(data, &old) != nil { return map[string][]string{} } _ = json.Unmarshal(data, &old) m = make(map[string][]string, len(old)) for host, dest := range old { if dest != "" { m[host] = []string{dest} } } return m}
// remembered is the user@host list stored for a bare hostfunc remembered(host string) []string { hostMu.Lock() defer hostMu.Unlock() return loadHosts()[host]}
// peerEnv is the TOBI_PEER=... exec prefix that introduces this machine to// remote RPCs as a ssh destination, so they can remember how to reach us// back; "" when this machine is unidentifiablefunc peerEnv() string { host := session.ShortHostname() u := os.Getenv("USER") if u == "" { if cur, err := user.Current(); err == nil { u = cur.Username } } if host == "" || host == "localhost" || u == "" { host := session.ShortHostname() if u == "" || host == "" || host == "localhost" { return "" } return "TOBI_PEER=" + u + "@" + host + " "}
// RememberHost records a user@host destination for its bare host, most// recent first. a host may serve several users, each with its own sessionsfunc RememberHost(dest string) { t := target.Parse(dest) if t.User == "" || t.Host == "" { return12 unmodified lines}
func saveHosts(m map[string][]string) { p := hostMapPath() if p == "" { data, err := json.Marshal(m) if p == "" || err != nil || os.MkdirAll(filepath.Dir(p), 0700) != nil { return } if data, err := json.Marshal(m); err == nil { if os.MkdirAll(filepath.Dir(p), 0700) == nil { _ = os.WriteFile(p, data, 0600) _ = os.WriteFile(p, data, 0600) } }}
// hostDest resolves a host to its ssh destination: the most recently used// user@host for that bare hostfunc hostDest(host string) string { if t := target.Parse(host); t.User != "" { if target.Parse(host).User != "" { RememberHost(host) return host } if list := remembered(host); len(list) > 0 && list[0] != "" { return list[0] if d := remembered(host); len(d) > 0 { return d[0] } return host}
// KnownDests lists every destination worth probing for a host: itself plus// each remembered user@host, most recent firstfunc KnownDests(host string) []string { known := slices.DeleteFunc(remembered(host), func(d string) bool { return d == "" || d == host }) return lo.Uniq(append([]string{host}, known...)) return lo.Uniq(append([]string{host}, slices.DeleteFunc(remembered(host), func(d string) bool { return d == "" || d == host })...))}
// ProbeDests is the list of destinations worth probing for a host: every// remembered user when one exists, the bare host otherwisefunc ProbeDests(host string) []string { if dests := KnownDests(host); len(dests) > 1 { return dests[1:] } return []string{host} d := KnownDests(host) return lo.Ternary(len(d) > 1, d[1:], []string{host})}
// QueryHost lists a host's sessions across every probed destination,// deduplicated by bare host and idfunc QueryHost(ctx context.Context, host string) []session.Session { t := target.Parse(host) dests := ProbeDests(t.Host) // an explicitly addressed user is probed even when never remembered if t.User != "" { dests = lo.Uniq(append([]string{host}, dests...)) } var found []session.Session6 unmodified lines return strings.ToLower(target.Parse(s.Host).Host + "." + s.ID) })}
// tssh exits 0 regardless of remote command failure, so remote steps confirm// themselves with stdout markers instead of relying on exit codesconst ( uploadMarker = "__TOBI_UPLOADED__" spawnMarker = "__TOBI_SPAWNED__" uploadMarker = "__TOBI_UPLOADED__" spawnMarker = "__TOBI_SPAWNED__" remotePathEnv = `PATH="$HOME/.local/bin:$PATH:$HOME/.nix-profile/bin:/opt/homebrew/bin:/usr/local/bin:$HOME/go/bin"` uploadCmd = "mkdir -p ~/.local/bin && cat > ~/.local/bin/tobi.tmp && chmod 755 ~/.local/bin/tobi.tmp && mv -f ~/.local/bin/tobi.tmp ~/.local/bin/tobi")
const remotePathEnv = `PATH="$HOME/.local/bin:$PATH:$HOME/.nix-profile/bin:/opt/homebrew/bin:/usr/local/bin:$HOME/go/bin"`
// rpcCommand is the remote exec prefix for a tobi RPC: PATH fixup, peer// identity, the RPC verb and optional session idfunc rpcCommand(op, id string) string { cmd := remotePathEnv + " " + peerEnv() + "TOBI_RPC=" + op if id != "" { cmd += " TOBI_ID=" + id } return cmd + " tobi"}
// archAlias maps uname -m names onto GOARCH names; uname -s is already a// GOOS name modulo casevar archAlias = map[string]string{"x86_64": "amd64", "aarch64": "arm64"}
var (var remoteInstalled sync.Map remoteInstalled sync.Map Version = "dev")
// sshOpts is the shape of a remote ssh exec: batch mode, master creation,// connect timeout, and the remote commandtype sshOpts struct { cmd string batch bool master bool timeout string cmd string batch, master bool timeout string}
func sshCommand(ctx context.Context, dest string, opts sshOpts) *exec.Cmd { // prefer tssh when installed so ssh_config options like UdpMode work bin, err := exec.LookPath("tssh") if err != nil { bin = "ssh" }1 unmodified line if opts.batch { args = append(args, "-o", "BatchMode=yes") } if home, err := os.UserHomeDir(); err == nil && home != "" { // every call reuses an existing master; only user-initiated flows // (attach, spawn) create one, so peer probes never litter masters ctl := filepath.Join(home, ".ssh", "tobi-control-%r@%h:%p") args = append(args, "-o", "ControlMaster="+lo.Ternary(opts.master, "auto", "no"), "-o", "ControlPath="+ctl, "-o", "ControlPersist=60") } c := exec.CommandContext(ctx, bin, append(args, dest, opts.cmd)...) // a killed ssh can leave children holding the stdout pipe; bound the // wait so probes settle at timeout + 500ms c.WaitDelay = 500 * time.Millisecond return c}
// ErrUnreachable means the host could not be reached over ssh at all// (connect timeout, refused, auth): the difference between "machine is away"// and "machine answered but has nothing"var ErrUnreachable = errors.New("unreachable")
func sshRun(ctx context.Context, dest string, in io.Reader, opts sshOpts) (string, error) { c := sshCommand(ctx, dest, opts) c.Stdin = in var out, errBuf bytes.Buffer c.Stdout, c.Stderr = &out, &errBuf if err := c.Run(); err != nil { // exit 255 is a transport failure (timeout, refused, auth); remote // command failures pass their own code through var ee *exec.ExitError if (errors.As(err, &ee) && ee.ExitCode() == 255) || errors.Is(err, context.DeadlineExceeded) || ctx.Err() != nil { err = fmt.Errorf("%w: %w", ErrUnreachable, err) }21 unmodified lines } return nil, fmt.Errorf("no binary available for %s/%s (build the carrier slices: nix build or scripts/build-universal.sh)", goos, arch)}
const uploadCmd = "mkdir -p ~/.local/bin && cat > ~/.local/bin/tobi.tmp && chmod 755 ~/.local/bin/tobi.tmp && mv -f ~/.local/bin/tobi.tmp ~/.local/bin/tobi"
func EnsureRemote(ctx context.Context, host string) error { dest := hostDest(host) if _, ok := remoteInstalled.Load(dest); ok { return nil } // skip the upload only when the host already runs THIS build; an older // (or newer, or foreign) install gets upgraded in place check := remotePathEnv + " " + peerEnv() + `command -v tobi >/dev/null 2>&1 && TOBI_RPC=version tobi` if out, err := sshRun(ctx, dest, nil, sshOpts{cmd: check, batch: true, master: true, timeout: "3"}); err == nil && strings.TrimSpace(out) == version.Version { if out, err := sshRun(ctx, dest, nil, sshOpts{cmd: check, batch: true, master: true, timeout: "3"}); err == nil && strings.TrimSpace(out) == Version { remoteInstalled.Store(dest, true) return nil } out, err := sshRun(ctx, dest, nil, sshOpts{cmd: "uname -s && uname -m", batch: true, timeout: "3"})22 unmodified lines remoteInstalled.Store(dest, true) return nil}
// QueryRemote lists a host's sessions without touching its tobi install;// a missing or failing tobi is just "no sessions", not an errorfunc QueryRemote(ctx context.Context, host string) ([]session.Session, error) { dest := hostDest(host) out, err := sshRun(ctx, dest, nil, sshOpts{cmd: rpcCommand("list", ""), batch: true, timeout: "10"}) if err != nil {8 unmodified lines } return list, nil}
// AttachRemote installs tobi on the host if needed, then opens an attach// session as a raw byte streamfunc AttachRemote(host, id string) (io.ReadWriteCloser, error) { ctx := context.Background() dest := hostDest(host) if err := EnsureRemote(ctx, dest); err != nil {14 unmodified lines } return &Stream{Reader: out, WriteCloser: in, cmd: c}, nil}
// Stream is a live remote attach channeltype Stream struct { io.Reader io.WriteCloser cmd *exec.Cmd7 unmodified lines }) return nil}
var DiscoverPeersFunc = DiscoverPeers
type tsPeer struct { Online bool OS string DNSName string HostName string Online bool OS, DNSName, HostName string}
// Peer is a tailnet machine that could run tobi, with its mesh presence.type Peer struct { Name string Online bool}
// DiscoverPeers asks the tailscale CLI for linux/mac peers with their mesh// presence, sorted by name; the CLI is exec'd so ssh_config options like// UdpMode keep workingfunc DiscoverPeers(ctx context.Context) []Peer { c := exec.CommandContext(ctx, "tailscale", "status", "--json") c.WaitDelay = 500 * time.Millisecond out, err := c.Output()19 unmodified lines return peers}
func RunRPC(op, id string) error { // the calling tobi introduced itself: remember how to reach it back, // so its sessions show up in our picker without manual user@ targets if t := target.Parse(os.Getenv("TOBI_PEER")); t.User != "" && t.Host != "" && !strings.EqualFold(t.Host, session.ShortHostname()) { RememberHost(t.Dest()) } switch op {1 unmodified line return json.NewEncoder(os.Stdout).Encode(session.ListLocal()) case "spawn": return runSpawnRPC(os.Stdin) case "version": fmt.Println(version.Version) fmt.Println(Version) return nil } conn, err := net.Dial("unix", session.SocketPath(id)) if err != nil {66 unmodified lines