2026-01-08 18:33:14 +00:00
|
|
|
/**
|
|
|
|
|
* EcoOS Daemon
|
|
|
|
|
*
|
|
|
|
|
* Main daemon class that orchestrates system services
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import { ProcessManager } from './process-manager.ts';
|
2026-01-09 18:14:26 +00:00
|
|
|
import { SystemInfo, type DisplayInfo } from './system-info.ts';
|
2026-01-09 16:55:43 +00:00
|
|
|
import { Updater } from './updater.ts';
|
2026-01-08 18:33:14 +00:00
|
|
|
import { UIServer } from '../ui/server.ts';
|
|
|
|
|
import { runCommand } from '../utils/command.ts';
|
2026-01-09 14:34:51 +00:00
|
|
|
import { VERSION } from '../version.ts';
|
2026-01-08 18:33:14 +00:00
|
|
|
|
|
|
|
|
export interface DaemonConfig {
|
|
|
|
|
uiPort: number;
|
|
|
|
|
user: string;
|
|
|
|
|
waylandDisplay: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export type ServiceState = 'stopped' | 'starting' | 'running' | 'failed';
|
|
|
|
|
|
|
|
|
|
export interface ServiceStatus {
|
|
|
|
|
state: ServiceState;
|
|
|
|
|
error?: string;
|
|
|
|
|
lastAttempt?: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export class EcoDaemon {
|
|
|
|
|
private config: DaemonConfig;
|
|
|
|
|
private processManager: ProcessManager;
|
|
|
|
|
private systemInfo: SystemInfo;
|
2026-01-09 16:55:43 +00:00
|
|
|
private updater: Updater;
|
2026-01-08 18:33:14 +00:00
|
|
|
private uiServer: UIServer;
|
|
|
|
|
private logs: string[] = [];
|
2026-01-09 16:55:43 +00:00
|
|
|
private systemLogs: string[] = [];
|
2026-01-08 18:33:14 +00:00
|
|
|
private swayStatus: ServiceStatus = { state: 'stopped' };
|
|
|
|
|
private chromiumStatus: ServiceStatus = { state: 'stopped' };
|
2026-01-09 09:41:47 +00:00
|
|
|
private manualRestartUntil: number = 0; // Timestamp until which auto-restart is disabled
|
2026-01-09 16:55:43 +00:00
|
|
|
private lastAutoUpgradeCheck: number = 0; // Timestamp of last auto-upgrade check
|
2026-01-08 18:33:14 +00:00
|
|
|
|
|
|
|
|
constructor(config?: Partial<DaemonConfig>) {
|
|
|
|
|
this.config = {
|
|
|
|
|
uiPort: 3006,
|
|
|
|
|
user: 'ecouser',
|
|
|
|
|
waylandDisplay: 'wayland-1',
|
|
|
|
|
...config,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
this.processManager = new ProcessManager(this.config.user);
|
|
|
|
|
this.systemInfo = new SystemInfo();
|
2026-01-09 16:55:43 +00:00
|
|
|
this.updater = new Updater((msg) => this.log(msg));
|
2026-01-08 18:33:14 +00:00
|
|
|
this.uiServer = new UIServer(this.config.uiPort, this);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
log(message: string): void {
|
|
|
|
|
const timestamp = new Date().toISOString();
|
|
|
|
|
const entry = `[${timestamp}] ${message}`;
|
|
|
|
|
this.logs.push(entry);
|
|
|
|
|
console.log(entry);
|
|
|
|
|
|
|
|
|
|
// Keep last 1000 log entries
|
|
|
|
|
if (this.logs.length > 1000) {
|
|
|
|
|
this.logs = this.logs.slice(-1000);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getLogs(): string[] {
|
|
|
|
|
return [...this.logs];
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-09 16:55:43 +00:00
|
|
|
getSystemLogs(): string[] {
|
|
|
|
|
return [...this.systemLogs];
|
2026-01-09 14:34:51 +00:00
|
|
|
}
|
|
|
|
|
|
2026-01-08 18:33:14 +00:00
|
|
|
async getStatus(): Promise<Record<string, unknown>> {
|
|
|
|
|
const systemInfo = await this.systemInfo.getInfo();
|
|
|
|
|
return {
|
2026-01-09 14:34:51 +00:00
|
|
|
version: VERSION,
|
2026-01-08 18:33:14 +00:00
|
|
|
sway: this.swayStatus.state === 'running',
|
|
|
|
|
swayStatus: this.swayStatus,
|
|
|
|
|
chromium: this.chromiumStatus.state === 'running',
|
|
|
|
|
chromiumStatus: this.chromiumStatus,
|
|
|
|
|
systemInfo,
|
|
|
|
|
logs: this.logs.slice(-50),
|
2026-01-09 16:55:43 +00:00
|
|
|
systemLogs: this.systemLogs.slice(-50),
|
2026-01-08 18:33:14 +00:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async rebootSystem(): Promise<{ success: boolean; message: string }> {
|
|
|
|
|
this.log('System reboot requested...');
|
|
|
|
|
try {
|
|
|
|
|
const result = await runCommand('systemctl', ['reboot']);
|
|
|
|
|
if (result.success) {
|
|
|
|
|
return { success: true, message: 'System is rebooting...' };
|
|
|
|
|
}
|
|
|
|
|
return { success: false, message: 'Failed to reboot: ' + result.stderr };
|
|
|
|
|
} catch (error) {
|
|
|
|
|
this.log(`Reboot failed: ${error}`);
|
|
|
|
|
return { success: false, message: String(error) };
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async restartChromium(): Promise<{ success: boolean; message: string }> {
|
|
|
|
|
this.log('Chromium restart requested...');
|
|
|
|
|
|
|
|
|
|
if (this.swayStatus.state !== 'running') {
|
|
|
|
|
return { success: false, message: 'Cannot restart Chromium: Sway is not running' };
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-09 09:41:47 +00:00
|
|
|
// Disable auto-restart for 15 seconds to prevent restart loop
|
|
|
|
|
this.manualRestartUntil = Date.now() + 15000;
|
|
|
|
|
|
2026-01-08 18:33:14 +00:00
|
|
|
try {
|
|
|
|
|
// Stop existing Chromium
|
|
|
|
|
await this.processManager.stopBrowser();
|
|
|
|
|
this.chromiumStatus = { state: 'stopped' };
|
|
|
|
|
|
|
|
|
|
// Wait a moment before restarting
|
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
|
|
|
|
|
|
|
|
// Start Chromium again
|
|
|
|
|
await this.startChromium();
|
|
|
|
|
this.chromiumStatus = { state: 'running' };
|
|
|
|
|
this.log('Chromium browser restarted successfully');
|
|
|
|
|
|
|
|
|
|
return { success: true, message: 'Chromium restarted successfully' };
|
|
|
|
|
} catch (error) {
|
|
|
|
|
this.chromiumStatus = {
|
|
|
|
|
state: 'failed',
|
|
|
|
|
error: String(error),
|
|
|
|
|
lastAttempt: new Date().toISOString()
|
|
|
|
|
};
|
|
|
|
|
this.log(`Failed to restart Chromium: ${error}`);
|
|
|
|
|
return { success: false, message: String(error) };
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-09 16:55:43 +00:00
|
|
|
async getUpdateInfo(): Promise<unknown> {
|
|
|
|
|
return this.updater.getUpdateInfo();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async checkForUpdates(): Promise<void> {
|
|
|
|
|
await this.updater.checkForUpdates();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async upgradeToVersion(version: string): Promise<{ success: boolean; message: string }> {
|
|
|
|
|
return this.updater.upgradeToVersion(version);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-09 18:14:26 +00:00
|
|
|
async getDisplays(): Promise<DisplayInfo[]> {
|
|
|
|
|
if (this.swayStatus.state !== 'running') {
|
2026-01-09 18:51:22 +00:00
|
|
|
this.log(`[displays] Sway not running (state: ${this.swayStatus.state}), skipping display query`);
|
2026-01-09 18:14:26 +00:00
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
const uid = await this.getUserUid();
|
|
|
|
|
return this.processManager.getDisplays({
|
|
|
|
|
runtimeDir: `/run/user/${uid}`,
|
|
|
|
|
waylandDisplay: this.config.waylandDisplay,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async setDisplayEnabled(name: string, enabled: boolean): Promise<{ success: boolean; message: string }> {
|
|
|
|
|
if (this.swayStatus.state !== 'running') {
|
|
|
|
|
return { success: false, message: 'Sway is not running' };
|
|
|
|
|
}
|
|
|
|
|
this.log(`${enabled ? 'Enabling' : 'Disabling'} display ${name}`);
|
|
|
|
|
const uid = await this.getUserUid();
|
|
|
|
|
const result = await this.processManager.setDisplayEnabled(
|
|
|
|
|
{ runtimeDir: `/run/user/${uid}`, waylandDisplay: this.config.waylandDisplay },
|
|
|
|
|
name,
|
|
|
|
|
enabled
|
|
|
|
|
);
|
|
|
|
|
return { success: result, message: result ? `Display ${name} ${enabled ? 'enabled' : 'disabled'}` : 'Failed' };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async setKioskDisplay(name: string): Promise<{ success: boolean; message: string }> {
|
|
|
|
|
if (this.swayStatus.state !== 'running') {
|
|
|
|
|
return { success: false, message: 'Sway is not running' };
|
|
|
|
|
}
|
|
|
|
|
if (this.chromiumStatus.state !== 'running') {
|
|
|
|
|
return { success: false, message: 'Chromium is not running' };
|
|
|
|
|
}
|
|
|
|
|
this.log(`Moving kiosk to display ${name}`);
|
|
|
|
|
const uid = await this.getUserUid();
|
|
|
|
|
const result = await this.processManager.setKioskDisplay(
|
|
|
|
|
{ runtimeDir: `/run/user/${uid}`, waylandDisplay: this.config.waylandDisplay },
|
|
|
|
|
name
|
|
|
|
|
);
|
|
|
|
|
return { success: result, message: result ? `Kiosk moved to ${name}` : 'Failed' };
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-08 18:33:14 +00:00
|
|
|
async start(): Promise<void> {
|
|
|
|
|
this.log('EcoOS Daemon starting...');
|
|
|
|
|
|
|
|
|
|
// Start UI server first - this must always succeed and remain responsive
|
|
|
|
|
this.log('Starting management UI on port ' + this.config.uiPort);
|
|
|
|
|
await this.uiServer.start();
|
|
|
|
|
this.log('Management UI started successfully');
|
|
|
|
|
|
2026-01-09 16:55:43 +00:00
|
|
|
// 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}`));
|
2026-01-09 14:34:51 +00:00
|
|
|
|
2026-01-08 18:33:14 +00:00
|
|
|
// Start the Sway/Chromium initialization in the background
|
|
|
|
|
// This allows the UI server to remain responsive even if Sway fails
|
|
|
|
|
this.startServicesInBackground();
|
|
|
|
|
|
|
|
|
|
// Keep the daemon running
|
|
|
|
|
await this.runForever();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async startServicesInBackground(): Promise<void> {
|
|
|
|
|
// Run service initialization without blocking the main thread
|
|
|
|
|
(async () => {
|
|
|
|
|
try {
|
|
|
|
|
// Ensure seatd is running
|
|
|
|
|
this.log('Checking seatd service...');
|
|
|
|
|
await this.ensureSeatd();
|
|
|
|
|
|
|
|
|
|
// Try to start Sway and Chromium
|
|
|
|
|
await this.tryStartSwayAndChromium();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
this.log(`Service initialization error: ${error}`);
|
|
|
|
|
}
|
|
|
|
|
})();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async tryStartSwayAndChromium(): Promise<void> {
|
|
|
|
|
// Try DRM mode first, fall back to headless if it fails
|
|
|
|
|
const modes = ['drm', 'headless'] as const;
|
|
|
|
|
|
|
|
|
|
for (const mode of modes) {
|
|
|
|
|
// Stop any existing Sway process
|
|
|
|
|
await this.processManager.stopSway();
|
|
|
|
|
|
|
|
|
|
this.swayStatus = { state: 'starting', lastAttempt: new Date().toISOString() };
|
|
|
|
|
this.log(`Trying Sway with ${mode} backend...`);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await this.startSwayWithMode(mode);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
this.log(`Failed to start Sway with ${mode}: ${error}`);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Wait for Wayland socket
|
|
|
|
|
this.log('Waiting for Wayland socket...');
|
|
|
|
|
const waylandReady = await this.waitForWayland();
|
|
|
|
|
|
|
|
|
|
if (waylandReady) {
|
|
|
|
|
this.swayStatus = { state: 'running' };
|
|
|
|
|
this.log(`Sway compositor running with ${mode} backend`);
|
|
|
|
|
|
|
|
|
|
// Start Chromium in kiosk mode
|
|
|
|
|
await this.startChromiumAfterSway();
|
|
|
|
|
return;
|
|
|
|
|
} else {
|
|
|
|
|
this.log(`Sway ${mode} backend failed - Wayland socket did not appear`);
|
|
|
|
|
await this.processManager.stopSway();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// All modes failed
|
|
|
|
|
this.swayStatus = {
|
|
|
|
|
state: 'failed',
|
|
|
|
|
error: 'All Sway backends failed (tried drm and headless)',
|
|
|
|
|
lastAttempt: new Date().toISOString()
|
|
|
|
|
};
|
|
|
|
|
this.log('All Sway backend modes failed');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async startChromiumAfterSway(): Promise<void> {
|
|
|
|
|
this.chromiumStatus = { state: 'starting', lastAttempt: new Date().toISOString() };
|
|
|
|
|
this.log('Starting Chromium browser...');
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await this.startChromium();
|
|
|
|
|
this.chromiumStatus = { state: 'running' };
|
|
|
|
|
this.log('Chromium browser started');
|
|
|
|
|
} catch (error) {
|
|
|
|
|
this.chromiumStatus = {
|
|
|
|
|
state: 'failed',
|
|
|
|
|
error: String(error),
|
|
|
|
|
lastAttempt: new Date().toISOString()
|
|
|
|
|
};
|
|
|
|
|
this.log(`Failed to start Chromium: ${error}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async ensureSeatd(): Promise<void> {
|
|
|
|
|
const status = await runCommand('systemctl', ['is-active', 'seatd']);
|
|
|
|
|
if (status.success && status.stdout.trim() === 'active') {
|
|
|
|
|
this.log('seatd is already running');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.log('Starting seatd service...');
|
|
|
|
|
const result = await runCommand('systemctl', ['start', 'seatd']);
|
|
|
|
|
if (!result.success) {
|
|
|
|
|
this.log('Warning: Failed to start seatd: ' + result.stderr);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async startSwayWithMode(mode: 'drm' | 'headless'): Promise<void> {
|
|
|
|
|
const uid = await this.getUserUid();
|
|
|
|
|
|
|
|
|
|
// Ensure XDG_RUNTIME_DIR exists
|
|
|
|
|
const runtimeDir = `/run/user/${uid}`;
|
|
|
|
|
await runCommand('mkdir', ['-p', runtimeDir]);
|
|
|
|
|
await runCommand('chown', [`${this.config.user}:${this.config.user}`, runtimeDir]);
|
|
|
|
|
await runCommand('chmod', ['700', runtimeDir]);
|
|
|
|
|
|
|
|
|
|
if (mode === 'drm') {
|
|
|
|
|
this.log('Starting Sway with DRM backend (hardware rendering)');
|
|
|
|
|
await this.processManager.startSway({
|
|
|
|
|
runtimeDir,
|
|
|
|
|
backends: 'drm,libinput',
|
|
|
|
|
allowSoftwareRendering: true,
|
|
|
|
|
headless: false,
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
this.log('Starting Sway with headless backend (software rendering)');
|
|
|
|
|
await this.processManager.startSway({
|
|
|
|
|
runtimeDir,
|
|
|
|
|
backends: 'headless',
|
|
|
|
|
allowSoftwareRendering: true,
|
|
|
|
|
headless: true,
|
|
|
|
|
headlessOutputs: 1,
|
|
|
|
|
renderer: 'pixman',
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async waitForWayland(): Promise<boolean> {
|
|
|
|
|
const uid = await this.getUserUid();
|
|
|
|
|
const socketPath = `/run/user/${uid}/${this.config.waylandDisplay}`;
|
|
|
|
|
|
|
|
|
|
// Wait up to 10 seconds for Wayland socket to appear
|
|
|
|
|
for (let i = 0; i < 10; i++) {
|
|
|
|
|
try {
|
|
|
|
|
const stat = await Deno.stat(socketPath);
|
|
|
|
|
if (stat.isFile || stat.isSymlink || stat.mode !== undefined) {
|
|
|
|
|
this.log('Wayland socket ready: ' + socketPath);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
// Socket doesn't exist yet
|
|
|
|
|
}
|
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async startChromium(): Promise<void> {
|
|
|
|
|
const uid = await this.getUserUid();
|
|
|
|
|
const runtimeDir = `/run/user/${uid}`;
|
|
|
|
|
|
|
|
|
|
await this.processManager.startBrowser({
|
|
|
|
|
runtimeDir,
|
|
|
|
|
waylandDisplay: this.config.waylandDisplay,
|
|
|
|
|
url: 'http://localhost:' + this.config.uiPort,
|
|
|
|
|
kiosk: true,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async getUserUid(): Promise<number> {
|
|
|
|
|
const result = await runCommand('id', ['-u', this.config.user]);
|
|
|
|
|
if (!result.success) {
|
|
|
|
|
throw new Error('Failed to get user UID: ' + result.stderr);
|
|
|
|
|
}
|
|
|
|
|
return parseInt(result.stdout.trim(), 10);
|
|
|
|
|
}
|
2026-01-09 14:34:51 +00:00
|
|
|
|
2026-01-09 16:55:43 +00:00
|
|
|
private startJournalReader(): void {
|
2026-01-09 14:34:51 +00:00
|
|
|
(async () => {
|
|
|
|
|
try {
|
2026-01-09 16:55:43 +00:00
|
|
|
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();
|
2026-01-09 14:34:51 +00:00
|
|
|
const decoder = new TextDecoder();
|
|
|
|
|
|
|
|
|
|
while (true) {
|
|
|
|
|
const { value, done } = await reader.read();
|
|
|
|
|
if (done) break;
|
|
|
|
|
const text = decoder.decode(value);
|
|
|
|
|
for (const line of text.split('\n').filter((l) => l.trim())) {
|
2026-01-09 16:55:43 +00:00
|
|
|
this.systemLogs.push(line);
|
|
|
|
|
if (this.systemLogs.length > 1000) {
|
|
|
|
|
this.systemLogs = this.systemLogs.slice(-1000);
|
2026-01-09 14:34:51 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
2026-01-09 16:55:43 +00:00
|
|
|
this.log(`Journal reader not available: ${error}`);
|
2026-01-09 14:34:51 +00:00
|
|
|
}
|
|
|
|
|
})();
|
|
|
|
|
}
|
2026-01-08 18:33:14 +00:00
|
|
|
|
|
|
|
|
private async runForever(): Promise<void> {
|
|
|
|
|
// Monitor processes and restart if needed
|
|
|
|
|
while (true) {
|
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 5000));
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// If Sway was running but died, try to restart
|
|
|
|
|
if (this.swayStatus.state === 'running' && !this.processManager.isSwayRunning()) {
|
|
|
|
|
this.log('Sway process died, attempting restart...');
|
|
|
|
|
this.swayStatus = { state: 'stopped' };
|
|
|
|
|
this.chromiumStatus = { state: 'stopped' };
|
|
|
|
|
await this.tryStartSwayAndChromium();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If Sway is running but Chromium died, restart Chromium
|
2026-01-09 09:41:47 +00:00
|
|
|
// Skip if manual restart is in progress (prevents restart loop)
|
2026-01-08 18:33:14 +00:00
|
|
|
if (this.swayStatus.state === 'running' && this.chromiumStatus.state === 'running'
|
2026-01-09 09:41:47 +00:00
|
|
|
&& !(await this.processManager.isBrowserRunning()) && Date.now() > this.manualRestartUntil) {
|
2026-01-08 18:33:14 +00:00
|
|
|
this.log('Chromium process died, attempting restart...');
|
|
|
|
|
this.chromiumStatus = { state: 'starting', lastAttempt: new Date().toISOString() };
|
|
|
|
|
try {
|
|
|
|
|
await this.startChromium();
|
|
|
|
|
this.chromiumStatus = { state: 'running' };
|
|
|
|
|
this.log('Chromium browser restarted');
|
|
|
|
|
} catch (error) {
|
|
|
|
|
this.chromiumStatus = {
|
|
|
|
|
state: 'failed',
|
|
|
|
|
error: String(error),
|
|
|
|
|
lastAttempt: new Date().toISOString()
|
|
|
|
|
};
|
|
|
|
|
this.log(`Failed to restart Chromium: ${error}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If Sway failed, retry every 30 seconds
|
|
|
|
|
if (this.swayStatus.state === 'failed') {
|
|
|
|
|
const lastAttempt = this.swayStatus.lastAttempt
|
|
|
|
|
? new Date(this.swayStatus.lastAttempt).getTime()
|
|
|
|
|
: 0;
|
|
|
|
|
const now = Date.now();
|
|
|
|
|
if (now - lastAttempt > 30000) {
|
|
|
|
|
this.log('Retrying Sway startup...');
|
|
|
|
|
await this.tryStartSwayAndChromium();
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-09 16:55:43 +00:00
|
|
|
|
|
|
|
|
// 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}`)
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-01-08 18:33:14 +00:00
|
|
|
} catch (error) {
|
|
|
|
|
this.log(`Error in monitoring loop: ${error}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|