feat: add daemon module with cross-platform process lifecycle
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
129
src/jianda_proxy/daemon.py
Normal file
129
src/jianda_proxy/daemon.py
Normal file
@@ -0,0 +1,129 @@
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import signal
|
||||
from jianda_proxy.config import get_pid_path
|
||||
|
||||
|
||||
def read_pid():
|
||||
path = get_pid_path()
|
||||
if not os.path.exists(path):
|
||||
return None
|
||||
try:
|
||||
with open(path) as f:
|
||||
return int(f.read().strip())
|
||||
except (ValueError, OSError):
|
||||
return None
|
||||
|
||||
|
||||
def write_pid(pid):
|
||||
path = get_pid_path()
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
with open(path, "w") as f:
|
||||
f.write(str(pid))
|
||||
|
||||
|
||||
def remove_pid():
|
||||
path = get_pid_path()
|
||||
if os.path.exists(path):
|
||||
os.remove(path)
|
||||
|
||||
|
||||
def is_process_alive(pid):
|
||||
if os.name == "nt":
|
||||
import ctypes
|
||||
import ctypes.wintypes
|
||||
|
||||
kernel32 = ctypes.windll.kernel32
|
||||
PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
|
||||
STILL_ACTIVE = 259
|
||||
handle = kernel32.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, False, pid)
|
||||
if not handle:
|
||||
return False
|
||||
try:
|
||||
exit_code = ctypes.wintypes.DWORD()
|
||||
kernel32.GetExitCodeProcess(handle, ctypes.byref(exit_code))
|
||||
return exit_code.value == STILL_ACTIVE
|
||||
finally:
|
||||
kernel32.CloseHandle(handle)
|
||||
else:
|
||||
try:
|
||||
os.kill(pid, 0)
|
||||
return True
|
||||
except ProcessLookupError:
|
||||
return False
|
||||
except PermissionError:
|
||||
return True
|
||||
|
||||
|
||||
def is_running():
|
||||
pid = read_pid()
|
||||
if pid is None:
|
||||
return False
|
||||
return is_process_alive(pid)
|
||||
|
||||
|
||||
def stop_daemon():
|
||||
pid = read_pid()
|
||||
if pid is None:
|
||||
return "not_running"
|
||||
|
||||
if not is_process_alive(pid):
|
||||
remove_pid()
|
||||
return "not_running"
|
||||
|
||||
if os.name == "nt":
|
||||
os.system(f"taskkill /PID {pid} /T")
|
||||
else:
|
||||
try:
|
||||
os.kill(pid, signal.SIGTERM)
|
||||
except ProcessLookupError:
|
||||
remove_pid()
|
||||
return "stopped"
|
||||
|
||||
for _ in range(6):
|
||||
time.sleep(0.5)
|
||||
if not is_process_alive(pid):
|
||||
remove_pid()
|
||||
return "stopped"
|
||||
|
||||
if os.name == "nt":
|
||||
os.system(f"taskkill /F /PID {pid} /T")
|
||||
else:
|
||||
try:
|
||||
os.kill(pid, signal.SIGKILL)
|
||||
except ProcessLookupError:
|
||||
pass
|
||||
|
||||
remove_pid()
|
||||
return "stopped"
|
||||
|
||||
|
||||
def get_status():
|
||||
pid = read_pid()
|
||||
if pid is None or not is_process_alive(pid):
|
||||
return {"running": False, "pid": None}
|
||||
return {"running": True, "pid": pid}
|
||||
|
||||
|
||||
def daemonize():
|
||||
if os.name == "nt":
|
||||
import subprocess
|
||||
|
||||
creation_flags = subprocess.CREATE_NEW_PROCESS_GROUP | subprocess.DETACHED_PROCESS
|
||||
subprocess.Popen(
|
||||
[sys.executable] + sys.argv + ["--_daemon_child"],
|
||||
creationflags=creation_flags,
|
||||
close_fds=True,
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
else:
|
||||
pid = os.fork()
|
||||
if pid > 0:
|
||||
os._exit(0)
|
||||
os.setsid()
|
||||
pid = os.fork()
|
||||
if pid > 0:
|
||||
os._exit(0)
|
||||
Reference in New Issue
Block a user