From 6a3be55cee6972bdf17045aee8de0f5fa3baac0d Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Fri, 9 Jan 2026 16:55:43 +0000 Subject: [PATCH] feat(daemon): add automatic update mechanism (Updater), switch to system journal logs, and expose update controls in the UI --- .gitea/workflows/release.yml | 12 +- changelog.md | 10 + ecoos_daemon/ts/daemon/index.ts | 64 ++++- ecoos_daemon/ts/daemon/updater.ts | 270 ++++++++++++++++++ ecoos_daemon/ts/ui/server.ts | 120 +++++++- ecoos_daemon/ts/version.ts | 2 +- .../includes.chroot/opt/eco/bin/eco-daemon | Bin 86948075 -> 86979372 bytes package.json | 4 +- 8 files changed, 449 insertions(+), 33 deletions(-) create mode 100644 ecoos_daemon/ts/daemon/updater.ts diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index 35c4ec9..7c2b6cf 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -32,16 +32,8 @@ jobs: npm version ${{ steps.version.outputs.version_number }} --no-git-tag-version --allow-same-version echo "export const VERSION = \"${{ steps.version.outputs.version_number }}\";" > ecoos_daemon/ts/version.ts - - name: Build daemon binary - run: pnpm run daemon:bundle - - - name: Build ISO with Docker - run: | - cp ecoos_daemon/bundle/eco-daemon isobuild/config/includes.chroot/opt/eco/bin/ - mkdir -p .nogit/iso - docker build -t ecoos-builder -f isobuild/Dockerfile . - docker run --rm --privileged -v ${{ github.workspace }}/.nogit/iso:/output ecoos-builder - ls -la .nogit/iso/ + - name: Build ISO + run: pnpm run build - name: Prepare release assets run: | diff --git a/changelog.md b/changelog.md index f4c3621..a659fe2 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,15 @@ # Changelog +## 2026-01-09 - 0.3.0 - feat(daemon) +add automatic update mechanism (Updater), switch to system journal logs, and expose update controls in the UI + +- Introduce Updater class: fetches releases from Gitea, computes auto-upgrade eligibility, downloads daemon binary, replaces binary and restarts service. +- Integrate updater into EcoDaemon: new methods getUpdateInfo, checkForUpdates, upgradeToVersion; run initial update check on startup and periodic auto-upgrade checks (hourly). +- Replace serial console reader with a journalctl-based system journal reader; rename serialLogs → systemLogs and update related logic and limits. +- UI/server: add API endpoints /api/updates, /api/updates/check and /api/upgrade; add an Updates panel to show current version, available releases, auto-upgrade status, and client-side actions to check and trigger upgrades; poll update info periodically. +- Version bump to 0.2.2 (package.json and ecoos_daemon/ts/version.ts). +- Build/workflow changes: release workflow now runs build step (Build ISO) and package.json build script adjusted for CI and updated Docker build/run handling. + ## 2026-01-09 - 0.2.1 - fix(ci) use GitHub Actions workspace for docker volume and add listing of build output directory for debugging diff --git a/ecoos_daemon/ts/daemon/index.ts b/ecoos_daemon/ts/daemon/index.ts index c648f5c..e0a31aa 100644 --- a/ecoos_daemon/ts/daemon/index.ts +++ b/ecoos_daemon/ts/daemon/index.ts @@ -6,6 +6,7 @@ import { ProcessManager } from './process-manager.ts'; import { SystemInfo } from './system-info.ts'; +import { Updater } from './updater.ts'; import { UIServer } from '../ui/server.ts'; import { runCommand } from '../utils/command.ts'; import { VERSION } from '../version.ts'; @@ -28,12 +29,14 @@ export class EcoDaemon { private config: DaemonConfig; private processManager: ProcessManager; private systemInfo: SystemInfo; + private updater: Updater; private uiServer: UIServer; private logs: string[] = []; - private serialLogs: string[] = []; + private systemLogs: string[] = []; private swayStatus: ServiceStatus = { state: 'stopped' }; private chromiumStatus: ServiceStatus = { state: 'stopped' }; private manualRestartUntil: number = 0; // Timestamp until which auto-restart is disabled + private lastAutoUpgradeCheck: number = 0; // Timestamp of last auto-upgrade check constructor(config?: Partial) { this.config = { @@ -45,6 +48,7 @@ export class EcoDaemon { this.processManager = new ProcessManager(this.config.user); this.systemInfo = new SystemInfo(); + this.updater = new Updater((msg) => this.log(msg)); this.uiServer = new UIServer(this.config.uiPort, this); } @@ -64,8 +68,8 @@ export class EcoDaemon { return [...this.logs]; } - getSerialLogs(): string[] { - return [...this.serialLogs]; + getSystemLogs(): string[] { + return [...this.systemLogs]; } async getStatus(): Promise> { @@ -78,7 +82,7 @@ export class EcoDaemon { chromiumStatus: this.chromiumStatus, systemInfo, logs: this.logs.slice(-50), - serialLogs: this.serialLogs.slice(-50), + systemLogs: this.systemLogs.slice(-50), }; } @@ -131,6 +135,18 @@ export class EcoDaemon { } } + async getUpdateInfo(): Promise { + return this.updater.getUpdateInfo(); + } + + async checkForUpdates(): Promise { + await this.updater.checkForUpdates(); + } + + async upgradeToVersion(version: string): Promise<{ success: boolean; message: string }> { + return this.updater.upgradeToVersion(version); + } + async start(): Promise { this.log('EcoOS Daemon starting...'); @@ -139,8 +155,11 @@ export class EcoDaemon { await this.uiServer.start(); this.log('Management UI started successfully'); - // Start serial console reader in the background - this.startSerialReader(); + // Start system journal reader in the background + this.startJournalReader(); + + // Check for updates on startup + this.updater.checkForUpdates().catch((e) => this.log(`Initial update check failed: ${e}`)); // Start the Sway/Chromium initialization in the background // This allows the UI server to remain responsive even if Sway fails @@ -313,12 +332,19 @@ export class EcoDaemon { return parseInt(result.stdout.trim(), 10); } - private startSerialReader(): void { + private startJournalReader(): void { (async () => { try { - const file = await Deno.open('/dev/ttyS0', { read: true }); - this.log('Serial console reader started on /dev/ttyS0'); - const reader = file.readable.getReader(); + const cmd = new Deno.Command('journalctl', { + args: ['-f', '--no-pager', '-n', '100', '-o', 'short-iso'], + stdout: 'piped', + stderr: 'piped', + }); + + const process = cmd.spawn(); + this.log('System journal reader started'); + + const reader = process.stdout.getReader(); const decoder = new TextDecoder(); while (true) { @@ -326,14 +352,14 @@ export class EcoDaemon { if (done) break; const text = decoder.decode(value); for (const line of text.split('\n').filter((l) => l.trim())) { - this.serialLogs.push(line); - if (this.serialLogs.length > 1000) { - this.serialLogs = this.serialLogs.slice(-1000); + this.systemLogs.push(line); + if (this.systemLogs.length > 1000) { + this.systemLogs = this.systemLogs.slice(-1000); } } } } catch (error) { - this.log(`Serial reader not available: ${error}`); + this.log(`Journal reader not available: ${error}`); } })(); } @@ -383,6 +409,16 @@ export class EcoDaemon { await this.tryStartSwayAndChromium(); } } + + // Check for auto-upgrades every hour + const now = Date.now(); + const oneHour = 60 * 60 * 1000; + if (now - this.lastAutoUpgradeCheck > oneHour) { + this.lastAutoUpgradeCheck = now; + this.updater.checkAutoUpgrade().catch((e) => + this.log(`Auto-upgrade check failed: ${e}`) + ); + } } catch (error) { this.log(`Error in monitoring loop: ${error}`); } diff --git a/ecoos_daemon/ts/daemon/updater.ts b/ecoos_daemon/ts/daemon/updater.ts new file mode 100644 index 0000000..5b6067e --- /dev/null +++ b/ecoos_daemon/ts/daemon/updater.ts @@ -0,0 +1,270 @@ +/** + * Updater + * + * Handles checking for updates, downloading new versions, and performing upgrades + */ + +import { VERSION } from '../version.ts'; +import { runCommand } from '../utils/command.ts'; + +export interface Release { + version: string; + tagName: string; + publishedAt: Date; + downloadUrl: string; + isCurrent: boolean; + isNewer: boolean; + ageHours: number; +} + +export interface AutoUpgradeStatus { + enabled: boolean; + targetVersion: string | null; + scheduledIn: string | null; + waitingForStability: boolean; +} + +export interface UpdateInfo { + currentVersion: string; + releases: Release[]; + autoUpgrade: AutoUpgradeStatus; + lastCheck: string | null; +} + +interface GiteaRelease { + id: number; + tag_name: string; + name: string; + body: string; + published_at: string; + assets: GiteaAsset[]; +} + +interface GiteaAsset { + id: number; + name: string; + browser_download_url: string; + size: number; +} + +export class Updater { + private repoApiUrl = 'https://code.foss.global/api/v1/repos/ecobridge/eco-os/releases'; + private binaryPath = '/opt/eco/bin/eco-daemon'; + private releases: Release[] = []; + private lastCheck: Date | null = null; + private logFn: (msg: string) => void; + + constructor(logFn: (msg: string) => void) { + this.logFn = logFn; + } + + private log(message: string): void { + this.logFn(`[Updater] ${message}`); + } + + /** + * Compare semantic versions + * Returns: -1 if a < b, 0 if a == b, 1 if a > b + */ + private compareVersions(a: string, b: string): number { + const partsA = a.replace(/^v/, '').split('.').map(Number); + const partsB = b.replace(/^v/, '').split('.').map(Number); + + for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) { + const numA = partsA[i] || 0; + const numB = partsB[i] || 0; + if (numA < numB) return -1; + if (numA > numB) return 1; + } + return 0; + } + + /** + * Fetch available releases from Gitea + */ + async checkForUpdates(): Promise { + this.log('Checking for updates...'); + + try { + const response = await fetch(this.repoApiUrl); + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + const giteaReleases: GiteaRelease[] = await response.json(); + const currentVersion = VERSION; + const now = new Date(); + + this.releases = giteaReleases + .filter((r) => r.tag_name.startsWith('v')) + .map((r) => { + const version = r.tag_name.replace(/^v/, ''); + const publishedAt = new Date(r.published_at); + const ageMs = now.getTime() - publishedAt.getTime(); + const ageHours = ageMs / (1000 * 60 * 60); + + // Find the daemon binary asset + const daemonAsset = r.assets.find((a) => + a.name.includes('eco-daemon') + ); + + return { + version, + tagName: r.tag_name, + publishedAt, + downloadUrl: daemonAsset?.browser_download_url || '', + isCurrent: version === currentVersion, + isNewer: this.compareVersions(version, currentVersion) > 0, + ageHours: Math.round(ageHours * 10) / 10, + }; + }) + .filter((r) => r.downloadUrl) // Only include releases with daemon binary + .sort((a, b) => this.compareVersions(b.version, a.version)); // Newest first + + this.lastCheck = now; + this.log(`Found ${this.releases.length} releases, ${this.releases.filter((r) => r.isNewer).length} newer than current`); + + return this.releases; + } catch (error) { + this.log(`Failed to check for updates: ${error}`); + return this.releases; + } + } + + /** + * Get cached releases (call checkForUpdates first) + */ + getReleases(): Release[] { + return this.releases; + } + + /** + * Determine if auto-upgrade should happen and to which version + */ + getAutoUpgradeStatus(): AutoUpgradeStatus { + const newerReleases = this.releases.filter((r) => r.isNewer); + + if (newerReleases.length === 0) { + return { + enabled: true, + targetVersion: null, + scheduledIn: null, + waitingForStability: false, + }; + } + + // Find the latest newer release + const latest = newerReleases[0]; + const hoursUntilUpgrade = 24 - latest.ageHours; + + if (hoursUntilUpgrade <= 0) { + // Ready to upgrade now + return { + enabled: true, + targetVersion: latest.version, + scheduledIn: 'now', + waitingForStability: false, + }; + } + + // Still waiting for stability period + const hours = Math.floor(hoursUntilUpgrade); + const minutes = Math.round((hoursUntilUpgrade - hours) * 60); + const scheduledIn = hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`; + + return { + enabled: true, + targetVersion: latest.version, + scheduledIn, + waitingForStability: true, + }; + } + + /** + * Get full update info for API response + */ + getUpdateInfo(): UpdateInfo { + return { + currentVersion: VERSION, + releases: this.releases, + autoUpgrade: this.getAutoUpgradeStatus(), + lastCheck: this.lastCheck?.toISOString() || null, + }; + } + + /** + * Download and install a specific version + */ + async upgradeToVersion(version: string): Promise<{ success: boolean; message: string }> { + const release = this.releases.find((r) => r.version === version); + + if (!release) { + return { success: false, message: `Version ${version} not found` }; + } + + if (release.isCurrent) { + return { success: false, message: `Already running version ${version}` }; + } + + this.log(`Starting upgrade to version ${version}...`); + + try { + // Download new binary + const tempPath = '/tmp/eco-daemon-new'; + this.log(`Downloading from ${release.downloadUrl}...`); + + const response = await fetch(release.downloadUrl); + if (!response.ok) { + throw new Error(`Download failed: HTTP ${response.status}`); + } + + const data = await response.arrayBuffer(); + await Deno.writeFile(tempPath, new Uint8Array(data)); + + // Verify download + const stat = await Deno.stat(tempPath); + if (stat.size < 1000000) { + // Daemon should be at least 1MB + throw new Error(`Downloaded file too small: ${stat.size} bytes`); + } + this.log(`Downloaded ${stat.size} bytes`); + + // Make executable + await Deno.chmod(tempPath, 0o755); + + // Replace binary + this.log('Replacing binary...'); + await runCommand('mv', [tempPath, this.binaryPath]); + await Deno.chmod(this.binaryPath, 0o755); + + // Restart daemon via systemd + this.log('Restarting daemon...'); + // Use spawn to avoid waiting for the restart + const restartCmd = new Deno.Command('systemctl', { + args: ['restart', 'eco-daemon'], + stdout: 'null', + stderr: 'null', + }); + restartCmd.spawn(); + + return { success: true, message: `Upgrading to v${version}...` }; + } catch (error) { + this.log(`Upgrade failed: ${error}`); + return { success: false, message: String(error) }; + } + } + + /** + * Check and perform auto-upgrade if conditions are met + */ + async checkAutoUpgrade(): Promise { + await this.checkForUpdates(); + + const status = this.getAutoUpgradeStatus(); + + if (status.targetVersion && status.scheduledIn === 'now') { + this.log(`Auto-upgrading to version ${status.targetVersion}...`); + await this.upgradeToVersion(status.targetVersion); + } + } +} diff --git a/ecoos_daemon/ts/ui/server.ts b/ecoos_daemon/ts/ui/server.ts index 64a4cf5..0219324 100644 --- a/ecoos_daemon/ts/ui/server.ts +++ b/ecoos_daemon/ts/ui/server.ts @@ -104,6 +104,31 @@ export class UIServer { return new Response(JSON.stringify(result), { headers }); } + if (path === '/api/updates') { + const updates = await this.daemon.getUpdateInfo(); + return new Response(JSON.stringify(updates), { headers }); + } + + if (path === '/api/updates/check' && req.method === 'POST') { + await this.daemon.checkForUpdates(); + const updates = await this.daemon.getUpdateInfo(); + return new Response(JSON.stringify(updates), { headers }); + } + + if (path === '/api/upgrade' && req.method === 'POST') { + try { + const body = await req.json(); + const version = body.version; + if (!version) { + return new Response(JSON.stringify({ success: false, message: 'Version required' }), { headers }); + } + const result = await this.daemon.upgradeToVersion(version); + return new Response(JSON.stringify(result), { headers }); + } catch (error) { + return new Response(JSON.stringify({ success: false, message: String(error) }), { headers }); + } + } + return new Response(JSON.stringify({ error: 'Not Found' }), { status: 404, headers, @@ -347,6 +372,18 @@ export class UIServer {
+
+

