@intentic/machine 1.344.0 → 1.346.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.
Files changed (231) hide show
  1. package/README.md +185 -154
  2. package/dist/autostart/autostart.d.ts +0 -1
  3. package/dist/autostart/autostart.d.ts.map +1 -1
  4. package/dist/autostart/autostart.js +3 -9
  5. package/dist/autostart/autostart.js.map +1 -1
  6. package/dist/commands.d.ts.map +1 -1
  7. package/dist/commands.js +4 -21
  8. package/dist/commands.js.map +1 -1
  9. package/dist/device/commands.d.ts +8 -2
  10. package/dist/device/commands.d.ts.map +1 -1
  11. package/dist/device/commands.js +23 -9
  12. package/dist/device/commands.js.map +1 -1
  13. package/dist/device/connection.d.ts.map +1 -1
  14. package/dist/device/connection.js +27 -9
  15. package/dist/device/connection.js.map +1 -1
  16. package/dist/device/grant.d.ts +244 -1
  17. package/dist/device/grant.d.ts.map +1 -1
  18. package/dist/device/mcp.d.ts.map +1 -1
  19. package/dist/device/mcp.js +25 -8
  20. package/dist/device/mcp.js.map +1 -1
  21. package/dist/device/router.d.ts +469 -226
  22. package/dist/device/router.d.ts.map +1 -1
  23. package/dist/device/router.js +18 -10
  24. package/dist/device/router.js.map +1 -1
  25. package/dist/device/sandbox-rounds/keeper.d.ts +4 -1
  26. package/dist/device/sandbox-rounds/keeper.d.ts.map +1 -1
  27. package/dist/device/sandbox-rounds/keeper.js +13 -7
  28. package/dist/device/sandbox-rounds/keeper.js.map +1 -1
  29. package/dist/device/sandbox-rounds/sleep.d.ts +15 -2
  30. package/dist/device/sandbox-rounds/sleep.d.ts.map +1 -1
  31. package/dist/device/sandbox-rounds/sleep.js +68 -13
  32. package/dist/device/sandbox-rounds/sleep.js.map +1 -1
  33. package/dist/device/tools/agent.d.ts +15 -0
  34. package/dist/device/tools/agent.d.ts.map +1 -1
  35. package/dist/device/tools/agent.js +75 -19
  36. package/dist/device/tools/agent.js.map +1 -1
  37. package/dist/device/tools/android.d.ts +1 -1
  38. package/dist/device/tools/android.d.ts.map +1 -1
  39. package/dist/device/tools/android.js +17 -8
  40. package/dist/device/tools/android.js.map +1 -1
  41. package/dist/device/tools/browser.d.ts.map +1 -1
  42. package/dist/device/tools/browser.js +3 -5
  43. package/dist/device/tools/browser.js.map +1 -1
  44. package/dist/device/tools/ic-binary.d.ts +2 -1
  45. package/dist/device/tools/ic-binary.d.ts.map +1 -1
  46. package/dist/device/tools/ic-binary.js +34 -4
  47. package/dist/device/tools/ic-binary.js.map +1 -1
  48. package/dist/device/tools/os/settings.js +3 -3
  49. package/dist/device/tools/os/settings.js.map +1 -1
  50. package/dist/device/tools/programs/app-wait-tools.d.ts +12 -0
  51. package/dist/device/tools/programs/app-wait-tools.d.ts.map +1 -0
  52. package/dist/device/tools/programs/app-wait-tools.js +60 -0
  53. package/dist/device/tools/programs/app-wait-tools.js.map +1 -0
  54. package/dist/device/tools/programs/app-wait.d.ts +19 -0
  55. package/dist/device/tools/programs/app-wait.d.ts.map +1 -0
  56. package/dist/device/tools/programs/app-wait.js +89 -0
  57. package/dist/device/tools/programs/app-wait.js.map +1 -0
  58. package/dist/device/tools/programs/file-changes.d.ts +2 -0
  59. package/dist/device/tools/programs/file-changes.d.ts.map +1 -0
  60. package/dist/device/tools/programs/file-changes.js +19 -0
  61. package/dist/device/tools/programs/file-changes.js.map +1 -0
  62. package/dist/device/tools/programs/programs.d.ts +12 -0
  63. package/dist/device/tools/programs/programs.d.ts.map +1 -1
  64. package/dist/device/tools/programs/programs.js +13 -12
  65. package/dist/device/tools/programs/programs.js.map +1 -1
  66. package/dist/device/tools/programs/stop-all.d.ts +10 -0
  67. package/dist/device/tools/programs/stop-all.d.ts.map +1 -0
  68. package/dist/device/tools/programs/stop-all.js +52 -0
  69. package/dist/device/tools/programs/stop-all.js.map +1 -0
  70. package/dist/device/tools/sandboxes.d.ts +2 -0
  71. package/dist/device/tools/sandboxes.d.ts.map +1 -1
  72. package/dist/device/tools/sandboxes.js +11 -3
  73. package/dist/device/tools/sandboxes.js.map +1 -1
  74. package/dist/device/tools/shell.d.ts +1 -1
  75. package/dist/device/tools/shell.d.ts.map +1 -1
  76. package/dist/install.d.ts +15 -1
  77. package/dist/install.d.ts.map +1 -1
  78. package/dist/install.js +35 -6
  79. package/dist/install.js.map +1 -1
  80. package/dist/keyring/app-commands.d.ts +15 -0
  81. package/dist/keyring/app-commands.d.ts.map +1 -0
  82. package/dist/keyring/app-commands.js +247 -0
  83. package/dist/keyring/app-commands.js.map +1 -0
  84. package/dist/keyring/commands.d.ts +1 -1
  85. package/dist/keyring/commands.d.ts.map +1 -1
  86. package/dist/keyring/commands.js +26 -32
  87. package/dist/keyring/commands.js.map +1 -1
  88. package/dist/keyring/git-helper.d.ts +8 -0
  89. package/dist/keyring/git-helper.d.ts.map +1 -0
  90. package/dist/keyring/git-helper.js +60 -0
  91. package/dist/keyring/git-helper.js.map +1 -0
  92. package/dist/keyring/keyring-status.d.ts +8 -1
  93. package/dist/keyring/keyring-status.d.ts.map +1 -1
  94. package/dist/keyring/keyring-status.js +39 -4
  95. package/dist/keyring/keyring-status.js.map +1 -1
  96. package/dist/keyring/native-host-install.d.ts +28 -0
  97. package/dist/keyring/native-host-install.d.ts.map +1 -0
  98. package/dist/keyring/native-host-install.js +99 -0
  99. package/dist/keyring/native-host-install.js.map +1 -0
  100. package/dist/keyring/relay.d.ts +5 -1
  101. package/dist/keyring/relay.d.ts.map +1 -1
  102. package/dist/keyring/relay.js +16 -0
  103. package/dist/keyring/relay.js.map +1 -1
  104. package/dist/keyring/sandbox-push.d.ts +4 -0
  105. package/dist/keyring/sandbox-push.d.ts.map +1 -0
  106. package/dist/keyring/sandbox-push.js +21 -0
  107. package/dist/keyring/sandbox-push.js.map +1 -0
  108. package/dist/keyring/service.d.ts +11 -5
  109. package/dist/keyring/service.d.ts.map +1 -1
  110. package/dist/keyring/service.js +34 -51
  111. package/dist/keyring/service.js.map +1 -1
  112. package/dist/resident/resident.d.ts +1 -0
  113. package/dist/resident/resident.d.ts.map +1 -1
  114. package/dist/resident/resident.js +9 -1
  115. package/dist/resident/resident.js.map +1 -1
  116. package/dist/status.d.ts.map +1 -1
  117. package/dist/status.js +1 -8
  118. package/dist/status.js.map +1 -1
  119. package/dist/sync/binary/mutagen-binary.d.ts +4 -0
  120. package/dist/sync/binary/mutagen-binary.d.ts.map +1 -0
  121. package/dist/sync/binary/mutagen-binary.js +65 -0
  122. package/dist/sync/binary/mutagen-binary.js.map +1 -0
  123. package/dist/sync/commands.d.ts +1 -9
  124. package/dist/sync/commands.d.ts.map +1 -1
  125. package/dist/sync/commands.js +49 -84
  126. package/dist/sync/commands.js.map +1 -1
  127. package/dist/sync/config.d.ts +21 -25
  128. package/dist/sync/config.d.ts.map +1 -1
  129. package/dist/sync/config.js +76 -35
  130. package/dist/sync/config.js.map +1 -1
  131. package/dist/sync/endpoint.d.ts +1 -1
  132. package/dist/sync/endpoint.d.ts.map +1 -1
  133. package/dist/sync/endpoint.js +2 -3
  134. package/dist/sync/endpoint.js.map +1 -1
  135. package/dist/sync/folders.d.ts.map +1 -1
  136. package/dist/sync/folders.js.map +1 -1
  137. package/dist/sync/forget-command.js +1 -1
  138. package/dist/sync/forget-command.js.map +1 -1
  139. package/dist/sync/gone-watch.d.ts +1 -1
  140. package/dist/sync/gone-watch.d.ts.map +1 -1
  141. package/dist/sync/gone-watch.js +16 -40
  142. package/dist/sync/gone-watch.js.map +1 -1
  143. package/dist/sync/holds.d.ts +30 -0
  144. package/dist/sync/holds.d.ts.map +1 -0
  145. package/dist/sync/holds.js +117 -0
  146. package/dist/sync/holds.js.map +1 -0
  147. package/dist/sync/low-disk.d.ts +2 -9
  148. package/dist/sync/low-disk.d.ts.map +1 -1
  149. package/dist/sync/low-disk.js +10 -45
  150. package/dist/sync/low-disk.js.map +1 -1
  151. package/dist/sync/mirror.d.ts +2 -3
  152. package/dist/sync/mirror.d.ts.map +1 -1
  153. package/dist/sync/mirror.js +61 -160
  154. package/dist/sync/mirror.js.map +1 -1
  155. package/dist/sync/mutagen-env.d.ts +1 -3
  156. package/dist/sync/mutagen-env.d.ts.map +1 -1
  157. package/dist/sync/mutagen-env.js +2 -12
  158. package/dist/sync/mutagen-env.js.map +1 -1
  159. package/dist/sync/mutagen-move.d.ts +3 -1
  160. package/dist/sync/mutagen-move.d.ts.map +1 -1
  161. package/dist/sync/mutagen-move.js +31 -22
  162. package/dist/sync/mutagen-move.js.map +1 -1
  163. package/dist/sync/mutagen.d.ts +6 -12
  164. package/dist/sync/mutagen.d.ts.map +1 -1
  165. package/dist/sync/mutagen.js +17 -144
  166. package/dist/sync/mutagen.js.map +1 -1
  167. package/dist/sync/project/project-commands.d.ts +0 -3
  168. package/dist/sync/project/project-commands.d.ts.map +1 -1
  169. package/dist/sync/project/project-commands.js +5 -3
  170. package/dist/sync/project/project-commands.js.map +1 -1
  171. package/dist/sync/project/project-delivery.d.ts.map +1 -1
  172. package/dist/sync/project/project-delivery.js +5 -4
  173. package/dist/sync/project/project-delivery.js.map +1 -1
  174. package/dist/sync/project/project-transfer.d.ts +4 -0
  175. package/dist/sync/project/project-transfer.d.ts.map +1 -1
  176. package/dist/sync/project/project-transfer.js +9 -2
  177. package/dist/sync/project/project-transfer.js.map +1 -1
  178. package/dist/sync/report.d.ts.map +1 -1
  179. package/dist/sync/report.js +18 -11
  180. package/dist/sync/report.js.map +1 -1
  181. package/dist/sync/retire.d.ts.map +1 -1
  182. package/dist/sync/retire.js +1 -2
  183. package/dist/sync/retire.js.map +1 -1
  184. package/dist/sync/tunnel.d.ts +4 -3
  185. package/dist/sync/tunnel.d.ts.map +1 -1
  186. package/dist/sync/tunnel.js +107 -42
  187. package/dist/sync/tunnel.js.map +1 -1
  188. package/dist/sync/uninstall.d.ts.map +1 -1
  189. package/dist/sync/uninstall.js +4 -3
  190. package/dist/sync/uninstall.js.map +1 -1
  191. package/dist/sync/wake.d.ts +3 -0
  192. package/dist/sync/wake.d.ts.map +1 -0
  193. package/dist/sync/wake.js +43 -0
  194. package/dist/sync/wake.js.map +1 -0
  195. package/dist/uninstall/command.d.ts +3 -0
  196. package/dist/uninstall/command.d.ts.map +1 -0
  197. package/dist/uninstall/command.js +40 -0
  198. package/dist/uninstall/command.js.map +1 -0
  199. package/dist/uninstall/distros.d.ts +8 -0
  200. package/dist/uninstall/distros.d.ts.map +1 -0
  201. package/dist/uninstall/distros.js +103 -0
  202. package/dist/uninstall/distros.js.map +1 -0
  203. package/dist/uninstall/plan.d.ts +23 -0
  204. package/dist/uninstall/plan.d.ts.map +1 -0
  205. package/dist/uninstall/plan.js +71 -0
  206. package/dist/uninstall/plan.js.map +1 -0
  207. package/dist/uninstall/purge.d.ts +3 -0
  208. package/dist/uninstall/purge.d.ts.map +1 -0
  209. package/dist/uninstall/purge.js +79 -0
  210. package/dist/uninstall/purge.js.map +1 -0
  211. package/dist/uninstall/steps.d.ts +12 -0
  212. package/dist/uninstall/steps.d.ts.map +1 -0
  213. package/dist/uninstall/steps.js +200 -0
  214. package/dist/uninstall/steps.js.map +1 -0
  215. package/dist/uninstall/sweep.d.ts +12 -0
  216. package/dist/uninstall/sweep.d.ts.map +1 -0
  217. package/dist/uninstall/sweep.js +90 -0
  218. package/dist/uninstall/sweep.js.map +1 -0
  219. package/dist/uninstall/tally.d.ts +14 -0
  220. package/dist/uninstall/tally.d.ts.map +1 -0
  221. package/dist/uninstall/tally.js +25 -0
  222. package/dist/uninstall/tally.js.map +1 -0
  223. package/package.json +9 -9
  224. package/dist/sync/attach-commands.d.ts +0 -39
  225. package/dist/sync/attach-commands.d.ts.map +0 -1
  226. package/dist/sync/attach-commands.js +0 -163
  227. package/dist/sync/attach-commands.js.map +0 -1
  228. package/dist/sync/swap-pause.d.ts +0 -10
  229. package/dist/sync/swap-pause.d.ts.map +0 -1
  230. package/dist/sync/swap-pause.js +0 -29
  231. package/dist/sync/swap-pause.js.map +0 -1
