fix(server): keep Explorer windows visible #983

Do not pass windowsHide to detached GUI open targets, which forwards SW_HIDE to Explorer and IDE windows on Windows. Preserve detached process lifetime and ignored stdio.

Tests: open-target 33/33; server 1624/1624; coverage 5/5 with changed lines 6/6; real macOS Finder reveal passed for a temporary CJK and ampersand path.

Remaining validation: packaged Windows 11 Explorer and IDE window smoke.

Confidence: high

Scope-risk: narrow
This commit is contained in:
程序员阿江(Relakkes) 2026-07-11 04:37:30 +08:00
parent 936d970751
commit 20bb86ab63
2 changed files with 62 additions and 5 deletions

View File

@ -0,0 +1,50 @@
import { describe, expect, it } from 'bun:test'
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { delimiter, join } from 'node:path'
import {
createOpenTargetService,
getDefaultLaunchSpawnOptions,
} from '../services/openTargetService.js'
describe('open target launch options', () => {
it('does not hide external application windows on Windows', () => {
const options = getDefaultLaunchSpawnOptions() as Record<string, unknown>
expect(options.windowsHide).toBeUndefined()
expect(options).toEqual({
detached: true,
stdio: 'ignore',
})
})
it.skipIf(process.platform === 'win32')('uses those options in the production launcher', async () => {
const root = await mkdtemp(join(tmpdir(), 'cc-haha-open-target-launch-'))
const binDir = join(root, 'bin')
const targetDir = join(root, 'target')
const commandPath = join(binDir, 'code')
const originalPath = process.env.PATH
await mkdir(binDir)
await mkdir(targetDir)
await writeFile(commandPath, '#!/bin/sh\nexit 0\n', { mode: 0o755 })
process.env.PATH = `${binDir}${delimiter}${originalPath ?? ''}`
try {
const service = createOpenTargetService({
platform: 'linux',
commandExists: async (command) => command === 'code',
})
await expect(service.openTarget({ targetId: 'vscode', path: targetDir }))
.resolves.toMatchObject({ ok: true, targetId: 'vscode' })
} finally {
if (originalPath === undefined) {
delete process.env.PATH
} else {
process.env.PATH = originalPath
}
await rm(root, { recursive: true, force: true })
}
})
})

View File

@ -265,6 +265,17 @@ async function defaultPathExists(targetPath: string): Promise<boolean> {
}
}
/**
* Keep external applications detached without setting `windowsHide`: on
* Windows that flag passes SW_HIDE to GUI targets such as Explorer and IDEs.
*/
export function getDefaultLaunchSpawnOptions() {
return {
detached: true as const,
stdio: 'ignore' as const,
}
}
async function defaultLaunch(command: string, args: string[]): Promise<OpenTargetLaunchResult> {
return await new Promise((resolveLaunch) => {
let settled = false
@ -275,11 +286,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({