refactor: extract shared runCommand utility and cleanup codebase
- Create shared command utility in ts/utils/command.ts - Update index.ts, process-manager.ts, system-info.ts to use shared utility - Remove duplicate runCommand implementations from all files - Remove legacy Chrome wrapper methods (startChrome, stopChrome, isChromeRunning) - Consolidate Sway window selectors from 5 to 2 - Remove unused isobuild TypeScript files (mod.ts, deno.json, ts/index.ts) - Make getStatus() async to properly await system info - Add disk and system info sections to UI
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
import { ProcessManager } from './process-manager.ts';
|
||||
import { SystemInfo } from './system-info.ts';
|
||||
import { UIServer } from '../ui/server.ts';
|
||||
import { runCommand } from '../utils/command.ts';
|
||||
|
||||
export interface DaemonConfig {
|
||||
uiPort: number;
|
||||
@@ -60,13 +61,14 @@ export class EcoDaemon {
|
||||
return [...this.logs];
|
||||
}
|
||||
|
||||
getStatus(): Record<string, unknown> {
|
||||
async getStatus(): Promise<Record<string, unknown>> {
|
||||
const systemInfo = await this.systemInfo.getInfo();
|
||||
return {
|
||||
sway: this.swayStatus.state === 'running',
|
||||
swayStatus: this.swayStatus,
|
||||
chromium: this.chromiumStatus.state === 'running',
|
||||
chromiumStatus: this.chromiumStatus,
|
||||
systemInfo: this.systemInfo.getInfo(),
|
||||
systemInfo,
|
||||
logs: this.logs.slice(-50),
|
||||
};
|
||||
}
|
||||
@@ -166,14 +168,14 @@ export class EcoDaemon {
|
||||
}
|
||||
|
||||
private async ensureSeatd(): Promise<void> {
|
||||
const status = await this.runCommand('systemctl', ['is-active', 'seatd']);
|
||||
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 this.runCommand('systemctl', ['start', 'seatd']);
|
||||
const result = await runCommand('systemctl', ['start', 'seatd']);
|
||||
if (!result.success) {
|
||||
this.log('Warning: Failed to start seatd: ' + result.stderr);
|
||||
}
|
||||
@@ -184,9 +186,9 @@ export class EcoDaemon {
|
||||
|
||||
// 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]);
|
||||
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)');
|
||||
@@ -243,31 +245,13 @@ export class EcoDaemon {
|
||||
}
|
||||
|
||||
private async getUserUid(): Promise<number> {
|
||||
const result = await this.runCommand('id', ['-u', this.config.user]);
|
||||
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);
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
* Manages spawning and monitoring of Sway and Chromium processes
|
||||
*/
|
||||
|
||||
import { runCommand } from '../utils/command.ts';
|
||||
|
||||
export interface SwayConfig {
|
||||
runtimeDir: string;
|
||||
backends: string;
|
||||
@@ -18,6 +20,7 @@ export interface BrowserConfig {
|
||||
runtimeDir: string;
|
||||
waylandDisplay: string;
|
||||
url: string;
|
||||
kiosk?: boolean;
|
||||
}
|
||||
|
||||
export class ProcessManager {
|
||||
@@ -70,13 +73,7 @@ focus_follows_mouse yes
|
||||
|
||||
# Force all windows fullscreen for kiosk mode
|
||||
for_window [app_id=".*"] fullscreen enable
|
||||
|
||||
# Chromium-specific fullscreen rules
|
||||
for_window [app_id="chromium-browser"] fullscreen enable
|
||||
for_window [app_id="Chromium-browser"] fullscreen enable
|
||||
for_window [app_id="chromium"] fullscreen enable
|
||||
for_window [class="Chromium-browser"] fullscreen enable
|
||||
for_window [class="chromium-browser"] fullscreen enable
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -88,37 +85,19 @@ for_window [class="chromium-browser"] fullscreen enable
|
||||
const configPath = `${configDir}/config`;
|
||||
|
||||
// Create config directory
|
||||
await this.runCommand('mkdir', ['-p', configDir]);
|
||||
await this.runCommand('chown', [`${this.user}:${this.user}`, `/home/${this.user}/.config`]);
|
||||
await this.runCommand('chown', [`${this.user}:${this.user}`, configDir]);
|
||||
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 this.runCommand('chown', [`${this.user}:${this.user}`, configPath]);
|
||||
await runCommand('chown', [`${this.user}:${this.user}`, configPath]);
|
||||
|
||||
console.log(`[sway] Config written to ${configPath}`);
|
||||
return configPath;
|
||||
}
|
||||
|
||||
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),
|
||||
};
|
||||
}
|
||||
|
||||
async startSway(config: SwayConfig): Promise<void> {
|
||||
// Write sway config before starting
|
||||
const configPath = await this.writeSwayConfig(config);
|
||||
@@ -287,10 +266,7 @@ for_window [class="chromium-browser"] fullscreen enable
|
||||
// Try multiple selectors to ensure we catch the window
|
||||
const selectors = [
|
||||
'[app_id="chromium-browser"]',
|
||||
'[app_id="Chromium-browser"]',
|
||||
'[app_id="chromium"]',
|
||||
'[class="Chromium-browser"]',
|
||||
'[class="chromium-browser"]',
|
||||
];
|
||||
|
||||
for (const selector of selectors) {
|
||||
@@ -301,11 +277,6 @@ for_window [class="chromium-browser"] fullscreen enable
|
||||
await this.swaymsg(config, '[app_id="chromium-browser"] focus');
|
||||
}
|
||||
|
||||
// Legacy method name for backwards compatibility
|
||||
async startChrome(config: BrowserConfig & { kiosk?: boolean }): Promise<void> {
|
||||
return this.startBrowser(config);
|
||||
}
|
||||
|
||||
isSwayRunning(): boolean {
|
||||
return this.swayProcess !== null;
|
||||
}
|
||||
@@ -314,11 +285,6 @@ for_window [class="chromium-browser"] fullscreen enable
|
||||
return this.browserProcess !== null;
|
||||
}
|
||||
|
||||
// Legacy method name for backwards compatibility
|
||||
isChromeRunning(): boolean {
|
||||
return this.isBrowserRunning();
|
||||
}
|
||||
|
||||
async stopSway(): Promise<void> {
|
||||
if (this.swayProcess) {
|
||||
try {
|
||||
@@ -343,11 +309,6 @@ for_window [class="chromium-browser"] fullscreen enable
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy method name for backwards compatibility
|
||||
async stopChrome(): Promise<void> {
|
||||
return this.stopBrowser();
|
||||
}
|
||||
|
||||
private async pipeOutput(
|
||||
process: Deno.ChildProcess,
|
||||
name: string
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
* Gathers CPU, RAM, disk, network, and GPU information
|
||||
*/
|
||||
|
||||
import { runCommand } from '../utils/command.ts';
|
||||
|
||||
export interface CpuInfo {
|
||||
model: string;
|
||||
cores: number;
|
||||
@@ -66,8 +68,9 @@ export class SystemInfo {
|
||||
|
||||
private async getHostname(): Promise<string> {
|
||||
try {
|
||||
const result = await this.runCommand('hostname');
|
||||
return result.trim();
|
||||
const result = await runCommand('hostname', []);
|
||||
if (!result.success) return 'unknown';
|
||||
return result.stdout.trim();
|
||||
} catch {
|
||||
return 'unknown';
|
||||
}
|
||||
@@ -120,8 +123,9 @@ export class SystemInfo {
|
||||
|
||||
private async getDiskInfo(): Promise<DiskInfo[]> {
|
||||
try {
|
||||
const output = await this.runCommand('df', ['-B1', '--output=source,target,size,used,avail']);
|
||||
const lines = output.trim().split('\n').slice(1);
|
||||
const result = await runCommand('df', ['-B1', '--output=source,target,size,used,avail']);
|
||||
if (!result.success) return [];
|
||||
const lines = result.stdout.trim().split('\n').slice(1);
|
||||
const disks: DiskInfo[] = [];
|
||||
|
||||
for (const line of lines) {
|
||||
@@ -150,8 +154,9 @@ export class SystemInfo {
|
||||
|
||||
private async getNetworkInfo(): Promise<NetworkInterface[]> {
|
||||
try {
|
||||
const output = await this.runCommand('ip', ['-j', 'addr']);
|
||||
const interfaces = JSON.parse(output);
|
||||
const cmdResult = await runCommand('ip', ['-j', 'addr']);
|
||||
if (!cmdResult.success) return [];
|
||||
const interfaces = JSON.parse(cmdResult.stdout);
|
||||
const result: NetworkInterface[] = [];
|
||||
|
||||
for (const iface of interfaces) {
|
||||
@@ -183,8 +188,9 @@ export class SystemInfo {
|
||||
|
||||
private async getGpuInfo(): Promise<GpuInfo[]> {
|
||||
try {
|
||||
const output = await this.runCommand('lspci', ['-mm']);
|
||||
const lines = output.split('\n');
|
||||
const result = await runCommand('lspci', ['-mm']);
|
||||
if (!result.success) return [];
|
||||
const lines = result.stdout.split('\n');
|
||||
const gpus: GpuInfo[] = [];
|
||||
|
||||
for (const line of lines) {
|
||||
@@ -213,19 +219,4 @@ export class SystemInfo {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private async runCommand(cmd: string, args: string[] = []): Promise<string> {
|
||||
const command = new Deno.Command(cmd, {
|
||||
args,
|
||||
stdout: 'piped',
|
||||
stderr: 'piped',
|
||||
});
|
||||
|
||||
const result = await command.output();
|
||||
if (!result.success) {
|
||||
throw new Error('Command failed');
|
||||
}
|
||||
|
||||
return new TextDecoder().decode(result.stdout);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ export class UIServer {
|
||||
};
|
||||
|
||||
if (path === '/api/status') {
|
||||
const status = this.daemon.getStatus();
|
||||
const status = await this.daemon.getStatus();
|
||||
return new Response(JSON.stringify(status), { headers });
|
||||
}
|
||||
|
||||
@@ -233,6 +233,25 @@ export class UIServer {
|
||||
<h2>Network</h2>
|
||||
<div id="network-list"></div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2>Disks</h2>
|
||||
<div id="disk-list"></div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2>System</h2>
|
||||
<div class="stat">
|
||||
<div class="stat-label">Hostname</div>
|
||||
<div class="stat-value" id="hostname">-</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-label">Uptime</div>
|
||||
<div class="stat-value" id="uptime">-</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-label">GPU</div>
|
||||
<div class="stat-value" id="gpu">-</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card" style="grid-column: 1 / -1;">
|
||||
<h2>Logs</h2>
|
||||
<div class="logs" id="logs"></div>
|
||||
@@ -248,6 +267,15 @@ export class UIServer {
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
function formatUptime(seconds) {
|
||||
const days = Math.floor(seconds / 86400);
|
||||
const hours = Math.floor((seconds % 86400) / 3600);
|
||||
const mins = Math.floor((seconds % 3600) / 60);
|
||||
if (days > 0) return days + 'd ' + hours + 'h ' + mins + 'm';
|
||||
if (hours > 0) return hours + 'h ' + mins + 'm';
|
||||
return mins + 'm';
|
||||
}
|
||||
|
||||
function updateStatus(data) {
|
||||
// Services
|
||||
document.getElementById('sway-status').className =
|
||||
|
||||
27
ecoos_daemon/ts/utils/command.ts
Normal file
27
ecoos_daemon/ts/utils/command.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Shared command execution utility
|
||||
*/
|
||||
|
||||
export interface CommandResult {
|
||||
success: boolean;
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
}
|
||||
|
||||
export async function runCommand(
|
||||
cmd: string,
|
||||
args: string[]
|
||||
): Promise<CommandResult> {
|
||||
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),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user