@@ -1,26 +1,27 @@
1
1
  import net from "node:net";
2
- import { sleep } from "@intentic/base/async";
3
2
  import { errorMessage } from "@intentic/base/errors";
4
3
  import { plural } from "@intentic/base/format";
5
4
  import { writeFileAtomic } from "@intentic/base/fs";
6
5
  import { PortsListSchema } from "@intentic/sandbox-contract/schemas/ports";
7
- import { isAttachedPairing, mirrorHeartbeatPath, pairingKey, pairingTransport, readState, removeSandboxPairings, setFileSyncAutoPaused, setFileSyncSwapPaused, setSandboxGone, setUnreachableSince, updateState, } from "./config.js";
6
+ import { mirrorHeartbeatPath, pairingKey, pairingTransport, readState, removeSandboxPairings, retireLegacyPairings, setSandboxGone, setSwapSince, setUnreachableSince, updateState, } from "./config.js";
8
7
  import { pairingEndpoint } from "./endpoint.js";
9
8
  import { runProcess, runProcessSync } from "./exec.js";
10
9
  import { candidateBases, createDaemonBases, dialedPairings } from "../daemon-base.js";
11
10
  import { answeredGone, goneStep, SandboxGoneError } from "./gone.js";
12
- import { checkContainers, checkKeptHere, sandboxAnswered, sandboxSaidGone } from "./gone-watch.js";
11
+ import { checkContainers, checkKeptHere, goneLingers, sandboxAnswered, sandboxSaidGone } from "./gone-watch.js";
13
12
  import { retireSandbox } from "./retire.js";
