@pnpm/fs.graceful-fs 1100.2.0 → 1100.2.2

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,17 @@
1
1
  # @pnpm/fs.graceful-fs
2
2
 
3
+ ## 1100.2.2
4
+
5
+ ### Patch Changes
6
+
7
+ - 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).
8
+
9
+ ## 1100.2.1
10
+
11
+ ### Patch Changes
12
+
13
+ - Windows filesystem operations now retry permission errors for up to one second. Permanent permission errors previously delayed failure by a minute. Sharing and lock violations retain their one-minute retry budget [pnpm/pnpm#14682](https://github.com/pnpm/pnpm/issues/14682).
14
+
3
15
  ## 1100.2.0
4
16
 
5
17
  ### Minor Changes
package/lib/index.d.ts CHANGED
@@ -21,12 +21,27 @@ declare const _default: {
21
21
  };
22
22
  export default _default;
23
23
  /**
24
- * Renames `src` over `dest`, waiting out a Windows sharing violation an
25
- * EPERM, EACCES or EBUSY from whoever else holds the file open — for up to a
26
- * minute before rethrowing it. Every other error is thrown right away.
24
+ * Renames `src` over `dest`, retrying Windows EBUSY errors for up to a minute.
25
+ * EPERM and EACCES have a one-second budget because they can also indicate
26
+ * permanent permission or destination conflicts. Other errors are thrown
27
+ * right away.
27
28
  *
28
29
  * `dest` is never removed to make room for the rename: a concurrent install may
29
30
  * still be reading that dirent, and a reader has to see either the whole file
30
31
  * that was there or the whole file replacing it.
31
32
  */
32
33
  export declare function renameFileWithRetry(src: string, dest: string): void;
34
+ /**
35
+ * Reads `target`'s stats without following it, with the retry policy of
36
+ * {@link renameFileWithRetry}.
37
+ *
38
+ * A Windows path another process has just unlinked stays delete-pending until
39
+ * the last handle on it closes, and inspecting it fails with EPERM for as long
40
+ * as that lasts. Retrying lets the unlink land, so a caller that reads ENOENT
41
+ * as an absent entry sees the same absence POSIX shows it at once.
42
+ */
43
+ export declare function lstatWithRetry(target: string): fs.Stats;
44
+ /**
45
+ * Removes a file, with the retry policy of {@link renameFileWithRetry}.
46
+ */
47
+ export declare function unlinkWithRetry(target: string): void;
package/lib/index.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import fs from 'node:fs';
2
2
  import util, { promisify } from 'node:util';
3
3
  import gfs from 'graceful-fs';
4
- const RENAME_RETRY_BUDGET_MS = 60_000;
5
- const RENAME_RETRY_BACKOFF_CAP_MS = 100;
6
- const renameRetrySleepBuffer = new Int32Array(new SharedArrayBuffer(4));
4
+ const FILE_LOCK_RETRY_BUDGET_MS = 60_000;
5
+ const PERMISSION_DENIED_RETRY_BUDGET_MS = 1_000;
6
+ const FILE_LOCK_RETRY_BACKOFF_CAP_MS = 100;
7
+ const fileLockRetrySleepBuffer = new Int32Array(new SharedArrayBuffer(4));
7
8
  export default {
8
9
  chmod: promisify(gfs.chmod),
9
10
  copyFile: promisify(gfs.copyFile),
@@ -46,32 +47,65 @@ function withEagainRetry(fn, maxRetries = 15) {
46
47
  };
47
48
  }
48
49
  /**
49
- * Renames `src` over `dest`, waiting out a Windows sharing violation an
50
- * EPERM, EACCES or EBUSY from whoever else holds the file open — for up to a
51
- * minute before rethrowing it. Every other error is thrown right away.
50
+ * Renames `src` over `dest`, retrying Windows EBUSY errors for up to a minute.
51
+ * EPERM and EACCES have a one-second budget because they can also indicate
52
+ * permanent permission or destination conflicts. Other errors are thrown
53
+ * right away.
52
54
  *
53
55
  * `dest` is never removed to make room for the rename: a concurrent install may
54
56
  * still be reading that dirent, and a reader has to see either the whole file
55
57
  * that was there or the whole file replacing it.
56
58
  */
57
59
  export function renameFileWithRetry(src, dest) {
60
+ withFileLockRetry(() => {
61
+ fs.renameSync(src, dest);
62
+ });
63
+ }
64
+ /**
65
+ * Reads `target`'s stats without following it, with the retry policy of
66
+ * {@link renameFileWithRetry}.
67
+ *
68
+ * A Windows path another process has just unlinked stays delete-pending until
69
+ * the last handle on it closes, and inspecting it fails with EPERM for as long
70
+ * as that lasts. Retrying lets the unlink land, so a caller that reads ENOENT
71
+ * as an absent entry sees the same absence POSIX shows it at once.
72
+ */
73
+ export function lstatWithRetry(target) {
74
+ return withFileLockRetry(() => fs.lstatSync(target));
75
+ }
76
+ /**
77
+ * Removes a file, with the retry policy of {@link renameFileWithRetry}.
78
+ */
79
+ export function unlinkWithRetry(target) {
80
+ withFileLockRetry(() => {
81
+ fs.unlinkSync(target);
82
+ });
83
+ }
84
+ function withFileLockRetry(operation) {
58
85
  const startedAt = Date.now();
59
86
  let backoffMs = 0;
87
+ let budgetMs = FILE_LOCK_RETRY_BUDGET_MS;
60
88
  for (;;) {
61
89
  try {
62
- fs.renameSync(src, dest);
63
- return;
90
+ return operation();
64
91
  }
65
92
  catch (err) {
66
- if (!isTransientRenameError(err) || Date.now() - startedAt >= RENAME_RETRY_BUDGET_MS)
93
+ if (!isTransientFileLockError(err))
94
+ throw err;
95
+ if (err.code === 'EPERM' || err.code === 'EACCES')
96
+ budgetMs = Math.min(budgetMs, PERMISSION_DENIED_RETRY_BUDGET_MS);
97
+ const remainingMs = budgetMs - (Date.now() - startedAt);
98
+ if (remainingMs <= 0)
67
99
  throw err;
68
100
  if (backoffMs > 0)
69
- Atomics.wait(renameRetrySleepBuffer, 0, 0, backoffMs);
70
- backoffMs = Math.min(backoffMs + 10, RENAME_RETRY_BACKOFF_CAP_MS);
101
+ Atomics.wait(fileLockRetrySleepBuffer, 0, 0, Math.min(backoffMs, remainingMs));
102
+ if (Date.now() - startedAt >= budgetMs)
103
+ throw err;
104
+ backoffMs = Math.min(backoffMs + 10, FILE_LOCK_RETRY_BACKOFF_CAP_MS);
71
105
  }
72
106
  }
73
107
  }
74
- function isTransientRenameError(err) {
108
+ function isTransientFileLockError(err) {
75
109
  return process.platform === 'win32' &&
76
110
  util.types.isNativeError(err) &&
77
111
  'code' in err &&
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pnpm/fs.graceful-fs",
3
- "version": "1100.2.0",
3
+ "version": "1100.2.2",
4
4
  "description": "Promisified graceful-fs",
5
5
  "keywords": [
6
6
  "pnpm",
@@ -30,7 +30,8 @@
30
30
  "graceful-fs": "^4.2.11"
31
31
  },
32
32
  "devDependencies": {
33
- "@pnpm/fs.graceful-fs": "1100.2.0",
33
+ "@jest/globals": "30.4.1",
34
+ "@pnpm/fs.graceful-fs": "1100.2.2",
34
35
  "@types/graceful-fs": "^4.1.9"
35
36
  },
36
37
  "engines": {
@@ -40,8 +41,9 @@
40
41
  "preset": "@pnpm/jest-config"
41
42
  },
42
43
  "scripts": {
43
- "lint": "eslint \"src/**/*.ts\"",
44
- "test": "pn compile",
45
- "compile": "tsgo --build && pn lint --fix"
44
+ "lint": "eslint \"src/**/*.ts\" \"test/**/*.ts\"",
45
+ "test": "pn compile && pn .test",
46
+ "compile": "tsgo --build && pn lint --fix",
47
+ ".test": "cross-env NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules --disable-warning=ExperimentalWarning --disable-warning=DEP0169\" jest"
46
48
  }
47
49
  }