session: kill old session when hopping to a new session if nested
Signed-off-by: dawn <90008@klbr.net>
34 unmodified lines35363738393940414243122 unmodified linesExpand all16616716816917017017117217317465 unmodified lines2402412422432442452462472442492452462472482497 unmodified lines25725825926026526126226326426532 unmodified lines34 unmodified lines return lo.Filter(list, func(s session.Session, _ int) bool { return strings.HasPrefix(strings.ToLower(s.ID), id) }) }
if t.ID != "" || t.User != "" { if isLocal(t.Host) { if session.IsLocal(t.Host) { 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 {122 unmodified linesExpand all }() colors := <-probed
dial := func(s session.Session) (io.ReadWriteCloser, error) { if isLocal(s.Host) { if session.IsLocal(s.Host) { return net.Dial("unix", session.SocketPath(s.ID)) } return remote.AttachRemote(s.Host, s.ID) }65 unmodified lines } }}
func isLocal(host string) bool { return strings.EqualFold(host, session.ShortHostname()) || strings.EqualFold(host, "localhost")}
func Kill(ctx context.Context, s session.Session) error { if isLocal(s.Host) { if session.IsLocal(s.Host) { return session.Kill(s.ID) } return remote.KillRemote(ctx, s.Host, s.ID)}7 unmodified lines id := cmp.Or(t.ID, session.GenerateID()) if !session.ValidateID(id) { return fmt.Errorf("invalid session id: %q (must be 1-64 alphanumeric, - or _)", id) } if t.User == "" && isLocal(t.Host) { if t.User == "" && session.IsLocal(t.Host) { if err := session.EnsureOwner(id, cmdArgs); err != nil { return err } return connectOrHop(session.Session{ID: id, Host: session.ShortHostname()})32 unmodified lines24 unmodified lines25262728293031323361 unmodified lines9596979899100101102103104105106273 unmodified linesExpand all38038138238338438538637938738838939039132 unmodified lines42442542642742842943043143243343427 unmodified lines462463464465455466467468469470471472473474329 unmodified linesExpand all24 unmodified lines xterm "github.com/gitpod-io/xterm-go" "github.com/samber/lo" "golang.org/x/term" "tobi/internal/protocol" "tobi/internal/target" "tobi/internal/termcolor")
var (61 unmodified lines } return filepath.Join(RuntimeDir(), id+".sock")}
func IsLocal(host string) bool { return strings.EqualFold(host, ShortHostname()) || strings.EqualFold(host, "localhost")}
func ShortHostname() string { h, _ := os.Hostname() h, _, _ = strings.Cut(h, ".") return lo.Ternary(h != "", h, "localhost")273 unmodified linesExpand all // so it gets its own lock colorMu sync.Mutex colorSets map[*viewerClient]map[int]termcolor.Color idleTimer *time.Timer // set once a hop has moved viewers elsewhere: the shell that asked for it // only ran tobi, so the owner goes with its last viewer retiring bool closed bool closed bool}
func (o *Owner) applySizeLocked() { cols, rows := 0, 032 unmodified lines
func (o *Owner) hop(p []byte) { o.mu.Lock() viewers := lo.Keys(o.viewers) if t := target.Parse(string(p)); len(viewers) > 0 && !(t.ID == o.sess.ID && IsLocal(t.Host)) { o.retiring = true } o.mu.Unlock() for _, vc := range viewers { vc.send(outMsg{t: protocol.TypeHop, p: p}) }27 unmodified lines delete(o.viewers, vc) o.colorMu.Lock() delete(o.colorSets, vc) o.colorMu.Unlock() if len(o.viewers) == 0 && !o.closed { switch { case len(o.viewers) > 0 || o.closed: case o.retiring: go o.Close() default: o.idleTimer = time.AfterFunc(time.Duration(detachedTTL.Load()), o.Close) } o.applySizeLocked()}329 unmodified linesExpand all193 unmodified linesExpand all194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256152 unmodified linesExpand all193 unmodified linesExpand all t.Fatalf("metadata mismatch: %v", list) }}
func TestHopRetiresOwnerOnceViewersLeave(t *testing.T) { t.Setenv("XDG_RUNTIME_DIR", shortTempDir(t)) sp := startOwner(t, "sh") v := attach(t, sp) if err := SendHop(sp, "remote.123456"); err != nil { t.Fatal(err) } expectFrame(t, v, protocol.TypeHop, "remote.123456") if !answers(sp, 100*time.Millisecond) { t.Fatal("owner went away while a viewer was still attached") } _ = v.Close() waitGone(t, sp)}
func TestHopToSelfKeepsOwner(t *testing.T) { t.Setenv("XDG_RUNTIME_DIR", shortTempDir(t)) sp := startOwner(t, "sh") self := ShortHostname() + "." + strings.TrimSuffix(filepath.Base(sp), ".sock") v := attach(t, sp) if err := SendHop(sp, self); err != nil { t.Fatal(err) } expectFrame(t, v, protocol.TypeHop, self) _ = v.Close() time.Sleep(200 * time.Millisecond) if !answers(sp, 100*time.Millisecond) { t.Fatal("hop to the same session retired it") }}
func TestHopWithoutViewersKeepsOwner(t *testing.T) { t.Setenv("XDG_RUNTIME_DIR", shortTempDir(t)) sp := startOwner(t, "sh") if err := SendHop(sp, "remote.123456"); err != nil { t.Fatal(err) } v := attach(t, sp) _ = v.Close() time.Sleep(200 * time.Millisecond) if !answers(sp, 100*time.Millisecond) { t.Fatal("a hop nobody saw retired the owner") }}
func waitGone(t *testing.T, sp string) { t.Helper() for deadline := time.Now().Add(2 * time.Second); time.Now().Before(deadline); time.Sleep(20 * time.Millisecond) { if !answers(sp, 50*time.Millisecond) { return } } t.Fatalf("owner still answering: %s", sp)}
func TestSlowViewerDoesNotBlockOwner(t *testing.T) { 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)152 unmodified linesExpand all