Files
2026-09-04 13:33:34 +05:30

222 lines
6.2 KiB
Python

from __future__ import annotations
import atexit
import ctypes
from ctypes import wintypes
import hashlib
import os
from pathlib import Path
import subprocess
import time
import urllib.request
from tkinter import Tk, messagebox
INSTALL_ROOT = Path(__file__).resolve().parent
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)
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 _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 _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 the dashboard is already healthy, never start another supervisor or worker.
if _ready():
return
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. The Supervisor Global mutex makes repeated launches harmless.
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,
)
def _wait_ready(seconds: int = 90) -> 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 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
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.0)
except Exception:
time.sleep(1.5)
webview.start(monitor)
if __name__ == "__main__":
main()