14
13
  import { readSwaps } from "../device/sandbox-rounds/ic-swaps.js";
15
14
  import { publishedPortsReader } from "./docker-ports.js";
16
15
  import { realBridgeExec, runGitBridge } from "./git-bridge.js";
16
+ import { holdSessions, holdsSync, recordOf, RELEASE_FORWARDS_MS, shouldReleaseForwards, swapBegan, swapStep, syncHeld, syncHolds, UNREACHABLE_HOLD_MS } from "./holds.js";
17
17
  import { guardDisks } from "./low-disk.js";
18
- import { ensureMutagen, ensureSyncSession, existingSyncSessions, forwardedPorts, forwardSessionName, healDerivedConflicts, MUTAGEN_CALL_TIMEOUT_MS, mutagenForwardArgs, ourForwardSessions, pauseRunningSync, relabelBudget, retireOrphanSessions, resumeAutoPausedSync, resumeSwapPausedSync, runMutagenAsync, sayDaemonEventsTo, sessionLabels, startMutagenDaemon, syncMode, syncSessionNames, } from "./mutagen.js";
18
+ import { ensureSyncSession, existingSyncSessions, forwardedPorts, forwardSessionName, healDerivedConflicts, MUTAGEN_CALL_TIMEOUT_MS, mutagenForwardArgs, ourForwardSessions, relabelBudget, retireOrphanSessions, runMutagenAsync, sayDaemonEventsTo, sessionLabels, startMutagenDaemon, syncMode, syncSessionNames, } from "./mutagen.js";
19
+ import { ensureMutagen } from "./binary/mutagen-binary.js";
19
20
  import { moveOffSharedMutagen } from "./mutagen-move.js";
