"""Setup.exe — universal Windows installer for the GTI Fiscal Agent. Two entry modes: - GUI (default): tkinter wizard with a single install-code field. - CLI (`--cli --code XXXX [--hub URL] [--install-dir PATH] [--no-service]`): scripted install used by the automated E2E and the build smoke test. Run elevated (PyInstaller --uac-admin). Enrollment is resolved by the Hub from the install-code hash alone — no enrollment token, no bundle. Flow on success: 1. POST /v1/agents/enroll {install_code, hostname, agent_version, machine_fingerprint} 2. write agent.env (non-secret only) + credentials.bin (DPAPI LocalMachine + ACL) 3. extract the bundled Service host (onedir) + Uninstall.exe + write executor into the install directory 4. register service GTIFiscalAgent (Delayed Auto) + failure recovery 5. Firebird not configured -> report "serviço instalado; configure o arquivo de banco para ativar" and do NOT start the service (no error loop) 6. write ARP uninstall key 7. best-effort WINDOWS_INSTALL_COMPLETED audit The install code is never persisted; agent.env never holds the token; no UI or flag enables Firebird/AI writes (always dry-run, writes disabled). """ from __future__ import annotations import argparse import os import shutil import sys import threading import tkinter as tk from pathlib import Path from tkinter import messagebox, ttk import httpx import install_common as common MEIPASS = Path(getattr(sys, "_MEIPASS", Path(__file__).resolve().parent)) def install_flow(code: str, *, hub: str | None = None, install_dir_path: str | None = None, register_service: bool = True) -> dict: """Perform the full install. Returns the enroll result dict; raises RuntimeError with a user-facing message on any failure.""" if install_dir_path: os.environ["GTI_INSTALL_DIR"] = install_dir_path install = common.install_dir() try: result = common.enroll(code, hub=hub) except httpx.HTTPStatusError as error: detail = "" try: detail = error.response.json().get("detail", "") except Exception: # noqa: BLE001 detail = "" reason = detail or error.response.reason_phrase or f"HTTP {error.response.status_code}" raise RuntimeError(f"enrollment rejeitado: {reason}") from error except Exception as error: # noqa: BLE001 raise RuntimeError(f"falha no enrollment: {error}") from error agent_version = str(result.get("agent_version") or os.environ.get("AGENT_VERSION") or "") os.environ["AGENT_VERSION"] = agent_version common.write_agent_env(install, result) common.write_credential(install, result["agent_token"]) extract_payload(install) if register_service: common.install_service(install) common.write_arp_uninstall(install) if common.service_running(): common.start_service() try: common.audit_event(result["agent_token"], result.get("hub_url") or hub, "WINDOWS_INSTALL_COMPLETED", f"agent {result.get('agent_id')} installed") except Exception: # noqa: BLE001 — best effort pass result["install_dir"] = str(install) result["agent_version"] = agent_version return result def extract_payload(install: Path) -> None: """Copy the bundled service host onedir, Uninstall.exe and the write executor out of the onefile extraction dir into the install directory.""" install.mkdir(parents=True, exist_ok=True) service_dir = MEIPASS / "GTIFiscalAgentService" targets = [ (service_dir, install / "GTIFiscalAgentService"), (MEIPASS / "Uninstall.exe", install / "Uninstall.exe"), (MEIPASS / "scripts" / "firebird_write_executor.py", install / "scripts" / "firebird_write_executor.py"), ] for source, dest in targets: if not source.exists(): raise RuntimeError(f"bundled payload missing: {source}") if source.is_dir(): shutil.copytree(source, dest, dirs_exist_ok=True) else: dest.parent.mkdir(parents=True, exist_ok=True) shutil.copy2(source, dest) class SetupWizard: def __init__(self, root: tk.Tk): self.root = root root.title("GTI Fiscal Agent") root.resizable(False, False) root.geometry("720x250") frame = ttk.Frame(root, padding=20) frame.pack(fill="both", expand=True) ttk.Label(frame, text="GTI Fiscal Agent — Instalação", font=("Segoe UI", 13, "bold")).pack(anchor="w") ttk.Label(frame, text="Digite o código de instalação gerado no painel GTI Fiscal.").pack(anchor="w", pady=(4, 2)) ttk.Label(frame, text=f"Hub: {common.hub_url()}", foreground="#555").pack(anchor="w") self.code_var = tk.StringVar() ttk.Entry(frame, textvariable=self.code_var, width=26, font=("Consolas", 14), justify="center").pack(anchor="w", pady=(14, 4)) self.status = ttk.Label(frame, text="", foreground="#333") self.status.pack(anchor="w", pady=(6, 0)) buttons = ttk.Frame(frame) buttons.pack(anchor="e", pady=(10, 0)) self.install_btn = ttk.Button(buttons, text="Instalar", command=self.on_install) self.install_btn.pack(side="left", padx=(0, 6)) ttk.Button(buttons, text="Fechar", command=root.destroy).pack(side="left") root.bind("", lambda _event: self.on_install()) def _set_busy(self, busy: bool, text: str = ""): self.status.config(text=text) self.install_btn.config(state="disabled" if busy else "normal") def on_install(self): code = self.code_var.get().strip() if not code: self.status.config(text="Informe o código de instalação.", foreground="#b00") return self._set_busy(True, "Enviando código ao hub…") threading.Thread(target=self._run, args=(code,), daemon=True).start() def _run(self, code: str): try: result = install_flow(code) except RuntimeError as error: self._set_busy(False) messagebox.showerror("GTI Fiscal Agent", str(error)) return self._set_busy(False) install = result["install_dir"] messagebox.showinfo( "GTI Fiscal Agent", "Serviço GTI Fiscal Agent instalado com sucesso.\n\n" "Firebird não está configurado — o serviço não foi iniciado.\n" f"Local: {install}\n" "Configure o arquivo de banco em agent.env para ativar.", ) self.root.destroy() def cli() -> int: parser = argparse.ArgumentParser(description="GTI Fiscal Agent Setup (headless)") parser.add_argument("--cli", action="store_true", help="run headless (no GUI)") parser.add_argument("--code", required=True, help="XXXX-XXXX-XXXX-XXXX install code") parser.add_argument("--hub", default=None, help="Hub base URL (dev override; default prod)") parser.add_argument("--install-dir", default=None, help="install directory (dev override)") parser.add_argument("--no-service", action="store_true", help="skip service registration + ARP (E2E scratch runs)") args = parser.parse_args() if args.hub: os.environ["GTI_HUB_URL"] = args.hub if args.install_dir: os.environ["GTI_INSTALL_DIR"] = args.install_dir result = install_flow(args.code, hub=args.hub, install_dir_path=args.install_dir, register_service=not args.no_service) print(f"INSTALL_OK agent_id={result.get('agent_id')} install_dir={result.get('install_dir')} hub={result.get('hub_url')}") return 0 def main(): if "--cli" in sys.argv: try: return cli() except RuntimeError as error: print(f"INSTALL_FAIL {error}", file=sys.stderr) return 1 root = tk.Tk() SetupWizard(root) root.mainloop() return 0 if __name__ == "__main__": sys.exit(main())