livedesk 0.1.592 → 0.1.593
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/README.md +8 -8
- package/bin/livedesk.js +115 -112
- package/bootstrap/client-process-identity.js +1 -1
- package/bootstrap/device-role.js +1 -1
- package/bootstrap/legacy-client-update.js +15 -15
- package/bootstrap/role-bootstrap-server.js +2 -2
- package/bootstrap/runtime-lock.js +4 -4
- package/bootstrap/runtime-shutdown.js +3 -3
- package/bootstrap/update-host-client.mjs +9 -9
- package/bootstrap/update-host.mjs +6 -6
- package/client/README.md +10 -10
- package/client/THIRD_PARTY_NOTICES.md +1 -1
- package/client/bin/client-version.js +1 -1
- package/client/bin/livedesk-client-node.js +102 -102
- package/client/bin/livedesk-client-update-bootstrap.cjs +12 -12
- package/client/bin/livedesk-client.js +136 -122
- package/client/package.json +7 -7
- package/client/src/runtime/agent-process-lifecycle.js +10 -10
- package/client/src/runtime/agent-shell.js +66 -66
- package/client/src/runtime/client-runtime-server.js +12 -11
- package/client/src/runtime/linux-video-acceleration.js +1 -1
- package/client/src/runtime/windows-owned-process-manifest.js +9 -9
- package/client/tests/client-version.test.mjs +2 -2
- package/diagnostics/livedesk-mac-physical-gate-core.mjs +5 -5
- package/diagnostics/livedesk-mode3-capture-smoke.mjs +7 -7
- package/diagnostics/livedesk-transport-physical.mjs +3 -3
- package/electron/auth-session-owner.mjs +24 -24
- package/electron/desktop-clipboard-owner.mjs +382 -382
- package/electron/electron-builder.e2e.yml +1 -1
- package/electron/electron-builder.mac-arm64.yml +1 -1
- package/electron/electron-builder.mac-x64.yml +1 -1
- package/electron/electron-builder.win-dev.yml +1 -1
- package/electron/electron-builder.win.yml +2 -2
- package/electron/electron-builder.yml +2 -2
- package/electron/main.mjs +175 -174
- package/electron/preload.cjs +13 -13
- package/electron/runtime-role-transition.mjs +17 -17
- package/hub/package.json +4 -4
- package/hub/src/agents/agent-manager.js +2 -2
- package/hub/src/agents/agent-permissions.js +2 -2
- package/hub/src/agents/agent-tool-registry.js +36 -36
- package/hub/src/agents/codex-agent-runtime.js +16 -16
- package/hub/src/agents/codex-mcp-server.js +5 -5
- package/hub/src/filesystem/roots.js +20 -20
- package/hub/src/filesystem/shared-folders.js +2 -2
- package/hub/src/live-desk-update.js +9 -9
- package/hub/src/remote-clipboard-contract.mjs +482 -482
- package/hub/src/remote-hub.js +1025 -1025
- package/hub/src/server.js +146 -146
- package/hub/src/settings/effective-device-policy.js +31 -31
- package/hub/src/settings/settings-schema.js +2 -2
- package/package.json +7 -7
- package/runtime-core/package.json +2 -2
- package/runtime-core/src/auth-config.js +1 -1
- package/scripts/sync-web-dist.js +9 -9
- package/web/dist/app.webmanifest +4 -4
- package/web/dist/assets/{LiveDeskApp-CTj_tAlH.js → LiveDeskApp-Djx1CsIp.js} +13 -13
- package/web/dist/assets/{index-CSz9F86o.js → index-DjIbOQxl.js} +6 -6
- package/web/dist/desktop-auth-callback.html +4 -4
- package/web/dist/desktop-auth-callback.js +13 -13
- package/web/dist/index.html +9 -8
- package/web/dist/livedesk-build-evidence.json +21 -21
- package/web/dist/offline.html +2 -2
- package/web/dist/sw.js +11 -11
|
@@ -38,7 +38,7 @@ mac:
|
|
|
38
38
|
arch:
|
|
39
39
|
- arm64
|
|
40
40
|
extendInfo:
|
|
41
|
-
NSMicrophoneUsageDescription:
|
|
41
|
+
NSMicrophoneUsageDescription: VuvoDesk uses audio only when remote computer audio is enabled.
|
|
42
42
|
publish:
|
|
43
43
|
provider: generic
|
|
44
44
|
url: https://livedesk-desktop-updates.lovecrdm.workers.dev/stable/macos/arm64
|
|
@@ -38,7 +38,7 @@ mac:
|
|
|
38
38
|
arch:
|
|
39
39
|
- x64
|
|
40
40
|
extendInfo:
|
|
41
|
-
NSMicrophoneUsageDescription:
|
|
41
|
+
NSMicrophoneUsageDescription: VuvoDesk uses audio only when remote computer audio is enabled.
|
|
42
42
|
publish:
|
|
43
43
|
provider: generic
|
|
44
44
|
url: https://livedesk-desktop-updates.lovecrdm.workers.dev/stable/macos/x64
|
|
@@ -29,14 +29,14 @@ win:
|
|
|
29
29
|
- target: nsis
|
|
30
30
|
arch:
|
|
31
31
|
- x64
|
|
32
|
-
executableName:
|
|
32
|
+
executableName: VuvoDesk
|
|
33
33
|
nsis:
|
|
34
34
|
oneClick: false
|
|
35
35
|
perMachine: false
|
|
36
36
|
allowToChangeInstallationDirectory: true
|
|
37
37
|
createDesktopShortcut: true
|
|
38
38
|
createStartMenuShortcut: true
|
|
39
|
-
shortcutName:
|
|
39
|
+
shortcutName: VuvoDesk
|
|
40
40
|
deleteAppDataOnUninstall: false
|
|
41
41
|
publish:
|
|
42
42
|
provider: generic
|
package/electron/main.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { app, 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
|
-
import { dirname, join, resolve } from 'node:path';
|
|
5
|
-
import { spawn } from 'node:child_process';
|
|
6
|
-
import { createRequire } from 'node:module';
|
|
4
|
+
import { dirname, join, resolve } from 'node:path';
|
|
5
|
+
import { spawn } from 'node:child_process';
|
|
6
|
+
import { createRequire } from 'node:module';
|
|
7
7
|
import { createProductUpdateManager } from './update-manager.mjs';
|
|
8
8
|
import { createRecurringProductUpdateOwner } from './recurring-update-owner.mjs';
|
|
9
9
|
import { createPkceAuthorizationUrl, exchangePkceCode, fetchSupabaseUser, parsePkceCallbackUrl, refreshSupabaseSession } from './oauth-pkce.mjs';
|
|
@@ -27,8 +27,8 @@ import {
|
|
|
27
27
|
registerOwnedWindowsChild,
|
|
28
28
|
terminateExactRegisteredWindowsTreeUntilStopped
|
|
29
29
|
} from '../runtime-core/src/windows-owned-process-tree.js';
|
|
30
|
-
import {
|
|
31
|
-
DEFAULT_RUNTIME_COOPERATIVE_REQUEST_MS,
|
|
30
|
+
import {
|
|
31
|
+
DEFAULT_RUNTIME_COOPERATIVE_REQUEST_MS,
|
|
32
32
|
DEFAULT_RUNTIME_FORCED_CLEANUP_RESERVE_MS,
|
|
33
33
|
DEFAULT_RUNTIME_STOP_TIMEOUT_MS,
|
|
34
34
|
claimRuntimeIntentionalStop,
|
|
@@ -37,14 +37,14 @@ import {
|
|
|
37
37
|
remainingRuntimeDeadlineMs,
|
|
38
38
|
shouldRestartOwnedRuntimeAfterExit,
|
|
39
39
|
terminateOwnedUnixRuntimeTree,
|
|
40
|
-
waitForOwnedRuntimeTreeExit,
|
|
41
|
-
waitForPromiseBeforeDeadline
|
|
42
|
-
} from './runtime-process-owner.mjs';
|
|
43
|
-
import {
|
|
44
|
-
desktopHubTakeoverPublicationConfirmed,
|
|
45
|
-
resolveDesktopHubTakeoverPending
|
|
46
|
-
} from './runtime-role-transition.mjs';
|
|
47
|
-
import { createDesktopClipboardOwner } from './desktop-clipboard-owner.mjs';
|
|
40
|
+
waitForOwnedRuntimeTreeExit,
|
|
41
|
+
waitForPromiseBeforeDeadline
|
|
42
|
+
} from './runtime-process-owner.mjs';
|
|
43
|
+
import {
|
|
44
|
+
desktopHubTakeoverPublicationConfirmed,
|
|
45
|
+
resolveDesktopHubTakeoverPending
|
|
46
|
+
} from './runtime-role-transition.mjs';
|
|
47
|
+
import { createDesktopClipboardOwner } from './desktop-clipboard-owner.mjs';
|
|
48
48
|
|
|
49
49
|
// Chromium otherwise leaves an out-of-process Audio Service (~80-90 MiB on
|
|
50
50
|
// Windows) alive after the exact Remote Audio owner, AudioContext, worklet,
|
|
@@ -53,7 +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 = '
|
|
56
|
+
const APP_NAME = 'VuvoDesk Desktop';
|
|
57
|
+
const DESKTOP_STARTUP_ENTRY_NAME = 'LiveDesk Desktop';
|
|
57
58
|
const appWindowTitle = () => `${APP_NAME} · ${app.getVersion()}`;
|
|
58
59
|
const LOCAL_RUNTIME_PORT = Number(process.env.LIVEDESK_LOCAL_RUNTIME_PORT || 5179);
|
|
59
60
|
const LOCAL_RUNTIME_URL = `http://127.0.0.1:${Number.isInteger(LOCAL_RUNTIME_PORT) && LOCAL_RUNTIME_PORT > 0 ? LOCAL_RUNTIME_PORT : 5179}/`;
|
|
@@ -77,11 +78,11 @@ const SUPABASE_URL = DESKTOP_AUTH_CONFIG.supabaseUrl;
|
|
|
77
78
|
const SUPABASE_PUBLISHABLE_KEY = DESKTOP_AUTH_CONFIG.publishableKey;
|
|
78
79
|
const OAUTH_REDIRECT_URI = DESKTOP_AUTH_CONFIG.oauthRedirectUri;
|
|
79
80
|
const OAUTH_PENDING_TTL_MS = 10 * 60 * 1000;
|
|
80
|
-
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'];
|
|
81
82
|
const configuredStateRoot = process.env.LIVEDESK_DESKTOP_STATE_DIR || process.env.LIVEDESK_STATE_DIR || '';
|
|
82
83
|
const stateRoot = configuredStateRoot
|
|
83
84
|
? resolve(configuredStateRoot)
|
|
84
|
-
: join(process.env.LOCALAPPDATA || join(homedir(), 'AppData', 'Local'), 'LiveDesk');
|
|
85
|
+
: join(process.env.LOCALAPPDATA || join(homedir(), 'AppData', 'Local'), 'LiveDesk');
|
|
85
86
|
const logDir = join(stateRoot, 'logs');
|
|
86
87
|
const logPath = join(logDir, 'desktop.log');
|
|
87
88
|
const authSessionPath = join(stateRoot, 'auth-session.json');
|
|
@@ -93,50 +94,50 @@ const unpackedAppPath = appPath.replace(/app\.asar([\\/]|$)/, 'app.asar.unpacked
|
|
|
93
94
|
const appRoot = [appPath, unpackedAppPath, resolve(appPath, '..'), resolve(unpackedAppPath, '..')]
|
|
94
95
|
.find(candidate => existsSync(join(candidate, 'bin', 'livedesk.js')) || existsSync(join(candidate, 'package.json')))
|
|
95
96
|
|| appPath;
|
|
96
|
-
const runtimeCwd = existsSync(join(unpackedAppPath, 'bin', 'livedesk.js')) ? unpackedAppPath : appRoot;
|
|
97
|
-
const require = createRequire(import.meta.url);
|
|
97
|
+
const runtimeCwd = existsSync(join(unpackedAppPath, 'bin', 'livedesk.js')) ? unpackedAppPath : appRoot;
|
|
98
|
+
const require = createRequire(import.meta.url);
|
|
99
|
+
|
|
100
|
+
function appResource(...parts) {
|
|
101
|
+
const unpacked = resolve(unpackedAppPath, ...parts);
|
|
102
|
+
return existsSync(unpacked) ? unpacked : resolve(appRoot, ...parts);
|
|
103
|
+
}
|
|
98
104
|
|
|
99
|
-
function
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
candidates.push(
|
|
136
|
-
appResource('client', 'fast', rid, executableName),
|
|
137
|
-
...sourceCandidates);
|
|
138
|
-
return candidates.find(candidate => existsSync(candidate)) || '';
|
|
139
|
-
}
|
|
105
|
+
function resolveClipboardHelperPath() {
|
|
106
|
+
const override = String(process.env.LIVEDESK_CLIPBOARD_HELPER_PATH || '').trim();
|
|
107
|
+
if (override && existsSync(override)) return resolve(override);
|
|
108
|
+
const platform = process.platform;
|
|
109
|
+
const arch = process.arch;
|
|
110
|
+
const rid = platform === 'win32' && arch === 'x64'
|
|
111
|
+
? 'win-x64'
|
|
112
|
+
: platform === 'darwin' && arch === 'arm64'
|
|
113
|
+
? 'osx-arm64'
|
|
114
|
+
: platform === 'darwin' && arch === 'x64'
|
|
115
|
+
? 'osx-x64'
|
|
116
|
+
: platform === 'linux' && arch === 'x64'
|
|
117
|
+
? 'linux-x64'
|
|
118
|
+
: '';
|
|
119
|
+
if (!rid) return '';
|
|
120
|
+
const executableName = platform === 'win32' ? 'livedesk-client-fast.exe' : 'livedesk-client-fast';
|
|
121
|
+
const packageName = `@livedesk/fast-${rid}`;
|
|
122
|
+
const candidates = [];
|
|
123
|
+
const sourceCandidates = [
|
|
124
|
+
resolve(appRoot, '..', `fast-${rid}`, 'fast', executableName),
|
|
125
|
+
resolve(appRoot, '..', 'client', 'fast', rid, executableName),
|
|
126
|
+
resolve(appRoot, '..', '..', 'packages', `fast-${rid}`, 'fast', executableName)
|
|
127
|
+
];
|
|
128
|
+
if (!app.isPackaged) candidates.push(...sourceCandidates);
|
|
129
|
+
try {
|
|
130
|
+
const packagePath = require.resolve(`${packageName}/package.json`);
|
|
131
|
+
const packagedPath = join(dirname(packagePath), 'fast', executableName);
|
|
132
|
+
candidates.push(packagedPath.replace(/app\.asar([\\/]|$)/, 'app.asar.unpacked$1'), packagedPath);
|
|
133
|
+
} catch {
|
|
134
|
+
// Development checkouts use the source/package staging candidates below.
|
|
135
|
+
}
|
|
136
|
+
candidates.push(
|
|
137
|
+
appResource('client', 'fast', rid, executableName),
|
|
138
|
+
...sourceCandidates);
|
|
139
|
+
return candidates.find(candidate => existsSync(candidate)) || '';
|
|
140
|
+
}
|
|
140
141
|
|
|
141
142
|
// Electron's ASAR loader keeps unpacked files addressable through the virtual
|
|
142
143
|
// app.asar path while resolving their package dependencies from the same tree.
|
|
@@ -151,11 +152,11 @@ let runtimeChild = null;
|
|
|
151
152
|
let isQuitting = false;
|
|
152
153
|
let runtimeRestartCount = 0;
|
|
153
154
|
let runtimeRestartWindowStartedAt = 0;
|
|
154
|
-
let runtimeRestartTimer = null;
|
|
155
|
-
let runtimeRestartPromise = null;
|
|
156
|
-
let runtimeLaunchStartedAt = 0;
|
|
157
|
-
let runtimeRoleTakeoverPending = process.env.LIVEDESK_ROLE_TAKEOVER === '1';
|
|
158
|
-
let quitSequenceStarted = false;
|
|
155
|
+
let runtimeRestartTimer = null;
|
|
156
|
+
let runtimeRestartPromise = null;
|
|
157
|
+
let runtimeLaunchStartedAt = 0;
|
|
158
|
+
let runtimeRoleTakeoverPending = process.env.LIVEDESK_ROLE_TAKEOVER === '1';
|
|
159
|
+
let quitSequenceStarted = false;
|
|
159
160
|
let updateInstallSequenceStarted = false;
|
|
160
161
|
let productUpdates = null;
|
|
161
162
|
let recurringProductUpdates = null;
|
|
@@ -165,10 +166,10 @@ let runtimeLogoutPromise = null;
|
|
|
165
166
|
let desktopAuthRefreshTimer = null;
|
|
166
167
|
let desktopSessionRestorePromise = null;
|
|
167
168
|
let lastDesktopResumeRecoveryAt = 0;
|
|
168
|
-
let desktopSessionEpoch = 0;
|
|
169
|
-
let lastKnownDesktopSession = null;
|
|
170
|
-
let desktopClipboardOwner = null;
|
|
171
|
-
const desktopAuthInvalidationGuard = createDesktopAuthInvalidationGuard();
|
|
169
|
+
let desktopSessionEpoch = 0;
|
|
170
|
+
let lastKnownDesktopSession = null;
|
|
171
|
+
let desktopClipboardOwner = null;
|
|
172
|
+
const desktopAuthInvalidationGuard = createDesktopAuthInvalidationGuard();
|
|
172
173
|
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>')}`;
|
|
173
174
|
const desktopLogger = createBoundedDesktopLogger({ logPath });
|
|
174
175
|
|
|
@@ -402,23 +403,23 @@ async function drainRuntimeTreeUntilStopped(child, deadline) {
|
|
|
402
403
|
);
|
|
403
404
|
}
|
|
404
405
|
|
|
405
|
-
function startRuntime(options = {}) {
|
|
406
|
-
if (runtimeChild) return runtimeChild;
|
|
407
|
-
runtimeLaunchStartedAt = Date.now();
|
|
408
|
-
const preResolvedRole = options?.role === 'hub' || options?.role === 'client' ? options.role : '';
|
|
409
|
-
const roleTakeover = options?.roleTakeover === true || runtimeRoleTakeoverPending;
|
|
410
|
-
if (roleTakeover) runtimeRoleTakeoverPending = true;
|
|
411
|
-
const ownsUnixProcessGroup = process.platform !== 'win32';
|
|
406
|
+
function startRuntime(options = {}) {
|
|
407
|
+
if (runtimeChild) return runtimeChild;
|
|
408
|
+
runtimeLaunchStartedAt = Date.now();
|
|
409
|
+
const preResolvedRole = options?.role === 'hub' || options?.role === 'client' ? options.role : '';
|
|
410
|
+
const roleTakeover = options?.roleTakeover === true || runtimeRoleTakeoverPending;
|
|
411
|
+
if (roleTakeover) runtimeRoleTakeoverPending = true;
|
|
412
|
+
const ownsUnixProcessGroup = process.platform !== 'win32';
|
|
412
413
|
const child = spawn(runtimeNode(), [launcherEntry, '--no-open'], {
|
|
413
414
|
cwd: runtimeCwd,
|
|
414
415
|
env: {
|
|
415
416
|
...process.env,
|
|
416
417
|
LIVEDESK_DESKTOP_HOST: '1',
|
|
417
418
|
LIVEDESK_MANAGER_VERSION: app.getVersion(),
|
|
418
|
-
LIVEDESK_NPM_LAUNCHER_NAME: 'livedesk',
|
|
419
|
-
LIVEDESK_NPM_LAUNCHER_VERSION: app.getVersion(),
|
|
420
|
-
LIVEDESK_STATE_DIR: runtimeStateDir,
|
|
421
|
-
LIVEDESK_ROLE_TAKEOVER: roleTakeover ? '1' : '',
|
|
419
|
+
LIVEDESK_NPM_LAUNCHER_NAME: 'livedesk',
|
|
420
|
+
LIVEDESK_NPM_LAUNCHER_VERSION: app.getVersion(),
|
|
421
|
+
LIVEDESK_STATE_DIR: runtimeStateDir,
|
|
422
|
+
LIVEDESK_ROLE_TAKEOVER: roleTakeover ? '1' : '',
|
|
422
423
|
...(preResolvedRole ? {
|
|
423
424
|
LIVEDESK_PRE_RESOLVED_ROLE: preResolvedRole,
|
|
424
425
|
LIVEDESK_ROLE_SOURCE: String(options.source || 'supabase'),
|
|
@@ -450,23 +451,23 @@ function startRuntime(options = {}) {
|
|
|
450
451
|
// `exit` starts descendant cleanup without waiting for inherited stdio to
|
|
451
452
|
// close. `close` is the failed-spawn fallback because Node does not promise
|
|
452
453
|
// a corresponding `exit` event after every ChildProcess `error`.
|
|
453
|
-
const handleRuntimeTerminal = (code, signal, source) => {
|
|
454
|
+
const handleRuntimeTerminal = (code, signal, source) => {
|
|
454
455
|
if (runtimeTerminalHandled) return;
|
|
455
|
-
runtimeTerminalHandled = true;
|
|
456
|
-
log(`runtime terminal source=${source} code=${code ?? 'null'} signal=${signal || 'none'}`);
|
|
457
|
-
const expectedRoleAfterExit = expectedRuntimeRole();
|
|
458
|
-
const takeoverWasPending = runtimeRoleTakeoverPending;
|
|
459
|
-
runtimeRoleTakeoverPending = resolveDesktopHubTakeoverPending({
|
|
460
|
-
pending: runtimeRoleTakeoverPending,
|
|
461
|
-
exitCode: code,
|
|
462
|
-
signal,
|
|
463
|
-
expectedRole: expectedRoleAfterExit
|
|
464
|
-
});
|
|
465
|
-
if (runtimeRoleTakeoverPending !== takeoverWasPending) {
|
|
466
|
-
log(runtimeRoleTakeoverPending
|
|
467
|
-
? 'desktop role transition retained one-shot Hub target takeover authority'
|
|
468
|
-
: 'desktop role transition cleared Hub target takeover authority for Client startup');
|
|
469
|
-
}
|
|
456
|
+
runtimeTerminalHandled = true;
|
|
457
|
+
log(`runtime terminal source=${source} code=${code ?? 'null'} signal=${signal || 'none'}`);
|
|
458
|
+
const expectedRoleAfterExit = expectedRuntimeRole();
|
|
459
|
+
const takeoverWasPending = runtimeRoleTakeoverPending;
|
|
460
|
+
runtimeRoleTakeoverPending = resolveDesktopHubTakeoverPending({
|
|
461
|
+
pending: runtimeRoleTakeoverPending,
|
|
462
|
+
exitCode: code,
|
|
463
|
+
signal,
|
|
464
|
+
expectedRole: expectedRoleAfterExit
|
|
465
|
+
});
|
|
466
|
+
if (runtimeRoleTakeoverPending !== takeoverWasPending) {
|
|
467
|
+
log(runtimeRoleTakeoverPending
|
|
468
|
+
? 'desktop role transition retained one-shot Hub target takeover authority'
|
|
469
|
+
: 'desktop role transition cleared Hub target takeover authority for Client startup');
|
|
470
|
+
}
|
|
470
471
|
const continueAfterExactDrain = async () => {
|
|
471
472
|
try {
|
|
472
473
|
const drainDeadline = Number(child.livedeskRuntimeStopDeadline || 0)
|
|
@@ -506,42 +507,42 @@ function startRuntime(options = {}) {
|
|
|
506
507
|
};
|
|
507
508
|
void continueAfterExactDrain();
|
|
508
509
|
};
|
|
509
|
-
child.once('exit', (code, signal) => handleRuntimeTerminal(code, signal, 'exit'));
|
|
510
|
-
child.once('close', (code, signal) => handleRuntimeTerminal(code, signal, 'close'));
|
|
511
|
-
if (roleTakeover) void monitorDesktopHubTakeoverPublication(child);
|
|
512
|
-
log(`runtime started pid=${child.pid}`);
|
|
513
|
-
return child;
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
async function monitorDesktopHubTakeoverPublication(child) {
|
|
517
|
-
while (!isQuitting && runtimeChild === child && runtimeRoleTakeoverPending) {
|
|
518
|
-
try {
|
|
519
|
-
const deadline = Date.now() + 1_000;
|
|
520
|
-
const status = await fetchBeforeRuntimeDeadline(
|
|
521
|
-
fetch,
|
|
522
|
-
`${LOCAL_RUNTIME_URL}api/hub/status`,
|
|
523
|
-
{ cache: 'no-store' },
|
|
524
|
-
{
|
|
525
|
-
deadline,
|
|
526
|
-
maxDurationMs: remainingRuntimeDeadlineMs(deadline),
|
|
527
|
-
label: 'hub-role-takeover-publication-deadline',
|
|
528
|
-
consumeResponse: response => response.json()
|
|
529
|
-
}
|
|
530
|
-
);
|
|
531
|
-
if (desktopHubTakeoverPublicationConfirmed(status)) {
|
|
532
|
-
if (runtimeChild === child) {
|
|
533
|
-
runtimeRoleTakeoverPending = false;
|
|
534
|
-
log('desktop Hub target takeover completed and one-shot authority was released');
|
|
535
|
-
}
|
|
536
|
-
return true;
|
|
537
|
-
}
|
|
538
|
-
} catch {
|
|
539
|
-
// The replacement Hub may still be starting or retrying its saved session.
|
|
540
|
-
}
|
|
541
|
-
await new Promise(resolveDelay => setTimeout(resolveDelay, 1_000));
|
|
542
|
-
}
|
|
543
|
-
return false;
|
|
544
|
-
}
|
|
510
|
+
child.once('exit', (code, signal) => handleRuntimeTerminal(code, signal, 'exit'));
|
|
511
|
+
child.once('close', (code, signal) => handleRuntimeTerminal(code, signal, 'close'));
|
|
512
|
+
if (roleTakeover) void monitorDesktopHubTakeoverPublication(child);
|
|
513
|
+
log(`runtime started pid=${child.pid}`);
|
|
514
|
+
return child;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
async function monitorDesktopHubTakeoverPublication(child) {
|
|
518
|
+
while (!isQuitting && runtimeChild === child && runtimeRoleTakeoverPending) {
|
|
519
|
+
try {
|
|
520
|
+
const deadline = Date.now() + 1_000;
|
|
521
|
+
const status = await fetchBeforeRuntimeDeadline(
|
|
522
|
+
fetch,
|
|
523
|
+
`${LOCAL_RUNTIME_URL}api/hub/status`,
|
|
524
|
+
{ cache: 'no-store' },
|
|
525
|
+
{
|
|
526
|
+
deadline,
|
|
527
|
+
maxDurationMs: remainingRuntimeDeadlineMs(deadline),
|
|
528
|
+
label: 'hub-role-takeover-publication-deadline',
|
|
529
|
+
consumeResponse: response => response.json()
|
|
530
|
+
}
|
|
531
|
+
);
|
|
532
|
+
if (desktopHubTakeoverPublicationConfirmed(status)) {
|
|
533
|
+
if (runtimeChild === child) {
|
|
534
|
+
runtimeRoleTakeoverPending = false;
|
|
535
|
+
log('desktop Hub target takeover completed and one-shot authority was released');
|
|
536
|
+
}
|
|
537
|
+
return true;
|
|
538
|
+
}
|
|
539
|
+
} catch {
|
|
540
|
+
// The replacement Hub may still be starting or retrying its saved session.
|
|
541
|
+
}
|
|
542
|
+
await new Promise(resolveDelay => setTimeout(resolveDelay, 1_000));
|
|
543
|
+
}
|
|
544
|
+
return false;
|
|
545
|
+
}
|
|
545
546
|
|
|
546
547
|
function expectedRuntimeRole() {
|
|
547
548
|
const forcedRole = String(process.env.LIVEDESK_FORCE_ROLE || '').trim().toLowerCase();
|
|
@@ -1071,11 +1072,11 @@ function registerIpc() {
|
|
|
1071
1072
|
return { ok: false, error: 'application-exit-already-started' };
|
|
1072
1073
|
}
|
|
1073
1074
|
quitSequenceStarted = true;
|
|
1074
|
-
isQuitting = true;
|
|
1075
|
-
try {
|
|
1076
|
-
await stopRuntime();
|
|
1077
|
-
await desktopClipboardOwner?.close();
|
|
1078
|
-
await closeDesktopLog();
|
|
1075
|
+
isQuitting = true;
|
|
1076
|
+
try {
|
|
1077
|
+
await stopRuntime();
|
|
1078
|
+
await desktopClipboardOwner?.close();
|
|
1079
|
+
await closeDesktopLog();
|
|
1079
1080
|
app.relaunch();
|
|
1080
1081
|
app.exit(0);
|
|
1081
1082
|
return { ok: true };
|
|
@@ -1142,32 +1143,32 @@ function registerIpc() {
|
|
|
1142
1143
|
} catch (error) { return { ok: false, error: mask(error?.message || error) }; }
|
|
1143
1144
|
});
|
|
1144
1145
|
handle('logs:open-folder', () => shell.openPath(logDir));
|
|
1145
|
-
handle('logs:copy-diagnostics', async () => {
|
|
1146
|
+
handle('logs:copy-diagnostics', async () => {
|
|
1146
1147
|
const status = await fetch(`${LOCAL_RUNTIME_URL}api/runtime/status`).then(response => response.json()).catch(error => ({ ok: false, error: mask(error?.message || error) }));
|
|
1147
1148
|
const diagnostics = JSON.stringify({ appVersion: app.getVersion(), runtime: status, logDir }, null, 2);
|
|
1148
|
-
clipboard.writeText(mask(diagnostics));
|
|
1149
|
-
return { ok: true };
|
|
1150
|
-
});
|
|
1151
|
-
handle('clipboard:read-snapshot', operationId => desktopClipboardOwner?.readSnapshot(operationId)
|
|
1152
|
-
|| Promise.reject(new Error('clipboard-helper-unavailable')));
|
|
1153
|
-
handle('clipboard:read-chunk', request => desktopClipboardOwner?.readChunk(request)
|
|
1154
|
-
|| Promise.reject(new Error('clipboard-helper-unavailable')));
|
|
1155
|
-
handle('clipboard:release-read', operationId => desktopClipboardOwner?.releaseRead(operationId)
|
|
1156
|
-
|| Promise.resolve({ ok: true }));
|
|
1157
|
-
handle('clipboard:begin-write', manifest => desktopClipboardOwner?.beginWrite(manifest)
|
|
1158
|
-
|| Promise.reject(new Error('clipboard-helper-unavailable')));
|
|
1159
|
-
handle('clipboard:write-chunk', request => desktopClipboardOwner?.writeChunk(request)
|
|
1160
|
-
|| Promise.reject(new Error('clipboard-helper-unavailable')));
|
|
1161
|
-
handle('clipboard:commit-write', operationId => desktopClipboardOwner?.commitWrite(operationId)
|
|
1162
|
-
|| Promise.reject(new Error('clipboard-helper-unavailable')));
|
|
1163
|
-
handle('clipboard:cancel-write', operationId => desktopClipboardOwner?.cancelWrite(operationId)
|
|
1164
|
-
|| Promise.resolve({ ok: true }));
|
|
1149
|
+
clipboard.writeText(mask(diagnostics));
|
|
1150
|
+
return { ok: true };
|
|
1151
|
+
});
|
|
1152
|
+
handle('clipboard:read-snapshot', operationId => desktopClipboardOwner?.readSnapshot(operationId)
|
|
1153
|
+
|| Promise.reject(new Error('clipboard-helper-unavailable')));
|
|
1154
|
+
handle('clipboard:read-chunk', request => desktopClipboardOwner?.readChunk(request)
|
|
1155
|
+
|| Promise.reject(new Error('clipboard-helper-unavailable')));
|
|
1156
|
+
handle('clipboard:release-read', operationId => desktopClipboardOwner?.releaseRead(operationId)
|
|
1157
|
+
|| Promise.resolve({ ok: true }));
|
|
1158
|
+
handle('clipboard:begin-write', manifest => desktopClipboardOwner?.beginWrite(manifest)
|
|
1159
|
+
|| Promise.reject(new Error('clipboard-helper-unavailable')));
|
|
1160
|
+
handle('clipboard:write-chunk', request => desktopClipboardOwner?.writeChunk(request)
|
|
1161
|
+
|| Promise.reject(new Error('clipboard-helper-unavailable')));
|
|
1162
|
+
handle('clipboard:commit-write', operationId => desktopClipboardOwner?.commitWrite(operationId)
|
|
1163
|
+
|| Promise.reject(new Error('clipboard-helper-unavailable')));
|
|
1164
|
+
handle('clipboard:cancel-write', operationId => desktopClipboardOwner?.cancelWrite(operationId)
|
|
1165
|
+
|| Promise.resolve({ ok: true }));
|
|
1165
1166
|
handle('app:about', async () => {
|
|
1166
1167
|
await dialog.showMessageBox(mainWindow, {
|
|
1167
1168
|
type: 'info',
|
|
1168
1169
|
title: APP_NAME,
|
|
1169
1170
|
message: APP_NAME,
|
|
1170
|
-
detail: `Version ${app.getVersion()}\nOne
|
|
1171
|
+
detail: `Version ${app.getVersion()}\nOne VuvoDesk runtime with Hub and Client roles.`
|
|
1171
1172
|
});
|
|
1172
1173
|
return { ok: true };
|
|
1173
1174
|
});
|
|
@@ -1180,14 +1181,14 @@ function createTray() {
|
|
|
1180
1181
|
const iconPath = appResource('electron', process.platform === 'win32' ? 'tray.ico' : 'tray.png');
|
|
1181
1182
|
tray = new Tray(existsSync(iconPath) ? iconPath : nativeImage.createFromDataURL(FALLBACK_ICON_DATA_URL));
|
|
1182
1183
|
const menu = Menu.buildFromTemplate([
|
|
1183
|
-
{ label: 'Show
|
|
1184
|
+
{ label: 'Show VuvoDesk', click: showWindow },
|
|
1184
1185
|
{ label: 'Restart runtime', click: () => requestRuntimeRestart('tray') },
|
|
1185
1186
|
{ label: 'Check for updates', click: () => { void productUpdates?.check(); } },
|
|
1186
|
-
{ label: 'About
|
|
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.` }); } },
|
|
1187
1188
|
{ label: 'Open logs', click: () => { void shell.openPath(logDir); } },
|
|
1188
1189
|
{ type: 'separator' },
|
|
1189
|
-
{ label: 'Start with Windows', type: 'checkbox', checked: app.getLoginItemSettings().openAtLogin, click: item => app.setLoginItemSettings({ openAtLogin: item.checked, name:
|
|
1190
|
-
{ label: 'Quit
|
|
1190
|
+
{ label: 'Start with Windows', type: 'checkbox', checked: app.getLoginItemSettings().openAtLogin, click: item => app.setLoginItemSettings({ openAtLogin: item.checked, name: DESKTOP_STARTUP_ENTRY_NAME }) },
|
|
1191
|
+
{ label: 'Quit VuvoDesk', click: () => { app.quit(); } }
|
|
1191
1192
|
]);
|
|
1192
1193
|
tray.setContextMenu(menu);
|
|
1193
1194
|
tray.on('click', showWindow);
|
|
@@ -1226,20 +1227,20 @@ if (!app.requestSingleInstanceLock()) {
|
|
|
1226
1227
|
beforeInstall: prepareRuntimeForUpdateInstall,
|
|
1227
1228
|
onInstallError: recoverRuntimeAfterUpdateInstallError
|
|
1228
1229
|
});
|
|
1229
|
-
recurringProductUpdates = createRecurringProductUpdateOwner({
|
|
1230
|
+
recurringProductUpdates = createRecurringProductUpdateOwner({
|
|
1230
1231
|
manager: productUpdates,
|
|
1231
1232
|
canRun: () => !isQuitting && !quitSequenceStarted && !updateInstallSequenceStarted,
|
|
1232
1233
|
initialDelayMs: 15_000,
|
|
1233
1234
|
intervalMs: 30_000,
|
|
1234
|
-
onError: error => log(`recurring desktop update failed: ${error?.message || error}`)
|
|
1235
|
-
});
|
|
1236
|
-
const clipboardHelperPath = resolveClipboardHelperPath();
|
|
1237
|
-
desktopClipboardOwner = createDesktopClipboardOwner({
|
|
1238
|
-
rootDir: join(stateRoot, 'clipboard-staging'),
|
|
1239
|
-
helperPath: clipboardHelperPath
|
|
1240
|
-
});
|
|
1241
|
-
if (!clipboardHelperPath) log('desktop clipboard helper unavailable for this platform');
|
|
1242
|
-
registerIpc();
|
|
1235
|
+
onError: error => log(`recurring desktop update failed: ${error?.message || error}`)
|
|
1236
|
+
});
|
|
1237
|
+
const clipboardHelperPath = resolveClipboardHelperPath();
|
|
1238
|
+
desktopClipboardOwner = createDesktopClipboardOwner({
|
|
1239
|
+
rootDir: join(stateRoot, 'clipboard-staging'),
|
|
1240
|
+
helperPath: clipboardHelperPath
|
|
1241
|
+
});
|
|
1242
|
+
if (!clipboardHelperPath) log('desktop clipboard helper unavailable for this platform');
|
|
1243
|
+
registerIpc();
|
|
1243
1244
|
powerMonitor.on('resume', () => requestDesktopResumeAuthRecovery('system-resume'));
|
|
1244
1245
|
powerMonitor.on('unlock-screen', () => requestDesktopResumeAuthRecovery('screen-unlock'));
|
|
1245
1246
|
if (DESKTOP_BUILD_FLAVOR !== 'e2e') {
|
|
@@ -1277,11 +1278,11 @@ if (!app.requestSingleInstanceLock()) {
|
|
|
1277
1278
|
event.preventDefault();
|
|
1278
1279
|
recurringProductUpdates?.stop();
|
|
1279
1280
|
quitSequenceStarted = true;
|
|
1280
|
-
isQuitting = true;
|
|
1281
|
-
void stopRuntime().then(() => {
|
|
1282
|
-
return desktopClipboardOwner?.close();
|
|
1283
|
-
}).then(() => {
|
|
1284
|
-
return closeDesktopLog();
|
|
1281
|
+
isQuitting = true;
|
|
1282
|
+
void stopRuntime().then(() => {
|
|
1283
|
+
return desktopClipboardOwner?.close();
|
|
1284
|
+
}).then(() => {
|
|
1285
|
+
return closeDesktopLog();
|
|
1285
1286
|
}).then(() => {
|
|
1286
1287
|
app.quit();
|
|
1287
1288
|
}).catch(error => {
|
package/electron/preload.cjs
CHANGED
|
@@ -24,20 +24,20 @@ contextBridge.exposeInMainWorld('liveDesk', {
|
|
|
24
24
|
restart: () => ipcRenderer.invoke('runtime:restart'),
|
|
25
25
|
switchRole: role => ipcRenderer.invoke('runtime:switch-role', role)
|
|
26
26
|
},
|
|
27
|
-
logs: {
|
|
27
|
+
logs: {
|
|
28
28
|
openFolder: () => ipcRenderer.invoke('logs:open-folder'),
|
|
29
|
-
copyDiagnostics: () => ipcRenderer.invoke('logs:copy-diagnostics')
|
|
30
|
-
},
|
|
31
|
-
clipboard: {
|
|
32
|
-
readSnapshot: operationId => ipcRenderer.invoke('clipboard:read-snapshot', operationId),
|
|
33
|
-
readChunk: request => ipcRenderer.invoke('clipboard:read-chunk', request),
|
|
34
|
-
releaseRead: operationId => ipcRenderer.invoke('clipboard:release-read', operationId),
|
|
35
|
-
beginWrite: manifest => ipcRenderer.invoke('clipboard:begin-write', manifest),
|
|
36
|
-
writeChunk: request => ipcRenderer.invoke('clipboard:write-chunk', request),
|
|
37
|
-
commitWrite: operationId => ipcRenderer.invoke('clipboard:commit-write', operationId),
|
|
38
|
-
cancelWrite: operationId => ipcRenderer.invoke('clipboard:cancel-write', operationId)
|
|
39
|
-
},
|
|
40
|
-
updates: {
|
|
29
|
+
copyDiagnostics: () => ipcRenderer.invoke('logs:copy-diagnostics')
|
|
30
|
+
},
|
|
31
|
+
clipboard: {
|
|
32
|
+
readSnapshot: operationId => ipcRenderer.invoke('clipboard:read-snapshot', operationId),
|
|
33
|
+
readChunk: request => ipcRenderer.invoke('clipboard:read-chunk', request),
|
|
34
|
+
releaseRead: operationId => ipcRenderer.invoke('clipboard:release-read', operationId),
|
|
35
|
+
beginWrite: manifest => ipcRenderer.invoke('clipboard:begin-write', manifest),
|
|
36
|
+
writeChunk: request => ipcRenderer.invoke('clipboard:write-chunk', request),
|
|
37
|
+
commitWrite: operationId => ipcRenderer.invoke('clipboard:commit-write', operationId),
|
|
38
|
+
cancelWrite: operationId => ipcRenderer.invoke('clipboard:cancel-write', operationId)
|
|
39
|
+
},
|
|
40
|
+
updates: {
|
|
41
41
|
getStatus: () => ipcRenderer.invoke('updates:get-status'),
|
|
42
42
|
check: () => ipcRenderer.invoke('updates:check'),
|
|
43
43
|
install: () => ipcRenderer.invoke('updates:install'),
|
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
export const DESKTOP_ROLE_TRANSITION_EXIT_CODE = 43;
|
|
2
|
-
|
|
3
|
-
export function resolveDesktopHubTakeoverPending(options = {}) {
|
|
4
|
-
const pending = options.pending === true;
|
|
5
|
-
const signal = String(options.signal || '').trim();
|
|
6
|
-
const exitCode = Number(options.exitCode);
|
|
7
|
-
if (signal || exitCode !== DESKTOP_ROLE_TRANSITION_EXIT_CODE) {
|
|
8
|
-
return pending;
|
|
9
|
-
}
|
|
10
|
-
return String(options.expectedRole || '').trim().toLowerCase() === 'hub';
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function desktopHubTakeoverPublicationConfirmed(status = {}) {
|
|
14
|
-
return String(status.role || '').trim().toLowerCase() === 'hub'
|
|
15
|
-
&& status.hostTargetActive === true
|
|
16
|
-
&& String(status.hostTargetLease?.state || '').trim().toLowerCase() === 'active';
|
|
17
|
-
}
|
|
1
|
+
export const DESKTOP_ROLE_TRANSITION_EXIT_CODE = 43;
|
|
2
|
+
|
|
3
|
+
export function resolveDesktopHubTakeoverPending(options = {}) {
|
|
4
|
+
const pending = options.pending === true;
|
|
5
|
+
const signal = String(options.signal || '').trim();
|
|
6
|
+
const exitCode = Number(options.exitCode);
|
|
7
|
+
if (signal || exitCode !== DESKTOP_ROLE_TRANSITION_EXIT_CODE) {
|
|
8
|
+
return pending;
|
|
9
|
+
}
|
|
10
|
+
return String(options.expectedRole || '').trim().toLowerCase() === 'hub';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function desktopHubTakeoverPublicationConfirmed(status = {}) {
|
|
14
|
+
return String(status.role || '').trim().toLowerCase() === 'hub'
|
|
15
|
+
&& status.hostTargetActive === true
|
|
16
|
+
&& String(status.hostTargetLease?.state || '').trim().toLowerCase() === 'active';
|
|
17
|
+
}
|