@mengruo/dsh-vision-toolkit 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.i18n.yaml +2 -2
- package/README.md +17 -42
- package/README.zh.md +16 -41
- package/assets/logo_eapi_dark.png +0 -0
- package/docs/aihubmix-gemini-vision.i18n.yaml +2 -2
- package/docs/aihubmix-gemini-vision.md +2 -2
- package/docs/aihubmix-gemini-vision.zh.md +2 -2
- package/lib/client.js +246 -68
- package/lib/client.js.map +1 -1
- package/lib/config.js +143 -24
- package/lib/config.js.map +1 -1
- package/lib/evidence-cache.js +14 -0
- package/lib/evidence-cache.js.map +1 -1
- package/lib/image-input-variants.js +39 -27
- package/lib/image-input-variants.js.map +1 -1
- package/lib/runtime-install.js +204 -19
- package/lib/runtime-install.js.map +1 -1
- package/lib/runtime.js +310 -154
- package/lib/runtime.js.map +1 -1
- package/lib/tools.js +1 -0
- package/lib/tools.js.map +1 -1
- package/lib/types/client/index.d.ts +45 -3
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/config.d.ts +53 -0
- package/lib/types/config.d.ts.map +1 -1
- package/lib/types/evidence-cache.d.ts.map +1 -1
- package/lib/types/image-input-variants.d.ts +1 -6
- package/lib/types/image-input-variants.d.ts.map +1 -1
- package/lib/types/runtime-install.d.ts +21 -0
- package/lib/types/runtime-install.d.ts.map +1 -1
- package/lib/types/runtime.d.ts +45 -10
- package/lib/types/runtime.d.ts.map +1 -1
- package/lib/types/tools.d.ts.map +1 -1
- package/lib/types/upstream.d.ts +3 -0
- package/lib/types/upstream.d.ts.map +1 -1
- package/lib/types/web.d.ts +8 -0
- package/lib/types/web.d.ts.map +1 -1
- package/lib/upstream.js +11 -6
- package/lib/upstream.js.map +1 -1
- package/lib/web.js +50 -7
- package/lib/web.js.map +1 -1
- package/package.json +1 -1
- package/src/client/index.tsx +331 -88
- package/src/config.ts +210 -28
- package/src/evidence-cache.ts +14 -0
- package/src/image-input-variants.ts +39 -27
- package/src/runtime-install.ts +231 -20
- package/src/runtime.ts +333 -180
- package/src/tools.ts +1 -0
- package/src/upstream.ts +15 -8
- package/src/web.ts +67 -8
package/src/runtime-install.ts
CHANGED
|
@@ -22,9 +22,9 @@ import {
|
|
|
22
22
|
utimes,
|
|
23
23
|
writeFile,
|
|
24
24
|
} from 'node:fs/promises'
|
|
25
|
-
import { createWriteStream } from 'node:fs'
|
|
25
|
+
import { createWriteStream, type Dirent } from 'node:fs'
|
|
26
26
|
import { homedir } from 'node:os'
|
|
27
|
-
import { dirname, join, resolve } from 'node:path'
|
|
27
|
+
import { basename, dirname, join, resolve } from 'node:path'
|
|
28
28
|
import { Transform } from 'node:stream'
|
|
29
29
|
import { pipeline } from 'node:stream/promises'
|
|
30
30
|
import { fileURLToPath } from 'node:url'
|
|
@@ -88,6 +88,9 @@ const PYPI_MIRROR_BASE_URL = 'https://mirrors.cloud.tencent.com/pypi/simple'
|
|
|
88
88
|
const PROBE_TIMEOUT_MS = 30_000
|
|
89
89
|
const LOCK_STALE_MS = 15 * 60 * 1000
|
|
90
90
|
const LOCK_HEARTBEAT_MS = 5_000
|
|
91
|
+
const WINDOWS_FILE_RETRY_ATTEMPTS = 5
|
|
92
|
+
const WINDOWS_FILE_RETRY_DELAY_MS = 250
|
|
93
|
+
const LEGACY_RUNTIME_GC_STALE_MS = 24 * 60 * 60 * 1000
|
|
91
94
|
|
|
92
95
|
/** Absolute root of the packaged upstream snapshot. */
|
|
93
96
|
export function bundledUpstreamRoot(): string {
|
|
@@ -103,6 +106,207 @@ function sha256(bytes: string | Buffer): string {
|
|
|
103
106
|
return createHash('sha256').update(bytes).digest('hex')
|
|
104
107
|
}
|
|
105
108
|
|
|
109
|
+
/**
|
|
110
|
+
* Windows Defender/antivirus real-time scanning briefly locks freshly written
|
|
111
|
+
* Python DLLs, so recursive removal and directory replacement can fail with
|
|
112
|
+
* EBUSY/EPERM immediately after installation. Retry those transient Windows
|
|
113
|
+
* errors before surfacing them; non-Windows platforms pass through unchanged.
|
|
114
|
+
*/
|
|
115
|
+
export async function withWindowsTransientRetry<T>(operation: () => Promise<T>): Promise<T> {
|
|
116
|
+
let lastError: unknown
|
|
117
|
+
for (let attempt = 1; attempt <= WINDOWS_FILE_RETRY_ATTEMPTS; attempt += 1) {
|
|
118
|
+
try {
|
|
119
|
+
return await operation()
|
|
120
|
+
} catch (error) {
|
|
121
|
+
lastError = error
|
|
122
|
+
const code = error instanceof Error ? (error as NodeJS.ErrnoException).code : undefined
|
|
123
|
+
const transient = process.platform === 'win32' && (code === 'EBUSY' || code === 'EPERM' || code === 'EACCES')
|
|
124
|
+
if (!transient) break
|
|
125
|
+
if (attempt < WINDOWS_FILE_RETRY_ATTEMPTS) {
|
|
126
|
+
await new Promise(resolveWait => setTimeout(resolveWait, WINDOWS_FILE_RETRY_DELAY_MS * attempt))
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
throw lastError
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Best-effort removal used after the primary runtime path has already
|
|
135
|
+
* succeeded or failed. Transient Windows locks must not turn a usable runtime
|
|
136
|
+
* into an error, but leaving the directory behind should still be audible.
|
|
137
|
+
*/
|
|
138
|
+
export async function ignoreCleanupFailure(ctx: Context, label: string, path: string): Promise<void> {
|
|
139
|
+
try {
|
|
140
|
+
await withWindowsTransientRetry(() => rm(path, { recursive: true, force: true }))
|
|
141
|
+
} catch (error) {
|
|
142
|
+
ctx.logger.warn(
|
|
143
|
+
'dsh-vision-toolkit: %s cleanup failed: %s',
|
|
144
|
+
label,
|
|
145
|
+
error instanceof Error ? error.message : String(error),
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
type RuntimeGarbageKind = 'managed runtime staging' | 'managed runtime quarantine' | 'bundled Python staging'
|
|
151
|
+
|
|
152
|
+
interface RuntimeGarbageCandidate {
|
|
153
|
+
kind: RuntimeGarbageKind
|
|
154
|
+
lockName?: string
|
|
155
|
+
lockToken?: string
|
|
156
|
+
minimumAgeMs?: number
|
|
157
|
+
createdAtMs?: number
|
|
158
|
+
observationMarker?: string
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function runtimeGarbageLockToken(lockBase: string): string {
|
|
162
|
+
return sha256(lockBase).slice(0, 12)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function managedRuntimeGarbage(name: string): RuntimeGarbageCandidate | undefined {
|
|
166
|
+
if (name.startsWith('.prepare-')) {
|
|
167
|
+
const current = /^\.prepare-([a-f0-9]{12})-[^/]{6}$/.exec(name)
|
|
168
|
+
return { kind: 'managed runtime staging', ...(current === null ? {} : { lockToken: current[1] }) }
|
|
169
|
+
}
|
|
170
|
+
const replaced = name.indexOf('.replaced-')
|
|
171
|
+
if (replaced > 0) {
|
|
172
|
+
const stamped = /^(\d{13})-/.exec(name.slice(replaced + '.replaced-'.length))
|
|
173
|
+
return {
|
|
174
|
+
kind: 'managed runtime quarantine',
|
|
175
|
+
lockName: `${name.slice(0, replaced)}.lock`,
|
|
176
|
+
minimumAgeMs: LEGACY_RUNTIME_GC_STALE_MS,
|
|
177
|
+
...(stamped === null
|
|
178
|
+
? { observationMarker: '.dsh-vision-toolkit-gc-observed' }
|
|
179
|
+
: { createdAtMs: Number(stamped[1]) }),
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return undefined
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function bundledPythonGarbage(name: string): RuntimeGarbageCandidate | undefined {
|
|
186
|
+
if (!name.startsWith('.python-bootstrap-')) return undefined
|
|
187
|
+
const current = /^\.python-bootstrap-([a-f0-9]{12})-[^/]{6}$/.exec(name)
|
|
188
|
+
return { kind: 'bundled Python staging', ...(current === null ? {} : { lockToken: current[1] }) }
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function runtimeLockIsActive(lockPath: string, now: number): Promise<boolean> {
|
|
192
|
+
try {
|
|
193
|
+
const info = await stat(lockPath)
|
|
194
|
+
return now - info.mtimeMs <= LOCK_STALE_MS
|
|
195
|
+
} catch (error) {
|
|
196
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return false
|
|
197
|
+
return true
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function garbageCollectDirectory(
|
|
202
|
+
ctx: Context,
|
|
203
|
+
parent: string,
|
|
204
|
+
classify: (name: string) => RuntimeGarbageCandidate | undefined,
|
|
205
|
+
now: number,
|
|
206
|
+
): Promise<void> {
|
|
207
|
+
let entries: Dirent[]
|
|
208
|
+
try {
|
|
209
|
+
entries = await readdir(parent, { withFileTypes: true })
|
|
210
|
+
} catch (error) {
|
|
211
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return
|
|
212
|
+
ctx.logger.warn(
|
|
213
|
+
'dsh-vision-toolkit: runtime garbage collection scan failed for %s: %s',
|
|
214
|
+
parent,
|
|
215
|
+
error instanceof Error ? error.message : String(error),
|
|
216
|
+
)
|
|
217
|
+
return
|
|
218
|
+
}
|
|
219
|
+
let activeLockTokens: Set<string> | undefined
|
|
220
|
+
const getActiveLockTokens = async (): Promise<Set<string>> => {
|
|
221
|
+
if (activeLockTokens !== undefined) return activeLockTokens
|
|
222
|
+
activeLockTokens = new Set<string>()
|
|
223
|
+
for (const lock of entries) {
|
|
224
|
+
if (
|
|
225
|
+
lock.isDirectory()
|
|
226
|
+
&& lock.name.endsWith('.lock')
|
|
227
|
+
&& await runtimeLockIsActive(join(parent, lock.name), now)
|
|
228
|
+
) {
|
|
229
|
+
activeLockTokens.add(runtimeGarbageLockToken(lock.name.slice(0, -'.lock'.length)))
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return activeLockTokens
|
|
233
|
+
}
|
|
234
|
+
let legacyCollectionSafe: boolean | undefined
|
|
235
|
+
for (const entry of entries) {
|
|
236
|
+
if (!entry.isDirectory()) continue
|
|
237
|
+
const candidate = classify(entry.name)
|
|
238
|
+
if (candidate === undefined) continue
|
|
239
|
+
const path = join(parent, entry.name)
|
|
240
|
+
if (candidate.lockName !== undefined) {
|
|
241
|
+
if (await runtimeLockIsActive(join(parent, candidate.lockName), now)) continue
|
|
242
|
+
} else if (candidate.lockToken !== undefined) {
|
|
243
|
+
if ((await getActiveLockTokens()).has(candidate.lockToken)) continue
|
|
244
|
+
} else {
|
|
245
|
+
if (legacyCollectionSafe === undefined) {
|
|
246
|
+
legacyCollectionSafe = (await getActiveLockTokens()).size === 0
|
|
247
|
+
}
|
|
248
|
+
if (!legacyCollectionSafe) continue
|
|
249
|
+
try {
|
|
250
|
+
const info = await stat(path)
|
|
251
|
+
if (now - info.mtimeMs <= LEGACY_RUNTIME_GC_STALE_MS) continue
|
|
252
|
+
} catch (error) {
|
|
253
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') continue
|
|
254
|
+
ctx.logger.warn(
|
|
255
|
+
'dsh-vision-toolkit: runtime garbage collection inspection failed for %s: %s',
|
|
256
|
+
path,
|
|
257
|
+
error instanceof Error ? error.message : String(error),
|
|
258
|
+
)
|
|
259
|
+
continue
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
if (candidate.minimumAgeMs !== undefined) {
|
|
263
|
+
let createdAt = candidate.createdAtMs
|
|
264
|
+
if (createdAt === undefined && candidate.observationMarker !== undefined) {
|
|
265
|
+
const marker = join(path, candidate.observationMarker)
|
|
266
|
+
try {
|
|
267
|
+
createdAt = (await stat(marker)).mtimeMs
|
|
268
|
+
} catch (error) {
|
|
269
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
270
|
+
await writeFile(marker, `${now}\n`, { flag: 'wx' }).catch(() => {})
|
|
271
|
+
}
|
|
272
|
+
continue
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (createdAt === undefined) {
|
|
276
|
+
try {
|
|
277
|
+
createdAt = (await stat(path)).mtimeMs
|
|
278
|
+
} catch (error) {
|
|
279
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') continue
|
|
280
|
+
ctx.logger.warn(
|
|
281
|
+
'dsh-vision-toolkit: runtime garbage collection inspection failed for %s: %s',
|
|
282
|
+
path,
|
|
283
|
+
error instanceof Error ? error.message : String(error),
|
|
284
|
+
)
|
|
285
|
+
continue
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
if (now - createdAt <= candidate.minimumAgeMs) continue
|
|
289
|
+
}
|
|
290
|
+
await ignoreCleanupFailure(ctx, `stale ${candidate.kind}`, path)
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Opportunistically remove abandoned runtime staging and quarantine trees.
|
|
296
|
+
* Current names encode their owning runtime lock, so live preparation is
|
|
297
|
+
* skipped. Quarantines retain a 24-hour recovery window; legacy names use the
|
|
298
|
+
* same grace period and are collected only when no runtime lock is active
|
|
299
|
+
* because they cannot be associated with a specific lock.
|
|
300
|
+
*/
|
|
301
|
+
export async function garbageCollectRuntimeCache(
|
|
302
|
+
ctx: Context,
|
|
303
|
+
stateRoot: string,
|
|
304
|
+
now: number = Date.now(),
|
|
305
|
+
): Promise<void> {
|
|
306
|
+
await garbageCollectDirectory(ctx, join(stateRoot, 'python'), managedRuntimeGarbage, now)
|
|
307
|
+
await garbageCollectDirectory(ctx, join(stateRoot, 'python-bootstrap'), bundledPythonGarbage, now)
|
|
308
|
+
}
|
|
309
|
+
|
|
106
310
|
export function isolatedPythonEnvironment(home: string): NodeJS.ProcessEnv {
|
|
107
311
|
return {
|
|
108
312
|
HOME: home,
|
|
@@ -488,7 +692,7 @@ async function withDirectoryLock<T>(lockPath: string, fn: () => Promise<T>): Pro
|
|
|
488
692
|
await writeFile(join(lockPath, 'owner'), `${owner}\n`, { flag: 'wx' })
|
|
489
693
|
} catch (error) {
|
|
490
694
|
if (acquired) {
|
|
491
|
-
await rm(lockPath, { recursive: true, force: true }).catch(() => {})
|
|
695
|
+
await withWindowsTransientRetry(() => rm(lockPath, { recursive: true, force: true })).catch(() => {})
|
|
492
696
|
throw error
|
|
493
697
|
}
|
|
494
698
|
if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error
|
|
@@ -497,7 +701,7 @@ async function withDirectoryLock<T>(lockPath: string, fn: () => Promise<T>): Pro
|
|
|
497
701
|
try {
|
|
498
702
|
const info = await stat(lockPath)
|
|
499
703
|
if (Date.now() - info.mtimeMs > LOCK_STALE_MS) {
|
|
500
|
-
await rm(lockPath, { recursive: true, force: true })
|
|
704
|
+
await withWindowsTransientRetry(() => rm(lockPath, { recursive: true, force: true }))
|
|
501
705
|
return withDirectoryLock(lockPath, fn)
|
|
502
706
|
}
|
|
503
707
|
} catch {
|
|
@@ -518,7 +722,7 @@ async function withDirectoryLock<T>(lockPath: string, fn: () => Promise<T>): Pro
|
|
|
518
722
|
clearInterval(heartbeat)
|
|
519
723
|
try {
|
|
520
724
|
if ((await readFile(join(lockPath, 'owner'), 'utf8')).trim() === owner) {
|
|
521
|
-
await rm(lockPath, { recursive: true, force: true })
|
|
725
|
+
await withWindowsTransientRetry(() => rm(lockPath, { recursive: true, force: true }))
|
|
522
726
|
}
|
|
523
727
|
} catch {
|
|
524
728
|
// The lock was already removed or replaced.
|
|
@@ -552,8 +756,8 @@ export async function acquireBundledPython(
|
|
|
552
756
|
if (ready !== undefined) return
|
|
553
757
|
const parent = dirname(root)
|
|
554
758
|
await mkdir(parent, { recursive: true })
|
|
555
|
-
await rm(root, { recursive: true, force: true })
|
|
556
|
-
const work = await mkdtemp(join(parent,
|
|
759
|
+
await withWindowsTransientRetry(() => rm(root, { recursive: true, force: true }))
|
|
760
|
+
const work = await mkdtemp(join(parent, `.python-bootstrap-${runtimeGarbageLockToken(basename(root))}-`))
|
|
557
761
|
try {
|
|
558
762
|
const archive = join(work, 'python.tar.gz')
|
|
559
763
|
const extractDir = join(work, 'extract')
|
|
@@ -589,9 +793,9 @@ export async function acquireBundledPython(
|
|
|
589
793
|
)
|
|
590
794
|
}
|
|
591
795
|
if (process.platform !== 'win32') await chmod(extractedInterpreter, 0o755)
|
|
592
|
-
await rename(extractDir, root)
|
|
796
|
+
await withWindowsTransientRetry(() => rename(extractDir, root))
|
|
593
797
|
} finally {
|
|
594
|
-
await
|
|
798
|
+
await ignoreCleanupFailure(ctx, 'bundled Python staging', work)
|
|
595
799
|
}
|
|
596
800
|
})
|
|
597
801
|
const metadata = await pythonMetadata(ctx, command, cwd)
|
|
@@ -632,6 +836,7 @@ export async function resolveBootstrapPython(
|
|
|
632
836
|
try {
|
|
633
837
|
const stateRoot = visionToolkitStateRoot()
|
|
634
838
|
await mkdir(stateRoot, { recursive: true })
|
|
839
|
+
await garbageCollectRuntimeCache(ctx, stateRoot)
|
|
635
840
|
const bundled = await acquireBundledPython(ctx, stateRoot, cwd, manifestOverride, requestImpl)
|
|
636
841
|
return {
|
|
637
842
|
command: bundled.command,
|
|
@@ -827,7 +1032,7 @@ async function waitForManagedRuntime(
|
|
|
827
1032
|
try {
|
|
828
1033
|
const info = await stat(lockPath)
|
|
829
1034
|
if (Date.now() - info.mtimeMs > LOCK_STALE_MS) {
|
|
830
|
-
await rm(lockPath, { recursive: true, force: true })
|
|
1035
|
+
await withWindowsTransientRetry(() => rm(lockPath, { recursive: true, force: true }))
|
|
831
1036
|
return undefined
|
|
832
1037
|
}
|
|
833
1038
|
} catch {
|
|
@@ -844,7 +1049,7 @@ async function releaseManagedLock(lockPath: string, owner: string): Promise<void
|
|
|
844
1049
|
} catch {
|
|
845
1050
|
return
|
|
846
1051
|
}
|
|
847
|
-
await rm(lockPath, { recursive: true, force: true })
|
|
1052
|
+
await withWindowsTransientRetry(() => rm(lockPath, { recursive: true, force: true }))
|
|
848
1053
|
}
|
|
849
1054
|
|
|
850
1055
|
async function prepareManaged(
|
|
@@ -854,6 +1059,7 @@ async function prepareManaged(
|
|
|
854
1059
|
): Promise<PreparedUpstreamRuntime> {
|
|
855
1060
|
const stateRoot = visionToolkitStateRoot()
|
|
856
1061
|
await mkdir(stateRoot, { recursive: true })
|
|
1062
|
+
await garbageCollectRuntimeCache(ctx, stateRoot)
|
|
857
1063
|
const cleanHome = join(stateRoot, 'home')
|
|
858
1064
|
await mkdir(cleanHome, { recursive: true })
|
|
859
1065
|
const bootstrap = await resolveBootstrapPython(ctx, config.runtime.python, cleanHome)
|
|
@@ -899,7 +1105,7 @@ async function prepareManaged(
|
|
|
899
1105
|
await writeFile(join(lockPath, 'owner'), `${lockOwner}\n`, { flag: 'wx' })
|
|
900
1106
|
} catch (error) {
|
|
901
1107
|
if (lockAcquired) {
|
|
902
|
-
await rm(lockPath, { recursive: true, force: true })
|
|
1108
|
+
await withWindowsTransientRetry(() => rm(lockPath, { recursive: true, force: true })).catch(() => {})
|
|
903
1109
|
throw error
|
|
904
1110
|
}
|
|
905
1111
|
if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error
|
|
@@ -923,7 +1129,7 @@ async function prepareManaged(
|
|
|
923
1129
|
return prepareManaged(ctx, config, manifest)
|
|
924
1130
|
}
|
|
925
1131
|
|
|
926
|
-
const staging = await mkdtemp(join(parent,
|
|
1132
|
+
const staging = await mkdtemp(join(parent, `.prepare-${runtimeGarbageLockToken(runtimeId)}-`))
|
|
927
1133
|
const installEnv: NodeJS.ProcessEnv = {
|
|
928
1134
|
...isolatedPythonEnvironment(cleanHome),
|
|
929
1135
|
UV_CACHE_DIR: join(stateRoot, 'uv-cache'),
|
|
@@ -1013,20 +1219,20 @@ async function prepareManaged(
|
|
|
1013
1219
|
manager,
|
|
1014
1220
|
}
|
|
1015
1221
|
await writeFile(join(staging, 'runtime.json'), `${JSON.stringify(marker, null, 2)}\n`)
|
|
1016
|
-
const quarantine = `${finalRoot}.replaced-${randomUUID()}`
|
|
1222
|
+
const quarantine = `${finalRoot}.replaced-${Date.now()}-${randomUUID()}`
|
|
1017
1223
|
let quarantined = false
|
|
1018
1224
|
try {
|
|
1019
|
-
await rename(finalRoot, quarantine)
|
|
1225
|
+
await withWindowsTransientRetry(() => rename(finalRoot, quarantine))
|
|
1020
1226
|
quarantined = true
|
|
1021
1227
|
} catch (error) {
|
|
1022
1228
|
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
|
|
1023
1229
|
}
|
|
1024
1230
|
try {
|
|
1025
|
-
await rename(staging, finalRoot)
|
|
1231
|
+
await withWindowsTransientRetry(() => rename(staging, finalRoot))
|
|
1026
1232
|
} catch (error) {
|
|
1027
1233
|
if (quarantined) {
|
|
1028
1234
|
try {
|
|
1029
|
-
await rename(quarantine, finalRoot)
|
|
1235
|
+
await withWindowsTransientRetry(() => rename(quarantine, finalRoot))
|
|
1030
1236
|
} catch (restoreError) {
|
|
1031
1237
|
throw new VisionToolkitError(
|
|
1032
1238
|
'runtime',
|
|
@@ -1037,13 +1243,18 @@ async function prepareManaged(
|
|
|
1037
1243
|
}
|
|
1038
1244
|
throw error
|
|
1039
1245
|
}
|
|
1040
|
-
await
|
|
1246
|
+
await ignoreCleanupFailure(ctx, 'managed runtime quarantine', quarantine)
|
|
1041
1247
|
const python: RuntimeCommand = { program: interpreter, prefix: [], display: interpreter }
|
|
1042
1248
|
return { source: 'managed', root: BUNDLED_ROOT, python, cleanHome, pythonVersion: metadata.version, dependencies }
|
|
1043
1249
|
} finally {
|
|
1044
1250
|
clearInterval(heartbeat)
|
|
1045
|
-
await
|
|
1046
|
-
await releaseManagedLock(lockPath, lockOwner)
|
|
1251
|
+
await ignoreCleanupFailure(ctx, 'managed runtime staging', staging)
|
|
1252
|
+
await releaseManagedLock(lockPath, lockOwner).catch(error => {
|
|
1253
|
+
ctx.logger.warn(
|
|
1254
|
+
'dsh-vision-toolkit: managed runtime lock cleanup failed: %s',
|
|
1255
|
+
error instanceof Error ? error.message : String(error),
|
|
1256
|
+
)
|
|
1257
|
+
})
|
|
1047
1258
|
}
|
|
1048
1259
|
}
|
|
1049
1260
|
|