Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ anchor: function draft
created: 2026-10-16T18:32:55Z
norm: '1'
sig: 9775c8890db42443
body_hash: 715d3c6a84062b19
raw_hash: 4ac416bc7ff419f2
vouched: 2026-10-21T21:54:51Z
body_hash: 6918a46a3994c088
raw_hash: 8984d6500c60a112
vouched: 2026-10-22T21:42:37Z
lines: 133-155
---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ anchor: function serve
created: 2026-10-15T03:39:45Z
norm: '1'
sig: 171797818ef4d836
body_hash: 0f15dc30991bf467
raw_hash: aa609814e48a6d89
vouched: 2026-10-22T21:10:47Z
body_hash: f0a0ef2c205726cc
raw_hash: a98db292f7a9e13d
vouched: 2026-10-22T21:42:37Z
lines: 668-751
---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ anchor: function serve
created: 2026-10-15T10:17:10Z
norm: '1'
sig: 171797818ef4d836
body_hash: 0f15dc30991bf467
raw_hash: aa609814e48a6d89
vouched: 2026-10-22T21:10:47Z
body_hash: f0a0ef2c205726cc
raw_hash: a98db292f7a9e13d
vouched: 2026-10-22T21:42:37Z
lines: 830-948
supersedes: 01M4Z2R1HEVDKEA8VEJ7XRJQ8F
---
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ anchor: '@file'
created: 2026-10-20T17:04:56Z
norm: '1'
sig: 1acdc0f6a0895ac5
body_hash: 734539b04f3c9fb5
raw_hash: faceff1158ed3942
vouched: 2026-10-22T21:30:21Z
body_hash: 94af0091ce8ea05b
raw_hash: 43d4d53a6b1fecd9
vouched: 2026-10-22T21:42:37Z
lines: 1-1153
---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ anchor: '@file'
created: 2026-10-22T14:00:47Z
norm: '1'
sig: 1acdc0f6a0895ac5
body_hash: 734539b04f3c9fb5
raw_hash: faceff1158ed3942
vouched: 2026-10-22T21:30:21Z
body_hash: 94af0091ce8ea05b
raw_hash: 43d4d53a6b1fecd9
vouched: 2026-10-22T21:42:37Z
lines: 1-1309
---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ anchor: function serve
created: 2026-10-22T21:10:40Z
norm: '1'
sig: 171797818ef4d836
body_hash: 0f15dc30991bf467
raw_hash: aa609814e48a6d89
body_hash: f0a0ef2c205726cc
raw_hash: a98db292f7a9e13d
vouched: 2026-10-22T21:42:37Z
lines: 1191-1341
---

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
id: 01M5JS1HEWX8RK77BQVSA9XQZG
anchor: function serve
created: 2026-10-22T21:42:29Z
norm: '1'
sig: 171797818ef4d836
body_hash: f0a0ef2c205726cc
raw_hash: a98db292f7a9e13d
lines: 1200-1357
---

Ace Helper's tailnet self-restart exits non-zero because launchd's KeepAlive relaunches only a failed exit
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
id: 01M5JS1HGCGP3SEXKET1EFDJ87
anchor: function watch
created: 2026-10-22T21:42:29Z
norm: '1'
sig: 6c1e8049ae309bc1
body_hash: 8530c8fc8dec8e95
raw_hash: 03c20bc7ed33888a
lines: 113-125
---

Tailscale stopping must not restart the host; only a running tailnet other than the one the host started on does
11 changes: 6 additions & 5 deletions apps/host/src/catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,12 +199,13 @@ export function fork(source: Listing, model: ModelRef | undefined, branches: Set
/** A new channel's record, not yet saved: a hosted channel saves it once the service has it. */
export function draft(project: string, model?: ModelRef, name?: string, hosted?: string): Listing {
if (!tailnet) {
// The tailnet is read once at startup, so Tailscale started later needs a restart.
const host = config.helper ? "Ace Helper" : "ace serve";
// The tailnet is read once at startup; Ace Helper restarts itself when Tailscale starts later.
throw new Error(
identity().tailnet
? `Restart ${host}: it started before Tailscale was running.`
: "Start Tailscale first: every channel belongs to a tailnet.",
!identity().tailnet
? "Start Tailscale first: every channel belongs to a tailnet."
: config.helper
? "Ace Helper is restarting to use Tailscale. Try again in a moment."
: "Restart ace serve: it started before Tailscale was running.",
);
}
const names = taken();
Expand Down
13 changes: 10 additions & 3 deletions apps/host/src/gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ import {
projectId,
type WorkerRequest,
} from "./protocol";
import { type Machine, self, whois } from "./tailnet";
import { type Machine, self, watch as tailscale, whois } from "./tailnet";
import {
checkKey,
forgetKey,
Expand Down Expand Up @@ -1231,7 +1231,7 @@ export async function serve(port: number, loopback = false, script?: string): Pr
tick = now;
}, 1000);
cleanup.push(() => clearInterval(stalls));
async function stop() {
async function stop(code = 0) {
if (closing) return;
closing = true;
log("info", "host.stop", { pending: pending.size });
Expand All @@ -1244,10 +1244,17 @@ export async function serve(port: number, loopback = false, script?: string): Pr
log("error", "host.shutdown.failed", failure(result.reason));
}
}
process.exit(results.some((result) => result.status === "rejected") ? 1 : 0);
process.exit(results.some((result) => result.status === "rejected") ? 1 : code);
}
process.on("SIGTERM", () => void stop());
process.on("SIGINT", () => void stop());
// The owner's login, the channels' tailnet, and every tailnet listener are read once at startup.
cleanup.push(tailscale(catalog.tailnet, (tailnet) => {
log("info", "host.tailnet.moved", { from: catalog.tailnet, to: tailnet });
// launchd starts Ace Helper again after a failed exit, which picks up the new tailnet.
if (config.helper) void stop(75);
else console.log(`Tailscale is on ${tailnet} now. Restart ace serve to use it.`);
}));
console.log(`Ace on http://${server.hostname}:${server.port}`);
// Ace Helper's console is its log file, which must not hold the token.
if (!config.helper) {
Expand Down
18 changes: 18 additions & 0 deletions apps/host/src/tailnet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,24 @@ export async function diagnostics(started?: string): Promise<Diagnostics["tailsc
}
}

/**
* Calls `moved` once Tailscale has run on a tailnet other than `started` for two checks in a row.
* Tailscale stopping never counts: the host's channels stay on the tailnet it started on.
*/
export function watch(started: string | undefined, moved: (tailnet: string) => void): () => void {
let seen: string | undefined;
const timer = setInterval(async () => {
const now = (await status())?.MagicDNSSuffix;
if (!now || now === started) seen = undefined;
else if (now !== seen) seen = now;
else {
clearInterval(timer);
moved(now);
}
}, 10_000);
return () => clearInterval(timer);
}

/** Online machines owned by people; tagged nodes are servers, not participants. */
export async function peers(): Promise<Machine[]> {
const value = await status();
Expand Down