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.
- package/out/main/index.js +418 -143
- package/out/renderer/assets/{_basePickBy--1nrQ8Hz.js → _basePickBy-C9qLySi4.js} +1 -1
- package/out/renderer/assets/{_baseUniq-CIl_ZKBb.js → _baseUniq-BjBdobRd.js} +1 -1
- package/out/renderer/assets/{agent-details-page-SAU23J1h.js → agent-details-page-B_AapBwI.js} +2 -2
- package/out/renderer/assets/{agent-hub-page-FBbZyDG1.js → agent-hub-page-DJtExkiE.js} +2 -2
- package/out/renderer/assets/{agents-page-DUHwVHUN.js → agents-page-KHk6v504.js} +2 -2
- package/out/renderer/assets/{arc-VjtCFy-V.js → arc-Cui5tHbV.js} +1 -1
- package/out/renderer/assets/{architectureDiagram-IEHRJDOE-U5Plnyki.js → architectureDiagram-IEHRJDOE-DoQgwV9a.js} +1 -1
- package/out/renderer/assets/{blockDiagram-JOT3LUYC-BUFzKhGE.js → blockDiagram-JOT3LUYC-DNBIKuj6.js} +1 -1
- package/out/renderer/assets/browser-page-DtcRd0cQ.js +1 -0
- package/out/renderer/assets/{c4Diagram-VJAJSXHY-QNK7Lnt9.js → c4Diagram-VJAJSXHY-DlxOmlVm.js} +1 -1
- package/out/renderer/assets/channel-8AJlXpIq.js +1 -0
- package/out/renderer/assets/{chunk-4BMEZGHF-BkvMXI70.js → chunk-4BMEZGHF-B7-pfcKD.js} +1 -1
- package/out/renderer/assets/{chunk-A2AXSNBT-CWRaCKQM.js → chunk-A2AXSNBT-Bq3x3bfB.js} +1 -1
- package/out/renderer/assets/{chunk-AEK57VVT-BLIYWGRO.js → chunk-AEK57VVT-D6VZ66Au.js} +1 -1
- package/out/renderer/assets/{chunk-D6G4REZN-BVfOPcBu.js → chunk-D6G4REZN-B5nxs9x8.js} +1 -1
- package/out/renderer/assets/{chunk-RZ5BOZE2-UUEZsBdp.js → chunk-RZ5BOZE2-C62EhzfA.js} +1 -1
- package/out/renderer/assets/{chunk-XZIHB7SX-Dyrks1Q5.js → chunk-XZIHB7SX-aMJiddzK.js} +1 -1
- package/out/renderer/assets/classDiagram-GIVACNV2-BziEYnmd.js +1 -0
- package/out/renderer/assets/classDiagram-v2-COTLJTTW-BziEYnmd.js +1 -0
- package/out/renderer/assets/clone-GQ-cyvhB.js +1 -0
- package/out/renderer/assets/{compact-pagination-BZGdtI9l.js → compact-pagination-FhHxf_Eu.js} +1 -1
- package/out/renderer/assets/{dagre-OKDRZEBW-DmyjQpEm.js → dagre-OKDRZEBW-BGH7ioeC.js} +1 -1
- package/out/renderer/assets/{diagram-SSKATNLV-Dvs6GT39.js → diagram-SSKATNLV-DGsrHy_U.js} +1 -1
- package/out/renderer/assets/{diagram-VNBRO52H-CdMcENf8.js → diagram-VNBRO52H-A7hPXIso.js} +1 -1
- package/out/renderer/assets/{erDiagram-Q7BY3M3F-DRk_quuH.js → erDiagram-Q7BY3M3F-BWLPSJ_9.js} +1 -1
- package/out/renderer/assets/{flowDiagram-4HSFHLVR-Dyx7rgOH.js → flowDiagram-4HSFHLVR-CAHT0JfL.js} +1 -1
- package/out/renderer/assets/{ganttDiagram-APWFNJXF-BKgI7FyT.js → ganttDiagram-APWFNJXF-8FI3J_q0.js} +1 -1
- package/out/renderer/assets/{gitGraphDiagram-7IBYFJ6S-CZY1NaZO.js → gitGraphDiagram-7IBYFJ6S-cpqr3qwe.js} +1 -1
- package/out/renderer/assets/{graph-BMAzdbIf.js → graph-DcyNrb-k.js} +1 -1
- package/out/renderer/assets/{icon-Y8w93nKk.css → icon-GhtZ9eY_.css} +1 -1
- package/out/renderer/assets/{index-BmiDOvs8.js → index-1aumAIKD.js} +1 -1
- package/out/renderer/assets/{index-D0o2SKci.js → index-BIjm9xg9.js} +1 -1
- package/out/renderer/assets/{index-BSf4qKVc.js → index-DwEheJBg.js} +391 -419
- package/out/renderer/assets/{infoDiagram-PH2N3AL5-CsWfudmo.js → infoDiagram-PH2N3AL5-DsYwEtIa.js} +1 -1
- package/out/renderer/assets/{installer-DbeF2a73.js → installer-ugVSuxQK.js} +1 -1
- package/out/renderer/assets/{journeyDiagram-U35MCT3I-Z04EcqBd.js → journeyDiagram-U35MCT3I-BQk-3VkJ.js} +1 -1
- package/out/renderer/assets/{kanban-definition-NDS4AKOZ-BIP64FFY.js → kanban-definition-NDS4AKOZ-BlN92WlQ.js} +1 -1
- package/out/renderer/assets/{layout-Df_GsS2N.js → layout-DSFPeg4W.js} +1 -1
- package/out/renderer/assets/{legacy-agents-page-CIrGO_6M.js → legacy-agents-page-CNgIc1H2.js} +5 -5
- package/out/renderer/assets/{mermaid.core-50Aa46l1.js → mermaid.core-Csj0_Xl7.js} +5 -5
- package/out/renderer/assets/{mindmap-definition-ALO5MXBD-CnCk-gns.js → mindmap-definition-ALO5MXBD-Bqbt4rKL.js} +1 -1
- package/out/renderer/assets/{page-header-hdhRg_jg.js → page-header-CUZjto_D.js} +1 -1
- package/out/renderer/assets/parseISO-BYhaeu0l.js +6 -0
- package/out/renderer/assets/{pieDiagram-IB7DONF6-WKzdnNpM.js → pieDiagram-IB7DONF6-DBEASiho.js} +1 -1
- package/out/renderer/assets/{quadrantDiagram-7GDLP6J5-CLkOTUOn.js → quadrantDiagram-7GDLP6J5-19bwKsb5.js} +1 -1
- package/out/renderer/assets/{radar-MK3ICKWK-C4tKiExL.js → radar-MK3ICKWK-7gH_bYXp.js} +1 -1
- package/out/renderer/assets/{radient-auth-buttons-DWFoNRqo.js → radient-auth-buttons-BTLeKmb7.js} +2 -2
- package/out/renderer/assets/{requirementDiagram-KVF5MWMF-CVi3ekXN.js → requirementDiagram-KVF5MWMF-DxdQEclc.js} +1 -1
- package/out/renderer/assets/{sankeyDiagram-QLVOVGJD-BHuodNrM.js → sankeyDiagram-QLVOVGJD-DMJiKfyS.js} +1 -1
- package/out/renderer/assets/{schedules-page-DHlixzhn.js → schedules-page-Szw182RW.js} +6 -6
- package/out/renderer/assets/{sequenceDiagram-X6HHIX6F-A8v49VTH.js → sequenceDiagram-X6HHIX6F-0-YWl2Dx.js} +1 -1
- package/out/renderer/assets/{settings-page-CZi7TtEG.js → settings-page-6PGUFRNP.js} +71 -66
- package/out/renderer/assets/{stateDiagram-DGXRK772-DmX3aEhB.js → stateDiagram-DGXRK772-CH3feh2C.js} +1 -1
- package/out/renderer/assets/stateDiagram-v2-YXO3MK2T-B4A_-d8Q.js +1 -0
- package/out/renderer/assets/{timeline-definition-BDJGKUSR-PlVSNy-D.js → timeline-definition-BDJGKUSR-jjkJ2-x_.js} +1 -1
- package/out/renderer/assets/{use-agent-like-mutation-nIpQtB-p.js → use-agent-like-mutation-COpJgvhn.js} +1 -1
- package/out/renderer/assets/{xychartDiagram-VJFVF3MP-BJWQkXTM.js → xychartDiagram-VJFVF3MP-Bk9pfVm-.js} +1 -1
- package/out/renderer/index.html +11 -4
- package/out/renderer/installer.html +3 -3
- package/package.json +3 -2
- package/out/renderer/assets/browser-page-C4aIl48r.js +0 -1
- package/out/renderer/assets/channel-BYLqClNb.js +0 -1
- package/out/renderer/assets/classDiagram-GIVACNV2-D01h7e3_.js +0 -1
- package/out/renderer/assets/classDiagram-v2-COTLJTTW-D01h7e3_.js +0 -1
- package/out/renderer/assets/clone-D8rnVSgS.js +0 -1
- package/out/renderer/assets/parseISO-Cit1EWS-.js +0 -6
- package/out/renderer/assets/stateDiagram-v2-YXO3MK2T-MojYDBV8.js +0 -1
- /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
|
|
2857
|
+
if (!input.marker) return false;
|
|
2855
2858
|
const age = pendingInstallAgeSeconds(input.marker, input.now);
|
|
2856
2859
|
if (age === null) return false;
|
|
2857
|
-
|
|
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
|
|
2860
|
-
|
|
2861
|
-
|
|
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
|
|
4538
|
-
if (
|
|
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
|
-
|
|
4551
|
-
|
|
4552
|
-
|
|
4553
|
-
|
|
4554
|
-
|
|
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
|
|
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)
|
|
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.
|
|
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
|
-
*
|
|
6616
|
-
*
|
|
6617
|
-
*
|
|
6618
|
-
*
|
|
6619
|
-
*
|
|
6620
|
-
*
|
|
6621
|
-
*
|
|
6622
|
-
*
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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
|
-
*
|
|
17457
|
-
*
|
|
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
|
-
|
|
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
|
|
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 `
|
|
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
|
-
|
|
17962
|
+
installJobStateProbe = null;
|
|
17633
17963
|
/**
|
|
17634
|
-
*
|
|
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
|
|
17638
|
-
*
|
|
17639
|
-
*
|
|
17640
|
-
*
|
|
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
|
-
|
|
17644
|
-
if (this.
|
|
17645
|
-
|
|
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
|
|
17716
|
-
*
|
|
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
|
-
|
|
17726
|
-
|
|
17727
|
-
|
|
17728
|
-
|
|
17729
|
-
|
|
17730
|
-
|
|
17731
|
-
|
|
17732
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
18233
|
-
*
|
|
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
|
-
|
|
18237
|
-
|
|
18238
|
-
|
|
18239
|
-
|
|
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({
|