From def9195802a357cc1f8d0c3f4e18ee7a64be10c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Flor=C3=AAncio?= Date: Sun, 8 Feb 2026 21:20:16 +0000 Subject: [PATCH] feat: preventing process kill on port use --- .../agentctl/client/launcher/launcher.go | 34 +------ .../agentctl/client/launcher/platform_unix.go | 94 ------------------- .../client/launcher/platform_windows.go | 69 -------------- 3 files changed, 4 insertions(+), 193 deletions(-) diff --git a/apps/backend/internal/agentctl/client/launcher/launcher.go b/apps/backend/internal/agentctl/client/launcher/launcher.go index 1f977ea536..53d7aafc6e 100644 --- a/apps/backend/internal/agentctl/client/launcher/launcher.go +++ b/apps/backend/internal/agentctl/client/launcher/launcher.go @@ -243,9 +243,8 @@ func findFreePort() (int, error) { return port, nil } -// ensurePortAvailable checks if the configured port is free. If not, it tries -// to diagnose and clean up stale processes. If the port is still unavailable, -// it falls back to an OS-assigned free port. +// ensurePortAvailable checks if the configured port is free. If not, it +// immediately falls back to an OS-assigned free port. func (l *Launcher) ensurePortAvailable() error { if err := checkPortAvailable(l.port); err == nil { return nil @@ -253,39 +252,14 @@ func (l *Launcher) ensurePortAvailable() error { originalPort := l.port - // Port is in use — try to find and kill whatever is holding it - l.logger.Warn("port in use, attempting to find and kill stale process", + l.logger.Info("port already in use, selecting a free port", zap.Int("port", l.port)) - l.diagnosePID() - - if killed := l.tryKillPortHolder(); killed { - // Wait briefly for the OS to release the port - time.Sleep(500 * time.Millisecond) - if err := checkPortAvailable(l.port); err == nil { - l.logger.Info("port freed after killing stale process", zap.Int("port", l.port)) - return nil - } - } - - // Brief wait for TIME_WAIT to expire (up to 3s) - l.logger.Warn("could not free port immediately, waiting for OS to release it", - zap.Int("port", l.port)) - deadline := time.Now().Add(3 * time.Second) - for time.Now().Before(deadline) { - time.Sleep(500 * time.Millisecond) - if err := checkPortAvailable(l.port); err == nil { - l.logger.Info("port became available", zap.Int("port", l.port)) - return nil - } - } - - // Fall back to OS-assigned free port freePort, err := findFreePort() if err != nil { return fmt.Errorf("port %d is in use and failed to find alternative: %w", originalPort, err) } - l.logger.Warn("configured port unavailable, using alternative", + l.logger.Info("using alternative port", zap.Int("original_port", originalPort), zap.Int("new_port", freePort)) l.port = freePort diff --git a/apps/backend/internal/agentctl/client/launcher/platform_unix.go b/apps/backend/internal/agentctl/client/launcher/platform_unix.go index c25a18ea66..6df28611ab 100644 --- a/apps/backend/internal/agentctl/client/launcher/platform_unix.go +++ b/apps/backend/internal/agentctl/client/launcher/platform_unix.go @@ -3,11 +3,6 @@ package launcher import ( - "fmt" - "net" - "os" - "os/exec" - "strings" "syscall" "go.uber.org/zap" @@ -28,92 +23,3 @@ func (l *Launcher) gracefulStop(pid int) error { func (l *Launcher) forceKill(pid int) { _ = syscall.Kill(pid, syscall.SIGKILL) } - -// diagnosePID logs detailed info about what is holding the port (best-effort, non-fatal). -func (l *Launcher) diagnosePID() { - portStr := fmt.Sprintf(":%d", l.port) - - // Try lsof first - if out, err := exec.Command("lsof", "-i", portStr, "-P", "-n").CombinedOutput(); err == nil { - output := strings.TrimSpace(string(out)) - if output != "" { - l.logger.Warn("lsof: process(es) holding port", - zap.Int("port", l.port), - zap.String("lsof_output", output)) - return - } - } - - // lsof found nothing — check netstat for TIME_WAIT / CLOSE_WAIT sockets - if out, err := exec.Command("netstat", "-an").CombinedOutput(); err == nil { - var matches []string - search := fmt.Sprintf(".%d ", l.port) // macOS netstat uses dot separator - search2 := fmt.Sprintf(":%d ", l.port) // Linux netstat uses colon - for _, line := range strings.Split(string(out), "\n") { - if strings.Contains(line, search) || strings.Contains(line, search2) { - matches = append(matches, strings.TrimSpace(line)) - } - } - if len(matches) > 0 { - l.logger.Warn("netstat: socket(s) on port (likely TIME_WAIT)", - zap.Int("port", l.port), - zap.String("netstat_output", strings.Join(matches, "\n"))) - return - } - } - - // Test IPv4 vs IPv6 separately to identify which is blocked - v4Err := tryBind("tcp4", portStr) - v6Err := tryBind("tcp6", portStr) - l.logger.Warn("port in use but no process found — checking address families", - zap.Int("port", l.port), - zap.NamedError("ipv4_bind_err", v4Err), - zap.NamedError("ipv6_bind_err", v6Err)) -} - -// tryBind attempts to bind to the given network/address and returns any error. -func tryBind(network, addr string) error { - ln, err := net.Listen(network, addr) - if err != nil { - return err - } - _ = ln.Close() - return nil -} - -// tryKillPortHolder tries to find and kill the process holding the port. -// Returns true if it found and signaled a process. -func (l *Launcher) tryKillPortHolder() bool { - out, err := exec.Command("lsof", "-ti", fmt.Sprintf(":%d", l.port)).CombinedOutput() - if err != nil || strings.TrimSpace(string(out)) == "" { - return false - } - - killed := false - for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") { - line = strings.TrimSpace(line) - if line == "" { - continue - } - pid := 0 - if _, err := fmt.Sscanf(line, "%d", &pid); err != nil || pid <= 0 { - continue - } - if pid == os.Getpid() { - continue - } - l.logger.Warn("killing stale process on port", - zap.Int("port", l.port), - zap.Int("pid", pid)) - if proc, err := os.FindProcess(pid); err == nil { - if err := proc.Signal(syscall.SIGTERM); err != nil { - l.logger.Debug("SIGTERM failed, trying SIGKILL", - zap.Int("pid", pid), - zap.Error(err)) - _ = proc.Kill() - } - killed = true - } - } - return killed -} diff --git a/apps/backend/internal/agentctl/client/launcher/platform_windows.go b/apps/backend/internal/agentctl/client/launcher/platform_windows.go index e14fdc62f3..0d5ec0b4e3 100644 --- a/apps/backend/internal/agentctl/client/launcher/platform_windows.go +++ b/apps/backend/internal/agentctl/client/launcher/platform_windows.go @@ -3,10 +3,7 @@ package launcher import ( - "fmt" "os" - "os/exec" - "strings" "go.uber.org/zap" ) @@ -33,69 +30,3 @@ func (l *Launcher) forceKill(pid int) { _ = proc.Kill() } } - -// diagnosePID uses netstat to log what is holding the port (best-effort, non-fatal). -func (l *Launcher) diagnosePID() { - out, err := exec.Command("netstat", "-ano").CombinedOutput() - if err != nil { - l.logger.Debug("netstat failed", zap.Error(err)) - return - } - - portStr := fmt.Sprintf(":%d ", l.port) - var matches []string - for _, line := range strings.Split(string(out), "\n") { - if strings.Contains(line, portStr) { - matches = append(matches, strings.TrimSpace(line)) - } - } - - if len(matches) == 0 { - l.logger.Warn("port in use but netstat found no process — likely TIME_WAIT or kernel hold", - zap.Int("port", l.port)) - } else { - l.logger.Warn("process(es) holding port", - zap.Int("port", l.port), - zap.String("netstat_output", strings.Join(matches, "\n"))) - } -} - -// tryKillPortHolder tries to find and kill the process holding the port using netstat + taskkill. -// Returns true if it found and signaled a process. -func (l *Launcher) tryKillPortHolder() bool { - out, err := exec.Command("netstat", "-ano").CombinedOutput() - if err != nil { - return false - } - - portStr := fmt.Sprintf(":%d ", l.port) - selfPid := os.Getpid() - killed := false - - for _, line := range strings.Split(string(out), "\n") { - if !strings.Contains(line, portStr) || !strings.Contains(line, "LISTENING") { - continue - } - fields := strings.Fields(strings.TrimSpace(line)) - if len(fields) < 5 { - continue - } - pid := 0 - if _, err := fmt.Sscanf(fields[len(fields)-1], "%d", &pid); err != nil || pid <= 0 { - continue - } - if pid == selfPid { - continue - } - l.logger.Warn("killing stale process on port", - zap.Int("port", l.port), - zap.Int("pid", pid)) - // taskkill /F /PID - if killErr := exec.Command("taskkill", "/F", "/PID", fmt.Sprintf("%d", pid)).Run(); killErr != nil { - l.logger.Debug("taskkill failed", zap.Int("pid", pid), zap.Error(killErr)) - } else { - killed = true - } - } - return killed -}