node-tools: start no bundle again once the runtime itself is stopping (issue 1126)
The service manager stops node-tools by signalling its whole group, so every bundle gets SIGTERM with it; the launcher took each exit for a fault and started the bundle again, for the 15 s the runtime took to stop. On ace's reboot of 2026-10-28 those restarted bundles dialled the system bus once polkit and the bus had stopped, the bus and polkit came back during the shutdown, nothing stopped them a second time, and the reboot waited 29 minutes on its job timeout. ShutDown, called first on SIGTERM, keeps any bundle from starting again, by a restart or by a call; a running one still answers.
This commit is contained in:
@@ -3,7 +3,12 @@ package launch
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type lostWrite struct{}
|
||||
@@ -28,3 +33,74 @@ func TestALostWriteIsAnsweredWithItsOwnCodeAndNothingElseIs(t *testing.T) {
|
||||
t.Errorf("a plain refusal is answered %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
// A bundle that answers the handshake and an empty tool list, and notes each start and its pid in $OUT.
|
||||
const notingBundle = `#!/bin/sh
|
||||
printf 'started %s\n' "$$" >> "$OUT"
|
||||
while IFS= read -r line; do
|
||||
id=$(printf '%s' "$line" | sed -n 's/.*"id":\([0-9]*\)[,}].*/\1/p')
|
||||
case "$line" in
|
||||
*'"method":"initialize"'*) printf '{"jsonrpc":"2.0","id":%s,"result":{}}\n' "$id" ;;
|
||||
*'"method":"tools/list"'*) printf '{"jsonrpc":"2.0","id":%s,"result":{"tools":[]}}\n' "$id" ;;
|
||||
esac
|
||||
done
|
||||
`
|
||||
|
||||
// The service manager stops the runtime by signalling its whole group, every bundle with it (issue 1126, ace's
|
||||
// reboot of 2026-10-28: every bundle "exited (SIGTERM); started again in 500ms" for the 15 s the runtime took
|
||||
// to stop, the power bundle among them dialling the system bus again, which restarted the bus during the
|
||||
// shutdown and held it 29 minutes). A bundle that exits while it serves is started again; once the runtime
|
||||
// itself is stopping, none is, by a restart or by a call.
|
||||
func TestABundleIsStartedAgainUnlessTheRuntimeIsStopping(t *testing.T) {
|
||||
saved := RestartFirst
|
||||
RestartFirst = 50 * time.Millisecond
|
||||
t.Cleanup(func() { RestartFirst = saved; shuttingDown.Store(false) })
|
||||
for _, row := range []struct {
|
||||
name string
|
||||
stopping bool
|
||||
starts int
|
||||
}{
|
||||
{"a bundle that dies while the runtime serves", false, 2},
|
||||
{"a bundle signalled with the stopping runtime", true, 1},
|
||||
} {
|
||||
t.Run(row.name, func(t *testing.T) {
|
||||
shuttingDown.Store(false)
|
||||
dir := t.TempDir()
|
||||
entry, out := filepath.Join(dir, "bundle"), filepath.Join(dir, "out")
|
||||
if err := os.WriteFile(entry, []byte(notingBundle), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
l, err := Start("noting", entry, append(os.Environ(), "OUT="+out), &subscribing{}, t.Logf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(l.Stop)
|
||||
said, _ := os.ReadFile(out)
|
||||
var pid int
|
||||
if _, err := fmt.Sscanf(string(said), "started %d", &pid); err != nil {
|
||||
t.Fatalf("the bundle noted %q", said)
|
||||
}
|
||||
if row.stopping {
|
||||
ShutDown()
|
||||
}
|
||||
if err := syscall.Kill(pid, syscall.SIGTERM); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
deadline := time.Now().Add(2 * time.Second)
|
||||
for {
|
||||
said, _ = os.ReadFile(out)
|
||||
n := strings.Count(string(said), "started")
|
||||
if n > row.starts {
|
||||
t.Fatalf("started %d times, want %d", n, row.starts)
|
||||
}
|
||||
if time.Now().After(deadline) {
|
||||
if n != row.starts {
|
||||
t.Fatalf("started %d times, want %d", n, row.starts)
|
||||
}
|
||||
break
|
||||
}
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user