fix: use Bun runtime for Windows daemon spawn (#1086)
Co-authored-by: root <root@localhost.localdomain>
This commit is contained in:
@@ -33,6 +33,93 @@ const ORPHAN_PROCESS_PATTERNS = [
|
|||||||
// Only kill processes older than this to avoid killing the current session
|
// Only kill processes older than this to avoid killing the current session
|
||||||
const ORPHAN_MAX_AGE_MINUTES = 30;
|
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 {
|
export interface PidInfo {
|
||||||
pid: number;
|
pid: number;
|
||||||
port: number;
|
port: number;
|
||||||
@@ -368,9 +455,16 @@ export function spawnDaemon(
|
|||||||
// Use PowerShell Start-Process to spawn a hidden, independent process
|
// Use PowerShell Start-Process to spawn a hidden, independent process
|
||||||
// Unlike WMIC, PowerShell inherits environment variables from parent
|
// Unlike WMIC, PowerShell inherits environment variables from parent
|
||||||
// -WindowStyle Hidden prevents console popup
|
// -WindowStyle Hidden prevents console popup
|
||||||
const execPath = process.execPath;
|
const runtimePath = resolveWorkerRuntimePath();
|
||||||
const script = scriptPath;
|
|
||||||
const psCommand = `Start-Process -FilePath '${execPath}' -ArgumentList '${script}','--daemon' -WindowStyle Hidden`;
|
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 {
|
try {
|
||||||
execSync(`powershell -NoProfile -Command "${psCommand}"`, {
|
execSync(`powershell -NoProfile -Command "${psCommand}"`, {
|
||||||
@@ -379,7 +473,8 @@ export function spawnDaemon(
|
|||||||
env
|
env
|
||||||
});
|
});
|
||||||
return 0;
|
return 0;
|
||||||
} catch {
|
} catch (error) {
|
||||||
|
logger.error('SYSTEM', 'Failed to spawn worker daemon on Windows', { runtimePath }, error as Error);
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
isProcessAlive,
|
isProcessAlive,
|
||||||
cleanStalePidFile,
|
cleanStalePidFile,
|
||||||
spawnDaemon,
|
spawnDaemon,
|
||||||
|
resolveWorkerRuntimePath,
|
||||||
type PidInfo
|
type PidInfo
|
||||||
} from '../../src/services/infrastructure/index.js';
|
} 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', () => {
|
describe('isProcessAlive', () => {
|
||||||
it('should return true for the current process', () => {
|
it('should return true for the current process', () => {
|
||||||
expect(isProcessAlive(process.pid)).toBe(true);
|
expect(isProcessAlive(process.pid)).toBe(true);
|
||||||
|
|||||||
Reference in New Issue
Block a user