Updates

+
+
Current Version
+
-
+
+
+
+ +

Input Devices

@@ -362,7 +399,7 @@ export class UIServer {
Daemon Logs
-
Serial Console
+
System Logs
@@ -522,13 +559,13 @@ export class UIServer { logsEl.scrollTop = logsEl.scrollHeight; } - // Serial Logs - if (data.serialLogs) { + // System Logs + if (data.systemLogs) { const serialEl = document.getElementById('serial-logs'); - if (data.serialLogs.length === 0) { - serialEl.innerHTML = '
No serial data available
'; + if (data.systemLogs.length === 0) { + serialEl.innerHTML = '
No system logs available
'; } else { - serialEl.innerHTML = data.serialLogs.map(l => + serialEl.innerHTML = data.systemLogs.map(l => '
' + l + '
' ).join(''); serialEl.scrollTop = serialEl.scrollHeight; @@ -590,6 +627,77 @@ export class UIServer { }); } + function checkForUpdates() { + fetch('/api/updates/check', { method: 'POST' }) + .then(r => r.json()) + .then(updateUpdatesUI) + .catch(err => console.error('Failed to check updates:', err)); + } + + function upgradeToVersion(version) { + if (!confirm('Upgrade to version ' + version + '? The daemon will restart.')) return; + + fetch('/api/upgrade', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ version: version }) + }) + .then(r => r.json()) + .then(result => { + if (result.success) { + document.getElementById('auto-upgrade-status').textContent = result.message; + } else { + alert('Upgrade failed: ' + result.message); + } + }) + .catch(err => alert('Upgrade error: ' + err)); + } + + function updateUpdatesUI(data) { + document.getElementById('current-version').textContent = 'v' + data.currentVersion; + + const list = document.getElementById('updates-list'); + const newerReleases = data.releases.filter(r => r.isNewer); + + if (newerReleases.length === 0) { + list.innerHTML = '
No updates available
'; + } else { + list.innerHTML = newerReleases.map(r => + '
' + + 'v' + r.version + ' (' + formatAge(r.ageHours) + ')' + + '' + + '
' + ).join(''); + } + + const autoStatus = document.getElementById('auto-upgrade-status'); + if (data.autoUpgrade.targetVersion) { + if (data.autoUpgrade.waitingForStability) { + autoStatus.textContent = 'Auto-upgrade to v' + data.autoUpgrade.targetVersion + ' in ' + data.autoUpgrade.scheduledIn + ' (stability period)'; + } else { + autoStatus.textContent = 'Auto-upgrade to v' + data.autoUpgrade.targetVersion + ' pending...'; + } + } else { + autoStatus.textContent = data.lastCheck ? 'Last checked: ' + new Date(data.lastCheck).toLocaleTimeString() : ''; + } + } + + function formatAge(hours) { + if (hours < 1) return Math.round(hours * 60) + 'm ago'; + if (hours < 24) return Math.round(hours) + 'h ago'; + return Math.round(hours / 24) + 'd ago'; + } + + // Fetch updates info periodically + function fetchUpdates() { + fetch('/api/updates') + .then(r => r.json()) + .then(updateUpdatesUI) + .catch(err => console.error('Failed to fetch updates:', err)); + } + fetchUpdates(); + setInterval(fetchUpdates, 60000); // Check every minute + // Initial fetch fetch('/api/status') .then(r => r.json()) diff --git a/ecoos_daemon/ts/version.ts b/ecoos_daemon/ts/version.ts index 782835e..f6c20ef 100644 --- a/ecoos_daemon/ts/version.ts +++ b/ecoos_daemon/ts/version.ts @@ -1 +1 @@ -export const VERSION = "0.1.3"; +export const VERSION = "0.2.3"; diff --git a/isobuild/config/includes.chroot/opt/eco/bin/eco-daemon b/isobuild/config/includes.chroot/opt/eco/bin/eco-daemon index ba03775e80b1ba7dd3191f03b5c42493545a111c..e14c1df403494dc663f0446ee172032111220a9a 100755 GIT binary patch delta 29550 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 diff --git a/package.json b/package.json index fb7ea2d..9b6f489 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,9 @@ { "name": "@ecobridge/eco-os", - "version": "0.2.1", + "version": "0.2.3", "private": true, "scripts": { - "build": "npm version patch --no-git-tag-version && node -e \"const v=require('./package.json').version; require('fs').writeFileSync('ecoos_daemon/ts/version.ts', 'export const VERSION = \\\"'+v+'\\\";\\n');\" && pnpm run daemon:bundle && cp ecoos_daemon/bundle/eco-daemon isobuild/config/includes.chroot/opt/eco/bin/ && mkdir -p .nogit/iso && docker build --no-cache -t ecoos-builder -f isobuild/Dockerfile . && docker run --rm --privileged -v $(pwd)/.nogit/iso:/output ecoos-builder", + "build": "[ -z \"$CI\" ] && npm version patch --no-git-tag-version || true && node -e \"const v=require('./package.json').version; require('fs').writeFileSync('ecoos_daemon/ts/version.ts', 'export const VERSION = \\\"'+v+'\\\";\\n');\" && pnpm run daemon:bundle && cp ecoos_daemon/bundle/eco-daemon isobuild/config/includes.chroot/opt/eco/bin/ && mkdir -p .nogit/iso && docker build --no-cache -t ecoos-builder -f isobuild/Dockerfile . && docker run --privileged --name ecoos-build ecoos-builder && docker cp ecoos-build:/output/ecoos.iso .nogit/iso/ecoos.iso && docker rm ecoos-build", "daemon:dev": "cd ecoos_daemon && deno run --allow-all --watch mod.ts", "daemon:start": "cd ecoos_daemon && deno run --allow-all mod.ts", "daemon:typecheck": "cd ecoos_daemon && deno check mod.ts",