botmux 3.6.0 → 3.7.0
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/dist/.runtime-build-id +1 -1
- package/dist/adapters/backend/riff-backend.d.ts +7 -6
- package/dist/adapters/backend/riff-backend.d.ts.map +1 -1
- package/dist/adapters/backend/riff-backend.js +8 -7
- package/dist/adapters/backend/riff-backend.js.map +1 -1
- package/dist/adapters/backend/sandbox.d.ts.map +1 -1
- package/dist/adapters/backend/sandbox.js +5 -2
- package/dist/adapters/backend/sandbox.js.map +1 -1
- package/dist/adapters/cli/codex.d.ts.map +1 -1
- package/dist/adapters/cli/codex.js +15 -6
- package/dist/adapters/cli/codex.js.map +1 -1
- package/dist/adapters/cli/fs-policy.d.ts +62 -6
- package/dist/adapters/cli/fs-policy.d.ts.map +1 -1
- package/dist/adapters/cli/fs-policy.js +70 -7
- package/dist/adapters/cli/fs-policy.js.map +1 -1
- package/dist/bot-registry.d.ts +13 -0
- package/dist/bot-registry.d.ts.map +1 -1
- package/dist/bot-registry.js +15 -7
- package/dist/bot-registry.js.map +1 -1
- package/dist/cli/daemon-lifecycle-env.d.ts +1 -1
- package/dist/cli/daemon-lifecycle-env.d.ts.map +1 -1
- package/dist/cli/daemon-lifecycle-env.js +12 -0
- package/dist/cli/daemon-lifecycle-env.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +23 -7
- package/dist/cli.js.map +1 -1
- package/dist/core/command-handler.d.ts +7 -5
- package/dist/core/command-handler.d.ts.map +1 -1
- package/dist/core/command-handler.js +37 -21
- package/dist/core/command-handler.js.map +1 -1
- package/dist/core/cost-calculator.d.ts +3 -0
- package/dist/core/cost-calculator.d.ts.map +1 -1
- package/dist/core/cost-calculator.js.map +1 -1
- package/dist/core/dashboard-ipc-server.d.ts.map +1 -1
- package/dist/core/dashboard-ipc-server.js +12 -9
- package/dist/core/dashboard-ipc-server.js.map +1 -1
- package/dist/core/dashboard-rows.d.ts.map +1 -1
- package/dist/core/dashboard-rows.js +1 -0
- package/dist/core/dashboard-rows.js.map +1 -1
- package/dist/core/initial-user-turn.d.ts +14 -0
- package/dist/core/initial-user-turn.d.ts.map +1 -0
- package/dist/core/initial-user-turn.js +65 -0
- package/dist/core/initial-user-turn.js.map +1 -0
- package/dist/core/maintenance.d.ts.map +1 -1
- package/dist/core/maintenance.js +6 -0
- package/dist/core/maintenance.js.map +1 -1
- package/dist/core/per-bot-env.d.ts.map +1 -1
- package/dist/core/per-bot-env.js +10 -0
- package/dist/core/per-bot-env.js.map +1 -1
- package/dist/core/plugins/pm2.d.ts.map +1 -1
- package/dist/core/plugins/pm2.js +4 -0
- package/dist/core/plugins/pm2.js.map +1 -1
- package/dist/core/session-discovery.d.ts +23 -0
- package/dist/core/session-discovery.d.ts.map +1 -1
- package/dist/core/session-discovery.js +162 -82
- package/dist/core/session-discovery.js.map +1 -1
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +23 -4
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/worker-pool.d.ts +2 -0
- package/dist/core/worker-pool.d.ts.map +1 -1
- package/dist/core/worker-pool.js +27 -4
- package/dist/core/worker-pool.js.map +1 -1
- package/dist/daemon.d.ts +18 -0
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +205 -39
- package/dist/daemon.js.map +1 -1
- package/dist/dashboard/bot-onboarding.d.ts.map +1 -1
- package/dist/dashboard/bot-onboarding.js +29 -5
- package/dist/dashboard/bot-onboarding.js.map +1 -1
- package/dist/dashboard/bot-payload.js +3 -3
- package/dist/dashboard/bot-payload.js.map +1 -1
- package/dist/dashboard/web/bot-defaults-page.js +5 -5
- package/dist/dashboard/web/bot-defaults-page.js.map +1 -1
- package/dist/dashboard/web/bot-onboarding.d.ts.map +1 -1
- package/dist/dashboard/web/bot-onboarding.js +1 -1
- package/dist/dashboard/web/bot-onboarding.js.map +1 -1
- package/dist/dashboard/web/i18n.js +18 -18
- package/dist/dashboard/web/i18n.js.map +1 -1
- package/dist/dashboard-web/app.js +1 -1
- package/dist/dashboard-web/chunks/{bot-defaults-page-VQMPUJ4Y.js → bot-defaults-page-UKA7K4JV.js} +2 -2
- package/dist/dashboard-web/chunks/{chunk-NP4JUBGS.js → chunk-3QAUJXAS.js} +1 -1
- package/dist/dashboard-web/chunks/{chunk-JMRGZQDB.js → chunk-FLGXZJTD.js} +1 -1
- package/dist/dashboard-web/chunks/chunk-FNPGOZMX.js +1 -0
- package/dist/dashboard-web/chunks/{chunk-SNKATZWK.js → chunk-KQB42ML5.js} +4 -4
- package/dist/dashboard-web/chunks/{connectors-page-DOY4J3ZL.js → connectors-page-A77YAB5Y.js} +1 -1
- package/dist/dashboard-web/chunks/{groups-page-74TS2DIH.js → groups-page-RUZNVXQB.js} +1 -1
- package/dist/dashboard-web/chunks/{insights-page-AYCO65JC.js → insights-page-RF7U7RTA.js} +1 -1
- package/dist/dashboard-web/chunks/{monitor-room-YKYLTSAN.js → monitor-room-7IVIS75W.js} +1 -1
- package/dist/dashboard-web/chunks/{monitoring-page-QBIUNRBR.js → monitoring-page-5SSJRQZF.js} +1 -1
- package/dist/dashboard-web/chunks/{office-page-4PH2A26P.js → office-page-EOND3L5D.js} +1 -1
- package/dist/dashboard-web/chunks/{overview-page-H73D5UT4.js → overview-page-NISBVBBJ.js} +1 -1
- package/dist/dashboard-web/chunks/{roles-page-U4BTOQ7F.js → roles-page-BUMKD7Y4.js} +1 -1
- package/dist/dashboard-web/chunks/{schedules-page-RHRHX7R7.js → schedules-page-AVOOKLUH.js} +1 -1
- package/dist/dashboard-web/chunks/{sessions-page-VSC6AM5G.js → sessions-page-6HLSEAB7.js} +2 -2
- package/dist/dashboard-web/chunks/{settings-page-LF3RKM5P.js → settings-page-MJDZHQ4X.js} +1 -1
- package/dist/dashboard-web/chunks/{skills-page-SYOIGGXH.js → skills-page-5MW54PCC.js} +1 -1
- package/dist/dashboard-web/chunks/{team-federation-page-RMBLASTW.js → team-federation-page-2SNIMVXI.js} +1 -1
- package/dist/dashboard-web/chunks/{v3-page-XJXKKJRN.js → v3-page-W6SBXXQC.js} +1 -1
- package/dist/dashboard-web/chunks/{whiteboards-page-K3YVSTHJ.js → whiteboards-page-STC54KTP.js} +1 -1
- package/dist/dashboard-web/style.css +5 -0
- package/dist/dashboard.js +2 -2
- package/dist/dashboard.js.map +1 -1
- package/dist/desktop/main/dashboard-compat.js +1 -1
- package/dist/i18n/en.js +6 -6
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/zh.js +6 -6
- package/dist/i18n/zh.js.map +1 -1
- package/dist/im/lark/card-builder.d.ts.map +1 -1
- package/dist/im/lark/card-builder.js +5 -4
- package/dist/im/lark/card-builder.js.map +1 -1
- package/dist/im/lark/card-handler.d.ts.map +1 -1
- package/dist/im/lark/card-handler.js +40 -7
- package/dist/im/lark/card-handler.js.map +1 -1
- package/dist/im/lark/client.d.ts.map +1 -1
- package/dist/im/lark/client.js +82 -1
- package/dist/im/lark/client.js.map +1 -1
- package/dist/im/lark/event-dispatcher.d.ts.map +1 -1
- package/dist/im/lark/event-dispatcher.js +46 -12
- package/dist/im/lark/event-dispatcher.js.map +1 -1
- package/dist/im/lark/relay-target-routing.d.ts.map +1 -1
- package/dist/im/lark/relay-target-routing.js +12 -11
- package/dist/im/lark/relay-target-routing.js.map +1 -1
- package/dist/im/lark/reply-mode-command.d.ts.map +1 -1
- package/dist/im/lark/reply-mode-command.js +4 -3
- package/dist/im/lark/reply-mode-command.js.map +1 -1
- package/dist/index-daemon.js +6 -1
- package/dist/index-daemon.js.map +1 -1
- package/dist/mir-runner.js +1 -1
- package/dist/mir-runner.js.map +1 -1
- package/dist/services/bot-config-store.d.ts +1 -1
- package/dist/services/bot-config-store.d.ts.map +1 -1
- package/dist/services/bot-config-store.js +1 -1
- package/dist/services/bot-config-store.js.map +1 -1
- package/dist/services/card-prefs-store.js +8 -8
- package/dist/services/card-prefs-store.js.map +1 -1
- package/dist/services/chat-reply-mode-store.d.ts.map +1 -1
- package/dist/services/chat-reply-mode-store.js +16 -11
- package/dist/services/chat-reply-mode-store.js.map +1 -1
- package/dist/services/insight/types.d.ts +3 -1
- package/dist/services/insight/types.d.ts.map +1 -1
- package/dist/services/insight/types.js.map +1 -1
- package/dist/services/project-scanner.d.ts +4 -4
- package/dist/services/project-scanner.d.ts.map +1 -1
- package/dist/services/project-scanner.js +13 -5
- package/dist/services/project-scanner.js.map +1 -1
- package/dist/services/transcript-resolver.d.ts +4 -0
- package/dist/services/transcript-resolver.d.ts.map +1 -1
- package/dist/services/transcript-resolver.js +53 -5
- package/dist/services/transcript-resolver.js.map +1 -1
- package/dist/services/usage-ledger.d.ts.map +1 -1
- package/dist/services/usage-ledger.js +1 -0
- package/dist/services/usage-ledger.js.map +1 -1
- package/dist/setup/bot-config-editor.d.ts +37 -1
- package/dist/setup/bot-config-editor.d.ts.map +1 -1
- package/dist/setup/bot-config-editor.js +61 -3
- package/dist/setup/bot-config-editor.js.map +1 -1
- package/dist/setup/setup-args.js +1 -1
- package/dist/setup/setup-args.js.map +1 -1
- package/dist/types.d.ts +20 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/allowed-users-apply.d.ts.map +1 -1
- package/dist/utils/allowed-users-apply.js +3 -2
- package/dist/utils/allowed-users-apply.js.map +1 -1
- package/dist/utils/child-env.d.ts +31 -2
- package/dist/utils/child-env.d.ts.map +1 -1
- package/dist/utils/child-env.js +61 -3
- package/dist/utils/child-env.js.map +1 -1
- package/dist/utils/file-lock.d.ts +10 -0
- package/dist/utils/file-lock.d.ts.map +1 -1
- package/dist/utils/file-lock.js +295 -53
- package/dist/utils/file-lock.js.map +1 -1
- package/dist/worker.js +52 -3
- package/dist/worker.js.map +1 -1
- package/dist/workflows/v3/run-envelope.d.ts +24 -24
- package/package.json +1 -1
- package/dist/dashboard-web/chunks/chunk-P327LXOT.js +0 -1
package/dist/utils/file-lock.js
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* don't need it and the equality check would re-open the stale-break race.)
|
|
25
25
|
*/
|
|
26
26
|
import { closeSync, constants, fstatSync, linkSync, lstatSync, openSync, promises as fsp, readFileSync, readdirSync, unlinkSync, writeFileSync, } from 'node:fs';
|
|
27
|
-
import { createHash } from 'node:crypto';
|
|
27
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
28
28
|
import { basename, dirname, join } from 'node:path';
|
|
29
29
|
import { readLinuxBootIdentity, readProcessStartIdentity } from '../core/session-marker.js';
|
|
30
30
|
import { logger } from './logger.js';
|
|
@@ -42,6 +42,11 @@ const MIN_INVALID_HOLDER_STALE_AGE_MS = 1_000;
|
|
|
42
42
|
const STALE_CLAIM_PREFIX = '.botmux-stale-claim-';
|
|
43
43
|
const STALE_CLAIM_OWNER_SUFFIX = '.owner-';
|
|
44
44
|
const STALE_CLAIM_OWNER_WIDTH = 12;
|
|
45
|
+
const STALE_CLAIM_OWNER_CANDIDATE_SUFFIX = '.candidate-';
|
|
46
|
+
let fileLockTestHooks;
|
|
47
|
+
export function __testOnly_setFileLockHooks(hooks) {
|
|
48
|
+
fileLockTestHooks = hooks;
|
|
49
|
+
}
|
|
45
50
|
let selfProcStartResolved = false;
|
|
46
51
|
let selfProcStart;
|
|
47
52
|
let selfBootIdResolved = false;
|
|
@@ -157,24 +162,28 @@ async function releaseOwnedLock(lockPath, owned) {
|
|
|
157
162
|
const current = await fsp.lstat(lockPath);
|
|
158
163
|
if (current.isFile() && !current.isSymbolicLink() && sameInode(current, owned)) {
|
|
159
164
|
await fsp.unlink(lockPath);
|
|
165
|
+
return true;
|
|
160
166
|
}
|
|
161
167
|
}
|
|
162
168
|
catch (error) {
|
|
163
169
|
if (error.code !== 'ENOENT')
|
|
164
170
|
throw error;
|
|
165
171
|
}
|
|
172
|
+
return false;
|
|
166
173
|
}
|
|
167
174
|
function releaseOwnedLockSync(lockPath, owned) {
|
|
168
175
|
try {
|
|
169
176
|
const current = lstatSync(lockPath);
|
|
170
177
|
if (current.isFile() && !current.isSymbolicLink() && sameInode(current, owned)) {
|
|
171
178
|
unlinkSync(lockPath);
|
|
179
|
+
return true;
|
|
172
180
|
}
|
|
173
181
|
}
|
|
174
182
|
catch (error) {
|
|
175
183
|
if (error.code !== 'ENOENT')
|
|
176
184
|
throw error;
|
|
177
185
|
}
|
|
186
|
+
return false;
|
|
178
187
|
}
|
|
179
188
|
function staleClaimPathFor(lockPath, observed) {
|
|
180
189
|
// link(2) changes ctime, so the generation deliberately uses only identity
|
|
@@ -206,17 +215,87 @@ function parseStaleClaimOwnerEpoch(claimPath, name) {
|
|
|
206
215
|
const epoch = Number(raw);
|
|
207
216
|
return Number.isSafeInteger(epoch) ? epoch : undefined;
|
|
208
217
|
}
|
|
218
|
+
function staleClaimOwnerCandidatePath(ownerPath) {
|
|
219
|
+
return `${ownerPath}${STALE_CLAIM_OWNER_CANDIDATE_SUFFIX}${process.pid}-${randomUUID()}`;
|
|
220
|
+
}
|
|
221
|
+
function isStaleClaimOwnerCandidate(claimPath, name) {
|
|
222
|
+
const prefix = `${basename(claimPath)}${STALE_CLAIM_OWNER_SUFFIX}`;
|
|
223
|
+
if (!name.startsWith(prefix))
|
|
224
|
+
return false;
|
|
225
|
+
const suffix = name.slice(prefix.length);
|
|
226
|
+
return new RegExp(`^\\d{${STALE_CLAIM_OWNER_WIDTH}}\\${STALE_CLAIM_OWNER_CANDIDATE_SUFFIX}` +
|
|
227
|
+
'\\d+-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$').test(suffix);
|
|
228
|
+
}
|
|
229
|
+
function pinnedHolderMetadataState(before, after) {
|
|
230
|
+
if (!before.isFile() || !after.isFile() || !sameInode(before, after))
|
|
231
|
+
return 'untrusted';
|
|
232
|
+
const sizeChanged = before.size !== after.size;
|
|
233
|
+
const mtimeChanged = before.mtimeMs !== after.mtimeMs;
|
|
234
|
+
const ctimeChanged = before.ctimeMs !== after.ctimeMs;
|
|
235
|
+
const linkCountChanged = before.nlink !== after.nlink;
|
|
236
|
+
// Compatibility with an older publisher that exposed the O_EXCL inode
|
|
237
|
+
// before writing its identity. Never trust either snapshot, but let the
|
|
238
|
+
// caller retry once the payload has become stable.
|
|
239
|
+
if (before.size === 0 && (sizeChanged || mtimeChanged || ctimeChanged)) {
|
|
240
|
+
return 'transient';
|
|
241
|
+
}
|
|
242
|
+
// Content-relevant metadata changing on a non-empty identity is not a
|
|
243
|
+
// normal owner lifecycle transition. A size or mtime change fails closed
|
|
244
|
+
// here. Note we cannot detect a same-size rewrite that also restores mtime
|
|
245
|
+
// purely from metadata: ctime often does not advance within it at ms
|
|
246
|
+
// resolution (e.g. ext4), so ctime is not a reliable content-tamper signal.
|
|
247
|
+
// The legitimate owner protocol never rewrites an epoch in place (O_EXCL
|
|
248
|
+
// once, then a new epoch pathname), so this residual window is defense in
|
|
249
|
+
// depth rather than a live hazard.
|
|
250
|
+
if (sizeChanged || mtimeChanged)
|
|
251
|
+
return 'untrusted';
|
|
252
|
+
// link(2)/unlink(2) legitimately change ctime and nlink without touching
|
|
253
|
+
// bytes. This means the pinned inode lost a publication/cleanup race; do not
|
|
254
|
+
// use the observation, and do not surface it as a business error.
|
|
255
|
+
if (linkCountChanged)
|
|
256
|
+
return 'transient';
|
|
257
|
+
if (ctimeChanged)
|
|
258
|
+
return 'untrusted';
|
|
259
|
+
if (before.nlink < 1 || after.nlink < 1)
|
|
260
|
+
return 'transient';
|
|
261
|
+
return 'stable';
|
|
262
|
+
}
|
|
263
|
+
function pinnedHolderIntegrityError() {
|
|
264
|
+
return new Error('file-lock stale-claim owner changed while reading');
|
|
265
|
+
}
|
|
209
266
|
async function readPinnedHolder(path) {
|
|
210
267
|
const handle = await fsp.open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
211
268
|
try {
|
|
212
269
|
const before = await handle.stat();
|
|
270
|
+
if (fileLockTestHooks?.afterPinnedHolderFirstStat) {
|
|
271
|
+
await fileLockTestHooks.afterPinnedHolderFirstStat(path);
|
|
272
|
+
}
|
|
213
273
|
const raw = await handle.readFile('utf8');
|
|
214
274
|
const after = await handle.stat();
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
275
|
+
const pinnedState = pinnedHolderMetadataState(before, after);
|
|
276
|
+
if (pinnedState === 'transient')
|
|
277
|
+
return undefined;
|
|
278
|
+
if (pinnedState === 'untrusted')
|
|
279
|
+
throw pinnedHolderIntegrityError();
|
|
280
|
+
let current;
|
|
281
|
+
try {
|
|
282
|
+
current = await fsp.lstat(path);
|
|
218
283
|
}
|
|
219
|
-
|
|
284
|
+
catch (error) {
|
|
285
|
+
if (error.code === 'ENOENT')
|
|
286
|
+
return undefined;
|
|
287
|
+
throw error;
|
|
288
|
+
}
|
|
289
|
+
if (!current.isFile() || current.isSymbolicLink())
|
|
290
|
+
throw pinnedHolderIntegrityError();
|
|
291
|
+
if (!sameInode(after, current))
|
|
292
|
+
return undefined;
|
|
293
|
+
const pathnameState = pinnedHolderMetadataState(after, current);
|
|
294
|
+
if (pathnameState === 'transient')
|
|
295
|
+
return undefined;
|
|
296
|
+
if (pathnameState === 'untrusted')
|
|
297
|
+
throw pinnedHolderIntegrityError();
|
|
298
|
+
return { holder: parseLockHolder(raw), ageMs: Date.now() - current.mtimeMs, stats: current };
|
|
220
299
|
}
|
|
221
300
|
finally {
|
|
222
301
|
await handle.close();
|
|
@@ -226,18 +305,76 @@ function readPinnedHolderSync(path) {
|
|
|
226
305
|
const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
227
306
|
try {
|
|
228
307
|
const before = fstatSync(fd);
|
|
308
|
+
fileLockTestHooks?.afterPinnedHolderFirstStatSync?.(path);
|
|
229
309
|
const raw = readFileSync(fd, 'utf8');
|
|
230
310
|
const after = fstatSync(fd);
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
311
|
+
const pinnedState = pinnedHolderMetadataState(before, after);
|
|
312
|
+
if (pinnedState === 'transient')
|
|
313
|
+
return undefined;
|
|
314
|
+
if (pinnedState === 'untrusted')
|
|
315
|
+
throw pinnedHolderIntegrityError();
|
|
316
|
+
let current;
|
|
317
|
+
try {
|
|
318
|
+
current = lstatSync(path);
|
|
319
|
+
}
|
|
320
|
+
catch (error) {
|
|
321
|
+
if (error.code === 'ENOENT')
|
|
322
|
+
return undefined;
|
|
323
|
+
throw error;
|
|
234
324
|
}
|
|
235
|
-
|
|
325
|
+
if (!current.isFile() || current.isSymbolicLink())
|
|
326
|
+
throw pinnedHolderIntegrityError();
|
|
327
|
+
if (!sameInode(after, current))
|
|
328
|
+
return undefined;
|
|
329
|
+
const pathnameState = pinnedHolderMetadataState(after, current);
|
|
330
|
+
if (pathnameState === 'transient')
|
|
331
|
+
return undefined;
|
|
332
|
+
if (pathnameState === 'untrusted')
|
|
333
|
+
throw pinnedHolderIntegrityError();
|
|
334
|
+
return { holder: parseLockHolder(raw), ageMs: Date.now() - current.mtimeMs, stats: current };
|
|
236
335
|
}
|
|
237
336
|
finally {
|
|
238
337
|
closeSync(fd);
|
|
239
338
|
}
|
|
240
339
|
}
|
|
340
|
+
async function unlinkUnchangedPinnedPath(path, pinned) {
|
|
341
|
+
let current;
|
|
342
|
+
try {
|
|
343
|
+
current = await fsp.lstat(path);
|
|
344
|
+
}
|
|
345
|
+
catch (error) {
|
|
346
|
+
if (error.code === 'ENOENT')
|
|
347
|
+
return false;
|
|
348
|
+
throw error;
|
|
349
|
+
}
|
|
350
|
+
if (!current.isFile() || current.isSymbolicLink())
|
|
351
|
+
throw pinnedHolderIntegrityError();
|
|
352
|
+
if (!sameInode(current, pinned))
|
|
353
|
+
return false;
|
|
354
|
+
if (pinnedHolderMetadataState(pinned, current) !== 'stable')
|
|
355
|
+
return false;
|
|
356
|
+
await fsp.unlink(path);
|
|
357
|
+
return true;
|
|
358
|
+
}
|
|
359
|
+
function unlinkUnchangedPinnedPathSync(path, pinned) {
|
|
360
|
+
let current;
|
|
361
|
+
try {
|
|
362
|
+
current = lstatSync(path);
|
|
363
|
+
}
|
|
364
|
+
catch (error) {
|
|
365
|
+
if (error.code === 'ENOENT')
|
|
366
|
+
return false;
|
|
367
|
+
throw error;
|
|
368
|
+
}
|
|
369
|
+
if (!current.isFile() || current.isSymbolicLink())
|
|
370
|
+
throw pinnedHolderIntegrityError();
|
|
371
|
+
if (!sameInode(current, pinned))
|
|
372
|
+
return false;
|
|
373
|
+
if (pinnedHolderMetadataState(pinned, current) !== 'stable')
|
|
374
|
+
return false;
|
|
375
|
+
unlinkSync(path);
|
|
376
|
+
return true;
|
|
377
|
+
}
|
|
241
378
|
async function tryAcquireStaleClaimOwnership(claimPath, claimWasCreated, minStaleAgeMs) {
|
|
242
379
|
let claim;
|
|
243
380
|
try {
|
|
@@ -275,6 +412,8 @@ async function tryAcquireStaleClaimOwnership(claimPath, claimWasCreated, minStal
|
|
|
275
412
|
return undefined;
|
|
276
413
|
throw error;
|
|
277
414
|
}
|
|
415
|
+
if (!observedOwner)
|
|
416
|
+
return undefined;
|
|
278
417
|
const staleAge = observedOwner.holder
|
|
279
418
|
? minStaleAgeMs
|
|
280
419
|
: Math.max(minStaleAgeMs, MIN_INVALID_HOLDER_STALE_AGE_MS);
|
|
@@ -288,9 +427,10 @@ async function tryAcquireStaleClaimOwnership(claimPath, claimWasCreated, minStal
|
|
|
288
427
|
}
|
|
289
428
|
}
|
|
290
429
|
const ownerPath = staleClaimOwnerPath(claimPath, nextEpoch);
|
|
430
|
+
const candidatePath = staleClaimOwnerCandidatePath(ownerPath);
|
|
291
431
|
let handle;
|
|
292
432
|
try {
|
|
293
|
-
handle = await fsp.open(
|
|
433
|
+
handle = await fsp.open(candidatePath, 'wx');
|
|
294
434
|
}
|
|
295
435
|
catch (error) {
|
|
296
436
|
if (error.code === 'EEXIST' ||
|
|
@@ -298,31 +438,58 @@ async function tryAcquireStaleClaimOwnership(claimPath, claimWasCreated, minStal
|
|
|
298
438
|
return undefined;
|
|
299
439
|
throw error;
|
|
300
440
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
const owner = await handle.stat();
|
|
305
|
-
const currentClaim = await fsp.lstat(claimPath);
|
|
306
|
-
if (!owner.isFile() || !sameInode(currentClaim, claim)) {
|
|
441
|
+
let owned;
|
|
442
|
+
const abandonCandidate = async () => {
|
|
443
|
+
try {
|
|
307
444
|
await handle.close();
|
|
445
|
+
}
|
|
446
|
+
catch { /* tolerate */ }
|
|
447
|
+
if (owned) {
|
|
448
|
+
try {
|
|
449
|
+
await releaseOwnedLock(ownerPath, owned);
|
|
450
|
+
}
|
|
451
|
+
catch { /* tolerate */ }
|
|
452
|
+
try {
|
|
453
|
+
await releaseOwnedLock(candidatePath, owned);
|
|
454
|
+
}
|
|
455
|
+
catch { /* tolerate */ }
|
|
456
|
+
}
|
|
457
|
+
else {
|
|
308
458
|
try {
|
|
309
|
-
await
|
|
459
|
+
await fsp.unlink(candidatePath);
|
|
310
460
|
}
|
|
311
461
|
catch { /* tolerate */ }
|
|
462
|
+
}
|
|
463
|
+
};
|
|
464
|
+
try {
|
|
465
|
+
writeFileSync(handle.fd, currentLockHolderPayload());
|
|
466
|
+
owned = await handle.stat();
|
|
467
|
+
if (!owned.isFile())
|
|
468
|
+
throw new Error('file-lock stale-claim owner candidate is not a regular file');
|
|
469
|
+
const currentClaimBeforePublish = await fsp.lstat(claimPath);
|
|
470
|
+
if (!sameInode(currentClaimBeforePublish, claim)) {
|
|
471
|
+
await abandonCandidate();
|
|
312
472
|
return undefined;
|
|
313
473
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
catch (error) {
|
|
317
|
-
try {
|
|
318
|
-
await handle.close();
|
|
474
|
+
if (fileLockTestHooks?.beforeStaleClaimOwnerPublish) {
|
|
475
|
+
await fileLockTestHooks.beforeStaleClaimOwnerPublish(ownerPath);
|
|
319
476
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
477
|
+
// link(2) is an atomic no-overwrite publication: readers can observe
|
|
478
|
+
// either no epoch pathname or the complete identity, never an empty file.
|
|
479
|
+
await fsp.link(candidatePath, ownerPath);
|
|
480
|
+
const publishedOwner = await fsp.lstat(ownerPath);
|
|
481
|
+
const currentClaim = await fsp.lstat(claimPath);
|
|
482
|
+
if (!publishedOwner.isFile() || publishedOwner.isSymbolicLink() ||
|
|
483
|
+
!sameInode(publishedOwner, owned) || !sameInode(currentClaim, claim)) {
|
|
484
|
+
await abandonCandidate();
|
|
485
|
+
return undefined;
|
|
323
486
|
}
|
|
324
|
-
|
|
325
|
-
|
|
487
|
+
return { ownerPath, candidatePath, owned, handle };
|
|
488
|
+
}
|
|
489
|
+
catch (error) {
|
|
490
|
+
await abandonCandidate();
|
|
491
|
+
if (error.code === 'EEXIST' ||
|
|
492
|
+
error.code === 'ENOENT')
|
|
326
493
|
return undefined;
|
|
327
494
|
throw error;
|
|
328
495
|
}
|
|
@@ -362,6 +529,8 @@ function tryAcquireStaleClaimOwnershipSync(claimPath, claimWasCreated, minStaleA
|
|
|
362
529
|
return undefined;
|
|
363
530
|
throw error;
|
|
364
531
|
}
|
|
532
|
+
if (!observedOwner)
|
|
533
|
+
return undefined;
|
|
365
534
|
const staleAge = observedOwner.holder
|
|
366
535
|
? minStaleAgeMs
|
|
367
536
|
: Math.max(minStaleAgeMs, MIN_INVALID_HOLDER_STALE_AGE_MS);
|
|
@@ -375,9 +544,10 @@ function tryAcquireStaleClaimOwnershipSync(claimPath, claimWasCreated, minStaleA
|
|
|
375
544
|
}
|
|
376
545
|
}
|
|
377
546
|
const ownerPath = staleClaimOwnerPath(claimPath, nextEpoch);
|
|
547
|
+
const candidatePath = staleClaimOwnerCandidatePath(ownerPath);
|
|
378
548
|
let fd;
|
|
379
549
|
try {
|
|
380
|
-
fd = openSync(
|
|
550
|
+
fd = openSync(candidatePath, 'wx');
|
|
381
551
|
}
|
|
382
552
|
catch (error) {
|
|
383
553
|
if (error.code === 'EEXIST' ||
|
|
@@ -385,30 +555,54 @@ function tryAcquireStaleClaimOwnershipSync(claimPath, claimWasCreated, minStaleA
|
|
|
385
555
|
return undefined;
|
|
386
556
|
throw error;
|
|
387
557
|
}
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
const currentClaim = lstatSync(claimPath);
|
|
392
|
-
if (!owner.isFile() || !sameInode(currentClaim, claim)) {
|
|
558
|
+
let owned;
|
|
559
|
+
const abandonCandidate = () => {
|
|
560
|
+
try {
|
|
393
561
|
closeSync(fd);
|
|
562
|
+
}
|
|
563
|
+
catch { /* tolerate */ }
|
|
564
|
+
if (owned) {
|
|
394
565
|
try {
|
|
395
|
-
releaseOwnedLockSync(ownerPath,
|
|
566
|
+
releaseOwnedLockSync(ownerPath, owned);
|
|
396
567
|
}
|
|
397
568
|
catch { /* tolerate */ }
|
|
569
|
+
try {
|
|
570
|
+
releaseOwnedLockSync(candidatePath, owned);
|
|
571
|
+
}
|
|
572
|
+
catch { /* tolerate */ }
|
|
573
|
+
}
|
|
574
|
+
else {
|
|
575
|
+
try {
|
|
576
|
+
unlinkSync(candidatePath);
|
|
577
|
+
}
|
|
578
|
+
catch { /* tolerate */ }
|
|
579
|
+
}
|
|
580
|
+
};
|
|
581
|
+
try {
|
|
582
|
+
writeFileSync(fd, currentLockHolderPayload());
|
|
583
|
+
owned = fstatSync(fd);
|
|
584
|
+
if (!owned.isFile())
|
|
585
|
+
throw new Error('file-lock stale-claim owner candidate is not a regular file');
|
|
586
|
+
const currentClaimBeforePublish = lstatSync(claimPath);
|
|
587
|
+
if (!sameInode(currentClaimBeforePublish, claim)) {
|
|
588
|
+
abandonCandidate();
|
|
398
589
|
return undefined;
|
|
399
590
|
}
|
|
400
|
-
|
|
591
|
+
fileLockTestHooks?.beforeStaleClaimOwnerPublishSync?.(ownerPath);
|
|
592
|
+
linkSync(candidatePath, ownerPath);
|
|
593
|
+
const publishedOwner = lstatSync(ownerPath);
|
|
594
|
+
const currentClaim = lstatSync(claimPath);
|
|
595
|
+
if (!publishedOwner.isFile() || publishedOwner.isSymbolicLink() ||
|
|
596
|
+
!sameInode(publishedOwner, owned) || !sameInode(currentClaim, claim)) {
|
|
597
|
+
abandonCandidate();
|
|
598
|
+
return undefined;
|
|
599
|
+
}
|
|
600
|
+
return { ownerPath, candidatePath, owned, fd };
|
|
401
601
|
}
|
|
402
602
|
catch (error) {
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
catch { /* tolerate */ }
|
|
407
|
-
try {
|
|
408
|
-
unlinkSync(ownerPath);
|
|
409
|
-
}
|
|
410
|
-
catch { /* tolerate */ }
|
|
411
|
-
if (error.code === 'ENOENT')
|
|
603
|
+
abandonCandidate();
|
|
604
|
+
if (error.code === 'EEXIST' ||
|
|
605
|
+
error.code === 'ENOENT')
|
|
412
606
|
return undefined;
|
|
413
607
|
throw error;
|
|
414
608
|
}
|
|
@@ -419,15 +613,20 @@ async function cleanStaleClaimOwnership(claimPath, ownership) {
|
|
|
419
613
|
}
|
|
420
614
|
catch { /* tolerate */ }
|
|
421
615
|
try {
|
|
422
|
-
await
|
|
616
|
+
await releaseOwnedLock(ownership.ownerPath, ownership.owned);
|
|
617
|
+
}
|
|
618
|
+
catch { /* tolerate */ }
|
|
619
|
+
try {
|
|
620
|
+
await releaseOwnedLock(ownership.candidatePath, ownership.owned);
|
|
423
621
|
}
|
|
424
622
|
catch { /* tolerate */ }
|
|
425
623
|
// Older dead epochs are no longer security-relevant after the claim itself
|
|
426
624
|
// is gone. Clean them best-effort; a loser that was already in flight may
|
|
427
|
-
// leave another harmless orphan owner file behind.
|
|
625
|
+
// leave another harmless orphan owner/candidate file behind.
|
|
428
626
|
try {
|
|
429
627
|
const names = await fsp.readdir(dirname(claimPath));
|
|
430
|
-
await Promise.all(names.filter(name => parseStaleClaimOwnerEpoch(claimPath, name) !== undefined
|
|
628
|
+
await Promise.all(names.filter(name => parseStaleClaimOwnerEpoch(claimPath, name) !== undefined ||
|
|
629
|
+
isStaleClaimOwnerCandidate(claimPath, name)).map(async (name) => {
|
|
431
630
|
try {
|
|
432
631
|
await fsp.unlink(join(dirname(claimPath), name));
|
|
433
632
|
}
|
|
@@ -442,12 +641,17 @@ function cleanStaleClaimOwnershipSync(claimPath, ownership) {
|
|
|
442
641
|
}
|
|
443
642
|
catch { /* tolerate */ }
|
|
444
643
|
try {
|
|
445
|
-
|
|
644
|
+
releaseOwnedLockSync(ownership.ownerPath, ownership.owned);
|
|
645
|
+
}
|
|
646
|
+
catch { /* tolerate */ }
|
|
647
|
+
try {
|
|
648
|
+
releaseOwnedLockSync(ownership.candidatePath, ownership.owned);
|
|
446
649
|
}
|
|
447
650
|
catch { /* tolerate */ }
|
|
448
651
|
try {
|
|
449
652
|
for (const name of readdirSync(dirname(claimPath))) {
|
|
450
|
-
if (parseStaleClaimOwnerEpoch(claimPath, name) === undefined
|
|
653
|
+
if (parseStaleClaimOwnerEpoch(claimPath, name) === undefined &&
|
|
654
|
+
!isStaleClaimOwnerCandidate(claimPath, name))
|
|
451
655
|
continue;
|
|
452
656
|
try {
|
|
453
657
|
unlinkSync(join(dirname(claimPath), name));
|
|
@@ -474,12 +678,21 @@ async function resumeStaleBreak(lockPath, claimPath, observed, claimWasCreated,
|
|
|
474
678
|
return false;
|
|
475
679
|
}
|
|
476
680
|
let current;
|
|
681
|
+
let publicPathMissing = false;
|
|
477
682
|
try {
|
|
478
683
|
current = await readPinnedHolder(lockPath);
|
|
479
684
|
}
|
|
480
685
|
catch (error) {
|
|
481
686
|
if (error.code !== 'ENOENT')
|
|
482
687
|
throw error;
|
|
688
|
+
publicPathMissing = true;
|
|
689
|
+
}
|
|
690
|
+
if (!current && !publicPathMissing) {
|
|
691
|
+
// The pinned inode or its pathname changed during validation. Withdraw
|
|
692
|
+
// this generation and retry from a fresh public-path observation.
|
|
693
|
+
await releaseOwnedLock(claimPath, claim);
|
|
694
|
+
claimRemoved = true;
|
|
695
|
+
return false;
|
|
483
696
|
}
|
|
484
697
|
if (current && sameInode(current.stats, observed)) {
|
|
485
698
|
const staleAge = current.holder
|
|
@@ -494,7 +707,16 @@ async function resumeStaleBreak(lockPath, claimPath, observed, claimWasCreated,
|
|
|
494
707
|
claimRemoved = true;
|
|
495
708
|
return false;
|
|
496
709
|
}
|
|
497
|
-
|
|
710
|
+
if (fileLockTestHooks?.beforeStalePublicLockUnlink) {
|
|
711
|
+
await fileLockTestHooks.beforeStalePublicLockUnlink(lockPath);
|
|
712
|
+
}
|
|
713
|
+
if (!(await unlinkUnchangedPinnedPath(lockPath, current.stats))) {
|
|
714
|
+
// A late holder publication or pathname replacement won the race.
|
|
715
|
+
// The pinned bytes are no longer authority to remove the public name.
|
|
716
|
+
await releaseOwnedLock(claimPath, claim);
|
|
717
|
+
claimRemoved = true;
|
|
718
|
+
return false;
|
|
719
|
+
}
|
|
498
720
|
logger.warn(`[file-lock] broke stale lock at ${lockPath} ` +
|
|
499
721
|
`(${current.holder ? `dead/reused pid ${current.holder.pid}` : 'empty/invalid holder'}, age ${current.ageMs}ms)`);
|
|
500
722
|
}
|
|
@@ -520,7 +742,11 @@ async function resumeStaleBreak(lockPath, claimPath, observed, claimWasCreated,
|
|
|
520
742
|
// unlink. Withdraw its epoch so the same process does not look like a
|
|
521
743
|
// permanently-live crashed breaker after a transient I/O error.
|
|
522
744
|
try {
|
|
523
|
-
await
|
|
745
|
+
await releaseOwnedLock(ownership.ownerPath, ownership.owned);
|
|
746
|
+
}
|
|
747
|
+
catch { /* tolerate */ }
|
|
748
|
+
try {
|
|
749
|
+
await releaseOwnedLock(ownership.candidatePath, ownership.owned);
|
|
524
750
|
}
|
|
525
751
|
catch { /* tolerate */ }
|
|
526
752
|
}
|
|
@@ -539,12 +765,19 @@ function resumeStaleBreakSync(lockPath, claimPath, observed, claimWasCreated, mi
|
|
|
539
765
|
return false;
|
|
540
766
|
}
|
|
541
767
|
let current;
|
|
768
|
+
let publicPathMissing = false;
|
|
542
769
|
try {
|
|
543
770
|
current = readPinnedHolderSync(lockPath);
|
|
544
771
|
}
|
|
545
772
|
catch (error) {
|
|
546
773
|
if (error.code !== 'ENOENT')
|
|
547
774
|
throw error;
|
|
775
|
+
publicPathMissing = true;
|
|
776
|
+
}
|
|
777
|
+
if (!current && !publicPathMissing) {
|
|
778
|
+
releaseOwnedLockSync(claimPath, claim);
|
|
779
|
+
claimRemoved = true;
|
|
780
|
+
return false;
|
|
548
781
|
}
|
|
549
782
|
if (current && sameInode(current.stats, observed)) {
|
|
550
783
|
const staleAge = current.holder
|
|
@@ -557,7 +790,12 @@ function resumeStaleBreakSync(lockPath, claimPath, observed, claimWasCreated, mi
|
|
|
557
790
|
claimRemoved = true;
|
|
558
791
|
return false;
|
|
559
792
|
}
|
|
560
|
-
|
|
793
|
+
fileLockTestHooks?.beforeStalePublicLockUnlinkSync?.(lockPath);
|
|
794
|
+
if (!unlinkUnchangedPinnedPathSync(lockPath, current.stats)) {
|
|
795
|
+
releaseOwnedLockSync(claimPath, claim);
|
|
796
|
+
claimRemoved = true;
|
|
797
|
+
return false;
|
|
798
|
+
}
|
|
561
799
|
logger.warn(`[file-lock] broke stale lock at ${lockPath} ` +
|
|
562
800
|
`(${current.holder ? `dead/reused pid ${current.holder.pid}` : 'empty/invalid holder'}, age ${current.ageMs}ms)`);
|
|
563
801
|
}
|
|
@@ -574,7 +812,11 @@ function resumeStaleBreakSync(lockPath, claimPath, observed, claimWasCreated, mi
|
|
|
574
812
|
}
|
|
575
813
|
catch { /* tolerate */ }
|
|
576
814
|
try {
|
|
577
|
-
|
|
815
|
+
releaseOwnedLockSync(ownership.ownerPath, ownership.owned);
|
|
816
|
+
}
|
|
817
|
+
catch { /* tolerate */ }
|
|
818
|
+
try {
|
|
819
|
+
releaseOwnedLockSync(ownership.candidatePath, ownership.owned);
|
|
578
820
|
}
|
|
579
821
|
catch { /* tolerate */ }
|
|
580
822
|
}
|