initial
This commit is contained in:
348
ecoos_daemon/ts/daemon/process-manager.ts
Normal file
348
ecoos_daemon/ts/daemon/process-manager.ts
Normal file
@@ -0,0 +1,348 @@
|
||||
/**
|
||||
* Process Manager
|
||||
*
|
||||
* Manages spawning and monitoring of Sway and Chromium processes
|
||||
*/
|
||||
|
||||
import { runCommand } from '../utils/command.ts';
|
||||
|
||||
export interface SwayConfig {
|
||||
runtimeDir: string;
|
||||
backends: string;
|
||||
allowSoftwareRendering: boolean;
|
||||
headless?: boolean;
|
||||
headlessOutputs?: number;
|
||||
renderer?: string;
|
||||
resolution?: string;
|
||||
}
|
||||
|
||||
export interface BrowserConfig {
|
||||
runtimeDir: string;
|
||||
waylandDisplay: string;
|
||||
url: string;
|
||||
kiosk?: boolean;
|
||||
}
|
||||
|
||||
export class ProcessManager {
|
||||
private user: string;
|
||||
private swayProcess: Deno.ChildProcess | null = null;
|
||||
private browserProcess: Deno.ChildProcess | null = null;
|
||||
|
||||
constructor(user: string) {
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate Sway configuration content for kiosk mode
|
||||
*/
|
||||
private generateSwayConfig(config: SwayConfig): string {
|
||||
const resolution = config.resolution || '1920x1080';
|
||||
|
||||
return `# EcoOS Sway Configuration (generated by eco-daemon)
|
||||
# Kiosk mode operation
|
||||
|
||||
# Variables
|
||||
set $mod Mod4
|
||||
|
||||
# Output configuration
|
||||
output * {
|
||||
bg #000000 solid_color
|
||||
resolution ${resolution}
|
||||
}
|
||||
|
||||
# For headless backend virtual outputs
|
||||
output HEADLESS-1 {
|
||||
resolution ${resolution}
|
||||
position 0 0
|
||||
}
|
||||
|
||||
# Input configuration
|
||||
input * {
|
||||
events enabled
|
||||
}
|
||||
|
||||
# Disable screen blanking for kiosk mode
|
||||
exec_always swaymsg "output * dpms on"
|
||||
|
||||
# No window decorations for fullscreen kiosk
|
||||
default_border none
|
||||
default_floating_border none
|
||||
|
||||
# Focus follows mouse
|
||||
focus_follows_mouse yes
|
||||
|
||||
# Force all windows fullscreen for kiosk mode
|
||||
for_window [app_id=".*"] fullscreen enable
|
||||
for_window [app_id="chromium-browser"] fullscreen enable
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write Sway config to user's config directory
|
||||
*/
|
||||
private async writeSwayConfig(config: SwayConfig): Promise<string> {
|
||||
const configDir = `/home/${this.user}/.config/sway`;
|
||||
const configPath = `${configDir}/config`;
|
||||
|
||||
// Create config directory
|
||||
await runCommand('mkdir', ['-p', configDir]);
|
||||
await runCommand('chown', [`${this.user}:${this.user}`, `/home/${this.user}/.config`]);
|
||||
await runCommand('chown', [`${this.user}:${this.user}`, configDir]);
|
||||
|
||||
// Write config file
|
||||
const configContent = this.generateSwayConfig(config);
|
||||
await Deno.writeTextFile(configPath, configContent);
|
||||
await runCommand('chown', [`${this.user}:${this.user}`, configPath]);
|
||||
|
||||
console.log(`[sway] Config written to ${configPath}`);
|
||||
return configPath;
|
||||
}
|
||||
|
||||
async startSway(config: SwayConfig): Promise<void> {
|
||||
// Write sway config before starting
|
||||
const configPath = await this.writeSwayConfig(config);
|
||||
|
||||
const env: Record<string, string> = {
|
||||
XDG_RUNTIME_DIR: config.runtimeDir,
|
||||
WLR_BACKENDS: config.backends,
|
||||
};
|
||||
|
||||
if (config.allowSoftwareRendering) {
|
||||
env.WLR_RENDERER_ALLOW_SOFTWARE = '1';
|
||||
}
|
||||
|
||||
// Headless mode configuration
|
||||
if (config.headless) {
|
||||
env.WLR_HEADLESS_OUTPUTS = String(config.headlessOutputs || 1);
|
||||
// Use libinput for input even in headless mode (for VNC/remote input)
|
||||
env.WLR_LIBINPUT_NO_DEVICES = '1';
|
||||
}
|
||||
|
||||
// Force specific renderer (pixman for software rendering)
|
||||
if (config.renderer) {
|
||||
env.WLR_RENDERER = config.renderer;
|
||||
}
|
||||
|
||||
// Build environment string for runuser
|
||||
const envString = Object.entries(env)
|
||||
.map(([k, v]) => `${k}=${v}`)
|
||||
.join(' ');
|
||||
|
||||
const command = new Deno.Command('runuser', {
|
||||
args: ['-u', this.user, '--', 'sh', '-c', `${envString} sway -c ${configPath}`],
|
||||
stdout: 'piped',
|
||||
stderr: 'piped',
|
||||
stdin: 'null',
|
||||
});
|
||||
|
||||
this.swayProcess = command.spawn();
|
||||
|
||||
// Log output in background
|
||||
this.pipeOutput(this.swayProcess, 'sway');
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a swaymsg command to control Sway
|
||||
*/
|
||||
async swaymsg(config: { runtimeDir: string; waylandDisplay: string }, command: string): Promise<boolean> {
|
||||
const env: Record<string, string> = {
|
||||
XDG_RUNTIME_DIR: config.runtimeDir,
|
||||
WAYLAND_DISPLAY: config.waylandDisplay,
|
||||
};
|
||||
|
||||
const envString = Object.entries(env)
|
||||
.map(([k, v]) => `${k}=${v}`)
|
||||
.join(' ');
|
||||
|
||||
const cmd = new Deno.Command('runuser', {
|
||||
args: [
|
||||
'-u',
|
||||
this.user,
|
||||
'--',
|
||||
'sh',
|
||||
'-c',
|
||||
`${envString} swaymsg '${command}'`,
|
||||
],
|
||||
stdout: 'piped',
|
||||
stderr: 'piped',
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await cmd.output();
|
||||
if (!result.success) {
|
||||
const stderr = new TextDecoder().decode(result.stderr);
|
||||
console.error(`[swaymsg] Command failed: ${stderr}`);
|
||||
}
|
||||
return result.success;
|
||||
} catch (error) {
|
||||
console.error(`[swaymsg] Error: ${error}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start Chromium browser in kiosk mode
|
||||
*/
|
||||
async startBrowser(config: BrowserConfig): Promise<void> {
|
||||
const env: Record<string, string> = {
|
||||
HOME: `/home/${this.user}`,
|
||||
XDG_RUNTIME_DIR: config.runtimeDir,
|
||||
WAYLAND_DISPLAY: config.waylandDisplay,
|
||||
XDG_CONFIG_HOME: `/home/${this.user}/.config`,
|
||||
XDG_DATA_HOME: `/home/${this.user}/.local/share`,
|
||||
};
|
||||
|
||||
// Chromium arguments for kiosk mode on Wayland
|
||||
const browserArgs = [
|
||||
'--ozone-platform=wayland',
|
||||
'--enable-features=UseOzonePlatform',
|
||||
'--kiosk',
|
||||
'--no-first-run',
|
||||
'--disable-infobars',
|
||||
'--disable-session-crashed-bubble',
|
||||
'--disable-restore-session-state',
|
||||
'--disable-background-networking',
|
||||
'--disable-sync',
|
||||
'--disable-translate',
|
||||
'--noerrdialogs',
|
||||
// Required for VM/headless/sandboxed environments
|
||||
'--no-sandbox',
|
||||
'--disable-setuid-sandbox',
|
||||
'--disable-dev-shm-usage',
|
||||
// GPU/rendering flags for VM environments
|
||||
'--disable-gpu',
|
||||
'--disable-gpu-compositing',
|
||||
'--disable-gpu-sandbox',
|
||||
'--disable-software-rasterizer',
|
||||
'--disable-accelerated-2d-canvas',
|
||||
'--disable-accelerated-video-decode',
|
||||
'--use-gl=swiftshader',
|
||||
'--in-process-gpu',
|
||||
// Disable features that may cause issues in kiosk mode
|
||||
'--disable-features=TranslateUI,VizDisplayCompositor',
|
||||
'--disable-hang-monitor',
|
||||
'--disable-breakpad',
|
||||
'--disable-component-update',
|
||||
config.url,
|
||||
];
|
||||
|
||||
// Build environment string for runuser
|
||||
const envString = Object.entries(env)
|
||||
.map(([k, v]) => `${k}=${v}`)
|
||||
.join(' ');
|
||||
|
||||
const command = new Deno.Command('runuser', {
|
||||
args: [
|
||||
'-u',
|
||||
this.user,
|
||||
'--',
|
||||
'sh',
|
||||
'-c',
|
||||
`${envString} chromium-browser ${browserArgs.join(' ')}`,
|
||||
],
|
||||
stdout: 'piped',
|
||||
stderr: 'piped',
|
||||
stdin: 'null',
|
||||
});
|
||||
|
||||
this.browserProcess = command.spawn();
|
||||
|
||||
// Log output in background
|
||||
this.pipeOutput(this.browserProcess, 'chromium');
|
||||
|
||||
// Force fullscreen via swaymsg after Chromium window appears (backup)
|
||||
this.forceFullscreenAfterDelay(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Force fullscreen for Chromium window after a delay (backup for kiosk mode)
|
||||
*/
|
||||
private async forceFullscreenAfterDelay(config: { runtimeDir: string; waylandDisplay: string }): Promise<void> {
|
||||
// Wait for Chromium window to appear
|
||||
await new Promise((resolve) => setTimeout(resolve, 3000));
|
||||
|
||||
console.log('[chromium] Forcing fullscreen via swaymsg');
|
||||
|
||||
// Try multiple selectors to ensure we catch the window
|
||||
const selectors = [
|
||||
'[app_id="chromium-browser"]',
|
||||
'[app_id="chromium"]',
|
||||
];
|
||||
|
||||
for (const selector of selectors) {
|
||||
await this.swaymsg(config, `${selector} fullscreen enable`);
|
||||
}
|
||||
|
||||
// Also try to focus the window
|
||||
await this.swaymsg(config, '[app_id="chromium-browser"] focus');
|
||||
}
|
||||
|
||||
isSwayRunning(): boolean {
|
||||
return this.swayProcess !== null;
|
||||
}
|
||||
|
||||
isBrowserRunning(): boolean {
|
||||
return this.browserProcess !== null;
|
||||
}
|
||||
|
||||
async stopSway(): Promise<void> {
|
||||
if (this.swayProcess) {
|
||||
try {
|
||||
this.swayProcess.kill('SIGTERM');
|
||||
await this.swayProcess.status;
|
||||
} catch {
|
||||
// Process may already be dead
|
||||
}
|
||||
this.swayProcess = null;
|
||||
}
|
||||
}
|
||||
|
||||
async stopBrowser(): Promise<void> {
|
||||
if (this.browserProcess) {
|
||||
try {
|
||||
this.browserProcess.kill('SIGTERM');
|
||||
await this.browserProcess.status;
|
||||
} catch {
|
||||
// Process may already be dead
|
||||
}
|
||||
this.browserProcess = null;
|
||||
}
|
||||
}
|
||||
|
||||
private async pipeOutput(
|
||||
process: Deno.ChildProcess,
|
||||
name: string
|
||||
): Promise<void> {
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
// Pipe stdout
|
||||
(async () => {
|
||||
for await (const chunk of process.stdout) {
|
||||
const text = decoder.decode(chunk);
|
||||
for (const line of text.split('\n').filter((l) => l.trim())) {
|
||||
console.log(`[${name}] ${line}`);
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
// Pipe stderr
|
||||
(async () => {
|
||||
for await (const chunk of process.stderr) {
|
||||
const text = decoder.decode(chunk);
|
||||
for (const line of text.split('\n').filter((l) => l.trim())) {
|
||||
console.error(`[${name}:err] ${line}`);
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
// Monitor process exit
|
||||
process.status.then((status) => {
|
||||
console.log(`[${name}] Process exited with code ${status.code}`);
|
||||
if (name === 'sway') {
|
||||
this.swayProcess = null;
|
||||
} else if (name === 'chromium') {
|
||||
this.browserProcess = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user