livedesk 0.1.593 → 0.1.595
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/bin/livedesk.js +8 -8
- package/bootstrap/role-bootstrap-server.js +1 -1
- package/client/bin/livedesk-client-fast.js +1 -1
- package/client/bin/livedesk-client-node.js +1 -1
- package/client/bin/livedesk-client.js +30 -30
- package/client/package.json +6 -6
- package/client/src/runtime/client-runtime-server.js +4 -4
- package/diagnostics/livedesk-mac-physical-isolation.mjs +354 -354
- package/diagnostics/livedesk-transport-physical-core.mjs +702 -702
- package/electron/auth-config.mjs +10 -10
- package/electron/bounded-desktop-log.mjs +609 -609
- package/electron/electron-builder.win-dev.yml +1 -1
- package/electron/icon.svg +11 -11
- package/electron/main.mjs +5 -5
- package/electron/runtime-process-owner.mjs +287 -287
- package/hub/package.json +2 -2
- package/hub/src/server.js +2 -2
- package/package.json +6 -6
- package/runtime-core/package.json +1 -1
- package/runtime-core/src/auth-config.js +1 -1
- package/web/dist/app.webmanifest +1 -1
- package/web/dist/assets/LiveDeskApp-CvQUgQ08.js +178 -0
- package/web/dist/assets/{icons-DwJmccVC.js → icons-tFaWqIIe.js} +1 -1
- package/web/dist/assets/{index-DjIbOQxl.js → index-BDVKTkV3.js} +3 -3
- package/web/dist/assets/{index-DRI7OJI6.css → index-CpHNEjHy.css} +1 -1
- package/web/dist/assets/{react-DOZUPHF8.js → react-BQos8uGS.js} +1 -1
- package/web/dist/favicon.svg +11 -11
- package/web/dist/index.html +6 -6
- package/web/dist/livedesk-build-evidence.json +25 -25
- package/web/dist/sw.js +1 -1
- package/web/dist/assets/LiveDeskApp-Djx1CsIp.js +0 -178
package/electron/icon.svg
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024" fill="none">
|
|
2
|
-
<rect x="24" y="24" width="976" height="976" rx="220" fill="#111827"/>
|
|
3
|
-
<g transform="translate(192 192) scale(22.8571428571)">
|
|
4
|
-
<rect x="2.5" y="4" width="14" height="9" rx="1.5" stroke="#F8FAFC" stroke-width="1.7" opacity="0.42"/>
|
|
5
|
-
<path d="M7 16v1.5h5" stroke="#F8FAFC" stroke-width="1.7" stroke-linecap="round" opacity="0.42"/>
|
|
6
|
-
<rect x="10.5" y="2.5" width="14" height="9" rx="1.5" stroke="#F8FAFC" stroke-width="1.7" opacity="0.62"/>
|
|
7
|
-
<path d="M15 14.5V16h5" stroke="#F8FAFC" stroke-width="1.7" stroke-linecap="round" opacity="0.62"/>
|
|
8
|
-
<rect x="6.5" y="8.5" width="15" height="10" rx="1.7" fill="#F8FAFC" fill-opacity="0.12" stroke="#F8FAFC" stroke-width="1.9"/>
|
|
9
|
-
<path d="M12 21v1.5H9.5M16 21v1.5h2.5M9.5 22.5h9" stroke="#F8FAFC" stroke-width="1.9" stroke-linecap="round"/>
|
|
10
|
-
</g>
|
|
11
|
-
</svg>
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024" fill="none">
|
|
2
|
+
<rect x="24" y="24" width="976" height="976" rx="220" fill="#111827"/>
|
|
3
|
+
<g transform="translate(192 192) scale(22.8571428571)">
|
|
4
|
+
<rect x="2.5" y="4" width="14" height="9" rx="1.5" stroke="#F8FAFC" stroke-width="1.7" opacity="0.42"/>
|
|
5
|
+
<path d="M7 16v1.5h5" stroke="#F8FAFC" stroke-width="1.7" stroke-linecap="round" opacity="0.42"/>
|
|
6
|
+
<rect x="10.5" y="2.5" width="14" height="9" rx="1.5" stroke="#F8FAFC" stroke-width="1.7" opacity="0.62"/>
|
|
7
|
+
<path d="M15 14.5V16h5" stroke="#F8FAFC" stroke-width="1.7" stroke-linecap="round" opacity="0.62"/>
|
|
8
|
+
<rect x="6.5" y="8.5" width="15" height="10" rx="1.7" fill="#F8FAFC" fill-opacity="0.12" stroke="#F8FAFC" stroke-width="1.9"/>
|
|
9
|
+
<path d="M12 21v1.5H9.5M16 21v1.5h2.5M9.5 22.5h9" stroke="#F8FAFC" stroke-width="1.9" stroke-linecap="round"/>
|
|
10
|
+
</g>
|
|
11
|
+
</svg>
|
package/electron/main.mjs
CHANGED
|
@@ -53,8 +53,8 @@ import { createDesktopClipboardOwner } from './desktop-clipboard-owner.mjs';
|
|
|
53
53
|
// warm-idle process baseline instead of retaining a hidden utility process.
|
|
54
54
|
app.commandLine.appendSwitch('disable-features', 'AudioServiceOutOfProcess');
|
|
55
55
|
|
|
56
|
-
const APP_NAME = 'VuvoDesk Desktop';
|
|
57
|
-
const DESKTOP_STARTUP_ENTRY_NAME = 'LiveDesk Desktop';
|
|
56
|
+
const APP_NAME = 'VuvoDesk Desktop';
|
|
57
|
+
const DESKTOP_STARTUP_ENTRY_NAME = 'LiveDesk Desktop';
|
|
58
58
|
const appWindowTitle = () => `${APP_NAME} · ${app.getVersion()}`;
|
|
59
59
|
const LOCAL_RUNTIME_PORT = Number(process.env.LIVEDESK_LOCAL_RUNTIME_PORT || 5179);
|
|
60
60
|
const LOCAL_RUNTIME_URL = `http://127.0.0.1:${Number.isInteger(LOCAL_RUNTIME_PORT) && LOCAL_RUNTIME_PORT > 0 ? LOCAL_RUNTIME_PORT : 5179}/`;
|
|
@@ -78,11 +78,11 @@ const SUPABASE_URL = DESKTOP_AUTH_CONFIG.supabaseUrl;
|
|
|
78
78
|
const SUPABASE_PUBLISHABLE_KEY = DESKTOP_AUTH_CONFIG.publishableKey;
|
|
79
79
|
const OAUTH_REDIRECT_URI = DESKTOP_AUTH_CONFIG.oauthRedirectUri;
|
|
80
80
|
const OAUTH_PENDING_TTL_MS = 10 * 60 * 1000;
|
|
81
|
-
const LEGACY_STARTUP_ENTRY_NAMES = ['LiveDesk Client.vbs', 'LiveDesk Client.cmd', 'LiveDesk Desktop.vbs'];
|
|
81
|
+
const LEGACY_STARTUP_ENTRY_NAMES = ['LiveDesk Client.vbs', 'LiveDesk Client.cmd', 'LiveDesk Desktop.vbs'];
|
|
82
82
|
const configuredStateRoot = process.env.LIVEDESK_DESKTOP_STATE_DIR || process.env.LIVEDESK_STATE_DIR || '';
|
|
83
83
|
const stateRoot = configuredStateRoot
|
|
84
84
|
? resolve(configuredStateRoot)
|
|
85
|
-
: join(process.env.LOCALAPPDATA || join(homedir(), 'AppData', 'Local'), 'LiveDesk');
|
|
85
|
+
: join(process.env.LOCALAPPDATA || join(homedir(), 'AppData', 'Local'), 'LiveDesk');
|
|
86
86
|
const logDir = join(stateRoot, 'logs');
|
|
87
87
|
const logPath = join(logDir, 'desktop.log');
|
|
88
88
|
const authSessionPath = join(stateRoot, 'auth-session.json');
|
|
@@ -1187,7 +1187,7 @@ function createTray() {
|
|
|
1187
1187
|
{ label: 'About VuvoDesk', click: () => { void dialog.showMessageBox(mainWindow, { type: 'info', title: APP_NAME, message: APP_NAME, detail: `Version ${app.getVersion()}\nOne VuvoDesk runtime with Hub and Client roles.` }); } },
|
|
1188
1188
|
{ label: 'Open logs', click: () => { void shell.openPath(logDir); } },
|
|
1189
1189
|
{ type: 'separator' },
|
|
1190
|
-
{ label: 'Start with Windows', type: 'checkbox', checked: app.getLoginItemSettings().openAtLogin, click: item => app.setLoginItemSettings({ openAtLogin: item.checked, name: DESKTOP_STARTUP_ENTRY_NAME }) },
|
|
1190
|
+
{ label: 'Start with Windows', type: 'checkbox', checked: app.getLoginItemSettings().openAtLogin, click: item => app.setLoginItemSettings({ openAtLogin: item.checked, name: DESKTOP_STARTUP_ENTRY_NAME }) },
|
|
1191
1191
|
{ label: 'Quit VuvoDesk', click: () => { app.quit(); } }
|
|
1192
1192
|
]);
|
|
1193
1193
|
tray.setContextMenu(menu);
|
|
@@ -1,287 +1,287 @@
|
|
|
1
|
-
export const DEFAULT_RUNTIME_STOP_TIMEOUT_MS = 12_000;
|
|
2
|
-
export const DEFAULT_RUNTIME_FORCED_CLEANUP_RESERVE_MS = 4_000;
|
|
3
|
-
export const DEFAULT_RUNTIME_COOPERATIVE_REQUEST_MS = 1_000;
|
|
4
|
-
export const DEFAULT_RUNTIME_TREE_POLL_MS = 50;
|
|
5
|
-
|
|
6
|
-
let runtimeStopOwnerSequence = 0;
|
|
7
|
-
|
|
8
|
-
function positiveInteger(value, fallback = 0) {
|
|
9
|
-
const number = Number(value);
|
|
10
|
-
return Number.isInteger(number) && number > 0 ? number : fallback;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function currentTime(now) {
|
|
14
|
-
return Number(typeof now === 'function' ? now() : Date.now());
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function childHasExited(child) {
|
|
18
|
-
return (child?.exitCode !== null && child?.exitCode !== undefined)
|
|
19
|
-
|| (child?.signalCode !== null && child?.signalCode !== undefined);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Bind an intentional stop to one immutable ChildProcess owner. The marker is
|
|
24
|
-
* deliberately stored on the child rather than in supervisor-global state so
|
|
25
|
-
* a late terminal event from an old runtime cannot suppress recovery for its
|
|
26
|
-
* replacement.
|
|
27
|
-
*/
|
|
28
|
-
export function claimRuntimeIntentionalStop(child, token = '') {
|
|
29
|
-
if (!child || typeof child !== 'object') {
|
|
30
|
-
throw new TypeError('claimRuntimeIntentionalStop requires a runtime child.');
|
|
31
|
-
}
|
|
32
|
-
if (child.livedeskRuntimeIntentionalStopOwner) {
|
|
33
|
-
return child.livedeskRuntimeIntentionalStopOwner;
|
|
34
|
-
}
|
|
35
|
-
runtimeStopOwnerSequence += 1;
|
|
36
|
-
const owner = Object.freeze({
|
|
37
|
-
token: String(token || `runtime-stop-${positiveInteger(child.pid)}-${runtimeStopOwnerSequence}`)
|
|
38
|
-
});
|
|
39
|
-
Object.defineProperty(child, 'livedeskRuntimeIntentionalStopOwner', {
|
|
40
|
-
value: owner,
|
|
41
|
-
enumerable: false,
|
|
42
|
-
configurable: false,
|
|
43
|
-
writable: false
|
|
44
|
-
});
|
|
45
|
-
return owner;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export function hasRuntimeIntentionalStop(child) {
|
|
49
|
-
return Boolean(child?.livedeskRuntimeIntentionalStopOwner?.token);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export function shouldRestartOwnedRuntimeAfterExit(child, {
|
|
53
|
-
isQuitting = false
|
|
54
|
-
} = {}) {
|
|
55
|
-
return !isQuitting && !hasRuntimeIntentionalStop(child);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Install exactly one descendant-drain operation for a child. Concurrent
|
|
60
|
-
* stop, terminal-event, quit, and update paths receive the same promise and
|
|
61
|
-
* the first absolute deadline. A rejected bounded attempt is removed so an
|
|
62
|
-
* explicit later retry can make another bounded attempt without allowing a
|
|
63
|
-
* replacement in between.
|
|
64
|
-
*/
|
|
65
|
-
export function getOrCreateRuntimeDrainPromise(child, {
|
|
66
|
-
deadline,
|
|
67
|
-
drain
|
|
68
|
-
} = {}) {
|
|
69
|
-
if (!child || typeof child !== 'object') {
|
|
70
|
-
throw new TypeError('getOrCreateRuntimeDrainPromise requires a runtime child.');
|
|
71
|
-
}
|
|
72
|
-
if (typeof drain !== 'function') {
|
|
73
|
-
throw new TypeError('getOrCreateRuntimeDrainPromise requires a drain operation.');
|
|
74
|
-
}
|
|
75
|
-
if (child.livedeskRuntimeTreeDrainPromise) {
|
|
76
|
-
return child.livedeskRuntimeTreeDrainPromise;
|
|
77
|
-
}
|
|
78
|
-
const boundDeadline = Number(deadline || 0);
|
|
79
|
-
if (!Number.isFinite(boundDeadline) || boundDeadline <= 0) {
|
|
80
|
-
throw new TypeError('getOrCreateRuntimeDrainPromise requires an absolute deadline.');
|
|
81
|
-
}
|
|
82
|
-
child.livedeskRuntimeTreeDrainDeadline = boundDeadline;
|
|
83
|
-
const operation = Promise.resolve().then(() => drain(boundDeadline));
|
|
84
|
-
child.livedeskRuntimeTreeDrainPromise = operation;
|
|
85
|
-
operation.catch(() => {
|
|
86
|
-
if (child.livedeskRuntimeTreeDrainPromise === operation) {
|
|
87
|
-
child.livedeskRuntimeTreeDrainPromise = null;
|
|
88
|
-
child.livedeskRuntimeTreeDrainDeadline = 0;
|
|
89
|
-
}
|
|
90
|
-
});
|
|
91
|
-
return operation;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export function remainingRuntimeDeadlineMs(deadline, now = Date.now) {
|
|
95
|
-
return Math.max(0, Number(deadline || 0) - currentTime(now));
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export function isOwnedUnixRuntimeGroupAlive(
|
|
99
|
-
child,
|
|
100
|
-
platform = process.platform,
|
|
101
|
-
signalProcess = (pid, signal) => process.kill(pid, signal)
|
|
102
|
-
) {
|
|
103
|
-
const processId = positiveInteger(child?.pid);
|
|
104
|
-
if (platform === 'win32'
|
|
105
|
-
|| child?.livedeskOwnsProcessGroup !== true
|
|
106
|
-
|| processId <= 1) {
|
|
107
|
-
return false;
|
|
108
|
-
}
|
|
109
|
-
try {
|
|
110
|
-
signalProcess(-processId, 0);
|
|
111
|
-
return true;
|
|
112
|
-
} catch (error) {
|
|
113
|
-
if (error?.code === 'ESRCH') return false;
|
|
114
|
-
// EPERM proves that the process group still exists. Every other inspection
|
|
115
|
-
// failure is unknown and therefore remains fail-closed.
|
|
116
|
-
return true;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
export function signalOwnedRuntimeTree(
|
|
121
|
-
child,
|
|
122
|
-
signal,
|
|
123
|
-
platform = process.platform,
|
|
124
|
-
signalProcess = (pid, requestedSignal) => process.kill(pid, requestedSignal)
|
|
125
|
-
) {
|
|
126
|
-
const processId = positiveInteger(child?.pid);
|
|
127
|
-
if (platform !== 'win32'
|
|
128
|
-
&& child?.livedeskOwnsProcessGroup === true
|
|
129
|
-
&& processId > 1) {
|
|
130
|
-
try {
|
|
131
|
-
signalProcess(-processId, signal);
|
|
132
|
-
return true;
|
|
133
|
-
} catch (error) {
|
|
134
|
-
if (error?.code === 'ESRCH') return false;
|
|
135
|
-
throw error;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
if (!child || childHasExited(child)) return false;
|
|
139
|
-
child.kill(signal);
|
|
140
|
-
return true;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
export async function waitForOwnedRuntimeTreeExit(child, {
|
|
144
|
-
deadline,
|
|
145
|
-
platform = process.platform,
|
|
146
|
-
now = Date.now,
|
|
147
|
-
wait = milliseconds => new Promise(resolveWait => setTimeout(resolveWait, milliseconds)),
|
|
148
|
-
pollMs = DEFAULT_RUNTIME_TREE_POLL_MS,
|
|
149
|
-
signalProcess = (pid, signal) => process.kill(pid, signal)
|
|
150
|
-
} = {}) {
|
|
151
|
-
const treeIsStopped = () => {
|
|
152
|
-
const rootStopped = childHasExited(child);
|
|
153
|
-
if (platform === 'win32' || child?.livedeskOwnsProcessGroup !== true) {
|
|
154
|
-
return rootStopped;
|
|
155
|
-
}
|
|
156
|
-
return rootStopped && !isOwnedUnixRuntimeGroupAlive(child, platform, signalProcess);
|
|
157
|
-
};
|
|
158
|
-
if (treeIsStopped()) return true;
|
|
159
|
-
const boundedPollMs = Math.max(10, positiveInteger(pollMs, DEFAULT_RUNTIME_TREE_POLL_MS));
|
|
160
|
-
while (remainingRuntimeDeadlineMs(deadline, now) > 0) {
|
|
161
|
-
await wait(Math.min(boundedPollMs, remainingRuntimeDeadlineMs(deadline, now)));
|
|
162
|
-
if (treeIsStopped()) return true;
|
|
163
|
-
}
|
|
164
|
-
return treeIsStopped();
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
export async function terminateOwnedUnixRuntimeTree(child, {
|
|
168
|
-
deadline,
|
|
169
|
-
platform = process.platform,
|
|
170
|
-
now = Date.now,
|
|
171
|
-
wait,
|
|
172
|
-
pollMs = DEFAULT_RUNTIME_TREE_POLL_MS,
|
|
173
|
-
forcedCleanupReserveMs = DEFAULT_RUNTIME_FORCED_CLEANUP_RESERVE_MS,
|
|
174
|
-
signalProcess = (pid, signal) => process.kill(pid, signal)
|
|
175
|
-
} = {}) {
|
|
176
|
-
if (platform === 'win32') {
|
|
177
|
-
throw new Error('terminateOwnedUnixRuntimeTree cannot own a Windows process tree.');
|
|
178
|
-
}
|
|
179
|
-
const waitOptions = { deadline, platform, now, wait, pollMs, signalProcess };
|
|
180
|
-
if (await waitForOwnedRuntimeTreeExit(child, {
|
|
181
|
-
...waitOptions,
|
|
182
|
-
deadline: currentTime(now)
|
|
183
|
-
})) {
|
|
184
|
-
return true;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
try {
|
|
188
|
-
signalOwnedRuntimeTree(child, 'SIGTERM', platform, signalProcess);
|
|
189
|
-
} catch {
|
|
190
|
-
// The exact group may reject TERM transiently. KILL still owns the bounded
|
|
191
|
-
// terminal fallback below.
|
|
192
|
-
}
|
|
193
|
-
const remainingAtTerm = remainingRuntimeDeadlineMs(deadline, now);
|
|
194
|
-
const reserveMs = Math.min(
|
|
195
|
-
Math.max(100, positiveInteger(
|
|
196
|
-
forcedCleanupReserveMs,
|
|
197
|
-
DEFAULT_RUNTIME_FORCED_CLEANUP_RESERVE_MS
|
|
198
|
-
)),
|
|
199
|
-
Math.max(100, Math.floor(remainingAtTerm / 2))
|
|
200
|
-
);
|
|
201
|
-
const gracefulDeadline = Math.max(
|
|
202
|
-
currentTime(now),
|
|
203
|
-
Number(deadline || 0) - reserveMs
|
|
204
|
-
);
|
|
205
|
-
if (await waitForOwnedRuntimeTreeExit(child, {
|
|
206
|
-
...waitOptions,
|
|
207
|
-
deadline: gracefulDeadline
|
|
208
|
-
})) {
|
|
209
|
-
return true;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
try {
|
|
213
|
-
signalOwnedRuntimeTree(child, 'SIGKILL', platform, signalProcess);
|
|
214
|
-
} catch {
|
|
215
|
-
// Absence proof below remains authoritative. A rejected KILL is never
|
|
216
|
-
// reported as successful merely because the signal call returned.
|
|
217
|
-
}
|
|
218
|
-
return waitForOwnedRuntimeTreeExit(child, waitOptions);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
export async function waitForPromiseBeforeDeadline(
|
|
222
|
-
promise,
|
|
223
|
-
deadline,
|
|
224
|
-
label = 'runtime-shutdown-deadline',
|
|
225
|
-
now = Date.now
|
|
226
|
-
) {
|
|
227
|
-
const remainingMs = remainingRuntimeDeadlineMs(deadline, now);
|
|
228
|
-
if (remainingMs <= 0) {
|
|
229
|
-
throw new Error(label);
|
|
230
|
-
}
|
|
231
|
-
let timer;
|
|
232
|
-
const timeout = new Promise((_, reject) => {
|
|
233
|
-
timer = setTimeout(() => reject(new Error(label)), remainingMs);
|
|
234
|
-
});
|
|
235
|
-
try {
|
|
236
|
-
return await Promise.race([Promise.resolve(promise), timeout]);
|
|
237
|
-
} finally {
|
|
238
|
-
clearTimeout(timer);
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
export async function fetchBeforeRuntimeDeadline(
|
|
243
|
-
fetchImpl,
|
|
244
|
-
url,
|
|
245
|
-
init,
|
|
246
|
-
{
|
|
247
|
-
deadline,
|
|
248
|
-
maxDurationMs = DEFAULT_RUNTIME_COOPERATIVE_REQUEST_MS,
|
|
249
|
-
now = Date.now,
|
|
250
|
-
label = 'runtime-shutdown-http-deadline',
|
|
251
|
-
consumeResponse = null
|
|
252
|
-
} = {}
|
|
253
|
-
) {
|
|
254
|
-
if (typeof fetchImpl !== 'function') {
|
|
255
|
-
throw new TypeError('fetchBeforeRuntimeDeadline requires fetch.');
|
|
256
|
-
}
|
|
257
|
-
const remainingMs = remainingRuntimeDeadlineMs(deadline, now);
|
|
258
|
-
if (remainingMs <= 0) throw new Error(label);
|
|
259
|
-
const durationMs = Math.max(
|
|
260
|
-
1,
|
|
261
|
-
Math.min(remainingMs, positiveInteger(maxDurationMs, remainingMs))
|
|
262
|
-
);
|
|
263
|
-
const controller = new AbortController();
|
|
264
|
-
const upstreamSignal = init?.signal;
|
|
265
|
-
const forwardAbort = () => controller.abort(upstreamSignal?.reason);
|
|
266
|
-
if (upstreamSignal?.aborted) {
|
|
267
|
-
forwardAbort();
|
|
268
|
-
} else {
|
|
269
|
-
upstreamSignal?.addEventListener?.('abort', forwardAbort, { once: true });
|
|
270
|
-
}
|
|
271
|
-
const timer = setTimeout(
|
|
272
|
-
() => controller.abort(new Error(label)),
|
|
273
|
-
durationMs
|
|
274
|
-
);
|
|
275
|
-
try {
|
|
276
|
-
const response = await fetchImpl(
|
|
277
|
-
url,
|
|
278
|
-
{ ...(init || {}), signal: controller.signal }
|
|
279
|
-
);
|
|
280
|
-
return typeof consumeResponse === 'function'
|
|
281
|
-
? await consumeResponse(response)
|
|
282
|
-
: response;
|
|
283
|
-
} finally {
|
|
284
|
-
clearTimeout(timer);
|
|
285
|
-
upstreamSignal?.removeEventListener?.('abort', forwardAbort);
|
|
286
|
-
}
|
|
287
|
-
}
|
|
1
|
+
export const DEFAULT_RUNTIME_STOP_TIMEOUT_MS = 12_000;
|
|
2
|
+
export const DEFAULT_RUNTIME_FORCED_CLEANUP_RESERVE_MS = 4_000;
|
|
3
|
+
export const DEFAULT_RUNTIME_COOPERATIVE_REQUEST_MS = 1_000;
|
|
4
|
+
export const DEFAULT_RUNTIME_TREE_POLL_MS = 50;
|
|
5
|
+
|
|
6
|
+
let runtimeStopOwnerSequence = 0;
|
|
7
|
+
|
|
8
|
+
function positiveInteger(value, fallback = 0) {
|
|
9
|
+
const number = Number(value);
|
|
10
|
+
return Number.isInteger(number) && number > 0 ? number : fallback;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function currentTime(now) {
|
|
14
|
+
return Number(typeof now === 'function' ? now() : Date.now());
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function childHasExited(child) {
|
|
18
|
+
return (child?.exitCode !== null && child?.exitCode !== undefined)
|
|
19
|
+
|| (child?.signalCode !== null && child?.signalCode !== undefined);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Bind an intentional stop to one immutable ChildProcess owner. The marker is
|
|
24
|
+
* deliberately stored on the child rather than in supervisor-global state so
|
|
25
|
+
* a late terminal event from an old runtime cannot suppress recovery for its
|
|
26
|
+
* replacement.
|
|
27
|
+
*/
|
|
28
|
+
export function claimRuntimeIntentionalStop(child, token = '') {
|
|
29
|
+
if (!child || typeof child !== 'object') {
|
|
30
|
+
throw new TypeError('claimRuntimeIntentionalStop requires a runtime child.');
|
|
31
|
+
}
|
|
32
|
+
if (child.livedeskRuntimeIntentionalStopOwner) {
|
|
33
|
+
return child.livedeskRuntimeIntentionalStopOwner;
|
|
34
|
+
}
|
|
35
|
+
runtimeStopOwnerSequence += 1;
|
|
36
|
+
const owner = Object.freeze({
|
|
37
|
+
token: String(token || `runtime-stop-${positiveInteger(child.pid)}-${runtimeStopOwnerSequence}`)
|
|
38
|
+
});
|
|
39
|
+
Object.defineProperty(child, 'livedeskRuntimeIntentionalStopOwner', {
|
|
40
|
+
value: owner,
|
|
41
|
+
enumerable: false,
|
|
42
|
+
configurable: false,
|
|
43
|
+
writable: false
|
|
44
|
+
});
|
|
45
|
+
return owner;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function hasRuntimeIntentionalStop(child) {
|
|
49
|
+
return Boolean(child?.livedeskRuntimeIntentionalStopOwner?.token);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function shouldRestartOwnedRuntimeAfterExit(child, {
|
|
53
|
+
isQuitting = false
|
|
54
|
+
} = {}) {
|
|
55
|
+
return !isQuitting && !hasRuntimeIntentionalStop(child);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Install exactly one descendant-drain operation for a child. Concurrent
|
|
60
|
+
* stop, terminal-event, quit, and update paths receive the same promise and
|
|
61
|
+
* the first absolute deadline. A rejected bounded attempt is removed so an
|
|
62
|
+
* explicit later retry can make another bounded attempt without allowing a
|
|
63
|
+
* replacement in between.
|
|
64
|
+
*/
|
|
65
|
+
export function getOrCreateRuntimeDrainPromise(child, {
|
|
66
|
+
deadline,
|
|
67
|
+
drain
|
|
68
|
+
} = {}) {
|
|
69
|
+
if (!child || typeof child !== 'object') {
|
|
70
|
+
throw new TypeError('getOrCreateRuntimeDrainPromise requires a runtime child.');
|
|
71
|
+
}
|
|
72
|
+
if (typeof drain !== 'function') {
|
|
73
|
+
throw new TypeError('getOrCreateRuntimeDrainPromise requires a drain operation.');
|
|
74
|
+
}
|
|
75
|
+
if (child.livedeskRuntimeTreeDrainPromise) {
|
|
76
|
+
return child.livedeskRuntimeTreeDrainPromise;
|
|
77
|
+
}
|
|
78
|
+
const boundDeadline = Number(deadline || 0);
|
|
79
|
+
if (!Number.isFinite(boundDeadline) || boundDeadline <= 0) {
|
|
80
|
+
throw new TypeError('getOrCreateRuntimeDrainPromise requires an absolute deadline.');
|
|
81
|
+
}
|
|
82
|
+
child.livedeskRuntimeTreeDrainDeadline = boundDeadline;
|
|
83
|
+
const operation = Promise.resolve().then(() => drain(boundDeadline));
|
|
84
|
+
child.livedeskRuntimeTreeDrainPromise = operation;
|
|
85
|
+
operation.catch(() => {
|
|
86
|
+
if (child.livedeskRuntimeTreeDrainPromise === operation) {
|
|
87
|
+
child.livedeskRuntimeTreeDrainPromise = null;
|
|
88
|
+
child.livedeskRuntimeTreeDrainDeadline = 0;
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
return operation;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function remainingRuntimeDeadlineMs(deadline, now = Date.now) {
|
|
95
|
+
return Math.max(0, Number(deadline || 0) - currentTime(now));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function isOwnedUnixRuntimeGroupAlive(
|
|
99
|
+
child,
|
|
100
|
+
platform = process.platform,
|
|
101
|
+
signalProcess = (pid, signal) => process.kill(pid, signal)
|
|
102
|
+
) {
|
|
103
|
+
const processId = positiveInteger(child?.pid);
|
|
104
|
+
if (platform === 'win32'
|
|
105
|
+
|| child?.livedeskOwnsProcessGroup !== true
|
|
106
|
+
|| processId <= 1) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
signalProcess(-processId, 0);
|
|
111
|
+
return true;
|
|
112
|
+
} catch (error) {
|
|
113
|
+
if (error?.code === 'ESRCH') return false;
|
|
114
|
+
// EPERM proves that the process group still exists. Every other inspection
|
|
115
|
+
// failure is unknown and therefore remains fail-closed.
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function signalOwnedRuntimeTree(
|
|
121
|
+
child,
|
|
122
|
+
signal,
|
|
123
|
+
platform = process.platform,
|
|
124
|
+
signalProcess = (pid, requestedSignal) => process.kill(pid, requestedSignal)
|
|
125
|
+
) {
|
|
126
|
+
const processId = positiveInteger(child?.pid);
|
|
127
|
+
if (platform !== 'win32'
|
|
128
|
+
&& child?.livedeskOwnsProcessGroup === true
|
|
129
|
+
&& processId > 1) {
|
|
130
|
+
try {
|
|
131
|
+
signalProcess(-processId, signal);
|
|
132
|
+
return true;
|
|
133
|
+
} catch (error) {
|
|
134
|
+
if (error?.code === 'ESRCH') return false;
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (!child || childHasExited(child)) return false;
|
|
139
|
+
child.kill(signal);
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export async function waitForOwnedRuntimeTreeExit(child, {
|
|
144
|
+
deadline,
|
|
145
|
+
platform = process.platform,
|
|
146
|
+
now = Date.now,
|
|
147
|
+
wait = milliseconds => new Promise(resolveWait => setTimeout(resolveWait, milliseconds)),
|
|
148
|
+
pollMs = DEFAULT_RUNTIME_TREE_POLL_MS,
|
|
149
|
+
signalProcess = (pid, signal) => process.kill(pid, signal)
|
|
150
|
+
} = {}) {
|
|
151
|
+
const treeIsStopped = () => {
|
|
152
|
+
const rootStopped = childHasExited(child);
|
|
153
|
+
if (platform === 'win32' || child?.livedeskOwnsProcessGroup !== true) {
|
|
154
|
+
return rootStopped;
|
|
155
|
+
}
|
|
156
|
+
return rootStopped && !isOwnedUnixRuntimeGroupAlive(child, platform, signalProcess);
|
|
157
|
+
};
|
|
158
|
+
if (treeIsStopped()) return true;
|
|
159
|
+
const boundedPollMs = Math.max(10, positiveInteger(pollMs, DEFAULT_RUNTIME_TREE_POLL_MS));
|
|
160
|
+
while (remainingRuntimeDeadlineMs(deadline, now) > 0) {
|
|
161
|
+
await wait(Math.min(boundedPollMs, remainingRuntimeDeadlineMs(deadline, now)));
|
|
162
|
+
if (treeIsStopped()) return true;
|
|
163
|
+
}
|
|
164
|
+
return treeIsStopped();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export async function terminateOwnedUnixRuntimeTree(child, {
|
|
168
|
+
deadline,
|
|
169
|
+
platform = process.platform,
|
|
170
|
+
now = Date.now,
|
|
171
|
+
wait,
|
|
172
|
+
pollMs = DEFAULT_RUNTIME_TREE_POLL_MS,
|
|
173
|
+
forcedCleanupReserveMs = DEFAULT_RUNTIME_FORCED_CLEANUP_RESERVE_MS,
|
|
174
|
+
signalProcess = (pid, signal) => process.kill(pid, signal)
|
|
175
|
+
} = {}) {
|
|
176
|
+
if (platform === 'win32') {
|
|
177
|
+
throw new Error('terminateOwnedUnixRuntimeTree cannot own a Windows process tree.');
|
|
178
|
+
}
|
|
179
|
+
const waitOptions = { deadline, platform, now, wait, pollMs, signalProcess };
|
|
180
|
+
if (await waitForOwnedRuntimeTreeExit(child, {
|
|
181
|
+
...waitOptions,
|
|
182
|
+
deadline: currentTime(now)
|
|
183
|
+
})) {
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
signalOwnedRuntimeTree(child, 'SIGTERM', platform, signalProcess);
|
|
189
|
+
} catch {
|
|
190
|
+
// The exact group may reject TERM transiently. KILL still owns the bounded
|
|
191
|
+
// terminal fallback below.
|
|
192
|
+
}
|
|
193
|
+
const remainingAtTerm = remainingRuntimeDeadlineMs(deadline, now);
|
|
194
|
+
const reserveMs = Math.min(
|
|
195
|
+
Math.max(100, positiveInteger(
|
|
196
|
+
forcedCleanupReserveMs,
|
|
197
|
+
DEFAULT_RUNTIME_FORCED_CLEANUP_RESERVE_MS
|
|
198
|
+
)),
|
|
199
|
+
Math.max(100, Math.floor(remainingAtTerm / 2))
|
|
200
|
+
);
|
|
201
|
+
const gracefulDeadline = Math.max(
|
|
202
|
+
currentTime(now),
|
|
203
|
+
Number(deadline || 0) - reserveMs
|
|
204
|
+
);
|
|
205
|
+
if (await waitForOwnedRuntimeTreeExit(child, {
|
|
206
|
+
...waitOptions,
|
|
207
|
+
deadline: gracefulDeadline
|
|
208
|
+
})) {
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
signalOwnedRuntimeTree(child, 'SIGKILL', platform, signalProcess);
|
|
214
|
+
} catch {
|
|
215
|
+
// Absence proof below remains authoritative. A rejected KILL is never
|
|
216
|
+
// reported as successful merely because the signal call returned.
|
|
217
|
+
}
|
|
218
|
+
return waitForOwnedRuntimeTreeExit(child, waitOptions);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export async function waitForPromiseBeforeDeadline(
|
|
222
|
+
promise,
|
|
223
|
+
deadline,
|
|
224
|
+
label = 'runtime-shutdown-deadline',
|
|
225
|
+
now = Date.now
|
|
226
|
+
) {
|
|
227
|
+
const remainingMs = remainingRuntimeDeadlineMs(deadline, now);
|
|
228
|
+
if (remainingMs <= 0) {
|
|
229
|
+
throw new Error(label);
|
|
230
|
+
}
|
|
231
|
+
let timer;
|
|
232
|
+
const timeout = new Promise((_, reject) => {
|
|
233
|
+
timer = setTimeout(() => reject(new Error(label)), remainingMs);
|
|
234
|
+
});
|
|
235
|
+
try {
|
|
236
|
+
return await Promise.race([Promise.resolve(promise), timeout]);
|
|
237
|
+
} finally {
|
|
238
|
+
clearTimeout(timer);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export async function fetchBeforeRuntimeDeadline(
|
|
243
|
+
fetchImpl,
|
|
244
|
+
url,
|
|
245
|
+
init,
|
|
246
|
+
{
|
|
247
|
+
deadline,
|
|
248
|
+
maxDurationMs = DEFAULT_RUNTIME_COOPERATIVE_REQUEST_MS,
|
|
249
|
+
now = Date.now,
|
|
250
|
+
label = 'runtime-shutdown-http-deadline',
|
|
251
|
+
consumeResponse = null
|
|
252
|
+
} = {}
|
|
253
|
+
) {
|
|
254
|
+
if (typeof fetchImpl !== 'function') {
|
|
255
|
+
throw new TypeError('fetchBeforeRuntimeDeadline requires fetch.');
|
|
256
|
+
}
|
|
257
|
+
const remainingMs = remainingRuntimeDeadlineMs(deadline, now);
|
|
258
|
+
if (remainingMs <= 0) throw new Error(label);
|
|
259
|
+
const durationMs = Math.max(
|
|
260
|
+
1,
|
|
261
|
+
Math.min(remainingMs, positiveInteger(maxDurationMs, remainingMs))
|
|
262
|
+
);
|
|
263
|
+
const controller = new AbortController();
|
|
264
|
+
const upstreamSignal = init?.signal;
|
|
265
|
+
const forwardAbort = () => controller.abort(upstreamSignal?.reason);
|
|
266
|
+
if (upstreamSignal?.aborted) {
|
|
267
|
+
forwardAbort();
|
|
268
|
+
} else {
|
|
269
|
+
upstreamSignal?.addEventListener?.('abort', forwardAbort, { once: true });
|
|
270
|
+
}
|
|
271
|
+
const timer = setTimeout(
|
|
272
|
+
() => controller.abort(new Error(label)),
|
|
273
|
+
durationMs
|
|
274
|
+
);
|
|
275
|
+
try {
|
|
276
|
+
const response = await fetchImpl(
|
|
277
|
+
url,
|
|
278
|
+
{ ...(init || {}), signal: controller.signal }
|
|
279
|
+
);
|
|
280
|
+
return typeof consumeResponse === 'function'
|
|
281
|
+
? await consumeResponse(response)
|
|
282
|
+
: response;
|
|
283
|
+
} finally {
|
|
284
|
+
clearTimeout(timer);
|
|
285
|
+
upstreamSignal?.removeEventListener?.('abort', forwardAbort);
|
|
286
|
+
}
|
|
287
|
+
}
|
package/hub/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@livedesk/hub",
|
|
3
|
-
"version": "0.1.50",
|
|
3
|
+
"version": "0.1.50",
|
|
4
4
|
"description": "VuvoDesk local Hub API and browser frame bridge",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/server.js",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
18
|
"@ffmpeg-installer/ffmpeg": "^1.1.0",
|
|
19
|
-
"@livedesk/runtime-core": "0.1.6",
|
|
19
|
+
"@livedesk/runtime-core": "0.1.6",
|
|
20
20
|
"@openai/codex-sdk": "0.145.0",
|
|
21
21
|
"cors": "^2.8.5",
|
|
22
22
|
"express": "^4.21.2",
|