fix process manager
This commit is contained in:
parent
9be10a14de
commit
20c6255b08
|
|
@ -26,39 +26,60 @@ func createSession(name string, rows, cols uint16) error {
|
|||
return err
|
||||
}
|
||||
|
||||
// setEnvironment sets an environment variable in the tmux session
|
||||
func setEnvironment(session, key, value string) error {
|
||||
_, err := tmuxCmd("set-environment", "-t", session, key, value)
|
||||
// createWindow creates a new window in an existing tmux session
|
||||
func createWindow(session, windowName string) error {
|
||||
_, err := tmuxCmd("new-window", "-t", session, "-n", windowName)
|
||||
return err
|
||||
}
|
||||
|
||||
// sendKeys sends keys to a tmux session
|
||||
// killWindow kills a window in a tmux session
|
||||
func killWindow(session, windowName string) error {
|
||||
target := fmt.Sprintf("%s:%s", session, windowName)
|
||||
cmd := exec.Command("tmux", "kill-window", "-t", target)
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("tmux kill-window -t %s: %w", target, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// windowTarget constructs a tmux target for a window in a session
|
||||
func windowTarget(session, windowName string) string {
|
||||
return fmt.Sprintf("%s:%s", session, windowName)
|
||||
}
|
||||
|
||||
// setEnvironment sets an environment variable in the tmux session or window
|
||||
func setEnvironment(target, key, value string) error {
|
||||
_, err := tmuxCmd("set-environment", "-t", target, key, value)
|
||||
return err
|
||||
}
|
||||
|
||||
// sendKeys sends keys to a tmux target (session or session:window)
|
||||
// Special keys: C-m (Enter), C-c (Ctrl+C), etc.
|
||||
func sendKeys(session string, keys ...string) error {
|
||||
args := []string{"send-keys", "-t", session}
|
||||
func sendKeys(target string, keys ...string) error {
|
||||
args := []string{"send-keys", "-t", target}
|
||||
args = append(args, keys...)
|
||||
_, err := tmuxCmd(args...)
|
||||
return err
|
||||
}
|
||||
|
||||
// sendLiteral sends literal text to a tmux session (doesn't interpret special chars)
|
||||
func sendLiteral(session, text string) error {
|
||||
_, err := tmuxCmd("send-keys", "-t", session, "-l", text)
|
||||
// sendLiteral sends literal text to a tmux target (doesn't interpret special chars)
|
||||
func sendLiteral(target, text string) error {
|
||||
_, err := tmuxCmd("send-keys", "-t", target, "-l", text)
|
||||
return err
|
||||
}
|
||||
|
||||
// setupPipePane sets up pipe-pane to redirect output to a file/FIFO
|
||||
func setupPipePane(session, target string) error {
|
||||
func setupPipePane(tmuxTarget, fifoPath string) error {
|
||||
// -o flag: only capture stdout (not stdin)
|
||||
cmd := fmt.Sprintf("cat >> %s", target)
|
||||
_, err := tmuxCmd("pipe-pane", "-o", "-t", session, cmd)
|
||||
cmd := fmt.Sprintf("cat >> %s", fifoPath)
|
||||
_, err := tmuxCmd("pipe-pane", "-o", "-t", tmuxTarget, cmd)
|
||||
return err
|
||||
}
|
||||
|
||||
// closePipePane closes the pipe-pane for a session
|
||||
func closePipePane(session string) error {
|
||||
// closePipePane closes the pipe-pane for a target
|
||||
func closePipePane(target string) error {
|
||||
// Calling pipe-pane with no command closes it
|
||||
_, err := tmuxCmd("pipe-pane", "-t", session)
|
||||
_, err := tmuxCmd("pipe-pane", "-t", target)
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
@ -76,10 +97,10 @@ func sessionExists(name string) bool {
|
|||
return cmd.Run() == nil
|
||||
}
|
||||
|
||||
// getSessionPID returns the PID of the process running in the tmux session
|
||||
func getSessionPID(session string) (int, error) {
|
||||
// getPanePID returns the PID of the process running in a tmux pane
|
||||
func getPanePID(target string) (int, error) {
|
||||
// Get the pane PID
|
||||
out, err := tmuxCmd("display-message", "-t", session, "-p", "#{pane_pid}")
|
||||
out, err := tmuxCmd("display-message", "-t", target, "-p", "#{pane_pid}")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,8 @@ import (
|
|||
// Session implements backend.Session for tmux-based tools
|
||||
type Session struct {
|
||||
id string
|
||||
sessionName string // tmux session name
|
||||
tmuxSession string // persistent tmux session name (e.g., "anvillm-kiro-cli")
|
||||
windowName string // window name within tmux session (same as id)
|
||||
cwd string
|
||||
alias string
|
||||
winID int
|
||||
|
|
@ -38,6 +39,11 @@ type Session struct {
|
|||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// target returns the tmux target for this window (session:window)
|
||||
func (s *Session) target() string {
|
||||
return windowTarget(s.tmuxSession, s.windowName)
|
||||
}
|
||||
|
||||
func (s *Session) ID() string {
|
||||
return s.id
|
||||
}
|
||||
|
|
@ -65,7 +71,8 @@ func (s *Session) Metadata() backend.SessionMetadata {
|
|||
WinID: s.winID,
|
||||
CreatedAt: s.createdAt,
|
||||
Extra: map[string]string{
|
||||
"tmux_session": s.sessionName,
|
||||
"tmux_session": s.tmuxSession,
|
||||
"tmux_window": s.windowName,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -141,7 +148,7 @@ func (s *Session) waitForReady(ctx context.Context, timeout time.Duration) error
|
|||
keys, done := s.startupHandler.HandleStartup(s.output.String())
|
||||
if keys != "" {
|
||||
log.Printf("[session %s] startup handler sending keys: %q", s.id, keys)
|
||||
if err := sendKeys(s.sessionName, keys); err != nil {
|
||||
if err := sendKeys(s.target(), keys); err != nil {
|
||||
return fmt.Errorf("failed to send startup response: %w", err)
|
||||
}
|
||||
}
|
||||
|
|
@ -182,7 +189,7 @@ func (s *Session) Send(ctx context.Context, prompt string) (string, error) {
|
|||
if strings.HasPrefix(prompt, "/") && s.commands != nil {
|
||||
if !s.commands.IsSupported(prompt) {
|
||||
cmd := strings.Fields(prompt)[0]
|
||||
return "", fmt.Errorf("slash command not supported: %s (attach to tmux session to run: tmux attach -t %s)", cmd, s.sessionName)
|
||||
return "", fmt.Errorf("slash command not supported: %s (attach to window: tmux select-window -t %s)", cmd, s.target())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -199,13 +206,13 @@ func (s *Session) Send(ctx context.Context, prompt string) (string, error) {
|
|||
log.Printf("[session %s] sending: %q", s.id, prompt)
|
||||
|
||||
// Send prompt using literal mode to avoid special character interpretation
|
||||
if err := sendLiteral(s.sessionName, prompt); err != nil {
|
||||
if err := sendLiteral(s.target(), prompt); err != nil {
|
||||
s.state = "idle"
|
||||
return "", fmt.Errorf("send literal failed: %w", err)
|
||||
}
|
||||
|
||||
// Send Enter
|
||||
if err := sendKeys(s.sessionName, "C-m"); err != nil {
|
||||
if err := sendKeys(s.target(), "C-m"); err != nil {
|
||||
s.state = "idle"
|
||||
return "", fmt.Errorf("send enter failed: %w", err)
|
||||
}
|
||||
|
|
@ -317,17 +324,22 @@ func (s *Session) Stop(ctx context.Context) error {
|
|||
}
|
||||
|
||||
// Send CTRL+C
|
||||
return sendKeys(s.sessionName, "C-c")
|
||||
return sendKeys(s.target(), "C-c")
|
||||
}
|
||||
|
||||
func (s *Session) Close() error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
// 1. Kill tmux session first (closes pipe-pane writer)
|
||||
if s.sessionName != "" {
|
||||
killSession(s.sessionName)
|
||||
s.sessionName = ""
|
||||
log.Printf("[session %s] Close() called, killing window %s:%s", s.id, s.tmuxSession, s.windowName)
|
||||
|
||||
// 1. Kill tmux window first (closes pipe-pane writer)
|
||||
if s.tmuxSession != "" && s.windowName != "" {
|
||||
if err := killWindow(s.tmuxSession, s.windowName); err != nil {
|
||||
log.Printf("[session %s] killWindow failed: %v", s.id, err)
|
||||
} else {
|
||||
log.Printf("[session %s] Window %s:%s killed", s.id, s.tmuxSession, s.windowName)
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Give writer time to close, which will send EOF to reader
|
||||
|
|
|
|||
|
|
@ -4,8 +4,6 @@ package tmux
|
|||
import (
|
||||
"acme-q/internal/backend"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
|
@ -58,8 +56,9 @@ type Config struct {
|
|||
|
||||
// Backend implements backend.Backend for tmux-based CLI tools
|
||||
type Backend struct {
|
||||
cfg Config
|
||||
counter uint64
|
||||
cfg Config
|
||||
counter uint64
|
||||
tmuxSession string // Persistent tmux session name (e.g., "anvillm-kiro-cli")
|
||||
}
|
||||
|
||||
// New creates a new tmux-based backend
|
||||
|
|
@ -73,7 +72,31 @@ func New(cfg Config) backend.Backend {
|
|||
if cfg.TmuxSize.Cols == 0 {
|
||||
cfg.TmuxSize.Cols = 120
|
||||
}
|
||||
return &Backend{cfg: cfg}
|
||||
return &Backend{
|
||||
cfg: cfg,
|
||||
tmuxSession: fmt.Sprintf("anvillm-%s", cfg.Name),
|
||||
}
|
||||
}
|
||||
|
||||
// ensureTmuxSession creates the persistent tmux session if it doesn't exist
|
||||
func (b *Backend) ensureTmuxSession() error {
|
||||
if sessionExists(b.tmuxSession) {
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Printf("[backend %s] creating persistent tmux session: %s", b.cfg.Name, b.tmuxSession)
|
||||
// Keep window 0 alive to hold the session open
|
||||
// It will be killed when the program exits
|
||||
return createSession(b.tmuxSession, b.cfg.TmuxSize.Rows, b.cfg.TmuxSize.Cols)
|
||||
}
|
||||
|
||||
// Cleanup kills the persistent tmux session (call on program exit)
|
||||
func (b *Backend) Cleanup() error {
|
||||
if sessionExists(b.tmuxSession) {
|
||||
log.Printf("[backend %s] cleaning up tmux session: %s", b.cfg.Name, b.tmuxSession)
|
||||
return killSession(b.tmuxSession)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Backend) Name() string {
|
||||
|
|
@ -82,45 +105,47 @@ func (b *Backend) Name() string {
|
|||
|
||||
func (b *Backend) CreateSession(ctx context.Context, cwd string) (backend.Session, error) {
|
||||
id := fmt.Sprintf("%d", atomic.AddUint64(&b.counter, 1))
|
||||
windowName := id // Use session ID as window name
|
||||
|
||||
// Generate unique session name with random suffix
|
||||
randomBytes := make([]byte, 4)
|
||||
rand.Read(randomBytes)
|
||||
sessionName := fmt.Sprintf("%s-%d-%s",
|
||||
b.cfg.Name,
|
||||
time.Now().Unix(),
|
||||
hex.EncodeToString(randomBytes))
|
||||
log.Printf("[session %s] creating window in tmux session %s", id, b.tmuxSession)
|
||||
|
||||
log.Printf("[session %s] creating tmux session %s", id, sessionName)
|
||||
|
||||
// 1. Create tmux session
|
||||
if err := createSession(sessionName, b.cfg.TmuxSize.Rows, b.cfg.TmuxSize.Cols); err != nil {
|
||||
return nil, fmt.Errorf("failed to create tmux session: %w", err)
|
||||
// 1. Ensure persistent tmux session exists
|
||||
if err := b.ensureTmuxSession(); err != nil {
|
||||
return nil, fmt.Errorf("failed to ensure tmux session: %w", err)
|
||||
}
|
||||
|
||||
// 2. Set environment variables
|
||||
// 2. Create window in tmux session
|
||||
if err := createWindow(b.tmuxSession, windowName); err != nil {
|
||||
return nil, fmt.Errorf("failed to create window: %w", err)
|
||||
}
|
||||
|
||||
target := windowTarget(b.tmuxSession, windowName)
|
||||
|
||||
// 3. Set environment variables for this window
|
||||
for k, v := range b.cfg.Environment {
|
||||
if err := setEnvironment(sessionName, k, v); err != nil {
|
||||
killSession(sessionName)
|
||||
if err := setEnvironment(target, k, v); err != nil {
|
||||
killWindow(b.tmuxSession, windowName)
|
||||
return nil, fmt.Errorf("failed to set environment: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Create FIFO for output
|
||||
fifoPath := fmt.Sprintf("/tmp/tmux-%s.fifo", sessionName)
|
||||
// 4. Create FIFO for output
|
||||
fifoPath := fmt.Sprintf("/tmp/tmux-%s-%s.fifo", b.tmuxSession, windowName)
|
||||
// Remove existing FIFO if it exists (from previous crashed session)
|
||||
os.Remove(fifoPath)
|
||||
if err := syscall.Mkfifo(fifoPath, 0600); err != nil {
|
||||
killSession(sessionName)
|
||||
killWindow(b.tmuxSession, windowName)
|
||||
return nil, fmt.Errorf("failed to create FIFO: %w", err)
|
||||
}
|
||||
|
||||
// 4. Setup pipe-pane
|
||||
if err := setupPipePane(sessionName, fifoPath); err != nil {
|
||||
// 5. Setup pipe-pane for this window
|
||||
if err := setupPipePane(target, fifoPath); err != nil {
|
||||
os.Remove(fifoPath)
|
||||
killSession(sessionName)
|
||||
killWindow(b.tmuxSession, windowName)
|
||||
return nil, fmt.Errorf("failed to setup pipe-pane: %w", err)
|
||||
}
|
||||
|
||||
// 5. Open FIFO for reading (this blocks until writer connects)
|
||||
// 6. Open FIFO for reading (this blocks until writer connects)
|
||||
// We need to do this in a goroutine to avoid blocking
|
||||
fifoOpenCh := make(chan *os.File, 1)
|
||||
fifoErrCh := make(chan error, 1)
|
||||
|
|
@ -133,7 +158,7 @@ func (b *Backend) CreateSession(ctx context.Context, cwd string) (backend.Sessio
|
|||
fifoOpenCh <- f
|
||||
}()
|
||||
|
||||
// 6. Start the command in tmux
|
||||
// 7. Start the command in tmux window
|
||||
cmdStr := ""
|
||||
for i, arg := range b.cfg.Command {
|
||||
if i > 0 {
|
||||
|
|
@ -148,36 +173,36 @@ func (b *Backend) CreateSession(ctx context.Context, cwd string) (backend.Sessio
|
|||
}
|
||||
|
||||
// Change to working directory first
|
||||
if err := sendKeys(sessionName, "cd", fmt.Sprintf("\"%s\"", cwd), "C-m"); err != nil {
|
||||
if err := sendKeys(target, "cd", fmt.Sprintf("\"%s\"", cwd), "C-m"); err != nil {
|
||||
os.Remove(fifoPath)
|
||||
killSession(sessionName)
|
||||
killWindow(b.tmuxSession, windowName)
|
||||
return nil, fmt.Errorf("failed to change directory: %w", err)
|
||||
}
|
||||
|
||||
// Send the command
|
||||
if err := sendKeys(sessionName, cmdStr, "C-m"); err != nil {
|
||||
if err := sendKeys(target, cmdStr, "C-m"); err != nil {
|
||||
os.Remove(fifoPath)
|
||||
killSession(sessionName)
|
||||
killWindow(b.tmuxSession, windowName)
|
||||
return nil, fmt.Errorf("failed to start command: %w", err)
|
||||
}
|
||||
|
||||
// 7. Wait for FIFO to open (should happen quickly after command starts)
|
||||
// 8. Wait for FIFO to open (should happen quickly after command starts)
|
||||
var fifo *os.File
|
||||
select {
|
||||
case fifo = <-fifoOpenCh:
|
||||
// Success
|
||||
case err := <-fifoErrCh:
|
||||
os.Remove(fifoPath)
|
||||
killSession(sessionName)
|
||||
killWindow(b.tmuxSession, windowName)
|
||||
return nil, fmt.Errorf("failed to open FIFO: %w", err)
|
||||
case <-time.After(5 * time.Second):
|
||||
os.Remove(fifoPath)
|
||||
killSession(sessionName)
|
||||
killWindow(b.tmuxSession, windowName)
|
||||
return nil, fmt.Errorf("timeout opening FIFO")
|
||||
}
|
||||
|
||||
// 8. Get PID
|
||||
pid, err := getSessionPID(sessionName)
|
||||
// 9. Get PID
|
||||
pid, err := getPanePID(target)
|
||||
if err != nil {
|
||||
log.Printf("[session %s] warning: failed to get PID: %v", id, err)
|
||||
pid = 0
|
||||
|
|
@ -185,7 +210,8 @@ func (b *Backend) CreateSession(ctx context.Context, cwd string) (backend.Sessio
|
|||
|
||||
sess := &Session{
|
||||
id: id,
|
||||
sessionName: sessionName,
|
||||
tmuxSession: b.tmuxSession,
|
||||
windowName: windowName,
|
||||
cwd: cwd,
|
||||
pid: pid,
|
||||
state: "starting",
|
||||
|
|
@ -200,10 +226,10 @@ func (b *Backend) CreateSession(ctx context.Context, cwd string) (backend.Sessio
|
|||
startupHandler: b.cfg.StartupHandler,
|
||||
}
|
||||
|
||||
// 9. Start reader goroutine
|
||||
// 10. Start reader goroutine
|
||||
go sess.reader()
|
||||
|
||||
// 10. Wait for ready
|
||||
// 11. Wait for ready
|
||||
if err := sess.waitForReady(ctx, b.cfg.StartupTime); err != nil {
|
||||
sess.Close()
|
||||
return nil, err
|
||||
|
|
@ -211,7 +237,7 @@ func (b *Backend) CreateSession(ctx context.Context, cwd string) (backend.Sessio
|
|||
|
||||
sess.state = "idle"
|
||||
sess.output.Reset()
|
||||
log.Printf("[session %s] ready (tmux=%s, pid=%d)", sess.ID(), sessionName, sess.pid)
|
||||
log.Printf("[session %s] ready (tmux=%s:%s, pid=%d)", sess.ID(), b.tmuxSession, windowName, sess.pid)
|
||||
|
||||
return sess, nil
|
||||
}
|
||||
|
|
|
|||
32
main.go
32
main.go
|
|
@ -56,6 +56,13 @@ func main() {
|
|||
|
||||
mgr := session.NewManager(backend)
|
||||
|
||||
// Cleanup tmux session on exit (if backend is tmux-based)
|
||||
defer func() {
|
||||
if tmuxBackend, ok := backend.(*tmux.Backend); ok {
|
||||
tmuxBackend.Cleanup()
|
||||
}
|
||||
}()
|
||||
|
||||
srv, err := p9.NewServer(mgr)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
|
|
@ -169,6 +176,13 @@ func main() {
|
|||
if sess != nil {
|
||||
meta := sess.Metadata()
|
||||
if meta.Pid == pid {
|
||||
// First kill the process
|
||||
if err := syscall.Kill(pid, syscall.SIGTERM); err != nil {
|
||||
log.Printf("[session %s] Failed to kill PID %d: %v", id, pid, err)
|
||||
} else {
|
||||
log.Printf("[session %s] Killed PID %d", id, pid)
|
||||
}
|
||||
// Then close the tmux window
|
||||
sess.Close()
|
||||
mgr.Remove(id)
|
||||
break
|
||||
|
|
@ -266,7 +280,7 @@ func openChatWindow(sess backend.Session) (*acme.Win, error) {
|
|||
return nil, err
|
||||
}
|
||||
w.Name(name)
|
||||
w.Write("tag", []byte("Send Stop Attach Alias Save Kill Sessions "))
|
||||
w.Write("tag", []byte("Send Stop Attach Alias Save Sessions "))
|
||||
w.Fprintf("body", "# Session %s\n# cwd: %s\n\nUSER:\n", sess.ID(), meta.Cwd)
|
||||
w.Ctl("clean")
|
||||
|
||||
|
|
@ -306,15 +320,19 @@ func handleChatWindow(w *acme.Win, sess backend.Session) {
|
|||
log.Printf("[session %s] Sent CTRL+C", sess.ID())
|
||||
}
|
||||
case "Attach":
|
||||
// Attach to tmux session (only works for tmux-based backends)
|
||||
// Attach to tmux window (only works for tmux-based backends)
|
||||
meta := sess.Metadata()
|
||||
if tmuxSession, ok := meta.Extra["tmux_session"]; ok {
|
||||
tmuxSession, hasSession := meta.Extra["tmux_session"]
|
||||
tmuxWindow, hasWindow := meta.Extra["tmux_window"]
|
||||
if hasSession && hasWindow {
|
||||
go func() {
|
||||
cmd := exec.Command(terminalCommand, "-e", "tmux", "attach", "-t", tmuxSession)
|
||||
// Attach directly to the specific window
|
||||
target := fmt.Sprintf("%s:%s", tmuxSession, tmuxWindow)
|
||||
cmd := exec.Command(terminalCommand, "-e", "tmux", "attach", "-t", target)
|
||||
if err := cmd.Start(); err != nil {
|
||||
log.Printf("[session %s] Failed to launch terminal: %v", sess.ID(), err)
|
||||
} else {
|
||||
log.Printf("[session %s] Launched terminal attached to tmux session: %s", sess.ID(), tmuxSession)
|
||||
log.Printf("[session %s] Launched terminal attached to window: %s", sess.ID(), target)
|
||||
}
|
||||
}()
|
||||
} else {
|
||||
|
|
@ -363,10 +381,6 @@ func handleChatWindow(w *acme.Win, sess backend.Session) {
|
|||
}
|
||||
log.Printf("[session %s] Saved to %s", sess.ID(), savePath)
|
||||
}()
|
||||
case "Kill":
|
||||
sess.Close()
|
||||
log.Printf("[session %s] Session killed", sess.ID())
|
||||
return
|
||||
case "Sessions":
|
||||
openSessionsWindow(w, sess)
|
||||
default:
|
||||
|
|
|
|||
Reference in New Issue