@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.
Files changed (271) hide show
  1. package/README.md +387 -33
  2. package/dist/commands.d.ts.map +1 -1
  3. package/dist/commands.js +2 -0
  4. package/dist/commands.js.map +1 -1
  5. package/dist/daemon-base.d.ts.map +1 -1
  6. package/dist/daemon-base.js +2 -3
  7. package/dist/daemon-base.js.map +1 -1
  8. package/dist/device/audit.d.ts +2 -0
  9. package/dist/device/audit.d.ts.map +1 -1
  10. package/dist/device/audit.js +23 -3
  11. package/dist/device/audit.js.map +1 -1
  12. package/dist/device/commands.d.ts +4 -0
  13. package/dist/device/commands.d.ts.map +1 -1
  14. package/dist/device/commands.js +7 -2
  15. package/dist/device/commands.js.map +1 -1
  16. package/dist/device/connection.d.ts +12 -1
  17. package/dist/device/connection.d.ts.map +1 -1
  18. package/dist/device/connection.js +77 -3
  19. package/dist/device/connection.js.map +1 -1
  20. package/dist/device/grant.d.ts +52 -0
  21. package/dist/device/grant.d.ts.map +1 -1
  22. package/dist/device/loopback-catch.d.ts +1 -1
  23. package/dist/device/loopback-catch.d.ts.map +1 -1
  24. package/dist/device/loopback-catch.js +5 -30
  25. package/dist/device/loopback-catch.js.map +1 -1
  26. package/dist/device/mcp.d.ts.map +1 -1
  27. package/dist/device/mcp.js +190 -30
  28. package/dist/device/mcp.js.map +1 -1
  29. package/dist/device/router.d.ts +52 -0
  30. package/dist/device/router.d.ts.map +1 -1
  31. package/dist/device/router.js +21 -0
  32. package/dist/device/router.js.map +1 -1
  33. package/dist/device/sandbox-rounds/auto-backup.d.ts +3 -1
  34. package/dist/device/sandbox-rounds/auto-backup.d.ts.map +1 -1
  35. package/dist/device/sandbox-rounds/auto-backup.js +14 -5
  36. package/dist/device/sandbox-rounds/auto-backup.js.map +1 -1
  37. package/dist/device/sandbox-rounds/auto-prepare.d.ts +3 -1
  38. package/dist/device/sandbox-rounds/auto-prepare.d.ts.map +1 -1
  39. package/dist/device/sandbox-rounds/auto-prepare.js +3 -3
  40. package/dist/device/sandbox-rounds/auto-prepare.js.map +1 -1
  41. package/dist/device/sandbox-rounds/in-flight.d.ts.map +1 -0
  42. package/dist/device/sandbox-rounds/in-flight.js.map +1 -0
  43. package/dist/device/sandbox-rounds/keeper.d.ts +26 -4
  44. package/dist/device/sandbox-rounds/keeper.d.ts.map +1 -1
  45. package/dist/device/sandbox-rounds/keeper.js +129 -31
  46. package/dist/device/sandbox-rounds/keeper.js.map +1 -1
  47. package/dist/device/sandbox-rounds/probation-watch.d.ts.map +1 -1
  48. package/dist/device/sandbox-rounds/probation-watch.js.map +1 -1
  49. package/dist/device/tools/android-parse.d.ts +69 -0
  50. package/dist/device/tools/android-parse.d.ts.map +1 -0
  51. package/dist/device/tools/android-parse.js +391 -0
  52. package/dist/device/tools/android-parse.js.map +1 -0
  53. package/dist/device/tools/android.d.ts +71 -0
  54. package/dist/device/tools/android.d.ts.map +1 -0
  55. package/dist/device/tools/android.js +534 -0
  56. package/dist/device/tools/android.js.map +1 -0
  57. package/dist/device/tools/apps.d.ts +3 -3
  58. package/dist/device/tools/apps.d.ts.map +1 -1
  59. package/dist/device/tools/apps.js +16 -6
  60. package/dist/device/tools/apps.js.map +1 -1
  61. package/dist/device/tools/describe.d.ts.map +1 -1
  62. package/dist/device/tools/describe.js +10 -5
  63. package/dist/device/tools/describe.js.map +1 -1
  64. package/dist/device/tools/device.d.ts +9 -15
  65. package/dist/device/tools/device.d.ts.map +1 -1
  66. package/dist/device/tools/device.js +34 -58
  67. package/dist/device/tools/device.js.map +1 -1
  68. package/dist/device/tools/elements.d.ts +13 -0
  69. package/dist/device/tools/elements.d.ts.map +1 -0
  70. package/dist/device/tools/elements.js +84 -0
  71. package/dist/device/tools/elements.js.map +1 -0
  72. package/dist/device/tools/ic-binary.d.ts +1 -0
  73. package/dist/device/tools/ic-binary.d.ts.map +1 -1
  74. package/dist/device/tools/ic-binary.js +40 -17
  75. package/dist/device/tools/ic-binary.js.map +1 -1
  76. package/dist/device/tools/sandboxes.d.ts +1 -1
  77. package/dist/device/tools/sandboxes.d.ts.map +1 -1
  78. package/dist/device/tools/sandboxes.js +9 -8
  79. package/dist/device/tools/sandboxes.js.map +1 -1
  80. package/dist/device/tools/shell.js +1 -1
  81. package/dist/device/tools/shell.js.map +1 -1
  82. package/dist/device/tools/view.d.ts +4 -0
  83. package/dist/device/tools/view.d.ts.map +1 -0
  84. package/dist/device/tools/view.js +4 -0
  85. package/dist/device/tools/view.js.map +1 -0
  86. package/dist/environments/children.js +1 -1
  87. package/dist/environments/children.js.map +1 -1
  88. package/dist/environments/commands.js +1 -1
  89. package/dist/environments/commands.js.map +1 -1
  90. package/dist/environments/crossing.d.ts +1 -1
  91. package/dist/environments/crossing.d.ts.map +1 -1
  92. package/dist/environments/machine.js +1 -1
  93. package/dist/environments/machine.js.map +1 -1
  94. package/dist/environments/wsl.d.ts.map +1 -0
  95. package/dist/environments/wsl.js.map +1 -0
  96. package/dist/resident.d.ts.map +1 -1
  97. package/dist/resident.js +14 -8
  98. package/dist/resident.js.map +1 -1
  99. package/dist/status.d.ts.map +1 -1
  100. package/dist/status.js +16 -13
  101. package/dist/status.js.map +1 -1
  102. package/dist/sync/attach-commands.d.ts +39 -0
  103. package/dist/sync/attach-commands.d.ts.map +1 -0
  104. package/dist/sync/attach-commands.js +152 -0
  105. package/dist/sync/attach-commands.js.map +1 -0
  106. package/dist/sync/commands.d.ts +8 -4
  107. package/dist/sync/commands.d.ts.map +1 -1
  108. package/dist/sync/commands.js +137 -79
  109. package/dist/sync/commands.js.map +1 -1
  110. package/dist/sync/config.d.ts +43 -9
  111. package/dist/sync/config.d.ts.map +1 -1
  112. package/dist/sync/config.js +86 -41
  113. package/dist/sync/config.js.map +1 -1
  114. package/dist/sync/endpoint.d.ts +8 -1
  115. package/dist/sync/endpoint.d.ts.map +1 -1
  116. package/dist/sync/endpoint.js +41 -1
  117. package/dist/sync/endpoint.js.map +1 -1
  118. package/dist/sync/environment.d.ts +11 -0
  119. package/dist/sync/environment.d.ts.map +1 -0
  120. package/dist/sync/environment.js +48 -0
  121. package/dist/sync/environment.js.map +1 -0
  122. package/dist/sync/exec.d.ts +1 -0
  123. package/dist/sync/exec.d.ts.map +1 -1
  124. package/dist/sync/exec.js +4 -5
  125. package/dist/sync/exec.js.map +1 -1
  126. package/dist/sync/folders.d.ts +3 -2
  127. package/dist/sync/folders.d.ts.map +1 -1
  128. package/dist/sync/folders.js +55 -2
  129. package/dist/sync/folders.js.map +1 -1
  130. package/dist/sync/forget-command.d.ts +22 -0
  131. package/dist/sync/forget-command.d.ts.map +1 -0
  132. package/dist/sync/forget-command.js +95 -0
  133. package/dist/sync/forget-command.js.map +1 -0
  134. package/dist/sync/git-bridge.d.ts +6 -0
  135. package/dist/sync/git-bridge.d.ts.map +1 -1
  136. package/dist/sync/git-bridge.js +64 -1
  137. package/dist/sync/git-bridge.js.map +1 -1
  138. package/dist/sync/gone-watch.d.ts +18 -0
  139. package/dist/sync/gone-watch.d.ts.map +1 -0
  140. package/dist/sync/gone-watch.js +142 -0
  141. package/dist/sync/gone-watch.js.map +1 -0
  142. package/dist/sync/gone.d.ts +22 -0
  143. package/dist/sync/gone.d.ts.map +1 -0
  144. package/dist/sync/gone.js +60 -0
  145. package/dist/sync/gone.js.map +1 -0
  146. package/dist/sync/local-sandboxes.d.ts +6 -0
  147. package/dist/sync/local-sandboxes.d.ts.map +1 -0
  148. package/dist/sync/local-sandboxes.js +26 -0
  149. package/dist/sync/local-sandboxes.js.map +1 -0
  150. package/dist/sync/mirror.d.ts +7 -0
  151. package/dist/sync/mirror.d.ts.map +1 -1
  152. package/dist/sync/mirror.js +434 -111
  153. package/dist/sync/mirror.js.map +1 -1
  154. package/dist/sync/mutagen.d.ts +50 -18
  155. package/dist/sync/mutagen.d.ts.map +1 -1
  156. package/dist/sync/mutagen.js +294 -80
  157. package/dist/sync/mutagen.js.map +1 -1
  158. package/dist/sync/project/adopt.d.ts +18 -0
  159. package/dist/sync/project/adopt.d.ts.map +1 -0
  160. package/dist/sync/project/adopt.js +86 -0
  161. package/dist/sync/project/adopt.js.map +1 -0
  162. package/dist/sync/{project-commands.d.ts → project/project-commands.d.ts} +4 -1
  163. package/dist/sync/project/project-commands.d.ts.map +1 -0
  164. package/dist/sync/{project-commands.js → project/project-commands.js} +10 -10
  165. package/dist/sync/project/project-commands.js.map +1 -0
  166. package/dist/sync/project/project-delivery.d.ts +23 -0
  167. package/dist/sync/project/project-delivery.d.ts.map +1 -0
  168. package/dist/sync/project/project-delivery.js +315 -0
  169. package/dist/sync/project/project-delivery.js.map +1 -0
  170. package/dist/sync/{project-files.d.ts → project/project-files.d.ts} +1 -1
  171. package/dist/sync/project/project-files.d.ts.map +1 -0
  172. package/dist/sync/{project-files.js → project/project-files.js} +5 -2
  173. package/dist/sync/project/project-files.js.map +1 -0
  174. package/dist/sync/{project-local.d.ts → project/project-local.d.ts} +1 -1
  175. package/dist/sync/project/project-local.d.ts.map +1 -0
  176. package/dist/sync/{project-local.js → project/project-local.js} +1 -1
  177. package/dist/sync/project/project-local.js.map +1 -0
  178. package/dist/sync/{project-remote.d.ts → project/project-remote.d.ts} +1 -1
  179. package/dist/sync/project/project-remote.d.ts.map +1 -0
  180. package/dist/sync/{project-remote.js → project/project-remote.js} +1 -1
  181. package/dist/sync/project/project-remote.js.map +1 -0
  182. package/dist/sync/{project-transfer.d.ts → project/project-transfer.d.ts} +20 -6
  183. package/dist/sync/project/project-transfer.d.ts.map +1 -0
  184. package/dist/sync/{project-transfer.js → project/project-transfer.js} +26 -23
  185. package/dist/sync/project/project-transfer.js.map +1 -0
  186. package/dist/sync/report.d.ts.map +1 -1
  187. package/dist/sync/report.js +13 -7
  188. package/dist/sync/report.js.map +1 -1
  189. package/dist/sync/restore-points.d.ts +15 -11
  190. package/dist/sync/restore-points.d.ts.map +1 -1
  191. package/dist/sync/restore-points.js +25 -24
  192. package/dist/sync/restore-points.js.map +1 -1
  193. package/dist/sync/retire.d.ts +14 -0
  194. package/dist/sync/retire.d.ts.map +1 -0
  195. package/dist/sync/retire.js +66 -0
  196. package/dist/sync/retire.js.map +1 -0
  197. package/dist/sync/siblings.d.ts +27 -0
  198. package/dist/sync/siblings.d.ts.map +1 -0
  199. package/dist/sync/siblings.js +112 -0
  200. package/dist/sync/siblings.js.map +1 -0
  201. package/dist/sync/ssh.d.ts +1 -0
  202. package/dist/sync/ssh.d.ts.map +1 -1
  203. package/dist/sync/ssh.js +37 -23
  204. package/dist/sync/ssh.js.map +1 -1
  205. package/dist/sync/tunnel.d.ts +6 -2
  206. package/dist/sync/tunnel.d.ts.map +1 -1
  207. package/dist/sync/tunnel.js +45 -48
  208. package/dist/sync/tunnel.js.map +1 -1
  209. package/dist/upkeep/doctor.d.ts +5 -0
  210. package/dist/upkeep/doctor.d.ts.map +1 -0
  211. package/dist/upkeep/doctor.js +73 -0
  212. package/dist/upkeep/doctor.js.map +1 -0
  213. package/dist/upkeep/entry.d.ts +26 -0
  214. package/dist/upkeep/entry.d.ts.map +1 -0
  215. package/dist/upkeep/entry.js +2 -0
  216. package/dist/upkeep/entry.js.map +1 -0
  217. package/dist/upkeep/login.d.ts +15 -0
  218. package/dist/upkeep/login.d.ts.map +1 -0
  219. package/dist/upkeep/login.js +128 -0
  220. package/dist/upkeep/login.js.map +1 -0
  221. package/dist/upkeep/manifest.d.ts +3 -0
  222. package/dist/upkeep/manifest.d.ts.map +1 -0
  223. package/dist/upkeep/manifest.js +6 -0
  224. package/dist/upkeep/manifest.js.map +1 -0
  225. package/dist/upkeep/reconcile.d.ts +45 -0
  226. package/dist/upkeep/reconcile.d.ts.map +1 -0
  227. package/dist/upkeep/reconcile.js +159 -0
  228. package/dist/upkeep/reconcile.js.map +1 -0
  229. package/dist/upkeep/retention.d.ts +10 -0
  230. package/dist/upkeep/retention.d.ts.map +1 -0
  231. package/dist/upkeep/retention.js +71 -0
  232. package/dist/upkeep/retention.js.map +1 -0
  233. package/dist/upkeep/retired.d.ts +12 -0
  234. package/dist/upkeep/retired.d.ts.map +1 -0
  235. package/dist/upkeep/retired.js +125 -0
  236. package/dist/upkeep/retired.js.map +1 -0
  237. package/dist/upkeep/second-ic.d.ts +4 -0
  238. package/dist/upkeep/second-ic.d.ts.map +1 -0
  239. package/dist/upkeep/second-ic.js +36 -0
  240. package/dist/upkeep/second-ic.js.map +1 -0
  241. package/dist/upkeep/trash.d.ts +9 -0
  242. package/dist/upkeep/trash.d.ts.map +1 -0
  243. package/dist/upkeep/trash.js +77 -0
  244. package/dist/upkeep/trash.js.map +1 -0
  245. package/dist/watchdog.d.ts +13 -0
  246. package/dist/watchdog.d.ts.map +1 -0
  247. package/dist/watchdog.js +38 -0
  248. package/dist/watchdog.js.map +1 -0
  249. package/package.json +9 -9
  250. package/dist/autostart/legacy-autostart.d.ts +0 -4
  251. package/dist/autostart/legacy-autostart.d.ts.map +0 -1
  252. package/dist/autostart/legacy-autostart.js +0 -33
  253. package/dist/autostart/legacy-autostart.js.map +0 -1
  254. package/dist/device/tools/in-flight.d.ts.map +0 -1
  255. package/dist/device/tools/in-flight.js.map +0 -1
  256. package/dist/sync/project-commands.d.ts.map +0 -1
  257. package/dist/sync/project-commands.js.map +0 -1
  258. package/dist/sync/project-files.d.ts.map +0 -1
  259. package/dist/sync/project-files.js.map +0 -1
  260. package/dist/sync/project-local.d.ts.map +0 -1
  261. package/dist/sync/project-local.js.map +0 -1
  262. package/dist/sync/project-remote.d.ts.map +0 -1
  263. package/dist/sync/project-remote.js.map +0 -1
  264. package/dist/sync/project-transfer.d.ts.map +0 -1
  265. package/dist/sync/project-transfer.js.map +0 -1
  266. package/dist/wsl.d.ts.map +0 -1
  267. package/dist/wsl.js.map +0 -1
  268. /package/dist/device/{tools → sandbox-rounds}/in-flight.d.ts +0 -0
  269. /package/dist/device/{tools → sandbox-rounds}/in-flight.js +0 -0
  270. /package/dist/{wsl.d.ts → environments/wsl.d.ts} +0 -0
  271. /package/dist/{wsl.js → environments/wsl.js} +0 -0
