Files
reverse-engineering-anything/src/hopper/LinuxPrivateDisplayDiagnostic.ts
T
HenglieandCursor Agent b1ff7cdacc fix(hopper): distinguish an absent demo dialog from publisher rejection (#8074)
* fix(hopper): distinguish an absent demo dialog from publisher rejection

The Linux demo watcher collapsed an admitted launcher with no first-run dialog into unsupported_demo_dialog. Report that post-authentication timeout as demo_dialog_absent, keep the unsupported dialog code for an observed but unrecognized Hopper window, and allow REA_HOPPER_DEMO_DIALOG_TIMEOUT_MS to widen the fixed wait. Closes #8008.

* fix(hopper): tolerate callers without the dialog timeout argument

* fix(hopper): report an invalid demo dialog timeout as its own failure

An invalid REA_HOPPER_DEMO_DIALOG_TIMEOUT_MS exited 73 and was shown as
invalid_launch_command. Exit 82 is invalid_dialog_timeout. The public error
names the setting, shows the rejected value and source, lists the allowed
range, and says to restart REA.

Drive the demo watcher with controlled windows and time. No window exits 81,
a Hopper window without a supported dialog exits 76, and a dialog that arrives
after the old 10 second limit is still accepted.

* fix(hopper): keep startup stderr capture on AdapterFailure

DemoWatchStop was inserted above with_stderr, so launch failures called a
method they no longer had and became unexpected_failure. Restore with_stderr
on AdapterFailure and reject a watcher fixture that cannot capture stderr.

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-10-30 18:29:18 +08:00

106 lines
3.5 KiB
TypeScript

import { z } from "zod";
import {
DEFAULT_HOPPER_DEMO_DIALOG_TIMEOUT_MS,
HOPPER_DEMO_DIALOG_TIMEOUT_SETTING,
isHopperStartupFailureCode,
MAXIMUM_HOPPER_DEMO_DIALOG_TIMEOUT_MS,
MINIMUM_HOPPER_DEMO_DIALOG_TIMEOUT_MS,
type HopperStartupDiagnostic,
} from "../domain/hopperStartupFailure.js";
import { safeParseJson } from "../domain/safeJson.js";
export const LINUX_PRIVATE_DISPLAY_DIAGNOSTIC_PREFIX = "REA_X11_DIAGNOSTIC=";
const diagnosticContextSchema = z.object({
component: z.literal("hopper_private_display"),
operation: z.enum(["probe", "launch"]),
reason: z.string().regex(/^[a-z0-9_]+$/u),
socket_directory: z.literal("/tmp/.X11-unix"),
socket_directory_mode: z
.string()
.regex(/^[0-7]{4}$/u)
.nullable(),
mount_read_only: z.boolean().nullable(),
effective_socket_directory_mode: z
.string()
.regex(/^[0-7]{4}$/u)
.nullable(),
effective_mount_read_only: z.boolean().nullable(),
wsl: z.boolean(),
strategy: z.enum(["direct", "user-mount-namespace", "unavailable"]),
fallback_reason: z
.string()
.regex(/^[a-z0-9_]+$/u)
.nullable(),
xvfb_stderr_bytes: z.int().nonnegative().safe(),
});
const dialogTimeoutSchema = z
.object({
setting: z.literal(HOPPER_DEMO_DIALOG_TIMEOUT_SETTING),
value: z.string().max(128),
source: z.literal("setting"),
minimum_ms: z.literal(MINIMUM_HOPPER_DEMO_DIALOG_TIMEOUT_MS),
maximum_ms: z.literal(MAXIMUM_HOPPER_DEMO_DIALOG_TIMEOUT_MS),
default_ms: z.literal(DEFAULT_HOPPER_DEMO_DIALOG_TIMEOUT_MS),
})
.strict();
const failureCodeSchema = z.string().transform((value, context) => {
if (isHopperStartupFailureCode(value)) return value;
context.addIssue({
code: "custom",
message: "Unknown startup failure code",
});
return z.NEVER;
});
const diagnosticSchema = z.discriminatedUnion("status", [
diagnosticContextSchema
.extend({ status: z.literal("ready"), failure_code: z.null() })
.strict(),
diagnosticContextSchema
.extend({
status: z.literal("error"),
failure_code: failureCodeSchema,
dialog_timeout: dialogTimeoutSchema.optional(),
})
.strict(),
]);
export type LinuxPrivateDisplayDiagnosticParse =
| { readonly ok: true; readonly value: HopperStartupDiagnostic }
| {
readonly ok: false;
readonly reason:
| "diagnostic_missing"
| "diagnostic_multiple"
| "diagnostic_malformed";
};
/** Parse exactly one helper diagnostic from stderr. */
export const parseLinuxPrivateDisplayDiagnostic = (
stderr: string,
): LinuxPrivateDisplayDiagnosticParse => {
const records = stderr
.split("\n")
.filter((line) => line.startsWith(LINUX_PRIVATE_DISPLAY_DIAGNOSTIC_PREFIX));
if (records.length === 0) return { ok: false, reason: "diagnostic_missing" };
if (records.length !== 1) return { ok: false, reason: "diagnostic_multiple" };
const record = records[0];
if (record === undefined) return { ok: false, reason: "diagnostic_missing" };
const decoded = safeParseJson(
record.slice(LINUX_PRIVATE_DISPLAY_DIAGNOSTIC_PREFIX.length),
);
if (!decoded.ok) return { ok: false, reason: "diagnostic_malformed" };
const parsed = diagnosticSchema.safeParse(decoded.value);
if (!parsed.success) return { ok: false, reason: "diagnostic_malformed" };
const value = parsed.data;
if (value.status === "error" && value.dialog_timeout === undefined) {
const { dialog_timeout: _omitted, ...diagnostic } = value;
return { ok: true, value: diagnostic };
}
return { ok: true, value };
};