livedesk 0.1.608 → 0.1.609
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/main.mjs +134 -60
- package/electron/runtime-window-load-owner.mjs +153 -0
- package/package.json +1 -1
package/electron/main.mjs
CHANGED
|
@@ -41,11 +41,12 @@ import {
|
|
|
41
41
|
waitForOwnedRuntimeTreeExit,
|
|
42
42
|
waitForPromiseBeforeDeadline
|
|
43
43
|
} from './runtime-process-owner.mjs';
|
|
44
|
-
import {
|
|
45
|
-
desktopHubTakeoverPublicationConfirmed,
|
|
46
|
-
resolveDesktopHubTakeoverPending
|
|
47
|
-
} from './runtime-role-transition.mjs';
|
|
48
|
-
import { createDesktopClipboardOwner } from './desktop-clipboard-owner.mjs';
|
|
44
|
+
import {
|
|
45
|
+
desktopHubTakeoverPublicationConfirmed,
|
|
46
|
+
resolveDesktopHubTakeoverPending
|
|
47
|
+
} from './runtime-role-transition.mjs';
|
|
48
|
+
import { createDesktopClipboardOwner } from './desktop-clipboard-owner.mjs';
|
|
49
|
+
import { createRuntimeWindowLoadOwner } from './runtime-window-load-owner.mjs';
|
|
49
50
|
|
|
50
51
|
// Chromium otherwise leaves an out-of-process Audio Service (~80-90 MiB on
|
|
51
52
|
// Windows) alive after the exact Remote Audio owner, AudioContext, worklet,
|
|
@@ -168,14 +169,34 @@ let desktopAuthRefreshTimer = null;
|
|
|
168
169
|
let desktopSessionRestorePromise = null;
|
|
169
170
|
let lastDesktopResumeRecoveryAt = 0;
|
|
170
171
|
let desktopSessionEpoch = 0;
|
|
171
|
-
let lastKnownDesktopSession = null;
|
|
172
|
-
let desktopClipboardOwner = null;
|
|
172
|
+
let lastKnownDesktopSession = null;
|
|
173
|
+
let desktopClipboardOwner = null;
|
|
174
|
+
let runtimeWindowLoadOwner = null;
|
|
173
175
|
const desktopAuthInvalidationGuard = createDesktopAuthInvalidationGuard();
|
|
174
176
|
const installerQuitGate = createInstallerQuitGate({
|
|
175
177
|
requestQuit: () => app.quit()
|
|
176
178
|
});
|
|
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>')}`;
|
|
178
|
-
const
|
|
179
|
+
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>')}`;
|
|
180
|
+
const DESKTOP_RUNTIME_RECOVERY_PAGE_URL = `data:text/html;charset=utf-8,${encodeURIComponent(`<!doctype html>
|
|
181
|
+
<html lang="en">
|
|
182
|
+
<head>
|
|
183
|
+
<meta charset="utf-8">
|
|
184
|
+
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'">
|
|
185
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
186
|
+
<title>VuvoDesk Desktop</title>
|
|
187
|
+
<style>
|
|
188
|
+
:root { color-scheme: dark; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
|
|
189
|
+
body { margin: 0; min-height: 100vh; display: grid; place-items: center; background: #0f172a; color: #f8fafc; }
|
|
190
|
+
main { display: grid; justify-items: center; gap: 18px; padding: 32px; text-align: center; }
|
|
191
|
+
.mark { width: 42px; height: 42px; border: 3px solid #334155; border-top-color: #38bdf8; border-radius: 999px; animation: spin .9s linear infinite; }
|
|
192
|
+
h1 { margin: 0; font-size: 22px; font-weight: 650; }
|
|
193
|
+
p { margin: 0; max-width: 480px; color: #cbd5e1; font-size: 15px; line-height: 1.65; }
|
|
194
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
195
|
+
</style>
|
|
196
|
+
</head>
|
|
197
|
+
<body><main><div class="mark" aria-hidden="true"></div><h1>VuvoDesk is reconnecting</h1><p>The service on this computer is starting. This window will reconnect automatically.</p></main></body>
|
|
198
|
+
</html>`)}`;
|
|
199
|
+
const desktopLogger = createBoundedDesktopLogger({ logPath });
|
|
179
200
|
|
|
180
201
|
function mask(value) {
|
|
181
202
|
return String(value ?? '')
|
|
@@ -600,9 +621,11 @@ async function waitForRuntime(options = {}) {
|
|
|
600
621
|
await new Promise(resolveDelay => setTimeout(resolveDelay, Math.min(200, remainingMs)));
|
|
601
622
|
}
|
|
602
623
|
}
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
}
|
|
624
|
+
if (config.logTimeout !== false) {
|
|
625
|
+
log(`runtime identity check timed out; expectedRole=${expectedRole || 'any'}`);
|
|
626
|
+
}
|
|
627
|
+
return null;
|
|
628
|
+
}
|
|
606
629
|
|
|
607
630
|
async function stopRuntime() {
|
|
608
631
|
if (runtimeRestartTimer) {
|
|
@@ -1056,25 +1079,28 @@ function updateTray(status = 'Starting') {
|
|
|
1056
1079
|
tray.setToolTip(`${APP_NAME} — ${status}`);
|
|
1057
1080
|
}
|
|
1058
1081
|
|
|
1059
|
-
function showWindow() {
|
|
1060
|
-
if (!mainWindow) return;
|
|
1061
|
-
|
|
1062
|
-
mainWindow.
|
|
1082
|
+
function showWindow() {
|
|
1083
|
+
if (!mainWindow) return;
|
|
1084
|
+
runtimeWindowLoadOwner?.start('window-show');
|
|
1085
|
+
if (mainWindow.isMinimized()) mainWindow.restore();
|
|
1086
|
+
mainWindow.show();
|
|
1063
1087
|
mainWindow.focus();
|
|
1064
1088
|
if (!mainWindow.webContents.isDestroyed()) mainWindow.webContents.invalidate();
|
|
1065
1089
|
requestDesktopResumeAuthRecovery('window-show');
|
|
1066
1090
|
}
|
|
1067
1091
|
|
|
1068
|
-
function createMainWindow() {
|
|
1069
|
-
const iconPath = appResource('electron', 'icon.png');
|
|
1070
|
-
Menu.setApplicationMenu(null);
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1092
|
+
function createMainWindow() {
|
|
1093
|
+
const iconPath = appResource('electron', 'icon.png');
|
|
1094
|
+
Menu.setApplicationMenu(null);
|
|
1095
|
+
runtimeWindowLoadOwner?.stop();
|
|
1096
|
+
const window = new BrowserWindow({
|
|
1097
|
+
width: 1440,
|
|
1098
|
+
height: 960,
|
|
1099
|
+
minWidth: 1080,
|
|
1100
|
+
minHeight: 720,
|
|
1101
|
+
show: false,
|
|
1102
|
+
backgroundColor: '#0f172a',
|
|
1103
|
+
autoHideMenuBar: true,
|
|
1078
1104
|
title: appWindowTitle(),
|
|
1079
1105
|
icon: existsSync(iconPath) ? iconPath : nativeImage.createFromDataURL(FALLBACK_ICON_DATA_URL),
|
|
1080
1106
|
webPreferences: {
|
|
@@ -1082,41 +1108,89 @@ function createMainWindow() {
|
|
|
1082
1108
|
contextIsolation: true,
|
|
1083
1109
|
nodeIntegration: false,
|
|
1084
1110
|
sandbox: true,
|
|
1085
|
-
spellcheck: false
|
|
1086
|
-
}
|
|
1087
|
-
});
|
|
1088
|
-
mainWindow
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
if (
|
|
1111
|
+
spellcheck: false
|
|
1112
|
+
}
|
|
1113
|
+
});
|
|
1114
|
+
mainWindow = window;
|
|
1115
|
+
let loadFailureCount = 0;
|
|
1116
|
+
const reportWindowLoadFailure = (message, context = {}) => {
|
|
1117
|
+
loadFailureCount += 1;
|
|
1118
|
+
if (loadFailureCount === 1 || loadFailureCount % 10 === 0) {
|
|
1119
|
+
log(`desktop renderer recovery ${message}; failures=${loadFailureCount}; generation=${context.generation || 'event'}; attempt=${context.attempt || 'event'}`);
|
|
1120
|
+
}
|
|
1121
|
+
};
|
|
1122
|
+
const windowLoadOwner = createRuntimeWindowLoadOwner({
|
|
1123
|
+
probeRuntime: () => waitForRuntime({ timeoutMs: 1_000, logTimeout: false }),
|
|
1124
|
+
showRecovery: async () => {
|
|
1125
|
+
if (window.isDestroyed() || window.webContents.isDestroyed()) return;
|
|
1126
|
+
if (window.webContents.getURL() !== DESKTOP_RUNTIME_RECOVERY_PAGE_URL) {
|
|
1127
|
+
await window.loadURL(DESKTOP_RUNTIME_RECOVERY_PAGE_URL);
|
|
1128
|
+
}
|
|
1129
|
+
if (process.env.LIVEDESK_E2E_HEADLESS !== '1') showWindow();
|
|
1130
|
+
},
|
|
1131
|
+
loadRuntime: async () => {
|
|
1132
|
+
if (window.isDestroyed() || window.webContents.isDestroyed()) {
|
|
1133
|
+
throw new Error('desktop-runtime-window-destroyed');
|
|
1134
|
+
}
|
|
1135
|
+
await window.loadURL(LOCAL_RUNTIME_URL);
|
|
1136
|
+
},
|
|
1137
|
+
onLoaded: context => {
|
|
1138
|
+
loadFailureCount = 0;
|
|
1139
|
+
updateTray('Running');
|
|
1140
|
+
log(`desktop renderer loaded current runtime; generation=${context.generation}; attempt=${context.attempt}`);
|
|
1141
|
+
if (process.env.LIVEDESK_E2E_HEADLESS !== '1') showWindow();
|
|
1142
|
+
},
|
|
1143
|
+
onAttemptError: (error, context) => {
|
|
1144
|
+
reportWindowLoadFailure(`${context.phase}: ${error?.message || error}`, context);
|
|
1145
|
+
},
|
|
1146
|
+
retryDelayMs: 1_000
|
|
1147
|
+
});
|
|
1148
|
+
runtimeWindowLoadOwner = windowLoadOwner;
|
|
1149
|
+
window.on('page-title-updated', event => {
|
|
1150
|
+
event.preventDefault();
|
|
1151
|
+
if (!window.isDestroyed()) window.setTitle(appWindowTitle());
|
|
1152
|
+
});
|
|
1153
|
+
window.on('focus', () => requestDesktopResumeAuthRecovery('window-focus'));
|
|
1154
|
+
window.webContents.on('will-navigate', (event, url) => {
|
|
1155
|
+
if (isAllowedRuntimeUrl(url) || url === DESKTOP_RUNTIME_RECOVERY_PAGE_URL) return;
|
|
1156
|
+
event.preventDefault();
|
|
1157
|
+
void shell.openExternal(url);
|
|
1158
|
+
});
|
|
1159
|
+
window.webContents.setWindowOpenHandler(({ url }) => {
|
|
1160
|
+
if (isAllowedRuntimeUrl(url)) return { action: 'allow' };
|
|
1161
|
+
void shell.openExternal(url);
|
|
1162
|
+
return { action: 'deny' };
|
|
1163
|
+
});
|
|
1164
|
+
window.webContents.on('did-fail-load', (_event, errorCode, errorDescription, validatedUrl, isMainFrame) => {
|
|
1165
|
+
if (!isMainFrame || errorCode === -3) return;
|
|
1166
|
+
reportWindowLoadFailure(`main-frame-failed code=${errorCode} url=${validatedUrl || 'unknown'} error=${errorDescription || 'unknown'}`);
|
|
1167
|
+
windowLoadOwner.noteMainFrameFailure(`main-frame-${errorCode}`);
|
|
1168
|
+
});
|
|
1169
|
+
window.webContents.on('render-process-gone', (_event, details) => {
|
|
1170
|
+
reportWindowLoadFailure(`renderer-gone reason=${details?.reason || 'unknown'} exitCode=${details?.exitCode ?? 'unknown'}`);
|
|
1171
|
+
windowLoadOwner.noteRendererGone(`renderer-${details?.reason || 'gone'}`);
|
|
1172
|
+
});
|
|
1173
|
+
window.webContents.on('did-finish-load', () => {
|
|
1174
|
+
if (!isAllowedRuntimeUrl(window.webContents.getURL())) return;
|
|
1175
|
+
void restoreEncryptedSession({ syncRuntime: true }).then(session => {
|
|
1176
|
+
if (desktopSessionIsUsable(session)) sendAuthEvent({ ok: true, session, restored: true });
|
|
1107
1177
|
}).catch(error => {
|
|
1108
|
-
log(`secure auth session window restore failed: ${error?.message || error}`);
|
|
1109
|
-
});
|
|
1110
|
-
});
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
if (!isQuitting) {
|
|
1114
|
-
event.preventDefault();
|
|
1115
|
-
|
|
1116
|
-
}
|
|
1117
|
-
});
|
|
1118
|
-
|
|
1119
|
-
|
|
1178
|
+
log(`secure auth session window restore failed: ${error?.message || error}`);
|
|
1179
|
+
});
|
|
1180
|
+
});
|
|
1181
|
+
windowLoadOwner.start('window-created');
|
|
1182
|
+
window.on('close', event => {
|
|
1183
|
+
if (!isQuitting) {
|
|
1184
|
+
event.preventDefault();
|
|
1185
|
+
window.hide();
|
|
1186
|
+
}
|
|
1187
|
+
});
|
|
1188
|
+
window.on('closed', () => {
|
|
1189
|
+
windowLoadOwner.stop();
|
|
1190
|
+
if (runtimeWindowLoadOwner === windowLoadOwner) runtimeWindowLoadOwner = null;
|
|
1191
|
+
if (mainWindow === window) mainWindow = null;
|
|
1192
|
+
});
|
|
1193
|
+
}
|
|
1120
1194
|
|
|
1121
1195
|
function registerIpc() {
|
|
1122
1196
|
const handle = (channel, handler) => ipcMain.handle(channel, async (event, ...args) => {
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
const DEFAULT_RETRY_DELAY_MS = 1_000;
|
|
2
|
+
|
|
3
|
+
function normalizeDelay(value, fallback = 0) {
|
|
4
|
+
const milliseconds = Number(value);
|
|
5
|
+
return Number.isFinite(milliseconds) && milliseconds >= 0
|
|
6
|
+
? Math.round(milliseconds)
|
|
7
|
+
: fallback;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Owns the one retry timer and one in-flight load attempt for a BrowserWindow.
|
|
12
|
+
* A successfully mounted runtime document is never reloaded by start(). Only a
|
|
13
|
+
* real main-frame failure or renderer termination opens a new generation.
|
|
14
|
+
*/
|
|
15
|
+
export function createRuntimeWindowLoadOwner({
|
|
16
|
+
probeRuntime,
|
|
17
|
+
showRecovery = async () => undefined,
|
|
18
|
+
loadRuntime,
|
|
19
|
+
onLoaded = () => undefined,
|
|
20
|
+
onAttemptError = () => undefined,
|
|
21
|
+
retryDelayMs = DEFAULT_RETRY_DELAY_MS,
|
|
22
|
+
setTimer = setTimeout,
|
|
23
|
+
clearTimer = clearTimeout
|
|
24
|
+
} = {}) {
|
|
25
|
+
if (typeof probeRuntime !== 'function') throw new Error('runtime-window-probe-required');
|
|
26
|
+
if (typeof loadRuntime !== 'function') throw new Error('runtime-window-loader-required');
|
|
27
|
+
|
|
28
|
+
const retryDelay = normalizeDelay(retryDelayMs, DEFAULT_RETRY_DELAY_MS);
|
|
29
|
+
let generation = 1;
|
|
30
|
+
let stopped = false;
|
|
31
|
+
let loaded = false;
|
|
32
|
+
let recoveryVisible = false;
|
|
33
|
+
let timer = null;
|
|
34
|
+
let operation = null;
|
|
35
|
+
let attemptCount = 0;
|
|
36
|
+
let pendingReason = 'created';
|
|
37
|
+
let pendingRetryDelay = retryDelay;
|
|
38
|
+
|
|
39
|
+
const snapshot = () => ({
|
|
40
|
+
generation,
|
|
41
|
+
stopped,
|
|
42
|
+
loaded,
|
|
43
|
+
recoveryVisible,
|
|
44
|
+
retryScheduled: timer !== null,
|
|
45
|
+
attemptPending: operation !== null,
|
|
46
|
+
attemptCount,
|
|
47
|
+
pendingReason
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const reportAttemptError = (error, context) => {
|
|
51
|
+
try {
|
|
52
|
+
onAttemptError(error, context);
|
|
53
|
+
} catch {
|
|
54
|
+
// Diagnostics cannot take ownership from the load recovery path.
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const ensureRecoveryVisible = async context => {
|
|
59
|
+
if (stopped || loaded || recoveryVisible || context.generation !== generation) return;
|
|
60
|
+
try {
|
|
61
|
+
await showRecovery(context);
|
|
62
|
+
if (!stopped && !loaded && context.generation === generation) recoveryVisible = true;
|
|
63
|
+
} catch (error) {
|
|
64
|
+
reportAttemptError(error, { ...context, phase: 'recovery-page' });
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const schedule = (reason = 'retry', delayMs = 0) => {
|
|
69
|
+
if (stopped || loaded || timer !== null || operation !== null) return false;
|
|
70
|
+
pendingReason = String(reason || 'retry');
|
|
71
|
+
const ownerGeneration = generation;
|
|
72
|
+
timer = setTimer(() => {
|
|
73
|
+
timer = null;
|
|
74
|
+
if (stopped || loaded || ownerGeneration !== generation) return;
|
|
75
|
+
const context = {
|
|
76
|
+
generation: ownerGeneration,
|
|
77
|
+
attempt: ++attemptCount,
|
|
78
|
+
reason: pendingReason
|
|
79
|
+
};
|
|
80
|
+
const currentOperation = (async () => {
|
|
81
|
+
const ready = await probeRuntime(context);
|
|
82
|
+
if (stopped || loaded || ownerGeneration !== generation) return;
|
|
83
|
+
if (!ready) {
|
|
84
|
+
await ensureRecoveryVisible({ ...context, phase: 'runtime-wait' });
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
await loadRuntime(context);
|
|
88
|
+
if (stopped || loaded || ownerGeneration !== generation) return;
|
|
89
|
+
loaded = true;
|
|
90
|
+
recoveryVisible = false;
|
|
91
|
+
try {
|
|
92
|
+
onLoaded(context);
|
|
93
|
+
} catch (error) {
|
|
94
|
+
reportAttemptError(error, { ...context, phase: 'loaded-callback' });
|
|
95
|
+
}
|
|
96
|
+
})()
|
|
97
|
+
.catch(async error => {
|
|
98
|
+
if (stopped || loaded || ownerGeneration !== generation) return;
|
|
99
|
+
reportAttemptError(error, { ...context, phase: 'runtime-load' });
|
|
100
|
+
await ensureRecoveryVisible({ ...context, phase: 'runtime-load' });
|
|
101
|
+
})
|
|
102
|
+
.finally(() => {
|
|
103
|
+
if (operation !== currentOperation) return;
|
|
104
|
+
operation = null;
|
|
105
|
+
if (!stopped && !loaded) {
|
|
106
|
+
const delay = pendingRetryDelay;
|
|
107
|
+
pendingRetryDelay = retryDelay;
|
|
108
|
+
schedule(pendingReason || 'retry', delay);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
operation = currentOperation;
|
|
112
|
+
}, normalizeDelay(delayMs));
|
|
113
|
+
timer?.unref?.();
|
|
114
|
+
return true;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const beginNewGeneration = reason => {
|
|
118
|
+
if (stopped) return false;
|
|
119
|
+
generation += 1;
|
|
120
|
+
loaded = false;
|
|
121
|
+
recoveryVisible = false;
|
|
122
|
+
pendingReason = String(reason || 'reload');
|
|
123
|
+
pendingRetryDelay = 0;
|
|
124
|
+
if (timer !== null) {
|
|
125
|
+
clearTimer(timer);
|
|
126
|
+
timer = null;
|
|
127
|
+
}
|
|
128
|
+
schedule(pendingReason, 0);
|
|
129
|
+
return true;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
getStatus: snapshot,
|
|
134
|
+
start(reason = 'start') {
|
|
135
|
+
return schedule(reason, 0);
|
|
136
|
+
},
|
|
137
|
+
noteMainFrameFailure(reason = 'main-frame-failed') {
|
|
138
|
+
return beginNewGeneration(reason);
|
|
139
|
+
},
|
|
140
|
+
noteRendererGone(reason = 'renderer-gone') {
|
|
141
|
+
return beginNewGeneration(reason);
|
|
142
|
+
},
|
|
143
|
+
stop() {
|
|
144
|
+
if (stopped) return;
|
|
145
|
+
stopped = true;
|
|
146
|
+
generation += 1;
|
|
147
|
+
if (timer !== null) {
|
|
148
|
+
clearTimer(timer);
|
|
149
|
+
timer = null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
}
|