fix Windows desktop startup readiness (#1167)

* fix desktop startup readiness on windows

* add manual desktop build workflow

* hide Windows desktop server process window

* hide Windows Python bridge worker windows

* use no-window Python for Windows desktop CLI calls

---------

Co-authored-by: xingzhi <chuzihao.czh@alibaba-inc.com>
This commit is contained in:
sir1st
2026-05-31 09:17:49 +08:00
committed by GitHub
parent c998a53566
commit 96bdf8d1af
8 changed files with 276 additions and 29 deletions
+18 -2
View File
@@ -9,12 +9,23 @@ import { app } from 'electron'
import { webuiServerEntry, webuiDir, hermesBin, webUiHome, hermesHome, tokenFile, pythonDir } from './paths'
const DEFAULT_PORT = 8748
const READY_TIMEOUT_MS = 30_000
const DEFAULT_READY_TIMEOUT_MS = 30_000
const execFileAsync = promisify(execFile)
let serverProc: ChildProcess | null = null
let cachedToken: string | null = null
function envPositiveInt(name: string): number | undefined {
const raw = process.env[name]
if (!raw) return undefined
const value = Number(raw)
return Number.isFinite(value) && value > 0 ? value : undefined
}
function readyTimeoutMs(): number {
return envPositiveInt('HERMES_DESKTOP_READY_TIMEOUT_MS') || DEFAULT_READY_TIMEOUT_MS
}
function ensureToken(): string {
if (cachedToken) return cachedToken
const file = tokenFile()
@@ -199,6 +210,9 @@ export async function startWebUiServer(port = DEFAULT_PORT): Promise<string> {
const bundledPython = isWin
? join(pythonDir(), 'python.exe')
: join(pythonDir(), 'bin', 'python3')
const bundledPythonNoWindow = isWin
? join(pythonDir(), 'pythonw.exe')
: bundledPython
const bridgePort = await getFreeTcpPort()
const workerPortBase = await getFreeTcpPortInRange(20000, 59000)
const loginShellPath = await getLoginShellPath()
@@ -219,6 +233,7 @@ export async function startWebUiServer(port = DEFAULT_PORT): Promise<string> {
HERMES_DESKTOP: 'true',
HERMES_BIN: hermesBin(),
HERMES_AGENT_BRIDGE_PYTHON: bundledPython,
HERMES_AGENT_CLI_PYTHON: existsSync(bundledPythonNoWindow) ? bundledPythonNoWindow : bundledPython,
HERMES_AGENT_ROOT: pythonDir(),
// Force TCP loopback for the agent bridge. The default `ipc:///tmp/...`
// unix socket is rejected on macOS in some EDR/sandbox setups (silent
@@ -256,6 +271,7 @@ export async function startWebUiServer(port = DEFAULT_PORT): Promise<string> {
serverProc = spawn(process.execPath, [entry], {
env,
stdio: ['ignore', 'pipe', 'pipe'],
windowsHide: true,
})
serverProc.stdout?.on('data', (chunk: Buffer) => {
@@ -272,7 +288,7 @@ export async function startWebUiServer(port = DEFAULT_PORT): Promise<string> {
}
})
await waitForReady(port, READY_TIMEOUT_MS)
await waitForReady(port, readyTimeoutMs())
return getServerUrl(port)
}
+26 -17
View File
@@ -18,7 +18,7 @@ import { setGroupChatServer } from './routes/hermes/group-chat'
import { setChatRunServer } from './routes/hermes/chat-run'
import { GroupChatServer } from './services/hermes/group-chat'
import { ChatRunSocket } from './services/hermes/run-chat'
import { startAgentBridgeManager } from './services/hermes/agent-bridge'
import { getAgentBridgeManager, startAgentBridgeManager } from './services/hermes/agent-bridge'
import { HermesSkillInjector } from './services/hermes/skill-injector'
import { ensureProfileGatewaysRunning } from './services/hermes/gateway-autostart'
import { logger } from './services/logger'
@@ -81,6 +81,28 @@ function safeNetworkInterfaces() {
}
}
function startRuntimeServicesAfterListen(): void {
void (async () => {
try {
await ensureProfileGatewaysRunning()
console.log('[bootstrap] profile gateways checked')
} catch (err) {
logger.warn(err, '[bootstrap] failed to ensure profile gateways')
console.warn('[bootstrap] failed to ensure profile gateways:', err instanceof Error ? err.message : err)
}
})()
void (async () => {
try {
agentBridgeManager = await startAgentBridgeManager()
console.log('[bootstrap] agent bridge started')
} catch (err) {
logger.warn(err, '[bootstrap] agent bridge failed to start')
console.warn('[bootstrap] agent bridge failed to start:', err instanceof Error ? err.message : err)
}
})()
}
export async function bootstrap() {
console.log(`hermes-web-ui v${APP_VERSION} starting...`)
await mkdir(config.uploadDir, { recursive: true })
@@ -109,23 +131,7 @@ export async function bootstrap() {
console.warn('[bootstrap] failed to inject bundled skills:', err instanceof Error ? err.message : err)
}
try {
await ensureProfileGatewaysRunning()
console.log('[bootstrap] profile gateways checked')
} catch (err) {
logger.warn(err, '[bootstrap] failed to ensure profile gateways')
console.warn('[bootstrap] failed to ensure profile gateways:', err instanceof Error ? err.message : err)
}
const app = new Koa()
try {
agentBridgeManager = await startAgentBridgeManager()
console.log('[bootstrap] agent bridge started')
} catch (err) {
logger.warn(err, '[bootstrap] agent bridge failed to start')
console.warn('[bootstrap] agent bridge failed to start:', err instanceof Error ? err.message : err)
}
await new Promise(resolve => setTimeout(resolve, 1000))
// Initialize all web-ui SQLite tables
const { initAllStores } = await import('./db/hermes/init')
@@ -201,6 +207,9 @@ export async function bootstrap() {
console.log(`Log: ${config.appHome}/logs/server.log`)
logger.info('Server: http://localhost:%d (LAN: http://%s:%d)', config.port, localIp, config.port)
agentBridgeManager = getAgentBridgeManager()
startRuntimeServicesAfterListen()
// Restore group chat agents after server is ready.
groupChatServer.restoreWhenReady()
@@ -65,6 +65,12 @@ def _positive_int(value: str | None) -> int | None:
return parsed if parsed > 0 else None
def _hidden_subprocess_kwargs() -> dict[str, int]:
if os.name != "nt":
return {}
return {"creationflags": getattr(subprocess, "CREATE_NO_WINDOW", 0)}
def _process_exists(pid: int) -> bool:
if pid <= 0:
return False
@@ -76,6 +82,7 @@ def _process_exists(pid: int) -> bool:
capture_output=True,
text=True,
timeout=5,
**_hidden_subprocess_kwargs(),
)
return str(pid) in (result.stdout or "")
except Exception:
@@ -2785,6 +2792,7 @@ class WorkerProcess:
stderr=subprocess.PIPE,
text=True,
bufsize=1,
**_hidden_subprocess_kwargs(),
)
self._pipe_stderr()
self._wait_ready()
@@ -2957,6 +2965,7 @@ def _windows_listening_pids_on_port(port: int) -> list[int]:
encoding=_platform_text_encoding(),
errors="ignore",
timeout=5,
**_hidden_subprocess_kwargs(),
)
except Exception:
return []
@@ -2999,6 +3008,7 @@ def _kill_windows_endpoint_occupants(endpoint: str) -> None:
capture_output=True,
text=True,
timeout=10,
**_hidden_subprocess_kwargs(),
)
except Exception as exc:
print(
@@ -52,10 +52,11 @@ export function shouldUseManagedGatewayRun(): boolean {
process.platform === 'win32'
}
export function shouldUseManagedGatewayRunForAutostart(): boolean {
export function shouldUseManagedGatewayRunForAutostart(platform: NodeJS.Platform = process.platform): boolean {
return envFlagEnabled('HERMES_WEB_UI_MANAGED_GATEWAY') ||
isDockerRuntime() ||
isTermuxRuntime()
isTermuxRuntime() ||
platform === 'win32'
}
export function gatewayStatusLooksRunning(output: string): boolean {
@@ -17,18 +17,20 @@ export function resolveHermesBin(customBin?: string): string {
return customBin?.trim() || process.env.HERMES_BIN?.trim() || 'hermes'
}
function bundledPythonForWindows(hermesBin: string): string | null {
const envPython = process.env.HERMES_AGENT_BRIDGE_PYTHON?.trim()
function bundledCliPythonForWindows(hermesBin: string): string | null {
const envPython = process.env.HERMES_AGENT_CLI_PYTHON?.trim()
if (envPython) return envPython
if (basename(hermesBin).toLowerCase() !== 'hermes.exe') return null
const pythonw = resolve(dirname(hermesBin), '..', 'pythonw.exe')
if (existsSync(pythonw)) return pythonw
const python = resolve(dirname(hermesBin), '..', 'python.exe')
return existsSync(python) ? python : null
}
export function resolveHermesInvocation(hermesBin = resolveHermesBin()): HermesInvocation {
if (process.platform === 'win32') {
const python = bundledPythonForWindows(hermesBin)
const python = bundledCliPythonForWindows(hermesBin)
if (python) return { command: python, argsPrefix: ['-m', 'hermes_cli.main'] }
}