refactor(gateway): migrate from child_process.spawn to utilityProcess.fork (#290)
This commit is contained in:
committed by
GitHub
Unverified
parent
76df84e68c
commit
52748d78b5
@@ -2,9 +2,8 @@
|
||||
* Gateway Process Manager
|
||||
* Manages the OpenClaw Gateway process lifecycle
|
||||
*/
|
||||
import { app } from 'electron';
|
||||
import { app, utilityProcess } from 'electron';
|
||||
import path from 'path';
|
||||
import { spawn, ChildProcess } from 'child_process';
|
||||
import { EventEmitter } from 'events';
|
||||
import { existsSync, writeFileSync } from 'fs';
|
||||
import WebSocket from 'ws';
|
||||
@@ -12,10 +11,8 @@ import { PORTS } from '../utils/config';
|
||||
import {
|
||||
getOpenClawDir,
|
||||
getOpenClawEntryPath,
|
||||
isOpenClawBuilt,
|
||||
isOpenClawPresent,
|
||||
appendNodeRequireToNodeOptions,
|
||||
quoteForCmd,
|
||||
} from '../utils/paths';
|
||||
import { getAllSettings, getSetting } from '../utils/store';
|
||||
import { getApiKey, getDefaultProvider, getProvider } from '../utils/secure-storage';
|
||||
@@ -83,38 +80,8 @@ const DEFAULT_RECONNECT_CONFIG: ReconnectConfig = {
|
||||
maxDelay: 30000,
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the Node.js-compatible executable path for spawning child processes.
|
||||
*
|
||||
* On macOS in packaged mode, using `process.execPath` directly causes the
|
||||
* child process to appear as a separate dock icon (named "exec") because the
|
||||
* binary lives inside a `.app` bundle that macOS treats as a GUI application.
|
||||
*
|
||||
* To avoid this, we resolve the Electron Helper binary which has
|
||||
* `LSUIElement` set in its Info.plist, preventing dock icon creation.
|
||||
* Falls back to `process.execPath` if the Helper binary is not found.
|
||||
*/
|
||||
function getNodeExecutablePath(): string {
|
||||
if (process.platform === 'darwin' && app.isPackaged) {
|
||||
// Electron Helper binary lives at:
|
||||
// <App>.app/Contents/Frameworks/<ProductName> Helper.app/Contents/MacOS/<ProductName> Helper
|
||||
const appName = app.getName();
|
||||
const helperName = `${appName} Helper`;
|
||||
const helperPath = path.join(
|
||||
path.dirname(process.execPath), // .../Contents/MacOS
|
||||
'../Frameworks',
|
||||
`${helperName}.app`,
|
||||
'Contents/MacOS',
|
||||
helperName,
|
||||
);
|
||||
if (existsSync(helperPath)) {
|
||||
logger.debug(`Using Electron Helper binary to avoid dock icon: ${helperPath}`);
|
||||
return helperPath;
|
||||
}
|
||||
logger.debug(`Electron Helper binary not found at ${helperPath}, falling back to process.execPath`);
|
||||
}
|
||||
return process.execPath;
|
||||
}
|
||||
// getNodeExecutablePath() removed: utilityProcess.fork() handles process isolation
|
||||
// natively on all platforms (no dock icon on macOS, no console on Windows).
|
||||
|
||||
/**
|
||||
* Ensure the gateway fetch-preload script exists in userData and return
|
||||
@@ -158,6 +125,35 @@ const GATEWAY_FETCH_PRELOAD_SOURCE = `'use strict';
|
||||
}
|
||||
return _f.call(globalThis, input, init);
|
||||
};
|
||||
|
||||
// Global monkey-patch for child_process to enforce windowsHide: true on Windows
|
||||
// This prevents OpenClaw's tools (e.g. Terminal, Python) from flashing black
|
||||
// command boxes during AI conversations, without triggering AVs.
|
||||
if (process.platform === 'win32') {
|
||||
try {
|
||||
var cp = require('child_process');
|
||||
if (!cp.__clawxPatched) {
|
||||
cp.__clawxPatched = true;
|
||||
['spawn', 'exec', 'execFile', 'spawnSync', 'execSync', 'execFileSync'].forEach(function(method) {
|
||||
var original = cp[method];
|
||||
if (typeof original === 'function') {
|
||||
cp[method] = function() {
|
||||
var args = Array.prototype.slice.call(arguments);
|
||||
var lastArg = args[args.length - 1];
|
||||
if (lastArg && typeof lastArg === 'object' && !Array.isArray(lastArg)) {
|
||||
lastArg.windowsHide = true;
|
||||
} else {
|
||||
args.push({ windowsHide: true });
|
||||
}
|
||||
return original.apply(this, args);
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
})();
|
||||
`;
|
||||
|
||||
@@ -179,7 +175,8 @@ class LifecycleSupersededError extends Error {
|
||||
* Handles starting, stopping, and communicating with the OpenClaw Gateway
|
||||
*/
|
||||
export class GatewayManager extends EventEmitter {
|
||||
private process: ChildProcess | null = null;
|
||||
private process: Electron.UtilityProcess | null = null;
|
||||
private processExitCode: number | null = null; // set by exit event, replaces exitCode/signalCode
|
||||
private ownsProcess = false;
|
||||
private ws: WebSocket | null = null;
|
||||
private status: GatewayStatus = { state: 'stopped', port: PORTS.OPENCLAW_GATEWAY };
|
||||
@@ -461,43 +458,32 @@ export class GatewayManager extends EventEmitter {
|
||||
// Kill process
|
||||
if (this.process && this.ownsProcess) {
|
||||
const child = this.process;
|
||||
// UtilityProcess doesn't expose exitCode/signalCode — track exit via event.
|
||||
let exited = false;
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
// If process already exited, resolve immediately
|
||||
if (child.exitCode !== null || child.signalCode !== null) {
|
||||
return resolve();
|
||||
}
|
||||
child.once('exit', () => {
|
||||
exited = true;
|
||||
resolve();
|
||||
});
|
||||
|
||||
// Kill the entire process group so respawned children are also terminated.
|
||||
// The gateway entry script may respawn itself; killing only the parent PID
|
||||
// leaves the child orphaned (PPID=1) and still holding the port.
|
||||
const pid = child.pid;
|
||||
logger.info(`Sending SIGTERM to Gateway process group (pid=${pid ?? 'unknown'})`);
|
||||
if (pid) {
|
||||
try { process.kill(-pid, 'SIGTERM'); } catch { /* group kill failed, fall back */ }
|
||||
}
|
||||
child.kill('SIGTERM');
|
||||
logger.info(`Sending kill to Gateway process (pid=${pid ?? 'unknown'})`);
|
||||
try { child.kill(); } catch { /* ignore if already exited */ }
|
||||
|
||||
// Force kill after timeout
|
||||
// Force kill after timeout via OS-level kill on the PID
|
||||
const timeout = setTimeout(() => {
|
||||
if (child.exitCode === null && child.signalCode === null) {
|
||||
logger.warn(`Gateway did not exit in time, sending SIGKILL (pid=${pid ?? 'unknown'})`);
|
||||
if (!exited) {
|
||||
logger.warn(`Gateway did not exit in time, force-killing (pid=${pid ?? 'unknown'})`);
|
||||
if (pid) {
|
||||
try { process.kill(-pid, 'SIGKILL'); } catch { /* ignore */ }
|
||||
try { process.kill(pid, 'SIGKILL'); } catch { /* ignore */ }
|
||||
}
|
||||
child.kill('SIGKILL');
|
||||
}
|
||||
resolve();
|
||||
}, 5000);
|
||||
|
||||
child.once('exit', () => {
|
||||
clearTimeout(timeout);
|
||||
resolve();
|
||||
});
|
||||
|
||||
child.once('error', () => {
|
||||
clearTimeout(timeout);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -721,11 +707,11 @@ export class GatewayManager extends EventEmitter {
|
||||
|
||||
try {
|
||||
// Platform-specific command to find processes listening on the gateway port.
|
||||
// On Windows, lsof doesn't exist; use PowerShell's Get-NetTCPConnection instead.
|
||||
// -WindowStyle Hidden is used to prevent PowerShell from popping up a brief console window
|
||||
// even when windowsHide: true is passed to cp.exec.
|
||||
// We use native commands (netstat on Windows) to avoid triggering AV blocks
|
||||
// that flag "powershell -WindowStyle Hidden" as malware behavior.
|
||||
// windowsHide: true in cp.exec natively prevents the black command window.
|
||||
const cmd = process.platform === 'win32'
|
||||
? `powershell -WindowStyle Hidden -NoProfile -Command "(Get-NetTCPConnection -LocalPort ${port} -State Listen -ErrorAction SilentlyContinue).OwningProcess"`
|
||||
? `netstat -ano | findstr :${port}`
|
||||
: `lsof -i :${port} -sTCP:LISTEN -t`;
|
||||
|
||||
const { stdout } = await new Promise<{ stdout: string }>((resolve, reject) => {
|
||||
@@ -738,9 +724,23 @@ export class GatewayManager extends EventEmitter {
|
||||
});
|
||||
|
||||
if (stdout.trim()) {
|
||||
const pids = stdout.trim().split(/\r?\n/)
|
||||
.map(s => s.trim())
|
||||
.filter(Boolean);
|
||||
// Parse netstat or lsof output to extract PIDs
|
||||
let pids: string[] = [];
|
||||
if (process.platform === 'win32') {
|
||||
// netstat -ano output format:
|
||||
// TCP 127.0.0.1:3000 0.0.0.0:0 LISTENING 12345
|
||||
const lines = stdout.trim().split(/\r?\n/);
|
||||
for (const line of lines) {
|
||||
const parts = line.trim().split(/\s+/);
|
||||
if (parts.length >= 5 && parts[3] === 'LISTENING') {
|
||||
pids.push(parts[4]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
pids = stdout.trim().split(/\r?\n/).map(s => s.trim()).filter(Boolean);
|
||||
}
|
||||
// Remove duplicate PIDs
|
||||
pids = [...new Set(pids)];
|
||||
|
||||
if (pids.length > 0) {
|
||||
if (!this.process || !pids.includes(String(this.process.pid))) {
|
||||
@@ -756,12 +756,11 @@ export class GatewayManager extends EventEmitter {
|
||||
for (const pid of pids) {
|
||||
try {
|
||||
if (process.platform === 'win32') {
|
||||
// Use PowerShell with -WindowStyle Hidden to kill the process without
|
||||
// flashing a black console window. taskkill.exe is a console app and
|
||||
// can flash a window even when windowsHide: true is set.
|
||||
// Use taskkill with windowsHide: true. This natively hides the console
|
||||
// flash without needing PowerShell, avoiding AV alerts.
|
||||
import('child_process').then(cp => {
|
||||
cp.exec(
|
||||
`powershell -WindowStyle Hidden -NoProfile -Command "Stop-Process -Id ${pid} -Force -ErrorAction SilentlyContinue"`,
|
||||
`taskkill /F /PID ${pid} /T`,
|
||||
{ timeout: 5000, windowsHide: true },
|
||||
() => { }
|
||||
);
|
||||
@@ -839,36 +838,23 @@ export class GatewayManager extends EventEmitter {
|
||||
: process.env.PATH || '';
|
||||
|
||||
const uvEnv = await getUvMirrorEnv();
|
||||
const command = app.isPackaged ? getNodeExecutablePath() : 'node';
|
||||
const args = [entryScript, 'doctor', '--fix', '--yes', '--non-interactive'];
|
||||
const doctorArgs = ['doctor', '--fix', '--yes', '--non-interactive'];
|
||||
logger.info(
|
||||
`Running OpenClaw doctor repair (command="${command}", args="${args.join(' ')}", cwd="${openclawDir}", bundledBin=${binPathExists ? 'yes' : 'no'})`
|
||||
`Running OpenClaw doctor repair (entry="${entryScript}", args="${doctorArgs.join(' ')}", cwd="${openclawDir}", bundledBin=${binPathExists ? 'yes' : 'no'})`
|
||||
);
|
||||
|
||||
return new Promise<boolean>((resolve) => {
|
||||
const spawnEnv: Record<string, string | undefined> = {
|
||||
const forkEnv: Record<string, string | undefined> = {
|
||||
...process.env,
|
||||
PATH: finalPath,
|
||||
...uvEnv,
|
||||
OPENCLAW_NO_RESPAWN: '1',
|
||||
};
|
||||
|
||||
if (app.isPackaged) {
|
||||
spawnEnv['ELECTRON_RUN_AS_NODE'] = '1';
|
||||
spawnEnv['OPENCLAW_NO_RESPAWN'] = '1';
|
||||
const existingNodeOpts = spawnEnv['NODE_OPTIONS'] ?? '';
|
||||
if (!existingNodeOpts.includes('--disable-warning=ExperimentalWarning') &&
|
||||
!existingNodeOpts.includes('--no-warnings')) {
|
||||
spawnEnv['NODE_OPTIONS'] = `${existingNodeOpts} --disable-warning=ExperimentalWarning`.trim();
|
||||
}
|
||||
}
|
||||
|
||||
const child = spawn(command, args, {
|
||||
const child = utilityProcess.fork(entryScript, doctorArgs, {
|
||||
cwd: openclawDir,
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
detached: false,
|
||||
shell: false,
|
||||
windowsHide: true,
|
||||
env: spawnEnv,
|
||||
stdio: 'pipe',
|
||||
env: forkEnv as NodeJS.ProcessEnv,
|
||||
});
|
||||
|
||||
let settled = false;
|
||||
@@ -881,7 +867,7 @@ export class GatewayManager extends EventEmitter {
|
||||
const timeout = setTimeout(() => {
|
||||
logger.error('OpenClaw doctor repair timed out after 120000ms');
|
||||
try {
|
||||
child.kill('SIGTERM');
|
||||
child.kill();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
@@ -912,14 +898,14 @@ export class GatewayManager extends EventEmitter {
|
||||
}
|
||||
});
|
||||
|
||||
child.on('exit', (code, signal) => {
|
||||
child.on('exit', (code: number) => {
|
||||
clearTimeout(timeout);
|
||||
if (code === 0) {
|
||||
logger.info('OpenClaw doctor repair completed successfully');
|
||||
finish(true);
|
||||
return;
|
||||
}
|
||||
logger.warn(`OpenClaw doctor repair exited (${this.formatExit(code, signal)})`);
|
||||
logger.warn(`OpenClaw doctor repair exited (code=${code})`);
|
||||
finish(false);
|
||||
});
|
||||
});
|
||||
@@ -976,40 +962,16 @@ export class GatewayManager extends EventEmitter {
|
||||
logger.warn('Failed to sync browser config to openclaw.json:', err);
|
||||
}
|
||||
|
||||
let command: string;
|
||||
let args: string[];
|
||||
let mode: 'packaged' | 'dev-built' | 'dev-pnpm';
|
||||
|
||||
// Determine the Node.js executable
|
||||
// In packaged Electron app, use process.execPath with ELECTRON_RUN_AS_NODE=1
|
||||
// which makes the Electron binary behave as plain Node.js.
|
||||
// In development, use system 'node'.
|
||||
const gatewayArgs = ['gateway', '--port', String(this.status.port), '--token', gatewayToken, '--allow-unconfigured'];
|
||||
|
||||
if (app.isPackaged) {
|
||||
// Production: use Electron binary as Node.js via ELECTRON_RUN_AS_NODE
|
||||
// On macOS, use the Electron Helper binary to avoid extra dock icons
|
||||
if (existsSync(entryScript)) {
|
||||
command = getNodeExecutablePath();
|
||||
args = [entryScript, ...gatewayArgs];
|
||||
mode = 'packaged';
|
||||
} else {
|
||||
const errMsg = `OpenClaw entry script not found at: ${entryScript}`;
|
||||
logger.error(errMsg);
|
||||
throw new Error(errMsg);
|
||||
}
|
||||
} else if (isOpenClawBuilt() && existsSync(entryScript)) {
|
||||
// Development with built package: use system node
|
||||
command = 'node';
|
||||
args = [entryScript, ...gatewayArgs];
|
||||
mode = 'dev-built';
|
||||
} else {
|
||||
// Development without build: use pnpm dev
|
||||
command = 'pnpm';
|
||||
args = ['run', 'dev', ...gatewayArgs];
|
||||
mode = 'dev-pnpm';
|
||||
// utilityProcess.fork() works for both dev and packaged — no ELECTRON_RUN_AS_NODE needed.
|
||||
if (!existsSync(entryScript)) {
|
||||
const errMsg = `OpenClaw entry script not found at: ${entryScript}`;
|
||||
logger.error(errMsg);
|
||||
throw new Error(errMsg);
|
||||
}
|
||||
|
||||
const gatewayArgs = ['gateway', '--port', String(this.status.port), '--token', gatewayToken, '--allow-unconfigured'];
|
||||
const mode = app.isPackaged ? 'packaged' : 'dev';
|
||||
|
||||
// Resolve bundled bin path for uv
|
||||
const platform = process.platform;
|
||||
const arch = process.arch;
|
||||
@@ -1067,13 +1029,15 @@ export class GatewayManager extends EventEmitter {
|
||||
const proxyEnv = buildProxyEnv(appSettings);
|
||||
const resolvedProxy = resolveProxySettings(appSettings);
|
||||
logger.info(
|
||||
`Starting Gateway process (mode=${mode}, port=${this.status.port}, command="${command}", args="${this.sanitizeSpawnArgs(args).join(' ')}", cwd="${openclawDir}", bundledBin=${binPathExists ? 'yes' : 'no'}, providerKeys=${loadedProviderKeyCount}, proxy=${appSettings.proxyEnabled ? `http=${resolvedProxy.httpProxy || '-'}, https=${resolvedProxy.httpsProxy || '-'}, all=${resolvedProxy.allProxy || '-'}` : 'disabled'})`
|
||||
`Starting Gateway process (mode=${mode}, port=${this.status.port}, entry="${entryScript}", args="${this.sanitizeSpawnArgs(gatewayArgs).join(' ')}", cwd="${openclawDir}", bundledBin=${binPathExists ? 'yes' : 'no'}, providerKeys=${loadedProviderKeyCount}, proxy=${appSettings.proxyEnabled ? `http=${resolvedProxy.httpProxy || '-'}, https=${resolvedProxy.httpsProxy || '-'}, all=${resolvedProxy.allProxy || '-'}` : 'disabled'})`
|
||||
);
|
||||
this.lastSpawnSummary = `mode=${mode}, command="${command}", args="${this.sanitizeSpawnArgs(args).join(' ')}", cwd="${openclawDir}"`;
|
||||
this.lastSpawnSummary = `mode=${mode}, entry="${entryScript}", args="${this.sanitizeSpawnArgs(gatewayArgs).join(' ')}", cwd="${openclawDir}"`;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
// Reset exit tracking for this new process instance.
|
||||
this.processExitCode = null;
|
||||
const { NODE_OPTIONS: _nodeOptions, ...baseEnv } = process.env;
|
||||
const spawnEnv: Record<string, string | undefined> = {
|
||||
const forkEnv: Record<string, string | undefined> = {
|
||||
...baseEnv,
|
||||
PATH: finalPath,
|
||||
...providerEnv,
|
||||
@@ -1082,29 +1046,17 @@ export class GatewayManager extends EventEmitter {
|
||||
OPENCLAW_GATEWAY_TOKEN: gatewayToken,
|
||||
OPENCLAW_SKIP_CHANNELS: '',
|
||||
CLAWDBOT_SKIP_CHANNELS: '',
|
||||
// Prevent OpenClaw from respawning itself inside the utility process
|
||||
OPENCLAW_NO_RESPAWN: '1',
|
||||
};
|
||||
|
||||
// Critical: In packaged mode, make Electron binary act as Node.js
|
||||
if (app.isPackaged) {
|
||||
spawnEnv['ELECTRON_RUN_AS_NODE'] = '1';
|
||||
// Prevent OpenClaw entry.ts from respawning itself (which would create
|
||||
// another child process and a second "exec" dock icon on macOS)
|
||||
spawnEnv['OPENCLAW_NO_RESPAWN'] = '1';
|
||||
// Pre-set the NODE_OPTIONS that entry.ts would have added via respawn
|
||||
const existingNodeOpts = spawnEnv['NODE_OPTIONS'] ?? '';
|
||||
if (!existingNodeOpts.includes('--disable-warning=ExperimentalWarning') &&
|
||||
!existingNodeOpts.includes('--no-warnings')) {
|
||||
spawnEnv['NODE_OPTIONS'] = `${existingNodeOpts} --disable-warning=ExperimentalWarning`.trim();
|
||||
}
|
||||
}
|
||||
|
||||
// Inject fetch preload so OpenRouter requests carry ClawX headers.
|
||||
// The preload patches globalThis.fetch before any module loads.
|
||||
try {
|
||||
const preloadPath = ensureGatewayFetchPreload();
|
||||
if (existsSync(preloadPath)) {
|
||||
spawnEnv['NODE_OPTIONS'] = appendNodeRequireToNodeOptions(
|
||||
spawnEnv['NODE_OPTIONS'],
|
||||
forkEnv['NODE_OPTIONS'] = appendNodeRequireToNodeOptions(
|
||||
forkEnv['NODE_OPTIONS'],
|
||||
preloadPath,
|
||||
);
|
||||
}
|
||||
@@ -1112,17 +1064,13 @@ export class GatewayManager extends EventEmitter {
|
||||
logger.warn('Failed to set up OpenRouter headers preload:', err);
|
||||
}
|
||||
|
||||
const useShell = !app.isPackaged && process.platform === 'win32';
|
||||
const spawnCmd = useShell ? quoteForCmd(command) : command;
|
||||
const spawnArgs = useShell ? args.map(a => quoteForCmd(a)) : args;
|
||||
|
||||
this.process = spawn(spawnCmd, spawnArgs, {
|
||||
// utilityProcess.fork() runs the .mjs entry directly without spawning a
|
||||
// shell or visible console window. Works identically in dev and packaged.
|
||||
this.process = utilityProcess.fork(entryScript, gatewayArgs, {
|
||||
cwd: openclawDir,
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
detached: false,
|
||||
shell: useShell,
|
||||
windowsHide: true,
|
||||
env: spawnEnv,
|
||||
stdio: 'pipe',
|
||||
env: forkEnv as NodeJS.ProcessEnv,
|
||||
serviceName: 'OpenClaw Gateway',
|
||||
});
|
||||
const child = this.process;
|
||||
this.ownsProcess = true;
|
||||
@@ -1133,10 +1081,11 @@ export class GatewayManager extends EventEmitter {
|
||||
reject(error);
|
||||
});
|
||||
|
||||
child.on('exit', (code, signal) => {
|
||||
child.on('exit', (code: number) => {
|
||||
this.processExitCode = code;
|
||||
const expectedExit = !this.shouldReconnect || this.status.state === 'stopped';
|
||||
const level = expectedExit ? logger.info : logger.warn;
|
||||
level(`Gateway process exited (${this.formatExit(code, signal)}, expected=${expectedExit ? 'yes' : 'no'})`);
|
||||
level(`Gateway process exited (code=${code}, expected=${expectedExit ? 'yes' : 'no'})`);
|
||||
this.ownsProcess = false;
|
||||
if (this.process === child) {
|
||||
this.process = null;
|
||||
@@ -1149,9 +1098,7 @@ export class GatewayManager extends EventEmitter {
|
||||
}
|
||||
});
|
||||
|
||||
child.on('close', (code, signal) => {
|
||||
logger.debug(`Gateway process stdio closed (${this.formatExit(code, signal)})`);
|
||||
});
|
||||
// UtilityProcess doesn't emit 'close'; stdout/stderr end naturally on exit.
|
||||
|
||||
// Log stderr
|
||||
child.stderr?.on('data', (data) => {
|
||||
@@ -1186,12 +1133,12 @@ export class GatewayManager extends EventEmitter {
|
||||
private async waitForReady(retries = 2400, interval = 250): Promise<void> {
|
||||
const child = this.process;
|
||||
for (let i = 0; i < retries; i++) {
|
||||
// Early exit if the gateway process has already exited
|
||||
if (child && (child.exitCode !== null || child.signalCode !== null)) {
|
||||
const code = child.exitCode;
|
||||
const signal = child.signalCode;
|
||||
logger.error(`Gateway process exited before ready (${this.formatExit(code, signal)})`);
|
||||
throw new Error(`Gateway process exited before becoming ready (${this.formatExit(code, signal)})`);
|
||||
// Early exit if the gateway process has already exited.
|
||||
// UtilityProcess has no synchronous exitCode/signalCode — use our tracked flag.
|
||||
if (child && this.processExitCode !== null) {
|
||||
const code = this.processExitCode;
|
||||
logger.error(`Gateway process exited before ready (code=${code})`);
|
||||
throw new Error(`Gateway process exited before becoming ready (code=${code})`);
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "clawx",
|
||||
"version": "0.1.22-alpha.2",
|
||||
"version": "0.1.22-alpha.5",
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"@whiskeysockets/baileys",
|
||||
|
||||
@@ -3,83 +3,64 @@
|
||||
; Install: enables long paths, adds resources\cli to user PATH for openclaw CLI.
|
||||
; Uninstall: removes the PATH entry and optionally deletes user data.
|
||||
|
||||
; When customCheckAppRunning is defined, electron-builder skips its conditional
|
||||
; !include for getProcessInfo.nsh and the "Var pid" declaration. We must do
|
||||
; both ourselves so ${GetProcessInfo} and $pid are available.
|
||||
!ifndef getProcessInfo_included
|
||||
!define getProcessInfo_included
|
||||
!include "getProcessInfo.nsh"
|
||||
Var pid
|
||||
!endif
|
||||
!ifndef nsProcess_included
|
||||
!define nsProcess_included
|
||||
!ifndef nsProcess::FindProcess
|
||||
!include "nsProcess.nsh"
|
||||
!endif
|
||||
|
||||
!macro customCheckAppRunning
|
||||
${GetProcessInfo} 0 $pid $1 $2 $3 $4
|
||||
${if} $3 != "${APP_EXECUTABLE_FILENAME}"
|
||||
${nsProcess::FindProcess} "${APP_EXECUTABLE_FILENAME}" $R0
|
||||
|
||||
${if} $R0 == 0
|
||||
${if} ${isUpdated}
|
||||
# allow app to exit without explicit kill
|
||||
Sleep 300
|
||||
Sleep 1000
|
||||
Goto doStopProcess
|
||||
${endIf}
|
||||
MessageBox MB_OKCANCEL|MB_ICONEXCLAMATION "$(appRunning)" /SD IDOK IDOK doStopProcess
|
||||
Quit
|
||||
|
||||
# Instead of launching cmd.exe /c tasklist, use the nsProcess plugin directly for all environments.
|
||||
# This prevents the brief black cmd window from flashing.
|
||||
${nsProcess::FindProcess} "${APP_EXECUTABLE_FILENAME}" $R0
|
||||
doStopProcess:
|
||||
DetailPrint `Closing running "${PRODUCT_NAME}"...`
|
||||
|
||||
${if} $R0 == 0
|
||||
${if} ${isUpdated}
|
||||
# allow app to exit without explicit kill
|
||||
# Silently kill the process using nsProcess instead of taskkill / cmd.exe
|
||||
${nsProcess::KillProcess} "${APP_EXECUTABLE_FILENAME}" $R0
|
||||
|
||||
# to ensure that files are not "in-use"
|
||||
Sleep 300
|
||||
|
||||
# Retry counter
|
||||
StrCpy $R1 0
|
||||
|
||||
loop:
|
||||
IntOp $R1 $R1 + 1
|
||||
|
||||
${nsProcess::FindProcess} "${APP_EXECUTABLE_FILENAME}" $R0
|
||||
${if} $R0 == 0
|
||||
# wait to give a chance to exit gracefully
|
||||
Sleep 1000
|
||||
Goto doStopProcess
|
||||
${endIf}
|
||||
MessageBox MB_OKCANCEL|MB_ICONEXCLAMATION "$(appRunning)" /SD IDOK IDOK doStopProcess
|
||||
Quit
|
||||
|
||||
doStopProcess:
|
||||
DetailPrint `Closing running "${PRODUCT_NAME}"...`
|
||||
|
||||
# Silently kill the process using nsProcess instead of taskkill / cmd.exe
|
||||
${nsProcess::KillProcess} "${APP_EXECUTABLE_FILENAME}" $R0
|
||||
|
||||
# to ensure that files are not "in-use"
|
||||
Sleep 300
|
||||
|
||||
# Retry counter
|
||||
StrCpy $R1 0
|
||||
|
||||
loop:
|
||||
IntOp $R1 $R1 + 1
|
||||
|
||||
${nsProcess::KillProcess} "${APP_EXECUTABLE_FILENAME}" $R0
|
||||
|
||||
${nsProcess::FindProcess} "${APP_EXECUTABLE_FILENAME}" $R0
|
||||
${if} $R0 == 0
|
||||
# wait to give a chance to exit gracefully
|
||||
Sleep 1000
|
||||
${nsProcess::KillProcess} "${APP_EXECUTABLE_FILENAME}" $R0
|
||||
|
||||
${nsProcess::FindProcess} "${APP_EXECUTABLE_FILENAME}" $R0
|
||||
${If} $R0 == 0
|
||||
DetailPrint `Waiting for "${PRODUCT_NAME}" to close.`
|
||||
Sleep 2000
|
||||
${else}
|
||||
Goto not_running
|
||||
${endIf}
|
||||
${If} $R0 == 0
|
||||
DetailPrint `Waiting for "${PRODUCT_NAME}" to close.`
|
||||
Sleep 2000
|
||||
${else}
|
||||
Goto not_running
|
||||
${endIf}
|
||||
${else}
|
||||
Goto not_running
|
||||
${endIf}
|
||||
|
||||
# App likely running with elevated permissions.
|
||||
# Ask user to close it manually
|
||||
${if} $R1 > 1
|
||||
MessageBox MB_RETRYCANCEL|MB_ICONEXCLAMATION "$(appCannotBeClosed)" /SD IDCANCEL IDRETRY loop
|
||||
Quit
|
||||
${else}
|
||||
Goto loop
|
||||
${endIf}
|
||||
not_running:
|
||||
nsProcess::Unload
|
||||
${endIf}
|
||||
# App likely running with elevated permissions.
|
||||
# Ask user to close it manually
|
||||
${if} $R1 > 1
|
||||
MessageBox MB_RETRYCANCEL|MB_ICONEXCLAMATION "$(appCannotBeClosed)" /SD IDCANCEL IDRETRY loop
|
||||
Quit
|
||||
${else}
|
||||
Goto loop
|
||||
${endIf}
|
||||
not_running:
|
||||
${nsProcess::Unload}
|
||||
${endIf}
|
||||
!macroend
|
||||
|
||||
|
||||
@@ -948,7 +948,8 @@ function ProviderContent({
|
||||
|
||||
try {
|
||||
// Validate key if the provider requires one and a key was entered
|
||||
if (requiresKey && apiKey) {
|
||||
const isApiKeyRequired = requiresKey || (supportsApiKey && authMode === 'apikey');
|
||||
if (isApiKeyRequired && apiKey) {
|
||||
const result = await window.electron.ipcRenderer.invoke(
|
||||
'provider:validateKey',
|
||||
selectedProviderConfigId || selectedProvider,
|
||||
@@ -1024,9 +1025,10 @@ function ProviderContent({
|
||||
};
|
||||
|
||||
// Can the user submit?
|
||||
const isApiKeyRequired = requiresKey || (supportsApiKey && authMode === 'apikey');
|
||||
const canSubmit =
|
||||
selectedProvider
|
||||
&& (requiresKey ? apiKey.length > 0 : true)
|
||||
&& (isApiKeyRequired ? apiKey.length > 0 : true)
|
||||
&& (showModelIdField ? modelId.trim().length > 0 : true)
|
||||
&& !useOAuthFlow;
|
||||
|
||||
@@ -1197,7 +1199,7 @@ function ProviderContent({
|
||||
)}
|
||||
|
||||
{/* API Key field (hidden for ollama) */}
|
||||
{(!isOAuth || (supportsApiKey && authMode === 'apikey')) && requiresKey && (
|
||||
{(!isOAuth || (supportsApiKey && authMode === 'apikey')) && (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="apiKey">{t('provider.apiKey')}</Label>
|
||||
<div className="relative">
|
||||
|
||||
Reference in New Issue
Block a user