tabby-rdp 0.3.1 → 0.3.2
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/CHANGELOG.md +31 -0
- package/README.md +17 -6
- package/dist/desktop.service.js +94 -6
- package/dist/desktop.service.js.map +1 -1
- package/dist/help.js +13 -5
- package/dist/help.js.map +1 -1
- package/dist/rdcleanpath.js +108 -1
- package/dist/rdcleanpath.js.map +1 -1
- package/dist/remoteSetup.js +78 -4
- package/dist/remoteSetup.js.map +1 -1
- package/dist/settingsPage.js +25 -12
- package/dist/settingsPage.js.map +1 -1
- package/dist/ui.js +2 -0
- package/dist/ui.js.map +1 -1
- package/dist/wake.js +61 -0
- package/dist/wake.js.map +1 -1
- package/package.json +1 -1
- package/vendor/iron-remote-desktop-rdp.js +12 -11
- package/vendor/iron-remote-desktop.js +1081 -1102
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.2
|
|
4
|
+
|
|
5
|
+
**New**
|
|
6
|
+
- **Shut down VMs it started:** a VM the plugin started to open its desktop can go back off after 5 minutes, 15 or an
|
|
7
|
+
hour with no desktop open (a setting; never, by default). A watcher on the SSH host sees to it, so it also works after
|
|
8
|
+
the tab is closed or Tabby has quit.
|
|
9
|
+
|
|
10
|
+
- **A GNOME desktop that's open elsewhere** (another computer, or another Tabby window): opening it asks whether to
|
|
11
|
+
take it over, as Windows does (the other connection is disconnected; the session and its apps carry on), or to open
|
|
12
|
+
a second screen, which is what happened before. A desktop taken over doesn't take itself back.
|
|
13
|
+
|
|
14
|
+
**Better**
|
|
15
|
+
- **The headless GNOME session** runs X11 apps (through XWayland), and GNOME's settings daemons for keyboard, media
|
|
16
|
+
keys and custom shortcuts, accessibility and sound; not power, so it never suspends the machine. A session that's
|
|
17
|
+
already running gets them on the next connect, without closing its apps.
|
|
18
|
+
|
|
19
|
+
**Fixed**
|
|
20
|
+
- **A blank desktop after reconnecting** (GNOME): when the server sent the end of the connection sequence and the start
|
|
21
|
+
of the graphics channel together, IronRDP lost the latter and the desktop stayed transparent. The proxy now hands it
|
|
22
|
+
each server message on its own.
|
|
23
|
+
- **The clipboard with several desktops open:** closing one desktop stopped the automatic clipboard for the others,
|
|
24
|
+
and a desktop in the background could overwrite what was copied on another. The desktop with the focus syncs the
|
|
25
|
+
clipboard; copies made on another reach the Mac when it gets the focus (IronRDP patches 12 and 13).
|
|
26
|
+
- IronRDP patch 14, meant for upstream: H.264 pictures read correctly in Safari, whose `copyTo` ignores the RGBA format.
|
|
27
|
+
- A closed desktop no longer leaves its pane's subscription and its label timer behind.
|
|
28
|
+
- The first-connect tip shows the desktop/console hotkey as text: a label from the config could carry markup into
|
|
29
|
+
the page.
|
|
30
|
+
|
|
31
|
+
**Docs**
|
|
32
|
+
- The limitation about Win+R from macOS is gone: ⌃⌘ with a key has been the Windows key with it since 0.3.0.
|
|
33
|
+
|
|
3
34
|
## 0.3.1
|
|
4
35
|
|
|
5
36
|
**New**
|
package/README.md
CHANGED
|
@@ -247,6 +247,13 @@ console on the host, [above](#use)). A VM found this way asks for its account th
|
|
|
247
247
|
desktop; **Save \<name\> to this host's desktops** in its menu keeps it, for a name of your own and other settings.
|
|
248
248
|
**Settings › Remote Desktop › Find virtual machines on SSH hosts** turns this off.
|
|
249
249
|
|
|
250
|
+
A VM the plugin started can go back off by itself: **Settings › Remote Desktop › Shut down VMs it started** (5
|
|
251
|
+
minutes, 15 or an hour without a desktop open; never, by default). A small watcher on the SSH host sees to it, so it
|
|
252
|
+
also works once the tab is closed or Tabby has quit: after that long with no connection to the VM's desktop through the
|
|
253
|
+
host, it asks the VM to shut down (`virsh shutdown`, with a key press first, since Windows ignores the request while its
|
|
254
|
+
screen sleeps), then ends. VMs that were already running are never touched, and neither are machines woken over the
|
|
255
|
+
network.
|
|
256
|
+
|
|
250
257
|
## desk: the console on the desktop
|
|
251
258
|
|
|
252
259
|
Type `desk` in an SSH console, and the tab switches to that machine's desktop with a terminal attached to the very same
|
|
@@ -281,6 +288,7 @@ They are stored in Tabby's config under `remoteDesktop`:
|
|
|
281
288
|
| Bring the console along with `desk` (`desk`) | Off | Installs `desk` and a login line on each machine you open a desktop on; applies on the next connection there. |
|
|
282
289
|
| Session backend (`sessionBackend`) | `native` | For `desk`: `native` (trd-pty) or `tmux`. Config file only. |
|
|
283
290
|
| Find virtual machines on SSH hosts (`discoverVMs`) | On | Lists a host's libvirt VMs with a desktop in its menu ([VMs on a host](#vms-on-a-host)). |
|
|
291
|
+
| Shut down VMs it started (`shutDownIdle`) | Never | 5, 15 or 60: minutes without a desktop open after which a VM the plugin started is shut down again ([VMs on a host](#vms-on-a-host)). |
|
|
284
292
|
| Tell me about new versions (`checkUpdates`) | On | Once a day, asks npm for the latest tabby-rdp (nothing else is sent), and says so in a note, the menus and the settings page when there's a newer one: Tabby itself shows plugin upgrades only on its Plugins page. |
|
|
285
293
|
| Desktop name overlay (`osd`) | When it helps | `show` (`auto`, `always`, `off`), `font`, `size`, `position`, `color` (empty: white) and `seconds`; the settings page previews it. |
|
|
286
294
|
|
|
@@ -345,17 +353,19 @@ npm run build:ironrdp # rebuild vendor/ from IronRDP and ironrdp/patches:
|
|
|
345
353
|
|
|
346
354
|
## Limitations
|
|
347
355
|
|
|
348
|
-
- **One
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
356
|
+
- **One screen per connection on GNOME.** GNOME Remote Desktop's headless mode can't show the same screen to two
|
|
357
|
+
clients (another computer, or another Tabby window): a second one gets an extra, empty screen. Opening a desktop
|
|
358
|
+
that's open elsewhere asks whether to take it over (the other connection is disconnected, as on Windows; the session
|
|
359
|
+
and its apps carry on) or to open a second screen.
|
|
360
|
+
- **The headless GNOME session is a shell without `gnome-session`.** Apps work, X11 ones too (through XWayland), and so
|
|
361
|
+
do GNOME's settings daemons for keyboard, media keys and custom shortcuts, accessibility and sound. The ones for
|
|
362
|
+
power, sharing and local hardware don't run, so the session never suspends or blanks the machine.
|
|
352
363
|
- **GNOME's login-screen mode** isn't supported: it hands clients over with a server redirection, which IronRDP can't
|
|
353
364
|
follow yet. The plugin uses per-user headless sessions instead.
|
|
354
365
|
- **Remembering a Windows password on Linux** needs an unlocked keyring (GNOME Keyring or KWallet), as it does for
|
|
355
366
|
Tabby's own passwords. Without one, the plugin asks each time.
|
|
356
367
|
- **GNOME Remote Desktop listens on all interfaces** (password-protected, TLS); it has no setting to listen on
|
|
357
368
|
loopback only. See [docs/architecture.md](docs/architecture.md#security-notes) to restrict it.
|
|
358
|
-
- **Windows-key combinations** such as Win+R don't come through from macOS. Tapping ⌘ for the Windows key does.
|
|
359
369
|
- **The microphone** needs the system's permission: macOS asks the first time a remote app records (if it was denied,
|
|
360
370
|
allow Tabby in System Settings › Privacy & Security › Microphone, then restart Tabby), and Windows needs "Let desktop
|
|
361
371
|
apps access your microphone". On GNOME, apps record from GNOME Remote Desktop's "Remoteaudio Source", which is the
|
|
@@ -371,7 +381,8 @@ npm run build:ironrdp # rebuild vendor/ from IronRDP and ironrdp/patches:
|
|
|
371
381
|
far as that xrdp version supports them), sound through the PipeWire or PulseAudio module. Live resize needs xrdp
|
|
372
382
|
0.10 or newer; with 0.9 (Ubuntu 24.04), the desktop keeps its size, scaled to fit, or choose **Reconnect at the new
|
|
373
383
|
size**. xrdp's standard RDP security without TLS (`security_layer=rdp`) isn't supported. `desk` needs GNOME.
|
|
374
|
-
- **Waking a desktop** starts it but never shuts it down again;
|
|
384
|
+
- **Waking a desktop** over the network (Wake-on-LAN) starts it but never shuts it down again; for VMs, see
|
|
385
|
+
[Shut down VMs it started](#vms-on-a-host).
|
|
375
386
|
|
|
376
387
|
## License
|
|
377
388
|
|
package/dist/desktop.service.js
CHANGED
|
@@ -277,6 +277,7 @@ class DesktopSession {
|
|
|
277
277
|
dispose() {
|
|
278
278
|
this.markDisposed();
|
|
279
279
|
this.setVisible(false);
|
|
280
|
+
clearTimeout(this.labelTimer);
|
|
280
281
|
this.container.removeEventListener('focusin', this.reclaimFocus, true);
|
|
281
282
|
this.disposers.forEach(f => f());
|
|
282
283
|
this.indicator?.dispose();
|
|
@@ -328,6 +329,10 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
328
329
|
this.reconnects = new Map();
|
|
329
330
|
/** Split tabs where keys typed on one desktop go to all of them (see DesktopKeyboard). */
|
|
330
331
|
this.broadcast = new WeakSet();
|
|
332
|
+
/** How the pane's next connection to a GNOME desktop that's open elsewhere goes, when that was chosen already. */
|
|
333
|
+
this.openChoices = new Map();
|
|
334
|
+
/** Each pane's name for itself on hosts, for the note a take-over leaves there (see prepareRemoteDesktop). */
|
|
335
|
+
this.clients = new WeakMap();
|
|
331
336
|
}
|
|
332
337
|
/**
|
|
333
338
|
* The remote a desktop opened in the pane now is on: the pane's own, or in an SSH tab where `ssh` was typed at
|
|
@@ -719,7 +724,9 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
719
724
|
}
|
|
720
725
|
session = created;
|
|
721
726
|
this.sessions.set(pane, created);
|
|
722
|
-
pane
|
|
727
|
+
// Released with the session: a pane outlives the desktops opened and closed in it.
|
|
728
|
+
const destroyed = pane.destroyed$.subscribe(() => this.disconnect(pane));
|
|
729
|
+
created.onDispose(() => destroyed.unsubscribe());
|
|
723
730
|
// Tabby focuses the terminal when the pane gets focus (tab switch, split focus); take it back.
|
|
724
731
|
const focused = pane.focused$.subscribe(() => {
|
|
725
732
|
if (created.visible) {
|
|
@@ -908,7 +915,8 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
908
915
|
}
|
|
909
916
|
}
|
|
910
917
|
scheduleReconnect(pane, target, spec, session, message) {
|
|
911
|
-
|
|
918
|
+
// since: when the desktop dropped (see openElsewhere).
|
|
919
|
+
const state = this.reconnects.get(pane) ?? { attempts: 0, since: Date.now() };
|
|
912
920
|
this.reconnects.set(pane, state);
|
|
913
921
|
clearTimeout(state.timer);
|
|
914
922
|
const current = () => this.sessions.get(pane) === session;
|
|
@@ -955,6 +963,7 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
955
963
|
osd: (0, osd_1.osdSettings)(store.osd),
|
|
956
964
|
discoverVMs: store.discoverVMs !== false,
|
|
957
965
|
checkUpdates: store.checkUpdates !== false,
|
|
966
|
+
shutDownIdle: [5, 15, 60].includes(Number(store.shutDownIdle)) ? Number(store.shutDownIdle) : 0,
|
|
958
967
|
};
|
|
959
968
|
}
|
|
960
969
|
/** The sharpness chosen for the pane's desktop in particular (`remoteDesktop.desktopSharpness`), or null. */
|
|
@@ -1371,11 +1380,11 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
1371
1380
|
* it finds xrdp or Windows instead of GNOME, as below. Others: the saved account, or the sign-in form (retryError: after a
|
|
1372
1381
|
* failed sign-in).
|
|
1373
1382
|
*/
|
|
1374
|
-
async endpointFor(target, spec, session, retryError) {
|
|
1383
|
+
async endpointFor(pane, target, spec, session, automatic, retryError) {
|
|
1375
1384
|
// A Windows host is known from an earlier connect: no setup to run there (it would only fail again).
|
|
1376
1385
|
if (spec.id === desktops_1.OWN_DESKTOP && !retryError && this.ownDesktops.get(target.key)?.kind !== 'windows') {
|
|
1377
1386
|
session.status(`Preparing the remote desktop on ${target.label}…`);
|
|
1378
|
-
const endpoint = await (0, remoteSetup_1.prepareRemoteDesktop)(target, this.settings().desk, this.config.store.remoteDesktop?.sessionBackend);
|
|
1387
|
+
const endpoint = await (0, remoteSetup_1.prepareRemoteDesktop)(target, this.settings().desk, this.config.store.remoteDesktop?.sessionBackend, this.clientOf(pane));
|
|
1379
1388
|
this.ownDesktops.set(target.key, { kind: endpoint.kind, xrdpPort: endpoint.xrdpPort, xrdpUser: endpoint.xrdpUser });
|
|
1380
1389
|
// Only the setup knows which desktop the host has; the session follows it (graphics, sign-in, resizing).
|
|
1381
1390
|
spec.kind = endpoint.kind;
|
|
@@ -1383,7 +1392,29 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
1383
1392
|
session.log.push('setup: the SSH host runs Windows; its own desktop is its RDP server');
|
|
1384
1393
|
}
|
|
1385
1394
|
if (endpoint.kind === 'gnome') {
|
|
1386
|
-
|
|
1395
|
+
let gnome = endpoint;
|
|
1396
|
+
const chosen = this.openChoices.get(pane);
|
|
1397
|
+
this.openChoices.delete(pane);
|
|
1398
|
+
// An automatic reconnect also gives way to a take-over whose connection isn't up yet: one that happened
|
|
1399
|
+
// when this desktop dropped (the host says how long ago, so the clocks needn't agree).
|
|
1400
|
+
const dropped = this.reconnects.get(pane)?.since;
|
|
1401
|
+
const displaced = automatic && gnome.takenOver !== undefined && dropped !== undefined &&
|
|
1402
|
+
Math.abs(gnome.takenOver - (Date.now() - dropped) / 1000) < 10;
|
|
1403
|
+
if (gnome.clients || displaced) {
|
|
1404
|
+
const choice = chosen ?? await this.openElsewhere(pane, target, spec, session, automatic);
|
|
1405
|
+
if (!choice) {
|
|
1406
|
+
return 'stopped';
|
|
1407
|
+
}
|
|
1408
|
+
if (choice === 'take') {
|
|
1409
|
+
session.status(`Taking over the desktop on ${target.label}…`);
|
|
1410
|
+
gnome = await (0, remoteSetup_1.prepareRemoteDesktop)(target, this.settings().desk, this.config.store.remoteDesktop?.sessionBackend, this.clientOf(pane), true);
|
|
1411
|
+
session.log.push(`setup: took the desktop over (${endpoint.clients} other connection(s))`);
|
|
1412
|
+
}
|
|
1413
|
+
else {
|
|
1414
|
+
session.log.push(`setup: a screen of its own, besides ${endpoint.clients} other connection(s)`);
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
return { host: '127.0.0.1', port: gnome.port, credentials: { username: gnome.username, password: gnome.password }, remember: false, certificate: gnome.certificate };
|
|
1387
1418
|
}
|
|
1388
1419
|
spec.host = '127.0.0.1';
|
|
1389
1420
|
spec.port = endpoint.port;
|
|
@@ -1426,10 +1457,17 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
1426
1457
|
}
|
|
1427
1458
|
// Signing in to a Windows account can take a few tries; a GNOME desktop the plugin set up can't fail that way.
|
|
1428
1459
|
for (let attempt = 0;; attempt++) {
|
|
1429
|
-
const [asked, rdp] = await Promise.all([endpoint ?? this.endpointFor(target, spec, session, retryError), this.loadIronRDP()]);
|
|
1460
|
+
const [asked, rdp] = await Promise.all([endpoint ?? this.endpointFor(pane, target, spec, session, automatic, retryError), this.loadIronRDP()]);
|
|
1430
1461
|
if (!alive()) {
|
|
1431
1462
|
return;
|
|
1432
1463
|
}
|
|
1464
|
+
if (asked === 'stopped') {
|
|
1465
|
+
// Not connecting: the reason and what to do instead are on the layer already.
|
|
1466
|
+
session.state = 'ended';
|
|
1467
|
+
this.changed$.next();
|
|
1468
|
+
this.cancelReconnect(pane);
|
|
1469
|
+
return;
|
|
1470
|
+
}
|
|
1433
1471
|
if (!asked) {
|
|
1434
1472
|
session.state = 'ended';
|
|
1435
1473
|
this.changed$.next();
|
|
@@ -1518,6 +1556,49 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
1518
1556
|
}
|
|
1519
1557
|
session.log.push(`certificate: SHA-256 ${fingerprint}, as remembered`);
|
|
1520
1558
|
}
|
|
1559
|
+
clientOf(pane) {
|
|
1560
|
+
let id = this.clients.get(pane);
|
|
1561
|
+
if (!id) {
|
|
1562
|
+
id = Math.random().toString(36).slice(2, 12);
|
|
1563
|
+
this.clients.set(pane, id);
|
|
1564
|
+
}
|
|
1565
|
+
return id;
|
|
1566
|
+
}
|
|
1567
|
+
/**
|
|
1568
|
+
* The host's GNOME desktop has RDP clients already (another Tabby window or computer), and GNOME gives each its own
|
|
1569
|
+
* screen. Opened by hand: asks whether to take it over (disconnecting them, as Windows does), or to add a screen.
|
|
1570
|
+
* An automatic reconnect doesn't ask, nor take it back: this connection gave way to the other, most likely by being
|
|
1571
|
+
* taken over from there, and taking it back by itself would have the two take turns. Resolves with the choice, or
|
|
1572
|
+
* null when not connecting (the layer then says so and offers the choices again).
|
|
1573
|
+
*/
|
|
1574
|
+
async openElsewhere(pane, target, spec, session, automatic) {
|
|
1575
|
+
const again = (choice) => () => {
|
|
1576
|
+
this.openChoices.set(pane, choice);
|
|
1577
|
+
this.reopen(pane, spec);
|
|
1578
|
+
};
|
|
1579
|
+
const stopped = (message) => {
|
|
1580
|
+
session.status(message, [
|
|
1581
|
+
{ label: automatic ? 'Take it back' : 'Take it over', run: again('take') },
|
|
1582
|
+
{ label: 'Open a second screen', run: again('second') },
|
|
1583
|
+
], true);
|
|
1584
|
+
return null;
|
|
1585
|
+
};
|
|
1586
|
+
if (automatic) {
|
|
1587
|
+
if (!session.visible) {
|
|
1588
|
+
this.notifications.notice(`The desktop on ${target.label} was opened somewhere else`);
|
|
1589
|
+
}
|
|
1590
|
+
return stopped(`The desktop on ${target.label} was opened somewhere else (another Tabby window or computer), and this connection gave way to it.`);
|
|
1591
|
+
}
|
|
1592
|
+
const choice = await new Promise(resolve => {
|
|
1593
|
+
session.status(`The desktop on ${target.label} is open somewhere else too (another Tabby window or computer). GNOME gives each connection a screen of its own, so this one would get a second, empty screen.`, [
|
|
1594
|
+
{ label: 'Take it over', run: () => resolve('take') },
|
|
1595
|
+
{ label: 'Open a second screen', run: () => resolve('second') },
|
|
1596
|
+
{ label: 'Cancel', run: () => resolve(null) },
|
|
1597
|
+
], true);
|
|
1598
|
+
session.disposed.then(() => resolve(null));
|
|
1599
|
+
});
|
|
1600
|
+
return choice ?? stopped(`Not opened: the desktop on ${target.label} is open somewhere else (another Tabby window or computer).`);
|
|
1601
|
+
}
|
|
1521
1602
|
/**
|
|
1522
1603
|
* After the proxy refused a certificate; nothing was sent to that server. The host's own desktop: an error, since
|
|
1523
1604
|
* the plugin made that certificate itself. Others: both fingerprints, to trust the new certificate or not. Either
|
|
@@ -1619,6 +1700,13 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
1619
1700
|
const up = await this.zone.runOutsideAngular(() => (0, wake_1.waitForRdp)(target, spec.host, spec.port, WAKE_TIMEOUT_MS, () => cancelled || !alive(), progress));
|
|
1620
1701
|
if (up) {
|
|
1621
1702
|
session.log.push(`wake: answered after ${Math.round((Date.now() - started) / 1000)} s`);
|
|
1703
|
+
// Started here, so it can go back off when it's no longer used, if the settings say so (a VM only: a machine
|
|
1704
|
+
// woken over the network would need rights on it).
|
|
1705
|
+
const idle = this.settings().shutDownIdle;
|
|
1706
|
+
if (idle > 0 && 'vm' in spec.wake) {
|
|
1707
|
+
(0, wake_1.shutDownWhenIdle)(target, spec.wake.vm, spec.host, spec.port, idle * 60)
|
|
1708
|
+
.then(done => session.log.push(`wake: ${done}`), e => session.log.push(`wake: no automatic shutdown: ${e?.message ?? e}`));
|
|
1709
|
+
}
|
|
1622
1710
|
return true;
|
|
1623
1711
|
}
|
|
1624
1712
|
if (cancelled || !alive()) {
|