"io"
"net"
"os"
"os/exec"
"os/user"
"path/filepath"
"runtime"
"sort"
"strings"
}
}
}
// peerIdent is this machine as a ssh destination: user@hostname
func peerIdent() string {
host := session.ShortHostname()
if host == "" || host == "localhost" {
return ""
}
u := os.Getenv("USER")
if u == "" {
if cur, err := user.Current(); err == nil {
u = cur.Username
}
}
if u == "" {
return ""
}
return u + "@" + host
}
// peerEnv prefixes remote RPC execs with TOBI_PEER so the remote tobi can
// remember how to reach us back (the reverse of RememberHost)
func peerEnv() string {
if p := peerIdent(); p != "" {
return "TOBI_PEER=" + p + " "
}
return ""
}
// RememberHost records a user@host destination for its bare host, most
// recent first. a host may serve several users, each with its own sessions
func RememberHost(dest string) {
_, bare, ok := strings.Cut(dest, "@")
}
hostMu.Lock()
defer hostMu.Unlock()
m := loadHosts()
if len(m[bare]) > 0 && m[bare][0] == dest {
return
}
rest := lo.Filter(m[bare], func(d string, _ int) bool { return d != dest })
m[bare] = append([]string{dest}, rest...)
if len(m[bare]) > 8 {
m[bare] = m[bare][:8]
214 unmodified lines
Expand all
}
// skip the upload only when the host already runs THIS build; an older
// (or newer, or foreign) install gets upgraded in place
checkCmd := fmt.Sprintf(`%s command -v tobi >/dev/null 2>&1 && TOBI_RPC=version tobi 2>/dev/null`, remotePathEnv)
checkCmd := fmt.Sprintf(`%s %scommand -v tobi >/dev/null 2>&1 && TOBI_RPC=version tobi 2>/dev/null`, remotePathEnv, peerEnv())
if out, err := sshRun(ctx, host, checkCmd, "3", nil, true); err == nil && strings.TrimSpace(out) == version.Version {
remoteInstalled.Store(host, 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 error
func QueryRemote(ctx context.Context, host string) ([]session.Session, error) {
host = hostDest(host)
out, err := sshRun(ctx, host, fmt.Sprintf(`%s TOBI_RPC=list tobi`, remotePathEnv), "10", nil, false)
out, err := sshRun(ctx, host, fmt.Sprintf(`%s %sTOBI_RPC=list tobi`, remotePathEnv, peerEnv()), "10", nil, false)
if err != nil {
return nil, err
}
if err := EnsureRemote(ctx, host); err != nil {
return nil, err
}
c := sshCommand(ctx, host, fmt.Sprintf(`%s TOBI_RPC=attach TOBI_ID=%s tobi`, remotePathEnv, id), true, "2", false)
c := sshCommand(ctx, host, fmt.Sprintf(`%s %sTOBI_RPC=attach TOBI_ID=%s tobi`, remotePathEnv, peerEnv(), id), true, "2", false)
in, err := c.StdinPipe()
if err != nil {
return nil, err
}
return &Stream{Reader: out, WriteCloser: in, cmd: c}, nil
}
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 peer := os.Getenv("TOBI_PEER"); strings.Contains(peer, "@") {
if _, bare, _ := strings.Cut(peer, "@"); bare != "" && !strings.EqualFold(bare, session.ShortHostname()) {
RememberHost(peer)
}
}
switch op {
case "list":
return json.NewEncoder(os.Stdout).Encode(session.ListLocal())
case "spawn":
if err != nil {
return err
}
c := sshCommand(context.Background(), host, fmt.Sprintf(`%s TOBI_RPC=spawn tobi && echo %s`, remotePathEnv, spawnMarker), false, "5", true)
c := sshCommand(context.Background(), host, fmt.Sprintf(`%s %sTOBI_RPC=spawn tobi && echo %s`, remotePathEnv, peerEnv(), spawnMarker), false, "5", true)
c.Stdin = bytes.NewReader(append(req, '\n'))
var out, errBuf bytes.Buffer
c.Stdout, c.Stderr = &out, &errBuf
if err := c.Run(); err != nil {