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:
co-authored by
Claude Fable 5.1
parent
a229f2b597
commit
1e992ca1eb
+32
-1
@@ -753,6 +753,16 @@ func (s *Server) stackAction(w http.ResponseWriter, r *http.Request) {
|
||||
jsonErr(w, 500, err.Error())
|
||||
return
|
||||
}
|
||||
if st.Via != "" {
|
||||
// compose runs inside st.Via, started by `docker exec`: stopping the exec client does not
|
||||
// stop what it started in there, so a cancel has to go in after it.
|
||||
via, pattern := st.Via, "compose -p "+regexp.QuoteMeta(st.Name)+" -f "
|
||||
t.OnCancel(func() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
_ = exec.CommandContext(ctx, dockerTool(), "exec", via, "pkill", "-TERM", "-f", pattern).Run()
|
||||
})
|
||||
}
|
||||
stackUpdatesForget(r.PathValue("node") + "/" + st.Name)
|
||||
log.Printf("stack %s: %s started as task %s (from %s)", st.Name, r.PathValue("action"), t.ID, clientIP(r))
|
||||
jsonOK(w, 202, t.Snapshot(false))
|
||||
@@ -929,7 +939,10 @@ func (s *Server) stackCreate(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 60*time.Second)
|
||||
defer cancel()
|
||||
raw, err := io.ReadAll(io.LimitReader(r.Body, 1<<20))
|
||||
var body struct{ Name, Text, Env, Home string }
|
||||
var body struct {
|
||||
Name, Text, Env, Home string
|
||||
EnvFrom string `json:"env_from"` // a stack being duplicated: its .env is copied here, on the machine, unseen
|
||||
}
|
||||
if err != nil || json.Unmarshal(raw, &body) != nil {
|
||||
jsonErr(w, 400, "bad request")
|
||||
return
|
||||
@@ -975,6 +988,24 @@ func (s *Server) stackCreate(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
}
|
||||
if body.EnvFrom != "" && strings.TrimSpace(body.Env) == "" {
|
||||
var from *stack
|
||||
for i := range all {
|
||||
if all[i].Name == body.EnvFrom && all[i].Editable && all[i].HasEnv {
|
||||
from = &all[i]
|
||||
}
|
||||
}
|
||||
if from == nil {
|
||||
jsonErr(w, 400, body.EnvFrom+" has no .env this console can read")
|
||||
return
|
||||
}
|
||||
b, err := readAsRoot(ctx, from.envPath())
|
||||
if err != nil {
|
||||
jsonErr(w, 500, "reading "+body.EnvFrom+"'s .env: "+err.Error())
|
||||
return
|
||||
}
|
||||
body.Env = string(b)
|
||||
}
|
||||
dir := filepath.Join(home.Dir, name)
|
||||
if readable(dir) || exec.Command("test", "-e", dir).Run() == nil {
|
||||
jsonErr(w, 409, dir+" already exists")
|
||||
|
||||
+37
-6
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user