20
21
  import { quieted } from "./repeats.js";
22
+ import { sleepUnlessWoken } from "./wake.js";
21
23
  import { deviceReport, scopedReport } from "./report.js";
22
24
  import { pairingSshConfig, writeManagedSshConfig } from "./ssh.js";
23
- import { holdsSync, recordOf, RELEASE_FORWARDS_MS, shouldReleaseForwards, swapPauseStep } from "./swap-pause.js";
24
25
  import { createTunnelPool, tunnelTargets } from "./tunnel.js";
25
26
  const POLL_MS = 5000;
26
27
  const PORTS_TIMEOUT_MS = 10_000;
@@ -28,8 +29,6 @@ const REPO_LIST_EVERY_TICKS = 12;
28
29
  const REPORT_EVERY_TICKS = 3;
29
30
  const REPORT_TIMEOUT_MS = 10_000;
30
31
  const REVOKED_POLLS = 3;
31
- const UNREACHABLE_PAUSE_MS = 60 * 60_000;
32
- export const shouldAutoPauseFileSync = (unreachableForMs) => unreachableForMs >= UNREACHABLE_PAUSE_MS;
33
32
  export const pollBackoffMs = (unreachableForMs) => unreachableForMs < 60_000 ? 0 : unreachableForMs < 10 * 60_000 ? 30_000 : 5 * 60_000;
34
33
  export const pollDue = (held, now) => held === undefined || now - held.lastTried >= pollBackoffMs(now - held.since);
