@dev.fast/whiteboard 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +146 -2
- package/THIRD_PARTY_NOTICES.md +93 -0
- package/dist/account-alias-DOHN1RSH.js +973 -0
- package/dist/agent-cli-ChOq0ZuO.js +117 -0
- package/dist/agent-cli-DzWfZr6G.js +2 -0
- package/dist/agent-client-CPQo7iTI.js +305 -0
- package/dist/authoring-tools-Cr6Kpjsx.js +234 -0
- package/dist/build-info.json +1 -0
- package/dist/cli-hVgHcdsv.js +47 -0
- package/dist/cli-runner-D8y2_luU.js +5095 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +128 -0
- package/dist/client-DFoh50W3.js +950 -0
- package/dist/desktop-discovery-DBO-PM2V.js +164 -0
- package/dist/error-message-OtiDonty.js +6 -0
- package/dist/fs-utils-BMPLt0cr.js +22 -0
- package/dist/fuzzy-match-BoAmcyak.js +30 -0
- package/dist/headless-host-BJxf4dUU.js +94 -0
- package/dist/input-error-OLgB_h31.js +11 -0
- package/dist/local-data-CxLk75rx.js +4491 -0
- package/dist/mcp-QeF8vkdf.js +140 -0
- package/dist/package-paths-B6-zxvIO.js +47 -0
- package/dist/process-error-telemetry-v9e5h6D7.js +3952 -0
- package/dist/profile-4ry2f3AM.js +76 -0
- package/dist/profile-Bh5FIrmf.js +2 -0
- package/dist/request-origin-D_4QIdoN.js +811 -0
- package/dist/review-agent-traces-DNJFYzc8.js +3866 -0
- package/dist/review-home-paths-6zZH-1c9.js +2560 -0
- package/dist/review-telemetry-DEFnQsQ2.js +1195 -0
- package/dist/runtime-CKiWhGrf.js +52 -0
- package/dist/runtime.d.ts +9 -0
- package/dist/runtime.js +2 -0
- package/dist/s3-SzLpeCG6.js +345 -0
- package/dist/s3-config-BgxkSoOy.js +2 -0
- package/dist/s3-config-CAxhO9u_.js +676 -0
- package/dist/server/desktop-host.d.ts +4 -0
- package/dist/server/desktop-host.js +1702 -0
- package/dist/server-discovery-Dbzn3w6Z.js +66 -0
- package/dist/sharing/index.d.ts +1977 -0
- package/dist/sharing/index.js +2 -0
- package/dist/src-CGP5ytbV.js +283 -0
- package/dist/src-CmdiBL20.js +3676 -0
- package/dist/src-DnwdaQ2r.js +865 -0
- package/dist/src-X9phtB2j.js +68 -0
- package/dist/stored-document-migration-DHeMpLMb.js +2671 -0
- package/dist/tool-failure-C8zB73HV.js +98 -0
- package/dist/tutorial-trace-DrBvkaeL.js +43 -0
- package/instructions/authoring.md +36 -0
- package/instructions/file-lenses.md +13 -0
- package/instructions/scratchpad.md +23 -0
- package/instructions/trace-archaeology.md +113 -0
- package/onboarding.md +8 -0
- package/package.json +106 -3
- package/src/agent-selection.ts +100 -0
- package/src/agent-session-ref.ts +80 -0
- package/src/ask/agents.ts +351 -0
- package/src/ask/checkout-files.ts +47 -0
- package/src/ask/file-refs.ts +111 -0
- package/src/ask/pi-mcp.ts +154 -0
- package/src/ask/protocol.ts +244 -0
- package/src/ask/thread-state.ts +350 -0
- package/src/ask/thread.ts +1400 -0
- package/src/ask/threads.ts +219 -0
- package/src/ask/watch.ts +79 -0
- package/src/cli-install.ts +1041 -0
- package/src/cli-runner.ts +1461 -0
- package/src/cli-runtime-info.ts +35 -0
- package/src/cli.ts +222 -0
- package/src/connect-prompts.ts +248 -0
- package/src/cursor-deeplink.ts +11 -0
- package/src/desktop-discovery.ts +355 -0
- package/src/diff-selection-migration.ts +63 -0
- package/src/embedded-posthog-key.ts +6 -0
- package/src/error-telemetry.ts +247 -0
- package/src/evidence.ts +14 -0
- package/src/exception-telemetry.ts +126 -0
- package/src/fixtures/blocks/call_stack_diff.json +27 -0
- package/src/fixtures/blocks/callout.json +11 -0
- package/src/fixtures/blocks/code.json +9 -0
- package/src/fixtures/blocks/code_peek.json +8 -0
- package/src/fixtures/blocks/database_lens.json +51 -0
- package/src/fixtures/blocks/divider.json +1 -0
- package/src/fixtures/blocks/fixtures.ts +33 -0
- package/src/fixtures/blocks/flow_diagram.json +36 -0
- package/src/fixtures/blocks/ids.ts +8 -0
- package/src/fixtures/blocks/image.json +9 -0
- package/src/fixtures/blocks/markdown.json +7 -0
- package/src/fixtures/blocks/section.json +11 -0
- package/src/fixtures/blocks/sequence.json +31 -0
- package/src/fixtures/blocks/software_map.json +7 -0
- package/src/fixtures/blocks/trace_quote.json +9 -0
- package/src/fixtures/blocks/tutorial.json +51 -0
- package/src/fs-utils.ts +32 -0
- package/src/fuzzy-match.ts +94 -0
- package/src/install.ts +29 -0
- package/src/legacy-skills.ts +133 -0
- package/src/lens-selection.ts +230 -0
- package/src/markdown-latex-math.ts +230 -0
- package/src/markdown.ts +81 -0
- package/src/package-paths.ts +53 -0
- package/src/posthog-capture-client.ts +610 -0
- package/src/review-api/README.md +295 -0
- package/src/review-api/activity.ts +340 -0
- package/src/review-api/agent-cli.ts +219 -0
- package/src/review-api/agent-client.ts +188 -0
- package/src/review-api/anchor-quotes.ts +88 -0
- package/src/review-api/ask-history.ts +242 -0
- package/src/review-api/authoring-tools.ts +211 -0
- package/src/review-api/blocks/call_stack_diff.ts +75 -0
- package/src/review-api/blocks/callout.ts +24 -0
- package/src/review-api/blocks/code.ts +9 -0
- package/src/review-api/blocks/code_peek.ts +13 -0
- package/src/review-api/blocks/database_lens.ts +150 -0
- package/src/review-api/blocks/definition.ts +40 -0
- package/src/review-api/blocks/divider.ts +6 -0
- package/src/review-api/blocks/flow_diagram.ts +111 -0
- package/src/review-api/blocks/image.ts +10 -0
- package/src/review-api/blocks/index.ts +90 -0
- package/src/review-api/blocks/markdown.ts +17 -0
- package/src/review-api/blocks/section.ts +24 -0
- package/src/review-api/blocks/sequence.ts +56 -0
- package/src/review-api/blocks/software_map.ts +9 -0
- package/src/review-api/blocks/trace_quote.ts +10 -0
- package/src/review-api/blocks/tutorial.ts +39 -0
- package/src/review-api/checkout-fs.ts +14 -0
- package/src/review-api/client.ts +1 -0
- package/src/review-api/comparison-coverage.ts +304 -0
- package/src/review-api/component-reference.ts +30 -0
- package/src/review-api/diff-lenses.ts +175 -0
- package/src/review-api/document-headings.ts +51 -0
- package/src/review-api/document-text.ts +200 -0
- package/src/review-api/document.ts +906 -0
- package/src/review-api/file-lenses.ts +100 -0
- package/src/review-api/http.ts +1877 -0
- package/src/review-api/image-decode.ts +29 -0
- package/src/review-api/input-error.ts +9 -0
- package/src/review-api/instructions.ts +89 -0
- package/src/review-api/lens-alignment.ts +52 -0
- package/src/review-api/local-data.ts +1837 -0
- package/src/review-api/map-input.ts +154 -0
- package/src/review-api/mcp-client-agent.ts +32 -0
- package/src/review-api/mcp.ts +240 -0
- package/src/review-api/origin.ts +43 -0
- package/src/review-api/profile.ts +146 -0
- package/src/review-api/public-tools.ts +102 -0
- package/src/review-api/pull-request.ts +389 -0
- package/src/review-api/read-schemas.ts +85 -0
- package/src/review-api/recovery.ts +2 -0
- package/src/review-api/request-origin.ts +33 -0
- package/src/review-api/review-progress.ts +382 -0
- package/src/review-api/status-tool.ts +10 -0
- package/src/review-api/store-schema.ts +22 -0
- package/src/review-api/store.ts +1647 -0
- package/src/review-api/tool-failure.ts +64 -0
- package/src/review-api/trace-schema.ts +13 -0
- package/src/review-api/traces.ts +119 -0
- package/src/review-api/unsupported-files.integration.ts +91 -0
- package/src/review-api/workspaces.ts +692 -0
- package/src/review-api/worktree-source.ts +170 -0
- package/src/review-api/worktree-structural.integration.ts +320 -0
- package/src/review-app-launcher.ts +432 -0
- package/src/review-app-picker.ts +168 -0
- package/src/review-app.ts +134 -0
- package/src/review-bundled-tools.ts +249 -0
- package/src/review-checkout-paths.ts +37 -0
- package/src/review-diff-files.ts +89 -0
- package/src/review-head-checkout.ts +300 -0
- package/src/review-home-paths.ts +73 -0
- package/src/review-info.ts +59 -0
- package/src/review-instances.ts +113 -0
- package/src/review-logger.ts +183 -0
- package/src/review-preferences.ts +88 -0
- package/src/review-prepare.ts +291 -0
- package/src/review-stack.ts +112 -0
- package/src/review-telemetry.ts +1066 -0
- package/src/runtime.ts +74 -0
- package/src/server/account-alias.ts +30 -0
- package/src/server/bounded-stream.ts +34 -0
- package/src/server/bug-report.ts +301 -0
- package/src/server/client-error-budget.ts +45 -0
- package/src/server/crash-report.ts +273 -0
- package/src/server/desktop-host-shutdown.ts +64 -0
- package/src/server/desktop-host.ts +190 -0
- package/src/server/desktop-server.ts +703 -0
- package/src/server/diffr-config.ts +509 -0
- package/src/server/diffr-languages.ts +97 -0
- package/src/server/global-verb-relay.ts +190 -0
- package/src/server/headless-host.ts +149 -0
- package/src/server/hono-http.ts +162 -0
- package/src/server/http-json.ts +24 -0
- package/src/server/json-review-reporting.ts +174 -0
- package/src/server/process-error-telemetry.ts +174 -0
- package/src/server/review-api-parsers.ts +90 -0
- package/src/server/review-lifecycle-telemetry.ts +73 -0
- package/src/server/review-open-watchdog.ts +46 -0
- package/src/server/review-server-core.ts +290 -0
- package/src/server/structural-comparisons.ts +153 -0
- package/src/server/structural-diff.ts +186 -0
- package/src/server/tutorial-service.ts +241 -0
- package/src/server/ui-telemetry.ts +198 -0
- package/src/server-discovery.ts +95 -0
- package/src/session-markers.ts +132 -0
- package/src/sharing/auth.ts +41 -0
- package/src/sharing/cli.ts +83 -0
- package/src/sharing/client.ts +334 -0
- package/src/sharing/export.ts +195 -0
- package/src/sharing/host.ts +316 -0
- package/src/sharing/import.ts +787 -0
- package/src/sharing/index.ts +16 -0
- package/src/sharing/repository.ts +145 -0
- package/src/sharing/routes.ts +34 -0
- package/src/slug.ts +23 -0
- package/src/software-map-diff-counts.ts +517 -0
- package/src/software-map-model.ts +1147 -0
- package/src/software-map-topology-diff.ts +260 -0
- package/src/source.ts +92 -0
- package/src/startup-trace.ts +232 -0
- package/src/stored-document-migration.ts +151 -0
- package/src/telemetry-clean-text.ts +257 -0
- package/src/telemetry-config.ts +298 -0
- package/src/telemetry-debug-sink.ts +38 -0
- package/src/telemetry.ts +13 -0
- package/src/trace-cli.ts +156 -0
- package/src/trace-storage-cli.ts +509 -0
- package/src/tutorial-conversation.ts +18 -0
- package/src/ui-telemetry-events.ts +765 -0
- package/src/unified-diff.ts +71 -0
- package/src/viewed-coverage.ts +259 -0
- package/src/windows-cli.ts +125 -0
- package/tutorial/document.json +275 -0
- package/tutorial/runtime-manifest.json +12 -0
- package/tutorial/sample-service/package.json +9 -0
- package/tutorial/sample-service/src/api/checkout-api.ts +17 -0
- package/tutorial/sample-service/src/app.ts +26 -0
- package/tutorial/sample-service/src/database/schema.ts +10 -0
- package/tutorial/sample-service/src/fulfillment/fulfillment-queue.ts +15 -0
- package/tutorial/sample-service/src/fulfillment/fulfillment-worker.ts +24 -0
- package/tutorial/sample-service/src/inventory/inventory-service.ts +11 -0
- package/tutorial/sample-service/src/orders/order-service.ts +34 -0
- package/tutorial/sample-service/src/orders/order.ts +21 -0
- package/tutorial/sample-service/src/orders/orders-repository.ts +24 -0
- package/tutorial/sample-service/src/payments/payment-gateway.ts +12 -0
- package/tutorial/sample-service/src/shipping/shipping-gateway.ts +15 -0
- package/tutorial/sample-service/tsconfig.json +12 -0
- package/tutorial/software-map.json +139 -0
- package/tutorial/trace.json +20 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFile, realpath, stat } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
import { gzip } from "node:zlib";
|
|
6
|
+
|
|
7
|
+
import { ReviewBugReportMetaV2Schema } from "@dev.fast/review-protocol";
|
|
8
|
+
import type { PostHogCaptureProperties } from "@review/posthog-capture-client";
|
|
9
|
+
import type { ReviewTelemetry } from "@review/review-telemetry";
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
|
|
12
|
+
const CRASH_REPORT_URL = "https://bug.dev.fast/api/v2/crashes";
|
|
13
|
+
|
|
14
|
+
const gzipAsync = promisify(gzip);
|
|
15
|
+
|
|
16
|
+
/** The Worker's limit on the gzip dump it stores. */
|
|
17
|
+
const DEFAULT_MAX_DUMP_BYTES = 50_000_000;
|
|
18
|
+
|
|
19
|
+
/** Reading a dump this large into memory is not worth it; it can never fit. */
|
|
20
|
+
const MAX_RAW_DUMP_BYTES = 512 * 1024 * 1024;
|
|
21
|
+
|
|
22
|
+
const DUMP_TOO_LARGE = {
|
|
23
|
+
ok: false,
|
|
24
|
+
status: 413,
|
|
25
|
+
error: "Crash dump is too large.",
|
|
26
|
+
} as const;
|
|
27
|
+
|
|
28
|
+
const UPSTREAM_TIMEOUT_MS = 60_000;
|
|
29
|
+
|
|
30
|
+
const version = z.string().regex(/^[0-9A-Za-z.+_-]{1,64}$/);
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The envelope keys the Worker's strict crash meta accepts, with the values it
|
|
34
|
+
* accepts. Any other key is dropped, and a value that would not pass is left
|
|
35
|
+
* out rather than failing the whole upload.
|
|
36
|
+
*/
|
|
37
|
+
const CrashEnvelopeSchema = z.object({
|
|
38
|
+
app_version: version.optional().catch(undefined),
|
|
39
|
+
cli_version: version.optional().catch(undefined),
|
|
40
|
+
channel: z.enum(["stable", "preview", "dev"]).optional().catch(undefined),
|
|
41
|
+
environment: z
|
|
42
|
+
.enum(["production", "ci", "internal", "e2e", "smoke"])
|
|
43
|
+
.optional()
|
|
44
|
+
.catch(undefined),
|
|
45
|
+
surface: z
|
|
46
|
+
.enum(["desktop", "cli", "headless", "mcp", "api"])
|
|
47
|
+
.optional()
|
|
48
|
+
.catch(undefined),
|
|
49
|
+
platform: ReviewBugReportMetaV2Schema.shape.platform
|
|
50
|
+
.optional()
|
|
51
|
+
.catch(undefined),
|
|
52
|
+
arch: z
|
|
53
|
+
.enum([
|
|
54
|
+
"arm",
|
|
55
|
+
"arm64",
|
|
56
|
+
"ia32",
|
|
57
|
+
"loong64",
|
|
58
|
+
"mips",
|
|
59
|
+
"mipsel",
|
|
60
|
+
"ppc",
|
|
61
|
+
"ppc64",
|
|
62
|
+
"riscv64",
|
|
63
|
+
"s390",
|
|
64
|
+
"s390x",
|
|
65
|
+
"x64",
|
|
66
|
+
])
|
|
67
|
+
.optional()
|
|
68
|
+
.catch(undefined),
|
|
69
|
+
os_version: version.optional().catch(undefined),
|
|
70
|
+
node_major: z.number().int().optional().catch(undefined),
|
|
71
|
+
ci: z.boolean().optional().catch(undefined),
|
|
72
|
+
internal: z.boolean().optional().catch(undefined),
|
|
73
|
+
app_session_id: z.uuid().optional().catch(undefined),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
/** The launch a dump was written during, from Electron main's dump ledger. */
|
|
77
|
+
const CrashLaunchSchema = CrashEnvelopeSchema.pick({
|
|
78
|
+
app_session_id: true,
|
|
79
|
+
app_version: true,
|
|
80
|
+
cli_version: true,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
type CrashLaunch = z.infer<typeof CrashLaunchSchema>;
|
|
84
|
+
|
|
85
|
+
/** The body Electron main posts to `/crash-reports`. */
|
|
86
|
+
export const CrashReportRequestSchema = z.object({
|
|
87
|
+
dump_path: z.string().min(1),
|
|
88
|
+
crashed_at: z.number().int().nonnegative(),
|
|
89
|
+
covered: z.boolean(),
|
|
90
|
+
launch: CrashLaunchSchema.optional(),
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
export type CrashReportRequest = z.infer<typeof CrashReportRequestSchema>;
|
|
94
|
+
|
|
95
|
+
export interface CrashUploadResult {
|
|
96
|
+
ok: boolean;
|
|
97
|
+
status?: number;
|
|
98
|
+
error?: string;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* A dump belongs to the launch that wrote it, not to this one: its session id
|
|
103
|
+
* and versions override the current envelope. Without a matching launch the
|
|
104
|
+
* crashed session is unknown, so its id is left out; the versions stay the
|
|
105
|
+
* current launch's, the best guess there is.
|
|
106
|
+
*/
|
|
107
|
+
function crashedLaunchProperties(
|
|
108
|
+
launch: CrashLaunch | undefined,
|
|
109
|
+
): PostHogCaptureProperties {
|
|
110
|
+
const properties = { app_session_id: launch?.app_session_id };
|
|
111
|
+
|
|
112
|
+
if (!launch) return properties;
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
...properties,
|
|
116
|
+
app_version: launch.app_version,
|
|
117
|
+
cli_version: launch.cli_version,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Upload one Crashpad minidump with the telemetry envelope as metadata. The
|
|
123
|
+
* caller has already checked the opt-out; this only packs and posts.
|
|
124
|
+
*/
|
|
125
|
+
export async function uploadCrashDump(input: {
|
|
126
|
+
dumpPath: string;
|
|
127
|
+
crashedAt: number;
|
|
128
|
+
covered: boolean;
|
|
129
|
+
distinctId: string;
|
|
130
|
+
envelope: PostHogCaptureProperties;
|
|
131
|
+
fetchImpl?: typeof fetch;
|
|
132
|
+
maxDumpBytes?: number;
|
|
133
|
+
}): Promise<CrashUploadResult> {
|
|
134
|
+
const maxDumpBytes = input.maxDumpBytes ?? DEFAULT_MAX_DUMP_BYTES;
|
|
135
|
+
|
|
136
|
+
if ((await stat(input.dumpPath)).size > MAX_RAW_DUMP_BYTES)
|
|
137
|
+
return DUMP_TOO_LARGE;
|
|
138
|
+
|
|
139
|
+
const dump = await gzipAsync(await readFile(input.dumpPath), { level: 6 });
|
|
140
|
+
|
|
141
|
+
if (dump.byteLength > maxDumpBytes) return DUMP_TOO_LARGE;
|
|
142
|
+
|
|
143
|
+
const meta = {
|
|
144
|
+
schema_version: 1,
|
|
145
|
+
distinct_id: input.distinctId,
|
|
146
|
+
crashed_at: input.crashedAt,
|
|
147
|
+
covered: input.covered,
|
|
148
|
+
dump_bytes: dump.byteLength,
|
|
149
|
+
dump_sha256: createHash("sha256").update(dump).digest("hex"),
|
|
150
|
+
...CrashEnvelopeSchema.parse(input.envelope),
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const form = new FormData();
|
|
154
|
+
form.append("meta", JSON.stringify(meta));
|
|
155
|
+
form.append(
|
|
156
|
+
"dump",
|
|
157
|
+
new Blob([Uint8Array.from(dump)], { type: "application/gzip" }),
|
|
158
|
+
"minidump.dmp.gz",
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
const response = await (input.fetchImpl ?? fetch)(CRASH_REPORT_URL, {
|
|
163
|
+
method: "POST",
|
|
164
|
+
body: form,
|
|
165
|
+
signal: AbortSignal.timeout(UPSTREAM_TIMEOUT_MS),
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
if (!response.ok)
|
|
169
|
+
return {
|
|
170
|
+
ok: false,
|
|
171
|
+
status: response.status,
|
|
172
|
+
error: "Crash report service failed.",
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
return { ok: true };
|
|
176
|
+
} catch (error) {
|
|
177
|
+
return {
|
|
178
|
+
ok: false,
|
|
179
|
+
status: 502,
|
|
180
|
+
error:
|
|
181
|
+
error instanceof Error ? error.message : "Crash report service failed.",
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* `POST /crash-reports` from Electron main. The server token also reaches the
|
|
188
|
+
* canvas, so only a `.dmp` file whose real path sits inside the Review dump
|
|
189
|
+
* directory is ever read. A dump no live listener covered is counted here as
|
|
190
|
+
* `review_crash`, stamped with its own launch, which the `/telemetry/event`
|
|
191
|
+
* route cannot do: that route stamps the sender's session. Nothing is uploaded
|
|
192
|
+
* when telemetry is off or only printed (the debug sink).
|
|
193
|
+
*/
|
|
194
|
+
export async function reportCrashDump(
|
|
195
|
+
telemetry: Pick<
|
|
196
|
+
ReviewTelemetry,
|
|
197
|
+
"captureEvent" | "envelope" | "getInstallationId" | "sendsEvents"
|
|
198
|
+
>,
|
|
199
|
+
request: CrashReportRequest,
|
|
200
|
+
crashDumpsDir: string | undefined,
|
|
201
|
+
fetchImpl?: typeof fetch,
|
|
202
|
+
): Promise<{
|
|
203
|
+
status: number;
|
|
204
|
+
body: CrashUploadResult & { counted?: boolean; skipped?: string };
|
|
205
|
+
}> {
|
|
206
|
+
const dumpPath = await dumpInside(request.dump_path, crashDumpsDir);
|
|
207
|
+
|
|
208
|
+
if (!dumpPath)
|
|
209
|
+
return {
|
|
210
|
+
status: 403,
|
|
211
|
+
body: { ok: false, error: "Not a Whiteboard crash dump." },
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const launch = crashedLaunchProperties(request.launch);
|
|
215
|
+
const counted = !request.covered;
|
|
216
|
+
|
|
217
|
+
if (counted)
|
|
218
|
+
await telemetry.captureEvent("review_crash", {
|
|
219
|
+
process: "unknown",
|
|
220
|
+
reason: "minidump",
|
|
221
|
+
source: "minidump",
|
|
222
|
+
...launch,
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
if (!(await telemetry.sendsEvents()))
|
|
226
|
+
return {
|
|
227
|
+
status: 200,
|
|
228
|
+
body: { ok: true, counted, skipped: "telemetry_disabled" },
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const result = await uploadCrashDump({
|
|
232
|
+
dumpPath,
|
|
233
|
+
crashedAt: request.crashed_at,
|
|
234
|
+
covered: request.covered,
|
|
235
|
+
distinctId: await telemetry.getInstallationId(),
|
|
236
|
+
envelope: { ...(await telemetry.envelope()), ...launch },
|
|
237
|
+
fetchImpl,
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
status: result.ok ? 200 : (result.status ?? 502),
|
|
242
|
+
body: { ...result, counted },
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function dumpInside(
|
|
247
|
+
file: string,
|
|
248
|
+
directory: string | undefined,
|
|
249
|
+
): Promise<string | undefined> {
|
|
250
|
+
if (!directory) return undefined;
|
|
251
|
+
|
|
252
|
+
try {
|
|
253
|
+
const [realFile, realDirectory] = await Promise.all([
|
|
254
|
+
realpath(file),
|
|
255
|
+
realpath(directory),
|
|
256
|
+
]);
|
|
257
|
+
|
|
258
|
+
const relative = path.relative(realDirectory, realFile);
|
|
259
|
+
|
|
260
|
+
if (
|
|
261
|
+
!realFile.endsWith(".dmp") ||
|
|
262
|
+
!relative ||
|
|
263
|
+
relative === ".." ||
|
|
264
|
+
relative.startsWith(`..${path.sep}`) ||
|
|
265
|
+
path.isAbsolute(relative)
|
|
266
|
+
)
|
|
267
|
+
return undefined;
|
|
268
|
+
|
|
269
|
+
return realFile;
|
|
270
|
+
} catch {
|
|
271
|
+
return undefined;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type JsonValue,
|
|
3
|
+
isJsonObject,
|
|
4
|
+
jsonBoolean,
|
|
5
|
+
jsonProperty,
|
|
6
|
+
jsonString,
|
|
7
|
+
} from "@dev.fast/review-protocol";
|
|
8
|
+
|
|
9
|
+
interface DesktopHostMessagePort {
|
|
10
|
+
on(event: "message", listener: (event: { data: JsonValue }) => void): void;
|
|
11
|
+
off(event: "message", listener: (event: { data: JsonValue }) => void): void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface DesktopHostProcess {
|
|
15
|
+
parentPort?: DesktopHostMessagePort;
|
|
16
|
+
on(event: "message", listener: (message: JsonValue) => void): void;
|
|
17
|
+
off(event: "message", listener: (message: JsonValue) => void): void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function listenForDesktopHostShutdown(
|
|
21
|
+
hostProcess: DesktopHostProcess,
|
|
22
|
+
onShutdown: () => void,
|
|
23
|
+
onTelemetrySetting?: (enabled: boolean) => void,
|
|
24
|
+
onStageRustAnalyzer?: (path: string) => void,
|
|
25
|
+
): () => void {
|
|
26
|
+
const handleMessage = (message: JsonValue) => {
|
|
27
|
+
if (!isJsonObject(message)) return;
|
|
28
|
+
const type = jsonProperty(message, "type");
|
|
29
|
+
|
|
30
|
+
if (type === "shutdown") {
|
|
31
|
+
onShutdown();
|
|
32
|
+
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (type === "telemetry-setting") {
|
|
37
|
+
const enabled = jsonBoolean(jsonProperty(message, "enabled"));
|
|
38
|
+
|
|
39
|
+
if (enabled !== undefined) onTelemetrySetting?.(enabled);
|
|
40
|
+
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (type === "stage-rust-analyzer") {
|
|
45
|
+
const path = jsonString(jsonProperty(message, "path"));
|
|
46
|
+
|
|
47
|
+
if (path) onStageRustAnalyzer?.(path);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
if (hostProcess.parentPort) {
|
|
52
|
+
const handleParentMessage = (event: { data: JsonValue }) => {
|
|
53
|
+
handleMessage(event.data);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
hostProcess.parentPort.on("message", handleParentMessage);
|
|
57
|
+
|
|
58
|
+
return () => hostProcess.parentPort?.off("message", handleParentMessage);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
hostProcess.on("message", handleMessage);
|
|
62
|
+
|
|
63
|
+
return () => hostProcess.off("message", handleMessage);
|
|
64
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { mkdir } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
import { reviewInstanceIdentity } from "@review/desktop-discovery";
|
|
7
|
+
import { findReviewPackageRoot } from "@review/package-paths";
|
|
8
|
+
import { openReviewProfile } from "@review/review-api/profile";
|
|
9
|
+
import { ensureBundledRustAnalyzer } from "@review/review-bundled-tools";
|
|
10
|
+
import { devReviewHome } from "@review/review-home-paths";
|
|
11
|
+
import { ReviewTelemetry } from "@review/review-telemetry";
|
|
12
|
+
import { SharedReviewStore } from "@review/sharing/import.js";
|
|
13
|
+
import { reviewTelemetryChannel } from "@review/telemetry-config";
|
|
14
|
+
|
|
15
|
+
import { listenForDesktopHostShutdown } from "./desktop-host-shutdown";
|
|
16
|
+
import { createGlobalReviewServer } from "./desktop-server";
|
|
17
|
+
import {
|
|
18
|
+
drainServerCrashReport,
|
|
19
|
+
installProcessErrorTelemetry,
|
|
20
|
+
serverCrashReportPath,
|
|
21
|
+
} from "./process-error-telemetry";
|
|
22
|
+
|
|
23
|
+
export async function runDesktopHost(
|
|
24
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
25
|
+
): Promise<void> {
|
|
26
|
+
// Port 0 lets the OS choose; the ready event below reports what was bound.
|
|
27
|
+
const port = requiredPort(env.DEV_FAST_REVIEW_SERVER_PORT);
|
|
28
|
+
|
|
29
|
+
const appPid = requiredPositiveInteger(
|
|
30
|
+
env.DEV_FAST_REVIEW_APP_PID,
|
|
31
|
+
"DEV_FAST_REVIEW_APP_PID",
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const packageRoot = findReviewPackageRoot(import.meta.url);
|
|
35
|
+
const toolingRoot = env.DEV_FAST_REVIEW_TOOLING_ROOT || packageRoot;
|
|
36
|
+
const telemetryEnv = { ...env };
|
|
37
|
+
delete telemetryEnv.DEV_FAST_REVIEW_TELEMETRY_DISABLED;
|
|
38
|
+
|
|
39
|
+
const telemetry = ReviewTelemetry.fromEnv(telemetryEnv, {
|
|
40
|
+
surface: "desktop",
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
installProcessErrorTelemetry(telemetry, {
|
|
44
|
+
appSessionId: env.DEV_FAST_REVIEW_APP_SESSION_ID,
|
|
45
|
+
crashReportPath: serverCrashReportPath(env),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
await telemetry.setEnabled(
|
|
49
|
+
!isEnabledEnvValue(env.DEV_FAST_REVIEW_TELEMETRY_DISABLED),
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
// An unpackaged Desktop is ours; remember that so every later process on
|
|
53
|
+
// this machine, CLI included, reports internal.
|
|
54
|
+
if (reviewTelemetryChannel(env) === "dev") await telemetry.setInternal(true);
|
|
55
|
+
await telemetry.captureInstallationCreated().catch(() => undefined);
|
|
56
|
+
await telemetry.reconcileOpenSessions().catch(() => undefined);
|
|
57
|
+
await drainServerCrashReport(telemetry, serverCrashReportPath(env));
|
|
58
|
+
const installationId = await telemetry.getInstallationId();
|
|
59
|
+
// This value bootstraps the stored setting. Remove it after persistence so
|
|
60
|
+
// a later in-app enable also reaches telemetry instances created elsewhere.
|
|
61
|
+
delete env.DEV_FAST_REVIEW_TELEMETRY_DISABLED;
|
|
62
|
+
|
|
63
|
+
const serverInput = {
|
|
64
|
+
appPid,
|
|
65
|
+
packageRoot,
|
|
66
|
+
toolingRoot,
|
|
67
|
+
port,
|
|
68
|
+
token: env.DEV_FAST_REVIEW_SERVER_TOKEN,
|
|
69
|
+
instanceId: env.DEV_FAST_REVIEW_INSTANCE_ID,
|
|
70
|
+
identity: reviewInstanceIdentity(env),
|
|
71
|
+
telemetry,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const home = devReviewHome(env);
|
|
75
|
+
await mkdir(home, { recursive: true });
|
|
76
|
+
|
|
77
|
+
const local = await openReviewProfile(home, {
|
|
78
|
+
manageWorkspaces: true,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// JSON is the sole user-review store. A failure is surfaced, never replaced
|
|
82
|
+
// by a second catalog or an old document renderer.
|
|
83
|
+
|
|
84
|
+
const shared = new SharedReviewStore(path.join(home, "shared-reviews"));
|
|
85
|
+
shared.connect(local.store, local.data);
|
|
86
|
+
await shared.load();
|
|
87
|
+
|
|
88
|
+
const server = createGlobalReviewServer({
|
|
89
|
+
...serverInput,
|
|
90
|
+
sharedReviews: shared,
|
|
91
|
+
reviewStore: local.store,
|
|
92
|
+
reviewData: local.data,
|
|
93
|
+
cliRuntimePath: env.DEV_FAST_REVIEW_CLI_RUNTIME,
|
|
94
|
+
crashDumpsDir: env.DEV_FAST_REVIEW_CRASH_DUMPS_DIR,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
await server.listen();
|
|
99
|
+
} catch (error) {
|
|
100
|
+
await local?.data.close();
|
|
101
|
+
await local?.store.close();
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
process.stdout.write(
|
|
106
|
+
`${JSON.stringify({ event: "ready", ...server.discovery, installationId })}\n`,
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const stageRustAnalyzer = () =>
|
|
110
|
+
ensureBundledRustAnalyzer({ env }).catch((error) => {
|
|
111
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
112
|
+
process.stderr.write(
|
|
113
|
+
`[Review tools] Could not stage bundled rust-analyzer: ${reason}\n`,
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
void stageRustAnalyzer();
|
|
118
|
+
|
|
119
|
+
let stopping: Promise<void> | null = null;
|
|
120
|
+
|
|
121
|
+
const stop = () => {
|
|
122
|
+
if (!stopping) {
|
|
123
|
+
stopping = server
|
|
124
|
+
.close("app-exit")
|
|
125
|
+
.finally(() => shared.close())
|
|
126
|
+
.finally(() => local?.data.close())
|
|
127
|
+
.finally(() => local?.store.close());
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return stopping;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
listenForDesktopHostShutdown(
|
|
134
|
+
process,
|
|
135
|
+
() => {
|
|
136
|
+
void stop().then(() => process.exit(0));
|
|
137
|
+
},
|
|
138
|
+
(enabled) => {
|
|
139
|
+
void telemetry.setEnabled(enabled).catch(() => undefined);
|
|
140
|
+
},
|
|
141
|
+
(sourcePath) => {
|
|
142
|
+
env.DEV_FAST_REVIEW_RUST_ANALYZER = sourcePath;
|
|
143
|
+
void stageRustAnalyzer();
|
|
144
|
+
},
|
|
145
|
+
);
|
|
146
|
+
process.once("SIGINT", () => {
|
|
147
|
+
void stop().then(() => process.exit(0));
|
|
148
|
+
});
|
|
149
|
+
process.once("SIGTERM", () => {
|
|
150
|
+
void stop().then(() => process.exit(0));
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function isEnabledEnvValue(value: string | undefined): boolean {
|
|
155
|
+
return value === "1" || value?.toLowerCase() === "true";
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function requiredPort(value: string | undefined): number {
|
|
159
|
+
const parsed = Number(value);
|
|
160
|
+
|
|
161
|
+
if (!Number.isSafeInteger(parsed) || parsed < 0 || parsed > 65_535) {
|
|
162
|
+
throw new Error(
|
|
163
|
+
"DEV_FAST_REVIEW_SERVER_PORT must be a port between 0 and 65535.",
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return parsed;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function requiredPositiveInteger(
|
|
171
|
+
value: string | undefined,
|
|
172
|
+
name: string,
|
|
173
|
+
): number {
|
|
174
|
+
const parsed = Number(value);
|
|
175
|
+
|
|
176
|
+
if (!Number.isSafeInteger(parsed) || parsed <= 0) {
|
|
177
|
+
throw new Error(`${name} must be a positive integer.`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return parsed;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (process.env.DEV_FAST_REVIEW_DESKTOP_HOST_AUTOSTART !== "0") {
|
|
184
|
+
void runDesktopHost().catch((error) => {
|
|
185
|
+
process.stderr.write(
|
|
186
|
+
`${error instanceof Error ? (error.stack ?? error.message) : String(error)}\n`,
|
|
187
|
+
);
|
|
188
|
+
process.exitCode = 1;
|
|
189
|
+
});
|
|
190
|
+
}
|