Fix desktop runtime cold start handling (#1233)
* fix desktop runtime cold start handling * fix windows desktop python startup env * Revert "fix windows desktop python startup env" This reverts commit 3718ba7586ab1a672c7e599ff1e315dfa76d7cda. * bump desktop release version to 0.6.8
This commit is contained in:
Generated
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hermes-web-ui",
|
"name": "hermes-web-ui",
|
||||||
"version": "0.6.7",
|
"version": "0.6.8",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hermes-web-ui",
|
"name": "hermes-web-ui",
|
||||||
"version": "0.6.7",
|
"version": "0.6.8",
|
||||||
"description": "Self-hosted AI chat dashboard for Hermes Agent — multi-model web UI with multi-platform integration",
|
"description": "Self-hosted AI chat dashboard for Hermes Agent — multi-model web UI with multi-platform integration",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "hermes-studio",
|
"name": "hermes-studio",
|
||||||
"version": "0.6.7",
|
"version": "0.6.8",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "hermes-studio",
|
"name": "hermes-studio",
|
||||||
"version": "0.6.7",
|
"version": "0.6.8",
|
||||||
"license": "BSL-1.1",
|
"license": "BSL-1.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"electron-updater": "^6.3.9"
|
"electron-updater": "^6.3.9"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hermes-studio",
|
"name": "hermes-studio",
|
||||||
"version": "0.6.7",
|
"version": "0.6.8",
|
||||||
"description": "Hermes Studio desktop distribution with bundled Python runtime and hermes-agent",
|
"description": "Hermes Studio desktop distribution with bundled Python runtime and hermes-agent",
|
||||||
"homepage": "https://ekkolearnai.com",
|
"homepage": "https://ekkolearnai.com",
|
||||||
"author": {
|
"author": {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
} from 'node:fs'
|
} from 'node:fs'
|
||||||
import { get as httpGet } from 'node:http'
|
import { get as httpGet } from 'node:http'
|
||||||
import { get as httpsGet } from 'node:https'
|
import { get as httpsGet } from 'node:https'
|
||||||
import { basename, dirname, join } from 'node:path'
|
import { basename, dirname, join, relative } from 'node:path'
|
||||||
import { promisify } from 'node:util'
|
import { promisify } from 'node:util'
|
||||||
import { app } from 'electron'
|
import { app } from 'electron'
|
||||||
import {
|
import {
|
||||||
@@ -58,6 +58,25 @@ export type RuntimeProgress = {
|
|||||||
|
|
||||||
type RuntimeProgressHandler = (progress: RuntimeProgress) => void
|
type RuntimeProgressHandler = (progress: RuntimeProgress) => void
|
||||||
|
|
||||||
|
function requiredRuntimeFiles(root: string): string[] {
|
||||||
|
const pythonBin = process.platform === 'win32'
|
||||||
|
? join(root, 'python', 'python.exe')
|
||||||
|
: join(root, 'python', 'bin', 'python3')
|
||||||
|
const hermesBin = process.platform === 'win32'
|
||||||
|
? join(root, 'python', 'Scripts', 'hermes.exe')
|
||||||
|
: join(root, 'python', 'bin', 'hermes')
|
||||||
|
const nodeBin = process.platform === 'win32'
|
||||||
|
? join(root, 'node', 'node.exe')
|
||||||
|
: join(root, 'node', 'bin', 'node')
|
||||||
|
const files = [pythonBin, hermesBin, nodeBin, join(root, RUNTIME_MANIFEST_NAME)]
|
||||||
|
if (process.platform === 'win32') files.push(join(root, 'git', 'cmd', 'git.exe'))
|
||||||
|
return files
|
||||||
|
}
|
||||||
|
|
||||||
|
function missingRuntimeFiles(root: string): string[] {
|
||||||
|
return requiredRuntimeFiles(root).filter(file => !existsSync(file))
|
||||||
|
}
|
||||||
|
|
||||||
function runtimeReady(): boolean {
|
function runtimeReady(): boolean {
|
||||||
const gitReady = process.platform !== 'win32' || !!bundledGit()
|
const gitReady = process.platform !== 'win32' || !!bundledGit()
|
||||||
return hermesBinExists() && existsSync(bundledNode()) && gitReady
|
return hermesBinExists() && existsSync(bundledNode()) && gitReady
|
||||||
@@ -230,10 +249,9 @@ async function extractRuntimeArchive(archive: string, targetRoot: string): Promi
|
|||||||
await execFileAsync(process.platform === 'win32' ? 'tar.exe' : 'tar', ['-xzf', archive, '-C', tempRoot], {
|
await execFileAsync(process.platform === 'win32' ? 'tar.exe' : 'tar', ['-xzf', archive, '-C', tempRoot], {
|
||||||
windowsHide: true,
|
windowsHide: true,
|
||||||
})
|
})
|
||||||
for (const required of ['python', 'node']) {
|
const missing = missingRuntimeFiles(tempRoot)
|
||||||
if (!existsSync(join(tempRoot, required))) {
|
if (missing.length > 0) {
|
||||||
throw new Error(`Runtime archive did not contain ${required}/`)
|
throw new Error(`Runtime archive is missing required files: ${missing.map(file => relative(tempRoot, file)).join(', ')}`)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
rmSync(targetRoot, { recursive: true, force: true })
|
rmSync(targetRoot, { recursive: true, force: true })
|
||||||
mkdirSync(parent, { recursive: true })
|
mkdirSync(parent, { recursive: true })
|
||||||
@@ -265,19 +283,23 @@ export async function ensureDesktopRuntime(onProgress?: RuntimeProgressHandler):
|
|||||||
const archive = join(dirname(runtimeRoot), `${descriptor.name}.download`)
|
const archive = join(dirname(runtimeRoot), `${descriptor.name}.download`)
|
||||||
console.log(`[runtime] downloading Hermes runtime ${descriptor.name}`)
|
console.log(`[runtime] downloading Hermes runtime ${descriptor.name}`)
|
||||||
onProgress?.({ stage: 'download', message: `Downloading ${descriptor.name}...` })
|
onProgress?.({ stage: 'download', message: `Downloading ${descriptor.name}...` })
|
||||||
await downloadFile(descriptor.url, archive, onProgress)
|
let archiveSize = 0
|
||||||
if (descriptor.sha256) {
|
try {
|
||||||
onProgress?.({ stage: 'verify', message: 'Verifying Hermes runtime...' })
|
await downloadFile(descriptor.url, archive, onProgress)
|
||||||
const actual = await sha256File(archive)
|
archiveSize = statSync(archive).size
|
||||||
if (actual !== descriptor.sha256) {
|
if (descriptor.sha256) {
|
||||||
throw new Error(`Runtime checksum mismatch for ${descriptor.name}`)
|
onProgress?.({ stage: 'verify', message: 'Verifying Hermes runtime...' })
|
||||||
|
const actual = await sha256File(archive)
|
||||||
|
if (actual !== descriptor.sha256) {
|
||||||
|
throw new Error(`Runtime checksum mismatch for ${descriptor.name}`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
onProgress?.({ stage: 'extract', message: 'Extracting Hermes runtime...' })
|
onProgress?.({ stage: 'extract', message: 'Extracting Hermes runtime...' })
|
||||||
await extractRuntimeArchive(archive, runtimeRoot)
|
await extractRuntimeArchive(archive, runtimeRoot)
|
||||||
const archiveSize = statSync(archive).size
|
} finally {
|
||||||
rmSync(archive, { force: true })
|
rmSync(archive, { force: true })
|
||||||
|
}
|
||||||
|
|
||||||
const manifestPath = join(runtimeRoot, RUNTIME_MANIFEST_NAME)
|
const manifestPath = join(runtimeRoot, RUNTIME_MANIFEST_NAME)
|
||||||
if (!existsSync(manifestPath)) {
|
if (!existsSync(manifestPath)) {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import {
|
|||||||
} from './paths'
|
} from './paths'
|
||||||
|
|
||||||
const DEFAULT_PORT = 8748
|
const DEFAULT_PORT = 8748
|
||||||
const DEFAULT_READY_TIMEOUT_MS = 30_000
|
const DEFAULT_READY_TIMEOUT_MS = 120_000
|
||||||
const AGENT_BRIDGE_STARTED_MARKER = '[bootstrap] agent bridge started'
|
const AGENT_BRIDGE_STARTED_MARKER = '[bootstrap] agent bridge started'
|
||||||
const AGENT_BRIDGE_FAILED_MARKER = '[bootstrap] agent bridge failed to start'
|
const AGENT_BRIDGE_FAILED_MARKER = '[bootstrap] agent bridge failed to start'
|
||||||
const execFileAsync = promisify(execFile)
|
const execFileAsync = promisify(execFile)
|
||||||
@@ -405,10 +405,10 @@ export async function startWebUiServer(port = DEFAULT_PORT): Promise<string> {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const timeoutMs = readyTimeoutMs()
|
const timeoutMs = readyTimeoutMs()
|
||||||
await Promise.all([
|
void bridgeStartup.wait(timeoutMs).catch(err => {
|
||||||
waitForReady(port, timeoutMs),
|
console.warn(`[webui] agent bridge was not ready during startup: ${err instanceof Error ? err.message : String(err)}`)
|
||||||
bridgeStartup.wait(timeoutMs),
|
})
|
||||||
])
|
await waitForReady(port, timeoutMs)
|
||||||
return getServerUrl(port)
|
return getServerUrl(port)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user