@zgeoff/atc 3.6.0 → 3.6.1

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zgeoff/atc",
3
- "version": "3.6.0",
3
+ "version": "3.6.1",
4
4
  "description": "Terminal control tower for coding-agent sessions",
5
5
  "homepage": "https://github.com/zgeoff/atc#readme",
6
6
  "bugs": "https://github.com/zgeoff/atc/issues",
@@ -85,5 +85,8 @@
85
85
  "smol-toml": "1.9.0",
86
86
  "tinypool": "2.1.2"
87
87
  },
88
- "packageManager": "bun@1.3.10"
88
+ "packageManager": "bun@1.3.10",
89
+ "patchedDependencies": {
90
+ "knip@6.24.0": "patches/knip@6.24.0.patch"
91
+ }
89
92
  }
@@ -163,6 +163,11 @@ export interface DaemonOptions {
163
163
  // Starts the eject settle timer; defaults to a real `setTimeout`.
164
164
  readonly scheduleEjectSettle?: SettleScheduler;
165
165
 
166
+ // How long a failed spawn's rollback waits for the killed process to
167
+ // exit, once after its kill and once more after a forced kill; 2 s when
168
+ // unset.
169
+ readonly failedSpawnExitWaitMs?: number;
170
+
166
171
  // A fleet-wide restore revives one session at a time, waiting for each to
167
172
  // report it has booted before starting the next so the machine is not
168
173
  // buried under a dozen simultaneous agent boots. This caps how long a
@@ -427,6 +432,10 @@ export async function startDaemon(opts: DaemonOptions): Promise<DaemonHandle> {
427
432
  mgr.log = opts.log;
428
433
  }
429
434
 
435
+ if (opts.failedSpawnExitWaitMs !== undefined) {
436
+ mgr.failedSpawnExitWaitMs = opts.failedSpawnExitWaitMs;
437
+ }
438
+
430
439
  const records = new PublishedRecords({
431
440
  store,
432
441
  daemonID: store.daemonID,
@@ -290,8 +290,8 @@ interface SpawnReadied {
290
290
  // stored identity ran on.
291
291
  const LOCAL_TARGET_IDENTITY = buildTargetIdentity('local-pty', {});
292
292
 
293
- // How long a failed spawn's rollback waits for the killed process to exit,
294
- // once after its kill and once more after a forced kill.
293
+ // How long a failed spawn's rollback waits for the killed process to exit
294
+ // unless the manager is given another wait.
295
295
  const FAILED_SPAWN_EXIT_WAIT_MS = 2000;
296
296
 
297
297
  // How a harness takes its credential from impd's broker: the broker the
@@ -364,6 +364,11 @@ export class SessionManager {
364
364
  // sessions start without a record.
365
365
  records: PublishedRecords | null = null;
366
366
 
367
+ // How long a failed spawn's rollback waits for the killed process to
368
+ // exit, in milliseconds, once after its kill and once more after a forced
369
+ // kill.
370
+ failedSpawnExitWaitMs = FAILED_SPAWN_EXIT_WAIT_MS;
371
+
367
372
  // Whether any registered adapter has a screen detector, decided once at
368
373
  // construction since the registry never changes afterward. Lets a hot path
369
374
  // that only cares about this answer skip walking live sessions to find it.
@@ -2531,7 +2536,7 @@ export class SessionManager {
2531
2536
  // Waits for a killed harness to exit, ending it with a signal it cannot
2532
2537
  // ignore when the first wait runs out and its provider can send one.
2533
2538
  private async waitForKilledExit(pty: HarnessHandle): Promise<boolean> {
2534
- const exited = await pty.waitForExit(FAILED_SPAWN_EXIT_WAIT_MS);
2539
+ const exited = await pty.waitForExit(this.failedSpawnExitWaitMs);
2535
2540
 
2536
2541
  if (exited) {
2537
2542
  return true;
@@ -2543,7 +2548,7 @@ export class SessionManager {
2543
2548
 
2544
2549
  pty.killForced();
2545
2550
 
2546
- return pty.waitForExit(FAILED_SPAWN_EXIT_WAIT_MS);
2551
+ return pty.waitForExit(this.failedSpawnExitWaitMs);
2547
2552
  }
2548
2553
 
2549
2554
  // Whether a failed spawn's rollback leaves the session free to revive:
@@ -0,0 +1,16 @@
1
+ import type { HarnessAttachment } from '../daemon/execution-provider';
2
+
3
+ /**
4
+ * An attachment listener that fails as a listener whose write to a closed
5
+ * peer fails: it throws an error with the given message each time a
6
+ * connection is attached, and returns quietly while one is reattaching.
7
+ */
8
+ export function buildStubFailingAttachmentListener(
9
+ message: string,
10
+ ): (attachment: HarnessAttachment) => void {
11
+ return (attachment) => {
12
+ if (attachment === 'attached') {
13
+ throw new Error(message);
14
+ }
15
+ };
16
+ }
@@ -1,5 +1,6 @@
1
1
  import type { AgentAdapter } from '../agents/agent-adapter';
2
2
  import { buildMockAgentAdapter } from './build-mock-agent-adapter';
3
+ import { runCommand } from './run-command';
3
4
 
4
5
  interface Plan {
5
6
  readonly bin: string;
@@ -34,20 +35,20 @@ interface FailingAgentAdapterConfig {
34
35
  * read finds no headless runner. Every member but the spawn plan and the
35
36
  * headless runner is the mock adapter's.
36
37
  *
37
- * With a ready pipe, the stub makes the pipe at that path, and the first
38
- * failing read blocks until a process writes its pid there, which the
39
- * headless runner read is synchronous for. A read that finds no writer
38
+ * With a ready pipe, the stub makes the pipe at that path before it
39
+ * resolves, and the first failing read blocks until a process writes its
40
+ * pid there, which the headless runner read is synchronous for. A read that finds no writer
40
41
  * within the timeout throws instead of failing the start. `getReadyPID`
41
42
  * returns the pid that read took. `countPlans` reads how many spawns the
42
43
  * adapter has planned.
43
44
  */
44
- export function createStubFailingAgentAdapter(config: FailingAgentAdapterConfig) {
45
+ export async function createStubFailingAgentAdapter(config: FailingAgentAdapterConfig) {
45
46
  let planned = 0;
46
47
  let readsToFail = 0;
47
48
  let readyPID: number | null = null;
48
49
 
49
50
  if (config.ready !== null) {
50
- createPipe(config.ready.path);
51
+ await createPipe(config.ready.path);
51
52
  }
52
53
 
53
54
  const adapter: AgentAdapter = {
@@ -90,11 +91,11 @@ export function createStubFailingAgentAdapter(config: FailingAgentAdapterConfig)
90
91
  };
91
92
  }
92
93
 
93
- function createPipe(path: string): void {
94
- const made = Bun.spawnSync(['mkfifo', path]);
94
+ async function createPipe(path: string): Promise<void> {
95
+ const made = await runCommand(['mkfifo', path]);
95
96
 
96
97
  if (made.exitCode !== 0) {
97
- throw new Error(`mkfifo could not make ${path} (${made.stderr.toString().trim()})`);
98
+ throw new Error(`mkfifo could not make ${path} (${made.stderr.trim()})`);
98
99
  }
99
100
  }
100
101
 
@@ -1,5 +1,6 @@
1
1
  import { join } from 'node:path';
2
2
  import { createStubBin } from './create-stub-bin';
3
+ import { runCommand } from './run-command';
3
4
 
4
5
  /**
5
6
  * Creates a stand-in guest program for a harness to run, under the
@@ -8,12 +9,13 @@ import { createStubBin } from './create-stub-bin';
8
9
  * `GOT:<line>`, prints `SIZE:<rows> <cols>` on `size`, and exits 3 on
9
10
  * `quit`. On `later` it waits for a line on the named pipe at `burstPath`,
10
11
  * then prints 300000 `x` bytes, more than impd's ring keeps, a newline, and
11
- * `BURST_DONE`. Returns the script's path and the pipe's.
12
+ * `BURST_DONE`. Resolves with the script's path and the pipe's once the pipe
13
+ * exists.
12
14
  */
13
- export function createStubHarnessGuest(dir: string) {
15
+ export async function createStubHarnessGuest(dir: string) {
14
16
  const burstPath = join(dir, 'burst');
15
17
 
16
- Bun.spawnSync(['mkfifo', burstPath]);
18
+ await runCommand(['mkfifo', burstPath]);
17
19
 
18
20
  const path = createStubBin(
19
21
  dir,
@@ -31,16 +31,92 @@ import { isImpNameAllowed } from '../daemon/is-imp-name-allowed';
31
31
  import { isBrokerVariable } from '../shared/is-broker-variable';
32
32
  import { registerTestCleanup } from './register-test-cleanup';
33
33
 
34
- // impd keeps exactly this many bytes of each generation's output.
35
- const RING_BYTES = 262_144;
34
+ /**
35
+ * Creates an in-process stand-in for impd behind the imp port, calling impd
36
+ * as the principal, `token:atc` when absent. Each imp is a set of
37
+ * real `bun-pty` processes on this machine; a sleeping imp stops them with
38
+ * SIGSTOP and a wake continues them, so a memory wake keeps each process
39
+ * and its generation. Every session keeps an exact 262144-byte ring with
40
+ * offsets, a fresh attach skips to the next line and sends a mode prelude,
41
+ * and impd's refusals carry its codes and data: `LEASED`, `LEASE_NOT_HELD`,
42
+ * `NO_SESSION`, `INVALID_STATE`, `INVALID_RESUME`, `NOT_FOUND`, `CONFLICT`,
43
+ * and `FORBIDDEN`. Leases belong to principals; the port acts as
44
+ * `principal`, and a test adds other owners' leases, cold boots, and
45
+ * dropped sockets through the controls. Grants follow impd 0.27: the
46
+ * caller's identity must reach the imp and, under imp patterns, list the
47
+ * secret as grantable; a grant is idempotent, one secret per host, and a
48
+ * destroyed imp or a rebound or removed secret takes its grants with it. A
49
+ * start or an attach that requires the broker is refused with
50
+ * `PRECONDITION_FAILED` and reason `broker_not_ready`, and runs nothing,
51
+ * while the broker fails or the imp holds no grant, when the start sets a
52
+ * broker variable, and when it would join a process that started without
53
+ * the broker required. Once the current test finishes, the stand-in kills
54
+ * every process and stops every forward it holds, so it must be created inside
55
+ * a test; `stop` does so sooner, and a second stop does nothing.
56
+ */
57
+ export function createStubImpPort(principal = 'token:atc'): StubImpPort {
58
+ return new StubImpPort(principal);
59
+ }
36
60
 
37
- // The mode bytes a fresh attach sends ahead of a ring that has wrapped.
38
- const PRELUDE = '\u001B[0m';
61
+ interface StubLease {
62
+ readonly principal: string;
63
+ readonly label: string;
64
+ readonly until: number;
65
+ }
66
+
67
+ interface StubConnection {
68
+ readonly handlers: ImpSessionHandlers;
69
+ sent: number;
70
+ finished: boolean;
71
+ process: StubProcess | null;
72
+ readonly stop: (outcome: ImpSessionOutcome) => void;
73
+ }
74
+
75
+ interface StubProcess {
76
+ readonly pty: IPty;
77
+ readonly generation: string;
78
+ ring: Uint8Array;
79
+ end: number;
80
+ exited: { readonly code: number | null } | null;
81
+
82
+ // Ended by a cold boot: its exit is never delivered.
83
+ ended: boolean;
84
+ connection: StubConnection | null;
85
+
86
+ // Whether its start required the broker, which an attach that requires
87
+ // it needs.
88
+ readonly requireBroker: boolean;
89
+ }
90
+
91
+ interface StubImp {
92
+ readonly id: string;
93
+ readonly name: string;
94
+ state: ImpState;
95
+ bootId: string;
96
+ coldBoots: ColdBoot[];
97
+ readonly leases: Map<string, StubLease>;
98
+ readonly sessions: Map<string, StubProcess>;
99
+ readonly previous: Map<string, PreviousGeneration>;
100
+
101
+ // The secrets granted to the imp.
102
+ readonly grants: Set<string>;
103
+ }
39
104
 
40
105
  // The PATH every stub process runs with: a guest's own, never the
41
106
  // daemon's.
42
107
  const GUEST_PATH = '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin';
43
108
 
109
+ interface StubRelay {
110
+ // oxlint-disable-next-line prefer-readonly-parameter-types -- relayed bytes have no readonly form
111
+ readonly dataListeners: ((data: Uint8Array) => void)[];
112
+ readonly closeListeners: (() => void)[];
113
+
114
+ // Bytes a write handed over that the socket has not taken yet, and the
115
+ // writes waiting for them to go.
116
+ unsent: Uint8Array;
117
+ readonly roomWaiters: (() => void)[];
118
+ }
119
+
44
120
  // A hold on the commands whose argv holds its text: entered resolves with
45
121
  // the argv of the first command it holds, and stop lets every command it
46
122
  // holds run.
@@ -49,8 +125,14 @@ interface StubCommandHold {
49
125
  readonly stop: () => void;
50
126
  }
51
127
 
128
+ // impd keeps exactly this many bytes of each generation's output.
129
+ const RING_BYTES = 262_144;
130
+
131
+ // The mode bytes a fresh attach sends ahead of a ring that has wrapped.
132
+ const PRELUDE = '\u001B[0m';
133
+
52
134
  /**
53
- * The impd stand-in the factory below creates, one per call.
135
+ * The impd stand-in the factory above creates, one per call.
54
136
  */
55
137
  class StubImpPort implements ImpPort {
56
138
  // Every port call, in order, as `<call> <imp> [<detail>]`.
@@ -1635,88 +1717,6 @@ class StubImpPort implements ImpPort {
1635
1717
  }
1636
1718
  }
1637
1719
 
1638
- /**
1639
- * Creates an in-process stand-in for impd behind the imp port, calling impd
1640
- * as the principal, `token:atc` when absent. Each imp is a set of
1641
- * real `bun-pty` processes on this machine; a sleeping imp stops them with
1642
- * SIGSTOP and a wake continues them, so a memory wake keeps each process
1643
- * and its generation. Every session keeps an exact 262144-byte ring with
1644
- * offsets, a fresh attach skips to the next line and sends a mode prelude,
1645
- * and impd's refusals carry its codes and data: `LEASED`, `LEASE_NOT_HELD`,
1646
- * `NO_SESSION`, `INVALID_STATE`, `INVALID_RESUME`, `NOT_FOUND`, `CONFLICT`,
1647
- * and `FORBIDDEN`. Leases belong to principals; the port acts as
1648
- * `principal`, and a test adds other owners' leases, cold boots, and
1649
- * dropped sockets through the controls. Grants follow impd 0.27: the
1650
- * caller's identity must reach the imp and, under imp patterns, list the
1651
- * secret as grantable; a grant is idempotent, one secret per host, and a
1652
- * destroyed imp or a rebound or removed secret takes its grants with it. A
1653
- * start or an attach that requires the broker is refused with
1654
- * `PRECONDITION_FAILED` and reason `broker_not_ready`, and runs nothing,
1655
- * while the broker fails or the imp holds no grant, when the start sets a
1656
- * broker variable, and when it would join a process that started without
1657
- * the broker required. Once the current test finishes, the stand-in kills
1658
- * every process and stops every forward it holds, so it must be created inside
1659
- * a test; `stop` does so sooner, and a second stop does nothing.
1660
- */
1661
- export function createStubImpPort(principal = 'token:atc'): StubImpPort {
1662
- return new StubImpPort(principal);
1663
- }
1664
-
1665
- interface StubLease {
1666
- readonly principal: string;
1667
- readonly label: string;
1668
- readonly until: number;
1669
- }
1670
-
1671
- interface StubImp {
1672
- readonly id: string;
1673
- readonly name: string;
1674
- state: ImpState;
1675
- bootId: string;
1676
- coldBoots: ColdBoot[];
1677
- readonly leases: Map<string, StubLease>;
1678
- readonly sessions: Map<string, StubProcess>;
1679
- readonly previous: Map<string, PreviousGeneration>;
1680
-
1681
- // The secrets granted to the imp.
1682
- readonly grants: Set<string>;
1683
- }
1684
-
1685
- interface StubProcess {
1686
- readonly pty: IPty;
1687
- readonly generation: string;
1688
- ring: Uint8Array;
1689
- end: number;
1690
- exited: { readonly code: number | null } | null;
1691
-
1692
- // Ended by a cold boot: its exit is never delivered.
1693
- ended: boolean;
1694
- connection: StubConnection | null;
1695
-
1696
- // Whether its start required the broker, which an attach that requires
1697
- // it needs.
1698
- readonly requireBroker: boolean;
1699
- }
1700
-
1701
- interface StubConnection {
1702
- readonly handlers: ImpSessionHandlers;
1703
- sent: number;
1704
- finished: boolean;
1705
- process: StubProcess | null;
1706
- readonly stop: (outcome: ImpSessionOutcome) => void;
1707
- }
1708
-
1709
- interface StubRelay {
1710
- // oxlint-disable-next-line prefer-readonly-parameter-types -- relayed bytes have no readonly form
1711
- readonly dataListeners: ((data: Uint8Array) => void)[];
1712
- readonly closeListeners: (() => void)[];
1713
-
1714
- // Bytes a write handed over that the socket has not taken yet, and the
1715
- // writes waiting for them to go.
1716
- unsent: Uint8Array;
1717
- readonly roomWaiters: (() => void)[];
1718
- }
1719
-
1720
1720
  function buildNotFound(name: string): ImpPortError {
1721
1721
  return new ImpPortError('NOT_FOUND', `no imp ${name}`, { kind: 'imp', name });
1722
1722
  }
@@ -0,0 +1,73 @@
1
+ import { registerTestCleanup } from './register-test-cleanup';
2
+
3
+ interface CommandOptions {
4
+ readonly cwd?: string;
5
+
6
+ // The whole environment of the run, as the spawn takes it; left out, the
7
+ // run inherits the test's environment.
8
+ readonly env?: Readonly<Record<string, string | undefined>>;
9
+
10
+ readonly stdin?: string | Buffer;
11
+ }
12
+
13
+ interface CommandResult {
14
+ // Null when a signal ended the run.
15
+ readonly exitCode: number | null;
16
+ readonly signalCode: string | null;
17
+ readonly stdout: string;
18
+ readonly stderr: string;
19
+ }
20
+
21
+ /**
22
+ * Runs a command as its own process and resolves once it exits and its
23
+ * output closes, with its exit code, the signal that ended it, and
24
+ * everything it printed. The run never blocks the test's event loop, so the
25
+ * test's timeout still applies. The command leads a process group of its
26
+ * own, and a run still incomplete when the test finishes has that whole
27
+ * group killed, so a child the command left holding its output ends too.
28
+ * Call it only inside a test, never from a cleanup.
29
+ */
30
+ export async function runCommand(
31
+ cmd: readonly string[],
32
+ options: Readonly<CommandOptions> = {},
33
+ ): Promise<CommandResult> {
34
+ const proc = Bun.spawn([...cmd], {
35
+ ...(options.cwd === undefined ? {} : { cwd: options.cwd }),
36
+ ...(options.env === undefined ? {} : { env: { ...options.env } }),
37
+ stdin: options.stdin === undefined ? 'ignore' : Buffer.from(options.stdin),
38
+ stdout: 'pipe',
39
+ stderr: 'pipe',
40
+ detached: true,
41
+ });
42
+
43
+ let complete = false;
44
+
45
+ registerTestCleanup(async () => {
46
+ if (!complete) {
47
+ killProcessGroup(proc.pid);
48
+ }
49
+
50
+ await proc.exited;
51
+ });
52
+
53
+ const [stdout, stderr] = await Promise.all([
54
+ new Response(proc.stdout).text(),
55
+ new Response(proc.stderr).text(),
56
+ proc.exited,
57
+ ]);
58
+
59
+ complete = true;
60
+
61
+ return { exitCode: proc.exitCode, signalCode: proc.signalCode, stdout, stderr };
62
+ }
63
+
64
+ // A group already empty has nothing left to kill.
65
+ function killProcessGroup(group: number): void {
66
+ try {
67
+ process.kill(-group, 'SIGKILL');
68
+ } catch (error) {
69
+ if (!(error instanceof Error && Reflect.get(error, 'code') === 'ESRCH')) {
70
+ throw error;
71
+ }
72
+ }
73
+ }
@@ -22,6 +22,14 @@ interface TUIHarnessOptions {
22
22
  // How long a wait gives the client to draw its first byte; 9 seconds
23
23
  // unless set.
24
24
  readonly bootMs?: number;
25
+
26
+ // The clock the waits read their deadlines from; the wall clock unless
27
+ // set.
28
+ readonly now?: () => number;
29
+
30
+ // Waits out the interval between a wait's retries; a real sleep unless
31
+ // set.
32
+ readonly wait?: (ms: number) => Promise<void>;
25
33
  }
26
34
 
27
35
  /**
@@ -39,12 +47,16 @@ interface TUIHarnessOptions {
39
47
  * transports the fixture repositories need, with the fields given laid over
40
48
  * them. `env` is the environment the client runs with, so a daemon started
41
49
  * with it serves the client. A wait made before the client draws anything
42
- * gets `bootMs` for that first byte. `stop` kills every client a boot
50
+ * gets `bootMs` for that first byte. Every wait reads its deadline from
51
+ * `now` and sleeps between retries through `wait`, the wall clock and a real
52
+ * sleep unless the options set them. `stop` kills every client a boot
43
53
  * started, stops the daemon in the home, and removes the home. That stop runs once the current test
44
54
  * finishes, so it must run inside a test; calling `stop` sooner runs it
45
55
  * then, and a second stop does nothing.
46
56
  */
47
57
  export function startTUIHarness(options: TUIHarnessOptions = {}) {
58
+ const now = options.now ?? Date.now;
59
+ const wait = options.wait ?? Bun.sleep;
48
60
  const tmp = setupTempDir('atc-tui-');
49
61
 
50
62
  // Registered after the home, so it releases first: the client and its
@@ -147,7 +159,7 @@ export function startTUIHarness(options: TUIHarnessOptions = {}) {
147
159
  exited = exit.promise;
148
160
 
149
161
  capture = booted.onData((data) => {
150
- firstOutputAt ??= Date.now();
162
+ firstOutputAt ??= now();
151
163
  out += data;
152
164
  });
153
165
 
@@ -171,7 +183,7 @@ export function startTUIHarness(options: TUIHarnessOptions = {}) {
171
183
  },
172
184
 
173
185
  async waitFor(needle: string, ms = 4000): Promise<void> {
174
- const start = Date.now();
186
+ const start = now();
175
187
 
176
188
  // The client gives its daemon 8 seconds to answer before it draws an
177
189
  // error, so the boot phase waits that long plus 1 second for the
@@ -190,7 +202,7 @@ export function startTUIHarness(options: TUIHarnessOptions = {}) {
190
202
 
191
203
  return firstOutputAt;
192
204
  },
193
- { timeoutMs: bootMs, intervalMs: 50 },
205
+ { timeoutMs: bootMs, intervalMs: 50, now, wait },
194
206
  );
195
207
 
196
208
  await waitFor(
@@ -201,7 +213,12 @@ export function startTUIHarness(options: TUIHarnessOptions = {}) {
201
213
  );
202
214
  }
203
215
  },
204
- { timeoutMs: Math.max(0, Math.max(start, firstAt) + ms - Date.now()), intervalMs: 50 },
216
+ {
217
+ timeoutMs: Math.max(0, Math.max(start, firstAt) + ms - now()),
218
+ intervalMs: 50,
219
+ now,
220
+ wait,
221
+ },
205
222
  );
206
223
  },
207
224
 
@@ -228,7 +245,7 @@ export function startTUIHarness(options: TUIHarnessOptions = {}) {
228
245
  );
229
246
  }
230
247
  },
231
- { timeoutMs: ms },
248
+ { timeoutMs: ms, now, wait },
232
249
  );
233
250
  },
234
251
 
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Creates a listener on a kernel-chosen port of every given address on this
3
+ * host and closes each at once, and returns whether every listener could be
4
+ * created, in place of the throw a failed one raises. A host without an
5
+ * address, such as stock macOS for any loopback address past 127.0.0.1,
6
+ * refuses the listener with EADDRNOTAVAIL.
7
+ */
8
+ export function tryCreateListeners(hostnames: readonly string[]): boolean {
9
+ for (const hostname of hostnames) {
10
+ try {
11
+ const listener = Bun.listen({ hostname, port: 0, socket: { data() {} } });
12
+
13
+ listener.stop(true);
14
+ } catch {
15
+ return false;
16
+ }
17
+ }
18
+
19
+ return true;
20
+ }
@@ -1,20 +0,0 @@
1
- /**
2
- * Tries to bind a socket to every given address on this host, by listening
3
- * on a kernel-chosen port of each and closing it at once, and returns whether
4
- * every bind succeeded. A host
5
- * without an address, such as stock macOS for any loopback address past
6
- * 127.0.0.1, refuses the bind with EADDRNOTAVAIL.
7
- */
8
- export function tryBindAddresses(hostnames: readonly string[]): boolean {
9
- for (const hostname of hostnames) {
10
- try {
11
- const listener = Bun.listen({ hostname, port: 0, socket: { data() {} } });
12
-
13
- listener.stop(true);
14
- } catch {
15
- return false;
16
- }
17
- }
18
-
19
- return true;
20
- }