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()