//go:build !windows package main import ( "os" "os/exec" "path/filepath" "runtime" "sort" "strconv" "strings" "syscall" "time" ) func prepareAIServerCommandForLifecycle(cmd *exec.Cmd) { if cmd == nil { return } if cmd.SysProcAttr == nil { cmd.SysProcAttr = &syscall.SysProcAttr{} } cmd.SysProcAttr.Setpgid = true } func terminateAIServerProcessTree(cmd *exec.Cmd) { if cmd == nil || cmd.Process == nil || cmd.Process.Pid <= 0 { return } pid := cmd.Process.Pid _ = syscall.Kill(-pid, syscall.SIGTERM) time.Sleep(1200 * time.Millisecond) _ = syscall.Kill(-pid, syscall.SIGKILL) _ = cmd.Process.Kill() } func stopStaleAIServerOnPort(port string) error { if runtime.GOOS != "linux" { return nil } portNumber, err := strconv.Atoi(strings.TrimSpace(port)) if err != nil || portNumber <= 0 || portNumber > 65535 { return nil } pids, err := linuxPIDsListeningOnPort(portNumber) if err != nil { return err } currentPID := os.Getpid() for _, pid := range pids { if pid <= 0 || pid == currentPID { continue } if !linuxPIDLooksLikeAIServer(pid) { appLogf("⚠️ Listener auf AI-Port %d übersprungen, Prozess sieht nicht nach AI Server aus: pid=%d", portNumber, pid) continue } appLogf("🧹 Alter AI Server auf Port %d wird beendet: pid=%d", portNumber, pid) terminateStaleUnixPID(pid) } if len(pids) > 0 { time.Sleep(500 * time.Millisecond) } return nil } func linuxPIDsListeningOnPort(port int) ([]int, error) { inodes := map[string]struct{}{} for _, path := range []string{"/proc/net/tcp", "/proc/net/tcp6"} { found, err := linuxSocketInodesListeningOnPort(path, port) if err != nil && !os.IsNotExist(err) { return nil, err } for inode := range found { inodes[inode] = struct{}{} } } if len(inodes) == 0 { return nil, nil } entries, err := os.ReadDir("/proc") if err != nil { return nil, err } pidSet := map[int]struct{}{} for _, entry := range entries { if !entry.IsDir() { continue } pid, err := strconv.Atoi(entry.Name()) if err != nil { continue } fdDir := filepath.Join("/proc", entry.Name(), "fd") fds, err := os.ReadDir(fdDir) if err != nil { continue } for _, fd := range fds { target, err := os.Readlink(filepath.Join(fdDir, fd.Name())) if err != nil { continue } if !strings.HasPrefix(target, "socket:[") || !strings.HasSuffix(target, "]") { continue } inode := strings.TrimSuffix(strings.TrimPrefix(target, "socket:["), "]") if _, ok := inodes[inode]; ok { pidSet[pid] = struct{}{} break } } } pids := make([]int, 0, len(pidSet)) for pid := range pidSet { pids = append(pids, pid) } sort.Ints(pids) return pids, nil } func linuxSocketInodesListeningOnPort(path string, port int) (map[string]struct{}, error) { data, err := os.ReadFile(path) if err != nil { return nil, err } inodes := map[string]struct{}{} lines := strings.Split(string(data), "\n") for _, line := range lines[1:] { fields := strings.Fields(line) if len(fields) < 10 || fields[3] != "0A" { continue } local := fields[1] idx := strings.LastIndex(local, ":") if idx < 0 || idx+1 >= len(local) { continue } localPort, err := strconv.ParseInt(local[idx+1:], 16, 32) if err != nil || int(localPort) != port { continue } inode := strings.TrimSpace(fields[9]) if inode != "" && inode != "0" { inodes[inode] = struct{}{} } } return inodes, nil } func terminateStaleUnixPID(pid int) { proc, err := os.FindProcess(pid) if err != nil || proc == nil { return } _ = proc.Signal(syscall.SIGTERM) time.Sleep(900 * time.Millisecond) _ = proc.Kill() } func linuxPIDLooksLikeAIServer(pid int) bool { data, err := os.ReadFile(filepath.Join("/proc", strconv.Itoa(pid), "cmdline")) if err != nil || len(data) == 0 { return false } cmdline := strings.ToLower(strings.ReplaceAll(string(data), "\x00", " ")) return strings.Contains(cmdline, "ai_server:app") || strings.Contains(cmdline, "ai_server.py") || (strings.Contains(cmdline, "uvicorn") && strings.Contains(cmdline, "ai_server")) }