livedesk 0.1.414 → 0.1.415

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.
@@ -0,0 +1,390 @@
1
+ import { spawn, spawnSync } from 'node:child_process';
2
+ import { accessSync, constants, existsSync, readFileSync, readdirSync, realpathSync } from 'node:fs';
3
+ import { delimiter, join, resolve } from 'node:path';
4
+
5
+ const PROBE_TIMEOUT_MS = 6_000;
6
+ const INTEL_VENDOR_ID = '0x8086';
7
+ const AMD_VENDOR_ID = '0x1002';
8
+
9
+ function cleanText(value, maxLength = 320) {
10
+ return String(value ?? '').replace(/[\r\n\t]+/g, ' ').trim().slice(0, maxLength);
11
+ }
12
+
13
+ function executableExists(path) {
14
+ try {
15
+ accessSync(path, constants.X_OK);
16
+ return true;
17
+ } catch {
18
+ return false;
19
+ }
20
+ }
21
+
22
+ function uniquePaths(values) {
23
+ const result = [];
24
+ for (const value of values) {
25
+ const candidate = String(value || '').trim();
26
+ if (!candidate) continue;
27
+ let normalized = candidate;
28
+ try {
29
+ normalized = realpathSync(candidate);
30
+ } catch {
31
+ normalized = resolve(candidate);
32
+ }
33
+ if (!result.includes(normalized)) result.push(normalized);
34
+ }
35
+ return result;
36
+ }
37
+
38
+ export function findExecutable(name, options = {}) {
39
+ const platform = options.platform || process.platform;
40
+ const env = options.env || process.env;
41
+ const exists = options.executableExists || executableExists;
42
+ const executableName = platform === 'win32' && !String(name).toLowerCase().endsWith('.exe')
43
+ ? `${name}.exe`
44
+ : String(name);
45
+ const candidates = String(env.PATH || '')
46
+ .split(delimiter)
47
+ .filter(Boolean)
48
+ .map(entry => join(entry, executableName));
49
+ return candidates.find(candidate => exists(candidate)) || '';
50
+ }
51
+
52
+ function findLinuxRenderDevices(options = {}) {
53
+ if (Array.isArray(options.renderDevices)) {
54
+ return uniquePaths(options.renderDevices);
55
+ }
56
+ const driRoot = options.driRoot || '/dev/dri';
57
+ try {
58
+ return uniquePaths(readdirSync(driRoot)
59
+ .filter(name => /^renderD\d+$/.test(name))
60
+ .map(name => join(driRoot, name))
61
+ .filter(executable => existsSync(executable)));
62
+ } catch {
63
+ return [];
64
+ }
65
+ }
66
+
67
+ function resolveSystemFfmpeg(options = {}) {
68
+ const env = options.env || process.env;
69
+ const exists = options.executableExists || executableExists;
70
+ const explicit = String(env.LIVEDESK_SYSTEM_FFMPEG || '').trim();
71
+ const candidates = uniquePaths([
72
+ explicit,
73
+ '/usr/bin/ffmpeg',
74
+ '/usr/local/bin/ffmpeg',
75
+ '/snap/bin/ffmpeg',
76
+ findExecutable('ffmpeg', { ...options, env, platform: 'linux', executableExists: exists })
77
+ ]);
78
+ return candidates.find(candidate => exists(candidate) && !candidate.includes('/node_modules/')) || '';
79
+ }
80
+
81
+ function readGpuVendor(renderDevice, options = {}) {
82
+ if (options.gpuVendor) return String(options.gpuVendor).trim().toLowerCase();
83
+ try {
84
+ return readFileSync(`/sys/class/drm/${renderDevice.split('/').pop()}/device/vendor`, 'utf8').trim().toLowerCase();
85
+ } catch {
86
+ return '';
87
+ }
88
+ }
89
+
90
+ export function detectLinuxPackageManager(options = {}) {
91
+ const managers = [
92
+ { id: 'apt', command: 'apt-get' },
93
+ { id: 'dnf', command: 'dnf' },
94
+ { id: 'pacman', command: 'pacman' },
95
+ { id: 'zypper', command: 'zypper' }
96
+ ];
97
+ for (const manager of managers) {
98
+ const path = findExecutable(manager.command, { ...options, platform: 'linux' });
99
+ if (path) return { ...manager, path };
100
+ }
101
+ return null;
102
+ }
103
+
104
+ function packagesFor(managerId, gpuVendor) {
105
+ if (managerId === 'apt') {
106
+ if (gpuVendor === INTEL_VENDOR_ID) return ['ffmpeg', 'vainfo', 'intel-media-va-driver', 'i965-va-driver'];
107
+ if (gpuVendor === AMD_VENDOR_ID) return ['ffmpeg', 'vainfo', 'mesa-va-drivers'];
108
+ return ['ffmpeg', 'vainfo'];
109
+ }
110
+ if (managerId === 'dnf') {
111
+ return ['ffmpeg', 'libva-utils'];
112
+ }
113
+ if (managerId === 'pacman') {
114
+ if (gpuVendor === INTEL_VENDOR_ID) return ['ffmpeg', 'libva-utils', 'intel-media-driver', 'libva-intel-driver'];
115
+ if (gpuVendor === AMD_VENDOR_ID) return ['ffmpeg', 'libva-utils', 'libva-mesa-driver'];
116
+ return ['ffmpeg', 'libva-utils'];
117
+ }
118
+ if (managerId === 'zypper') {
119
+ return ['ffmpeg', 'libva-utils'];
120
+ }
121
+ return [];
122
+ }
123
+
124
+ export function buildLinuxVideoInstallPlan(status, options = {}) {
125
+ const manager = options.packageManager || status?.packageManager;
126
+ if (!manager?.id || !manager?.path) return null;
127
+ const packages = packagesFor(manager.id, status?.gpuVendor || '');
128
+ if (packages.length === 0) return null;
129
+ let args;
130
+ if (manager.id === 'apt' || manager.id === 'dnf') {
131
+ args = ['install', '-y', ...packages];
132
+ } else if (manager.id === 'pacman') {
133
+ args = ['-S', '--needed', '--noconfirm', ...packages];
134
+ } else if (manager.id === 'zypper') {
135
+ args = ['--non-interactive', 'install', ...packages];
136
+ } else {
137
+ return null;
138
+ }
139
+ return {
140
+ manager,
141
+ packages,
142
+ args,
143
+ displayCommand: `sudo ${manager.command} ${args.join(' ')}`
144
+ };
145
+ }
146
+
147
+ export function probeLinuxVideoAcceleration(ffmpegPath, renderDevice, options = {}) {
148
+ const run = options.spawnSyncImpl || spawnSync;
149
+ const env = options.env || process.env;
150
+ const drivers = Array.isArray(options.drivers) && options.drivers.length > 0
151
+ ? options.drivers
152
+ : ['', 'iHD', 'i965'];
153
+ let lastError = '';
154
+ for (const driver of drivers) {
155
+ const probeEnv = { ...env };
156
+ if (driver) probeEnv.LIBVA_DRIVER_NAME = driver;
157
+ else delete probeEnv.LIBVA_DRIVER_NAME;
158
+ const result = run(ffmpegPath, [
159
+ '-hide_banner', '-loglevel', 'error',
160
+ '-vaapi_device', renderDevice,
161
+ '-f', 'lavfi',
162
+ '-i', 'color=c=black:s=64x64:r=1',
163
+ '-vf', 'format=nv12,hwupload',
164
+ '-frames:v', '1',
165
+ '-an',
166
+ '-c:v', 'h264_vaapi',
167
+ '-f', 'h264',
168
+ 'pipe:1'
169
+ ], {
170
+ env: probeEnv,
171
+ encoding: null,
172
+ timeout: options.timeoutMs || PROBE_TIMEOUT_MS,
173
+ maxBuffer: 2 * 1024 * 1024,
174
+ windowsHide: true
175
+ });
176
+ const outputBytes = Buffer.isBuffer(result.stdout) ? result.stdout.length : 0;
177
+ if (result.status === 0 && outputBytes > 0) {
178
+ return {
179
+ ok: true,
180
+ encoder: 'h264_vaapi',
181
+ driver: driver || 'auto',
182
+ outputBytes
183
+ };
184
+ }
185
+ lastError = cleanText(
186
+ result.error?.message
187
+ || (Buffer.isBuffer(result.stderr) ? result.stderr.toString('utf8') : result.stderr)
188
+ || `ffmpeg exited with ${result.status ?? 'no status'}`
189
+ );
190
+ }
191
+ return { ok: false, error: lastError || 'VAAPI hardware encoding probe failed.' };
192
+ }
193
+
194
+ function baseStatus(platform) {
195
+ return {
196
+ platform,
197
+ supported: platform === 'linux',
198
+ state: platform === 'linux' ? 'checking' : 'not-applicable',
199
+ ready: false,
200
+ busy: false,
201
+ canInstall: false,
202
+ encoder: '',
203
+ driver: '',
204
+ ffmpegPath: '',
205
+ renderDevice: '',
206
+ gpuVendor: '',
207
+ packageManager: null,
208
+ manualCommand: '',
209
+ message: '',
210
+ error: '',
211
+ checkedAt: new Date().toISOString()
212
+ };
213
+ }
214
+
215
+ export function inspectLinuxVideoAcceleration(options = {}) {
216
+ const platform = options.platform || process.platform;
217
+ const status = baseStatus(platform);
218
+ if (platform !== 'linux') return status;
219
+
220
+ const renderDevices = findLinuxRenderDevices(options);
221
+ const renderDevice = renderDevices[0] || '';
222
+ const packageManager = options.packageManager === undefined
223
+ ? detectLinuxPackageManager(options)
224
+ : options.packageManager;
225
+ const gpuVendor = renderDevice ? readGpuVendor(renderDevice, options) : '';
226
+ Object.assign(status, { renderDevice, gpuVendor, packageManager });
227
+ const installPlan = buildLinuxVideoInstallPlan(status, { packageManager });
228
+ status.canInstall = Boolean(installPlan);
229
+ status.manualCommand = installPlan?.displayCommand || '';
230
+
231
+ if (!renderDevice) {
232
+ status.state = 'unavailable';
233
+ status.message = 'No Linux hardware video device was found.';
234
+ return status;
235
+ }
236
+
237
+ const ffmpegPath = options.ffmpegPath === undefined
238
+ ? resolveSystemFfmpeg(options)
239
+ : String(options.ffmpegPath || '');
240
+ status.ffmpegPath = ffmpegPath;
241
+ if (!ffmpegPath) {
242
+ status.state = 'setup-required';
243
+ status.message = 'Hardware video setup is available.';
244
+ return status;
245
+ }
246
+
247
+ const probe = (options.probeImpl || probeLinuxVideoAcceleration)(ffmpegPath, renderDevice, options);
248
+ if (!probe?.ok) {
249
+ status.state = 'setup-required';
250
+ status.message = 'Hardware video needs setup.';
251
+ status.error = cleanText(probe?.error || 'VAAPI hardware encoding probe failed.');
252
+ return status;
253
+ }
254
+
255
+ status.state = 'ready';
256
+ status.ready = true;
257
+ status.canInstall = false;
258
+ status.encoder = cleanText(probe.encoder || 'h264_vaapi', 80);
259
+ status.driver = cleanText(probe.driver || 'auto', 80);
260
+ status.message = 'VAAPI hardware video is ready.';
261
+ status.error = '';
262
+ return status;
263
+ }
264
+
265
+ function runChild(command, args, options = {}) {
266
+ const launch = options.spawnImpl || spawn;
267
+ return new Promise((resolvePromise, reject) => {
268
+ let child;
269
+ try {
270
+ child = launch(command, args, {
271
+ env: options.env || process.env,
272
+ stdio: 'inherit',
273
+ windowsHide: true
274
+ });
275
+ } catch (error) {
276
+ reject(error);
277
+ return;
278
+ }
279
+ child.once('error', reject);
280
+ child.once('exit', (code, signal) => resolvePromise({ code: code ?? 1, signal: signal || '' }));
281
+ });
282
+ }
283
+
284
+ export async function installLinuxVideoAcceleration(currentStatus, options = {}) {
285
+ if ((options.platform || process.platform) !== 'linux') {
286
+ return { ...baseStatus(options.platform || process.platform), state: 'not-applicable', error: 'linux-required' };
287
+ }
288
+ const plan = buildLinuxVideoInstallPlan(currentStatus, options);
289
+ if (!plan) {
290
+ return {
291
+ ...currentStatus,
292
+ state: 'error',
293
+ busy: false,
294
+ ready: false,
295
+ error: 'No supported Linux package manager was found.'
296
+ };
297
+ }
298
+
299
+ const env = options.env || process.env;
300
+ const find = name => findExecutable(name, { ...options, env, platform: 'linux' });
301
+ const isRoot = options.isRoot ?? (typeof process.getuid === 'function' && process.getuid() === 0);
302
+ const pkexec = find('pkexec');
303
+ const sudo = find('sudo');
304
+ const graphicalSession = Boolean(env.DISPLAY || env.WAYLAND_DISPLAY);
305
+ let command = '';
306
+ let args = [];
307
+ let elevation = '';
308
+ if (isRoot) {
309
+ command = plan.manager.path;
310
+ args = plan.args;
311
+ elevation = 'root';
312
+ } else if (pkexec && graphicalSession) {
313
+ command = pkexec;
314
+ args = [plan.manager.path, ...plan.args];
315
+ elevation = 'pkexec';
316
+ } else if (sudo && (options.stdinIsTTY ?? process.stdin.isTTY)) {
317
+ command = sudo;
318
+ args = [plan.manager.path, ...plan.args];
319
+ elevation = 'sudo';
320
+ } else {
321
+ return {
322
+ ...currentStatus,
323
+ state: 'error',
324
+ busy: false,
325
+ ready: false,
326
+ manualCommand: plan.displayCommand,
327
+ error: 'Administrator approval is unavailable in this session.'
328
+ };
329
+ }
330
+
331
+ options.onProgress?.({
332
+ ...currentStatus,
333
+ state: 'installing',
334
+ busy: true,
335
+ ready: false,
336
+ error: '',
337
+ message: 'Installing Linux hardware video support…',
338
+ elevation
339
+ });
340
+
341
+ let result;
342
+ try {
343
+ result = options.runInstall
344
+ ? await options.runInstall({ command, args, plan, elevation })
345
+ : await runChild(command, args, { ...options, env });
346
+ } catch (error) {
347
+ return {
348
+ ...currentStatus,
349
+ state: 'error',
350
+ busy: false,
351
+ ready: false,
352
+ manualCommand: plan.displayCommand,
353
+ error: cleanText(error?.message || error)
354
+ };
355
+ }
356
+ if (result?.code !== 0) {
357
+ return {
358
+ ...currentStatus,
359
+ state: 'error',
360
+ busy: false,
361
+ ready: false,
362
+ manualCommand: plan.displayCommand,
363
+ error: `Package installation stopped with exit code ${result?.code ?? 'unknown'}.`
364
+ };
365
+ }
366
+
367
+ const refreshed = (options.inspectImpl || inspectLinuxVideoAcceleration)({
368
+ ...options,
369
+ platform: 'linux',
370
+ packageManager: plan.manager,
371
+ ffmpegPath: undefined
372
+ });
373
+ if (!refreshed.ready) {
374
+ return {
375
+ ...refreshed,
376
+ state: 'error',
377
+ busy: false,
378
+ ready: false,
379
+ canInstall: true,
380
+ manualCommand: plan.displayCommand,
381
+ error: refreshed.error || 'Hardware video was installed but the VAAPI test did not pass.'
382
+ };
383
+ }
384
+ return {
385
+ ...refreshed,
386
+ busy: false,
387
+ restartAgent: true,
388
+ message: 'VAAPI hardware video is ready. Restarting the LiveDesk Agent…'
389
+ };
390
+ }
@@ -4328,8 +4328,21 @@ export function createRemoteHub(options = {}) {
4328
4328
  return;
4329
4329
  }
