Repair Local Agent automatic update flow
This commit is contained in:
@@ -1,2 +1,2 @@
|
||||
__version__ = "1.22.1"
|
||||
__version__ = "1.22.2"
|
||||
AGENT_NAME = "ERP Local Agent"
|
||||
|
||||
@@ -29,7 +29,8 @@ class AgentConfig:
|
||||
tunnel_reconnect_seconds: int = 10
|
||||
auto_update: bool = True
|
||||
update_check_interval_seconds: int = 300
|
||||
auto_install_updates: bool = False
|
||||
auto_install_updates: bool = True
|
||||
manual_update_only: bool = False
|
||||
dashboard_enabled: bool = True
|
||||
dashboard_host: str = "127.0.0.1"
|
||||
dashboard_port: int = 8788
|
||||
@@ -107,6 +108,13 @@ def load_config(env_file: str | None = None) -> AgentConfig:
|
||||
storage_root = Path(storage_root_raw).expanduser().resolve()
|
||||
storage_root.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
auto_update = _get_bool("AUTO_UPDATE", True)
|
||||
# Packages through 1.22.1 generated AUTO_INSTALL_UPDATES=false even though the ERP
|
||||
# described updates as automatic. Treat that legacy generated setting as migrated
|
||||
# unless the administrator opts into the new explicit MANUAL_UPDATE_ONLY switch.
|
||||
manual_update_only = _get_bool("MANUAL_UPDATE_ONLY", False)
|
||||
auto_install_updates = bool(auto_update and not manual_update_only)
|
||||
|
||||
return AgentConfig(
|
||||
erp_base_url=erp_base_url,
|
||||
node_code=node_code,
|
||||
@@ -122,9 +130,10 @@ def load_config(env_file: str | None = None) -> AgentConfig:
|
||||
max_retries=_get_int("MAX_RETRIES", 3),
|
||||
tunnel_enabled=_get_bool("TUNNEL_ENABLED", True),
|
||||
tunnel_reconnect_seconds=_get_int("TUNNEL_RECONNECT_SECONDS", 10),
|
||||
auto_update=_get_bool("AUTO_UPDATE", True),
|
||||
auto_update=auto_update,
|
||||
update_check_interval_seconds=max(60, _get_int("UPDATE_CHECK_INTERVAL_SECONDS", 300)),
|
||||
auto_install_updates=_get_bool("AUTO_INSTALL_UPDATES", False),
|
||||
auto_install_updates=auto_install_updates,
|
||||
manual_update_only=manual_update_only,
|
||||
dashboard_enabled=_get_bool("DASHBOARD_ENABLED", True),
|
||||
dashboard_host=os.getenv("DASHBOARD_HOST", "127.0.0.1").strip() or "127.0.0.1",
|
||||
dashboard_port=max(1, min(65535, _get_int("DASHBOARD_PORT", 8788))),
|
||||
|
||||
@@ -3,8 +3,8 @@ from __future__ import annotations
|
||||
import hashlib
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
import zipfile
|
||||
@@ -12,11 +12,28 @@ import zipfile
|
||||
from . import __version__
|
||||
|
||||
|
||||
class AgentUpdater:
|
||||
"""Manual-install updater.
|
||||
def _version_tuple(value: str) -> tuple[int, ...]:
|
||||
"""Return a stable numeric tuple for ERP Local Agent release versions."""
|
||||
parts = [int(piece) for piece in re.findall(r"\d+", str(value or ""))]
|
||||
return tuple(parts or [0])
|
||||
|
||||
Background work checks the ERP manifest only. Download and installation happen only
|
||||
when the local user requests them from the dashboard.
|
||||
|
||||
def _is_newer(latest: str, current: str) -> bool:
|
||||
left = list(_version_tuple(latest))
|
||||
right = list(_version_tuple(current))
|
||||
width = max(len(left), len(right))
|
||||
left.extend([0] * (width - len(left)))
|
||||
right.extend([0] * (width - len(right)))
|
||||
return tuple(left) > tuple(right)
|
||||
|
||||
|
||||
class AgentUpdater:
|
||||
"""Supervisor-controlled ERP Local Agent updater.
|
||||
|
||||
The worker checks the ERP manifest in the background. When automatic installation
|
||||
is enabled it downloads the hash-verified package, stages it, and hands installation
|
||||
to the persistent desktop supervisor. Manual dashboard download/install actions are
|
||||
preserved and use the same code paths.
|
||||
"""
|
||||
|
||||
def __init__(self, config, client, logger, install_dir: Path, db=None):
|
||||
@@ -48,7 +65,7 @@ class AgentUpdater:
|
||||
latest = str(manifest.get("latest_version") or "").strip()
|
||||
expected = str(manifest.get("sha256") or "").lower().strip()
|
||||
self.latest_manifest = dict(manifest or {})
|
||||
available = bool(latest and latest != __version__)
|
||||
available = bool(latest and _is_newer(latest, __version__))
|
||||
if self.db is not None:
|
||||
self.db.set_meta("latest_agent_version", latest or __version__)
|
||||
self.db.set_meta("update_available", "1" if available else "0")
|
||||
@@ -65,8 +82,27 @@ class AgentUpdater:
|
||||
return False
|
||||
try:
|
||||
state = self.check_for_update(force=force)
|
||||
return bool(state.get("update_available"))
|
||||
except Exception:
|
||||
available = bool(state.get("update_available"))
|
||||
if not available:
|
||||
return False
|
||||
if not self.config.auto_install_updates:
|
||||
return True
|
||||
|
||||
latest = str(state.get("latest_version") or "").strip()
|
||||
request_path = self.install_dir / "updates" / "supervisor_request.json"
|
||||
if request_path.exists():
|
||||
# The persistent supervisor will consume the pending request. Avoid
|
||||
# repeatedly downloading the same package while restart is underway.
|
||||
return True
|
||||
|
||||
staged_version = str(state.get("staged_version") or "").strip()
|
||||
if not state.get("downloaded") or staged_version != latest:
|
||||
state = self.download_latest()
|
||||
if state.get("downloaded"):
|
||||
self.install_downloaded()
|
||||
return True
|
||||
except Exception as exc:
|
||||
self.logger.exception("ERP Local Agent automatic update failed: %s", exc)
|
||||
return False
|
||||
|
||||
def download_latest(self) -> dict:
|
||||
@@ -79,7 +115,7 @@ class AgentUpdater:
|
||||
self.check_for_update(force=True)
|
||||
latest = str(self.latest_manifest.get("latest_version") or "").strip()
|
||||
expected = str(self.latest_manifest.get("sha256") or "").lower().strip()
|
||||
if not latest or latest == __version__:
|
||||
if not latest or not _is_newer(latest, __version__):
|
||||
return self.status()
|
||||
package = self.client.download_update_package()
|
||||
actual = hashlib.sha256(package).hexdigest().lower()
|
||||
@@ -97,6 +133,8 @@ class AgentUpdater:
|
||||
archive.extractall(staged)
|
||||
if not (staged / "erp_local_agent" / "__init__.py").exists():
|
||||
raise RuntimeError("ERP Local Agent update package is incomplete.")
|
||||
if not (staged / "requirements.txt").exists():
|
||||
raise RuntimeError("ERP Local Agent update package is missing requirements.txt.")
|
||||
self._record("download", latest, "downloaded", sha256=actual, package_path=str(package_path))
|
||||
if self.db is not None:
|
||||
self.db.set_meta("staged_update_version", latest)
|
||||
@@ -110,11 +148,7 @@ class AgentUpdater:
|
||||
self.busy = False
|
||||
|
||||
def install_downloaded(self) -> dict:
|
||||
"""Ask the persistent desktop supervisor to install the staged worker.
|
||||
|
||||
The worker does not terminate itself. This lets the HTTP request finish cleanly,
|
||||
then the supervisor owns stop/replace/restart/rollback.
|
||||
"""
|
||||
"""Ask the persistent desktop supervisor to install the staged worker."""
|
||||
with self.lock:
|
||||
latest = str(
|
||||
(self.db.get_meta("staged_update_version") if self.db else "")
|
||||
@@ -123,6 +157,8 @@ class AgentUpdater:
|
||||
).strip()
|
||||
if not latest:
|
||||
raise RuntimeError("No downloaded ERP Local Agent update is available to install.")
|
||||
if not _is_newer(latest, __version__):
|
||||
return self.status()
|
||||
|
||||
safe = "".join(ch for ch in latest if ch.isalnum() or ch in ".-_") or "update"
|
||||
staged = self.install_dir / "updates" / f"staged_{safe}"
|
||||
@@ -144,6 +180,7 @@ class AgentUpdater:
|
||||
"staged_dir": str(staged.resolve()),
|
||||
"requested_at_utc": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
||||
"requested_by_pid": os.getpid(),
|
||||
"automatic": bool(self.config.auto_install_updates),
|
||||
}
|
||||
import json
|
||||
tmp.write_text(json.dumps(payload, indent=2, sort_keys=True), encoding="utf-8")
|
||||
@@ -164,7 +201,11 @@ class AgentUpdater:
|
||||
}
|
||||
|
||||
def status(self) -> dict:
|
||||
latest = str(self.latest_manifest.get("latest_version") or (self.db.get_meta("latest_agent_version") if self.db else "") or __version__).strip()
|
||||
latest = str(
|
||||
self.latest_manifest.get("latest_version")
|
||||
or (self.db.get_meta("latest_agent_version") if self.db else "")
|
||||
or __version__
|
||||
).strip()
|
||||
staged_version = str((self.db.get_meta("staged_update_version") if self.db else "") or "").strip()
|
||||
staged_ok = False
|
||||
if staged_version:
|
||||
@@ -182,11 +223,12 @@ class AgentUpdater:
|
||||
return {
|
||||
"current_version": __version__,
|
||||
"latest_version": latest,
|
||||
"update_available": bool(latest and latest != __version__),
|
||||
"update_available": bool(latest and _is_newer(latest, __version__)),
|
||||
"downloaded": bool(staged_ok),
|
||||
"staged_version": staged_version if staged_ok else "",
|
||||
"busy": bool(self.busy),
|
||||
"automatic_check_enabled": bool(self.config.auto_update),
|
||||
"automatic_install_enabled": False,
|
||||
"automatic_install_enabled": bool(self.config.auto_update and self.config.auto_install_updates),
|
||||
"manual_update_only": bool(getattr(self.config, "manual_update_only", False)),
|
||||
"supervisor": supervisor_state,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user