mirror of
https://github.com/EKKOLearnAI/hermes-web-ui.git
synced 2026-07-20 23:40:20 +00:00
fix desktop runtime download temp file (#1814)
This commit is contained in:
@@ -3,6 +3,7 @@ import {
|
||||
createReadStream,
|
||||
createWriteStream,
|
||||
existsSync,
|
||||
mkdtempSync,
|
||||
mkdirSync,
|
||||
readFileSync,
|
||||
renameSync,
|
||||
@@ -12,6 +13,7 @@ import {
|
||||
} from 'node:fs'
|
||||
import { get as httpGet } from 'node:http'
|
||||
import { get as httpsGet } from 'node:https'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { basename, dirname, join, relative } from 'node:path'
|
||||
import { app } from 'electron'
|
||||
import {
|
||||
@@ -382,7 +384,8 @@ export async function ensureDesktopRuntime(
|
||||
|
||||
if (cachedRuntimeMatches(runtimeRoot, descriptor) && !process.env.HERMES_DESKTOP_RUNTIME_FORCE_UPDATE) return
|
||||
|
||||
const archive = join(dirname(runtimeRoot), `${descriptor.name}.download`)
|
||||
const downloadTempDir = mkdtempSync(join(tmpdir(), 'hermes-runtime-download-'))
|
||||
const archive = join(downloadTempDir, `${descriptor.name}.download`)
|
||||
console.log(`[runtime] downloading Hermes runtime ${descriptor.name}`)
|
||||
onProgress?.({ stage: 'download', message: t('runtime.downloadingPackage', { name: descriptor.name }) })
|
||||
let archiveSize = 0
|
||||
@@ -401,6 +404,7 @@ export async function ensureDesktopRuntime(
|
||||
await extractRuntimeArchive(archive, runtimeRoot)
|
||||
} finally {
|
||||
rmSync(archive, { force: true })
|
||||
rmSync(downloadTempDir, { recursive: true, force: true })
|
||||
}
|
||||
|
||||
const manifestPath = join(runtimeRoot, RUNTIME_MANIFEST_NAME)
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
import { createReadStream, existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { createServer, type Server } from 'node:http'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import * as tar from 'tar'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const mockElectronApp = vi.hoisted(() => ({
|
||||
isPackaged: false,
|
||||
getAppPath: () => process.cwd(),
|
||||
getVersion: () => '0.6.21',
|
||||
getLocale: () => 'en',
|
||||
}))
|
||||
|
||||
vi.mock('electron', () => ({
|
||||
app: mockElectronApp,
|
||||
}))
|
||||
|
||||
const originalEnv = { ...process.env }
|
||||
const tempDirs: string[] = []
|
||||
const servers: Server[] = []
|
||||
|
||||
function tempDir(prefix: string): string {
|
||||
const dir = mkdtempSync(join(tmpdir(), prefix))
|
||||
tempDirs.push(dir)
|
||||
return dir
|
||||
}
|
||||
|
||||
function createRuntimeFiles(root: string) {
|
||||
if (process.platform === 'win32') {
|
||||
mkdirSync(join(root, 'python', 'Scripts'), { recursive: true })
|
||||
mkdirSync(join(root, 'node'), { recursive: true })
|
||||
mkdirSync(join(root, 'git', 'cmd'), { recursive: true })
|
||||
writeFileSync(join(root, 'python', 'python.exe'), '')
|
||||
writeFileSync(join(root, 'python', 'Scripts', 'hermes.exe'), '')
|
||||
writeFileSync(join(root, 'node', 'node.exe'), '')
|
||||
writeFileSync(join(root, 'git', 'cmd', 'git.exe'), '')
|
||||
} else {
|
||||
mkdirSync(join(root, 'python', 'bin'), { recursive: true })
|
||||
mkdirSync(join(root, 'node', 'bin'), { recursive: true })
|
||||
writeFileSync(join(root, 'python', 'bin', 'python3'), '')
|
||||
writeFileSync(join(root, 'python', 'bin', 'hermes'), '')
|
||||
writeFileSync(join(root, 'node', 'bin', 'node'), '')
|
||||
}
|
||||
writeFileSync(join(root, 'runtime-manifest.json'), JSON.stringify({
|
||||
schema: 1,
|
||||
platform: process.platform,
|
||||
hermesAgentVersion: '0.15.2',
|
||||
asset: { name: 'hermes-runtime-test.tar.gz' },
|
||||
}))
|
||||
}
|
||||
|
||||
async function createRuntimeArchive(): Promise<string> {
|
||||
const source = tempDir('hermes-runtime-source-')
|
||||
const archive = join(tempDir('hermes-runtime-archive-'), 'hermes-runtime-test.tar.gz')
|
||||
createRuntimeFiles(source)
|
||||
await tar.c({ gzip: true, cwd: source, file: archive }, ['.'])
|
||||
return archive
|
||||
}
|
||||
|
||||
async function serveFile(file: string): Promise<string> {
|
||||
const server = createServer((request, response) => {
|
||||
if (request.url !== '/hermes-runtime-test.tar.gz') {
|
||||
response.writeHead(404)
|
||||
response.end()
|
||||
return
|
||||
}
|
||||
response.writeHead(200, { 'content-type': 'application/gzip' })
|
||||
createReadStream(file).pipe(response)
|
||||
})
|
||||
servers.push(server)
|
||||
await new Promise<void>(resolve => server.listen(0, '127.0.0.1', resolve))
|
||||
const address = server.address()
|
||||
if (!address || typeof address === 'string') throw new Error('test server did not bind to a port')
|
||||
return `http://127.0.0.1:${address.port}/hermes-runtime-test.tar.gz`
|
||||
}
|
||||
|
||||
describe('desktop runtime manager', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules()
|
||||
process.env = { ...originalEnv }
|
||||
process.env.HERMES_WEB_UI_HOME = tempDir('hermes-runtime-home-')
|
||||
process.env.HERMES_DESKTOP_RUNTIME_RELEASE_TAG = 'hermes-0.15.2-runtime'
|
||||
mockElectronApp.isPackaged = false
|
||||
mockElectronApp.getAppPath = () => process.cwd()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
process.env = { ...originalEnv }
|
||||
vi.resetModules()
|
||||
await Promise.all(servers.splice(0).map(server => new Promise<void>(resolve => server.close(() => resolve()))))
|
||||
for (const dir of tempDirs.splice(0)) {
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it('downloads through a unique temp file instead of reusing a stale runtime .download path', async () => {
|
||||
const archive = await createRuntimeArchive()
|
||||
process.env.HERMES_DESKTOP_RUNTIME_URL = await serveFile(archive)
|
||||
|
||||
const { runtimePlatformKey } = await import('../../packages/desktop/src/main/runtime-paths')
|
||||
const staleDownloadPath = join(
|
||||
process.env.HERMES_WEB_UI_HOME!,
|
||||
'desktop-runtime',
|
||||
'hermes',
|
||||
'0.15.2',
|
||||
'hermes-runtime-test.tar.gz.download',
|
||||
)
|
||||
mkdirSync(staleDownloadPath, { recursive: true })
|
||||
|
||||
const { ensureDesktopRuntime } = await import('../../packages/desktop/src/main/runtime-manager')
|
||||
await ensureDesktopRuntime()
|
||||
|
||||
const runtimeRoot = join(
|
||||
process.env.HERMES_WEB_UI_HOME!,
|
||||
'desktop-runtime',
|
||||
'hermes',
|
||||
'0.15.2',
|
||||
runtimePlatformKey(),
|
||||
)
|
||||
expect(existsSync(staleDownloadPath)).toBe(true)
|
||||
expect(existsSync(join(runtimeRoot, 'runtime-manifest.json'))).toBe(true)
|
||||
expect(existsSync(join(process.env.HERMES_WEB_UI_HOME!, 'desktop-runtime', 'active-version.json'))).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,6 @@
|
||||
export const app = {
|
||||
isPackaged: false,
|
||||
getAppPath: () => process.cwd(),
|
||||
getVersion: () => '0.0.0',
|
||||
getLocale: () => 'en',
|
||||
}
|
||||
@@ -7,6 +7,7 @@ export default defineConfig({
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve(__dirname, 'packages/client/src'),
|
||||
electron: resolve(__dirname, 'tests/mocks/electron.ts'),
|
||||
'/logo.png': resolve(__dirname, 'packages/client/public/logo.png'),
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user