@serverkgg/bridge 0.0.9 → 0.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/package.json +1 -1
  2. package/src/cli/validate.ts +1 -0
  3. package/src/context/bridgeContext.ts +8 -1
  4. package/src/core/bridgeKind.ts +1 -0
  5. package/src/core/bridgeProbeError.ts +16 -0
  6. package/src/core/bridgeSetup.ts +20 -0
  7. package/src/core/bridgeValue.test.ts +29 -0
  8. package/src/core/bridgeValue.ts +12 -0
  9. package/src/core/index.ts +2 -0
  10. package/src/guides/blocks.ts +8 -0
  11. package/src/guides/markdown.test.ts +18 -0
  12. package/src/guides/markdown.ts +7 -1
  13. package/src/manifest/manifest.test.ts +49 -2
  14. package/src/manifest/presence.ts +9 -1
  15. package/src/modules/bridge.ts +20 -0
  16. package/src/modules/bridgeDriver.ts +2 -0
  17. package/src/modules/bridgeModule.ts +7 -0
  18. package/src/panel/bridgeDetail.ts +5 -5
  19. package/src/probes/a2s.test.ts +71 -0
  20. package/src/probes/a2s.ts +38 -11
  21. package/src/protocol/bridgeFrame.ts +5 -0
  22. package/src/protocol/bridgeLimits.ts +2 -0
  23. package/src/protocol/bridgeManifest.ts +2 -0
  24. package/src/protocol/bridgeSetup.ts +97 -0
  25. package/src/protocol/index.ts +1 -0
  26. package/src/runtime/bridgeConfigReader.ts +12 -8
  27. package/src/runtime/bridgeContext.ts +28 -2
  28. package/src/runtime/bridgeDriver.test.ts +272 -2
  29. package/src/runtime/bridgeDriver.ts +82 -1
  30. package/src/runtime/bridgeEvents.test.ts +96 -0
  31. package/src/runtime/bridgeEvents.ts +4 -1
  32. package/src/runtime/bridgeRegistry.test.ts +38 -1
  33. package/src/runtime/bridgeRegistry.ts +14 -1
  34. package/src/runtime/bridgeSecrets.test.ts +121 -0
  35. package/src/runtime/bridgeSecrets.ts +43 -0
  36. package/src/runtime/bridgeServer.test.ts +262 -2
  37. package/src/runtime/bridgeServer.ts +17 -0
  38. package/src/runtime/bridgeSetup.test.ts +189 -0
  39. package/src/runtime/bridgeSetup.ts +63 -0
  40. package/src/runtime/bridgeSignal.test.ts +114 -0
  41. package/src/runtime/bridgeSignal.ts +31 -0
  42. package/src/runtime/bridgeSupervisor.test.ts +111 -0
  43. package/src/runtime/bridgeSupervisor.ts +37 -5
  44. package/src/runtime/serve.ts +52 -1
  45. package/src/utils/index.ts +1 -0
  46. package/src/utils/stripAnsi.ts +12 -0
  47. package/src/wire/panel.test.ts +93 -0
  48. package/src/wire/panel.ts +40 -0
