mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-27 15:13:37 +08:00
fix(server): stop hiding GUI windows when opening external IDEs on Windows (#52)
* fix(server): stop hiding GUI windows when opening external IDEs on Windows
Symptom: clicking the right-side 'Open in...' menu and picking VS Code or
Cursor on Windows starts the process (visible in Task Manager) but no
window ever appears. Repeated clicks pile up zombie processes which
eventually starves the system and makes the app feel hung.
Cause: defaultLaunch passed { detached: true, stdio: 'ignore',
windowsHide: true } to spawn. On Windows, windowsHide sets
STARTUPINFO.wShowWindow = SW_HIDE. For console-subsystem commands
(cmd.exe /c start ...) that hides the cmd flash, which is what we
wanted. But for GUI-subsystem executables (Code.exe, Cursor.exe,
explorer.exe, ...) Windows treats SW_HIDE as the initial nCmdShow
handed to the app's first ShowWindow() call, so the main window is
created and immediately hidden.
Fix: drop windowsHide. The brief cmd flash from the explorer
file-manager fallback (cmd.exe /c start "" path) is acceptable;
cmd /c start exits in milliseconds and is rarely visible.
Tested: server open-target tests 28/28 still pass (ran via
bun test src/server/__tests__/open-target-*.test.ts).
Spawn-option behavior is platform-specific and not unit-testable
without mocking node:child_process — verified by inspection against
Node.js docs and Windows STARTUPINFO semantics.
Confidence: high
Scope-risk: narrow
* test(server): freeze defaultLaunch spawn options as a regression guard
Extracts the spawn options into a pure helper getDefaultLaunchSpawnOptions and adds 4 cases asserting (a) detached:true, (b) stdio:ignore, (c) windowsHide unset (the regression guard for the VS Code hidden-window bug fixed in the parent commit), (d) no extra fields silently changing spawn semantics.
Tested: 32/32 (28 previous open-target tests still green plus 4 new)
---------
Co-authored-by: 你的姓名 <you@example.com>
This commit is contained in:
parent
149d6a9b44
commit
ca4e7f65ce
27
src/server/__tests__/open-target-launch-options.test.ts
Normal file
27
src/server/__tests__/open-target-launch-options.test.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { describe, expect, it } from 'bun:test'
|
||||
import { getDefaultLaunchSpawnOptions } from '../services/openTargetService.js'
|
||||
|
||||
describe('getDefaultLaunchSpawnOptions', () => {
|
||||
it('detaches the child process so VS Code/Cursor outlive cc-haha', () => {
|
||||
expect(getDefaultLaunchSpawnOptions().detached).toBe(true)
|
||||
})
|
||||
|
||||
it("ignores stdio so it can't keep the parent alive on graceful exit", () => {
|
||||
expect(getDefaultLaunchSpawnOptions().stdio).toBe('ignore')
|
||||
})
|
||||
|
||||
// Regression guard for the "VS Code starts but no window appears" bug:
|
||||
// setting windowsHide: true forwards SW_HIDE as the initial nCmdShow to
|
||||
// GUI-subsystem .exe targets, which Windows then applies to the app's
|
||||
// first ShowWindow() call — keeping the main window hidden even though
|
||||
// the process is fully running. See the docblock on the helper.
|
||||
it('does NOT set windowsHide (would hide the GUI window on Windows)', () => {
|
||||
const options = getDefaultLaunchSpawnOptions() as Record<string, unknown>
|
||||
expect(options.windowsHide).toBeUndefined()
|
||||
})
|
||||
|
||||
it('does not pass any extra fields that could change spawn semantics', () => {
|
||||
const options = getDefaultLaunchSpawnOptions()
|
||||
expect(Object.keys(options).sort()).toEqual(['detached', 'stdio'])
|
||||
})
|
||||
})
|
||||
@ -265,6 +265,29 @@ async function defaultPathExists(targetPath: string): Promise<boolean> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link SpawnOptions} we want for {@link defaultLaunch}.
|
||||
*
|
||||
* Critically, on Windows we do **not** set `windowsHide: true`. For console
|
||||
* subsystem commands like `cmd.exe /c start ...` it would hide the brief cmd
|
||||
* flash, but for GUI subsystem executables (VS Code's `Code.exe`,
|
||||
* `Cursor.exe`, `explorer.exe`, ...) Windows treats `STARTUPINFO.wShowWindow
|
||||
* = SW_HIDE` as the initial `nCmdShow` handed to the app's first
|
||||
* `ShowWindow()` call. The process starts, the main window is created, and
|
||||
* then it stays hidden — matching the bug report "VS Code is in Task Manager
|
||||
* but no window appears". The brief cmd flash from the file-manager fallback
|
||||
* (`cmd.exe /c start "" path`) is acceptable; cmd /c start exits in
|
||||
* milliseconds.
|
||||
*
|
||||
* Exported for unit testing — callers should keep using {@link defaultLaunch}.
|
||||
*/
|
||||
export function getDefaultLaunchSpawnOptions(): {
|
||||
detached: true
|
||||
stdio: 'ignore'
|
||||
} {
|
||||
return { detached: true, stdio: 'ignore' }
|
||||
}
|
||||
|
||||
async function defaultLaunch(command: string, args: string[]): Promise<OpenTargetLaunchResult> {
|
||||
return await new Promise((resolveLaunch) => {
|
||||
let settled = false
|
||||
@ -275,11 +298,7 @@ async function defaultLaunch(command: string, args: string[]): Promise<OpenTarge
|
||||
}
|
||||
|
||||
try {
|
||||
const child = spawn(command, args, {
|
||||
detached: true,
|
||||
stdio: 'ignore',
|
||||
windowsHide: true,
|
||||
})
|
||||
const child = spawn(command, args, getDefaultLaunchSpawnOptions())
|
||||
|
||||
child.once('error', (error) => {
|
||||
settle({
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user