fix(crew): detect running sessions started with shell compound commands
IsClaudeRunning now checks for child processes when the pane command is a shell (bash/zsh). This fixes gt crew start --all killing running crew members that were started with "export ... && claude ..." commands. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
+53
-1
@@ -406,6 +406,43 @@ func (t *Tmux) GetPaneWorkDir(session string) (string, error) {
|
||||
return strings.TrimSpace(out), nil
|
||||
}
|
||||
|
||||
// GetPanePID returns the PID of the pane's main process.
|
||||
func (t *Tmux) GetPanePID(session string) (string, error) {
|
||||
out, err := t.run("list-panes", "-t", session, "-F", "#{pane_pid}")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimSpace(out), nil
|
||||
}
|
||||
|
||||
// hasClaudeChild checks if a process has a child running claude/node.
|
||||
// Used when the pane command is a shell (bash, zsh) that launched claude.
|
||||
func hasClaudeChild(pid string) bool {
|
||||
// Use pgrep to find child processes
|
||||
cmd := exec.Command("pgrep", "-P", pid, "-l")
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
// Check if any child is node or claude
|
||||
lines := strings.Split(string(out), "\n")
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
// Format: "PID name" e.g., "29677 node"
|
||||
parts := strings.Fields(line)
|
||||
if len(parts) >= 2 {
|
||||
name := parts[1]
|
||||
if name == "node" || name == "claude" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// FindSessionByWorkDir finds tmux sessions where the pane's current working directory
|
||||
// matches or is under the target directory. Returns session names that match.
|
||||
// If processNames is provided, only returns sessions that match those processes.
|
||||
@@ -597,6 +634,7 @@ func (t *Tmux) IsAgentRunning(session string, expectedPaneCommands ...string) bo
|
||||
// IsClaudeRunning checks if Claude appears to be running in the session.
|
||||
// Only trusts the pane command - UI markers in scrollback cause false positives.
|
||||
// Claude can report as "node", "claude", or a version number like "2.0.76".
|
||||
// Also checks for child processes when the pane is a shell running claude via "bash -c".
|
||||
func (t *Tmux) IsClaudeRunning(session string) bool {
|
||||
// Check for known command names first
|
||||
if t.IsAgentRunning(session, "node", "claude") {
|
||||
@@ -608,7 +646,21 @@ func (t *Tmux) IsClaudeRunning(session string) bool {
|
||||
return false
|
||||
}
|
||||
matched, _ := regexp.MatchString(`^\d+\.\d+\.\d+`, cmd)
|
||||
return matched
|
||||
if matched {
|
||||
return true
|
||||
}
|
||||
// If pane command is a shell, check for claude/node child processes.
|
||||
// This handles the case where sessions are started with "bash -c 'export ... && claude ...'"
|
||||
for _, shell := range constants.SupportedShells {
|
||||
if cmd == shell {
|
||||
pid, err := t.GetPanePID(session)
|
||||
if err == nil && pid != "" {
|
||||
return hasClaudeChild(pid)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsRuntimeRunning checks if a runtime appears to be running in the session.
|
||||
|
||||
@@ -460,3 +460,70 @@ func TestIsClaudeRunning_VersionPattern(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsClaudeRunning_ShellWithNodeChild(t *testing.T) {
|
||||
if !hasTmux() {
|
||||
t.Skip("tmux not installed")
|
||||
}
|
||||
|
||||
tm := NewTmux()
|
||||
sessionName := "gt-test-shell-child-" + t.Name()
|
||||
|
||||
// Clean up any existing session
|
||||
_ = tm.KillSession(sessionName)
|
||||
|
||||
// Create session with "bash -c" running a node process
|
||||
// Use a simple node command that runs for a few seconds
|
||||
cmd := `node -e "setTimeout(() => {}, 10000)"`
|
||||
if err := tm.NewSessionWithCommand(sessionName, "", cmd); err != nil {
|
||||
t.Fatalf("NewSessionWithCommand: %v", err)
|
||||
}
|
||||
defer func() { _ = tm.KillSession(sessionName) }()
|
||||
|
||||
// Give the node process time to start
|
||||
// WaitForCommand waits until NOT running bash/zsh/sh
|
||||
shellsToExclude := []string{"bash", "zsh", "sh"}
|
||||
err := tm.WaitForCommand(sessionName, shellsToExclude, 2000*1000000) // 2 second timeout
|
||||
if err != nil {
|
||||
// If we timeout waiting, it means the pane command is still a shell
|
||||
// This is the case we're testing - shell with a node child
|
||||
paneCmd, _ := tm.GetPaneCommand(sessionName)
|
||||
t.Logf("Pane command is %q - testing shell+child detection", paneCmd)
|
||||
}
|
||||
|
||||
// Now test IsClaudeRunning - it should detect node as a child process
|
||||
paneCmd, _ := tm.GetPaneCommand(sessionName)
|
||||
if paneCmd == "node" {
|
||||
// Direct node detection should work
|
||||
if !tm.IsClaudeRunning(sessionName) {
|
||||
t.Error("IsClaudeRunning should return true when pane command is 'node'")
|
||||
}
|
||||
} else {
|
||||
// Pane is a shell (bash/zsh) with node as child
|
||||
// The new child process detection should catch this
|
||||
got := tm.IsClaudeRunning(sessionName)
|
||||
t.Logf("Pane command: %q, IsClaudeRunning: %v", paneCmd, got)
|
||||
// Note: This may or may not detect depending on how tmux runs the command.
|
||||
// On some systems, tmux runs the command directly; on others via a shell.
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasClaudeChild(t *testing.T) {
|
||||
// Test the hasClaudeChild helper function directly
|
||||
// This uses the current process as a test subject
|
||||
|
||||
// Get current process PID as string
|
||||
currentPID := "1" // init/launchd - should have children but not claude/node
|
||||
|
||||
// hasClaudeChild should return false for init (no node/claude children)
|
||||
got := hasClaudeChild(currentPID)
|
||||
if got {
|
||||
t.Logf("hasClaudeChild(%q) = true - init has claude/node child?", currentPID)
|
||||
}
|
||||
|
||||
// Test with a definitely nonexistent PID
|
||||
got = hasClaudeChild("999999999")
|
||||
if got {
|
||||
t.Error("hasClaudeChild should return false for nonexistent PID")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user