import { assert, assertEquals, assertStringIncludes } from "@std/assert"; import { BACKGROUND_CONTEXT as context } from "@earendil-works/chord/context"; import { onPath, tempDir, until } from "@berry/util"; import { BOOTSTRAP, Connection, RemoteEnvironment } from "./connection.ts"; Deno.test({ name: "remote process exit is independent of inherited pipes, which close within a bounded drain", ignore: Deno.build.os !== "linux" || !onPath("setsid"), async fn() { await using dir = await tempDir({ prefix: "remote-pipes-" }); const connection = await Connection.open("pipes", [ Deno.execPath(), "eval", "--quiet", BOOTSTRAP, ]); const env = new RemoteEnvironment("pipes", connection, dir.path); let pid: number | undefined; try { const child = await env.spawn( [ "sh", "-c", "setsid sh -c 'echo $$ > descendant; exec sleep 300' /dev/null & exit 0", ], { group: true }, context, ); await until(() => Deno.stat(dir.path + "/descendant").then(() => true, () => false)); pid = Number(await Deno.readTextFile(dir.path + "/descendant")); let exited = false; void child.exited.then(() => exited = true); await until(() => exited, { timeout: 500, message: "process exit was held up by inherited stderr", }); assertEquals(await child.exited, 0); assertEquals(await new Response(child.stdout).text(), ""); let ended = false; const stderr = new Response(child.stderr).text().then((text) => { ended = true; return text; }); await until(() => ended, { message: "inherited stderr never finished draining" }); assertEquals(await stderr, ""); assertEquals((await env.readTextFile("descendant", context)).ok, true); } finally { if (pid) { try { Deno.kill(pid, "SIGKILL"); } catch { /* already gone */ } } await connection.close(); } }, }); Deno.test({ name: "ordinary remote spawn preserves delayed descendant output after its leader exits", ignore: Deno.build.os !== "linux", async fn() { const connection = await Connection.open("late", [ Deno.execPath(), "eval", "--quiet", BOOTSTRAP, ]); const env = new RemoteEnvironment("late", connection, Deno.cwd()); try { // ignored before the fork, so the leader's exit cannot hang up a child yet to ignore it itself const argv = ["sh", "-c", "trap '' HUP; (sleep 1.5; echo late) & echo early"]; const program = await env.spawn(argv, {}, context); assertEquals(await program.exited, 0); const output = await new Response(program.stdout).text(); assertStringIncludes(output, "early"); assertStringIncludes(output, "late"); } finally { await connection.close(); } }, }); Deno.test({ name: "closing a remote transport does not wait for descendants retaining its stderr", ignore: Deno.build.os !== "linux", async fn() { await using dir = await tempDir({ prefix: "transport-stderr-" }); const connection = await Connection.open("stderr", [ "sh", "-c", 'sleep 300 /dev/null & echo $! > "$1"; shift; exec "$@"', "sh", dir.path + "/descendant", Deno.execPath(), "eval", "--quiet", BOOTSTRAP, ]); const pid = Number(await Deno.readTextFile(dir.path + "/descendant")); try { let closed = false; const closing = connection.close().then(() => closed = true); await until(() => closed, { timeout: 1500, message: "transport close waited for descendant stderr EOF", }); await closing; assert(connection.lifetime.aborted); } finally { try { Deno.kill(pid, "SIGKILL"); } catch { /* already gone */ } await connection.close(); } }, });