fix: use Bun runtime for Windows daemon spawn (#1086)

Co-authored-by: root <root@localhost.localdomain>
This commit is contained in:
Mark L
2026-02-16 13:31:04 +08:00
committed by GitHub
parent ddc25372c1
commit 6d1f17adee
2 changed files with 156 additions and 4 deletions
+99 -4
View File
@@ -33,6 +33,93 @@ const ORPHAN_PROCESS_PATTERNS = [
// Only kill processes older than this to avoid killing the current session
const ORPHAN_MAX_AGE_MINUTES = 30;
interface RuntimeResolverOptions {
platform?: NodeJS.Platform;
execPath?: string;
env?: NodeJS.ProcessEnv;
homeDirectory?: string;
pathExists?: (candidatePath: string) => boolean;
lookupInPath?: (binaryName: string, platform: NodeJS.Platform) => string | null;
}
function isBunExecutablePath(executablePath: string | undefined | null): boolean {
if (!executablePath) return false;
return /(^|[\\/])bun(\.exe)?$/i.test(executablePath.trim());
}
function lookupBinaryInPath(binaryName: string, platform: NodeJS.Platform): string | null {
const command = platform === 'win32' ? `where ${binaryName}` : `which ${binaryName}`;
try {
const output = execSync(command, {
stdio: ['ignore', 'pipe', 'ignore'],
encoding: 'utf-8'
});
const firstMatch = output
.split(/\r?\n/)
.map(line => line.trim())
.find(line => line.length > 0);
return firstMatch || null;
} catch {
return null;
}
}
/**
* Resolve the runtime executable for spawning the worker daemon.
*
* Windows must prefer Bun because worker-service.cjs imports bun:sqlite,
* which is unavailable in Node.js.
*/
export function resolveWorkerRuntimePath(options: RuntimeResolverOptions = {}): string | null {
const platform = options.platform ?? process.platform;
const execPath = options.execPath ?? process.execPath;
// Non-Windows currently relies on the runtime that launched worker-service.
if (platform !== 'win32') {
return execPath;
}
// If already running under Bun, reuse it directly.
if (isBunExecutablePath(execPath)) {
return execPath;
}
const env = options.env ?? process.env;
const homeDirectory = options.homeDirectory ?? homedir();
const pathExists = options.pathExists ?? existsSync;
const lookupInPath = options.lookupInPath ?? lookupBinaryInPath;
const candidatePaths = [
env.BUN,
env.BUN_PATH,
path.join(homeDirectory, '.bun', 'bin', 'bun.exe'),
path.join(homeDirectory, '.bun', 'bin', 'bun'),
env.USERPROFILE ? path.join(env.USERPROFILE, '.bun', 'bin', 'bun.exe') : undefined,
env.LOCALAPPDATA ? path.join(env.LOCALAPPDATA, 'bun', 'bun.exe') : undefined,
env.LOCALAPPDATA ? path.join(env.LOCALAPPDATA, 'bun', 'bin', 'bun.exe') : undefined,
];
for (const candidate of candidatePaths) {
const normalized = candidate?.trim();
if (!normalized) continue;
if (isBunExecutablePath(normalized) && pathExists(normalized)) {
return normalized;
}
// Allow command-style values from env (e.g. BUN=bun)
if (normalized.toLowerCase() === 'bun') {
return normalized;
}
}
return lookupInPath('bun', platform);
}
export interface PidInfo {
pid: number;
port: number;
@@ -368,9 +455,16 @@ export function spawnDaemon(
// Use PowerShell Start-Process to spawn a hidden, independent process
// Unlike WMIC, PowerShell inherits environment variables from parent
// -WindowStyle Hidden prevents console popup
const execPath = process.execPath;
const script = scriptPath;
const psCommand = `Start-Process -FilePath '${execPath}' -ArgumentList '${script}','--daemon' -WindowStyle Hidden`;
const runtimePath = resolveWorkerRuntimePath();
if (!runtimePath) {
logger.error('SYSTEM', 'Failed to locate Bun runtime for Windows worker spawn');
return undefined;
}
const escapedRuntimePath = runtimePath.replace(/'/g, "''");
const escapedScriptPath = scriptPath.replace(/'/g, "''");
const psCommand = `Start-Process -FilePath '${escapedRuntimePath}' -ArgumentList '${escapedScriptPath}','--daemon' -WindowStyle Hidden`;
try {
execSync(`powershell -NoProfile -Command "${psCommand}"`, {
@@ -379,7 +473,8 @@ export function spawnDaemon(
env
});
return 0;
} catch {
} catch (error) {
logger.error('SYSTEM', 'Failed to spawn worker daemon on Windows', { runtimePath }, error as Error);
return undefined;
}
}
@@ -11,6 +11,7 @@ import {
isProcessAlive,
cleanStalePidFile,
spawnDaemon,
resolveWorkerRuntimePath,
type PidInfo
} from '../../src/services/infrastructure/index.js';
@@ -225,6 +226,62 @@ describe('ProcessManager', () => {
});
});
describe('resolveWorkerRuntimePath', () => {
it('should return current runtime on non-Windows platforms', () => {
const resolved = resolveWorkerRuntimePath({
platform: 'linux',
execPath: '/usr/bin/node'
});
expect(resolved).toBe('/usr/bin/node');
});
it('should reuse execPath when already running under Bun on Windows', () => {
const resolved = resolveWorkerRuntimePath({
platform: 'win32',
execPath: 'C:\\Users\\alice\\.bun\\bin\\bun.exe'
});
expect(resolved).toBe('C:\\Users\\alice\\.bun\\bin\\bun.exe');
});
it('should prefer configured Bun path from environment when available', () => {
const resolved = resolveWorkerRuntimePath({
platform: 'win32',
execPath: 'C:\\Program Files\\nodejs\\node.exe',
env: { BUN: 'C:\\tools\\bun.exe' } as NodeJS.ProcessEnv,
pathExists: candidatePath => candidatePath === 'C:\\tools\\bun.exe',
lookupInPath: () => null
});
expect(resolved).toBe('C:\\tools\\bun.exe');
});
it('should fall back to PATH lookup when no Bun candidate exists', () => {
const resolved = resolveWorkerRuntimePath({
platform: 'win32',
execPath: 'C:\\Program Files\\nodejs\\node.exe',
env: {} as NodeJS.ProcessEnv,
pathExists: () => false,
lookupInPath: () => 'C:\\Program Files\\Bun\\bun.exe'
});
expect(resolved).toBe('C:\\Program Files\\Bun\\bun.exe');
});
it('should return null when Bun cannot be resolved on Windows', () => {
const resolved = resolveWorkerRuntimePath({
platform: 'win32',
execPath: 'C:\\Program Files\\nodejs\\node.exe',
env: {} as NodeJS.ProcessEnv,
pathExists: () => false,
lookupInPath: () => null
});
expect(resolved).toBeNull();
});
});
describe('isProcessAlive', () => {
it('should return true for the current process', () => {
expect(isProcessAlive(process.pid)).toBe(true);