mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-11 16:30:57 +00:00
Fixes #11009 ## Summary On Windows, Chromium may still hold file handles on the user-data directory when global teardown runs `restorePath`. The `fs.moveSync(..., { overwrite: true })` call fails with EPERM because it can't remove the target while handles are held. ## Changes - Split `restorePath` into explicit remove-then-move - Added `removeWithRetry` that retries up to 3× on EPERM/EBUSY with 500ms delay between attempts - Downgraded the catch from `console.error` (which looks like a test failure) to `console.warn` so teardown noise doesn't mask real failures No E2E regression test added: this is a test-infrastructure fix for a Windows-specific race condition in teardown that cannot be reliably reproduced in CI. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-11013-fix-handle-EPERM-EBUSY-in-global-teardown-restorePath-33e6d73d3650815ebe0cd42af23e6c0e) by [Unito](https://www.unito.io)
93 lines
2.7 KiB
TypeScript
93 lines
2.7 KiB
TypeScript
import fs from 'fs-extra'
|
|
import path from 'path'
|
|
|
|
type PathParts = readonly [string, ...string[]]
|
|
|
|
const getBackupPath = (originalPath: string): string => `${originalPath}.bak`
|
|
|
|
const resolvePathIfExists = (pathParts: PathParts): string | null => {
|
|
const resolvedPath = path.resolve(...pathParts)
|
|
if (!fs.pathExistsSync(resolvedPath)) {
|
|
console.warn(`Path not found: ${resolvedPath}`)
|
|
return null
|
|
}
|
|
return resolvedPath
|
|
}
|
|
|
|
const createScaffoldingCopy = (srcDir: string, destDir: string) => {
|
|
// Get all items (files and directories) in the source directory
|
|
const items = fs.readdirSync(srcDir, { withFileTypes: true })
|
|
|
|
for (const item of items) {
|
|
const srcPath = path.join(srcDir, item.name)
|
|
const destPath = path.join(destDir, item.name)
|
|
|
|
if (item.isDirectory()) {
|
|
// Create the corresponding directory in the destination
|
|
fs.ensureDirSync(destPath)
|
|
|
|
// Recursively copy the directory structure
|
|
createScaffoldingCopy(srcPath, destPath)
|
|
}
|
|
}
|
|
}
|
|
|
|
export function backupPath(
|
|
pathParts: PathParts,
|
|
{ renameAndReplaceWithScaffolding = false } = {}
|
|
) {
|
|
const originalPath = resolvePathIfExists(pathParts)
|
|
if (!originalPath) return
|
|
|
|
const backupPath = getBackupPath(originalPath)
|
|
try {
|
|
if (renameAndReplaceWithScaffolding) {
|
|
// Rename the original path and create scaffolding in its place
|
|
fs.moveSync(originalPath, backupPath)
|
|
createScaffoldingCopy(backupPath, originalPath)
|
|
} else {
|
|
// Create a copy of the original path
|
|
fs.copySync(originalPath, backupPath)
|
|
}
|
|
} catch (error) {
|
|
console.error(`Failed to backup ${originalPath} from ${backupPath}`, error)
|
|
}
|
|
}
|
|
|
|
function removeWithRetry(targetPath: string, retries = 3, delayMs = 500) {
|
|
for (let attempt = 1; attempt <= retries; attempt++) {
|
|
try {
|
|
fs.removeSync(targetPath)
|
|
return
|
|
} catch (error: unknown) {
|
|
const code = (error as NodeJS.ErrnoException).code
|
|
if ((code === 'EPERM' || code === 'EBUSY') && attempt < retries) {
|
|
console.warn(
|
|
`Retry ${attempt}/${retries}: ${code} removing ${targetPath}`
|
|
)
|
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, delayMs)
|
|
continue
|
|
}
|
|
throw error
|
|
}
|
|
}
|
|
}
|
|
|
|
export function restorePath(pathParts: PathParts) {
|
|
const originalPath = resolvePathIfExists(pathParts)
|
|
if (!originalPath) return
|
|
|
|
const backupPath = getBackupPath(originalPath)
|
|
if (!fs.pathExistsSync(backupPath)) return
|
|
|
|
try {
|
|
removeWithRetry(originalPath)
|
|
fs.moveSync(backupPath, originalPath)
|
|
} catch (error) {
|
|
console.warn(
|
|
`Could not fully restore ${originalPath} from ${backupPath}:`,
|
|
(error as NodeJS.ErrnoException).message
|
|
)
|
|
}
|
|
}
|