Files
gti d21a9d1140 Add 'fiscal/' from commit '102ba84500dbd9655a0471288c3e278652593ca9'
git-subtree-dir: fiscal
git-subtree-mainline: d95165fc8f
git-subtree-split: 102ba84500
2026-09-29 14:43:23 -03:00

110 lines
4.7 KiB
Python

"""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
try:
agent_service.run(config, os.environ, stopper=self.stop_flag)
except BaseException as error: # noqa: BLE001 - a dead loop must never look like a healthy "Running" service
import logging
logging.getLogger("gti_fiscal_agent").exception("fatal error in the Agent loop; stopping the service")
servicemanager.LogErrorMsg(f"GTIFiscalAgent: fatal error, stopping: {type(error).__name__}: {error}")
self.stop_flag.stop = True
win32event.SetEvent(self.stop_event)
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()