@gajae-code/coding-agent 0.15.6 → 0.16.1
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 +99 -0
- package/dist/types/async/job-manager.d.ts +32 -1
- package/dist/types/browser-backend.d.ts +12 -0
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/root-flags.d.ts +14 -0
- package/dist/types/cli/update-cli.d.ts +14 -0
- package/dist/types/cli.d.ts +7 -0
- package/dist/types/commands/launch.d.ts +10 -0
- package/dist/types/commands/sdk.d.ts +2 -0
- package/dist/types/config/autorouting-tier-map.d.ts +24 -0
- package/dist/types/config/billing-path.d.ts +25 -0
- package/dist/types/config/config-schema-version.d.ts +13 -1
- package/dist/types/config/file-lock.d.ts +5 -0
- package/dist/types/config/model-registry.d.ts +22 -0
- package/dist/types/config/settings-schema.d.ts +27 -8
- package/dist/types/config/settings.d.ts +2 -0
- package/dist/types/coordinator-mcp/projection-scan.d.ts +8 -0
- package/dist/types/crash/record-loader.d.ts +15 -0
- package/dist/types/extensibility/extensions/types.d.ts +8 -3
- package/dist/types/gjc-runtime/launch-worktree-reservation.d.ts +21 -0
- package/dist/types/gjc-runtime/launch-worktree.d.ts +25 -1
- package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +2 -0
- package/dist/types/gjc-runtime/session-state-lock.d.ts +22 -0
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +66 -1
- package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +1 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +27 -0
- package/dist/types/main.d.ts +1 -0
- package/dist/types/master-mode/context.d.ts +21 -0
- package/dist/types/master-mode/first-request.d.ts +34 -0
- package/dist/types/master-mode/snapshot.d.ts +32 -0
- package/dist/types/modes/acp/acp-agent.d.ts +21 -21
- package/dist/types/modes/components/frictionless-onboarding-selector.d.ts +10 -0
- package/dist/types/modes/components/model-selector.d.ts +4 -0
- package/dist/types/modes/components/oauth-selector.d.ts +1 -0
- package/dist/types/modes/components/session-selector.d.ts +1 -1
- package/dist/types/modes/components/sessions-dashboard.d.ts +2 -1
- package/dist/types/modes/components/status-line/model-name.d.ts +18 -0
- package/dist/types/modes/components/status-line/priority-row.d.ts +34 -0
- package/dist/types/modes/components/status-line/segments.d.ts +10 -0
- package/dist/types/modes/interactive-mode.d.ts +16 -2
- package/dist/types/modes/print-mode.d.ts +17 -0
- package/dist/types/modes/theme/theme.d.ts +2 -1
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/modes/ui-language.d.ts +7 -1
- package/dist/types/sdk/broker/broker.d.ts +79 -2
- package/dist/types/sdk/broker/endpoint-authority.d.ts +24 -0
- package/dist/types/sdk/broker/ensure.d.ts +3 -0
- package/dist/types/sdk/broker/index.d.ts +3 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +6 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +40 -0
- package/dist/types/sdk/broker/master-capability.d.ts +10 -0
- package/dist/types/sdk/broker/session-index.d.ts +40 -11
- package/dist/types/sdk/broker/session-scope.d.ts +72 -0
- package/dist/types/sdk/broker/spawn-authority.d.ts +201 -0
- package/dist/types/sdk/broker/spawn-substrate.d.ts +30 -0
- package/dist/types/sdk/broker/startup-budget.d.ts +52 -0
- package/dist/types/sdk/broker/state-version.d.ts +21 -1
- package/dist/types/sdk/broker/worktree-occupancy.d.ts +28 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +3 -0
- package/dist/types/sdk/bus/discord-daemon.d.ts +1 -1
- package/dist/types/sdk/bus/index.d.ts +9 -1
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +1 -1
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +1 -1
- package/dist/types/sdk/bus/slack-conversation.d.ts +1 -1
- package/dist/types/sdk/bus/slack-daemon.d.ts +1 -1
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
- package/dist/types/sdk/cli/index.d.ts +1 -0
- package/dist/types/sdk/cli/master-cli.d.ts +63 -0
- package/dist/types/sdk/cli/rows.d.ts +12 -5
- package/dist/types/sdk/cli/session-cli.d.ts +20 -3
- package/dist/types/sdk/client/client.d.ts +3 -2
- package/dist/types/sdk/host/control/dispatch.d.ts +10 -0
- package/dist/types/sdk/host/control/index.d.ts +1 -1
- package/dist/types/sdk/host/control/operations.d.ts +2 -0
- package/dist/types/sdk/host/control/runtime-gate.d.ts +21 -7
- package/dist/types/sdk/host/events.d.ts +1 -0
- package/dist/types/sdk/host/host.d.ts +10 -0
- package/dist/types/sdk/host/query/cursor.d.ts +4 -2
- package/dist/types/sdk/host/query/handlers.d.ts +1 -0
- package/dist/types/sdk/host/query/revision-store.d.ts +1 -0
- package/dist/types/sdk/host/session-runtime.d.ts +70 -17
- package/dist/types/sdk/lifecycle/broker-client.d.ts +6 -0
- package/dist/types/sdk/lifecycle/service.d.ts +15 -3
- package/dist/types/sdk/lifecycle-session.d.ts +3 -1
- package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -5
- package/dist/types/sdk/receipt-state.d.ts +2 -0
- package/dist/types/sdk/router/session-router.d.ts +6 -0
- package/dist/types/sdk/session.d.ts +16 -0
- package/dist/types/sdk/startup-capability.d.ts +2 -1
- package/dist/types/sdk/turn-result.d.ts +6 -0
- package/dist/types/session/agent-session.d.ts +52 -7
- package/dist/types/session/credential-store-errors.d.ts +1 -0
- package/dist/types/session/fold-coordinator.d.ts +29 -2
- package/dist/types/session/session-manager.d.ts +79 -2
- package/dist/types/session/yield-queue.d.ts +5 -4
- package/dist/types/setup/frictionless-onboarding.d.ts +1 -0
- package/dist/types/tools/ask.d.ts +1 -0
- package/dist/types/tools/bash.d.ts +16 -0
- package/dist/types/tools/browser/launch.d.ts +10 -0
- package/dist/types/tools/browser/registry.d.ts +8 -0
- package/dist/types/tools/gh.d.ts +8 -0
- package/dist/types/tools/index.d.ts +10 -10
- package/dist/types/tools/job.d.ts +3 -1
- package/dist/types/utils/external-editor.d.ts +7 -0
- package/examples/extensions/README.md +1 -1
- package/examples/extensions/pirate.ts +7 -4
- package/package.json +7 -7
- package/scripts/generate-sdk-operation-inventory.ts +13 -1
- package/scripts/verify-gjc-sdk-canonicalization.ts +35 -1
- package/src/async/job-manager.ts +107 -9
- package/src/browser-backend.ts +29 -0
- package/src/cli/accounts-cli.ts +9 -5
- package/src/cli/args.ts +17 -0
- package/src/cli/fast-help.ts +1 -0
- package/src/cli/plugin-cli.ts +29 -0
- package/src/cli/root-flags.ts +5 -0
- package/src/cli/update-cli.ts +140 -23
- package/src/commands/launch.ts +86 -18
- package/src/commands/sdk.ts +135 -28
- package/src/config/autorouting-tier-map.ts +12 -0
- package/src/config/billing-path.ts +42 -0
- package/src/config/config-schema-version.ts +18 -1
- package/src/config/file-lock.ts +158 -83
- package/src/config/model-preset-registry.ts +178 -23
- package/src/config/model-profiles.ts +5 -5
- package/src/config/model-registry.ts +291 -42
- package/src/config/model-resolver.ts +5 -0
- package/src/config/settings-schema.ts +27 -8
- package/src/config/settings.ts +51 -15
- package/src/coordinator-mcp/projection-scan.ts +27 -6
- package/src/coordinator-mcp/question-state.ts +9 -11
- package/src/coordinator-mcp/server.ts +115 -21
- package/src/crash/index-store.ts +2 -0
- package/src/crash/record-loader.ts +10 -0
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
- package/src/extensibility/extensions/runner.ts +27 -0
- package/src/extensibility/extensions/types.ts +8 -3
- package/src/extensibility/gjc-plugins/installer.ts +51 -11
- package/src/extensibility/runtime-skill-discovery.ts +110 -3
- package/src/extensibility/skills.ts +22 -12
- package/src/gjc-runtime/launch-worktree-reservation.ts +242 -0
- package/src/gjc-runtime/launch-worktree.ts +285 -4
- package/src/gjc-runtime/managed-owner-supervisor.ts +63 -51
- package/src/gjc-runtime/session-state-lock.ts +213 -14
- package/src/gjc-runtime/session-state-sidecar.ts +938 -39
- package/src/gjc-runtime/tmux-owner-isolation-cli.ts +4 -7
- package/src/gjc-runtime/tmux-owner-isolation.ts +18 -3
- package/src/gjc-runtime/tmux-sessions.ts +252 -13
- package/src/hooks/skill-state.ts +6 -0
- package/src/internal-urls/docs-index.generated.ts +15 -15
- package/src/internal-urls/issue-pr-protocol.ts +4 -2
- package/src/main.ts +61 -3
- package/src/master-mode/context.ts +50 -0
- package/src/master-mode/first-request.ts +65 -0
- package/src/master-mode/snapshot.ts +205 -0
- package/src/modes/acp/acp-agent.ts +610 -161
- package/src/modes/components/footer.ts +4 -2
- package/src/modes/components/frictionless-onboarding-selector.ts +48 -2
- package/src/modes/components/model-selector.ts +76 -8
- package/src/modes/components/oauth-selector.ts +6 -1
- package/src/modes/components/session-selector.ts +23 -15
- package/src/modes/components/sessions-dashboard.ts +8 -5
- package/src/modes/components/settings-selector.ts +1 -0
- package/src/modes/components/status-line/model-name.ts +53 -0
- package/src/modes/components/status-line/priority-row.ts +107 -0
- package/src/modes/components/status-line/segments.ts +23 -9
- package/src/modes/components/tool-status-header.ts +151 -15
- package/src/modes/controllers/command-controller.ts +1 -1
- package/src/modes/controllers/event-controller.ts +30 -4
- package/src/modes/controllers/extension-ui-controller.ts +13 -13
- package/src/modes/controllers/input-controller.ts +30 -2
- package/src/modes/controllers/selector-controller.ts +200 -98
- package/src/modes/interactive-mode.ts +36 -12
- package/src/modes/print-mode.ts +64 -0
- package/src/modes/prompt-action-autocomplete.ts +30 -4
- package/src/modes/runtime-init.ts +12 -9
- package/src/modes/theme/theme.ts +5 -0
- package/src/modes/types.ts +2 -0
- package/src/modes/ui-language.ts +49 -2
- package/src/modes/utils/ui-helpers.ts +22 -3
- package/src/prompts/agent-fragments/browser-aside.md +22 -0
- package/src/prompts/system/master-mode.md +28 -0
- package/src/prompts/tools/ask.md +1 -1
- package/src/prompts/tools/bash.md +1 -0
- package/src/prompts/tools/job.md +1 -1
- package/src/sdk/acp/adapter.ts +5 -0
- package/src/sdk/broker/broker.ts +1646 -27
- package/src/sdk/broker/endpoint-authority.ts +86 -0
- package/src/sdk/broker/ensure.ts +81 -3
- package/src/sdk/broker/index.ts +3 -0
- package/src/sdk/broker/lifecycle-ledger.ts +6 -0
- package/src/sdk/broker/lifecycle.ts +440 -75
- package/src/sdk/broker/master-capability.ts +232 -0
- package/src/sdk/broker/session-index.ts +121 -17
- package/src/sdk/broker/session-scope.ts +272 -0
- package/src/sdk/broker/spawn-authority.ts +1031 -0
- package/src/sdk/broker/spawn-substrate.ts +591 -0
- package/src/sdk/broker/startup-budget.ts +144 -2
- package/src/sdk/broker/state-version.ts +43 -14
- package/src/sdk/broker/transport.ts +1 -0
- package/src/sdk/broker/worktree-occupancy.ts +90 -0
- package/src/sdk/bus/chat-daemon-control.ts +5 -2
- package/src/sdk/bus/chat-daemon-runtime.ts +136 -18
- package/src/sdk/bus/discord-daemon.ts +50 -10
- package/src/sdk/bus/index.ts +84 -7
- package/src/sdk/bus/kind-aware-reconciliation.ts +296 -48
- package/src/sdk/bus/lifecycle-commands.ts +4 -0
- package/src/sdk/bus/notification-service.ts +22 -5
- package/src/sdk/bus/prompt-reconciliation.ts +7 -2
- package/src/sdk/bus/reconciliation-store.ts +55 -31
- package/src/sdk/bus/slack-conversation.ts +1 -1
- package/src/sdk/bus/slack-daemon.ts +72 -38
- package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
- package/src/sdk/cli/index.ts +1 -0
- package/src/sdk/cli/master-cli.ts +259 -0
- package/src/sdk/cli/rows.ts +41 -15
- package/src/sdk/cli/session-cli.ts +297 -26
- package/src/sdk/client/client.ts +14 -8
- package/src/sdk/host/control/dispatch.ts +101 -40
- package/src/sdk/host/control/index.ts +2 -0
- package/src/sdk/host/control/operations.ts +2 -0
- package/src/sdk/host/control/runtime-gate.ts +203 -11
- package/src/sdk/host/events.ts +1 -0
- package/src/sdk/host/host.ts +29 -2
- package/src/sdk/host/query/cursor.ts +52 -7
- package/src/sdk/host/query/handlers.ts +33 -12
- package/src/sdk/host/query/revision-store.ts +12 -0
- package/src/sdk/host/session-runtime.test.ts +673 -14
- package/src/sdk/host/session-runtime.ts +674 -103
- package/src/sdk/host/surface-policy.ts +3 -3
- package/src/sdk/lifecycle/broker-client.ts +28 -0
- package/src/sdk/lifecycle/service.ts +279 -20
- package/src/sdk/lifecycle-session.ts +17 -4
- package/src/sdk/prompt-deadline-manager.ts +2 -1
- package/src/sdk/protocol/operation-inventory.generated.json +105 -3
- package/src/sdk/protocol/operation-registry.ts +14 -4
- package/src/sdk/receipt-state.ts +10 -0
- package/src/sdk/router/session-router.ts +219 -118
- package/src/sdk/session.ts +416 -195
- package/src/sdk/startup-capability.ts +1 -0
- package/src/sdk/turn-result.ts +22 -0
- package/src/session/agent-session.ts +1628 -318
- package/src/session/blob-store.ts +15 -5
- package/src/session/credential-store-errors.ts +2 -0
- package/src/session/fold-coordinator.ts +40 -3
- package/src/session/internal/managed-session-storage.ts +153 -78
- package/src/session/session-manager.ts +850 -97
- package/src/session/yield-queue.ts +24 -10
- package/src/setup/frictionless-onboarding.ts +2 -0
- package/src/slash-commands/builtin-registry.ts +41 -0
- package/src/task/executor.ts +9 -4
- package/src/telemetry/events.ts +1138 -12
- package/src/tools/ask-contract.test.ts +64 -0
- package/src/tools/ask-contract.ts +36 -1
- package/src/tools/ask.ts +10 -8
- package/src/tools/bash-interactive.ts +11 -2
- package/src/tools/bash.ts +412 -75
- package/src/tools/browser/launch.ts +37 -0
- package/src/tools/browser/registry.ts +99 -9
- package/src/tools/cron.ts +6 -4
- package/src/tools/descriptors.test.ts +15 -0
- package/src/tools/descriptors.ts +5 -1
- package/src/tools/gh.ts +104 -48
- package/src/tools/github-cache.ts +2 -2
- package/src/tools/index.ts +10 -6
- package/src/tools/job.ts +4 -1
- package/src/tools/monitor.ts +12 -9
- package/src/tools/resolve.ts +30 -10
- package/src/tools/tool-catalog.generated.ts +2 -2
- package/src/utils/external-editor.ts +72 -1
package/src/telemetry/events.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
import type { BigIntStats } from "node:fs";
|
|
2
3
|
import * as fs from "node:fs/promises";
|
|
3
4
|
import * as path from "node:path";
|
|
5
|
+
import type { NativeExactFileIdentity } from "@gajae-code/natives";
|
|
6
|
+
import * as natives from "@gajae-code/natives";
|
|
4
7
|
import { getTrustedAgentFile } from "@gajae-code/utils";
|
|
5
8
|
|
|
6
9
|
export const TELEMETRY_SCHEMA_VERSION = 1 as const;
|
|
@@ -25,7 +28,6 @@ export interface TelemetryEvent {
|
|
|
25
28
|
result?: "available" | "up_to_date" | "installed" | "failed" | "skipped";
|
|
26
29
|
installMethod?: "bun" | "npm" | "binary" | "migrate";
|
|
27
30
|
}
|
|
28
|
-
|
|
29
31
|
type EventInput = {
|
|
30
32
|
event?: unknown;
|
|
31
33
|
installId?: unknown;
|
|
@@ -42,6 +44,21 @@ const RESULTS = new Set(["available", "up_to_date", "installed", "failed", "skip
|
|
|
42
44
|
const INSTALL_METHODS = new Set(["bun", "npm", "binary", "migrate"]);
|
|
43
45
|
const FORBIDDEN_KEY = /(?:prompt|argv|path|env|secret|account|model|provider|repo|error|hostname|username|machine|ip)/i;
|
|
44
46
|
const UUID_V4 = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
47
|
+
const INSTALL_ID_CLAIM_TIMEOUT_MS = 2_000;
|
|
48
|
+
const INSTALL_ID_CLAIM_RECOVERY_MARGIN_MS = 500;
|
|
49
|
+
const INSTALL_ID_CLAIM_WAIT_TIMEOUT_MS = INSTALL_ID_CLAIM_TIMEOUT_MS + INSTALL_ID_CLAIM_RECOVERY_MARGIN_MS;
|
|
50
|
+
const INSTALL_ID_CLAIM_LEASE_MS = 1_000;
|
|
51
|
+
const INSTALL_ID_HEARTBEAT_INTERVAL_MS = Math.floor(INSTALL_ID_CLAIM_LEASE_MS / 2);
|
|
52
|
+
const INSTALL_ID_CLAIM_POLL_INITIAL_MS = 25;
|
|
53
|
+
const INSTALL_ID_MAX_SERIALIZED_BYTES = 128;
|
|
54
|
+
const CLAIM_MAX_SERIALIZED_BYTES = 16_384;
|
|
55
|
+
const INSTALL_ID_PARTIAL_GRACE_MS = 250;
|
|
56
|
+
const INSTALL_ID_NOFOLLOW_NONBLOCK_FLAGS =
|
|
57
|
+
process.platform === "win32" ? 0 : (fs.constants.O_NOFOLLOW ?? 0) | (fs.constants.O_NONBLOCK ?? 0);
|
|
58
|
+
const INSTALL_ID_READ_FLAGS = fs.constants.O_RDONLY | INSTALL_ID_NOFOLLOW_NONBLOCK_FLAGS;
|
|
59
|
+
const INSTALL_ID_WRITE_FLAGS = fs.constants.O_RDWR | INSTALL_ID_NOFOLLOW_NONBLOCK_FLAGS;
|
|
60
|
+
const durableInstallIdPaths = new Map<string, { identity: string; promise: Promise<void> }>();
|
|
61
|
+
const scheduledClaimCleanups = new Set<string>();
|
|
45
62
|
|
|
46
63
|
function hasForbiddenKey(value: unknown, seen = new Set<object>()): boolean {
|
|
47
64
|
if (value === null || typeof value !== "object") return false;
|
|
@@ -103,12 +120,1100 @@ export function serializeTelemetryEvent(input: unknown): string {
|
|
|
103
120
|
return `${JSON.stringify(output)}\n`;
|
|
104
121
|
}
|
|
105
122
|
|
|
106
|
-
async function
|
|
107
|
-
const
|
|
123
|
+
async function publishNewInstallId(filePath: string, installId: string): Promise<void> {
|
|
124
|
+
const tempPath = `${filePath}.${randomUUID()}.tmp`;
|
|
125
|
+
let handle: fs.FileHandle | undefined;
|
|
126
|
+
let stagedIdentity: BigIntStats | undefined;
|
|
127
|
+
let cleanupScheduled = false;
|
|
108
128
|
try {
|
|
129
|
+
handle = await fs.open(tempPath, "wx", 0o600);
|
|
109
130
|
await handle.writeFile(`${installId}\n`, "utf8");
|
|
131
|
+
await handle.sync();
|
|
132
|
+
stagedIdentity = await handle.stat({ bigint: true });
|
|
133
|
+
} finally {
|
|
134
|
+
await handle?.close().catch(() => undefined);
|
|
135
|
+
if (stagedIdentity === undefined) {
|
|
136
|
+
const failedIdentity = await fs.lstat(tempPath, { bigint: true }).catch(() => undefined);
|
|
137
|
+
if (failedIdentity !== undefined) {
|
|
138
|
+
const failedContent = (await readStagedContentBounded(tempPath)) ?? "";
|
|
139
|
+
scheduleExactStagedCleanup(tempPath, failedIdentity, failedContent);
|
|
140
|
+
cleanupScheduled = true;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
try {
|
|
146
|
+
await fs.link(tempPath, filePath);
|
|
147
|
+
} catch (error) {
|
|
148
|
+
if (isHardLinkUnsupported(error)) {
|
|
149
|
+
const unsupported = new Error("exclusive hard links are unavailable") as NodeJS.ErrnoException;
|
|
150
|
+
unsupported.code = "EUNSUPPORTED";
|
|
151
|
+
throw unsupported;
|
|
152
|
+
}
|
|
153
|
+
throw error;
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
stagedIdentity = await fs.lstat(tempPath, { bigint: true });
|
|
157
|
+
} catch (error) {
|
|
158
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
159
|
+
}
|
|
160
|
+
await syncDirectory(path.dirname(filePath));
|
|
161
|
+
if (stagedIdentity === undefined) throw claimRaceError();
|
|
162
|
+
let published: PersistedInstallIdSnapshot;
|
|
163
|
+
try {
|
|
164
|
+
published = await readPublishedInstallIdSnapshot(filePath);
|
|
165
|
+
} catch (error) {
|
|
166
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") throw claimRaceError();
|
|
167
|
+
throw error;
|
|
168
|
+
}
|
|
169
|
+
if (
|
|
170
|
+
published.value !== installId ||
|
|
171
|
+
published.stat.dev !== stagedIdentity.dev ||
|
|
172
|
+
published.stat.ino !== stagedIdentity.ino ||
|
|
173
|
+
published.stat.size !== stagedIdentity.size ||
|
|
174
|
+
published.stat.mtimeNs !== stagedIdentity.mtimeNs
|
|
175
|
+
)
|
|
176
|
+
throw claimRaceError();
|
|
177
|
+
} finally {
|
|
178
|
+
if (stagedIdentity !== undefined && !cleanupScheduled)
|
|
179
|
+
scheduleExactStagedCleanup(tempPath, stagedIdentity, `${installId}\n`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function scheduleExactStagedCleanup(filePath: string, identity: BigIntStats, content: string): void {
|
|
184
|
+
scheduleExactDetachedCleanup(filePath, {
|
|
185
|
+
dev: identity.dev,
|
|
186
|
+
ino: identity.ino,
|
|
187
|
+
nlink: identity.nlink,
|
|
188
|
+
size: identity.size,
|
|
189
|
+
mtimeNs: identity.mtimeNs,
|
|
190
|
+
sha256: hashClaimBytes(content),
|
|
191
|
+
allowHardLink: true,
|
|
192
|
+
quarantineName: `.${path.basename(filePath)}.${randomUUID()}.quarantine`,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function scheduleExactDetachedCleanup(pathname: string, identity: NativeExactFileIdentity, attempt = 0): void {
|
|
197
|
+
if (natives.exactUnlinkDirectDetached(pathname, identity) || attempt >= 3) return;
|
|
198
|
+
const retry = setTimeout(() => scheduleExactDetachedCleanup(pathname, identity, attempt + 1), 25 * (attempt + 1));
|
|
199
|
+
retry.unref();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function isHardLinkUnsupported(error: unknown): boolean {
|
|
203
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
204
|
+
return code === "EOPNOTSUPP" || code === "ENOTSUP" || code === "EPERM" || code === "ENOSYS";
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function syncDirectory(directory: string): Promise<void> {
|
|
208
|
+
let handle: fs.FileHandle;
|
|
209
|
+
try {
|
|
210
|
+
handle = await fs.open(directory, "r");
|
|
211
|
+
} catch (error) {
|
|
212
|
+
if (isUnsupportedDirectorySync(error)) return;
|
|
213
|
+
throw error;
|
|
214
|
+
}
|
|
215
|
+
try {
|
|
216
|
+
try {
|
|
217
|
+
await handle.sync();
|
|
218
|
+
} catch (error) {
|
|
219
|
+
if (!isUnsupportedDirectorySync(error)) throw error;
|
|
220
|
+
}
|
|
221
|
+
} finally {
|
|
222
|
+
await handle.close();
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function openRegularFileNoFollow(
|
|
227
|
+
filePath: string,
|
|
228
|
+
flags: number,
|
|
229
|
+
failureMessage: string,
|
|
230
|
+
failureCode?: string,
|
|
231
|
+
): Promise<fs.FileHandle> {
|
|
232
|
+
let named: BigIntStats | undefined;
|
|
233
|
+
if (process.platform === "win32") {
|
|
234
|
+
named = await fs.lstat(filePath, { bigint: true });
|
|
235
|
+
if (!named.isFile()) {
|
|
236
|
+
const error = new Error(failureMessage) as NodeJS.ErrnoException;
|
|
237
|
+
if (failureCode !== undefined) error.code = failureCode;
|
|
238
|
+
throw error;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
const handle = await fs.open(filePath, flags);
|
|
242
|
+
if (named === undefined) return handle;
|
|
243
|
+
try {
|
|
244
|
+
const opened = await handle.stat({ bigint: true });
|
|
245
|
+
if (!opened.isFile() || opened.dev !== named.dev || opened.ino !== named.ino) throw claimRaceError();
|
|
246
|
+
return handle;
|
|
247
|
+
} catch (error) {
|
|
248
|
+
await handle.close().catch(() => undefined);
|
|
249
|
+
throw error;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function openInstallIdFile(filePath: string, flags: number): Promise<fs.FileHandle> {
|
|
254
|
+
return openRegularFileNoFollow(filePath, flags, "telemetry install ID is malformed");
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async function openClaimFile(claimPath: string, flags: number): Promise<fs.FileHandle> {
|
|
258
|
+
return openRegularFileNoFollow(claimPath, flags, "telemetry install ID claim is nonregular", "ECLAIMNONREGULAR");
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async function syncFile(filePath: string, expected?: BigIntStats): Promise<void> {
|
|
262
|
+
const handle = await openInstallIdFile(filePath, INSTALL_ID_WRITE_FLAGS);
|
|
263
|
+
try {
|
|
264
|
+
const stat = await handle.stat({ bigint: true });
|
|
265
|
+
if (
|
|
266
|
+
expected !== undefined &&
|
|
267
|
+
(stat.dev !== expected.dev ||
|
|
268
|
+
stat.ino !== expected.ino ||
|
|
269
|
+
stat.size !== expected.size ||
|
|
270
|
+
stat.mtimeNs !== expected.mtimeNs)
|
|
271
|
+
)
|
|
272
|
+
throw claimRaceError();
|
|
273
|
+
await handle.sync();
|
|
274
|
+
} finally {
|
|
275
|
+
await handle.close();
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async function chmodWinner(filePath: string, expected: BigIntStats): Promise<boolean> {
|
|
280
|
+
let handle: fs.FileHandle;
|
|
281
|
+
let readOnly = false;
|
|
282
|
+
try {
|
|
283
|
+
handle = await openInstallIdFile(filePath, INSTALL_ID_WRITE_FLAGS);
|
|
284
|
+
} catch (error) {
|
|
285
|
+
if ((error as NodeJS.ErrnoException).code !== "EACCES") throw error;
|
|
286
|
+
handle = await openInstallIdFile(filePath, INSTALL_ID_READ_FLAGS);
|
|
287
|
+
readOnly = true;
|
|
288
|
+
}
|
|
289
|
+
try {
|
|
290
|
+
const opened = await handle.stat({ bigint: true });
|
|
291
|
+
if (
|
|
292
|
+
opened.dev !== expected.dev ||
|
|
293
|
+
opened.ino !== expected.ino ||
|
|
294
|
+
opened.size !== expected.size ||
|
|
295
|
+
opened.mtimeNs !== expected.mtimeNs
|
|
296
|
+
)
|
|
297
|
+
return false;
|
|
298
|
+
await handle.chmod(0o600);
|
|
299
|
+
if (readOnly) {
|
|
300
|
+
await handle.close();
|
|
301
|
+
handle = await openInstallIdFile(filePath, INSTALL_ID_WRITE_FLAGS);
|
|
302
|
+
const reopened = await handle.stat({ bigint: true });
|
|
303
|
+
if (reopened.dev !== expected.dev || reopened.ino !== expected.ino) return false;
|
|
304
|
+
}
|
|
305
|
+
await handle.sync();
|
|
306
|
+
const settled = await handle.stat({ bigint: true });
|
|
307
|
+
return (
|
|
308
|
+
settled.dev === expected.dev &&
|
|
309
|
+
settled.ino === expected.ino &&
|
|
310
|
+
settled.size === expected.size &&
|
|
311
|
+
settled.mtimeNs === expected.mtimeNs &&
|
|
312
|
+
(process.platform === "win32" || (settled.mode & 0o777n) === 0o600n)
|
|
313
|
+
);
|
|
314
|
+
} finally {
|
|
315
|
+
await handle.close();
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async function tightenInstallIdPermissions(filePath: string, expectedValue: string): Promise<void> {
|
|
320
|
+
try {
|
|
321
|
+
const snapshot = await readPublishedInstallIdSnapshot(filePath);
|
|
322
|
+
if (snapshot.value !== expectedValue || !(await chmodWinner(filePath, snapshot.stat))) throw claimRaceError();
|
|
323
|
+
const settled = await readPublishedInstallIdSnapshot(filePath);
|
|
324
|
+
if (
|
|
325
|
+
settled.value !== expectedValue ||
|
|
326
|
+
settled.stat.dev !== snapshot.stat.dev ||
|
|
327
|
+
settled.stat.ino !== snapshot.stat.ino ||
|
|
328
|
+
settled.stat.size !== snapshot.stat.size ||
|
|
329
|
+
settled.stat.mtimeNs !== snapshot.stat.mtimeNs
|
|
330
|
+
)
|
|
331
|
+
throw claimRaceError();
|
|
332
|
+
} catch (error) {
|
|
333
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") throw claimRaceError();
|
|
334
|
+
throw error;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function isUnsupportedDirectorySync(error: unknown): boolean {
|
|
339
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
340
|
+
return process.platform === "win32" && (code === "EPERM" || code === "EACCES");
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
type PersistedInstallIdSnapshot = { value: string; stat: BigIntStats };
|
|
344
|
+
|
|
345
|
+
async function readPublishedInstallIdSnapshot(filePath: string): Promise<PersistedInstallIdSnapshot> {
|
|
346
|
+
let handle: fs.FileHandle | undefined;
|
|
347
|
+
try {
|
|
348
|
+
handle = await openInstallIdFile(filePath, INSTALL_ID_READ_FLAGS);
|
|
349
|
+
const stat = await handle.stat({ bigint: true });
|
|
350
|
+
if (!stat.isFile() || stat.size > BigInt(INSTALL_ID_MAX_SERIALIZED_BYTES))
|
|
351
|
+
throw new Error("telemetry install ID is malformed");
|
|
352
|
+
const bytes = Buffer.alloc(Number(stat.size));
|
|
353
|
+
let offset = 0;
|
|
354
|
+
while (offset < bytes.length) {
|
|
355
|
+
const result = await handle.read(bytes, offset, bytes.length - offset, offset);
|
|
356
|
+
if (result.bytesRead <= 0) throw new Error("telemetry install ID is malformed");
|
|
357
|
+
offset += result.bytesRead;
|
|
358
|
+
}
|
|
359
|
+
const settled = await handle.stat({ bigint: true });
|
|
360
|
+
if (settled.size !== stat.size) throw claimRaceError();
|
|
361
|
+
if (settled.size > BigInt(INSTALL_ID_MAX_SERIALIZED_BYTES)) throw new Error("telemetry install ID is malformed");
|
|
362
|
+
const verification = Buffer.alloc(Number(settled.size));
|
|
363
|
+
let verificationOffset = 0;
|
|
364
|
+
while (verificationOffset < verification.length) {
|
|
365
|
+
const result = await handle.read(
|
|
366
|
+
verification,
|
|
367
|
+
verificationOffset,
|
|
368
|
+
verification.length - verificationOffset,
|
|
369
|
+
verificationOffset,
|
|
370
|
+
);
|
|
371
|
+
if (result.bytesRead <= 0) throw claimRaceError();
|
|
372
|
+
verificationOffset += result.bytesRead;
|
|
373
|
+
}
|
|
374
|
+
if (!verification.equals(bytes)) throw claimRaceError();
|
|
375
|
+
const named = await fs.lstat(filePath, { bigint: true });
|
|
376
|
+
if (
|
|
377
|
+
settled.dev !== stat.dev ||
|
|
378
|
+
settled.ino !== stat.ino ||
|
|
379
|
+
settled.size !== stat.size ||
|
|
380
|
+
settled.mtimeNs !== stat.mtimeNs ||
|
|
381
|
+
named.dev !== settled.dev ||
|
|
382
|
+
named.ino !== settled.ino
|
|
383
|
+
)
|
|
384
|
+
throw claimRaceError();
|
|
385
|
+
const existing = bytes.toString("utf8").trim();
|
|
386
|
+
if (!UUID_V4.test(existing)) throw new Error("telemetry install ID is malformed");
|
|
387
|
+
return { value: existing, stat: settled };
|
|
388
|
+
} catch (error) {
|
|
389
|
+
if ((error as NodeJS.ErrnoException).code === "ELOOP") throw new Error("telemetry install ID is malformed");
|
|
390
|
+
throw error;
|
|
391
|
+
} finally {
|
|
392
|
+
await handle?.close().catch(() => undefined);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
async function readPublishedInstallId(filePath: string): Promise<string> {
|
|
397
|
+
return (await readPublishedInstallIdSnapshot(filePath)).value;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
async function readStagedContentBounded(filePath: string): Promise<string | undefined> {
|
|
401
|
+
let handle: fs.FileHandle | undefined;
|
|
402
|
+
try {
|
|
403
|
+
handle = await openRegularFileNoFollow(filePath, INSTALL_ID_READ_FLAGS, "telemetry staged file is nonregular");
|
|
404
|
+
const identity = await handle.stat({ bigint: true });
|
|
405
|
+
if (!identity.isFile() || identity.size > BigInt(CLAIM_MAX_SERIALIZED_BYTES)) return undefined;
|
|
406
|
+
const bytes = await readClaimHandleBounded(handle, identity.size);
|
|
407
|
+
const settled = await handle.stat({ bigint: true });
|
|
408
|
+
const named = await fs.lstat(filePath, { bigint: true });
|
|
409
|
+
if (
|
|
410
|
+
settled.dev !== identity.dev ||
|
|
411
|
+
settled.ino !== identity.ino ||
|
|
412
|
+
settled.size !== identity.size ||
|
|
413
|
+
settled.mtimeNs !== identity.mtimeNs ||
|
|
414
|
+
named.dev !== settled.dev ||
|
|
415
|
+
named.ino !== settled.ino ||
|
|
416
|
+
named.size !== settled.size ||
|
|
417
|
+
named.mtimeNs !== settled.mtimeNs
|
|
418
|
+
)
|
|
419
|
+
return undefined;
|
|
420
|
+
return bytes;
|
|
421
|
+
} catch {
|
|
422
|
+
return undefined;
|
|
423
|
+
} finally {
|
|
424
|
+
await handle?.close().catch(() => undefined);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
async function readExistingInstallId(filePath: string): Promise<string> {
|
|
429
|
+
const deadline = performance.now() + INSTALL_ID_CLAIM_WAIT_TIMEOUT_MS;
|
|
430
|
+
while (true) {
|
|
431
|
+
try {
|
|
432
|
+
return await readExistingInstallIdSnapshot(filePath);
|
|
433
|
+
} catch (error) {
|
|
434
|
+
if (error instanceof Error && error.message === "telemetry install ID is malformed") {
|
|
435
|
+
const stat = await fs.lstat(filePath, { bigint: true });
|
|
436
|
+
if (Date.now() - Number(stat.mtimeMs) > INSTALL_ID_PARTIAL_GRACE_MS) throw error;
|
|
437
|
+
if (performance.now() >= deadline) throw error;
|
|
438
|
+
await Bun.sleep(INSTALL_ID_CLAIM_POLL_INITIAL_MS);
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
if ((error as NodeJS.ErrnoException).code !== "ECLAIMRACE") throw error;
|
|
442
|
+
if (performance.now() >= deadline) throw new Error("telemetry install ID claim did not clear");
|
|
443
|
+
await Bun.sleep(INSTALL_ID_CLAIM_POLL_INITIAL_MS);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
async function readExistingInstallIdSnapshot(filePath: string): Promise<string> {
|
|
449
|
+
const claimPath = `${filePath}.lock`;
|
|
450
|
+
const claimBefore = await readClaimIdentity(claimPath);
|
|
451
|
+
let fileMissing = false;
|
|
452
|
+
let malformed: Error | undefined;
|
|
453
|
+
try {
|
|
454
|
+
await readPublishedInstallId(filePath);
|
|
455
|
+
const claimAfter = await readClaimIdentity(claimPath);
|
|
456
|
+
if (claimBefore?.state === "publishing" && UUID_V4.test(claimBefore.token) && claimAfter?.state === undefined)
|
|
457
|
+
throw claimLostError();
|
|
458
|
+
if (claimBefore === undefined && claimAfter === undefined)
|
|
459
|
+
return readPublishedInstallIdWhenUnclaimed(filePath, claimPath);
|
|
460
|
+
if (claimBefore !== undefined && claimAfter === undefined)
|
|
461
|
+
return readPublishedInstallIdWhenUnclaimed(filePath, claimPath);
|
|
462
|
+
} catch (error) {
|
|
463
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") fileMissing = true;
|
|
464
|
+
else if (error instanceof Error && error.message === "telemetry install ID is malformed") malformed = error;
|
|
465
|
+
else throw error;
|
|
466
|
+
}
|
|
467
|
+
const activeClaim = await readClaimIdentity(claimPath);
|
|
468
|
+
if (activeClaim === undefined) {
|
|
469
|
+
if (malformed !== undefined) throw malformed;
|
|
470
|
+
if (fileMissing) {
|
|
471
|
+
const missing = new Error("telemetry install ID is missing") as NodeJS.ErrnoException;
|
|
472
|
+
missing.code = "ENOENT";
|
|
473
|
+
throw missing;
|
|
474
|
+
}
|
|
475
|
+
return readPublishedInstallIdWhenUnclaimed(filePath, claimPath);
|
|
476
|
+
}
|
|
477
|
+
if (claimBefore === undefined && activeClaim.state === undefined) throw claimLostError();
|
|
478
|
+
await waitForClaimRelease(claimPath);
|
|
479
|
+
return readPublishedInstallIdWhenUnclaimed(filePath, claimPath);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
type ClaimState = "publishing" | "committed" | undefined;
|
|
483
|
+
type ClaimIdentity = {
|
|
484
|
+
dev: bigint;
|
|
485
|
+
ino: bigint;
|
|
486
|
+
mtimeMs: number;
|
|
487
|
+
size: bigint;
|
|
488
|
+
digest: string;
|
|
489
|
+
token: string;
|
|
490
|
+
state: ClaimState;
|
|
491
|
+
expiresAt: number | undefined;
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
async function readClaimIdentity(claimPath: string): Promise<ClaimIdentity | undefined> {
|
|
495
|
+
let handle: fs.FileHandle | undefined;
|
|
496
|
+
try {
|
|
497
|
+
handle = await openClaimFile(claimPath, INSTALL_ID_READ_FLAGS);
|
|
498
|
+
const stat = await handle.stat({ bigint: true });
|
|
499
|
+
if (!stat.isFile()) {
|
|
500
|
+
const error = new Error("telemetry install ID claim is nonregular") as NodeJS.ErrnoException;
|
|
501
|
+
error.code = "ECLAIMNONREGULAR";
|
|
502
|
+
throw error;
|
|
503
|
+
}
|
|
504
|
+
if (stat.size > BigInt(CLAIM_MAX_SERIALIZED_BYTES)) {
|
|
505
|
+
const error = new Error("telemetry install ID claim is oversized") as NodeJS.ErrnoException;
|
|
506
|
+
error.code = "ECLAIMOVERSIZE";
|
|
507
|
+
throw error;
|
|
508
|
+
}
|
|
509
|
+
const content = await readClaimHandleBounded(handle, stat.size);
|
|
510
|
+
const settled = await handle.stat({ bigint: true });
|
|
511
|
+
const named = await fs.lstat(claimPath, { bigint: true });
|
|
512
|
+
const verified = await readClaimHandleBounded(handle, settled.size);
|
|
513
|
+
const final = await handle.stat({ bigint: true });
|
|
514
|
+
const finalNamed = await fs.lstat(claimPath, { bigint: true });
|
|
515
|
+
if (
|
|
516
|
+
settled.dev !== stat.dev ||
|
|
517
|
+
settled.ino !== stat.ino ||
|
|
518
|
+
settled.size !== stat.size ||
|
|
519
|
+
settled.mtimeNs !== stat.mtimeNs ||
|
|
520
|
+
named.ino !== settled.ino ||
|
|
521
|
+
hashClaimBytes(content) !== hashClaimBytes(verified) ||
|
|
522
|
+
final.dev !== settled.dev ||
|
|
523
|
+
final.ino !== settled.ino ||
|
|
524
|
+
final.size !== settled.size ||
|
|
525
|
+
final.mtimeNs !== settled.mtimeNs ||
|
|
526
|
+
finalNamed.dev !== final.dev ||
|
|
527
|
+
finalNamed.ino !== final.ino ||
|
|
528
|
+
finalNamed.size !== final.size ||
|
|
529
|
+
finalNamed.mtimeNs !== final.mtimeNs
|
|
530
|
+
)
|
|
531
|
+
throw claimRaceError();
|
|
532
|
+
const parsed = parseClaim(content);
|
|
533
|
+
return {
|
|
534
|
+
dev: settled.dev,
|
|
535
|
+
ino: settled.ino,
|
|
536
|
+
mtimeMs: Number(settled.mtimeMs),
|
|
537
|
+
size: settled.size,
|
|
538
|
+
digest: hashClaimBytes(content),
|
|
539
|
+
token: parsed.token,
|
|
540
|
+
state: parsed.state,
|
|
541
|
+
expiresAt: parsed.expiresAt,
|
|
542
|
+
};
|
|
543
|
+
} catch (error) {
|
|
544
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
|
|
545
|
+
throw error;
|
|
546
|
+
} finally {
|
|
547
|
+
await handle?.close().catch(() => undefined);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function hashClaimBytes(content: string): string {
|
|
552
|
+
const hash = new Bun.CryptoHasher("sha256");
|
|
553
|
+
hash.update(content);
|
|
554
|
+
return hash.digest("hex");
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
type ClaimSnapshot = { stat: BigIntStats; content: string; digest: string };
|
|
558
|
+
|
|
559
|
+
async function readClaimSnapshot(claimPath: string): Promise<ClaimSnapshot> {
|
|
560
|
+
const handle = await openClaimFile(claimPath, INSTALL_ID_READ_FLAGS);
|
|
561
|
+
try {
|
|
562
|
+
const opened = await handle.stat({ bigint: true });
|
|
563
|
+
if (!opened.isFile()) throw new Error("telemetry install ID claim is nonregular");
|
|
564
|
+
if (opened.size > BigInt(CLAIM_MAX_SERIALIZED_BYTES)) {
|
|
565
|
+
const error = new Error("telemetry install ID claim is oversized") as NodeJS.ErrnoException;
|
|
566
|
+
error.code = "ECLAIMOVERSIZE";
|
|
567
|
+
throw error;
|
|
568
|
+
}
|
|
569
|
+
const content = await readClaimHandleBounded(handle, opened.size);
|
|
570
|
+
const settled = await handle.stat({ bigint: true });
|
|
571
|
+
const named = await fs.lstat(claimPath, { bigint: true });
|
|
572
|
+
if (
|
|
573
|
+
settled.dev !== opened.dev ||
|
|
574
|
+
settled.ino !== opened.ino ||
|
|
575
|
+
settled.size !== opened.size ||
|
|
576
|
+
settled.mtimeNs !== opened.mtimeNs ||
|
|
577
|
+
named.dev !== settled.dev ||
|
|
578
|
+
named.ino !== settled.ino
|
|
579
|
+
)
|
|
580
|
+
throw claimRaceError();
|
|
581
|
+
return { stat: settled, content, digest: hashClaimBytes(content) };
|
|
582
|
+
} finally {
|
|
583
|
+
await handle.close().catch(() => undefined);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
async function readClaimHandleBounded(handle: fs.FileHandle, size: bigint): Promise<string> {
|
|
588
|
+
const length = Number(size > BigInt(CLAIM_MAX_SERIALIZED_BYTES) ? BigInt(CLAIM_MAX_SERIALIZED_BYTES) : size);
|
|
589
|
+
const buffer = Buffer.alloc(length);
|
|
590
|
+
let offset = 0;
|
|
591
|
+
const position = Number(size) - length;
|
|
592
|
+
while (offset < length) {
|
|
593
|
+
const { bytesRead } = await handle.read(buffer, offset, length - offset, position + offset);
|
|
594
|
+
if (bytesRead <= 0) throw claimRaceError();
|
|
595
|
+
offset += bytesRead;
|
|
596
|
+
}
|
|
597
|
+
return buffer.toString("utf8");
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function claimRaceError(): NodeJS.ErrnoException {
|
|
601
|
+
const error = new Error("telemetry install ID claim changed during observation") as NodeJS.ErrnoException;
|
|
602
|
+
error.code = "ECLAIMRACE";
|
|
603
|
+
return error;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
async function readPublishedInstallIdWhenUnclaimed(filePath: string, claimPath: string): Promise<string> {
|
|
607
|
+
const deadline = performance.now() + INSTALL_ID_CLAIM_WAIT_TIMEOUT_MS;
|
|
608
|
+
while (true) {
|
|
609
|
+
if (performance.now() >= deadline) throw new Error("telemetry install ID claim did not clear");
|
|
610
|
+
const identity = await fs.lstat(filePath, { bigint: true });
|
|
611
|
+
const identityKey = `${identity.dev}:${identity.ino}:${identity.size}:${identity.mtimeNs}`;
|
|
612
|
+
let durability = durableInstallIdPaths.get(filePath);
|
|
613
|
+
if (durability === undefined || durability.identity !== identityKey) {
|
|
614
|
+
const promise = syncDirectory(path.dirname(filePath));
|
|
615
|
+
durability = { identity: identityKey, promise };
|
|
616
|
+
durableInstallIdPaths.set(filePath, durability);
|
|
617
|
+
promise.catch(() => {
|
|
618
|
+
if (durableInstallIdPaths.get(filePath)?.promise === promise) durableInstallIdPaths.delete(filePath);
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
await durability.promise;
|
|
622
|
+
try {
|
|
623
|
+
await syncFile(filePath, identity);
|
|
624
|
+
} catch (error) {
|
|
625
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
626
|
+
if (code === "EACCES") {
|
|
627
|
+
// A valid owner-read-only ID is repaired by the caller after this read.
|
|
628
|
+
} else if (code === "ECLAIMRACE") {
|
|
629
|
+
await Bun.sleep(INSTALL_ID_CLAIM_POLL_INITIAL_MS);
|
|
630
|
+
continue;
|
|
631
|
+
} else throw error;
|
|
632
|
+
}
|
|
633
|
+
let value: string;
|
|
634
|
+
try {
|
|
635
|
+
value = await readPublishedInstallId(filePath);
|
|
636
|
+
} catch (error) {
|
|
637
|
+
if ((error as NodeJS.ErrnoException).code === "ECLAIMRACE") {
|
|
638
|
+
if (performance.now() >= deadline) throw new Error("telemetry install ID claim did not clear");
|
|
639
|
+
await Bun.sleep(INSTALL_ID_CLAIM_POLL_INITIAL_MS);
|
|
640
|
+
continue;
|
|
641
|
+
}
|
|
642
|
+
if (!(error instanceof Error) || error.message !== "telemetry install ID is malformed") throw error;
|
|
643
|
+
if (Date.now() - Number((await fs.lstat(filePath, { bigint: true })).mtimeMs) > INSTALL_ID_PARTIAL_GRACE_MS)
|
|
644
|
+
throw error;
|
|
645
|
+
if (performance.now() >= deadline) throw error;
|
|
646
|
+
await Bun.sleep(INSTALL_ID_CLAIM_POLL_INITIAL_MS);
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
const after = await fs.lstat(filePath, { bigint: true });
|
|
650
|
+
const afterKey = `${after.dev}:${after.ino}:${after.size}:${after.mtimeNs}`;
|
|
651
|
+
if (afterKey !== identityKey) {
|
|
652
|
+
await Bun.sleep(INSTALL_ID_CLAIM_POLL_INITIAL_MS);
|
|
653
|
+
continue;
|
|
654
|
+
}
|
|
655
|
+
try {
|
|
656
|
+
if (await readClaimIdentity(claimPath)) {
|
|
657
|
+
await waitForClaimRelease(claimPath);
|
|
658
|
+
continue;
|
|
659
|
+
}
|
|
660
|
+
const finalIdentity = await fs.lstat(filePath, { bigint: true });
|
|
661
|
+
const finalIdentityKey = `${finalIdentity.dev}:${finalIdentity.ino}:${finalIdentity.size}:${finalIdentity.mtimeNs}`;
|
|
662
|
+
if (finalIdentityKey !== identityKey) {
|
|
663
|
+
await Bun.sleep(INSTALL_ID_CLAIM_POLL_INITIAL_MS);
|
|
664
|
+
continue;
|
|
665
|
+
}
|
|
666
|
+
return value;
|
|
667
|
+
} catch (error) {
|
|
668
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
669
|
+
if (code !== "ECLAIMRACE" && code !== "ECLAIMLOST") throw error;
|
|
670
|
+
if (performance.now() >= deadline) throw new Error("telemetry install ID claim did not clear");
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
async function readWinnerUnderOwnedClaim(filePath: string, claimPath: string, token: string): Promise<string> {
|
|
676
|
+
const deadline = performance.now() + INSTALL_ID_CLAIM_WAIT_TIMEOUT_MS;
|
|
677
|
+
while (performance.now() < deadline) {
|
|
678
|
+
const before = await fs.lstat(filePath, { bigint: true });
|
|
679
|
+
let value: string;
|
|
680
|
+
try {
|
|
681
|
+
value = await readPublishedInstallIdWithPartialGrace(filePath, deadline);
|
|
682
|
+
} catch (error) {
|
|
683
|
+
if ((error as NodeJS.ErrnoException).code !== "ECLAIMRACE") throw error;
|
|
684
|
+
await Bun.sleep(INSTALL_ID_CLAIM_POLL_INITIAL_MS);
|
|
685
|
+
continue;
|
|
686
|
+
}
|
|
687
|
+
try {
|
|
688
|
+
await syncFile(filePath, before);
|
|
689
|
+
} catch (error) {
|
|
690
|
+
if ((error as NodeJS.ErrnoException).code !== "ECLAIMRACE") throw error;
|
|
691
|
+
await Bun.sleep(INSTALL_ID_CLAIM_POLL_INITIAL_MS);
|
|
692
|
+
continue;
|
|
693
|
+
}
|
|
694
|
+
await syncDirectory(path.dirname(filePath));
|
|
695
|
+
const after = await fs.lstat(filePath, { bigint: true });
|
|
696
|
+
if (
|
|
697
|
+
before.dev === after.dev &&
|
|
698
|
+
before.ino === after.ino &&
|
|
699
|
+
before.size === after.size &&
|
|
700
|
+
before.mtimeNs === after.mtimeNs
|
|
701
|
+
) {
|
|
702
|
+
await assertClaimOwned(claimPath, token, "publishing");
|
|
703
|
+
if (!(await chmodWinner(filePath, after))) {
|
|
704
|
+
await Bun.sleep(INSTALL_ID_CLAIM_POLL_INITIAL_MS);
|
|
705
|
+
continue;
|
|
706
|
+
}
|
|
707
|
+
const permissionChecked = await fs.lstat(filePath, { bigint: true });
|
|
708
|
+
if (
|
|
709
|
+
permissionChecked.dev !== after.dev ||
|
|
710
|
+
permissionChecked.ino !== after.ino ||
|
|
711
|
+
permissionChecked.size !== after.size ||
|
|
712
|
+
permissionChecked.mtimeNs !== after.mtimeNs
|
|
713
|
+
)
|
|
714
|
+
continue;
|
|
715
|
+
await assertClaimOwned(claimPath, token, "publishing");
|
|
716
|
+
return value;
|
|
717
|
+
}
|
|
718
|
+
await Bun.sleep(INSTALL_ID_CLAIM_POLL_INITIAL_MS);
|
|
719
|
+
}
|
|
720
|
+
throw new Error("telemetry install ID claim did not clear");
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
async function readWinnerAfterCollision(filePath: string, claimPath: string, token: string): Promise<string> {
|
|
724
|
+
try {
|
|
725
|
+
return await readWinnerUnderOwnedClaim(filePath, claimPath, token);
|
|
726
|
+
} catch (error) {
|
|
727
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") throw claimRaceError();
|
|
728
|
+
throw error;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
async function readPublishedInstallIdWithPartialGrace(filePath: string, deadline: number): Promise<string> {
|
|
733
|
+
while (true) {
|
|
734
|
+
try {
|
|
735
|
+
return await readPublishedInstallId(filePath);
|
|
736
|
+
} catch (error) {
|
|
737
|
+
if (!(error instanceof Error) || error.message !== "telemetry install ID is malformed") throw error;
|
|
738
|
+
const stat = await fs.lstat(filePath, { bigint: true });
|
|
739
|
+
if (Date.now() - Number(stat.mtimeMs) > INSTALL_ID_PARTIAL_GRACE_MS || performance.now() >= deadline)
|
|
740
|
+
throw error;
|
|
741
|
+
await Bun.sleep(INSTALL_ID_CLAIM_POLL_INITIAL_MS);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
async function refreshClaimLease(claimPath: string, token: string): Promise<void> {
|
|
747
|
+
let handle: fs.FileHandle | undefined;
|
|
748
|
+
try {
|
|
749
|
+
const named = await fs.lstat(claimPath, { bigint: true });
|
|
750
|
+
handle = await openClaimFile(claimPath, INSTALL_ID_WRITE_FLAGS);
|
|
751
|
+
const opened = await handle.stat({ bigint: true });
|
|
752
|
+
if (named.dev !== opened.dev || named.ino !== opened.ino) return;
|
|
753
|
+
const content = await readClaimHandleBounded(handle, opened.size);
|
|
754
|
+
const claim = parseClaim(content);
|
|
755
|
+
if (claim.token !== token || claim.state === undefined) return;
|
|
756
|
+
const contentDigest = hashClaimBytes(content);
|
|
757
|
+
await handle.close();
|
|
758
|
+
handle = await openClaimFile(claimPath, INSTALL_ID_WRITE_FLAGS);
|
|
759
|
+
const reopened = await handle.stat({ bigint: true });
|
|
760
|
+
if (
|
|
761
|
+
reopened.dev !== opened.dev ||
|
|
762
|
+
reopened.ino !== opened.ino ||
|
|
763
|
+
reopened.size !== opened.size ||
|
|
764
|
+
reopened.mtimeNs !== opened.mtimeNs ||
|
|
765
|
+
hashClaimBytes(await readClaimHandleBounded(handle, reopened.size)) !== contentDigest
|
|
766
|
+
)
|
|
767
|
+
throw claimLostError();
|
|
768
|
+
const record = Buffer.from(serializeClaim(token, claim.state, Date.now() + INSTALL_ID_CLAIM_LEASE_MS));
|
|
769
|
+
await writeBoundedClaimRecord(handle, record, reopened.size);
|
|
770
|
+
await handle.sync();
|
|
771
|
+
await handle.utimes(new Date(), new Date());
|
|
772
|
+
} catch (error) {
|
|
773
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return;
|
|
774
|
+
throw error;
|
|
110
775
|
} finally {
|
|
776
|
+
await handle?.close().catch(() => undefined);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
async function assertClaimOwned(
|
|
781
|
+
claimPath: string,
|
|
782
|
+
token: string,
|
|
783
|
+
state: Exclude<ClaimState, undefined>,
|
|
784
|
+
): Promise<void> {
|
|
785
|
+
const claim = await readClaimIdentity(claimPath);
|
|
786
|
+
if (claim?.token === token && claim.state === state) return;
|
|
787
|
+
const error = new Error("telemetry install ID claim ownership was lost") as NodeJS.ErrnoException;
|
|
788
|
+
error.code = "ECLAIMLOST";
|
|
789
|
+
throw error;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
async function transitionClaimCommitted(claimPath: string, token: string): Promise<void> {
|
|
793
|
+
let handle: fs.FileHandle | undefined;
|
|
794
|
+
try {
|
|
795
|
+
const before = await fs.lstat(claimPath, { bigint: true });
|
|
796
|
+
handle = await openClaimFile(claimPath, INSTALL_ID_WRITE_FLAGS);
|
|
797
|
+
const opened = await handle.stat({ bigint: true });
|
|
798
|
+
if (opened.dev !== before.dev || opened.ino !== before.ino) throw new Error("telemetry install ID claim changed");
|
|
799
|
+
const content = await readClaimHandleBounded(handle, opened.size);
|
|
800
|
+
const parsed = parseClaim(content);
|
|
801
|
+
if (parsed.token !== token || parsed.state !== "publishing")
|
|
802
|
+
throw new Error("telemetry install ID claim changed");
|
|
803
|
+
const contentDigest = hashClaimBytes(content);
|
|
111
804
|
await handle.close();
|
|
805
|
+
handle = await openClaimFile(claimPath, INSTALL_ID_WRITE_FLAGS);
|
|
806
|
+
const reopened = await handle.stat({ bigint: true });
|
|
807
|
+
if (
|
|
808
|
+
reopened.dev !== opened.dev ||
|
|
809
|
+
reopened.ino !== opened.ino ||
|
|
810
|
+
reopened.size !== opened.size ||
|
|
811
|
+
reopened.mtimeNs !== opened.mtimeNs ||
|
|
812
|
+
hashClaimBytes(await readClaimHandleBounded(handle, reopened.size)) !== contentDigest
|
|
813
|
+
)
|
|
814
|
+
throw new Error("telemetry install ID claim changed");
|
|
815
|
+
const record = Buffer.from(serializeClaim(token, "committed", Date.now() + INSTALL_ID_CLAIM_LEASE_MS));
|
|
816
|
+
await writeBoundedClaimRecord(handle, record, reopened.size);
|
|
817
|
+
} catch (error) {
|
|
818
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
819
|
+
if (code === "ENOENT" || code === "ECLAIMRACE") throw claimLostError();
|
|
820
|
+
throw error;
|
|
821
|
+
} finally {
|
|
822
|
+
await handle?.close();
|
|
823
|
+
}
|
|
824
|
+
await assertClaimOwned(claimPath, token, "committed");
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
async function syncClaimDurably(claimPath: string, token: string): Promise<void> {
|
|
828
|
+
const handle = await openClaimFile(claimPath, INSTALL_ID_WRITE_FLAGS);
|
|
829
|
+
try {
|
|
830
|
+
const before = await handle.stat({ bigint: true });
|
|
831
|
+
if (!before.isFile() || parseClaim(await readClaimHandleBounded(handle, before.size)).token !== token)
|
|
832
|
+
throw claimLostError();
|
|
833
|
+
await handle.sync();
|
|
834
|
+
} finally {
|
|
835
|
+
await handle.close();
|
|
836
|
+
}
|
|
837
|
+
await assertClaimOwned(claimPath, token, "committed");
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
async function writeClaimRecord(handle: fs.FileHandle, record: Uint8Array, position: number): Promise<void> {
|
|
841
|
+
let offset = 0;
|
|
842
|
+
while (offset < record.byteLength) {
|
|
843
|
+
const { bytesWritten } = await handle.write(record, offset, record.byteLength - offset, position + offset);
|
|
844
|
+
if (bytesWritten <= 0) throw new Error("telemetry claim generation write made no progress");
|
|
845
|
+
offset += bytesWritten;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
async function writeBoundedClaimRecord(handle: fs.FileHandle, record: Uint8Array, currentSize: bigint): Promise<void> {
|
|
850
|
+
if (currentSize + BigInt(record.byteLength) <= BigInt(CLAIM_MAX_SERIALIZED_BYTES)) {
|
|
851
|
+
await writeClaimRecord(handle, record, Number(currentSize));
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
await writeClaimRecord(handle, record, 0);
|
|
855
|
+
await handle.sync();
|
|
856
|
+
await handle.truncate(record.byteLength);
|
|
857
|
+
await handle.sync();
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
function claimLostError(): NodeJS.ErrnoException {
|
|
861
|
+
const error = new Error("telemetry install ID claim ownership was lost") as NodeJS.ErrnoException;
|
|
862
|
+
error.code = "ECLAIMLOST";
|
|
863
|
+
return error;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
function parseClaim(content: string): { token: string; state: ClaimState; expiresAt: number | undefined } {
|
|
867
|
+
const lines = content.split("\n");
|
|
868
|
+
if (lines.at(-1) === "") lines.pop();
|
|
869
|
+
else lines.pop();
|
|
870
|
+
for (const line of lines.reverse()) {
|
|
871
|
+
const match = /^([^|\n]+)\|(publishing|committed)(?:\|(\d+))?$/.exec(line);
|
|
872
|
+
if (match)
|
|
873
|
+
return {
|
|
874
|
+
token: match[1],
|
|
875
|
+
state: match[2] as Exclude<ClaimState, undefined>,
|
|
876
|
+
expiresAt: match[3] === undefined ? undefined : Number(match[3]),
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
const [token, stateOrExpiry, expiryValue] = content.split("\n", 3);
|
|
880
|
+
const state = stateOrExpiry === "publishing" || stateOrExpiry === "committed" ? stateOrExpiry : undefined;
|
|
881
|
+
const expiry = state === undefined ? stateOrExpiry : expiryValue;
|
|
882
|
+
const expiresAt = expiry === undefined ? undefined : Number(expiry);
|
|
883
|
+
return { token, state, expiresAt: Number.isFinite(expiresAt) ? expiresAt : undefined };
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
function serializeClaim(token: string, state: Exclude<ClaimState, undefined>, expiresAt?: number): string {
|
|
887
|
+
return `${token}|${state}${expiresAt === undefined ? "" : `|${expiresAt}`}\n`;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
async function waitForClaimRelease(
|
|
891
|
+
claimPath: string,
|
|
892
|
+
expectedClaim: ClaimIdentity | undefined = undefined,
|
|
893
|
+
deadline = performance.now() + INSTALL_ID_CLAIM_WAIT_TIMEOUT_MS,
|
|
894
|
+
): Promise<void> {
|
|
895
|
+
while (performance.now() < deadline) {
|
|
896
|
+
try {
|
|
897
|
+
const stat = await fs.stat(claimPath, { bigint: true });
|
|
898
|
+
const claim = await readClaimIdentity(claimPath);
|
|
899
|
+
if (
|
|
900
|
+
expectedClaim !== undefined &&
|
|
901
|
+
(claim === undefined ||
|
|
902
|
+
claim.dev !== expectedClaim.dev ||
|
|
903
|
+
claim.ino !== expectedClaim.ino ||
|
|
904
|
+
claim.token !== expectedClaim.token ||
|
|
905
|
+
claim.size !== expectedClaim.size ||
|
|
906
|
+
claim.digest !== expectedClaim.digest)
|
|
907
|
+
)
|
|
908
|
+
throw claimLostError();
|
|
909
|
+
const publishingExpired =
|
|
910
|
+
claim?.state === "publishing" &&
|
|
911
|
+
((claim.expiresAt !== undefined &&
|
|
912
|
+
claim.expiresAt <= Date.now() - INSTALL_ID_CLAIM_LEASE_MS &&
|
|
913
|
+
Date.now() - claim.mtimeMs > INSTALL_ID_CLAIM_TIMEOUT_MS) ||
|
|
914
|
+
(claim.expiresAt === undefined && Date.now() - claim.mtimeMs > INSTALL_ID_CLAIM_TIMEOUT_MS));
|
|
915
|
+
const committedStale =
|
|
916
|
+
claim?.state === "committed" &&
|
|
917
|
+
((claim.expiresAt !== undefined && claim.expiresAt <= Date.now()) ||
|
|
918
|
+
(claim.expiresAt === undefined && Date.now() - claim.mtimeMs > INSTALL_ID_CLAIM_LEASE_MS));
|
|
919
|
+
const malformedStale =
|
|
920
|
+
claim !== undefined &&
|
|
921
|
+
claim.state === undefined &&
|
|
922
|
+
stat.size === 0n &&
|
|
923
|
+
Date.now() - claim.mtimeMs > INSTALL_ID_CLAIM_TIMEOUT_MS;
|
|
924
|
+
if (claim !== undefined && (publishingExpired || committedStale || malformedStale)) {
|
|
925
|
+
await reclaimStaleClaim(claimPath, stat, claim);
|
|
926
|
+
await Bun.sleep(INSTALL_ID_CLAIM_POLL_INITIAL_MS);
|
|
927
|
+
continue;
|
|
928
|
+
}
|
|
929
|
+
} catch (error) {
|
|
930
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
931
|
+
if (code === "ENOENT") return;
|
|
932
|
+
if (code === "ECLAIMRACE") {
|
|
933
|
+
await Bun.sleep(INSTALL_ID_CLAIM_POLL_INITIAL_MS);
|
|
934
|
+
continue;
|
|
935
|
+
}
|
|
936
|
+
throw error;
|
|
937
|
+
}
|
|
938
|
+
await Bun.sleep(INSTALL_ID_CLAIM_POLL_INITIAL_MS);
|
|
939
|
+
}
|
|
940
|
+
throw new Error("telemetry install ID claim did not clear");
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
async function convergeAfterClaim(filePath: string): Promise<string> {
|
|
944
|
+
const deadline = performance.now() + INSTALL_ID_CLAIM_WAIT_TIMEOUT_MS;
|
|
945
|
+
while (performance.now() < deadline) {
|
|
946
|
+
const claimPath = `${filePath}.lock`;
|
|
947
|
+
let observed: ClaimIdentity | undefined;
|
|
948
|
+
try {
|
|
949
|
+
observed = await readClaimIdentity(claimPath);
|
|
950
|
+
const expected = observed?.state === "publishing" ? observed : undefined;
|
|
951
|
+
await waitForClaimRelease(claimPath, expected, deadline);
|
|
952
|
+
} catch (error) {
|
|
953
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
954
|
+
if (code !== "ECLAIMLOST" && code !== "ECLAIMRACE") throw error;
|
|
955
|
+
await Bun.sleep(INSTALL_ID_CLAIM_POLL_INITIAL_MS);
|
|
956
|
+
continue;
|
|
957
|
+
}
|
|
958
|
+
try {
|
|
959
|
+
return await readExistingInstallId(filePath);
|
|
960
|
+
} catch (error) {
|
|
961
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
962
|
+
}
|
|
963
|
+
try {
|
|
964
|
+
return await publishPortably(filePath, randomUUID());
|
|
965
|
+
} catch (error) {
|
|
966
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
967
|
+
if (code !== "ECLAIM" && code !== "EEXIST") throw error;
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
throw new Error("telemetry install ID claim did not clear");
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
async function reclaimStaleClaim(claimPath: string, stat: BigIntStats, claim: ClaimIdentity): Promise<void> {
|
|
974
|
+
if (!stat.isFile()) return;
|
|
975
|
+
if (claim.state === "publishing") await syncDirectory(path.dirname(claimPath));
|
|
976
|
+
const currentClaim = await readClaimIdentity(claimPath);
|
|
977
|
+
if (
|
|
978
|
+
currentClaim === undefined ||
|
|
979
|
+
currentClaim.dev !== claim.dev ||
|
|
980
|
+
currentClaim.ino !== claim.ino ||
|
|
981
|
+
currentClaim.token !== claim.token ||
|
|
982
|
+
currentClaim.size !== claim.size ||
|
|
983
|
+
currentClaim.digest !== claim.digest ||
|
|
984
|
+
currentClaim.state !== claim.state ||
|
|
985
|
+
currentClaim.expiresAt !== claim.expiresAt ||
|
|
986
|
+
((currentClaim.state === "publishing" || currentClaim.state === "committed") &&
|
|
987
|
+
(currentClaim.expiresAt === undefined
|
|
988
|
+
? Date.now() - currentClaim.mtimeMs <= INSTALL_ID_CLAIM_TIMEOUT_MS
|
|
989
|
+
: currentClaim.expiresAt > Date.now()))
|
|
990
|
+
)
|
|
991
|
+
return;
|
|
992
|
+
const snapshot = await readClaimSnapshot(claimPath);
|
|
993
|
+
const finalClaim = parseClaim(snapshot.content);
|
|
994
|
+
if (
|
|
995
|
+
finalClaim.token !== claim.token ||
|
|
996
|
+
finalClaim.state !== claim.state ||
|
|
997
|
+
((finalClaim.state === "publishing" || finalClaim.state === "committed") &&
|
|
998
|
+
finalClaim.expiresAt !== undefined &&
|
|
999
|
+
finalClaim.expiresAt > Date.now())
|
|
1000
|
+
)
|
|
1001
|
+
return;
|
|
1002
|
+
const current = snapshot.stat;
|
|
1003
|
+
const currentMtimeMs = Number(current.mtimeMs);
|
|
1004
|
+
if (
|
|
1005
|
+
(finalClaim.state === "publishing" || finalClaim.state === "committed") &&
|
|
1006
|
+
(finalClaim.expiresAt === undefined
|
|
1007
|
+
? Date.now() - currentMtimeMs <= INSTALL_ID_CLAIM_TIMEOUT_MS
|
|
1008
|
+
: finalClaim.expiresAt > Date.now())
|
|
1009
|
+
)
|
|
1010
|
+
return;
|
|
1011
|
+
if (current.dev !== stat.dev || current.ino !== stat.ino) return;
|
|
1012
|
+
const cleanupDigest = snapshot.digest;
|
|
1013
|
+
const cleanupKey = `${claimPath}:${current.dev}:${current.ino}:${current.nlink}:${current.size}:${current.mtimeNs}:${cleanupDigest}:${claim.token}:${claim.state}:${claim.expiresAt ?? ""}`;
|
|
1014
|
+
if (scheduledClaimCleanups.has(cleanupKey)) return;
|
|
1015
|
+
scheduledClaimCleanups.add(cleanupKey);
|
|
1016
|
+
const releaseTimer = setTimeout(() => scheduledClaimCleanups.delete(cleanupKey), INSTALL_ID_CLAIM_TIMEOUT_MS);
|
|
1017
|
+
releaseTimer.unref();
|
|
1018
|
+
scheduleExactDetachedCleanup(claimPath, {
|
|
1019
|
+
dev: current.dev,
|
|
1020
|
+
ino: current.ino,
|
|
1021
|
+
nlink: current.nlink,
|
|
1022
|
+
size: current.size,
|
|
1023
|
+
mtimeNs: current.mtimeNs,
|
|
1024
|
+
sha256: cleanupDigest,
|
|
1025
|
+
allowHardLink: true,
|
|
1026
|
+
quarantineName: `.${path.basename(claimPath)}.${randomUUID()}.quarantine`,
|
|
1027
|
+
});
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
async function removeOwnedClaim(claimPath: string, token: string): Promise<void> {
|
|
1031
|
+
try {
|
|
1032
|
+
const snapshot = await readClaimSnapshot(claimPath);
|
|
1033
|
+
if (parseClaim(snapshot.content).token !== token) return;
|
|
1034
|
+
scheduleExactDetachedCleanup(claimPath, {
|
|
1035
|
+
dev: snapshot.stat.dev,
|
|
1036
|
+
ino: snapshot.stat.ino,
|
|
1037
|
+
nlink: snapshot.stat.nlink,
|
|
1038
|
+
size: snapshot.stat.size,
|
|
1039
|
+
mtimeNs: snapshot.stat.mtimeNs,
|
|
1040
|
+
sha256: snapshot.digest,
|
|
1041
|
+
allowHardLink: true,
|
|
1042
|
+
quarantineName: `.${path.basename(claimPath)}.${randomUUID()}.quarantine`,
|
|
1043
|
+
});
|
|
1044
|
+
} catch (error) {
|
|
1045
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
function scheduleOwnedClaimCleanup(claimPath: string, token: string): void {
|
|
1050
|
+
const cleanupKey = `${claimPath}:${token}`;
|
|
1051
|
+
if (scheduledClaimCleanups.has(cleanupKey)) return;
|
|
1052
|
+
scheduledClaimCleanups.add(cleanupKey);
|
|
1053
|
+
void removeOwnedClaim(claimPath, token).catch(() => undefined);
|
|
1054
|
+
const releaseTimer = setTimeout(() => scheduledClaimCleanups.delete(cleanupKey), INSTALL_ID_CLAIM_TIMEOUT_MS);
|
|
1055
|
+
releaseTimer.unref();
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
async function publishWithClaim(filePath: string, installId: string): Promise<string> {
|
|
1059
|
+
const claimPath = `${filePath}.lock`;
|
|
1060
|
+
const token = randomUUID();
|
|
1061
|
+
const temporaryPath = `${filePath}.${randomUUID()}.tmp`;
|
|
1062
|
+
const claimTemporaryPath = `${claimPath}.${randomUUID()}.tmp`;
|
|
1063
|
+
let ownsClaim = false;
|
|
1064
|
+
let publishedFinal = false;
|
|
1065
|
+
let committed = false;
|
|
1066
|
+
let leaseTimer: NodeJS.Timeout | undefined;
|
|
1067
|
+
let heartbeat: Promise<void> = Promise.resolve();
|
|
1068
|
+
let heartbeatFailure: unknown;
|
|
1069
|
+
let heartbeatStopped = false;
|
|
1070
|
+
let claimTemporaryIdentity: BigIntStats | undefined;
|
|
1071
|
+
let claimTemporaryContent: string | undefined;
|
|
1072
|
+
let claimTemporaryCleanupScheduled = false;
|
|
1073
|
+
let temporaryIdentity: BigIntStats | undefined;
|
|
1074
|
+
let temporaryCleanupScheduled = false;
|
|
1075
|
+
let claim: fs.FileHandle;
|
|
1076
|
+
try {
|
|
1077
|
+
try {
|
|
1078
|
+
claim = await fs.open(claimTemporaryPath, "wx", 0o600);
|
|
1079
|
+
} catch (error) {
|
|
1080
|
+
if ((error as NodeJS.ErrnoException).code === "EEXIST") {
|
|
1081
|
+
const busy = new Error("telemetry install ID claim is busy") as NodeJS.ErrnoException;
|
|
1082
|
+
busy.code = "ECLAIM";
|
|
1083
|
+
throw busy;
|
|
1084
|
+
}
|
|
1085
|
+
throw error;
|
|
1086
|
+
}
|
|
1087
|
+
try {
|
|
1088
|
+
claimTemporaryContent = serializeClaim(token, "publishing", Date.now() + INSTALL_ID_CLAIM_LEASE_MS);
|
|
1089
|
+
await claim.writeFile(claimTemporaryContent, "utf8");
|
|
1090
|
+
await claim.sync();
|
|
1091
|
+
claimTemporaryIdentity = await claim.stat({ bigint: true });
|
|
1092
|
+
} finally {
|
|
1093
|
+
await claim.close();
|
|
1094
|
+
}
|
|
1095
|
+
const claimPublication = await natives.renameNoReplacePathAsync(claimTemporaryPath, claimPath);
|
|
1096
|
+
if (!claimPublication.ok) {
|
|
1097
|
+
if (claimPublication.code === "destination_exists" || claimPublication.reason === "destination_exists") {
|
|
1098
|
+
const busy = new Error("telemetry install ID claim is busy") as NodeJS.ErrnoException;
|
|
1099
|
+
busy.code = "ECLAIM";
|
|
1100
|
+
throw busy;
|
|
1101
|
+
}
|
|
1102
|
+
throw new Error(`telemetry install ID claim publication failed: ${claimPublication.reason}`);
|
|
1103
|
+
}
|
|
1104
|
+
claimTemporaryIdentity = undefined;
|
|
1105
|
+
ownsClaim = (await readClaimIdentity(claimPath))?.token === token;
|
|
1106
|
+
if (!ownsClaim) throw new Error("telemetry install ID claim changed");
|
|
1107
|
+
const scheduleHeartbeat = (): void => {
|
|
1108
|
+
if (heartbeatStopped) return;
|
|
1109
|
+
leaseTimer = setTimeout(() => {
|
|
1110
|
+
leaseTimer = undefined;
|
|
1111
|
+
heartbeat = refreshClaimLease(claimPath, token)
|
|
1112
|
+
.catch(error => {
|
|
1113
|
+
heartbeatFailure = error;
|
|
1114
|
+
})
|
|
1115
|
+
.finally(scheduleHeartbeat);
|
|
1116
|
+
}, INSTALL_ID_HEARTBEAT_INTERVAL_MS);
|
|
1117
|
+
leaseTimer.unref();
|
|
1118
|
+
};
|
|
1119
|
+
scheduleHeartbeat();
|
|
1120
|
+
try {
|
|
1121
|
+
return await readWinnerUnderOwnedClaim(filePath, claimPath, token);
|
|
1122
|
+
} catch (error) {
|
|
1123
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
1124
|
+
}
|
|
1125
|
+
let handle: fs.FileHandle;
|
|
1126
|
+
try {
|
|
1127
|
+
handle = await fs.open(temporaryPath, "wx", 0o600);
|
|
1128
|
+
} catch (error) {
|
|
1129
|
+
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
|
|
1130
|
+
return await readWinnerAfterCollision(filePath, claimPath, token);
|
|
1131
|
+
}
|
|
1132
|
+
try {
|
|
1133
|
+
await handle.writeFile(`${installId}\n`, "utf8");
|
|
1134
|
+
await handle.sync();
|
|
1135
|
+
temporaryIdentity = await handle.stat({ bigint: true });
|
|
1136
|
+
} finally {
|
|
1137
|
+
await handle.close().catch(() => undefined);
|
|
1138
|
+
}
|
|
1139
|
+
heartbeatStopped = true;
|
|
1140
|
+
if (leaseTimer !== undefined) clearTimeout(leaseTimer);
|
|
1141
|
+
await heartbeat;
|
|
1142
|
+
if (heartbeatFailure !== undefined) throw heartbeatFailure;
|
|
1143
|
+
await refreshClaimLease(claimPath, token);
|
|
1144
|
+
heartbeatStopped = false;
|
|
1145
|
+
scheduleHeartbeat();
|
|
1146
|
+
await assertClaimOwned(claimPath, token, "publishing");
|
|
1147
|
+
const publication = await natives.renameNoReplacePathAsync(temporaryPath, filePath);
|
|
1148
|
+
if (!publication.ok) {
|
|
1149
|
+
if (publication.code !== "destination_exists" && publication.reason !== "destination_exists")
|
|
1150
|
+
throw new Error(`telemetry install ID publication failed: ${publication.reason}`);
|
|
1151
|
+
return await readWinnerAfterCollision(filePath, claimPath, token);
|
|
1152
|
+
}
|
|
1153
|
+
publishedFinal = true;
|
|
1154
|
+
await assertClaimOwned(claimPath, token, "publishing");
|
|
1155
|
+
await syncDirectory(path.dirname(filePath));
|
|
1156
|
+
heartbeatStopped = true;
|
|
1157
|
+
if (leaseTimer !== undefined) clearTimeout(leaseTimer);
|
|
1158
|
+
await heartbeat;
|
|
1159
|
+
if (heartbeatFailure !== undefined) throw heartbeatFailure;
|
|
1160
|
+
await transitionClaimCommitted(claimPath, token);
|
|
1161
|
+
heartbeatStopped = false;
|
|
1162
|
+
scheduleHeartbeat();
|
|
1163
|
+
await syncClaimDurably(claimPath, token);
|
|
1164
|
+
heartbeatStopped = true;
|
|
1165
|
+
if (leaseTimer !== undefined) clearTimeout(leaseTimer);
|
|
1166
|
+
await heartbeat;
|
|
1167
|
+
if (heartbeatFailure !== undefined) throw heartbeatFailure;
|
|
1168
|
+
committed = true;
|
|
1169
|
+
return installId;
|
|
1170
|
+
} finally {
|
|
1171
|
+
heartbeatStopped = true;
|
|
1172
|
+
if (leaseTimer !== undefined) clearTimeout(leaseTimer);
|
|
1173
|
+
await heartbeat;
|
|
1174
|
+
if (claimTemporaryIdentity === undefined && claimTemporaryContent !== undefined) {
|
|
1175
|
+
const failedIdentity = await fs.lstat(claimTemporaryPath, { bigint: true }).catch(() => undefined);
|
|
1176
|
+
if (failedIdentity !== undefined) {
|
|
1177
|
+
const failedContent = (await readStagedContentBounded(claimTemporaryPath)) ?? "";
|
|
1178
|
+
scheduleExactStagedCleanup(claimTemporaryPath, failedIdentity, failedContent);
|
|
1179
|
+
claimTemporaryCleanupScheduled = true;
|
|
1180
|
+
claimTemporaryIdentity = failedIdentity;
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
if (temporaryIdentity === undefined) {
|
|
1184
|
+
const failedIdentity = await fs.lstat(temporaryPath, { bigint: true }).catch(() => undefined);
|
|
1185
|
+
if (failedIdentity !== undefined) {
|
|
1186
|
+
const failedContent = (await readStagedContentBounded(temporaryPath)) ?? "";
|
|
1187
|
+
scheduleExactStagedCleanup(temporaryPath, failedIdentity, failedContent);
|
|
1188
|
+
temporaryCleanupScheduled = true;
|
|
1189
|
+
temporaryIdentity = failedIdentity;
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
if (
|
|
1193
|
+
claimTemporaryIdentity !== undefined &&
|
|
1194
|
+
claimTemporaryContent !== undefined &&
|
|
1195
|
+
!claimTemporaryCleanupScheduled
|
|
1196
|
+
) {
|
|
1197
|
+
const currentIdentity = await fs.lstat(claimTemporaryPath, { bigint: true }).catch(() => undefined);
|
|
1198
|
+
if (currentIdentity !== undefined) {
|
|
1199
|
+
const currentContent = (await readStagedContentBounded(claimTemporaryPath)) ?? claimTemporaryContent;
|
|
1200
|
+
scheduleExactStagedCleanup(claimTemporaryPath, currentIdentity, currentContent);
|
|
1201
|
+
claimTemporaryCleanupScheduled = true;
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
if (temporaryIdentity !== undefined && !temporaryCleanupScheduled)
|
|
1205
|
+
scheduleExactStagedCleanup(temporaryPath, temporaryIdentity, `${installId}\n`);
|
|
1206
|
+
if (ownsClaim && (!publishedFinal || committed)) scheduleOwnedClaimCleanup(claimPath, token);
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
async function publishPortably(filePath: string, installId: string): Promise<string> {
|
|
1211
|
+
try {
|
|
1212
|
+
await publishNewInstallId(filePath, installId);
|
|
1213
|
+
return installId;
|
|
1214
|
+
} catch (error) {
|
|
1215
|
+
if ((error as NodeJS.ErrnoException).code !== "EUNSUPPORTED") throw error;
|
|
1216
|
+
return publishWithClaim(filePath, installId);
|
|
112
1217
|
}
|
|
113
1218
|
}
|
|
114
1219
|
|
|
@@ -116,10 +1221,24 @@ async function writeNewInstallId(filePath: string, installId: string): Promise<v
|
|
|
116
1221
|
export async function getTelemetryInstallId(
|
|
117
1222
|
filePath = getTrustedAgentFile(TELEMETRY_INSTALL_ID_FILE),
|
|
118
1223
|
): Promise<string> {
|
|
1224
|
+
const deadline = performance.now() + INSTALL_ID_CLAIM_WAIT_TIMEOUT_MS;
|
|
1225
|
+
while (true) {
|
|
1226
|
+
try {
|
|
1227
|
+
return await getTelemetryInstallIdOnce(filePath);
|
|
1228
|
+
} catch (error) {
|
|
1229
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
1230
|
+
if (code !== "ECLAIMRACE" && code !== "ECLAIMLOST") throw error;
|
|
1231
|
+
if (performance.now() >= deadline) throw new Error("telemetry install ID claim did not clear");
|
|
1232
|
+
await Bun.sleep(INSTALL_ID_CLAIM_POLL_INITIAL_MS);
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
async function getTelemetryInstallIdOnce(filePath: string): Promise<string> {
|
|
119
1238
|
try {
|
|
120
|
-
const existing =
|
|
1239
|
+
const existing = await readExistingInstallId(filePath);
|
|
121
1240
|
if (UUID_V4.test(existing)) {
|
|
122
|
-
await
|
|
1241
|
+
await tightenInstallIdPermissions(filePath, existing);
|
|
123
1242
|
return existing;
|
|
124
1243
|
}
|
|
125
1244
|
throw new Error("telemetry install ID is malformed");
|
|
@@ -130,13 +1249,20 @@ export async function getTelemetryInstallId(
|
|
|
130
1249
|
await fs.mkdir(path.dirname(filePath), { recursive: true, mode: 0o700 });
|
|
131
1250
|
const generated = randomUUID();
|
|
132
1251
|
try {
|
|
133
|
-
await
|
|
134
|
-
|
|
1252
|
+
const published = await publishPortably(filePath, generated);
|
|
1253
|
+
await tightenInstallIdPermissions(filePath, published);
|
|
1254
|
+
return published;
|
|
135
1255
|
} catch (error) {
|
|
136
|
-
if ((error as NodeJS.ErrnoException).code
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
1256
|
+
if ((error as NodeJS.ErrnoException).code === "EEXIST") {
|
|
1257
|
+
const existing = await readExistingInstallId(filePath);
|
|
1258
|
+
await tightenInstallIdPermissions(filePath, existing);
|
|
1259
|
+
return existing;
|
|
1260
|
+
}
|
|
1261
|
+
if ((error as NodeJS.ErrnoException).code === "ECLAIM") {
|
|
1262
|
+
const existing = await convergeAfterClaim(filePath);
|
|
1263
|
+
await tightenInstallIdPermissions(filePath, existing);
|
|
1264
|
+
return existing;
|
|
1265
|
+
}
|
|
1266
|
+
throw error;
|
|
141
1267
|
}
|
|
142
1268
|
}
|