Skip to content

Commit 14ec48b

Browse files
committed
implement
1 parent 274ab9f commit 14ec48b

File tree

11 files changed

+662
-27
lines changed

11 files changed

+662
-27
lines changed

apps/desktop/main/desktop-diagnostics.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type {
55
RuntimeEventQueryResult,
66
RuntimeLogEntry,
77
RuntimeState,
8+
StartupProbePayload,
89
} from "../shared/host";
910
import type { RuntimeOrchestrator } from "./runtime/daemon-supervisor";
1011
import {
@@ -54,6 +55,17 @@ type DesktopDiagnosticsSnapshot = {
5455
updatedAt: string;
5556
isPackaged: boolean;
5657
coldStart: DesktopColdStartSnapshot;
58+
startupProbe: {
59+
preloadSeen: boolean;
60+
rendererSeen: boolean;
61+
entries: Array<{
62+
source: StartupProbePayload["source"];
63+
stage: string;
64+
status: StartupProbePayload["status"];
65+
detail: string | null;
66+
at: string;
67+
}>;
68+
};
5769
sleepGuard: SleepGuardSnapshot;
5870
renderer: DesktopRendererSnapshot;
5971
embeddedContents: DesktopEmbeddedContentSnapshot[];
@@ -68,6 +80,8 @@ function nowIso(): string {
6880
return new Date().toISOString();
6981
}
7082

83+
const MAX_STARTUP_PROBE_ENTRIES = 200;
84+
7185
export function getDesktopDiagnosticsFilePath(): string {
7286
return resolve(app.getPath("userData"), "logs", "desktop-diagnostics.json");
7387
}
@@ -83,6 +97,12 @@ export class DesktopDiagnosticsReporter {
8397
error: null,
8498
};
8599

100+
private readonly startupProbe: DesktopDiagnosticsSnapshot["startupProbe"] = {
101+
preloadSeen: false,
102+
rendererSeen: false,
103+
entries: [],
104+
};
105+
86106
private sleepGuard: SleepGuardSnapshot = createInitialSleepGuardSnapshot();
87107

88108
private readonly renderer: DesktopRendererSnapshot = {
@@ -152,6 +172,33 @@ export class DesktopDiagnosticsReporter {
152172
this.scheduleFlush();
153173
}
154174

175+
recordStartupProbe(payload: StartupProbePayload): void {
176+
if (payload.source === "preload") {
177+
this.startupProbe.preloadSeen = true;
178+
}
179+
180+
if (payload.source === "renderer") {
181+
this.startupProbe.rendererSeen = true;
182+
}
183+
184+
this.startupProbe.entries.push({
185+
source: payload.source,
186+
stage: payload.stage,
187+
status: payload.status,
188+
detail: payload.detail ?? null,
189+
at: nowIso(),
190+
});
191+
192+
if (this.startupProbe.entries.length > MAX_STARTUP_PROBE_ENTRIES) {
193+
this.startupProbe.entries.splice(
194+
0,
195+
this.startupProbe.entries.length - MAX_STARTUP_PROBE_ENTRIES,
196+
);
197+
}
198+
199+
this.scheduleFlush();
200+
}
201+
155202
recordRendererDidFinishLoad(url: string): void {
156203
this.renderer.didFinishLoad = true;
157204
this.renderer.lastUrl = url;
@@ -277,6 +324,11 @@ export class DesktopDiagnosticsReporter {
277324
updatedAt: nowIso(),
278325
isPackaged: app.isPackaged,
279326
coldStart: { ...this.coldStart },
327+
startupProbe: {
328+
preloadSeen: this.startupProbe.preloadSeen,
329+
rendererSeen: this.startupProbe.rendererSeen,
330+
entries: this.startupProbe.entries.map((entry) => ({ ...entry })),
331+
},
280332
sleepGuard: {
281333
...this.sleepGuard,
282334
counters: { ...this.sleepGuard.counters },

apps/desktop/main/diagnostics-export.ts

Lines changed: 191 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { execFileSync } from "node:child_process";
1+
import { spawnSync } from "node:child_process";
22
import type { Dirent } from "node:fs";
33
import { access, readFile, readdir, stat, writeFile } from "node:fs/promises";
44
import { homedir, hostname } from "node:os";
@@ -167,6 +167,14 @@ function redactJsonBuffer(raw: Buffer): Buffer {
167167
}
168168
}
169169

170+
function parseJsonBuffer<T>(raw: Buffer): T | null {
171+
try {
172+
return JSON.parse(raw.toString("utf8")) as T;
173+
} catch {
174+
return null;
175+
}
176+
}
177+
170178
// ---------------------------------------------------------------------------
171179
// Artifact collection
172180
// ---------------------------------------------------------------------------
@@ -229,19 +237,109 @@ async function listFilesRecursive(directoryPath: string): Promise<string[]> {
229237
return output;
230238
}
231239

240+
function runCommand(
241+
binaryPath: string,
242+
args: string[],
243+
): {
244+
binaryPath: string;
245+
args: string[];
246+
ok: boolean;
247+
status: number | null;
248+
signal: NodeJS.Signals | null;
249+
stdout: string | null;
250+
stderr: string | null;
251+
error: string | null;
252+
} {
253+
const result = spawnSync(binaryPath, args, {
254+
encoding: "utf8",
255+
timeout: 5000,
256+
});
257+
258+
const stdout = result.stdout.trim();
259+
const stderr = result.stderr.trim();
260+
261+
return {
262+
binaryPath,
263+
args,
264+
ok: result.status === 0 && !result.error,
265+
status: result.status,
266+
signal: result.signal,
267+
stdout: stdout.length > 0 ? stdout : null,
268+
stderr: stderr.length > 0 ? stderr : null,
269+
error: result.error ? String(result.error.message) : null,
270+
};
271+
}
272+
232273
function readMacOsProductVersion(): string | null {
233274
if (process.platform !== "darwin") {
234275
return null;
235276
}
236277

237-
try {
238-
const output = execFileSync("sw_vers", ["-productVersion"], {
239-
encoding: "utf8",
240-
}).trim();
241-
return output.length > 0 ? output : null;
242-
} catch {
278+
const result = runCommand("/usr/bin/sw_vers", ["-productVersion"]);
279+
return result.ok ? result.stdout : null;
280+
}
281+
282+
function buildMachineSummary(runtimeConfig: DesktopRuntimeConfig): object {
283+
const rosettaCheck =
284+
process.platform === "darwin"
285+
? runCommand("/usr/sbin/sysctl", ["-n", "sysctl.proc_translated"])
286+
: null;
287+
288+
const unameMachine =
289+
process.platform === "darwin" ? runCommand("/usr/bin/uname", ["-m"]) : null;
290+
291+
return {
292+
buildInfo: runtimeConfig.buildInfo,
293+
hostName: hostname(),
294+
platform: process.platform,
295+
arch: process.arch,
296+
osVersion: readMacOsProductVersion(),
297+
processVersions: process.versions,
298+
executablePath: app.getPath("exe"),
299+
processExecPath: process.execPath,
300+
resourcesPath: process.resourcesPath,
301+
isPackaged: app.isPackaged,
302+
rosetta: rosettaCheck
303+
? {
304+
translated:
305+
rosettaCheck.ok && rosettaCheck.stdout !== null
306+
? rosettaCheck.stdout === "1"
307+
: null,
308+
command: rosettaCheck,
309+
}
310+
: null,
311+
uname: unameMachine,
312+
appPaths: {
313+
userData: app.getPath("userData"),
314+
logs: app.getPath("logs"),
315+
crashDumps: app.getPath("crashDumps"),
316+
nexuHome: runtimeConfig.paths.nexuHome,
317+
},
318+
};
319+
}
320+
321+
function buildAppSigningSummary(): object | null {
322+
if (process.platform !== "darwin") {
243323
return null;
244324
}
325+
326+
const appExecutablePath = app.getPath("exe");
327+
328+
return {
329+
executablePath: appExecutablePath,
330+
codesign: runCommand("/usr/bin/codesign", [
331+
"-dv",
332+
"--verbose=4",
333+
appExecutablePath,
334+
]),
335+
spctl: runCommand("/usr/sbin/spctl", [
336+
"--assess",
337+
"--type",
338+
"execute",
339+
"-vv",
340+
appExecutablePath,
341+
]),
342+
};
245343
}
246344

247345
function getTimestampSlug(): string {
@@ -265,6 +363,7 @@ async function collectArtifacts(
265363
const included: string[] = [];
266364
const missing: string[] = [];
267365
const warnings: string[] = [];
366+
let desktopDiagnosticsSummary: unknown = null;
268367

269368
const additionalArtifacts = {
270369
startupHealth: null as CollectedFileMetadata | null,
@@ -305,14 +404,59 @@ async function collectArtifacts(
305404
}
306405

307406
// Desktop diagnostics snapshot
308-
await addFile(
407+
const desktopDiagnosticsMetadata = await addFile(
309408
"diagnostics/desktop-diagnostics.json",
310409
getDesktopDiagnosticsFilePath(),
311410
{
312411
redact: true,
313412
},
314413
);
315414

415+
if (desktopDiagnosticsMetadata) {
416+
const desktopDiagnosticsFile = await tryReadFile(
417+
getDesktopDiagnosticsFilePath(),
418+
);
419+
const parsedDiagnostics = desktopDiagnosticsFile
420+
? parseJsonBuffer<{
421+
startupProbe?: {
422+
preloadSeen?: boolean;
423+
rendererSeen?: boolean;
424+
entries?: Array<{
425+
source?: string;
426+
stage?: string;
427+
status?: string;
428+
detail?: string | null;
429+
at?: string;
430+
}>;
431+
};
432+
renderer?: {
433+
didFinishLoad?: boolean;
434+
lastError?: string | null;
435+
processGone?: {
436+
seen?: boolean;
437+
reason?: string | null;
438+
exitCode?: number | null;
439+
at?: string | null;
440+
};
441+
};
442+
coldStart?: {
443+
status?: string;
444+
step?: string | null;
445+
error?: string | null;
446+
};
447+
}>(desktopDiagnosticsFile.data)
448+
: null;
449+
450+
if (parsedDiagnostics) {
451+
desktopDiagnosticsSummary = {
452+
sourceArchivePath: desktopDiagnosticsMetadata.archivePath,
453+
coldStart: parsedDiagnostics.coldStart ?? null,
454+
renderer: parsedDiagnostics.renderer ?? null,
455+
startupProbe: parsedDiagnostics.startupProbe ?? null,
456+
};
457+
}
458+
}
459+
316460
// Main process logs
317461
const logsDir = resolve(app.getPath("userData"), "logs");
318462
await addFile("logs/cold-start.log", resolve(logsDir, "cold-start.log"), {
@@ -440,6 +584,8 @@ async function collectArtifacts(
440584

441585
// Environment summary (safe metadata only)
442586
const envSummary = buildEnvironmentSummary(runtimeConfig);
587+
const machineSummary = buildMachineSummary(runtimeConfig);
588+
const appSigningSummary = buildAppSigningSummary();
443589
const now = new Date();
444590
entries.push({
445591
name: `${archiveRoot}/summary/environment-summary.json`,
@@ -448,6 +594,37 @@ async function collectArtifacts(
448594
});
449595
included.push("summary/environment-summary.json");
450596

597+
entries.push({
598+
name: `${archiveRoot}/summary/machine-info.json`,
599+
data: Buffer.from(`${JSON.stringify(machineSummary, null, 2)}\n`, "utf8"),
600+
modTime: now,
601+
});
602+
included.push("summary/machine-info.json");
603+
604+
if (appSigningSummary) {
605+
entries.push({
606+
name: `${archiveRoot}/summary/app-signing.json`,
607+
data: Buffer.from(
608+
`${JSON.stringify(appSigningSummary, null, 2)}\n`,
609+
"utf8",
610+
),
611+
modTime: now,
612+
});
613+
included.push("summary/app-signing.json");
614+
}
615+
616+
if (desktopDiagnosticsSummary) {
617+
entries.push({
618+
name: `${archiveRoot}/summary/startup-probe-summary.json`,
619+
data: Buffer.from(
620+
`${JSON.stringify(desktopDiagnosticsSummary, null, 2)}\n`,
621+
"utf8",
622+
),
623+
modTime: now,
624+
});
625+
included.push("summary/startup-probe-summary.json");
626+
}
627+
451628
const extraArtifactsSummary = {
452629
startupHealth: additionalArtifacts.startupHealth,
453630
openclawLogs: additionalArtifacts.openclawLogs,
@@ -465,6 +642,12 @@ async function collectArtifacts(
465642
});
466643
included.push("summary/additional-artifacts.json");
467644

645+
if (missing.length > 0) {
646+
warnings.push(`${missing.length} file(s) were not found and were skipped.`);
647+
}
648+
649+
included.push("summary/manifest.json");
650+
468651
// Manifest
469652
const manifest = {
470653
exportedAt: now.toISOString(),
@@ -482,10 +665,6 @@ async function collectArtifacts(
482665
modTime: now,
483666
});
484667

485-
if (missing.length > 0) {
486-
warnings.push(`${missing.length} file(s) were not found and were skipped.`);
487-
}
488-
489668
return { entries, warnings };
490669
}
491670

0 commit comments

Comments
 (0)