mirror of
https://github.com/pnpm/pnpm.git
synced 2026-06-27 17:35:30 -04:00
fix(dlx): make failed-install cache cleanup best-effort on Windows (#12575)
* fix(dlx): make failed-install cache cleanup best-effort
On Windows, `pnpm dlx` could fail with a spurious "EBUSY: resource busy
or locked, rmdir" error. When an install into the dlx cache failed, the
catch block removed the partially-populated prepare dir with
`fs.promises.rm(cachedDir, { recursive: true, force: true })`. That call
has no retries, so it died on the same lingering Windows handle (a
just-run install script's child process, or antivirus scanning freshly
written files) and threw EBUSY — which then replaced and masked the
original install error.
Make the cleanup best-effort: swallow its failure so the original error
always surfaces, and add `maxRetries`/`retryDelay` so the removal itself
succeeds once the transient lock clears. A leftover prepare dir is
harmless — it has a unique name and findCache only trusts the `pkg`
symlink.
The pacquet port already removes the prepare dir best-effort (`let _ =
fs::remove_dir_all(...)`) and returns the original error, so the
user-visible behavior already matches; only the (non-observable) retry
is absent there.
* fix(dlx): log dlx cache cleanup failures instead of swallowing them
Catch the best-effort cache cleanup with a narrow handler that logs the
failure via logger.warn (mirroring tryRemovePkg in modules-cleaner's
prune) instead of a blanket `.catch(() => {})`. The original install
error is still the one rethrown, so cleanup failures stay visible without
ever masking the real cause.
This commit is contained in:
6
.changeset/dlx-resilient-cache-cleanup.md
Normal file
6
.changeset/dlx-resilient-cache-cleanup.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@pnpm/exec.commands": patch
|
||||
"pnpm": patch
|
||||
---
|
||||
|
||||
Fixed a Windows flakiness in `pnpm dlx` where a failed install could surface a spurious `EBUSY: resource busy or locked` error. The cleanup of a partially-populated dlx cache is now best-effort with retries and no longer masks the original error.
|
||||
@@ -17,6 +17,7 @@ import { createHexHash } from '@pnpm/crypto.hash'
|
||||
import { PnpmError } from '@pnpm/error'
|
||||
import { createResolver, makeResolutionStrict } from '@pnpm/installing.client'
|
||||
import { add } from '@pnpm/installing.commands'
|
||||
import { logger } from '@pnpm/logger'
|
||||
import { readPackageJsonFromDir } from '@pnpm/pkg-manifest.reader'
|
||||
import { parseWantedDependency } from '@pnpm/resolving.parse-wanted-dependency'
|
||||
import type { PackageManifest, PnpmSettings, SupportedArchitectures } from '@pnpm/types'
|
||||
@@ -212,8 +213,22 @@ export async function handler (
|
||||
cachedDir = completedDir
|
||||
} else {
|
||||
// Drop the partially-populated cache so a subsequent dlx run starts
|
||||
// clean instead of reusing a broken install.
|
||||
await fs.promises.rm(cachedDir, { recursive: true, force: true })
|
||||
// clean instead of reusing a broken install. This is best-effort: on
|
||||
// Windows the just-run install scripts (or antivirus) can briefly hold
|
||||
// handles on freshly written files, so retry with backoff. A cleanup
|
||||
// failure must never mask the original install error, which is the one
|
||||
// worth surfacing — log it and rethrow err. A leftover prepare dir is
|
||||
// harmless: it has a unique name and findCache only trusts the `pkg`
|
||||
// symlink.
|
||||
try {
|
||||
await fs.promises.rm(cachedDir, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 })
|
||||
} catch (cleanupErr) {
|
||||
logger.warn({
|
||||
error: cleanupErr as Error,
|
||||
message: `Failed to clean up the dlx cache directory at "${cachedDir}"`,
|
||||
prefix: cachedDir,
|
||||
})
|
||||
}
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user