Repair Local Agent supervisor and add update progress UI

This commit is contained in:
A R R R Associates
2026-09-04 13:33:34 +05:30
parent 4deef57364
commit 7bc97be1b1
8 changed files with 282 additions and 343 deletions
@@ -1,14 +1,12 @@
from __future__ import annotations
import atexit
import ctypes
from ctypes import wintypes
import hashlib
import os
import atexit
from pathlib import Path
import subprocess
import sys
import threading
import time
import urllib.request
@@ -16,8 +14,6 @@ from tkinter import Tk, messagebox
INSTALL_ROOT = Path(__file__).resolve().parent
DATA_DIR = INSTALL_ROOT / "data"
SUPERVISOR_LOCK = DATA_DIR / "supervisor.lock"
SUPERVISOR_SCRIPT = INSTALL_ROOT / "ERPAgentSupervisor.pyw"
URL = "http://127.0.0.1:8788"
ERROR_ALREADY_EXISTS = 183
@@ -69,7 +65,7 @@ def _focus_existing_dashboard() -> None:
buf = ctypes.create_unicode_buffer(length + 1)
user32.GetWindowTextW(hwnd, buf, length + 1)
if "ERP Local Agent Dashboard" in buf.value:
user32.ShowWindow(hwnd, 9) # SW_RESTORE
user32.ShowWindow(hwnd, 9)
user32.SetForegroundWindow(hwnd)
return False
return True
@@ -93,42 +89,6 @@ def _popup(title: str, message: str, error: bool = False) -> None:
root.destroy()
def _pid_alive(pid: int) -> bool:
if pid <= 0:
return False
if os.name == "nt":
try:
import ctypes
PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
STILL_ACTIVE = 259
kernel32 = ctypes.windll.kernel32
handle = kernel32.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, False, int(pid))
if not handle:
return False
try:
exit_code = ctypes.c_ulong()
if not kernel32.GetExitCodeProcess(handle, ctypes.byref(exit_code)):
return False
return exit_code.value == STILL_ACTIVE
finally:
kernel32.CloseHandle(handle)
except Exception:
return False
try:
os.kill(pid, 0)
return True
except OSError:
return False
def _supervisor_running() -> bool:
try:
if not SUPERVISOR_LOCK.exists():
return False
return _pid_alive(int(SUPERVISOR_LOCK.read_text(encoding="utf-8").strip()))
except Exception:
return False
def _pythonw() -> Path:
pyw = INSTALL_ROOT / ".venv" / "Scripts" / "pythonw.exe"
if pyw.exists():
@@ -139,18 +99,30 @@ def _pythonw() -> Path:
raise RuntimeError("ERP Local Agent Python environment was not found.")
def _ready(timeout: float = 1.5) -> bool:
try:
with urllib.request.urlopen(URL + "/api/status", timeout=timeout) as response:
return int(getattr(response, "status", 200)) < 500
except Exception:
return False
def _ensure_supervisor() -> None:
if _supervisor_running():
# If the dashboard is already healthy, never start another supervisor or worker.
if _ready():
return
# The Scheduled Task is the canonical owner/launcher. Starting it repeatedly
# is safe because the task is configured with MultipleInstances=IgnoreNew,
# and the supervisor itself also owns a machine-wide Global mutex.
started_by_task = False
if os.name == "nt":
result = subprocess.run(
["powershell.exe", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command",
"Start-ScheduledTask -TaskName 'ERP Local Agent' -ErrorAction Stop"],
[
"powershell.exe",
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-Command",
"Start-ScheduledTask -TaskName 'ERP Local Agent' -ErrorAction Stop",
],
cwd=str(INSTALL_ROOT),
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
@@ -160,8 +132,7 @@ def _ensure_supervisor() -> None:
started_by_task = result.returncode == 0
if not started_by_task:
# Recovery fallback for a workstation where the scheduled task was not
# installed yet. The Global mutex still prevents a second supervisor.
# Recovery fallback. The Supervisor Global mutex makes repeated launches harmless.
subprocess.Popen(
[str(_pythonw()), str(SUPERVISOR_SCRIPT), "--background"],
cwd=str(INSTALL_ROOT),
@@ -171,23 +142,8 @@ def _ensure_supervisor() -> None:
close_fds=True,
)
deadline = time.time() + 20
while time.time() < deadline:
if _supervisor_running():
return
time.sleep(0.5)
raise RuntimeError("ERP Local Agent supervisor did not start.")
def _ready(timeout: float = 1.5) -> bool:
try:
with urllib.request.urlopen(URL + "/api/status", timeout=timeout) as response:
return int(getattr(response, "status", 200)) < 500
except Exception:
return False
def _wait_ready(seconds: int = 75) -> bool:
def _wait_ready(seconds: int = 90) -> bool:
deadline = time.time() + seconds
while time.time() < deadline:
if _ready():
@@ -202,6 +158,7 @@ def main() -> None:
_focus_existing_dashboard()
return
atexit.register(_close_dashboard_mutex, dashboard_mutex)
try:
_ensure_supervisor()
except Exception as exc:
@@ -211,8 +168,9 @@ def main() -> None:
if not _wait_ready():
_popup(
"ERP Local Agent",
"The Local Agent supervisor is running, but the dashboard did not become ready.\n\n"
f"Check logs under:\n{INSTALL_ROOT / 'logs'}",
"The Local Agent did not become ready.\n\n"
"Check the 'ERP Local Agent' Scheduled Task and the logs under:\n"
f"{INSTALL_ROOT / 'logs'}",
error=True,
)
return
@@ -252,9 +210,9 @@ def main() -> None:
pass
outage_seen = False
was_ready = current
time.sleep(1.5)
time.sleep(1.0)
except Exception:
time.sleep(2)
time.sleep(1.5)
webview.start(monitor)