2026-01-08 18:33:14 +00:00
|
|
|
/**
|
|
|
|
|
* Process Manager
|
|
|
|
|
*
|
|
|
|
|
* Manages spawning and monitoring of Sway and Chromium processes
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import { runCommand } from '../utils/command.ts';
|
2026-01-09 18:14:26 +00:00
|
|
|
import type { DisplayInfo } from './system-info.ts';
|
2026-01-08 18:33:14 +00:00
|
|
|
|
|
|
|
|
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;
|
2026-01-09 18:51:22 +00:00
|
|
|
private swaySocket: string | null = null;
|
2026-01-08 18:33:14 +00:00
|
|
|
|
|
|
|
|
constructor(user: string) {
|
|
|
|
|
this.user = user;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-09 18:51:22 +00:00
|
|
|
/**
|
|
|
|
|
* Find the Sway IPC socket path in the runtime directory
|
|
|
|
|
* Sway creates sockets like: sway-ipc.$UID.$PID.sock
|
|
|
|
|
*/
|
|
|
|
|
async findSwaySocket(runtimeDir: string): Promise<string | null> {
|
|
|
|
|
try {
|
|
|
|
|
for await (const entry of Deno.readDir(runtimeDir)) {
|
|
|
|
|
if (entry.name.startsWith('sway-ipc.') && entry.name.endsWith('.sock')) {
|
|
|
|
|
const socketPath = `${runtimeDir}/${entry.name}`;
|
|
|
|
|
console.log(`[sway] Found IPC socket: ${socketPath}`);
|
|
|
|
|
return socketPath;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error(`[sway] Error finding socket: ${error}`);
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getSwaySocket(): string | null {
|
|
|
|
|
return this.swaySocket;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setSwaySocket(socket: string | null): void {
|
|
|
|
|
this.swaySocket = socket;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-08 18:33:14 +00:00
|
|
|
/**
|
|
|
|
|
* 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);
|
|
|
|
|
|
2026-01-10 00:03:22 +00:00
|
|
|
// Use a fixed socket path so we can reliably connect
|
|
|
|
|
const swaySocketPath = `${config.runtimeDir}/sway-ipc.sock`;
|
|
|
|
|
this.swaySocket = swaySocketPath;
|
|
|
|
|
|
2026-01-08 18:33:14 +00:00
|
|
|
const env: Record<string, string> = {
|
|
|
|
|
XDG_RUNTIME_DIR: config.runtimeDir,
|
|
|
|
|
WLR_BACKENDS: config.backends,
|
2026-01-10 00:03:22 +00:00
|
|
|
SWAYSOCK: swaySocketPath,
|
2026-01-08 18:33:14 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
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> {
|
2026-01-09 18:51:22 +00:00
|
|
|
// Find socket if not already found
|
|
|
|
|
if (!this.swaySocket) {
|
|
|
|
|
this.swaySocket = await this.findSwaySocket(config.runtimeDir);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!this.swaySocket) {
|
|
|
|
|
console.error('[swaymsg] No Sway IPC socket found');
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-08 18:33:14 +00:00
|
|
|
const env: Record<string, string> = {
|
|
|
|
|
XDG_RUNTIME_DIR: config.runtimeDir,
|
2026-01-09 18:51:22 +00:00
|
|
|
SWAYSOCK: this.swaySocket,
|
2026-01-08 18:33:14 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
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
|
2026-01-09 09:41:47 +00:00
|
|
|
// Hardware acceleration is enabled where available but falls back gracefully
|
2026-01-08 18:33:14 +00:00
|
|
|
const browserArgs = [
|
2026-01-09 09:41:47 +00:00
|
|
|
// Wayland/Ozone configuration
|
2026-01-08 18:33:14 +00:00
|
|
|
'--ozone-platform=wayland',
|
|
|
|
|
'--enable-features=UseOzonePlatform',
|
2026-01-09 09:41:47 +00:00
|
|
|
// Kiosk mode settings
|
2026-01-08 18:33:14 +00:00
|
|
|
'--kiosk',
|
|
|
|
|
'--no-first-run',
|
|
|
|
|
'--disable-infobars',
|
|
|
|
|
'--disable-session-crashed-bubble',
|
|
|
|
|
'--disable-restore-session-state',
|
2026-01-09 09:41:47 +00:00
|
|
|
'--noerrdialogs',
|
|
|
|
|
// Disable unnecessary features for kiosk
|
2026-01-08 18:33:14 +00:00
|
|
|
'--disable-background-networking',
|
|
|
|
|
'--disable-sync',
|
|
|
|
|
'--disable-translate',
|
2026-01-09 09:41:47 +00:00
|
|
|
'--disable-features=TranslateUI',
|
2026-01-08 18:33:14 +00:00
|
|
|
'--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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-09 09:41:47 +00:00
|
|
|
async isBrowserRunning(): Promise<boolean> {
|
|
|
|
|
// Check if any chromium process is running (Chromium forks, so we can't just track the parent)
|
|
|
|
|
try {
|
|
|
|
|
const cmd = new Deno.Command('pgrep', { args: ['-f', 'chromium'], stdout: 'null', stderr: 'null' });
|
|
|
|
|
const result = await cmd.output();
|
|
|
|
|
return result.success;
|
|
|
|
|
} catch {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2026-01-08 18:33:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
2026-01-09 18:51:22 +00:00
|
|
|
this.swaySocket = null; // Reset socket so we find new one on restart
|
2026-01-08 18:33:14 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-09 18:14:26 +00:00
|
|
|
/**
|
|
|
|
|
* Get connected displays via swaymsg
|
|
|
|
|
*/
|
|
|
|
|
async getDisplays(config: { runtimeDir: string; waylandDisplay: string }): Promise<DisplayInfo[]> {
|
2026-01-09 18:51:22 +00:00
|
|
|
// Find socket if not already found
|
|
|
|
|
if (!this.swaySocket) {
|
|
|
|
|
this.swaySocket = await this.findSwaySocket(config.runtimeDir);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!this.swaySocket) {
|
|
|
|
|
console.error('[displays] No Sway IPC socket found');
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-09 18:14:26 +00:00
|
|
|
const env: Record<string, string> = {
|
|
|
|
|
XDG_RUNTIME_DIR: config.runtimeDir,
|
2026-01-09 18:51:22 +00:00
|
|
|
SWAYSOCK: this.swaySocket,
|
2026-01-09 18:14:26 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
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 -t get_outputs`],
|
|
|
|
|
stdout: 'piped',
|
|
|
|
|
stderr: 'piped',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const result = await cmd.output();
|
|
|
|
|
if (!result.success) {
|
2026-01-09 18:51:22 +00:00
|
|
|
const stderr = new TextDecoder().decode(result.stderr);
|
|
|
|
|
console.error(`[displays] Failed to get outputs: ${stderr}`);
|
2026-01-09 18:14:26 +00:00
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const outputs = JSON.parse(new TextDecoder().decode(result.stdout));
|
|
|
|
|
return outputs.map((output: {
|
|
|
|
|
name: string;
|
|
|
|
|
make: string;
|
|
|
|
|
model: string;
|
|
|
|
|
serial: string;
|
|
|
|
|
active: boolean;
|
|
|
|
|
current_mode?: { width: number; height: number; refresh: number };
|
|
|
|
|
focused: boolean;
|
|
|
|
|
}) => ({
|
|
|
|
|
name: output.name,
|
|
|
|
|
make: output.make || 'Unknown',
|
|
|
|
|
model: output.model || 'Unknown',
|
|
|
|
|
serial: output.serial || '',
|
|
|
|
|
active: output.active,
|
|
|
|
|
width: output.current_mode?.width || 0,
|
|
|
|
|
height: output.current_mode?.height || 0,
|
|
|
|
|
refreshRate: Math.round((output.current_mode?.refresh || 0) / 1000),
|
|
|
|
|
isPrimary: output.focused,
|
|
|
|
|
}));
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error(`[displays] Error: ${error}`);
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Enable or disable a display
|
|
|
|
|
*/
|
|
|
|
|
async setDisplayEnabled(
|
|
|
|
|
config: { runtimeDir: string; waylandDisplay: string },
|
|
|
|
|
name: string,
|
|
|
|
|
enabled: boolean
|
|
|
|
|
): Promise<boolean> {
|
2026-01-09 19:39:14 +00:00
|
|
|
if (enabled) {
|
|
|
|
|
console.log(`[displays] Enabling ${name}`);
|
|
|
|
|
// First try to set resolution, then enable
|
|
|
|
|
await this.swaymsg(config, `output ${name} resolution 1920x1080`);
|
|
|
|
|
return this.swaymsg(config, `output ${name} enable`);
|
|
|
|
|
} else {
|
|
|
|
|
console.log(`[displays] Disabling ${name}`);
|
|
|
|
|
return this.swaymsg(config, `output ${name} disable`);
|
|
|
|
|
}
|
2026-01-09 18:14:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Move the kiosk browser to a specific display
|
|
|
|
|
*/
|
|
|
|
|
async setKioskDisplay(
|
|
|
|
|
config: { runtimeDir: string; waylandDisplay: string },
|
|
|
|
|
name: string
|
|
|
|
|
): Promise<boolean> {
|
|
|
|
|
console.log(`[displays] Setting primary display to ${name}`);
|
|
|
|
|
|
|
|
|
|
// Focus the chromium window and move it to the target output
|
|
|
|
|
const commands = [
|
|
|
|
|
`[app_id="chromium-browser"] focus`,
|
|
|
|
|
`move container to output ${name}`,
|
|
|
|
|
`focus output ${name}`,
|
|
|
|
|
`[app_id="chromium-browser"] fullscreen enable`,
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
for (const cmd of commands) {
|
|
|
|
|
await this.swaymsg(config, cmd);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-08 18:33:14 +00:00
|
|
|
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}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})();
|
|
|
|
|
|
2026-01-09 09:41:47 +00:00
|
|
|
// Monitor process exit - only nullify if still the same process (prevents race condition on restart)
|
2026-01-08 18:33:14 +00:00
|
|
|
process.status.then((status) => {
|
|
|
|
|
console.log(`[${name}] Process exited with code ${status.code}`);
|
2026-01-09 09:41:47 +00:00
|
|
|
if (name === 'sway' && this.swayProcess === process) {
|
2026-01-08 18:33:14 +00:00
|
|
|
this.swayProcess = null;
|
2026-01-09 18:51:22 +00:00
|
|
|
this.swaySocket = null; // Reset socket so we find new one on restart
|
2026-01-09 09:41:47 +00:00
|
|
|
} else if (name === 'chromium' && this.browserProcess === process) {
|
2026-01-08 18:33:14 +00:00
|
|
|
this.browserProcess = null;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|