Skip to content

Commit d577eea

Browse files
authored
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.
1 parent 0ec878d commit d577eea

2 files changed

Lines changed: 23 additions & 2 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@pnpm/exec.commands": patch
3+
"pnpm": patch
4+
---
5+
6+
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.

pnpm11/exec/commands/src/dlx.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { createHexHash } from '@pnpm/crypto.hash'
1717
import { PnpmError } from '@pnpm/error'
1818
import { createResolver, makeResolutionStrict } from '@pnpm/installing.client'
1919
import { add } from '@pnpm/installing.commands'
20+
import { logger } from '@pnpm/logger'
2021
import { readPackageJsonFromDir } from '@pnpm/pkg-manifest.reader'
2122
import { parseWantedDependency } from '@pnpm/resolving.parse-wanted-dependency'
2223
import type { PackageManifest, PnpmSettings, SupportedArchitectures } from '@pnpm/types'
@@ -212,8 +213,22 @@ export async function handler (
212213
cachedDir = completedDir
213214
} else {
214215
// Drop the partially-populated cache so a subsequent dlx run starts
215-
// clean instead of reusing a broken install.
216-
await fs.promises.rm(cachedDir, { recursive: true, force: true })
216+
// clean instead of reusing a broken install. This is best-effort: on
217+
// Windows the just-run install scripts (or antivirus) can briefly hold
218+
// handles on freshly written files, so retry with backoff. A cleanup
219+
// failure must never mask the original install error, which is the one
220+
// worth surfacing — log it and rethrow err. A leftover prepare dir is
221+
// harmless: it has a unique name and findCache only trusts the `pkg`
222+
// symlink.
223+
try {
224+
await fs.promises.rm(cachedDir, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 })
225+
} catch (cleanupErr) {
226+
logger.warn({
227+
error: cleanupErr as Error,
228+
message: `Failed to clean up the dlx cache directory at "${cachedDir}"`,
229+
prefix: cachedDir,
230+
})
231+
}
217232
throw err
218233
}
219234
}

0 commit comments

Comments
 (0)