@oclif/plugin-update 4.7.42 → 4.7.44

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/README.md CHANGED
@@ -81,7 +81,7 @@ EXAMPLES
81
81
  $ oclif-example update --available
82
82
  ```
83
83
 
84
- _See code: [src/commands/update.ts](https://github.com/oclif/plugin-update/blob/4.7.42/src/commands/update.ts)_
84
+ _See code: [src/commands/update.ts](https://github.com/oclif/plugin-update/blob/4.7.44/src/commands/update.ts)_
85
85
  <!-- commandsstop -->
86
86
 
87
87
  # Contributing
@@ -1,7 +1,7 @@
1
1
  import makeDebug from 'debug';
2
2
  import { spawn } from 'node:child_process';
3
3
  import { existsSync } from 'node:fs';
4
- import { mkdir, open, stat, writeFile } from 'node:fs/promises';
4
+ import { mkdir, open, stat, unlink, writeFile } from 'node:fs/promises';
5
5
  import { join } from 'node:path';
6
6
  import { touch } from '../util.js';
7
7
  const debug = makeDebug('cli:updater');
@@ -56,10 +56,23 @@ export const init = async function (opts) {
56
56
  const clientDir = join(clientRoot, config.version);
57
57
  if (existsSync(clientDir))
58
58
  await touch(clientDir);
59
- if (!(await autoupdateNeeded()))
59
+ // Atomically claim the right to spawn an autoupdate.
60
+ //
61
+ // The original `if (!(await autoupdateNeeded())) return; await writeFile(...)`
62
+ // sequence had a non-atomic read-then-write window: several otto invocations
63
+ // starting in parallel on a machine with no marker file (e.g. a fresh
64
+ // laptop being set up) could all pass the autoupdateNeeded() check before
65
+ // any one of them wrote the marker, and each would spawn its own
66
+ // `<cli> update --autoupdate` child. Those children then pin in debounce()
67
+ // (which never exits while CLI activity continues) and accumulate until OOM.
68
+ //
69
+ // Fix: combine the check and the marker creation into a single atomic step
70
+ // using O_EXCL (`open(path, 'wx')`). Only one process can create the marker;
71
+ // others see EEXIST and bail (or, if the marker is stale, race to reclaim it
72
+ // — which is bounded to one race per debounce window per machine).
73
+ if (!(await claimAutoupdate(autoupdatefile)))
60
74
  return;
61
75
  debug('autoupdate running');
62
- await writeFile(autoupdatefile, '');
63
76
  debug(`spawning autoupdate on ${binPath}`);
64
77
  const fd = await open(autoupdatelogfile, 'a');
65
78
  await writeFile(fd, timestamp(`starting \`${binPath} update --autoupdate\` from ${process.argv.slice(1, 3).join(' ')}\n`));
@@ -73,4 +86,45 @@ export const init = async function (opts) {
73
86
  .on('error', (e) => process.emitWarning(e))
74
87
  .on('close', () => fd.close())
75
88
  .unref();
89
+ async function claimAutoupdate(markerPath) {
90
+ // Fast path: try to atomically create the marker. Wins the race when no
91
+ // marker exists yet (fresh-laptop case — the catastrophic scenario).
92
+ try {
93
+ const fd = await open(markerPath, 'wx');
94
+ await fd.close();
95
+ return true;
96
+ }
97
+ catch (error) {
98
+ const err = error;
99
+ if (err.code !== 'EEXIST')
100
+ throw err;
101
+ }
102
+ // Marker exists. If it's within the debounce window, nothing to do.
103
+ if (!(await autoupdateNeeded()))
104
+ return false;
105
+ // Marker is stale (debounce window has elapsed). Reclaim by unlinking and
106
+ // re-creating atomically. There remains a tiny window between unlink and
107
+ // open where two stale-marker processes could both win, but it's
108
+ // microseconds vs the multi-await window of the original bug, and
109
+ // bounded to one race per debounce window per machine.
110
+ try {
111
+ await unlink(markerPath);
112
+ }
113
+ catch (error) {
114
+ const err = error;
115
+ if (err.code !== 'ENOENT')
116
+ throw err;
117
+ }
118
+ try {
119
+ const fd = await open(markerPath, 'wx');
120
+ await fd.close();
121
+ return true;
122
+ }
123
+ catch (error) {
124
+ const err = error;
125
+ if (err.code === 'EEXIST')
126
+ return false;
127
+ throw err;
128
+ }
129
+ }
76
130
  };
@@ -106,5 +106,5 @@
106
106
  ]
107
107
  }
108
108
  },
109
- "version": "4.7.42"
109
+ "version": "4.7.44"
110
110
  }
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@oclif/plugin-update",
3
- "version": "4.7.42",
3
+ "version": "4.7.44",
4
4
  "author": "Salesforce",
5
5
  "bugs": "https://github.com/oclif/plugin-update/issues",
6
6
  "dependencies": {
7
7
  "@inquirer/select": "^2.5.0",
8
8
  "@oclif/core": "^4",
9
- "@oclif/table": "^0.5.8",
9
+ "@oclif/table": "^0.5.9",
10
10
  "ansis": "^3.17.0",
11
11
  "debug": "^4.4.1",
12
12
  "filesize": "^6.1.0",
@@ -31,7 +31,7 @@
31
31
  "chai": "^4.5.0",
32
32
  "commitlint": "^19",
33
33
  "eslint": "^9.39.4",
34
- "eslint-config-oclif": "^6.0.164",
34
+ "eslint-config-oclif": "^6.0.165",
35
35
  "eslint-config-prettier": "^10.1.8",
36
36
  "husky": "^9.1.7",
37
37
  "lint-staged": "^15",