mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-16 13:03:31 +08:00
fix(desktop): hide Windows subprocess consoles (#802)
Ensure Electron sidecar launch and Windows taskkill calls hide console windows, and pass the same hidden-window spawn option through desktop CLI and scheduled-task subprocess launches. Tested: cd desktop && bun test ./electron/services/sidecarManager.test.ts Tested: bun test src/server/__tests__/conversation-service.test.ts src/server/__tests__/cron-scheduler-launcher.test.ts Tested: bun run check:native Tested: bun run check:server Not-tested: Windows GUI quit smoke Confidence: medium Scope-risk: narrow
This commit is contained in:
parent
dd72901ba3
commit
be4984009c
@ -12,6 +12,7 @@ import {
|
||||
proxyUrlFromElectronProxyRules,
|
||||
pushStartupLog,
|
||||
resolveHostTriple,
|
||||
spawnSidecar,
|
||||
windowsPowerShellOverride,
|
||||
type SidecarChild,
|
||||
} from './sidecarManager'
|
||||
@ -145,7 +146,7 @@ describe('Electron sidecar manager', () => {
|
||||
const spawnAsync = vi.fn()
|
||||
const spawnSyncFn = vi.fn()
|
||||
killSidecar(child, false, { platform: 'win32', spawnAsync: spawnAsync as never, spawnSyncFn: spawnSyncFn as never })
|
||||
expect(spawnAsync).toHaveBeenCalledWith('taskkill', ['/F', '/T', '/PID', '777'], { stdio: 'ignore' })
|
||||
expect(spawnAsync).toHaveBeenCalledWith('taskkill', ['/F', '/T', '/PID', '777'], { stdio: 'ignore', windowsHide: true })
|
||||
expect(spawnSyncFn).not.toHaveBeenCalled()
|
||||
expect(child.kill).not.toHaveBeenCalled()
|
||||
})
|
||||
@ -155,10 +156,29 @@ describe('Electron sidecar manager', () => {
|
||||
const spawnAsync = vi.fn()
|
||||
const spawnSyncFn = vi.fn()
|
||||
killSidecar(child, true, { platform: 'win32', spawnAsync: spawnAsync as never, spawnSyncFn: spawnSyncFn as never })
|
||||
expect(spawnSyncFn).toHaveBeenCalledWith('taskkill', ['/F', '/T', '/PID', '777'], { stdio: 'ignore' })
|
||||
expect(spawnSyncFn).toHaveBeenCalledWith('taskkill', ['/F', '/T', '/PID', '777'], { stdio: 'ignore', windowsHide: true })
|
||||
expect(spawnAsync).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('hides Windows console windows when launching sidecars', () => {
|
||||
const spawned = {} as SidecarChild
|
||||
const spawnFn = vi.fn(() => spawned)
|
||||
const existsSyncFn = vi.fn(() => true)
|
||||
const plan = {
|
||||
command: '/app/desktop/src-tauri/binaries/claude-sidecar-x86_64-pc-windows-msvc.exe',
|
||||
args: ['server', '--port', '49321'],
|
||||
env: { CLAUDE_H5_AUTO_PUBLIC_URL: '1' },
|
||||
}
|
||||
|
||||
expect(spawnSidecar(plan, { existsSyncFn, spawnFn: spawnFn as never })).toBe(spawned)
|
||||
expect(existsSyncFn).toHaveBeenCalledWith(plan.command)
|
||||
expect(spawnFn).toHaveBeenCalledWith(plan.command, plan.args, {
|
||||
env: plan.env,
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
windowsHide: true,
|
||||
})
|
||||
})
|
||||
|
||||
it('forwards a PowerShell shell choice to the sidecar only on Windows', () => {
|
||||
expect(windowsPowerShellOverride('pwsh.exe', 'win32')).toBe('pwsh.exe')
|
||||
expect(windowsPowerShellOverride('powershell.exe', 'win32')).toBe('powershell.exe')
|
||||
|
||||
@ -16,6 +16,11 @@ export type SidecarPlan = {
|
||||
env: NodeJS.ProcessEnv
|
||||
}
|
||||
|
||||
export type SpawnSidecarDeps = {
|
||||
existsSyncFn?: typeof existsSync
|
||||
spawnFn?: typeof spawn
|
||||
}
|
||||
|
||||
const PROXY_ENV_KEYS = [
|
||||
'HTTP_PROXY',
|
||||
'HTTPS_PROXY',
|
||||
@ -225,13 +230,15 @@ export function createAdapterPlan({
|
||||
}
|
||||
}
|
||||
|
||||
export function spawnSidecar(plan: SidecarPlan): SidecarChild {
|
||||
if (!existsSync(plan.command)) {
|
||||
export function spawnSidecar(plan: SidecarPlan, deps: SpawnSidecarDeps = {}): SidecarChild {
|
||||
const exists = deps.existsSyncFn ?? existsSync
|
||||
if (!exists(plan.command)) {
|
||||
throw new Error(`Electron sidecar binary not found: ${plan.command}. Run "cd desktop && bun run build:sidecars" first.`)
|
||||
}
|
||||
return spawn(plan.command, plan.args, {
|
||||
return (deps.spawnFn ?? spawn)(plan.command, plan.args, {
|
||||
env: plan.env,
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
windowsHide: true,
|
||||
})
|
||||
}
|
||||
|
||||
@ -251,8 +258,9 @@ export function killSidecar(child: SidecarChild, sync = false, deps: KillSidecar
|
||||
const platform = deps.platform ?? process.platform
|
||||
if (platform === 'win32' && child.pid) {
|
||||
const args = ['/F', '/T', '/PID', String(child.pid)]
|
||||
if (sync) (deps.spawnSyncFn ?? spawnSync)('taskkill', args, { stdio: 'ignore' })
|
||||
else (deps.spawnAsync ?? spawn)('taskkill', args, { stdio: 'ignore' })
|
||||
const options = { stdio: 'ignore', windowsHide: true } as const
|
||||
if (sync) (deps.spawnSyncFn ?? spawnSync)('taskkill', args, options)
|
||||
else (deps.spawnAsync ?? spawn)('taskkill', args, options)
|
||||
return
|
||||
}
|
||||
child.kill()
|
||||
|
||||
@ -3,6 +3,7 @@ import * as fs from 'node:fs/promises'
|
||||
import * as os from 'node:os'
|
||||
import * as path from 'node:path'
|
||||
import {
|
||||
buildConversationCliSpawnOptions,
|
||||
ConversationService,
|
||||
DESKTOP_CLI_GRACEFUL_SHUTDOWN_TIMEOUT_MS,
|
||||
} from '../services/conversationService.js'
|
||||
@ -181,6 +182,19 @@ describe('ConversationService', () => {
|
||||
await expect(fs.stat(path.dirname(env.CLAUDE_CODE_DIAGNOSTICS_FILE))).resolves.toBeTruthy()
|
||||
})
|
||||
|
||||
test('builds hidden CLI spawn options for desktop session subprocesses', () => {
|
||||
const env = { CLAUDECODE: '1' }
|
||||
|
||||
expect(buildConversationCliSpawnOptions('/workspace/project', env)).toEqual({
|
||||
cwd: '/workspace/project',
|
||||
env,
|
||||
stdin: 'pipe',
|
||||
stdout: 'pipe',
|
||||
stderr: 'pipe',
|
||||
windowsHide: true,
|
||||
})
|
||||
})
|
||||
|
||||
test('buildChildEnv pins desktop memory to the current sanitized project directory', async () => {
|
||||
const service = new ConversationService() as any
|
||||
const workDir = path.join(tmpDir, 'workspace', 'myself_code', 'claude-code-haha')
|
||||
|
||||
@ -5,6 +5,7 @@ import * as path from 'path'
|
||||
|
||||
import {
|
||||
buildCronCliArgs,
|
||||
buildCronTaskSpawnOptions,
|
||||
CronScheduler,
|
||||
resolveCronProjectRoot,
|
||||
} from '../services/cronScheduler.js'
|
||||
@ -146,6 +147,19 @@ describe('cron scheduler launcher resolution', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('builds hidden CLI spawn options for scheduled task subprocesses', () => {
|
||||
const env = { CLAUDECODE: '1' }
|
||||
|
||||
expect(buildCronTaskSpawnOptions('/workspace/project', env)).toEqual({
|
||||
stdin: 'pipe',
|
||||
stdout: 'pipe',
|
||||
stderr: 'pipe',
|
||||
cwd: '/workspace/project',
|
||||
env,
|
||||
windowsHide: true,
|
||||
})
|
||||
})
|
||||
|
||||
it('prefers an explicit CC_HAHA_ROOT when it points at a source checkout', async () => {
|
||||
const sourceRoot = path.join(tmpDir, 'source')
|
||||
await createSourceRoot(sourceRoot)
|
||||
|
||||
@ -62,6 +62,20 @@ export function cliExitSeverity(code: number | null): 'info' | 'error' {
|
||||
return 'error'
|
||||
}
|
||||
|
||||
export function buildConversationCliSpawnOptions(
|
||||
cwd: string,
|
||||
env: NodeJS.ProcessEnv,
|
||||
) {
|
||||
return {
|
||||
cwd,
|
||||
env,
|
||||
stdin: 'pipe',
|
||||
stdout: 'pipe',
|
||||
stderr: 'pipe',
|
||||
windowsHide: true,
|
||||
} as const
|
||||
}
|
||||
|
||||
type AttachmentRef = {
|
||||
type: 'file' | 'image'
|
||||
name?: string
|
||||
@ -285,13 +299,7 @@ export class ConversationService {
|
||||
|
||||
let proc: ReturnType<typeof Bun.spawn>
|
||||
try {
|
||||
proc = Bun.spawn(args, {
|
||||
cwd: launchWorkDir,
|
||||
env: childEnv,
|
||||
stdin: 'pipe',
|
||||
stdout: 'pipe',
|
||||
stderr: 'pipe',
|
||||
})
|
||||
proc = Bun.spawn(args, buildConversationCliSpawnOptions(launchWorkDir, childEnv))
|
||||
} catch (spawnErr) {
|
||||
void diagnosticsService.recordEvent({
|
||||
type: 'cli_spawn_failed',
|
||||
|
||||
@ -41,6 +41,20 @@ export type TaskRun = {
|
||||
sessionId?: string // links to a session for rich output rendering
|
||||
}
|
||||
|
||||
export function buildCronTaskSpawnOptions(
|
||||
cwd: string,
|
||||
env: NodeJS.ProcessEnv,
|
||||
) {
|
||||
return {
|
||||
stdin: 'pipe',
|
||||
stdout: 'pipe',
|
||||
stderr: 'pipe',
|
||||
cwd,
|
||||
env,
|
||||
windowsHide: true,
|
||||
} as const
|
||||
}
|
||||
|
||||
// ─── Output extraction ────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
@ -518,13 +532,7 @@ export class CronScheduler {
|
||||
const childEnv = await this.buildTaskChildEnv(workDir, task)
|
||||
const proc = Bun.spawn(
|
||||
cliArgs,
|
||||
{
|
||||
stdin: 'pipe',
|
||||
stdout: 'pipe',
|
||||
stderr: 'pipe',
|
||||
cwd: workDir,
|
||||
env: childEnv,
|
||||
},
|
||||
buildCronTaskSpawnOptions(workDir, childEnv),
|
||||
)
|
||||
|
||||
this.runningTasks.set(task.id, { proc, startedAt: Date.now(), runId })
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user