diff --git a/apps/hub/src/builds/posix-sandbox.ts b/apps/hub/src/builds/posix-sandbox.ts index e20bce39..ceeb5984 100644 --- a/apps/hub/src/builds/posix-sandbox.ts +++ b/apps/hub/src/builds/posix-sandbox.ts @@ -236,7 +236,12 @@ export async function createPosixBuildExecutor( `command_build_failed:${name} ${built.logs.join("; ")}`, ); const shim = join(shimDirectory, name); - await writeFile(shim, `#!/bin/sh\nexec ${quote(binary)} "$@"\n`); + // srt points TMPDIR at a folder it does not make inside the sandbox's own /tmp, and Bun's + // WebView cannot start Chrome without a temp folder. + await writeFile( + shim, + `#!/bin/sh\n[ -z "$TMPDIR" ] || mkdir -p -- "$TMPDIR"\nexec ${quote(binary)} "$@"\n`, + ); await chmod(shim, 0o755); } const pageWatcher = join(shimDirectory, "watch-page.ts"); @@ -327,8 +332,30 @@ export async function createPosixBuildExecutor( : undefined, ); initialized = true; + // On Linux srt relays the sandbox's proxy ports to the host over Unix sockets in the host's temp + // folder, which the default-deny read policy hides; srt binds them before that deny lands, so + // every connection through the proxy was reset. Named in allowRead, they are bound back over it. + if (process.platform === "linux") { + await SandboxManager.waitForNetworkInitialization(); + const bridges = new Set( + [ + SandboxManager.getLinuxHttpSocketPath(), + SandboxManager.getLinuxSocksSocketPath(), + ].filter((path): path is string => path !== undefined), + ); + if (bridges.size) + SandboxManager.updateConfig({ + ...policy, + filesystem: { + ...policy.filesystem, + allowRead: [...(policy.filesystem.allowRead ?? []), ...bridges], + }, + }); + } stage = "wrap"; - const childCommand = `exec ${[executable, "--no-env-file", layout.childPath, cwd].map(quote).join(" ")}`; + // The child resolves the workspace packages from source like the hub does; + // the vendored packages ship no built dist. + const childCommand = `exec ${[executable, "--no-env-file", "--conditions=intx-src", layout.childPath, cwd].map(quote).join(" ")}`; const wrapped = await SandboxManager.wrapWithSandbox( childCommand, "/bin/sh", @@ -779,7 +806,7 @@ export async function createPosixBuildExecutor( env: childEnv, stdin: "ignore", stdout: "pipe", - stderr: "ignore", + stderr: "pipe", detached: process.platform !== "linux", }, ); @@ -788,11 +815,19 @@ export async function createPosixBuildExecutor( const timer = setTimeout(stop, limits.commandTimeoutMs); signal.addEventListener("abort", stop, { once: true }); try { - const output = await new Response(child.stdout).text(); - return { - exitCode: await child.exited, - output: output.replaceAll(cwd + sep, ""), - }; + const [output, errors] = await Promise.all([ + new Response(child.stdout).text(), + new Response(child.stderr).text(), + ]); + const exitCode = await child.exited; + // A command that died before printing its checklist would otherwise fail with nothing said. + const text = + exitCode === 0 + ? output + : [output, errors.slice(-2000)] + .filter((part) => part.trim()) + .join("\n"); + return { exitCode, output: text.replaceAll(cwd + sep, "") }; } finally { clearTimeout(timer); signal.removeEventListener("abort", stop); diff --git a/apps/hub/src/builds/starter.ts b/apps/hub/src/builds/starter.ts index 74b52f31..626f7388 100644 --- a/apps/hub/src/builds/starter.ts +++ b/apps/hub/src/builds/starter.ts @@ -42,6 +42,7 @@ const CHROME_VERSION = "156.0.8078.4"; const CHROME_PLATFORMS: Record = { "darwin-arm64": "mac-arm64", "darwin-x64": "mac-x64", + "linux-arm64": "linux-arm64", "linux-x64": "linux64", }; const CHROME_PLATFORM = CHROME_PLATFORMS[`${process.platform}-${process.arch}`]; @@ -55,8 +56,8 @@ const exists = (path: string) => ); // One Chrome for every sandbox, unpacked once beside the hub's other caches and only ever read by -// the sandboxes. Chrome for Testing has no build for some hosts (linux-arm64); there no browser is -// installed and check-app reports it missing. +// the sandboxes. On a host Chrome for Testing has no build for, no browser is installed and +// check-app reports it missing. const BROWSER_CACHE = join( homedir(), ".cache", diff --git a/workflows/coder/check/check-app.ts b/workflows/coder/check/check-app.ts index b3d57881..d5bd3de8 100644 --- a/workflows/coder/check/check-app.ts +++ b/workflows/coder/check/check-app.ts @@ -572,12 +572,13 @@ if (staticRoot) { // The sandbox setup unpacks Chrome's headless shell under $HOME/browsers. A sandboxed process may not // register Mach services, so Chrome runs as one process with its own sandbox off. +// With no browser installed the folder is not there at all, which is the same finding. const [chrome] = await Array.fromAsync( new Bun.Glob("chrome-headless-shell-*/chrome-headless-shell").scan({ cwd: join(homedir(), "browsers"), absolute: true, }), -); +).catch(() => [] as string[]); if (!chrome) { stop(); fail("browser", "nothing under $HOME/browsers");