char-slop/berry

multipurpose self-modifiable agentic harness

git clone https://git.t4t.associates/char-slop/berry

Charlotte Somcleanup: hang up PTYs when their process exits0575637

main
3.8 KiB116 linesraw
1import { assert, assertEquals, assertStringIncludes } from "@std/assert";
2import { BACKGROUND_CONTEXT as context } from "@earendil-works/chord/context";
3import { onPath, tempDir, until } from "@berry/util";
4import { BOOTSTRAP, Connection, RemoteEnvironment } from "./connection.ts";
5
6Deno.test({
7  name: "remote process exit is independent of inherited pipes, which close within a bounded drain",
8  ignore: Deno.build.os !== "linux" || !onPath("setsid"),
9  async fn() {
10    await using dir = await tempDir({ prefix: "remote-pipes-" });
11    const connection = await Connection.open("pipes", [
12      Deno.execPath(),
13      "eval",
14      "--quiet",
15      BOOTSTRAP,
16    ]);
17    const env = new RemoteEnvironment("pipes", connection, dir.path);
18    let pid: number | undefined;
19    try {
20      const child = await env.spawn(
21        [
22          "sh",
23          "-c",
24          "setsid sh -c 'echo $$ > descendant; exec sleep 300' </dev/null >/dev/null & exit 0",
25        ],
26        { group: true },
27        context,
28      );
29      await until(() => Deno.stat(dir.path + "/descendant").then(() => true, () => false));
30      pid = Number(await Deno.readTextFile(dir.path + "/descendant"));
31      let exited = false;
32      void child.exited.then(() => exited = true);
33      await until(() => exited, {
34        timeout: 500,
35        message: "process exit was held up by inherited stderr",
36      });
37      assertEquals(await child.exited, 0);
38      assertEquals(await new Response(child.stdout).text(), "");
39      let ended = false;
40      const stderr = new Response(child.stderr).text().then((text) => {
41        ended = true;
42        return text;
43      });
44      await until(() => ended, { message: "inherited stderr never finished draining" });
45      assertEquals(await stderr, "");
46      assertEquals((await env.readTextFile("descendant", context)).ok, true);
47    } finally {
48      if (pid) {
49        try {
50          Deno.kill(pid, "SIGKILL");
51        } catch { /* already gone */ }
52      }
53      await connection.close();
54    }
55  },
56});
57
58Deno.test({
59  name: "ordinary remote spawn preserves delayed descendant output after its leader exits",
60  ignore: Deno.build.os !== "linux",
61  async fn() {
62    const connection = await Connection.open("late", [
63      Deno.execPath(),
64      "eval",
65      "--quiet",
66      BOOTSTRAP,
67    ]);
68    const env = new RemoteEnvironment("late", connection, Deno.cwd());
69    try {
70      // ignored before the fork, so the leader's exit cannot hang up a child yet to ignore it itself
71      const argv = ["sh", "-c", "trap '' HUP; (sleep 1.5; echo late) & echo early"];
72      const program = await env.spawn(argv, {}, context);
73      assertEquals(await program.exited, 0);
74      const output = await new Response(program.stdout).text();
75      assertStringIncludes(output, "early");
76      assertStringIncludes(output, "late");
77    } finally {
78      await connection.close();
79    }
80  },
81});
82
83Deno.test({
84  name: "closing a remote transport does not wait for descendants retaining its stderr",
85  ignore: Deno.build.os !== "linux",
86  async fn() {
87    await using dir = await tempDir({ prefix: "transport-stderr-" });
88    const connection = await Connection.open("stderr", [
89      "sh",
90      "-c",
91      'sleep 300 </dev/null >/dev/null & echo $! > "$1"; shift; exec "$@"',
92      "sh",
93      dir.path + "/descendant",
94      Deno.execPath(),
95      "eval",
96      "--quiet",
97      BOOTSTRAP,
98    ]);
99    const pid = Number(await Deno.readTextFile(dir.path + "/descendant"));
100    try {
101      let closed = false;
102      const closing = connection.close().then(() => closed = true);
103      await until(() => closed, {
104        timeout: 1500,
105        message: "transport close waited for descendant stderr EOF",
106      });
107      await closing;
108      assert(connection.lifetime.aborted);
109    } finally {
110      try {
111        Deno.kill(pid, "SIGKILL");
112      } catch { /* already gone */ }
113      await connection.close();
114    }
115  },
116});