local-operator-ui 0.29.0 → 0.29.2

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 (69) hide show
  1. package/out/main/index.js +418 -143
  2. package/out/renderer/assets/{_basePickBy--1nrQ8Hz.js → _basePickBy-C9qLySi4.js} +1 -1
  3. package/out/renderer/assets/{_baseUniq-CIl_ZKBb.js → _baseUniq-BjBdobRd.js} +1 -1
  4. package/out/renderer/assets/{agent-details-page-SAU23J1h.js → agent-details-page-B_AapBwI.js} +2 -2
  5. package/out/renderer/assets/{agent-hub-page-FBbZyDG1.js → agent-hub-page-DJtExkiE.js} +2 -2
  6. package/out/renderer/assets/{agents-page-DUHwVHUN.js → agents-page-KHk6v504.js} +2 -2
  7. package/out/renderer/assets/{arc-VjtCFy-V.js → arc-Cui5tHbV.js} +1 -1
  8. package/out/renderer/assets/{architectureDiagram-IEHRJDOE-U5Plnyki.js → architectureDiagram-IEHRJDOE-DoQgwV9a.js} +1 -1
  9. package/out/renderer/assets/{blockDiagram-JOT3LUYC-BUFzKhGE.js → blockDiagram-JOT3LUYC-DNBIKuj6.js} +1 -1
  10. package/out/renderer/assets/browser-page-DtcRd0cQ.js +1 -0
  11. package/out/renderer/assets/{c4Diagram-VJAJSXHY-QNK7Lnt9.js → c4Diagram-VJAJSXHY-DlxOmlVm.js} +1 -1
  12. package/out/renderer/assets/channel-8AJlXpIq.js +1 -0
  13. package/out/renderer/assets/{chunk-4BMEZGHF-BkvMXI70.js → chunk-4BMEZGHF-B7-pfcKD.js} +1 -1
  14. package/out/renderer/assets/{chunk-A2AXSNBT-CWRaCKQM.js → chunk-A2AXSNBT-Bq3x3bfB.js} +1 -1
  15. package/out/renderer/assets/{chunk-AEK57VVT-BLIYWGRO.js → chunk-AEK57VVT-D6VZ66Au.js} +1 -1
  16. package/out/renderer/assets/{chunk-D6G4REZN-BVfOPcBu.js → chunk-D6G4REZN-B5nxs9x8.js} +1 -1
  17. package/out/renderer/assets/{chunk-RZ5BOZE2-UUEZsBdp.js → chunk-RZ5BOZE2-C62EhzfA.js} +1 -1
  18. package/out/renderer/assets/{chunk-XZIHB7SX-Dyrks1Q5.js → chunk-XZIHB7SX-aMJiddzK.js} +1 -1
  19. package/out/renderer/assets/classDiagram-GIVACNV2-BziEYnmd.js +1 -0
  20. package/out/renderer/assets/classDiagram-v2-COTLJTTW-BziEYnmd.js +1 -0
  21. package/out/renderer/assets/clone-GQ-cyvhB.js +1 -0
  22. package/out/renderer/assets/{compact-pagination-BZGdtI9l.js → compact-pagination-FhHxf_Eu.js} +1 -1
  23. package/out/renderer/assets/{dagre-OKDRZEBW-DmyjQpEm.js → dagre-OKDRZEBW-BGH7ioeC.js} +1 -1
  24. package/out/renderer/assets/{diagram-SSKATNLV-Dvs6GT39.js → diagram-SSKATNLV-DGsrHy_U.js} +1 -1
  25. package/out/renderer/assets/{diagram-VNBRO52H-CdMcENf8.js → diagram-VNBRO52H-A7hPXIso.js} +1 -1
  26. package/out/renderer/assets/{erDiagram-Q7BY3M3F-DRk_quuH.js → erDiagram-Q7BY3M3F-BWLPSJ_9.js} +1 -1
  27. package/out/renderer/assets/{flowDiagram-4HSFHLVR-Dyx7rgOH.js → flowDiagram-4HSFHLVR-CAHT0JfL.js} +1 -1
  28. package/out/renderer/assets/{ganttDiagram-APWFNJXF-BKgI7FyT.js → ganttDiagram-APWFNJXF-8FI3J_q0.js} +1 -1
  29. package/out/renderer/assets/{gitGraphDiagram-7IBYFJ6S-CZY1NaZO.js → gitGraphDiagram-7IBYFJ6S-cpqr3qwe.js} +1 -1
  30. package/out/renderer/assets/{graph-BMAzdbIf.js → graph-DcyNrb-k.js} +1 -1
  31. package/out/renderer/assets/{icon-Y8w93nKk.css → icon-GhtZ9eY_.css} +1 -1
  32. package/out/renderer/assets/{index-BmiDOvs8.js → index-1aumAIKD.js} +1 -1
  33. package/out/renderer/assets/{index-D0o2SKci.js → index-BIjm9xg9.js} +1 -1
  34. package/out/renderer/assets/{index-BSf4qKVc.js → index-DwEheJBg.js} +391 -419
  35. package/out/renderer/assets/{infoDiagram-PH2N3AL5-CsWfudmo.js → infoDiagram-PH2N3AL5-DsYwEtIa.js} +1 -1
  36. package/out/renderer/assets/{installer-DbeF2a73.js → installer-ugVSuxQK.js} +1 -1
  37. package/out/renderer/assets/{journeyDiagram-U35MCT3I-Z04EcqBd.js → journeyDiagram-U35MCT3I-BQk-3VkJ.js} +1 -1
  38. package/out/renderer/assets/{kanban-definition-NDS4AKOZ-BIP64FFY.js → kanban-definition-NDS4AKOZ-BlN92WlQ.js} +1 -1
  39. package/out/renderer/assets/{layout-Df_GsS2N.js → layout-DSFPeg4W.js} +1 -1
  40. package/out/renderer/assets/{legacy-agents-page-CIrGO_6M.js → legacy-agents-page-CNgIc1H2.js} +5 -5
  41. package/out/renderer/assets/{mermaid.core-50Aa46l1.js → mermaid.core-Csj0_Xl7.js} +5 -5
  42. package/out/renderer/assets/{mindmap-definition-ALO5MXBD-CnCk-gns.js → mindmap-definition-ALO5MXBD-Bqbt4rKL.js} +1 -1
  43. package/out/renderer/assets/{page-header-hdhRg_jg.js → page-header-CUZjto_D.js} +1 -1
  44. package/out/renderer/assets/parseISO-BYhaeu0l.js +6 -0
  45. package/out/renderer/assets/{pieDiagram-IB7DONF6-WKzdnNpM.js → pieDiagram-IB7DONF6-DBEASiho.js} +1 -1
  46. package/out/renderer/assets/{quadrantDiagram-7GDLP6J5-CLkOTUOn.js → quadrantDiagram-7GDLP6J5-19bwKsb5.js} +1 -1
  47. package/out/renderer/assets/{radar-MK3ICKWK-C4tKiExL.js → radar-MK3ICKWK-7gH_bYXp.js} +1 -1
  48. package/out/renderer/assets/{radient-auth-buttons-DWFoNRqo.js → radient-auth-buttons-BTLeKmb7.js} +2 -2
  49. package/out/renderer/assets/{requirementDiagram-KVF5MWMF-CVi3ekXN.js → requirementDiagram-KVF5MWMF-DxdQEclc.js} +1 -1
  50. package/out/renderer/assets/{sankeyDiagram-QLVOVGJD-BHuodNrM.js → sankeyDiagram-QLVOVGJD-DMJiKfyS.js} +1 -1
  51. package/out/renderer/assets/{schedules-page-DHlixzhn.js → schedules-page-Szw182RW.js} +6 -6
  52. package/out/renderer/assets/{sequenceDiagram-X6HHIX6F-A8v49VTH.js → sequenceDiagram-X6HHIX6F-0-YWl2Dx.js} +1 -1
  53. package/out/renderer/assets/{settings-page-CZi7TtEG.js → settings-page-6PGUFRNP.js} +71 -66
  54. package/out/renderer/assets/{stateDiagram-DGXRK772-DmX3aEhB.js → stateDiagram-DGXRK772-CH3feh2C.js} +1 -1
  55. package/out/renderer/assets/stateDiagram-v2-YXO3MK2T-B4A_-d8Q.js +1 -0
  56. package/out/renderer/assets/{timeline-definition-BDJGKUSR-PlVSNy-D.js → timeline-definition-BDJGKUSR-jjkJ2-x_.js} +1 -1
  57. package/out/renderer/assets/{use-agent-like-mutation-nIpQtB-p.js → use-agent-like-mutation-COpJgvhn.js} +1 -1
  58. package/out/renderer/assets/{xychartDiagram-VJFVF3MP-BJWQkXTM.js → xychartDiagram-VJFVF3MP-Bk9pfVm-.js} +1 -1
  59. package/out/renderer/index.html +11 -4
  60. package/out/renderer/installer.html +3 -3
  61. package/package.json +3 -2
  62. package/out/renderer/assets/browser-page-C4aIl48r.js +0 -1
  63. package/out/renderer/assets/channel-BYLqClNb.js +0 -1
  64. package/out/renderer/assets/classDiagram-GIVACNV2-D01h7e3_.js +0 -1
  65. package/out/renderer/assets/classDiagram-v2-COTLJTTW-D01h7e3_.js +0 -1
  66. package/out/renderer/assets/clone-D8rnVSgS.js +0 -1
  67. package/out/renderer/assets/parseISO-Cit1EWS-.js +0 -6
  68. package/out/renderer/assets/stateDiagram-v2-YXO3MK2T-MojYDBV8.js +0 -1
  69. /package/out/renderer/assets/{icon-02RQhVJ6.js → icon-jiDg9XP5.js} +0 -0
