190 lines
6.4 KiB
TypeScript
190 lines
6.4 KiB
TypeScript
import { app, utilityProcess } from 'electron';
|
|
import { existsSync, writeFileSync } from 'fs';
|
|
import path from 'path';
|
|
import type { GatewayLaunchContext } from './config-sync';
|
|
import type { GatewayLifecycleState } from './process-policy';
|
|
import { logger } from '../utils/logger';
|
|
import { appendNodeRequireToNodeOptions } from '../utils/paths';
|
|
|
|
const GATEWAY_FETCH_PRELOAD_SOURCE = `'use strict';
|
|
(function () {
|
|
var _f = globalThis.fetch;
|
|
if (typeof _f !== 'function') return;
|
|
if (globalThis.__clawxFetchPatched) return;
|
|
globalThis.__clawxFetchPatched = true;
|
|
|
|
globalThis.fetch = function clawxFetch(input, init) {
|
|
var url =
|
|
typeof input === 'string' ? input
|
|
: input && typeof input === 'object' && typeof input.url === 'string'
|
|
? input.url : '';
|
|
|
|
if (url.indexOf('openrouter.ai') !== -1) {
|
|
init = init ? Object.assign({}, init) : {};
|
|
var prev = init.headers;
|
|
var flat = {};
|
|
if (prev && typeof prev.forEach === 'function') {
|
|
prev.forEach(function (v, k) { flat[k] = v; });
|
|
} else if (prev && typeof prev === 'object') {
|
|
Object.assign(flat, prev);
|
|
}
|
|
delete flat['http-referer'];
|
|
delete flat['HTTP-Referer'];
|
|
delete flat['x-title'];
|
|
delete flat['X-Title'];
|
|
flat['HTTP-Referer'] = 'https://claw-x.com';
|
|
flat['X-Title'] = 'ClawX';
|
|
init.headers = flat;
|
|
}
|
|
return _f.call(globalThis, input, init);
|
|
};
|
|
|
|
if (process.platform === 'win32') {
|
|
try {
|
|
var cp = require('child_process');
|
|
if (!cp.__clawxPatched) {
|
|
cp.__clawxPatched = 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 a = args[i];
|
|
if (a && typeof a === 'object' && !Array.isArray(a)) {
|
|
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 (e) {
|
|
// ignore
|
|
}
|
|
}
|
|
})();
|
|
`;
|
|
|
|
function ensureGatewayFetchPreload(): string {
|
|
const dest = path.join(app.getPath('userData'), 'gateway-fetch-preload.cjs');
|
|
try {
|
|
writeFileSync(dest, GATEWAY_FETCH_PRELOAD_SOURCE, 'utf-8');
|
|
} catch {
|
|
// best-effort
|
|
}
|
|
return dest;
|
|
}
|
|
|
|
export async function launchGatewayProcess(options: {
|
|
port: number;
|
|
launchContext: GatewayLaunchContext;
|
|
sanitizeSpawnArgs: (args: string[]) => string[];
|
|
getCurrentState: () => GatewayLifecycleState;
|
|
getShouldReconnect: () => boolean;
|
|
onStderrLine: (line: string) => void;
|
|
onSpawn: (pid: number | undefined) => void;
|
|
onExit: (child: Electron.UtilityProcess, code: number | null) => void;
|
|
onError: (error: Error) => void;
|
|
}): Promise<{ child: Electron.UtilityProcess; lastSpawnSummary: string }> {
|
|
const {
|
|
openclawDir,
|
|
entryScript,
|
|
gatewayArgs,
|
|
forkEnv,
|
|
mode,
|
|
binPathExists,
|
|
loadedProviderKeyCount,
|
|
proxySummary,
|
|
channelStartupSummary,
|
|
} = options.launchContext;
|
|
|
|
logger.info(
|
|
`Starting Gateway process (mode=${mode}, port=${options.port}, entry="${entryScript}", args="${options.sanitizeSpawnArgs(gatewayArgs).join(' ')}", cwd="${openclawDir}", bundledBin=${binPathExists ? 'yes' : 'no'}, providerKeys=${loadedProviderKeyCount}, channels=${channelStartupSummary}, proxy=${proxySummary})`,
|
|
);
|
|
const lastSpawnSummary = `mode=${mode}, entry="${entryScript}", args="${options.sanitizeSpawnArgs(gatewayArgs).join(' ')}", cwd="${openclawDir}"`;
|
|
|
|
const runtimeEnv = { ...forkEnv };
|
|
// Only apply the fetch/child_process preload in dev mode.
|
|
// In packaged builds Electron's UtilityProcess rejects NODE_OPTIONS
|
|
// with --require, logging "Most NODE_OPTIONs are not supported in
|
|
// packaged apps" and the preload never loads.
|
|
if (!app.isPackaged) {
|
|
try {
|
|
const preloadPath = ensureGatewayFetchPreload();
|
|
if (existsSync(preloadPath)) {
|
|
runtimeEnv.NODE_OPTIONS = appendNodeRequireToNodeOptions(
|
|
runtimeEnv.NODE_OPTIONS,
|
|
preloadPath,
|
|
);
|
|
}
|
|
} catch (err) {
|
|
logger.warn('Failed to set up OpenRouter headers preload:', err);
|
|
}
|
|
}
|
|
|
|
return await new Promise<{ child: Electron.UtilityProcess; lastSpawnSummary: string }>((resolve, reject) => {
|
|
const child = utilityProcess.fork(entryScript, gatewayArgs, {
|
|
cwd: openclawDir,
|
|
stdio: 'pipe',
|
|
env: runtimeEnv as NodeJS.ProcessEnv,
|
|
serviceName: 'OpenClaw Gateway',
|
|
});
|
|
|
|
let settled = false;
|
|
const resolveOnce = () => {
|
|
if (settled) return;
|
|
settled = true;
|
|
resolve({ child, lastSpawnSummary });
|
|
};
|
|
const rejectOnce = (error: Error) => {
|
|
if (settled) return;
|
|
settled = true;
|
|
reject(error);
|
|
};
|
|
|
|
child.on('error', (error) => {
|
|
logger.error('Gateway process spawn error:', error);
|
|
options.onError(error);
|
|
rejectOnce(error);
|
|
});
|
|
|
|
child.on('exit', (code: number) => {
|
|
// Only check shouldReconnect — not current state. On Windows the WS
|
|
// close handler fires before the process exit handler and sets state to
|
|
// 'stopped', which would make an unexpected crash look like a planned
|
|
// shutdown in logs. shouldReconnect is the reliable indicator: stop()
|
|
// sets it to false (expected), crashes leave it true (unexpected).
|
|
const expectedExit = !options.getShouldReconnect();
|
|
const level = expectedExit ? logger.info : logger.warn;
|
|
level(`Gateway process exited (code=${code}, expected=${expectedExit ? 'yes' : 'no'})`);
|
|
options.onExit(child, code);
|
|
});
|
|
|
|
child.stderr?.on('data', (data) => {
|
|
const raw = data.toString();
|
|
for (const line of raw.split(/\r?\n/)) {
|
|
options.onStderrLine(line);
|
|
}
|
|
});
|
|
|
|
child.on('spawn', () => {
|
|
logger.info(`Gateway process started (pid=${child.pid})`);
|
|
options.onSpawn(child.pid);
|
|
resolveOnce();
|
|
});
|
|
});
|
|
}
|