@@ -1,4 +1,5 @@
1
1
  import { spawnSync } from "node:child_process";
2
+ import { createHash } from "node:crypto";
2
3
  import { existsSync } from "node:fs";
3
4
  import { chmod, rm } from "node:fs/promises";
4
5
  import { join } from "node:path";
@@ -8,62 +9,149 @@ import { STATE_DIR, WORKSPACE_ROOT } from "@intentic/constants";
8
9
  import { clearWindowsRunValue, quotedCommandLine, setWindowsRunValue, stubCommand, windowsLaunchStub, } from "@intentic/local-agent";
9
10
  import { binDir } from "../config.js";
10
11
  import { archToken, download, exe, osToken, renameIfPresent } from "../release.js";
11
- import { isProjectPairing, mutagenDaemonLogPath, pairingRemoteDir, projectDirection } from "./config.js";
12
+ import { adoptFolder } from "./project/adopt.js";
13
+ import { clearAdoptFolder, isProjectPairing, mutagenDaemonLogPath, pairingKey, pairingRemoteDir, projectDirection } from "./config.js";
12
14
  import { dockerEndpointAnswers, liveIdentity, mutagenForwardUrl, mutagenUrl, pairingEndpoint } from "./endpoint.js";
15
+ import { labelValue, sessionOwner } from "./environment.js";
13
16
  import { runProcess } from "./exec.js";
