Add persistent desktop supervisor for ERP Local Agent
This commit is contained in:
@@ -1,2 +1,2 @@
|
||||
__version__ = "1.8.2"
|
||||
__version__ = "1.9.0"
|
||||
AGENT_NAME = "ERP Local Agent"
|
||||
|
||||
@@ -144,8 +144,8 @@ body{font-family:Segoe UI,Arial,sans-serif;background:#f4f6f8;color:#17202a;marg
|
||||
<div class="card"><div class="label">TallyPrime</div><div id="tally" class="value">—</div><div id="companies" class="muted"></div></div>
|
||||
<div class="card"><div class="label">Storage Free</div><div id="storage" class="value">—</div><div id="storageRoot" class="muted"></div></div>
|
||||
</div>
|
||||
<div class="section"><h2>Updates</h2><p class="muted">The agent checks for new versions automatically, but it will never install an update without a local user clicking Install.</p>
|
||||
<button onclick="action('/api/update/check')">Check for Update</button><button class="secondary" id="download" onclick="action('/api/update/download')">Download Update</button><button id="install" onclick="installUpdate()">Install Downloaded Update</button><div id="message"></div></div>
|
||||
<div class="section"><h2>Updates</h2><p class="muted">The agent checks for new versions automatically. Installation occurs only when a local user clicks Install; the persistent supervisor then restarts the worker safely.</p>
|
||||
<button onclick="action('/api/update/check')">Check for Update</button><button class="secondary" id="download" onclick="action('/api/update/download')">Download Update</button><button id="install" onclick="installUpdate()">Install & Restart Safely</button><div id="message"></div></div>
|
||||
<div class="section"><h2>Tally</h2><button onclick="action('/api/tally/check')">Test Tally Connection</button><p class="muted">Tally is checked locally at <code>127.0.0.1:9000</code>. Port 9000 is not exposed to the internet.</p></div>
|
||||
<div class="section"><h2>Recent History</h2><div id="history" class="muted">Loading…</div></div>
|
||||
</div>
|
||||
@@ -161,7 +161,7 @@ document.getElementById('download').disabled=!s.update.update_available;document
|
||||
async function refreshHistory(){const r=await fetch('/api/history',{cache:'no-store'});const h=await r.json();let rows=h.events.slice(0,12).map(x=>'<tr><td>'+x.occurred_at_utc+'</td><td>'+x.level+'</td><td>'+x.event_type+'</td><td>'+escapeHtml(x.message)+'</td></tr>').join('');document.getElementById('history').innerHTML='<table><thead><tr><th>Time UTC</th><th>Level</th><th>Event</th><th>Message</th></tr></thead><tbody>'+rows+'</tbody></table>'}
|
||||
function escapeHtml(v){return String(v||'').replace(/[&<>'"]/g,m=>({'&':'&','<':'<','>':'>',"'":''','"':'"'}[m]))}
|
||||
async function action(url){msg('Working…',true);try{const r=await fetch(url,{method:'POST'});const j=await r.json();if(!r.ok||!j.ok)throw new Error(j.error||'Operation failed');msg('Completed successfully.',true);await refresh()}catch(e){msg(e.message,false)}}
|
||||
async function installUpdate(){if(!confirm('Install the downloaded update now? The ERP Local Agent will restart.'))return;msg('Starting update installation…',true);try{const r=await fetch('/api/update/install',{method:'POST'});const j=await r.json();if(!r.ok||!j.ok)throw new Error(j.error||'Install failed');msg(j.message||'Update started. Refresh this page after the agent restarts.',true)}catch(e){msg(e.message,false)}}
|
||||
async function installUpdate(){if(!confirm('Install the downloaded update now? The desktop supervisor will restart only the Local Agent worker and will keep supervising the restart.'))return;msg('Starting update installation…',true);try{const r=await fetch('/api/update/install',{method:'POST'});const j=await r.json();if(!r.ok||!j.ok)throw new Error(j.error||'Install failed');msg(j.message||'Update handed to supervisor. The desktop dashboard will reconnect automatically after restart.',true)}catch(e){msg(e.message,false)}}
|
||||
function msg(t,ok){let e=document.getElementById('message');e.textContent=t;e.className=ok?'msgok':'msgbad'}
|
||||
refresh();setInterval(refresh,5000);
|
||||
</script></body></html>'''
|
||||
|
||||
@@ -109,28 +109,59 @@ class AgentUpdater:
|
||||
finally:
|
||||
self.busy = False
|
||||
|
||||
def install_downloaded(self) -> None:
|
||||
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.
|
||||
"""
|
||||
with self.lock:
|
||||
latest = str((self.db.get_meta("staged_update_version") if self.db else "") or self.latest_manifest.get("latest_version") or "").strip()
|
||||
latest = str(
|
||||
(self.db.get_meta("staged_update_version") if self.db else "")
|
||||
or self.latest_manifest.get("latest_version")
|
||||
or ""
|
||||
).strip()
|
||||
if not latest:
|
||||
raise RuntimeError("No downloaded ERP Local Agent update is available to install.")
|
||||
|
||||
safe = "".join(ch for ch in latest if ch.isalnum() or ch in ".-_") or "update"
|
||||
staged = self.install_dir / "updates" / f"staged_{safe}"
|
||||
if not (staged / "erp_local_agent" / "__init__.py").exists():
|
||||
raise RuntimeError("The downloaded update is not staged correctly. Download it again.")
|
||||
script_path = self.install_dir / "updates" / f"apply_{safe}.ps1"
|
||||
script_path.write_text(self._powershell_update_script(staged, latest), encoding="utf-8")
|
||||
|
||||
supervisor_lock = self.install_dir / "data" / "supervisor.lock"
|
||||
if not supervisor_lock.exists():
|
||||
raise RuntimeError(
|
||||
"ERP Local Agent Supervisor is not running. Start the Local Agent from the "
|
||||
"desktop shortcut or restart the 'ERP Local Agent' Scheduled Task."
|
||||
)
|
||||
|
||||
request_path = self.install_dir / "updates" / "supervisor_request.json"
|
||||
tmp = request_path.with_suffix(".json.tmp")
|
||||
payload = {
|
||||
"action": "install_update",
|
||||
"version": latest,
|
||||
"staged_dir": str(staged.resolve()),
|
||||
"requested_at_utc": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
||||
"requested_by_pid": os.getpid(),
|
||||
}
|
||||
import json
|
||||
tmp.write_text(json.dumps(payload, indent=2, sort_keys=True), encoding="utf-8")
|
||||
tmp.replace(request_path)
|
||||
|
||||
self._record("install_requested", latest, "pending", package_path=str(staged))
|
||||
flags = getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0) | getattr(subprocess, "DETACHED_PROCESS", 0)
|
||||
process = subprocess.Popen(
|
||||
["powershell.exe", "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", str(script_path), "-ParentPid", str(os.getpid())],
|
||||
cwd=str(self.install_dir), creationflags=flags, close_fds=True,
|
||||
self.logger.warning(
|
||||
"ERP Local Agent update %s handed to persistent supervisor for controlled restart.",
|
||||
latest,
|
||||
)
|
||||
if process.pid <= 0:
|
||||
raise RuntimeError("Could not launch the ERP Local Agent update installer.")
|
||||
self.logger.warning("ERP Local Agent update %s installation requested by local user; restarting.", latest)
|
||||
time.sleep(0.3)
|
||||
os._exit(0)
|
||||
return {
|
||||
"ok": True,
|
||||
"message": (
|
||||
f"ERP Local Agent {latest} installation has been handed to the desktop supervisor. "
|
||||
"The worker will restart automatically and the desktop dashboard will reconnect."
|
||||
),
|
||||
"target_version": latest,
|
||||
}
|
||||
|
||||
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()
|
||||
@@ -139,6 +170,15 @@ class AgentUpdater:
|
||||
if staged_version:
|
||||
safe = "".join(ch for ch in staged_version if ch.isalnum() or ch in ".-_") or "update"
|
||||
staged_ok = (self.install_dir / "updates" / f"staged_{safe}" / "erp_local_agent" / "__init__.py").exists()
|
||||
supervisor_state = {}
|
||||
state_path = self.install_dir / "data" / "supervisor_state.json"
|
||||
try:
|
||||
if state_path.exists():
|
||||
import json
|
||||
supervisor_state = json.loads(state_path.read_text(encoding="utf-8")) or {}
|
||||
except Exception:
|
||||
supervisor_state = {}
|
||||
|
||||
return {
|
||||
"current_version": __version__,
|
||||
"latest_version": latest,
|
||||
@@ -148,44 +188,5 @@ class AgentUpdater:
|
||||
"busy": bool(self.busy),
|
||||
"automatic_check_enabled": bool(self.config.auto_update),
|
||||
"automatic_install_enabled": False,
|
||||
"supervisor": supervisor_state,
|
||||
}
|
||||
|
||||
def _powershell_update_script(self, staged: Path, latest: str) -> str:
|
||||
install = str(self.install_dir).replace("'", "''")
|
||||
stage = str(staged.resolve()).replace("'", "''")
|
||||
latest_safe = str(latest).replace("'", "''")
|
||||
lines = [
|
||||
"param([int]$ParentPid)",
|
||||
'$ErrorActionPreference = "Stop"',
|
||||
"$InstallDir = '" + install + "'",
|
||||
"$StagedDir = '" + stage + "'",
|
||||
"$TargetVersion = '" + latest_safe + "'",
|
||||
"$TaskName = 'ERP Local Agent'",
|
||||
"$BackupDir = Join-Path $InstallDir ('updates\\backup_' + (Get-Date -Format 'yyyyMMdd_HHmmss'))",
|
||||
"try {",
|
||||
" if ($ParentPid -gt 0) { try { Wait-Process -Id $ParentPid -Timeout 120 -ErrorAction SilentlyContinue } catch {} }",
|
||||
" try { Stop-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue } catch {}",
|
||||
" Start-Sleep -Seconds 2",
|
||||
" Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -match 'erp_local_agent\\.main' -and $_.CommandLine -like ('*' + $InstallDir + '*') } | ForEach-Object { try { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue } catch {} }",
|
||||
" Start-Sleep -Seconds 2",
|
||||
" New-Item -ItemType Directory -Force -Path $BackupDir | Out-Null",
|
||||
" if (Test-Path (Join-Path $InstallDir 'erp_local_agent')) { Copy-Item (Join-Path $InstallDir 'erp_local_agent') -Destination $BackupDir -Recurse -Force }",
|
||||
" Remove-Item (Join-Path $InstallDir 'erp_local_agent') -Recurse -Force -ErrorAction Stop",
|
||||
" Copy-Item (Join-Path $StagedDir 'erp_local_agent') -Destination $InstallDir -Recurse -Force",
|
||||
" foreach ($Item in @('requirements.txt','run_agent.bat','run_once.bat','check_config.py','check_config.bat','open_dashboard.bat','start_task_scheduler.bat','stop_task_scheduler.bat','status_task_scheduler.bat','install_task_scheduler.bat','uninstall_task_scheduler.bat')) { $Source=Join-Path $StagedDir $Item; if (Test-Path $Source) { Copy-Item $Source -Destination (Join-Path $InstallDir $Item) -Force } }",
|
||||
" $Python = Join-Path $InstallDir '.venv\\Scripts\\python.exe'",
|
||||
" if (-not (Test-Path $Python)) { throw 'ERP Local Agent virtual environment is missing.' }",
|
||||
" & $Python -m pip install -r (Join-Path $InstallDir 'requirements.txt')",
|
||||
" if ($LASTEXITCODE -ne 0) { throw 'Dependency update failed.' }",
|
||||
" Push-Location $InstallDir",
|
||||
" try { $InstalledVersion = & $Python -c \"import erp_local_agent; print(getattr(erp_local_agent,'__version__',''))\" } finally { Pop-Location }",
|
||||
" if (($InstalledVersion | Out-String).Trim() -ne $TargetVersion) { throw ('Installed version verification failed. Expected ' + $TargetVersion + ', got ' + (($InstalledVersion | Out-String).Trim())) }",
|
||||
" Start-ScheduledTask -TaskName $TaskName",
|
||||
"} catch {",
|
||||
" try { if (Test-Path (Join-Path $BackupDir 'erp_local_agent')) { Remove-Item (Join-Path $InstallDir 'erp_local_agent') -Recurse -Force -ErrorAction SilentlyContinue; Copy-Item (Join-Path $BackupDir 'erp_local_agent') -Destination $InstallDir -Recurse -Force }; Start-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue } catch {}",
|
||||
" New-Item -ItemType Directory -Force -Path (Join-Path $InstallDir 'logs') | Out-Null",
|
||||
" Add-Content -Path (Join-Path $InstallDir 'logs\\update_error.log') -Value ((Get-Date).ToString('s') + ' ' + $_.Exception.Message)",
|
||||
" exit 1",
|
||||
"}",
|
||||
]
|
||||
return "\r\n".join(lines) + "\r\n"
|
||||
|
||||
Reference in New Issue
Block a user