diff --git a/pkg/pid/pidfile.go b/pkg/pid/pidfile.go index 00601195f..3efc45f09 100644 --- a/pkg/pid/pidfile.go +++ b/pkg/pid/pidfile.go @@ -64,7 +64,14 @@ func WritePidFile(homePath, host string, port int) (*PidFileData, error) { // pass the isProcessRunning check, blocking new gateway starts. // Treat recorded PID 1 as always stale. if data.PID != 1 && isProcessRunning(data.PID) { - return nil, fmt.Errorf("gateway is already running (PID: %d, version: %s)", data.PID, data.Version) + // Verify the process is actually a picoclaw instance. + // If the PID was reused by an unrelated process + // (e.g. systemd-resolved after a kill -9), treat + // the PID file as stale and proceed with startup. + if isPicoclawProcess(data.PID) { + return nil, fmt.Errorf("gateway is already running (PID: %d, version: %s)", data.PID, data.Version) + } + logger.Warnf("found pid file (PID: %d) but process is not picoclaw", data.PID) } logger.Warnf("not running (PID: %d) so will remove the pid file: %s", data.PID, pidPath) } diff --git a/pkg/pid/pidfile_unix.go b/pkg/pid/pidfile_unix.go index 7bc53b752..35a098ddf 100644 --- a/pkg/pid/pidfile_unix.go +++ b/pkg/pid/pidfile_unix.go @@ -4,7 +4,9 @@ package pid import ( "errors" + "fmt" "os" + "strings" "syscall" ) @@ -18,7 +20,7 @@ func isProcessRunning(pid int) bool { if err != nil { return false } - // Signal(nil) does not kill the process but checks existence on Unix. + // Signal(0) does not kill the process but checks existence on Unix. err = p.Signal(syscall.Signal(0)) if err == nil { return true @@ -27,3 +29,16 @@ func isProcessRunning(pid int) bool { // EPERM means the process exists but we are not allowed to signal it. return errors.As(err, &errno) && errno == syscall.EPERM } + +// isPicoclawProcess reads /proc//comm to confirm the process name +// contains "picoclaw". Returns false when the comm file can be read and +// the name does not match (e.g., PID was reused by an unrelated process). +// Returns true if /proc//comm is unreadable so the call site falls +// back to trusting the liveness check alone. +func isPicoclawProcess(pid int) bool { + data, err := os.ReadFile(fmt.Sprintf("/proc/%d/comm", pid)) + if err != nil { + return true // cannot verify — trust liveness check + } + return strings.Contains(strings.TrimSpace(string(data)), "picoclaw") +} diff --git a/pkg/pid/pidfile_windows.go b/pkg/pid/pidfile_windows.go index 6d8b79552..5d03eafd8 100644 --- a/pkg/pid/pidfile_windows.go +++ b/pkg/pid/pidfile_windows.go @@ -3,6 +3,7 @@ package pid import ( + "strings" "syscall" "unsafe" ) @@ -12,6 +13,7 @@ var ( procOpenProcess = kernel32.NewProc("OpenProcess") procGetExitCodeProcess = kernel32.NewProc("GetExitCodeProcess") procCloseHandle = kernel32.NewProc("CloseHandle") + procQueryFullProcessImageNameW = kernel32.NewProc("QueryFullProcessImageNameW") processQueryLimitedInformation = uint32(0x1000) stillActive = uint32(259) ) @@ -40,3 +42,33 @@ func isProcessRunning(pid int) bool { } return exitCode == stillActive } + +// isPicoclawProcess uses QueryFullProcessImageNameW to confirm the +// process image name contains "picoclaw". Returns false when the name +// clearly does not match. Returns true if the query fails, falling +// back to trusting the liveness check alone. +func isPicoclawProcess(pid int) bool { + handle, _, _ := procOpenProcess.Call( + uintptr(processQueryLimitedInformation), + 0, + uintptr(pid), + ) + if handle == 0 { + return true // cannot open — trust liveness check + } + defer procCloseHandle.Call(handle) + + var buf [260]uint16 + var size uint32 = 260 + ret, _, _ := procQueryFullProcessImageNameW.Call( + uintptr(handle), + 0, // WIN32_NAME_FORMAT + uintptr(unsafe.Pointer(&buf[0])), + uintptr(unsafe.Pointer(&size)), + ) + if ret == 0 { + return true // cannot verify — trust liveness check + } + name := strings.ToLower(syscall.UTF16ToString(buf[:size])) + return strings.Contains(name, "picoclaw") +}