From d577eeaf769e881e23f1336ad2f5132941645119 Mon Sep 17 00:00:00 2001 From: Zoltan Kochan Date: Mon, 22 Jun 2026 15:13:10 +0200 Subject: [PATCH] fix(dlx): make failed-install cache cleanup best-effort on Windows (#12575) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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. --- .changeset/dlx-resilient-cache-cleanup.md | 6 ++++++ pnpm11/exec/commands/src/dlx.ts | 19 +++++++++++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 .changeset/dlx-resilient-cache-cleanup.md diff --git a/.changeset/dlx-resilient-cache-cleanup.md b/.changeset/dlx-resilient-cache-cleanup.md new file mode 100644 index 0000000000..579e019a63 --- /dev/null +++ b/.changeset/dlx-resilient-cache-cleanup.md @@ -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. diff --git a/pnpm11/exec/commands/src/dlx.ts b/pnpm11/exec/commands/src/dlx.ts index 481755deec..bc2f011fc1 100644 --- a/pnpm11/exec/commands/src/dlx.ts +++ b/pnpm11/exec/commands/src/dlx.ts @@ -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 } }