proc: the login shell magpie asks for its PATH runs in a session of its own, so it can't take the terminal from magpie (DD on Discord: magpie web → zsh: suspended (tty input))

askShell ran `$SHELL -ilc` (and ReadCLI's zsh, bash and fish, for Settings › Command line; LoginPath for Library › RTK) in magpie's own session and process group. An interactive zsh puts itself in the terminal's foreground, so while it ran, magpie's group was in the background of the terminal it was started from: Ctrl-C no longer reached `magpie web`, and anything left in magpie's group that touched the terminal — the bash asked beside it, whose profile runs in magpie's group without job control, or another child — had the kernel send SIGTTIN to the whole group, stopping magpie ("zsh: suspended (tty input)"). Run with `&`, the user's own shell then got EIO reading the terminal and exited. A timeout also killed only the shell, leaving what its profile started holding the terminal.

The shell is now a probe (ProbeContext): it leads a session with no controlling terminal, and its timeout and EndProbes end its whole group. It still answers with its PATH and variables.

TestAskShellOwnSession (a fake shell answering with its process group; a real zsh still gives a PATH) fails without the change: "the login shell ran in magpie's process group …". Reproduced on a pty under an interactive zsh -f, temp HOME, MAGPIE_ADDR 127.0.0.1:47811, magpie web on 47812, ~/.zshrc `sleep 2`, ~/.bash_profile reading /dev/tty, GET /api/cli: the old build prints "zsh: suspended (tty input)  magpie web" in the foreground and "[1] + suspended (tty input)" with `&` (then "zsh: error on TTY read"); with a plain profile it loses the foreground (ps S+ → S). The fixed build stays S+ / running in both, and /api/cli still says zsh and bash are known. go vet -tags nogui ./..., GOOS=linux go build -tags nogui, GOOS=windows go build, and go test -tags nogui of internal/proc, internal/update, internal/library and internal/gui under a temp HOME pass; -race -count=10 of the new test passes. Not tried with DD's own profile, or on Linux.
This commit is contained in:
yetone committed 2026-10-08 03:45:32 +08:00
1 parent b7f1c7d2aa
commit bbf9914307
2 files changed
+46 -1

No files matched your search

+12 -1
View File
@@ -171,11 +171,22 @@ func shellEnv() (string, map[string]string) {
}
// askShell runs sh as a terminal opens it and reads what shellEnv says.
//
// The shell is a probe (ProbeContext): it leads a session of its own, with
// no controlling terminal. An interactive zsh in magpie's session took the
// terminal it was started from — it puts itself in the foreground, as a
// shell does for its jobs — and while it ran, magpie's own process group
// was in the background there: Ctrl-C no longer reached `magpie web`, and
// anything in that group touching the terminal (the bash asked beside it,
// whose profile runs in magpie's group, an agent's CLI) had the kernel stop
// the whole group, magpie with it (DD on Discord: magpie web → zsh:
// suspended (tty input)). A timeout also killed only the shell, leaving
// what its profile started holding the terminal; a probe's whole group goes.
func askShell(sh string) (string, map[string]string) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// interactive too, since many put PATH in .zshrc/.bashrc
cmd := CommandContext(ctx, sh, "-ilc", shellProbe(sh))
cmd := ProbeContext(ctx, sh, "-ilc", shellProbe(sh))
cmd.Stdin = nil
out, _ := cmd.Output()
return parseShellEnv(string(out), shellMark)
+34
View File
@@ -6,7 +6,9 @@ import (
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"syscall"
"testing"
"time"
@@ -142,3 +144,35 @@ func TestShellEnvNushell(t *testing.T) {
t.Fatalf("CLAUDE_CONFIG_DIR = %q, want none", got)
}
}
// The login shell is asked outside magpie's process group and session (DD
// on Discord: magpie web → zsh: suspended (tty input)). An interactive zsh
// in magpie's session puts itself in the terminal's foreground, which left
// magpie's group in the background there: Ctrl-C no longer reached it, and
// anything in the group that touched the terminal had the kernel stop all
// of it. The fake shell answers with its own process group and session; a
// real zsh, where there is one, still gives its PATH when asked that way.
func TestAskShellOwnSession(t *testing.T) {
home := t.TempDir()
t.Setenv("HOME", home)
sh := filepath.Join(home, "sh")
testenv.Program(t, sh, "#!/bin/sh\nprintf '"+shellMark+"%s %s"+shellMark+"' \"$(ps -o pgid= -p $$ | tr -d ' ')\" \"$(ps -o sess= -p $$ | tr -d ' ')\"\n")
out, _ := askShell(sh)
f := strings.Fields(out)
if len(f) != 2 {
t.Fatalf("the fake shell's answer: %q", out)
}
if mine := strconv.Itoa(syscall.Getpgrp()); f[0] == mine {
t.Fatalf("the login shell ran in magpie's process group %s: a shell there takes the terminal from magpie", mine)
}
if sid, err := syscall.Getsid(0); err == nil && f[1] == strconv.Itoa(sid) && f[1] != "0" {
t.Fatalf("the login shell ran in magpie's session %s: it can take magpie's terminal", f[1])
}
zsh, err := exec.LookPath("zsh")
if err != nil {
t.Skip("zsh isn't installed")
}
if p := ShellPath(zsh); len(p) == 0 {
t.Fatalf("zsh asked in a session of its own gave no PATH")
}
}