@@ -0,0 +1,189 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { BridgeSetupPromptKind, BridgeSetupStepState } from "../core";
3
+ import type { BridgeSetupRuntime } from "../protocol";
4
+ import { failedSetup, setupSummaryOf, unsupportedSetup } from "./bridgeSetup";
5
+
6
+ const text = (value: string) => ({
7
+ ar: value,
8
+ en: value,
9
+ });
10
+
11
+ const runtimeWith = (prompt: BridgeSetupRuntime["prompt"]): BridgeSetupRuntime => ({
12
+ steps: [
13
+ {
14
+ id: "sign-in",
15
+ state: BridgeSetupStepState.Active,
16
+ },
17
+ {
18
+ id: "download",
19
+ state: BridgeSetupStepState.Pending,
20
+ },
21
+ ],
22
+ prompt,
23
+ });
24
+
25
+ describe("the setup summary", () => {
26
+ test("has nothing to summarise while the driver reported no runtime", () => {
27
+ expect(setupSummaryOf(null)).toBeNull();
28
+ });
29
+
30
+ test("names the active driver step", () => {
31
+ expect(
32
+ setupSummaryOf(
33
+ runtimeWith({
34
+ kind: BridgeSetupPromptKind.Wait,
35
+ message: text("starting your server"),
36
+ }),
37
+ ),
38
+ ).toEqual({
39
+ step: "sign-in",
40
+ attention: false,
41
+ });
42
+ });
43
+
44
+ test("names no step once every driver step is done", () => {
45
+ expect(
46
+ setupSummaryOf({
47
+ steps: [
48
+ {
49
+ id: "sign-in",
50
+ state: BridgeSetupStepState.Done,
51
+ },
52
+ ],
53
+ prompt: null,
54
+ }),
55
+ ).toEqual({
56
+ step: null,
57
+ attention: false,
58
+ });
59
+ });
60
+
61
+ test("asks for attention while a link prompt waits on the customer", () => {
62
+ expect(
63
+ setupSummaryOf(
64
+ runtimeWith({
65
+ kind: BridgeSetupPromptKind.Link,
66
+ message: text("sign in with your account"),
67
+ url: "https://hytale.com/device",
68
+ code: "ABCD-EFGH",
69
+ expiresAt: null,
70
+ actions: [],
71
+ }),
72
+ )?.attention,
73
+ ).toBe(true);
74
+ });
75
+
76
+ test("asks for attention on an action prompt and on a failed one", () => {
77
+ expect(
78
+ setupSummaryOf(
79
+ runtimeWith({
80
+ kind: BridgeSetupPromptKind.Action,
81
+ message: text("get a new code"),
82
+ actions: [],
83
+ }),
84
+ )?.attention,
85
+ ).toBe(true);
86
+
87
+ expect(
88
+ setupSummaryOf(
89
+ runtimeWith({
90
+ kind: BridgeSetupPromptKind.Failed,
91
+ message: text("the sign-in expired"),
92
+ actions: [],
93
+ }),
94
+ )?.attention,
95
+ ).toBe(true);
96
+ });
97
+
98
+ test("waits quietly while a progress prompt runs", () => {
99
+ expect(
100
+ setupSummaryOf(
101
+ runtimeWith({
102
+ kind: BridgeSetupPromptKind.Progress,
103
+ message: text("downloading the game"),
104
+ percent: 10,
105
+ detail: "146.6 MiB / 1.4 GiB",
106
+ }),
107
+ )?.attention,
108
+ ).toBe(false);
109
+ });
110
+ });
111
+
112
+ describe("the unsupported sentinel", () => {
113
+ test("answers a driver that declares no setup", () => {
114
+ expect(unsupportedSetup()).toEqual({
115
+ supported: false,
116
+ runtime: null,
117
+ });
118
+ });
119
+ });
120
+
121
+ describe("a rejected submit", () => {
122
+ test("turns the user error into a failed prompt keeping the previous actions", () => {
123
+ const result = failedSetup(
124
+ runtimeWith({
125
+ kind: BridgeSetupPromptKind.Link,
126
+ message: text("sign in with your account"),
127
+ url: "https://hytale.com/device",
128
+ code: "ABCD-EFGH",
129
+ expiresAt: null,
130
+ actions: [
131
+ {
132
+ id: "renew",
133
+ label: text("Get a new code"),
134
+ },
135
+ ],
136
+ }),
137
+ text("this code cannot be renewed any more"),
138
+ );
139
+
140
+ expect(result).toEqual({
141
+ supported: true,
142
+ runtime: {
143
+ steps: [
144
+ {
145
+ id: "sign-in",
146
+ state: BridgeSetupStepState.Active,
147
+ },
148
+ {
149
+ id: "download",
150
+ state: BridgeSetupStepState.Pending,
151
+ },
152
+ ],
153
+ prompt: {
154
+ kind: BridgeSetupPromptKind.Failed,
155
+ message: text("this code cannot be renewed any more"),
156
+ actions: [
157
+ {
158
+ id: "renew",
159
+ label: text("Get a new code"),
160
+ },
161
+ ],
162
+ },
163
+ },
164
+ });
165
+ });
166
+
167
+ test("carries no actions over from a prompt that had none", () => {
168
+ const result = failedSetup(
169
+ runtimeWith({
170
+ kind: BridgeSetupPromptKind.Wait,
171
+ message: text("starting your server"),
172
+ }),
173
+ text("the server never came up"),
174
+ );
175
+
176
+ expect(result.runtime?.prompt).toEqual({
177
+ kind: BridgeSetupPromptKind.Failed,
178
+ message: text("the server never came up"),
179
+ actions: [],
180
+ });
181
+ });
182
+
183
+ test("stays empty when the driver never reported a runtime", () => {
184
+ expect(failedSetup(null, text("nothing to fail"))).toEqual({
185
+ supported: true,
186
+ runtime: null,
187
+ });
188
+ });
189
+ });
@@ -0,0 +1,63 @@
1
+ import { BridgeSetupPromptKind, BridgeSetupStepState, type LocalizedText } from "../core";
2
+ import type {
3
+ BridgeSetupAction,
4
+ BridgeSetupPrompt,
5
+ BridgeSetupResult,
6
+ BridgeSetupRuntime,
7
+ BridgeSetupSummary,
8
+ } from "../protocol";
9
+
10
+ const ATTENTION_KINDS: ReadonlySet<BridgeSetupPromptKind> = new Set([
11
+ BridgeSetupPromptKind.Link,
12
+ BridgeSetupPromptKind.Action,
13
+ BridgeSetupPromptKind.Failed,
14
+ ]);
15
+
16
+ const actionsOf = (prompt: BridgeSetupPrompt | null): BridgeSetupAction[] => {
17
+ if (!prompt || prompt.kind === BridgeSetupPromptKind.Wait || prompt.kind === BridgeSetupPromptKind.Progress) {
18
+ return [];
19
+ }
20
+
21
+ return prompt.actions;
22
+ };
23
+
24
+ export const setupSummaryOf = (runtime: BridgeSetupRuntime | null): BridgeSetupSummary | null => {
25
+ if (!runtime) {
26
+ return null;
27
+ }
28
+
29
+ const active = runtime.steps.find((step) => step.state === BridgeSetupStepState.Active);
30
+
31
+ return {
32
+ step: active?.id ?? null,
33
+ attention: runtime.prompt !== null && ATTENTION_KINDS.has(runtime.prompt.kind),
34
+ };
35
+ };
36
+
37
+ export const unsupportedSetup = (): BridgeSetupResult => {
38
+ return {
39
+ supported: false,
40
+ runtime: null,
41
+ };
42
+ };
43
+
44
+ export const failedSetup = (runtime: BridgeSetupRuntime | null, text: LocalizedText): BridgeSetupResult => {
45
+ if (!runtime) {
46
+ return {
47
+ supported: true,
48
+ runtime: null,
49
+ };
50
+ }
51
+
52
+ return {
53
+ supported: true,
54
+ runtime: {
55
+ steps: runtime.steps,
56
+ prompt: {
57
+ kind: BridgeSetupPromptKind.Failed,
58
+ message: text,
59
+ actions: actionsOf(runtime.prompt),
60
+ },
61
+ },
62
+ };
63
+ };
@@ -0,0 +1,114 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { leadsGroup, readProcessGroup, signalGroup } from "./bridgeSignal";
3
+
4
+ const MISSING_PID = 2 ** 22 - 1;
5
+
6
+ describe("readProcessGroup", () => {
7
+ test("reads the process group of this process", async () => {
8
+ const group = await readProcessGroup(process.pid);
9
+
10
+ expect(group).not.toBeNull();
11
+ expect(group).toBeGreaterThan(0);
12
+ });
13
+
14
+ test("puts a detached child in a group of its own and an attached one in ours", async () => {
15
+ const detached = Bun.spawn(
16
+ [
17
+ "sleep",
18
+ "5",
19
+ ],
20
+ {
21
+ detached: true,
22
+ stdout: "ignore",
23
+ stderr: "ignore",
24
+ },
25
+ );
26
+
27
+ const attached = Bun.spawn(
28
+ [
29
+ "sleep",
30
+ "5",
31
+ ],
32
+ {
33
+ stdout: "ignore",
34
+ stderr: "ignore",
35
+ },
36
+ );
37
+
38
+ try {
39
+ expect(await readProcessGroup(detached.pid)).toBe(detached.pid);
40
+ expect(await readProcessGroup(attached.pid)).toBe(await readProcessGroup(process.pid));
41
+ } finally {
42
+ detached.kill("SIGKILL");
43
+ attached.kill("SIGKILL");
44
+
45
+ await Promise.all([
46
+ detached.exited,
47
+ attached.exited,
48
+ ]);
49
+ }
50
+ });
51
+
52
+ test("answers null for a pid that does not exist", async () => {
53
+ expect(await readProcessGroup(MISSING_PID)).toBeNull();
54
+ });
55
+ });
56
+
57
+ describe("leadsGroup", () => {
58
+ test("only a pid equal to its own group leads it", () => {
59
+ expect(leadsGroup(1234, 1234)).toBe(true);
60
+ expect(leadsGroup(1234, 1200)).toBe(false);
61
+ expect(leadsGroup(1234, null)).toBe(false);
62
+ });
63
+ });
64
+
65
+ describe("signalGroup", () => {
66
+ test("signals every process sharing the leader's group", async () => {
67
+ const leader = Bun.spawn(
68
+ [
69
+ "bash",
70
+ "-c",
71
+ "sleep 30 & echo $!; wait",
72
+ ],
73
+ {
74
+ detached: true,
75
+ stdout: "pipe",
76
+ stderr: "ignore",
77
+ },
78
+ );
79
+
80
+ const first = await leader.stdout.getReader().read();
81
+ const child = Number(new TextDecoder().decode(first.value).trim());
82
+
83
+ expect(Number.isSafeInteger(child)).toBe(true);
84
+
85
+ expect(signalGroup(leader.pid, await readProcessGroup(leader.pid), "SIGKILL")).toBe(true);
86
+
87
+ await leader.exited;
88
+ await Bun.sleep(200);
89
+
90
+ expect(() => process.kill(child, 0)).toThrow();
91
+ }, 20_000);
92
+
93
+ test("refuses to signal a group the pid does not lead", async () => {
94
+ const attached = Bun.spawn(
95
+ [
96
+ "sleep",
97
+ "5",
98
+ ],
99
+ {
100
+ stdout: "ignore",
101
+ stderr: "ignore",
102
+ },
103
+ );
104
+
105
+ try {
106
+ expect(signalGroup(attached.pid, await readProcessGroup(attached.pid), "SIGKILL")).toBe(false);
107
+ expect(attached.killed).toBe(false);
108
+ } finally {
109
+ attached.kill("SIGKILL");
110
+
111
+ await attached.exited;
112
+ }
113
+ });
114
+ });
@@ -0,0 +1,31 @@
1
+ const STAT_PGID_INDEX = 2;
2
+
3
+ export const readProcessGroup = async (pid: number) => {
4
+ try {
5
+ const stat = await Bun.file(`/proc/${pid}/stat`).text();
6
+ const fields = stat.slice(stat.lastIndexOf(")") + 2).split(" ");
7
+ const group = Number(fields.at(STAT_PGID_INDEX));
8
+
9
+ return Number.isSafeInteger(group) && group > 0 ? group : null;
10
+ } catch {
11
+ return null;
12
+ }
13
+ };
14
+
15
+ export const leadsGroup = (pid: number, group: number | null) => {
16
+ return group !== null && group === pid;
17
+ };
18
+
19
+ export const signalGroup = (pid: number, group: number | null, signal: NodeJS.Signals) => {
20
+ if (!leadsGroup(pid, group)) {
21
+ return false;
22
+ }
23
+
24
+ try {
25
+ process.kill(-pid, signal);
26
+
27
+ return true;
28
+ } catch {
29
+ return false;
30
+ }
31
+ };
@@ -0,0 +1,111 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { tmpdir } from "node:os";
3
+ import { BridgeRuntime } from "../core";
4
+ import type { BridgeLeveledLogger } from "./bridgeLog";
5
+ import { readProcessGroup } from "./bridgeSignal";
6
+ import { createSupervisor } from "./bridgeSupervisor";
7
+
8
+ const CHILD_PATTERN = /child=(?<pid>\d+)/;
9
+
10
+ const WRAPPER = "sleep 300 & echo child=$!; wait";
11
+
12
+ const STUBBORN_WRAPPER = 'trap "" TERM; sleep 300 & echo child=$!; wait';
13
+
14
+ const silent: BridgeLeveledLogger = Object.assign(() => {}, {
15
+ warn: () => {},
16
+ error: () => {},
17
+ });
18
+
19
+ const alive = (pid: number) => {
20
+ try {
21
+ process.kill(pid, 0);
22
+
23
+ return true;
24
+ } catch {
25
+ return false;
26
+ }
27
+ };
28
+
29
+ const waitGone = async (pid: number, timeoutMs: number) => {
30
+ for (let waited = 0; waited < timeoutMs; waited += 50) {
31
+ if (!alive(pid)) {
32
+ return true;
33
+ }
34
+
35
+ await Bun.sleep(50);
36
+ }
37
+
38
+ return !alive(pid);
39
+ };
40
+
41
+ const wrapperSupervisor = (script: string, stopTimeoutSeconds: number) => {
42
+ return createSupervisor({
43
+ cwd: tmpdir(),
44
+ uid: process.getuid?.() ?? 0,
45
+ gid: process.getgid?.() ?? 0,
46
+ stopTimeoutSeconds,
47
+ resolveCommand: async () => {
48
+ return [
49
+ "bash",
50
+ "-c",
51
+ script,
52
+ ];
53
+ },
54
+ onLog: silent,
55
+ });
56
+ };
57
+
58
+ const startWrapper = async (script: string, stopTimeoutSeconds: number) => {
59
+ const supervisor = wrapperSupervisor(script, stopTimeoutSeconds);
60
+ const started = supervisor.watch(CHILD_PATTERN, 10_000);
61
+
62
+ await supervisor.start();
63
+
64
+ const match = await started;
65
+ const pid = Number(match?.groups?.pid);
66
+
67
+ expect(match, "the wrapper never reported the pid it spawned").not.toBeNull();
68
+ expect(Number.isSafeInteger(pid)).toBe(true);
69
+
70
+ return {
71
+ supervisor,
72
+ pid,
73
+ };
74
+ };
75
+
76
+ describe("createSupervisor", () => {
77
+ test("runs the game outside the bridge's own process group", async () => {
78
+ const { supervisor, pid } = await startWrapper(WRAPPER, 1);
79
+
80
+ try {
81
+ const gameGroup = await readProcessGroup(pid);
82
+ const bridgeGroup = await readProcessGroup(process.pid);
83
+
84
+ expect(gameGroup, "the game must land in a group of its own or a group kill would hit the bridge").not.toBe(
85
+ bridgeGroup,
86
+ );
87
+ } finally {
88
+ await supervisor.stop();
89
+ }
90
+
91
+ expect(await waitGone(pid, 5000)).toBe(true);
92
+ }, 20_000);
93
+
94
+ test("takes the wrapper's child down with it when the stop times out", async () => {
95
+ const { supervisor, pid } = await startWrapper(WRAPPER, 1);
96
+
97
+ await supervisor.stop();
98
+
99
+ expect(supervisor.runtime).toBe(BridgeRuntime.Stopped);
100
+ expect(await waitGone(pid, 5000), "the game was orphaned when the wrapper was signalled").toBe(true);
101
+ }, 20_000);
102
+
103
+ test("kills the wrapper's child through the escalation when the wrapper ignores SIGTERM", async () => {
104
+ const { supervisor, pid } = await startWrapper(STUBBORN_WRAPPER, 1);
105
+
106
+ await supervisor.stop();
107
+
108
+ expect(supervisor.runtime).toBe(BridgeRuntime.Stopped);
109
+ expect(await waitGone(pid, 5000), "the game outlived the SIGKILL escalation").toBe(true);
110
+ }, 30_000);
111
+ });
@@ -1,8 +1,10 @@
1
1
  import type { BridgeCommandOptions, BridgeReply } from "../context";