17
+ import { projectShell, realProjectRunner, sandboxCopy } from "./project/project-remote.js";
14
18
  import { clearConflictResidue, sweepDerivedResidue } from "./residue.js";
15
19
  import { BACKUP_IGNORES, ignoresFor, mutagenSshPath, sanitizeId, sshTransportAnswers } from "./ssh.js";
16
20
  import { deviceSymlinks } from "./symlinks.js";
17
21
  const MUTAGEN_VERSION = "0.18.1";
22
+ export const MUTAGEN_CALL_TIMEOUT_MS = 60_000;
23
+ export const MUTAGEN_CREATE_TIMEOUT_MS = 5 * 60_000;
18
24
  const SESSION_PREFIX = "intentic-";
19
- export const sessionName = (sandboxId) => `${SESSION_PREFIX}${sanitizeId(sandboxId)}`;
20
- export const backupSessionName = (sandboxId) => `${sessionName(sandboxId)}-state`;
21
- export const syncSessionNames = (pairing) => isProjectPairing(pairing) ? [sessionName(pairing.sandboxId)] : [sessionName(pairing.sandboxId), backupSessionName(pairing.sandboxId)];
25
+ export const OWNER_LABEL = "intentic-owner";
26
+ const SANDBOX_LABEL = "intentic-sandbox";
27
+ const KIND_LABEL = "intentic-kind";
28
+ export const sessionLabels = (sandboxId, kind, owner = sessionOwner()) => ({
29
+ owner,
30
+ sandbox: labelValue(sanitizeId(sandboxId)),
31
+ kind,
32
+ });
33
+ export const labelArgs = (labels) => labels === undefined
34
+ ? []
35
+ : ["--label", `${OWNER_LABEL}=${labels.owner}`, "--label", `${SANDBOX_LABEL}=${labels.sandbox}`, "--label", `${KIND_LABEL}=${labels.kind}`];
36
+ export const ownershipOf = (labels, owner) => {
37
+ const said = labels?.[OWNER_LABEL];
38
+ return said === undefined || said === "" ? "unlabelled" : said === owner ? "ours" : "theirs";
39
+ };
40
+ export const sessionName = (key) => {
41
+ const at = key.lastIndexOf("~");
42
+ if (at === -1) {
43
+ return `${SESSION_PREFIX}${sanitizeId(key)}`;
44
+ }
45
+ const name = key.slice(at + 1);
46
+ const hash = createHash("sha256").update(name).digest("hex").slice(0, 8);
47
+ return `${SESSION_PREFIX}${sanitizeId(key.slice(0, at))}--${sanitizeId(name)}-${hash}`;
48
+ };
49
+ export const backupSessionName = (key) => `${sessionName(key)}-state`;
50
+ export const syncSessionNames = (pairing) => {
51
+ if (pairing.projectsHost === true) {
52
+ return [];
53
+ }
54
+ const key = pairingKey(pairing);
55
+ return isProjectPairing(pairing) ? [sessionName(key)] : [sessionName(key), backupSessionName(key)];
56
+ };
22
57
  const FORWARD_PREFIX = "intentic-fwd-";
23
58
  export const forwardSessionName = (sandboxId, port) => `${FORWARD_PREFIX}${sanitizeId(sandboxId)}-${port}`;
