nsfwapp/backend/ai_server_process_other.go
2026-06-25 14:54:58 +02:00

189 lines
4.0 KiB
Go

//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"))
}