@intentic/machine 1.325.0 → 1.327.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 +387 -33
- package/dist/commands.d.ts.map +1 -1
- package/dist/commands.js +2 -0
- package/dist/commands.js.map +1 -1
- package/dist/daemon-base.d.ts.map +1 -1
- package/dist/daemon-base.js +2 -3
- package/dist/daemon-base.js.map +1 -1
- package/dist/device/audit.d.ts +2 -0
- package/dist/device/audit.d.ts.map +1 -1
- package/dist/device/audit.js +23 -3
- package/dist/device/audit.js.map +1 -1
- package/dist/device/commands.d.ts +4 -0
- package/dist/device/commands.d.ts.map +1 -1
- package/dist/device/commands.js +7 -2
- package/dist/device/commands.js.map +1 -1
- package/dist/device/connection.d.ts +12 -1
- package/dist/device/connection.d.ts.map +1 -1
- package/dist/device/connection.js +77 -3
- package/dist/device/connection.js.map +1 -1
- package/dist/device/grant.d.ts +52 -0
- package/dist/device/grant.d.ts.map +1 -1
- package/dist/device/loopback-catch.d.ts +1 -1
- package/dist/device/loopback-catch.d.ts.map +1 -1
- package/dist/device/loopback-catch.js +5 -30
- package/dist/device/loopback-catch.js.map +1 -1
- package/dist/device/mcp.d.ts.map +1 -1
- package/dist/device/mcp.js +190 -30
- package/dist/device/mcp.js.map +1 -1
- package/dist/device/router.d.ts +52 -0
- package/dist/device/router.d.ts.map +1 -1
- package/dist/device/router.js +21 -0
- package/dist/device/router.js.map +1 -1
- package/dist/device/sandbox-rounds/auto-backup.d.ts +3 -1
- package/dist/device/sandbox-rounds/auto-backup.d.ts.map +1 -1
- package/dist/device/sandbox-rounds/auto-backup.js +14 -5
- package/dist/device/sandbox-rounds/auto-backup.js.map +1 -1
- package/dist/device/sandbox-rounds/auto-prepare.d.ts +3 -1
- package/dist/device/sandbox-rounds/auto-prepare.d.ts.map +1 -1
- package/dist/device/sandbox-rounds/auto-prepare.js +3 -3
- package/dist/device/sandbox-rounds/auto-prepare.js.map +1 -1
- package/dist/device/sandbox-rounds/in-flight.d.ts.map +1 -0
- package/dist/device/sandbox-rounds/in-flight.js.map +1 -0
- package/dist/device/sandbox-rounds/keeper.d.ts +26 -4
- package/dist/device/sandbox-rounds/keeper.d.ts.map +1 -1
- package/dist/device/sandbox-rounds/keeper.js +129 -31
- package/dist/device/sandbox-rounds/keeper.js.map +1 -1
- package/dist/device/sandbox-rounds/probation-watch.d.ts.map +1 -1
- package/dist/device/sandbox-rounds/probation-watch.js.map +1 -1
- package/dist/device/tools/android-parse.d.ts +69 -0
- package/dist/device/tools/android-parse.d.ts.map +1 -0
- package/dist/device/tools/android-parse.js +391 -0
- package/dist/device/tools/android-parse.js.map +1 -0
- package/dist/device/tools/android.d.ts +71 -0
- package/dist/device/tools/android.d.ts.map +1 -0
- package/dist/device/tools/android.js +534 -0
- package/dist/device/tools/android.js.map +1 -0
- package/dist/device/tools/apps.d.ts +3 -3
- package/dist/device/tools/apps.d.ts.map +1 -1
- package/dist/device/tools/apps.js +16 -6
- package/dist/device/tools/apps.js.map +1 -1
- package/dist/device/tools/describe.d.ts.map +1 -1
- package/dist/device/tools/describe.js +10 -5
- package/dist/device/tools/describe.js.map +1 -1
- package/dist/device/tools/device.d.ts +9 -15
- package/dist/device/tools/device.d.ts.map +1 -1
- package/dist/device/tools/device.js +34 -58
- package/dist/device/tools/device.js.map +1 -1
- package/dist/device/tools/elements.d.ts +13 -0
- package/dist/device/tools/elements.d.ts.map +1 -0
- package/dist/device/tools/elements.js +84 -0
- package/dist/device/tools/elements.js.map +1 -0
- package/dist/device/tools/ic-binary.d.ts +1 -0
- package/dist/device/tools/ic-binary.d.ts.map +1 -1
- package/dist/device/tools/ic-binary.js +40 -17
- package/dist/device/tools/ic-binary.js.map +1 -1
- package/dist/device/tools/sandboxes.d.ts +1 -1
- package/dist/device/tools/sandboxes.d.ts.map +1 -1
- package/dist/device/tools/sandboxes.js +9 -8
- package/dist/device/tools/sandboxes.js.map +1 -1
- package/dist/device/tools/shell.js +1 -1
- package/dist/device/tools/shell.js.map +1 -1
- package/dist/device/tools/view.d.ts +4 -0
- package/dist/device/tools/view.d.ts.map +1 -0
- package/dist/device/tools/view.js +4 -0
- package/dist/device/tools/view.js.map +1 -0
- package/dist/environments/children.js +1 -1
- package/dist/environments/children.js.map +1 -1
- package/dist/environments/commands.js +1 -1
- package/dist/environments/commands.js.map +1 -1
- package/dist/environments/crossing.d.ts +1 -1
- package/dist/environments/crossing.d.ts.map +1 -1
- package/dist/environments/machine.js +1 -1
- package/dist/environments/machine.js.map +1 -1
- package/dist/environments/wsl.d.ts.map +1 -0
- package/dist/environments/wsl.js.map +1 -0
- package/dist/resident.d.ts.map +1 -1
- package/dist/resident.js +14 -8
- package/dist/resident.js.map +1 -1
- package/dist/status.d.ts.map +1 -1
- package/dist/status.js +16 -13
- package/dist/status.js.map +1 -1
- package/dist/sync/attach-commands.d.ts +39 -0
- package/dist/sync/attach-commands.d.ts.map +1 -0
- package/dist/sync/attach-commands.js +152 -0
- package/dist/sync/attach-commands.js.map +1 -0
- package/dist/sync/commands.d.ts +8 -4
- package/dist/sync/commands.d.ts.map +1 -1
- package/dist/sync/commands.js +137 -79
- package/dist/sync/commands.js.map +1 -1
- package/dist/sync/config.d.ts +43 -9
- package/dist/sync/config.d.ts.map +1 -1
- package/dist/sync/config.js +86 -41
- package/dist/sync/config.js.map +1 -1
- package/dist/sync/endpoint.d.ts +8 -1
- package/dist/sync/endpoint.d.ts.map +1 -1
- package/dist/sync/endpoint.js +41 -1
- package/dist/sync/endpoint.js.map +1 -1
- package/dist/sync/environment.d.ts +11 -0
- package/dist/sync/environment.d.ts.map +1 -0
- package/dist/sync/environment.js +48 -0
- package/dist/sync/environment.js.map +1 -0
- package/dist/sync/exec.d.ts +1 -0
- package/dist/sync/exec.d.ts.map +1 -1
- package/dist/sync/exec.js +4 -5
- package/dist/sync/exec.js.map +1 -1
- package/dist/sync/folders.d.ts +3 -2
- package/dist/sync/folders.d.ts.map +1 -1
- package/dist/sync/folders.js +55 -2
- package/dist/sync/folders.js.map +1 -1
- package/dist/sync/forget-command.d.ts +22 -0
- package/dist/sync/forget-command.d.ts.map +1 -0
- package/dist/sync/forget-command.js +95 -0
- package/dist/sync/forget-command.js.map +1 -0
- package/dist/sync/git-bridge.d.ts +6 -0
- package/dist/sync/git-bridge.d.ts.map +1 -1
- package/dist/sync/git-bridge.js +64 -1
- package/dist/sync/git-bridge.js.map +1 -1
- package/dist/sync/gone-watch.d.ts +18 -0
- package/dist/sync/gone-watch.d.ts.map +1 -0
- package/dist/sync/gone-watch.js +142 -0
- package/dist/sync/gone-watch.js.map +1 -0
- package/dist/sync/gone.d.ts +22 -0
- package/dist/sync/gone.d.ts.map +1 -0
- package/dist/sync/gone.js +60 -0
- package/dist/sync/gone.js.map +1 -0
- package/dist/sync/local-sandboxes.d.ts +6 -0
- package/dist/sync/local-sandboxes.d.ts.map +1 -0
- package/dist/sync/local-sandboxes.js +26 -0
- package/dist/sync/local-sandboxes.js.map +1 -0
- package/dist/sync/mirror.d.ts +7 -0
- package/dist/sync/mirror.d.ts.map +1 -1
- package/dist/sync/mirror.js +434 -111
- package/dist/sync/mirror.js.map +1 -1
- package/dist/sync/mutagen.d.ts +50 -18
- package/dist/sync/mutagen.d.ts.map +1 -1
- package/dist/sync/mutagen.js +294 -80
- package/dist/sync/mutagen.js.map +1 -1
- package/dist/sync/project/adopt.d.ts +18 -0
- package/dist/sync/project/adopt.d.ts.map +1 -0
- package/dist/sync/project/adopt.js +86 -0
- package/dist/sync/project/adopt.js.map +1 -0
- package/dist/sync/{project-commands.d.ts → project/project-commands.d.ts} +4 -1
- package/dist/sync/project/project-commands.d.ts.map +1 -0
- package/dist/sync/{project-commands.js → project/project-commands.js} +10 -10
- package/dist/sync/project/project-commands.js.map +1 -0
- package/dist/sync/project/project-delivery.d.ts +23 -0
- package/dist/sync/project/project-delivery.d.ts.map +1 -0
- package/dist/sync/project/project-delivery.js +315 -0
- package/dist/sync/project/project-delivery.js.map +1 -0
- package/dist/sync/{project-files.d.ts → project/project-files.d.ts} +1 -1
- package/dist/sync/project/project-files.d.ts.map +1 -0
- package/dist/sync/{project-files.js → project/project-files.js} +5 -2
- package/dist/sync/project/project-files.js.map +1 -0
- package/dist/sync/{project-local.d.ts → project/project-local.d.ts} +1 -1
- package/dist/sync/project/project-local.d.ts.map +1 -0
- package/dist/sync/{project-local.js → project/project-local.js} +1 -1
- package/dist/sync/project/project-local.js.map +1 -0
- package/dist/sync/{project-remote.d.ts → project/project-remote.d.ts} +1 -1
- package/dist/sync/project/project-remote.d.ts.map +1 -0
- package/dist/sync/{project-remote.js → project/project-remote.js} +1 -1
- package/dist/sync/project/project-remote.js.map +1 -0
- package/dist/sync/{project-transfer.d.ts → project/project-transfer.d.ts} +20 -6
- package/dist/sync/project/project-transfer.d.ts.map +1 -0
- package/dist/sync/{project-transfer.js → project/project-transfer.js} +26 -23
- package/dist/sync/project/project-transfer.js.map +1 -0
- package/dist/sync/report.d.ts.map +1 -1
- package/dist/sync/report.js +13 -7
- package/dist/sync/report.js.map +1 -1
- package/dist/sync/restore-points.d.ts +15 -11
- package/dist/sync/restore-points.d.ts.map +1 -1
- package/dist/sync/restore-points.js +25 -24
- package/dist/sync/restore-points.js.map +1 -1
- package/dist/sync/retire.d.ts +14 -0
- package/dist/sync/retire.d.ts.map +1 -0
- package/dist/sync/retire.js +66 -0
- package/dist/sync/retire.js.map +1 -0
- package/dist/sync/siblings.d.ts +27 -0
- package/dist/sync/siblings.d.ts.map +1 -0
- package/dist/sync/siblings.js +112 -0
- package/dist/sync/siblings.js.map +1 -0
- package/dist/sync/ssh.d.ts +1 -0
- package/dist/sync/ssh.d.ts.map +1 -1
- package/dist/sync/ssh.js +37 -23
- package/dist/sync/ssh.js.map +1 -1
- package/dist/sync/tunnel.d.ts +6 -2
- package/dist/sync/tunnel.d.ts.map +1 -1
- package/dist/sync/tunnel.js +45 -48
- package/dist/sync/tunnel.js.map +1 -1
- package/dist/upkeep/doctor.d.ts +5 -0
- package/dist/upkeep/doctor.d.ts.map +1 -0
- package/dist/upkeep/doctor.js +73 -0
- package/dist/upkeep/doctor.js.map +1 -0
- package/dist/upkeep/entry.d.ts +26 -0
- package/dist/upkeep/entry.d.ts.map +1 -0
- package/dist/upkeep/entry.js +2 -0
- package/dist/upkeep/entry.js.map +1 -0
- package/dist/upkeep/login.d.ts +15 -0
- package/dist/upkeep/login.d.ts.map +1 -0
- package/dist/upkeep/login.js +128 -0
- package/dist/upkeep/login.js.map +1 -0
- package/dist/upkeep/manifest.d.ts +3 -0
- package/dist/upkeep/manifest.d.ts.map +1 -0
- package/dist/upkeep/manifest.js +6 -0
- package/dist/upkeep/manifest.js.map +1 -0
- package/dist/upkeep/reconcile.d.ts +45 -0
- package/dist/upkeep/reconcile.d.ts.map +1 -0
- package/dist/upkeep/reconcile.js +159 -0
- package/dist/upkeep/reconcile.js.map +1 -0
- package/dist/upkeep/retention.d.ts +10 -0
- package/dist/upkeep/retention.d.ts.map +1 -0
- package/dist/upkeep/retention.js +71 -0
- package/dist/upkeep/retention.js.map +1 -0
- package/dist/upkeep/retired.d.ts +12 -0
- package/dist/upkeep/retired.d.ts.map +1 -0
- package/dist/upkeep/retired.js +125 -0
- package/dist/upkeep/retired.js.map +1 -0
- package/dist/upkeep/second-ic.d.ts +4 -0
- package/dist/upkeep/second-ic.d.ts.map +1 -0
- package/dist/upkeep/second-ic.js +36 -0
- package/dist/upkeep/second-ic.js.map +1 -0
- package/dist/upkeep/trash.d.ts +9 -0
- package/dist/upkeep/trash.d.ts.map +1 -0
- package/dist/upkeep/trash.js +77 -0
- package/dist/upkeep/trash.js.map +1 -0
- package/dist/watchdog.d.ts +13 -0
- package/dist/watchdog.d.ts.map +1 -0
- package/dist/watchdog.js +38 -0
- package/dist/watchdog.js.map +1 -0
- package/package.json +9 -9
- package/dist/autostart/legacy-autostart.d.ts +0 -4
- package/dist/autostart/legacy-autostart.d.ts.map +0 -1
- package/dist/autostart/legacy-autostart.js +0 -33
- package/dist/autostart/legacy-autostart.js.map +0 -1
- package/dist/device/tools/in-flight.d.ts.map +0 -1
- package/dist/device/tools/in-flight.js.map +0 -1
- package/dist/sync/project-commands.d.ts.map +0 -1
- package/dist/sync/project-commands.js.map +0 -1
- package/dist/sync/project-files.d.ts.map +0 -1
- package/dist/sync/project-files.js.map +0 -1
- package/dist/sync/project-local.d.ts.map +0 -1
- package/dist/sync/project-local.js.map +0 -1
- package/dist/sync/project-remote.d.ts.map +0 -1
- package/dist/sync/project-remote.js.map +0 -1
- package/dist/sync/project-transfer.d.ts.map +0 -1
- package/dist/sync/project-transfer.js.map +0 -1
- package/dist/wsl.d.ts.map +0 -1
- package/dist/wsl.js.map +0 -1
- /package/dist/device/{tools → sandbox-rounds}/in-flight.d.ts +0 -0
- /package/dist/device/{tools → sandbox-rounds}/in-flight.js +0 -0
- /package/dist/{wsl.d.ts → environments/wsl.d.ts} +0 -0
- /package/dist/{wsl.js → environments/wsl.js} +0 -0
package/dist/sync/mirror.js
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import net from "node:net";
|
|
3
|
-
import {
|
|
3
|
+
import { sleep } from "@intentic/base/async";
|
|
4
4
|
import { errorMessage } from "@intentic/base/errors";
|
|
5
5
|
import { plural } from "@intentic/base/format";
|
|
6
6
|
import { writeFileAtomic } from "@intentic/base/fs";
|
|
7
7
|
import { PortsListSchema } from "@intentic/sandbox-contract";
|
|
8
|
-
import { mirrorHeartbeatPath, readState,
|
|
8
|
+
import { isAttachedPairing, mirrorHeartbeatPath, pairingKey, pairingTransport, readState, removeSandboxPairings, setFileSyncAutoPaused, setFileSyncSwapPaused, setSandboxGone, setUnreachableSince, updateState, } from "./config.js";
|
|
9
9
|
import { pairingEndpoint } from "./endpoint.js";
|
|
10
|
-
import { createDaemonBases, dialedPairings } from "../daemon-base.js";
|
|
10
|
+
import { candidateBases, createDaemonBases, dialedPairings } from "../daemon-base.js";
|
|
11
|
+
import { answeredGone, goneStep, SandboxGoneError } from "./gone.js";
|
|
12
|
+
import { checkContainers, checkKeptHere, sandboxAnswered, sandboxSaidGone } from "./gone-watch.js";
|
|
13
|
+
import { retireSandbox } from "./retire.js";
|
|
11
14
|
import { readSwapRecords } from "../device/sandbox-rounds/swap-records.js";
|
|
12
15
|
import { publishedPortsReader } from "./docker-ports.js";
|
|
13
16
|
import { realBridgeExec, runGitBridge } from "./git-bridge.js";
|
|
14
|
-
import { ensureMutagen, ensureSyncSession, existingSyncSessions, forwardedPorts, forwardSessionName, healDerivedConflicts, mutagenForwardArgs, ourForwardSessions, pauseRunningSync, pauseUnreachableSync, retireOrphanSessions, resumeAutoPausedSync, resumeSwapPausedSync, runMutagenAsync, syncMode, syncSessionNames, } from "./mutagen.js";
|
|
17
|
+
import { ensureMutagen, ensureSyncSession, existingSyncSessions, forwardedPorts, forwardSessionName, healDerivedConflicts, MUTAGEN_CALL_TIMEOUT_MS, mutagenForwardArgs, ourForwardSessions, pauseRunningSync, pauseUnreachableSync, relabelBudget, retireOrphanSessions, resumeAutoPausedSync, resumeSwapPausedSync, runMutagenAsync, sayDaemonEventsTo, sessionLabels, syncMode, syncSessionNames, } from "./mutagen.js";
|
|
15
18
|
import { quieted } from "./repeats.js";
|
|
16
19
|
import { deviceReport, scopedReport } from "./report.js";
|
|
17
20
|
import { pairingSshConfig, writeManagedSshConfig } from "./ssh.js";
|
|
@@ -37,6 +40,9 @@ export const fetchWorkspacePorts = async (base, syncToken) => {
|
|
|
37
40
|
if (response.status === 401 || response.status === 403) {
|
|
38
41
|
throw new SyncAuthError("the sandbox rejected the sync token: click 'Enable desktop sync' in your browser and re-run setup to mint a fresh one.");
|
|
39
42
|
}
|
|
43
|
+
if (answeredGone(response)) {
|
|
44
|
+
throw new SandboxGoneError(`the platform says this sandbox no longer exists (${response.status}): ${await response.text()}`);
|
|
45
|
+
}
|
|
40
46
|
if (!response.ok) {
|
|
41
47
|
throw new Error(`reading the sandbox's ports failed (${response.status}): ${await response.text()}`);
|
|
42
48
|
}
|
|
@@ -48,12 +54,17 @@ const localPortFree = (port) => new Promise((resolvePort) => {
|
|
|
48
54
|
probe.listen(port, "127.0.0.1", () => probe.close(() => resolvePort(true)));
|
|
49
55
|
});
|
|
50
56
|
const mutagenExecutor = (mutagen, pairing, log) => ({
|
|
51
|
-
terminate: (port) => void spawnSync(mutagen, ["forward", "terminate", forwardSessionName(pairing.sandboxId, port)], {
|
|
57
|
+
terminate: (port) => void spawnSync(mutagen, ["forward", "terminate", forwardSessionName(pairing.sandboxId, port)], {
|
|
58
|
+
stdio: "ignore",
|
|
59
|
+
windowsHide: true,
|
|
60
|
+
timeout: MUTAGEN_CALL_TIMEOUT_MS,
|
|
61
|
+
}),
|
|
52
62
|
create: async (summary) => await runMutagenAsync(mutagen, mutagenForwardArgs({
|
|
53
63
|
name: forwardSessionName(pairing.sandboxId, summary.port),
|
|
54
64
|
port: summary.port,
|
|
55
65
|
remote: pairingEndpoint(pairing),
|
|
56
66
|
host: summary.host,
|
|
67
|
+
labels: sessionLabels(pairing.sandboxId, "forward"),
|
|
57
68
|
}), log),
|
|
58
69
|
isLocalPortFree: localPortFree,
|
|
59
70
|
});
|
|
@@ -141,8 +152,13 @@ export const strandedForwards = (held, mirrored) => {
|
|
|
141
152
|
return held.filter((port) => !kept.has(port));
|
|
142
153
|
};
|
|
143
154
|
const sweepStrandedForwards = (mutagen, sandboxId, mirrored, log) => {
|
|
144
|
-
|
|
145
|
-
|
|
155
|
+
const held = forwardedPorts(mutagen, sandboxId);
|
|
156
|
+
for (const port of held === undefined ? [] : strandedForwards(held, mirrored)) {
|
|
157
|
+
spawnSync(mutagen, ["forward", "terminate", forwardSessionName(sandboxId, port)], {
|
|
158
|
+
stdio: "ignore",
|
|
159
|
+
windowsHide: true,
|
|
160
|
+
timeout: MUTAGEN_CALL_TIMEOUT_MS,
|
|
161
|
+
});
|
|
146
162
|
log(` localhost:${port}: stopped (this device was still holding a forward for it)`);
|
|
147
163
|
}
|
|
148
164
|
};
|
|
@@ -169,7 +185,13 @@ export const skippedPortsOf = (desired, mirrored, claimedBy, ignored) => {
|
|
|
169
185
|
.map((summary) => {
|
|
170
186
|
const heldBy = claimedBy.get(summary.port);
|
|
171
187
|
const reason = ignored.has(summary.port) ? "ignored" : heldBy === undefined ? "busy" : "held-by-sandbox";
|
|
172
|
-
return {
|
|
188
|
+
return {
|
|
189
|
+
port: summary.port,
|
|
190
|
+
host: summary.host,
|
|
191
|
+
reason,
|
|
192
|
+
heldBy: reason === "held-by-sandbox" ? heldBy : undefined,
|
|
193
|
+
command: summary.command,
|
|
194
|
+
};
|
|
173
195
|
});
|
|
174
196
|
};
|
|
175
197
|
export const recordedForwards = (pairings) => new Map(pairings.flatMap((held) => (held.mirroredPorts ?? []).map((port) => [port.port, held.sandboxId])));
|
|
@@ -185,8 +207,8 @@ export const handOver = (holding, sandboxId, mirrored) => {
|
|
|
185
207
|
};
|
|
186
208
|
export const othersHolding = (holding, sandboxId) => new Map([...holding].filter(([, holder]) => holder !== sandboxId));
|
|
187
209
|
const beat = async () => await writeFileAtomic(mirrorHeartbeatPath, String(Date.now())).catch(() => { });
|
|
188
|
-
const savePorts = async (
|
|
189
|
-
pairings: state.pairings.map((held) => (held
|
|
210
|
+
const savePorts = async (key, mirroredPorts, skippedPorts) => await updateState((state) => ({
|
|
211
|
+
pairings: state.pairings.map((held) => (pairingKey(held) === key ? { ...held, mirroredPorts, skippedPorts } : held)),
|
|
190
212
|
}));
|
|
191
213
|
const retireSwitchedOff = async (mutagen, pairing, log) => {
|
|
192
214
|
const mirrored = pairing.mirroredPorts ?? [];
|
|
@@ -206,6 +228,9 @@ const servePairing = async (context, pairing, base) => {
|
|
|
206
228
|
const { mutagen, claimedBy, holding, holders, say: log } = context;
|
|
207
229
|
const baseline = pairing.mirroredPorts ?? [];
|
|
208
230
|
const ports = pairing.syncToken === undefined ? [] : await fetchWorkspacePorts(base, pairing.syncToken);
|
|
231
|
+
if (isAttachedPairing(pairing)) {
|
|
232
|
+
return [];
|
|
233
|
+
}
|
|
209
234
|
if (pairing.mirrorOff === true) {
|
|
210
235
|
await retireSwitchedOff(mutagen, pairing, log);
|
|
211
236
|
return [];
|
|
@@ -215,28 +240,45 @@ const servePairing = async (context, pairing, base) => {
|
|
|
215
240
|
const next = await reconcileForwards(mutagenExecutor(mutagen, pairing, log), baseline, ports, claimedBy, ignored, holds, log);
|
|
216
241
|
const skipped = skippedPortsOf(ports, next, othersHolding(holding, pairing.sandboxId), ignored);
|
|
217
242
|
if (!sameMirrorSet(baseline, next) || !sameSkippedSet(pairing.skippedPorts ?? [], skipped)) {
|
|
218
|
-
await savePorts(pairing
|
|
243
|
+
await savePorts(pairingKey(pairing), next, skipped);
|
|
219
244
|
}
|
|
220
245
|
return next;
|
|
221
246
|
};
|
|
222
|
-
const
|
|
223
|
-
const
|
|
247
|
+
export const reportCarriers = (dialed, unsupported) => {
|
|
248
|
+
const seen = new Set();
|
|
249
|
+
return dialed.filter(({ pairing }) => {
|
|
250
|
+
if (pairing.syncToken === undefined || pairing.goneSince !== undefined || unsupported.has(pairing.sandboxId) || seen.has(pairing.sandboxId)) {
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
seen.add(pairing.sandboxId);
|
|
254
|
+
return true;
|
|
255
|
+
});
|
|
256
|
+
};
|
|
257
|
+
const sendReport = async (base, pairing, report) => await fetch(`${base.replace(/\/$/, "")}/system/sync/report`, {
|
|
258
|
+
method: "POST",
|
|
259
|
+
headers: { "content-type": "application/json", "x-intentic-sync": pairing.syncToken ?? "" },
|
|
260
|
+
body: JSON.stringify(scopedReport(report, pairing.sandboxId)),
|
|
261
|
+
signal: AbortSignal.timeout(REPORT_TIMEOUT_MS),
|
|
262
|
+
});
|
|
263
|
+
const postReports = async (dialed, mutagen, unsupported, gone, log) => {
|
|
264
|
+
const reportable = reportCarriers(dialed, unsupported);
|
|
224
265
|
if (reportable.length === 0) {
|
|
225
266
|
return;
|
|
226
267
|
}
|
|
227
268
|
const report = await deviceReport(mutagen);
|
|
228
269
|
for (const { pairing, base } of reportable) {
|
|
229
270
|
try {
|
|
230
|
-
const response = await
|
|
231
|
-
method: "POST",
|
|
232
|
-
headers: { "content-type": "application/json", "x-intentic-sync": pairing.syncToken ?? "" },
|
|
233
|
-
body: JSON.stringify(scopedReport(report, pairing.sandboxId)),
|
|
234
|
-
signal: AbortSignal.timeout(REPORT_TIMEOUT_MS),
|
|
235
|
-
});
|
|
271
|
+
const response = await sendReport(base, pairing, report);
|
|
236
272
|
if (response.status === 404) {
|
|
237
273
|
unsupported.add(pairing.sandboxId);
|
|
238
274
|
log(` ${pairing.sandboxId}: this sandbox is running a daemon without machine reports, its Devices view will stay empty.`);
|
|
239
275
|
}
|
|
276
|
+
else if (answeredGone(response)) {
|
|
277
|
+
await gone(pairing);
|
|
278
|
+
}
|
|
279
|
+
else if (!response.ok) {
|
|
280
|
+
log(` ${pairing.sandboxId}: report not taken (${response.status})`);
|
|
281
|
+
}
|
|
240
282
|
}
|
|
241
283
|
catch (error) {
|
|
242
284
|
log(` ${pairing.sandboxId}: report skipped: ${errorMessage(error)}`);
|
|
@@ -244,9 +286,10 @@ const postReports = async (dialed, mutagen, unsupported, log) => {
|
|
|
244
286
|
}
|
|
245
287
|
};
|
|
246
288
|
const dropRevokedPairing = async (mutagen, sandboxId, log) => {
|
|
247
|
-
await
|
|
289
|
+
await removeSandboxPairings(sandboxId);
|
|
248
290
|
retireOrphanSessions(mutagen, (await readState()).pairings, log);
|
|
249
291
|
};
|
|
292
|
+
const keyOfSandbox = (key, sandboxId) => key === sandboxId || key.startsWith(`${sandboxId}~`);
|
|
250
293
|
const guard = async (log, what, step) => {
|
|
251
294
|
try {
|
|
252
295
|
await step();
|
|
@@ -268,27 +311,39 @@ const absorbRejectedPoll = async (mutagen, pairing, rejectedPolls, repos, sessio
|
|
|
268
311
|
log(`${pairing.sandboxId} rejected the sync token ${REVOKED_POLLS} polls in a row: this machine's enrollment was revoked.`);
|
|
269
312
|
rejectedPolls.delete(pairing.sandboxId);
|
|
270
313
|
repos.delete(pairing.sandboxId);
|
|
271
|
-
sessionsPending.
|
|
314
|
+
for (const key of [...sessionsPending].filter((held) => keyOfSandbox(held, pairing.sandboxId))) {
|
|
315
|
+
sessionsPending.delete(key);
|
|
316
|
+
}
|
|
272
317
|
await dropRevokedPairing(mutagen, pairing.sandboxId, log);
|
|
273
318
|
return true;
|
|
274
319
|
};
|
|
275
320
|
const absorbUnreachablePoll = async (mutagen, pairing, unreachable, log) => {
|
|
276
321
|
const now = Date.now();
|
|
277
|
-
const since = unreachable.get(pairing.sandboxId)?.since ?? now;
|
|
322
|
+
const since = unreachable.get(pairing.sandboxId)?.since ?? pairing.unreachableSince ?? now;
|
|
278
323
|
unreachable.set(pairing.sandboxId, { since, lastTried: now });
|
|
324
|
+
if (pairing.unreachableSince === undefined) {
|
|
325
|
+
await setUnreachableSince(pairing.sandboxId, since);
|
|
326
|
+
}
|
|
279
327
|
const mirrored = (pairing.mirroredPorts ?? []).length;
|
|
280
328
|
if (shouldReleaseForwards(now - since, mirrored)) {
|
|
281
329
|
await retirePairingMirror(mutagen, pairing.sandboxId);
|
|
282
330
|
log(` ${pairing.sandboxId}: unreachable for ${RELEASE_FORWARDS_MS / 60_000} minutes; took its ${plural(mirrored, "port")} off localhost. They come back when it answers again.`);
|
|
283
331
|
}
|
|
284
|
-
|
|
332
|
+
return await pauseIfLongUnreachable(mutagen, pairing, now - since, log);
|
|
333
|
+
};
|
|
334
|
+
const pauseIfLongUnreachable = async (mutagen, pairing, unreachableForMs, log) => {
|
|
335
|
+
if (pairing.fileSyncAutoPaused === true || !shouldAutoPauseFileSync(unreachableForMs) || !pauseUnreachableSync(mutagen, pairing)) {
|
|
285
336
|
return false;
|
|
286
337
|
}
|
|
287
|
-
await setFileSyncAutoPaused(pairing
|
|
288
|
-
log(` ${pairing
|
|
338
|
+
await setFileSyncAutoPaused(pairingKey(pairing), true);
|
|
339
|
+
log(` ${pairingKey(pairing)}: unreachable for one hour; paused its Mutagen sessions to stop permanent reconnect/scanning load. They resume automatically when it returns.`);
|
|
289
340
|
return true;
|
|
290
341
|
};
|
|
291
|
-
const absorbPairingFailure = async (error, mutagen, pairing, tracking, log) => {
|
|
342
|
+
const absorbPairingFailure = async (error, mutagen, pairing, tracking, fate, log) => {
|
|
343
|
+
if (error instanceof SandboxGoneError) {
|
|
344
|
+
await sandboxSaidGone(fate, pairing.sandboxId, "edge");
|
|
345
|
+
return { drop: true, paused: true };
|
|
346
|
+
}
|
|
292
347
|
if (error instanceof SyncAuthError) {
|
|
293
348
|
const drop = await absorbRejectedPoll(mutagen, pairing, tracking.rejectedPolls, tracking.repos, tracking.sessionsPending, log);
|
|
294
349
|
return { drop, paused: false };
|
|
@@ -299,42 +354,97 @@ const healPairing = async (mutagen, pairing, tick, log) => {
|
|
|
299
354
|
if (tick % HEAL_EVERY_TICKS !== 0) {
|
|
300
355
|
return;
|
|
301
356
|
}
|
|
302
|
-
await guard(log, `${pairing
|
|
357
|
+
await guard(log, `${pairingKey(pairing)}: clearing derived residue`, async () => void (await healDerivedConflicts(mutagen, pairing, log)));
|
|
303
358
|
};
|
|
304
|
-
const setupOf = (pairing) => [pairing
|
|
359
|
+
const setupOf = (pairing) => [pairingKey(pairing), pairing.localDir ?? "", pairing.syncToken ?? "", syncMode(pairing), pairingTransport(pairing)].join("\n");
|
|
305
360
|
export const unpreparedSetups = (pairings, prepared) => pairings.filter((held) => !prepared.has(setupOf(held)));
|
|
306
361
|
export const markPrepared = (prepared, pairing) => void prepared.add(setupOf(pairing));
|
|
307
|
-
const prepareSession = async (mutagen, pairing, pending, say) => {
|
|
362
|
+
const prepareSession = async (mutagen, pairing, pending, relabels, say) => {
|
|
308
363
|
let converged = false;
|
|
309
|
-
await guard(say, `${pairing
|
|
310
|
-
converged = await ensureSyncSession(mutagen, pairing, say);
|
|
364
|
+
await guard(say, `${pairingKey(pairing)}: preparing its file sync`, async () => {
|
|
365
|
+
converged = await ensureSyncSession(mutagen, pairing, say, relabels);
|
|
311
366
|
});
|
|
312
367
|
if (converged) {
|
|
313
|
-
pending.delete(pairing
|
|
368
|
+
pending.delete(pairingKey(pairing));
|
|
314
369
|
}
|
|
315
370
|
else {
|
|
316
|
-
pending.add(pairing
|
|
371
|
+
pending.add(pairingKey(pairing));
|
|
317
372
|
}
|
|
318
373
|
return converged;
|
|
319
374
|
};
|
|
320
|
-
const dormant = (mutagen, pairing) => pairing.
|
|
321
|
-
|
|
322
|
-
|
|
375
|
+
const dormant = (mutagen, pairing) => pairing.goneSince !== undefined ||
|
|
376
|
+
pairing.fileSyncPausedFor !== undefined ||
|
|
377
|
+
(pairing.fileSyncAutoPaused === true && (existingSyncSessions(mutagen, syncSessionNames(pairing)) ?? [undefined]).length > 0);
|
|
378
|
+
export const readyToPrepare = async (setups, firstCopyAhead, announce) => {
|
|
379
|
+
const told = new Map();
|
|
380
|
+
const ready = [];
|
|
381
|
+
for (const pairing of setups) {
|
|
382
|
+
if (isAttachedPairing(pairing) && pairing.mode === "sync" && pairing.localDir !== undefined && firstCopyAhead(pairing)) {
|
|
383
|
+
const said = told.get(pairing.sandboxId) ?? (await announce(pairing));
|
|
384
|
+
told.set(pairing.sandboxId, said);
|
|
385
|
+
if (!said) {
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
ready.push(pairing);
|
|
390
|
+
}
|
|
391
|
+
return ready;
|
|
392
|
+
};
|
|
393
|
+
const announcer = (mutagen, bases, unsupported, unreachable, gone, say) => async (pairing) => {
|
|
394
|
+
if (pairing.goneSince !== undefined) {
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
397
|
+
if (pairing.syncToken === undefined || unsupported.has(pairing.sandboxId)) {
|
|
398
|
+
return true;
|
|
399
|
+
}
|
|
400
|
+
if (!pollDue(unreachable.get(pairing.sandboxId), Date.now())) {
|
|
401
|
+
return false;
|
|
402
|
+
}
|
|
403
|
+
try {
|
|
404
|
+
const response = await sendReport(await bases.resolve(pairing), pairing, await deviceReport(mutagen));
|
|
405
|
+
if (response.status === 404) {
|
|
406
|
+
unsupported.add(pairing.sandboxId);
|
|
407
|
+
say(` ${pairing.sandboxId}: this sandbox is running a daemon without machine reports, its Devices view will stay empty.`);
|
|
408
|
+
return true;
|
|
409
|
+
}
|
|
410
|
+
if (answeredGone(response)) {
|
|
411
|
+
await gone(pairing);
|
|
412
|
+
return false;
|
|
413
|
+
}
|
|
414
|
+
if (!response.ok) {
|
|
415
|
+
say(` ${pairingKey(pairing)}: ${pairing.sandboxId} did not take the report naming this folder (${response.status}); its first copy waits for it.`);
|
|
416
|
+
}
|
|
417
|
+
return response.ok;
|
|
418
|
+
}
|
|
419
|
+
catch (error) {
|
|
420
|
+
say(` ${pairingKey(pairing)}: ${pairing.sandboxId} could not be told of this folder (${errorMessage(error)}); its first copy waits until it can.`);
|
|
421
|
+
return false;
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
const prepareSessions = async (mutagen, pairings, prepared, pending, announce, passes, say) => {
|
|
425
|
+
const firstCopyAhead = (pairing) => (existingSyncSessions(mutagen, syncSessionNames(pairing)) ?? []).length === 0;
|
|
426
|
+
const unprepared = unpreparedSetups(pairings, prepared);
|
|
427
|
+
const docker = unprepared.filter((pairing) => pairingTransport(pairing) === "docker");
|
|
428
|
+
if (docker.length > 0) {
|
|
429
|
+
await guard(say, "asking after the containers docker pairings reach", async () => await checkContainers(passes.fate, docker, answeringOf(passes.tracking)));
|
|
430
|
+
}
|
|
431
|
+
for (const pairing of await readyToPrepare(unprepared, firstCopyAhead, announce)) {
|
|
323
432
|
markPrepared(prepared, pairing);
|
|
324
433
|
if (dormant(mutagen, pairing)) {
|
|
325
|
-
pending.add(pairing
|
|
434
|
+
pending.add(pairingKey(pairing));
|
|
326
435
|
continue;
|
|
327
436
|
}
|
|
328
|
-
await prepareSession(mutagen, pairing, pending, say);
|
|
437
|
+
await prepareSession(mutagen, pairing, pending, passes.relabels, say);
|
|
329
438
|
}
|
|
330
439
|
};
|
|
331
|
-
const
|
|
440
|
+
const answeringOf = (tracking) => (sandboxId) => !tracking.unreachable.has(sandboxId);
|
|
441
|
+
const retryPendingSessions = async (mutagen, pairings, sessionsPending, tick, relabels, say) => {
|
|
332
442
|
if (sessionsPending.size === 0 || tick % SESSION_RETRY_EVERY_TICKS !== 0) {
|
|
333
443
|
return;
|
|
334
444
|
}
|
|
335
|
-
for (const pairing of pairings.filter((held) => sessionsPending.has(held
|
|
336
|
-
if (await prepareSession(mutagen, pairing, sessionsPending, say)) {
|
|
337
|
-
say(` ${pairing
|
|
445
|
+
for (const pairing of pairings.filter((held) => sessionsPending.has(pairingKey(held)) && !dormant(mutagen, held))) {
|
|
446
|
+
if (await prepareSession(mutagen, pairing, sessionsPending, relabels, say)) {
|
|
447
|
+
say(` ${pairingKey(pairing)}: file sync is running on this build's rules`);
|
|
338
448
|
}
|
|
339
449
|
}
|
|
340
450
|
};
|
|
@@ -350,19 +460,19 @@ const holdSyncDuringSwaps = async (mutagen, pairings, unreachable, say) => {
|
|
|
350
460
|
}
|
|
351
461
|
const step = swapPauseStep(pairing, holds);
|
|
352
462
|
if (step === "pause" && pauseRunningSync(mutagen, pairing)) {
|
|
353
|
-
await setFileSyncSwapPaused(pairing
|
|
354
|
-
say(` ${pairing
|
|
463
|
+
await setFileSyncSwapPaused(pairingKey(pairing), true);
|
|
464
|
+
say(` ${pairingKey(pairing)}: its sandbox is being swapped on this machine; file sync is paused until the new version has held.`);
|
|
355
465
|
}
|
|
356
466
|
else if (step === "resume") {
|
|
357
467
|
resumeSwapPausedSync(mutagen, pairing);
|
|
358
|
-
await setFileSyncSwapPaused(pairing
|
|
359
|
-
say(` ${pairing
|
|
468
|
+
await setFileSyncSwapPaused(pairingKey(pairing), false);
|
|
469
|
+
say(` ${pairingKey(pairing)}: its sandbox's swap is settled; file sync resumed.`);
|
|
360
470
|
}
|
|
361
471
|
}
|
|
362
472
|
return held;
|
|
363
473
|
};
|
|
364
474
|
const runPairingPass = async (context, pairing, base) => {
|
|
365
|
-
const { mutagen, tick, claimedBy, holding, tracking, bases, swapHeld, say } = context;
|
|
475
|
+
const { mutagen, tick, claimedBy, holding, tracking, bases, swapHeld, fate, say } = context;
|
|
366
476
|
const { rejectedPolls, unreachable, repos } = tracking;
|
|
367
477
|
const swapping = swapHeld.has(pairing.sandboxId);
|
|
368
478
|
let pausedThisPass = false;
|
|
@@ -370,28 +480,36 @@ const runPairingPass = async (context, pairing, base) => {
|
|
|
370
480
|
const mirrored = await servePairing(context, pairing, base);
|
|
371
481
|
rejectedPolls.delete(pairing.sandboxId);
|
|
372
482
|
unreachable.delete(pairing.sandboxId);
|
|
373
|
-
if (
|
|
374
|
-
await
|
|
375
|
-
say(` ${pairing.sandboxId}: reachable again; resumed its automatically paused file sync`);
|
|
483
|
+
if (context.pairings.some((held) => held.sandboxId === pairing.sandboxId && held.goneSince !== undefined)) {
|
|
484
|
+
await sandboxAnswered(fate, pairing.sandboxId);
|
|
376
485
|
}
|
|
377
|
-
|
|
378
|
-
|
|
486
|
+
if (context.pairings.some((held) => held.sandboxId === pairing.sandboxId && held.unreachableSince !== undefined)) {
|
|
487
|
+
await setUnreachableSince(pairing.sandboxId, undefined);
|
|
379
488
|
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
489
|
+
if (!swapping && resumeAutoPausedSync(mutagen, pairing)) {
|
|
490
|
+
await setFileSyncAutoPaused(pairingKey(pairing), false);
|
|
491
|
+
say(` ${pairingKey(pairing)}: reachable again; resumed its automatically paused file sync`);
|
|
492
|
+
}
|
|
493
|
+
if (!isAttachedPairing(pairing)) {
|
|
494
|
+
for (const port of mirrored) {
|
|
495
|
+
claimedBy.set(port.port, pairing.sandboxId);
|
|
496
|
+
}
|
|
497
|
+
handOver(holding, pairing.sandboxId, mirrored);
|
|
498
|
+
if (tick % STRANDED_SWEEP_EVERY_TICKS === 0) {
|
|
499
|
+
sweepStrandedForwards(mutagen, pairing.sandboxId, mirrored, say);
|
|
500
|
+
}
|
|
383
501
|
}
|
|
384
502
|
}
|
|
385
503
|
catch (error) {
|
|
386
504
|
bases.failed(pairing.sandboxId);
|
|
387
|
-
const outcome = await absorbPairingFailure(error, mutagen, pairing, tracking, say);
|
|
505
|
+
const outcome = await absorbPairingFailure(error, mutagen, pairing, tracking, fate, say);
|
|
388
506
|
if (outcome.drop) {
|
|
389
507
|
return;
|
|
390
508
|
}
|
|
391
509
|
pausedThisPass = outcome.paused;
|
|
392
|
-
say(` ${pairing
|
|
510
|
+
say(` ${pairingKey(pairing)}: reconcile skipped: ${errorMessage(error)}`);
|
|
393
511
|
}
|
|
394
|
-
if (pairing.fileSyncAutoPaused === true || pausedThisPass || swapping) {
|
|
512
|
+
if (pairing.fileSyncAutoPaused === true || pairing.fileSyncPausedFor !== undefined || pausedThisPass || swapping) {
|
|
395
513
|
return;
|
|
396
514
|
}
|
|
397
515
|
await healPairing(mutagen, pairing, tick, say);
|
|
@@ -409,83 +527,288 @@ const runPairingPass = async (context, pairing, base) => {
|
|
|
409
527
|
say(` ${pairing.sandboxId}: git bridge skipped: ${errorMessage(error)}`);
|
|
410
528
|
}
|
|
411
529
|
};
|
|
412
|
-
export const
|
|
413
|
-
const
|
|
414
|
-
const
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
530
|
+
export const pollingPairings = (pairings) => {
|
|
531
|
+
const polling = new Set();
|
|
532
|
+
for (const sandboxId of new Set(pairings.map((pairing) => pairing.sandboxId))) {
|
|
533
|
+
const own = pairings.filter((pairing) => pairing.sandboxId === sandboxId);
|
|
534
|
+
const poller = own.find((pairing) => !isAttachedPairing(pairing)) ?? own[0];
|
|
535
|
+
if (poller !== undefined) {
|
|
536
|
+
polling.add(pairingKey(poller));
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
return polling;
|
|
540
|
+
};
|
|
541
|
+
const followSandbox = async (context, pairing) => {
|
|
542
|
+
const { mutagen, tick, tracking, swapHeld, say } = context;
|
|
543
|
+
if (pairing.goneSince !== undefined || pairing.fileSyncPausedFor !== undefined) {
|
|
418
544
|
return;
|
|
419
545
|
}
|
|
420
|
-
|
|
421
|
-
const
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
546
|
+
const down = tracking.unreachable.get(pairing.sandboxId)?.since ?? pairing.unreachableSince;
|
|
547
|
+
const swapping = swapHeld.has(pairing.sandboxId);
|
|
548
|
+
if (down === undefined) {
|
|
549
|
+
if (!swapping && resumeAutoPausedSync(mutagen, pairing)) {
|
|
550
|
+
await setFileSyncAutoPaused(pairingKey(pairing), false);
|
|
551
|
+
say(` ${pairingKey(pairing)}: reachable again; resumed its automatically paused file sync`);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
else if (await pauseIfLongUnreachable(mutagen, pairing, Date.now() - down, say)) {
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
if (pairing.fileSyncAutoPaused === true || swapping) {
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
await healPairing(mutagen, pairing, tick, say);
|
|
561
|
+
};
|
|
562
|
+
const passGone = async (context, pairing) => {
|
|
563
|
+
const now = Date.now();
|
|
564
|
+
const step = goneStep(pairing, now);
|
|
565
|
+
if (step === undefined) {
|
|
566
|
+
return "pass";
|
|
567
|
+
}
|
|
568
|
+
if (step === "recheck") {
|
|
569
|
+
await setSandboxGone(pairing.sandboxId, { since: pairing.goneSince ?? now, checkedAt: now, by: pairing.goneBy ?? "edge" });
|
|
570
|
+
return "ask";
|
|
571
|
+
}
|
|
572
|
+
if (step === "retire") {
|
|
573
|
+
const { mutagen, tracking, say } = context;
|
|
574
|
+
await guard(say, `${pairing.sandboxId}: retiring its pairing`, async () => {
|
|
575
|
+
await retireSandbox(pairing.sandboxId, say, `gone since ${new Date(pairing.goneSince ?? Date.now()).toISOString().slice(0, 10)}`, {
|
|
576
|
+
mutagen,
|
|
577
|
+
});
|
|
439
578
|
});
|
|
440
|
-
|
|
441
|
-
|
|
579
|
+
tracking.unreachable.delete(pairing.sandboxId);
|
|
580
|
+
tracking.rejectedPolls.delete(pairing.sandboxId);
|
|
581
|
+
tracking.repos.delete(pairing.sandboxId);
|
|
582
|
+
for (const key of [...tracking.sessionsPending].filter((held) => keyOfSandbox(held, pairing.sandboxId))) {
|
|
583
|
+
tracking.sessionsPending.delete(key);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
return "skip";
|
|
587
|
+
};
|
|
588
|
+
const runPasses = async (context, dialed, pairings, live) => {
|
|
589
|
+
const polling = pollingPairings(pairings);
|
|
590
|
+
for (const { pairing, base } of dialed.filter((held) => polling.has(pairingKey(held.pairing)))) {
|
|
591
|
+
if (!live()) {
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
const gone = await passGone(context, pairing);
|
|
595
|
+
if (gone === "skip") {
|
|
596
|
+
continue;
|
|
597
|
+
}
|
|
598
|
+
if (gone === "pass" && !pollDue(context.tracking.unreachable.get(pairing.sandboxId), Date.now())) {
|
|
442
599
|
continue;
|
|
443
600
|
}
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
601
|
+
await runPairingPass(context, pairing, base);
|
|
602
|
+
context.progress(`${pairingKey(pairing)}'s pass`);
|
|
603
|
+
}
|
|
604
|
+
for (const { pairing } of dialed.filter((held) => !polling.has(pairingKey(held.pairing)))) {
|
|
605
|
+
if (!live()) {
|
|
447
606
|
return;
|
|
448
607
|
}
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
608
|
+
await guard(context.say, `${pairingKey(pairing)}: following its sandbox`, async () => await followSandbox(context, pairing));
|
|
609
|
+
}
|
|
610
|
+
};
|
|
611
|
+
const ORPHAN_SWEEP_EVERY_TICKS = 12;
|
|
612
|
+
const CONTAINER_CHECK_EVERY_TICKS = 12;
|
|
613
|
+
const KEPT_HERE_CHECK_EVERY_TICKS = 120;
|
|
614
|
+
const reachedLocally = (dialed) => new Set(dialed.flatMap(({ pairing, base }) => {
|
|
615
|
+
const candidates = candidateBases(pairing.sandboxUrl);
|
|
616
|
+
return candidates.length > 1 && candidates[0] === base ? [pairing.sandboxId] : [];
|
|
617
|
+
}));
|
|
618
|
+
const standingSweeps = async (context, dialed) => {
|
|
619
|
+
const { mutagen, tick, tracking, fate, say } = context;
|
|
620
|
+
if (tick > 0 && tick % ORPHAN_SWEEP_EVERY_TICKS === 0) {
|
|
621
|
+
await guard(say, "retiring orphaned sessions", async () => retireOrphanSessions(mutagen, (await readState()).pairings, say));
|
|
622
|
+
}
|
|
623
|
+
if (tick % CONTAINER_CHECK_EVERY_TICKS === 0) {
|
|
624
|
+
await guard(say, "asking after the containers docker pairings reach", async () => await checkContainers(fate, (await readState()).pairings, answeringOf(tracking)));
|
|
625
|
+
}
|
|
626
|
+
if (tick % KEPT_HERE_CHECK_EVERY_TICKS === 0) {
|
|
627
|
+
await guard(say, "asking this machine's ic which sandboxes it keeps", async () => await checkKeptHere(fate, (await readState()).pairings, reachedLocally(dialed), undefined, answeringOf(tracking)));
|
|
628
|
+
}
|
|
629
|
+
};
|
|
630
|
+
export const WATCHER_STALL_MS = 20 * 60_000;
|
|
631
|
+
const STALL_CHECK_MS = 30_000;
|
|
632
|
+
export const watcherStalled = (progress, now) => now - progress >= WATCHER_STALL_MS;
|
|
633
|
+
export const runMirrorWatch = async (log) => {
|
|
634
|
+
for (;;) {
|
|
635
|
+
if ((await watchGeneration(log)) === "done") {
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
};
|
|
640
|
+
const watchGeneration = async (log) => {
|
|
641
|
+
const abandon = new AbortController();
|
|
642
|
+
const generation = { signal: abandon.signal, progress: Date.now(), step: "starting", stopTransports: undefined };
|
|
643
|
+
const loop = watchLoop(log, generation);
|
|
644
|
+
let timer;
|
|
645
|
+
const stall = new Promise((resolve) => {
|
|
646
|
+
timer = setInterval(() => {
|
|
647
|
+
if (watcherStalled(generation.progress, Date.now())) {
|
|
648
|
+
resolve("stalled");
|
|
462
649
|
}
|
|
463
|
-
|
|
650
|
+
}, STALL_CHECK_MS);
|
|
651
|
+
});
|
|
652
|
+
try {
|
|
653
|
+
const outcome = await Promise.race([loop.then(() => "done"), stall]);
|
|
654
|
+
if (outcome === "stalled") {
|
|
655
|
+
abandon.abort();
|
|
656
|
+
loop.catch(() => undefined);
|
|
657
|
+
log(`sync watcher: no progress for ${WATCHER_STALL_MS / 60_000} minutes (it was at: ${generation.step}); restarting it. Its heartbeat had gone stale, which \`intentic-machine status\` reports.`);
|
|
658
|
+
await generation
|
|
659
|
+
.stopTransports?.()
|
|
660
|
+
.catch((error) => log(`sync watcher: could not stop the stalled watcher's tunnels (${errorMessage(error)})`));
|
|
464
661
|
}
|
|
465
|
-
|
|
466
|
-
|
|
662
|
+
return outcome;
|
|
663
|
+
}
|
|
664
|
+
finally {
|
|
665
|
+
clearInterval(timer);
|
|
666
|
+
}
|
|
667
|
+
};
|
|
668
|
+
const watchLoop = async (log, generation) => {
|
|
669
|
+
const watch = await startWatch(log, generation);
|
|
670
|
+
if (watch === undefined) {
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
for (let tick = 0; watch.live(); tick += 1) {
|
|
674
|
+
if ((await watchTick(watch, tick)) === "stop") {
|
|
675
|
+
return;
|
|
467
676
|
}
|
|
468
|
-
await
|
|
469
|
-
|
|
677
|
+
await sleep(POLL_MS, { signal: generation.signal });
|
|
678
|
+
}
|
|
679
|
+
};
|
|
680
|
+
const startWatch = async (log, generation) => {
|
|
681
|
+
const mutagen = await ensureMutagen();
|
|
682
|
+
const say = quieted(log);
|
|
683
|
+
sayDaemonEventsTo(say);
|
|
684
|
+
const initial = await readState();
|
|
685
|
+
if (initial.pairings.length === 0) {
|
|
686
|
+
log("no sandboxes are paired: nothing to mirror. Enable it from a sandbox's Desktop sync card.");
|
|
687
|
+
return undefined;
|
|
470
688
|
}
|
|
689
|
+
const sshConfig = pairingSshConfig(initial.pairings);
|
|
690
|
+
await guard(say, "refreshing the ssh configuration", async () => await writeManagedSshConfig(sshConfig));
|
|
691
|
+
const tunnels = createTunnelPool(say, generation.signal);
|
|
692
|
+
generation.stopTransports = tunnels.stopAll;
|
|
693
|
+
const bases = createDaemonBases(say);
|
|
694
|
+
await guard(say, "opening the sync transports", async () => await tunnels.reconcile(tunnelTargets(await dialedPairings(initial.pairings, bases))));
|
|
695
|
+
const tracking = { rejectedPolls: new Map(), unreachable: new Map(), repos: new Map(), sessionsPending: new Set() };
|
|
696
|
+
const reportUnsupported = new Set();
|
|
697
|
+
const fate = { mutagen, releaseForwards: async (sandboxId) => await retirePairingMirror(mutagen, sandboxId), say };
|
|
698
|
+
const gone = async (pairing) => await sandboxSaidGone(fate, pairing.sandboxId, "edge");
|
|
699
|
+
const watch = {
|
|
700
|
+
log,
|
|
701
|
+
say,
|
|
702
|
+
mutagen,
|
|
703
|
+
live: () => !generation.signal.aborted,
|
|
704
|
+
progress: (step) => {
|
|
705
|
+
generation.signal.throwIfAborted();
|
|
706
|
+
generation.progress = Date.now();
|
|
707
|
+
generation.step = step;
|
|
708
|
+
},
|
|
709
|
+
tunnels,
|
|
710
|
+
bases,
|
|
711
|
+
tracking,
|
|
712
|
+
sessionsPrepared: new Set(),
|
|
713
|
+
reportUnsupported,
|
|
714
|
+
fate,
|
|
715
|
+
gone,
|
|
716
|
+
announce: announcer(mutagen, bases, reportUnsupported, tracking.unreachable, gone, say),
|
|
717
|
+
holders: { dockerPorts: publishedPortsReader(say), freeSince: new Map() },
|
|
718
|
+
sshConfig,
|
|
719
|
+
};
|
|
720
|
+
await prepareSessions(mutagen, initial.pairings, watch.sessionsPrepared, tracking.sessionsPending, watch.announce, { fate, relabels: relabelBudget(1), tracking }, say);
|
|
721
|
+
await guard(say, "retiring orphaned sessions", () => retireOrphanSessions(mutagen, initial.pairings, say));
|
|
722
|
+
log(`sync started; polling ${plural(initial.pairings.length, "paired sandbox")} every ${POLL_MS / 1000}s`);
|
|
723
|
+
return watch;
|
|
724
|
+
};
|
|
725
|
+
const watchTick = async (watch, tick) => {
|
|
726
|
+
const { mutagen, say, tracking, fate, progress } = watch;
|
|
727
|
+
progress("the start of a pass");
|
|
728
|
+
const state = await readState().catch((error) => {
|
|
729
|
+
say(` tick skipped: the sync state didn't read (${errorMessage(error)})`);
|
|
730
|
+
return undefined;
|
|
731
|
+
});
|
|
732
|
+
if (state === undefined) {
|
|
733
|
+
return "next";
|
|
734
|
+
}
|
|
735
|
+
if (state.pairings.length === 0) {
|
|
736
|
+
await watch.tunnels.stopAll();
|
|
737
|
+
watch.log("no sandboxes are paired any more: sync stopping. Re-enable from a sandbox's Desktop sync card.");
|
|
738
|
+
return "stop";
|
|
739
|
+
}
|
|
740
|
+
const wantedConfig = pairingSshConfig(state.pairings);
|
|
741
|
+
if (wantedConfig !== watch.sshConfig) {
|
|
742
|
+
await guard(say, "refreshing the ssh configuration", async () => await writeManagedSshConfig(wantedConfig));
|
|
743
|
+
watch.sshConfig = wantedConfig;
|
|
744
|
+
}
|
|
745
|
+
const dialed = await dialedPairings(state.pairings, watch.bases);
|
|
746
|
+
await guard(say, "reconciling the sync transports", async () => await watch.tunnels.reconcile(tunnelTargets(dialed)));
|
|
747
|
+
progress("the sync transports");
|
|
748
|
+
const relabels = relabelBudget(1);
|
|
749
|
+
await prepareSessions(mutagen, state.pairings, watch.sessionsPrepared, tracking.sessionsPending, watch.announce, { fate, relabels, tracking }, say);
|
|
750
|
+
await retryPendingSessions(mutagen, state.pairings, tracking.sessionsPending, tick, relabels, say);
|
|
751
|
+
progress("preparing file sync");
|
|
752
|
+
if (!watch.live()) {
|
|
753
|
+
return "stop";
|
|
754
|
+
}
|
|
755
|
+
let swapHeld = new Set();
|
|
756
|
+
await guard(say, "holding file sync still during swaps", async () => {
|
|
757
|
+
swapHeld = await holdSyncDuringSwaps(mutagen, state.pairings, tracking.unreachable, say);
|
|
758
|
+
});
|
|
759
|
+
const context = {
|
|
760
|
+
mutagen,
|
|
761
|
+
tick,
|
|
762
|
+
claimedBy: new Map(),
|
|
763
|
+
holding: recordedForwards(state.pairings),
|
|
764
|
+
tracking,
|
|
765
|
+
bases: watch.bases,
|
|
766
|
+
swapHeld,
|
|
767
|
+
holders: watch.holders,
|
|
768
|
+
fate,
|
|
769
|
+
relabels,
|
|
770
|
+
progress,
|
|
771
|
+
pairings: state.pairings,
|
|
772
|
+
say,
|
|
773
|
+
};
|
|
774
|
+
await runPasses(context, dialed, state.pairings, watch.live);
|
|
775
|
+
if (!watch.live()) {
|
|
776
|
+
return "stop";
|
|
777
|
+
}
|
|
778
|
+
if (tick % REPORT_EVERY_TICKS === 0) {
|
|
779
|
+
await guard(say, "posting this machine's reports", async () => await postReports(dialed, mutagen, watch.reportUnsupported, watch.gone, say));
|
|
780
|
+
}
|
|
781
|
+
await standingSweeps(context, dialed);
|
|
782
|
+
progress("the end of a pass");
|
|
783
|
+
await beat();
|
|
784
|
+
return "next";
|
|
471
785
|
};
|
|
472
786
|
const teardownForwards = async (mutagen, sandboxId) => {
|
|
473
|
-
const
|
|
787
|
+
const recorded = async () => (await readState()).pairings
|
|
788
|
+
.filter((held) => sandboxId === undefined || held.sandboxId === sandboxId)
|
|
789
|
+
.flatMap((held) => (held.mirroredPorts ?? []).map((port) => forwardSessionName(held.sandboxId, port.port)));
|
|
790
|
+
const names = ourForwardSessions(mutagen, sandboxId) ?? (await recorded());
|
|
474
791
|
if (names.length > 0) {
|
|
475
|
-
spawnSync(mutagen, ["forward", "terminate", ...names], { stdio: "ignore", windowsHide: true });
|
|
792
|
+
spawnSync(mutagen, ["forward", "terminate", ...names], { stdio: "ignore", windowsHide: true, timeout: MUTAGEN_CALL_TIMEOUT_MS });
|
|
476
793
|
}
|
|
477
794
|
await updateState((state) => ({
|
|
478
|
-
pairings: state.pairings.map((held) => sandboxId === undefined || held.sandboxId === sandboxId
|
|
795
|
+
pairings: state.pairings.map((held) => (sandboxId === undefined || held.sandboxId === sandboxId) && !isAttachedPairing(held)
|
|
796
|
+
? { ...held, mirroredPorts: [], skippedPorts: [] }
|
|
797
|
+
: held),
|
|
479
798
|
}));
|
|
480
799
|
return names.length;
|
|
481
800
|
};
|
|
482
801
|
export const retirePairingMirror = async (mutagen, sandboxId) => await teardownForwards(mutagen, sandboxId);
|
|
483
802
|
export const retireMirroredPort = async (mutagen, sandboxId, port) => {
|
|
484
|
-
spawnSync(mutagen, ["forward", "terminate", forwardSessionName(sandboxId, port)], {
|
|
803
|
+
spawnSync(mutagen, ["forward", "terminate", forwardSessionName(sandboxId, port)], {
|
|
804
|
+
stdio: "ignore",
|
|
805
|
+
windowsHide: true,
|
|
806
|
+
timeout: MUTAGEN_CALL_TIMEOUT_MS,
|
|
807
|
+
});
|
|
485
808
|
let wasMirrored = false;
|
|
486
809
|
await updateState((state) => ({
|
|
487
810
|
pairings: state.pairings.map((held) => {
|
|
488
|
-
if (held.sandboxId !== sandboxId) {
|
|
811
|
+
if (held.sandboxId !== sandboxId || isAttachedPairing(held)) {
|
|
489
812
|
return held;
|
|
490
813
|
}
|
|
491
814
|
const mirrored = held.mirroredPorts ?? [];
|