livedesk 0.1.697 → 0.1.701
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 +21 -3
- package/bin/livedesk.js +16 -8
- package/client/package.json +5 -5
- package/diagnostics/vuvodesk-linux-desktop.mjs +188 -0
- package/electron/entitlements.mac.plist +12 -12
- package/electron/recurring-update-owner.mjs +2 -2
- package/electron/update-manager.mjs +59 -16
- package/hub/package.json +1 -1
- package/hub/src/plan-device-access.test.mjs +8 -8
- package/hub/src/remote-hub.js +12 -3
- package/hub/src/server.js +1 -1
- package/package.json +7 -7
- package/web/dist/app.html +1 -1
- package/web/dist/app.webmanifest +1 -1
- package/web/dist/assets/{AgentSettingsTab-DRWDCG0o.js → AgentSettingsTab-D0oebRk-.js} +1 -1
- package/web/dist/assets/{AgentsPage-BnF3p9G1.js → AgentsPage-BBj64SpA.js} +1 -1
- package/web/dist/assets/{CaptureGallery-8lr9Ct1i.js → CaptureGallery-DF6gZHy0.js} +1 -1
- package/web/dist/assets/DesktopUpdatePanel-BaRZzRao.js +1 -0
- package/web/dist/assets/HubApp-C-E5cpCr.css +1 -0
- package/web/dist/assets/HubApp-DJsiVqWP.js +182 -0
- package/web/dist/assets/{MonitorStackIcon-led0pnag.js → MonitorStackIcon-BD7Yn-Z9.js} +5 -5
- package/web/dist/assets/{SettingsPage-k202bRU1.js → SettingsPage-CXmk161K.js} +1 -1
- package/web/dist/assets/SettingsTabs-C_xNvq7v.js +1 -0
- package/web/dist/assets/{ShareFilesPage-DRW1qS1Z.js → ShareFilesPage-Dx5yYsbk.js} +1 -1
- package/web/dist/assets/{SupportPage-DXkBQh_W.js → SupportPage-DAsms8Kr.js} +1 -1
- package/web/dist/assets/{VuvoDeskApp-BHcF72r3.js → VuvoDeskApp-9sUnuvyS.js} +1 -1
- package/web/dist/assets/{app-DcrmhmnT.js → app-CUM2wTSb.js} +2 -2
- package/web/dist/assets/{client-D-D35ghV.js → client-DV0v4wud.js} +1 -1
- package/web/dist/assets/frame-lifecycle-D-BxeTQV.js +1 -0
- package/web/dist/assets/{icons-ChMazCZR.js → icons-BwhnWKHR.js} +1 -1
- package/web/dist/assets/main-BbK4NG31.js +2 -0
- package/web/dist/assets/{pwa-startup-BiuYh5eN.js → pwa-startup-C8kDd3Vd.js} +1 -1
- package/web/dist/assets/{react-CnkEM7Rb.js → react-Dm67yaJf.js} +1 -1
- package/web/dist/index.html +6 -6
- package/web/dist/sw.js +2 -2
- package/web/dist/vuvodesk-build-evidence.json +58 -58
- package/web/dist/assets/DesktopUpdatePanel-DopP2EeD.js +0 -1
- package/web/dist/assets/HubApp-CqNxUVJD.js +0 -182
- package/web/dist/assets/HubApp-DdHHVOM0.css +0 -1
- package/web/dist/assets/SettingsTabs-v57M-CCZ.js +0 -1
- package/web/dist/assets/frame-lifecycle-D91nchvm.js +0 -1
- package/web/dist/assets/main-BXBJt96-.js +0 -2
package/README.md
CHANGED
|
@@ -44,7 +44,25 @@ npx -y --prefer-online vuvodesk@latest --force-role hub
|
|
|
44
44
|
npx -y --prefer-online vuvodesk@latest --force-role client
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
-
##
|
|
47
|
+
## Linux desktop diagnosis
|
|
48
|
+
|
|
49
|
+
The source launcher provides a read-only preparation diagnostic:
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
node packages/vuvodesk/bin/livedesk.js diagnose linux-desktop
|
|
53
|
+
node packages/vuvodesk/bin/livedesk.js diagnose linux-desktop --pid <native-agent-pid>
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Run from a graphical terminal, or specify the existing same-user
|
|
57
|
+
`livedesk-client-fast` PID when using SSH. It reports Xorg/Wayland session
|
|
58
|
+
evidence and the system's ScreenCast/RemoteDesktop portal properties. It does
|
|
59
|
+
not start capture, send input, show permission prompts, restart the runtime or
|
|
60
|
+
change OS settings. Portal presence does not mean VuvoDesk Wayland support is
|
|
61
|
+
implemented. The current capture/input path is X11; Wayland integration remains
|
|
62
|
+
in preparation. This command must be included in a future published launcher
|
|
63
|
+
before it is available through `vuvodesk@latest`.
|
|
64
|
+
|
|
65
|
+
## Hub
|
|
48
66
|
|
|
49
67
|
```powershell
|
|
50
68
|
npx -y --prefer-online vuvodesk@latest hub
|
|
@@ -154,8 +172,8 @@ device, Hub URL, timeout, and report-path options.
|
|
|
154
172
|
|
|
155
173
|
## Plans
|
|
156
174
|
|
|
157
|
-
- Free:
|
|
158
|
-
- Plus LTD launch:
|
|
175
|
+
- Free: 3 personal devices with a standard wall banner ad.
|
|
176
|
+
- Plus LTD launch: 20 devices for personal and commercial use, no ads, USD 79 one-time.
|
|
159
177
|
- Pro LTD launch: 50 devices for schools, academies, and commercial use, USD 199 one-time.
|
|
160
178
|
- Team: unlimited devices, up to 5 people, and commercial use.
|
|
161
179
|
|
package/bin/livedesk.js
CHANGED
|
@@ -347,7 +347,8 @@ Usage:
|
|
|
347
347
|
npx -y --prefer-online vuvodesk@latest --force-role hub
|
|
348
348
|
npx -y --prefer-online vuvodesk@latest --force-role client
|
|
349
349
|
npx -y --prefer-online vuvodesk@latest diagnose mac-physical --confirm-active-motion
|
|
350
|
-
npx -y --prefer-online vuvodesk@latest diagnose transport-physical
|
|
350
|
+
npx -y --prefer-online vuvodesk@latest diagnose transport-physical
|
|
351
|
+
npx -y --prefer-online vuvodesk@latest diagnose linux-desktop
|
|
351
352
|
|
|
352
353
|
Commands:
|
|
353
354
|
No command Resolve the signed-in device role and start the matching runtime.
|
|
@@ -355,8 +356,10 @@ Commands:
|
|
|
355
356
|
client Legacy alias for --force-role client.
|
|
356
357
|
diagnose mac-physical
|
|
357
358
|
Run the isolated two-display macOS capture/control physical gate.
|
|
358
|
-
diagnose transport-physical
|
|
359
|
-
Prove Direct, UDP P2P, encrypted relay, and Direct recovery on a current stream.
|
|
359
|
+
diagnose transport-physical
|
|
360
|
+
Prove Direct, UDP P2P, encrypted relay, and Direct recovery on a current stream.
|
|
361
|
+
diagnose linux-desktop
|
|
362
|
+
Inspect Linux session and Wayland portal availability without starting capture.
|
|
360
363
|
|
|
361
364
|
Hub options:
|
|
362
365
|
--no-open Do not open the browser automatically.
|
|
@@ -419,7 +422,7 @@ async function runDiagnosticCommand(args) {
|
|
|
419
422
|
printHelp();
|
|
420
423
|
return;
|
|
421
424
|
}
|
|
422
|
-
if (name === 'transport-physical') {
|
|
425
|
+
if (name === 'transport-physical') {
|
|
423
426
|
const diagnosticEntry = resolve(
|
|
424
427
|
packageRoot,
|
|
425
428
|
'diagnostics',
|
|
@@ -430,10 +433,15 @@ async function runDiagnosticCommand(args) {
|
|
|
430
433
|
}
|
|
431
434
|
const diagnostic = await import(pathToFileURL(diagnosticEntry).href);
|
|
432
435
|
await diagnostic.runTransportPhysicalDiagnosticCli(args.slice(1));
|
|
433
|
-
return;
|
|
434
|
-
}
|
|
435
|
-
if (name
|
|
436
|
-
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
if (name === 'linux-desktop') {
|
|
439
|
+
const diagnostic = await import('../diagnostics/vuvodesk-linux-desktop.mjs');
|
|
440
|
+
await diagnostic.runLinuxDesktopDiagnosticCli(args.slice(1));
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
if (name !== 'mac-physical') {
|
|
444
|
+
throw new Error(`Unknown VuvoDesk diagnostic: ${name}. Supported diagnostics: mac-physical, transport-physical, linux-desktop.`);
|
|
437
445
|
}
|
|
438
446
|
if (args.slice(1).some(argument => ['--help', '-h', 'help'].includes(String(argument).toLowerCase()))) {
|
|
439
447
|
printMacPhysicalDiagnosticHelp();
|
package/client/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@livedesk/client",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.277",
|
|
4
4
|
"description": "VuvoDesk local remote client",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -42,10 +42,10 @@
|
|
|
42
42
|
"ws": "^8.18.3"
|
|
43
43
|
},
|
|
44
44
|
"optionalDependencies": {
|
|
45
|
-
"@livedesk/fast-linux-x64": "0.1.
|
|
46
|
-
"@livedesk/fast-osx-arm64": "0.1.
|
|
47
|
-
"@livedesk/fast-osx-x64": "0.1.
|
|
48
|
-
"@livedesk/fast-win-x64": "0.1.
|
|
45
|
+
"@livedesk/fast-linux-x64": "0.1.472",
|
|
46
|
+
"@livedesk/fast-osx-arm64": "0.1.472",
|
|
47
|
+
"@livedesk/fast-osx-x64": "0.1.472",
|
|
48
|
+
"@livedesk/fast-win-x64": "0.1.472"
|
|
49
49
|
},
|
|
50
50
|
"publishConfig": {
|
|
51
51
|
"access": "public"
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { execFile } from 'node:child_process';
|
|
3
|
+
import { open, readlink, stat } from 'node:fs/promises';
|
|
4
|
+
import { basename } from 'node:path';
|
|
5
|
+
import { pathToFileURL } from 'node:url';
|
|
6
|
+
import { promisify } from 'node:util';
|
|
7
|
+
|
|
8
|
+
const exec = promisify(execFile);
|
|
9
|
+
const ENV_KEYS = ['DISPLAY', 'WAYLAND_DISPLAY', 'XDG_SESSION_TYPE', 'XDG_CURRENT_DESKTOP',
|
|
10
|
+
'XDG_SESSION_DESKTOP', 'XDG_RUNTIME_DIR', 'DBUS_SESSION_BUS_ADDRESS'];
|
|
11
|
+
const PORTAL_LIMIT = 16384;
|
|
12
|
+
|
|
13
|
+
function sessionEnvironment(env) {
|
|
14
|
+
return Object.fromEntries(ENV_KEYS.filter(key => typeof env[key] === 'string'
|
|
15
|
+
&& env[key].length <= 4096).map(key => [key, env[key]]));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function readBoundedText(path, limit = 8192) {
|
|
19
|
+
const file = await open(path, 'r');
|
|
20
|
+
try {
|
|
21
|
+
const data = Buffer.alloc(limit + 1);
|
|
22
|
+
const { bytesRead } = await file.read(data, 0, data.length, 0);
|
|
23
|
+
if (bytesRead > limit) throw new Error('linux-desktop-file-limit');
|
|
24
|
+
return data.toString('utf8', 0, bytesRead);
|
|
25
|
+
} finally { await file.close(); }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function classifyLinuxDesktopSession(env = {}) {
|
|
29
|
+
const explicit = String(env.XDG_SESSION_TYPE || '').trim().toLowerCase();
|
|
30
|
+
const type = explicit === 'x11' || explicit === 'wayland' ? explicit
|
|
31
|
+
: env.WAYLAND_DISPLAY ? 'wayland'
|
|
32
|
+
: env.DISPLAY ? 'x11' : explicit === 'tty' ? 'headless' : 'unknown';
|
|
33
|
+
const source = explicit === 'x11' || explicit === 'wayland' ? 'xdg-session-type'
|
|
34
|
+
: env.WAYLAND_DISPLAY ? 'wayland-display'
|
|
35
|
+
: env.DISPLAY ? 'display-only' : 'no-graphical-evidence';
|
|
36
|
+
const rawDesktop = String(env.XDG_CURRENT_DESKTOP || env.XDG_SESSION_DESKTOP || '').toLowerCase();
|
|
37
|
+
const desktop = ['gnome', 'kde', 'lxqt', 'xfce', 'cinnamon', 'sway', 'hyprland']
|
|
38
|
+
.find(name => rawDesktop.split(/[:;\s]+/).includes(name)) || 'unknown';
|
|
39
|
+
return { type, source, desktop, xDisplayPresent: Boolean(env.DISPLAY),
|
|
40
|
+
waylandDisplayPresent: Boolean(env.WAYLAND_DISPLAY) };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function processStartTime(text) {
|
|
44
|
+
const fields = text.slice(text.lastIndexOf(')') + 2).trim().split(/\s+/);
|
|
45
|
+
if (!/^\d+$/.test(fields[19] || '')) throw new Error('linux-desktop-invalid-process-stat');
|
|
46
|
+
return fields[19];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function readNativeAgentContext(pid, {
|
|
50
|
+
uid = process.getuid?.(), readText = readBoundedText, readLink = readlink, statPath = stat
|
|
51
|
+
} = {}) {
|
|
52
|
+
if (!Number.isSafeInteger(pid) || pid < 2) throw new Error('linux-desktop-invalid-pid');
|
|
53
|
+
const root = `/proc/${pid}`;
|
|
54
|
+
const first = processStartTime(await readText(`${root}/stat`, 4096));
|
|
55
|
+
if ((await statPath(root)).uid !== uid) throw new Error('linux-desktop-requires-same-user-agent');
|
|
56
|
+
const executable = await readLink(`${root}/exe`);
|
|
57
|
+
if (basename(executable) !== 'livedesk-client-fast') throw new Error('linux-desktop-requires-native-agent');
|
|
58
|
+
const raw = await readText(`${root}/environ`, 131072);
|
|
59
|
+
const all = Object.fromEntries(raw.split('\0').filter(x => x.includes('=')).map(x => {
|
|
60
|
+
const equals = x.indexOf('=');
|
|
61
|
+
return [x.slice(0, equals), x.slice(equals + 1)];
|
|
62
|
+
}));
|
|
63
|
+
const env = sessionEnvironment(all);
|
|
64
|
+
const assertCurrent = async () => {
|
|
65
|
+
if (processStartTime(await readText(`${root}/stat`, 4096)) !== first
|
|
66
|
+
|| await readLink(`${root}/exe`) !== executable) throw new Error('linux-desktop-agent-changed');
|
|
67
|
+
};
|
|
68
|
+
await assertCurrent();
|
|
69
|
+
return { env, binding: { pid, startTime: first }, assertCurrent };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function parsePortalProperties(text, kind) {
|
|
73
|
+
if (typeof text !== 'string' || text.length > PORTAL_LIMIT) throw new Error('portal-properties-invalid');
|
|
74
|
+
const integer = key => {
|
|
75
|
+
const match = text.match(new RegExp(`['"]${key}['"]\\s*:\\s*<uint32\\s+(\\d+)>`));
|
|
76
|
+
if (!match) return null;
|
|
77
|
+
const value = Number(match[1]);
|
|
78
|
+
return Number.isSafeInteger(value) && value >= 0 && value <= 0xffffffff ? value : null;
|
|
79
|
+
};
|
|
80
|
+
const version = integer('version');
|
|
81
|
+
const sourceTypes = kind === 'ScreenCast' ? integer('AvailableSourceTypes') : null;
|
|
82
|
+
const deviceTypes = kind === 'RemoteDesktop' ? integer('AvailableDeviceTypes') : null;
|
|
83
|
+
if (!version || (kind === 'ScreenCast' ? sourceTypes === null : deviceTypes === null)) {
|
|
84
|
+
throw new Error('portal-properties-invalid');
|
|
85
|
+
}
|
|
86
|
+
return { state: 'available', version, sourceTypes, deviceTypes,
|
|
87
|
+
cursorModes: kind === 'ScreenCast' ? integer('AvailableCursorModes') : null };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function missingPortal(state) {
|
|
91
|
+
return { state, version: null, sourceTypes: null, deviceTypes: null, cursorModes: null };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function inspectPortal(kind, env, run) {
|
|
95
|
+
if (!env.DBUS_SESSION_BUS_ADDRESS || !env.DBUS_SESSION_BUS_ADDRESS.startsWith('unix:')) {
|
|
96
|
+
return missingPortal('session-bus-unavailable');
|
|
97
|
+
}
|
|
98
|
+
try {
|
|
99
|
+
const result = await run('/usr/bin/gdbus', ['call', '--session', '--dest',
|
|
100
|
+
'org.freedesktop.portal.Desktop', '--object-path', '/org/freedesktop/portal/desktop',
|
|
101
|
+
'--method', 'org.freedesktop.DBus.Properties.GetAll', `org.freedesktop.portal.${kind}`], {
|
|
102
|
+
env: { ...sessionEnvironment(env), PATH: '/usr/bin:/bin', LANG: 'C.UTF-8' },
|
|
103
|
+
encoding: 'utf8', timeout: 1500, maxBuffer: PORTAL_LIMIT,
|
|
104
|
+
killSignal: 'SIGKILL', shell: false, windowsHide: true
|
|
105
|
+
});
|
|
106
|
+
return parsePortalProperties(result.stdout, kind);
|
|
107
|
+
} catch (error) {
|
|
108
|
+
const state = error?.code === 'ENOENT' ? 'probe-tool-unavailable'
|
|
109
|
+
: error?.killed || error?.code === 'ETIMEDOUT' ? 'timeout'
|
|
110
|
+
: error?.message === 'portal-properties-invalid' ? 'properties-unavailable'
|
|
111
|
+
: 'query-failed';
|
|
112
|
+
return missingPortal(state);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function distribution(text) {
|
|
117
|
+
const field = key => {
|
|
118
|
+
const value = String(text).match(new RegExp(`^${key}=(.*)$`, 'm'))?.[1]?.trim().replace(/^"|"$/g, '');
|
|
119
|
+
return value && /^[a-zA-Z0-9._ -]{1,80}$/.test(value) ? value : null;
|
|
120
|
+
};
|
|
121
|
+
return { id: field('ID'), versionId: field('VERSION_ID') };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export async function inspectLinuxDesktop({ platform = process.platform, env = process.env,
|
|
125
|
+
readText = readBoundedText, run = exec, assertCurrent = async () => {}, binding = null
|
|
126
|
+
} = {}) {
|
|
127
|
+
if (platform !== 'linux') return { schemaVersion: 1, state: 'not-linux' };
|
|
128
|
+
// Snapshot only session fields. Never emit addresses, paths, environment dumps,
|
|
129
|
+
// portal request/restore tokens or raw command errors.
|
|
130
|
+
const context = sessionEnvironment(env);
|
|
131
|
+
const session = classifyLinuxDesktopSession(context);
|
|
132
|
+
let os = { id: null, versionId: null };
|
|
133
|
+
try { os = distribution(await readText('/etc/os-release', 8192)); } catch { /* unknown */ }
|
|
134
|
+
await assertCurrent();
|
|
135
|
+
const screenCast = await inspectPortal('ScreenCast', context, run);
|
|
136
|
+
await assertCurrent();
|
|
137
|
+
const remoteDesktop = await inspectPortal('RemoteDesktop', context, run);
|
|
138
|
+
await assertCurrent();
|
|
139
|
+
return {
|
|
140
|
+
schemaVersion: 1, state: 'inspected', distribution: os, session, binding,
|
|
141
|
+
portals: { screenCast, remoteDesktop,
|
|
142
|
+
monitorAdvertised: screenCast.sourceTypes === null ? null : (screenCast.sourceTypes & 1) !== 0,
|
|
143
|
+
keyboardPointerAdvertised: remoteDesktop.deviceTypes === null ? null : (remoteDesktop.deviceTypes & 3) === 3 },
|
|
144
|
+
vuvodesk: {
|
|
145
|
+
requiredBackend: session.type === 'wayland' ? 'wayland-portal' : session.type === 'x11' ? 'x11' : 'unknown',
|
|
146
|
+
waylandBackendImplemented: false, permissionGranted: null, captureTested: false, inputTested: false,
|
|
147
|
+
note: session.type === 'wayland'
|
|
148
|
+
? 'Wayland capture and input are not implemented yet. Portal availability is not permission or working video.'
|
|
149
|
+
: 'Read-only environment inspection. Capture, input and performance are not tested.'
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function parseLinuxDesktopArgs(args) {
|
|
155
|
+
let pid = null;
|
|
156
|
+
let help = false;
|
|
157
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
158
|
+
const arg = args[index];
|
|
159
|
+
if (arg === '--help' || arg === '-h') help = true;
|
|
160
|
+
else if (arg === '--pid' && pid === null && /^[1-9]\d*$/.test(args[index + 1] || '')) {
|
|
161
|
+
pid = Number(args[++index]);
|
|
162
|
+
if (!Number.isSafeInteger(pid) || pid < 2) throw new Error('linux-desktop-invalid-pid');
|
|
163
|
+
} else throw new Error('linux-desktop-invalid-arguments');
|
|
164
|
+
}
|
|
165
|
+
return { pid, help };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export async function runLinuxDesktopDiagnosticCli(args = []) {
|
|
169
|
+
const { pid, help } = parseLinuxDesktopArgs(args);
|
|
170
|
+
if (help) {
|
|
171
|
+
console.log(`VuvoDesk Linux desktop / Wayland preparation diagnostic
|
|
172
|
+
Usage: vuvodesk diagnose linux-desktop [--pid <native-agent-pid>]
|
|
173
|
+
Run from the graphical terminal, or use --pid over SSH for an existing same-user
|
|
174
|
+
livedesk-client-fast process. Reads only session and portal properties as JSON.
|
|
175
|
+
No capture, input, approval prompt, OS setting change, runtime restart or files.
|
|
176
|
+
Portal availability does not establish working Wayland support or permission.`);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const context = pid && process.platform === 'linux' ? await readNativeAgentContext(pid) : {};
|
|
180
|
+
console.log(JSON.stringify(await inspectLinuxDesktop(context), null, 2));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
184
|
+
runLinuxDesktopDiagnosticCli(process.argv.slice(2)).catch(() => {
|
|
185
|
+
console.error('Linux desktop diagnosis failed. Check arguments and the current same-user native Agent PID.');
|
|
186
|
+
process.exitCode = 1;
|
|
187
|
+
});
|
|
188
|
+
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
-
<plist version="1.0">
|
|
4
|
-
<dict>
|
|
5
|
-
<key>com.apple.security.cs.allow-jit</key>
|
|
6
|
-
<true/>
|
|
7
|
-
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
|
8
|
-
<true/>
|
|
9
|
-
<key>com.apple.security.device.audio-input</key>
|
|
10
|
-
<true/>
|
|
11
|
-
</dict>
|
|
12
|
-
</plist>
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
+
<plist version="1.0">
|
|
4
|
+
<dict>
|
|
5
|
+
<key>com.apple.security.cs.allow-jit</key>
|
|
6
|
+
<true/>
|
|
7
|
+
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
|
8
|
+
<true/>
|
|
9
|
+
<key>com.apple.security.device.audio-input</key>
|
|
10
|
+
<true/>
|
|
11
|
+
</dict>
|
|
12
|
+
</plist>
|
|
@@ -103,12 +103,12 @@ export function createRecurringProductUpdateOwner(options = {}) {
|
|
|
103
103
|
if (running) return running;
|
|
104
104
|
|
|
105
105
|
const operation = (async () => {
|
|
106
|
-
const checked = await manager.check();
|
|
106
|
+
const checked = await manager.check({ automatic: true });
|
|
107
107
|
if (!stopped
|
|
108
108
|
&& ownerGeneration === generation
|
|
109
109
|
&& canRun()
|
|
110
110
|
&& checked?.downloaded === true) {
|
|
111
|
-
return manager.install();
|
|
111
|
+
return manager.install({ automatic: true });
|
|
112
112
|
}
|
|
113
113
|
return checked;
|
|
114
114
|
})();
|
|
@@ -130,6 +130,8 @@ export function createProductUpdateManager({
|
|
|
130
130
|
let activeOwner = null;
|
|
131
131
|
let availableDownload = null;
|
|
132
132
|
let installRuntimePrepared = false;
|
|
133
|
+
let installTargetVersion = '';
|
|
134
|
+
let automaticInstallFailure = null;
|
|
133
135
|
let installRecoveryPromise = null;
|
|
134
136
|
const checkDeadline = normalizeDeadline(checkDeadlineMs, DEFAULT_CHECK_DEADLINE_MS);
|
|
135
137
|
const downloadStallTimeout = normalizeDeadline(downloadStallTimeoutMs, DEFAULT_DOWNLOAD_STALL_TIMEOUT_MS);
|
|
@@ -154,6 +156,27 @@ export function createProductUpdateManager({
|
|
|
154
156
|
onStatus({ ...status });
|
|
155
157
|
};
|
|
156
158
|
const snapshot = () => ({ ...status, packaged: Boolean(app.isPackaged) });
|
|
159
|
+
const rememberInstallerFailure = error => {
|
|
160
|
+
const message = String(error?.message || error);
|
|
161
|
+
const failure = {
|
|
162
|
+
state: 'error',
|
|
163
|
+
error: message,
|
|
164
|
+
errorCode: platform === 'linux' && /Command pkexec exited with code 127\b/.test(message)
|
|
165
|
+
? 'desktop-update-authorization-required'
|
|
166
|
+
: 'desktop-update-installer-launch-failed'
|
|
167
|
+
};
|
|
168
|
+
if (installTargetVersion) {
|
|
169
|
+
automaticInstallFailure = { version: installTargetVersion, failure };
|
|
170
|
+
updater.autoInstallOnAppQuit = false;
|
|
171
|
+
}
|
|
172
|
+
return failure;
|
|
173
|
+
};
|
|
174
|
+
const blockedAutomaticInstall = () => {
|
|
175
|
+
if (!automaticInstallFailure
|
|
176
|
+
|| automaticInstallFailure.version !== status.availableVersion) return null;
|
|
177
|
+
publish(automaticInstallFailure.failure);
|
|
178
|
+
return snapshot();
|
|
179
|
+
};
|
|
157
180
|
const releaseAvailableDownload = record => {
|
|
158
181
|
const owned = record || availableDownload;
|
|
159
182
|
if (!owned) return;
|
|
@@ -417,7 +440,7 @@ export function createProductUpdateManager({
|
|
|
417
440
|
const connectUpdater = candidate => {
|
|
418
441
|
if (!candidate || connectedUpdaters.has(candidate)) return;
|
|
419
442
|
candidate.autoDownload = false;
|
|
420
|
-
candidate.autoInstallOnAppQuit =
|
|
443
|
+
candidate.autoInstallOnAppQuit = !automaticInstallFailure;
|
|
421
444
|
if (feed.url) {
|
|
422
445
|
// Enable multipart reads only for the exact supported electron-updater
|
|
423
446
|
// implementation patched with VuvoDesk's 48-range/32 MiB batch owner.
|
|
@@ -521,11 +544,7 @@ export function createProductUpdateManager({
|
|
|
521
544
|
const owner = activeOwner?.acceptsEvents === true ? activeOwner : null;
|
|
522
545
|
if (!owner) {
|
|
523
546
|
if (!installRuntimePrepared) return;
|
|
524
|
-
publish(
|
|
525
|
-
state: 'error',
|
|
526
|
-
error: String(error?.message || error),
|
|
527
|
-
errorCode: 'desktop-update-installer-launch-failed'
|
|
528
|
-
});
|
|
547
|
+
publish(rememberInstallerFailure(error));
|
|
529
548
|
void recoverRuntime();
|
|
530
549
|
return;
|
|
531
550
|
}
|
|
@@ -634,6 +653,7 @@ export function createProductUpdateManager({
|
|
|
634
653
|
});
|
|
635
654
|
}
|
|
636
655
|
if (owner.outcome === 'available') {
|
|
656
|
+
updater.autoInstallOnAppQuit = automaticInstallFailure?.version !== owner.expectedVersion;
|
|
637
657
|
availableDownload = {
|
|
638
658
|
generation: owner.generation,
|
|
639
659
|
version: String(owner.expectedVersion || result?.updateInfo?.version || status.availableVersion || ''),
|
|
@@ -791,10 +811,17 @@ export function createProductUpdateManager({
|
|
|
791
811
|
}
|
|
792
812
|
};
|
|
793
813
|
|
|
794
|
-
const checkAndDownload = async () => {
|
|
814
|
+
const checkAndDownload = async ({ automatic = false } = {}) => {
|
|
795
815
|
if (cyclePromise) return cyclePromise;
|
|
796
816
|
const operation = (async () => {
|
|
797
|
-
const checked = await checkForUpdates();
|
|
817
|
+
const checked = await checkForUpdates();
|
|
818
|
+
// Continue discovering newer versions, but a failed installer must not
|
|
819
|
+
// repeatedly stop a healthy Client or ask for authorization on each poll.
|
|
820
|
+
if (automatic && !checked.reopenRequired
|
|
821
|
+
&& (checked.state === 'available' || checked.state === 'downloaded')) {
|
|
822
|
+
const blocked = blockedAutomaticInstall();
|
|
823
|
+
if (blocked) return blocked;
|
|
824
|
+
}
|
|
798
825
|
if (checked.state === 'available') return downloadUpdate();
|
|
799
826
|
return checked;
|
|
800
827
|
})();
|
|
@@ -810,15 +837,15 @@ export function createProductUpdateManager({
|
|
|
810
837
|
getStatus() {
|
|
811
838
|
return snapshot();
|
|
812
839
|
},
|
|
813
|
-
async check() {
|
|
814
|
-
return checkAndDownload();
|
|
840
|
+
async check(options) {
|
|
841
|
+
return checkAndDownload(options);
|
|
815
842
|
},
|
|
816
843
|
async checkAtStartup() {
|
|
817
844
|
if (startupPromise) return startupPromise;
|
|
818
845
|
const operation = (async () => {
|
|
819
846
|
if (!app.isPackaged || !feed.url) return checkForUpdates();
|
|
820
847
|
publish({ startupCheck: 'checking' });
|
|
821
|
-
const background = checkAndDownload().finally(() => {
|
|
848
|
+
const background = checkAndDownload({ automatic: true }).finally(() => {
|
|
822
849
|
publish({ startupCheck: 'complete' });
|
|
823
850
|
});
|
|
824
851
|
let timeoutId;
|
|
@@ -841,7 +868,7 @@ export function createProductUpdateManager({
|
|
|
841
868
|
if (startupPromise === operation) startupPromise = null;
|
|
842
869
|
}
|
|
843
870
|
},
|
|
844
|
-
async install() {
|
|
871
|
+
async install({ automatic = false } = {}) {
|
|
845
872
|
if (!app.isPackaged) return { ...snapshot(), state: 'development', error: 'desktop-updates-require-packaged-app' };
|
|
846
873
|
if (!feed.url) return unavailableStatus();
|
|
847
874
|
if (status.reopenRequired) return snapshot();
|
|
@@ -850,6 +877,10 @@ export function createProductUpdateManager({
|
|
|
850
877
|
return snapshot();
|
|
851
878
|
}
|
|
852
879
|
if (installPromise) return installPromise;
|
|
880
|
+
if (automatic) {
|
|
881
|
+
const blocked = blockedAutomaticInstall();
|
|
882
|
+
if (blocked) return blocked;
|
|
883
|
+
}
|
|
853
884
|
installOperationPending = true;
|
|
854
885
|
const operation = (async () => {
|
|
855
886
|
try {
|
|
@@ -867,9 +898,20 @@ export function createProductUpdateManager({
|
|
|
867
898
|
if (!status.downloaded) {
|
|
868
899
|
const downloadResult = availableDownload
|
|
869
900
|
? await downloadUpdate()
|
|
870
|
-
: await checkAndDownload();
|
|
871
|
-
if (!downloadResult.downloaded) return downloadResult;
|
|
872
|
-
}
|
|
901
|
+
: await checkAndDownload({ automatic });
|
|
902
|
+
if (!downloadResult.downloaded) return downloadResult;
|
|
903
|
+
}
|
|
904
|
+
if (automatic) {
|
|
905
|
+
const blocked = blockedAutomaticInstall();
|
|
906
|
+
if (blocked) return blocked;
|
|
907
|
+
}
|
|
908
|
+
installTargetVersion = status.downloadedVersion;
|
|
909
|
+
// Only an explicit Install action retries this failed version. A
|
|
910
|
+
// manual Check/download alone never grants another automatic drain.
|
|
911
|
+
if (!automatic) {
|
|
912
|
+
automaticInstallFailure = null;
|
|
913
|
+
updater.autoInstallOnAppQuit = true;
|
|
914
|
+
}
|
|
873
915
|
// The updater may launch its installer synchronously from
|
|
874
916
|
// quitAndInstall. Prove the exact runtime descendant tree is already
|
|
875
917
|
// zero before allowing that replacement process to exist.
|
|
@@ -885,6 +927,7 @@ export function createProductUpdateManager({
|
|
|
885
927
|
if (installRecoveryPromise) await installRecoveryPromise;
|
|
886
928
|
return snapshot();
|
|
887
929
|
} catch (error) {
|
|
930
|
+
const failure = rememberInstallerFailure(error);
|
|
888
931
|
await recoverPreparedRuntime(error);
|
|
889
932
|
if (!status.reopenRequired) {
|
|
890
933
|
const message = String(error?.message || error);
|
|
@@ -894,7 +937,7 @@ export function createProductUpdateManager({
|
|
|
894
937
|
error: message,
|
|
895
938
|
errorCode: preinstallRecoveryFailed
|
|
896
939
|
? 'desktop-update-runtime-recovery-failed'
|
|
897
|
-
:
|
|
940
|
+
: failure.errorCode,
|
|
898
941
|
reopenRequired: preinstallRecoveryFailed
|
|
899
942
|
});
|
|
900
943
|
}
|
package/hub/package.json
CHANGED
|
@@ -15,15 +15,15 @@ const clients = [6, 3, 1, 5, 2, 4].map(slotNumber => ({
|
|
|
15
15
|
}));
|
|
16
16
|
|
|
17
17
|
test('Hub and browser choose the same stable Free, Plus, Pro, and Team device owners', () => {
|
|
18
|
-
for (const limit of [
|
|
18
|
+
for (const limit of [3, 20, 50, Number.POSITIVE_INFINITY]) {
|
|
19
19
|
assert.deepEqual(
|
|
20
20
|
stableHubPlanAllowedDeviceIds(clients, limit),
|
|
21
21
|
stableBrowserPlanAllowedDeviceIds(clients, limit)
|
|
22
22
|
);
|
|
23
23
|
}
|
|
24
24
|
assert.deepEqual(
|
|
25
|
-
stableHubPlanAllowedDeviceIds(clients,
|
|
26
|
-
['client-1', 'client-2', 'client-3'
|
|
25
|
+
stableHubPlanAllowedDeviceIds(clients, 3),
|
|
26
|
+
['client-1', 'client-2', 'client-3']
|
|
27
27
|
);
|
|
28
28
|
});
|
|
29
29
|
|
|
@@ -66,10 +66,10 @@ test('finite plans reject the exact excess Client instead of counting request wi
|
|
|
66
66
|
|
|
67
67
|
test('a Team to Free downgrade changes one bounded snapshot and preserves stable owners', () => {
|
|
68
68
|
const unlimited = createPlanDeviceAccessSnapshot(clients, Number.POSITIVE_INFINITY, 1);
|
|
69
|
-
const downgraded = createPlanDeviceAccessSnapshot(clients,
|
|
69
|
+
const downgraded = createPlanDeviceAccessSnapshot(clients, 3, 2);
|
|
70
70
|
assert.equal(planDeviceAccessSnapshotChanged(unlimited, downgraded), true);
|
|
71
|
-
assert.deepEqual(downgraded.allowedDeviceIds, ['client-1', 'client-2', 'client-3'
|
|
72
|
-
assert.deepEqual(downgraded.blockedDeviceIds, ['client-6']);
|
|
71
|
+
assert.deepEqual(downgraded.allowedDeviceIds, ['client-1', 'client-2', 'client-3']);
|
|
72
|
+
assert.deepEqual(downgraded.blockedDeviceIds, ['client-6', 'client-5', 'client-4']);
|
|
73
73
|
assert.equal(planDeviceAccessSnapshotChanged(downgraded, {
|
|
74
74
|
...downgraded,
|
|
75
75
|
generation: 3,
|
|
@@ -90,12 +90,12 @@ test('the exact Hub manager stays controllable without consuming a Client slot',
|
|
|
90
90
|
const snapshot = createPlanDeviceAccessSnapshot([
|
|
91
91
|
...clients,
|
|
92
92
|
{ deviceId: 'hub-manager', slotNumber: 0, connected: true }
|
|
93
|
-
],
|
|
93
|
+
], 3, 8, { exemptDeviceIds: ['hub-manager'] });
|
|
94
94
|
|
|
95
95
|
assert.deepEqual(snapshot.connectedDeviceIds, clients.map(client => client.deviceId));
|
|
96
96
|
assert.deepEqual(snapshot.exemptConnectedDeviceIds, ['hub-manager']);
|
|
97
97
|
assert.equal(snapshot.allowedDeviceIdSet.has('hub-manager'), true);
|
|
98
|
-
assert.deepEqual(snapshot.blockedDeviceIds, ['client-6']);
|
|
98
|
+
assert.deepEqual(snapshot.blockedDeviceIds, ['client-6', 'client-5', 'client-4']);
|
|
99
99
|
assert.deepEqual(partitionPlanDeviceIds(['hub-manager', 'client-6'], snapshot), {
|
|
100
100
|
allowedDeviceIds: ['hub-manager'],
|
|
101
101
|
blockedDeviceIds: ['client-6']
|
package/hub/src/remote-hub.js
CHANGED
|
@@ -11621,9 +11621,18 @@ export function createRemoteHub(options = {}) {
|
|
|
11621
11621
|
}
|
|
11622
11622
|
|
|
11623
11623
|
function startLiveStream(deviceId, options = {}) {
|
|
11624
|
-
const device = devices.get(String(deviceId || ''));
|
|
11625
|
-
const denied = policyError(device);
|
|
11626
|
-
if (denied) return { ok: false, error: denied };
|
|
11624
|
+
const device = devices.get(String(deviceId || ''));
|
|
11625
|
+
const denied = policyError(device);
|
|
11626
|
+
if (denied) return { ok: false, error: denied };
|
|
11627
|
+
const expectedSessionId = safeString(options.expectedSessionId, 160);
|
|
11628
|
+
if (expectedSessionId && device?.sessionId && expectedSessionId !== device.sessionId) {
|
|
11629
|
+
// A delayed Wall restoration cannot create capture on a replacement
|
|
11630
|
+
// Client session. This is a retired intent, not a successful start.
|
|
11631
|
+
return {
|
|
11632
|
+
ok: true, skipped: true, staleOwner: true,
|
|
11633
|
+
retainedNewerOwner: true, reason: 'wall-restore-session-replaced'
|
|
11634
|
+
};
|
|
11635
|
+
}
|
|
11627
11636
|
if (device?.synthetic !== true
|
|
11628
11637
|
&& device?.connected
|
|
11629
11638
|
&& !isDeviceConnectionFresh(device)) {
|
package/hub/src/server.js
CHANGED
|
@@ -177,7 +177,7 @@ const atlasPool = new Mode4AtlasPool({
|
|
|
177
177
|
});
|
|
178
178
|
const inputClients = new Set();
|
|
179
179
|
const audioClients = new Set();
|
|
180
|
-
const FREE_DEVICE_LIMIT =
|
|
180
|
+
const FREE_DEVICE_LIMIT = 3;
|
|
181
181
|
const PLUS_DEVICE_LIMIT = 20;
|
|
182
182
|
const PRO_DEVICE_LIMIT = 50;
|
|
183
183
|
const LICENSE_VERIFY_MAX_AGE_MS = 6 * 60 * 60 * 1000;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "livedesk",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"livedeskClientVersion": "0.1.
|
|
3
|
+
"version": "0.1.701",
|
|
4
|
+
"livedeskClientVersion": "0.1.277",
|
|
5
5
|
"buildFlavor": "production",
|
|
6
6
|
"description": "VuvoDesk Hub and client launcher",
|
|
7
7
|
"type": "module",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"README.md"
|
|
24
24
|
],
|
|
25
25
|
"scripts": {
|
|
26
|
-
"check": "node --check bin/livedesk.js && node --check bootstrap/npm-update-cache.mjs && node --check bootstrap/update-host-client.mjs && node --check bootstrap/update-host.mjs && node --check diagnostics/vuvodesk-mode3-capture-smoke.mjs && node --check diagnostics/vuvodesk-mac-physical-gate-core.mjs && node --check diagnostics/vuvodesk-mac-physical-isolation.mjs && node --check diagnostics/vuvodesk-transport-physical-core.mjs && node --check diagnostics/vuvodesk-transport-physical.mjs && node --check scripts/sync-web-dist.js",
|
|
26
|
+
"check": "node --check bin/livedesk.js && node --check bootstrap/npm-update-cache.mjs && node --check bootstrap/update-host-client.mjs && node --check bootstrap/update-host.mjs && node --check diagnostics/vuvodesk-mode3-capture-smoke.mjs && node --check diagnostics/vuvodesk-mac-physical-gate-core.mjs && node --check diagnostics/vuvodesk-mac-physical-isolation.mjs && node --check diagnostics/vuvodesk-transport-physical-core.mjs && node --check diagnostics/vuvodesk-transport-physical.mjs && node --check diagnostics/vuvodesk-linux-desktop.mjs && node --check scripts/sync-web-dist.js",
|
|
27
27
|
"build": "node scripts/sync-web-dist.js",
|
|
28
28
|
"prepack": "npm --prefix ../.. run build -w @livedesk/web && node scripts/sync-web-dist.js",
|
|
29
29
|
"pack:dry": "npm pack --dry-run",
|
|
@@ -54,10 +54,10 @@
|
|
|
54
54
|
"ws": "^8.18.3"
|
|
55
55
|
},
|
|
56
56
|
"optionalDependencies": {
|
|
57
|
-
"@livedesk/fast-linux-x64": "0.1.
|
|
58
|
-
"@livedesk/fast-osx-arm64": "0.1.
|
|
59
|
-
"@livedesk/fast-osx-x64": "0.1.
|
|
60
|
-
"@livedesk/fast-win-x64": "0.1.
|
|
57
|
+
"@livedesk/fast-linux-x64": "0.1.472",
|
|
58
|
+
"@livedesk/fast-osx-arm64": "0.1.472",
|
|
59
|
+
"@livedesk/fast-osx-x64": "0.1.472",
|
|
60
|
+
"@livedesk/fast-win-x64": "0.1.472"
|
|
61
61
|
},
|
|
62
62
|
"publishConfig": {
|
|
63
63
|
"access": "public"
|
package/web/dist/app.html
CHANGED
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
.vuvodesk-static-boot [hidden] { display: none; }
|
|
53
53
|
@keyframes vuvodesk-static-boot-spin { to { transform: rotate(360deg); } }
|
|
54
54
|
</style>
|
|
55
|
-
<script type="module" crossorigin src="/assets/app-
|
|
55
|
+
<script type="module" crossorigin src="/assets/app-CUM2wTSb.js" data-vuvodesk-entry></script>
|
|
56
56
|
<link rel="modulepreload" crossorigin href="/assets/public-site-routing-pddZbLCj.js">
|
|
57
57
|
<link rel="modulepreload" crossorigin href="/assets/pwa-bootstrap-ljOQTt2e.js">
|
|
58
58
|
</head>
|
package/web/dist/app.webmanifest
CHANGED