@gajae-code/coding-agent 0.15.5 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +85 -1
- package/dist/types/async/job-manager.d.ts +83 -0
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/auth-gateway-cli.d.ts +17 -0
- package/dist/types/cli/model-presets-cli.d.ts +9 -0
- package/dist/types/cli/root-flags.d.ts +14 -0
- package/dist/types/cli/update-cli.d.ts +15 -0
- package/dist/types/cli.d.ts +7 -0
- package/dist/types/commands/auth-gateway.d.ts +3 -0
- package/dist/types/commands/gc.d.ts +13 -0
- package/dist/types/commands/launch.d.ts +10 -0
- package/dist/types/commands/model-presets.d.ts +21 -0
- package/dist/types/config/autorouting-tier-map.d.ts +120 -0
- package/dist/types/config/file-lock.d.ts +54 -2
- package/dist/types/config/internal/model-preset-registry-test-state.d.ts +4 -0
- package/dist/types/config/internal/model-preset-registry-test-support.d.ts +3 -0
- package/dist/types/config/keybindings.d.ts +3 -2
- package/dist/types/config/model-preset-registry.d.ts +368 -0
- package/dist/types/config/model-profile-activation.d.ts +39 -0
- package/dist/types/config/model-profiles.d.ts +5 -3
- package/dist/types/config/model-registry.d.ts +17 -5
- package/dist/types/config/settings-schema.d.ts +19 -1
- package/dist/types/config/settings.d.ts +2 -0
- package/dist/types/coordinator-mcp/model-preset.d.ts +7 -5
- package/dist/types/coordinator-mcp/projection-scan.d.ts +54 -0
- package/dist/types/crash/record-loader.d.ts +15 -0
- package/dist/types/extensibility/extensions/types.d.ts +2 -5
- package/dist/types/gjc-runtime/empty-delete-gc.d.ts +58 -0
- package/dist/types/gjc-runtime/gc-runtime.d.ts +2 -0
- package/dist/types/gjc-runtime/launch-worktree-reservation.d.ts +21 -0
- package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +2 -0
- package/dist/types/gjc-runtime/session-state-lock.d.ts +35 -6
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +72 -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/index.d.ts +1 -0
- package/dist/types/main.d.ts +3 -1
- 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/components/frictionless-onboarding-selector.d.ts +10 -0
- package/dist/types/modes/components/jobs-overlay-model.d.ts +4 -2
- package/dist/types/modes/components/oauth-selector.d.ts +1 -0
- 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/jobs-observer.d.ts +36 -0
- package/dist/types/modes/print-mode.d.ts +17 -0
- package/dist/types/modes/ui-language.d.ts +7 -1
- package/dist/types/sdk/broker/broker.d.ts +80 -1
- package/dist/types/sdk/broker/endpoint-authority.d.ts +24 -0
- package/dist/types/sdk/broker/index.d.ts +3 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +26 -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/state-version.d.ts +4 -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/index.d.ts +6 -0
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +3 -0
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +4 -0
- 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 +4 -3
- package/dist/types/sdk/cli/session-cli.d.ts +18 -2
- package/dist/types/sdk/client/client.d.ts +3 -2
- package/dist/types/sdk/client/discovery.d.ts +1 -0
- 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/host.d.ts +10 -0
- package/dist/types/sdk/host/model-pin.d.ts +7 -2
- package/dist/types/sdk/host/sdk-run-capability.d.ts +1 -0
- package/dist/types/sdk/host/session-runtime.d.ts +67 -1
- 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 +2 -0
- package/dist/types/sdk/prompt-deadline-manager.d.ts +11 -1
- package/dist/types/sdk/protocol/adapter-validation.d.ts +2 -0
- package/dist/types/sdk/router/session-router.d.ts +11 -0
- package/dist/types/sdk/session.d.ts +16 -0
- package/dist/types/sdk/startup-capability.d.ts +2 -1
- package/dist/types/session/account-inventory.d.ts +1 -0
- package/dist/types/session/agent-session.d.ts +24 -7
- package/dist/types/session/credential-store-errors.d.ts +1 -0
- package/dist/types/session/fold-coordinator.d.ts +201 -0
- package/dist/types/session/sdk-run-capability-internal.d.ts +7 -0
- package/dist/types/session/sdk-run-capability.d.ts +2 -0
- package/dist/types/session/session-manager.d.ts +65 -0
- package/dist/types/session/yield-queue.d.ts +5 -4
- package/dist/types/setup/frictionless-onboarding.d.ts +1 -0
- package/dist/types/telemetry/control.d.ts +11 -0
- package/dist/types/telemetry/events.d.ts +20 -0
- package/dist/types/telemetry/index.d.ts +3 -0
- package/dist/types/telemetry/transport.d.ts +22 -0
- package/dist/types/tools/bash-interactive.d.ts +23 -0
- package/dist/types/tools/bash.d.ts +4 -0
- package/dist/types/tools/gh.d.ts +8 -0
- package/dist/types/tools/index.d.ts +10 -5
- package/dist/types/web/search/providers/base.d.ts +9 -4
- package/dist/types/web/search/providers/xai.d.ts +2 -1
- package/dist/types/web/search/types.d.ts +14 -0
- package/package.json +10 -7
- package/scripts/generate-hotkeys-docs.ts +4 -0
- package/scripts/generate-sdk-operation-inventory.ts +9 -3
- package/scripts/verify-gjc-sdk-canonicalization.ts +35 -1
- package/src/async/job-manager.ts +671 -39
- package/src/cli/accounts-cli.ts +9 -5
- package/src/cli/args.ts +17 -0
- package/src/cli/auth-broker-cli.ts +40 -12
- package/src/cli/auth-gateway-cli.ts +273 -97
- package/src/cli/config-cli.ts +2 -2
- package/src/cli/fast-help.ts +4 -0
- package/src/cli/model-presets-cli.ts +100 -0
- package/src/cli/root-flags.ts +5 -0
- package/src/cli/update-cli.ts +117 -16
- package/src/cli.ts +6 -2
- package/src/commands/auth-gateway.ts +12 -8
- package/src/commands/gc.ts +19 -0
- package/src/commands/launch.ts +86 -18
- package/src/commands/model-presets.ts +24 -0
- package/src/commands/sdk.ts +113 -21
- package/src/config/autorouting-tier-map.ts +44 -0
- package/src/config/file-lock-gc.ts +9 -4
- package/src/config/file-lock.ts +890 -99
- package/src/config/internal/model-preset-registry-test-state.ts +28 -0
- package/src/config/internal/model-preset-registry-test-support.ts +11 -0
- package/src/config/keybindings.ts +6 -2
- package/src/config/model-preset-registry.ts +2795 -0
- package/src/config/model-profile-activation.ts +115 -28
- package/src/config/model-profile-contract.ts +4 -1
- package/src/config/model-profiles.ts +56 -14
- package/src/config/model-registry.ts +1044 -122
- package/src/config/model-resolver.ts +5 -0
- package/src/config/settings-schema.ts +19 -1
- package/src/config/settings.ts +11 -1
- package/src/coordinator-mcp/model-preset.ts +32 -17
- package/src/coordinator-mcp/projection-scan.ts +592 -0
- package/src/coordinator-mcp/question-state.ts +9 -11
- package/src/coordinator-mcp/server.ts +111 -42
- package/src/coordinator-mcp/session-reaper.ts +9 -3
- package/src/crash/index-store.ts +2 -0
- package/src/crash/record-loader.ts +10 -0
- package/src/extensibility/extensions/loader.ts +0 -1
- package/src/extensibility/extensions/types.ts +2 -3
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +1 -0
- package/src/gjc-runtime/empty-delete-gc.ts +301 -0
- package/src/gjc-runtime/gc-render.ts +29 -0
- package/src/gjc-runtime/gc-runtime.ts +174 -11
- package/src/gjc-runtime/launch-worktree-reservation.ts +242 -0
- package/src/gjc-runtime/managed-owner-supervisor.ts +63 -51
- package/src/gjc-runtime/session-state-lock.ts +1321 -147
- package/src/gjc-runtime/session-state-sidecar.ts +1004 -25
- package/src/gjc-runtime/tmux-owner-isolation-cli.ts +4 -7
- package/src/gjc-runtime/tmux-owner-isolation.ts +2 -2
- package/src/gjc-runtime/tmux-sessions.ts +252 -13
- package/src/hooks/skill-state.ts +6 -0
- package/src/index.ts +1 -0
- package/src/internal-urls/docs-index.generated.ts +14 -13
- package/src/internal-urls/issue-pr-protocol.ts +4 -2
- package/src/lsp/config.ts +4 -3
- package/src/lsp/path-trust.ts +12 -6
- package/src/main.ts +66 -4
- 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 +616 -161
- package/src/modes/components/footer.ts +4 -2
- package/src/modes/components/frictionless-onboarding-selector.ts +48 -2
- package/src/modes/components/jobs-overlay-model.ts +99 -16
- package/src/modes/components/model-selector.ts +158 -18
- package/src/modes/components/oauth-selector.ts +6 -1
- 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 +29 -10
- package/src/modes/components/tool-status-header.ts +151 -15
- package/src/modes/controllers/extension-ui-controller.ts +1 -1
- package/src/modes/controllers/input-controller.ts +51 -6
- package/src/modes/controllers/selector-controller.ts +191 -96
- package/src/modes/interactive-mode.ts +14 -1
- package/src/modes/jobs-observer.ts +149 -16
- package/src/modes/print-mode.ts +64 -0
- package/src/modes/runtime-init.ts +1 -1
- package/src/modes/ui-language.ts +49 -2
- package/src/modes/utils/injected-user-submission.ts +15 -1
- package/src/prompts/system/master-mode.md +28 -0
- package/src/prompts/tools/bash.md +2 -2
- package/src/prompts/tools/fold-receipt.md +3 -0
- package/src/prompts/tools/job.md +1 -1
- package/src/sdk/acp/adapter.ts +17 -2
- package/src/sdk/broker/broker.ts +1671 -34
- package/src/sdk/broker/endpoint-authority.ts +86 -0
- package/src/sdk/broker/ensure.ts +5 -0
- package/src/sdk/broker/index.ts +3 -0
- package/src/sdk/broker/lifecycle.ts +217 -46
- 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 +548 -0
- package/src/sdk/broker/state-version.ts +26 -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 +46 -5
- package/src/sdk/bus/index.ts +51 -4
- package/src/sdk/bus/kind-aware-reconciliation.ts +36 -6
- package/src/sdk/bus/prompt-reconciliation.ts +28 -4
- package/src/sdk/bus/slack-conversation.ts +5 -1
- 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 +11 -8
- package/src/sdk/cli/session-cli.ts +276 -6
- package/src/sdk/client/client.ts +14 -8
- package/src/sdk/client/discovery.ts +3 -1
- package/src/sdk/host/control/dispatch.ts +101 -37
- 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/host.ts +27 -2
- package/src/sdk/host/model-pin.ts +185 -25
- package/src/sdk/host/sdk-run-capability.ts +1 -0
- package/src/sdk/host/session-runtime.test.ts +581 -43
- package/src/sdk/host/session-runtime.ts +655 -101
- package/src/sdk/lifecycle/broker-client.ts +28 -0
- package/src/sdk/lifecycle/service.ts +279 -20
- package/src/sdk/lifecycle-session.ts +10 -2
- package/src/sdk/prompt-deadline-manager.ts +16 -1
- package/src/sdk/protocol/adapter-validation.ts +27 -0
- package/src/sdk/protocol/operation-inventory.generated.json +53 -2
- package/src/sdk/protocol/operation-registry.ts +17 -4
- package/src/sdk/router/session-router.ts +294 -154
- package/src/sdk/session.ts +440 -167
- package/src/sdk/startup-capability.ts +1 -0
- package/src/session/account-inventory.ts +27 -11
- package/src/session/agent-session.ts +1292 -180
- package/src/session/blob-store.ts +15 -5
- package/src/session/credential-store-errors.ts +2 -0
- package/src/session/fold-coordinator.ts +433 -0
- package/src/session/internal/managed-session-storage.ts +153 -78
- package/src/session/sdk-run-capability-internal.ts +15 -0
- package/src/session/sdk-run-capability.ts +2 -0
- package/src/session/session-manager.ts +342 -28
- package/src/session/yield-queue.ts +24 -10
- package/src/setup/frictionless-onboarding.ts +2 -0
- package/src/setup/provider-presets.json +1 -1
- package/src/slash-commands/helpers/credential-switch.ts +6 -1
- package/src/task/executor.ts +19 -3
- package/src/telemetry/control.ts +26 -0
- package/src/telemetry/events.ts +1268 -0
- package/src/telemetry/index.ts +3 -0
- package/src/telemetry/transport.ts +87 -0
- package/src/tools/bash-interactive.ts +182 -85
- package/src/tools/bash.ts +1028 -109
- package/src/tools/gh.ts +104 -48
- package/src/tools/github-cache.ts +2 -2
- package/src/tools/index.ts +11 -5
- package/src/tools/tool-catalog.generated.ts +1 -1
- package/src/web/search/provider.ts +120 -11
- package/src/web/search/providers/anthropic.ts +89 -17
- package/src/web/search/providers/base.ts +9 -4
- package/src/web/search/providers/gemini.ts +87 -15
- package/src/web/search/providers/openai-compatible.ts +23 -8
- package/src/web/search/providers/xai.ts +98 -12
- package/src/web/search/types.ts +16 -0
|
@@ -0,0 +1,1268 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import type { BigIntStats } from "node:fs";
|
|
3
|
+
import * as fs from "node:fs/promises";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import type { NativeExactFileIdentity } from "@gajae-code/natives";
|
|
6
|
+
import * as natives from "@gajae-code/natives";
|
|
7
|
+
import { getTrustedAgentFile } from "@gajae-code/utils";
|
|
8
|
+
|
|
9
|
+
export const TELEMETRY_SCHEMA_VERSION = 1 as const;
|
|
10
|
+
export const TELEMETRY_INSTALL_ID_FILE = "telemetry-install-id" as const;
|
|
11
|
+
|
|
12
|
+
export const TELEMETRY_EVENT_NAMES = [
|
|
13
|
+
"update_check_started",
|
|
14
|
+
"update_check_completed",
|
|
15
|
+
"update_install_started",
|
|
16
|
+
"update_install_completed",
|
|
17
|
+
"update_install_failed",
|
|
18
|
+
] as const;
|
|
19
|
+
|
|
20
|
+
export type TelemetryEventName = (typeof TELEMETRY_EVENT_NAMES)[number];
|
|
21
|
+
|
|
22
|
+
export interface TelemetryEvent {
|
|
23
|
+
schemaVersion: typeof TELEMETRY_SCHEMA_VERSION;
|
|
24
|
+
event: TelemetryEventName;
|
|
25
|
+
installId: string;
|
|
26
|
+
occurredAt: string;
|
|
27
|
+
channel?: "stable" | "nightly";
|
|
28
|
+
result?: "available" | "up_to_date" | "installed" | "failed" | "skipped";
|
|
29
|
+
installMethod?: "bun" | "npm" | "binary" | "migrate";
|
|
30
|
+
}
|
|
31
|
+
type EventInput = {
|
|
32
|
+
event?: unknown;
|
|
33
|
+
installId?: unknown;
|
|
34
|
+
occurredAt?: unknown;
|
|
35
|
+
channel?: unknown;
|
|
36
|
+
result?: unknown;
|
|
37
|
+
installMethod?: unknown;
|
|
38
|
+
[key: string]: unknown;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const EVENT_NAMES = new Set<string>(TELEMETRY_EVENT_NAMES);
|
|
42
|
+
const CHANNELS = new Set(["stable", "nightly"]);
|
|
43
|
+
const RESULTS = new Set(["available", "up_to_date", "installed", "failed", "skipped"]);
|
|
44
|
+
const INSTALL_METHODS = new Set(["bun", "npm", "binary", "migrate"]);
|
|
45
|
+
const FORBIDDEN_KEY = /(?:prompt|argv|path|env|secret|account|model|provider|repo|error|hostname|username|machine|ip)/i;
|
|
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>();
|
|
62
|
+
|
|
63
|
+
function hasForbiddenKey(value: unknown, seen = new Set<object>()): boolean {
|
|
64
|
+
if (value === null || typeof value !== "object") return false;
|
|
65
|
+
if (seen.has(value)) return false;
|
|
66
|
+
seen.add(value);
|
|
67
|
+
for (const [key, child] of Object.entries(value)) {
|
|
68
|
+
if (FORBIDDEN_KEY.test(key) || hasForbiddenKey(child, seen)) return true;
|
|
69
|
+
}
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function requireString(value: unknown, field: string): string {
|
|
74
|
+
if (typeof value !== "string" || value.length === 0 || value.length > 128) {
|
|
75
|
+
throw new Error(`invalid telemetry ${field}`);
|
|
76
|
+
}
|
|
77
|
+
return value;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Serialize only the versioned telemetry allowlist. Unknown fields are never
|
|
82
|
+
* emitted; forbidden fields anywhere in the input fail closed.
|
|
83
|
+
*/
|
|
84
|
+
export function serializeTelemetryEvent(input: unknown): string {
|
|
85
|
+
if (input === null || typeof input !== "object" || Array.isArray(input)) {
|
|
86
|
+
throw new Error("telemetry event must be an object");
|
|
87
|
+
}
|
|
88
|
+
if (hasForbiddenKey(input)) throw new Error("telemetry event contains forbidden data");
|
|
89
|
+
const value = input as EventInput;
|
|
90
|
+
const event = requireString(value.event, "event");
|
|
91
|
+
if (!EVENT_NAMES.has(event)) throw new Error("invalid telemetry event");
|
|
92
|
+
const installId = requireString(value.installId, "installId");
|
|
93
|
+
if (!UUID_V4.test(installId)) throw new Error("invalid telemetry installId");
|
|
94
|
+
const occurredAt = requireString(value.occurredAt, "occurredAt");
|
|
95
|
+
if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(occurredAt) || Number.isNaN(Date.parse(occurredAt))) {
|
|
96
|
+
throw new Error("invalid telemetry occurredAt");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const output: TelemetryEvent = {
|
|
100
|
+
schemaVersion: TELEMETRY_SCHEMA_VERSION,
|
|
101
|
+
event: event as TelemetryEventName,
|
|
102
|
+
installId,
|
|
103
|
+
occurredAt,
|
|
104
|
+
};
|
|
105
|
+
if (value.channel !== undefined) {
|
|
106
|
+
if (typeof value.channel !== "string" || !CHANNELS.has(value.channel))
|
|
107
|
+
throw new Error("invalid telemetry channel");
|
|
108
|
+
output.channel = value.channel as TelemetryEvent["channel"];
|
|
109
|
+
}
|
|
110
|
+
if (value.result !== undefined) {
|
|
111
|
+
if (typeof value.result !== "string" || !RESULTS.has(value.result)) throw new Error("invalid telemetry result");
|
|
112
|
+
output.result = value.result as TelemetryEvent["result"];
|
|
113
|
+
}
|
|
114
|
+
if (value.installMethod !== undefined) {
|
|
115
|
+
if (typeof value.installMethod !== "string" || !INSTALL_METHODS.has(value.installMethod)) {
|
|
116
|
+
throw new Error("invalid telemetry installMethod");
|
|
117
|
+
}
|
|
118
|
+
output.installMethod = value.installMethod as TelemetryEvent["installMethod"];
|
|
119
|
+
}
|
|
120
|
+
return `${JSON.stringify(output)}\n`;
|
|
121
|
+
}
|
|
122
|
+
|
|
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;
|
|
128
|
+
try {
|
|
129
|
+
handle = await fs.open(tempPath, "wx", 0o600);
|
|
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;
|
|
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);
|
|
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);
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
/** Load or create a random UUIDv4 that is not derived from machine data. */
|
|
1221
|
+
export async function getTelemetryInstallId(
|
|
1222
|
+
filePath = getTrustedAgentFile(TELEMETRY_INSTALL_ID_FILE),
|
|
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> {
|
|
1238
|
+
try {
|
|
1239
|
+
const existing = await readExistingInstallId(filePath);
|
|
1240
|
+
if (UUID_V4.test(existing)) {
|
|
1241
|
+
await tightenInstallIdPermissions(filePath, existing);
|
|
1242
|
+
return existing;
|
|
1243
|
+
}
|
|
1244
|
+
throw new Error("telemetry install ID is malformed");
|
|
1245
|
+
} catch (error) {
|
|
1246
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true, mode: 0o700 });
|
|
1250
|
+
const generated = randomUUID();
|
|
1251
|
+
try {
|
|
1252
|
+
const published = await publishPortably(filePath, generated);
|
|
1253
|
+
await tightenInstallIdPermissions(filePath, published);
|
|
1254
|
+
return published;
|
|
1255
|
+
} catch (error) {
|
|
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;
|
|
1267
|
+
}
|
|
1268
|
+
}
|