tassello 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/README.md +175 -8
  3. package/dist/server/behavior_tree_files.d.ts +0 -1
  4. package/dist/server/behavior_tree_files.js +2 -25
  5. package/dist/server/diagnostics.d.ts +16 -0
  6. package/dist/server/diagnostics.js +200 -0
  7. package/dist/server/diagnostics_state.d.ts +23 -0
  8. package/dist/server/diagnostics_state.js +146 -0
  9. package/dist/server/disks.d.ts +14 -0
  10. package/dist/server/disks.js +152 -0
  11. package/dist/server/file_manager.d.ts +25 -0
  12. package/dist/server/file_manager.js +717 -0
  13. package/dist/server/http.js +248 -5
  14. package/dist/server/netpbm.d.ts +3 -0
  15. package/dist/server/netpbm.js +69 -0
  16. package/dist/server/networking.d.ts +41 -0
  17. package/dist/server/networking.js +508 -0
  18. package/dist/server/networking_format.d.ts +38 -0
  19. package/dist/server/networking_format.js +651 -0
  20. package/dist/server/pty.d.ts +26 -0
  21. package/dist/server/pty.js +150 -0
  22. package/dist/server/server.js +13 -2
  23. package/dist/server/session.d.ts +10 -1
  24. package/dist/server/session.js +88 -1
  25. package/dist/server/terminal_output.d.ts +25 -0
  26. package/dist/server/terminal_output.js +108 -0
  27. package/dist/server/terminals.d.ts +32 -0
  28. package/dist/server/terminals.js +366 -0
  29. package/dist/server/util.d.ts +4 -0
  30. package/dist/server/util.js +45 -1
  31. package/dist/shared/file_manager.d.ts +130 -0
  32. package/dist/shared/file_manager.js +7 -0
  33. package/dist/shared/networking.d.ts +178 -0
  34. package/dist/shared/networking.js +248 -0
  35. package/dist/shared/protocol.d.ts +103 -2
  36. package/dist/shared/protocol.js +8 -0
  37. package/dist/types/client/api.d.ts +22 -0
  38. package/dist/types/client/connection.d.ts +30 -1
  39. package/dist/types/client/icons.d.ts +33 -0
  40. package/dist/types/client/sdk/index.d.ts +5 -3
  41. package/dist/types/shared/file_manager.d.ts +130 -0
  42. package/dist/types/shared/networking.d.ts +178 -0
  43. package/dist/types/shared/protocol.d.ts +103 -2
  44. package/dist/www/assets/Joystick.js +1 -1
  45. package/dist/www/assets/RobotModelView.js +2 -2
  46. package/dist/www/assets/TerminalView.css +1 -0
  47. package/dist/www/assets/TerminalView.js +17 -0
  48. package/dist/www/assets/dialogs.js +1 -0
  49. package/dist/www/assets/icons.js +1 -1
  50. package/dist/www/assets/index.css +1 -1
  51. package/dist/www/assets/index.js +59 -19
  52. package/dist/www/assets/keys.js +3 -0
  53. package/dist/www/assets/shared/tassello-sdk.js +1 -1
  54. package/dist/www/index.html +2 -0
  55. package/docs/plugins.md +22 -0
  56. package/package.json +6 -2
