Skip to content

Commit bcceb73

Browse files
committed
test(tailscale): wait on events in the probe cleanup test
The test waited for its fixture with a wall-clock Effect.timeout and polled for the group's exit on a real-clock schedule. It now drives the probe timeout with TestClock.adjust and waits on a FIFO that every process-group member holds open, so EOF means the whole group is gone. A group member that catches SIGTERM reports itself after a short linger, so a probe that never escalates to SIGKILL fails the assertion in a few seconds instead of hanging on the read.
1 parent dae7732 commit bcceb73

1 file changed

Lines changed: 28 additions & 47 deletions

File tree

‎packages/tailscale/src/tailscale.test.ts‎

Lines changed: 28 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@ import * as Fiber from "effect/Fiber";
55
import * as FileSystem from "effect/FileSystem";
66
import * as Layer from "effect/Layer";
77
import * as Path from "effect/Path";
8-
import * as PlatformError from "effect/PlatformError";
9-
import * as Schedule from "effect/Schedule";
108
import * as Sink from "effect/Sink";
119
import * as Stream from "effect/Stream";
1210
import * as TestClock from "effect/testing/TestClock";
@@ -229,36 +227,33 @@ describe("tailscale", () => {
229227
});
230228
});
231229

232-
// The wrapper keeps the default SIGTERM disposition while its `sleep` child
233-
// inherits an ignored one, so SIGTERM ends the leader and leaves a group member.
234-
const sigtermIgnoringTailscale = (readyPath: string) => `#!/bin/sh
235-
trap '' TERM
236-
sleep 60 &
237-
child=$!
238-
trap - TERM
239-
echo "$$ $child" > '${readyPath}'
230+
// The leader dies on SIGTERM while a subshell in its process group catches it and
231+
// lingers, like a helper that outlives `tailscale`. Every member holds fd 3, the
232+
// write end of the `group` FIFO, so reading it reaches EOF once the whole group is
233+
// gone. A survivor SIGKILL never reached reports itself on the way out, so a leak
234+
// fails an assertion instead of hanging the read.
235+
const lingeringTailscale = (readyPath: string, groupPath: string) => `#!/bin/sh
236+
exec 3> '${groupPath}'
237+
( trap 'sleep 3; echo survived >&3; exit' TERM
238+
sleep 60 &
239+
echo ready > '${readyPath}'
240+
wait ) &
240241
wait
241242
`;
242243

243-
const isProcessAlive = (pid: number) =>
244-
Effect.try(() => process.kill(pid, 0)).pipe(
245-
Effect.match({ onFailure: () => false, onSuccess: () => true }),
246-
);
247-
248244
describe.skipIf(HostProcess.Platform.defaultValue() === "win32")("tailscale probe cleanup", () => {
249-
it.effect("kills a timed-out status probe whose process group ignores SIGTERM", () =>
245+
it.effect("kills a timed-out status probe whose process group outlives SIGTERM", () =>
250246
Effect.gen(function* () {
251247
const fs = yield* FileSystem.FileSystem;
252248
const path = yield* Path.Path;
253249
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
254250
const directory = yield* fs.makeTempDirectoryScoped({ prefix: "t3-tailscale-" });
255251
const readyPath = path.join(directory, "ready");
256-
// The fixture reports its PIDs through a FIFO so the test blocks on the
257-
// fixture being up instead of polling for a file.
258-
yield* spawner.exitCode(ChildProcess.make("mkfifo", [readyPath]));
252+
const groupPath = path.join(directory, "group");
253+
yield* spawner.exitCode(ChildProcess.make("mkfifo", [readyPath, groupPath]));
259254
yield* fs.writeFileString(
260255
path.join(directory, "tailscale"),
261-
sigtermIgnoringTailscale(readyPath),
256+
lingeringTailscale(readyPath, groupPath),
262257
{ mode: 0o700 },
263258
);
264259
yield* Effect.acquireRelease(
@@ -272,41 +267,27 @@ describe.skipIf(HostProcess.Platform.defaultValue() === "win32")("tailscale prob
272267
process.env.PATH = originalPath;
273268
}),
274269
);
275-
276-
const fiber = yield* readTailscaleStatus.pipe(Effect.flip, Effect.forkScoped);
277-
const pids: Array<number> = [];
270+
// Interrupting a FIFO read leaves its threadpool thread parked until a
271+
// writer opens the FIFO, so opening both releases readers a failed run left.
278272
yield* Effect.addFinalizer(() =>
279273
Effect.forEach(
280-
pids,
281-
(pid) => Effect.try(() => process.kill(pid, "SIGKILL")).pipe(Effect.ignore),
274+
[readyPath, groupPath],
275+
(fifo) => fs.open(fifo, { flag: "r+" }).pipe(Effect.scoped, Effect.ignore),
282276
{ discard: true },
283277
),
284278
);
285-
// Interrupting a FIFO read leaves its threadpool thread parked until a
286-
// writer opens the FIFO, so a failed wait opens it to release the reader.
287-
const report = yield* fs.readFileString(readyPath).pipe(
288-
Effect.raceFirst(Fiber.join(fiber).pipe(Effect.flatMap(Effect.fail))),
289-
Effect.timeout("5 seconds"),
290-
TestClock.withLive,
291-
Effect.onError(() => fs.open(readyPath, { flag: "r+" }).pipe(Effect.scoped, Effect.ignore)),
292-
);
293-
pids.push(...report.trim().split(" ").map(Number));
294-
assert.deepEqual(yield* Effect.forEach(pids, isProcessAlive), [true, true]);
295279

296-
yield* TestClock.adjust("3500 millis");
297-
const error = yield* Fiber.join(fiber);
280+
const group = yield* fs.readFileString(groupPath).pipe(Effect.forkScoped);
281+
const probe = yield* readTailscaleStatus.pipe(Effect.flip, Effect.forkScoped);
282+
yield* fs
283+
.readFileString(readyPath)
284+
.pipe(Effect.raceFirst(Fiber.join(probe).pipe(Effect.flatMap(Effect.fail))));
285+
286+
yield* TestClock.adjust("1 minute");
287+
const error = yield* Fiber.join(probe);
298288

299289
assert.instanceOf(error, TailscaleCommandTimeoutError);
300-
// SIGKILL lands asynchronously, so give the group a bounded real-time window to go.
301-
const alive = yield* Effect.forEach(pids, isProcessAlive).pipe(
302-
Effect.repeat({
303-
until: (states) => states.every((state) => !state),
304-
schedule: Schedule.spaced("20 millis"),
305-
times: 100,
306-
}),
307-
TestClock.withLive,
308-
);
309-
assert.deepEqual(alive, [false, false]);
290+
assert.equal(yield* Fiber.join(group), "", "a process group member outlived the probe");
310291
}).pipe(Effect.provide(NodeServices.layer)),
311292
);
312293
});

0 commit comments

Comments
 (0)