livedesk 0.1.607 → 0.1.608
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/electron/installer-quit-gate.mjs +183 -0
- package/electron/main.mjs +115 -42
- package/electron/update-manager.mjs +762 -144
- package/package.json +1 -1
- package/web/dist/app.webmanifest +1 -1
- package/web/dist/assets/LiveDeskApp-Dr77Bkmu.js +179 -0
- package/web/dist/assets/{icons-JxbtiHLQ.js → icons-tFaWqIIe.js} +1 -1
- package/web/dist/assets/{index-C4QRbhif.js → index-Dzx6WcDO.js} +4 -4
- package/web/dist/assets/{react-hDor0Pi9.js → react-BQos8uGS.js} +1 -1
- package/web/dist/assets/supabase-C7qjtLN3.js +29 -0
- package/web/dist/index.html +3 -3
- package/web/dist/livedesk-build-evidence.json +25 -25
- package/web/dist/sw.js +1 -1
- package/web/dist/assets/LiveDeskApp-Dw86X0VP.js +0 -179
- package/web/dist/assets/supabase-C0Z0FS3K.js +0 -29
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
export const DEFAULT_INSTALLER_QUIT_CONFIRMATION_MS = 1_000;
|
|
2
|
+
|
|
3
|
+
export const InstallerQuitDecision = Object.freeze({
|
|
4
|
+
NORMAL: 'normal-quit',
|
|
5
|
+
HOLD: 'hold-update-quit',
|
|
6
|
+
BLOCK: 'block-stale-update-quit',
|
|
7
|
+
ALLOW: 'allow-update-quit'
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
function normalizeDelay(value) {
|
|
11
|
+
const milliseconds = Number(value);
|
|
12
|
+
return Number.isFinite(milliseconds) && milliseconds >= 0
|
|
13
|
+
? Math.round(milliseconds)
|
|
14
|
+
: DEFAULT_INSTALLER_QUIT_CONFIRMATION_MS;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* electron-updater reports that it intends to quit before the Windows NSIS
|
|
19
|
+
* spawn promise has necessarily rejected. This gate holds the first quit for
|
|
20
|
+
* a short error window without importing Electron, so its race is executable
|
|
21
|
+
* in a plain Node regression test.
|
|
22
|
+
*/
|
|
23
|
+
export function createInstallerQuitGate({
|
|
24
|
+
requestQuit,
|
|
25
|
+
confirmationDelayMs = DEFAULT_INSTALLER_QUIT_CONFIRMATION_MS,
|
|
26
|
+
setTimer = setTimeout,
|
|
27
|
+
clearTimer = clearTimeout
|
|
28
|
+
} = {}) {
|
|
29
|
+
if (typeof requestQuit !== 'function') {
|
|
30
|
+
throw new TypeError('installer-quit-gate-request-quit-required');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const confirmationDelay = normalizeDelay(confirmationDelayMs);
|
|
34
|
+
let generation = 0;
|
|
35
|
+
let attempt = null;
|
|
36
|
+
|
|
37
|
+
const clearConfirmationTimer = ownedAttempt => {
|
|
38
|
+
if (!ownedAttempt || ownedAttempt.confirmationTimer === null) return;
|
|
39
|
+
clearTimer(ownedAttempt.confirmationTimer);
|
|
40
|
+
ownedAttempt.confirmationTimer = null;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const snapshot = () => {
|
|
44
|
+
if (!attempt) {
|
|
45
|
+
return {
|
|
46
|
+
generation,
|
|
47
|
+
state: 'idle',
|
|
48
|
+
intentObserved: false,
|
|
49
|
+
failed: false,
|
|
50
|
+
firstQuitHeld: false,
|
|
51
|
+
quitReissueRequested: false,
|
|
52
|
+
staleQuitBlocked: false,
|
|
53
|
+
error: ''
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
generation: attempt.generation,
|
|
58
|
+
state: attempt.state,
|
|
59
|
+
intentObserved: attempt.intentObserved,
|
|
60
|
+
failed: attempt.failed,
|
|
61
|
+
firstQuitHeld: attempt.firstQuitHeld,
|
|
62
|
+
quitReissueRequested: attempt.quitReissueRequested,
|
|
63
|
+
staleQuitBlocked: attempt.staleQuitBlocked,
|
|
64
|
+
error: attempt.error
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const beginInstall = () => {
|
|
69
|
+
clearConfirmationTimer(attempt);
|
|
70
|
+
attempt = {
|
|
71
|
+
generation: ++generation,
|
|
72
|
+
state: 'preparing',
|
|
73
|
+
intentObserved: false,
|
|
74
|
+
failed: false,
|
|
75
|
+
firstQuitHeld: false,
|
|
76
|
+
quitReissueRequested: false,
|
|
77
|
+
staleQuitBlocked: false,
|
|
78
|
+
error: '',
|
|
79
|
+
confirmationTimer: null
|
|
80
|
+
};
|
|
81
|
+
return attempt.generation;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const noteBeforeQuitForUpdate = () => {
|
|
85
|
+
if (!attempt) return false;
|
|
86
|
+
attempt.intentObserved = true;
|
|
87
|
+
if (attempt.failed) {
|
|
88
|
+
attempt.state = 'failed-update-quit-intent';
|
|
89
|
+
} else if (!attempt.firstQuitHeld) {
|
|
90
|
+
attempt.state = 'update-quit-intent';
|
|
91
|
+
}
|
|
92
|
+
return true;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const failInstall = error => {
|
|
96
|
+
if (!attempt) return false;
|
|
97
|
+
attempt.failed = true;
|
|
98
|
+
attempt.error = String(error?.message || error || 'desktop-update-installer-launch-failed');
|
|
99
|
+
clearConfirmationTimer(attempt);
|
|
100
|
+
|
|
101
|
+
if (attempt.firstQuitHeld && !attempt.quitReissueRequested) {
|
|
102
|
+
// The updater-owned quit was already intercepted. There is no later quit
|
|
103
|
+
// to consume, so a user's subsequent normal quit must remain available.
|
|
104
|
+
attempt.intentObserved = false;
|
|
105
|
+
attempt.state = 'failed-after-held-quit';
|
|
106
|
+
} else if (attempt.intentObserved) {
|
|
107
|
+
// The updater has announced a quit, or our delayed reissue is in flight.
|
|
108
|
+
// Consume that one stale quit before returning to normal quit handling.
|
|
109
|
+
attempt.state = 'failed-update-quit-pending';
|
|
110
|
+
} else {
|
|
111
|
+
// electron-updater can dispatch its spawn error before its queued
|
|
112
|
+
// before-quit-for-update event. Retain the failed attempt for that event.
|
|
113
|
+
attempt.state = 'failed-awaiting-update-quit-intent';
|
|
114
|
+
}
|
|
115
|
+
return true;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const scheduleConfirmedQuit = ownedAttempt => {
|
|
119
|
+
if (ownedAttempt.confirmationTimer !== null) return;
|
|
120
|
+
const fire = () => {
|
|
121
|
+
ownedAttempt.confirmationTimer = null;
|
|
122
|
+
if (
|
|
123
|
+
attempt !== ownedAttempt
|
|
124
|
+
|| ownedAttempt.failed
|
|
125
|
+
|| !ownedAttempt.intentObserved
|
|
126
|
+
|| ownedAttempt.quitReissueRequested
|
|
127
|
+
) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
ownedAttempt.quitReissueRequested = true;
|
|
131
|
+
ownedAttempt.state = 'reissuing-update-quit';
|
|
132
|
+
try {
|
|
133
|
+
requestQuit();
|
|
134
|
+
} catch (error) {
|
|
135
|
+
ownedAttempt.failed = true;
|
|
136
|
+
ownedAttempt.intentObserved = false;
|
|
137
|
+
ownedAttempt.error = String(error?.message || error);
|
|
138
|
+
ownedAttempt.state = 'quit-reissue-failed';
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
ownedAttempt.confirmationTimer = setTimer(fire, confirmationDelay);
|
|
142
|
+
ownedAttempt.confirmationTimer?.unref?.();
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const handleBeforeQuit = () => {
|
|
146
|
+
const ownedAttempt = attempt;
|
|
147
|
+
if (!ownedAttempt?.intentObserved) return InstallerQuitDecision.NORMAL;
|
|
148
|
+
|
|
149
|
+
if (ownedAttempt.failed) {
|
|
150
|
+
ownedAttempt.intentObserved = false;
|
|
151
|
+
ownedAttempt.staleQuitBlocked = true;
|
|
152
|
+
ownedAttempt.state = 'failed-update-quit-blocked';
|
|
153
|
+
return InstallerQuitDecision.BLOCK;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (ownedAttempt.quitReissueRequested) {
|
|
157
|
+
ownedAttempt.intentObserved = false;
|
|
158
|
+
ownedAttempt.state = 'update-quit-allowed';
|
|
159
|
+
return InstallerQuitDecision.ALLOW;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (!ownedAttempt.firstQuitHeld) {
|
|
163
|
+
ownedAttempt.firstQuitHeld = true;
|
|
164
|
+
ownedAttempt.state = 'holding-update-quit';
|
|
165
|
+
scheduleConfirmedQuit(ownedAttempt);
|
|
166
|
+
}
|
|
167
|
+
return InstallerQuitDecision.HOLD;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const dispose = () => {
|
|
171
|
+
clearConfirmationTimer(attempt);
|
|
172
|
+
attempt = null;
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
beginInstall,
|
|
177
|
+
noteBeforeQuitForUpdate,
|
|
178
|
+
failInstall,
|
|
179
|
+
handleBeforeQuit,
|
|
180
|
+
getState: snapshot,
|
|
181
|
+
dispose
|
|
182
|
+
};
|
|
183
|
+
}
|
package/electron/main.mjs
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { app, BrowserWindow, clipboard, dialog, ipcMain, Menu, nativeImage, powerMonitor, safeStorage, shell, Tray } from 'electron';
|
|
1
|
+
import { app, autoUpdater as electronAutoUpdater, BrowserWindow, clipboard, dialog, ipcMain, Menu, nativeImage, powerMonitor, safeStorage, shell, Tray } from 'electron';
|
|
2
2
|
import { existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { dirname, join, resolve } from 'node:path';
|
|
5
5
|
import { spawn } from 'node:child_process';
|
|
6
6
|
import { createRequire } from 'node:module';
|
|
7
|
-
import { createProductUpdateManager } from './update-manager.mjs';
|
|
8
|
-
import { createRecurringProductUpdateOwner } from './recurring-update-owner.mjs';
|
|
7
|
+
import { createProductUpdateManager } from './update-manager.mjs';
|
|
8
|
+
import { createRecurringProductUpdateOwner } from './recurring-update-owner.mjs';
|
|
9
|
+
import { createInstallerQuitGate, InstallerQuitDecision } from './installer-quit-gate.mjs';
|
|
9
10
|
import { AUTH_REQUEST_TIMEOUT_MS, createPkceAuthorizationUrl, exchangePkceCode, fetchSupabaseUser, parsePkceCallbackUrl, refreshSupabaseSession } from './oauth-pkce.mjs';
|
|
10
11
|
import { getRuntimeAuthEnvironment, resolveDesktopAuthConfig, resolveDesktopBuildFlavor } from './auth-config.mjs';
|
|
11
12
|
import { createBoundedDesktopLogger } from './bounded-desktop-log.mjs';
|
|
@@ -155,10 +156,10 @@ let runtimeRestartWindowStartedAt = 0;
|
|
|
155
156
|
let runtimeRestartTimer = null;
|
|
156
157
|
let runtimeRestartPromise = null;
|
|
157
158
|
let runtimeLaunchStartedAt = 0;
|
|
158
|
-
let runtimeRoleTakeoverPending = process.env.LIVEDESK_ROLE_TAKEOVER === '1';
|
|
159
|
-
let quitSequenceStarted = false;
|
|
160
|
-
let updateInstallSequenceStarted = false;
|
|
161
|
-
let productUpdates = null;
|
|
159
|
+
let runtimeRoleTakeoverPending = process.env.LIVEDESK_ROLE_TAKEOVER === '1';
|
|
160
|
+
let quitSequenceStarted = false;
|
|
161
|
+
let updateInstallSequenceStarted = false;
|
|
162
|
+
let productUpdates = null;
|
|
162
163
|
let recurringProductUpdates = null;
|
|
163
164
|
let pendingOAuth = null;
|
|
164
165
|
let pendingProtocolUrl = '';
|
|
@@ -169,7 +170,10 @@ let lastDesktopResumeRecoveryAt = 0;
|
|
|
169
170
|
let desktopSessionEpoch = 0;
|
|
170
171
|
let lastKnownDesktopSession = null;
|
|
171
172
|
let desktopClipboardOwner = null;
|
|
172
|
-
const desktopAuthInvalidationGuard = createDesktopAuthInvalidationGuard();
|
|
173
|
+
const desktopAuthInvalidationGuard = createDesktopAuthInvalidationGuard();
|
|
174
|
+
const installerQuitGate = createInstallerQuitGate({
|
|
175
|
+
requestQuit: () => app.quit()
|
|
176
|
+
});
|
|
173
177
|
const FALLBACK_ICON_DATA_URL = `data:image/svg+xml;charset=utf-8,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" fill="none"><rect x="1" y="1" width="30" height="30" rx="7" fill="#111827"/><g transform="translate(5 5) scale(.785714)"><rect x="2.5" y="4" width="14" height="9" rx="1.5" stroke="#F8FAFC" stroke-width="1.7" opacity=".42"/><path d="M7 16v1.5h5" stroke="#F8FAFC" stroke-width="1.7" stroke-linecap="round" opacity=".42"/><rect x="10.5" y="2.5" width="14" height="9" rx="1.5" stroke="#F8FAFC" stroke-width="1.7" opacity=".62"/><path d="M15 14.5V16h5" stroke="#F8FAFC" stroke-width="1.7" stroke-linecap="round" opacity=".62"/><rect x="6.5" y="8.5" width="15" height="10" rx="1.7" fill="#F8FAFC" fill-opacity=".12" stroke="#F8FAFC" stroke-width="1.9"/><path d="M12 21v1.5H9.5M16 21v1.5h2.5M9.5 22.5h9" stroke="#F8FAFC" stroke-width="1.9" stroke-linecap="round"/></g></svg>')}`;
|
|
174
178
|
const desktopLogger = createBoundedDesktopLogger({ logPath });
|
|
175
179
|
|
|
@@ -615,12 +619,18 @@ async function stopRuntime() {
|
|
|
615
619
|
child.livedeskRuntimeStopDeadline = deadline;
|
|
616
620
|
const operation = (async () => {
|
|
617
621
|
await requestRuntimeShutdown(deadline);
|
|
618
|
-
const gracefulDeadline = Math.max(
|
|
619
|
-
Date.now(),
|
|
620
|
-
deadline - DEFAULT_RUNTIME_FORCED_CLEANUP_RESERVE_MS
|
|
621
|
-
);
|
|
622
|
-
|
|
623
|
-
|
|
622
|
+
const gracefulDeadline = Math.max(
|
|
623
|
+
Date.now(),
|
|
624
|
+
deadline - DEFAULT_RUNTIME_FORCED_CLEANUP_RESERVE_MS
|
|
625
|
+
);
|
|
626
|
+
try {
|
|
627
|
+
await waitForOwnedRuntimeTreeExit(child, { deadline: gracefulDeadline });
|
|
628
|
+
} catch (error) {
|
|
629
|
+
// Preserve the forced-cleanup reserve. A stale ChildProcess terminal
|
|
630
|
+
// notification must not prevent the exact registered tree drain below.
|
|
631
|
+
log(`runtime graceful exit wait expired; forcing exact drain: ${error?.message || error}`);
|
|
632
|
+
}
|
|
633
|
+
await drainRuntimeTreeUntilStopped(child, deadline);
|
|
624
634
|
if (runtimeChild === child) runtimeChild = null;
|
|
625
635
|
return true;
|
|
626
636
|
})();
|
|
@@ -669,32 +679,56 @@ function requestRuntimeRestart(source = 'manual') {
|
|
|
669
679
|
async function prepareRuntimeForUpdateInstall() {
|
|
670
680
|
if (updateInstallSequenceStarted) {
|
|
671
681
|
throw new Error('desktop-update-install-already-started');
|
|
672
|
-
}
|
|
673
|
-
updateInstallSequenceStarted = true;
|
|
682
|
+
}
|
|
683
|
+
updateInstallSequenceStarted = true;
|
|
684
|
+
installerQuitGate.beginInstall();
|
|
674
685
|
isQuitting = true;
|
|
675
686
|
recurringProductUpdates?.stop();
|
|
676
687
|
try {
|
|
677
688
|
await quiesceDesktopAuthOwner('desktop-update-install');
|
|
678
689
|
await stopRuntime();
|
|
679
690
|
} catch (error) {
|
|
691
|
+
installerQuitGate.failInstall(error);
|
|
680
692
|
updateInstallSequenceStarted = false;
|
|
681
693
|
isQuitting = false;
|
|
682
694
|
scheduleDesktopAuthRefresh(readEncryptedSession());
|
|
695
|
+
let recoveryError = null;
|
|
696
|
+
try {
|
|
697
|
+
const ready = await restartRuntimeProcess();
|
|
698
|
+
if (!ready) throw new Error('desktop-update-runtime-recovery-not-ready');
|
|
699
|
+
} catch (cause) {
|
|
700
|
+
recoveryError = cause;
|
|
701
|
+
log(`desktop update preparation recovery failed: ${cause?.message || cause}`);
|
|
702
|
+
}
|
|
703
|
+
if (!recoveryError) {
|
|
704
|
+
recurringProductUpdates?.start();
|
|
705
|
+
}
|
|
706
|
+
if (recoveryError) {
|
|
707
|
+
throw new Error(
|
|
708
|
+
`desktop-update-preinstall-recovery-failed:${recoveryError?.message || recoveryError}`,
|
|
709
|
+
{ cause: error }
|
|
710
|
+
);
|
|
711
|
+
}
|
|
683
712
|
throw error;
|
|
684
713
|
}
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
async function recoverRuntimeAfterUpdateInstallError(error) {
|
|
688
|
-
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
async function recoverRuntimeAfterUpdateInstallError(error) {
|
|
717
|
+
// electron-updater can report a Windows installer spawn failure just before
|
|
718
|
+
// its queued app.quit(). Seal that exact update quit before restarting.
|
|
719
|
+
installerQuitGate.failInstall(error);
|
|
720
|
+
log(`desktop update install launch failed after runtime drain: ${error?.message || error}`);
|
|
689
721
|
updateInstallSequenceStarted = false;
|
|
690
722
|
isQuitting = false;
|
|
691
723
|
scheduleDesktopAuthRefresh(readEncryptedSession());
|
|
692
|
-
if (!runtimeChild)
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
}
|
|
697
|
-
|
|
724
|
+
if (!runtimeChild) startRuntime();
|
|
725
|
+
const ready = await waitForRuntime();
|
|
726
|
+
if (!ready) {
|
|
727
|
+
throw new Error('desktop-update-runtime-recovery-not-ready', { cause: error });
|
|
728
|
+
}
|
|
729
|
+
mainWindow?.loadURL(LOCAL_RUNTIME_URL);
|
|
730
|
+
recurringProductUpdates?.start();
|
|
731
|
+
}
|
|
698
732
|
|
|
699
733
|
async function syncSessionToRuntime(session) {
|
|
700
734
|
const normalized = normalizeSession(session);
|
|
@@ -1221,12 +1255,18 @@ function createTray() {
|
|
|
1221
1255
|
updateTray('Starting');
|
|
1222
1256
|
}
|
|
1223
1257
|
|
|
1224
|
-
app.on('open-url', (event, url) => {
|
|
1258
|
+
app.on('open-url', (event, url) => {
|
|
1225
1259
|
event.preventDefault();
|
|
1226
1260
|
if (!isLiveDeskAuthCallbackUrl(url)) return;
|
|
1227
1261
|
if (app.isReady()) void handleProtocolUrl(url);
|
|
1228
1262
|
else pendingProtocolUrl = url;
|
|
1229
|
-
});
|
|
1263
|
+
});
|
|
1264
|
+
|
|
1265
|
+
electronAutoUpdater.on('before-quit-for-update', () => {
|
|
1266
|
+
// On Windows this is updater intent, not proof that the asynchronous NSIS
|
|
1267
|
+
// spawn succeeded. The first updater-owned quit is confirmed below.
|
|
1268
|
+
installerQuitGate.noteBeforeQuitForUpdate();
|
|
1269
|
+
});
|
|
1230
1270
|
|
|
1231
1271
|
if (!app.requestSingleInstanceLock()) {
|
|
1232
1272
|
app.quit();
|
|
@@ -1237,19 +1277,43 @@ if (!app.requestSingleInstanceLock()) {
|
|
|
1237
1277
|
showWindow();
|
|
1238
1278
|
});
|
|
1239
1279
|
app.whenReady().then(async () => {
|
|
1240
|
-
app.setAsDefaultProtocolClient('livedesk');
|
|
1241
|
-
removeLegacyStartupEntries();
|
|
1242
|
-
migrateLegacyPlaintextSession();
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1280
|
+
app.setAsDefaultProtocolClient('livedesk');
|
|
1281
|
+
removeLegacyStartupEntries();
|
|
1282
|
+
migrateLegacyPlaintextSession();
|
|
1283
|
+
let lastProductUpdateLogSignature = '';
|
|
1284
|
+
productUpdates = createProductUpdateManager({
|
|
1285
|
+
app,
|
|
1286
|
+
onStatus: status => {
|
|
1246
1287
|
if (mainWindow && !mainWindow.isDestroyed() && !mainWindow.webContents.isDestroyed()) {
|
|
1247
1288
|
mainWindow.webContents.send('updates:status', status);
|
|
1248
1289
|
}
|
|
1249
|
-
if (status.state === 'available') updateTray(`Update ${status.availableVersion} available`);
|
|
1250
|
-
else if (status.state === 'downloading') updateTray(`Downloading update ${Math.round(status.progress || 0)}%`);
|
|
1251
|
-
else if (status.state === 'downloaded') updateTray(`Update ${status.availableVersion} ready`);
|
|
1252
|
-
|
|
1290
|
+
if (status.state === 'available') updateTray(`Update ${status.availableVersion} available`);
|
|
1291
|
+
else if (status.state === 'downloading') updateTray(`Downloading update ${Math.round(status.progress || 0)}%`);
|
|
1292
|
+
else if (status.state === 'downloaded') updateTray(`Update ${status.availableVersion} ready`);
|
|
1293
|
+
const progressBucket = Math.floor(Math.max(0, Number(status.progress || 0)) / 10) * 10;
|
|
1294
|
+
const signature = [
|
|
1295
|
+
status.state,
|
|
1296
|
+
status.channel,
|
|
1297
|
+
status.currentVersion,
|
|
1298
|
+
status.availableVersion,
|
|
1299
|
+
status.downloadedVersion,
|
|
1300
|
+
progressBucket,
|
|
1301
|
+
status.cleanupPending,
|
|
1302
|
+
status.reopenRequired,
|
|
1303
|
+
status.errorCode,
|
|
1304
|
+
status.error,
|
|
1305
|
+
status.lastCheckStartedAt,
|
|
1306
|
+
status.lastCheckCompletedAt,
|
|
1307
|
+
status.startupCheck
|
|
1308
|
+
].join('|');
|
|
1309
|
+
if (signature !== lastProductUpdateLogSignature) {
|
|
1310
|
+
lastProductUpdateLogSignature = signature;
|
|
1311
|
+
log(
|
|
1312
|
+
`desktop update state=${status.state || 'unknown'} channel=${status.channel || 'unknown'} current=${status.currentVersion || 'unknown'} available=${status.availableVersion || 'none'} downloaded=${status.downloadedVersion || 'none'} progress=${progressBucket} cleanupPending=${Boolean(status.cleanupPending)} reopenRequired=${Boolean(status.reopenRequired)} feed=${status.feedUrl || 'none'} checkStarted=${status.lastCheckStartedAt || 'none'} checkCompleted=${status.lastCheckCompletedAt || 'none'} errorCode=${status.errorCode || 'none'} error=${status.error || 'none'}`,
|
|
1313
|
+
{ priority: status.state === 'error' || status.state === 'installing' ? 'normal' : 'low' }
|
|
1314
|
+
);
|
|
1315
|
+
}
|
|
1316
|
+
},
|
|
1253
1317
|
beforeInstall: prepareRuntimeForUpdateInstall,
|
|
1254
1318
|
onInstallError: recoverRuntimeAfterUpdateInstallError
|
|
1255
1319
|
});
|
|
@@ -1298,10 +1362,17 @@ if (!app.requestSingleInstanceLock()) {
|
|
|
1298
1362
|
pendingProtocolUrl = '';
|
|
1299
1363
|
if (initialProtocolUrl) void handleProtocolUrl(initialProtocolUrl);
|
|
1300
1364
|
});
|
|
1301
|
-
app.on('window-all-closed', () => { /* tray-owned desktop stays alive */ });
|
|
1302
|
-
app.on('before-quit', event => {
|
|
1303
|
-
if (quitSequenceStarted) return;
|
|
1304
|
-
|
|
1365
|
+
app.on('window-all-closed', () => { /* tray-owned desktop stays alive */ });
|
|
1366
|
+
app.on('before-quit', event => {
|
|
1367
|
+
if (quitSequenceStarted) return;
|
|
1368
|
+
const installerQuitDecision = installerQuitGate.handleBeforeQuit();
|
|
1369
|
+
if (installerQuitDecision === InstallerQuitDecision.HOLD
|
|
1370
|
+
|| installerQuitDecision === InstallerQuitDecision.BLOCK) {
|
|
1371
|
+
event.preventDefault();
|
|
1372
|
+
return;
|
|
1373
|
+
}
|
|
1374
|
+
if (installerQuitDecision === InstallerQuitDecision.ALLOW) return;
|
|
1375
|
+
event.preventDefault();
|
|
1305
1376
|
recurringProductUpdates?.stop();
|
|
1306
1377
|
quitSequenceStarted = true;
|
|
1307
1378
|
isQuitting = true;
|
|
@@ -1317,7 +1388,9 @@ if (!app.requestSingleInstanceLock()) {
|
|
|
1317
1388
|
isQuitting = false;
|
|
1318
1389
|
quitSequenceStarted = false;
|
|
1319
1390
|
updateInstallSequenceStarted = false;
|
|
1391
|
+
installerQuitGate.failInstall(error);
|
|
1320
1392
|
scheduleDesktopAuthRefresh(readEncryptedSession());
|
|
1393
|
+
recurringProductUpdates?.start();
|
|
1321
1394
|
updateTray('Runtime cleanup blocked');
|
|
1322
1395
|
log(`runtime cleanup blocked application quit: ${error?.message || error}`);
|
|
1323
1396
|
});
|