4330
4330
 
4331
- if (!state.authenticated) {
4332
- if (message.type !== 'hello') {
4331
+ if (!state.authenticated) {
4332
+ if (message.type === 'slot.assign') {
4333
+ if (!timingSafeStringEqual(message.pairToken, pairToken)) {
4334
+ writeJsonLine(socket, { type: 'slot.assignment', ok: false, error: 'invalid-pair-token' });
4335
+ socket.end();
4336
+ return;
4337
+ }
4338
+ const result = assignDeviceSlot(
4339
+ message.deviceId || message.DeviceId,
4340
+ message.slotNumber ?? message.slot ?? message.SlotNumber);
4341
+ writeJsonLine(socket, { type: 'slot.assignment', ...result });
4342
+ socket.end();
4343
+ return;
4344
+ }
4345
+ if (message.type !== 'hello') {
4333
4346
  writeJsonLine(socket, { type: 'error', error: 'hello-required' });
4334
4347
  socket.destroy();
4335
4348
  return;
@@ -4445,12 +4458,13 @@ export function createRemoteHub(options = {}) {
4445
4458
  device.status = typeof message.status === 'object' && message.status
4446
4459
  ? { ...message.status }
4447
4460
  : {};
4448
- {
4449
- const nextSlotNumber = normalizeSlotNumber(device.status.slotNumber);
4450
- if (nextSlotNumber > 0) {
4451
- device.slotNumber = nextSlotNumber;
4452
- }
4453
- }
4461
+ {
4462
+ const nextSlotNumber = normalizeSlotNumber(device.status.slotNumber);
4463
+ const assignedSlotNumber = normalizeSlotNumber(device.slotAssignment?.slotNumber);
4464
+ if (nextSlotNumber > 0 && (!assignedSlotNumber || nextSlotNumber === assignedSlotNumber)) {
4465
+ device.slotNumber = nextSlotNumber;
4466
+ }
4467
+ }
4454
4468
  device.lastStatusAt = device.lastSeenAt;
4455
4469
  device.counters.statusReceived += 1;
4456
4470
  emitRemoteEvent('RemoteDeviceStatus', device);
@@ -4956,7 +4970,7 @@ export function createRemoteHub(options = {}) {
4956
4970
  started = false;
4957
4971
  }
4958
4972
 
4959
- function disconnectDevice(deviceId, reason = 'manager-disconnect') {
4973
+ function disconnectDevice(deviceId, reason = 'manager-disconnect') {
4960
4974
  const device = devices.get(String(deviceId || ''));
4961
4975
  if (device?.synthetic === true) {
4962
4976
  device.connected = false;
@@ -4974,11 +4988,67 @@ export function createRemoteHub(options = {}) {
4974
4988
  }
4975
4989
 
4976
4990
  writeJsonLine(device.socket, { type: 'disconnect', reason });
4977
- device.socket.destroy();
4978
- return true;
4979
- }
4980
-
4981
- function sendCommand(deviceId, command) {
4991
+ device.socket.destroy();
4992
+ return true;
4993
+ }
4994
+
4995
+ function assignDeviceSlot(deviceId, value) {
4996
+ const normalizedDeviceId = safeString(deviceId, 160);
4997
+ const slotNumber = normalizeSlotNumber(value);
4998
+ if (!normalizedDeviceId) {
4999
+ return { ok: false, error: 'device-id-required' };
5000
+ }
5001
+ if (slotNumber <= 0) {
5002
+ return { ok: false, error: 'invalid-slot-number' };
5003
+ }
5004
+
5005
+ const device = devices.get(normalizedDeviceId);
5006
+ if (!device?.connected) {
5007
+ return { ok: false, error: 'device-not-connected' };
5008
+ }
5009
+
5010
+ const conflict = [...devices.values()].find(candidate =>
5011
+ candidate?.connected
5012
+ && candidate.deviceId !== normalizedDeviceId
5013
+ && normalizeSlotNumber(candidate.slotNumber) === slotNumber);
5014
+ if (conflict) {
5015
+ return {
5016
+ ok: false,
5017
+ error: 'slot-taken',
5018
+ slotNumber,
5019
+ conflict: {
5020
+ deviceId: conflict.deviceId,
5021
+ deviceName: conflict.deviceName || conflict.hostname || conflict.deviceId,
5022
+ hostname: conflict.hostname || '',
5023
+ slotNumber
5024
+ }
5025
+ };
5026
+ }
5027
+
5028
+ const previousSlotNumber = normalizeSlotNumber(device.slotNumber);
5029
+ device.slotNumber = slotNumber;
5030
+ device.status = {
5031
+ ...(device.status && typeof device.status === 'object' ? device.status : {}),
5032
+ slotNumber
5033
+ };
5034
+ device.slotAssignment = {
5035
+ slotNumber,
5036
+ assignedAt: new Date().toISOString()
5037
+ };
5038
+ emitRemoteEvent('RemoteDeviceSlotAssigned', device, {
5039
+ previousSlotNumber,
5040
+ slotNumber
5041
+ });
5042
+ return {
5043
+ ok: true,
5044
+ deviceId: normalizedDeviceId,
5045
+ deviceName: device.deviceName || device.hostname || normalizedDeviceId,
5046
+ previousSlotNumber,
5047
+ slotNumber
5048
+ };
5049
+ }
5050
+
5051
+ function sendCommand(deviceId, command) {
4982
5052
  const device = devices.get(String(deviceId || ''));
4983
5053
  const commandName = safeString(command?.command || 'ping', 80);
4984
5054
  const requiredPermission = commandName === 'input.control'
@@ -6324,9 +6394,10 @@ export function createRemoteHub(options = {}) {
6324
6394
  getTaskBatch,
6325
6395
  cancelTaskBatch,
6326
6396
  retryTaskBatch,
6327
- listDeviceFrames,
6328
- disconnectDevice,
6329
- sendCommand,
6397
+ listDeviceFrames,
6398
+ disconnectDevice,
6399
+ assignDeviceSlot,
6400
+ sendCommand,
6330
6401
  sendLegacyClientUpdate,
6331
6402
  sendInputControl,
6332
6403
  notifyAgentProgress,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "livedesk",
3
- "version": "0.1.414",
4
- "livedeskClientVersion": "0.1.179",
3
+ "version": "0.1.415",
4
+ "livedeskClientVersion": "0.1.180",
5
5
  "buildFlavor": "production",
6
6
  "description": "LiveDesk Hub and client launcher",
7
7
  "type": "module",
@@ -49,10 +49,10 @@
49
49
  "ws": "^8.18.3"
50
50
  },
51
51
  "optionalDependencies": {
52
- "@livedesk/fast-linux-x64": "0.1.385",
53
- "@livedesk/fast-osx-arm64": "0.1.385",
54
- "@livedesk/fast-osx-x64": "0.1.385",
55
- "@livedesk/fast-win-x64": "0.1.385"
52
+ "@livedesk/fast-linux-x64": "0.1.386",
53
+ "@livedesk/fast-osx-arm64": "0.1.386",
54
+ "@livedesk/fast-osx-x64": "0.1.386",
55
+ "@livedesk/fast-win-x64": "0.1.386"
56
56
  },
57
57
  "publishConfig": {
58
58
  "access": "public"