package/out/main/index.js CHANGED
@@ -2360,6 +2360,9 @@ const WATCHDOG_TIMEOUT_SECONDS = 600;
2360
2360
  const WATCHDOG_HARD_TIMEOUT_SECONDS = 1800;
2361
2361
  const PENDING_INSTALL_RECENCY_MARGIN_SECONDS = 300;
2362
2362
  const PENDING_INSTALL_RECENCY_SECONDS = WATCHDOG_HARD_TIMEOUT_SECONDS + PENDING_INSTALL_RECENCY_MARGIN_SECONDS;
2363
+ const LAUNCH_HOLD_END_MARGIN_SECONDS = 60;
2364
+ const PENDING_INSTALL_LAUNCH_HOLD_SECONDS = WATCHDOG_HARD_TIMEOUT_SECONDS - LAUNCH_HOLD_END_MARGIN_SECONDS;
2365
+ const PENDING_INSTALL_HANDOFF_SECONDS = 30;
2363
2366
  const PLIST_READ_TIMEOUT_SECONDS = 5;
2364
2367
  const WATCHDOG_TOKEN = "local-operator-update-watchdog";
2365
2368
  const BAD_BUNDLE_MARKERS = [
@@ -2851,14 +2854,53 @@ function pendingInstallAgeSeconds(marker, now = Date.now()) {
2851
2854
  return (now - started) / 1e3;
2852
2855
  }
2853
2856
  function isInstallInFlight(input) {
2854
- if (!input.marker || !input.jobLoaded) return false;
2857
+ if (!input.marker) return false;
2855
2858
  const age = pendingInstallAgeSeconds(input.marker, input.now);
2856
2859
  if (age === null) return false;
2857
- return age >= 0 && age <= PENDING_INSTALL_RECENCY_SECONDS;
2860
+ if (age < 0 || age > PENDING_INSTALL_RECENCY_SECONDS) return false;
2861
+ if (input.jobState === "running") return true;
2862
+ return input.jobState === "registered" && age <= PENDING_INSTALL_HANDOFF_SECONDS;
2858
2863
  }
2859
- function launchdJobLoaded(jobLabel, probe) {
2860
- if (!jobLabel) return false;
2861
- return probe(jobLabel) === 0;
2864
+ function evaluateLaunchDuringInstall(input) {
2865
+ const { marker } = input;
2866
+ if (!marker) return { kind: "open" };
2867
+ const outcome = evaluatePendingInstall({
2868
+ marker,
2869
+ runningVersion: input.runningVersion,
2870
+ installInFlight: isInstallInFlight({
2871
+ marker,
2872
+ jobState: input.jobState,
2873
+ now: input.now
2874
+ })
2875
+ });
2876
+ if (outcome.kind !== "in-flight") return { kind: "open" };
2877
+ const age = pendingInstallAgeSeconds(marker, input.now);
2878
+ if (age === null || age > PENDING_INSTALL_LAUNCH_HOLD_SECONDS) {
2879
+ return { kind: "open" };
2880
+ }
2881
+ return { kind: "hold", marker };
2882
+ }
2883
+ function launchdJobPid(output) {
2884
+ const dictionary = /"PID"\s*=\s*(\d+)\s*;/.exec(output);
2885
+ if (dictionary) {
2886
+ const pid = Number.parseInt(dictionary[1], 10);
2887
+ return pid > 0 ? pid : null;
2888
+ }
2889
+ for (const line of output.split("\n")) {
2890
+ const columns = line.trim().split(/\s+/);
2891
+ if (columns.length < 3) continue;
2892
+ if (!/^\d+$/.test(columns[0]) || !/^\d+$/.test(columns[1])) continue;
2893
+ const pid = Number.parseInt(columns[0], 10);
2894
+ return pid > 0 ? pid : null;
2895
+ }
2896
+ return null;
2897
+ }
2898
+ function installJobState(jobLabel, probe) {
2899
+ if (!jobLabel) return "unread";
2900
+ const reading = probe(jobLabel);
2901
+ if (reading == null || reading.status == null) return "unread";
2902
+ if (reading.status !== 0) return "absent";
2903
+ return launchdJobPid(reading.output) == null ? "registered" : "running";
2862
2904
  }
2863
2905
  function installStartedText(marker) {
2864
2906
  const started = marker.startedAt ? new Date(marker.startedAt) : null;
@@ -2898,6 +2940,13 @@ function installInFlightPayload(marker, runningVersion) {
2898
2940
  detail: `Install started ${installStartedText(marker)} from ${marker.artifactPath || "an unknown artifact"}, while version ${runningVersion} was running.`
2899
2941
  };
2900
2942
  }
2943
+ function installLaunchHoldNotice(marker, options = {}) {
2944
+ const relaunch = options.relaunchEnsured === false ? "No relaunch could be arranged for this install, so start Local Operator by hand when it finishes." : "Local Operator will open by itself when the install finishes.";
2945
+ return {
2946
+ title: "Local Operator is still updating",
2947
+ body: `The update to version ${marker.targetVersion} is still installing — this can take a few minutes, and opening the app now would cancel it. ${relaunch} Clicking this notice will not open it any sooner.`
2948
+ };
2949
+ }
2901
2950
  function shipItJobLabel(bundleId) {
2902
2951
  return `${bundleId}.ShipIt`;
2903
2952
  }
@@ -4426,6 +4475,19 @@ function transportFailureDetail(error) {
4426
4475
  const SILENCE_TIMEOUT_MS = 45e3;
4427
4476
  const WATCHDOG_TICK_MS = 1e3;
4428
4477
  const MAX_FRAME_BYTES$1 = 8 * 1024 * 1024;
4478
+ const LINE_ENDING = /\r\n|\n|\r/;
4479
+ const BLANK_LINE = /(?:\r\n|\n|\r)(?:\r\n|\n|\r)/;
4480
+ function nextRecord(buffer) {
4481
+ const blank = BLANK_LINE.exec(buffer);
4482
+ if (blank === null) return null;
4483
+ const data = [];
4484
+ for (const line of buffer.slice(0, blank.index).split(LINE_ENDING)) {
4485
+ if (!line.startsWith("data:")) continue;
4486
+ const value = line.slice(5);
4487
+ data.push(value.startsWith(" ") ? value.slice(1) : value);
4488
+ }
4489
+ return { data, rest: buffer.slice(blank.index + blank[0].length) };
4490
+ }
4429
4491
  class DesktopStreamRelay {
4430
4492
  constructor(backendUrl, token, timings = {}) {
4431
4493
  this.backendUrl = backendUrl;
@@ -4534,8 +4596,8 @@ class DesktopStreamRelay {
4534
4596
  lastActivity = Date.now();
4535
4597
  buffer += decoder.decode(value, { stream: true });
4536
4598
  for (; ; ) {
4537
- const boundary = buffer.indexOf("\n\n");
4538
- if (boundary < 0) {
4599
+ const record = nextRecord(buffer);
4600
+ if (record === null) {
4539
4601
  if (buffer.length > MAX_FRAME_BYTES$1) {
4540
4602
  emit({
4541
4603
  streamId,
@@ -4547,15 +4609,11 @@ class DesktopStreamRelay {
4547
4609
  }
4548
4610
  break;
4549
4611
  }
4550
- const record = buffer.slice(0, boundary);
4551
- buffer = buffer.slice(boundary + 2);
4552
- for (const line of record.split("\n")) {
4553
- if (line.startsWith("data:")) {
4554
- const data = line.slice(5).replace(/^ /, "");
4555
- this.observer?.(sessionId2, data);
4556
- emit({ streamId, kind: "data", data });
4557
- }
4558
- }
4612
+ buffer = record.rest;
4613
+ if (record.data.length === 0) continue;
4614
+ const data = record.data.join("\n");
4615
+ this.observer?.(sessionId2, data);
4616
+ emit({ streamId, kind: "data", data });
4559
4617
  }
4560
4618
  }
4561
4619
  } finally {
@@ -4701,6 +4759,14 @@ function deadlineExceeded(error) {
4701
4759
  const name = error?.name;
4702
4760
  return name === "TimeoutError" || name === "AbortError";
4703
4761
  }
4762
+ const ADMITTED_PATH_PREFIX = "/v1/desktop/";
4763
+ function desktopAnswerProvesPairing(input, response) {
4764
+ const parsed = desktopRequestSchema.safeParse(input);
4765
+ if (!parsed.success) return false;
4766
+ if (!desktopEndpoint(parsed.data).path.startsWith(ADMITTED_PATH_PREFIX))
4767
+ return false;
4768
+ return response.status >= 200 && response.status < 300;
4769
+ }
4704
4770
  function trustedDesktopFrame(actual, expected) {
4705
4771
  try {
4706
4772
  const current = new URL(actual);
@@ -5141,6 +5207,15 @@ class DaemonStateMachine {
5141
5207
  }
5142
5208
  this.detail = observation.detail;
5143
5209
  break;
5210
+ case "contradicted":
5211
+ this.failures += 1;
5212
+ if (this.failures >= DEGRADED_AFTER_FAILURES) {
5213
+ this.enterDetached(observation.detail, true);
5214
+ } else {
5215
+ this.state = "degraded";
5216
+ this.detail = `${observation.detail} (probe ${this.failures} of ${DEGRADED_AFTER_FAILURES}).`;
5217
+ }
5218
+ break;
5144
5219
  case "failed":
5145
5220
  this.failures += 1;
5146
5221
  if (this.failures >= DEGRADED_AFTER_FAILURES) {
@@ -5201,7 +5276,42 @@ class DaemonStateMachine {
5201
5276
  this.detail = `${detail} (no answer to probe ${this.unanswered} of ${UNANSWERED_BEFORE_DETACHED})`;
5202
5277
  }
5203
5278
  /**
5204
- * Record that a real request against the attached daemon was answered.
5279
+ * Record that a request of this app's was ANSWERED by the attached daemon.
5280
+ *
5281
+ * Liveness evidence and NOTHING ELSE. An answer says a process read the request
5282
+ * and wrote a status line - which is exactly what a probe that ran out of its
5283
+ * 2 s budget failed to establish - and a refusal is an answer. What a refusal
5284
+ * does not say is that this app is still PAIRED with the process that
5285
+ * answered: a `401`, a `403` and a shut plane's `503` are what an app holding
5286
+ * the wrong credential collects from a daemon that was replaced underneath it,
5287
+ * and one of them arrives every few seconds on a machine whose renderer is
5288
+ * re-asking capabilities and whose presence heartbeat is running. Counting
5289
+ * those as a successful attachment cleared the identity-failure count on every
5290
+ * pass, held the state at `attached` for a pid that was gone, and left the app
5291
+ * unable to reach the re-discovery that would have re-paired it (2026-09-18;
5292
+ * see the module docstring).
5293
+ *
5294
+ * So this stamps the two facts a refusal DOES establish - the transport is
5295
+ * alive, and no probe budget is outstanding - and leaves the state alone. A
5296
+ * caller holding proof that the plane admitted the request calls
5297
+ * {@link recordTransportSuccess} instead.
5298
+ */
5299
+ recordTransportAnswer() {
5300
+ this.lastTransportAt = this.now();
5301
+ this.unanswered = 0;
5302
+ this.updatedAt = this.now();
5303
+ }
5304
+ /**
5305
+ * Record that a real request against the attached daemon was ADMITTED.
5306
+ *
5307
+ * "Admitted" is the caller's judgement and the distinction this method exists
5308
+ * for: the daemon let the request through to a route that depends on this
5309
+ * app's desktop credential, so the pairing is proven to still hold. Every
5310
+ * answer below that bar - a refused socket, a `401`/`403`, a shut plane's
5311
+ * `503`, and the capability op the plane deliberately serves WITHOUT admitting
5312
+ * anyone - is {@link recordTransportAnswer} instead, liveness only, because
5313
+ * treating one of them as an admission is how a replaced daemon's refusals
5314
+ * kept an unpaired app looking attached.
5205
5315
  *
5206
5316
  * WHY this is the state machine's business and not a caller's: the probe and
5207
5317
  * the transport disagree regularly on a box running long turns - the probe
@@ -6586,7 +6696,8 @@ class BackendServiceManager {
6586
6696
  this.backendUrl,
6587
6697
  this.desktopToken
6588
6698
  ).then(({ response, answered }) => {
6589
- if (answered) this.noteTransportAnswer();
6699
+ if (answered)
6700
+ this.noteTransportAnswer(desktopAnswerProvesPairing(input, response));
6590
6701
  return response;
6591
6702
  });
6592
6703
  }
@@ -6597,7 +6708,7 @@ class BackendServiceManager {
6597
6708
  this.backendUrl,
6598
6709
  this.desktopToken
6599
6710
  ).then(({ response, answered }) => {
6600
- if (answered) this.noteTransportAnswer();
6711
+ if (answered) this.daemonState.recordTransportAnswer();
6601
6712
  return response;
6602
6713
  });
6603
6714
  }
@@ -6612,19 +6723,29 @@ class BackendServiceManager {
6612
6723
  * `degraded` - which is not a state `checkBackendHealth` recovers from, so a
6613
6724
  * genuinely gone daemon could never be reported as gone.
6614
6725
  *
6615
- * WHY this still counts EVERY answer, including a gated route's `401`/`403`:
6616
- * the capability rules elsewhere in this file say a refusal is not a LIVENESS
6617
- * signal, and that is about not calling it "connected". Turned around, a
6618
- * refusal is the strongest liveness evidence this app has - a process read the
6619
- * request and wrote a status line - which is exactly what a probe that ran out
6620
- * of its 2 s budget failed to establish. Without this, one long agent turn was
6621
- * enough to detach the connection and disable every read the daemon was still
6622
- * answering.
6726
+ * `admitted` is the second, narrower question and the two are deliberately
6727
+ * separate calls rather than one boolean: an answer of ANY status is liveness
6728
+ * evidence (a `401`, a `403` or a `503` is a process that read the request and
6729
+ * wrote a status line, which is exactly what a probe that ran out of its 2 s
6730
+ * budget failed to establish - without that, one long agent turn was enough to
6731
+ * detach the connection and disable every read the daemon was still answering),
6732
+ * while only an answer the plane ADMITTED proves the pairing still holds.
6733
+ * Collapsing the two is the defect this split fixes, measured 2026-09-18: a
6734
+ * replaced daemon's refusals - plus the capability poll the renderer runs every
6735
+ * 15 s while the plane is shut, which the plane serves without admitting
6736
+ * anyone - each cleared the identity-failure count, so the app never detached,
6737
+ * never re-discovered and never re-claimed, and reported "not paired with the
6738
+ * running Local Operator server" until it was restarted (see
6739
+ * `daemon-status.ts`'s `recordTransportAnswer`/`recordTransportSuccess`).
6623
6740
  *
6624
6741
  * Only a state that actually moves pushes a snapshot: this runs on every
6625
6742
  * desktop call, and one IPC wake-up per request would be worse than the bug.
6626
6743
  */
6627
- noteTransportAnswer() {
6744
+ noteTransportAnswer(admitted) {
6745
+ if (!admitted) {
6746
+ this.daemonState.recordTransportAnswer();
6747
+ return;
6748
+ }
6628
6749
  if (this.daemonState.recordTransportSuccess()) this.notifyStatus();
6629
6750
  }
6630
6751
  isAppClosing = false;
@@ -8107,17 +8228,17 @@ class BackendServiceManager {
8107
8228
  switch (probe.outcome) {
8108
8229
  case "identified":
8109
8230
  return probe.identity.pid === this.daemonState.snapshot().pid ? { kind: "identified" } : {
8110
- kind: "failed",
8231
+ kind: "contradicted",
8111
8232
  detail: "The answering daemon's PID no longer matches the attachment."
8112
8233
  };
8113
8234
  case "identity-mismatch":
8114
8235
  return {
8115
- kind: "failed",
8236
+ kind: "contradicted",
8116
8237
  detail: `Another process is answering at ${this.backendUrl} (${probe.detail})`
8117
8238
  };
8118
8239
  case "not-a-daemon":
8119
8240
  return {
8120
- kind: "failed",
8241
+ kind: "contradicted",
8121
8242
  detail: `${this.backendUrl} answered but is not a Local Operator daemon (${probe.detail})`
8122
8243
  };
8123
8244
  case "unreachable":
@@ -8254,7 +8375,7 @@ class BackendServiceManager {
8254
8375
  if (after.state !== before.state || after.detail !== before.detail) {
8255
8376
  this.notifyStatus();
8256
8377
  }
8257
- if (!processGone) return;
8378
+ if (!processGone && observation.kind !== "contradicted") return;
8258
8379
  }
8259
8380
  if (this.process && this.process.exitCode === null && this.process.signalCode == null)
8260
8381
  return;
@@ -17030,6 +17151,216 @@ function isWithinAnyRoot(roots, candidate) {
17030
17151
  return !relative.startsWith("..") && !path__namespace.isAbsolute(relative);
17031
17152
  });
17032
17153
  }
17154
+ const LAUNCH_HOLD_NOTICE_DEADLINE_MS = 2e3;
17155
+ const LAUNCH_HOLD_FORCE_QUIT_DEADLINE_MS = 1e4;
17156
+ function runningBundleIdentifier() {
17157
+ const bundlePath = appBundleFromExecutable(process.execPath);
17158
+ if (!bundlePath) return null;
17159
+ const value = readCommandOutput("/usr/bin/defaults", [
17160
+ "read",
17161
+ path$1.join(bundlePath, "Contents", "Info.plist"),
17162
+ "CFBundleIdentifier"
17163
+ ]);
17164
+ return value && BUNDLE_ID_REGEX.test(value) ? value : null;
17165
+ }
17166
+ function shipItJobForRunningBundle() {
17167
+ const bundleId = runningBundleIdentifier();
17168
+ return bundleId ? shipItJobLabel(bundleId) : null;
17169
+ }
17170
+ function probeShipItInstallJobState() {
17171
+ const jobProbe = watchdogSignals(process.platform).jobProbe;
17172
+ if (!jobProbe) return "unread";
17173
+ return installJobState(shipItJobForRunningBundle(), (label) => {
17174
+ const result = node_child_process.spawnSync(jobProbe, ["list", label], {
17175
+ encoding: "utf8",
17176
+ timeout: 5e3
17177
+ });
17178
+ return result.error || result.status == null ? null : { status: result.status, output: result.stdout ?? "" };
17179
+ });
17180
+ }
17181
+ function startRelaunchWatchdog(input) {
17182
+ if (process.platform !== "darwin" || !electron.app.isPackaged) return null;
17183
+ const bundlePath = appBundleFromExecutable(process.execPath);
17184
+ if (!bundlePath) return null;
17185
+ const plan = buildWatchdogPlan({
17186
+ appBundlePath: bundlePath,
17187
+ executableName: path$1.basename(process.execPath),
17188
+ // The app's own pid, captured here - before the quit - because the
17189
+ // watchdog cannot ask a name about its own ancestor (review R1).
17190
+ appPid: process.pid,
17191
+ shipItJob: shipItJobForRunningBundle(),
17192
+ // What "the install is over" is measured against on disk (review R11):
17193
+ // the updater's advertised version, and nothing that is already in place
17194
+ // (review R15).
17195
+ targetVersion: watchdogSwapTarget({
17196
+ target: input.targetVersion,
17197
+ running: input.runningVersion
17198
+ }),
17199
+ // Stated rather than read off `process.platform` at the plan: this
17200
+ // watchdog exists for Squirrel.Mac's ShipIt, the guard above already
17201
+ // refuses it anywhere else, and the script's two probes (launchd's job
17202
+ // lookup, `plutil`) are generated for the platform the script runs on
17203
+ // rather than for whichever host planned it (see `watchdogSignals`).
17204
+ platform: "darwin"
17205
+ });
17206
+ try {
17207
+ const child = node_child_process.spawn("sh", ["-c", plan.script], {
17208
+ detached: true,
17209
+ stdio: "ignore",
17210
+ env: { ...process.env, ...plan.env }
17211
+ });
17212
+ child.unref();
17213
+ input.log(
17214
+ `Started the update relaunch watchdog (pid ${child.pid ?? "unknown"}): it starts the app again as soon as the install's job goes or version ${plan.env.LO_UPDATE_WATCHDOG_TARGET_VERSION || "unknown"} is in place, and at the ${plan.timeoutSeconds}s bound if neither happens - or at the ${plan.hardTimeoutSeconds}s hard bound when the install's job is still loaded at the first, which is the case where starting the app would cancel the install.`
17215
+ );
17216
+ return child.pid ?? null;
17217
+ } catch (error) {
17218
+ input.log(`Could not start the update relaunch watchdog: ${String(error)}`);
17219
+ return null;
17220
+ }
17221
+ }
17222
+ function ensureRelaunchWatchdog(input) {
17223
+ const pid = input.marker.watchdogPid;
17224
+ const commandLine = pid == null ? null : readCommandOutput("/bin/ps", ["-o", "command=", "-p", String(pid)]);
17225
+ if (watchdogIsOurs({
17226
+ alive: commandLine != null,
17227
+ commandLine,
17228
+ installSucceeded: true
17229
+ })) {
17230
+ input.log(
17231
+ `Relaunch watchdog ${pid} is still running; it will start the app when the install settles.`
17232
+ );
17233
+ return true;
17234
+ }
17235
+ const launched = input.startWatchdog(input.marker.targetVersion);
17236
+ input.log(
17237
+ launched != null ? `Started relaunch watchdog ${launched} for the install already in flight; it will start the app when the install's job goes.` : "No relaunch watchdog is running for the install in flight and none could be started; the app will need starting by hand after it quits."
17238
+ );
17239
+ return launched != null;
17240
+ }
17241
+ const LAUNCH_HOLD_SCRIPT_DEADLINE_MS = 2e3;
17242
+ const LAUNCH_HOLD_LOG_FLUSH_MS = 300;
17243
+ const LAUNCH_HOLD_NOTICE_BUDGET_MS = LAUNCH_HOLD_NOTICE_DEADLINE_MS + LAUNCH_HOLD_SCRIPT_DEADLINE_MS + LAUNCH_HOLD_LOG_FLUSH_MS + /*
17244
+ * Headroom, so the quit and the flush it exists to allow cannot land in the same
17245
+ * millisecond (review R10): without it this budget IS its own worst case exactly,
17246
+ * and a host where `osascript` burns its whole deadline would make the record's
17247
+ * landing a coin flip rather than a guarantee.
17248
+ */
17249
+ 250;
17250
+ function notificationsSilenced() {
17251
+ const value = launchEnv[NOTIFICATIONS_ENV];
17252
+ return typeof value === "string" && value.length > 0;
17253
+ }
17254
+ function bannerReportedShown(notice, deadlineMs) {
17255
+ if (!electron.Notification.isSupported()) return Promise.resolve(false);
17256
+ return new Promise((resolve) => {
17257
+ const deadline2 = setTimeout(() => resolve(false), deadlineMs);
17258
+ deadline2.unref();
17259
+ try {
17260
+ const notification = new electron.Notification({
17261
+ title: notice.title,
17262
+ body: notice.body,
17263
+ silent: false
17264
+ });
17265
+ notification.on("show", () => {
17266
+ clearTimeout(deadline2);
17267
+ resolve(true);
17268
+ });
17269
+ notification.show();
17270
+ } catch {
17271
+ clearTimeout(deadline2);
17272
+ resolve(false);
17273
+ }
17274
+ });
17275
+ }
17276
+ function raiseThroughOsascript(notice) {
17277
+ if (process.platform !== "darwin") return null;
17278
+ const result = node_child_process.spawnSync(
17279
+ "/usr/bin/osascript",
17280
+ [
17281
+ "-e",
17282
+ "on run argv",
17283
+ "-e",
17284
+ "display notification (item 1 of argv) with title (item 2 of argv)",
17285
+ "-e",
17286
+ "end run",
17287
+ notice.body,
17288
+ notice.title
17289
+ ],
17290
+ { stdio: "ignore", timeout: LAUNCH_HOLD_SCRIPT_DEADLINE_MS }
17291
+ );
17292
+ return result.error || result.status == null ? null : result.status;
17293
+ }
17294
+ async function showInstallHoldNotice(notice, bannerDeadlineMs = LAUNCH_HOLD_NOTICE_DEADLINE_MS) {
17295
+ if (notificationsSilenced()) {
17296
+ return "suppressed: this launch switched notifications off, so no banner was raised";
17297
+ }
17298
+ const status2 = raiseThroughOsascript(notice);
17299
+ if (status2 === 0) {
17300
+ return "handed to the install's own notification channel (osascript, exit 0); nothing here observes whether the system displayed it";
17301
+ }
17302
+ if (await bannerReportedShown(notice, bannerDeadlineMs)) {
17303
+ return "shown by the app's own banner, which is the one leg that reports delivery (the install's channel being unavailable)";
17304
+ }
17305
+ if (status2 == null) {
17306
+ return "not raised: the install's channel is not available on this host, and the app's banner did not report itself shown";
17307
+ }
17308
+ return `not raised: the install's channel exited ${status2}, and the app's banner did not report itself shown`;
17309
+ }
17310
+ function holdLaunchForLiveInstall(options) {
17311
+ if (!electron.app.isPackaged) return false;
17312
+ const marker = readPendingInstallMarker(electron.app.getPath("userData"));
17313
+ const jobState = (options.jobState ?? probeShipItInstallJobState)();
17314
+ const reading = evaluateLaunchDuringInstall({
17315
+ marker,
17316
+ jobState,
17317
+ runningVersion: options.runningVersion ?? electron.app.getVersion(),
17318
+ now: options.now
17319
+ });
17320
+ if (reading.kind === "open") return false;
17321
+ options.log(
17322
+ `Holding this launch for the install of version ${reading.marker.targetVersion} that is still running (install job ${jobState}): a window here would make this process the running instance Squirrel's final check aborts the install on, so the app tells the user and quits.`
17323
+ );
17324
+ const relaunchEnsured = ensureRelaunchWatchdog({
17325
+ marker: reading.marker,
17326
+ startWatchdog: options.startWatchdog ?? defaultStartRelaunchWatchdog,
17327
+ log: options.log
17328
+ });
17329
+ const quit = options.quit ?? (() => electron.app.quit());
17330
+ const forceQuit = options.forceQuit ?? (() => electron.app.exit(0));
17331
+ const forceDeadline = setTimeout(
17332
+ forceQuit,
17333
+ LAUNCH_HOLD_FORCE_QUIT_DEADLINE_MS
17334
+ );
17335
+ forceDeadline.unref();
17336
+ const noticeDeadline = setTimeout(quit, LAUNCH_HOLD_NOTICE_BUDGET_MS);
17337
+ noticeDeadline.unref();
17338
+ const notice = installLaunchHoldNotice(reading.marker, { relaunchEnsured });
17339
+ const showNotice = options.showNotice ?? showInstallHoldNotice;
17340
+ options.log(
17341
+ `Telling the user the install of version ${reading.marker.targetVersion} is still running, and closing this launch so the install's final check finds no running instance of the app.`
17342
+ );
17343
+ void showNotice(notice).then((record) => {
17344
+ options.log(
17345
+ `The notice for the install of version ${reading.marker.targetVersion}: ${record}.`
17346
+ );
17347
+ }).catch(() => {
17348
+ }).finally(() => {
17349
+ const flush = setTimeout(() => {
17350
+ clearTimeout(noticeDeadline);
17351
+ quit();
17352
+ }, LAUNCH_HOLD_LOG_FLUSH_MS);
17353
+ flush.unref();
17354
+ });
17355
+ return true;
17356
+ }
17357
+ function defaultStartRelaunchWatchdog(targetVersion) {
17358
+ return startRelaunchWatchdog({
17359
+ targetVersion,
17360
+ runningVersion: electron.app.getVersion(),
17361
+ log: (message) => logger.info(message, LogFileType.UPDATE_SERVICE)
17362
+ });
17363
+ }
17033
17364
  class UpdateService {
17034
17365
  mainWindow = null;
17035
17366
  isDevMode;
@@ -17368,15 +17699,28 @@ class UpdateService {
17368
17699
  return;
17369
17700
  }
17370
17701
  const marker = readPendingInstallMarker(this.markerDir());
17702
+ const jobState = this.shipItInstallJobState();
17703
+ if (marker) {
17704
+ logger.info(
17705
+ `Pending install marker for version ${marker.targetVersion} on disk; its install job is ${jobState}.`,
17706
+ LogFileType.UPDATE_SERVICE
17707
+ );
17708
+ }
17371
17709
  const outcome = evaluatePendingInstall({
17372
17710
  marker,
17373
17711
  runningVersion: electron.app.getVersion(),
17374
- // Probed once per evaluation, and only on macOS: a loaded job plus a
17712
+ // Probed once per evaluation, and only on macOS: a RUNNING job plus a
17375
17713
  // current marker is what makes "still on the old version" an install in
17376
- // progress rather than an install that failed.
17714
+ // progress rather than an install that failed. A job that is registered
17715
+ // but not running is the state a finished install leaves behind, and
17716
+ // reading it as live is what held a launch - and so swallowed this very
17717
+ // report - for ~29 minutes after every successful update (review U1, U2);
17718
+ // the one exception is the hand-off, the window in which launchd has the
17719
+ // job but ShipIt has no pid yet, which `PENDING_INSTALL_HANDOFF_SECONDS`
17720
+ // bounds (review R6, R11).
17377
17721
  installInFlight: isInstallInFlight({
17378
17722
  marker,
17379
- jobLoaded: this.shipItInstallJobLoaded()
17723
+ jobState
17380
17724
  })
17381
17725
  });
17382
17726
  switch (outcome.kind) {
@@ -17401,7 +17745,7 @@ class UpdateService {
17401
17745
  case "in-flight": {
17402
17746
  this.installWasInFlight = true;
17403
17747
  logger.info(
17404
- `Update marker: the install of version ${outcome.marker.targetVersion} is still running (its install job is loaded). Leaving the marker, the install job and the relaunch watchdog in place.`,
17748
+ `Update marker: the install of version ${outcome.marker.targetVersion} is still running (install job ${jobState}). Leaving the marker, the install job and the relaunch watchdog in place.`,
17405
17749
  LogFileType.UPDATE_SERVICE
17406
17750
  );
17407
17751
  this.pendingInstallInFlight = installInFlightPayload(
@@ -17453,25 +17797,11 @@ class UpdateService {
17453
17797
  /**
17454
17798
  * This app's bundle identifier, as macOS records it.
17455
17799
  *
17456
- * Read from the running bundle rather than from `package.json`: the running
17457
- * app is the only thing whose ShipIt job and cache directory this process may
17458
- * touch, and a build whose `appId` drifted would otherwise have us cleaning up
17459
- * some other app's leftovers.
17800
+ * The service's name for `runningBundleIdentifier`, which the launch hold needs
17801
+ * before a service exists; the rule itself lives there.
17460
17802
  */
17461
17803
  bundleIdentifier() {
17462
- const bundlePath = appBundleFromExecutable(process.execPath);
17463
- if (!bundlePath) return null;
17464
- const value = readCommandOutput("/usr/bin/defaults", [
17465
- "read",
17466
- path$1.join(bundlePath, "Contents", "Info.plist"),
17467
- "CFBundleIdentifier"
17468
- ]);
17469
- return value && BUNDLE_ID_REGEX.test(value) ? value : null;
17470
- }
17471
- /** The launchd job Squirrel's install runs under, by label. */
17472
- shipItJob() {
17473
- const bundleId = this.bundleIdentifier();
17474
- return bundleId ? shipItJobLabel(bundleId) : null;
17804
+ return runningBundleIdentifier();
17475
17805
  }
17476
17806
  /**
17477
17807
  * Squirrel's own stderr log for this app, when it exists.
@@ -17617,9 +17947,9 @@ class UpdateService {
17617
17947
  );
17618
17948
  }
17619
17949
  /**
17620
- * A substituted answer to "is this app's install job loaded", or null.
17950
+ * A substituted answer to "what is this app's install job doing", or null.
17621
17951
  *
17622
- * The production answer is the launchd probe in `shipItInstallJobLoaded`, and
17952
+ * The production answer is the launchd probe in `shipItInstallJobState`, and
17623
17953
  * it asks about a label read from the RUNNING BUNDLE (`shipItJob`). A test or a
17624
17954
  * harness process that is not that bundle has no label to ask about, so the
17625
17955
  * whole in-flight path - recovery's reading, the re-check, and the quit below -
@@ -17629,28 +17959,21 @@ class UpdateService {
17629
17959
  * rule production uses rather than a second copy of it. Production never sets
17630
17960
  * it.
17631
17961
  */
17632
- installJobLoadedProbe = null;
17962
+ installJobStateProbe = null;
17633
17963
  /**
17634
- * Whether this app's ShipIt install job is loaded, right now.
17964
+ * What this app's ShipIt install job is doing, right now.
17635
17965
  *
17636
17966
  * This is the machine's own answer to "is an install in flight", and the only
17637
- * answer that can tell a live install apart from the job a failed one left
17638
- * behind. It is asked of launchd by label rather than inferred from a process
17639
- * name, so nothing else on the machine can be mistaken for it. Off macOS there
17640
- * is no launchd to ask, so the answer is a plain no and recovery stays
17967
+ * answer that can tell a live install from the job a finished one left behind -
17968
+ * which is why it reports `running` separately from `registered` rather than a
17969
+ * boolean (review U1). It is asked of launchd by label rather than inferred from
17970
+ * a process name, so nothing else on the machine can be mistaken for it. Off
17971
+ * macOS there is no launchd to ask, so the answer is `unread` and recovery stays
17641
17972
  * version-only there, exactly as it is today.
17642
17973
  */
17643
- shipItInstallJobLoaded() {
17644
- if (this.installJobLoadedProbe) return this.installJobLoadedProbe();
17645
- const jobProbe = watchdogSignals(process.platform).jobProbe;
17646
- if (!jobProbe) return false;
17647
- return launchdJobLoaded(this.shipItJob(), (label) => {
17648
- const result = node_child_process.spawnSync(jobProbe, ["list", label], {
17649
- encoding: "utf8",
17650
- timeout: 5e3
17651
- });
17652
- return result.error || result.status == null ? null : result.status;
17653
- });
17974
+ shipItInstallJobState() {
17975
+ if (this.installJobStateProbe) return this.installJobStateProbe();
17976
+ return probeShipItInstallJobState();
17654
17977
  }
17655
17978
  /**
17656
17979
  * Deliver the still-installing notice once the renderer can hear it.
@@ -17712,34 +18035,18 @@ class UpdateService {
17712
18035
  * Make sure something will start the app again after a quit that is only
17713
18036
  * getting out of an install's way.
17714
18037
  *
17715
- * The watchdog from the original install is usually still there - it holds
17716
- * while the job is loaded now, rather than starting the app into the swap -
17717
- * but it may have run out its own bounds, and the user may be quitting hours
17718
- * later. Quitting without one would leave them with no app at all, which is
17719
- * the outcome this whole change exists to remove. Nothing is started when ours
17720
- * is alive, because two watchdogs relaunching at the same moment is a race
17721
- * with nothing to gain. The marker is left exactly as it is: its start time is
17722
- * what tells recovery this is still the same install.
18038
+ * The rule is `ensureRelaunchWatchdog`'s, shared with the launch hold; what is
18039
+ * the service's own here is the starter, which a harness substitutes.
17723
18040
  */
17724
18041
  ensureWatchdogAfterInFlightQuit(marker) {
17725
- const pid = marker.watchdogPid;
17726
- const commandLine = pid == null ? null : readCommandOutput("/bin/ps", ["-o", "command=", "-p", String(pid)]);
17727
- if (watchdogIsOurs({
17728
- alive: commandLine != null,
17729
- commandLine,
17730
- installSucceeded: true
17731
- })) {
17732
- logger.info(
17733
- `Relaunch watchdog ${pid} is still running; it will start the app when the install settles.`,
17734
- LogFileType.UPDATE_SERVICE
17735
- );
17736
- return;
17737
- }
17738
- const launched = this.launchWatchdog(marker.targetVersion);
17739
- logger.info(
17740
- launched ? `Started relaunch watchdog ${launched} for the install already in flight; it will start the app when the install's job goes.` : "No relaunch watchdog is running for the install in flight and none could be started; the app will need starting by hand after it quits.",
17741
- LogFileType.UPDATE_SERVICE
17742
- );
18042
+ ensureRelaunchWatchdog({
18043
+ marker,
18044
+ // The service's own method, not the module function, because a harness
18045
+ // substitutes this one: substituting the seam is how a test counts the
18046
+ // watchdogs a quit leaves running without spawning any.
18047
+ startWatchdog: (targetVersion) => this.launchWatchdog(targetVersion),
18048
+ log: (message) => logger.info(message, LogFileType.UPDATE_SERVICE)
18049
+ });
17743
18050
  }
17744
18051
  /**
17745
18052
  * The pending-install marker, if the machine says an install is live now.
@@ -17754,7 +18061,7 @@ class UpdateService {
17754
18061
  if (!marker) return null;
17755
18062
  return isInstallInFlight({
17756
18063
  marker,
17757
- jobLoaded: this.shipItInstallJobLoaded()
18064
+ jobState: this.shipItInstallJobState()
17758
18065
  }) ? marker : null;
17759
18066
  }
17760
18067
  /**
@@ -18229,53 +18536,16 @@ ${heal.removed.join("\n")}`,
18229
18536
  /**
18230
18537
  * Start the detached relaunch watchdog.
18231
18538
  *
18232
- * Spawned detached so it survives this process exiting, with stdio ignored so
18233
- * it holds no pipe open and cannot keep the app alive.
18539
+ * The service's name for `startRelaunchWatchdog`, which the launch hold also
18540
+ * uses; the plan, the bounds and the log line live there. Kept as a method
18541
+ * because a harness substitutes it to count watchdogs without spawning one.
18234
18542
  */
18235
18543
  launchWatchdog(targetVersion) {
18236
- if (process.platform !== "darwin" || !electron.app.isPackaged) return null;
18237
- const bundlePath = appBundleFromExecutable(process.execPath);
18238
- if (!bundlePath) return null;
18239
- const plan = buildWatchdogPlan({
18240
- appBundlePath: bundlePath,
18241
- executableName: path$1.basename(process.execPath),
18242
- // The app's own pid, captured here - before the quit - because the
18243
- // watchdog cannot ask a name about its own ancestor (review R1).
18244
- appPid: process.pid,
18245
- shipItJob: this.shipItJob(),
18246
- // What "the install is over" is measured against on disk (review R11):
18247
- // the updater's advertised version, and nothing that is already in place
18248
- // (review R15).
18249
- targetVersion: watchdogSwapTarget({
18250
- target: targetVersion,
18251
- running: electron.app.getVersion()
18252
- }),
18253
- // Stated rather than read off `process.platform` at the plan: this
18254
- // watchdog exists for Squirrel.Mac's ShipIt, the guard above already
18255
- // refuses it anywhere else, and the script's two probes (launchd's job
18256
- // lookup, `plutil`) are generated for the platform the script runs on
18257
- // rather than for whichever host planned it (see `watchdogSignals`).
18258
- platform: "darwin"
18544
+ return startRelaunchWatchdog({
18545
+ targetVersion,
18546
+ runningVersion: electron.app.getVersion(),
18547
+ log: (message) => logger.info(message, LogFileType.UPDATE_SERVICE)
18259
18548
  });
18260
- try {
18261
- const child = node_child_process.spawn("sh", ["-c", plan.script], {
18262
- detached: true,
18263
- stdio: "ignore",
18264
- env: { ...process.env, ...plan.env }
18265
- });
18266
- child.unref();
18267
- logger.info(
18268
- `Started the update relaunch watchdog (pid ${child.pid ?? "unknown"}): it starts the app again as soon as the install's job goes or version ${plan.env.LO_UPDATE_WATCHDOG_TARGET_VERSION || "unknown"} is in place, and at the ${plan.timeoutSeconds}s bound if neither happens - or at the ${plan.hardTimeoutSeconds}s hard bound when the install's job is still loaded at the first, which is the case where starting the app would cancel the install.`,
18269
- LogFileType.UPDATE_SERVICE
18270
- );
18271
- return child.pid ?? null;
18272
- } catch (error) {
18273
- logger.error(
18274
- `Could not start the update relaunch watchdog: ${String(error)}`,
18275
- LogFileType.UPDATE_SERVICE
18276
- );
18277
- return null;
18278
- }
18279
18549
  }
18280
18550
  /**
18281
18551
  * Clean up resources when the service is no longer needed
@@ -22097,6 +22367,11 @@ electron.app.whenReady().then(async () => {
22097
22367
  electron.app.exit(0);
22098
22368
  return;
22099
22369
  }
22370
+ if (holdLaunchForLiveInstall({
22371
+ log: (message) => logger.info(message, LogFileType.UPDATE_SERVICE)
22372
+ })) {
22373
+ return;
22374
+ }
22100
22375
  configureAboutPanel();
22101
22376
  createApplicationMenu();
22102
22377
  const launcher = resolveLauncherWatchPlan({