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:
@@ -35,12 +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.1"
|
||||
AGENT_VERSION = "3.2"
|
||||
|
||||
# Set by the service wrapper so self_update knows to stop instead of exec
|
||||
_is_service = False
|
||||
_stop_event = threading.Event()
|
||||
_update_restart = False # True when stopping for self-update; triggers SCM restart
|
||||
_is_service = False
|
||||
_stop_event = threading.Event()
|
||||
|
||||
# ── Logging ────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -93,7 +92,7 @@ def api_post(url: str, payload: dict, headers: dict = {}, timeout: int = 15,
|
||||
body = json.dumps(payload).encode()
|
||||
req = urllib.request.Request(url, data=body, method="POST")
|
||||
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:
|
||||
req.add_header("Host", _host_header)
|
||||
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,
|
||||
ssl_verify: bool = True) -> dict:
|
||||
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:
|
||||
req.add_header("Host", _host_header)
|
||||
for k, v in headers.items():
|
||||
@@ -376,18 +375,28 @@ def _sysinfo_snapshot() -> dict:
|
||||
# ── Self-update ────────────────────────────────────────────────────────────────
|
||||
|
||||
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("/")
|
||||
default_update_url = f"{jarvis_url}/agent/jarvis-agent-windows.py" if jarvis_url else ""
|
||||
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 ""
|
||||
update_url = cfg.get("update_url", default_update_url)
|
||||
if not update_url:
|
||||
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))
|
||||
try:
|
||||
# Download expected hash
|
||||
hash_url = update_url + ".sha256"
|
||||
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:
|
||||
req_hash.add_header("Host", _host_header)
|
||||
expected_hash = None
|
||||
@@ -398,13 +407,13 @@ def self_update(cfg: dict) -> bool:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Download new script
|
||||
# Download new script/exe
|
||||
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:
|
||||
req.add_header("Host", _host_header)
|
||||
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()
|
||||
|
||||
# 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")
|
||||
return False
|
||||
|
||||
with open(script_path, "rb") as f:
|
||||
with open(target_path, "rb") as f:
|
||||
current = f.read()
|
||||
if new_content != current:
|
||||
log(f"Update verified — replacing {script_path} and restarting...")
|
||||
with open(script_path, "wb") as f:
|
||||
if new_content == current:
|
||||
return False
|
||||
|
||||
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)
|
||||
if _is_service:
|
||||
global _update_restart
|
||||
_update_restart = True
|
||||
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
|
||||
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:
|
||||
# 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()
|
||||
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:
|
||||
os.execv(sys.executable, [sys.executable] + sys.argv)
|
||||
return True
|
||||
except Exception as e:
|
||||
log(f"Self-update check failed: {e}")
|
||||
return False
|
||||
@@ -592,9 +622,6 @@ if _HAS_WIN32:
|
||||
(self._svc_name_, ""),
|
||||
)
|
||||
main()
|
||||
if _update_restart:
|
||||
# Non-zero exit triggers SCM failure recovery → automatic restart
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user