Files
DeskClaw/electron/gateway/manager.ts
Haze b8ab0208d0 feat(core): initialize project skeleton with Electron + React + TypeScript
Set up the complete project foundation for ClawX, a graphical AI assistant:

- Electron main process with IPC handlers, menu, tray, and gateway management
- React renderer with routing, layout components, and page scaffolding
- Zustand state management for gateway, settings, channels, skills, chat, and cron
- shadcn/ui components with Tailwind CSS and CSS variable theming
- Build tooling with Vite, electron-builder, and TypeScript configuration
- Testing setup with Vitest and Playwright
- Development configurations (ESLint, Prettier, gitignore, env example)
2026-02-05 23:09:17 +08:00

371 lines
9.0 KiB
TypeScript

/**
* Gateway Process Manager
* Manages the OpenClaw Gateway process lifecycle
*/
import { spawn, ChildProcess, exec } from 'child_process';
import { EventEmitter } from 'events';
import WebSocket from 'ws';
import { promisify } from 'util';
import { PORTS } from '../utils/config';
const execAsync = promisify(exec);
/**
* Gateway connection status
*/
export interface GatewayStatus {
state: 'stopped' | 'starting' | 'running' | 'error';
port: number;
pid?: number;
uptime?: number;
error?: string;
connectedAt?: number;
}
/**
* Gateway Manager Events
*/
export interface GatewayManagerEvents {
status: (status: GatewayStatus) => void;
message: (message: unknown) => void;
exit: (code: number | null) => void;
error: (error: Error) => void;
}
/**
* Gateway Manager
* Handles starting, stopping, and communicating with the OpenClaw Gateway
*/
export class GatewayManager extends EventEmitter {
private process: ChildProcess | null = null;
private ws: WebSocket | null = null;
private status: GatewayStatus = { state: 'stopped', port: PORTS.OPENCLAW_GATEWAY };
private reconnectTimer: NodeJS.Timeout | null = null;
private pingInterval: NodeJS.Timeout | null = null;
private pendingRequests: Map<string, {
resolve: (value: unknown) => void;
reject: (error: Error) => void;
timeout: NodeJS.Timeout;
}> = new Map();
constructor() {
super();
}
/**
* Get current Gateway status
*/
getStatus(): GatewayStatus {
return { ...this.status };
}
/**
* Start Gateway process
*/
async start(): Promise<void> {
if (this.status.state === 'running') {
return;
}
this.setStatus({ state: 'starting' });
try {
// Check if Gateway is already running
const existing = await this.findExistingGateway();
if (existing) {
console.log('Found existing Gateway on port', existing.port);
await this.connect(existing.port);
return;
}
// Start new Gateway process
await this.startProcess();
// Wait for Gateway to be ready
await this.waitForReady();
// Connect WebSocket
await this.connect(this.status.port);
} catch (error) {
this.setStatus({ state: 'error', error: String(error) });
throw error;
}
}
/**
* Stop Gateway process
*/
async stop(): Promise<void> {
// Clear timers
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
if (this.pingInterval) {
clearInterval(this.pingInterval);
this.pingInterval = null;
}
// Close WebSocket
if (this.ws) {
this.ws.close();
this.ws = null;
}
// Kill process
if (this.process) {
this.process.kill();
this.process = null;
}
// Reject all pending requests
for (const [id, request] of this.pendingRequests) {
clearTimeout(request.timeout);
request.reject(new Error('Gateway stopped'));
}
this.pendingRequests.clear();
this.setStatus({ state: 'stopped' });
}
/**
* Make an RPC call to the Gateway
*/
async rpc<T>(method: string, params?: unknown): Promise<T> {
return new Promise((resolve, reject) => {
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
reject(new Error('Gateway not connected'));
return;
}
const id = crypto.randomUUID();
// Set timeout for request
const timeout = setTimeout(() => {
this.pendingRequests.delete(id);
reject(new Error(`RPC timeout: ${method}`));
}, 30000);
// Store pending request
this.pendingRequests.set(id, {
resolve: resolve as (value: unknown) => void,
reject,
timeout,
});
// Send request
const request = {
jsonrpc: '2.0',
id,
method,
params,
};
this.ws.send(JSON.stringify(request));
});
}
/**
* Find existing Gateway process
*/
private async findExistingGateway(): Promise<{ port: number } | null> {
try {
// Try to connect to default port
const port = PORTS.OPENCLAW_GATEWAY;
const response = await fetch(`http://localhost:${port}/health`, {
signal: AbortSignal.timeout(2000),
});
if (response.ok) {
return { port };
}
} catch {
// Gateway not running
}
return null;
}
/**
* Start Gateway process
*/
private async startProcess(): Promise<void> {
return new Promise((resolve, reject) => {
// Find openclaw command
const command = 'openclaw';
const args = ['gateway', 'run', '--port', String(this.status.port)];
this.process = spawn(command, args, {
stdio: ['ignore', 'pipe', 'pipe'],
detached: false,
shell: true,
});
this.process.on('error', (error) => {
console.error('Gateway process error:', error);
reject(error);
});
this.process.on('exit', (code) => {
console.log('Gateway process exited with code:', code);
this.emit('exit', code);
if (this.status.state === 'running') {
this.setStatus({ state: 'stopped' });
// Attempt to reconnect
this.scheduleReconnect();
}
});
// Log stdout
this.process.stdout?.on('data', (data) => {
console.log('Gateway:', data.toString());
});
// Log stderr
this.process.stderr?.on('data', (data) => {
console.error('Gateway error:', data.toString());
});
// Store PID
if (this.process.pid) {
this.setStatus({ pid: this.process.pid });
}
resolve();
});
}
/**
* Wait for Gateway to be ready
*/
private async waitForReady(retries = 30, interval = 1000): Promise<void> {
for (let i = 0; i < retries; i++) {
try {
const response = await fetch(`http://localhost:${this.status.port}/health`, {
signal: AbortSignal.timeout(1000),
});
if (response.ok) {
return;
}
} catch {
// Gateway not ready yet
}
await new Promise((resolve) => setTimeout(resolve, interval));
}
throw new Error('Gateway failed to start');
}
/**
* Connect WebSocket to Gateway
*/
private async connect(port: number): Promise<void> {
return new Promise((resolve, reject) => {
const wsUrl = `ws://localhost:${port}/ws`;
this.ws = new WebSocket(wsUrl);
this.ws.on('open', () => {
console.log('WebSocket connected to Gateway');
this.setStatus({
state: 'running',
port,
connectedAt: Date.now(),
});
this.startPing();
resolve();
});
this.ws.on('message', (data) => {
try {
const message = JSON.parse(data.toString());
this.handleMessage(message);
} catch (error) {
console.error('Failed to parse WebSocket message:', error);
}
});
this.ws.on('close', () => {
console.log('WebSocket disconnected');
if (this.status.state === 'running') {
this.setStatus({ state: 'stopped' });
this.scheduleReconnect();
}
});
this.ws.on('error', (error) => {
console.error('WebSocket error:', error);
reject(error);
});
});
}
/**
* Handle incoming WebSocket message
*/
private handleMessage(message: { id?: string; result?: unknown; error?: unknown }): void {
// Check if this is a response to a pending request
if (message.id && this.pendingRequests.has(message.id)) {
const request = this.pendingRequests.get(message.id)!;
clearTimeout(request.timeout);
this.pendingRequests.delete(message.id);
if (message.error) {
request.reject(new Error(String(message.error)));
} else {
request.resolve(message.result);
}
return;
}
// Emit message for other handlers
this.emit('message', message);
}
/**
* Start ping interval to keep connection alive
*/
private startPing(): void {
if (this.pingInterval) {
clearInterval(this.pingInterval);
}
this.pingInterval = setInterval(() => {
if (this.ws?.readyState === WebSocket.OPEN) {
this.ws.ping();
}
}, 30000);
}
/**
* Schedule reconnection attempt
*/
private scheduleReconnect(): void {
if (this.reconnectTimer) {
return;
}
this.reconnectTimer = setTimeout(async () => {
this.reconnectTimer = null;
try {
await this.start();
} catch (error) {
console.error('Reconnection failed:', error);
this.scheduleReconnect();
}
}, 5000);
}
/**
* Update status and emit event
*/
private setStatus(update: Partial<GatewayStatus>): void {
this.status = { ...this.status, ...update };
this.emit('status', this.status);
}
}