import { spawn } from 'node:child_process'; import { existsSync, writeFileSync } from 'node:fs'; import path from 'node:path'; import { app, utilityProcess } from 'electron'; import logManager from '@electron/service/logger'; import { appendNodeRequireToNodeOptions } from '@electron/utils/paths'; import type { GatewayLaunchContext } from './config-sync'; import { resolveGatewayLaunchStrategy, type GatewayLaunchStrategy } from './launch-strategy'; import type { GatewayProcessHandle } from './process-handle'; const GATEWAY_PRELOAD_SOURCE = `'use strict'; (function () { if (process.platform !== 'win32') return; try { var cp = require('child_process'); if (cp.__znAiGatewayPatched) return; cp.__znAiGatewayPatched = true; ['spawn', 'exec', 'execFile', 'fork', 'spawnSync', 'execSync', 'execFileSync'].forEach(function (method) { var original = cp[method]; if (typeof original !== 'function') return; cp[method] = function () { var args = Array.prototype.slice.call(arguments); var optIdx = -1; for (var i = 1; i < args.length; i++) { var candidate = args[i]; if (candidate && typeof candidate === 'object' && !Array.isArray(candidate)) { optIdx = i; break; } } if (optIdx >= 0) { args[optIdx].windowsHide = true; } else { var opts = { windowsHide: true }; if (typeof args[args.length - 1] === 'function') { args.splice(args.length - 1, 0, opts); } else { args.push(opts); } } return original.apply(this, args); }; }); } catch { // best-effort } })(); `; function ensureGatewayPreload(): string { const destination = path.join(app.getPath('userData'), 'gateway-preload.cjs'); try { writeFileSync(destination, GATEWAY_PRELOAD_SOURCE, 'utf-8'); } catch { // best-effort } return destination; } export interface LaunchGatewayProcessOptions { port: number; launchContext: GatewayLaunchContext; sanitizeSpawnArgs?: (args: string[]) => string[]; onStdoutLine?: (line: string) => void; onStderrLine?: (line: string) => void; onSpawn?: (pid: number | undefined) => void; onExit?: (child: GatewayProcessHandle, code: number | null) => void; onError?: (error: Error) => void; } function emitProcessOutput( child: GatewayProcessHandle, stream: 'stdout' | 'stderr', onLine: ((line: string) => void) | undefined, ): void { const readable = child[stream]; if (!readable) { return; } readable.on('data', (data) => { const raw = data.toString(); for (const line of raw.split(/\r?\n/)) { const trimmed = line.trim(); if (!trimmed) { continue; } if (onLine) { onLine(trimmed); continue; } if (stream === 'stdout') { logManager.debug(`[OpenClaw stdout] ${trimmed}`); continue; } logManager.warn(`[OpenClaw] ${trimmed}`); } }); } function buildGatewayRuntimeEnv( launchContext: GatewayLaunchContext, ): Record { const runtimeEnv = { ...launchContext.forkEnv }; if (!app.isPackaged) { try { const preloadPath = ensureGatewayPreload(); if (existsSync(preloadPath)) { runtimeEnv.NODE_OPTIONS = appendNodeRequireToNodeOptions( runtimeEnv.NODE_OPTIONS, preloadPath, ); } } catch (error) { logManager.warn('Failed to prepare Gateway preload', error); } } return runtimeEnv; } function resolvePreferredLaunchStrategy( launchContext: GatewayLaunchContext, ): GatewayLaunchStrategy { return resolveGatewayLaunchStrategy({ platform: process.platform, mode: launchContext.mode, forced: process.env.ZN_AI_GATEWAY_LAUNCH_STRATEGY, }); } function resolveNodeRuntimeHost(options: { launchContext: GatewayLaunchContext; runtimeEnv: Record; }): { command: string; env: NodeJS.ProcessEnv; hostKind: 'bundled-node' | 'electron-run-as-node'; } { const bundledNodePath = process.platform === 'win32' ? path.join(options.launchContext.binDir, 'node.exe') : ''; if (bundledNodePath && existsSync(bundledNodePath)) { const { ELECTRON_RUN_AS_NODE: _electronRunAsNode, ...envWithoutElectronNode } = options.runtimeEnv; return { command: bundledNodePath, env: envWithoutElectronNode as NodeJS.ProcessEnv, hostKind: 'bundled-node', }; } return { command: process.execPath, env: { ...options.runtimeEnv, ELECTRON_RUN_AS_NODE: '1', } as NodeJS.ProcessEnv, hostKind: 'electron-run-as-node', }; } async function launchWithUtilityProcess(options: { launchContext: GatewayLaunchContext; runtimeEnv: Record; lastSpawnSummary: string; onStdoutLine?: (line: string) => void; onStderrLine?: (line: string) => void; onSpawn?: (pid: number | undefined) => void; onExit?: (child: GatewayProcessHandle, code: number | null) => void; onError?: (error: Error) => void; }): Promise<{ child: GatewayProcessHandle; lastSpawnSummary: string }> { return await new Promise<{ child: GatewayProcessHandle; lastSpawnSummary: string }>((resolve, reject) => { const child = utilityProcess.fork( options.launchContext.entryScript, options.launchContext.gatewayArgs, { cwd: options.launchContext.openclawDir, stdio: 'pipe', env: options.runtimeEnv as NodeJS.ProcessEnv, serviceName: 'OpenClaw Gateway', }, ) as GatewayProcessHandle; let settled = false; const resolveOnce = () => { if (settled) return; settled = true; resolve({ child, lastSpawnSummary: options.lastSpawnSummary }); }; const rejectOnce = (error: Error) => { if (settled) return; settled = true; reject(error); }; child.once('spawn', () => { logManager.info('OpenClaw Gateway process spawned', { pid: child.pid, launchStrategy: 'utility-process', }); options.onSpawn?.(child.pid); resolveOnce(); }); child.once('error', (error) => { logManager.error('OpenClaw Gateway process spawn error:', error); options.onError?.(error); rejectOnce(error); }); child.once('exit', (code) => { options.onExit?.(child, code ?? null); if (!settled) { rejectOnce(new Error(`OpenClaw Gateway exited before spawn completed (code=${code ?? 'unknown'})`)); } }); emitProcessOutput(child, 'stdout', options.onStdoutLine); emitProcessOutput(child, 'stderr', options.onStderrLine); }); } async function launchWithNodeRuntime(options: { launchContext: GatewayLaunchContext; runtimeEnv: Record; lastSpawnSummary: string; onStdoutLine?: (line: string) => void; onStderrLine?: (line: string) => void; onSpawn?: (pid: number | undefined) => void; onExit?: (child: GatewayProcessHandle, code: number | null) => void; onError?: (error: Error) => void; }): Promise<{ child: GatewayProcessHandle; lastSpawnSummary: string }> { const runtimeHost = resolveNodeRuntimeHost({ launchContext: options.launchContext, runtimeEnv: options.runtimeEnv, }); return await new Promise<{ child: GatewayProcessHandle; lastSpawnSummary: string }>((resolve, reject) => { const child = spawn( runtimeHost.command, [options.launchContext.entryScript, ...options.launchContext.gatewayArgs], { cwd: options.launchContext.openclawDir, env: runtimeHost.env, stdio: 'pipe', windowsHide: true, }, ) as GatewayProcessHandle; let settled = false; const resolveOnce = () => { if (settled) return; settled = true; resolve({ child, lastSpawnSummary: options.lastSpawnSummary }); }; const rejectOnce = (error: Error) => { if (settled) return; settled = true; reject(error); }; child.once('spawn', () => { logManager.info('OpenClaw Gateway process spawned', { pid: child.pid, launchStrategy: 'node-runtime', runtimeHost: runtimeHost.hostKind, execPath: runtimeHost.command, }); options.onSpawn?.(child.pid); resolveOnce(); }); child.once('error', (error) => { logManager.error('OpenClaw Gateway process spawn error:', error); options.onError?.(error); rejectOnce(error); }); child.once('exit', (code) => { options.onExit?.(child, code ?? null); if (!settled) { rejectOnce(new Error(`OpenClaw Gateway exited before spawn completed (code=${code ?? 'unknown'})`)); } }); emitProcessOutput(child, 'stdout', options.onStdoutLine); emitProcessOutput(child, 'stderr', options.onStderrLine); }); } export async function launchGatewayProcess( options: LaunchGatewayProcessOptions, ): Promise<{ child: GatewayProcessHandle; lastSpawnSummary: string }> { const { launchContext } = options; const sanitizedArgs = options.sanitizeSpawnArgs?.(launchContext.gatewayArgs) ?? launchContext.gatewayArgs; const launchStrategy = resolvePreferredLaunchStrategy(launchContext); const lastSpawnSummary = `mode=${launchContext.mode}, launcher="${launchStrategy}", entry="${launchContext.entryScript}", args="${sanitizedArgs.join(' ')}", cwd="${launchContext.openclawDir}"`; const runtimeEnv = buildGatewayRuntimeEnv(launchContext); logManager.info('Starting OpenClaw Gateway process', { port: options.port, entryScript: launchContext.entryScript, cwd: launchContext.openclawDir, args: sanitizedArgs, mode: launchContext.mode, bundledBin: launchContext.binPathExists, launchStrategy, }); const launchOptions = { launchContext, runtimeEnv, lastSpawnSummary, onStdoutLine: options.onStdoutLine, onStderrLine: options.onStderrLine, onSpawn: options.onSpawn, onExit: options.onExit, onError: options.onError, }; if (launchStrategy === 'node-runtime') { return await launchWithNodeRuntime(launchOptions); } return await launchWithUtilityProcess(launchOptions); }