Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions apps/desktop/src/app/DesktopCrashReport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ const bundledPosthogHost =
: "https://us.i.posthog.com";

const ANONYMOUS_ID_FILE_NAME = "anonymous-id";
const STDERR_TAIL_MAX_CHARS = 2_000;
const OUTPUT_TAIL_MAX_CHARS = 2_000;

// Values following credential-shaped labels are cut before anything leaves
// the machine. Boot-phase stderr should never contain these, but a config
Expand All @@ -56,7 +56,8 @@ export interface DesktopStartupFailureReport {
readonly attempts: number;
readonly lastExitCode: Option.Option<number>;
readonly lastReason: string;
readonly stderrTail: string;
/** Interleaved stdout+stderr tail; the server logs fatal causes on stdout. */
readonly outputTail: string;
}

export interface DesktopCrashReportShape {
Expand Down Expand Up @@ -92,7 +93,7 @@ export function scrubUserPaths(text: string, homeDirectory: string): string {
}

/** Keeps the end of the captured stderr, where the fatal error lands. */
export function truncateTail(text: string, maxChars: number = STDERR_TAIL_MAX_CHARS): string {
export function truncateTail(text: string, maxChars: number = OUTPUT_TAIL_MAX_CHARS): string {
return text.length <= maxChars ? text : text.slice(text.length - maxChars);
}

Expand Down Expand Up @@ -187,7 +188,7 @@ const makeDesktopCrashReport = Effect.gen(function* () {
attempts: report.attempts,
exitCode: Option.getOrNull(report.lastExitCode),
reason: scrub(report.lastReason),
stderrTail: scrub(truncateTail(report.stderrTail)),
outputTail: scrub(truncateTail(report.outputTail)),
},
timestamp: new Date().toISOString(),
},
Expand Down
11 changes: 9 additions & 2 deletions apps/desktop/src/backend/DesktopBackendManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -587,7 +587,13 @@ describe("DesktopBackendManager", () => {
const close = Deferred.succeed(closed, void 0).pipe(Effect.asVoid);
yield* Scope.addFinalizer(scope, close);
return makeProcess({
stderr: Stream.make(new TextEncoder().encode("EADDRINUSE: port already bound\n")),
// The fatal cause lands on stdout, like the server's Effect
// logger; stderr carries a secondary line. Both must reach
// the crash-report tail.
stdout: Stream.make(
new TextEncoder().encode("[FATAL] EADDRINUSE: port already bound\n"),
),
stderr: Stream.make(new TextEncoder().encode("node exited\n")),
exitCode: Deferred.await(closed).pipe(Effect.as(ChildProcessSpawner.ExitCode(143))),
kill: () => close,
});
Expand Down Expand Up @@ -615,7 +621,8 @@ describe("DesktopBackendManager", () => {
assert.equal(report.failureKind, "readiness-timeout");
assert.equal(report.attempts, 1);
assert.include(report.lastReason, "Timed out");
assert.include(report.stderrTail, "EADDRINUSE");
assert.include(report.outputTail, "EADDRINUSE");
assert.include(report.outputTail, "node exited");

// The unresponsive process was killed and nothing respawns.
yield* TestClock.adjust(Duration.seconds(30));
Expand Down
43 changes: 23 additions & 20 deletions apps/desktop/src/backend/DesktopBackendManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ const MAX_RESTART_DELAY = Duration.seconds(10);
// session, restarts stay unbounded: the window exists, so a crash there is
// not an invisible wedge.
const MAX_STARTUP_ATTEMPTS = 3;
// Cap on the retained stderr needed for a useful crash report.
const STDERR_TAIL_MAX_CHARS = 8_192;
// Cap on the retained stdout+stderr needed for a useful crash report.
const OUTPUT_TAIL_MAX_CHARS = 8_192;
// How long a finished process's output drains may lag its exit.
const DRAIN_FLUSH_TIMEOUT = Duration.seconds(1);
const DEFAULT_BACKEND_READINESS_TIMEOUT = Duration.minutes(1);
Expand Down Expand Up @@ -419,7 +419,7 @@ const makeDesktopBackendManager = Effect.fn("makeDesktopBackendManager")(functio
attempts: latest.restartAttempt + 1,
lastExitCode: Option.none(),
lastReason: reason,
stderrTail: "",
outputTail: "",
});
return;
}
Expand All @@ -428,10 +428,14 @@ const makeDesktopBackendManager = Effect.fn("makeDesktopBackendManager")(functio
}

const runScope = yield* Scope.make("sequential");
// Per-run crash-report inputs: the stderr tail carries the fatal
// Per-run crash-report inputs: the output tail carries the fatal
// error when the process dies, the timeout marker reroutes a
// killed-for-unresponsiveness exit away from the restart path.
const stderrTailRef = yield* Ref.make("");
// killed-for-unresponsiveness exit away from the restart path. Both
// streams feed the tail — the server's Effect logger reports fatal
// causes on stdout, and field crash reports came back empty when
// only stderr was kept.
const outputTailRef = yield* Ref.make("");
const stdoutDecoder = new TextDecoder();
const stderrDecoder = new TextDecoder();
const readinessTimeoutRef = yield* Ref.make(Option.none<BackendTimeoutError>());
const runId = yield* Ref.modify(state, (latest) => [
Expand Down Expand Up @@ -526,7 +530,7 @@ const makeDesktopBackendManager = Effect.fn("makeDesktopBackendManager")(functio
}));
// Let buffered output land in the tail before reading it.
yield* flushOutput;
const stderrTail = yield* Ref.get(stderrTailRef);
const outputTail = yield* Ref.get(outputTailRef);
yield* triggerStartupFailure({
failureKind: Option.isSome(readinessTimeout) ? "readiness-timeout" : "process-exit",
attempts: nextState.restartAttempt + 1,
Expand All @@ -535,7 +539,7 @@ const makeDesktopBackendManager = Effect.fn("makeDesktopBackendManager")(functio
onNone: () => reason,
onSome: (timeout) => timeout.message,
}),
stderrTail,
outputTail,
});
}),
);
Expand Down Expand Up @@ -607,18 +611,17 @@ const makeDesktopBackendManager = Effect.fn("makeDesktopBackendManager")(functio
}
}),
onOutput: (streamName, chunk) =>
streamName === "stderr"
? backendOutputLog.writeOutputChunk(streamName, chunk).pipe(
Effect.andThen(
Ref.update(stderrTailRef, (tail) => {
const appended = tail + stderrDecoder.decode(chunk, { stream: true });
return appended.length <= STDERR_TAIL_MAX_CHARS
? appended
: appended.slice(appended.length - STDERR_TAIL_MAX_CHARS);
}),
),
)
: backendOutputLog.writeOutputChunk(streamName, chunk),
backendOutputLog.writeOutputChunk(streamName, chunk).pipe(
Effect.andThen(
Ref.update(outputTailRef, (tail) => {
const decoder = streamName === "stderr" ? stderrDecoder : stdoutDecoder;
const appended = tail + decoder.decode(chunk, { stream: true });
return appended.length <= OUTPUT_TAIL_MAX_CHARS
? appended
: appended.slice(appended.length - OUTPUT_TAIL_MAX_CHARS);
}),
),
),
}).pipe(
Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
Effect.provideService(HttpClient.HttpClient, httpClient),
Expand Down
Loading