fix(bash): kill entire process group on cancel

Sets Setpgid on bash commands and sends SIGTERM/SIGKILL to the negative pgid so backgrounded children are cleaned up on esc. Also splits the status bar onto multiple lines on narrow terminals when a spinner is active.
This commit is contained in:
patriceckhart 2026-04-23 09:43:57 +02:00
parent fcd6d7c9a2
commit b6529cf5c4
4 changed files with 70 additions and 12 deletions

View file

@ -13,7 +13,6 @@ import (
"path/filepath"
"runtime"
"strings"
"syscall"
"time"
"github.com/patriceckhart/zot/internal/core"
@ -69,6 +68,7 @@ func (t *BashTool) Execute(ctx context.Context, raw json.RawMessage, progress fu
cmd := newShellCmd(runCtx, a.Command)
cmd.Dir = cwd
cmd.Env = os.Environ()
setProcessGroup(cmd)
// Capture merged stdout+stderr with line-by-line streaming.
pr, pw := io.Pipe()
@ -225,14 +225,3 @@ func newShellCmd(ctx context.Context, command string) *exec.Cmd {
}
return exec.CommandContext(ctx, "/bin/sh", "-c", command)
}
// killProcessGroup best-effort SIGTERM then SIGKILL.
func killProcessGroup(cmd *exec.Cmd) {
if cmd.Process == nil {
return
}
_ = cmd.Process.Signal(syscall.SIGTERM)
time.AfterFunc(3*time.Second, func() {
_ = cmd.Process.Kill()
})
}

View file

@ -0,0 +1,29 @@
//go:build !windows
package tools
import (
"os/exec"
"syscall"
"time"
)
// setProcessGroup puts the command in its own process group so
// killProcessGroup can target the entire tree including background
// children spawned with &.
func setProcessGroup(cmd *exec.Cmd) {
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
}
// killProcessGroup sends SIGTERM then SIGKILL to the entire process
// group so backgrounded children (cmd &) are also cleaned up.
func killProcessGroup(cmd *exec.Cmd) {
if cmd.Process == nil {
return
}
pgid := cmd.Process.Pid
_ = syscall.Kill(-pgid, syscall.SIGTERM)
time.AfterFunc(3*time.Second, func() {
_ = syscall.Kill(-pgid, syscall.SIGKILL)
})
}

View file

@ -0,0 +1,20 @@
//go:build windows
package tools
import (
"os/exec"
"time"
)
func setProcessGroup(_ *exec.Cmd) {}
func killProcessGroup(cmd *exec.Cmd) {
if cmd.Process == nil {
return
}
_ = cmd.Process.Kill()
time.AfterFunc(3*time.Second, func() {
_ = cmd.Process.Kill()
})
}

View file

@ -1420,6 +1420,26 @@ func StatusBar(p StatusBarParams) []string {
}
primary := leftBuilder.String()
// On narrow terminals the single line wraps badly. If the visible
// width exceeds cols and we have a busy prefix, split: keep the
// busy prefix on line 1, put model+stats on line 2.
if p.Cols > 0 && p.BusyPrefix != "" && visibleWidth(primary) > p.Cols {
busyLine := pad + p.BusyPrefix
var infoBuilder strings.Builder
infoBuilder.WriteString(pad)
infoBuilder.WriteString(th.FG256(th.Muted, left))
if middle != "" {
infoBuilder.WriteString(pad)
infoBuilder.WriteString(th.FG256(th.Muted, middle))
}
lines := []string{busyLine, infoBuilder.String()}
if cwd != "" {
lines = append(lines, pad+th.FG256(th.Muted, cwd))
}
return lines
}
if cwd == "" {
return []string{primary}
}