mirror of https://github.com/garrytan/gstack.git
Merge 299b3a4e63 into a3259400a3
This commit is contained in:
commit
7be3368b74
|
|
@ -11,7 +11,7 @@
|
|||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { spawn as nodeSpawn } from 'child_process';
|
||||
import { spawn as nodeSpawn, spawnSync as nodeSpawnSync } from 'child_process';
|
||||
import { safeUnlink, safeUnlinkQuiet, safeKill, isProcessAlive } from './error-handling';
|
||||
import { writeSecureFile, mkdirSecure } from './file-permissions';
|
||||
import { resolveConfig, ensureStateDir, readVersionHash } from './config';
|
||||
|
|
@ -141,10 +141,13 @@ async function killServer(pid: number): Promise<void> {
|
|||
if (IS_WINDOWS) {
|
||||
// taskkill /T /F kills the process tree (Node + Chromium)
|
||||
try {
|
||||
Bun.spawnSync(
|
||||
['taskkill', '/PID', String(pid), '/T', '/F'],
|
||||
{ stdout: 'pipe', stderr: 'pipe', timeout: 5000 }
|
||||
);
|
||||
const proc = nodeSpawnSync('taskkill', ['/PID', String(pid), '/T', '/F'], {
|
||||
stdout: 'pipe',
|
||||
stderr: 'pipe',
|
||||
timeout: 5000,
|
||||
windowsHide: true,
|
||||
});
|
||||
if (proc.error) throw proc.error;
|
||||
} catch (err: any) {
|
||||
if (err?.code !== 'ENOENT') throw err;
|
||||
}
|
||||
|
|
@ -285,6 +288,7 @@ function raiseHeadedWindowMacOS(): void {
|
|||
nodeSpawn('osascript', ['-e', 'tell application "Google Chrome for Testing" to activate'], {
|
||||
stdio: 'ignore',
|
||||
detached: true,
|
||||
windowsHide: true,
|
||||
}).unref();
|
||||
} catch {
|
||||
// osascript missing or app not present — non-fatal
|
||||
|
|
@ -323,9 +327,13 @@ async function startServer(extraEnv?: Record<string, string>): Promise<ServerSta
|
|||
const launcherCode =
|
||||
`const{spawn}=require('child_process');` +
|
||||
`spawn(process.execPath,[${JSON.stringify(NODE_SERVER_SCRIPT)}],` +
|
||||
`{detached:true,stdio:['ignore','ignore','ignore'],env:Object.assign({},process.env,` +
|
||||
`{detached:true,windowsHide:true,stdio:['ignore','ignore','ignore'],env:Object.assign({},process.env,` +
|
||||
`${extraEnvStr})}).unref()`;
|
||||
Bun.spawnSync(['node', '-e', launcherCode], { stdio: ['ignore', 'ignore', 'ignore'] });
|
||||
const proc = nodeSpawnSync('node', ['-e', launcherCode], {
|
||||
stdio: ['ignore', 'ignore', 'ignore'],
|
||||
windowsHide: true,
|
||||
});
|
||||
if (proc.error) throw proc.error;
|
||||
} 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
|
||||
|
|
@ -340,6 +348,7 @@ async function startServer(extraEnv?: Record<string, string>): Promise<ServerSta
|
|||
// the Windows path's rationale — same root cause, different OS API.
|
||||
nodeSpawn('bun', ['run', SERVER_SCRIPT], {
|
||||
detached: true,
|
||||
windowsHide: true,
|
||||
stdio: ['ignore', 'ignore', 'ignore'],
|
||||
env: { ...process.env, BROWSE_STATE_FILE: config.stateFile, BROWSE_PARENT_PID: parentPid, ...extraEnv },
|
||||
}).unref();
|
||||
|
|
|
|||
|
|
@ -13,9 +13,12 @@
|
|||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { spawnSync as nodeSpawnSync } from 'child_process';
|
||||
import { mkdirSecure } from './file-permissions';
|
||||
import { safeUnlinkQuiet } from './error-handling';
|
||||
|
||||
const IS_WINDOWS = process.platform === 'win32';
|
||||
|
||||
export interface BrowseConfig {
|
||||
projectDir: string;
|
||||
stateDir: string;
|
||||
|
|
@ -31,6 +34,16 @@ export interface BrowseConfig {
|
|||
*/
|
||||
export function getGitRoot(): string | null {
|
||||
try {
|
||||
if (IS_WINDOWS) {
|
||||
const proc = nodeSpawnSync('git', ['rev-parse', '--show-toplevel'], {
|
||||
stdout: 'pipe',
|
||||
stderr: 'pipe',
|
||||
timeout: 2_000, // Don't hang if .git is broken
|
||||
windowsHide: true,
|
||||
});
|
||||
if (proc.error || proc.status !== 0) return null;
|
||||
return proc.stdout.toString().trim() || null;
|
||||
}
|
||||
const proc = Bun.spawnSync(['git', 'rev-parse', '--show-toplevel'], {
|
||||
stdout: 'pipe',
|
||||
stderr: 'pipe',
|
||||
|
|
@ -123,6 +136,21 @@ export function ensureStateDir(config: BrowseConfig): void {
|
|||
*/
|
||||
export function getRemoteSlug(): string {
|
||||
try {
|
||||
if (IS_WINDOWS) {
|
||||
const proc = nodeSpawnSync('git', ['remote', 'get-url', 'origin'], {
|
||||
stdout: 'pipe',
|
||||
stderr: 'pipe',
|
||||
timeout: 2_000,
|
||||
windowsHide: true,
|
||||
});
|
||||
if (proc.error || proc.status !== 0) throw new Error('no remote');
|
||||
const url = proc.stdout.toString().trim();
|
||||
// SSH: git@github.com:owner/repo.git → owner-repo
|
||||
// HTTPS: https://github.com/owner/repo.git → owner-repo
|
||||
const match = url.match(/[:/]([^/]+)\/([^/]+?)(?:\.git)?$/);
|
||||
if (match) return `${match[1]}-${match[2]}`;
|
||||
throw new Error('unparseable');
|
||||
}
|
||||
const proc = Bun.spawnSync(['git', 'remote', 'get-url', 'origin'], {
|
||||
stdout: 'pipe',
|
||||
stderr: 'pipe',
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ export function restrictFilePermissions(filePath: string): void {
|
|||
execFileSync(
|
||||
'icacls',
|
||||
[filePath, '/inheritance:r', '/grant:r', `${user}:(F)`],
|
||||
{ stdio: 'ignore' },
|
||||
{ stdio: 'ignore', windowsHide: true },
|
||||
);
|
||||
} catch (err) {
|
||||
warnIcaclsFailure(filePath, err);
|
||||
|
|
@ -101,7 +101,7 @@ export function restrictDirectoryPermissions(dirPath: string): void {
|
|||
execFileSync(
|
||||
'icacls',
|
||||
[dirPath, '/inheritance:r', '/grant:r', `${user}:(OI)(CI)(F)`],
|
||||
{ stdio: 'ignore' },
|
||||
{ stdio: 'ignore', windowsHide: true },
|
||||
);
|
||||
} catch (err) {
|
||||
warnIcaclsFailure(dirPath, err);
|
||||
|
|
|
|||
|
|
@ -8,17 +8,31 @@
|
|||
import { accessSync, constants } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { homedir } from 'os';
|
||||
import { spawnSync as nodeSpawnSync } from 'child_process';
|
||||
|
||||
const IS_WINDOWS = process.platform === 'win32';
|
||||
|
||||
// ─── Binary Discovery ───────────────────────────────────────────
|
||||
|
||||
function getGitRoot(): string | null {
|
||||
try {
|
||||
if (IS_WINDOWS) {
|
||||
const proc = nodeSpawnSync('git', ['rev-parse', '--show-toplevel'], {
|
||||
stdout: 'pipe',
|
||||
stderr: 'pipe',
|
||||
timeout: 2_000,
|
||||
windowsHide: true,
|
||||
});
|
||||
if (proc.error || proc.status !== 0) return null;
|
||||
return proc.stdout.toString().trim() || null;
|
||||
}
|
||||
const proc = Bun.spawnSync(['git', 'rev-parse', '--show-toplevel'], {
|
||||
stdout: 'pipe',
|
||||
stderr: 'pipe',
|
||||
timeout: 2_000,
|
||||
});
|
||||
if (proc.exitCode !== 0) return null;
|
||||
return proc.stdout.toString().trim();
|
||||
return proc.stdout.toString().trim() || null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,98 @@
|
|||
/**
|
||||
* Static regression coverage for #1784.
|
||||
*
|
||||
* Bun.spawnSync does not honor windowsHide on Windows (Bun 1.3.11), so
|
||||
* cold-start console-subsystem helpers must route through Node's
|
||||
* child_process APIs with windowsHide:true. These checks pin the spawn sites
|
||||
* that otherwise create focus-stealing conhost.exe windows.
|
||||
*/
|
||||
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
|
||||
const ROOT = path.resolve(import.meta.dir, '..', '..');
|
||||
const CLI = path.join(ROOT, 'browse', 'src', 'cli.ts');
|
||||
const CONFIG = path.join(ROOT, 'browse', 'src', 'config.ts');
|
||||
const FIND_BROWSE = path.join(ROOT, 'browse', 'src', 'find-browse.ts');
|
||||
const FILE_PERMISSIONS = path.join(ROOT, 'browse', 'src', 'file-permissions.ts');
|
||||
|
||||
function read(filePath: string): string {
|
||||
return fs.readFileSync(filePath, 'utf-8');
|
||||
}
|
||||
|
||||
describe('#1784 Windows console flash suppression', () => {
|
||||
test('Windows detach launcher uses hidden Node spawnSync', () => {
|
||||
const body = read(CLI);
|
||||
expect(body).toMatch(/spawnSync as nodeSpawnSync/);
|
||||
expect(body).toMatch(
|
||||
/nodeSpawnSync\(\s*['"]node['"],\s*\[\s*['"]-e['"],\s*launcherCode\s*\][\s\S]{0,250}windowsHide:\s*true/,
|
||||
);
|
||||
expect(body).not.toMatch(/Bun\.spawnSync\(\s*\[\s*['"]node['"]/);
|
||||
});
|
||||
|
||||
test('Windows taskkill helper uses hidden Node spawnSync', () => {
|
||||
const body = read(CLI);
|
||||
expect(body).toMatch(
|
||||
/nodeSpawnSync\(\s*['"]taskkill['"][\s\S]{0,250}windowsHide:\s*true/,
|
||||
);
|
||||
expect(body).not.toMatch(/Bun\.spawnSync\(\s*\[\s*['"]taskkill['"]/);
|
||||
});
|
||||
|
||||
test('git probes use hidden Node spawnSync on Windows and keep Bun on POSIX', () => {
|
||||
const body = read(CONFIG);
|
||||
const hiddenGitSpawns = body.match(
|
||||
/nodeSpawnSync\(\s*['"]git['"][\s\S]{0,250}windowsHide:\s*true/g,
|
||||
) || [];
|
||||
expect(body).toContain("process.platform === 'win32'");
|
||||
expect(hiddenGitSpawns).toHaveLength(2);
|
||||
expect(body).toMatch(
|
||||
/Bun\.spawnSync\(\s*\[\s*['"]git['"],\s*['"]rev-parse['"],\s*['"]--show-toplevel['"]/,
|
||||
);
|
||||
expect(body).toMatch(
|
||||
/Bun\.spawnSync\(\s*\[\s*['"]git['"],\s*['"]remote['"],\s*['"]get-url['"],\s*['"]origin['"]/,
|
||||
);
|
||||
});
|
||||
|
||||
test('find-browse git probe uses hidden Node spawnSync on Windows and keeps Bun on POSIX', () => {
|
||||
const body = read(FIND_BROWSE);
|
||||
const hiddenGitSpawns = body.match(
|
||||
/nodeSpawnSync\(\s*['"]git['"][\s\S]{0,250}windowsHide:\s*true/g,
|
||||
) || [];
|
||||
expect(body).toContain("process.platform === 'win32'");
|
||||
expect(hiddenGitSpawns).toHaveLength(1);
|
||||
expect(body).toMatch(
|
||||
/Bun\.spawnSync\(\s*\[\s*['"]git['"],\s*['"]rev-parse['"],\s*['"]--show-toplevel['"]/,
|
||||
);
|
||||
});
|
||||
|
||||
test('icacls ACL helpers pass windowsHide to execFileSync', () => {
|
||||
const body = read(FILE_PERMISSIONS);
|
||||
const hiddenIcaclsCalls = body.match(
|
||||
/execFileSync\(\s*['"]icacls['"][\s\S]{0,250}windowsHide:\s*true/g,
|
||||
) || [];
|
||||
expect(hiddenIcaclsCalls).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('detached server spawns carry windowsHide (#1863 fold-in)', () => {
|
||||
test('Windows Node launcher inner spawn carries windowsHide:true', () => {
|
||||
const body = read(CLI);
|
||||
expect(body).toMatch(/spawn\(process\.execPath,[\s\S]{0,500}detached:true,windowsHide:true/);
|
||||
});
|
||||
|
||||
test('non-Windows server nodeSpawn carries windowsHide:true', () => {
|
||||
const body = read(CLI);
|
||||
expect(body).toMatch(/nodeSpawn\('bun',[\s\S]{0,500}detached:\s*true[\s\S]{0,100}windowsHide:\s*true/);
|
||||
});
|
||||
|
||||
test('every detached spawn site in cli.ts carries windowsHide:true', () => {
|
||||
const body = read(CLI)
|
||||
.split('\n')
|
||||
.filter((line) => !line.trim().startsWith('//') && !line.trim().startsWith('*'))
|
||||
.join('\n');
|
||||
const detachedSpawns = body.match(/detached:\s*true/g)?.length ?? 0;
|
||||
const windowsHideFlags = body.match(/windowsHide:\s*true/g)?.length ?? 0;
|
||||
expect(windowsHideFlags).toBeGreaterThanOrEqual(detachedSpawns);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue