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()
+210 -104
View File
@@ -1,8 +1,12 @@
# JARVIS Agent Installer — Windows (PowerShell)
# Registers the agent as a proper Windows Service (Win 8.1+, no open window required).
# Requires pywin32. Runs the service as LocalSystem.
#
# Run as Administrator:
# Set-ExecutionPolicy Bypass -Scope Process
# .\install-windows.ps1 -JarvisUrl https://jarvis.orbishosting.com -Key YOUR_KEY
# Or one-liner (from PowerShell as Admin):
#
# One-liner (PowerShell as Admin):
# irm https://jarvis.orbishosting.com/agent/install-windows.ps1 | iex
param(
@@ -13,140 +17,242 @@ param(
$ErrorActionPreference = "Stop"
$InstallDir = "C:\ProgramData\jarvis-agent"
$AgentScript = "$InstallDir\jarvis-agent.py"
$AgentScript = "$InstallDir\jarvis-agent-windows.py"
$ConfigFile = "$InstallDir\config.json"
$TaskName = "JARVIS-Agent"
$ServiceName = "JARVISAgent"
$OldTaskName = "JARVIS-Agent" # legacy scheduled-task name
Write-Host ""
Write-Host " ====================================" -ForegroundColor Cyan
Write-Host " JARVIS Agent Installer v2.2 " -ForegroundColor Cyan
Write-Host " JARVIS Agent Installer v3.1 " -ForegroundColor Cyan
Write-Host " Windows Service Edition " -ForegroundColor Cyan
Write-Host " ====================================" -ForegroundColor Cyan
Write-Host ""
# ── Require admin ─────────────────────────────────────────────────────────────
if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
# ── Require admin ─────────────────────────────────────────────────────────────
if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole(
[Security.Principal.WindowsBuiltInRole]::Administrator)) {
Write-Error "Run PowerShell as Administrator and try again."
}
# ── Prompt if not provided ────────────────────────────────────────────────────
# ── Prompt if not provided ────────────────────────────────────────────────────
$JarvisUrl = $JarvisUrl.TrimEnd("/")
if (-not $Key) { $Key = Read-Host "Enter registration key" }
# ── Find Python 3 ────────────────────────────────────────────────────────────
Write-Host "Checking Python 3..." -NoNewline
$python = $null
$searchPaths = @(
"python", "python3", "py",
"$env:LOCALAPPDATA\Programs\Python\Python312\python.exe",
"$env:LOCALAPPDATA\Programs\Python\Python311\python.exe",
"$env:LOCALAPPDATA\Programs\Python\Python310\python.exe",
"C:\Python312\python.exe", "C:\Python311\python.exe"
# ── Find or install Python 3 (system-wide so LocalSystem service can reach it)
Write-Host "[1/6] Checking for Python 3..." -ForegroundColor Cyan
$pythonPath = $null
# Search for a system-wide Python first (accessible by LocalSystem)
$systemPaths = @(
"C:\Program Files\Python313\python.exe",
"C:\Program Files\Python312\python.exe",
"C:\Program Files\Python311\python.exe",
"C:\Program Files\Python310\python.exe",
"C:\Program Files\Python39\python.exe",
"C:\Python313\python.exe",
"C:\Python312\python.exe",
"C:\Python311\python.exe",
"C:\Python310\python.exe"
)
foreach ($p in $searchPaths) {
try {
$ver = & $p --version 2>&1
if ("$ver" -match "Python 3") { $python = $p; break }
} catch {}
}
if (-not $python) {
Write-Host " not found." -ForegroundColor Yellow
Write-Host "Installing Python 3.12 via winget..." -ForegroundColor Yellow
try {
winget install Python.Python.3.12 --silent --accept-package-agreements --accept-source-agreements
$env:PATH = [System.Environment]::GetEnvironmentVariable("PATH","Machine") + ";" +
[System.Environment]::GetEnvironmentVariable("PATH","User")
foreach ($p in @("python","python3")) {
try { $ver = & $p --version 2>&1; if ("$ver" -match "Python 3") { $python = $p; break } } catch {}
}
} catch {}
}
if (-not $python) { Write-Error "Python 3 not found. Install from https://python.org then re-run." }
# Resolve full path for task scheduler (PS 5.1 compatible — no ?. operator)
$_pyCmd = Get-Command $python -ErrorAction SilentlyContinue
$pythonPath = if ($_pyCmd) { $_pyCmd.Source } else { $null }
if (-not $pythonPath -or -not (Test-Path $pythonPath)) {
foreach ($p in @("$env:LOCALAPPDATA\Programs\Python\Python312\python.exe",
"$env:LOCALAPPDATA\Programs\Python\Python311\python.exe",
"C:\Python312\python.exe")) {
if (Test-Path $p) { $pythonPath = $p; break }
foreach ($p in $systemPaths) {
if (Test-Path $p) {
try {
$ver = & $p --version 2>&1
if ("$ver" -match "Python 3") { $pythonPath = $p; break }
} catch {}
}
}
if (-not $pythonPath) { $pythonPath = $python }
Write-Host " $pythonPath" -ForegroundColor Green
# ── Create install directory ──────────────────────────────────────────────────
New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null
Write-Host "Install dir: $InstallDir"
# ── Download Windows agent script ─────────────────────────────────────────────
Write-Host "Downloading jarvis-agent-windows.py..." -NoNewline
try {
[System.Net.ServicePointManager]::ServerCertificateValidationCallback = { $true }
$wc = New-Object System.Net.WebClient
$wc.Headers.Add("User-Agent", "JARVIS-Installer/1.0")
$wc.DownloadFile("$JarvisUrl/agent/jarvis-agent-windows.py", $AgentScript)
Write-Host " done." -ForegroundColor Green
} catch {
Write-Error "Download failed from $JarvisUrl/agent/jarvis-agent-windows.py`nError: $_"
# Fall back to PATH
if (-not $pythonPath) {
foreach ($cmd in @("python", "python3", "py")) {
try {
$ver = & $cmd --version 2>&1
if ("$ver" -match "Python 3") {
$resolved = (Get-Command $cmd -ErrorAction SilentlyContinue)
if ($resolved) { $pythonPath = $resolved.Source; break }
}
} catch {}
}
}
# ── Write config ──────────────────────────────────────────────────────────────
if (-not $pythonPath) {
Write-Host " Python 3 not found. Installing system-wide..." -ForegroundColor Yellow
# Try winget first (Win 10 1709+ / Win 11)
$wingetOk = $false
try {
$null = Get-Command winget -ErrorAction Stop
Write-Host " Using winget..." -NoNewline
winget install Python.Python.3.12 --silent --scope machine `
--accept-package-agreements --accept-source-agreements 2>&1 | Out-Null
$wingetOk = $true
Write-Host " done." -ForegroundColor Green
} catch {}
if (-not $wingetOk) {
# Direct download — works on Win 8.1 without winget
# Python 3.11 is explicitly documented to support Win 8.1+
Write-Host " Downloading Python 3.11 installer (Win 8.1+ compatible)..." -NoNewline
$pyInstaller = "$env:TEMP\python-installer.exe"
$pyUrl = "https://www.python.org/ftp/python/3.11.9/python-3.11.9-amd64.exe"
try {
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12
$wc = New-Object System.Net.WebClient
$wc.DownloadFile($pyUrl, $pyInstaller)
Write-Host " downloaded." -ForegroundColor Green
} catch {
Write-Error "Could not download Python installer. Install Python 3.11+ from https://python.org (choose 'Install for all users') then re-run."
}
Write-Host " Running Python installer (system-wide, silent)..." -NoNewline
$proc = Start-Process -FilePath $pyInstaller `
-ArgumentList "/quiet InstallAllUsers=1 PrependPath=1 Include_test=0" `
-Wait -PassThru
if ($proc.ExitCode -ne 0) {
Write-Error "Python installer exited with code $($proc.ExitCode). Install manually from https://python.org then re-run."
}
Write-Host " done." -ForegroundColor Green
Remove-Item $pyInstaller -ErrorAction SilentlyContinue
}
# Refresh PATH and locate installed Python
$env:PATH = [System.Environment]::GetEnvironmentVariable("PATH","Machine") + ";" +
[System.Environment]::GetEnvironmentVariable("PATH","User")
foreach ($p in $systemPaths) {
if (Test-Path $p) { $pythonPath = $p; break }
}
if (-not $pythonPath) {
foreach ($cmd in @("python", "python3")) {
try {
$ver = & $cmd --version 2>&1
if ("$ver" -match "Python 3") {
$resolved = (Get-Command $cmd -ErrorAction SilentlyContinue)
if ($resolved) { $pythonPath = $resolved.Source; break }
}
} catch {}
}
}
if (-not $pythonPath) {
Write-Error "Python 3 still not found after installation. Open a new Admin PowerShell and re-run this installer."
}
}
Write-Host " Python: $pythonPath" -ForegroundColor Green
# ── Install pywin32 (required for Windows service support) ────────────────────
Write-Host "[2/6] Installing pywin32..." -ForegroundColor Cyan
try {
& $pythonPath -m pip install --quiet --upgrade pywin32
# Run postinstall to register service runner DLLs system-wide
& $pythonPath -c "import pywin32_postinstall; pywin32_postinstall.install()" 2>$null
Write-Host " pywin32 installed." -ForegroundColor Green
} catch {
Write-Error "pip install pywin32 failed: $_`nEnsure pip is available: $pythonPath -m ensurepip"
}
# ── Create install directory and download agent ────────────────────────────────
Write-Host "[3/6] Downloading agent..." -ForegroundColor Cyan
New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null
try {
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12
[System.Net.ServicePointManager]::ServerCertificateValidationCallback = { $true }
$wc = New-Object System.Net.WebClient
$wc.Headers.Add("User-Agent", "JARVIS-Installer/3.1")
$wc.DownloadFile("$JarvisUrl/agent/jarvis-agent-windows.py", $AgentScript)
Write-Host " Downloaded to $AgentScript" -ForegroundColor Green
} catch {
Write-Error "Download failed: $_"
}
# ── Write config ───────────────────────────────────────────────────────────────
Write-Host "[4/6] Writing config..." -ForegroundColor Cyan
$agentId = "${AgentName}_windows"
$config = [ordered]@{
jarvis_url = $JarvisUrl
host_header = ""
ssl_verify = $true
registration_key = $Key
agent_type = "windows"
hostname = $AgentName
agent_id = $agentId
poll_interval = 30
heartbeat_every = 10
watch_services = @("WinDefend", "Spooler")
jarvis_url = $JarvisUrl
host_header = ""
ssl_verify = $true
registration_key = $Key
agent_type = "windows"
hostname = $AgentName
agent_id = $agentId
poll_interval = 30
heartbeat_every = 10
update_check_hours = 24
watch_services = @("WinDefend", "Spooler")
} | ConvertTo-Json -Depth 3
[System.IO.File]::WriteAllText($ConfigFile, $config, [System.Text.UTF8Encoding]::new($false))
Write-Host "Config: $ConfigFile"
Write-Host " Config: $ConfigFile" -ForegroundColor Green
# ── Register scheduled task ───────────────────────────────────────────────────
try { Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -ErrorAction SilentlyContinue } catch {}
# ── Remove legacy scheduled task if present ────────────────────────────────────
try {
$oldTask = Get-ScheduledTask -TaskName $OldTaskName -ErrorAction SilentlyContinue
if ($oldTask) {
Stop-ScheduledTask -TaskName $OldTaskName -ErrorAction SilentlyContinue
Unregister-ScheduledTask -TaskName $OldTaskName -Confirm:$false -ErrorAction SilentlyContinue
Write-Host " Removed legacy scheduled task '$OldTaskName'." -ForegroundColor Yellow
}
} catch {}
$action = New-ScheduledTaskAction -Execute "`"$pythonPath`"" `
-Argument "`"$AgentScript`"" -WorkingDirectory $InstallDir
$trigger = New-ScheduledTaskTrigger -AtStartup
$settings = New-ScheduledTaskSettingsSet `
-ExecutionTimeLimit (New-TimeSpan -Seconds 0) `
-RestartCount 10 -RestartInterval (New-TimeSpan -Minutes 1) `
-StartWhenAvailable -MultipleInstances IgnoreNew
# ── Register Windows service ───────────────────────────────────────────────────
Write-Host "[5/6] Registering Windows service '$ServiceName'..." -ForegroundColor Cyan
# Run as current user (not SYSTEM) so per-user Python install is accessible
$currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
$principal = New-ScheduledTaskPrincipal -UserId $currentUser -LogonType Interactive -RunLevel Highest
# Stop + remove any existing service first
$existing = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue
if ($existing) {
if ($existing.Status -eq "Running") {
Write-Host " Stopping existing service..." -NoNewline
& $pythonPath $AgentScript stop 2>&1 | Out-Null
Start-Sleep -Seconds 3
Write-Host " stopped." -ForegroundColor Yellow
}
Write-Host " Removing existing service..." -NoNewline
& $pythonPath $AgentScript remove 2>&1 | Out-Null
Start-Sleep -Seconds 2
Write-Host " removed." -ForegroundColor Yellow
}
Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger `
-Settings $settings -Principal $principal `
-Description "JARVIS AI System Monitoring Agent" -Force | Out-Null
# Install the service (--startup auto = start at boot)
& $pythonPath $AgentScript --startup auto install
if ($LASTEXITCODE -ne 0) {
Write-Error "Service registration failed (exit $LASTEXITCODE). Check that pywin32 postinstall completed."
}
Write-Host "Scheduled task '$TaskName' registered." -ForegroundColor Green
# Configure failure recovery: restart after 5s, 10s, 30s
sc.exe failure $ServiceName reset= 86400 actions= restart/5000/restart/10000/restart/30000 | Out-Null
Write-Host " Service registered with auto-restart on failure." -ForegroundColor Green
# ── Start now ─────────────────────────────────────────────────────────────────
Write-Host "Starting agent..." -NoNewline
Start-ScheduledTask -TaskName $TaskName
Start-Sleep -Seconds 3
$state = (Get-ScheduledTask -TaskName $TaskName).State
Write-Host " $state" -ForegroundColor $(if ($state -eq "Running") {"Green"} else {"Yellow"})
# ── Start the service ──────────────────────────────────────────────────────────
Write-Host "[6/6] Starting service..." -ForegroundColor Cyan
& $pythonPath $AgentScript start
Start-Sleep -Seconds 4
$svc = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue
$status = if ($svc) { $svc.Status } else { "NotFound" }
$color = if ($status -eq "Running") { "Green" } else { "Yellow" }
Write-Host " Service status: $status" -ForegroundColor $color
Write-Host ""
Write-Host " Installation complete!" -ForegroundColor Green
Write-Host " Machine : $AgentName ($agentId)" -ForegroundColor White
Write-Host " JARVIS : $JarvisUrl" -ForegroundColor White
Write-Host " Logs : $InstallDir\jarvis-agent.log" -ForegroundColor White
Write-Host " ====================================" -ForegroundColor Green
Write-Host " Installation complete! " -ForegroundColor Green
Write-Host " ====================================" -ForegroundColor Green
Write-Host ""
Write-Host " Useful commands:" -ForegroundColor Gray
Write-Host " Get-Content '$InstallDir\jarvis-agent.log' -Tail 20 -Wait" -ForegroundColor Gray
Write-Host " Stop-ScheduledTask -TaskName '$TaskName'" -ForegroundColor Gray
Write-Host " Start-ScheduledTask -TaskName '$TaskName'" -ForegroundColor Gray
Write-Host " Unregister-ScheduledTask -TaskName '$TaskName' -Confirm:`$false" -ForegroundColor Gray
Write-Host " Machine : $AgentName ($agentId)" -ForegroundColor White
Write-Host " JARVIS : $JarvisUrl" -ForegroundColor White
Write-Host " Python : $pythonPath" -ForegroundColor White
Write-Host " Logs : $InstallDir\jarvis-agent.log" -ForegroundColor White
Write-Host ""
Write-Host " Manage the service:" -ForegroundColor Gray
Write-Host " Get-Service JARVISAgent" -ForegroundColor Gray
Write-Host " Start-Service JARVISAgent" -ForegroundColor Gray
Write-Host " Stop-Service JARVISAgent" -ForegroundColor Gray
Write-Host " Restart-Service JARVISAgent" -ForegroundColor Gray
Write-Host " Get-Content '$InstallDir\jarvis-agent.log' -Tail 30 -Wait" -ForegroundColor Gray
Write-Host ""
Write-Host " To uninstall:" -ForegroundColor Gray
Write-Host " Stop-Service JARVISAgent" -ForegroundColor Gray
Write-Host " & '$pythonPath' '$AgentScript' remove" -ForegroundColor Gray
Write-Host ""
+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()
@@ -1 +1 @@
1232a5ffa6dda93fca04878952d26e889fde5db479b9f23ea35111be2c3f77f5 jarvis-agent-windows.py
974c117db29ae2cc417cf70046af32a688037b887bb08b17a18b1a0be37dec6f