Windows agent: run as background Windows Service (Win 8.1+)

Replace the scheduled-task approach (required user to stay logged in) with a
proper Windows Service using pywin32. The service runs as LocalSystem, starts
at boot, and auto-restarts on failure — no PowerShell window needed.

Agent changes (jarvis-agent-windows.py):
- Add Windows Service class via pywin32 (JarvisAgentService)
- Cleanly handles SvcStop by setting a threading.Event
- main() loop uses _stop_event.wait() instead of time.sleep() so stop is immediate
- self_update() signals the stop event when running as a service (SCM restarts it)
- __main__ block dispatches to SCM entry point or HandleCommandLine (install/stop/remove)
- Falls back to direct run if pywin32 not installed (for debugging)

Installer changes (install-windows.ps1):
- pip install pywin32 + postinstall (registers service runner DLLs)
- Python search prefers system-wide install (accessible by LocalSystem)
- Downloads Python 3.11 directly from python.org for Win 8.1 machines without winget
- Removes legacy JARVIS-Agent scheduled task if present
- Registers JARVISAgent service with --startup auto
- Configures sc.exe failure recovery (restart at 5s/10s/30s)
- Updated management commands in summary (Start-Service, Stop-Service, etc.)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-12 12:11:38 +00:00
parent 04bd412b65
commit b19ce85d84
4 changed files with 371 additions and 119 deletions
+80 -7
View File
@@ -1,7 +1,18 @@
#!/usr/bin/env python3
"""
JARVIS Agent for Windows — system monitor that reports metrics to JARVIS HUD.
Install via PowerShell (as Admin): irm https://jarvis.orbishosting.com/agent/install-windows.ps1 | iex
Runs as a Windows Service (Win 8.1+) via pywin32.
Install (run PowerShell as Admin):
irm https://jarvis.orbishosting.com/agent/install-windows.ps1 | iex
Service management:
python jarvis-agent-windows.py --startup auto install # register service
python jarvis-agent-windows.py start # start
python jarvis-agent-windows.py stop # stop
python jarvis-agent-windows.py remove # uninstall
python jarvis-agent-windows.py debug # run in console (for testing)
Config: C:\ProgramData\jarvis-agent\config.json
Logs: C:\ProgramData\jarvis-agent\jarvis-agent.log
"""
@@ -12,6 +23,7 @@ import platform
import socket
import subprocess
import sys
import threading
import time
import urllib.request
import urllib.error
@@ -23,7 +35,11 @@ INSTALL_DIR = Path(r"C:\ProgramData\jarvis-agent")
CONFIG_PATH = INSTALL_DIR / "config.json"
STATE_PATH = INSTALL_DIR / "state.json"
LOG_PATH = INSTALL_DIR / "jarvis-agent.log"
AGENT_VERSION = "3.0"
AGENT_VERSION = "3.1"
# Set by the service wrapper so self_update knows to stop instead of exec
_is_service = False
_stop_event = threading.Event()
# ── Logging ────────────────────────────────────────────────────────────────────
@@ -403,7 +419,12 @@ def self_update(cfg: dict) -> bool:
log(f"Update verified — replacing {script_path} and restarting...")
with open(script_path, "wb") as f:
f.write(new_content)
os.execv(sys.executable, [sys.executable] + sys.argv)
if _is_service:
# Signal the main loop to exit; SCM failure-recovery will restart us
log("Running as service — stopping for SCM-managed restart after update.")
_stop_event.set()
else:
os.execv(sys.executable, [sys.executable] + sys.argv)
return True
return False
except Exception as e:
@@ -490,7 +511,8 @@ def main():
api_key = register(cfg, state)
if not api_key:
log("[ERROR] Could not register with JARVIS. Retrying in 60s...")
time.sleep(60)
if _stop_event.wait(60):
return
headers = {"X-Agent-Key": api_key}
last_metrics = 0
@@ -498,7 +520,7 @@ def main():
log(f"Agent v{AGENT_VERSION} (Windows) running. Polling {jarvis_url} every {heartbeat_every}s.")
while True:
while not _stop_event.is_set():
now = time.time()
try:
hb = api_post(f"{jarvis_url}/api/agent/heartbeat", {}, headers, ssl_verify=ssl_verify)
@@ -527,8 +549,59 @@ def main():
except Exception as e:
log(f"[ERROR] Loop error: {e}")
time.sleep(heartbeat_every)
if _stop_event.wait(heartbeat_every):
break # service stop was requested
log("Agent stopped.")
# ── Windows Service wrapper ────────────────────────────────────────────────────
try:
import win32serviceutil
import win32service
import win32event
import servicemanager
_HAS_WIN32 = True
except ImportError:
_HAS_WIN32 = False
if _HAS_WIN32:
class JarvisAgentService(win32serviceutil.ServiceFramework):
_svc_name_ = "JARVISAgent"
_svc_display_name_ = "JARVIS AI Agent"
_svc_description_ = "JARVIS system monitoring and AI agent — reports metrics to JARVIS HUD"
def __init__(self, args):
win32serviceutil.ServiceFramework.__init__(self, args)
self._svc_stop_event = win32event.CreateEvent(None, 0, 0, None)
def SvcStop(self):
self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
_stop_event.set()
win32event.SetEvent(self._svc_stop_event)
def SvcDoRun(self):
global _is_service
_is_service = True
servicemanager.LogMsg(
servicemanager.EVENTLOG_INFORMATION_TYPE,
servicemanager.PYS_SERVICE_STARTED,
(self._svc_name_, ""),
)
main()
if __name__ == "__main__":
main()
if _HAS_WIN32:
if len(sys.argv) == 1:
# No args — SCM is starting us as a service
servicemanager.Initialize()
servicemanager.PrepareToHostSingle(JarvisAgentService)
servicemanager.StartServiceCtrlDispatcher()
else:
# install / start / stop / remove / debug
win32serviceutil.HandleCommandLine(JarvisAgentService)
else:
# pywin32 not available — run directly (useful for testing)
main()