local-operator-ui 0.29.9 → 0.29.11
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 +938 -55
- package/out/preload/index.js +28 -0
- package/out/renderer/assets/{_basePickBy-C6Zpso0Q.js → _basePickBy-gHcXb1KL.js} +1 -1
- package/out/renderer/assets/{_baseUniq-C1DyLv6t.js → _baseUniq-Cjg_TxLc.js} +1 -1
- package/out/renderer/assets/{agent-details-page-B5SKXAv5.js → agent-details-page-BLVP0oIP.js} +1 -1
- package/out/renderer/assets/{agent-hub-page-D64F7S88.js → agent-hub-page-DAzUXdCl.js} +1 -1
- package/out/renderer/assets/{agents-page-BJK0Pq9n.js → agents-page-BqqWqJRL.js} +2 -2
- package/out/renderer/assets/{arc-C5QSgRE-.js → arc-7-q8aDG3.js} +1 -1
- package/out/renderer/assets/{architectureDiagram-IEHRJDOE-_1BluMs5.js → architectureDiagram-IEHRJDOE-Ba7yCUVS.js} +1 -1
- package/out/renderer/assets/{blockDiagram-JOT3LUYC-CGBO2CqU.js → blockDiagram-JOT3LUYC-t6SLXRXN.js} +1 -1
- package/out/renderer/assets/{browser-page-BwHs3-XU.js → browser-page-B3ww4TIa.js} +1 -1
- package/out/renderer/assets/{browser-webauthn-prompt-Bku2Q0DD.js → browser-webauthn-prompt-BiFtkxJX.js} +1 -1
- package/out/renderer/assets/{c4Diagram-VJAJSXHY-CTzOtZaJ.js → c4Diagram-VJAJSXHY-C6nxJeUu.js} +1 -1
- package/out/renderer/assets/channel-D05avaQg.js +1 -0
- package/out/renderer/assets/{chunk-4BMEZGHF-D4V7GOyK.js → chunk-4BMEZGHF-iV0vwEQH.js} +1 -1
- package/out/renderer/assets/{chunk-A2AXSNBT-C2JjWyq4.js → chunk-A2AXSNBT-Bo_VOUDE.js} +1 -1
- package/out/renderer/assets/{chunk-AEK57VVT-i5O0M59b.js → chunk-AEK57VVT-CdHexsQ8.js} +1 -1
- package/out/renderer/assets/{chunk-D6G4REZN-CQ-FSZuM.js → chunk-D6G4REZN-CeLv_5W7.js} +1 -1
- package/out/renderer/assets/{chunk-RZ5BOZE2-BwQQ0vAi.js → chunk-RZ5BOZE2-BnQffRFA.js} +1 -1
- package/out/renderer/assets/{chunk-XZIHB7SX-DhDwyjwr.js → chunk-XZIHB7SX-5It_CStB.js} +1 -1
- package/out/renderer/assets/classDiagram-GIVACNV2-DjtkcyI4.js +1 -0
- package/out/renderer/assets/classDiagram-v2-COTLJTTW-DjtkcyI4.js +1 -0
- package/out/renderer/assets/clone-CkDaYVxE.js +1 -0
- package/out/renderer/assets/{compact-pagination-CkriC8GH.js → compact-pagination-BJfBJjZq.js} +1 -1
- package/out/renderer/assets/{dagre-OKDRZEBW-DdcDClZN.js → dagre-OKDRZEBW-CPHSSmct.js} +1 -1
- package/out/renderer/assets/{diagram-SSKATNLV-Bg_QnBVO.js → diagram-SSKATNLV-uoifaHV7.js} +1 -1
- package/out/renderer/assets/{diagram-VNBRO52H-CPziysfK.js → diagram-VNBRO52H-DiPorn-j.js} +1 -1
- package/out/renderer/assets/{erDiagram-Q7BY3M3F-_ol5WsYH.js → erDiagram-Q7BY3M3F-D3_RVsgS.js} +1 -1
- package/out/renderer/assets/{flowDiagram-4HSFHLVR-Bmu20sRQ.js → flowDiagram-4HSFHLVR-9kpTgY8l.js} +1 -1
- package/out/renderer/assets/{ganttDiagram-APWFNJXF-DzIMMuhC.js → ganttDiagram-APWFNJXF-C1hwJAlh.js} +1 -1
- package/out/renderer/assets/{gitGraphDiagram-7IBYFJ6S-B-OgztQz.js → gitGraphDiagram-7IBYFJ6S-BvA7T2vP.js} +1 -1
- package/out/renderer/assets/{graph-B_JuzgF_.js → graph-DpmQwtUv.js} +1 -1
- package/out/renderer/assets/{index-BWYtIPrm.js → index-Bw89Hsw-.js} +380 -379
- package/out/renderer/assets/{index-BukjYK9S.js → index-CxwcyLWF.js} +1 -1
- package/out/renderer/assets/{index-BPazDpDx.js → index-Dl2E40WV.js} +1 -1
- package/out/renderer/assets/{infoDiagram-PH2N3AL5-Bi062Ijx.js → infoDiagram-PH2N3AL5-W9OqWzoG.js} +1 -1
- package/out/renderer/assets/{journeyDiagram-U35MCT3I-BDtZIJa2.js → journeyDiagram-U35MCT3I-Deb5RarE.js} +1 -1
- package/out/renderer/assets/{kanban-definition-NDS4AKOZ-DnZ6Y5H5.js → kanban-definition-NDS4AKOZ-BsbQgmLq.js} +1 -1
- package/out/renderer/assets/{layout-Ca77fqmE.js → layout-CrJ6Fu9B.js} +1 -1
- package/out/renderer/assets/{legacy-agents-page-Chbmzrq4.js → legacy-agents-page-egSNYRnK.js} +1 -1
- package/out/renderer/assets/{mermaid.core-BkcuejWg.js → mermaid.core-BYxmKNp-.js} +5 -5
- package/out/renderer/assets/{mindmap-definition-ALO5MXBD-BSewSz0i.js → mindmap-definition-ALO5MXBD-C-9egivQ.js} +1 -1
- package/out/renderer/assets/{parseISO-DtZurrLZ.js → parseISO-B1ekvrn4.js} +1 -1
- package/out/renderer/assets/{pieDiagram-IB7DONF6-BkAZXmpD.js → pieDiagram-IB7DONF6-B8qVlKET.js} +1 -1
- package/out/renderer/assets/{quadrantDiagram-7GDLP6J5-DbuM9Vo7.js → quadrantDiagram-7GDLP6J5-DFTB7_6X.js} +1 -1
- package/out/renderer/assets/{radar-MK3ICKWK-CHIyxMEL.js → radar-MK3ICKWK-BBqHNsuC.js} +1 -1
- package/out/renderer/assets/{radient-auth-buttons-DAhpbmde.js → radient-auth-buttons-D7Z-gfFA.js} +1 -1
- package/out/renderer/assets/{requirementDiagram-KVF5MWMF-D3Uob2JK.js → requirementDiagram-KVF5MWMF-nfWLgIi1.js} +1 -1
- package/out/renderer/assets/{sankeyDiagram-QLVOVGJD-CJCHGLBa.js → sankeyDiagram-QLVOVGJD-kz3aMr3g.js} +1 -1
- package/out/renderer/assets/{schedules-page-BrJT9xxG.js → schedules-page-9k1r0G5Z.js} +1 -1
- package/out/renderer/assets/{sequenceDiagram-X6HHIX6F-Dez-Y-IR.js → sequenceDiagram-X6HHIX6F-BlPj6-VP.js} +1 -1
- package/out/renderer/assets/{settings-page-DLhpz6kA.js → settings-page-7FNkwi6H.js} +1 -1
- package/out/renderer/assets/{stateDiagram-DGXRK772-CZ4fm__h.js → stateDiagram-DGXRK772-U-C3BZ-1.js} +1 -1
- package/out/renderer/assets/stateDiagram-v2-YXO3MK2T-Co-dAGQa.js +1 -0
- package/out/renderer/assets/{timeline-definition-BDJGKUSR-DERFLdV1.js → timeline-definition-BDJGKUSR-Bg_ZEH7m.js} +1 -1
- package/out/renderer/assets/{use-agent-like-mutation-DUtwAxLu.js → use-agent-like-mutation-B5uInGPk.js} +1 -1
- package/out/renderer/assets/{xychartDiagram-VJFVF3MP-BKGu1d8V.js → xychartDiagram-VJFVF3MP-CUjkSuIE.js} +1 -1
- package/out/renderer/index.html +1 -1
- package/package.json +2 -2
- package/out/renderer/assets/channel-Df29J43d.js +0 -1
- package/out/renderer/assets/classDiagram-GIVACNV2-svv09F8A.js +0 -1
- package/out/renderer/assets/classDiagram-v2-COTLJTTW-svv09F8A.js +0 -1
- package/out/renderer/assets/clone-B2YTR_FA.js +0 -1
- package/out/renderer/assets/stateDiagram-v2-YXO3MK2T-DQcOVGs7.js +0 -1
package/out/main/index.js
CHANGED
|
@@ -2450,6 +2450,141 @@ const MACOS_ENTITLEMENT_POLICY = {
|
|
|
2450
2450
|
unrestrictedEntitlementKeys,
|
|
2451
2451
|
unrestrictedEntitlementPrefixes
|
|
2452
2452
|
};
|
|
2453
|
+
const DITTO_PATH = "/usr/bin/ditto";
|
|
2454
|
+
const SHIPIT_RELATIVE_PATH = path$1.join(
|
|
2455
|
+
"Contents",
|
|
2456
|
+
"Frameworks",
|
|
2457
|
+
"Squirrel.framework",
|
|
2458
|
+
"Versions",
|
|
2459
|
+
"Current",
|
|
2460
|
+
"Resources",
|
|
2461
|
+
"ShipIt"
|
|
2462
|
+
);
|
|
2463
|
+
const INSTALLER_TOKEN = "local-operator-update-installer";
|
|
2464
|
+
const UPDATE_STAGING_DIR = "update-staging";
|
|
2465
|
+
const INSTALLER_POLL_SECONDS = "0.2";
|
|
2466
|
+
const EXTRACTION_TIMEOUT_MS = 10 * 6e4;
|
|
2467
|
+
function shipItPathInBundle(bundlePath) {
|
|
2468
|
+
if (!bundlePath) return null;
|
|
2469
|
+
return path$1.join(bundlePath, SHIPIT_RELATIVE_PATH);
|
|
2470
|
+
}
|
|
2471
|
+
function updateStagingRoot(userDataDir) {
|
|
2472
|
+
return path$1.join(userDataDir, UPDATE_STAGING_DIR);
|
|
2473
|
+
}
|
|
2474
|
+
function updateStagingDir(input) {
|
|
2475
|
+
return path$1.join(
|
|
2476
|
+
updateStagingRoot(input.userDataDir),
|
|
2477
|
+
`${input.version}-${input.nonce}`
|
|
2478
|
+
);
|
|
2479
|
+
}
|
|
2480
|
+
function stagingStatePath(stagingRoot) {
|
|
2481
|
+
return path$1.join(stagingRoot, "state.plist");
|
|
2482
|
+
}
|
|
2483
|
+
function installerLogPath(shipItCacheDir2) {
|
|
2484
|
+
return path$1.join(shipItCacheDir2, "ShipIt_stderr.log");
|
|
2485
|
+
}
|
|
2486
|
+
function extractionArguments(zipPath, stagingRoot) {
|
|
2487
|
+
return ["-x", "-k", zipPath, stagingRoot];
|
|
2488
|
+
}
|
|
2489
|
+
function evaluateStagedBundle(input) {
|
|
2490
|
+
const { reading } = input;
|
|
2491
|
+
if (!reading.exists) {
|
|
2492
|
+
return {
|
|
2493
|
+
ok: false,
|
|
2494
|
+
detail: `the extraction produced no bundle at ${reading.appPath}`
|
|
2495
|
+
};
|
|
2496
|
+
}
|
|
2497
|
+
if (reading.bundleIdentifier !== input.expectedBundleIdentifier) {
|
|
2498
|
+
return {
|
|
2499
|
+
ok: false,
|
|
2500
|
+
detail: `the staged bundle is ${reading.bundleIdentifier ?? "unsigned for an identifier"}, not ${input.expectedBundleIdentifier}`
|
|
2501
|
+
};
|
|
2502
|
+
}
|
|
2503
|
+
if (reading.version !== input.expectedVersion) {
|
|
2504
|
+
return {
|
|
2505
|
+
ok: false,
|
|
2506
|
+
detail: `the staged bundle reports version ${reading.version ?? "unknown"}, not ${input.expectedVersion}`
|
|
2507
|
+
};
|
|
2508
|
+
}
|
|
2509
|
+
if (!reading.executablePresent) {
|
|
2510
|
+
return {
|
|
2511
|
+
ok: false,
|
|
2512
|
+
detail: `the staged bundle at ${reading.appPath} has no executable of its own`
|
|
2513
|
+
};
|
|
2514
|
+
}
|
|
2515
|
+
if (reading.architectures != null && reading.architectures.length > 0 && !reading.architectures.includes(input.runningArchitecture)) {
|
|
2516
|
+
return {
|
|
2517
|
+
ok: false,
|
|
2518
|
+
detail: `the staged bundle is built for ${reading.architectures.join(
|
|
2519
|
+
", "
|
|
2520
|
+
)} and this app is running as ${input.runningArchitecture}`
|
|
2521
|
+
};
|
|
2522
|
+
}
|
|
2523
|
+
return {
|
|
2524
|
+
ok: true,
|
|
2525
|
+
detail: `${input.expectedBundleIdentifier} ${input.expectedVersion}${reading.architectures ? ` (${reading.architectures.join(", ")})` : ""}`
|
|
2526
|
+
};
|
|
2527
|
+
}
|
|
2528
|
+
function sameVolume(stagingDevice, targetDevice) {
|
|
2529
|
+
return stagingDevice != null && stagingDevice === targetDevice;
|
|
2530
|
+
}
|
|
2531
|
+
function fileUrlForDirectory(path2) {
|
|
2532
|
+
const withSlash = path2.endsWith("/") ? path2 : `${path2}/`;
|
|
2533
|
+
return `file://${encodeURI(withSlash)}`;
|
|
2534
|
+
}
|
|
2535
|
+
function shipItStatePlist(input) {
|
|
2536
|
+
return {
|
|
2537
|
+
bundleIdentifier: input.bundleIdentifier,
|
|
2538
|
+
targetBundleURL: fileUrlForDirectory(input.targetBundlePath),
|
|
2539
|
+
updateBundleURL: fileUrlForDirectory(input.updateBundlePath),
|
|
2540
|
+
launchAfterInstallation: true,
|
|
2541
|
+
useUpdateBundleName: true
|
|
2542
|
+
};
|
|
2543
|
+
}
|
|
2544
|
+
function buildInstallerSpawn(input) {
|
|
2545
|
+
const script = `#!/bin/sh
|
|
2546
|
+
# ${INSTALLER_TOKEN}
|
|
2547
|
+
#
|
|
2548
|
+
# Why this exists: the app is about to quit so an install can replace it, and the
|
|
2549
|
+
# installer must not be running its own check for a running instance of the app
|
|
2550
|
+
# while one is still there - ShipIt asks that question once, as its last check
|
|
2551
|
+
# before the swap, and an instance of the target app answers it with "abort"
|
|
2552
|
+
# (SQRLInstallerErrorDomain Code=-9, the 2026-09-18 09:37 install). So this waits
|
|
2553
|
+
# for the app's pid to go, and then replaces itself with the installer: one
|
|
2554
|
+
# process, one pid for the app to record, and no shell left in the way.
|
|
2555
|
+
while kill -0 "$1" 2>/dev/null; do
|
|
2556
|
+
sleep ${INSTALLER_POLL_SECONDS}
|
|
2557
|
+
done
|
|
2558
|
+
exec "$LO_UPDATE_INSTALLER_SHIPIT" "$LO_UPDATE_INSTALLER_LABEL" "$LO_UPDATE_INSTALLER_STATE" >>"$LO_UPDATE_INSTALLER_LOG" 2>&1
|
|
2559
|
+
`;
|
|
2560
|
+
return {
|
|
2561
|
+
script,
|
|
2562
|
+
env: {
|
|
2563
|
+
LO_UPDATE_INSTALLER_SHIPIT: input.shipItPath,
|
|
2564
|
+
LO_UPDATE_INSTALLER_LABEL: input.label,
|
|
2565
|
+
LO_UPDATE_INSTALLER_STATE: input.statePath,
|
|
2566
|
+
LO_UPDATE_INSTALLER_LOG: input.logPath
|
|
2567
|
+
},
|
|
2568
|
+
// `sh -c <script> <name> <pid>`: the name is $0 and the pid is $1, so the
|
|
2569
|
+
// script text carries no path and no number that a process listing of it
|
|
2570
|
+
// would not otherwise show.
|
|
2571
|
+
args: ["-c", script, "lo-update-installer", String(input.appPid)]
|
|
2572
|
+
};
|
|
2573
|
+
}
|
|
2574
|
+
function installerIsAlive(input) {
|
|
2575
|
+
if (input.pid == null || input.commandLine == null) return false;
|
|
2576
|
+
const line = input.commandLine;
|
|
2577
|
+
if (line.includes(INSTALLER_TOKEN)) return true;
|
|
2578
|
+
return input.shipItPath != null && input.stagingRoot != null && line.includes(input.shipItPath) && line.includes(input.stagingRoot);
|
|
2579
|
+
}
|
|
2580
|
+
function installerElsewhere(input) {
|
|
2581
|
+
if (!input.processList || !input.shipItPath || !input.stagingRoot) {
|
|
2582
|
+
return false;
|
|
2583
|
+
}
|
|
2584
|
+
return input.processList.split("\n").some(
|
|
2585
|
+
(line) => line.includes(input.shipItPath) && line.includes(input.stagingRoot)
|
|
2586
|
+
);
|
|
2587
|
+
}
|
|
2453
2588
|
const PENDING_INSTALL_MARKER_FILE = "pending-update-install.json";
|
|
2454
2589
|
const INSTALL_DISK_SLACK_BYTES = 256 * 1024 * 1024;
|
|
2455
2590
|
const DOWNLOAD_PAGE_URL = "https://local-operator.com/download";
|
|
@@ -2639,7 +2774,7 @@ function matchArtifactMetadata(filePath, files, updatePath) {
|
|
|
2639
2774
|
return null;
|
|
2640
2775
|
}
|
|
2641
2776
|
function requiredDiskBytes(input) {
|
|
2642
|
-
return input.artifactSize + input.installedBundleSize *
|
|
2777
|
+
return input.artifactSize + input.installedBundleSize * 3 + INSTALL_DISK_SLACK_BYTES;
|
|
2643
2778
|
}
|
|
2644
2779
|
function formatGiB(bytes) {
|
|
2645
2780
|
return `${(bytes / 1024 ** 3).toFixed(1)} GiB`;
|
|
@@ -2720,7 +2855,7 @@ function verifyStagedArtifact(input) {
|
|
|
2720
2855
|
const installedBundleSize = input.installedBundleSize ?? 0;
|
|
2721
2856
|
const needed = requiredDiskBytes({ artifactSize, installedBundleSize });
|
|
2722
2857
|
if (input.freeBytes < needed) {
|
|
2723
|
-
const footprint = installedBundleSize ? `${formatGiB(artifactSize)} artifact +
|
|
2858
|
+
const footprint = installedBundleSize ? `${formatGiB(artifactSize)} artifact + three ${formatGiB(
|
|
2724
2859
|
installedBundleSize
|
|
2725
2860
|
)} app copies + ${formatGiB(INSTALL_DISK_SLACK_BYTES)} slack` : `${formatGiB(artifactSize)} artifact + ${formatGiB(INSTALL_DISK_SLACK_BYTES)} slack (the installed app could not be measured)`;
|
|
2726
2861
|
return {
|
|
@@ -2837,7 +2972,10 @@ function parsePendingInstallMarker(raw) {
|
|
|
2837
2972
|
targetVersion: parsed.targetVersion,
|
|
2838
2973
|
artifactPath: typeof parsed.artifactPath === "string" ? parsed.artifactPath : "",
|
|
2839
2974
|
startedAt: typeof parsed.startedAt === "string" ? parsed.startedAt : "",
|
|
2840
|
-
watchdogPid: typeof parsed.watchdogPid === "number" ? parsed.watchdogPid : null
|
|
2975
|
+
watchdogPid: typeof parsed.watchdogPid === "number" ? parsed.watchdogPid : null,
|
|
2976
|
+
// Absent on a marker written before this field existed, and absent is the
|
|
2977
|
+
// same answer as the fallback's `null`: ask the install's own launchd job.
|
|
2978
|
+
installerPid: typeof parsed.installerPid === "number" ? parsed.installerPid : null
|
|
2841
2979
|
};
|
|
2842
2980
|
} catch {
|
|
2843
2981
|
return null;
|
|
@@ -3039,9 +3177,48 @@ function isInstallInFlight(input) {
|
|
|
3039
3177
|
const age = pendingInstallAgeSeconds(input.marker, input.now);
|
|
3040
3178
|
if (age === null) return false;
|
|
3041
3179
|
if (age < 0 || age > PENDING_INSTALL_RECENCY_SECONDS) return false;
|
|
3180
|
+
if (input.marker.installerPid != null) return input.installerRunning;
|
|
3042
3181
|
if (input.jobState === "running") return true;
|
|
3043
3182
|
return input.jobState === "registered" && age <= PENDING_INSTALL_HANDOFF_SECONDS;
|
|
3044
3183
|
}
|
|
3184
|
+
function installLivenessText(liveness, installerPid) {
|
|
3185
|
+
if (liveness.decidedBy !== "installer-pid") {
|
|
3186
|
+
return `install job ${liveness.jobState}`;
|
|
3187
|
+
}
|
|
3188
|
+
const named = installerPid ?? "unknown";
|
|
3189
|
+
if (liveness.installerSignal === "elsewhere") {
|
|
3190
|
+
return `the install this app started is still running (pid ${named} has gone; its ShipIt is still at work)`;
|
|
3191
|
+
}
|
|
3192
|
+
if (liveness.installerSignal === "pid") {
|
|
3193
|
+
return `the installer this app started (pid ${named}) is still running`;
|
|
3194
|
+
}
|
|
3195
|
+
return `the installer this app started (pid ${named}) has gone`;
|
|
3196
|
+
}
|
|
3197
|
+
function installLivenessNow(input) {
|
|
3198
|
+
const pid = input.marker?.installerPid ?? null;
|
|
3199
|
+
if (pid == null) {
|
|
3200
|
+
return {
|
|
3201
|
+
installerRunning: false,
|
|
3202
|
+
jobState: input.jobState(),
|
|
3203
|
+
decidedBy: "install-job",
|
|
3204
|
+
// No pid was asked about, so no pid-shaped fact decided.
|
|
3205
|
+
installerSignal: null
|
|
3206
|
+
};
|
|
3207
|
+
}
|
|
3208
|
+
const pidIsLive = installerIsAlive({
|
|
3209
|
+
pid,
|
|
3210
|
+
commandLine: input.installerCommandLine(),
|
|
3211
|
+
shipItPath: input.shipItPath,
|
|
3212
|
+
stagingRoot: input.stagingRoot
|
|
3213
|
+
});
|
|
3214
|
+
const elsewhere = pidIsLive ? false : input.installerElsewhere();
|
|
3215
|
+
return {
|
|
3216
|
+
installerRunning: pidIsLive || elsewhere,
|
|
3217
|
+
jobState: "unread",
|
|
3218
|
+
decidedBy: "installer-pid",
|
|
3219
|
+
installerSignal: pidIsLive ? "pid" : elsewhere ? "elsewhere" : null
|
|
3220
|
+
};
|
|
3221
|
+
}
|
|
3045
3222
|
function evaluateLaunchDuringInstall(input) {
|
|
3046
3223
|
const { marker } = input;
|
|
3047
3224
|
if (!marker) return { kind: "open" };
|
|
@@ -3050,7 +3227,8 @@ function evaluateLaunchDuringInstall(input) {
|
|
|
3050
3227
|
runningVersion: input.runningVersion,
|
|
3051
3228
|
installInFlight: isInstallInFlight({
|
|
3052
3229
|
marker,
|
|
3053
|
-
jobState: input.jobState,
|
|
3230
|
+
jobState: input.liveness.jobState,
|
|
3231
|
+
installerRunning: input.liveness.installerRunning,
|
|
3054
3232
|
now: input.now
|
|
3055
3233
|
})
|
|
3056
3234
|
});
|
|
@@ -3125,7 +3303,7 @@ function installLaunchHoldNotice(marker, options = {}) {
|
|
|
3125
3303
|
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.";
|
|
3126
3304
|
return {
|
|
3127
3305
|
title: "Local Operator is still updating",
|
|
3128
|
-
body: `The update to version ${marker.targetVersion} is still installing —
|
|
3306
|
+
body: `The update to version ${marker.targetVersion} is still installing — opening the app now would cancel it. ${relaunch} Clicking this notice will not open it any sooner.`
|
|
3129
3307
|
};
|
|
3130
3308
|
}
|
|
3131
3309
|
function shipItJobLabel(bundleId) {
|
|
@@ -3209,15 +3387,36 @@ function buildWatchdogPlan(input) {
|
|
|
3209
3387
|
# osascript blocked, no notifier at all), so no notification failure may change
|
|
3210
3388
|
# what this script decides or what it exits with.
|
|
3211
3389
|
#
|
|
3212
|
-
# With no
|
|
3213
|
-
# state is the whole signal: the script waits for (b) rather than
|
|
3214
|
-
# appear window on
|
|
3390
|
+
# With no liveness signal to ask about, the install cannot be consulted at all,
|
|
3391
|
+
# so the swap state is the whole signal: the script waits for (b) rather than
|
|
3392
|
+
# spending an appear window on an install it cannot see and then relaunching on no
|
|
3393
|
+
# evidence. That covers both "no job label" and "no probe", and it is why an
|
|
3394
|
+
# install the app started itself - which has no job by construction - is asked
|
|
3395
|
+
# about by the installer's pid instead (install_live).
|
|
3396
|
+
#
|
|
3397
|
+
# "The install is over" is therefore install_live: the installer's own pid when
|
|
3398
|
+
# the app spawned it, and ShipIt's launchd job otherwise. The job is Squirrel's own
|
|
3399
|
+
# signal and is not deleted - a marker or a watchdog planned by the older version
|
|
3400
|
+
# still has only that one - but it is demoted, for a reason measured on this
|
|
3401
|
+
# machine: the job stays REGISTERED long after a successful install, so asking
|
|
3402
|
+
# launchd first would hold a finished install open for the whole bound.
|
|
3215
3403
|
set -u
|
|
3216
3404
|
APP_PID="\${LO_UPDATE_WATCHDOG_APP_PID:-}"
|
|
3217
3405
|
BUNDLE="\${LO_UPDATE_WATCHDOG_APP_BUNDLE:-}"
|
|
3218
3406
|
NAME="\${LO_UPDATE_WATCHDOG_APP_NAME:-}"
|
|
3219
3407
|
SHIPIT_JOB="\${LO_UPDATE_WATCHDOG_SHIPIT_JOB:-}"
|
|
3408
|
+
# The installer's own pid, when the app started the installer itself. An install
|
|
3409
|
+
# started that way has no launchd job to ask about, so this is the whole liveness
|
|
3410
|
+
# signal and the job label above is not consulted at all - see install_live.
|
|
3411
|
+
INSTALLER_PID="\${LO_UPDATE_WATCHDOG_INSTALLER_PID:-}"
|
|
3220
3412
|
TARGET_VERSION="\${LO_UPDATE_WATCHDOG_TARGET_VERSION:-}"
|
|
3413
|
+
# The two facts that identify this install's installer when the recorded pid is no
|
|
3414
|
+
# longer it: this app's own ShipIt, and this app's own staging root. Empty (an
|
|
3415
|
+
# older plan, or a platform the app could not resolve them on) means the second
|
|
3416
|
+
# half of installer_running declines rather than guessing - the pid alone decides,
|
|
3417
|
+
# which is exactly what this script did before review R2-1.
|
|
3418
|
+
SHIPIT_PATH="\${LO_UPDATE_WATCHDOG_SHIPIT_PATH:-}"
|
|
3419
|
+
STAGING_ROOT="\${LO_UPDATE_WATCHDOG_STAGING_ROOT:-}"
|
|
3221
3420
|
# The two probes, from the platform the plan was built for. Empty means the
|
|
3222
3421
|
# platform has no such tool and the signal is unavailable - not "the job is not
|
|
3223
3422
|
# loaded" and not "the swap has not landed". shipit_loaded, swap_landed and
|
|
@@ -3230,6 +3429,57 @@ if [ -z "$APP_PID" ] || [ -z "$BUNDLE" ]; then
|
|
|
3230
3429
|
fi
|
|
3231
3430
|
now() { date +%s; }
|
|
3232
3431
|
app_running() { kill -0 "$APP_PID" 2>/dev/null; }
|
|
3432
|
+
# Whether ANY process is this install's ShipIt, for the pid that stopped being it.
|
|
3433
|
+
#
|
|
3434
|
+
# The same two facts installerElsewhere requires of a line in its listing: this
|
|
3435
|
+
# app's OWN ShipIt path AND a state plist under this app's OWN staging root. Both,
|
|
3436
|
+
# never either - this machine runs several other applications' ShipIt processes,
|
|
3437
|
+
# and a live foreign one must not be read as this install's.
|
|
3438
|
+
#
|
|
3439
|
+
# One listing per call and only reached when the recorded pid has stopped
|
|
3440
|
+
# answering, which is the same economy the app's own reader keeps: a live pid is
|
|
3441
|
+
# the whole answer (installer_running below).
|
|
3442
|
+
shipit_elsewhere() {
|
|
3443
|
+
[ -n "$SHIPIT_PATH" ] && [ -n "$STAGING_ROOT" ] || return 1
|
|
3444
|
+
ps -Ao command= -ww 2>/dev/null | grep -F "$SHIPIT_PATH" | grep -F "$STAGING_ROOT" >/dev/null 2>&1
|
|
3445
|
+
}
|
|
3446
|
+
# The installer the app spawned, alive or gone. A pid with nothing under it is
|
|
3447
|
+
# gone, which is the same question app_running asks of the app and the same one
|
|
3448
|
+
# the app's own reap asks of a watchdog.
|
|
3449
|
+
#
|
|
3450
|
+
# TWO STEPS, because the app's own reader has two (review R2-1: installLivenessNow
|
|
3451
|
+
# asks installerIsAlive, and installerElsewhere before it declares the installer
|
|
3452
|
+
# dead). One pid is a snapshot: the spawner execs ShipIt so the common case keeps
|
|
3453
|
+
# the pid, but a ShipIt that re-execs or forks internally would leave the recorded
|
|
3454
|
+
# pid dead while an install it is in the middle of continues - and a script that
|
|
3455
|
+
# read that as "the install is over" would relaunch the app into it, which is the
|
|
3456
|
+
# Code=-9 abort this whole change exists to remove.
|
|
3457
|
+
#
|
|
3458
|
+
# The one asymmetry is deliberate and it is in the FIRST step only: the app also
|
|
3459
|
+
# reads the pid's command line (installerIsAlive), because a false "alive" there
|
|
3460
|
+
# would make recovery refuse forever. Here the only cost of a stale pid being
|
|
3461
|
+
# reused is waiting out the bound, and the bound is what ends it - so this asks
|
|
3462
|
+
# kill -0 and spends no listing on a pid that answers. The second step is not
|
|
3463
|
+
# optional in the same way: its false negative is the direction that starts the
|
|
3464
|
+
# app into a live install.
|
|
3465
|
+
installer_running() {
|
|
3466
|
+
[ -n "$INSTALLER_PID" ] || return 1
|
|
3467
|
+
if kill -0 "$INSTALLER_PID" 2>/dev/null; then return 0; fi
|
|
3468
|
+
shipit_elsewhere
|
|
3469
|
+
}
|
|
3470
|
+
# "The install is still running", from whichever signal this install has: the pid
|
|
3471
|
+
# the app spawned, or - for an install Squirrel submitted - the job. There is
|
|
3472
|
+
# never both: an install the app started itself has no job, and the fallback
|
|
3473
|
+
# install has no pid. Asking launchd first would be wrong rather than merely
|
|
3474
|
+
# redundant, because this machine keeps the job REGISTERED long after a
|
|
3475
|
+
# successful install and a registered job is not a running one.
|
|
3476
|
+
install_live() {
|
|
3477
|
+
if [ -n "$INSTALLER_PID" ]; then
|
|
3478
|
+
installer_running
|
|
3479
|
+
return $?
|
|
3480
|
+
fi
|
|
3481
|
+
shipit_loaded
|
|
3482
|
+
}
|
|
3233
3483
|
# Tell the user what is going on, in the only way available: the app is dead
|
|
3234
3484
|
# while this runs, so this is osascript and nothing else - no app process, no
|
|
3235
3485
|
# updater. Backgrounded and status-dropped, because a notification is best-effort
|
|
@@ -3378,8 +3628,14 @@ swap_landed() {
|
|
|
3378
3628
|
}
|
|
3379
3629
|
job_known=0
|
|
3380
3630
|
[ -n "$SHIPIT_PROBE" ] && [ -n "$SHIPIT_JOB" ] && job_known=1
|
|
3631
|
+
# Whether there is any liveness signal to ask at all. An absent signal is not "the
|
|
3632
|
+
# install is over": with neither one the script has the swap state and the bound,
|
|
3633
|
+
# exactly as it does on a platform with no probes.
|
|
3634
|
+
signal_known=0
|
|
3635
|
+
[ -n "$INSTALLER_PID" ] && signal_known=1
|
|
3636
|
+
[ "$job_known" -eq 1 ] && signal_known=1
|
|
3381
3637
|
decided() {
|
|
3382
|
-
if [ "$
|
|
3638
|
+
if [ "$signal_known" -eq 1 ] && ! install_live; then return 0; fi
|
|
3383
3639
|
swap_landed && return 0
|
|
3384
3640
|
return 1
|
|
3385
3641
|
}
|
|
@@ -3390,28 +3646,69 @@ while app_running; do
|
|
|
3390
3646
|
if [ "$(now)" -ge "$deadline" ]; then break; fi
|
|
3391
3647
|
sleep ${intervalSeconds}
|
|
3392
3648
|
done
|
|
3393
|
-
# 1b. The app is gone
|
|
3394
|
-
#
|
|
3395
|
-
#
|
|
3396
|
-
#
|
|
3397
|
-
#
|
|
3398
|
-
#
|
|
3649
|
+
# 1b. The app is gone and an install is at work, so the window that just
|
|
3650
|
+
# vanished owes the person an explanation a few seconds in. Only the PAUSE is
|
|
3651
|
+
# here: the notice is raised at 2b below, once the liveness question has an
|
|
3652
|
+
# answer (QA round 2, Q1).
|
|
3653
|
+
#
|
|
3654
|
+
# It used to be raised from here, and that is the one thing this step could
|
|
3655
|
+
# not do on Squirrel's path: the job is submitted as part of the quit, so
|
|
3656
|
+
# launchd can answer 113 for the first seconds of the appear window below -
|
|
3657
|
+
# which is exactly why that window exists - and a gate evaluated once, before
|
|
3658
|
+
# the thing it gates on has appeared, suppressed the only notice the user was
|
|
3659
|
+
# going to get in the moment they most needed it.
|
|
3399
3660
|
if ! app_running; then
|
|
3400
3661
|
sleep ${announceSeconds}
|
|
3401
|
-
notify "Installing the update. Keep Local Operator closed until it opens again by itself — this can take a few minutes."
|
|
3402
3662
|
fi
|
|
3403
|
-
# 2.
|
|
3404
|
-
# instant the app is gone: give it a bounded window to appear
|
|
3405
|
-
# "
|
|
3406
|
-
#
|
|
3407
|
-
#
|
|
3408
|
-
|
|
3663
|
+
# 2. An install Squirrel submitted starts as part of the quit, so it may not be
|
|
3664
|
+
# visible the instant the app is gone: give it a bounded window to appear
|
|
3665
|
+
# before treating "nothing there" as "the install is over". Two cases skip it,
|
|
3666
|
+
# and the second is UX U6:
|
|
3667
|
+
#
|
|
3668
|
+
# - no signal to ask about: an appear window for an unaskable install is
|
|
3669
|
+
# ${appearSeconds}s of pretending, and it used to end by relaunching with
|
|
3670
|
+
# no evidence at all;
|
|
3671
|
+
# - an installer THIS APP spawned itself. That pid exists before the quit, so
|
|
3672
|
+
# the window cannot tell a late start from no start at all: if it is gone at
|
|
3673
|
+
# the first poll the install is over (or never began), and waiting out the
|
|
3674
|
+
# window only pushes the failure report and the app's return half a minute
|
|
3675
|
+
# further away.
|
|
3676
|
+
if [ "$signal_known" -eq 1 ] && [ -z "$INSTALLER_PID" ]; then
|
|
3409
3677
|
appear_deadline=$(( $(now) + ${appearSeconds} ))
|
|
3410
|
-
while !
|
|
3678
|
+
while ! install_live; do
|
|
3411
3679
|
if [ "$(now)" -ge "$appear_deadline" ] || [ "$(now)" -ge "$deadline" ]; then break; fi
|
|
3412
3680
|
sleep ${intervalSeconds}
|
|
3413
3681
|
done
|
|
3414
3682
|
fi
|
|
3683
|
+
# 2b. The window that vanished, explained - and it is raised HERE rather than at
|
|
3684
|
+
# 1b so that on Squirrel's path it is asked once the job it asks about has
|
|
3685
|
+
# been submitted (QA round 2, Q1). The two conditions are the ones the notice
|
|
3686
|
+
# has to meet, each of them a defect it once had (UX U1):
|
|
3687
|
+
#
|
|
3688
|
+
# - the app is still gone: a quit the user cancelled installs nothing, and
|
|
3689
|
+
# nothing should be claimed;
|
|
3690
|
+
# - the install is STILL RUNNING at the moment the notice is raised. Naming
|
|
3691
|
+
# a duration was the other half of that defect: it promised minutes, and
|
|
3692
|
+
# on the direct path the whole window is 2-5 s - so the notice arrived
|
|
3693
|
+
# telling a person to keep closed an app ShipIt had already reopened. The
|
|
3694
|
+
# instruction that matters, "don't reopen it", is the same on both paths
|
|
3695
|
+
# and is all this says now.
|
|
3696
|
+
#
|
|
3697
|
+
# What moving it costs: on Squirrel's path the notice now waits for the job to
|
|
3698
|
+
# appear (the loop above breaks the moment it does, so that is a poll
|
|
3699
|
+
# interval, not the whole window), and a job that never appears raises no
|
|
3700
|
+
# notice at all - correct, because nothing is installing and the app is about
|
|
3701
|
+
# to come back with its own failure panel. On the direct path the loop is
|
|
3702
|
+
# skipped and this is the same instant the old code used.
|
|
3703
|
+
#
|
|
3704
|
+
# With no signal to ask about, the notice is unconditional: an install that
|
|
3705
|
+
# cannot be asked about cannot be shown to be over either, and staying silent
|
|
3706
|
+
# there would be the original defect back again.
|
|
3707
|
+
if ! app_running; then
|
|
3708
|
+
if [ "$signal_known" -eq 0 ] || install_live; then
|
|
3709
|
+
notify "Installing the update. Keep Local Operator closed until it opens again by itself."
|
|
3710
|
+
fi
|
|
3711
|
+
fi
|
|
3415
3712
|
# 3. Wait for the install to be decided - by the job going, or by the swap
|
|
3416
3713
|
# landing. The soft bound ends this wait only when there is no loaded job
|
|
3417
3714
|
# answering for the install: see the hold below.
|
|
@@ -3419,7 +3716,7 @@ holding=0
|
|
|
3419
3716
|
while :; do
|
|
3420
3717
|
if decided; then break; fi
|
|
3421
3718
|
if [ "$(now)" -ge "$deadline" ]; then
|
|
3422
|
-
if [ "$
|
|
3719
|
+
if [ "$signal_known" -eq 1 ] && install_live; then
|
|
3423
3720
|
holding=1
|
|
3424
3721
|
notify "The update is still installing. Keep Local Operator closed; it will open again when the install finishes."
|
|
3425
3722
|
fi
|
|
@@ -3457,9 +3754,23 @@ if [ "$holding" -eq 1 ]; then
|
|
|
3457
3754
|
fi
|
|
3458
3755
|
# Let Squirrel's own relaunch (which happens as the job finishes) land first.
|
|
3459
3756
|
sleep ${settleSeconds}
|
|
3460
|
-
# 4.
|
|
3461
|
-
#
|
|
3462
|
-
#
|
|
3757
|
+
# 4. A landed swap gets a LAUNCH OF ITS OWN, and the only difference is -g, which
|
|
3758
|
+
# is UX U7. ShipIt relaunches the app itself once it has swapped - that
|
|
3759
|
+
# instruction IS the state plist this app writes (launchAfterInstallation) - so
|
|
3760
|
+
# on a successful install the app was already back, under a new pid, when this
|
|
3761
|
+
# ran, and the unflagged open -a below fronted a window the user was working in
|
|
3762
|
+
# ~40 s after an install that takes seconds. The check above cannot see that
|
|
3763
|
+
# instance: it holds the OLD pid, which is dead by then, and a name probe must
|
|
3764
|
+
# not be used here (the script's own text is in the watchdog's command line, so
|
|
3765
|
+
# "is anything named like the app running" answers yes about the watchdog). So
|
|
3766
|
+
# the launch still happens - the app has to be running whether or not ShipIt's
|
|
3767
|
+
# own relaunch worked - and -g is what keeps it from taking focus.
|
|
3768
|
+
if swap_landed; then
|
|
3769
|
+
if [ -n "$NAME" ] && [ -x "$BUNDLE/Contents/MacOS/$NAME" ]; then
|
|
3770
|
+
open -g -a "$BUNDLE" >/dev/null 2>&1 || "$BUNDLE/Contents/MacOS/$NAME" >/dev/null 2>&1 &
|
|
3771
|
+
fi
|
|
3772
|
+
exit 0
|
|
3773
|
+
fi
|
|
3463
3774
|
if app_running; then exit 0; fi
|
|
3464
3775
|
if [ -n "$NAME" ] && [ -x "$BUNDLE/Contents/MacOS/$NAME" ]; then
|
|
3465
3776
|
open -a "$BUNDLE" >/dev/null 2>&1 || "$BUNDLE/Contents/MacOS/$NAME" >/dev/null 2>&1 &
|
|
@@ -3473,6 +3784,15 @@ exit 0
|
|
|
3473
3784
|
LO_UPDATE_WATCHDOG_APP_BUNDLE: input.appBundlePath,
|
|
3474
3785
|
LO_UPDATE_WATCHDOG_APP_NAME: input.executableName,
|
|
3475
3786
|
LO_UPDATE_WATCHDOG_SHIPIT_JOB: input.shipItJob ?? "",
|
|
3787
|
+
// Empty rather than absent when the app spawned no installer: the script
|
|
3788
|
+
// reads this with a default (`:-`) and its emptiness is the whole answer to
|
|
3789
|
+
// "which liveness signal is this install's" (see `install_live`).
|
|
3790
|
+
LO_UPDATE_WATCHDOG_INSTALLER_PID: input.installerPid != null ? String(input.installerPid) : "",
|
|
3791
|
+
// The identity pair, empty rather than absent for the same reason as the pid:
|
|
3792
|
+
// the script reads them with `:-` defaults and an empty value is the whole
|
|
3793
|
+
// answer to "can this install's installer be recognised under another pid".
|
|
3794
|
+
LO_UPDATE_WATCHDOG_SHIPIT_PATH: input.shipItPath ?? "",
|
|
3795
|
+
LO_UPDATE_WATCHDOG_STAGING_ROOT: input.stagingRoot ?? "",
|
|
3476
3796
|
LO_UPDATE_WATCHDOG_TARGET_VERSION: input.targetVersion ?? "",
|
|
3477
3797
|
// Empty off macOS, which the script reads as "this signal is not available
|
|
3478
3798
|
// here" rather than as a negative answer (see `watchdogSignals`).
|
|
@@ -17875,6 +18195,7 @@ function serverUpdateFailureSentence(input) {
|
|
|
17875
18195
|
const VERSION_LINE_REGEX = /Version:\s*([^\n]+)/;
|
|
17876
18196
|
const VERSION_CLEAN_REGEX = /^v/i;
|
|
17877
18197
|
const BETA_VERSION_REGEX = /v\d+\.\d+\.\d+\.beta\.\d+/;
|
|
18198
|
+
const ARCHITECTURE_SEPARATOR_REGEX = /\s+/;
|
|
17878
18199
|
const GLOBAL_UPDATE_TIMEOUT_MS = 15 * 60 * 1e3;
|
|
17879
18200
|
const SOURCE_REBUILD_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
17880
18201
|
const BUNDLE_ID_REGEX = /^[\w.-]+$/;
|
|
@@ -17938,6 +18259,112 @@ function readCommandOutput(command, args) {
|
|
|
17938
18259
|
return null;
|
|
17939
18260
|
}
|
|
17940
18261
|
}
|
|
18262
|
+
function runExtraction(zipPath, stagingRoot, options = {}) {
|
|
18263
|
+
const command = options.command ?? DITTO_PATH;
|
|
18264
|
+
const timeoutMs = options.timeoutMs ?? EXTRACTION_TIMEOUT_MS;
|
|
18265
|
+
return new Promise((resolve) => {
|
|
18266
|
+
let settled = false;
|
|
18267
|
+
const finish = (result) => {
|
|
18268
|
+
if (settled) return;
|
|
18269
|
+
settled = true;
|
|
18270
|
+
clearTimeout(timer);
|
|
18271
|
+
resolve(result);
|
|
18272
|
+
};
|
|
18273
|
+
const child = node_child_process.spawn(command, extractionArguments(zipPath, stagingRoot), {
|
|
18274
|
+
/*
|
|
18275
|
+
* Its own process group, so the bound below can take down the whole tree.
|
|
18276
|
+
* A kill by pid alone leaves anything the extractor started running, and
|
|
18277
|
+
* this is the one process in the change that is killed rather than waited
|
|
18278
|
+
* for: the same teardown discipline the app applies to its own children,
|
|
18279
|
+
* for the same reason - a half-written staging tree with a live process
|
|
18280
|
+
* inside it is worse than no staging tree.
|
|
18281
|
+
*/
|
|
18282
|
+
detached: true,
|
|
18283
|
+
stdio: ["ignore", "ignore", "pipe"]
|
|
18284
|
+
});
|
|
18285
|
+
let stderr = "";
|
|
18286
|
+
child.stderr?.on("data", (chunk) => {
|
|
18287
|
+
stderr += String(chunk);
|
|
18288
|
+
});
|
|
18289
|
+
const timer = setTimeout(() => {
|
|
18290
|
+
logger.warn(
|
|
18291
|
+
`The update bundle's extraction did not finish within ${timeoutMs} ms; stopping it.`,
|
|
18292
|
+
LogFileType.UPDATE_SERVICE
|
|
18293
|
+
);
|
|
18294
|
+
killExtractionTree(
|
|
18295
|
+
child.pid,
|
|
18296
|
+
(message) => logger.warn(message, LogFileType.UPDATE_SERVICE)
|
|
18297
|
+
);
|
|
18298
|
+
finish({
|
|
18299
|
+
ok: false,
|
|
18300
|
+
reason: `it was still running after ${timeoutMs} ms`
|
|
18301
|
+
});
|
|
18302
|
+
}, timeoutMs);
|
|
18303
|
+
child.on("error", (error) => finish({ ok: false, reason: String(error) }));
|
|
18304
|
+
child.on(
|
|
18305
|
+
"close",
|
|
18306
|
+
(code) => finish(
|
|
18307
|
+
code === 0 ? { ok: true } : {
|
|
18308
|
+
ok: false,
|
|
18309
|
+
reason: `ditto exited ${code}${stderr.trim() ? `: ${stderr.trim()}` : ""}`
|
|
18310
|
+
}
|
|
18311
|
+
)
|
|
18312
|
+
);
|
|
18313
|
+
});
|
|
18314
|
+
}
|
|
18315
|
+
function killExtractionTree(pid, warn) {
|
|
18316
|
+
if (pid == null) return;
|
|
18317
|
+
try {
|
|
18318
|
+
process.kill(-pid, "SIGKILL");
|
|
18319
|
+
} catch (groupError) {
|
|
18320
|
+
try {
|
|
18321
|
+
process.kill(pid, "SIGKILL");
|
|
18322
|
+
} catch (error) {
|
|
18323
|
+
warn(
|
|
18324
|
+
`Could not stop the wedged extraction (pid ${pid}): ${String(error)}`
|
|
18325
|
+
);
|
|
18326
|
+
}
|
|
18327
|
+
}
|
|
18328
|
+
}
|
|
18329
|
+
function isExecutableFile(path2) {
|
|
18330
|
+
try {
|
|
18331
|
+
fs.accessSync(path2, fs.constants.X_OK);
|
|
18332
|
+
return fs.statSync(path2).isFile();
|
|
18333
|
+
} catch {
|
|
18334
|
+
return false;
|
|
18335
|
+
}
|
|
18336
|
+
}
|
|
18337
|
+
function deviceId(target2) {
|
|
18338
|
+
try {
|
|
18339
|
+
return fs.statSync(target2).dev;
|
|
18340
|
+
} catch {
|
|
18341
|
+
return null;
|
|
18342
|
+
}
|
|
18343
|
+
}
|
|
18344
|
+
function readPlistValue(plistPath, key) {
|
|
18345
|
+
return readCommandOutput("/usr/bin/plutil", [
|
|
18346
|
+
"-extract",
|
|
18347
|
+
key,
|
|
18348
|
+
"raw",
|
|
18349
|
+
"-o",
|
|
18350
|
+
"-",
|
|
18351
|
+
plistPath
|
|
18352
|
+
]);
|
|
18353
|
+
}
|
|
18354
|
+
function readStagedBundle(appPath) {
|
|
18355
|
+
const plistPath = path$1.join(appPath, "Contents", "Info.plist");
|
|
18356
|
+
const executableName = readPlistValue(plistPath, "CFBundleExecutable");
|
|
18357
|
+
const executablePath = executableName == null ? null : path$1.join(appPath, "Contents", "MacOS", executableName);
|
|
18358
|
+
const architectures2 = executablePath != null && fs.existsSync(executablePath) ? (readCommandOutput("/usr/bin/lipo", ["-archs", executablePath]) ?? "").split(ARCHITECTURE_SEPARATOR_REGEX).filter((entry) => entry.length > 0) : null;
|
|
18359
|
+
return {
|
|
18360
|
+
appPath,
|
|
18361
|
+
exists: fs.existsSync(appPath),
|
|
18362
|
+
bundleIdentifier: readPlistValue(plistPath, "CFBundleIdentifier"),
|
|
18363
|
+
version: readPlistValue(plistPath, "CFBundleShortVersionString"),
|
|
18364
|
+
executablePresent: executablePath != null && fs.existsSync(executablePath),
|
|
18365
|
+
architectures: architectures2 && architectures2.length > 0 ? architectures2 : null
|
|
18366
|
+
};
|
|
18367
|
+
}
|
|
17941
18368
|
const INSTALLER_PROBE_TIMEOUT_MS = 5e3;
|
|
17942
18369
|
const DRIFT_HOLD_REPORT = {
|
|
17943
18370
|
"no-drift": "nothing to do",
|
|
@@ -18020,6 +18447,34 @@ function probeShipItInstallJobState() {
|
|
|
18020
18447
|
return result.error || result.status == null ? null : { status: result.status, output: result.stdout ?? "" };
|
|
18021
18448
|
});
|
|
18022
18449
|
}
|
|
18450
|
+
function installLivenessFor(input) {
|
|
18451
|
+
const commandLine = input.installerCommandLine ?? defaultInstallerCommandLine;
|
|
18452
|
+
const processList = input.processList ?? defaultProcessList;
|
|
18453
|
+
const bundlePath = input.bundlePath ?? (() => appBundleFromExecutable(process.execPath));
|
|
18454
|
+
const shipItPath = shipItPathInBundle(bundlePath());
|
|
18455
|
+
const stagingRoot = updateStagingRoot(electron.app.getPath("userData"));
|
|
18456
|
+
return installLivenessNow({
|
|
18457
|
+
marker: input.marker,
|
|
18458
|
+
installerCommandLine: () => {
|
|
18459
|
+
const pid = input.marker?.installerPid ?? null;
|
|
18460
|
+
return pid == null ? null : commandLine(pid);
|
|
18461
|
+
},
|
|
18462
|
+
installerElsewhere: () => installerElsewhere({
|
|
18463
|
+
processList: processList(),
|
|
18464
|
+
shipItPath,
|
|
18465
|
+
stagingRoot
|
|
18466
|
+
}),
|
|
18467
|
+
shipItPath,
|
|
18468
|
+
stagingRoot,
|
|
18469
|
+
jobState: input.jobState ?? probeShipItInstallJobState
|
|
18470
|
+
});
|
|
18471
|
+
}
|
|
18472
|
+
function defaultInstallerCommandLine(pid) {
|
|
18473
|
+
return readCommandOutput("/bin/ps", ["-o", "command=", "-p", String(pid)]);
|
|
18474
|
+
}
|
|
18475
|
+
function defaultProcessList() {
|
|
18476
|
+
return readCommandOutput("/bin/ps", ["-Ao", "command=", "-ww"]);
|
|
18477
|
+
}
|
|
18023
18478
|
function startRelaunchWatchdog(input) {
|
|
18024
18479
|
if (process.platform !== "darwin" || !electron.app.isPackaged) return null;
|
|
18025
18480
|
const bundlePath = appBundleFromExecutable(process.execPath);
|
|
@@ -18031,6 +18486,16 @@ function startRelaunchWatchdog(input) {
|
|
|
18031
18486
|
// watchdog cannot ask a name about its own ancestor (review R1).
|
|
18032
18487
|
appPid: process.pid,
|
|
18033
18488
|
shipItJob: shipItJobForRunningBundle(),
|
|
18489
|
+
// The installer this app started itself, when there is one: without it the
|
|
18490
|
+
// script would wait on a launchd job that this path never submits, and settle
|
|
18491
|
+
// on the bound instead of on the install (management MAJOR-1).
|
|
18492
|
+
installerPid: input.installerPid ?? null,
|
|
18493
|
+
// The same two facts `installerIsAlive`/`installerElsewhere` use on this side
|
|
18494
|
+
// of the seam, so the script's own "is the installer gone" answer cannot
|
|
18495
|
+
// disagree with the app's (review R2-1): the script asks them of a process
|
|
18496
|
+
// listing before it relaunches the app into an install that is still running.
|
|
18497
|
+
shipItPath: shipItPathInBundle(bundlePath),
|
|
18498
|
+
stagingRoot: updateStagingRoot(electron.app.getPath("userData")),
|
|
18034
18499
|
// What "the install is over" is measured against on disk (review R11):
|
|
18035
18500
|
// the updater's advertised version, and nothing that is already in place
|
|
18036
18501
|
// (review R15).
|
|
@@ -18053,7 +18518,7 @@ function startRelaunchWatchdog(input) {
|
|
|
18053
18518
|
});
|
|
18054
18519
|
child.unref();
|
|
18055
18520
|
input.log(
|
|
18056
|
-
`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
|
|
18521
|
+
`Started the update relaunch watchdog (pid ${child.pid ?? "unknown"}): it starts the app again as soon as ${input.installerPid != null ? "the installer this app started is gone" : "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 is still live at the first, which is the case where starting the app would cancel the install.`
|
|
18057
18522
|
);
|
|
18058
18523
|
return child.pid ?? null;
|
|
18059
18524
|
} catch (error) {
|
|
@@ -18074,9 +18539,12 @@ function ensureRelaunchWatchdog(input) {
|
|
|
18074
18539
|
);
|
|
18075
18540
|
return true;
|
|
18076
18541
|
}
|
|
18077
|
-
const launched = input.startWatchdog(
|
|
18542
|
+
const launched = input.startWatchdog(
|
|
18543
|
+
input.marker.targetVersion,
|
|
18544
|
+
input.marker.installerPid ?? null
|
|
18545
|
+
);
|
|
18078
18546
|
input.log(
|
|
18079
|
-
launched != null ? `Started relaunch watchdog ${launched} for the install already in flight; it will start the app when the install
|
|
18547
|
+
launched != null ? `Started relaunch watchdog ${launched} for the install already in flight; it will start the app when the install 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."
|
|
18080
18548
|
);
|
|
18081
18549
|
return launched != null;
|
|
18082
18550
|
}
|
|
@@ -18152,16 +18620,20 @@ async function showInstallHoldNotice(notice, bannerDeadlineMs = LAUNCH_HOLD_NOTI
|
|
|
18152
18620
|
function holdLaunchForLiveInstall(options) {
|
|
18153
18621
|
if (!electron.app.isPackaged) return false;
|
|
18154
18622
|
const marker = readPendingInstallMarker(electron.app.getPath("userData"));
|
|
18155
|
-
const
|
|
18623
|
+
const liveness = installLivenessFor({
|
|
18624
|
+
marker,
|
|
18625
|
+
jobState: options.jobState,
|
|
18626
|
+
installerCommandLine: options.installerCommandLine
|
|
18627
|
+
});
|
|
18156
18628
|
const reading = evaluateLaunchDuringInstall({
|
|
18157
18629
|
marker,
|
|
18158
|
-
|
|
18630
|
+
liveness,
|
|
18159
18631
|
runningVersion: options.runningVersion ?? electron.app.getVersion(),
|
|
18160
18632
|
now: options.now
|
|
18161
18633
|
});
|
|
18162
18634
|
if (reading.kind === "open") return false;
|
|
18163
18635
|
options.log(
|
|
18164
|
-
`Holding this launch for the install of version ${reading.marker.targetVersion} that is still running (
|
|
18636
|
+
`Holding this launch for the install of version ${reading.marker.targetVersion} that is still running (${installLivenessText(liveness, reading.marker.installerPid)}): 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.`
|
|
18165
18637
|
);
|
|
18166
18638
|
const relaunchEnsured = ensureRelaunchWatchdog({
|
|
18167
18639
|
marker: reading.marker,
|
|
@@ -18196,10 +18668,11 @@ function holdLaunchForLiveInstall(options) {
|
|
|
18196
18668
|
});
|
|
18197
18669
|
return true;
|
|
18198
18670
|
}
|
|
18199
|
-
function defaultStartRelaunchWatchdog(targetVersion) {
|
|
18671
|
+
function defaultStartRelaunchWatchdog(targetVersion, installerPid) {
|
|
18200
18672
|
return startRelaunchWatchdog({
|
|
18201
18673
|
targetVersion,
|
|
18202
18674
|
runningVersion: electron.app.getVersion(),
|
|
18675
|
+
installerPid,
|
|
18203
18676
|
log: (message) => logger.info(message, LogFileType.UPDATE_SERVICE)
|
|
18204
18677
|
});
|
|
18205
18678
|
}
|
|
@@ -18218,6 +18691,12 @@ class UpdateService {
|
|
|
18218
18691
|
* available", which is how `shouldFilterUpdateError` would otherwise read it.
|
|
18219
18692
|
*/
|
|
18220
18693
|
updateStage = "idle";
|
|
18694
|
+
/**
|
|
18695
|
+
* The install that just landed, for the one-line affirmation the next launch
|
|
18696
|
+
* owes the user (UX U4). Null once it has been reported.
|
|
18697
|
+
*/
|
|
18698
|
+
pendingInstallSucceeded = null;
|
|
18699
|
+
installSucceededDelivered = false;
|
|
18221
18700
|
/**
|
|
18222
18701
|
* How many app-channel checks are in flight right now.
|
|
18223
18702
|
*
|
|
@@ -18273,6 +18752,17 @@ class UpdateService {
|
|
|
18273
18752
|
lastPublishedBackendVersion = null;
|
|
18274
18753
|
/** Path of the artifact this run downloaded, when the updater reported one. */
|
|
18275
18754
|
downloadedArtifactPath = null;
|
|
18755
|
+
/**
|
|
18756
|
+
* The artifact path the current pre-flight verified, for the staging step.
|
|
18757
|
+
*
|
|
18758
|
+
* `ensureStagedArtifactVerified` resolves it (the updater's own recorded path
|
|
18759
|
+
* first, then the cache's pending directory) and hashes it; staging needs the
|
|
18760
|
+
* same path and must not resolve it a second time, because the second answer is
|
|
18761
|
+
* allowed to be a different file than the one whose sha512 was checked. Set
|
|
18762
|
+
* only on a passing pre-flight, so a refusal leaves this null and staging
|
|
18763
|
+
* refuses too (`stageInstallerHandoff`).
|
|
18764
|
+
*/
|
|
18765
|
+
verifiedArtifactPath = null;
|
|
18276
18766
|
/**
|
|
18277
18767
|
* Artifacts that failed their size/sha512/disk check, by version.
|
|
18278
18768
|
*
|
|
@@ -18541,17 +19031,17 @@ class UpdateService {
|
|
|
18541
19031
|
return;
|
|
18542
19032
|
}
|
|
18543
19033
|
const marker = readPendingInstallMarker(this.markerDir());
|
|
18544
|
-
const
|
|
19034
|
+
const liveness = this.installLiveness(marker);
|
|
18545
19035
|
if (marker) {
|
|
18546
19036
|
logger.info(
|
|
18547
|
-
`Pending install marker for version ${marker.targetVersion} on disk;
|
|
19037
|
+
`Pending install marker for version ${marker.targetVersion} on disk; ${installLivenessText(liveness, marker.installerPid)}.`,
|
|
18548
19038
|
LogFileType.UPDATE_SERVICE
|
|
18549
19039
|
);
|
|
18550
19040
|
}
|
|
18551
19041
|
const outcome = evaluatePendingInstall({
|
|
18552
19042
|
marker,
|
|
18553
19043
|
runningVersion: electron.app.getVersion(),
|
|
18554
|
-
// Probed once per evaluation, and only on macOS: a
|
|
19044
|
+
// Probed once per evaluation, and only on macOS: a live installer plus a
|
|
18555
19045
|
// current marker is what makes "still on the old version" an install in
|
|
18556
19046
|
// progress rather than an install that failed. A job that is registered
|
|
18557
19047
|
// but not running is the state a finished install leaves behind, and
|
|
@@ -18562,7 +19052,8 @@ class UpdateService {
|
|
|
18562
19052
|
// bounds (review R6, R11).
|
|
18563
19053
|
installInFlight: isInstallInFlight({
|
|
18564
19054
|
marker,
|
|
18565
|
-
jobState
|
|
19055
|
+
jobState: liveness.jobState,
|
|
19056
|
+
installerRunning: liveness.installerRunning
|
|
18566
19057
|
})
|
|
18567
19058
|
});
|
|
18568
19059
|
switch (outcome.kind) {
|
|
@@ -18574,8 +19065,13 @@ class UpdateService {
|
|
|
18574
19065
|
LogFileType.UPDATE_SERVICE
|
|
18575
19066
|
);
|
|
18576
19067
|
this.reapWatchdog(outcome.marker, true);
|
|
19068
|
+
this.reapStagedUpdateTrees();
|
|
18577
19069
|
clearPendingInstallMarker(this.markerDir());
|
|
18578
19070
|
this.retireSupersededInstallRecord();
|
|
19071
|
+
this.pendingInstallSucceeded = {
|
|
19072
|
+
version: outcome.marker.targetVersion
|
|
19073
|
+
};
|
|
19074
|
+
this.scheduleInstallSucceededDelivery();
|
|
18579
19075
|
return;
|
|
18580
19076
|
case "stale":
|
|
18581
19077
|
logger.info(
|
|
@@ -18583,13 +19079,14 @@ class UpdateService {
|
|
|
18583
19079
|
LogFileType.UPDATE_SERVICE
|
|
18584
19080
|
);
|
|
18585
19081
|
this.reapWatchdog(outcome.marker, true);
|
|
19082
|
+
this.reapStagedUpdateTrees();
|
|
18586
19083
|
clearPendingInstallMarker(this.markerDir());
|
|
18587
19084
|
this.retireSupersededInstallRecord();
|
|
18588
19085
|
return;
|
|
18589
19086
|
case "in-flight": {
|
|
18590
19087
|
this.installWasInFlight = true;
|
|
18591
19088
|
logger.info(
|
|
18592
|
-
`Update marker: the install of version ${outcome.marker.targetVersion} is still running (
|
|
19089
|
+
`Update marker: the install of version ${outcome.marker.targetVersion} is still running (${installLivenessText(liveness, outcome.marker.installerPid)}). Leaving the marker, the staged tree, the install job and the relaunch watchdog in place.`,
|
|
18593
19090
|
LogFileType.UPDATE_SERVICE
|
|
18594
19091
|
);
|
|
18595
19092
|
this.pendingInstallInFlight = installInFlightPayload(
|
|
@@ -18634,6 +19131,7 @@ class UpdateService {
|
|
|
18634
19131
|
this.stopInstallInFlightRecheck();
|
|
18635
19132
|
this.reapWatchdog(outcome.marker, false);
|
|
18636
19133
|
this.reapFailedInstallLeftovers();
|
|
19134
|
+
this.reapStagedUpdateTrees();
|
|
18637
19135
|
this.schedulePendingInstallFailureDelivery();
|
|
18638
19136
|
}
|
|
18639
19137
|
}
|
|
@@ -18645,7 +19143,14 @@ class UpdateService {
|
|
|
18645
19143
|
* before a service exists; the rule itself lives there.
|
|
18646
19144
|
*/
|
|
18647
19145
|
bundleIdentifier() {
|
|
18648
|
-
|
|
19146
|
+
const bundlePath = this.runningBundleProbe ? this.runningBundleProbe() : appBundleFromExecutable(process.execPath);
|
|
19147
|
+
if (!bundlePath) return null;
|
|
19148
|
+
const value = readCommandOutput("/usr/bin/defaults", [
|
|
19149
|
+
"read",
|
|
19150
|
+
path$1.join(bundlePath, "Contents", "Info.plist"),
|
|
19151
|
+
"CFBundleIdentifier"
|
|
19152
|
+
]);
|
|
19153
|
+
return value && BUNDLE_ID_REGEX.test(value) ? value : null;
|
|
18649
19154
|
}
|
|
18650
19155
|
/**
|
|
18651
19156
|
* Squirrel's own stderr log for this app, when it exists.
|
|
@@ -18804,6 +19309,23 @@ class UpdateService {
|
|
|
18804
19309
|
* it.
|
|
18805
19310
|
*/
|
|
18806
19311
|
installJobStateProbe = null;
|
|
19312
|
+
/**
|
|
19313
|
+
* The `.app` a harness wants this service to treat as the one it runs from.
|
|
19314
|
+
*
|
|
19315
|
+
* A seam, and only a seam: production never sets it, and it exists because
|
|
19316
|
+
* `appBundleFromExecutable(process.execPath)` answers "not a bundle" for every
|
|
19317
|
+
* process that is not a packaged Electron app - which is every harness that
|
|
19318
|
+
* drives this module. Without it, the staging step and the installer's own
|
|
19319
|
+
* liveness could only be asserted by reading their source, and the shipped
|
|
19320
|
+
* code (the `ditto` extraction, the seal probe, the state plist, the detached
|
|
19321
|
+
* spawner) would be exactly the part nothing exercises.
|
|
19322
|
+
*
|
|
19323
|
+
* Substituting it does not change what the code does to the substitute: the
|
|
19324
|
+
* bundle that is pointed at is staged, sealed, probed and handed over as if it
|
|
19325
|
+
* were the running one, and a test that points it at a bundle it built in its
|
|
19326
|
+
* own scratch directory is driving the real path rather than a stub.
|
|
19327
|
+
*/
|
|
19328
|
+
runningBundleProbe = null;
|
|
18807
19329
|
/**
|
|
18808
19330
|
* What this app's ShipIt install job is doing, right now.
|
|
18809
19331
|
*
|
|
@@ -18819,6 +19341,19 @@ class UpdateService {
|
|
|
18819
19341
|
if (this.installJobStateProbe) return this.installJobStateProbe();
|
|
18820
19342
|
return probeShipItInstallJobState();
|
|
18821
19343
|
}
|
|
19344
|
+
/**
|
|
19345
|
+
* What the pre-quit work is doing, on the update surface that is already up.
|
|
19346
|
+
*
|
|
19347
|
+
* A phase rather than a percentage: the three steps are `codesign` over the
|
|
19348
|
+
* installed bundle plus a hash of the artifact, the `ditto` extraction and its
|
|
19349
|
+
* seal probe, and the spawn - each of them one indivisible call this app does not
|
|
19350
|
+
* get progress out of, and inventing a bar for them would be a lie about what is
|
|
19351
|
+
* known (UX U5). Sent before each step starts, so the sentence on screen always
|
|
19352
|
+
* names the work that is actually happening.
|
|
19353
|
+
*/
|
|
19354
|
+
reportInstallProgress(phase) {
|
|
19355
|
+
this.sendToRenderer("update-install-progress", { phase });
|
|
19356
|
+
}
|
|
18822
19357
|
/**
|
|
18823
19358
|
* Deliver the still-installing notice once the renderer can hear it.
|
|
18824
19359
|
*
|
|
@@ -18835,6 +19370,39 @@ class UpdateService {
|
|
|
18835
19370
|
}
|
|
18836
19371
|
setTimeout(deliver, 5e3);
|
|
18837
19372
|
}
|
|
19373
|
+
/**
|
|
19374
|
+
* Deliver the "the update went in" notice once the renderer can hear it.
|
|
19375
|
+
*
|
|
19376
|
+
* Same shape and the same reason as the in-flight and failure notices: recovery
|
|
19377
|
+
* runs while the window is still loading, and the component subscribes from a
|
|
19378
|
+
* React effect that can run after `did-finish-load`. Unlike the failure there is
|
|
19379
|
+
* nothing to keep on disk if the push is lost - the marker is cleared either way,
|
|
19380
|
+
* because it is the marker's job to be gone and this is a courtesy - so a missed
|
|
19381
|
+
* notice costs the sentence and nothing else.
|
|
19382
|
+
*/
|
|
19383
|
+
scheduleInstallSucceededDelivery() {
|
|
19384
|
+
if (!this.pendingInstallSucceeded || this.installSucceededDelivered) return;
|
|
19385
|
+
const deliver = () => this.deliverPendingInstallSucceeded();
|
|
19386
|
+
const webContents = this.mainWindow?.webContents;
|
|
19387
|
+
if (webContents && !webContents.isDestroyed()) {
|
|
19388
|
+
webContents.once("did-finish-load", deliver);
|
|
19389
|
+
}
|
|
19390
|
+
setTimeout(deliver, 5e3);
|
|
19391
|
+
}
|
|
19392
|
+
deliverPendingInstallSucceeded() {
|
|
19393
|
+
if (!this.pendingInstallSucceeded || this.installSucceededDelivered) return;
|
|
19394
|
+
if (!this.sendToRenderer(
|
|
19395
|
+
"update-install-succeeded",
|
|
19396
|
+
this.pendingInstallSucceeded
|
|
19397
|
+
)) {
|
|
19398
|
+
return;
|
|
19399
|
+
}
|
|
19400
|
+
this.installSucceededDelivered = true;
|
|
19401
|
+
logger.info(
|
|
19402
|
+
"Reported a completed update install to the renderer",
|
|
19403
|
+
LogFileType.UPDATE_SERVICE
|
|
19404
|
+
);
|
|
19405
|
+
}
|
|
18838
19406
|
deliverPendingInstallInFlight() {
|
|
18839
19407
|
if (!this.pendingInstallInFlight || this.installInFlightDelivered) return;
|
|
18840
19408
|
if (!this.sendToRenderer(
|
|
@@ -18887,8 +19455,12 @@ class UpdateService {
|
|
|
18887
19455
|
marker,
|
|
18888
19456
|
// The service's own method, not the module function, because a harness
|
|
18889
19457
|
// substitutes this one: substituting the seam is how a test counts the
|
|
18890
|
-
// watchdogs a quit leaves running without spawning any.
|
|
18891
|
-
|
|
19458
|
+
// watchdogs a quit leaves running without spawning any. The marker's own
|
|
19459
|
+
// installer pid travels with the call: a watchdog ensured for an install
|
|
19460
|
+
// that is already in flight has to hold on the signal that install is
|
|
19461
|
+
// actually visible by, and for an install this app started itself that is
|
|
19462
|
+
// the pid rather than a job that will never appear (management MAJOR-1).
|
|
19463
|
+
startWatchdog: (targetVersion, installerPid) => this.launchWatchdog(targetVersion, installerPid),
|
|
18892
19464
|
log: (message) => logger.info(message, LogFileType.UPDATE_SERVICE)
|
|
18893
19465
|
});
|
|
18894
19466
|
}
|
|
@@ -18896,16 +19468,18 @@ class UpdateService {
|
|
|
18896
19468
|
* The pending-install marker, if the machine says an install is live now.
|
|
18897
19469
|
*
|
|
18898
19470
|
* The marker is read first, because the ordinary answer to "is an update
|
|
18899
|
-
* installing" is no and a file read answers it; the
|
|
18900
|
-
*
|
|
18901
|
-
*
|
|
19471
|
+
* installing" is no and a file read answers it; the probes behind it are process
|
|
19472
|
+
* spawns and are only worth paying for when there is a marker to ask about - and
|
|
19473
|
+
* only the one that can answer for that marker is paid for.
|
|
18902
19474
|
*/
|
|
18903
19475
|
livePendingInstallMarker() {
|
|
18904
19476
|
const marker = readPendingInstallMarker(this.markerDir());
|
|
18905
19477
|
if (!marker) return null;
|
|
19478
|
+
const liveness = this.installLiveness(marker);
|
|
18906
19479
|
return isInstallInFlight({
|
|
18907
19480
|
marker,
|
|
18908
|
-
jobState:
|
|
19481
|
+
jobState: liveness.jobState,
|
|
19482
|
+
installerRunning: liveness.installerRunning
|
|
18909
19483
|
}) ? marker : null;
|
|
18910
19484
|
}
|
|
18911
19485
|
/**
|
|
@@ -19049,6 +19623,7 @@ class UpdateService {
|
|
|
19049
19623
|
* against.
|
|
19050
19624
|
*/
|
|
19051
19625
|
runningBundlePath() {
|
|
19626
|
+
if (this.runningBundleProbe) return this.runningBundleProbe();
|
|
19052
19627
|
if (process.platform !== "darwin" || !electron.app.isPackaged) return null;
|
|
19053
19628
|
const bundlePath = appBundleFromExecutable(process.execPath);
|
|
19054
19629
|
if (!bundlePath) {
|
|
@@ -19353,6 +19928,7 @@ ${heal.removed.join("\n")}`,
|
|
|
19353
19928
|
version: info?.version ?? null
|
|
19354
19929
|
});
|
|
19355
19930
|
if (verdict.ok) {
|
|
19931
|
+
this.verifiedArtifactPath = artifactPath;
|
|
19356
19932
|
const required = requiredDiskBytes({
|
|
19357
19933
|
artifactSize: verdict.size,
|
|
19358
19934
|
installedBundleSize: installedBundleSize ?? 0
|
|
@@ -19490,6 +20066,290 @@ ${heal.removed.join("\n")}`,
|
|
|
19490
20066
|
if (sealBlock) return sealBlock;
|
|
19491
20067
|
return this.ensureStagedArtifactVerified(info);
|
|
19492
20068
|
}
|
|
20069
|
+
/**
|
|
20070
|
+
* The update bundle this app staged for itself, and everything ShipIt needs to
|
|
20071
|
+
* be pointed at it.
|
|
20072
|
+
*
|
|
20073
|
+
* Assembled in one place rather than passed as six arguments: the spawner, the
|
|
20074
|
+
* state plist and the marker all have to agree about which staging directory
|
|
20075
|
+
* this install got, and a path computed twice is two chances to disagree.
|
|
20076
|
+
*/
|
|
20077
|
+
async stageInstallerHandoff(info) {
|
|
20078
|
+
const refuse = (reason) => {
|
|
20079
|
+
logger.info(
|
|
20080
|
+
`Not staging the update for the installer: ${reason}. The install will be handed to Squirrel's own path instead, which takes minutes but is the same install.`,
|
|
20081
|
+
LogFileType.UPDATE_SERVICE
|
|
20082
|
+
);
|
|
20083
|
+
return { kind: "fallback" };
|
|
20084
|
+
};
|
|
20085
|
+
const block = (detail, message, remedy) => {
|
|
20086
|
+
logger.warn(
|
|
20087
|
+
`Refusing to install the staged update: ${detail}`,
|
|
20088
|
+
LogFileType.UPDATE_SERVICE
|
|
20089
|
+
);
|
|
20090
|
+
return {
|
|
20091
|
+
kind: "blocked",
|
|
20092
|
+
block: {
|
|
20093
|
+
code: "download-verification-failed",
|
|
20094
|
+
message,
|
|
20095
|
+
remedy: { text: remedy },
|
|
20096
|
+
detail
|
|
20097
|
+
}
|
|
20098
|
+
};
|
|
20099
|
+
};
|
|
20100
|
+
if (process.platform !== "darwin") {
|
|
20101
|
+
return refuse("the installer this stages for is macOS's ShipIt");
|
|
20102
|
+
}
|
|
20103
|
+
if (!electron.app.isPackaged) {
|
|
20104
|
+
return refuse("this instance is not the bundle an install replaces");
|
|
20105
|
+
}
|
|
20106
|
+
const bundlePath = this.runningBundlePath();
|
|
20107
|
+
if (!bundlePath) {
|
|
20108
|
+
return refuse("the running executable is not inside an app bundle");
|
|
20109
|
+
}
|
|
20110
|
+
const shipItPath = shipItPathInBundle(bundlePath);
|
|
20111
|
+
if (!shipItPath || !isExecutableFile(shipItPath)) {
|
|
20112
|
+
return refuse(
|
|
20113
|
+
`${shipItPath ?? "no Squirrel.framework installer"} is not an executable inside the running bundle`
|
|
20114
|
+
);
|
|
20115
|
+
}
|
|
20116
|
+
const bundleId = this.bundleIdentifier();
|
|
20117
|
+
if (!bundleId) {
|
|
20118
|
+
return refuse("the running bundle has no readable CFBundleIdentifier");
|
|
20119
|
+
}
|
|
20120
|
+
const version = info?.version ?? null;
|
|
20121
|
+
if (!version) {
|
|
20122
|
+
return refuse("the update carries no version to install");
|
|
20123
|
+
}
|
|
20124
|
+
const zipPath = this.verifiedArtifactPath;
|
|
20125
|
+
if (!zipPath) {
|
|
20126
|
+
return refuse("the pre-flight verified no artifact path to extract from");
|
|
20127
|
+
}
|
|
20128
|
+
const userDataDir = electron.app.getPath("userData");
|
|
20129
|
+
const stagingRoot = updateStagingDir({
|
|
20130
|
+
userDataDir,
|
|
20131
|
+
version,
|
|
20132
|
+
nonce: node_crypto.randomBytes(6).toString("hex")
|
|
20133
|
+
});
|
|
20134
|
+
if (!sameVolume(deviceId(userDataDir), deviceId(bundlePath))) {
|
|
20135
|
+
return refuse(
|
|
20136
|
+
`the staging root ${stagingRoot} is on a different volume from the bundle it replaces (${bundlePath})`
|
|
20137
|
+
);
|
|
20138
|
+
}
|
|
20139
|
+
try {
|
|
20140
|
+
fs.mkdirSync(stagingRoot, { recursive: true });
|
|
20141
|
+
} catch (error) {
|
|
20142
|
+
return refuse(`the staging root could not be created: ${String(error)}`);
|
|
20143
|
+
}
|
|
20144
|
+
const started = Date.now();
|
|
20145
|
+
const extraction = await runExtraction(zipPath, stagingRoot);
|
|
20146
|
+
if (!extraction.ok) {
|
|
20147
|
+
return refuse(
|
|
20148
|
+
`the update bundle could not be extracted (${extraction.reason})`
|
|
20149
|
+
);
|
|
20150
|
+
}
|
|
20151
|
+
logger.info(
|
|
20152
|
+
`Extracted the update bundle into ${stagingRoot} in ${Date.now() - started} ms.`,
|
|
20153
|
+
LogFileType.UPDATE_SERVICE
|
|
20154
|
+
);
|
|
20155
|
+
const stagedAppPath = path$1.join(stagingRoot, path$1.basename(bundlePath));
|
|
20156
|
+
const verdict = evaluateStagedBundle({
|
|
20157
|
+
reading: readStagedBundle(stagedAppPath),
|
|
20158
|
+
expectedBundleIdentifier: bundleId,
|
|
20159
|
+
expectedVersion: version,
|
|
20160
|
+
runningArchitecture: process.arch
|
|
20161
|
+
});
|
|
20162
|
+
if (!verdict.ok) {
|
|
20163
|
+
return block(
|
|
20164
|
+
`The staged update bundle is not this application: ${verdict.detail}.`,
|
|
20165
|
+
`The downloaded update${versionSuffix(version)} is not this application, so it wasn't installed.`,
|
|
20166
|
+
"Check for updates again to re-download the release."
|
|
20167
|
+
);
|
|
20168
|
+
}
|
|
20169
|
+
let stagedSeal;
|
|
20170
|
+
try {
|
|
20171
|
+
({ seal: stagedSeal } = await this.readBundleSeal(stagedAppPath));
|
|
20172
|
+
} catch (error) {
|
|
20173
|
+
return refuse(
|
|
20174
|
+
`the staged bundle's seal could not be probed: ${String(error)}`
|
|
20175
|
+
);
|
|
20176
|
+
}
|
|
20177
|
+
if (stagedSeal.kind === "unsealed") {
|
|
20178
|
+
return block(
|
|
20179
|
+
`The staged update bundle's seal reads \`unsealed\` (${stagedSeal.detail}), so Apple's own validation would have the same answer.`,
|
|
20180
|
+
`The downloaded update${versionSuffix(version)} is damaged, so it wasn't installed.`,
|
|
20181
|
+
"Check for updates again to re-download the release."
|
|
20182
|
+
);
|
|
20183
|
+
}
|
|
20184
|
+
if (stagedSeal.kind !== "sealed") {
|
|
20185
|
+
return refuse(
|
|
20186
|
+
`the staged bundle's seal could not be read (${stagedSeal.detail})`
|
|
20187
|
+
);
|
|
20188
|
+
}
|
|
20189
|
+
const statePath = stagingStatePath(stagingRoot);
|
|
20190
|
+
try {
|
|
20191
|
+
fs.writeFileSync(
|
|
20192
|
+
statePath,
|
|
20193
|
+
JSON.stringify(
|
|
20194
|
+
shipItStatePlist({
|
|
20195
|
+
bundleIdentifier: bundleId,
|
|
20196
|
+
targetBundlePath: bundlePath,
|
|
20197
|
+
updateBundlePath: stagedAppPath
|
|
20198
|
+
})
|
|
20199
|
+
)
|
|
20200
|
+
);
|
|
20201
|
+
} catch (error) {
|
|
20202
|
+
return refuse(
|
|
20203
|
+
`the installer's state plist could not be written: ${String(error)}`
|
|
20204
|
+
);
|
|
20205
|
+
}
|
|
20206
|
+
const label = shipItJobLabel(bundleId);
|
|
20207
|
+
const logPath = installerLogPath(
|
|
20208
|
+
shipItCacheDir(path$1.join(os.homedir(), "Library", "Caches"), bundleId)
|
|
20209
|
+
);
|
|
20210
|
+
try {
|
|
20211
|
+
fs.mkdirSync(path$1.dirname(logPath), { recursive: true });
|
|
20212
|
+
fs.closeSync(fs.openSync(logPath, "a"));
|
|
20213
|
+
} catch (error) {
|
|
20214
|
+
return refuse(
|
|
20215
|
+
`the installer's log ${logPath} could not be opened for appending: ${String(error)}`
|
|
20216
|
+
);
|
|
20217
|
+
}
|
|
20218
|
+
return {
|
|
20219
|
+
kind: "staged",
|
|
20220
|
+
handoff: {
|
|
20221
|
+
stagingRoot,
|
|
20222
|
+
stagedAppPath,
|
|
20223
|
+
shipItPath,
|
|
20224
|
+
statePath,
|
|
20225
|
+
label,
|
|
20226
|
+
// The SAME log launchd's redirection writes for a Squirrel-run
|
|
20227
|
+
// install: the failure panel's detail line and
|
|
20228
|
+
// scripts/update-window-report.mjs both read that path (see
|
|
20229
|
+
// installerLogPath).
|
|
20230
|
+
logPath
|
|
20231
|
+
}
|
|
20232
|
+
};
|
|
20233
|
+
}
|
|
20234
|
+
/**
|
|
20235
|
+
* Start the detached spawner that waits for this process to go, then installs.
|
|
20236
|
+
*
|
|
20237
|
+
* Detached with its stdio ignored, like the relaunch watchdog and for the same
|
|
20238
|
+
* reason: it must outlive this process, and a pipe this process holds is a pipe
|
|
20239
|
+
* that can keep it alive. Both of the installer's streams are appended to
|
|
20240
|
+
* Squirrel's own log inside the script rather than inherited from here, so no
|
|
20241
|
+
* descriptor of ours is passed at all.
|
|
20242
|
+
*
|
|
20243
|
+
* Returns the pid, which is the app's own answer to "is an install in flight"
|
|
20244
|
+
* from this moment on (`installerPid` in the marker, `install_live` in the
|
|
20245
|
+
* watchdog's script, `installLivenessNow` in recovery): the process is `exec`d
|
|
20246
|
+
* into ShipIt, so this one pid covers the wait and the whole install.
|
|
20247
|
+
*/
|
|
20248
|
+
startInstaller(handoff) {
|
|
20249
|
+
const plan = buildInstallerSpawn({
|
|
20250
|
+
appPid: process.pid,
|
|
20251
|
+
shipItPath: handoff.shipItPath,
|
|
20252
|
+
label: handoff.label,
|
|
20253
|
+
statePath: handoff.statePath,
|
|
20254
|
+
logPath: handoff.logPath
|
|
20255
|
+
});
|
|
20256
|
+
try {
|
|
20257
|
+
const child = node_child_process.spawn("sh", plan.args, {
|
|
20258
|
+
detached: true,
|
|
20259
|
+
stdio: "ignore",
|
|
20260
|
+
env: { ...process.env, ...plan.env }
|
|
20261
|
+
});
|
|
20262
|
+
child.unref();
|
|
20263
|
+
if (child.pid == null) {
|
|
20264
|
+
logger.error(
|
|
20265
|
+
"The installer was spawned but reported no pid; falling back to Squirrel's own install path.",
|
|
20266
|
+
LogFileType.UPDATE_SERVICE
|
|
20267
|
+
);
|
|
20268
|
+
return null;
|
|
20269
|
+
}
|
|
20270
|
+
logger.info(
|
|
20271
|
+
`Started the installer (pid ${child.pid}) for version ${handoff.label}: it waits for this process (pid ${process.pid}) to go and then becomes ${handoff.shipItPath}, which installs ${handoff.stagedAppPath} and starts the app again.`,
|
|
20272
|
+
LogFileType.UPDATE_SERVICE
|
|
20273
|
+
);
|
|
20274
|
+
return child.pid;
|
|
20275
|
+
} catch (error) {
|
|
20276
|
+
logger.error(
|
|
20277
|
+
`Could not start the installer for ${handoff.stagedAppPath}: ${String(error)}`,
|
|
20278
|
+
LogFileType.UPDATE_SERVICE
|
|
20279
|
+
);
|
|
20280
|
+
return null;
|
|
20281
|
+
}
|
|
20282
|
+
}
|
|
20283
|
+
/**
|
|
20284
|
+
* Stop a spawner that has been started but whose marker could not be written.
|
|
20285
|
+
*
|
|
20286
|
+
* The one hole in "spawn, then record": a spawner with no marker would wait
|
|
20287
|
+
* for a quit that this process is not going to make, and a wait nothing can
|
|
20288
|
+
* tell apart from an install in progress is worse than no installer at all. So
|
|
20289
|
+
* the marker write is the only step whose failure undoes the spawn, and the
|
|
20290
|
+
* error the caller sees is the same one it saw before this change.
|
|
20291
|
+
*/
|
|
20292
|
+
abortInstaller(pid) {
|
|
20293
|
+
try {
|
|
20294
|
+
process.kill(pid, "SIGTERM");
|
|
20295
|
+
logger.warn(
|
|
20296
|
+
`Stopped the installer (pid ${pid}) because this app could not record the install it was started for.`,
|
|
20297
|
+
LogFileType.UPDATE_SERVICE
|
|
20298
|
+
);
|
|
20299
|
+
} catch (error) {
|
|
20300
|
+
logger.warn(
|
|
20301
|
+
`Could not stop the installer (pid ${pid}) started for an install that was not recorded: ${String(error)}`,
|
|
20302
|
+
LogFileType.UPDATE_SERVICE
|
|
20303
|
+
);
|
|
20304
|
+
}
|
|
20305
|
+
}
|
|
20306
|
+
/**
|
|
20307
|
+
* Remove the staging trees left by installs that are over.
|
|
20308
|
+
*
|
|
20309
|
+
* The app owns this tree now where Squirrel used to own `update.<random>` under
|
|
20310
|
+
* its own cache: it is one install's worth of app either way, but a Squirrel
|
|
20311
|
+
* install's leftovers sit in Apple's cache directory and this one's sit in the
|
|
20312
|
+
* app's own, so nothing else was ever going to clean them up. Called only from
|
|
20313
|
+
* the branches that have decided an install is NOT in flight - reaping a tree a
|
|
20314
|
+
* live installer is reading is the one thing this must never do (the design
|
|
20315
|
+
* note's falsification list).
|
|
20316
|
+
*/
|
|
20317
|
+
reapStagedUpdateTrees() {
|
|
20318
|
+
const root = updateStagingRoot(electron.app.getPath("userData"));
|
|
20319
|
+
if (!fs.existsSync(root)) return;
|
|
20320
|
+
for (const entry of fs.readdirSync(root)) {
|
|
20321
|
+
const tree = path$1.join(root, entry);
|
|
20322
|
+
try {
|
|
20323
|
+
reapStagedTree(tree);
|
|
20324
|
+
logger.info(
|
|
20325
|
+
`Reaped the staged update tree ${tree} left by an install that is not running.`,
|
|
20326
|
+
LogFileType.UPDATE_SERVICE
|
|
20327
|
+
);
|
|
20328
|
+
} catch (error) {
|
|
20329
|
+
logger.warn(
|
|
20330
|
+
`Could not reap the staged update tree ${tree}: ${String(error)}`,
|
|
20331
|
+
LogFileType.UPDATE_SERVICE
|
|
20332
|
+
);
|
|
20333
|
+
}
|
|
20334
|
+
}
|
|
20335
|
+
}
|
|
20336
|
+
/**
|
|
20337
|
+
* Both of the machine's liveness answers for this marker.
|
|
20338
|
+
*
|
|
20339
|
+
* The one place the machine is asked, so recovery's reading, the live re-check,
|
|
20340
|
+
* the quit's own guard and the launch hold cannot disagree about the same install
|
|
20341
|
+
* (management MAJOR-1). Which of the two decides (`installLivenessNow`) turns on
|
|
20342
|
+
* the marker's own `installerPid`: an install this app started has no launchd
|
|
20343
|
+
* job, and an install Squirrel submitted has no pid. Both probes are process
|
|
20344
|
+
* spawns, so only the one that can answer is asked.
|
|
20345
|
+
*/
|
|
20346
|
+
installLiveness(marker) {
|
|
20347
|
+
return installLivenessFor({
|
|
20348
|
+
marker,
|
|
20349
|
+
bundlePath: () => this.runningBundlePath(),
|
|
20350
|
+
jobState: () => this.shipItInstallJobState()
|
|
20351
|
+
});
|
|
20352
|
+
}
|
|
19493
20353
|
/**
|
|
19494
20354
|
* Start the detached relaunch watchdog.
|
|
19495
20355
|
*
|
|
@@ -19497,10 +20357,11 @@ ${heal.removed.join("\n")}`,
|
|
|
19497
20357
|
* uses; the plan, the bounds and the log line live there. Kept as a method
|
|
19498
20358
|
* because a harness substitutes it to count watchdogs without spawning one.
|
|
19499
20359
|
*/
|
|
19500
|
-
launchWatchdog(targetVersion) {
|
|
20360
|
+
launchWatchdog(targetVersion, installerPid = null) {
|
|
19501
20361
|
return startRelaunchWatchdog({
|
|
19502
20362
|
targetVersion,
|
|
19503
20363
|
runningVersion: electron.app.getVersion(),
|
|
20364
|
+
installerPid,
|
|
19504
20365
|
log: (message) => logger.info(message, LogFileType.UPDATE_SERVICE)
|
|
19505
20366
|
});
|
|
19506
20367
|
}
|
|
@@ -19917,8 +20778,16 @@ ${heal.removed.join("\n")}`,
|
|
|
19917
20778
|
this.installPreflightInFlight = true;
|
|
19918
20779
|
this.updateStage = "installing";
|
|
19919
20780
|
let block = null;
|
|
20781
|
+
let handoff = null;
|
|
20782
|
+
this.reportInstallProgress("verifying");
|
|
19920
20783
|
try {
|
|
19921
20784
|
block = await this.runInstallPreflight(this.lastUpdateInfo);
|
|
20785
|
+
if (!block) {
|
|
20786
|
+
this.reportInstallProgress("staging");
|
|
20787
|
+
const staged = await this.stageInstallerHandoff(this.lastUpdateInfo);
|
|
20788
|
+
if (staged.kind === "staged") handoff = staged.handoff;
|
|
20789
|
+
if (staged.kind === "blocked") block = staged.block;
|
|
20790
|
+
}
|
|
19922
20791
|
if (!block) await this.backendService?.stop(false);
|
|
19923
20792
|
} catch (error) {
|
|
19924
20793
|
this.updateStage = "idle";
|
|
@@ -19937,18 +20806,32 @@ ${heal.removed.join("\n")}`,
|
|
|
19937
20806
|
return false;
|
|
19938
20807
|
}
|
|
19939
20808
|
const targetVersion = this.lastUpdateInfo?.version ?? electron.app.getVersion();
|
|
19940
|
-
|
|
19941
|
-
const
|
|
19942
|
-
|
|
19943
|
-
|
|
19944
|
-
|
|
19945
|
-
|
|
19946
|
-
|
|
20809
|
+
if (handoff) this.reportInstallProgress("starting");
|
|
20810
|
+
const installerPid = handoff ? this.startInstaller(handoff) : null;
|
|
20811
|
+
const watchdogPid = this.launchWatchdog(targetVersion, installerPid);
|
|
20812
|
+
let marker;
|
|
20813
|
+
try {
|
|
20814
|
+
marker = writePendingInstallMarker(this.markerDir(), {
|
|
20815
|
+
targetVersion,
|
|
20816
|
+
artifactPath: this.downloadedArtifactPath ?? this.downloadHelper()?.file ?? "unknown",
|
|
20817
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
20818
|
+
watchdogPid,
|
|
20819
|
+
installerPid
|
|
20820
|
+
});
|
|
20821
|
+
} catch (error) {
|
|
20822
|
+
if (installerPid != null) this.abortInstaller(installerPid);
|
|
20823
|
+
this.updateStage = "idle";
|
|
20824
|
+
throw error;
|
|
20825
|
+
}
|
|
19947
20826
|
logger.info(
|
|
19948
|
-
`Pending install marker written for version ${marker.targetVersion} (watchdog pid ${watchdogPid ?? "none"}).`,
|
|
20827
|
+
`Pending install marker written for version ${marker.targetVersion} (watchdog pid ${watchdogPid ?? "none"}, ${installerPid != null ? `installer pid ${installerPid}` : "Squirrel's own installer"}).`,
|
|
19949
20828
|
LogFileType.UPDATE_SERVICE
|
|
19950
20829
|
);
|
|
19951
20830
|
this.backendService?.setAutoUpdating(true);
|
|
20831
|
+
if (installerPid != null) {
|
|
20832
|
+
electron.app.quit();
|
|
20833
|
+
return true;
|
|
20834
|
+
}
|
|
19952
20835
|
electronUpdater.autoUpdater.quitAndInstall(false, true);
|
|
19953
20836
|
return true;
|
|
19954
20837
|
});
|