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.
Files changed (31) hide show
  1. package/bin/livedesk.js +8 -8
  2. package/bootstrap/role-bootstrap-server.js +1 -1
  3. package/client/bin/livedesk-client-fast.js +1 -1
  4. package/client/bin/livedesk-client-node.js +1 -1
  5. package/client/bin/livedesk-client.js +30 -30
  6. package/client/package.json +6 -6
  7. package/client/src/runtime/client-runtime-server.js +4 -4
  8. package/diagnostics/livedesk-mac-physical-isolation.mjs +354 -354
  9. package/diagnostics/livedesk-transport-physical-core.mjs +702 -702
  10. package/electron/auth-config.mjs +10 -10
  11. package/electron/bounded-desktop-log.mjs +609 -609
  12. package/electron/electron-builder.win-dev.yml +1 -1
  13. package/electron/icon.svg +11 -11
  14. package/electron/main.mjs +5 -5
  15. package/electron/runtime-process-owner.mjs +287 -287
  16. package/hub/package.json +2 -2
  17. package/hub/src/server.js +2 -2
  18. package/package.json +6 -6
  19. package/runtime-core/package.json +1 -1
  20. package/runtime-core/src/auth-config.js +1 -1
  21. package/web/dist/app.webmanifest +1 -1
  22. package/web/dist/assets/LiveDeskApp-CvQUgQ08.js +178 -0
  23. package/web/dist/assets/{icons-DwJmccVC.js → icons-tFaWqIIe.js} +1 -1
  24. package/web/dist/assets/{index-DjIbOQxl.js → index-BDVKTkV3.js} +3 -3
  25. package/web/dist/assets/{index-DRI7OJI6.css → index-CpHNEjHy.css} +1 -1
  26. package/web/dist/assets/{react-DOZUPHF8.js → react-BQos8uGS.js} +1 -1
  27. package/web/dist/favicon.svg +11 -11
  28. package/web/dist/index.html +6 -6
  29. package/web/dist/livedesk-build-evidence.json +25 -25
  30. package/web/dist/sw.js +1 -1
  31. package/web/dist/assets/LiveDeskApp-Djx1CsIp.js +0 -178
@@ -1,5 +1,5 @@
1
1
  extends: file:packages/livedesk/electron/electron-builder.win.yml
2
- artifactName: VuvoDesk-${version}-dev-${arch}.${ext}
2
+ artifactName: VuvoDesk-${version}-dev-${arch}.${ext}
3
3
  directories:
4
4
  output: dist/desktop-dev
5
5
  extraMetadata:
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",