fix(windows): set windowsHide on remaining child spawns to stop console flash (#1092) (#1104)

On Windows, a black console (conhost) window flashed briefly when CodeGraph
ran as a background MCP server. Several child spawns were missing
`windowsHide: true`, so Windows created a visible console for the child:

- scripts/npm-shim.js — launching the bundled runtime (every server start /
  daemon-idle reconnect) and the self-heal `tar` extraction of a missing
  platform bundle.
- src/reasoning/login.ts — the detached `cmd /c start` browser open.
- src/upgrade/index.ts — package-manager spawn (console-attached, so no flash
  in practice, but set for uniformity: every child spawn now hides).

The daemon spawn (#411) and all git execFileSync sites already set it; this
closes the remaining gaps. Adds an all-platforms source guard to
__tests__/npm-shim.test.ts asserting every spawn in the shim sets windowsHide.

Closes #1092

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-07-01 09:55:29 -05:00
committed by GitHub
co-authored by Claude Opus 4.8
parent 00765200d8
commit 6dd5512d8d
5 changed files with 22 additions and 4 deletions
+1
View File
@@ -11,6 +11,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
### Fixes ### Fixes
- On Windows, a console window no longer briefly flashes when CodeGraph runs as a background MCP server. When the npm launcher started the bundled runtime — which happens every time an editor starts the server or reconnects after the daemon idles out — and during its self-heal step that extracts a missing platform bundle, Windows would pop up a black console (conhost) window for a moment. Both now launch hidden, matching how the daemon already behaved; the browser-open step of `codegraph login` was hardened the same way. Thanks @luoyerr for the report and root-cause analysis. (#1092)
- C++ forward declarations no longer crowd out the real class definition. A `class Foo;` forward declaration — common in large C++ and Unreal Engine codebases, where a heavily used class is forward-declared across dozens of headers — was indexed as its own class node every time it appeared. So exploring that class returned mostly forward-declaration sites, and could even pick one of them as the representative for blast-radius, burying the actual definition and its members and callers. Bodiless forward declarations are now skipped for C and C++, exactly as forward-declared structs and enums already were, so only the real definition is indexed. Languages where a class with no body is a complete definition — such as Kotlin's `class Empty` and Scala — are unaffected. Thanks @luoyxy for the report and root-cause analysis. (#1093) - C++ forward declarations no longer crowd out the real class definition. A `class Foo;` forward declaration — common in large C++ and Unreal Engine codebases, where a heavily used class is forward-declared across dozens of headers — was indexed as its own class node every time it appeared. So exploring that class returned mostly forward-declaration sites, and could even pick one of them as the representative for blast-radius, burying the actual definition and its members and callers. Bodiless forward declarations are now skipped for C and C++, exactly as forward-declared structs and enums already were, so only the real definition is indexed. Languages where a class with no body is a complete definition — such as Kotlin's `class Empty` and Scala — are unaffected. Thanks @luoyxy for the report and root-cause analysis. (#1093)
- C++ methods that return a reference, and user-defined conversion operators, are now indexed under their correct names. An inline getter like `const FGameplayTagContainer& GetActiveTags() const` — everywhere in Unreal Engine headers — was indexed as `& GetActiveTags() const` instead of `GetActiveTags`, and a conversion operator like `operator EALSMovementState() const` kept its trailing `() const` instead of reading `operator EALSMovementState`. In both cases the garbled name meant you couldn't find the symbol by name and its callers weren't linked. Both now read cleanly, matching how pointer-returning and value-returning methods already worked. (#1096) - C++ methods that return a reference, and user-defined conversion operators, are now indexed under their correct names. An inline getter like `const FGameplayTagContainer& GetActiveTags() const` — everywhere in Unreal Engine headers — was indexed as `& GetActiveTags() const` instead of `GetActiveTags`, and a conversion operator like `operator EALSMovementState() const` kept its trailing `() const` instead of reading `operator EALSMovementState`. In both cases the garbled name meant you couldn't find the symbol by name and its callers weren't linked. Both now read cleanly, matching how pointer-returning and value-returning methods already worked. (#1096)
- C++ functions written with an inline-specifier macro before the return type are now indexed correctly. In Unreal Engine, inline helpers are commonly written `FORCEINLINE FString GetEnumerationToString(...)`; the `FORCEINLINE` macro made the parser read the return type as part of the function's name (`FString GetEnumerationToString` instead of `GetEnumerationToString`) and lose the real return type, so the function couldn't be found by name and its callers weren't linked. CodeGraph now recognizes the standard Unreal inline macros (`FORCEINLINE`, `FORCENOINLINE`, `FORCEINLINE_DEBUGGABLE`), so both the name and the return type are captured. (#1100) - C++ functions written with an inline-specifier macro before the return type are now indexed correctly. In Unreal Engine, inline helpers are commonly written `FORCEINLINE FString GetEnumerationToString(...)`; the `FORCEINLINE` macro made the parser read the return type as part of the function's name (`FString GetEnumerationToString` instead of `GetEnumerationToString`) and lose the real return type, so the function couldn't be found by name and its callers weren't linked. CodeGraph now recognizes the standard Unreal inline macros (`FORCEINLINE`, `FORCENOINLINE`, `FORCEINLINE_DEBUGGABLE`), so both the name and the return type are captured. (#1100)
+17
View File
@@ -71,6 +71,23 @@ function runShim(pkgDir: string, args: string[], env: Record<string, string>) {
}); });
} }
// Static source guard (all platforms): every child spawn in the shim must set
// windowsHide, or a console (conhost) window flashes when the shim runs as a
// background MCP server on Windows (issue #1092). windowsHide is a Windows-only
// spawn behavior that can't be observed from these POSIX-only subprocess tests,
// so we assert it at the source level instead — and this also catches any new
// spawn site added to the shim later.
describe('npm-shim windowsHide (#1092)', () => {
it('sets windowsHide: true on every spawn in the shim', () => {
const src = fs.readFileSync(SHIM_SRC, 'utf8');
const spawnLines = src.split('\n').filter((l) => /\.spawn(Sync)?\(/.test(l));
expect(spawnLines.length).toBeGreaterThan(0); // guard against a false pass if the calls move
for (const line of spawnLines) {
expect(line, `spawn without windowsHide: ${line.trim()}`).toContain('windowsHide: true');
}
});
});
describe.skipIf(isWindows)('npm-shim launcher', () => { describe.skipIf(isWindows)('npm-shim launcher', () => {
it('runs the installed optional-dependency bundle without any download', async () => { it('runs the installed optional-dependency bundle without any download', async () => {
const pkg = makePkg(); const pkg = makePkg();
+2 -2
View File
@@ -45,7 +45,7 @@ async function main() {
// Happy path: the npm-installed optional dependency. Fall back to a download // Happy path: the npm-installed optional dependency. Fall back to a download
// when the registry didn't deliver it. // when the registry didn't deliver it.
var resolved = resolveInstalledBundle() || (await selfHealBundle()); var resolved = resolveInstalledBundle() || (await selfHealBundle());
var res = childProcess.spawnSync(resolved.command, resolved.args, { stdio: 'inherit' }); var res = childProcess.spawnSync(resolved.command, resolved.args, { stdio: 'inherit', windowsHide: true });
if (res.error) { if (res.error) {
process.stderr.write('codegraph: ' + res.error.message + '\n'); process.stderr.write('codegraph: ' + res.error.message + '\n');
process.exit(1); process.exit(1);
@@ -222,7 +222,7 @@ function extract(archive, destDir) {
var args = isWindows var args = isWindows
? ['-xf', archive, '-C', destDir, '--strip-components=1'] ? ['-xf', archive, '-C', destDir, '--strip-components=1']
: ['-xzf', archive, '-C', destDir, '--strip-components=1']; : ['-xzf', archive, '-C', destDir, '--strip-components=1'];
var res = childProcess.spawnSync('tar', args, { stdio: 'ignore' }); var res = childProcess.spawnSync('tar', args, { stdio: 'ignore', windowsHide: true });
if (res.error) throw new Error('tar unavailable: ' + res.error.message); if (res.error) throw new Error('tar unavailable: ' + res.error.message);
if (res.status !== 0) throw new Error('tar exited ' + res.status); if (res.status !== 0) throw new Error('tar exited ' + res.status);
} }
+1 -1
View File
@@ -80,7 +80,7 @@ export async function openBrowser(url: string): Promise<void> {
: process.platform === 'win32' ? ['cmd', ['/c', 'start', '', url]] : process.platform === 'win32' ? ['cmd', ['/c', 'start', '', url]]
: ['xdg-open', [url]]; : ['xdg-open', [url]];
try { try {
const child = spawn(cmd as string, args as string[], { stdio: 'ignore', detached: true }); const child = spawn(cmd as string, args as string[], { stdio: 'ignore', detached: true, windowsHide: true });
child.on('error', () => {}); child.on('error', () => {});
child.unref(); child.unref();
} catch { } catch {
+1 -1
View File
@@ -546,7 +546,7 @@ export function hasCommand(cmd: string): boolean {
} }
export function defaultRun(cmd: string, args: string[], env?: NodeJS.ProcessEnv): number { export function defaultRun(cmd: string, args: string[], env?: NodeJS.ProcessEnv): number {
const r = spawnSync(cmd, args, { stdio: 'inherit', env: env ?? process.env }); const r = spawnSync(cmd, args, { stdio: 'inherit', env: env ?? process.env, windowsHide: true });
if (r.error) return -1; if (r.error) return -1;
return r.status ?? -1; return r.status ?? -1;
} }