mirror of https://github.com/garrytan/gstack.git
Fix Windows browse daemon cold start
This commit is contained in:
parent
6324160dcc
commit
3aa2846738
|
|
@ -239,7 +239,17 @@ async function startServer(extraEnv?: Record<string, string>): Promise<ServerSta
|
|||
`spawn(process.execPath,[${JSON.stringify(NODE_SERVER_SCRIPT)}],` +
|
||||
`{detached:true,stdio:['ignore','ignore','ignore'],env:Object.assign({},process.env,` +
|
||||
`${extraEnvStr})}).unref()`;
|
||||
spawnSync('node', ['-e', launcherCode], { stdio: 'ignore' });
|
||||
const launcher = Bun.spawnSync(['node', '-e', launcherCode], {
|
||||
stdout: 'pipe',
|
||||
stderr: 'pipe',
|
||||
timeout: 5000,
|
||||
});
|
||||
const launcherExit = (launcher as any).exitCode ?? (launcher as any).status ?? 0;
|
||||
if (launcherExit !== 0) {
|
||||
const stderr = launcher.stderr?.toString().trim();
|
||||
const stdout = launcher.stdout?.toString().trim();
|
||||
throw new Error(`Windows launcher failed${stderr || stdout ? `:\n${stderr || stdout}` : ''}`);
|
||||
}
|
||||
} else {
|
||||
// macOS/Linux: Bun.spawn().unref() only removes the child from Bun's event
|
||||
// loop — it does NOT call setsid(), so the spawned server stays in the
|
||||
|
|
|
|||
|
|
@ -129,7 +129,7 @@ let activeShutdown: ((code?: number) => Promise<void>) | null = null;
|
|||
// silently weakened by misconfiguration.
|
||||
function sanitizeAuthToken(raw: string | undefined): string | null {
|
||||
if (!raw) return null;
|
||||
const stripped = raw.replace(/[\s -]/g, '');
|
||||
const stripped = raw.replace(/[\s\u00a0\u200b-\u200d\ufeff]/g, '');
|
||||
if (stripped.length < 16) return null;
|
||||
return stripped;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,54 @@ import * as path from 'path';
|
|||
import { safeUnlink, safeKill, isProcessAlive } from './error-handling';
|
||||
import { writeSecureFile, mkdirSecure } from './file-permissions';
|
||||
|
||||
export interface BunSpawnCommand {
|
||||
command: string;
|
||||
argsPrefix: string[];
|
||||
}
|
||||
|
||||
function envPath(env: Record<string, string | undefined>): string {
|
||||
return env.PATH || env.Path || env.path || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a Bun executable suitable for a detached child spawn.
|
||||
*
|
||||
* On Windows, npm installs expose `bun` as a POSIX shell shim plus `bun.cmd`.
|
||||
* Node/Bun process spawning does not reliably execute those shims directly,
|
||||
* so prefer the real `node_modules/bun/bin/bun.exe` beside them.
|
||||
*/
|
||||
export function resolveBunSpawnCommand(
|
||||
env: Record<string, string | undefined> = process.env,
|
||||
platform: NodeJS.Platform = process.platform,
|
||||
execPath: string = process.execPath,
|
||||
): BunSpawnCommand | null {
|
||||
if (platform !== 'win32') return { command: 'bun', argsPrefix: [] };
|
||||
|
||||
if (execPath && path.basename(execPath).toLowerCase() === 'bun.exe' && fs.existsSync(execPath)) {
|
||||
return { command: execPath, argsPrefix: [] };
|
||||
}
|
||||
|
||||
const dirs = envPath(env).split(path.delimiter).filter(Boolean);
|
||||
for (const dir of dirs) {
|
||||
const candidates = [
|
||||
path.join(dir, 'bun.exe'),
|
||||
path.join(dir, 'node_modules', 'bun', 'bin', 'bun.exe'),
|
||||
];
|
||||
for (const candidate of candidates) {
|
||||
if (fs.existsSync(candidate)) return { command: candidate, argsPrefix: [] };
|
||||
}
|
||||
|
||||
for (const shim of ['bun.cmd', 'bun.bat']) {
|
||||
const shimPath = path.join(dir, shim);
|
||||
if (fs.existsSync(shimPath)) {
|
||||
return { command: 'cmd.exe', argsPrefix: ['/d', '/s', '/c', `"${shimPath}"`] };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Locate the terminal-agent script on disk. In dev (cli.ts running via
|
||||
* `bun run`), it lives next to this file in browse/src. In a compiled
|
||||
|
|
@ -68,16 +116,24 @@ export function spawnTerminalAgent(opts: {
|
|||
}
|
||||
const script = opts.scriptPath || resolveTerminalAgentScript();
|
||||
if (!script || !fs.existsSync(script)) return null;
|
||||
const proc = (Bun as any).spawn(['bun', 'run', script], {
|
||||
cwd: opts.cwd || process.cwd(),
|
||||
env: {
|
||||
...process.env,
|
||||
BROWSE_STATE_FILE: opts.stateFile,
|
||||
BROWSE_SERVER_PORT: String(opts.serverPort),
|
||||
...(opts.extraEnv || {}),
|
||||
},
|
||||
stdio: ['ignore', 'ignore', 'ignore'],
|
||||
});
|
||||
const bun = resolveBunSpawnCommand();
|
||||
if (!bun) return null;
|
||||
let proc: any;
|
||||
try {
|
||||
proc = (Bun as any).spawn([bun.command, ...bun.argsPrefix, 'run', script], {
|
||||
cwd: opts.cwd || process.cwd(),
|
||||
env: {
|
||||
...process.env,
|
||||
BROWSE_STATE_FILE: opts.stateFile,
|
||||
BROWSE_SERVER_PORT: String(opts.serverPort),
|
||||
...(opts.extraEnv || {}),
|
||||
},
|
||||
stdio: ['ignore', 'ignore', 'ignore'],
|
||||
});
|
||||
} catch (err: any) {
|
||||
if (err?.code === 'ENOENT' || err?.code === 'EACCES') return null;
|
||||
throw err;
|
||||
}
|
||||
proc.unref?.();
|
||||
return proc.pid ?? null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { describe, test, expect } from 'bun:test';
|
||||
import { resolveConfig, ensureStateDir, readVersionHash, getGitRoot, getRemoteSlug, resolveGstackHome, resolveChromiumProfile, cleanSingletonLocks } from '../src/config';
|
||||
import { resolveBunSpawnCommand } from '../src/terminal-agent-control';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
|
|
@ -342,6 +343,42 @@ describe('cli command dispatch', () => {
|
|||
expect(cliSource).toContain('Starting server in headed mode');
|
||||
expect(cliSource).toContain('Starting server with proxy');
|
||||
});
|
||||
|
||||
test('Windows launcher uses Bun.spawnSync for compiled CLI compatibility', () => {
|
||||
expect(cliSource).toContain("Bun.spawnSync(['node', '-e', launcherCode]");
|
||||
expect(cliSource).not.toContain("spawnSync('node', ['-e', launcherCode]");
|
||||
});
|
||||
});
|
||||
|
||||
describe('server source portability', () => {
|
||||
const serverSource = fs.readFileSync(path.resolve(__dirname, '../src/server.ts'), 'utf-8');
|
||||
|
||||
test('auth-token whitespace regex uses ASCII escapes for Node bundle safety', () => {
|
||||
expect(serverSource).toContain('raw.replace(/[\\s\\u00a0\\u200b-\\u200d\\ufeff]/g');
|
||||
expect(serverSource).not.toContain('raw.replace(/[\\s ');
|
||||
});
|
||||
});
|
||||
|
||||
describe('terminal agent spawn command', () => {
|
||||
test('Windows npm bun shim resolves to real bun.exe', () => {
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'browse-bun-shim-'));
|
||||
const bunExe = path.join(tmpDir, 'node_modules', 'bun', 'bin', 'bun.exe');
|
||||
fs.mkdirSync(path.dirname(bunExe), { recursive: true });
|
||||
fs.writeFileSync(path.join(tmpDir, 'bun.cmd'), '@echo off\n');
|
||||
fs.writeFileSync(bunExe, '');
|
||||
|
||||
try {
|
||||
const result = resolveBunSpawnCommand({ PATH: tmpDir }, 'win32', 'C:\\Windows\\System32\\node.exe');
|
||||
expect(result).toEqual({ command: bunExe, argsPrefix: [] });
|
||||
} finally {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('Windows missing bun is non-fatal for terminal-agent spawn', () => {
|
||||
const result = resolveBunSpawnCommand({ PATH: '' }, 'win32', 'C:\\Windows\\System32\\node.exe');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('write command dispatch', () => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue