1import { describe, expect, it } from "@effect/vitest";
2import * as HostProcess from "@t3tools/shared/HostProcess";
3import * as Effect from "effect/Effect";
4import * as Sink from "effect/Sink";
5import * as Stream from "effect/Stream";
6import { ChildProcessSpawner } from "effect/process";
7
8import * as PreviewBrowserHost from "./PreviewBrowserHost.ts";
9
10// What `ldd` printed for the pinned headless shell in a bare debian:trixie-slim container.
11const LDD_IN_BARE_DEBIAN = `\tlinux-vdso.so.1 (0x00007ffd2d1f8000)
12\tlibdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f0c1b6a1000)
13\tlibglib-2.0.so.0 => not found
14\tlibnss3.so => not found
15\tlibX11.so.6 => not found
16\tlibc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f0c1b4c0000)
17`;
18
19// Chrome's own abort on Ubuntu 26.04 with unprivileged user namespaces restricted.
20
21const LOADER_ERROR =
22 "chrome-headless-shell: error while loading shared libraries: libglib-2.0.so.0: cannot open shared object file: No such file or directory";
23
24const hostReporting = (output: { readonly version: string; readonly ldd: string }) => {
25 const commands: Array<string> = [];
26 const spawner = ChildProcessSpawner.make((command) =>
27 Effect.sync(() => {
28 const { command: name, args } = command as unknown as {
29 readonly command: string;
30 readonly args: ReadonlyArray<string>;
31 };
32 commands.push([name, ...args].join(" "));
33 return ChildProcessSpawner.makeHandle({
34 pid: ChildProcessSpawner.ProcessId(1),
35 exitCode: Effect.succeed(ChildProcessSpawner.ExitCode(0)),
36 isRunning: Effect.succeed(false),
37 kill: () => Effect.void,
38 unref: Effect.succeed(Effect.void),
39 stdin: Sink.drain,
40 stdout: Stream.empty,
41 stderr: Stream.empty,
42 all: Stream.make(new TextEncoder().encode(name === "ldd" ? output.ldd : output.version)),
43 getInputFd: () => Sink.drain,
44 getOutputFd: () => Stream.empty,
45 });
46 }),
47 );
48 return { spawner, commands };
49};
50
51const diagnose = (input: {
52 platform: NodeJS.Platform;
53 output: string;
54 ldd: string;
55 version?: string;
56 uid?: number;
57}) => {
58 const { spawner, commands } = hostReporting({
59 ldd: input.ldd,
60 version: input.version ?? LOADER_ERROR,
61 });
62 return PreviewBrowserHost.diagnoseLaunchFailure({
63 executable: "/home/me/.t3/tools/chrome-headless-shell/linux64/154/chrome-headless-shell",
64 output: input.output,
65 setupCommand: "sudo t3 browser setup",
66 }).pipe(
67 Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
68 Effect.provideService(HostProcess.Platform, input.platform),
69 Effect.provideService(HostProcess.UserId, input.uid ?? 1000),
70 Effect.map((error) => ({ error, commands })),
71 );
72};
73
74describe("diagnoseLaunchFailure", () => {
75 it.effect("explains how to restart a root server without Chrome's sandbox", () =>
76 Effect.gen(function* () {
77 const { error, commands } = yield* diagnose({
78 platform: "linux",
79 uid: 0,
80 output: PreviewBrowserHost.NO_SANDBOX_SIGNATURE,
81 ldd: "",
82 });
83 expect(error).toBeInstanceOf(PreviewBrowserHost.PreviewBrowserRootError);
84 expect(error?.message).toContain("T3 server is running as root");
85 expect(error?.message).toContain("T3 server's environment and restart the server");
86 expect(error?.message).toContain("T3CODE_SERVER_BROWSER_SANDBOX=0 t3 serve");
87 expect(error?.message).toContain("Environment=T3CODE_SERVER_BROWSER_SANDBOX=0");
88 expect(error?.message).toContain("this disables Chrome's sandbox");
89 expect(commands).toEqual([]);
90 }),
91 );
92
93 it.effect("keeps host setup guidance for a nonroot sandbox failure", () =>
94 Effect.gen(function* () {
95 const { error } = yield* diagnose({
96 platform: "linux",
97 output: PreviewBrowserHost.NO_SANDBOX_SIGNATURE,
98 ldd: "",
99 });
100 expect(error).toBeInstanceOf(PreviewBrowserHost.PreviewBrowserSandboxError);
101 expect(error?.message).toContain("Run `sudo t3 browser setup` on the host");
102 }),
103 );
104
105 it.effect("names every library the loader cannot find", () =>
106 Effect.gen(function* () {
107 const { error } = yield* diagnose({
108 platform: "linux",
109 output: "error while loading shared libraries: libglib-2.0.so.0",
110 ldd: LDD_IN_BARE_DEBIAN,
111 });
112 expect(error).toMatchObject({
113 _tag: "PreviewBrowserLibrariesError",
114 libraries: ["libglib-2.0.so.0", "libnss3.so", "libX11.so.6"],
115 });
116 expect(error?.message).toContain("Run `sudo t3 browser setup` on the host");
117 }),
118 );
119
120 it.effect("trusts the browser's own start over ldd", () =>
121 Effect.gen(function* () {
122 // NixOS: ldd lists libraries that nix-ld still provides to the real loader.
123 const { error } = yield* diagnose({
124 platform: "linux",
125 output: "crashed",
126 ldd: LDD_IN_BARE_DEBIAN,
127 version: "Google Chrome for Testing 154.0.8037.92\n",
128 });
129 expect(error).toBeUndefined();
130 }),
131 );
132});