Simplify session lifecycle and MCP cleanup
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strconv"
|
||||
@@ -88,6 +89,7 @@ type Child struct {
|
||||
ptyMu sync.RWMutex
|
||||
pty *pkgpty.PTY
|
||||
em *vt.GhosttyEmulator
|
||||
runID uint64
|
||||
|
||||
status atomic.Pointer[ChildStatus]
|
||||
exitCode atomic.Int32
|
||||
@@ -115,6 +117,10 @@ type Child struct {
|
||||
// portsMu guards ports. Best-effort port detection: regex on stream.
|
||||
portsMu sync.Mutex
|
||||
ports []PortSighting
|
||||
|
||||
cleanupMu sync.Mutex
|
||||
cleanupPaths []string
|
||||
restarting atomic.Bool
|
||||
}
|
||||
|
||||
// PortSighting is one entry returned by get_process_ports.
|
||||
@@ -126,10 +132,7 @@ type PortSighting struct {
|
||||
|
||||
const ringCap = 1 << 20 // 1 MiB per SPEC §5
|
||||
|
||||
// newChildEntry builds the in-memory Child record but does NOT start a
|
||||
// PTY. Used so command entries can exist in the `stopped` state from the
|
||||
// moment they're created. Agents and terminals call newChild() which
|
||||
// chains newChildEntry + startPTY for the initial run.
|
||||
// newChildEntry builds the in-memory Child record but does NOT start a PTY.
|
||||
func newChildEntry(id, name string, kind ChildKind, argv, env []string, parentID, workDir, presetRef string) *Child {
|
||||
c := &Child{
|
||||
ID: id,
|
||||
@@ -156,25 +159,14 @@ func newChildEntry(id, name string, kind ChildKind, argv, env []string, parentID
|
||||
return c
|
||||
}
|
||||
|
||||
func newChild(id, name string, kind ChildKind, argv, env []string, cols, rows uint16, parentID, workDir, presetRef string) (*Child, error) {
|
||||
if len(argv) == 0 {
|
||||
return nil, errors.New("child: empty argv")
|
||||
}
|
||||
c := newChildEntry(id, name, kind, argv, env, parentID, workDir, presetRef)
|
||||
if err := c.startPTY(cols, rows); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// startPTY (re)builds the emulator + PTY for this entry. Called by
|
||||
// newChild on initial spawn and by Restart on subsequent runs. The
|
||||
// status transitions stopped/exited → starting → running. On error the
|
||||
// entry returns to errored.
|
||||
func (c *Child) startPTY(cols, rows uint16) error {
|
||||
func (c *Child) startPTY(cols, rows uint16) (uint64, error) {
|
||||
em, err := vt.NewGhosttyEmulator(cols, rows)
|
||||
if err != nil {
|
||||
return fmt.Errorf("child %s emulator: %w", c.ID, err)
|
||||
return 0, fmt.Errorf("child %s emulator: %w", c.ID, err)
|
||||
}
|
||||
starting := StatusStarting
|
||||
c.status.Store(&starting)
|
||||
@@ -183,12 +175,14 @@ func (c *Child) startPTY(cols, rows uint16) error {
|
||||
em.Close()
|
||||
errored := StatusErrored
|
||||
c.status.Store(&errored)
|
||||
return fmt.Errorf("child %s pty: %w", c.ID, err)
|
||||
return 0, fmt.Errorf("child %s pty: %w", c.ID, err)
|
||||
}
|
||||
em.OnWritePTY(func(b []byte) {
|
||||
_, _ = p.Write(b)
|
||||
})
|
||||
c.ptyMu.Lock()
|
||||
c.runID++
|
||||
runID := c.runID
|
||||
c.pty = p
|
||||
c.em = em
|
||||
c.ptyMu.Unlock()
|
||||
@@ -196,7 +190,7 @@ func (c *Child) startPTY(cols, rows uint16) error {
|
||||
c.status.Store(&running)
|
||||
c.exitCode.Store(-1)
|
||||
c.lastWriteNS.Store(0)
|
||||
return nil
|
||||
return runID, nil
|
||||
}
|
||||
|
||||
// IsLive reports whether the PTY is currently attached and running.
|
||||
@@ -222,6 +216,21 @@ func (c *Child) Emulator() *vt.GhosttyEmulator {
|
||||
return c.em
|
||||
}
|
||||
|
||||
func (c *Child) ptyForRun(runID uint64) *pkgpty.PTY {
|
||||
c.ptyMu.RLock()
|
||||
defer c.ptyMu.RUnlock()
|
||||
if c.runID != runID {
|
||||
return nil
|
||||
}
|
||||
return c.pty
|
||||
}
|
||||
|
||||
func (c *Child) isCurrentRun(runID uint64) bool {
|
||||
c.ptyMu.RLock()
|
||||
defer c.ptyMu.RUnlock()
|
||||
return c.runID == runID
|
||||
}
|
||||
|
||||
// DisplayName is the rename_process-aware accessor for Name. Callers
|
||||
// that read Name directly skip the lock; the field is still safe to
|
||||
// read because Go strings are immutable, but DisplayName signals intent.
|
||||
@@ -425,6 +434,25 @@ func (c *Child) teardownPTY() {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Child) AddCleanupPath(path string) {
|
||||
if path == "" {
|
||||
return
|
||||
}
|
||||
c.cleanupMu.Lock()
|
||||
c.cleanupPaths = append(c.cleanupPaths, path)
|
||||
c.cleanupMu.Unlock()
|
||||
}
|
||||
|
||||
func (c *Child) cleanupOwnedPaths() {
|
||||
c.cleanupMu.Lock()
|
||||
paths := c.cleanupPaths
|
||||
c.cleanupPaths = nil
|
||||
c.cleanupMu.Unlock()
|
||||
for _, p := range paths {
|
||||
_ = os.RemoveAll(p)
|
||||
}
|
||||
}
|
||||
|
||||
// InjectAsUser is the path the human takes when typing in the focused
|
||||
// pane. SPEC §6: the user's first keystroke flips ownership.
|
||||
func (c *Child) InjectAsUser(b []byte) error {
|
||||
|
||||
Reference in New Issue
Block a user