fix(cli): verify embedding runtime before atomic rename

This commit is contained in:
digua committed 2026-08-01 13:19:41 +08:00
1 parent f96fef7b46
commit 39644b2507
2 files changed
+94 -4

No files matched your search

@@ -15,6 +15,9 @@ function setupRuntimeTest(t: TestContext, overrides: Partial<LocalEmbeddingRunti
t.after(() => fs.rmSync(root, { recursive: true, force: true }))
const installs: Array<{ command: string; args: string[]; cwd: string }> = []
let verifies = 0
const verifyRuntime = async () => {
verifies++
}
const manager = createLocalEmbeddingRuntimeManager({
baseDir: root,
installMode: 'auto',
@@ -26,9 +29,8 @@ function setupRuntimeTest(t: TestContext, overrides: Partial<LocalEmbeddingRunti
fs.mkdirSync(packageDir, { recursive: true })
fs.writeFileSync(path.join(packageDir, 'package.json'), '{}\n')
},
verifyRuntime: async () => {
verifies++
},
verifyStagedRuntime: verifyRuntime,
verifyRuntime,
...overrides,
})
return { root, manager, installs, getVerifyCount: () => verifies }
@@ -95,6 +97,43 @@ test('cleans a failed staging install and retries on the next request', async (t
assert.equal(fixture.manager.getStatus(), 'ready')
})
test('verifies the staged native runtime in a disposable subprocess before renaming', async (t) => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chatlab-local-runtime-subprocess-'))
t.after(() => fs.rmSync(root, { recursive: true, force: true }))
const verifierPidPath = path.join(root, 'verifier.pid')
const manager = createLocalEmbeddingRuntimeManager({
baseDir: root,
installMode: 'auto',
platform: 'test-platform',
arch: 'test-arch',
runInstall: async (input) => {
const transformersDir = path.join(input.cwd, 'node_modules', '@huggingface', 'transformers')
const onnxDir = path.join(input.cwd, 'node_modules', 'onnxruntime-node')
fs.mkdirSync(transformersDir, { recursive: true })
fs.mkdirSync(onnxDir, { recursive: true })
fs.writeFileSync(
path.join(transformersDir, 'package.json'),
`${JSON.stringify({ type: 'module', exports: './index.js' })}\n`
)
fs.writeFileSync(
path.join(transformersDir, 'index.js'),
`import fs from 'node:fs'\nfs.writeFileSync(${JSON.stringify(verifierPidPath)}, String(process.pid))\nexport const pipeline = () => {}\nexport const env = {}\n`
)
fs.writeFileSync(
path.join(onnxDir, 'package.json'),
`${JSON.stringify({ type: 'module', exports: './index.js' })}\n`
)
fs.writeFileSync(path.join(onnxDir, 'index.js'), 'export const listSupportedBackends = () => []\n')
},
verifyRuntime: async () => {},
})
const runtimeDir = await manager.ensureInstalled()
assert.notEqual(Number(fs.readFileSync(verifierPidPath, 'utf8')), process.pid)
assert.equal(fs.existsSync(runtimeDir), true)
})
test('preinstalled mode fails clearly instead of invoking npm', async (t) => {
let installCalled = false
const fixture = setupRuntimeTest(t, {
@@ -120,6 +159,10 @@ test('repairs an invalid managed runtime instead of keeping a broken install', a
installMode: 'auto',
platform: 'test-platform',
arch: 'test-arch',
verifyStagedRuntime: async () => {
verifyAttempts++
if (verifyAttempts === 1) throw new Error('corrupt native runtime')
},
verifyRuntime: async () => {
verifyAttempts++
if (verifyAttempts === 1) throw new Error('corrupt native runtime')
@@ -35,12 +35,36 @@ export interface LocalEmbeddingRuntimeManagerOptions extends LocalEmbeddingRunti
arch?: string
npmCommand?: string
runInstall?: (input: InstallCommand) => Promise<void>
verifyStagedRuntime?: (runtimeDir: string) => Promise<void>
verifyRuntime?: (runtimeDir: string) => Promise<void>
importTransformers?: (runtimeDir: string) => Promise<typeof import('@huggingface/transformers')>
}
const RUNTIME_MANIFEST_FILE = 'runtime.json'
const VERIFY_RUNTIME_SCRIPT = `
import { createRequire } from 'node:module'
import path from 'node:path'
import { pathToFileURL } from 'node:url'
const [, runtimeDir, transformersPackage] = process.argv
const runtimeRequire = createRequire(path.join(runtimeDir, 'package.json'))
const importFromRuntime = async (packageName) => {
const resolved = runtimeRequire.resolve(packageName)
return await import(pathToFileURL(resolved).href)
}
const transformers = await importFromRuntime(transformersPackage)
if (typeof transformers.pipeline !== 'function' || typeof transformers.env !== 'object') {
throw new Error('Installed Transformers runtime does not expose the expected Node API.')
}
const onnxRuntime = await importFromRuntime('onnxruntime-node')
if (typeof onnxRuntime.listSupportedBackends !== 'function') {
throw new Error('Installed ONNX runtime does not expose listSupportedBackends().')
}
`
function executeInstall(input: InstallCommand): Promise<void> {
return new Promise((resolve, reject) => {
execFile(
@@ -63,6 +87,26 @@ function executeInstall(input: InstallCommand): Promise<void> {
})
}
function verifyRuntimeInSubprocess(runtimeDir: string): Promise<void> {
return new Promise((resolve, reject) => {
execFile(
process.execPath,
['--input-type=module', '--eval', VERIFY_RUNTIME_SCRIPT, runtimeDir, LOCAL_EMBEDDING_RUNTIME_PACKAGE],
{
env: { ...process.env, ELECTRON_RUN_AS_NODE: '1' },
maxBuffer: 10 * 1024 * 1024,
},
(error, _stdout, stderr) => {
if (!error) {
resolve()
return
}
reject(new Error(stderr.trim() || error.message, { cause: error }))
}
)
})
}
function createRuntimeRequire(runtimeDir: string): NodeJS.Require {
return createRequire(path.join(runtimeDir, 'package.json'))
}
@@ -98,6 +142,7 @@ export class LocalEmbeddingRuntimeManager {
private readonly arch: string
private readonly npmCommand: string
private readonly runInstall: (input: InstallCommand) => Promise<void>
private readonly verifyStagedRuntime: (runtimeDir: string) => Promise<void>
private readonly verifyRuntime: (runtimeDir: string) => Promise<void>
private readonly importTransformers: (runtimeDir: string) => Promise<typeof import('@huggingface/transformers')>
@@ -113,6 +158,7 @@ export class LocalEmbeddingRuntimeManager {
this.arch = options.arch ?? process.arch
this.npmCommand = options.npmCommand ?? (this.platform === 'win32' ? 'npm.cmd' : 'npm')
this.runInstall = options.runInstall ?? executeInstall
this.verifyStagedRuntime = options.verifyStagedRuntime ?? verifyRuntimeInSubprocess
this.verifyRuntime = options.verifyRuntime ?? verifyInstalledRuntime
this.importTransformers = options.importTransformers ?? loadInstalledTransformers
this.status = 'idle'
@@ -221,7 +267,8 @@ export class LocalEmbeddingRuntimeManager {
],
cwd: stagingDir,
})
await this.verifyRuntime(stagingDir)
// Keep native modules in a disposable process so Windows releases DLL handles before the directory rename.
await this.verifyStagedRuntime(stagingDir)
fs.writeFileSync(path.join(stagingDir, RUNTIME_MANIFEST_FILE), `${JSON.stringify(this.manifest(), null, 2)}\n`)
if (fs.existsSync(runtimeDir)) {