From b19ce85d84eef32c87e7e84578b32e79f918852e Mon Sep 17 00:00:00 2001 From: Myron Blair Date: Fri, 12 Jun 2026 12:11:38 +0000 Subject: [PATCH] Windows agent: run as background Windows Service (Win 8.1+) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- agent/jarvis-agent-windows.py | 87 ++++- public_html/agent/install-windows.ps1 | 314 ++++++++++++------ public_html/agent/jarvis-agent-windows.py | 87 ++++- .../agent/jarvis-agent-windows.py.sha256 | 2 +- 4 files changed, 371 insertions(+), 119 deletions(-) diff --git a/agent/jarvis-agent-windows.py b/agent/jarvis-agent-windows.py index 0b5c9ec..48369ce 100644 --- a/agent/jarvis-agent-windows.py +++ b/agent/jarvis-agent-windows.py @@ -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() diff --git a/public_html/agent/install-windows.ps1 b/public_html/agent/install-windows.ps1 index c83fba1..34fa0ed 100644 --- a/public_html/agent/install-windows.ps1 +++ b/public_html/agent/install-windows.ps1 @@ -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 "" diff --git a/public_html/agent/jarvis-agent-windows.py b/public_html/agent/jarvis-agent-windows.py index 0b5c9ec..48369ce 100644 --- a/public_html/agent/jarvis-agent-windows.py +++ b/public_html/agent/jarvis-agent-windows.py @@ -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() diff --git a/public_html/agent/jarvis-agent-windows.py.sha256 b/public_html/agent/jarvis-agent-windows.py.sha256 index 919cf24..e415c83 100644 --- a/public_html/agent/jarvis-agent-windows.py.sha256 +++ b/public_html/agent/jarvis-agent-windows.py.sha256 @@ -1 +1 @@ -1232a5ffa6dda93fca04878952d26e889fde5db479b9f23ea35111be2c3f77f5 jarvis-agent-windows.py +974c117db29ae2cc417cf70046af32a688037b887bb08b17a18b1a0be37dec6f