mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-26 02:04:37 +08:00
fix(cli): verify embedding runtime before atomic rename
This commit is contained in:
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)) {
|
||||
|
||||
Reference in new issue
Block a user