Files
arrr-erp/app/modules/documents/local_agent_runtime/ERPAgentDashboard.pyw
T
2026-09-04 09:39:07 +05:30

264 lines
7.7 KiB
Python

from __future__ import annotations
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
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
def _dashboard_mutex_name() -> str:
normalized = str(INSTALL_ROOT.resolve()).casefold().encode("utf-8", errors="ignore")
suffix = hashlib.sha256(normalized).hexdigest()[:20]
return f"Global\\ARRR_ERP_Local_Agent_DASHBOARD_{suffix}"
def _acquire_dashboard_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, _dashboard_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_dashboard_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 _focus_existing_dashboard() -> None:
if os.name != "nt":
return
try:
user32 = ctypes.WinDLL("user32", use_last_error=True)
EnumWindowsProc = ctypes.WINFUNCTYPE(wintypes.BOOL, wintypes.HWND, wintypes.LPARAM)
@EnumWindowsProc
def callback(hwnd, _lparam):
length = user32.GetWindowTextLengthW(hwnd)
if length <= 0:
return True
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.SetForegroundWindow(hwnd)
return False
return True
user32.EnumWindows(callback, 0)
except Exception:
pass
def _create_no_window() -> int:
return getattr(subprocess, "CREATE_NO_WINDOW", 0)
def _popup(title: str, message: str, error: bool = False) -> None:
root = Tk()
root.withdraw()
if error:
messagebox.showerror(title, message)
else:
messagebox.showinfo(title, message)
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():
return pyw
py = INSTALL_ROOT / ".venv" / "Scripts" / "python.exe"
if py.exists():
return py
raise RuntimeError("ERP Local Agent Python environment was not found.")
def _ensure_supervisor() -> None:
if _supervisor_running():
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"],
cwd=str(INSTALL_ROOT),
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
creationflags=_create_no_window(),
check=False,
)
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.
subprocess.Popen(
[str(_pythonw()), str(SUPERVISOR_SCRIPT), "--background"],
cwd=str(INSTALL_ROOT),
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
creationflags=_create_no_window(),
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:
deadline = time.time() + seconds
while time.time() < deadline:
if _ready():
return True
time.sleep(0.75)
return False
def main() -> None:
dashboard_mutex = _acquire_dashboard_mutex()
if dashboard_mutex is None:
_focus_existing_dashboard()
return
atexit.register(_close_dashboard_mutex, dashboard_mutex)
try:
_ensure_supervisor()
except Exception as exc:
_popup("ERP Local Agent", str(exc), error=True)
return
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'}",
error=True,
)
return
try:
import webview
except Exception as exc:
_popup(
"ERP Local Agent Dashboard",
"Desktop dashboard dependency could not be loaded.\n\n"
f"{exc}\n\nRun the Local Agent updater/installer again.",
error=True,
)
return
window = webview.create_window(
"ERP Local Agent Dashboard",
URL,
width=1180,
height=820,
min_size=(980, 650),
confirm_close=False,
)
def monitor():
was_ready = True
outage_seen = False
while True:
try:
current = _ready()
if was_ready and not current:
outage_seen = True
if outage_seen and current:
try:
window.load_url(URL)
except Exception:
pass
outage_seen = False
was_ready = current
time.sleep(1.5)
except Exception:
time.sleep(2)
webview.start(monitor)
if __name__ == "__main__":
main()