Enforce single instance Local Agent supervisor worker and dashboard
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import ctypes
|
||||
from ctypes import wintypes
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
@@ -25,6 +28,60 @@ DASHBOARD_URL = "http://127.0.0.1:8788"
|
||||
WORKER_MODULE = "erp_local_agent.main"
|
||||
RESTART_DELAY_SECONDS = 3
|
||||
HEALTH_TIMEOUT_SECONDS = 60
|
||||
ERROR_ALREADY_EXISTS = 183
|
||||
|
||||
|
||||
def _supervisor_mutex_name() -> str:
|
||||
normalized = str(INSTALL_ROOT.resolve()).casefold().encode("utf-8", errors="ignore")
|
||||
suffix = hashlib.sha256(normalized).hexdigest()[:20]
|
||||
return f"Local\\ARRR_ERP_Local_Agent_SUPERVISOR_{suffix}"
|
||||
|
||||
|
||||
def _acquire_os_mutex():
|
||||
if os.name != "nt":
|
||||
return object()
|
||||
kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
|
||||
kernel32.CreateMutexW.argtypes = (ctypes.c_void_p, wintypes.BOOL, wintypes.LPCWSTR)
|
||||
kernel32.CreateMutexW.restype = wintypes.HANDLE
|
||||
ctypes.set_last_error(0)
|
||||
handle = kernel32.CreateMutexW(None, False, _supervisor_mutex_name())
|
||||
if not handle:
|
||||
raise ctypes.WinError(ctypes.get_last_error())
|
||||
if ctypes.get_last_error() == ERROR_ALREADY_EXISTS:
|
||||
kernel32.CloseHandle(handle)
|
||||
return None
|
||||
return handle
|
||||
|
||||
|
||||
def _close_os_mutex(handle) -> None:
|
||||
if os.name != "nt" or handle is None or not isinstance(handle, int):
|
||||
return
|
||||
try:
|
||||
ctypes.WinDLL("kernel32", use_last_error=True).CloseHandle(handle)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _kill_duplicate_supervisors() -> None:
|
||||
if os.name != "nt":
|
||||
return
|
||||
root = str(INSTALL_ROOT).replace("'", "''")
|
||||
script = (
|
||||
"$root='" + root + "';$self=" + str(os.getpid()) + ";"
|
||||
"Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | "
|
||||
"Where-Object { $_.ProcessId -ne $self -and $_.CommandLine -and "
|
||||
"$_.CommandLine -match 'ERPAgentSupervisor\\.pyw' -and "
|
||||
"$_.CommandLine -like ('*'+$root+'*') } | "
|
||||
"ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }"
|
||||
)
|
||||
subprocess.run(
|
||||
["powershell.exe", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script],
|
||||
cwd=str(INSTALL_ROOT),
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
creationflags=_create_no_window(),
|
||||
check=False,
|
||||
)
|
||||
|
||||
|
||||
def _create_no_window() -> int:
|
||||
@@ -135,8 +192,14 @@ class Supervisor:
|
||||
def __init__(self):
|
||||
self.worker: subprocess.Popen | None = None
|
||||
self.running = True
|
||||
self._os_mutex = None
|
||||
|
||||
def acquire_lock(self) -> bool:
|
||||
# The named mutex is atomic at the Windows kernel level and prevents
|
||||
# the lock-file race that allowed multiple supervisors before 1.22.3.
|
||||
self._os_mutex = _acquire_os_mutex()
|
||||
if self._os_mutex is None:
|
||||
return False
|
||||
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
||||
if LOCK_FILE.exists():
|
||||
try:
|
||||
@@ -154,6 +217,8 @@ class Supervisor:
|
||||
LOCK_FILE.unlink()
|
||||
except Exception:
|
||||
pass
|
||||
_close_os_mutex(self._os_mutex)
|
||||
self._os_mutex = None
|
||||
|
||||
def start_worker(self) -> None:
|
||||
if self.worker is not None and self.worker.poll() is None:
|
||||
@@ -368,6 +433,7 @@ class Supervisor:
|
||||
return 0
|
||||
|
||||
try:
|
||||
_kill_duplicate_supervisors()
|
||||
_state("starting", "ERP Local Agent supervisor starting.")
|
||||
self.start_worker()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user