mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-11 08:42:11 +00:00
fix(desktop): hide Windows console children launched by GUI
This commit is contained in:
parent
189ffe7362
commit
4c797d0e23
4 changed files with 88 additions and 18 deletions
|
|
@ -40,6 +40,15 @@ const path = require('node:path')
|
|||
const https = require('node:https')
|
||||
const { spawn } = require('node:child_process')
|
||||
|
||||
const IS_WINDOWS = process.platform === 'win32'
|
||||
|
||||
function hiddenWindowsChildOptions(options = {}) {
|
||||
if (!IS_WINDOWS || Object.prototype.hasOwnProperty.call(options, 'windowsHide')) {
|
||||
return options
|
||||
}
|
||||
return { ...options, windowsHide: true }
|
||||
}
|
||||
|
||||
const STAMP_COMMIT_RE = /^[0-9a-f]{7,40}$/i
|
||||
|
||||
// Stages flagged needs_user_input=true in the manifest are skipped by the
|
||||
|
|
@ -284,7 +293,7 @@ function spawnPowerShell(scriptPath, args, { emit, stageName, abortSignal, herme
|
|||
const ps = process.platform === 'win32' ? resolveWindowsPowerShell() : 'pwsh'
|
||||
const fullArgs = ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', scriptPath, ...args]
|
||||
|
||||
const child = spawn(ps, fullArgs, {
|
||||
const child = spawn(ps, fullArgs, hiddenWindowsChildOptions({
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
env: {
|
||||
...process.env,
|
||||
|
|
@ -292,7 +301,7 @@ function spawnPowerShell(scriptPath, args, { emit, stageName, abortSignal, herme
|
|||
// choice rather than re-computing the default.
|
||||
HERMES_HOME: hermesHome || process.env.HERMES_HOME || ''
|
||||
}
|
||||
})
|
||||
}))
|
||||
|
||||
let stdout = ''
|
||||
let stderr = ''
|
||||
|
|
|
|||
|
|
@ -107,6 +107,13 @@ const IS_WINDOWS = process.platform === 'win32'
|
|||
const IS_WSL = isWslEnvironment()
|
||||
const APP_ROOT = app.getAppPath()
|
||||
|
||||
function hiddenWindowsChildOptions(options = {}) {
|
||||
if (!IS_WINDOWS || Object.prototype.hasOwnProperty.call(options, 'windowsHide')) {
|
||||
return options
|
||||
}
|
||||
return { ...options, windowsHide: true }
|
||||
}
|
||||
|
||||
// Remote displays (SSH X11 forwarding, VNC, RDP) make Chromium's GPU
|
||||
// compositor flicker — accelerated layers can't be presented cleanly over the
|
||||
// wire, so the window flashes during scroll/streaming/animation. Local
|
||||
|
|
@ -1106,7 +1113,7 @@ function findSystemPython() {
|
|||
const out = execFileSync(
|
||||
'reg',
|
||||
['query', `${hive}\\SOFTWARE\\Python\\PythonCore\\${version}\\InstallPath`, '/ve', '/reg:64'],
|
||||
{ encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }
|
||||
hiddenWindowsChildOptions({ encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] })
|
||||
)
|
||||
// Output format: " (Default) REG_SZ C:\Path\To\Python\"
|
||||
const match = out.match(/REG_SZ\s+(.+?)\s*$/m)
|
||||
|
|
@ -1142,10 +1149,10 @@ function findSystemPython() {
|
|||
if (pyExe) {
|
||||
for (const version of SUPPORTED_VERSIONS) {
|
||||
try {
|
||||
const out = execFileSync(pyExe, [`-${version}`, '-c', 'import sys; print(sys.executable)'], {
|
||||
const out = execFileSync(pyExe, [`-${version}`, '-c', 'import sys; print(sys.executable)'], hiddenWindowsChildOptions({
|
||||
encoding: 'utf8',
|
||||
stdio: ['ignore', 'pipe', 'ignore']
|
||||
})
|
||||
}))
|
||||
const candidate = out.trim()
|
||||
if (candidate && fileExists(candidate)) return candidate
|
||||
} catch {
|
||||
|
|
@ -1280,11 +1287,11 @@ function resolveUpdateRoot() {
|
|||
|
||||
function runGit(args, options = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(resolveGitBinary(), IS_WINDOWS ? ['-c', 'windows.appendAtomically=false', ...args] : args, {
|
||||
const child = spawn(resolveGitBinary(), IS_WINDOWS ? ['-c', 'windows.appendAtomically=false', ...args] : args, hiddenWindowsChildOptions({
|
||||
cwd: options.cwd,
|
||||
env: { ...process.env, ...(options.env || {}), GIT_TERMINAL_PROMPT: '0' },
|
||||
stdio: ['ignore', 'pipe', 'pipe']
|
||||
})
|
||||
}))
|
||||
|
||||
let stdout = ''
|
||||
let stderr = ''
|
||||
|
|
@ -1494,7 +1501,7 @@ function forceKillProcessTree(pid) {
|
|||
if (!IS_WINDOWS) return
|
||||
if (!Number.isInteger(pid) || pid <= 0) return
|
||||
try {
|
||||
execFileSync('taskkill', ['/PID', String(pid), '/T', '/F'], { stdio: 'ignore' })
|
||||
execFileSync('taskkill', ['/PID', String(pid), '/T', '/F'], hiddenWindowsChildOptions({ stdio: 'ignore' }))
|
||||
} catch {
|
||||
// Already gone, or no permission — best effort; the unlock wait below is
|
||||
// the real gate.
|
||||
|
|
@ -1680,11 +1687,11 @@ function runStreamedUpdate(command, args, { cwd, env, stage } = {}) {
|
|||
return new Promise(resolve => {
|
||||
let child
|
||||
try {
|
||||
child = spawn(command, args, {
|
||||
child = spawn(command, args, hiddenWindowsChildOptions({
|
||||
cwd,
|
||||
env: { ...process.env, ...(env || {}) },
|
||||
stdio: ['ignore', 'pipe', 'pipe']
|
||||
})
|
||||
}))
|
||||
} catch (err) {
|
||||
resolve({ code: 1, error: err.message })
|
||||
return
|
||||
|
|
@ -2671,7 +2678,7 @@ function fetchHtmlTitleWithCurl(rawUrl) {
|
|||
'--raw',
|
||||
url
|
||||
]
|
||||
const child = spawn('curl', args, { stdio: ['ignore', 'pipe', 'ignore'] })
|
||||
const child = spawn('curl', args, hiddenWindowsChildOptions({ stdio: ['ignore', 'pipe', 'ignore'] }))
|
||||
const chunks = []
|
||||
let bytes = 0
|
||||
|
||||
|
|
@ -4491,7 +4498,7 @@ async function spawnPoolBackend(profile, entry) {
|
|||
|
||||
rememberLog(`Starting Hermes backend for profile "${profile}" via ${backend.label}`)
|
||||
|
||||
const child = spawn(backend.command, backend.args, {
|
||||
const child = spawn(backend.command, backend.args, hiddenWindowsChildOptions({
|
||||
cwd: hermesCwd,
|
||||
env: {
|
||||
...process.env,
|
||||
|
|
@ -4509,7 +4516,7 @@ async function spawnPoolBackend(profile, entry) {
|
|||
},
|
||||
shell: backend.shell,
|
||||
stdio: ['ignore', 'pipe', 'pipe']
|
||||
})
|
||||
}))
|
||||
entry.process = child
|
||||
entry.port = port
|
||||
entry.token = token
|
||||
|
|
@ -4691,7 +4698,7 @@ async function startHermes() {
|
|||
await advanceBootProgress('backend.spawn', `Starting Hermes backend via ${backend.label}`, 84)
|
||||
rememberLog(`Starting Hermes backend via ${backend.label}`)
|
||||
|
||||
hermesProcess = spawn(backend.command, backend.args, {
|
||||
hermesProcess = spawn(backend.command, backend.args, hiddenWindowsChildOptions({
|
||||
cwd: hermesCwd,
|
||||
env: {
|
||||
...process.env,
|
||||
|
|
@ -4714,7 +4721,7 @@ async function startHermes() {
|
|||
},
|
||||
shell: backend.shell,
|
||||
stdio: ['ignore', 'pipe', 'pipe']
|
||||
})
|
||||
}))
|
||||
|
||||
hermesProcess.stdout.on('data', rememberLog)
|
||||
hermesProcess.stderr.on('data', rememberLog)
|
||||
|
|
@ -5986,11 +5993,11 @@ async function getUninstallSummary() {
|
|||
resolve(value)
|
||||
}
|
||||
try {
|
||||
const child = spawn(py, ['-m', 'hermes_cli.main', 'uninstall', '--gui-summary'], {
|
||||
const child = spawn(py, ['-m', 'hermes_cli.main', 'uninstall', '--gui-summary'], hiddenWindowsChildOptions({
|
||||
cwd: agentRoot,
|
||||
env: { ...process.env, HERMES_HOME, NO_COLOR: '1' },
|
||||
stdio: ['ignore', 'pipe', 'ignore']
|
||||
})
|
||||
}))
|
||||
child.stdout.on('data', chunk => {
|
||||
stdout += chunk.toString()
|
||||
})
|
||||
|
|
|
|||
54
apps/desktop/electron/windows-child-process.test.cjs
Normal file
54
apps/desktop/electron/windows-child-process.test.cjs
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
'use strict'
|
||||
|
||||
const test = require('node:test')
|
||||
const assert = require('node:assert/strict')
|
||||
const fs = require('node:fs')
|
||||
const path = require('node:path')
|
||||
|
||||
const ELECTRON_DIR = __dirname
|
||||
|
||||
function readElectronFile(name) {
|
||||
return fs.readFileSync(path.join(ELECTRON_DIR, name), 'utf8')
|
||||
}
|
||||
|
||||
function requireHiddenChildOptions(source, needle) {
|
||||
const index = source.indexOf(needle)
|
||||
assert.notEqual(index, -1, `missing call site: ${needle}`)
|
||||
const snippet = source.slice(index, index + 700)
|
||||
assert.match(
|
||||
snippet,
|
||||
/hiddenWindowsChildOptions\(/,
|
||||
`expected ${needle} to wrap child-process options with hiddenWindowsChildOptions`
|
||||
)
|
||||
}
|
||||
|
||||
test('desktop background child processes opt into hidden Windows consoles', () => {
|
||||
const source = readElectronFile('main.cjs')
|
||||
|
||||
assert.match(source, /function hiddenWindowsChildOptions\(options = \{\}\)/)
|
||||
|
||||
requireHiddenChildOptions(source, "execFileSync(\n 'reg'")
|
||||
requireHiddenChildOptions(source, 'execFileSync(pyExe')
|
||||
requireHiddenChildOptions(source, 'spawn(resolveGitBinary()')
|
||||
requireHiddenChildOptions(source, "execFileSync('taskkill'")
|
||||
requireHiddenChildOptions(source, 'spawn(command, args')
|
||||
requireHiddenChildOptions(source, "spawn('curl'")
|
||||
requireHiddenChildOptions(source, 'spawn(backend.command, backend.args')
|
||||
requireHiddenChildOptions(source, 'hermesProcess = spawn(backend.command, backend.args')
|
||||
requireHiddenChildOptions(source, "spawn(py, ['-m', 'hermes_cli.main', 'uninstall', '--gui-summary']")
|
||||
})
|
||||
|
||||
test('intentional or interactive desktop child processes stay documented', () => {
|
||||
const source = readElectronFile('main.cjs')
|
||||
|
||||
assert.match(source, /windowsHide: false/)
|
||||
assert.match(source, /nodePty\.spawn\(command, args/)
|
||||
assert.match(source, /spawn\('cmd\.exe', \['\/c', 'start'/)
|
||||
})
|
||||
|
||||
test('bootstrap PowerShell runner hides Windows console children', () => {
|
||||
const source = readElectronFile('bootstrap-runner.cjs')
|
||||
|
||||
assert.match(source, /function hiddenWindowsChildOptions\(options = \{\}\)/)
|
||||
requireHiddenChildOptions(source, 'spawn(ps, fullArgs')
|
||||
})
|
||||
|
|
@ -35,7 +35,7 @@
|
|||
"test:desktop:nsis": "node scripts/test-desktop.mjs nsis",
|
||||
"test:desktop:existing": "node scripts/test-desktop.mjs existing",
|
||||
"test:desktop:fresh": "node scripts/test-desktop.mjs fresh",
|
||||
"test:desktop:platforms": "node --test electron/bootstrap-platform.test.cjs electron/hardening.test.cjs electron/backend-probes.test.cjs electron/bootstrap-runner.test.cjs electron/connection-config.test.cjs electron/gateway-ws-probe.test.cjs electron/oauth-net-request.test.cjs electron/desktop-uninstall.test.cjs electron/session-windows.test.cjs electron/workspace-cwd.test.cjs",
|
||||
"test:desktop:platforms": "node --test electron/bootstrap-platform.test.cjs electron/hardening.test.cjs electron/backend-probes.test.cjs electron/bootstrap-runner.test.cjs electron/connection-config.test.cjs electron/gateway-ws-probe.test.cjs electron/oauth-net-request.test.cjs electron/desktop-uninstall.test.cjs electron/session-windows.test.cjs electron/workspace-cwd.test.cjs electron/windows-child-process.test.cjs",
|
||||
"type-check": "tsc -b",
|
||||
"lint": "eslint src/ electron/",
|
||||
"lint:fix": "eslint src/ electron/ --fix",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue