fix windows desktop terminal popups (#1199)

This commit is contained in:
ekko
2026-06-01 12:20:10 +08:00
committed by GitHub
parent aa7c1c4fbb
commit 022e18dc8f
5 changed files with 87 additions and 20 deletions
+31 -1
View File
@@ -107,7 +107,7 @@ print(json.dumps({
})
describe('agent bridge Windows desktop subprocess defaults', () => {
it('adds CREATE_NO_WINDOW to nested subprocesses without replacing existing flags', async () => {
it('adds CREATE_NO_WINDOW to sync and async nested subprocesses without replacing existing flags', async () => {
const result = await runBridgeProbe(String.raw`
import importlib.util
import json
@@ -121,6 +121,8 @@ spec.loader.exec_module(bridge)
original_os_name = bridge.os.name
original_popen = bridge.subprocess.Popen
original_async_exec = bridge.asyncio.create_subprocess_exec
original_async_shell = bridge.asyncio.create_subprocess_shell
original_create_no_window = getattr(bridge.subprocess, "CREATE_NO_WINDOW", None)
original_installed = getattr(bridge.subprocess, "_hermes_hidden_defaults_installed", None)
@@ -130,10 +132,22 @@ class FakePopen:
def __init__(self, *args, **kwargs):
FakePopen.calls.append({"args": args, "kwargs": kwargs})
async_calls = []
async def fake_create_subprocess_exec(*args, **kwargs):
async_calls.append({"kind": "exec", "args": args, "kwargs": kwargs})
return {"kind": "exec"}
async def fake_create_subprocess_shell(*args, **kwargs):
async_calls.append({"kind": "shell", "args": args, "kwargs": kwargs})
return {"kind": "shell"}
try:
bridge.os.name = "nt"
bridge.os.environ["HERMES_DESKTOP"] = "true"
bridge.subprocess.Popen = FakePopen
bridge.asyncio.create_subprocess_exec = fake_create_subprocess_exec
bridge.asyncio.create_subprocess_shell = fake_create_subprocess_shell
bridge.subprocess.CREATE_NO_WINDOW = 0x08000000
if hasattr(bridge.subprocess, "_hermes_hidden_defaults_installed"):
delattr(bridge.subprocess, "_hermes_hidden_defaults_installed")
@@ -141,9 +155,15 @@ try:
bridge._install_windows_hidden_subprocess_defaults()
bridge.subprocess.Popen(["git", "status"], creationflags=0x00000200)
flags = FakePopen.calls[0]["kwargs"]["creationflags"]
bridge.asyncio.run(bridge.asyncio.create_subprocess_exec("git", "status", creationflags=0x00000400))
bridge.asyncio.run(bridge.asyncio.create_subprocess_shell("git status"))
async_exec_flags = async_calls[0]["kwargs"]["creationflags"]
async_shell_flags = async_calls[1]["kwargs"]["creationflags"]
finally:
bridge.os.name = original_os_name
bridge.subprocess.Popen = original_popen
bridge.asyncio.create_subprocess_exec = original_async_exec
bridge.asyncio.create_subprocess_shell = original_async_shell
if original_create_no_window is None:
try:
delattr(bridge.subprocess, "CREATE_NO_WINDOW")
@@ -163,6 +183,11 @@ print(json.dumps({
"flags": flags,
"has_create_no_window": bool(flags & 0x08000000),
"kept_existing_flag": bool(flags & 0x00000200),
"async_exec_flags": async_exec_flags,
"async_exec_has_create_no_window": bool(async_exec_flags & 0x08000000),
"async_exec_kept_existing_flag": bool(async_exec_flags & 0x00000400),
"async_shell_flags": async_shell_flags,
"async_shell_has_create_no_window": bool(async_shell_flags & 0x08000000),
}))
`)
@@ -170,6 +195,11 @@ print(json.dumps({
flags: 0x08000200,
has_create_no_window: true,
kept_existing_flag: true,
async_exec_flags: 0x08000400,
async_exec_has_create_no_window: true,
async_exec_kept_existing_flag: true,
async_shell_flags: 0x08000000,
async_shell_has_create_no_window: true,
})
})
})
+26 -6
View File
@@ -4,13 +4,17 @@ import { tmpdir } from 'os'
import { join } from 'path'
const execFileCalls = vi.hoisted(() => [] as Array<{ command: string; args: string[]; options: any }>)
const spawnCalls = vi.hoisted(() => [] as Array<{ command: string; args: string[]; options: any }>)
vi.mock('child_process', () => ({
execFile: vi.fn((command: string, args: string[], options: any, callback: (error: Error | null, stdout: string, stderr: string) => void) => {
execFileCalls.push({ command, args, options })
callback(null, 'ok\n', '')
}),
spawn: vi.fn(),
spawn: vi.fn((command: string, args: string[], options: any) => {
spawnCalls.push({ command, args, options })
return {} as any
}),
}))
const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform')
@@ -21,6 +25,7 @@ function setPlatform(platform: NodeJS.Platform): void {
afterEach(() => {
execFileCalls.length = 0
spawnCalls.length = 0
delete process.env.HERMES_AGENT_BRIDGE_PYTHON
delete process.env.HERMES_AGENT_CLI_PYTHON
if (originalPlatform) Object.defineProperty(process, 'platform', originalPlatform)
@@ -30,7 +35,7 @@ afterEach(() => {
describe('Hermes process invocation', () => {
it('bypasses the uv hermes.exe trampoline on Windows packaged installs', async () => {
setPlatform('win32')
process.env.HERMES_AGENT_CLI_PYTHON = 'C:\\Users\\me\\AppData\\Local\\Programs\\Hermes Studio\\resources\\python\\pythonw.exe'
process.env.HERMES_AGENT_CLI_PYTHON = 'C:\\Users\\me\\AppData\\Local\\Programs\\Hermes Studio\\resources\\python\\python.exe'
const { execHermesWithBin } = await import('../../packages/server/src/services/hermes/hermes-process')
const result = await execHermesWithBin(
@@ -43,25 +48,26 @@ describe('Hermes process invocation', () => {
expect(execFileCalls[0]).toMatchObject({
command: process.env.HERMES_AGENT_CLI_PYTHON,
args: ['-m', 'hermes_cli.main', 'kanban', '--board', 'default', 'create', 'demo', '--json'],
options: expect.objectContaining({ windowsHide: true }),
})
})
it('prefers sibling pythonw.exe for a Windows hermes.exe launcher', async () => {
it('discovers sibling python.exe for a Windows hermes.exe launcher', async () => {
setPlatform('win32')
const root = mkdtempSync(join(tmpdir(), 'hermes-process-'))
try {
const scripts = join(root, 'Scripts')
mkdirSync(scripts)
writeFileSync(join(root, 'python.exe'), '')
writeFileSync(join(root, 'pythonw.exe'), '')
writeFileSync(join(scripts, 'hermes.exe'), '')
const { execHermesWithBin } = await import('../../packages/server/src/services/hermes/hermes-process')
await execHermesWithBin(join(scripts, 'hermes.exe'), ['--version'], { windowsHide: true })
await execHermesWithBin(join(scripts, 'hermes.exe'), ['--version'])
expect(execFileCalls[0]).toMatchObject({
command: join(root, 'pythonw.exe'),
command: join(root, 'python.exe'),
args: ['-m', 'hermes_cli.main', '--version'],
options: expect.objectContaining({ windowsHide: true }),
})
} finally {
rmSync(root, { recursive: true, force: true })
@@ -79,4 +85,18 @@ describe('Hermes process invocation', () => {
args: ['--version'],
})
})
it('defaults spawned Windows Hermes processes to hidden windows', async () => {
setPlatform('win32')
process.env.HERMES_AGENT_CLI_PYTHON = 'C:\\Hermes Studio\\resources\\python\\python.exe'
const { spawnHermesWithBin } = await import('../../packages/server/src/services/hermes/hermes-process')
spawnHermesWithBin('C:\\Hermes Studio\\resources\\python\\Scripts\\hermes.exe', ['gateway', 'run'])
expect(spawnCalls[0]).toMatchObject({
command: process.env.HERMES_AGENT_CLI_PYTHON,
args: ['-m', 'hermes_cli.main', 'gateway', 'run'],
options: expect.objectContaining({ windowsHide: true }),
})
})
})