@openclaw/crabline 0.1.11 → 0.1.12
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 +99 -12
- package/dist/src/bin/crabline.js +3 -1
- package/dist/src/bin/crabline.js.map +1 -1
- package/dist/src/cli/program.d.ts +5 -1
- package/dist/src/cli/program.js +264 -50
- package/dist/src/cli/program.js.map +1 -1
- package/dist/src/config/load.js +39 -3
- package/dist/src/config/load.js.map +1 -1
- package/dist/src/config/schema.d.ts +20 -0
- package/dist/src/config/schema.js +163 -35
- package/dist/src/config/schema.js.map +1 -1
- package/dist/src/core/errors.d.ts +1 -0
- package/dist/src/core/errors.js +6 -3
- package/dist/src/core/errors.js.map +1 -1
- package/dist/src/core/http-path.d.ts +1 -0
- package/dist/src/core/http-path.js +14 -0
- package/dist/src/core/http-path.js.map +1 -0
- package/dist/src/core/matcher.d.ts +3 -1
- package/dist/src/core/matcher.js +58 -2
- package/dist/src/core/matcher.js.map +1 -1
- package/dist/src/core/nonces.d.ts +2 -0
- package/dist/src/core/nonces.js +11 -1
- package/dist/src/core/nonces.js.map +1 -1
- package/dist/src/core/reporters.d.ts +5 -0
- package/dist/src/core/reporters.js +26 -2
- package/dist/src/core/reporters.js.map +1 -1
- package/dist/src/core/run.d.ts +3 -1
- package/dist/src/core/run.js +178 -66
- package/dist/src/core/run.js.map +1 -1
- package/dist/src/matrix-ids.d.ts +5 -0
- package/dist/src/matrix-ids.js +83 -0
- package/dist/src/matrix-ids.js.map +1 -0
- package/dist/src/openclaw/artifact-generation.d.ts +7 -2
- package/dist/src/openclaw/artifact-generation.js +399 -27
- package/dist/src/openclaw/artifact-generation.js.map +1 -1
- package/dist/src/openclaw/bridges/matrix.js +21 -4
- package/dist/src/openclaw/bridges/matrix.js.map +1 -1
- package/dist/src/openclaw/bridges/mattermost.js +13 -8
- package/dist/src/openclaw/bridges/mattermost.js.map +1 -1
- package/dist/src/openclaw/bridges/probe-response.d.ts +1 -0
- package/dist/src/openclaw/bridges/probe-response.js +5 -0
- package/dist/src/openclaw/bridges/probe-response.js.map +1 -0
- package/dist/src/openclaw/bridges/signal.js +15 -6
- package/dist/src/openclaw/bridges/signal.js.map +1 -1
- package/dist/src/openclaw/bridges/slack.js +116 -23
- package/dist/src/openclaw/bridges/slack.js.map +1 -1
- package/dist/src/openclaw/bridges/telegram.js +45 -19
- package/dist/src/openclaw/bridges/telegram.js.map +1 -1
- package/dist/src/openclaw/bridges/whatsapp.js +32 -16
- package/dist/src/openclaw/bridges/whatsapp.js.map +1 -1
- package/dist/src/openclaw/bridges/zalo.js +8 -3
- package/dist/src/openclaw/bridges/zalo.js.map +1 -1
- package/dist/src/openclaw/private-file.d.ts +38 -3
- package/dist/src/openclaw/private-file.js +2008 -87
- package/dist/src/openclaw/private-file.js.map +1 -1
- package/dist/src/openclaw/shared.d.ts +1 -0
- package/dist/src/openclaw/shared.js +48 -13
- package/dist/src/openclaw/shared.js.map +1 -1
- package/dist/src/openclaw/smoke-lock.d.ts +9 -0
- package/dist/src/openclaw/smoke-lock.js +626 -172
- package/dist/src/openclaw/smoke-lock.js.map +1 -1
- package/dist/src/openclaw.js +225 -42
- package/dist/src/openclaw.js.map +1 -1
- package/dist/src/platform/process-owned-lock.d.ts +11 -0
- package/dist/src/platform/process-owned-lock.js +1698 -0
- package/dist/src/platform/process-owned-lock.js.map +1 -0
- package/dist/src/platform/recorder-directory.d.ts +1 -0
- package/dist/src/platform/recorder-directory.js +14 -0
- package/dist/src/platform/recorder-directory.js.map +1 -0
- package/dist/src/platform/windows-acl.d.ts +18 -0
- package/dist/src/platform/windows-acl.js +1367 -0
- package/dist/src/platform/windows-acl.js.map +1 -0
- package/dist/src/platform/windows-lock-root.d.ts +13 -0
- package/dist/src/platform/windows-lock-root.js +98 -0
- package/dist/src/platform/windows-lock-root.js.map +1 -0
- package/dist/src/providers/builtin/discord.d.ts +4 -0
- package/dist/src/providers/builtin/discord.js +52 -9
- package/dist/src/providers/builtin/discord.js.map +1 -1
- package/dist/src/providers/builtin/external-webhook-auth.d.ts +1 -0
- package/dist/src/providers/builtin/external-webhook-auth.js +38 -1
- package/dist/src/providers/builtin/external-webhook-auth.js.map +1 -1
- package/dist/src/providers/builtin/feishu.d.ts +1 -0
- package/dist/src/providers/builtin/feishu.js +96 -26
- package/dist/src/providers/builtin/feishu.js.map +1 -1
- package/dist/src/providers/builtin/googlechat.d.ts +1 -0
- package/dist/src/providers/builtin/googlechat.js +95 -26
- package/dist/src/providers/builtin/googlechat.js.map +1 -1
- package/dist/src/providers/builtin/imessage.js +60 -17
- package/dist/src/providers/builtin/imessage.js.map +1 -1
- package/dist/src/providers/builtin/loopback.js +88 -35
- package/dist/src/providers/builtin/loopback.js.map +1 -1
- package/dist/src/providers/builtin/matrix.d.ts +2 -2
- package/dist/src/providers/builtin/matrix.js +78 -21
- package/dist/src/providers/builtin/matrix.js.map +1 -1
- package/dist/src/providers/builtin/mattermost.d.ts +11 -7
- package/dist/src/providers/builtin/mattermost.js +138 -24
- package/dist/src/providers/builtin/mattermost.js.map +1 -1
- package/dist/src/providers/builtin/msteams.d.ts +1 -0
- package/dist/src/providers/builtin/msteams.js +118 -38
- package/dist/src/providers/builtin/msteams.js.map +1 -1
- package/dist/src/providers/builtin/native-local-mock.js +4 -4
- package/dist/src/providers/builtin/native-local-mock.js.map +1 -1
- package/dist/src/providers/builtin/script.js +1057 -146
- package/dist/src/providers/builtin/script.js.map +1 -1
- package/dist/src/providers/builtin/slack.d.ts +8 -1
- package/dist/src/providers/builtin/slack.js +327 -24
- package/dist/src/providers/builtin/slack.js.map +1 -1
- package/dist/src/providers/builtin/telegram.d.ts +1 -1
- package/dist/src/providers/builtin/telegram.js +55 -14
- package/dist/src/providers/builtin/telegram.js.map +1 -1
- package/dist/src/providers/builtin/whatsapp.d.ts +4 -3
- package/dist/src/providers/builtin/whatsapp.js +157 -29
- package/dist/src/providers/builtin/whatsapp.js.map +1 -1
- package/dist/src/providers/builtin/zalo.d.ts +5 -2
- package/dist/src/providers/builtin/zalo.js +31 -7
- package/dist/src/providers/builtin/zalo.js.map +1 -1
- package/dist/src/providers/catalog.d.ts +1 -1
- package/dist/src/providers/catalog.js +1 -1
- package/dist/src/providers/catalog.js.map +1 -1
- package/dist/src/providers/local-mock.d.ts +4 -0
- package/dist/src/providers/local-mock.js +162 -35
- package/dist/src/providers/local-mock.js.map +1 -1
- package/dist/src/providers/native-ids.d.ts +2 -0
- package/dist/src/providers/native-ids.js +3 -0
- package/dist/src/providers/native-ids.js.map +1 -1
- package/dist/src/providers/recorder.d.ts +14 -3
- package/dist/src/providers/recorder.js +521 -41
- package/dist/src/providers/recorder.js.map +1 -1
- package/dist/src/providers/registry.d.ts +1 -0
- package/dist/src/providers/registry.js +65 -20
- package/dist/src/providers/registry.js.map +1 -1
- package/dist/src/providers/signed-jwt.d.ts +6 -1
- package/dist/src/providers/signed-jwt.js +241 -50
- package/dist/src/providers/signed-jwt.js.map +1 -1
- package/dist/src/providers/slack-ids.d.ts +1 -0
- package/dist/src/providers/slack-ids.js +5 -0
- package/dist/src/providers/slack-ids.js.map +1 -1
- package/dist/src/providers/target-normalizers.d.ts +9 -1
- package/dist/src/providers/target-normalizers.js +177 -58
- package/dist/src/providers/target-normalizers.js.map +1 -1
- package/dist/src/providers/webhook-server.d.ts +3 -0
- package/dist/src/providers/webhook-server.js +268 -47
- package/dist/src/providers/webhook-server.js.map +1 -1
- package/dist/src/servers/http.d.ts +14 -2
- package/dist/src/servers/http.js +229 -58
- package/dist/src/servers/http.js.map +1 -1
- package/dist/src/servers/matrix.d.ts +3 -0
- package/dist/src/servers/matrix.js +268 -110
- package/dist/src/servers/matrix.js.map +1 -1
- package/dist/src/servers/mattermost.d.ts +5 -0
- package/dist/src/servers/mattermost.js +389 -91
- package/dist/src/servers/mattermost.js.map +1 -1
- package/dist/src/servers/recorder.d.ts +11 -0
- package/dist/src/servers/recorder.js +1072 -69
- package/dist/src/servers/recorder.js.map +1 -1
- package/dist/src/servers/signal.d.ts +1 -0
- package/dist/src/servers/signal.js +279 -44
- package/dist/src/servers/signal.js.map +1 -1
- package/dist/src/servers/slack.d.ts +15 -0
- package/dist/src/servers/slack.js +259 -51
- package/dist/src/servers/slack.js.map +1 -1
- package/dist/src/servers/telegram-identity.d.ts +6 -0
- package/dist/src/servers/telegram-identity.js +27 -0
- package/dist/src/servers/telegram-identity.js.map +1 -0
- package/dist/src/servers/telegram.d.ts +17 -6
- package/dist/src/servers/telegram.js +1169 -107
- package/dist/src/servers/telegram.js.map +1 -1
- package/dist/src/servers/webhook-target.d.ts +12 -4
- package/dist/src/servers/webhook-target.js +226 -28
- package/dist/src/servers/webhook-target.js.map +1 -1
- package/dist/src/servers/whatsapp-baileys-websocket.d.ts +60 -1
- package/dist/src/servers/whatsapp-baileys-websocket.js +782 -112
- package/dist/src/servers/whatsapp-baileys-websocket.js.map +1 -1
- package/dist/src/servers/whatsapp-jid.d.ts +1 -0
- package/dist/src/servers/whatsapp-jid.js +9 -5
- package/dist/src/servers/whatsapp-jid.js.map +1 -1
- package/dist/src/servers/whatsapp-wire/binary-node.d.ts +1 -0
- package/dist/src/servers/whatsapp-wire/binary-node.js +37 -13
- package/dist/src/servers/whatsapp-wire/binary-node.js.map +1 -1
- package/dist/src/servers/whatsapp-wire/crypto.d.ts +3 -0
- package/dist/src/servers/whatsapp-wire/crypto.js +23 -8
- package/dist/src/servers/whatsapp-wire/crypto.js.map +1 -1
- package/dist/src/servers/whatsapp-wire/handshake.d.ts +9 -3
- package/dist/src/servers/whatsapp-wire/handshake.js +173 -12
- package/dist/src/servers/whatsapp-wire/handshake.js.map +1 -1
- package/dist/src/servers/whatsapp-wire/signal.js +3 -3
- package/dist/src/servers/whatsapp-wire/signal.js.map +1 -1
- package/dist/src/servers/whatsapp.d.ts +7 -0
- package/dist/src/servers/whatsapp.js +149 -24
- package/dist/src/servers/whatsapp.js.map +1 -1
- package/dist/src/servers/zalo.d.ts +1 -0
- package/dist/src/servers/zalo.js +193 -82
- package/dist/src/servers/zalo.js.map +1 -1
- package/docs/channel-setup.md +185 -43
- package/fixtures/examples/crabline.example.yaml +3 -2
- package/package.json +18 -10
- package/fixtures/examples/openclaw-bridge.yaml +0 -125
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { constants as fsConstants } from "node:fs";
|
|
2
|
+
import { chmod, lstat, mkdir, open, readFile, readlink, realpath } from "node:fs/promises";
|
|
3
|
+
import { userInfo } from "node:os";
|
|
2
4
|
import path from "node:path";
|
|
3
5
|
import { lock } from "proper-lockfile";
|
|
6
|
+
import { createProcessOwnedLockFileSystem } from "../platform/process-owned-lock.js";
|
|
7
|
+
import { isManagedRecorderDirectory } from "../platform/recorder-directory.js";
|
|
8
|
+
import { createOwnerOnlyWindowsDirectory, readWindowsDirectoryNamespaceSecuritySnapshot, readWindowsDirectorySecuritySnapshot, } from "../platform/windows-acl.js";
|
|
9
|
+
import { secureCachedWindowsLockRoot, } from "../platform/windows-lock-root.js";
|
|
4
10
|
function sleep(ms, signal) {
|
|
5
11
|
if (signal?.aborted) {
|
|
6
12
|
return Promise.resolve();
|
|
@@ -17,6 +23,8 @@ function sleep(ms, signal) {
|
|
|
17
23
|
}
|
|
18
24
|
const pendingAppends = new Map();
|
|
19
25
|
const recordIdentityIndexes = new Map();
|
|
26
|
+
const securedWindowsLockRoots = new Map();
|
|
27
|
+
const securedWindowsRecorderDirectories = new Map();
|
|
20
28
|
const MAX_RECORD_IDENTITY_INDEXES = 128;
|
|
21
29
|
const MAX_RECENT_RECORD_KEYS = 4096;
|
|
22
30
|
const RECORDER_BATCH_VERSION = 1;
|
|
@@ -25,6 +33,14 @@ const RECORDER_LOCK_UPDATE_MS = 10_000;
|
|
|
25
33
|
const RECORDER_ROTATION_ATTEMPTS = 3;
|
|
26
34
|
class RecorderRotatedError extends Error {
|
|
27
35
|
}
|
|
36
|
+
export class ProviderRecorderCommittedError extends AggregateError {
|
|
37
|
+
committed = true;
|
|
38
|
+
indeterminate = true;
|
|
39
|
+
constructor(filePath, cause, relatedErrors = []) {
|
|
40
|
+
super([cause, ...relatedErrors], `Provider recorder append was published for "${filePath}", but durability or identity confirmation failed.`, { cause });
|
|
41
|
+
this.name = "ProviderRecorderCommittedError";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
28
44
|
const CONTINUITY_BYTES = 4096;
|
|
29
45
|
const MAX_INCREMENTAL_READ_BYTES = 256 * 1024;
|
|
30
46
|
const MAX_PENDING_RECORD_BYTES = 4 * 1024 * 1024;
|
|
@@ -38,6 +54,20 @@ function createIncrementalReadState() {
|
|
|
38
54
|
pending: Buffer.alloc(0),
|
|
39
55
|
};
|
|
40
56
|
}
|
|
57
|
+
function snapshotIncrementalReadState(state) {
|
|
58
|
+
return {
|
|
59
|
+
...state,
|
|
60
|
+
identity: state.identity ? { ...state.identity } : undefined,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function restoreIncrementalReadState(state, snapshot) {
|
|
64
|
+
state.caughtUp = snapshot.caughtUp;
|
|
65
|
+
state.continuity = snapshot.continuity;
|
|
66
|
+
state.generation = snapshot.generation;
|
|
67
|
+
state.identity = snapshot.identity;
|
|
68
|
+
state.offset = snapshot.offset;
|
|
69
|
+
state.pending = snapshot.pending;
|
|
70
|
+
}
|
|
41
71
|
export function createRecordedInboundCursor() {
|
|
42
72
|
return {
|
|
43
73
|
buffered: [],
|
|
@@ -188,11 +218,11 @@ function consumeRecordedChunk(state, chunk) {
|
|
|
188
218
|
}
|
|
189
219
|
return events;
|
|
190
220
|
}
|
|
191
|
-
async function appendJsonLine(filePath, line) {
|
|
221
|
+
async function appendJsonLine(filePath, line, firstCreatedDirectory) {
|
|
192
222
|
for (let attempt = 0;; attempt++) {
|
|
193
223
|
try {
|
|
194
|
-
await serializeAppend(filePath, async (publicationPath, logicalPath) => {
|
|
195
|
-
await appendCommittedLine(publicationPath, logicalPath, line, true);
|
|
224
|
+
await serializeAppend(filePath, async (publicationPath, logicalPath, _lockCreatedFile, lockedIdentity) => {
|
|
225
|
+
await appendCommittedLine(publicationPath, logicalPath, line, true, lockedIdentity, firstCreatedDirectory);
|
|
196
226
|
});
|
|
197
227
|
return;
|
|
198
228
|
}
|
|
@@ -212,6 +242,7 @@ async function readRecorderFileIdentity(filePath) {
|
|
|
212
242
|
return {
|
|
213
243
|
dev: stats.dev,
|
|
214
244
|
ino: stats.ino,
|
|
245
|
+
nlink: stats.nlink,
|
|
215
246
|
};
|
|
216
247
|
}
|
|
217
248
|
catch (error) {
|
|
@@ -224,6 +255,19 @@ async function readRecorderFileIdentity(filePath) {
|
|
|
224
255
|
function sameRecorderFileIdentity(left, right) {
|
|
225
256
|
return left?.dev === right?.dev && left?.ino === right?.ino;
|
|
226
257
|
}
|
|
258
|
+
function sameRecorderLockIdentity(left, right) {
|
|
259
|
+
return sameRecorderFileIdentity(left, right) && left?.nlink === right?.nlink;
|
|
260
|
+
}
|
|
261
|
+
function requireRecorderHandleIdentity(stats) {
|
|
262
|
+
if (stats.isFile?.() === false) {
|
|
263
|
+
throw new Error("Recorder path is not a regular file.");
|
|
264
|
+
}
|
|
265
|
+
return {
|
|
266
|
+
dev: stats.dev,
|
|
267
|
+
ino: stats.ino,
|
|
268
|
+
nlink: stats.nlink,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
227
271
|
async function resolveRecorderPublicationPath(filePath) {
|
|
228
272
|
try {
|
|
229
273
|
return await realpath(filePath);
|
|
@@ -247,6 +291,94 @@ async function resolveRecorderPublicationPath(filePath) {
|
|
|
247
291
|
}
|
|
248
292
|
return path.join(await realpath(path.dirname(filePath)), path.basename(filePath));
|
|
249
293
|
}
|
|
294
|
+
async function openRecorderForAppend(filePath) {
|
|
295
|
+
const createFlags = process.platform === "win32"
|
|
296
|
+
? "wx+"
|
|
297
|
+
: fsConstants.O_RDWR |
|
|
298
|
+
fsConstants.O_APPEND |
|
|
299
|
+
fsConstants.O_CREAT |
|
|
300
|
+
fsConstants.O_EXCL |
|
|
301
|
+
fsConstants.O_NONBLOCK |
|
|
302
|
+
fsConstants.O_NOFOLLOW;
|
|
303
|
+
const existingFlags = process.platform === "win32"
|
|
304
|
+
? "r+"
|
|
305
|
+
: fsConstants.O_RDWR | fsConstants.O_APPEND | fsConstants.O_NONBLOCK | fsConstants.O_NOFOLLOW;
|
|
306
|
+
try {
|
|
307
|
+
return {
|
|
308
|
+
created: true,
|
|
309
|
+
handle: await open(filePath, createFlags, 0o600),
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
catch (error) {
|
|
313
|
+
if (error.code !== "EEXIST") {
|
|
314
|
+
throw error;
|
|
315
|
+
}
|
|
316
|
+
return {
|
|
317
|
+
created: false,
|
|
318
|
+
handle: await open(filePath, existingFlags, 0o600),
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
async function appendRecorderData(handle, data) {
|
|
323
|
+
if (process.platform !== "win32") {
|
|
324
|
+
await handle.writeFile(data, "utf8");
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
const bytes = Buffer.from(data, "utf8");
|
|
328
|
+
const position = (await handle.stat()).size;
|
|
329
|
+
let offset = 0;
|
|
330
|
+
while (offset < bytes.length) {
|
|
331
|
+
const { bytesWritten } = await handle.write(bytes, offset, bytes.length - offset, position + offset);
|
|
332
|
+
if (bytesWritten === 0) {
|
|
333
|
+
throw new Error("Could not append recorder data.");
|
|
334
|
+
}
|
|
335
|
+
offset += bytesWritten;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
async function syncParentDirectory(filePath) {
|
|
339
|
+
if (process.platform === "win32") {
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
const directory = await open(path.dirname(filePath), "r");
|
|
343
|
+
try {
|
|
344
|
+
await directory.sync();
|
|
345
|
+
}
|
|
346
|
+
finally {
|
|
347
|
+
await directory.close();
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
async function syncRecorderPathAncestry(filePath, firstCreatedDirectory) {
|
|
351
|
+
if (process.platform === "win32") {
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
const resolvedFilePath = path.resolve(filePath);
|
|
355
|
+
let currentPath = resolvedFilePath;
|
|
356
|
+
const syncThroughPath = firstCreatedDirectory === undefined ? undefined : path.resolve(firstCreatedDirectory);
|
|
357
|
+
for (;;) {
|
|
358
|
+
const directoryPath = path.dirname(currentPath);
|
|
359
|
+
const mandatory = syncThroughPath !== undefined || currentPath === resolvedFilePath;
|
|
360
|
+
try {
|
|
361
|
+
await syncParentDirectory(currentPath);
|
|
362
|
+
}
|
|
363
|
+
catch (error) {
|
|
364
|
+
const code = error.code;
|
|
365
|
+
if (!mandatory && (code === "EACCES" || code === "EPERM")) {
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
throw error;
|
|
369
|
+
}
|
|
370
|
+
if (syncThroughPath === undefined) {
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
if (currentPath === syncThroughPath) {
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
if (path.dirname(directoryPath) === directoryPath) {
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
currentPath = directoryPath;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
250
382
|
async function prepareRecorderTailForAppend(handle) {
|
|
251
383
|
const stats = await handle.stat();
|
|
252
384
|
if (stats.size === 0) {
|
|
@@ -265,8 +397,8 @@ async function prepareRecorderTailForAppend(handle) {
|
|
|
265
397
|
const tailStart = stats.size - windowSize + lastNewline + 1;
|
|
266
398
|
const tail = tailWindow.subarray(lastNewline + 1).toString("utf8");
|
|
267
399
|
try {
|
|
268
|
-
|
|
269
|
-
await handle
|
|
400
|
+
parseRecordedLine(tail);
|
|
401
|
+
await appendRecorderData(handle, "\n");
|
|
270
402
|
return true;
|
|
271
403
|
}
|
|
272
404
|
catch (error) {
|
|
@@ -277,11 +409,15 @@ async function prepareRecorderTailForAppend(handle) {
|
|
|
277
409
|
return true;
|
|
278
410
|
}
|
|
279
411
|
}
|
|
280
|
-
async function prepareRecorderPathForAppend(publicationPath, logicalPath, durable) {
|
|
281
|
-
const
|
|
282
|
-
const
|
|
283
|
-
const recorderIdentity =
|
|
412
|
+
async function prepareRecorderPathForAppend(publicationPath, logicalPath, durable, expectedIdentity) {
|
|
413
|
+
const opened = await openRecorderForAppend(publicationPath);
|
|
414
|
+
const { handle } = opened;
|
|
415
|
+
const recorderIdentity = requireRecorderHandleIdentity(await handle.stat({ bigint: true }));
|
|
284
416
|
try {
|
|
417
|
+
if (expectedIdentity !== undefined &&
|
|
418
|
+
!sameRecorderFileIdentity(expectedIdentity, recorderIdentity)) {
|
|
419
|
+
throw new RecorderRotatedError("Recorder rotated before preparing a committed line.");
|
|
420
|
+
}
|
|
285
421
|
const changed = await prepareRecorderTailForAppend(handle);
|
|
286
422
|
if (changed && durable) {
|
|
287
423
|
await handle.sync();
|
|
@@ -293,35 +429,299 @@ async function prepareRecorderPathForAppend(publicationPath, logicalPath, durabl
|
|
|
293
429
|
if (!sameRecorderFileIdentity(recorderIdentity, await readRecorderFileIdentity(await resolveRecorderPublicationPath(logicalPath)))) {
|
|
294
430
|
throw new RecorderRotatedError("Recorder rotated while preparing a committed line.");
|
|
295
431
|
}
|
|
296
|
-
return recorderIdentity;
|
|
432
|
+
return { created: opened.created, identity: recorderIdentity };
|
|
297
433
|
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
434
|
+
function providerRecorderCloseError(filePath, operationError, closeError) {
|
|
435
|
+
return new AggregateError([operationError, closeError], `Provider recorder operation and file close both failed for "${filePath}".`, { cause: closeError });
|
|
436
|
+
}
|
|
437
|
+
async function appendCommittedLine(publicationPath, logicalPath, line, durable, expectedIdentity, firstCreatedDirectory) {
|
|
438
|
+
const opened = await openRecorderForAppend(publicationPath);
|
|
439
|
+
const { handle } = opened;
|
|
440
|
+
const recorderIdentity = requireRecorderHandleIdentity(await handle.stat({ bigint: true }));
|
|
441
|
+
let published = false;
|
|
442
|
+
let operationError;
|
|
302
443
|
try {
|
|
303
444
|
if (expectedIdentity !== undefined &&
|
|
304
445
|
!sameRecorderFileIdentity(expectedIdentity, recorderIdentity)) {
|
|
305
446
|
throw new RecorderRotatedError("Recorder rotated before appending a committed line.");
|
|
306
447
|
}
|
|
307
448
|
await prepareRecorderTailForAppend(handle);
|
|
308
|
-
|
|
449
|
+
published = true;
|
|
450
|
+
await appendRecorderData(handle, line);
|
|
309
451
|
if (durable) {
|
|
310
452
|
await handle.sync();
|
|
311
453
|
}
|
|
454
|
+
if (durable) {
|
|
455
|
+
const firstCreatedPath = firstCreatedDirectory ?? (opened.created ? publicationPath : undefined);
|
|
456
|
+
await syncRecorderPathAncestry(publicationPath, firstCreatedPath);
|
|
457
|
+
}
|
|
312
458
|
}
|
|
313
|
-
|
|
459
|
+
catch (error) {
|
|
460
|
+
operationError = published ? new ProviderRecorderCommittedError(logicalPath, error) : error;
|
|
461
|
+
}
|
|
462
|
+
try {
|
|
314
463
|
await handle.close();
|
|
315
464
|
}
|
|
316
|
-
|
|
465
|
+
catch (closeError) {
|
|
466
|
+
if (operationError !== undefined) {
|
|
467
|
+
if (published) {
|
|
468
|
+
throw new ProviderRecorderCommittedError(logicalPath, operationError, [closeError]);
|
|
469
|
+
}
|
|
470
|
+
throw providerRecorderCloseError(logicalPath, operationError, closeError);
|
|
471
|
+
}
|
|
472
|
+
throw published ? new ProviderRecorderCommittedError(logicalPath, closeError) : closeError;
|
|
473
|
+
}
|
|
474
|
+
if (operationError !== undefined) {
|
|
475
|
+
throw operationError;
|
|
476
|
+
}
|
|
477
|
+
let publishedIdentity;
|
|
478
|
+
try {
|
|
479
|
+
publishedIdentity = await readRecorderFileIdentity(await resolveRecorderPublicationPath(logicalPath));
|
|
480
|
+
}
|
|
481
|
+
catch (error) {
|
|
482
|
+
throw new ProviderRecorderCommittedError(logicalPath, error);
|
|
483
|
+
}
|
|
484
|
+
if (!sameRecorderFileIdentity(recorderIdentity, publishedIdentity)) {
|
|
317
485
|
throw new RecorderRotatedError("Recorder rotated while appending a committed line.");
|
|
318
486
|
}
|
|
319
487
|
}
|
|
320
|
-
function recorderLockReleaseError(filePath, operationError,
|
|
321
|
-
return new AggregateError([operationError,
|
|
488
|
+
function recorderLockReleaseError(filePath, operationError, releaseErrors) {
|
|
489
|
+
return new AggregateError([operationError, ...releaseErrors], `Recorder append and lock release both failed for "${filePath}".`, { cause: operationError });
|
|
322
490
|
}
|
|
323
|
-
|
|
324
|
-
|
|
491
|
+
function recorderErrorDetail(error) {
|
|
492
|
+
if (error instanceof AggregateError) {
|
|
493
|
+
return error.errors.map(recorderErrorDetail).join("; ");
|
|
494
|
+
}
|
|
495
|
+
return error instanceof Error ? error.message : String(error);
|
|
496
|
+
}
|
|
497
|
+
function reportRecorderLockReleaseFailure(filePath, releaseError) {
|
|
498
|
+
const detail = recorderErrorDetail(releaseError);
|
|
499
|
+
try {
|
|
500
|
+
process.emitWarning(`Provider recorder append committed but lock cleanup failed for "${filePath}": ${detail}`, {
|
|
501
|
+
code: "CRABLINE_RECORDER_LOCK_CLEANUP",
|
|
502
|
+
type: "ProviderRecorderWarning",
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
catch {
|
|
506
|
+
// Cleanup reporting must not change the result of a committed append.
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
async function verifyProviderRecorderLockAncestry(directory, currentUserId) {
|
|
510
|
+
let currentPath = path.resolve(directory);
|
|
511
|
+
for (;;) {
|
|
512
|
+
const current = await lstat(currentPath, { bigint: true });
|
|
513
|
+
const ownerIsTrusted = current.uid === BigInt(currentUserId) || current.uid === 0n;
|
|
514
|
+
if (current.isSymbolicLink()) {
|
|
515
|
+
if (!ownerIsTrusted) {
|
|
516
|
+
throw new Error("Provider recorder lock directory parent namespace is not trusted.");
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
else {
|
|
520
|
+
const peerWritable = (current.mode & 18n) !== 0n;
|
|
521
|
+
const sticky = (current.mode & 512n) !== 0n;
|
|
522
|
+
if (!current.isDirectory() || !ownerIsTrusted || (peerWritable && !sticky)) {
|
|
523
|
+
throw new Error("Provider recorder lock directory parent namespace is not trusted.");
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
const parent = path.dirname(currentPath);
|
|
527
|
+
if (parent === currentPath) {
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
currentPath = parent;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
async function prepareProviderRecorderLockRoot(root, currentUserId) {
|
|
534
|
+
let existingPath = path.resolve(root);
|
|
535
|
+
const missingComponents = [];
|
|
536
|
+
for (;;) {
|
|
537
|
+
try {
|
|
538
|
+
await lstat(existingPath);
|
|
539
|
+
break;
|
|
540
|
+
}
|
|
541
|
+
catch (error) {
|
|
542
|
+
if (error.code !== "ENOENT") {
|
|
543
|
+
throw error;
|
|
544
|
+
}
|
|
545
|
+
const parent = path.dirname(existingPath);
|
|
546
|
+
if (parent === existingPath) {
|
|
547
|
+
throw error;
|
|
548
|
+
}
|
|
549
|
+
missingComponents.unshift(path.basename(existingPath));
|
|
550
|
+
existingPath = parent;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
await verifyProviderRecorderLockAncestry(existingPath, currentUserId);
|
|
554
|
+
let canonicalPath = await realpath(existingPath);
|
|
555
|
+
if (canonicalPath !== existingPath) {
|
|
556
|
+
await verifyProviderRecorderLockAncestry(canonicalPath, currentUserId);
|
|
557
|
+
}
|
|
558
|
+
for (const component of missingComponents) {
|
|
559
|
+
canonicalPath = path.join(canonicalPath, component);
|
|
560
|
+
try {
|
|
561
|
+
await mkdir(canonicalPath, { mode: 0o700 });
|
|
562
|
+
}
|
|
563
|
+
catch (error) {
|
|
564
|
+
if (error.code !== "EEXIST") {
|
|
565
|
+
throw error;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
const identity = await lstat(canonicalPath, { bigint: true });
|
|
569
|
+
if (!identity.isDirectory() ||
|
|
570
|
+
identity.isSymbolicLink() ||
|
|
571
|
+
identity.uid !== BigInt(currentUserId)) {
|
|
572
|
+
throw new Error("Provider recorder lock directory is not privately owned.");
|
|
573
|
+
}
|
|
574
|
+
await chmod(canonicalPath, 0o700);
|
|
575
|
+
}
|
|
576
|
+
return canonicalPath;
|
|
577
|
+
}
|
|
578
|
+
export async function secureProviderRecorderLockRoot(root, currentUserId, options = {}) {
|
|
579
|
+
if ((options.platform ?? process.platform) === "win32") {
|
|
580
|
+
const cacheKey = path.win32.normalize(path.resolve(root)).toLowerCase();
|
|
581
|
+
return secureCachedWindowsLockRoot({
|
|
582
|
+
cache: securedWindowsLockRoots,
|
|
583
|
+
cacheKey,
|
|
584
|
+
createDirectory: () => (options.createWindowsDirectory ?? createOwnerOnlyWindowsDirectory)(root),
|
|
585
|
+
errorPrefix: "Provider recorder lock directory",
|
|
586
|
+
readSecuritySnapshot: () => (options.readWindowsDirectorySecuritySnapshot ?? readWindowsDirectorySecuritySnapshot)(root),
|
|
587
|
+
root,
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
if (currentUserId === undefined) {
|
|
591
|
+
throw new Error("Provider recorder identity locking requires a current user id.");
|
|
592
|
+
}
|
|
593
|
+
const canonicalRoot = await prepareProviderRecorderLockRoot(root, currentUserId);
|
|
594
|
+
const handle = await open(canonicalRoot, fsConstants.O_RDONLY | fsConstants.O_DIRECTORY | fsConstants.O_NOFOLLOW);
|
|
595
|
+
try {
|
|
596
|
+
const identity = await handle.stat({ bigint: true });
|
|
597
|
+
const current = await lstat(canonicalRoot, { bigint: true });
|
|
598
|
+
if (!identity.isDirectory() ||
|
|
599
|
+
!current.isDirectory() ||
|
|
600
|
+
identity.dev !== current.dev ||
|
|
601
|
+
identity.ino !== current.ino ||
|
|
602
|
+
identity.uid !== BigInt(currentUserId) ||
|
|
603
|
+
current.uid !== BigInt(currentUserId)) {
|
|
604
|
+
throw new Error("Provider recorder lock directory is not privately owned.");
|
|
605
|
+
}
|
|
606
|
+
if ((identity.mode & 511n) !== 448n) {
|
|
607
|
+
await handle.chmod(0o700);
|
|
608
|
+
const secured = await handle.stat({ bigint: true });
|
|
609
|
+
if ((secured.mode & 511n) !== 448n) {
|
|
610
|
+
throw new Error("Provider recorder lock directory permissions are not private.");
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
finally {
|
|
615
|
+
await handle.close();
|
|
616
|
+
}
|
|
617
|
+
return canonicalRoot;
|
|
618
|
+
}
|
|
619
|
+
async function secureProviderRecorderWindowsDirectory(root) {
|
|
620
|
+
const cacheKey = path.win32.normalize(path.resolve(root)).toLowerCase();
|
|
621
|
+
return secureCachedWindowsLockRoot({
|
|
622
|
+
cache: securedWindowsRecorderDirectories,
|
|
623
|
+
cacheKey,
|
|
624
|
+
createDirectory: async () => {
|
|
625
|
+
try {
|
|
626
|
+
const identity = await lstat(root);
|
|
627
|
+
if (!identity.isDirectory() || identity.isSymbolicLink()) {
|
|
628
|
+
throw new Error("Provider recorder Windows directory is not a private directory.");
|
|
629
|
+
}
|
|
630
|
+
if (isManagedRecorderDirectory(root)) {
|
|
631
|
+
await createOwnerOnlyWindowsDirectory(root);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
catch (error) {
|
|
635
|
+
if (error.code !== "ENOENT") {
|
|
636
|
+
throw error;
|
|
637
|
+
}
|
|
638
|
+
await createOwnerOnlyWindowsDirectory(root);
|
|
639
|
+
}
|
|
640
|
+
},
|
|
641
|
+
errorPrefix: "Provider recorder Windows directory",
|
|
642
|
+
readSecuritySnapshot: () => readWindowsDirectoryNamespaceSecuritySnapshot(root),
|
|
643
|
+
root,
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
async function prepareRecorderDirectory(filePath) {
|
|
647
|
+
const directory = path.dirname(filePath);
|
|
648
|
+
if (process.platform === "win32") {
|
|
649
|
+
await secureProviderRecorderWindowsDirectory(directory);
|
|
650
|
+
return undefined;
|
|
651
|
+
}
|
|
652
|
+
const createdDirectory = await mkdir(directory, { recursive: true });
|
|
653
|
+
return createdDirectory === undefined ? undefined : await realpath(createdDirectory);
|
|
654
|
+
}
|
|
655
|
+
function recorderLockRootUnavailable(error) {
|
|
656
|
+
const code = error.code;
|
|
657
|
+
return (code === "EACCES" ||
|
|
658
|
+
code === "EDQUOT" ||
|
|
659
|
+
code === "ENOSPC" ||
|
|
660
|
+
code === "EPERM" ||
|
|
661
|
+
code === "EROFS");
|
|
662
|
+
}
|
|
663
|
+
function adjacentRecorderLockRoot(filePath, currentUserId) {
|
|
664
|
+
return path.join(path.dirname(filePath), currentUserId === undefined
|
|
665
|
+
? ".crabline-provider-recorder-locks"
|
|
666
|
+
: `.crabline-provider-recorder-locks-${currentUserId}`);
|
|
667
|
+
}
|
|
668
|
+
function recorderIdentityLockPath(root, identity) {
|
|
669
|
+
return path.join(root, `recorder-${identity.dev}-${identity.ino}`);
|
|
670
|
+
}
|
|
671
|
+
async function recorderIdentityLockTargets(filePath, identity) {
|
|
672
|
+
const currentUserId = process.platform === "win32" ? undefined : process.geteuid?.();
|
|
673
|
+
let sharedRoot;
|
|
674
|
+
try {
|
|
675
|
+
const account = userInfo();
|
|
676
|
+
sharedRoot =
|
|
677
|
+
process.platform === "win32"
|
|
678
|
+
? path.join(account.homedir, "AppData", "Local", "Crabline", "locks", "provider-recorder")
|
|
679
|
+
: path.join(account.homedir, ".cache", "crabline", "locks", "provider-recorder");
|
|
680
|
+
}
|
|
681
|
+
catch {
|
|
682
|
+
// Arbitrary container UIDs may not have an OS account entry.
|
|
683
|
+
}
|
|
684
|
+
if (sharedRoot) {
|
|
685
|
+
try {
|
|
686
|
+
return {
|
|
687
|
+
...(identity.nlink === 1n
|
|
688
|
+
? { fallbackRoot: adjacentRecorderLockRoot(filePath, currentUserId) }
|
|
689
|
+
: {}),
|
|
690
|
+
preferred: recorderIdentityLockPath(await secureProviderRecorderLockRoot(sharedRoot, currentUserId), identity),
|
|
691
|
+
userId: currentUserId,
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
catch (error) {
|
|
695
|
+
if (!recorderLockRootUnavailable(error)) {
|
|
696
|
+
throw error;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
if (identity.nlink > 1n) {
|
|
701
|
+
throw new Error("Provider recorder hardlinks require a writable shared per-user lock directory.");
|
|
702
|
+
}
|
|
703
|
+
const adjacentRoot = await secureProviderRecorderLockRoot(adjacentRecorderLockRoot(filePath, currentUserId), currentUserId);
|
|
704
|
+
return {
|
|
705
|
+
preferred: recorderIdentityLockPath(adjacentRoot, identity),
|
|
706
|
+
userId: currentUserId,
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
async function acquireRecorderIdentityLock(filePath, identity) {
|
|
710
|
+
const targets = await recorderIdentityLockTargets(filePath, identity);
|
|
711
|
+
try {
|
|
712
|
+
return await acquireRecorderLock(targets.preferred);
|
|
713
|
+
}
|
|
714
|
+
catch (error) {
|
|
715
|
+
if (!targets.fallbackRoot || !recorderLockRootUnavailable(error)) {
|
|
716
|
+
throw error;
|
|
717
|
+
}
|
|
718
|
+
const fallbackRoot = await secureProviderRecorderLockRoot(targets.fallbackRoot, targets.userId);
|
|
719
|
+
return await acquireRecorderLock(recorderIdentityLockPath(fallbackRoot, identity));
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
async function acquireRecorderLock(filePath) {
|
|
723
|
+
return await lock(filePath, {
|
|
724
|
+
fs: createProcessOwnedLockFileSystem(),
|
|
325
725
|
realpath: false,
|
|
326
726
|
retries: {
|
|
327
727
|
factor: 1,
|
|
@@ -332,28 +732,97 @@ async function withRecorderLock(filePath, operation) {
|
|
|
332
732
|
stale: RECORDER_LOCK_STALE_MS,
|
|
333
733
|
update: RECORDER_LOCK_UPDATE_MS,
|
|
334
734
|
});
|
|
735
|
+
}
|
|
736
|
+
async function ensureRecorderExistsForLock(filePath) {
|
|
737
|
+
if (await readRecorderFileIdentity(filePath)) {
|
|
738
|
+
return false;
|
|
739
|
+
}
|
|
740
|
+
let handle;
|
|
741
|
+
try {
|
|
742
|
+
handle = await open(filePath, "ax", 0o600);
|
|
743
|
+
}
|
|
744
|
+
catch (error) {
|
|
745
|
+
if (error.code === "EEXIST") {
|
|
746
|
+
return false;
|
|
747
|
+
}
|
|
748
|
+
throw error;
|
|
749
|
+
}
|
|
750
|
+
await handle.close();
|
|
751
|
+
return true;
|
|
752
|
+
}
|
|
753
|
+
async function releaseRecorderLocks(releases) {
|
|
754
|
+
const errors = [];
|
|
755
|
+
for (const release of releases.reverse()) {
|
|
756
|
+
try {
|
|
757
|
+
await release();
|
|
758
|
+
}
|
|
759
|
+
catch (error) {
|
|
760
|
+
errors.push(error);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
return errors;
|
|
764
|
+
}
|
|
765
|
+
async function withRecorderLock(filePath, operation) {
|
|
766
|
+
const releases = [];
|
|
767
|
+
let lockCreatedFile = false;
|
|
768
|
+
let lockedIdentity;
|
|
769
|
+
try {
|
|
770
|
+
releases.push(await acquireRecorderLock(filePath));
|
|
771
|
+
for (let attempt = 0; attempt < RECORDER_ROTATION_ATTEMPTS; attempt += 1) {
|
|
772
|
+
lockCreatedFile = await ensureRecorderExistsForLock(filePath);
|
|
773
|
+
const identity = await readRecorderFileIdentity(filePath);
|
|
774
|
+
if (!identity) {
|
|
775
|
+
continue;
|
|
776
|
+
}
|
|
777
|
+
const releaseIdentityLock = await acquireRecorderIdentityLock(filePath, identity);
|
|
778
|
+
releases.push(releaseIdentityLock);
|
|
779
|
+
const currentIdentity = await readRecorderFileIdentity(filePath);
|
|
780
|
+
if (sameRecorderLockIdentity(identity, currentIdentity)) {
|
|
781
|
+
lockedIdentity = identity;
|
|
782
|
+
break;
|
|
783
|
+
}
|
|
784
|
+
const registeredIdentityRelease = releases.pop();
|
|
785
|
+
const releaseErrors = await releaseRecorderLocks(registeredIdentityRelease ? [registeredIdentityRelease] : []);
|
|
786
|
+
if (releaseErrors.length > 0) {
|
|
787
|
+
throw recorderLockReleaseError(filePath, new RecorderRotatedError("Recorder rotated while acquiring its identity lock."), releaseErrors);
|
|
788
|
+
}
|
|
789
|
+
if (attempt + 1 >= RECORDER_ROTATION_ATTEMPTS) {
|
|
790
|
+
throw new RecorderRotatedError("Recorder kept rotating while acquiring its identity lock.");
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
if (!lockedIdentity) {
|
|
794
|
+
throw new RecorderRotatedError("Recorder disappeared while acquiring its identity lock.");
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
catch (error) {
|
|
798
|
+
const releaseErrors = await releaseRecorderLocks(releases);
|
|
799
|
+
if (releaseErrors.length > 0) {
|
|
800
|
+
throw recorderLockReleaseError(filePath, error, releaseErrors);
|
|
801
|
+
}
|
|
802
|
+
throw error;
|
|
803
|
+
}
|
|
335
804
|
let operationFailed = false;
|
|
336
805
|
let operationError;
|
|
337
806
|
let result;
|
|
338
807
|
try {
|
|
339
|
-
result = await operation();
|
|
808
|
+
result = await operation(lockCreatedFile, lockedIdentity);
|
|
340
809
|
}
|
|
341
810
|
catch (error) {
|
|
342
811
|
operationFailed = true;
|
|
343
812
|
operationError = error;
|
|
344
813
|
}
|
|
345
|
-
|
|
346
|
-
await release();
|
|
347
|
-
}
|
|
348
|
-
catch (releaseError) {
|
|
349
|
-
if (operationFailed) {
|
|
350
|
-
throw recorderLockReleaseError(filePath, operationError, releaseError);
|
|
351
|
-
}
|
|
352
|
-
throw releaseError;
|
|
353
|
-
}
|
|
814
|
+
const releaseErrors = await releaseRecorderLocks(releases);
|
|
354
815
|
if (operationFailed) {
|
|
816
|
+
if (releaseErrors.length > 0) {
|
|
817
|
+
throw recorderLockReleaseError(filePath, operationError, releaseErrors);
|
|
818
|
+
}
|
|
355
819
|
throw operationError;
|
|
356
820
|
}
|
|
821
|
+
if (releaseErrors.length > 0) {
|
|
822
|
+
reportRecorderLockReleaseFailure(filePath, releaseErrors.length === 1
|
|
823
|
+
? releaseErrors[0]
|
|
824
|
+
: new AggregateError(releaseErrors, "Multiple recorder lock cleanup operations failed."));
|
|
825
|
+
}
|
|
357
826
|
return result;
|
|
358
827
|
}
|
|
359
828
|
async function serializeAppend(filePath, operation) {
|
|
@@ -364,7 +833,7 @@ async function serializeAppend(filePath, operation) {
|
|
|
364
833
|
const current = previous
|
|
365
834
|
.catch(() => { })
|
|
366
835
|
.then(async () => {
|
|
367
|
-
result = await withRecorderLock(key, async () => await operation(key, logicalPath));
|
|
836
|
+
result = await withRecorderLock(key, async (lockCreatedFile, lockedIdentity) => await operation(key, logicalPath, lockCreatedFile, lockedIdentity));
|
|
368
837
|
});
|
|
369
838
|
pendingAppends.set(key, current);
|
|
370
839
|
try {
|
|
@@ -378,6 +847,7 @@ async function serializeAppend(filePath, operation) {
|
|
|
378
847
|
}
|
|
379
848
|
}
|
|
380
849
|
async function readRecordedInboundAppend(filePath, state) {
|
|
850
|
+
const snapshot = snapshotIncrementalReadState(state);
|
|
381
851
|
let handle;
|
|
382
852
|
try {
|
|
383
853
|
handle = await open(filePath, "r");
|
|
@@ -393,7 +863,8 @@ async function readRecordedInboundAppend(filePath, state) {
|
|
|
393
863
|
}
|
|
394
864
|
try {
|
|
395
865
|
const stats = await handle.stat();
|
|
396
|
-
const
|
|
866
|
+
const identityStats = await handle.stat({ bigint: true });
|
|
867
|
+
const identity = { dev: identityStats.dev, ino: identityStats.ino };
|
|
397
868
|
const sameFile = state.identity?.dev === identity.dev && state.identity.ino === identity.ino;
|
|
398
869
|
const rotated = state.identity !== undefined && !sameFile;
|
|
399
870
|
let hasContinuity = stats.size >= state.offset;
|
|
@@ -430,12 +901,16 @@ async function readRecordedInboundAppend(filePath, state) {
|
|
|
430
901
|
state.caughtUp = reachedUnexpectedEof || position >= stats.size;
|
|
431
902
|
return events;
|
|
432
903
|
}
|
|
904
|
+
catch (error) {
|
|
905
|
+
restoreIncrementalReadState(state, snapshot);
|
|
906
|
+
throw error;
|
|
907
|
+
}
|
|
433
908
|
finally {
|
|
434
909
|
await handle.close();
|
|
435
910
|
}
|
|
436
911
|
}
|
|
437
912
|
export async function appendRecordedInbound(filePath, event) {
|
|
438
|
-
await
|
|
913
|
+
const firstCreatedDirectory = await prepareRecorderDirectory(filePath);
|
|
439
914
|
const recorded = {
|
|
440
915
|
...event,
|
|
441
916
|
recordedAt: new Date().toISOString(),
|
|
@@ -444,18 +919,18 @@ export async function appendRecordedInbound(filePath, event) {
|
|
|
444
919
|
if (Buffer.byteLength(line) > MAX_PENDING_RECORD_BYTES) {
|
|
445
920
|
throw new Error(`Recorder record exceeded ${MAX_PENDING_RECORD_BYTES} bytes without a newline.`);
|
|
446
921
|
}
|
|
447
|
-
await appendJsonLine(filePath, line);
|
|
922
|
+
await appendJsonLine(filePath, line, firstCreatedDirectory);
|
|
448
923
|
return recorded;
|
|
449
924
|
}
|
|
450
925
|
export async function appendRecordedInboundBatch(filePath, events) {
|
|
451
926
|
if (events.length === 0) {
|
|
452
927
|
return [];
|
|
453
928
|
}
|
|
454
|
-
await
|
|
929
|
+
const firstCreatedDirectory = await prepareRecorderDirectory(filePath);
|
|
455
930
|
for (let attempt = 0;; attempt++) {
|
|
456
931
|
try {
|
|
457
|
-
return await serializeAppend(filePath, async (publicationPath, logicalPath) => {
|
|
458
|
-
const generation = await prepareRecorderPathForAppend(publicationPath, logicalPath, true);
|
|
932
|
+
return await serializeAppend(filePath, async (publicationPath, logicalPath, _lockCreatedFile, lockedIdentity) => {
|
|
933
|
+
const generation = await prepareRecorderPathForAppend(publicationPath, logicalPath, true, lockedIdentity);
|
|
459
934
|
const seen = await syncRecordIdentityIndex(publicationPath);
|
|
460
935
|
const pendingIdentities = new Set();
|
|
461
936
|
const recorded = [];
|
|
@@ -480,10 +955,10 @@ export async function appendRecordedInboundBatch(filePath, events) {
|
|
|
480
955
|
if (Buffer.byteLength(line) > MAX_PENDING_RECORD_BYTES) {
|
|
481
956
|
throw new Error(`Recorder record exceeded ${MAX_PENDING_RECORD_BYTES} bytes without a newline.`);
|
|
482
957
|
}
|
|
483
|
-
await appendCommittedLine(publicationPath, logicalPath, line, true, generation);
|
|
958
|
+
await appendCommittedLine(publicationPath, logicalPath, line, true, generation.identity, firstCreatedDirectory);
|
|
484
959
|
await syncRecordIdentityIndex(publicationPath);
|
|
485
960
|
}
|
|
486
|
-
else if (!sameRecorderFileIdentity(generation, await readRecorderFileIdentity(await resolveRecorderPublicationPath(logicalPath)))) {
|
|
961
|
+
else if (!sameRecorderFileIdentity(generation.identity, await readRecorderFileIdentity(await resolveRecorderPublicationPath(logicalPath)))) {
|
|
487
962
|
throw new RecorderRotatedError("Recorder rotated before confirming a duplicate batch.");
|
|
488
963
|
}
|
|
489
964
|
return recorded;
|
|
@@ -519,6 +994,11 @@ async function syncRecordIdentityIndex(filePath) {
|
|
|
519
994
|
if (index.readState.generation !== generation) {
|
|
520
995
|
index.seen.clear();
|
|
521
996
|
generation = index.readState.generation;
|
|
997
|
+
index.readState.caughtUp = false;
|
|
998
|
+
index.readState.continuity = Buffer.alloc(0);
|
|
999
|
+
index.readState.offset = 0;
|
|
1000
|
+
index.readState.pending = Buffer.alloc(0);
|
|
1001
|
+
continue;
|
|
522
1002
|
}
|
|
523
1003
|
for (const event of appended) {
|
|
524
1004
|
rememberRecentRecord(index.seen, event);
|