2
2
  import { BridgeRuntime, BridgeStream } from "../core";
3
+ import { stripAnsi } from "../utils";
3
4
  import type { BridgeLeveledLogger } from "./bridgeLog";
4
5
  import { errorText } from "./bridgeLog";
5
6
  import { settleUid, unprivilegedArgv } from "./bridgePrivilege";
7
+ import { readProcessGroup, signalGroup } from "./bridgeSignal";
6
8
 
7
9
  const TAIL_LIMIT = 500;
8
10
 
@@ -39,6 +41,18 @@ export interface SupervisorOptions {
39
41
 
40
42
  type LineWatcher = (entry: SupervisorLine) => void;
41
43
 
44
+ type SpawnedGame = Bun.Subprocess<"pipe", "pipe", "pipe">;
45
+
46
+ const terminate = (spawned: SpawnedGame, group: number | null, signal: NodeJS.Signals) => {
47
+ if (!signalGroup(spawned.pid, group, signal)) {
48
+ spawned.kill(signal);
49
+ }
50
+ };
51
+
52
+ const sweep = (pid: number, group: number | null) => {
53
+ signalGroup(pid, group, "SIGKILL");
54
+ };
55
+
42
56
  const preferOomVictim = async (pid: number) => {
43
57
  try {
44
58
  await Bun.write(`/proc/${pid}/oom_score_adj`, `${GAME_OOM_SCORE_ADJ}\n`);
@@ -110,7 +124,8 @@ export const createSupervisor = (options: SupervisorOptions) => {
110
124
  const watchers = new Set<LineWatcher>();
111
125
  const restartStamps: number[] = [];
112
126
 
113
- let child: Bun.Subprocess<"pipe", "pipe", "pipe"> | null = null;
127
+ let child: SpawnedGame | null = null;
128
+ let childGroup: number | null = null;
114
129
  let runtime: BridgeRuntime = BridgeRuntime.Stopped;
115
130
  let shouldRun = false;
116
131
  let breaker = false;
@@ -183,7 +198,7 @@ export const createSupervisor = (options: SupervisorOptions) => {
183
198
  }, timeoutMs);
184
199
 
185
200
  const watcher: LineWatcher = (entry) => {
186
- const match = entry.line.match(pattern);
201
+ const match = stripAnsi(entry.line).match(pattern);
187
202
 
188
203
  if (!match) {
189
204
  return;
@@ -225,6 +240,7 @@ export const createSupervisor = (options: SupervisorOptions) => {
225
240
  const spawned = Bun.spawn(spawnArgv, {
226
241
  ...credentials,
227
242
  cwd: options.cwd,
243
+ detached: true,
228
244
  stderr: "pipe",
229
245
  stdin: "pipe",
230
246
  stdout: "pipe",
@@ -233,6 +249,7 @@ export const createSupervisor = (options: SupervisorOptions) => {
233
249
  },
234
250
  });
235
251
 
252
+ const group = await readProcessGroup(spawned.pid);
236
253
  const observed = await settleUid(spawned.pid, options.uid);
237
254
 
238
255
  if (observed !== options.uid) {
@@ -247,10 +264,12 @@ export const createSupervisor = (options: SupervisorOptions) => {
247
264
  );
248
265
  }
249
266
 
250
- spawned.kill("SIGKILL");
267
+ terminate(spawned, group, "SIGKILL");
251
268
 
252
269
  await spawned.exited;
253
270
 
271
+ sweep(spawned.pid, group);
272
+
254
273
  throw new Error(
255
274
  `the game process runs as uid ${String(observed)}, expected ${options.uid} — refusing to run it as the bridge`,
256
275
  );
@@ -259,6 +278,7 @@ export const createSupervisor = (options: SupervisorOptions) => {
259
278
  await preferOomVictim(spawned.pid);
260
279
 
261
280
  child = spawned;
281
+ childGroup = group;
262
282
  exitCode = null;
263
283
 
264
284
  void pump(spawned.stdout, BridgeStream.Stdout);
@@ -266,6 +286,7 @@ export const createSupervisor = (options: SupervisorOptions) => {
266
286
 
267
287
  options.onLog("game started", {
268
288
  pid: spawned.pid,
289
+ group,
269
290
  uid: options.uid,
270
291
  });
271
292
 
@@ -306,9 +327,17 @@ export const createSupervisor = (options: SupervisorOptions) => {
306
327
  };
307
328
 
308
329
  function handleExit(code: number) {
330
+ const finished = child;
331
+
309
332
  child = null;
310
333
  exitCode = code;
311
334
 
335
+ if (finished) {
336
+ sweep(finished.pid, childGroup);
337
+ }
338
+
339
+ childGroup = null;
340
+
312
341
  if (!shouldRun) {
313
342
  outOfMemory = false;
314
343
 
@@ -419,6 +448,7 @@ export const createSupervisor = (options: SupervisorOptions) => {
419
448
  }
420
449
 
421
450
  const active = child;
451
+ const group = childGroup;
422
452
 
423
453
  runtime = BridgeRuntime.Stopping;
424
454
 
@@ -441,7 +471,7 @@ export const createSupervisor = (options: SupervisorOptions) => {
441
471
  ]);
442
472
 
443
473
  if (settled === null) {
444
- active.kill("SIGTERM");
474
+ terminate(active, group, "SIGTERM");
445
475
 
446
476
  const afterTerm = await Promise.race([
447
477
  active.exited,
@@ -449,12 +479,14 @@ export const createSupervisor = (options: SupervisorOptions) => {
449
479
  ]);
450
480
 
451
481
  if (afterTerm === null) {
452
- active.kill("SIGKILL");
482
+ terminate(active, group, "SIGKILL");
453
483
 
454
484
  await active.exited;
455
485
  }
456
486
  }
457
487
 
488
+ sweep(active.pid, group);
489
+
458
490
  runtime = BridgeRuntime.Stopped;
459
491
  },
460
492