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
+78 -5
View File
@@ -1,7 +1,18 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
JARVIS Agent for Windows — system monitor that reports metrics to JARVIS HUD. 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 Config: C:\ProgramData\jarvis-agent\config.json
Logs: C:\ProgramData\jarvis-agent\jarvis-agent.log Logs: C:\ProgramData\jarvis-agent\jarvis-agent.log
""" """
@@ -12,6 +23,7 @@ import platform
import socket import socket
import subprocess import subprocess
import sys import sys
import threading
import time import time
import urllib.request import urllib.request
import urllib.error import urllib.error
@@ -23,7 +35,11 @@ INSTALL_DIR = Path(r"C:\ProgramData\jarvis-agent")
CONFIG_PATH = INSTALL_DIR / "config.json" CONFIG_PATH = INSTALL_DIR / "config.json"
STATE_PATH = INSTALL_DIR / "state.json" STATE_PATH = INSTALL_DIR / "state.json"
LOG_PATH = INSTALL_DIR / "jarvis-agent.log" 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 ──────────────────────────────────────────────────────────────────── # ── Logging ────────────────────────────────────────────────────────────────────
@@ -403,6 +419,11 @@ def self_update(cfg: dict) -> bool:
log(f"Update verified — replacing {script_path} and restarting...") log(f"Update verified — replacing {script_path} and restarting...")
with open(script_path, "wb") as f: with open(script_path, "wb") as f:
f.write(new_content) f.write(new_content)
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) os.execv(sys.executable, [sys.executable] + sys.argv)
return True return True
return False return False
@@ -490,7 +511,8 @@ def main():
api_key = register(cfg, state) api_key = register(cfg, state)
if not api_key: if not api_key:
log("[ERROR] Could not register with JARVIS. Retrying in 60s...") 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} headers = {"X-Agent-Key": api_key}
last_metrics = 0 last_metrics = 0
@@ -498,7 +520,7 @@ def main():
log(f"Agent v{AGENT_VERSION} (Windows) running. Polling {jarvis_url} every {heartbeat_every}s.") 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() now = time.time()
try: try:
hb = api_post(f"{jarvis_url}/api/agent/heartbeat", {}, headers, ssl_verify=ssl_verify) hb = api_post(f"{jarvis_url}/api/agent/heartbeat", {}, headers, ssl_verify=ssl_verify)
@@ -527,8 +549,59 @@ def main():
except Exception as e: except Exception as e:
log(f"[ERROR] Loop error: {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__": if __name__ == "__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() main()
+182 -76
View File
@@ -1,8 +1,12 @@
# JARVIS Agent Installer — Windows (PowerShell) # 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: # Run as Administrator:
# Set-ExecutionPolicy Bypass -Scope Process # Set-ExecutionPolicy Bypass -Scope Process
# .\install-windows.ps1 -JarvisUrl https://jarvis.orbishosting.com -Key YOUR_KEY # .\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 # irm https://jarvis.orbishosting.com/agent/install-windows.ps1 | iex
param( param(
@@ -13,86 +17,158 @@ param(
$ErrorActionPreference = "Stop" $ErrorActionPreference = "Stop"
$InstallDir = "C:\ProgramData\jarvis-agent" $InstallDir = "C:\ProgramData\jarvis-agent"
$AgentScript = "$InstallDir\jarvis-agent.py" $AgentScript = "$InstallDir\jarvis-agent-windows.py"
$ConfigFile = "$InstallDir\config.json" $ConfigFile = "$InstallDir\config.json"
$TaskName = "JARVIS-Agent" $ServiceName = "JARVISAgent"
$OldTaskName = "JARVIS-Agent" # legacy scheduled-task name
Write-Host "" Write-Host ""
Write-Host " ====================================" -ForegroundColor Cyan 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 " ====================================" -ForegroundColor Cyan
Write-Host "" Write-Host ""
# ── Require admin ───────────────────────────────────────────────────────────── # ── Require admin ─────────────────────────────────────────────────────────────
if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole(
[Security.Principal.WindowsBuiltInRole]::Administrator)) {
Write-Error "Run PowerShell as Administrator and try again." Write-Error "Run PowerShell as Administrator and try again."
} }
# ── Prompt if not provided ──────────────────────────────────────────────────── # ── Prompt if not provided ────────────────────────────────────────────────────
$JarvisUrl = $JarvisUrl.TrimEnd("/") $JarvisUrl = $JarvisUrl.TrimEnd("/")
if (-not $Key) { $Key = Read-Host "Enter registration key" } if (-not $Key) { $Key = Read-Host "Enter registration key" }
# ── Find Python 3 ──────────────────────────────────────────────────────────── # ── Find or install Python 3 (system-wide so LocalSystem service can reach it)
Write-Host "Checking Python 3..." -NoNewline Write-Host "[1/6] Checking for Python 3..." -ForegroundColor Cyan
$python = $null
$searchPaths = @( $pythonPath = $null
"python", "python3", "py",
"$env:LOCALAPPDATA\Programs\Python\Python312\python.exe", # Search for a system-wide Python first (accessible by LocalSystem)
"$env:LOCALAPPDATA\Programs\Python\Python311\python.exe", $systemPaths = @(
"$env:LOCALAPPDATA\Programs\Python\Python310\python.exe", "C:\Program Files\Python313\python.exe",
"C:\Python312\python.exe", "C:\Python311\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) { foreach ($p in $systemPaths) {
if (Test-Path $p) {
try { try {
$ver = & $p --version 2>&1 $ver = & $p --version 2>&1
if ("$ver" -match "Python 3") { $python = $p; break } if ("$ver" -match "Python 3") { $pythonPath = $p; break }
} catch {} } catch {}
}
} }
if (-not $python) { # Fall back to PATH
Write-Host " not found." -ForegroundColor Yellow if (-not $pythonPath) {
Write-Host "Installing Python 3.12 via winget..." -ForegroundColor Yellow foreach ($cmd in @("python", "python3", "py")) {
try { try {
winget install Python.Python.3.12 --silent --accept-package-agreements --accept-source-agreements $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-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") + ";" + $env:PATH = [System.Environment]::GetEnvironmentVariable("PATH","Machine") + ";" +
[System.Environment]::GetEnvironmentVariable("PATH","User") [System.Environment]::GetEnvironmentVariable("PATH","User")
foreach ($p in @("python","python3")) { foreach ($p in $systemPaths) {
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 } 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."
}
} }
if (-not $pythonPath) { $pythonPath = $python }
Write-Host " $pythonPath" -ForegroundColor Green
# ── Create install directory ────────────────────────────────────────────────── Write-Host " Python: $pythonPath" -ForegroundColor Green
New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null
Write-Host "Install dir: $InstallDir"
# ── Download Windows agent script ───────────────────────────────────────────── # ── Install pywin32 (required for Windows service support) ────────────────────
Write-Host "Downloading jarvis-agent-windows.py..." -NoNewline Write-Host "[2/6] Installing pywin32..." -ForegroundColor Cyan
try { 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 } [System.Net.ServicePointManager]::ServerCertificateValidationCallback = { $true }
$wc = New-Object System.Net.WebClient $wc = New-Object System.Net.WebClient
$wc.Headers.Add("User-Agent", "JARVIS-Installer/1.0") $wc.Headers.Add("User-Agent", "JARVIS-Installer/3.1")
$wc.DownloadFile("$JarvisUrl/agent/jarvis-agent-windows.py", $AgentScript) $wc.DownloadFile("$JarvisUrl/agent/jarvis-agent-windows.py", $AgentScript)
Write-Host " done." -ForegroundColor Green Write-Host " Downloaded to $AgentScript" -ForegroundColor Green
} catch { } catch {
Write-Error "Download failed from $JarvisUrl/agent/jarvis-agent-windows.py`nError: $_" Write-Error "Download failed: $_"
} }
# ── Write config ────────────────────────────────────────────────────────────── # ── Write config ──────────────────────────────────────────────────────────────
Write-Host "[4/6] Writing config..." -ForegroundColor Cyan
$agentId = "${AgentName}_windows" $agentId = "${AgentName}_windows"
$config = [ordered]@{ $config = [ordered]@{
jarvis_url = $JarvisUrl jarvis_url = $JarvisUrl
@@ -104,49 +180,79 @@ $config = [ordered]@{
agent_id = $agentId agent_id = $agentId
poll_interval = 30 poll_interval = 30
heartbeat_every = 10 heartbeat_every = 10
update_check_hours = 24
watch_services = @("WinDefend", "Spooler") watch_services = @("WinDefend", "Spooler")
} | ConvertTo-Json -Depth 3 } | ConvertTo-Json -Depth 3
[System.IO.File]::WriteAllText($ConfigFile, $config, [System.Text.UTF8Encoding]::new($false)) [System.IO.File]::WriteAllText($ConfigFile, $config, [System.Text.UTF8Encoding]::new($false))
Write-Host "Config: $ConfigFile" Write-Host " Config: $ConfigFile" -ForegroundColor Green
# ── Register scheduled task ─────────────────────────────────────────────────── # ── Remove legacy scheduled task if present ────────────────────────────────────
try { Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -ErrorAction SilentlyContinue } catch {} 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`"" ` # ── Register Windows service ───────────────────────────────────────────────────
-Argument "`"$AgentScript`"" -WorkingDirectory $InstallDir Write-Host "[5/6] Registering Windows service '$ServiceName'..." -ForegroundColor Cyan
$trigger = New-ScheduledTaskTrigger -AtStartup
$settings = New-ScheduledTaskSettingsSet `
-ExecutionTimeLimit (New-TimeSpan -Seconds 0) `
-RestartCount 10 -RestartInterval (New-TimeSpan -Minutes 1) `
-StartWhenAvailable -MultipleInstances IgnoreNew
# Run as current user (not SYSTEM) so per-user Python install is accessible # Stop + remove any existing service first
$currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name $existing = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue
$principal = New-ScheduledTaskPrincipal -UserId $currentUser -LogonType Interactive -RunLevel Highest 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 ` # Install the service (--startup auto = start at boot)
-Settings $settings -Principal $principal ` & $pythonPath $AgentScript --startup auto install
-Description "JARVIS AI System Monitoring Agent" -Force | Out-Null 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 ───────────────────────────────────────────────────────────────── # ── Start the service ──────────────────────────────────────────────────────────
Write-Host "Starting agent..." -NoNewline Write-Host "[6/6] Starting service..." -ForegroundColor Cyan
Start-ScheduledTask -TaskName $TaskName & $pythonPath $AgentScript start
Start-Sleep -Seconds 3 Start-Sleep -Seconds 4
$state = (Get-ScheduledTask -TaskName $TaskName).State
Write-Host " $state" -ForegroundColor $(if ($state -eq "Running") {"Green"} else {"Yellow"}) $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 ""
Write-Host " Installation complete!" -ForegroundColor Green Write-Host " ====================================" -ForegroundColor Green
Write-Host " Installation complete! " -ForegroundColor Green
Write-Host " ====================================" -ForegroundColor Green
Write-Host ""
Write-Host " Machine : $AgentName ($agentId)" -ForegroundColor White Write-Host " Machine : $AgentName ($agentId)" -ForegroundColor White
Write-Host " JARVIS : $JarvisUrl" -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 " Logs : $InstallDir\jarvis-agent.log" -ForegroundColor White
Write-Host "" Write-Host ""
Write-Host " Useful commands:" -ForegroundColor Gray Write-Host " Manage the service:" -ForegroundColor Gray
Write-Host " Get-Content '$InstallDir\jarvis-agent.log' -Tail 20 -Wait" -ForegroundColor Gray Write-Host " Get-Service JARVISAgent" -ForegroundColor Gray
Write-Host " Stop-ScheduledTask -TaskName '$TaskName'" -ForegroundColor Gray Write-Host " Start-Service JARVISAgent" -ForegroundColor Gray
Write-Host " Start-ScheduledTask -TaskName '$TaskName'" -ForegroundColor Gray Write-Host " Stop-Service JARVISAgent" -ForegroundColor Gray
Write-Host " Unregister-ScheduledTask -TaskName '$TaskName' -Confirm:`$false" -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 "" Write-Host ""
+78 -5
View File
@@ -1,7 +1,18 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
JARVIS Agent for Windows system monitor that reports metrics to JARVIS HUD. 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 Config: C:\ProgramData\jarvis-agent\config.json
Logs: C:\ProgramData\jarvis-agent\jarvis-agent.log Logs: C:\ProgramData\jarvis-agent\jarvis-agent.log
""" """
@@ -12,6 +23,7 @@ import platform
import socket import socket
import subprocess import subprocess
import sys import sys
import threading
import time import time
import urllib.request import urllib.request
import urllib.error import urllib.error
@@ -23,7 +35,11 @@ INSTALL_DIR = Path(r"C:\ProgramData\jarvis-agent")
CONFIG_PATH = INSTALL_DIR / "config.json" CONFIG_PATH = INSTALL_DIR / "config.json"
STATE_PATH = INSTALL_DIR / "state.json" STATE_PATH = INSTALL_DIR / "state.json"
LOG_PATH = INSTALL_DIR / "jarvis-agent.log" 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 ──────────────────────────────────────────────────────────────────── # ── Logging ────────────────────────────────────────────────────────────────────
@@ -403,6 +419,11 @@ def self_update(cfg: dict) -> bool:
log(f"Update verified — replacing {script_path} and restarting...") log(f"Update verified — replacing {script_path} and restarting...")
with open(script_path, "wb") as f: with open(script_path, "wb") as f:
f.write(new_content) f.write(new_content)
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) os.execv(sys.executable, [sys.executable] + sys.argv)
return True return True
return False return False
@@ -490,7 +511,8 @@ def main():
api_key = register(cfg, state) api_key = register(cfg, state)
if not api_key: if not api_key:
log("[ERROR] Could not register with JARVIS. Retrying in 60s...") 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} headers = {"X-Agent-Key": api_key}
last_metrics = 0 last_metrics = 0
@@ -498,7 +520,7 @@ def main():
log(f"Agent v{AGENT_VERSION} (Windows) running. Polling {jarvis_url} every {heartbeat_every}s.") 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() now = time.time()
try: try:
hb = api_post(f"{jarvis_url}/api/agent/heartbeat", {}, headers, ssl_verify=ssl_verify) hb = api_post(f"{jarvis_url}/api/agent/heartbeat", {}, headers, ssl_verify=ssl_verify)
@@ -527,8 +549,59 @@ def main():
except Exception as e: except Exception as e:
log(f"[ERROR] Loop error: {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__": if __name__ == "__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() main()
@@ -1 +1 @@
1232a5ffa6dda93fca04878952d26e889fde5db479b9f23ea35111be2c3f77f5 jarvis-agent-windows.py 974c117db29ae2cc417cf70046af32a688037b887bb08b17a18b1a0be37dec6f