mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-09 14:38:59 +02:00
chore(browse): explicit windowsHide on every Bun.spawn site + census tripwire (#2575 residual)
Bun.spawn sites were structurally outside the windowsHide census (it swept child_process bindings only). The runtime was already safe — native Bun hides consoles by default and bun-polyfill.cjs defaults windowsHide !== false since #2523/#2539 — but implicit defaults are exactly what regress silently. Every Bun.spawn/spawnSync in browse/src now carries the explicit flag (harmless on unix-only sites like Xvfb/xattr/open), and a second SWEEP in windows-spawn-hide.test.ts fails CI on any new flagless Bun.spawn site. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
ddeeb18edb
commit
c4d91507dd
@@ -256,6 +256,7 @@ async function runToFiles(cmd: string[], opts: RunToFilesOptions): Promise<RunTo
|
||||
// here and closing them after exit instead put us in Bun's fd bookkeeping,
|
||||
// which surfaced as a stray EBADF from epoll_ctl on a later spawn.
|
||||
const proc = Bun.spawn(cmd, {
|
||||
windowsHide: true,
|
||||
cwd: opts.cwd,
|
||||
env: opts.env as any,
|
||||
stdout: Bun.file(outPath) as any,
|
||||
|
||||
@@ -212,6 +212,7 @@ function cleanupLegacyState(): void {
|
||||
if (data.pid && isProcessAlive(data.pid)) {
|
||||
// Verify this is actually a browse server before killing
|
||||
const check = Bun.spawnSync(['ps', '-p', String(data.pid), '-o', 'command='], {
|
||||
windowsHide: true,
|
||||
stdout: 'pipe', stderr: 'pipe', timeout: 2000,
|
||||
});
|
||||
const cmd = check.stdout.toString().trim();
|
||||
@@ -1645,6 +1646,7 @@ Refs: After 'snapshot', use @e1, @e2... as selectors:
|
||||
// (the user asked to SEE the shared browser), and connect's #2219 guard
|
||||
// would otherwise refuse to replace the healthy headless daemon.
|
||||
const connectProc = Bun.spawn([browseBin, 'connect', '--force-restart'], {
|
||||
windowsHide: true,
|
||||
cwd: process.cwd(),
|
||||
stdio: ['ignore', 'inherit', 'inherit'],
|
||||
// Disable parent-PID monitoring: pair-agent needs the server to outlive
|
||||
|
||||
@@ -32,6 +32,7 @@ export interface BrowseConfig {
|
||||
export function getGitRoot(): string | null {
|
||||
try {
|
||||
const proc = Bun.spawnSync(['git', 'rev-parse', '--show-toplevel'], {
|
||||
windowsHide: true,
|
||||
stdout: 'pipe',
|
||||
stderr: 'pipe',
|
||||
// Raised from 2s: under heavy machine load `git rev-parse` routinely
|
||||
@@ -86,6 +87,7 @@ export function resolveConfig(
|
||||
function isIgnoredByGit(projectDir: string, relPath: string): boolean {
|
||||
try {
|
||||
const proc = Bun.spawnSync(['git', 'check-ignore', '-q', '--', relPath], {
|
||||
windowsHide: true,
|
||||
cwd: projectDir, stdout: 'pipe', stderr: 'pipe',
|
||||
timeout: 2_000,
|
||||
});
|
||||
@@ -160,6 +162,7 @@ export function ensureStateDir(config: BrowseConfig): void {
|
||||
export function getRemoteSlug(): string {
|
||||
try {
|
||||
const proc = Bun.spawnSync(['git', 'remote', 'get-url', 'origin'], {
|
||||
windowsHide: true,
|
||||
stdout: 'pipe',
|
||||
stderr: 'pipe',
|
||||
timeout: 2_000,
|
||||
|
||||
@@ -564,7 +564,7 @@ async function getMacKeychainPassword(service: string): Promise<string> {
|
||||
// macOS may show an Allow/Deny dialog that blocks until the user responds.
|
||||
const proc = Bun.spawn(
|
||||
['security', 'find-generic-password', '-s', service, '-w'],
|
||||
{ stdout: 'pipe', stderr: 'pipe' },
|
||||
{ stdout: 'pipe', stderr: 'pipe', windowsHide: true },
|
||||
);
|
||||
|
||||
const timeout = new Promise<never>((_, reject) =>
|
||||
@@ -639,7 +639,7 @@ async function getLinuxSecretPassword(browser: BrowserInfo): Promise<string | nu
|
||||
|
||||
async function runPasswordLookup(cmd: string[], timeoutMs: number): Promise<string | null> {
|
||||
try {
|
||||
const proc = Bun.spawn(cmd, { stdout: 'pipe', stderr: 'pipe' });
|
||||
const proc = Bun.spawn(cmd, { stdout: 'pipe', stderr: 'pipe', windowsHide: true });
|
||||
const timeout = new Promise<never>((_, reject) =>
|
||||
setTimeout(() => {
|
||||
proc.kill();
|
||||
@@ -870,7 +870,7 @@ export async function importCookiesViaCdp(
|
||||
'--disable-extensions',
|
||||
'--disable-sync',
|
||||
'--no-default-browser-check',
|
||||
], { stdout: 'pipe', stderr: 'pipe' });
|
||||
], { stdout: 'pipe', stderr: 'pipe', windowsHide: true });
|
||||
|
||||
// Wait for Chrome to start, then find a page target's WebSocket URL.
|
||||
// Network.getAllCookies is only available on page targets, not browser.
|
||||
|
||||
@@ -14,6 +14,7 @@ import { homedir } from 'os';
|
||||
function getGitRoot(): string | null {
|
||||
try {
|
||||
const proc = Bun.spawnSync(['git', 'rev-parse', '--show-toplevel'], {
|
||||
windowsHide: true,
|
||||
stdout: 'pipe',
|
||||
stderr: 'pipe',
|
||||
});
|
||||
|
||||
@@ -347,6 +347,7 @@ function spawnClaude(cols: number, rows: number, onData: (chunk: Buffer) => void
|
||||
const tabHint = buildTabAwarenessHint(stateDir);
|
||||
|
||||
const proc = (Bun as any).spawn([claudePath, '--append-system-prompt', tabHint], {
|
||||
windowsHide: true,
|
||||
terminal: {
|
||||
rows,
|
||||
cols,
|
||||
|
||||
@@ -754,7 +754,7 @@ export async function handleWriteCommand(
|
||||
const code = generatePickerCode();
|
||||
const pickerUrl = `http://127.0.0.1:${port}/cookie-picker?code=${code}`;
|
||||
try {
|
||||
Bun.spawn(['open', pickerUrl], { stdout: 'ignore', stderr: 'ignore' });
|
||||
Bun.spawn(['open', pickerUrl], { stdout: 'ignore', stderr: 'ignore', windowsHide: true });
|
||||
} catch (err: any) {
|
||||
// open may fail on non-macOS or if 'open' binary is missing — URL is in the message below
|
||||
if (err?.code !== 'ENOENT' && !err?.message?.includes('spawn')) throw err;
|
||||
|
||||
@@ -167,6 +167,7 @@ export function findGstackInstallRoot(
|
||||
|
||||
function defaultRunXattr(target: string): number | null {
|
||||
const res = Bun.spawnSync(['xattr', '-dr', 'com.apple.quarantine', target], {
|
||||
windowsHide: true,
|
||||
stdout: 'pipe',
|
||||
stderr: 'pipe',
|
||||
timeout: 10_000,
|
||||
|
||||
@@ -63,6 +63,7 @@ export function isDisplayFree(displayNum: number): boolean {
|
||||
// the X socket/lock files, the same signal X servers themselves use.
|
||||
try {
|
||||
const result = Bun.spawnSync(['xdpyinfo', '-display', `:${displayNum}`], {
|
||||
windowsHide: true,
|
||||
stdout: 'ignore', stderr: 'ignore', timeout: 2000,
|
||||
});
|
||||
return result.exitCode !== 0;
|
||||
@@ -95,6 +96,7 @@ export function pickFreeDisplay(
|
||||
export function readPidStartTime(pid: number): string {
|
||||
if (!isProcessAlive(pid)) return '';
|
||||
const result = Bun.spawnSync(['ps', '-p', String(pid), '-o', 'lstart='], {
|
||||
windowsHide: true,
|
||||
stdout: 'pipe', stderr: 'pipe', timeout: 2000,
|
||||
});
|
||||
if (result.exitCode !== 0) return '';
|
||||
@@ -159,6 +161,7 @@ export async function spawnXvfb(displayNum: number): Promise<XvfbHandle> {
|
||||
// Spawn detached: Xvfb's lifetime is tied to whether we've explicitly
|
||||
// killed it via the handle's close() method, not to the parent process.
|
||||
const proc = Bun.spawn(['Xvfb', display, '-screen', '0', '1920x1080x24', '-ac'], {
|
||||
windowsHide: true,
|
||||
stdio: ['ignore', 'ignore', 'ignore'],
|
||||
});
|
||||
proc.unref();
|
||||
|
||||
@@ -127,4 +127,31 @@ describe('windowsHide on Windows-reachable spawns (#1835)', () => {
|
||||
}
|
||||
expect(offenders).toEqual([]);
|
||||
});
|
||||
|
||||
test('SWEEP: every Bun.spawn call in src/ passes windowsHide (#2575 residual)', () => {
|
||||
// Bun.spawn sites are structurally outside the child_process sweep above.
|
||||
// Native Bun hides consoles by default and the Node polyfill
|
||||
// (bun-polyfill.cjs) defaults windowsHide !== false since #2523/#2539 —
|
||||
// this census exists so an explicit flag documents the intent at every
|
||||
// site AND catches a regression if either default ever flips. Exemptions
|
||||
// carry reasons, same contract as the child_process sweep.
|
||||
const EXEMPT: Array<{ file: string; needle: string; reason: string }> = [];
|
||||
|
||||
const srcDir = path.join(import.meta.dir, '../src');
|
||||
const offenders: string[] = [];
|
||||
for (const file of fs.readdirSync(srcDir).filter((f) => f.endsWith('.ts'))) {
|
||||
const raw = fs.readFileSync(path.join(srcDir, file), 'utf-8');
|
||||
const code = raw.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '');
|
||||
const re = /(?:\(Bun as any\)|Bun)\.spawn(?:Sync)?\(/g;
|
||||
for (const m of code.matchAll(re)) {
|
||||
const slice = code.slice(m.index!, m.index! + 900);
|
||||
const exempt = EXEMPT.some((e) => e.file === file && slice.includes(e.needle));
|
||||
if (exempt) continue;
|
||||
if (!/windowsHide:\s*true/.test(slice)) {
|
||||
offenders.push(`${file}: ${slice.split('\n')[0].slice(0, 100)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
expect(offenders).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user