update
This commit is contained in:
320
ecoos_daemon/ts/daemon/index.ts
Normal file
320
ecoos_daemon/ts/daemon/index.ts
Normal file
@@ -0,0 +1,320 @@
|
||||
/**
|
||||
* EcoOS Daemon
|
||||
*
|
||||
* Main daemon class that orchestrates system services
|
||||
*/
|
||||
|
||||
import { ProcessManager } from './process-manager.ts';
|
||||
import { SystemInfo } from './system-info.ts';
|
||||
import { UIServer } from '../ui/server.ts';
|
||||
|
||||
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;
|
||||
private uiServer: UIServer;
|
||||
private logs: string[] = [];
|
||||
private swayStatus: ServiceStatus = { state: 'stopped' };
|
||||
private chromeStatus: ServiceStatus = { state: 'stopped' };
|
||||
|
||||
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();
|
||||
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];
|
||||
}
|
||||
|
||||
getStatus(): Record<string, unknown> {
|
||||
return {
|
||||
sway: this.swayStatus.state === 'running',
|
||||
swayStatus: this.swayStatus,
|
||||
chrome: this.chromeStatus.state === 'running',
|
||||
chromeStatus: this.chromeStatus,
|
||||
systemInfo: this.systemInfo.getInfo(),
|
||||
logs: this.logs.slice(-50),
|
||||
};
|
||||
}
|
||||
|
||||
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');
|
||||
|
||||
// Start the Sway/Chrome 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 Chrome
|
||||
await this.tryStartSwayAndChrome();
|
||||
} catch (error) {
|
||||
this.log(`Service initialization error: ${error}`);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
private async tryStartSwayAndChrome(): 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 Chrome in kiosk mode
|
||||
await this.startChromeAfterSway();
|
||||
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 startChromeAfterSway(): Promise<void> {
|
||||
this.chromeStatus = { state: 'starting', lastAttempt: new Date().toISOString() };
|
||||
this.log('Starting Chrome browser...');
|
||||
|
||||
try {
|
||||
await this.startChrome();
|
||||
this.chromeStatus = { state: 'running' };
|
||||
this.log('Chrome browser started');
|
||||
} catch (error) {
|
||||
this.chromeStatus = {
|
||||
state: 'failed',
|
||||
error: String(error),
|
||||
lastAttempt: new Date().toISOString()
|
||||
};
|
||||
this.log(`Failed to start Chrome: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async ensureSeatd(): Promise<void> {
|
||||
const status = await this.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 this.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 this.runCommand('mkdir', ['-p', runtimeDir]);
|
||||
await this.runCommand('chown', [`${this.config.user}:${this.config.user}`, runtimeDir]);
|
||||
await this.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 startChrome(): Promise<void> {
|
||||
const uid = await this.getUserUid();
|
||||
const runtimeDir = `/run/user/${uid}`;
|
||||
|
||||
await this.processManager.startChrome({
|
||||
runtimeDir,
|
||||
waylandDisplay: this.config.waylandDisplay,
|
||||
url: 'http://localhost:' + this.config.uiPort,
|
||||
kiosk: true,
|
||||
});
|
||||
}
|
||||
|
||||
private async getUserUid(): Promise<number> {
|
||||
const result = await this.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);
|
||||
}
|
||||
|
||||
private async runCommand(
|
||||
cmd: string,
|
||||
args: string[]
|
||||
): Promise<{ success: boolean; stdout: string; stderr: string }> {
|
||||
const command = new Deno.Command(cmd, {
|
||||
args,
|
||||
stdout: 'piped',
|
||||
stderr: 'piped',
|
||||
});
|
||||
|
||||
const result = await command.output();
|
||||
return {
|
||||
success: result.success,
|
||||
stdout: new TextDecoder().decode(result.stdout),
|
||||
stderr: new TextDecoder().decode(result.stderr),
|
||||
};
|
||||
}
|
||||
|
||||
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.chromeStatus = { state: 'stopped' };
|
||||
await this.tryStartSwayAndChrome();
|
||||
}
|
||||
|
||||
// If Sway is running but Chrome died, restart Chrome
|
||||
if (this.swayStatus.state === 'running' && this.chromeStatus.state === 'running'
|
||||
&& !this.processManager.isChromeRunning()) {
|
||||
this.log('Chrome process died, attempting restart...');
|
||||
this.chromeStatus = { state: 'starting', lastAttempt: new Date().toISOString() };
|
||||
try {
|
||||
await this.startChrome();
|
||||
this.chromeStatus = { state: 'running' };
|
||||
this.log('Chrome browser restarted');
|
||||
} catch (error) {
|
||||
this.chromeStatus = {
|
||||
state: 'failed',
|
||||
error: String(error),
|
||||
lastAttempt: new Date().toISOString()
|
||||
};
|
||||
this.log(`Failed to restart Chrome: ${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.tryStartSwayAndChrome();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.log(`Error in monitoring loop: ${error}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user