@pnpm/fs.graceful-fs 1100.2.1 → 1100.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # @pnpm/fs.graceful-fs
2
2
 
3
+ ## 1100.2.3
4
+
5
+ ### Patch Changes
6
+
7
+ - `pnpm install` in WSL now waits out Windows file locks on a Windows drive such as `/mnt/c`, as it already does on Windows. Before, an antivirus or indexer scan holding a file open could fail the install with `EACCES` [pnpm/pnpm#6155](https://github.com/pnpm/pnpm/issues/6155).
8
+
9
+ - On Windows, pnpm now retries saving `pnpm-lock.yaml` for up to a minute while another process holds the file open. The save used to fail at once with `EPERM`, `EBUSY`, or "Access is denied" [#9461](https://github.com/pnpm/pnpm/issues/9461).
10
+
11
+ ## 1100.2.2
12
+
13
+ ### Patch Changes
14
+
15
+ - Installs in different projects that share a global virtual store no longer fail on Windows with `Access is denied` while repairing the same slot [#15114](https://github.com/pnpm/pnpm/issues/15114).
16
+
3
17
  ## 1100.2.1
4
18
 
5
19
  ### Patch Changes
package/lib/index.d.ts CHANGED
@@ -22,6 +22,7 @@ declare const _default: {
22
22
  export default _default;
23
23
  /**
24
24
  * Renames `src` over `dest`, retrying Windows EBUSY errors for up to a minute.
25
+ * Windows drives mounted into WSL get the same retries.
25
26
  * EPERM and EACCES have a one-second budget because they can also indicate
26
27
  * permanent permission or destination conflicts. Other errors are thrown
27
28
  * right away.
@@ -31,3 +32,27 @@ export default _default;
31
32
  * that was there or the whole file replacing it.
32
33
  */
33
34
  export declare function renameFileWithRetry(src: string, dest: string): void;
35
+ /**
36
+ * Asynchronous {@link renameFileWithRetry}, which waits between attempts
37
+ * without blocking the event loop.
38
+ */
39
+ export declare function renameFileWithRetryAsync(src: string, dest: string): Promise<void>;
40
+ /**
41
+ * Reads `target`'s stats without following it, with the retry policy of
42
+ * {@link renameFileWithRetry}.
43
+ *
44
+ * A Windows path another process has just unlinked stays delete-pending until
45
+ * the last handle on it closes, and inspecting it fails with EPERM for as long
46
+ * as that lasts. Retrying lets the unlink land, so a caller that reads ENOENT
47
+ * as an absent entry sees the same absence POSIX shows it at once.
48
+ */
49
+ export declare function lstatWithRetry(target: string): fs.Stats;
50
+ /**
51
+ * Removes a file, with the retry policy of {@link renameFileWithRetry}.
52
+ */
53
+ export declare function unlinkWithRetry(target: string): void;
54
+ /**
55
+ * Runs a filesystem operation with the retry policy of
56
+ * {@link renameFileWithRetry}.
57
+ */
58
+ export declare function withFileLockRetry<T>(operation: () => T): T;
package/lib/index.js CHANGED
@@ -1,10 +1,11 @@
1
1
  import fs from 'node:fs';
2
+ import os from 'node:os';
2
3
  import util, { promisify } from 'node:util';
3
4
  import gfs from 'graceful-fs';
4
- const RENAME_RETRY_BUDGET_MS = 60_000;
5
+ const FILE_LOCK_RETRY_BUDGET_MS = 60_000;
5
6
  const PERMISSION_DENIED_RETRY_BUDGET_MS = 1_000;
6
- const RENAME_RETRY_BACKOFF_CAP_MS = 100;
7
- const renameRetrySleepBuffer = new Int32Array(new SharedArrayBuffer(4));
7
+ const FILE_LOCK_RETRY_BACKOFF_CAP_MS = 100;
8
+ const fileLockRetrySleepBuffer = new Int32Array(new SharedArrayBuffer(4));
8
9
  export default {
9
10
  chmod: promisify(gfs.chmod),
10
11
  copyFile: promisify(gfs.copyFile),
@@ -48,6 +49,7 @@ function withEagainRetry(fn, maxRetries = 15) {
48
49
  }
49
50
  /**
50
51
  * Renames `src` over `dest`, retrying Windows EBUSY errors for up to a minute.
52
+ * Windows drives mounted into WSL get the same retries.
51
53
  * EPERM and EACCES have a one-second budget because they can also indicate
52
54
  * permanent permission or destination conflicts. Other errors are thrown
53
55
  * right away.
@@ -57,34 +59,104 @@ function withEagainRetry(fn, maxRetries = 15) {
57
59
  * that was there or the whole file replacing it.
58
60
  */
59
61
  export function renameFileWithRetry(src, dest) {
60
- const startedAt = Date.now();
61
- let backoffMs = 0;
62
- let budgetMs = RENAME_RETRY_BUDGET_MS;
62
+ withFileLockRetry(() => {
63
+ fs.renameSync(src, dest);
64
+ });
65
+ }
66
+ /**
67
+ * Asynchronous {@link renameFileWithRetry}, which waits between attempts
68
+ * without blocking the event loop.
69
+ */
70
+ export async function renameFileWithRetryAsync(src, dest) {
71
+ await withFileLockRetryAsync(() => fs.promises.rename(src, dest));
72
+ }
73
+ /**
74
+ * Reads `target`'s stats without following it, with the retry policy of
75
+ * {@link renameFileWithRetry}.
76
+ *
77
+ * A Windows path another process has just unlinked stays delete-pending until
78
+ * the last handle on it closes, and inspecting it fails with EPERM for as long
79
+ * as that lasts. Retrying lets the unlink land, so a caller that reads ENOENT
80
+ * as an absent entry sees the same absence POSIX shows it at once.
81
+ */
82
+ export function lstatWithRetry(target) {
83
+ return withFileLockRetry(() => fs.lstatSync(target));
84
+ }
85
+ /**
86
+ * Removes a file, with the retry policy of {@link renameFileWithRetry}.
87
+ */
88
+ export function unlinkWithRetry(target) {
89
+ withFileLockRetry(() => {
90
+ fs.unlinkSync(target);
91
+ });
92
+ }
93
+ /**
94
+ * Runs a filesystem operation with the retry policy of
95
+ * {@link renameFileWithRetry}.
96
+ */
97
+ export function withFileLockRetry(operation) {
98
+ const retry = createFileLockRetry();
99
+ for (;;) {
100
+ try {
101
+ return operation();
102
+ }
103
+ catch (err) {
104
+ const delayMs = retry.delayBeforeNextAttempt(err);
105
+ if (delayMs > 0)
106
+ Atomics.wait(fileLockRetrySleepBuffer, 0, 0, delayMs);
107
+ retry.checkBudgetAfterDelay(err);
108
+ }
109
+ }
110
+ }
111
+ async function withFileLockRetryAsync(operation) {
112
+ const retry = createFileLockRetry();
63
113
  for (;;) {
64
114
  try {
65
- fs.renameSync(src, dest);
66
- return;
115
+ // eslint-disable-next-line no-await-in-loop
116
+ return await operation();
67
117
  }
68
118
  catch (err) {
69
- if (!isTransientRenameError(err))
119
+ const delayMs = retry.delayBeforeNextAttempt(err);
120
+ if (delayMs > 0) {
121
+ // eslint-disable-next-line no-await-in-loop
122
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
123
+ }
124
+ retry.checkBudgetAfterDelay(err);
125
+ }
126
+ }
127
+ }
128
+ function createFileLockRetry() {
129
+ const startedAt = Date.now();
130
+ let backoffMs = 0;
131
+ let budgetMs = FILE_LOCK_RETRY_BUDGET_MS;
132
+ return {
133
+ delayBeforeNextAttempt(err) {
134
+ if (!isTransientFileLockError(err))
70
135
  throw err;
71
136
  if (err.code === 'EPERM' || err.code === 'EACCES')
72
137
  budgetMs = Math.min(budgetMs, PERMISSION_DENIED_RETRY_BUDGET_MS);
73
138
  const remainingMs = budgetMs - (Date.now() - startedAt);
74
139
  if (remainingMs <= 0)
75
140
  throw err;
76
- if (backoffMs > 0)
77
- Atomics.wait(renameRetrySleepBuffer, 0, 0, Math.min(backoffMs, remainingMs));
141
+ return Math.min(backoffMs, remainingMs);
142
+ },
143
+ checkBudgetAfterDelay(err) {
78
144
  if (Date.now() - startedAt >= budgetMs)
79
145
  throw err;
80
- backoffMs = Math.min(backoffMs + 10, RENAME_RETRY_BACKOFF_CAP_MS);
81
- }
82
- }
146
+ backoffMs = Math.min(backoffMs + 10, FILE_LOCK_RETRY_BACKOFF_CAP_MS);
147
+ },
148
+ };
83
149
  }
84
- function isTransientRenameError(err) {
85
- return process.platform === 'win32' &&
150
+ function isTransientFileLockError(err) {
151
+ return (process.platform === 'win32' || isWsl()) &&
86
152
  util.types.isNativeError(err) &&
87
153
  'code' in err &&
88
154
  (err.code === 'EPERM' || err.code === 'EACCES' || err.code === 'EBUSY');
89
155
  }
156
+ // A Windows drive mounted into WSL (/mnt/c) keeps Windows file locking, so an
157
+ // antivirus or indexer handle fails a rename there with EACCES. WSL kernels
158
+ // carry "microsoft" in their release, e.g. 5.15.167.4-microsoft-standard-WSL2.
159
+ function isWsl() {
160
+ return process.platform === 'linux' && os.release().toLowerCase().includes('microsoft');
161
+ }
90
162
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pnpm/fs.graceful-fs",
3
- "version": "1100.2.1",
3
+ "version": "1100.2.3",
4
4
  "description": "Promisified graceful-fs",
5
5
  "keywords": [
6
6
  "pnpm",
@@ -31,7 +31,7 @@
31
31
  },
32
32
  "devDependencies": {
33
33
  "@jest/globals": "30.4.1",
34
- "@pnpm/fs.graceful-fs": "1100.2.1",
34
+ "@pnpm/fs.graceful-fs": "1100.2.3",
35
35
  "@types/graceful-fs": "^4.1.9"
36
36
  },
37
37
  "engines": {