Stacks: Duplicate, Labels and Network in the form, task verdict badges, a short Where; Cancel really cancels

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Mitchell-Johnson
2026-09-20 13:12:54 -07:00
co-authored by Claude Fable 5.1
parent a229f2b597
commit 1e992ca1eb
8 changed files with 183 additions and 36 deletions
+37 -6
View File
@@ -12,6 +12,7 @@ import (
"os/exec"
"sort"
"sync"
"syscall"
"time"
)
@@ -31,10 +32,11 @@ type Task struct {
ExitCode int
Err string
mu sync.Mutex
buf bytes.Buffer
subs map[chan []byte]struct{}
cancel context.CancelFunc
mu sync.Mutex
buf bytes.Buffer
subs map[chan []byte]struct{}
cancel context.CancelFunc
onCancel func()
}
type Snapshot struct {
@@ -71,6 +73,15 @@ func (m *Manager) Start(kind, title string, cmd *exec.Cmd) (*Task, error) {
subs: map[chan []byte]struct{}{}, cancel: cancel}
w := &writer{t: t}
cmd.Stdout, cmd.Stderr = w, w
// Its own process group: a task is usually `sh -c "a && b"`, and cancelling must reach a and
// b, not just the shell — killing only the shell left compose running and, because compose
// still held the output pipe, left the task "running" too. WaitDelay is the backstop for
// anything that escapes the group and keeps the pipe open.
if cmd.SysProcAttr == nil {
cmd.SysProcAttr = &syscall.SysProcAttr{}
}
cmd.SysProcAttr.Setpgid = true
cmd.WaitDelay = 5 * time.Second
if err := cmd.Start(); err != nil {
cancel()
return nil, err
@@ -87,8 +98,20 @@ func (m *Manager) Start(kind, title string, cmd *exec.Cmd) (*Task, error) {
select {
case err = <-done:
case <-ctx.Done():
_ = cmd.Process.Kill()
err = <-done
t.mu.Lock()
hook := t.onCancel
t.mu.Unlock()
if hook != nil {
go hook()
}
_ = syscall.Kill(-cmd.Process.Pid, syscall.SIGTERM) // asked first: compose and apt both stop cleanly
select {
case err = <-done:
case <-time.After(4 * time.Second):
_ = syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
_ = cmd.Process.Kill()
err = <-done
}
t.mu.Lock()
t.Err = "cancelled"
t.mu.Unlock()
@@ -207,6 +230,14 @@ func (t *Task) Subscribe() (backlog []byte, ch chan []byte, unsub func()) {
func (t *Task) Cancel() { t.cancel() }
// OnCancel sets something to do, besides stopping the task's own processes, when the task is
// cancelled: whatever it started that isn't its child (a command inside a container).
func (t *Task) OnCancel(fn func()) {
t.mu.Lock()
t.onCancel = fn
t.mu.Unlock()
}
type writer struct{ t *Task }
func (w *writer) Write(p []byte) (int, error) {