mirror of
https://github.com/civitai/civitai.git
synced 2026-09-20 22:08:18 +08:00
06e2651d47
`startDaemon` spawned the daemon with `shell: true`. Node applies `windowsHide` only to the process it creates, so the flag landed on cmd.exe and never on the node cmd.exe then started. That node got a fresh console, Windows 11 handed it to the default terminal app, and a Windows Terminal window opened and took focus off whatever the user was doing. Measured: the window appeared on every run of the unpatched code and stole focus on four of six. Dropping the shell fixes that, but it also leaves the daemon with no console of its own — so every child it spawns is then in the same position and pops its own window, once per dev server started, per taskkill, per queued vitest run, per branch-watch `git`. Verified that too, so `windowsHide` goes on all of them. Guarded by scripts/__tests__/dev-server-no-console-window.test.ts, which fails if either half regresses (checked by mutating both back). Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
72 lines
2.5 KiB
TypeScript
72 lines
2.5 KiB
TypeScript
import { readFileSync } from 'fs';
|
|
import { resolve } from 'path';
|
|
import { describe, expect, it } from 'vitest';
|
|
|
|
// On Windows a console child with no console of its own gets a fresh one, and Windows 11 hands that
|
|
// to the default terminal app — a Windows Terminal window opens and takes focus. `windowsHide`
|
|
// suppresses it, but node applies it only to the process IT creates: with `shell: true` it lands on
|
|
// cmd.exe, and the process cmd.exe then starts is created with default flags. The daemon is spawned
|
|
// detached with stdio ignored, so it owns no console to lend, and every one of its children is in
|
|
// that position. Piping stdio does not prevent the allocation.
|
|
const SKILL = resolve(__dirname, '../../.claude/skills/dev-server');
|
|
|
|
const FILES = [
|
|
'cli.mjs',
|
|
'console.mjs',
|
|
'scripts/daemon.mjs',
|
|
'scripts/test-queue.mjs',
|
|
'scripts/worktree.mjs',
|
|
];
|
|
|
|
const read = (file: string) => readFileSync(resolve(SKILL, file), 'utf8');
|
|
|
|
/** The object literal enclosing `at`, by brace balance. */
|
|
function enclosingLiteral(source: string, at: number) {
|
|
let depth = 0;
|
|
let open = -1;
|
|
for (let i = at; i >= 0; i--) {
|
|
if (source[i] === '}') depth++;
|
|
else if (source[i] === '{') {
|
|
if (depth === 0) {
|
|
open = i;
|
|
break;
|
|
}
|
|
depth--;
|
|
}
|
|
}
|
|
if (open === -1) return null;
|
|
depth = 0;
|
|
for (let i = open; i < source.length; i++) {
|
|
if (source[i] === '{') depth++;
|
|
else if (source[i] === '}') {
|
|
depth--;
|
|
if (depth === 0)
|
|
return { text: source.slice(open, i + 1), line: source.slice(0, open).split('\n').length };
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
describe('the dev-server skill never pops a console window', () => {
|
|
it.each(FILES)('every spawn option object in %s hides the console', (file) => {
|
|
const source = read(file);
|
|
const offenders: string[] = [];
|
|
for (let i = source.indexOf('stdio:'); i !== -1; i = source.indexOf('stdio:', i + 1)) {
|
|
const literal = enclosingLiteral(source, i);
|
|
if (literal && !literal.text.includes('windowsHide')) {
|
|
offenders.push(`${file}:${literal.line}`);
|
|
}
|
|
}
|
|
expect(offenders).toEqual([]);
|
|
});
|
|
|
|
it.each(['cli.mjs', 'console.mjs'])('%s starts the daemon without a shell', (file) => {
|
|
const source = read(file);
|
|
const start = source.indexOf('async function startDaemon');
|
|
expect(start).toBeGreaterThan(-1);
|
|
const body = source.slice(start, source.indexOf('\n}', start));
|
|
expect(body).toContain('spawn(process.execPath, [serverScript]');
|
|
expect(body.replace(/^\s*\/\/.*$/gm, '')).not.toMatch(/\bshell\s*:/);
|
|
});
|
|
});
|