feat: preventing process kill on port use
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 <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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user