From b02b57e6fbec4ec44d040757b43c679d9506f996 Mon Sep 17 00:00:00 2001 From: Snider Date: Sun, 1 Feb 2026 01:56:44 +0000 Subject: [PATCH] fix: add Windows compatibility for process management (#58) Add build tags to separate Unix and Windows process handling in pkg/php: - services_unix.go: Unix-specific process group handling (Setpgid, Getpgid, Kill) - services_windows.go: Windows-compatible alternatives using os.Signal - services.go: Use platform-agnostic helper functions The pkg/php package now compiles on Windows. Process termination works via os.Interrupt/os.Kill instead of Unix signals. Fixes #56 Co-authored-by: Claude Opus 4.5 --- pkg/php/services.go | 22 +++++--------------- pkg/php/services_unix.go | 41 +++++++++++++++++++++++++++++++++++++ pkg/php/services_windows.go | 34 ++++++++++++++++++++++++++++++ 3 files changed, 80 insertions(+), 17 deletions(-) create mode 100644 pkg/php/services_unix.go create mode 100644 pkg/php/services_windows.go diff --git a/pkg/php/services.go b/pkg/php/services.go index 44e0a61c..47a8b780 100644 --- a/pkg/php/services.go +++ b/pkg/php/services.go @@ -10,7 +10,6 @@ import ( "path/filepath" "strings" "sync" - "syscall" "time" "github.com/host-uk/core/pkg/cli" @@ -120,10 +119,8 @@ func (s *baseService) startProcess(ctx context.Context, cmdName string, args []s s.cmd.Stderr = logFile s.cmd.Env = append(os.Environ(), env...) - // Set process group for clean shutdown - s.cmd.SysProcAttr = &syscall.SysProcAttr{ - Setpgid: true, - } + // Set platform-specific process attributes for clean shutdown + setSysProcAttr(s.cmd) if err := s.cmd.Start(); err != nil { logFile.Close() @@ -159,13 +156,8 @@ func (s *baseService) stopProcess() error { return nil } - // Send SIGTERM to process group - pgid, err := syscall.Getpgid(s.cmd.Process.Pid) - if err == nil { - syscall.Kill(-pgid, syscall.SIGTERM) - } else { - s.cmd.Process.Signal(syscall.SIGTERM) - } + // Send termination signal to process (group on Unix) + signalProcessGroup(s.cmd, termSignal()) // Wait for graceful shutdown with timeout done := make(chan struct{}) @@ -179,11 +171,7 @@ func (s *baseService) stopProcess() error { // Process exited gracefully case <-time.After(5 * time.Second): // Force kill - if pgid, err := syscall.Getpgid(s.cmd.Process.Pid); err == nil { - syscall.Kill(-pgid, syscall.SIGKILL) - } else { - s.cmd.Process.Kill() - } + signalProcessGroup(s.cmd, killSignal()) } s.running = false diff --git a/pkg/php/services_unix.go b/pkg/php/services_unix.go new file mode 100644 index 00000000..b7eb31e3 --- /dev/null +++ b/pkg/php/services_unix.go @@ -0,0 +1,41 @@ +//go:build unix + +package php + +import ( + "os/exec" + "syscall" +) + +// setSysProcAttr sets Unix-specific process attributes for clean process group handling. +func setSysProcAttr(cmd *exec.Cmd) { + cmd.SysProcAttr = &syscall.SysProcAttr{ + Setpgid: true, + } +} + +// signalProcessGroup sends a signal to the process group. +// On Unix, this uses negative PID to signal the entire group. +func signalProcessGroup(cmd *exec.Cmd, sig syscall.Signal) error { + if cmd.Process == nil { + return nil + } + + pgid, err := syscall.Getpgid(cmd.Process.Pid) + if err == nil { + return syscall.Kill(-pgid, sig) + } + + // Fallback to signaling just the process + return cmd.Process.Signal(sig) +} + +// termSignal returns SIGTERM for Unix. +func termSignal() syscall.Signal { + return syscall.SIGTERM +} + +// killSignal returns SIGKILL for Unix. +func killSignal() syscall.Signal { + return syscall.SIGKILL +} diff --git a/pkg/php/services_windows.go b/pkg/php/services_windows.go new file mode 100644 index 00000000..3da98b94 --- /dev/null +++ b/pkg/php/services_windows.go @@ -0,0 +1,34 @@ +//go:build windows + +package php + +import ( + "os" + "os/exec" +) + +// setSysProcAttr sets Windows-specific process attributes. +// Windows doesn't support Setpgid, so this is a no-op. +func setSysProcAttr(cmd *exec.Cmd) { + // No-op on Windows - process groups work differently +} + +// signalProcessGroup sends a termination signal to the process. +// On Windows, we can only signal the main process, not a group. +func signalProcessGroup(cmd *exec.Cmd, sig os.Signal) error { + if cmd.Process == nil { + return nil + } + + return cmd.Process.Signal(sig) +} + +// termSignal returns os.Interrupt for Windows (closest to SIGTERM). +func termSignal() os.Signal { + return os.Interrupt +} + +// killSignal returns os.Kill for Windows. +func killSignal() os.Signal { + return os.Kill +}