moshcode 0.89.0 → 0.91.0

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/src/dns.mjs CHANGED
@@ -2148,6 +2148,59 @@ export async function verifyResolution({
2148
2148
  return { ok: checks.every((c) => c.ok), checks };
2149
2149
  }
2150
2150
 
2151
+ /**
2152
+ * Wait for a bridge that systemd has just restarted to start answering.
2153
+ *
2154
+ * `startDaemon` cannot be asked this. It spawns, watches its own child, and
2155
+ * decides from a pidfile — none of which describes a unit that systemd owns and
2156
+ * has just cycled. Calling it here would either report the pre-restart pid as
2157
+ * "already running" or, on a pidfile not yet rewritten, spawn a second bridge
2158
+ * against the one systemd is bringing up.
2159
+ *
2160
+ * `Type=simple` reports active the moment the process forks, so systemd saying
2161
+ * the restart worked is not yet a resolver that answers. Hence the probe.
2162
+ *
2163
+ * A timeout is reported as started-but-unverified rather than as a failure, the
2164
+ * same way `startDaemon` treats a live process that has not answered yet: the
2165
+ * unit is active, and refusing this machine its DNS over a slow first registry
2166
+ * fetch would be the worse mistake.
2167
+ */
2168
+ export async function supervisedReady({
2169
+ host = DEFAULT_HOST,
2170
+ port,
2171
+ probe = probeResolver,
2172
+ status = daemonStatus,
2173
+ timeoutMs = READY_TIMEOUT_MS,
2174
+ sleep = (ms) => new Promise((r) => setTimeout(r, ms)),
2175
+ } = {}) {
2176
+ const deadline = Date.now() + timeoutMs;
2177
+ let answered = false;
2178
+ while (Date.now() < deadline) {
2179
+ if (await probe({ host, port }).catch(() => false)) {
2180
+ answered = true;
2181
+ break;
2182
+ }
2183
+ await sleep(150);
2184
+ }
2185
+ const current = await Promise.resolve(status()).catch(() => null);
2186
+ if (!answered) {
2187
+ // Reported as a failure, unlike `startDaemon`'s slow-but-alive case, and
2188
+ // for a reason that does not apply there: that one has watched its own
2189
+ // child and knows it is running. Nothing here has. `systemctl restart`
2190
+ // returns as soon as a Type=simple unit forks, so it returns 0 for a bridge
2191
+ // that forked and died — and the next thing this run does is point every
2192
+ // lookup on the machine at that port. Refusing is the safe direction.
2193
+ return {
2194
+ started: false,
2195
+ alreadyRunning: false,
2196
+ pid: current?.pid ?? null,
2197
+ supervised: true,
2198
+ error: `${UNIT_NAME} restarted but the bridge did not answer on ${host}:${port}`,
2199
+ };
2200
+ }
2201
+ return { started: true, pid: current?.pid ?? null, alreadyRunning: false, supervised: true, verified: true };
2202
+ }
2203
+
2151
2204
  const defaultReadMaybe = async (path) => {
2152
2205
  const { readFile: rf } = await import("node:fs/promises");
2153
2206
  return rf(path, "utf8").catch(() => null);
@@ -2407,10 +2460,10 @@ import { readFile, writeFile } from "node:fs/promises";
2407
2460
  import { existsSync } from "node:fs";
2408
2461
  import { fileURLToPath } from "node:url";
2409
2462
  import { isRealTld } from "./iana-tlds.mjs";
2410
- import { installService, removeService, serviceUnit, servicePaths, UNIT_NAME, ensureProxyService, removeProxyService, proxyServicePaths, proxyWrapperPath } from "./dns-service.mjs";
2463
+ import { installService, refreshService, removeService, serviceUnit, servicePaths, stopService, UNIT_NAME, ensureProxyService, removeProxyService, proxyServicePaths, proxyWrapperPath } from "./dns-service.mjs";
2411
2464
  import {
2412
2465
  applyPlan, daemonStatus, describePlan, detectPlatform, disablePlan, enablePlan,
2413
- probeResolver, requiredPort, startDaemon, stopDaemon,
2466
+ probeResolver, READY_TIMEOUT_MS, requiredPort, startDaemon, stopDaemon,
2414
2467
  } from "./dns-system.mjs";
2415
2468
  import { escalateSelf } from "./escalate.mjs";
2416
2469
 
@@ -2508,10 +2561,13 @@ export async function dnsCommand(args = [], out = console.log, deps = {}) {
2508
2561
  presenceImpl = bridgePresence,
2509
2562
  exists = existsSync,
2510
2563
  startBridge = startDaemon,
2564
+ refreshBridge = refreshService,
2565
+ bridgeReady = supervisedReady,
2511
2566
  proxyReachableImpl = proxyReachable,
2512
2567
  findLocalProxyImpl = findLocalProxy,
2513
2568
  autoTrustImpl = createAutoTrust,
2514
2569
  stopBridge = stopDaemon,
2570
+ stopSupervised = stopService,
2515
2571
  // The two proxy-service calls, injected for the same reason as every
2516
2572
  // other system call here: a test must be able to exercise the branch
2517
2573
  // without shelling out to systemctl or writing to /etc.
@@ -3224,6 +3280,19 @@ export async function dnsCommand(args = [], out = console.log, deps = {}) {
3224
3280
  return 1;
3225
3281
  }
3226
3282
 
3283
+ // The supervised bridge first, and by asking systemd rather than by
3284
+ // signalling a pid. `stopDaemon` kills what the pidfile names, and the
3285
+ // unit is `Restart=always` — so the process died, systemd replaced it
3286
+ // within the second, and this command reported "bridge stopped" on a
3287
+ // machine where the bridge was still up and answering, just no longer on
3288
+ // anything's path.
3289
+ const unsupervised = await stopSupervised();
3290
+ if (unsupervised.reason !== "no unit installed") {
3291
+ out(unsupervised.stopped
3292
+ ? ` ok ${UNIT_NAME} stopped and disabled`
3293
+ : ` -- could not stop ${UNIT_NAME} (${unsupervised.reason}) — it will restart itself`);
3294
+ }
3295
+
3227
3296
  const stopped = await stopBridge();
3228
3297
  out(stopped.stopped ? " ok bridge stopped" : ` ok bridge was not running${stopped.reason ? ` (${stopped.reason})` : ""}`);
3229
3298
 
@@ -3412,17 +3481,42 @@ export async function dnsCommand(args = [], out = console.log, deps = {}) {
3412
3481
  }
3413
3482
  }
3414
3483
 
3484
+ // A bridge under systemd is re-described, not started — and this is the step
3485
+ // whose absence left the last of this to be done by hand.
3486
+ //
3487
+ // `startBridge` below reports "already running" for a supervised bridge and
3488
+ // leaves it alone. That is correct, and it is also why proxy mode arrived a
3489
+ // reboot late: what a resolver answers with is fixed when it spawns, so a
3490
+ // bridge that came up before the proxy existed goes on answering origins no
3491
+ // matter what this run decides. Stopping it does not help either, since
3492
+ // `Restart=always` brings the same ExecStart back.
3493
+ //
3494
+ // So the unit is rewritten to match the run happening now, and restarted.
3495
+ // v4 by preference: `dns start --proxy` takes one address and probes both
3496
+ // families itself, so handing it the v4 loopback lets it find ::1 too
3497
+ // rather than pinning the answer to one family.
3498
+ const proxyArg = proxyAddress ? (proxyAddress.v4 || proxyAddress.v6) : null;
3499
+
3500
+ let refreshed = null;
3501
+ if (!reusing && platform === "linux") {
3502
+ refreshed = await refreshBridge({ entry: cliEntry(), port: wanted, registryBase, proxy: proxyArg });
3503
+ for (const step of refreshed.steps || []) {
3504
+ out(` ${step.ok ? "ok " : "-- "} ${step.step}${step.error ? ` — ${step.error}` : ""}`);
3505
+ }
3506
+ if (refreshed.refreshed) {
3507
+ const forwarding = refreshed.upstreams?.length ? `, forwarding the clearnet to ${refreshed.upstreams.join(", ")}` : "";
3508
+ out(` ok ${UNIT_NAME} restarted with proxy mode ${proxyArg ? "on" : "off"}${forwarding}`);
3509
+ } else if (refreshed.reason !== "no unit installed") {
3510
+ out(` -- could not update ${UNIT_NAME} (${refreshed.reason})`);
3511
+ out(" the bridge already running keeps the mode it started with");
3512
+ }
3513
+ }
3514
+
3415
3515
  const started = reusing
3416
3516
  ? { started: false, pid: reusing.pid, alreadyRunning: true, reused: true }
3417
- : await startBridge({
3418
- port: wanted,
3419
- registryBase,
3420
- entry: cliEntry(),
3421
- // v4 by preference: `dns start --proxy` takes one address and probes
3422
- // both families itself, so handing it the v4 loopback lets it find ::1
3423
- // too rather than pinning the answer to one family.
3424
- proxy: proxyAddress ? (proxyAddress.v4 || proxyAddress.v6) : null,
3425
- });
3517
+ : refreshed?.refreshed
3518
+ ? await bridgeReady({ host: DEFAULT_HOST, port: wanted })
3519
+ : await startBridge({ port: wanted, registryBase, entry: cliEntry(), proxy: proxyArg });
3426
3520
  // The routing this is about to install is catch-all — every lookup on the
3427
3521
  // machine, not just Moshpit ones — so a bridge that did not come up is not
3428
3522
  // a degraded feature, it is the machine's resolver pointed at nothing.
@@ -3504,7 +3598,13 @@ export async function dnsCommand(args = [], out = console.log, deps = {}) {
3504
3598
  out(`Moshpit names now resolve on this machine. Try: moshcode dns resolve ${moshpitProbe || "<name>"}`);
3505
3599
  out(`Routing covers the ${tlds.length} TLDs claimed right now. New ones do not route`);
3506
3600
  out("until you re-run this — there is no common suffix to match, so every TLD is listed.");
3507
- out("Note: the bridge does not yet survive a reboot. Re-run `moshcode dns enable` after one.");
3601
+ // Only where it is still true. On a supervised machine the unit was just
3602
+ // enabled and restarted, so the bridge does come back — and telling
3603
+ // someone to re-run a command they do not need is how advice stops being
3604
+ // read at all.
3605
+ out(started.supervised
3606
+ ? `Note: ${UNIT_NAME} brings the bridge back after a reboot.`
3607
+ : "Note: the bridge does not yet survive a reboot. Re-run `moshcode dns enable` after one.");
3508
3608
  return 0;
3509
3609
  }
3510
3610
 
@@ -3512,7 +3612,14 @@ export async function dnsCommand(args = [], out = console.log, deps = {}) {
3512
3612
  report(outcome.rolledBack.results);
3513
3613
  // Started by this run and no longer routed to, so leaving it would be a
3514
3614
  // process holding 5354 that the next enable's preflight refuses to run past.
3515
- if (started.started) {
3615
+ //
3616
+ // A supervised bridge is exempt: this run did not start it, only restarted
3617
+ // it, so it was holding that port before the run and is meant to go on
3618
+ // holding it. Signalling its pid would not stop it anyway — `Restart=always`
3619
+ // replaces it within the second — so the only thing the old line achieved
3620
+ // there was printing "remove bridge started by this run" about a bridge
3621
+ // that was neither started by this run nor removed.
3622
+ if (started.started && !started.supervised) {
3516
3623
  const stopped = await stopBridge();
3517
3624
  if (stopped.stopped) out(" ok remove bridge started by this run");
3518
3625
  }