Make the wizard supervisor portable (no signal probing) and bind tests to an ephemeral port
coordinator / test (push) Waiting to run
python / test (push) Waiting to run
release / binaries (amd64, darwin) (push) Waiting to run
release / binaries (amd64, linux) (push) Waiting to run
release / binaries (amd64, windows) (push) Waiting to run
release / binaries (arm64, darwin) (push) Waiting to run
release / binaries (arm64, linux) (push) Waiting to run
release / binaries (arm64, windows) (push) Waiting to run
release / release (push) Blocked by required conditions
release / image (push) Waiting to run
users / test (push) Waiting to run

This commit is contained in:
Emil
2026-08-03 00:38:33 +03:00
parent c301499bef
commit 82eaec4c55
2 changed files with 45 additions and 25 deletions
+44 -25
View File
@@ -21,7 +21,6 @@ import (
"strconv"
"strings"
"sync"
"syscall"
"time"
"github.com/emil28092005/SciMesh/coordinator/internal/agent"
@@ -51,10 +50,13 @@ type Supervisor interface {
}
// PIDSupervisor is the real Supervisor: it spawns the running binary with
// --config and manages its pid file.
// --config and manages its pid file. Liveness comes from a Wait goroutine, so
// it works on every platform (no signal probing, which Windows lacks).
type PIDSupervisor struct {
mu sync.Mutex
pidPath string
proc *os.Process
done chan struct{} // closed when the spawned process exits; nil when not started
}
func NewPIDSupervisor(pidPath string) *PIDSupervisor { return &PIDSupervisor{pidPath: pidPath} }
@@ -78,24 +80,27 @@ func (s *PIDSupervisor) readPid() int {
}
func (s *PIDSupervisor) Alive() bool {
pid := s.Pid()
if pid == 0 {
s.mu.Lock()
defer s.mu.Unlock()
if s.done == nil {
return false
}
// Signal 0 probes liveness without sending anything.
return syscall.Kill(pid, 0) == nil
}
func (s *PIDSupervisor) processAlive(pid int) bool {
return syscall.Kill(pid, 0) == nil
select {
case <-s.done:
return false
default:
return true
}
}
func (s *PIDSupervisor) Start(configPath, logPath string) (int, error) {
s.mu.Lock()
defer s.mu.Unlock()
if pid := s.readPid(); pid > 0 {
if s.processAlive(pid) {
return pid, fmt.Errorf("worker is already running (pid %d)", pid)
if s.done != nil {
select {
case <-s.done:
default:
return s.readPid(), fmt.Errorf("worker is already running (pid %d)", s.readPid())
}
}
exe, err := os.Executable()
@@ -117,6 +122,9 @@ func (s *PIDSupervisor) Start(configPath, logPath string) (int, error) {
// The child inherits our stdout/stderr descriptors pointing at the log
// file, so we can close our copy; the child keeps it open.
_ = logFile.Close()
s.proc = cmd.Process
s.done = make(chan struct{})
go func() { _ = cmd.Wait(); close(s.done) }()
if err := os.WriteFile(s.pidPath, []byte(strconv.Itoa(cmd.Process.Pid)+"\n"), 0o600); err != nil {
_ = cmd.Process.Kill()
return 0, fmt.Errorf("write pid file: %w", err)
@@ -127,27 +135,38 @@ func (s *PIDSupervisor) Start(configPath, logPath string) (int, error) {
func (s *PIDSupervisor) Stop() error {
s.mu.Lock()
defer s.mu.Unlock()
pid := s.readPid()
if pid == 0 {
if s.done == nil {
return nil
}
proc, err := os.FindProcess(pid)
if err != nil {
select {
case <-s.done:
s.done = nil
s.proc = nil
_ = os.Remove(s.pidPath)
return nil
default:
}
if err := proc.Signal(os.Interrupt); err != nil {
_ = os.Remove(s.pidPath)
return nil
}
// Give the agent a moment to exit cleanly, then reap.
// Ask politely, then force. os.Interrupt terminates on Windows too.
_ = s.proc.Signal(os.Interrupt)
deadline := time.Now().Add(3 * time.Second)
for time.Now().Before(deadline) {
if !s.processAlive(pid) {
break
select {
case <-s.done:
s.done = nil
s.proc = nil
_ = os.Remove(s.pidPath)
return nil
default:
time.Sleep(100 * time.Millisecond)
}
time.Sleep(100 * time.Millisecond)
}
_ = s.proc.Kill()
select {
case <-s.done:
case <-time.After(2 * time.Second):
}
s.done = nil
s.proc = nil
_ = os.Remove(s.pidPath)
return nil
}
@@ -25,6 +25,7 @@ func newTestServer(t *testing.T, sup Supervisor) (*Server, string) {
t.Helper()
dir := t.TempDir()
server := New(testLogger(), Options{
Port: 0, // ephemeral: tests must never collide on the default 12700
ConfigPath: filepath.Join(dir, "config.json"),
Dir: dir,
Supervisor: sup,