24
- const oursIn = (listed, prefix) => listed.split(/\s+/).filter((name) => name.startsWith(prefix));
59
+ const present = (text) => (text === undefined || text === "" ? undefined : text);
60
+ export const listedSessions = (listed) => listed
61
+ .split(/\s+/)
62
+ .filter((token) => token !== "")
63
+ .map((token) => {
64
+ const [name = "", owner, identifier] = token.split("@");
65
+ return { name, owner: present(owner), identifier: present(identifier) };
66
+ });
67
+ const isOurs = (session, prefix, owner) => session.name.startsWith(prefix) && (session.owner === undefined || owner === undefined || session.owner === owner);
68
+ const oursIn = (listed, prefix, owner) => listedSessions(listed)
69
+ .filter((session) => isOurs(session, prefix, owner))
70
+ .map((session) => session.name);
71
+ export const terminateTargetsIn = (listed, names, prefix, owner) => {
72
+ const wanted = new Set(names);
73
+ return listedSessions(listed)
74
+ .filter((session) => wanted.has(session.name) && isOurs(session, prefix, owner))
75
+ .map((session) => session.identifier ?? session.name)
76
+ .filter((target, index, all) => all.indexOf(target) === index);
77
+ };
25
78
  const FORWARD_NAME = new RegExp(`^${FORWARD_PREFIX}(.+)-(\\d+)$`);
