mirror of
https://github.com/myronblair/jarvis
synced 2026-07-28 08:43:00 -05:00
Windows agent v3.2: build standalone PyInstaller exe (no Python/pywin32 install needed on target machines), add frozen-mode self-update (rename-swap since a running exe cant be overwritten in place), simplify installer accordingly, fix default install URL (jarvis.orbishosting.com default port is unreachable from outside the LAN - use :1972), fix stale windows=3.0 in the admin panel version-check map
This commit is contained in:
+17
-38
@@ -6,20 +6,26 @@
|
|||||||
.DESCRIPTION
|
.DESCRIPTION
|
||||||
Installs JARVIS Agent as a Windows Service that auto-starts at boot.
|
Installs JARVIS Agent as a Windows Service that auto-starts at boot.
|
||||||
Requires: PowerShell 5.1+, internet access, and Administrator rights.
|
Requires: PowerShell 5.1+, internet access, and Administrator rights.
|
||||||
|
No Python installation needed — this installs the standalone .exe build.
|
||||||
|
|
||||||
.EXAMPLE
|
.EXAMPLE
|
||||||
# Interactive install (prompts for registration key):
|
# Interactive install (prompts for registration key):
|
||||||
irm https://jarvis.orbishosting.com/agent/install-windows.ps1 | iex
|
irm https://jarvis.orbishosting.com:1972/agent/install-windows.ps1 | iex
|
||||||
|
|
||||||
# Silent install with key:
|
# Silent install with key:
|
||||||
$env:JARVIS_REG_KEY='your_key_here'; irm https://jarvis.orbishosting.com/agent/install-windows.ps1 | iex
|
$env:JARVIS_REG_KEY='your_key_here'; irm https://jarvis.orbishosting.com:1972/agent/install-windows.ps1 | iex
|
||||||
#>
|
#>
|
||||||
|
|
||||||
$ErrorActionPreference = 'Stop'
|
$ErrorActionPreference = 'Stop'
|
||||||
$JARVIS_URL = 'https://jarvis.orbishosting.com'
|
# Fixed 2026-07-07: jarvis.orbishosting.com on the default port (80/443) is not
|
||||||
|
# reachable from outside the LAN at all (no FortiGate VIP forwards it) — every
|
||||||
|
# external install using the old default URL would have failed outright. Port
|
||||||
|
# 1972 is the confirmed-working external path (same fix applied to the GitHub
|
||||||
|
# webhook the same day).
|
||||||
|
$JARVIS_URL = 'http://jarvis.orbishosting.com:1972'
|
||||||
$INSTALL_DIR = 'C:\ProgramData\jarvis-agent'
|
$INSTALL_DIR = 'C:\ProgramData\jarvis-agent'
|
||||||
$SERVICE_NAME = 'JARVISAgent'
|
$SERVICE_NAME = 'JARVISAgent'
|
||||||
$AGENT_SCRIPT = "$INSTALL_DIR\jarvis-agent-windows.py"
|
$AGENT_EXE = "$INSTALL_DIR\jarvis-agent-windows.exe"
|
||||||
$CONFIG_FILE = "$INSTALL_DIR\config.json"
|
$CONFIG_FILE = "$INSTALL_DIR\config.json"
|
||||||
|
|
||||||
function Write-Step { param($msg) Write-Host "`n[JARVIS] $msg" -ForegroundColor Cyan }
|
function Write-Step { param($msg) Write-Host "`n[JARVIS] $msg" -ForegroundColor Cyan }
|
||||||
@@ -39,49 +45,23 @@ if ($existing) {
|
|||||||
Start-Sleep 2
|
Start-Sleep 2
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
& python "$INSTALL_DIR\jarvis-agent-windows.py" remove 2>$null
|
if (Test-Path $AGENT_EXE) { & $AGENT_EXE remove 2>$null }
|
||||||
} catch {}
|
} catch {}
|
||||||
Write-OK "Existing service removed."
|
Write-OK "Existing service removed."
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── Check / install Python ────────────────────────────────────────────────────
|
|
||||||
Write-Step "Checking Python..."
|
|
||||||
$py = Get-Command python -ErrorAction SilentlyContinue
|
|
||||||
if (-not $py) {
|
|
||||||
Write-Host " Python not found. Installing via winget..." -ForegroundColor Yellow
|
|
||||||
if (-not (Get-Command winget -ErrorAction SilentlyContinue)) {
|
|
||||||
Write-Fail "winget not available. Please install Python 3.11+ from https://python.org and re-run."
|
|
||||||
}
|
|
||||||
winget install -e --id Python.Python.3.11 --silent --accept-package-agreements --accept-source-agreements
|
|
||||||
$env:PATH = [System.Environment]::GetEnvironmentVariable("PATH","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("PATH","User")
|
|
||||||
$py = Get-Command python -ErrorAction SilentlyContinue
|
|
||||||
if (-not $py) { Write-Fail "Python install failed. Please install manually from https://python.org" }
|
|
||||||
}
|
|
||||||
$pyVersion = & python --version 2>&1
|
|
||||||
Write-OK $pyVersion
|
|
||||||
|
|
||||||
# ── Install pywin32 ───────────────────────────────────────────────────────────
|
|
||||||
Write-Step "Checking pywin32..."
|
|
||||||
$checkWin32 = & python -c "import win32service; print('ok')" 2>&1
|
|
||||||
if ($checkWin32 -ne 'ok') {
|
|
||||||
Write-Host " Installing pywin32..." -ForegroundColor Yellow
|
|
||||||
& python -m pip install --quiet pywin32
|
|
||||||
& python -m pywin32_postinstall -install 2>$null
|
|
||||||
Write-OK "pywin32 installed."
|
|
||||||
} else {
|
|
||||||
Write-OK "pywin32 already installed."
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── Create install dir ────────────────────────────────────────────────────────
|
# ── Create install dir ────────────────────────────────────────────────────────
|
||||||
Write-Step "Creating install directory..."
|
Write-Step "Creating install directory..."
|
||||||
New-Item -ItemType Directory -Path $INSTALL_DIR -Force | Out-Null
|
New-Item -ItemType Directory -Path $INSTALL_DIR -Force | Out-Null
|
||||||
Write-OK $INSTALL_DIR
|
Write-OK $INSTALL_DIR
|
||||||
|
|
||||||
# ── Download agent script ─────────────────────────────────────────────────────
|
# ── Download agent exe ─────────────────────────────────────────────────────────
|
||||||
|
# No Python/pywin32 dependency anymore — this is a self-contained PyInstaller
|
||||||
|
# build with everything it needs bundled in.
|
||||||
Write-Step "Downloading JARVIS agent..."
|
Write-Step "Downloading JARVIS agent..."
|
||||||
try {
|
try {
|
||||||
Invoke-WebRequest -Uri "$JARVIS_URL/agent/jarvis-agent-windows.py" -OutFile $AGENT_SCRIPT -UseBasicParsing
|
Invoke-WebRequest -Uri "$JARVIS_URL/agent/jarvis-agent-windows.exe" -OutFile $AGENT_EXE -UseBasicParsing
|
||||||
Write-OK "Agent downloaded to $AGENT_SCRIPT"
|
Write-OK "Agent downloaded to $AGENT_EXE"
|
||||||
} catch {
|
} catch {
|
||||||
Write-Fail "Failed to download agent: $_"
|
Write-Fail "Failed to download agent: $_"
|
||||||
}
|
}
|
||||||
@@ -121,8 +101,7 @@ Write-OK "Config written to $CONFIG_FILE"
|
|||||||
|
|
||||||
# ── Install Windows Service ───────────────────────────────────────────────────
|
# ── Install Windows Service ───────────────────────────────────────────────────
|
||||||
Write-Step "Installing Windows service..."
|
Write-Step "Installing Windows service..."
|
||||||
$pyPath = (Get-Command python).Source
|
& $AGENT_EXE --startup auto install
|
||||||
& $pyPath "$AGENT_SCRIPT" --startup auto install
|
|
||||||
if ($LASTEXITCODE -ne 0) { Write-Fail "Service install failed." }
|
if ($LASTEXITCODE -ne 0) { Write-Fail "Service install failed." }
|
||||||
Write-OK "Service '$SERVICE_NAME' installed."
|
Write-OK "Service '$SERVICE_NAME' installed."
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -35,7 +35,7 @@ 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.1"
|
AGENT_VERSION = "3.2"
|
||||||
|
|
||||||
# Set by the service wrapper so self_update knows to stop instead of exec
|
# Set by the service wrapper so self_update knows to stop instead of exec
|
||||||
_is_service = False
|
_is_service = False
|
||||||
@@ -92,7 +92,7 @@ def api_post(url: str, payload: dict, headers: dict = {}, timeout: int = 15,
|
|||||||
body = json.dumps(payload).encode()
|
body = json.dumps(payload).encode()
|
||||||
req = urllib.request.Request(url, data=body, method="POST")
|
req = urllib.request.Request(url, data=body, method="POST")
|
||||||
req.add_header("Content-Type", "application/json")
|
req.add_header("Content-Type", "application/json")
|
||||||
req.add_header("User-Agent", "JARVIS-Agent-Windows/3.0")
|
req.add_header("User-Agent", "JARVIS-Agent-Windows/3.2")
|
||||||
if _host_header:
|
if _host_header:
|
||||||
req.add_header("Host", _host_header)
|
req.add_header("Host", _host_header)
|
||||||
for k, v in headers.items():
|
for k, v in headers.items():
|
||||||
@@ -109,7 +109,7 @@ def api_post(url: str, payload: dict, headers: dict = {}, timeout: int = 15,
|
|||||||
def api_get(url: str, headers: dict = {}, timeout: int = 10,
|
def api_get(url: str, headers: dict = {}, timeout: int = 10,
|
||||||
ssl_verify: bool = True) -> dict:
|
ssl_verify: bool = True) -> dict:
|
||||||
req = urllib.request.Request(url)
|
req = urllib.request.Request(url)
|
||||||
req.add_header("User-Agent", "JARVIS-Agent-Windows/3.0")
|
req.add_header("User-Agent", "JARVIS-Agent-Windows/3.2")
|
||||||
if _host_header:
|
if _host_header:
|
||||||
req.add_header("Host", _host_header)
|
req.add_header("Host", _host_header)
|
||||||
for k, v in headers.items():
|
for k, v in headers.items():
|
||||||
@@ -375,18 +375,28 @@ def _sysinfo_snapshot() -> dict:
|
|||||||
# ── Self-update ────────────────────────────────────────────────────────────────
|
# ── Self-update ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def self_update(cfg: dict) -> bool:
|
def self_update(cfg: dict) -> bool:
|
||||||
|
# Added: supports both script-mode (plain .py, run via a system Python) and
|
||||||
|
# frozen-mode (standalone PyInstaller .exe — sys.frozen is set, __file__ isn't
|
||||||
|
# meaningful/writable the way it is for a real .py file on disk). A running
|
||||||
|
# .exe can't be overwritten in place on Windows, but CAN be renamed while
|
||||||
|
# running, so frozen mode uses a download-new/rename-old/rename-new swap
|
||||||
|
# instead of the direct overwrite the script-mode path uses.
|
||||||
jarvis_url = cfg.get("jarvis_url", "").rstrip("/")
|
jarvis_url = cfg.get("jarvis_url", "").rstrip("/")
|
||||||
|
is_frozen = bool(getattr(sys, "frozen", False))
|
||||||
|
if is_frozen:
|
||||||
|
default_update_url = f"{jarvis_url}/agent/jarvis-agent-windows.exe" if jarvis_url else ""
|
||||||
|
else:
|
||||||
default_update_url = f"{jarvis_url}/agent/jarvis-agent-windows.py" if jarvis_url else ""
|
default_update_url = f"{jarvis_url}/agent/jarvis-agent-windows.py" if jarvis_url else ""
|
||||||
update_url = cfg.get("update_url", default_update_url)
|
update_url = cfg.get("update_url", default_update_url)
|
||||||
if not update_url:
|
if not update_url:
|
||||||
return False
|
return False
|
||||||
script_path = os.path.abspath(__file__)
|
target_path = os.path.abspath(sys.executable) if is_frozen else os.path.abspath(__file__)
|
||||||
ssl_verify = bool(cfg.get("ssl_verify", True))
|
ssl_verify = bool(cfg.get("ssl_verify", True))
|
||||||
try:
|
try:
|
||||||
# Download expected hash
|
# Download expected hash
|
||||||
hash_url = update_url + ".sha256"
|
hash_url = update_url + ".sha256"
|
||||||
req_hash = urllib.request.Request(hash_url)
|
req_hash = urllib.request.Request(hash_url)
|
||||||
req_hash.add_header("User-Agent", "JARVIS-Agent-Windows/3.0")
|
req_hash.add_header("User-Agent", "JARVIS-Agent-Windows/3.2")
|
||||||
if _host_header:
|
if _host_header:
|
||||||
req_hash.add_header("Host", _host_header)
|
req_hash.add_header("Host", _host_header)
|
||||||
expected_hash = None
|
expected_hash = None
|
||||||
@@ -397,13 +407,13 @@ def self_update(cfg: dict) -> bool:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Download new script
|
# Download new script/exe
|
||||||
req = urllib.request.Request(update_url)
|
req = urllib.request.Request(update_url)
|
||||||
req.add_header("User-Agent", "JARVIS-Agent-Windows/3.0")
|
req.add_header("User-Agent", "JARVIS-Agent-Windows/3.2")
|
||||||
if _host_header:
|
if _host_header:
|
||||||
req.add_header("Host", _host_header)
|
req.add_header("Host", _host_header)
|
||||||
ctx = _make_ssl_ctx(ssl_verify)
|
ctx = _make_ssl_ctx(ssl_verify)
|
||||||
with urllib.request.urlopen(req, timeout=30, context=ctx) as resp:
|
with urllib.request.urlopen(req, timeout=60, context=ctx) as resp:
|
||||||
new_content = resp.read()
|
new_content = resp.read()
|
||||||
|
|
||||||
# Verify hash
|
# Verify hash
|
||||||
@@ -413,20 +423,42 @@ def self_update(cfg: dict) -> bool:
|
|||||||
log(f"Update hash mismatch (expected {expected_hash[:16]}… got {actual_hash[:16]}…) — aborting")
|
log(f"Update hash mismatch (expected {expected_hash[:16]}… got {actual_hash[:16]}…) — aborting")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
with open(script_path, "rb") as f:
|
with open(target_path, "rb") as f:
|
||||||
current = f.read()
|
current = f.read()
|
||||||
if new_content != current:
|
if new_content == current:
|
||||||
log(f"Update verified — replacing {script_path} and restarting...")
|
return False
|
||||||
with open(script_path, "wb") as f:
|
|
||||||
|
log(f"Update verified — replacing {target_path} and restarting...")
|
||||||
|
if is_frozen:
|
||||||
|
# Can't overwrite a running exe, but can rename it and drop the new
|
||||||
|
# one in its place; the old copy is cleaned up on the next update.
|
||||||
|
old_path = target_path + ".old"
|
||||||
|
new_path = target_path + ".new"
|
||||||
|
with open(new_path, "wb") as f:
|
||||||
f.write(new_content)
|
f.write(new_content)
|
||||||
|
try:
|
||||||
|
if os.path.exists(old_path):
|
||||||
|
os.remove(old_path)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
os.rename(target_path, old_path)
|
||||||
|
os.rename(new_path, target_path)
|
||||||
|
else:
|
||||||
|
with open(target_path, "wb") as f:
|
||||||
|
f.write(new_content)
|
||||||
|
|
||||||
if _is_service:
|
if _is_service:
|
||||||
# Signal the main loop to exit; SCM failure-recovery will restart us
|
# Signal the main loop to exit; SCM failure-recovery will restart us
|
||||||
log("Running as service — stopping for SCM-managed restart after update.")
|
log("Running as service — stopping for SCM-managed restart after update.")
|
||||||
_stop_event.set()
|
_stop_event.set()
|
||||||
|
elif is_frozen:
|
||||||
|
# sys.argv[0] is already the exe's own path for a frozen app — don't
|
||||||
|
# prepend sys.executable again or the new process misreads its own
|
||||||
|
# path as a command-line argument.
|
||||||
|
os.execv(sys.executable, sys.argv)
|
||||||
else:
|
else:
|
||||||
os.execv(sys.executable, [sys.executable] + sys.argv)
|
os.execv(sys.executable, [sys.executable] + sys.argv)
|
||||||
return True
|
return True
|
||||||
return False
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log(f"Self-update check failed: {e}")
|
log(f"Self-update check failed: {e}")
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -493,7 +493,7 @@ if ($action) {
|
|||||||
$latestVersions = [
|
$latestVersions = [
|
||||||
'linux' => '3.1',
|
'linux' => '3.1',
|
||||||
'proxmox' => '3.1',
|
'proxmox' => '3.1',
|
||||||
'windows' => '3.0',
|
'windows' => '3.2',
|
||||||
'macos' => '3.0',
|
'macos' => '3.0',
|
||||||
'homeassistant' => null,
|
'homeassistant' => null,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -6,20 +6,26 @@
|
|||||||
.DESCRIPTION
|
.DESCRIPTION
|
||||||
Installs JARVIS Agent as a Windows Service that auto-starts at boot.
|
Installs JARVIS Agent as a Windows Service that auto-starts at boot.
|
||||||
Requires: PowerShell 5.1+, internet access, and Administrator rights.
|
Requires: PowerShell 5.1+, internet access, and Administrator rights.
|
||||||
|
No Python installation needed — this installs the standalone .exe build.
|
||||||
|
|
||||||
.EXAMPLE
|
.EXAMPLE
|
||||||
# Interactive install (prompts for registration key):
|
# Interactive install (prompts for registration key):
|
||||||
irm https://jarvis.orbishosting.com/agent/install-windows.ps1 | iex
|
irm https://jarvis.orbishosting.com:1972/agent/install-windows.ps1 | iex
|
||||||
|
|
||||||
# Silent install with key:
|
# Silent install with key:
|
||||||
$env:JARVIS_REG_KEY='your_key_here'; irm https://jarvis.orbishosting.com/agent/install-windows.ps1 | iex
|
$env:JARVIS_REG_KEY='your_key_here'; irm https://jarvis.orbishosting.com:1972/agent/install-windows.ps1 | iex
|
||||||
#>
|
#>
|
||||||
|
|
||||||
$ErrorActionPreference = 'Stop'
|
$ErrorActionPreference = 'Stop'
|
||||||
$JARVIS_URL = 'https://jarvis.orbishosting.com'
|
# Fixed 2026-07-07: jarvis.orbishosting.com on the default port (80/443) is not
|
||||||
|
# reachable from outside the LAN at all (no FortiGate VIP forwards it) — every
|
||||||
|
# external install using the old default URL would have failed outright. Port
|
||||||
|
# 1972 is the confirmed-working external path (same fix applied to the GitHub
|
||||||
|
# webhook the same day).
|
||||||
|
$JARVIS_URL = 'http://jarvis.orbishosting.com:1972'
|
||||||
$INSTALL_DIR = 'C:\ProgramData\jarvis-agent'
|
$INSTALL_DIR = 'C:\ProgramData\jarvis-agent'
|
||||||
$SERVICE_NAME = 'JARVISAgent'
|
$SERVICE_NAME = 'JARVISAgent'
|
||||||
$AGENT_SCRIPT = "$INSTALL_DIR\jarvis-agent-windows.py"
|
$AGENT_EXE = "$INSTALL_DIR\jarvis-agent-windows.exe"
|
||||||
$CONFIG_FILE = "$INSTALL_DIR\config.json"
|
$CONFIG_FILE = "$INSTALL_DIR\config.json"
|
||||||
|
|
||||||
function Write-Step { param($msg) Write-Host "`n[JARVIS] $msg" -ForegroundColor Cyan }
|
function Write-Step { param($msg) Write-Host "`n[JARVIS] $msg" -ForegroundColor Cyan }
|
||||||
@@ -39,49 +45,23 @@ if ($existing) {
|
|||||||
Start-Sleep 2
|
Start-Sleep 2
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
& python "$INSTALL_DIR\jarvis-agent-windows.py" remove 2>$null
|
if (Test-Path $AGENT_EXE) { & $AGENT_EXE remove 2>$null }
|
||||||
} catch {}
|
} catch {}
|
||||||
Write-OK "Existing service removed."
|
Write-OK "Existing service removed."
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── Check / install Python ────────────────────────────────────────────────────
|
|
||||||
Write-Step "Checking Python..."
|
|
||||||
$py = Get-Command python -ErrorAction SilentlyContinue
|
|
||||||
if (-not $py) {
|
|
||||||
Write-Host " Python not found. Installing via winget..." -ForegroundColor Yellow
|
|
||||||
if (-not (Get-Command winget -ErrorAction SilentlyContinue)) {
|
|
||||||
Write-Fail "winget not available. Please install Python 3.11+ from https://python.org and re-run."
|
|
||||||
}
|
|
||||||
winget install -e --id Python.Python.3.11 --silent --accept-package-agreements --accept-source-agreements
|
|
||||||
$env:PATH = [System.Environment]::GetEnvironmentVariable("PATH","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("PATH","User")
|
|
||||||
$py = Get-Command python -ErrorAction SilentlyContinue
|
|
||||||
if (-not $py) { Write-Fail "Python install failed. Please install manually from https://python.org" }
|
|
||||||
}
|
|
||||||
$pyVersion = & python --version 2>&1
|
|
||||||
Write-OK $pyVersion
|
|
||||||
|
|
||||||
# ── Install pywin32 ───────────────────────────────────────────────────────────
|
|
||||||
Write-Step "Checking pywin32..."
|
|
||||||
$checkWin32 = & python -c "import win32service; print('ok')" 2>&1
|
|
||||||
if ($checkWin32 -ne 'ok') {
|
|
||||||
Write-Host " Installing pywin32..." -ForegroundColor Yellow
|
|
||||||
& python -m pip install --quiet pywin32
|
|
||||||
& python -m pywin32_postinstall -install 2>$null
|
|
||||||
Write-OK "pywin32 installed."
|
|
||||||
} else {
|
|
||||||
Write-OK "pywin32 already installed."
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── Create install dir ────────────────────────────────────────────────────────
|
# ── Create install dir ────────────────────────────────────────────────────────
|
||||||
Write-Step "Creating install directory..."
|
Write-Step "Creating install directory..."
|
||||||
New-Item -ItemType Directory -Path $INSTALL_DIR -Force | Out-Null
|
New-Item -ItemType Directory -Path $INSTALL_DIR -Force | Out-Null
|
||||||
Write-OK $INSTALL_DIR
|
Write-OK $INSTALL_DIR
|
||||||
|
|
||||||
# ── Download agent script ─────────────────────────────────────────────────────
|
# ── Download agent exe ─────────────────────────────────────────────────────────
|
||||||
|
# No Python/pywin32 dependency anymore — this is a self-contained PyInstaller
|
||||||
|
# build with everything it needs bundled in.
|
||||||
Write-Step "Downloading JARVIS agent..."
|
Write-Step "Downloading JARVIS agent..."
|
||||||
try {
|
try {
|
||||||
Invoke-WebRequest -Uri "$JARVIS_URL/agent/jarvis-agent-windows.py" -OutFile $AGENT_SCRIPT -UseBasicParsing
|
Invoke-WebRequest -Uri "$JARVIS_URL/agent/jarvis-agent-windows.exe" -OutFile $AGENT_EXE -UseBasicParsing
|
||||||
Write-OK "Agent downloaded to $AGENT_SCRIPT"
|
Write-OK "Agent downloaded to $AGENT_EXE"
|
||||||
} catch {
|
} catch {
|
||||||
Write-Fail "Failed to download agent: $_"
|
Write-Fail "Failed to download agent: $_"
|
||||||
}
|
}
|
||||||
@@ -121,8 +101,7 @@ Write-OK "Config written to $CONFIG_FILE"
|
|||||||
|
|
||||||
# ── Install Windows Service ───────────────────────────────────────────────────
|
# ── Install Windows Service ───────────────────────────────────────────────────
|
||||||
Write-Step "Installing Windows service..."
|
Write-Step "Installing Windows service..."
|
||||||
$pyPath = (Get-Command python).Source
|
& $AGENT_EXE --startup auto install
|
||||||
& $pyPath "$AGENT_SCRIPT" --startup auto install
|
|
||||||
if ($LASTEXITCODE -ne 0) { Write-Fail "Service install failed." }
|
if ($LASTEXITCODE -ne 0) { Write-Fail "Service install failed." }
|
||||||
Write-OK "Service '$SERVICE_NAME' installed."
|
Write-OK "Service '$SERVICE_NAME' installed."
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
ad9b59c09e5862c5abc35f73999aa2666f8401817b053c385505fa420ca473e7
|
||||||
@@ -35,12 +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.1"
|
AGENT_VERSION = "3.2"
|
||||||
|
|
||||||
# Set by the service wrapper so self_update knows to stop instead of exec
|
# Set by the service wrapper so self_update knows to stop instead of exec
|
||||||
_is_service = False
|
_is_service = False
|
||||||
_stop_event = threading.Event()
|
_stop_event = threading.Event()
|
||||||
_update_restart = False # True when stopping for self-update; triggers SCM restart
|
|
||||||
|
|
||||||
# ── Logging ────────────────────────────────────────────────────────────────────
|
# ── Logging ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -93,7 +92,7 @@ def api_post(url: str, payload: dict, headers: dict = {}, timeout: int = 15,
|
|||||||
body = json.dumps(payload).encode()
|
body = json.dumps(payload).encode()
|
||||||
req = urllib.request.Request(url, data=body, method="POST")
|
req = urllib.request.Request(url, data=body, method="POST")
|
||||||
req.add_header("Content-Type", "application/json")
|
req.add_header("Content-Type", "application/json")
|
||||||
req.add_header("User-Agent", "JARVIS-Agent-Windows/3.0")
|
req.add_header("User-Agent", "JARVIS-Agent-Windows/3.2")
|
||||||
if _host_header:
|
if _host_header:
|
||||||
req.add_header("Host", _host_header)
|
req.add_header("Host", _host_header)
|
||||||
for k, v in headers.items():
|
for k, v in headers.items():
|
||||||
@@ -110,7 +109,7 @@ def api_post(url: str, payload: dict, headers: dict = {}, timeout: int = 15,
|
|||||||
def api_get(url: str, headers: dict = {}, timeout: int = 10,
|
def api_get(url: str, headers: dict = {}, timeout: int = 10,
|
||||||
ssl_verify: bool = True) -> dict:
|
ssl_verify: bool = True) -> dict:
|
||||||
req = urllib.request.Request(url)
|
req = urllib.request.Request(url)
|
||||||
req.add_header("User-Agent", "JARVIS-Agent-Windows/3.0")
|
req.add_header("User-Agent", "JARVIS-Agent-Windows/3.2")
|
||||||
if _host_header:
|
if _host_header:
|
||||||
req.add_header("Host", _host_header)
|
req.add_header("Host", _host_header)
|
||||||
for k, v in headers.items():
|
for k, v in headers.items():
|
||||||
@@ -376,18 +375,28 @@ def _sysinfo_snapshot() -> dict:
|
|||||||
# ── Self-update ────────────────────────────────────────────────────────────────
|
# ── Self-update ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def self_update(cfg: dict) -> bool:
|
def self_update(cfg: dict) -> bool:
|
||||||
|
# Added: supports both script-mode (plain .py, run via a system Python) and
|
||||||
|
# frozen-mode (standalone PyInstaller .exe — sys.frozen is set, __file__ isn't
|
||||||
|
# meaningful/writable the way it is for a real .py file on disk). A running
|
||||||
|
# .exe can't be overwritten in place on Windows, but CAN be renamed while
|
||||||
|
# running, so frozen mode uses a download-new/rename-old/rename-new swap
|
||||||
|
# instead of the direct overwrite the script-mode path uses.
|
||||||
jarvis_url = cfg.get("jarvis_url", "").rstrip("/")
|
jarvis_url = cfg.get("jarvis_url", "").rstrip("/")
|
||||||
|
is_frozen = bool(getattr(sys, "frozen", False))
|
||||||
|
if is_frozen:
|
||||||
|
default_update_url = f"{jarvis_url}/agent/jarvis-agent-windows.exe" if jarvis_url else ""
|
||||||
|
else:
|
||||||
default_update_url = f"{jarvis_url}/agent/jarvis-agent-windows.py" if jarvis_url else ""
|
default_update_url = f"{jarvis_url}/agent/jarvis-agent-windows.py" if jarvis_url else ""
|
||||||
update_url = cfg.get("update_url", default_update_url)
|
update_url = cfg.get("update_url", default_update_url)
|
||||||
if not update_url:
|
if not update_url:
|
||||||
return False
|
return False
|
||||||
script_path = os.path.abspath(__file__)
|
target_path = os.path.abspath(sys.executable) if is_frozen else os.path.abspath(__file__)
|
||||||
ssl_verify = bool(cfg.get("ssl_verify", True))
|
ssl_verify = bool(cfg.get("ssl_verify", True))
|
||||||
try:
|
try:
|
||||||
# Download expected hash
|
# Download expected hash
|
||||||
hash_url = update_url + ".sha256"
|
hash_url = update_url + ".sha256"
|
||||||
req_hash = urllib.request.Request(hash_url)
|
req_hash = urllib.request.Request(hash_url)
|
||||||
req_hash.add_header("User-Agent", "JARVIS-Agent-Windows/3.0")
|
req_hash.add_header("User-Agent", "JARVIS-Agent-Windows/3.2")
|
||||||
if _host_header:
|
if _host_header:
|
||||||
req_hash.add_header("Host", _host_header)
|
req_hash.add_header("Host", _host_header)
|
||||||
expected_hash = None
|
expected_hash = None
|
||||||
@@ -398,13 +407,13 @@ def self_update(cfg: dict) -> bool:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Download new script
|
# Download new script/exe
|
||||||
req = urllib.request.Request(update_url)
|
req = urllib.request.Request(update_url)
|
||||||
req.add_header("User-Agent", "JARVIS-Agent-Windows/3.0")
|
req.add_header("User-Agent", "JARVIS-Agent-Windows/3.2")
|
||||||
if _host_header:
|
if _host_header:
|
||||||
req.add_header("Host", _host_header)
|
req.add_header("Host", _host_header)
|
||||||
ctx = _make_ssl_ctx(ssl_verify)
|
ctx = _make_ssl_ctx(ssl_verify)
|
||||||
with urllib.request.urlopen(req, timeout=30, context=ctx) as resp:
|
with urllib.request.urlopen(req, timeout=60, context=ctx) as resp:
|
||||||
new_content = resp.read()
|
new_content = resp.read()
|
||||||
|
|
||||||
# Verify hash
|
# Verify hash
|
||||||
@@ -414,21 +423,42 @@ def self_update(cfg: dict) -> bool:
|
|||||||
log(f"Update hash mismatch (expected {expected_hash[:16]}… got {actual_hash[:16]}…) — aborting")
|
log(f"Update hash mismatch (expected {expected_hash[:16]}… got {actual_hash[:16]}…) — aborting")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
with open(script_path, "rb") as f:
|
with open(target_path, "rb") as f:
|
||||||
current = f.read()
|
current = f.read()
|
||||||
if new_content != current:
|
if new_content == current:
|
||||||
log(f"Update verified — replacing {script_path} and restarting...")
|
return False
|
||||||
with open(script_path, "wb") as f:
|
|
||||||
|
log(f"Update verified — replacing {target_path} and restarting...")
|
||||||
|
if is_frozen:
|
||||||
|
# Can't overwrite a running exe, but can rename it and drop the new
|
||||||
|
# one in its place; the old copy is cleaned up on the next update.
|
||||||
|
old_path = target_path + ".old"
|
||||||
|
new_path = target_path + ".new"
|
||||||
|
with open(new_path, "wb") as f:
|
||||||
f.write(new_content)
|
f.write(new_content)
|
||||||
|
try:
|
||||||
|
if os.path.exists(old_path):
|
||||||
|
os.remove(old_path)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
os.rename(target_path, old_path)
|
||||||
|
os.rename(new_path, target_path)
|
||||||
|
else:
|
||||||
|
with open(target_path, "wb") as f:
|
||||||
|
f.write(new_content)
|
||||||
|
|
||||||
if _is_service:
|
if _is_service:
|
||||||
global _update_restart
|
# Signal the main loop to exit; SCM failure-recovery will restart us
|
||||||
_update_restart = True
|
|
||||||
log("Running as service — stopping for SCM-managed restart after update.")
|
log("Running as service — stopping for SCM-managed restart after update.")
|
||||||
_stop_event.set()
|
_stop_event.set()
|
||||||
|
elif is_frozen:
|
||||||
|
# sys.argv[0] is already the exe's own path for a frozen app — don't
|
||||||
|
# prepend sys.executable again or the new process misreads its own
|
||||||
|
# path as a command-line argument.
|
||||||
|
os.execv(sys.executable, sys.argv)
|
||||||
else:
|
else:
|
||||||
os.execv(sys.executable, [sys.executable] + sys.argv)
|
os.execv(sys.executable, [sys.executable] + sys.argv)
|
||||||
return True
|
return True
|
||||||
return False
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log(f"Self-update check failed: {e}")
|
log(f"Self-update check failed: {e}")
|
||||||
return False
|
return False
|
||||||
@@ -592,9 +622,6 @@ if _HAS_WIN32:
|
|||||||
(self._svc_name_, ""),
|
(self._svc_name_, ""),
|
||||||
)
|
)
|
||||||
main()
|
main()
|
||||||
if _update_restart:
|
|
||||||
# Non-zero exit triggers SCM failure recovery → automatic restart
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
224a634375b5d49ccc0a012e0e122ade5f8a1302615450dffbf9a03eac6b7a19
|
fff217657488830084780115665d0c772af1f7fe31f2084d61a7560424a5a91a
|
||||||
|
|||||||
Reference in New Issue
Block a user