"""Windows Service wrapper around run_agent_service.run() — reuses it as-is, does not reimplement any polling/pipeline logic. Requires pywin32 (`pip install pywin32`), Windows-only. Commands (run from an elevated/administrator terminal): python gti_agent_windows_service.py install # register the service, Startup=Automatic python gti_agent_windows_service.py start python gti_agent_windows_service.py stop python gti_agent_windows_service.py restart python gti_agent_windows_service.py remove # uninstall python gti_agent_windows_service.py --startup=auto --username . --password . install Recovery (restart on failure) is not set by pywin32's `install` alone; run once after installing: sc.exe failure "GTIFiscalAgent" reset= 86400 actions= restart/60000/restart/60000/restart/60000 This module only wraps agent/scripts/run_agent_service.py's `run()` — the actual polling/heartbeat/pipeline logic lives there, unchanged. """ import sys import threading from pathlib import Path AGENT_ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(AGENT_ROOT / "scripts")) sys.path.insert(0, str(AGENT_ROOT / "src")) import run_agent_service as agent_service # noqa: E402 try: import servicemanager import win32event import win32service import win32serviceutil except ImportError: # pragma: no cover — only available on Windows with pywin32 installed servicemanager = win32event = win32service = win32serviceutil = None class _ServiceStopFlag: """Plain stop flag (no signal.signal registration) — safe to flip from any thread. The Windows Service Control Manager stops the service, not a console SIGINT/SIGTERM, so this replaces GracefulStop for this host.""" def __init__(self): self.stop = False if win32serviceutil is not None: class GTIFiscalAgentService(win32serviceutil.ServiceFramework): _svc_name_ = "GTIFiscalAgent" _svc_display_name_ = "GTI Fiscal Agent" _svc_description_ = "Reads the local Firebird ERP, polls the GTI Fiscal Hub for jobs, and applies guarded fiscal updates." def __init__(self, args): win32serviceutil.ServiceFramework.__init__(self, args) self.stop_event = win32event.CreateEvent(None, 0, 0, None) self.stop_flag = _ServiceStopFlag() self._thread = None def SvcStop(self): self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING) self.stop_flag.stop = True win32event.SetEvent(self.stop_event) def SvcDoRun(self): servicemanager.LogMsg(servicemanager.EVENTLOG_INFORMATION_TYPE, servicemanager.PYS_SERVICE_STARTED, (self._svc_name_, "")) self._thread = threading.Thread(target=self._run_agent, daemon=True) self._thread.start() win32event.WaitForSingleObject(self.stop_event, win32event.INFINITE) def _run_agent(self): import os agent_service.load_dotenv(Path(os.environ.get("GTI_AGENT_ENV_FILE") or agent_service.PROJECT_ROOT / ".env")) config = agent_service.Config(os.environ) agent_service.setup_logging(config.log_path) try: agent_service.enforce_startup_preflight(os.environ) # fail closed: a failing preflight stops the service except SystemExit: self.stop_flag.stop = True servicemanager.LogErrorMsg("GTIFiscalAgent: startup preflight failed; see the agent log") win32event.SetEvent(self.stop_event) return agent_service.run(config, os.environ, stopper=self.stop_flag) def main(): if win32serviceutil is None: raise SystemExit("pywin32 is required: pip install pywin32") if len(sys.argv) == 1: # SCM launches a service executable without CLI arguments. HandleCommandLine # requires a command (install/start/debug) and exits with usage when none # is present, which makes SCM report error 1053 for a frozen service host. servicemanager.Initialize() servicemanager.PrepareToHostSingle(GTIFiscalAgentService) servicemanager.StartServiceCtrlDispatcher() else: win32serviceutil.HandleCommandLine(GTIFiscalAgentService) if __name__ == "__main__": main()