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/README.md +91 -0
- package/bin/moshcode.mjs +7 -0
- package/package.json +1 -1
- package/prd/0013-persistent-ssh-workspaces.md +1181 -0
- package/prd/README.md +2 -0
- package/src/cli-schema.mjs +109 -0
- package/src/commands.mjs +164 -0
- package/src/dns-service.mjs +167 -10
- package/src/dns.mjs +120 -13
- package/src/ssh.mjs +1228 -0
- package/src/tui.mjs +14 -0
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
|
-
:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|