livedesk 0.1.413 → 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.
- package/client/README.md +4 -3
- package/client/bin/livedesk-client.js +214 -29
- package/client/package.json +5 -5
- package/client/src/runtime/client-runtime-server.js +134 -10
- package/client/src/runtime/linux-video-acceleration.js +390 -0
- package/hub/src/remote-hub.js +88 -17
- package/package.json +6 -6
- package/web/dist/assets/{icons-HeE5A4Vk.js → icons-grMviB7T.js} +1 -1
- package/web/dist/assets/index-C2aVLw-n.css +1 -0
- package/web/dist/assets/index-U4475peT.js +25 -0
- package/web/dist/assets/{react-DW3yB5LY.js → react-BPvGG2Bl.js} +1 -1
- package/web/dist/index.html +4 -4
- package/web/dist/assets/index-CZw2jLf-.css +0 -1
- package/web/dist/assets/index-DBQT5fVa.js +0 -25
|
@@ -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
|
+
}
|
package/hub/src/remote-hub.js
CHANGED
|
@@ -4328,8 +4328,21 @@ export function createRemoteHub(options = {}) {
|
|
|
4328
4328
|
return;
|
|
4329
4329
|
}
|
|
4330
4330
|
|
|
4331
|
-
if (!state.authenticated) {
|
|
4332
|
-
if (message.type
|
|
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
|
-
|
|
4451
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
4
|
-
"livedeskClientVersion": "0.1.
|
|
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.
|
|
53
|
-
"@livedesk/fast-osx-arm64": "0.1.
|
|
54
|
-
"@livedesk/fast-osx-x64": "0.1.
|
|
55
|
-
"@livedesk/fast-win-x64": "0.1.
|
|
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"
|