Make the wizard supervisor portable (no signal probing) and bind tests to an ephemeral port
coordinator / test (push) Canceled after 0s
python / test (push) Canceled after 0s
release / binaries (amd64, darwin) (push) Canceled after 0s
release / binaries (amd64, linux) (push) Canceled after 0s
release / binaries (amd64, windows) (push) Canceled after 0s
release / binaries (arm64, darwin) (push) Canceled after 0s
release / binaries (arm64, linux) (push) Canceled after 0s
release / binaries (arm64, windows) (push) Canceled after 0s
release / image (push) Canceled after 0s
users / test (push) Canceled after 0s
release / release (push) Canceled after 0s
coordinator / test (push) Canceled after 0s
python / test (push) Canceled after 0s
release / binaries (amd64, darwin) (push) Canceled after 0s
release / binaries (amd64, linux) (push) Canceled after 0s
release / binaries (amd64, windows) (push) Canceled after 0s
release / binaries (arm64, darwin) (push) Canceled after 0s
release / binaries (arm64, linux) (push) Canceled after 0s
release / binaries (arm64, windows) (push) Canceled after 0s
release / image (push) Canceled after 0s
users / test (push) Canceled after 0s
release / release (push) Canceled after 0s
This commit is contained in:
@@ -21,7 +21,6 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"syscall"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/emil28092005/SciMesh/coordinator/internal/agent"
|
"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
|
// 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 {
|
type PIDSupervisor struct {
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
pidPath string
|
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} }
|
func NewPIDSupervisor(pidPath string) *PIDSupervisor { return &PIDSupervisor{pidPath: pidPath} }
|
||||||
@@ -78,24 +80,27 @@ func (s *PIDSupervisor) readPid() int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *PIDSupervisor) Alive() bool {
|
func (s *PIDSupervisor) Alive() bool {
|
||||||
pid := s.Pid()
|
s.mu.Lock()
|
||||||
if pid == 0 {
|
defer s.mu.Unlock()
|
||||||
|
if s.done == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
// Signal 0 probes liveness without sending anything.
|
select {
|
||||||
return syscall.Kill(pid, 0) == nil
|
case <-s.done:
|
||||||
}
|
return false
|
||||||
|
default:
|
||||||
func (s *PIDSupervisor) processAlive(pid int) bool {
|
return true
|
||||||
return syscall.Kill(pid, 0) == nil
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *PIDSupervisor) Start(configPath, logPath string) (int, error) {
|
func (s *PIDSupervisor) Start(configPath, logPath string) (int, error) {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
defer s.mu.Unlock()
|
defer s.mu.Unlock()
|
||||||
if pid := s.readPid(); pid > 0 {
|
if s.done != nil {
|
||||||
if s.processAlive(pid) {
|
select {
|
||||||
return pid, fmt.Errorf("worker is already running (pid %d)", pid)
|
case <-s.done:
|
||||||
|
default:
|
||||||
|
return s.readPid(), fmt.Errorf("worker is already running (pid %d)", s.readPid())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
exe, err := os.Executable()
|
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
|
// The child inherits our stdout/stderr descriptors pointing at the log
|
||||||
// file, so we can close our copy; the child keeps it open.
|
// file, so we can close our copy; the child keeps it open.
|
||||||
_ = logFile.Close()
|
_ = 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 {
|
if err := os.WriteFile(s.pidPath, []byte(strconv.Itoa(cmd.Process.Pid)+"\n"), 0o600); err != nil {
|
||||||
_ = cmd.Process.Kill()
|
_ = cmd.Process.Kill()
|
||||||
return 0, fmt.Errorf("write pid file: %w", err)
|
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 {
|
func (s *PIDSupervisor) Stop() error {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
defer s.mu.Unlock()
|
defer s.mu.Unlock()
|
||||||
pid := s.readPid()
|
if s.done == nil {
|
||||||
if pid == 0 {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
proc, err := os.FindProcess(pid)
|
select {
|
||||||
if err != nil {
|
case <-s.done:
|
||||||
|
s.done = nil
|
||||||
|
s.proc = nil
|
||||||
_ = os.Remove(s.pidPath)
|
_ = os.Remove(s.pidPath)
|
||||||
return nil
|
return nil
|
||||||
|
default:
|
||||||
}
|
}
|
||||||
if err := proc.Signal(os.Interrupt); err != nil {
|
// Ask politely, then force. os.Interrupt terminates on Windows too.
|
||||||
_ = os.Remove(s.pidPath)
|
_ = s.proc.Signal(os.Interrupt)
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// Give the agent a moment to exit cleanly, then reap.
|
|
||||||
deadline := time.Now().Add(3 * time.Second)
|
deadline := time.Now().Add(3 * time.Second)
|
||||||
for time.Now().Before(deadline) {
|
for time.Now().Before(deadline) {
|
||||||
if !s.processAlive(pid) {
|
select {
|
||||||
break
|
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)
|
_ = os.Remove(s.pidPath)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ func newTestServer(t *testing.T, sup Supervisor) (*Server, string) {
|
|||||||
t.Helper()
|
t.Helper()
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
server := New(testLogger(), Options{
|
server := New(testLogger(), Options{
|
||||||
|
Port: 0, // ephemeral: tests must never collide on the default 12700
|
||||||
ConfigPath: filepath.Join(dir, "config.json"),
|
ConfigPath: filepath.Join(dir, "config.json"),
|
||||||
Dir: dir,
|
Dir: dir,
|
||||||
Supervisor: sup,
|
Supervisor: sup,
|
||||||
|
|||||||
Reference in New Issue
Block a user