fix windows desktop bridge startup (#1196)
This commit is contained in:
@@ -28,16 +28,22 @@ function quitApp() {
|
|||||||
app.quit()
|
app.quit()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function loginItemOptions() {
|
||||||
|
return {
|
||||||
|
path: process.execPath,
|
||||||
|
args: ['--hidden'],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getOpenAtLogin(): boolean {
|
function getOpenAtLogin(): boolean {
|
||||||
return app.getLoginItemSettings().openAtLogin
|
return app.getLoginItemSettings(loginItemOptions()).openAtLogin
|
||||||
}
|
}
|
||||||
|
|
||||||
function setOpenAtLogin(openAtLogin: boolean) {
|
function setOpenAtLogin(openAtLogin: boolean) {
|
||||||
app.setLoginItemSettings({
|
app.setLoginItemSettings({
|
||||||
|
...loginItemOptions(),
|
||||||
openAtLogin,
|
openAtLogin,
|
||||||
openAsHidden: true,
|
openAsHidden: true,
|
||||||
path: process.execPath,
|
|
||||||
args: ['--hidden'],
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -228,14 +228,12 @@ export async function startWebUiServer(port = DEFAULT_PORT): Promise<string> {
|
|||||||
// setup is a #!/bin/sh wrapper, not a python interpreter, so detection
|
// setup is a #!/bin/sh wrapper, not a python interpreter, so detection
|
||||||
// resolves to /bin/sh and the bridge crashes (exit code 2) immediately.
|
// resolves to /bin/sh and the bridge crashes (exit code 2) immediately.
|
||||||
const isWin = process.platform === 'win32'
|
const isWin = process.platform === 'win32'
|
||||||
const bundledPythonNoWindow = isWin
|
const bundledPython = isWin
|
||||||
? join(pythonDir(), 'pythonw.exe')
|
|
||||||
: join(pythonDir(), 'bin', 'python3')
|
|
||||||
const bundledPython = isWin && existsSync(bundledPythonNoWindow)
|
|
||||||
? bundledPythonNoWindow
|
|
||||||
: isWin
|
|
||||||
? join(pythonDir(), 'python.exe')
|
? join(pythonDir(), 'python.exe')
|
||||||
: join(pythonDir(), 'bin', 'python3')
|
: join(pythonDir(), 'bin', 'python3')
|
||||||
|
const bundledPythonNoWindow = isWin
|
||||||
|
? join(pythonDir(), 'pythonw.exe')
|
||||||
|
: bundledPython
|
||||||
const bridgePort = await getFreeTcpPort()
|
const bridgePort = await getFreeTcpPort()
|
||||||
const workerPortBase = await getFreeTcpPortInRange(20000, 59000)
|
const workerPortBase = await getFreeTcpPortInRange(20000, 59000)
|
||||||
const loginShellPath = await getLoginShellPath()
|
const loginShellPath = await getLoginShellPath()
|
||||||
@@ -255,6 +253,9 @@ export async function startWebUiServer(port = DEFAULT_PORT): Promise<string> {
|
|||||||
NODE_ENV: 'production',
|
NODE_ENV: 'production',
|
||||||
HERMES_DESKTOP: 'true',
|
HERMES_DESKTOP: 'true',
|
||||||
HERMES_BIN: hermesBin(),
|
HERMES_BIN: hermesBin(),
|
||||||
|
// The bridge and its per-profile workers need working stdout/stderr for
|
||||||
|
// ready handshakes. Use python.exe on Windows and hide windows at the
|
||||||
|
// process creation layer instead of switching the bridge to pythonw.exe.
|
||||||
HERMES_AGENT_BRIDGE_PYTHON: bundledPython,
|
HERMES_AGENT_BRIDGE_PYTHON: bundledPython,
|
||||||
HERMES_AGENT_CLI_PYTHON: existsSync(bundledPythonNoWindow) ? bundledPythonNoWindow : bundledPython,
|
HERMES_AGENT_CLI_PYTHON: existsSync(bundledPythonNoWindow) ? bundledPythonNoWindow : bundledPython,
|
||||||
HERMES_AGENT_ROOT: pythonDir(),
|
HERMES_AGENT_ROOT: pythonDir(),
|
||||||
|
|||||||
@@ -71,6 +71,40 @@ def _hidden_subprocess_kwargs() -> dict[str, int]:
|
|||||||
return {"creationflags": getattr(subprocess, "CREATE_NO_WINDOW", 0)}
|
return {"creationflags": getattr(subprocess, "CREATE_NO_WINDOW", 0)}
|
||||||
|
|
||||||
|
|
||||||
|
def _install_windows_hidden_subprocess_defaults() -> None:
|
||||||
|
"""Hide console windows for subprocesses launched inside desktop bridge runs.
|
||||||
|
|
||||||
|
The desktop bridge itself must keep stdout/stderr pipes for readiness and
|
||||||
|
worker handshakes, so it runs under python.exe. On Windows that means any
|
||||||
|
nested console executable, including git.exe from context expansion, can
|
||||||
|
flash a window unless the child process is created with CREATE_NO_WINDOW.
|
||||||
|
"""
|
||||||
|
if os.name != "nt":
|
||||||
|
return
|
||||||
|
if os.environ.get("HERMES_DESKTOP", "").strip().lower() != "true":
|
||||||
|
return
|
||||||
|
if getattr(subprocess, "_hermes_hidden_defaults_installed", False):
|
||||||
|
return
|
||||||
|
|
||||||
|
original_popen = subprocess.Popen
|
||||||
|
create_no_window = getattr(subprocess, "CREATE_NO_WINDOW", 0) or 0x08000000
|
||||||
|
|
||||||
|
class HiddenPopen(original_popen): # type: ignore[misc, valid-type]
|
||||||
|
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||||
|
flags = kwargs.get("creationflags", 0) or 0
|
||||||
|
try:
|
||||||
|
kwargs["creationflags"] = int(flags) | create_no_window
|
||||||
|
except Exception:
|
||||||
|
kwargs["creationflags"] = create_no_window
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
subprocess.Popen = HiddenPopen # type: ignore[assignment]
|
||||||
|
subprocess._hermes_hidden_defaults_installed = True # type: ignore[attr-defined]
|
||||||
|
|
||||||
|
|
||||||
|
_install_windows_hidden_subprocess_defaults()
|
||||||
|
|
||||||
|
|
||||||
def _process_exists(pid: int) -> bool:
|
def _process_exists(pid: int) -> bool:
|
||||||
if pid <= 0:
|
if pid <= 0:
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -106,6 +106,74 @@ print(json.dumps({
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('agent bridge Windows desktop subprocess defaults', () => {
|
||||||
|
it('adds CREATE_NO_WINDOW to nested subprocesses without replacing existing flags', async () => {
|
||||||
|
const result = await runBridgeProbe(String.raw`
|
||||||
|
import importlib.util
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
spec = importlib.util.spec_from_file_location("hermes_bridge", os.environ["BRIDGE_PATH"])
|
||||||
|
bridge = importlib.util.module_from_spec(spec)
|
||||||
|
sys.modules["hermes_bridge"] = bridge
|
||||||
|
spec.loader.exec_module(bridge)
|
||||||
|
|
||||||
|
original_os_name = bridge.os.name
|
||||||
|
original_popen = bridge.subprocess.Popen
|
||||||
|
original_create_no_window = getattr(bridge.subprocess, "CREATE_NO_WINDOW", None)
|
||||||
|
original_installed = getattr(bridge.subprocess, "_hermes_hidden_defaults_installed", None)
|
||||||
|
|
||||||
|
class FakePopen:
|
||||||
|
calls = []
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
FakePopen.calls.append({"args": args, "kwargs": kwargs})
|
||||||
|
|
||||||
|
try:
|
||||||
|
bridge.os.name = "nt"
|
||||||
|
bridge.os.environ["HERMES_DESKTOP"] = "true"
|
||||||
|
bridge.subprocess.Popen = FakePopen
|
||||||
|
bridge.subprocess.CREATE_NO_WINDOW = 0x08000000
|
||||||
|
if hasattr(bridge.subprocess, "_hermes_hidden_defaults_installed"):
|
||||||
|
delattr(bridge.subprocess, "_hermes_hidden_defaults_installed")
|
||||||
|
|
||||||
|
bridge._install_windows_hidden_subprocess_defaults()
|
||||||
|
bridge.subprocess.Popen(["git", "status"], creationflags=0x00000200)
|
||||||
|
flags = FakePopen.calls[0]["kwargs"]["creationflags"]
|
||||||
|
finally:
|
||||||
|
bridge.os.name = original_os_name
|
||||||
|
bridge.subprocess.Popen = original_popen
|
||||||
|
if original_create_no_window is None:
|
||||||
|
try:
|
||||||
|
delattr(bridge.subprocess, "CREATE_NO_WINDOW")
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
bridge.subprocess.CREATE_NO_WINDOW = original_create_no_window
|
||||||
|
if original_installed is None:
|
||||||
|
try:
|
||||||
|
delattr(bridge.subprocess, "_hermes_hidden_defaults_installed")
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
bridge.subprocess._hermes_hidden_defaults_installed = original_installed
|
||||||
|
|
||||||
|
print(json.dumps({
|
||||||
|
"flags": flags,
|
||||||
|
"has_create_no_window": bool(flags & 0x08000000),
|
||||||
|
"kept_existing_flag": bool(flags & 0x00000200),
|
||||||
|
}))
|
||||||
|
`)
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
flags: 0x08000200,
|
||||||
|
has_create_no_window: true,
|
||||||
|
kept_existing_flag: true,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('agent bridge profile environment', () => {
|
describe('agent bridge profile environment', () => {
|
||||||
it('runs agent calls with the requested profile HERMES_HOME and restores the bridge home', async () => {
|
it('runs agent calls with the requested profile HERMES_HOME and restores the bridge home', async () => {
|
||||||
const profileHome = join(tempDir, 'profiles', 'work')
|
const profileHome = join(tempDir, 'profiles', 'work')
|
||||||
|
|||||||
Reference in New Issue
Block a user