35
34
  export class SyncAuthError extends Error {
@@ -225,9 +224,6 @@ const servePairing = async (context, pairing, base) => {
225
224
  const { mutagen, claimedBy, holding, holders, say: log } = context;
226
225
  const baseline = pairing.mirroredPorts ?? [];
227
226
  const ports = pairing.syncToken === undefined ? [] : await fetchWorkspacePorts(base, pairing.syncToken);
228
- if (isAttachedPairing(pairing)) {
229
- return [];
230
- }
231
227
  if (pairing.mirrorOff === true) {
232
228
  await retireSwitchedOff(mutagen, pairing, log);
233
229
  return [];
@@ -286,7 +282,6 @@ const dropRevokedPairing = async (mutagen, sandboxId, log) => {
286
282
  await removeSandboxPairings(sandboxId);
287
283
  retireOrphanSessions(mutagen, (await readState()).pairings, log);
288
284
  };
289
- const keyOfSandbox = (key, sandboxId) => key === sandboxId || key.startsWith(`${sandboxId}~`);
290
285
  const guard = async (log, what, step) => {
291
286
  try {
292
287
  await step();
@@ -308,15 +303,14 @@ const absorbRejectedPoll = async (mutagen, pairing, rejectedPolls, repos, sessio
308
303
  log(`${pairing.sandboxId} rejected the sync token ${REVOKED_POLLS} polls in a row: this machine's enrollment was revoked.`);
309
304
  rejectedPolls.delete(pairing.sandboxId);
310
305
  repos.delete(pairing.sandboxId);
311
- for (const key of [...sessionsPending].filter((held) => keyOfSandbox(held, pairing.sandboxId))) {
312
- sessionsPending.delete(key);
313
- }
306
+ sessionsPending.delete(pairingKey(pairing));
314
307
  await dropRevokedPairing(mutagen, pairing.sandboxId, log);
315
308
  return true;
316
309
  };
317
310
  const absorbUnreachablePoll = async (mutagen, pairing, unreachable, log) => {
318
311
  const now = Date.now();
319
- const since = unreachable.get(pairing.sandboxId)?.since ?? pairing.unreachableSince ?? now;
312
+ const before = unreachable.get(pairing.sandboxId);
313
+ const since = before?.since ?? pairing.unreachableSince ?? now;
320
314
  unreachable.set(pairing.sandboxId, { since, lastTried: now });
321
315
  if (pairing.unreachableSince === undefined) {
322
316
  await setUnreachableSince(pairing.sandboxId, since);
@@ -326,15 +320,11 @@ const absorbUnreachablePoll = async (mutagen, pairing, unreachable, log) => {
326
320
  await retirePairingMirror(mutagen, pairing.sandboxId);
327
321
  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.`);
328
322
  }
329
- return await pauseIfLongUnreachable(mutagen, pairing, now - since, log);
330
- };
331
- const pauseIfLongUnreachable = async (mutagen, pairing, unreachableForMs, log) => {
332
- if (pairing.fileSyncAutoPaused === true || !shouldAutoPauseFileSync(unreachableForMs) || !pauseRunningSync(mutagen, pairing)) {
333
- return false;
323
+ const holds = now - since >= UNREACHABLE_HOLD_MS;
324
+ if (holds && (before === undefined || before.lastTried - since < UNREACHABLE_HOLD_MS)) {
325
+ log(` ${pairing.sandboxId}: unreachable for an hour; its file sync holds still to stop permanent reconnect and scanning load, and carries on when it answers.`);
334
326
  }
335
- await setFileSyncAutoPaused(pairingKey(pairing), true);
336
- log(` ${pairingKey(pairing)}: unreachable for one hour; paused its Mutagen sessions to stop permanent reconnect/scanning load. They resume automatically when it returns.`);
337
- return true;
327
+ return holds;
338
328
  };
339
329
  const startBridge = (tracking, pairing, tick, say) => {
340
330
  const id = pairing.sandboxId;
@@ -392,7 +382,7 @@ export const markPrepared = (prepared, pairing) => void prepared.add(setupOf(pai
392
382
  const prepareSession = async (mutagen, pairing, pending, relabels, say) => {
393
383
  let converged = false;
394
384
  await guard(say, `${pairingKey(pairing)}: preparing its file sync`, async () => {
395
- converged = await ensureSyncSession(mutagen, pairing, say, relabels);
385
+ converged = await ensureSyncSession(mutagen, pairing, say, syncHeld(pairing, Date.now()), relabels);
396
386
  });
397
387
  if (converged) {
398
388
  pending.delete(pairingKey(pairing));
@@ -403,62 +393,15 @@ const prepareSession = async (mutagen, pairing, pending, relabels, say) => {
403
393
  return converged;
404
394
  };
405
395
  const dormant = (mutagen, pairing) => pairing.goneSince !== undefined ||
406
- pairing.fileSyncPausedFor !== undefined ||
407
- (pairing.fileSyncAutoPaused === true && (existingSyncSessions(mutagen, syncSessionNames(pairing)) ?? [undefined]).length > 0);
408
- export const readyToPrepare = async (setups, firstCopyAhead, announce) => {
409
- const told = new Map();
410
- const ready = [];
411
- for (const pairing of setups) {
412
- if (isAttachedPairing(pairing) && pairing.mode === "sync" && pairing.localDir !== undefined && firstCopyAhead(pairing)) {
413
- const said = told.get(pairing.sandboxId) ?? (await announce(pairing));
414
- told.set(pairing.sandboxId, said);
415
- if (!said) {
416
- continue;
417
- }
418
- }
419
- ready.push(pairing);
420
- }
421
- return ready;
422
- };
423
- const announcer = (mutagen, bases, unsupported, unreachable, gone, say) => async (pairing) => {
424
- if (pairing.goneSince !== undefined) {
425
- return false;
426
- }
427
- if (pairing.syncToken === undefined || unsupported.has(pairing.sandboxId)) {
428
- return true;
429
- }
430
- if (!pollDue(unreachable.get(pairing.sandboxId), Date.now())) {
431
- return false;
432
- }
433
- try {
434
- const response = await sendReport(await bases.resolve(pairing), pairing, await deviceReport(mutagen));
435
- if (response.status === 404) {
436
- unsupported.add(pairing.sandboxId);
437
- say(` ${pairing.sandboxId}: this sandbox is running a daemon without machine reports, its Devices view will stay empty.`);
438
- return true;
439
- }
440
- if (answeredGone(response)) {
441
- await gone(pairing);
442
- return false;
443
- }
444
- if (!response.ok) {
445
- say(` ${pairingKey(pairing)}: ${pairing.sandboxId} did not take the report naming this folder (${response.status}); its first copy waits for it.`);
446
- }
447
- return response.ok;
448
- }
449
- catch (error) {
450
- say(` ${pairingKey(pairing)}: ${pairing.sandboxId} could not be told of this folder (${errorMessage(error)}); its first copy waits until it can.`);
451
- return false;
452
- }
453
- };
454
- const prepareSessions = async (mutagen, pairings, prepared, pending, announce, passes, say) => {
455
- const firstCopyAhead = (pairing) => (existingSyncSessions(mutagen, syncSessionNames(pairing)) ?? []).length === 0;
396
+ pairing.containerAway !== undefined ||
397
+ (syncHolds(pairing, Date.now()).includes("unreachable") && (existingSyncSessions(mutagen, syncSessionNames(pairing)) ?? [undefined]).length > 0);
398
+ const prepareSessions = async (mutagen, pairings, prepared, pending, passes, say) => {
456
399
  const unprepared = unpreparedSetups(pairings, prepared);
457
400
  const docker = unprepared.filter((pairing) => pairingTransport(pairing) === "docker");
458
401
  if (docker.length > 0) {
459
402
  await guard(say, "asking after the containers docker pairings reach", async () => await checkContainers(passes.fate, docker, answeringOf(passes.tracking)));
460
403
  }
461
- for (const pairing of await readyToPrepare(unprepared, firstCopyAhead, announce)) {
404
+ for (const pairing of unprepared) {
462
405
  markPrepared(prepared, pairing);
463
406
  if (dormant(mutagen, pairing)) {
464
407
  pending.add(pairingKey(pairing));
@@ -478,25 +421,23 @@ const retryPendingSessions = async (mutagen, pairings, sessionsPending, tick, re
478
421
  }
479
422
  }
480
423
  };
481
- const holdSyncDuringSwaps = async (mutagen, pairings, unreachable, say) => {
482
- const records = await readSwaps();
424
+ export const holdSyncDuringSwaps = async (pairings, unreachable, say, read = readSwaps, record = setSwapSince) => {
425
+ const records = await read();
483
426
  const now = Date.now();
484
427
  const held = new Set();
485
428
  for (const pairing of pairings) {
486
- const record = recordOf(pairing, records);
487
- const holds = holdsSync(record, now, !unreachable.has(pairing.sandboxId));
429
+ const swap = recordOf(pairing, records);
430
+ const holds = holdsSync(swap, pairing.swapSince, now, !unreachable.has(pairing.sandboxId));
488
431
  if (holds) {
489
432
  held.add(pairing.sandboxId);
490
433
  }
491
- const step = swapPauseStep(pairing, holds);
492
- if (step === "pause" && pauseRunningSync(mutagen, pairing)) {
493
- await setFileSyncSwapPaused(pairingKey(pairing), true);
494
- say(` ${pairingKey(pairing)}: its sandbox is being swapped on this machine; file sync is paused until the new version has held.`);
495
- }
496
- else if (step === "resume") {
497
- resumeSwapPausedSync(mutagen, pairing);
498
- await setFileSyncSwapPaused(pairingKey(pairing), false);
499
- say(` ${pairingKey(pairing)}: its sandbox's swap is settled; file sync resumed.`);
434
+ const began = swapBegan(swap, pairing.swapSince, now);
435
+ const step = swapStep(pairing, holds, began);
436
+ if (step !== undefined) {
437
+ await record(pairingKey(pairing), step === "began" ? began : undefined);
438
+ say(step === "began"
439
+ ? ` ${pairingKey(pairing)}: its sandbox is being swapped on this machine; file sync holds still until the new version has held.`
440
+ : ` ${pairingKey(pairing)}: its sandbox's swap is settled; file sync carries on.`);
500
441
  }
501
442
  }
502
443
  return held;
@@ -506,29 +447,23 @@ const runPairingPass = async (context, pairing, base) => {
506
447
  const { rejectedPolls, unreachable } = tracking;
507
448
  const swapping = swapHeld.has(pairing.sandboxId);
508
449
  const short = context.diskHeld.has(pairingKey(pairing));
509
- let pausedThisPass = false;
450
+ let heldThisPass = false;
510
451
  try {
511
452
  const mirrored = await servePairing(context, pairing, base);
512
453
  rejectedPolls.delete(pairing.sandboxId);
513
454
  unreachable.delete(pairing.sandboxId);
514
- if (context.pairings.some((held) => held.sandboxId === pairing.sandboxId && held.goneSince !== undefined)) {
455
+ if (goneLingers(context.pairings, pairing.sandboxId)) {
515
456
  await sandboxAnswered(fate, pairing.sandboxId);
516
457
  }
517
458
  if (context.pairings.some((held) => held.sandboxId === pairing.sandboxId && held.unreachableSince !== undefined)) {
518
459
  await setUnreachableSince(pairing.sandboxId, undefined);
519
460
  }
520
- if (!swapping && !short && resumeAutoPausedSync(mutagen, pairing)) {
521
- await setFileSyncAutoPaused(pairingKey(pairing), false);
522
- say(` ${pairingKey(pairing)}: reachable again; resumed its automatically paused file sync`);
461
+ for (const port of mirrored) {
462
+ claimedBy.set(port.port, pairing.sandboxId);
523
463
  }
524
- if (!isAttachedPairing(pairing)) {
525
- for (const port of mirrored) {
526
- claimedBy.set(port.port, pairing.sandboxId);
527
- }
528
- handOver(holding, pairing.sandboxId, mirrored);
529
- if (tick % STRANDED_SWEEP_EVERY_TICKS === 0) {
530
- sweepStrandedForwards(mutagen, pairing.sandboxId, mirrored, say);
531
- }
464
+ handOver(holding, pairing.sandboxId, mirrored);
465
+ if (tick % STRANDED_SWEEP_EVERY_TICKS === 0) {
466
+ sweepStrandedForwards(mutagen, pairing.sandboxId, mirrored, say);
532
467
  }
533
468
  }
534
469
  catch (error) {
@@ -537,49 +472,16 @@ const runPairingPass = async (context, pairing, base) => {
537
472
  if (outcome.drop) {
538
473
  return;
539
474
  }
540
- pausedThisPass = outcome.paused;
475
+ heldThisPass = outcome.paused;
541
476
  say(` ${pairingKey(pairing)}: reconcile skipped: ${errorMessage(error)}`);
542
477
  }
543
- if (pairing.fileSyncAutoPaused === true || pairing.fileSyncPausedFor !== undefined || pausedThisPass || swapping || short) {
478
+ if (syncHeld({ ...pairing, unreachableSince: undefined }, Date.now()) || heldThisPass || swapping || short) {
544
479
  stopBridge(tracking, pairing.sandboxId);
545
480
  return;
546
481
  }
547
482
  await healPairing(mutagen, pairing, tick, say);
548
483
  startBridge(tracking, pairing, tick, say);
549
484
  };
550
- export const pollingPairings = (pairings) => {
551
- const polling = new Set();
552
- for (const sandboxId of new Set(pairings.map((pairing) => pairing.sandboxId))) {
553
- const own = pairings.filter((pairing) => pairing.sandboxId === sandboxId);
554
- const poller = own.find((pairing) => !isAttachedPairing(pairing)) ?? own[0];
555
- if (poller !== undefined) {
556
- polling.add(pairingKey(poller));
557
- }
558
- }
559
- return polling;
560
- };
561
- const followSandbox = async (context, pairing) => {
562
- const { mutagen, tick, tracking, swapHeld, say } = context;
563
- if (pairing.goneSince !== undefined || pairing.fileSyncPausedFor !== undefined) {
564
- return;
565
- }
566
- const down = tracking.unreachable.get(pairing.sandboxId)?.since ?? pairing.unreachableSince;
567
- const swapping = swapHeld.has(pairing.sandboxId);
568
- const short = context.diskHeld.has(pairingKey(pairing));
569
- if (down === undefined) {
570
- if (!swapping && !short && resumeAutoPausedSync(mutagen, pairing)) {
571
- await setFileSyncAutoPaused(pairingKey(pairing), false);
572
- say(` ${pairingKey(pairing)}: reachable again; resumed its automatically paused file sync`);
573
- }
574
- }
575
- else if (await pauseIfLongUnreachable(mutagen, pairing, Date.now() - down, say)) {
576
- return;
577
- }
578
- if (pairing.fileSyncAutoPaused === true || swapping || short) {
579
- return;
580
- }
581
- await healPairing(mutagen, pairing, tick, say);
582
- };
583
485
  const passGone = async (context, pairing) => {
584
486
  const now = Date.now();
585
487
  const step = goneStep(pairing, now);
@@ -600,15 +502,12 @@ const passGone = async (context, pairing) => {
600
502
  tracking.unreachable.delete(pairing.sandboxId);
601
503
  tracking.rejectedPolls.delete(pairing.sandboxId);
602
504
  tracking.repos.delete(pairing.sandboxId);
603
- for (const key of [...tracking.sessionsPending].filter((held) => keyOfSandbox(held, pairing.sandboxId))) {
604
- tracking.sessionsPending.delete(key);
605
- }
505
+ tracking.sessionsPending.delete(pairingKey(pairing));
606
506
  }
607
507
  return "skip";
608
508
  };
609
- const runPasses = async (context, dialed, pairings, live) => {
610
- const polling = pollingPairings(pairings);
611
- for (const { pairing, base } of dialed.filter((held) => polling.has(pairingKey(held.pairing)))) {
509
+ const runPasses = async (context, dialed, live) => {
510
+ for (const { pairing, base } of dialed) {
612
511
  if (!live()) {
613
512
  return;
614
513
  }
@@ -622,12 +521,6 @@ const runPasses = async (context, dialed, pairings, live) => {
622
521
  await runPairingPass(context, pairing, base);
623
522
  context.progress(`${pairingKey(pairing)}'s pass`);
624
523
  }
625
- for (const { pairing } of dialed.filter((held) => !polling.has(pairingKey(held.pairing)))) {
626
- if (!live()) {
627
- return;
628
- }
629
- await guard(context.say, `${pairingKey(pairing)}: following its sandbox`, async () => await followSandbox(context, pairing));
630
- }
631
524
  };
632
525
  const ORPHAN_SWEEP_EVERY_TICKS = 12;
633
526
  const CONTAINER_CHECK_EVERY_TICKS = 12;
@@ -695,7 +588,7 @@ const watchLoop = async (log, generation) => {
695
588
  if ((await watchTick(watch, tick)) === "stop") {
696
589
  return;
697
590
  }
698
- await sleep(POLL_MS, { signal: generation.signal });
591
+ await sleepUnlessWoken(POLL_MS, generation.signal);
699
592
  }
700
593
  };
701
594
  const startWatch = async (log, generation) => {
@@ -703,8 +596,15 @@ const startWatch = async (log, generation) => {
703
596
  const say = quieted(log);
704
597
  sayDaemonEventsTo(say);
705
598
  await guard(say, "moving off Mutagen's shared data directory", async () => await moveOffSharedMutagen(mutagen, say));
599
+ let retired = 0;
600
+ await guard(say, "retiring the pairings of this computer's old sandbox for folders", async () => {
601
+ retired = (await retireLegacyPairings(log)).length;
602
+ });
706
603
  const initial = await readState();
707
604
  if (initial.pairings.length === 0) {
605
+ if (retired > 0 && startMutagenDaemon(mutagen)) {
606
+ await guard(say, "retiring orphaned sessions", () => retireOrphanSessions(mutagen, [], say));
607
+ }
708
608
  log("no sandboxes are paired: nothing to mirror. Enable it from a sandbox's Desktop sync card.");
709
609
  return undefined;
710
610
  }
@@ -713,9 +613,9 @@ const startWatch = async (log, generation) => {
713
613
  }
714
614
  const sshConfig = pairingSshConfig(initial.pairings);
715
615
  await guard(say, "refreshing the ssh configuration", async () => await writeManagedSshConfig(sshConfig));
716
- const tunnels = createTunnelPool(say, generation.signal);
717
- generation.stopTransports = tunnels.stopAll;
718
616
  const bases = createDaemonBases(say);
617
+ const tunnels = createTunnelPool(say, generation.signal, (sandboxId) => bases.failed(sandboxId));
618
+ generation.stopTransports = tunnels.stopAll;
719
619
  await guard(say, "opening the sync transports", async () => await tunnels.reconcile(tunnelTargets(await dialedPairings(initial.pairings, bases))));
720
620
  const tracking = {
721
621
  rejectedPolls: new Map(),
@@ -726,7 +626,7 @@ const startWatch = async (log, generation) => {
726
626
  };
727
627
  generation.signal.addEventListener("abort", () => stopBridges(tracking), { once: true });
728
628
  const reportUnsupported = new Set();
729
- const fate = { mutagen, releaseForwards: async (sandboxId) => await retirePairingMirror(mutagen, sandboxId), say };
629
+ const fate = { releaseForwards: async (sandboxId) => await retirePairingMirror(mutagen, sandboxId), say };
730
630
  const gone = async (pairing) => await sandboxSaidGone(fate, pairing.sandboxId, "edge");
731
631
  const watch = {
732
632
  log,
@@ -745,11 +645,10 @@ const startWatch = async (log, generation) => {
745
645
  reportUnsupported,
746
646
  fate,
747
647
  gone,
748
- announce: announcer(mutagen, bases, reportUnsupported, tracking.unreachable, gone, say),
749
648
  holders: { dockerPorts: publishedPortsReader(say), freeSince: new Map() },
750
649
  sshConfig,
751
650
  };
752
- await prepareSessions(mutagen, initial.pairings, watch.sessionsPrepared, tracking.sessionsPending, watch.announce, { fate, relabels: relabelBudget(1), tracking }, say);
651
+ await prepareSessions(mutagen, initial.pairings, watch.sessionsPrepared, tracking.sessionsPending, { fate, relabels: relabelBudget(1), tracking }, say);
753
652
  await guard(say, "retiring orphaned sessions", () => retireOrphanSessions(mutagen, initial.pairings, say));
754
653
  log(`sync started; polling ${plural(initial.pairings.length, "paired sandbox")} every ${POLL_MS / 1000}s`);
755
654
  return watch;
@@ -779,7 +678,7 @@ const watchTick = async (watch, tick) => {
779
678
  await guard(say, "reconciling the sync transports", async () => await watch.tunnels.reconcile(tunnelTargets(dialed)));
780
679
  progress("the sync transports");
781
680
  const relabels = relabelBudget(1);
782
- await prepareSessions(mutagen, state.pairings, watch.sessionsPrepared, tracking.sessionsPending, watch.announce, { fate, relabels, tracking }, say);
681
+ await prepareSessions(mutagen, state.pairings, watch.sessionsPrepared, tracking.sessionsPending, { fate, relabels, tracking }, say);
783
682
  await retryPendingSessions(mutagen, state.pairings, tracking.sessionsPending, tick, relabels, say);
784
683
  progress("preparing file sync");
785
684
  if (!watch.live()) {
@@ -787,11 +686,11 @@ const watchTick = async (watch, tick) => {
787
686
  }
788
687
  let swapHeld = new Set();
789
688
  await guard(say, "holding file sync still during swaps", async () => {
790
- swapHeld = await holdSyncDuringSwaps(mutagen, state.pairings, tracking.unreachable, say);
689
+ swapHeld = await holdSyncDuringSwaps(state.pairings, tracking.unreachable, say);
791
690
  });
792
691
  let diskHeld = new Set();
793
692
  await guard(say, "measuring the drives synced folders are on", async () => {
794
- diskHeld = await guardDisks(mutagen, state.pairings, tick, say);
693
+ diskHeld = await guardDisks(state.pairings, say);
795
694
  });
796
695
  stopBridges(tracking, state.pairings);
797
696
  const context = {
@@ -810,10 +709,12 @@ const watchTick = async (watch, tick) => {
810
709
  pairings: state.pairings,
811
710
  say,
812
711
  };
813
- await runPasses(context, dialed, state.pairings, watch.live);
712
+ await runPasses(context, dialed, watch.live);
814
713
  if (!watch.live()) {
815
714
  return "stop";
816
715
  }
716
+ await guard(say, "holding file sync still where something holds it", async () => void (await holdSessions(mutagen, (await readState()).pairings, say)));
717
+ progress("holding file sync");
817
718
  if (tick % REPORT_EVERY_TICKS === 0) {
818
719
  await guard(say, "posting this machine's reports", async () => await postReports(dialed, mutagen, watch.reportUnsupported, watch.gone, say));
819
720
  }
@@ -831,7 +732,7 @@ const teardownForwards = async (mutagen, sandboxId) => {
831
732
  await runProcess(mutagen, ["forward", "terminate", ...names], { timeoutMs: MUTAGEN_CALL_TIMEOUT_MS });
832
733
  }
833
734
  await updateState((state) => ({
834
- pairings: state.pairings.map((held) => (sandboxId === undefined || held.sandboxId === sandboxId) && !isAttachedPairing(held)
735
+ pairings: state.pairings.map((held) => sandboxId === undefined || held.sandboxId === sandboxId
835
736
  ? { ...held, mirroredPorts: [], skippedPorts: [] }
836
737
  : held),
837
738
  }));
@@ -843,7 +744,7 @@ export const retireMirroredPort = async (mutagen, sandboxId, port) => {
843
744
  let wasMirrored = false;
844
745
  await updateState((state) => ({
845
746
  pairings: state.pairings.map((held) => {
846
- if (held.sandboxId !== sandboxId || isAttachedPairing(held)) {
747
+ if (held.sandboxId !== sandboxId) {
847
748
  return held;
848
749
  }
849
750
  const mirrored = held.mirroredPorts ?? [];