@@ -0,0 +1,508 @@
1
+ // The robot's networks for the Networking widgets, through NetworkManager's
2
+ // nmcli: its interfaces, the connections saved and the Wi-Fi networks in
3
+ // range, followed for every page at once (read again as soon as nmcli
4
+ // monitor reports a change, and every few seconds for the signals), and the
5
+ // pages' changes: connections activated and deactivated, made, changed and
6
+ // deleted, access points started, Wi-Fi turned on and off. What is being
7
+ // done is part of the state: every page shows it, the one that asked
8
+ // included, which may have lost the robot meanwhile (it switched networks).
9
+ //
10
+ // The server's user needs the right to manage NetworkManager, e.g. a polkit
11
+ // rule (see the README); without it the actions fail, with nmcli's reason,
12
+ // and the state says what is not allowed.
13
+ import { execFile, spawn } from 'node:child_process';
14
+ import { needsPassword, passwordProblem, settingsOf, } from '../shared/networking.js';
15
+ import { CONNECTION_FIELDS, CONNECTION_SETTINGS, connectionUuids, createArgs, DEVICE_SECTIONS, GENERAL_FIELDS, keyManagementSecurity, modification, nmcliProblem, parseConnections, parseDevices, parseGeneral, parsePermissions, parseWifiList, passwordArgs, settingsBlocks, terseBlocks, WIFI_FIELDS, } from './networking_format.js';
16
+ import { errorMessage } from './util.js';
17
+ // How often the state is read while pages follow it: the signals of the
18
+ // networks in range change without nmcli monitor saying so
19
+ const POLL_PERIOD_MS = 3000;
20
+ // The connections' settings change when changed (and monitor says so): read
21
+ // again at most this often otherwise
22
+ const SETTINGS_PERIOD_MS = 30_000;
23
+ const PERMISSIONS_PERIOD_MS = 60_000;
24
+ // nmcli monitor reports a change in bursts of lines
25
+ const MONITOR_DEBOUNCE_MS = 250;
26
+ const MONITOR_RETRY_MS = 5000;
27
+ // How long NetworkManager is given to activate a connection: DHCP alone may
28
+ // take 45 s
29
+ const ACTIVATION_WAIT_S = 60;
30
+ // A page opening the widget scans for networks if the last scan is older
31
+ const SCAN_AGE_MS = 30_000;
32
+ // The scan's results come in the next seconds
33
+ const SCAN_MS = 5000;
34
+ // What was done, shown that long
35
+ const ACTIVITY_KEPT_MS = 10 * 60_000;
36
+ const ACTIVITY_LIMIT = 20;
37
+ // Untranslated, as the parsers read it, and in UTF-8: the names of networks
38
+ const ENV = { ...process.env, LC_ALL: 'C.UTF-8', LANGUAGE: '' };
39
+ const NO_PERMISSIONS = { control: false, modify: false, wifi: false, scan: false, share: false };
40
+ function nmcli(args, timeout = 10_000) {
41
+ return new Promise((resolve) => {
42
+ execFile('nmcli', args, { timeout, maxBuffer: 8 * 1024 * 1024, env: ENV }, (error, stdout, stderr) => resolve({
43
+ code: error ? (typeof error.code === 'number' ? error.code : 1) : 0,
44
+ stdout,
45
+ stderr: stderr || (error && !stdout ? error.message : ''),
46
+ missing: error?.code === 'ENOENT',
47
+ }));
48
+ });
49
+ }
50
+ // Runs nmcli for an action: throws its reason. Never says the arguments,
51
+ // which may be secrets.
52
+ async function run(args, timeout) {
53
+ const result = await nmcli(args, timeout);
54
+ if (result.missing) {
55
+ throw new Error('nmcli is not installed: the Networking widget needs NetworkManager');
56
+ }
57
+ if (result.code !== 0) {
58
+ throw new Error(nmcliProblem(result.stderr || result.stdout, `nmcli failed (${result.code})`));
59
+ }
60
+ return result.stdout;
61
+ }
62
+ function activate(uuid, device) {
63
+ return run(['--wait', String(ACTIVATION_WAIT_S), 'connection', 'up', 'uuid', uuid, ...(device ? ['ifname', device] : [])], (ACTIVATION_WAIT_S + 10) * 1000);
64
+ }
65
+ function emptyState(error, activity) {
66
+ return {
67
+ error,
68
+ version: '',
69
+ state: '',
70
+ connectivity: '',
71
+ wifi: false,
72
+ wifiHardware: false,
73
+ permissions: NO_PERMISSIONS,
74
+ devices: [],
75
+ connections: [],
76
+ networks: [],
77
+ scanning: false,
78
+ activity,
79
+ };
80
+ }
81
+ // For the messages: the network's name if it has one
82
+ function nameOf(connection) {
83
+ return connection.kind === 'wifi' && connection.ssid ? connection.ssid : connection.name;
84
+ }
85
+ export class Networking {
86
+ logger;
87
+ listeners = new Set();
88
+ latest = null;
89
+ latestJson = '';
90
+ timer = null;
91
+ reading = false;
92
+ again = false;
93
+ monitor = null;
94
+ monitorTimer = null;
95
+ debounce = null;
96
+ // The connections' settings, by uuid, read for the list of uuids key
97
+ settings = new Map();
98
+ settingsKey = '';
99
+ settingsAt = 0;
100
+ permissions = NO_PERMISSIONS;
101
+ permissionsAt = 0;
102
+ activity = [];
103
+ nextActivity = 1;
104
+ scannedAt = 0;
105
+ // From the scan's request until its results are read
106
+ scanning = false;
107
+ scanTimer = null;
108
+ constructor(logger) {
109
+ this.logger = logger;
110
+ }
111
+ // The networks, whenever they change, until the returned function is
112
+ // called. A page opening the widget looks for networks if the last look
113
+ // is old.
114
+ watch(listener) {
115
+ this.listeners.add(listener);
116
+ if (this.latest) {
117
+ listener(this.latest);
118
+ }
119
+ if (this.listeners.size === 1) {
120
+ this.startMonitor();
121
+ void this.refresh();
122
+ }
123
+ if (Date.now() - this.scannedAt > SCAN_AGE_MS) {
124
+ void this.rescan(null);
125
+ }
126
+ return () => {
127
+ this.listeners.delete(listener);
128
+ if (this.listeners.size === 0) {
129
+ this.stop();
130
+ }
131
+ };
132
+ }
133
+ // Resolves once NetworkManager is done: NetworkResult, or NetworkSecrets.
134
+ // Throws why not. The request is checked (checkRequest).
135
+ async act(request) {
136
+ switch (request.action) {
137
+ case 'connect': {
138
+ const connection = await this.connection(request.uuid);
139
+ const { password, device } = request;
140
+ if (password !== null) {
141
+ const problem = connection.security && needsPassword(connection.security)
142
+ ? passwordProblem(connection.security, password)
143
+ : 'It has no password';
144
+ if (problem) {
145
+ throw new Error(problem);
146
+ }
147
+ }
148
+ const label = connection.kind === 'hotspot' ? `Starting the access point ${nameOf(connection)}` : `Connecting to ${nameOf(connection)}`;
149
+ return this.track('connect', label, { uuid: connection.uuid, ssid: connection.ssid || null, device }, async () => {
150
+ if (password !== null) {
151
+ await run(['connection', 'modify', 'uuid', connection.uuid, ...passwordArgs(connection, password)]);
152
+ }
153
+ await activate(connection.uuid, device);
154
+ return { uuid: connection.uuid, message: null };
155
+ });
156
+ }
157
+ case 'disconnect': {
158
+ const connection = await this.connection(request.uuid);
159
+ const label = connection.kind === 'hotspot'
160
+ ? `Stopping the access point ${nameOf(connection)}`
161
+ : `Disconnecting from ${nameOf(connection)}`;
162
+ return this.track('disconnect', label, { uuid: connection.uuid, ssid: connection.ssid || null, device: connection.device }, async () => {
163
+ await run(['connection', 'down', 'uuid', connection.uuid], 30_000);
164
+ return { uuid: connection.uuid, message: null };
165
+ });
166
+ }
167
+ case 'create': {
168
+ const { settings, connect } = request;
169
+ const device = request.device ?? (settings.kind === 'hotspot' ? settings.interface : null);
170
+ let label = `Saving ${settings.name}`;
171
+ if (settings.kind === 'hotspot') {
172
+ label = connect ? `Starting the access point ${settings.ssid}` : `Saving the access point ${settings.ssid}`;
173
+ }
174
+ else if (connect) {
175
+ label = settings.kind === 'wifi' ? `Joining ${settings.ssid}` : `Connecting ${settings.name}`;
176
+ }
177
+ return this.track('create', label, { uuid: null, ssid: settings.ssid || null, device }, async (activity) => {
178
+ const added = await run(createArgs(settings));
179
+ // Connection 'Lab' (4e1c…) successfully added.
180
+ const uuid = /\(([0-9a-f-]{36})\)/i.exec(added)?.[1];
181
+ if (!uuid) {
182
+ throw new Error('nmcli did not say which connection it saved');
183
+ }
184
+ activity.uuid = uuid;
185
+ if (!connect) {
186
+ return { uuid, message: null };
187
+ }
188
+ try {
189
+ await activate(uuid, device);
190
+ }
191
+ catch (e) {
192
+ // A cable's connects once plugged in; a Wi-Fi network's that
193
+ // failed, with a password mistyped, is not kept
194
+ if (settings.kind === 'ethernet') {
195
+ return { uuid, message: `Saved, not connected: ${errorMessage(e)}` };
196
+ }
197
+ await run(['connection', 'delete', 'uuid', uuid]).catch(() => { });
198
+ activity.uuid = null;
199
+ throw e;
200
+ }
201
+ return { uuid, message: null };
202
+ });
203
+ }
204
+ case 'modify': {
205
+ const connection = await this.connection(request.uuid);
206
+ const current = settingsOf(connection);
207
+ const { settings } = request;
208
+ if (!current || current.kind !== settings.kind) {
209
+ throw new Error(`${connection.name} is not changed here: it is not a Wi-Fi network, an access point or a cable's`);
210
+ }
211
+ if (settings.kind !== 'ethernet' &&
212
+ settings.security !== current.security &&
213
+ needsPassword(settings.security) &&
214
+ settings.password === null) {
215
+ throw new Error('The network’s password, for its new security');
216
+ }
217
+ const change = modification(connection, settings);
218
+ if (change.args.length === 0) {
219
+ return { uuid: connection.uuid, message: null };
220
+ }
221
+ const active = connection.activeState !== null && change.reactivate;
222
+ return this.track('modify', `Saving the changes of ${connection.name}`, { uuid: connection.uuid, ssid: connection.ssid || null, device: connection.device }, async () => {
223
+ await run(['connection', 'modify', 'uuid', connection.uuid, ...change.args]);
224
+ if (active) {
225
+ await activate(connection.uuid, connection.device);
226
+ }
227
+ return { uuid: connection.uuid, message: null };
228
+ });
229
+ }
230
+ case 'delete': {
231
+ const connection = await this.connection(request.uuid);
232
+ return this.track('delete', `Forgetting ${connection.name}`, { uuid: connection.uuid, ssid: connection.ssid || null, device: connection.device }, async () => {
233
+ await run(['connection', 'delete', 'uuid', connection.uuid], 30_000);
234
+ return { uuid: null, message: null };
235
+ });
236
+ }
237
+ case 'wifi':
238
+ return this.track('wifi', `Turning Wi-Fi ${request.enabled ? 'on' : 'off'}`, {}, async () => {
239
+ await run(['radio', 'wifi', request.enabled ? 'on' : 'off']);
240
+ return { uuid: null, message: null };
241
+ });
242
+ case 'rescan':
243
+ await this.rescan(request.device);
244
+ return { uuid: null, message: null };
245
+ case 'secrets':
246
+ return this.secrets(await this.connection(request.uuid));
247
+ }
248
+ }
249
+ // The server is shutting down
250
+ stopAll() {
251
+ this.listeners.clear();
252
+ this.stop();
253
+ }
254
+ // Of the latest state (read if no page follows it): throws if not known
255
+ async connection(uuid) {
256
+ const state = this.latest ?? (await this.read());
257
+ const connection = state.connections.find((c) => c.uuid === uuid);
258
+ if (!connection) {
259
+ throw new Error('No such connection: it may have been deleted meanwhile');
260
+ }
261
+ return connection;
262
+ }
263
+ async track(action, label, target, work) {
264
+ const activity = {
265
+ id: this.nextActivity++,
266
+ action,
267
+ label,
268
+ uuid: target.uuid ?? null,
269
+ ssid: target.ssid ?? null,
270
+ device: target.device ?? null,
271
+ started: Date.now(),
272
+ finished: null,
273
+ error: null,
274
+ };
275
+ this.activity = [activity, ...this.activity];
276
+ this.logger.info(label);
277
+ this.publish();
278
+ try {
279
+ return await work(activity);
280
+ }
281
+ catch (e) {
282
+ activity.error = errorMessage(e);
283
+ this.logger.warn(`${label}: ${activity.error}`);
284
+ throw e;
285
+ }
286
+ finally {
287
+ activity.finished = Date.now();
288
+ const now = Date.now();
289
+ this.activity = this.activity
290
+ .filter((a) => a.finished === null || now - a.finished < ACTIVITY_KEPT_MS)
291
+ .slice(0, ACTIVITY_LIMIT);
292
+ this.settingsAt = 0;
293
+ this.publish();
294
+ if (this.listeners.size > 0) {
295
+ void this.refresh();
296
+ }
297
+ }
298
+ }
299
+ async rescan(device) {
300
+ if (this.latest && (!this.latest.wifi || this.latest.error)) {
301
+ return;
302
+ }
303
+ this.scannedAt = Date.now();
304
+ this.scanning = true;
305
+ this.setScanning(true);
306
+ // Refused if asked again too soon, or not allowed: the networks known
307
+ // are shown all the same
308
+ const result = await nmcli(['device', 'wifi', 'rescan', ...(device ? ['ifname', device] : [])]);
309
+ if (result.code !== 0 && result.stderr) {
310
+ this.logger.debug(`Wi-Fi scan: ${result.stderr.trim()}`);
311
+ }
312
+ if (this.scanTimer) {
313
+ clearTimeout(this.scanTimer);
314
+ }
315
+ this.scanTimer = setTimeout(() => {
316
+ this.scanTimer = null;
317
+ this.scanning = false;
318
+ this.setScanning(false);
319
+ void this.refresh();
320
+ }, SCAN_MS);
321
+ }
322
+ setScanning(scanning) {
323
+ if (this.latest && this.latest.scanning !== scanning) {
324
+ this.latest = { ...this.latest, scanning };
325
+ this.emit();
326
+ }
327
+ }
328
+ async secrets(connection) {
329
+ if (connection.kind !== 'wifi' && connection.kind !== 'hotspot') {
330
+ throw new Error(`${connection.name} is not a Wi-Fi network`);
331
+ }
332
+ const output = await run([
333
+ '--show-secrets',
334
+ '-t',
335
+ '-f',
336
+ '802-11-wireless.ssid,802-11-wireless.hidden,802-11-wireless-security.key-mgmt,802-11-wireless-security.psk,802-11-wireless-security.wep-key0,802-1x.password',
337
+ 'connection',
338
+ 'show',
339
+ 'uuid',
340
+ connection.uuid,
341
+ ]);
342
+ const fields = terseBlocks(output)[0] ?? new Map();
343
+ const field = (key) => fields.get(key)?.[0] ?? '';
344
+ const security = keyManagementSecurity(field('802-11-wireless-security.key-mgmt'));
345
+ const password = security === 'wpa-eap'
346
+ ? field('802-1x.password')
347
+ : security === 'wep'
348
+ ? field('802-11-wireless-security.wep-key0')
349
+ : field('802-11-wireless-security.psk');
350
+ this.logger.info(`Showing the password of ${connection.name}`);
351
+ return {
352
+ ssid: field('802-11-wireless.ssid') || connection.ssid,
353
+ security,
354
+ // Not readable: nmcli shows <hidden>
355
+ password: password === '<hidden>' ? '' : password,
356
+ hidden: field('802-11-wireless.hidden') === 'yes',
357
+ };
358
+ }
359
+ // The latest state, with what is being done now
360
+ publish() {
361
+ if (this.latest) {
362
+ this.latest = { ...this.latest, activity: this.activity.map((a) => ({ ...a })) };
363
+ this.emit();
364
+ }
365
+ }
366
+ emit() {
367
+ const latest = this.latest;
368
+ if (!latest) {
369
+ return;
370
+ }
371
+ this.latestJson = JSON.stringify(latest);
372
+ this.listeners.forEach((listener) => listener(latest));
373
+ }
374
+ // Now, then every POLL_PERIOD_MS; sent only when something changed
375
+ async refresh() {
376
+ if (this.reading) {
377
+ this.again = true;
378
+ return;
379
+ }
380
+ if (this.timer) {
381
+ clearTimeout(this.timer);
382
+ this.timer = null;
383
+ }
384
+ this.reading = true;
385
+ let latest;
386
+ try {
387
+ latest = await this.read();
388
+ }
389
+ catch (e) {
390
+ latest = emptyState(`Cannot read the networks: ${errorMessage(e)}`, this.activity);
391
+ }
392
+ this.reading = false;
393
+ if (this.listeners.size === 0) {
394
+ return;
395
+ }
396
+ const json = JSON.stringify(latest);
397
+ if (json !== this.latestJson) {
398
+ this.latest = latest;
399
+ this.emit();
400
+ }
401
+ if (this.again) {
402
+ this.again = false;
403
+ void this.refresh();
404
+ }
405
+ else {
406
+ this.timer = setTimeout(() => void this.refresh(), POLL_PERIOD_MS);
407
+ }
408
+ }
409
+ async read() {
410
+ const activity = this.activity.map((a) => ({ ...a }));
411
+ const general = await nmcli(['-t', '-f', GENERAL_FIELDS, 'general']);
412
+ if (general.missing) {
413
+ return emptyState('nmcli is not installed: the Networking widget needs NetworkManager', activity);
414
+ }
415
+ const status = parseGeneral(general.stdout);
416
+ if (general.code !== 0 || !status.running) {
417
+ const reason = nmcliProblem(general.stderr, 'NetworkManager is not running');
418
+ return emptyState(/not running/i.test(reason) ? 'NetworkManager is not running' : reason, activity);
419
+ }
420
+ const now = Date.now();
421
+ const [devicesRead, listRead, wifiRead, permissionsRead] = await Promise.all([
422
+ nmcli(['-t', '-f', DEVICE_SECTIONS, 'device', 'show']),
423
+ nmcli(['-t', '-f', CONNECTION_FIELDS, 'connection', 'show']),
424
+ status.wifi ? nmcli(['-t', '-f', WIFI_FIELDS, 'device', 'wifi', 'list', '--rescan', 'no']) : null,
425
+ now - this.permissionsAt > PERMISSIONS_PERIOD_MS ? nmcli(['-t', 'general', 'permissions']) : null,
426
+ ]);
427
+ if (permissionsRead?.code === 0) {
428
+ this.permissions = parsePermissions(permissionsRead.stdout);
429
+ this.permissionsAt = now;
430
+ }
431
+ const devices = parseDevices(devicesRead.stdout);
432
+ // The settings of each connection, when the list changes, when monitor
433
+ // says something changed, and every SETTINGS_PERIOD_MS
434
+ const uuids = connectionUuids(listRead.stdout);
435
+ const key = uuids.join(' ');
436
+ if (key !== this.settingsKey || now - this.settingsAt > SETTINGS_PERIOD_MS) {
437
+ const shown = uuids.length > 0
438
+ ? await nmcli(['-t', '-f', CONNECTION_SETTINGS, 'connection', 'show', ...uuids.flatMap((uuid) => ['uuid', uuid])])
439
+ : null;
440
+ this.settings = shown ? settingsBlocks(shown.stdout) : new Map();
441
+ this.settingsKey = key;
442
+ // One deleted meanwhile fails the others: read again next time
443
+ this.settingsAt = shown && shown.code !== 0 ? 0 : now;
444
+ }
445
+ return {
446
+ error: null,
447
+ version: status.version,
448
+ state: status.state,
449
+ connectivity: status.connectivity,
450
+ wifi: status.wifi,
451
+ wifiHardware: status.wifiHardware,
452
+ permissions: this.permissions,
453
+ devices,
454
+ connections: parseConnections(listRead.stdout, this.settings, devices),
455
+ networks: wifiRead ? parseWifiList(wifiRead.stdout) : [],
456
+ scanning: this.scanning,
457
+ activity,
458
+ };
459
+ }
460
+ // nmcli monitor: a line whenever NetworkManager's state changes, for the
461
+ // state to be read again at once
462
+ startMonitor() {
463
+ if (this.monitor || this.listeners.size === 0) {
464
+ return;
465
+ }
466
+ const child = spawn('nmcli', ['monitor'], { stdio: ['ignore', 'pipe', 'ignore'], env: ENV });
467
+ this.monitor = child;
468
+ child.stdout?.on('data', () => {
469
+ this.settingsAt = 0;
470
+ if (this.debounce) {
471
+ clearTimeout(this.debounce);
472
+ }
473
+ this.debounce = setTimeout(() => {
474
+ this.debounce = null;
475
+ void this.refresh();
476
+ }, MONITOR_DEBOUNCE_MS);
477
+ });
478
+ // Not installed: the state says so, and it is polled
479
+ child.on('error', () => { });
480
+ child.on('exit', () => {
481
+ if (this.monitor !== child) {
482
+ return;
483
+ }
484
+ this.monitor = null;
485
+ if (this.listeners.size > 0) {
486
+ this.monitorTimer = setTimeout(() => {
487
+ this.monitorTimer = null;
488
+ this.startMonitor();
489
+ }, MONITOR_RETRY_MS);
490
+ }
491
+ });
492
+ }
493
+ stop() {
494
+ for (const timer of [this.timer, this.monitorTimer, this.debounce, this.scanTimer]) {
495
+ if (timer) {
496
+ clearTimeout(timer);
497
+ }
498
+ }
499
+ this.timer = this.monitorTimer = this.debounce = this.scanTimer = null;
500
+ this.scanning = false;
501
+ const monitor = this.monitor;
502
+ this.monitor = null;
503
+ monitor?.kill();
504
+ // Read anew when a page follows them again
505
+ this.latest = null;
506
+ this.latestJson = '';
507
+ }
508
+ }
@@ -0,0 +1,38 @@
1
+ import { type ConnectionKind, type NetworkConnection, type NetworkDevice, type NetworkPermissions, type NetworkRequest, type NetworkSettings, type WifiNetwork, type WifiSecurity } from '../shared/networking.ts';
2
+ export declare const GENERAL_FIELDS = "RUNNING,VERSION,STATE,CONNECTIVITY,NETWORKING,WIFI-HW,WIFI";
3
+ export declare const DEVICE_SECTIONS = "GENERAL,CAPABILITIES,WIRED-PROPERTIES,WIFI-PROPERTIES,IP4,IP6";
4
+ export declare const CONNECTION_FIELDS = "NAME,UUID,TYPE,TIMESTAMP,AUTOCONNECT,AUTOCONNECT-PRIORITY,READONLY,DEVICE,ACTIVE,STATE,FILENAME";
5
+ export declare const CONNECTION_SETTINGS: string;
6
+ export declare const WIFI_FIELDS = "SSID,SSID-HEX,BSSID,MODE,CHAN,FREQ,RATE,SIGNAL,SECURITY,DEVICE,ACTIVE";
7
+ export declare function terseFields(line: string): string[];
8
+ export declare function terseBlocks(output: string): Map<string, string[]>[];
9
+ export interface GeneralState {
10
+ running: boolean;
11
+ version: string;
12
+ state: string;
13
+ connectivity: string;
14
+ networking: boolean;
15
+ wifiHardware: boolean;
16
+ wifi: boolean;
17
+ }
18
+ export declare function parseGeneral(output: string): GeneralState;
19
+ export declare function parsePermissions(output: string): NetworkPermissions;
20
+ export declare function parseDevices(output: string): NetworkDevice[];
21
+ export declare function connectionKind(type: string, mode: string): ConnectionKind;
22
+ export declare function keyManagementSecurity(keyManagement: string): WifiSecurity;
23
+ export declare function scanSecurity(text: string): WifiSecurity;
24
+ export declare function parseConnections(listOutput: string, settings: ReadonlyMap<string, Map<string, string[]>>, devices: NetworkDevice[]): NetworkConnection[];
25
+ export declare function connectionUuids(listOutput: string): string[];
26
+ export declare function settingsBlocks(output: string): Map<string, Map<string, string[]>>;
27
+ export declare function parseWifiList(output: string): WifiNetwork[];
28
+ export declare function deviceOfAddress(devices: NetworkDevice[], address: string | null | undefined): string | null;
29
+ export declare function nmcliProblem(stderr: string, fallback: string): string;
30
+ export declare function createArgs(settings: NetworkSettings): string[];
31
+ export interface Modification {
32
+ args: string[];
33
+ reactivate: boolean;
34
+ }
35
+ export declare function modification(current: NetworkConnection, settings: NetworkSettings): Modification;
36
+ export declare function passwordArgs(connection: NetworkConnection, password: string): string[];
37
+ export declare function checkSettings(value: unknown, creating: boolean): NetworkSettings;
38
+ export declare function checkRequest(value: unknown): NetworkRequest;