26
- export const parseForwardNames = (listed, sandboxId) => {
27
- const names = oursIn(listed, FORWARD_PREFIX);
79
+ export const parseForwardNames = (listed, sandboxId, owner) => {
80
+ const names = oursIn(listed, FORWARD_PREFIX, owner);
28
81
  if (sandboxId === undefined) {
29
82
  return names;
30
83
  }
31
84
  const wanted = sanitizeId(sandboxId);
32
85
  return names.filter((name) => FORWARD_NAME.exec(name)?.[1] === wanted);
33
86
  };
34
- export const parseOrphanForwardNames = (listed, keptSandboxIds) => {
87
+ export const parseOrphanForwardNames = (listed, keptSandboxIds, owner) => {
35
88
  const kept = new Set(keptSandboxIds.map(sanitizeId));
36
- return parseForwardNames(listed).filter((name) => {
37
- const owner = FORWARD_NAME.exec(name)?.[1];
38
- return owner === undefined || !kept.has(owner);
89
+ return parseForwardNames(listed, undefined, owner).filter((name) => {
90
+ const held = FORWARD_NAME.exec(name)?.[1];
91
+ return held === undefined || !kept.has(held);
39
92
  });
40
93
  };
41
- export const parseOrphanSyncNames = (listed, keep) => {
94
+ export const parseOrphanSyncNames = (listed, keep, owner) => {
42
95
  const kept = new Set(keep);
43
- return oursIn(listed, SESSION_PREFIX).filter((name) => !kept.has(name));
96
+ return oursIn(listed, SESSION_PREFIX, owner).filter((name) => !kept.has(name));
97
+ };
98
+ const LIST_TEMPLATE = `{{range .}}{{.Name}}@{{index .Labels "${OWNER_LABEL}"}}@{{.Identifier}} {{end}}`;
99
+ const VERSION_MISMATCH = /version mismatch/i;
100
+ const DAEMON_RESTART_MS = 30 * 60_000;
101
+ let daemonRestartedAt = Number.NEGATIVE_INFINITY;
102
+ let daemonLog;
103
+ export const sayDaemonEventsTo = (log) => {
104
+ daemonLog = log;
105
+ };
106
+ export const restartsDaemonFor = (stderr, now, lastRestart) => VERSION_MISMATCH.test(stderr) && now - lastRestart >= DAEMON_RESTART_MS;
107
+ const recoverDaemon = (mutagen, stderr) => {
108
+ const now = Date.now();
109
+ if (!restartsDaemonFor(stderr, now, daemonRestartedAt)) {
110
+ return false;
111
+ }
112
+ daemonRestartedAt = now;
113
+ spawnSync(mutagen, ["daemon", "stop"], { stdio: "ignore", windowsHide: true, timeout: MUTAGEN_CALL_TIMEOUT_MS });
114
+ const started = spawnSync(mutagen, ["daemon", "start"], { stdio: "ignore", windowsHide: true, timeout: MUTAGEN_CALL_TIMEOUT_MS });
115
+ daemonLog?.(`the Mutagen daemon running here is another version than this agent's (${mutagen}); ${started.status === 0 ? "restarted it with this agent's copy" : "could not restart it"}. Its sessions are kept on disk and come back with it.`);
116
+ return started.status === 0;
117
+ };
118
+ const listCall = (mutagen, args) => {
119
+ const run = () => spawnSync(mutagen, [...args], { encoding: "utf8", windowsHide: true, timeout: MUTAGEN_CALL_TIMEOUT_MS });
120
+ const first = run();
121
+ if (first.status === 0) {
122
+ return first.stdout;
123
+ }
124
+ if (!recoverDaemon(mutagen, first.stderr ?? "")) {
125
+ return undefined;
126
+ }
127
+ const again = run();
128
+ return again.status === 0 ? again.stdout : undefined;
44
129
  };
45
- const listSessionNames = (mutagen, kind) => {
46
- const result = spawnSync(mutagen, [kind, "list", "--template", "{{range .}}{{.Name}} {{end}}"], { encoding: "utf8", windowsHide: true });
47
- return result.status === 0 ? result.stdout : "";
130
+ const listSessionNames = (mutagen, kind) => listCall(mutagen, [kind, "list", "--template", LIST_TEMPLATE]);
131
+ export const ourForwardSessions = (mutagen, sandboxId) => {
132
+ const listed = listSessionNames(mutagen, "forward");
133
+ return listed === undefined ? undefined : parseForwardNames(listed, sandboxId, sessionOwner());
48
134
  };
49
- export const ourForwardSessions = (mutagen, sandboxId) => parseForwardNames(listSessionNames(mutagen, "forward"), sandboxId);
50
- export const parseForwardPorts = (listed, sandboxId) => parseForwardNames(listed, sandboxId).flatMap((name) => {
135
+ export const parseForwardPorts = (listed, sandboxId, owner) => parseForwardNames(listed, sandboxId, owner).flatMap((name) => {
51
136
  const port = Number(FORWARD_NAME.exec(name)?.[2]);
52
137
  return Number.isInteger(port) ? [port] : [];
53
138
  });
54
- export const forwardedPorts = (mutagen, sandboxId) => parseForwardPorts(listSessionNames(mutagen, "forward"), sandboxId);
55
- const orphanForwardSessions = (mutagen, keptSandboxIds) => parseOrphanForwardNames(listSessionNames(mutagen, "forward"), keptSandboxIds);
139
+ export const forwardedPorts = (mutagen, sandboxId) => {
140
+ const listed = listSessionNames(mutagen, "forward");
141
+ return listed === undefined ? undefined : parseForwardPorts(listed, sandboxId, sessionOwner());
142
+ };
56
143
  export const mutagenForwardArgs = (args) => [
57
144
  "forward",
58
145
  "create",
59
146
  "--name",
60
147
  args.name,
148
+ ...labelArgs(args.labels),
61
149
  `tcp:127.0.0.1:${args.port}`,
62
150
  mutagenForwardUrl(args.remote, `tcp:${args.host.includes(":") ? `[${args.host}]` : args.host}:${args.port}`),
63
151
  ];
64
152
  export const syncMode = (pairing) => isProjectPairing(pairing) && projectDirection(pairing) === "to-sandbox" ? "one-way-safe" : "two-way-safe";
65
- export const sessionSpec = (pairing, symlinks) => ({
66
- name: sessionName(pairing.sandboxId),
153
+ export const sessionSpec = (pairing, symlinks, owner) => ({
154
+ name: sessionName(pairingKey(pairing)),
67
155
  localDir: pairing.localDir,
68
156
  remote: pairingEndpoint(pairing),
69
157
  remoteDir: pairingRemoteDir(pairing),
@@ -72,9 +160,10 @@ export const sessionSpec = (pairing, symlinks) => ({
72
160
  from: "local",
73
161
  symlinks,
74
162
  pollSeconds: SANDBOX_POLL_SECONDS,
163
+ ...(owner === undefined ? {} : { labels: sessionLabels(pairing.sandboxId, "sync", owner) }),
75
164
  });
76
- const backupSpec = (pairing, symlinks) => ({
77
- name: backupSessionName(pairing.sandboxId),
165
+ const backupSpec = (pairing, symlinks, owner) => ({
166
+ name: backupSessionName(pairingKey(pairing)),
78
167
  localDir: join(pairing.localDir, STATE_DIR),
79
168
  remote: pairingEndpoint(pairing),
80
169
  remoteDir: `${WORKSPACE_ROOT}/${STATE_DIR}`,
@@ -83,6 +172,7 @@ const backupSpec = (pairing, symlinks) => ({
83
172
  from: "sandbox",
84
173
  symlinks,
85
174
  pollSeconds: BACKUP_POLL_SECONDS,
175
+ ...(owner === undefined ? {} : { labels: sessionLabels(pairing.sandboxId, "state", owner) }),
86
176
  });
87
177
  const SANDBOX_POLL_SECONDS = 2;
88
178
  const SANDBOX_WATCH_MODE = "force-poll";
@@ -99,6 +189,7 @@ export const mutagenCreateArgs = (spec, paused) => {
99
189
  spec.name,
100
190
  "--sync-mode",
101
191
  spec.mode,
192
+ ...labelArgs(spec.labels),
102
193
  "--symlink-mode",
103
194
  spec.symlinks,
104
195
  ...(paused ? ["--paused"] : []),
@@ -117,8 +208,18 @@ export const liveProtocol = (end) => end.protocol ?? (end.host === undefined ? "
117
208
  const readSessions = (mutagen, name) => listSessions(mutagen, [name]);
118
209
  export const readAllSessions = (mutagen) => listSessions(mutagen, []);
119
210
  const listSessions = (mutagen, names) => {
120
- const result = spawnSync(mutagen, ["sync", "list", "--template", "{{json .}}", ...names], { encoding: "utf8", windowsHide: true });
121
- return result.status === 0 ? (JSON.parse(result.stdout) ?? []) : [];
211
+ const listed = listCall(mutagen, ["sync", "list", "--template", "{{json .}}"]);
212
+ if (listed === undefined) {
213
+ return undefined;
214
+ }
215
+ let sessions;
216
+ try {
217
+ sessions = JSON.parse(listed) ?? [];
218
+ }
219
+ catch {
220
+ return undefined;
221
+ }
222
+ return names.length === 0 ? sessions : sessions.filter((session) => session.name !== undefined && names.includes(session.name));
122
223
  };
123
224
  export const sessionsByName = (sessions) => {
124
225
  const byName = new Map();
@@ -168,11 +269,15 @@ const conflictedPath = (conflict) => {
168
269
  };
169
270
  };
170
271
  export const readSessionConflicts = (mutagen, name) => {
171
- const session = readSessions(mutagen, name)[0];
272
+ const sessions = readSessions(mutagen, name);
273
+ if (sessions === undefined) {
274
+ return "unanswered";
275
+ }
276
+ const session = sessions[0];
172
277
  return session === undefined ? undefined : { ignores: session.ignore.paths ?? [], conflicts: (session.conflicts ?? []).map(conflictedPath) };
173
278
  };
174
279
  export const flushSession = async (mutagen, name) => {
175
- await runProcess(mutagen, ["sync", "flush", "--skip-wait", name]);
280
+ await runProcess(mutagen, ["sync", "flush", "--skip-wait", name], { timeoutMs: MUTAGEN_CALL_TIMEOUT_MS });
176
281
  };
177
282
  export const conflictsFrom = (session) => {
178
283
  const listed = session.conflicts ?? [];
@@ -180,7 +285,7 @@ export const conflictsFrom = (session) => {
180
285
  const count = listed.length + (Number.isFinite(excluded) ? excluded : 0);
181
286
  return count === 0 ? undefined : { count, paths: listed.slice(0, CONFLICT_PATHS_MAX).map(conflictedPath) };
182
287
  };
183
- export const readSessionState = (mutagen, name) => sessionStateOf(readSessions(mutagen, name)[0]);
288
+ export const readSessionState = (mutagen, name) => sessionStateOf(readSessions(mutagen, name)?.[0]);
184
289
  export const sessionStateOf = (session) => {
185
290
  if (session === undefined) {
186
291
  return { exists: false };
@@ -195,41 +300,50 @@ export const sessionStateOf = (session) => {
195
300
  };
196
301
  };
197
302
  export const existingSyncSessions = (mutagen, names) => {
198
- const listed = new Set(oursIn(listSessionNames(mutagen, "sync"), SESSION_PREFIX));
199
- return names.filter((name) => listed.has(name));
303
+ const listed = listSessionNames(mutagen, "sync");
304
+ if (listed === undefined) {
305
+ return undefined;
306
+ }
307
+ const held = new Set(oursIn(listed, SESSION_PREFIX, sessionOwner()));
308
+ return names.filter((name) => held.has(name));
200
309
  };
201
310
  export const pauseRunningSync = (mutagen, pairing) => {
202
311
  if (pairing.mode !== "sync") {
203
312
  return false;
204
313
  }
205
314
  const names = existingSyncSessions(mutagen, syncSessionNames(pairing));
206
- if (names.every((name) => readSessionState(mutagen, name).paused === true)) {
315
+ if (names === undefined || names.every((name) => readSessionState(mutagen, name).paused === true)) {
207
316
  return false;
208
317
  }
209
- const result = spawnSync(mutagen, ["sync", "pause", ...names], { stdio: "ignore", windowsHide: true });
318
+ const result = spawnSync(mutagen, ["sync", "pause", ...names], { stdio: "ignore", windowsHide: true, timeout: MUTAGEN_CALL_TIMEOUT_MS });
210
319
  return result.status === 0;
211
320
  };
212
321
  export const pauseUnreachableSync = pauseRunningSync;
213
322
  export const resumeSwapPausedSync = (mutagen, pairing) => {
214
323
  const names = existingSyncSessions(mutagen, syncSessionNames(pairing));
215
- if (names.length === 0) {
324
+ if (names === undefined || names.length === 0) {
216
325
  return false;
217
326
  }
218
- return spawnSync(mutagen, ["sync", "resume", ...names], { stdio: "ignore", windowsHide: true }).status === 0;
327
+ return spawnSync(mutagen, ["sync", "resume", ...names], { stdio: "ignore", windowsHide: true, timeout: MUTAGEN_CALL_TIMEOUT_MS }).status === 0;
219
328
  };
220
329
  export const resumeAutoPausedSync = (mutagen, pairing) => {
221
330
  if (pairing.mode !== "sync" || pairing.fileSyncAutoPaused !== true) {
222
331
  return false;
223
332
  }
224
333
  const names = existingSyncSessions(mutagen, syncSessionNames(pairing));
225
- if (names.length === 0) {
334
+ if (names === undefined || names.length === 0) {
226
335
  return false;
227
336
  }
228
- const result = spawnSync(mutagen, ["sync", "resume", ...names], { stdio: "ignore", windowsHide: true });
337
+ const result = spawnSync(mutagen, ["sync", "resume", ...names], { stdio: "ignore", windowsHide: true, timeout: MUTAGEN_CALL_TIMEOUT_MS });
229
338
  return result.status === 0;
230
339
  };
340
+ export const resumeWatcherPausedSync = (mutagen, pairing) => resumeSwapPausedSync(mutagen, pairing);
231
341
  export const sameEnds = (session, spec) => {
232
- const local = { protocol: "local", host: undefined, path: spec.localDir };
342
+ const local = {
343
+ protocol: "local",
344
+ host: undefined,
345
+ path: spec.localDir,
346
+ };
233
347
  const remote = { ...liveIdentity(spec.remote), path: spec.remoteDir };
234
348
  const [alpha, beta] = spec.from === "local" ? [local, remote] : [remote, local];
235
349
  const matches = (live, wanted) => live.path === wanted.path && live.host === wanted.host && liveProtocol(live) === wanted.protocol;
@@ -248,41 +362,83 @@ export const sessionMatchesSpec = (session, spec) => {
248
362
  (session.ignore.paths ?? []).join("\n") === spec.ignores.join("\n") &&
249
363
  (session.symlink?.mode ?? "portable") === spec.symlinks);
250
364
  };
251
- export const ensureSyncSession = async (mutagen, pairing, log) => {
365
+ export const relabelBudget = (count) => {
366
+ let left = count;
367
+ return {
368
+ take: () => {
369
+ if (left <= 0) {
370
+ return false;
371
+ }
372
+ left -= 1;
373
+ return true;
374
+ },
375
+ };
376
+ };
377
+ const NO_RELABELS = { take: () => false };
378
+ export const ensureSyncSession = async (mutagen, pairing, log, relabels = NO_RELABELS) => {
252
379
  if (pairing.mode !== "sync" || pairing.localDir === undefined) {
253
380
  return true;
254
381
  }
255
382
  const held = { ...pairing, localDir: pairing.localDir };
383
+ if (pairing.adoptFolder === true && !(await adoptBeforeFirstSession(mutagen, held, log))) {
384
+ return false;
385
+ }
256
386
  const symlinks = await deviceSymlinks();
257
387
  if (symlinks.refusal !== undefined) {
258
- log(`${pairing.sandboxId}: this device cannot create symbolic links (${symlinks.refusal}), so its file sync leaves them out instead of failing on each one every cycle. On Windows, turning on Developer Mode lets the next start of this agent carry them.`);
388
+ log(`${pairingKey(pairing)}: this device cannot create symbolic links (${symlinks.refusal}), so its file sync leaves them out instead of failing on each one every cycle. On Windows, turning on Developer Mode lets the next start of this agent carry them.`);
259
389
  }
260
390
  let converged = true;
261
391
  const answers = transportAnswers(pairing);
262
- for (const spec of sessionSpecs(held, symlinks.mode)) {
263
- converged = (await convergeSession(mutagen, spec, log, answers)) && converged;
392
+ for (const spec of sessionSpecs(held, symlinks.mode, sessionOwner())) {
393
+ converged = (await convergeSession(mutagen, spec, log, answers, relabels)) && converged;
264
394
  }
265
395
  retireStrayBackup(mutagen, pairing, log);
266
396
  return converged;
267
397
  };
268
- export const sessionSpecs = (pairing, symlinks) => isProjectPairing(pairing) ? [sessionSpec(pairing, symlinks)] : [sessionSpec(pairing, symlinks), backupSpec(pairing, symlinks)];
269
- export const strayBackupSessions = (pairing, live) => isProjectPairing(pairing) ? live.filter((name) => name === backupSessionName(pairing.sandboxId)) : [];
398
+ const adoptBeforeFirstSession = async (mutagen, pairing, log) => {
399
+ const key = pairingKey(pairing);
400
+ const existing = existingSyncSessions(mutagen, [sessionName(key)]);
401
+ if (existing === undefined) {
402
+ return false;
403
+ }
404
+ if (existing.length === 0) {
405
+ try {
406
+ await adoptFolder({
407
+ localDir: pairing.localDir,
408
+ sandbox: sandboxCopy(realProjectRunner, projectShell(pairingEndpoint(pairing), mutagenSshPath(process.platform, process.env["MUTAGEN_SSH_PATH"])), pairingRemoteDir(pairing)),
409
+ ignores: ignoresFor(pairing),
410
+ log: (line) => log(`${key}: ${line}`),
411
+ });
412
+ }
413
+ catch (error) {
414
+ log(`${key}: ${pairing.localDir} could not be compared with the sandbox's copy (${errorMessage(error)}), so its file sync waits rather than start over two copies that disagree. Retrying later.`);
415
+ return false;
416
+ }
417
+ }
418
+ await clearAdoptFolder(key);
419
+ return true;
420
+ };
421
+ export const sessionSpecs = (pairing, symlinks, owner) => isProjectPairing(pairing)
422
+ ? [sessionSpec(pairing, symlinks, owner)]
423
+ : [sessionSpec(pairing, symlinks, owner), backupSpec(pairing, symlinks, owner)];
424
+ export const strayBackupSessions = (pairing, live) => isProjectPairing(pairing) ? live.filter((name) => name === backupSessionName(pairingKey(pairing))) : [];
270
425
  const retireStrayBackup = (mutagen, pairing, log) => {
271
- const stray = strayBackupSessions(pairing, existingSyncSessions(mutagen, [backupSessionName(pairing.sandboxId)]));
426
+ const live = existingSyncSessions(mutagen, [backupSessionName(pairingKey(pairing))]);
427
+ const stray = live === undefined ? [] : strayBackupSessions(pairing, live);
272
428
  if (stray.length === 0) {
273
429
  return;
274
430
  }
275
- spawnSync(mutagen, ["sync", "terminate", ...stray], { stdio: "ignore", windowsHide: true });
276
- log(`${pairing.sandboxId}: its folder is a project of yours, which carries no copy of the sandbox's state; terminated the state backup that was writing one into it.`);
431
+ spawnSync(mutagen, ["sync", "terminate", ...stray], { stdio: "ignore", windowsHide: true, timeout: MUTAGEN_CALL_TIMEOUT_MS });
432
+ log(`${pairingKey(pairing)}: its folder is a project of yours, which carries no copy of the sandbox's state; terminated the state backup that was writing one into it.`);
277
433
  };
278
434
  export const surplusSessions = (sessions) => sessions.slice(1).flatMap((session) => (session.identifier === undefined ? [] : [session.identifier]));
279
435
  const retireSurplus = (mutagen, name, log) => {
280
436
  const sessions = readSessions(mutagen, name);
281
- const surplus = surplusSessions(sessions);
282
- if (surplus.length === 0) {
437
+ const surplus = sessions === undefined ? [] : surplusSessions(sessions);
438
+ if (sessions === undefined || surplus.length === 0) {
283
439
  return sessions;
284
440
  }
285
- spawnSync(mutagen, ["sync", "terminate", ...surplus], { stdio: "ignore", windowsHide: true });
441
+ spawnSync(mutagen, ["sync", "terminate", ...surplus], { stdio: "ignore", windowsHide: true, timeout: MUTAGEN_CALL_TIMEOUT_MS });
286
442
  log(`${name}: ${sessions.length} sync sessions shared this name and flagged each other's writes as conflicts; kept the oldest and terminated the rest.`);
287
443
  return readSessions(mutagen, name);
288
444
  };
@@ -291,7 +447,10 @@ export const convergePlan = (sessions, spec) => {
291
447
  return "create";
292
448
  }
293
449
  const only = sessions.length === 1 ? sessions[0] : undefined;
294
- return only !== undefined && sessionMatchesSpec(only, spec) ? "keep" : "replace";
450
+ if (only === undefined || !sessionMatchesSpec(only, spec)) {
451
+ return "replace";
452
+ }
453
+ return spec.labels !== undefined && ownershipOf(only.labels, spec.labels.owner) === "unlabelled" ? "relabel" : "keep";
295
454
  };
296
455
  export const settlesFirst = (spec, live) => spec.mode === "two-way-safe" && live.every((session) => liveMode(session) === "two-way-safe");
297
456
  const readyForReplacement = async (mutagen, spec, live, log) => {
@@ -300,6 +459,9 @@ const readyForReplacement = async (mutagen, spec, live, log) => {
300
459
  }
301
460
  await flushSession(mutagen, spec.name);
302
461
  const held = readSessionConflicts(mutagen, spec.name);
462
+ if (held === "unanswered") {
463
+ return false;
464
+ }
303
465
  const cleared = held === undefined || held.conflicts.length === 0
304
466
  ? { standing: 0 }
305
467
  : await clearConflictResidue({ root: spec.localDir, conflicts: held.conflicts, ignores: held.ignores, log });
@@ -330,12 +492,31 @@ export const transportAnswers = (pairing) => {
330
492
  ? async () => await dockerEndpointAnswers(endpoint.container, pairing.sandboxUrl)
331
493
  : async () => await sshTransportAnswers(mutagenSshPath(process.platform, process.env["MUTAGEN_SSH_PATH"]), endpoint.alias);
332
494
  };
333
- const convergeSession = async (mutagen, spec, log, answers) => {
495
+ export const terminationTargets = (sessions, name) => sessions.length > 0 && sessions.every((session) => session.identifier !== undefined)
496
+ ? sessions.flatMap((session) => session.identifier ?? [])
497
+ : [name];
498
+ const convergeSession = async (mutagen, spec, log, answers, relabels) => {
499
+ const listed = readSessions(mutagen, spec.name);
500
+ if (listed === undefined) {
501
+ log(`${spec.name}: Mutagen did not answer a listing, so this session is left as it is this pass. Retrying later.`);
502
+ return false;
503
+ }
504
+ const owner = spec.labels?.owner;
505
+ if (owner !== undefined && listed.some((session) => ownershipOf(session.labels, owner) === "theirs")) {
506
+ log(`${spec.name}: a session of this name belongs to another owner (${OWNER_LABEL} says so), so this one is neither replaced nor made beside it.`);
507
+ return false;
508
+ }
334
509
  const sessions = retireSurplus(mutagen, spec.name, log);
510
+ if (sessions === undefined) {
511
+ return false;
512
+ }
335
513
  const plan = convergePlan(sessions, spec);
336
514
  if (plan === "keep") {
337
515
  return true;
338
516
  }
517
+ if (plan === "relabel" && !relabels.take()) {
518
+ return false;
519
+ }
339
520
  const live = sessions.length === 1 ? sessions[0] : undefined;
340
521
  if (sessions.length > 1) {
341
522
  log(`${spec.name}: ${sessions.length} sync sessions share this name, which conflict with each other; replacing them with one.`);
@@ -344,7 +525,7 @@ const convergeSession = async (mutagen, spec, log, answers) => {
344
525
  log(`${spec.name}: ${spec.remote.container} is not running on this machine's Docker engine as this sandbox, so no file sync is made into it. Retrying later.`);
345
526
  return false;
346
527
  }
347
- if (plan === "replace") {
528
+ if (plan === "replace" || plan === "relabel") {
348
529
  if (!(await answers())) {
349
530
  log(`${spec.name}: the sandbox is not answering, so its existing file sync is left running as it is rather than terminated for a replacement that cannot be created. Retrying later.`);
350
531
  return false;
@@ -352,8 +533,14 @@ const convergeSession = async (mutagen, spec, log, answers) => {
352
533
  if (!(await readyForReplacement(mutagen, spec, sessions, log))) {
353
534
  return false;
354
535
  }
355
- log(`${spec.name}: the running sync session does not match this build's rules (its ignores, its folder, which way it syncs, or what it does with symbolic links): recreating it so they apply, as ${spec.mode}. This starts the comparison from scratch.`);
356
- spawnSync(mutagen, ["sync", "terminate", spec.name], { stdio: "ignore", windowsHide: true });
536
+ log(plan === "relabel"
537
+ ? `${spec.name}: made before this agent labelled its sessions, and Mutagen cannot label a session that exists: recreating it with this machine's ownership labels, as ${spec.mode}. Both sides are rescanned; nothing that already matches is copied.`
538
+ : `${spec.name}: the running sync session does not match this build's rules (its ignores, its folder, which way it syncs, or what it does with symbolic links): recreating it so they apply, as ${spec.mode}. This starts the comparison from scratch.`);
539
+ spawnSync(mutagen, ["sync", "terminate", ...terminationTargets(sessions, spec.name)], {
540
+ stdio: "ignore",
541
+ windowsHide: true,
542
+ timeout: MUTAGEN_CALL_TIMEOUT_MS,
543
+ });
357
544
  }
358
545
  await runMutagenAsync(mutagen, mutagenCreateArgs(spec, live?.paused === true), log);
359
546
  return true;
@@ -367,47 +554,61 @@ export const healDerivedConflicts = async (mutagen, pairing, log, asked = false)
367
554
  if (!asked && (pairing.autoHealOff === true || pairing.fileSyncAutoPaused === true)) {
368
555
  return idle;
369
556
  }
370
- const held = readSessionConflicts(mutagen, sessionName(pairing.sandboxId));
371
- if (held === undefined || held.conflicts.length === 0) {
557
+ const held = readSessionConflicts(mutagen, sessionName(pairingKey(pairing)));
558
+ if (held === undefined || held === "unanswered" || held.conflicts.length === 0) {
372
559
  return idle;
373
560
  }
374
561
  const outcome = await clearConflictResidue({ root: pairing.localDir, conflicts: held.conflicts, ignores: held.ignores, log });
375
562
  if (outcome.removed.length > 0) {
376
- await flushSession(mutagen, sessionName(pairing.sandboxId));
563
+ await flushSession(mutagen, sessionName(pairingKey(pairing)));
377
564
  }
378
565
  return outcome;
379
566
  };
380
- export const retireOrphanSessions = (mutagen, pairings, log) => {
381
- const ids = pairings.map((pairing) => pairing.sandboxId);
382
- const sessions = parseOrphanSyncNames(listSessionNames(mutagen, "sync"), pairings.flatMap((pairing) => syncSessionNames(pairing)));
383
- if (sessions.length > 0) {
384
- spawnSync(mutagen, ["sync", "terminate", ...sessions], { stdio: "ignore", windowsHide: true });
567
+ export const retireOrphanSessions = (mutagen, pairings, log, owner = sessionOwner()) => {
568
+ const listedSync = listSessionNames(mutagen, "sync");
569
+ const sessions = listedSync === undefined
570
+ ? []
571
+ : parseOrphanSyncNames(listedSync, pairings.flatMap((pairing) => syncSessionNames(pairing)), owner);
572
+ if (listedSync !== undefined && sessions.length > 0) {
573
+ spawnSync(mutagen, ["sync", "terminate", ...terminateTargetsIn(listedSync, sessions, SESSION_PREFIX, owner)], {
574
+ stdio: "ignore",
575
+ windowsHide: true,
576
+ timeout: MUTAGEN_CALL_TIMEOUT_MS,
577
+ });
385
578
  log(`retired ${plural(sessions.length, "file-sync session")} belonging to sandboxes this machine no longer pairs.`);
386
579
  }
387
- const forwards = orphanForwardSessions(mutagen, ids);
388
- if (forwards.length > 0) {
389
- spawnSync(mutagen, ["forward", "terminate", ...forwards], { stdio: "ignore", windowsHide: true });
580
+ const listedForwards = listSessionNames(mutagen, "forward");
581
+ const forwards = listedForwards === undefined
582
+ ? []
583
+ : parseOrphanForwardNames(listedForwards, pairings.map((pairing) => pairing.sandboxId), owner);
584
+ if (listedForwards !== undefined && forwards.length > 0) {
585
+ spawnSync(mutagen, ["forward", "terminate", ...terminateTargetsIn(listedForwards, forwards, FORWARD_PREFIX, owner)], {
586
+ stdio: "ignore",
587
+ windowsHide: true,
588
+ timeout: MUTAGEN_CALL_TIMEOUT_MS,
589
+ });
390
590
  log(`released ${plural(forwards.length, "port forward")} left holding localhost for sandboxes this machine no longer pairs.`);
391
591
  }
392
592
  };
393
593
  const installedVersion = (binary, versionArgs) => {
394
- const result = spawnSync(binary, versionArgs, { encoding: "utf8", windowsHide: true });
594
+ const result = spawnSync(binary, versionArgs, { encoding: "utf8", windowsHide: true, timeout: MUTAGEN_CALL_TIMEOUT_MS });
395
595
  if (result.error !== undefined || result.status !== 0) {
396
596
  return undefined;
397
597
  }
398
598
  return /\d+\.\d+\.\d+/.exec(result.stdout)?.[0];
399
599
  };
400
600
  const extractTarball = (tarball) => {
401
- const extract = spawnSync("tar", ["-xzf", tarball, "-C", binDir], { stdio: "inherit", windowsHide: true });
601
+ const extract = spawnSync("tar", ["-xzf", tarball, "-C", binDir], { stdio: "inherit", windowsHide: true, timeout: 2 * 60_000 });
402
602
  if (extract.status !== 0) {
403
603
  throw new Error(`failed to extract ${tarball}: tar's own reason is above (no \`tar\` on PATH, or a file it must replace is in use)`);
404
604
  }
405
605
  };
606
+ const PATH_LOOKUP_TIMEOUT_MS = 10_000;
406
607
  const resolveOnPath = (command) => {
407
608
  const whereExe = join(process.env["SystemRoot"] ?? "C:\\Windows", "System32", "where.exe");
408
609
  const found = process.platform === "win32"
409
- ? spawnSync(whereExe, [command], { encoding: "utf8", windowsHide: true })
410
- : spawnSync("sh", ["-c", `command -v ${command}`], { encoding: "utf8" });
610
+ ? spawnSync(whereExe, [command], { encoding: "utf8", windowsHide: true, timeout: PATH_LOOKUP_TIMEOUT_MS })
611
+ : spawnSync("sh", ["-c", `command -v ${command}`], { encoding: "utf8", timeout: PATH_LOOKUP_TIMEOUT_MS });
411
612
  const first = found.status === 0 ? found.stdout.split("\n")[0]?.trim() : undefined;
412
613
  return first === undefined || first === "" || !existsSync(first) ? undefined : first;
413
614
  };
@@ -416,16 +617,30 @@ export const isOwnMutagen = (mutagen, own = ownMutagenPath(), platform = process
416
617
  const fold = (path) => (platform === "win32" ? path.replaceAll("/", "\\").toLowerCase() : path);
417
618
  return fold(mutagen) === fold(own);
418
619
  };
620
+ export const userMutagenOnPath = () => {
621
+ const found = resolveOnPath("mutagen");
622
+ return found === undefined || isOwnMutagen(found) ? undefined : found;
623
+ };
419
624
  export const ensureMutagen = async () => {
420
- const own = resolveOnPath("mutagen");
421
- if (own !== undefined && installedVersion(own, ["version"]) !== undefined) {
422
- return own;
423
- }
424
625
  const dest = ownMutagenPath();
425
626
  if (installedVersion(dest, ["version"]) === MUTAGEN_VERSION) {
426
627
  return dest;
427
628
  }
428
- spawnSync(dest, ["daemon", "stop"], { stdio: "ignore", windowsHide: true });
629
+ try {
630
+ await installPinnedMutagen(dest);
631
+ return dest;
632
+ }
633
+ catch (error) {
634
+ const onPath = resolveOnPath("mutagen");
635
+ if (onPath !== undefined && installedVersion(onPath, ["version"]) !== undefined) {
636
+ daemonLog?.(`this agent's own Mutagen ${MUTAGEN_VERSION} could not be fetched (${errorMessage(error)}); using ${onPath} until it can.`);
637
+ return onPath;
638
+ }
639
+ throw error;
640
+ }
641
+ };
642
+ const installPinnedMutagen = async (dest) => {
643
+ spawnSync(dest, ["daemon", "stop"], { stdio: "ignore", windowsHide: true, timeout: MUTAGEN_CALL_TIMEOUT_MS });
429
644
  const tarball = join(binDir, "mutagen.tar.gz");
430
645
  await download(`https://github.com/mutagen-io/mutagen/releases/download/v${MUTAGEN_VERSION}/mutagen_${osToken()}_${archToken()}_v${MUTAGEN_VERSION}.tar.gz`, tarball);
431
646
  const displaced = `${dest}.old`;
@@ -434,10 +649,9 @@ export const ensureMutagen = async () => {
434
649
  await chmod(dest, 0o755);
435
650
  await rm(displaced, { force: true }).catch(() => undefined);
436
651
  await rm(tarball, { force: true }).catch(() => undefined);
437
- return dest;
438
652
  };
439
653
  export const runMutagenAsync = async (mutagen, args, log) => {
440
- const result = await runProcess(mutagen, args);
654
+ const result = await runProcess(mutagen, args, { timeoutMs: MUTAGEN_CREATE_TIMEOUT_MS });
441
655
  const said = `${result.stdout}${result.stderr}`.trim();
442
656
  if (result.status === 0) {
443
657
  return;
@@ -448,7 +662,7 @@ export const runMutagenAsync = async (mutagen, args, log) => {
448
662
  throw new Error(`mutagen ${args[0] ?? ""} exited with code ${result.status}`);
449
663
  };
450
664
  export const runMutagen = (mutagen, args) => {
451
- const result = spawnSync(mutagen, args, { stdio: "inherit", windowsHide: true });
665
+ const result = spawnSync(mutagen, args, { stdio: "inherit", windowsHide: true, timeout: MUTAGEN_CREATE_TIMEOUT_MS });
452
666
  if (result.error !== undefined) {
453
667
  throw result.error;
454
668
  }
@@ -468,7 +682,7 @@ export const registerMutagenAutostart = (mutagen, launcher, log) => {
468
682
  runMutagen(mutagen, ["daemon", "register"]);
469
683
  return;
470
684
  }
471
- spawnSync(mutagen, ["daemon", "unregister"], { stdio: "ignore", windowsHide: true });
685
+ spawnSync(mutagen, ["daemon", "unregister"], { stdio: "ignore", windowsHide: true, timeout: MUTAGEN_CALL_TIMEOUT_MS });
472
686
  setWindowsRunValue(MUTAGEN_RUN_VALUE, quotedCommandLine(stubCommand(stub, mutagenDaemonLogPath, [mutagen, "daemon", "start"])));
473
687
  }
474
688
  catch (error) {
@@ -480,7 +694,7 @@ export const unregisterMutagenAutostart = (mutagen) => {
480
694
  clearWindowsRunValue(MUTAGEN_RUN_VALUE);
481
695
  }
482
696
  if (process.platform !== "linux") {
483
- spawnSync(mutagen, ["daemon", "unregister"], { stdio: "ignore", windowsHide: true });
697
+ spawnSync(mutagen, ["daemon", "unregister"], { stdio: "ignore", windowsHide: true, timeout: MUTAGEN_CALL_TIMEOUT_MS });
484
698
  }
485
699
  };
486
700
  //# sourceMappingURL=mutagen.js.map