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:
Zoltan Kochan
2026-06-22 15:13:10 +02:00
committed by GitHub
parent 0ec878d829
commit d577eeaf76
2 changed files with 23 additions and 2 deletions

View 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.

View File

@@ -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
}
}