tabby-rdp 0.5.0 → 0.5.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 +516 -0
- package/README.md +216 -58
- package/TROUBLESHOOTING.md +77 -0
- package/dist/accountForm.js +0 -1
- package/dist/accountRevisions.js +46 -0
- package/dist/accounts.js +2 -2
- package/dist/audio.js +0 -1
- package/dist/authorities.js +331 -0
- package/dist/byteQueue.js +102 -0
- package/dist/clipboard.js +106 -0
- package/dist/connectionStatus.js +0 -1
- package/dist/desk.js +16 -47
- package/dist/deskScript.js +196 -6
- package/dist/desktop.service.js +1499 -269
- package/dist/desktopForm.js +53 -18
- package/dist/desktops.js +232 -24
- package/dist/diskStorage.js +212 -0
- package/dist/drives.js +271 -30
- package/dist/fileTransfer.js +960 -132
- package/dist/gateway.js +216 -57
- package/dist/header.js +76 -5
- package/dist/help.js +16 -47
- package/dist/hostCompat.js +86 -0
- package/dist/hyperv.js +65 -15
- package/dist/index.js +7 -4
- package/dist/keyboard.js +0 -1
- package/dist/microphone.js +57 -13
- package/dist/ntlm.js +0 -1
- package/dist/osd.js +56 -15
- package/dist/packageChangeDialog.js +186 -0
- package/dist/probe.js +15 -3
- package/dist/rdcleanpath.js +319 -44
- package/dist/rdpFile.js +22 -7
- package/dist/rdpProfile.js +39 -21
- package/dist/releaseSettings.js +72 -0
- package/dist/releases.js +408 -0
- package/dist/remoteSetup.js +103 -23
- package/dist/sessionLog.js +53 -0
- package/dist/settingsPage.js +148 -122
- package/dist/signin.js +478 -48
- package/dist/ssh.js +99 -18
- package/dist/sshChannelStream.js +40 -6
- package/dist/targets.js +194 -40
- package/dist/ui.js +88 -20
- package/dist/unshowable.js +41 -0
- package/dist/updates.js +79 -56
- package/dist/vms.js +17 -3
- package/dist/wake.js +15 -8
- package/docs/desk.md +157 -0
- package/package.json +14 -4
- package/remote/trd-pty.py +74 -13
- package/vendor/SHA256SUMS +26 -0
- package/vendor/ironrdp_web_bg.wasm +0 -0
- package/dist/accountForm.js.map +0 -1
- package/dist/accounts.js.map +0 -1
- package/dist/audio.js.map +0 -1
- package/dist/connectionStatus.js.map +0 -1
- package/dist/desk.js.map +0 -1
- package/dist/deskScript.js.map +0 -1
- package/dist/desktop.service.js.map +0 -1
- package/dist/desktopForm.js.map +0 -1
- package/dist/desktops.js.map +0 -1
- package/dist/drives.js.map +0 -1
- package/dist/fileTransfer.js.map +0 -1
- package/dist/gateway.js.map +0 -1
- package/dist/header.js.map +0 -1
- package/dist/help.js.map +0 -1
- package/dist/hyperv.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/keyboard.js.map +0 -1
- package/dist/microphone.js.map +0 -1
- package/dist/ntlm.js.map +0 -1
- package/dist/osd.js.map +0 -1
- package/dist/probe.js.map +0 -1
- package/dist/rdcleanpath.js.map +0 -1
- package/dist/rdpFile.js.map +0 -1
- package/dist/rdpProfile.js.map +0 -1
- package/dist/remoteSetup.js.map +0 -1
- package/dist/settingsPage.js.map +0 -1
- package/dist/signin.js.map +0 -1
- package/dist/ssh.js.map +0 -1
- package/dist/sshChannelStream.js.map +0 -1
- package/dist/targets.js.map +0 -1
- package/dist/ui.js.map +0 -1
- package/dist/updates.js.map +0 -1
- package/dist/vms.js.map +0 -1
- package/dist/wake.js.map +0 -1
package/dist/ui.js
CHANGED
|
@@ -17,9 +17,12 @@ exports.settingsMenu = settingsMenu;
|
|
|
17
17
|
const core_1 = require("@angular/core");
|
|
18
18
|
const tabby_core_1 = require("tabby-core");
|
|
19
19
|
const tabby_terminal_1 = require("tabby-terminal");
|
|
20
|
+
const clipboard_1 = require("./clipboard");
|
|
20
21
|
const desktop_service_1 = require("./desktop.service");
|
|
22
|
+
const desktops_1 = require("./desktops");
|
|
21
23
|
const help_1 = require("./help");
|
|
22
24
|
Object.defineProperty(exports, "TOGGLE_HOTKEY", { enumerable: true, get: function () { return help_1.TOGGLE_HOTKEY; } });
|
|
25
|
+
const hyperv_1 = require("./hyperv");
|
|
23
26
|
const keyboard_1 = require("./keyboard");
|
|
24
27
|
const updates_1 = require("./updates");
|
|
25
28
|
const ssh_1 = require("./ssh");
|
|
@@ -48,6 +51,9 @@ let RemoteDesktopConfig = class RemoteDesktopConfig extends tabby_core_1.ConfigP
|
|
|
48
51
|
sharpness: 'standard',
|
|
49
52
|
// `desk`: installs a login hook on remotes (applied the next time a desktop opens there).
|
|
50
53
|
desk: false,
|
|
54
|
+
// The keys `desk` sends on each host, as that host's setup reported them: [{ desktop: <host account>,
|
|
55
|
+
// sha256 }] (a digest, not the key). Requests without the host's key are ignored (see openConsole).
|
|
56
|
+
deskKeys: [],
|
|
51
57
|
// macOS: ⌘ acts as Ctrl on the remote desktop (false: ⌘ is the Windows/Super key).
|
|
52
58
|
macShortcuts: true,
|
|
53
59
|
// Send the characters typed (Unicode) rather than key positions: dead keys, layouts the remote lacks.
|
|
@@ -60,8 +66,12 @@ let RemoteDesktopConfig = class RemoteDesktopConfig extends tabby_core_1.ConfigP
|
|
|
60
66
|
zoom: 'fit',
|
|
61
67
|
// A small indicator on the desktop: throughput, frames per second, SSH round trip, connection path.
|
|
62
68
|
connectionStatus: false,
|
|
63
|
-
// Send the microphone to
|
|
69
|
+
// Send the microphone to a remote desktop that asks for it, as one does while an app there records
|
|
70
|
+
// (applies on the next connect).
|
|
64
71
|
microphone: false,
|
|
72
|
+
// Which ways the clipboard (text, pictures, files) goes, unless a desktop has its own: 'both' | 'fromRemote'
|
|
73
|
+
// (only from the remote desktop to this computer) | 'off' (applies on the next connect; see clipboard.ts).
|
|
74
|
+
clipboard: 'both',
|
|
65
75
|
// More desktops behind SSH hosts (e.g. a Windows VM whose RDP port the host forwards); see desktops.ts.
|
|
66
76
|
desktops: [],
|
|
67
77
|
// Saved accounts desktops can sign in with: [{ id, name, username, domain }]; passwords are in the keychain
|
|
@@ -71,8 +81,17 @@ let RemoteDesktopConfig = class RemoteDesktopConfig extends tabby_core_1.ConfigP
|
|
|
71
81
|
desktopSharpness: [],
|
|
72
82
|
// Folders shared with remote desktops as drives (\\tsclient\<name>): [{ path, name, readOnly }]; see drives.ts.
|
|
73
83
|
sharedFolders: [],
|
|
74
|
-
// Certificates
|
|
84
|
+
// Certificates remembered for desktops and RD Gateways: [{ desktop: <session key, or gateway#host:port>, sha256,
|
|
85
|
+
// authority }]; `authority` true where a certificate authority vouched for it, false where the user trusted
|
|
86
|
+
// it, none where it was trusted on first use (desktops behind hosts). See RemoteDesktopService.checkTrusted.
|
|
75
87
|
trustedCertificates: [],
|
|
88
|
+
// Desktops allowed to sign in without Network Level Authentication ("Send the password anyway"), each with
|
|
89
|
+
// the certificate it was allowed with: [{ desktop: <session key>, sha256 }]. Tabby saves only keys named
|
|
90
|
+
// here (test/unit/config-defaults.ts).
|
|
91
|
+
withoutNla: [],
|
|
92
|
+
// Machines reached with ssh typed in an SSH tab whose desktop was chosen there "from now on" (see remoteFor):
|
|
93
|
+
// [{ via: <that tab's key>, destination }]. Desktop opens such a one at once; others are asked about first.
|
|
94
|
+
nestedSSH: [],
|
|
76
95
|
// The on-screen display naming a desktop for a moment (see osd.ts).
|
|
77
96
|
osd: { show: 'auto', font: 'condensed', size: 'medium', position: 'top-right', color: '', seconds: 2.5 },
|
|
78
97
|
// Look for VMs with a desktop on SSH hosts (libvirt) and offer them in the menus (see vms.ts).
|
|
@@ -85,6 +104,10 @@ let RemoteDesktopConfig = class RemoteDesktopConfig extends tabby_core_1.ConfigP
|
|
|
85
104
|
updateNoted: '',
|
|
86
105
|
// The tip shown the first time a desktop connects (see help.ts) has been shown.
|
|
87
106
|
tipShown: false,
|
|
107
|
+
// The passwords 0.5.0 saved for desktops reached through an RD Gateway, under keys that are now those of the
|
|
108
|
+
// same addresses without one, have been forgotten from the Vault (see
|
|
109
|
+
// RemoteDesktopService.gatewayPasswords; each computer's keychain keeps a mark of its own).
|
|
110
|
+
gatewayPasswordsMigrated: false,
|
|
88
111
|
},
|
|
89
112
|
};
|
|
90
113
|
this.platformDefaults = {
|
|
@@ -114,30 +137,38 @@ function toggleLabel(desktop, pane) {
|
|
|
114
137
|
return desktop.isOpenElsewhere(pane) ? `Switch to ${what} (open in another tab)` : `Open ${what}`;
|
|
115
138
|
}
|
|
116
139
|
/**
|
|
117
|
-
* The host's other desktops (besides the one the toggle acts on): open one here,
|
|
118
|
-
* Then "Add a desktop behind <host>…".
|
|
140
|
+
* The host's other desktops (besides the one the toggle acts on, see RemoteDesktopService.choicesOf): open one here,
|
|
141
|
+
* replacing the current one, a machine's reached through the host included. Then "Add a desktop behind <host>…".
|
|
119
142
|
*/
|
|
120
143
|
function desktopChoices(desktop, pane, targetLabel) {
|
|
121
|
-
const { specs,
|
|
122
|
-
const items = specs.filter(spec => spec
|
|
144
|
+
const { specs, chosen } = desktop.choicesOf(pane);
|
|
145
|
+
const items = specs.filter(spec => !chosen(spec)).map(spec => ({
|
|
123
146
|
label: desktop.isOpenElsewhere(pane, spec) ? `Switch to ${spec.name} (open in another tab)`
|
|
124
147
|
// Found on the host (see vms.ts): say so, and that opening one that's off starts it.
|
|
125
148
|
: spec.found === 'off' ? `Start and open ${spec.name} (VM, shut off)` : spec.found ? `Open ${spec.name} (VM)` : `Open ${spec.name}`,
|
|
126
149
|
click: () => desktop.showDesktop(pane, spec.id),
|
|
127
150
|
}));
|
|
151
|
+
// A host with more VMs than a menu lists: the rest are left out, and that is said rather than hidden.
|
|
152
|
+
if (desktop.vmsCut(pane)) {
|
|
153
|
+
items.push({ label: `Only the first ${hyperv_1.MAX_VMS} VMs found are listed`, enabled: false });
|
|
154
|
+
}
|
|
128
155
|
// Not in a remote desktop tab: it isn't an SSH host.
|
|
129
156
|
if (targetLabel && desktop.hasConsole(pane)) {
|
|
130
157
|
items.push({ label: `Add a desktop behind ${targetLabel}…`, click: () => desktop.addDesktop(pane) });
|
|
131
158
|
}
|
|
132
159
|
return items;
|
|
133
160
|
}
|
|
134
|
-
/**
|
|
161
|
+
/**
|
|
162
|
+
* What can be done with the pane's connected desktop: send keys, view only, its microphone, a screenshot. Shared by
|
|
163
|
+
* the menus.
|
|
164
|
+
*/
|
|
135
165
|
function desktopActions(desktop, keyboard, pane) {
|
|
136
166
|
const spec = desktop.desktopOf(pane);
|
|
137
167
|
if (!spec || !desktop.isConnected(pane)) {
|
|
138
168
|
return [];
|
|
139
169
|
}
|
|
140
170
|
const viewOnly = desktop.isViewOnly(pane);
|
|
171
|
+
const microphoneStopped = desktop.microphoneStoppedFor(pane);
|
|
141
172
|
return [
|
|
142
173
|
{
|
|
143
174
|
label: 'Send keys',
|
|
@@ -150,6 +181,14 @@ function desktopActions(desktop, keyboard, pane) {
|
|
|
150
181
|
checked: viewOnly,
|
|
151
182
|
click: () => desktop.setViewOnly(pane, !viewOnly),
|
|
152
183
|
},
|
|
184
|
+
// With the setting on, every desktop gets the microphone when it asks for it; this one can be left out,
|
|
185
|
+
// reconnects included, and taken back in (see RemoteDesktopService.stopMicrophone).
|
|
186
|
+
...desktop.settings().microphone ? [{
|
|
187
|
+
type: 'checkbox',
|
|
188
|
+
label: 'Send the microphone (when this desktop asks for it)',
|
|
189
|
+
checked: !microphoneStopped,
|
|
190
|
+
click: () => microphoneStopped ? desktop.resumeMicrophone(pane) : desktop.stopMicrophone(pane),
|
|
191
|
+
}] : [],
|
|
153
192
|
{ label: 'Save a screenshot (to Downloads and the clipboard)', click: () => desktop.saveScreenshot(pane) },
|
|
154
193
|
];
|
|
155
194
|
}
|
|
@@ -197,19 +236,23 @@ let RemoteDesktopContextMenu = class RemoteDesktopContextMenu extends tabby_core
|
|
|
197
236
|
if (this.desktop.isConnected(pane)) {
|
|
198
237
|
items.push({ label: 'Send files…', click: () => this.desktop.sendFiles(pane) });
|
|
199
238
|
items.push(...desktopActions(this.desktop, this.keyboard, pane));
|
|
200
|
-
// Several desktops in this tab: paste on all of them, or type into all of them.
|
|
239
|
+
// Several desktops showing in this tab: paste on all of them, or type into all of them. Typing into all, once
|
|
240
|
+
// on, stays here to be turned off while fewer of them show (those hidden get nothing: see
|
|
241
|
+
// RemoteDesktopService.takesAll).
|
|
201
242
|
const all = this.desktop.desktopsInTab(pane);
|
|
202
243
|
if (all > 1) {
|
|
203
244
|
items.push({ label: `Paste to all ${all} desktops in this tab`, click: () => this.desktop.pasteToAll(pane) });
|
|
245
|
+
}
|
|
246
|
+
if (all > 1 || this.desktop.isBroadcast(pane)) {
|
|
204
247
|
items.push({
|
|
205
248
|
type: 'checkbox',
|
|
206
|
-
label: `Type into all ${all} desktops in this tab
|
|
249
|
+
label: all > 1 ? `Type into all ${all} desktops in this tab` : 'Type into all desktops in this tab',
|
|
207
250
|
checked: this.desktop.isBroadcast(pane),
|
|
208
251
|
click: () => this.desktop.setBroadcast(pane, !this.desktop.isBroadcast(pane)),
|
|
209
252
|
});
|
|
210
253
|
}
|
|
211
254
|
}
|
|
212
|
-
if (this.desktop.
|
|
255
|
+
if (this.desktop.canSaveFoundDesktop(pane)) {
|
|
213
256
|
items.push({ label: `Save ${this.desktop.desktopOf(pane).name} to this host's desktops`, click: () => this.desktop.saveFoundDesktop(pane) });
|
|
214
257
|
}
|
|
215
258
|
if (this.desktop.canSignInAgain(pane)) {
|
|
@@ -256,6 +299,11 @@ function settingsMenu(desktop, pane, help) {
|
|
|
256
299
|
// The desktop open in this pane can have its own sharpness (for example, Retina for Windows only).
|
|
257
300
|
const spec = pane ? desktop.desktopOf(pane) : null;
|
|
258
301
|
const own = pane ? desktop.ownSharpness(pane) : null;
|
|
302
|
+
// Narrowed while it was open, by another way of opening it (see DesktopSession.clipboardCap): said only where that
|
|
303
|
+
// is narrower than what the desktop would have otherwise, its own setting or this one.
|
|
304
|
+
const capOf = pane ? desktop.clipboardCapOf(pane) : 'both';
|
|
305
|
+
const otherwise = spec?.clipboard ?? current.clipboard;
|
|
306
|
+
const cap = (0, clipboard_1.narrowest)(capOf, otherwise) !== otherwise ? (0, clipboard_1.narrowest)(capOf, otherwise) : 'both';
|
|
259
307
|
const ownRadio = (value, label) => ({
|
|
260
308
|
type: 'radio',
|
|
261
309
|
label,
|
|
@@ -268,6 +316,12 @@ function settingsMenu(desktop, pane, help) {
|
|
|
268
316
|
ownRadio('standard', 'Standard'),
|
|
269
317
|
ownRadio('retina', 'Retina'),
|
|
270
318
|
] : [];
|
|
319
|
+
// The configured desktops, each with its place in the list (what editing and removing take); an item that is no
|
|
320
|
+
// entry describes no desktop, and isn't listed. Shown as text whatever an entry holds (see entryText): this menu is
|
|
321
|
+
// part of every SSH tab's, which Tabby builds from every plugin's items at once, so one entry that can't be shown
|
|
322
|
+
// would take Tabby's own items too.
|
|
323
|
+
const entries = desktop.configuredDesktops().flatMap((d, i) => (0, desktops_1.isDesktopEntry)(d) ? [{ index: i, ...(0, desktops_1.entryText)(d) }] : []);
|
|
324
|
+
const entryLabel = (e) => `${e.name}${e.via ? ` (behind ${e.via})` : ''}…`;
|
|
271
325
|
return [
|
|
272
326
|
...help ? [
|
|
273
327
|
{ label: 'All settings, keys and help…', click: () => help.open() },
|
|
@@ -304,10 +358,25 @@ function settingsMenu(desktop, pane, help) {
|
|
|
304
358
|
},
|
|
305
359
|
{
|
|
306
360
|
type: 'checkbox',
|
|
307
|
-
label: 'Send the microphone
|
|
361
|
+
label: 'Send the microphone to remote desktops that ask for it (on applies on the next connect, off at once)',
|
|
308
362
|
checked: current.microphone,
|
|
309
363
|
click: () => desktop.updateSettings({ microphone: !current.microphone }),
|
|
310
364
|
},
|
|
365
|
+
{
|
|
366
|
+
// A desktop's own (its profile, or its edit form) takes the place of this: the one open here says so.
|
|
367
|
+
label: 'Clipboard (applies on the next connect)',
|
|
368
|
+
submenu: [
|
|
369
|
+
// Its own: from its profile (or that profile's group's or type's defaults), or its entry behind a host.
|
|
370
|
+
...spec?.clipboard ? [
|
|
371
|
+
{ label: `${spec.name} doesn't follow this: its profile or its own settings say ${clipboard_1.CLIPBOARD_LABELS[spec.clipboard].toLowerCase()}`, enabled: false },
|
|
372
|
+
] : [],
|
|
373
|
+
...cap !== 'both' ? [
|
|
374
|
+
{ label: `Clipboard sharing with ${spec?.name ?? 'this desktop'} is ${clipboard_1.CLIPBOARD_LABELS[cap].toLowerCase()} until it is closed, as another way of opening it asked`, enabled: false },
|
|
375
|
+
] : [],
|
|
376
|
+
...spec?.clipboard || cap !== 'both' ? [{ type: 'separator' }] : [],
|
|
377
|
+
...Object.entries(clipboard_1.CLIPBOARD_LABELS).map(([mode, label]) => radio('clipboard', mode, label)),
|
|
378
|
+
],
|
|
379
|
+
},
|
|
311
380
|
{
|
|
312
381
|
type: 'checkbox',
|
|
313
382
|
label: 'Show connection status on the desktop (throughput, frames per second, round trip)',
|
|
@@ -331,18 +400,18 @@ function settingsMenu(desktop, pane, help) {
|
|
|
331
400
|
{
|
|
332
401
|
// The form shows over a console (a remote desktop tab edits its desktop in Tabby's profile settings).
|
|
333
402
|
label: 'Edit a desktop',
|
|
334
|
-
enabled: !!pane && desktop.hasConsole(pane) &&
|
|
335
|
-
submenu:
|
|
336
|
-
label:
|
|
337
|
-
click: () => pane && desktop.editDesktop(pane,
|
|
403
|
+
enabled: !!pane && desktop.hasConsole(pane) && entries.length > 0,
|
|
404
|
+
submenu: entries.map(e => ({
|
|
405
|
+
label: entryLabel(e),
|
|
406
|
+
click: () => pane && desktop.editDesktop(pane, e.index),
|
|
338
407
|
})),
|
|
339
408
|
},
|
|
340
409
|
{
|
|
341
410
|
label: 'Remove a desktop',
|
|
342
|
-
enabled:
|
|
343
|
-
submenu:
|
|
344
|
-
label:
|
|
345
|
-
click: () => desktop.confirmRemoveDesktop(
|
|
411
|
+
enabled: entries.length > 0,
|
|
412
|
+
submenu: entries.map(e => ({
|
|
413
|
+
label: entryLabel(e),
|
|
414
|
+
click: () => desktop.confirmRemoveDesktop(e.index),
|
|
346
415
|
})),
|
|
347
416
|
},
|
|
348
417
|
];
|
|
@@ -400,4 +469,3 @@ exports.RemoteDesktopToolbarButton = RemoteDesktopToolbarButton = __decorate([
|
|
|
400
469
|
(0, core_1.Injectable)(),
|
|
401
470
|
__metadata("design:paramtypes", [desktop_service_1.RemoteDesktopService, core_1.NgZone])
|
|
402
471
|
], RemoteDesktopToolbarButton);
|
|
403
|
-
//# sourceMappingURL=ui.js.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.UNSHOWABLE = void 0;
|
|
4
|
+
exports.withoutWordJoiners = withoutWordJoiners;
|
|
5
|
+
exports.showable = showable;
|
|
6
|
+
/**
|
|
7
|
+
* A character that can change how the text around it reads without showing itself, in a name someone else chose (an
|
|
8
|
+
* .rdp file's, see RemoteDesktopService.importRdp; an `ssh` destination a host reports, see RemoteTargets.nestedSSH;
|
|
9
|
+
* what a server's certificate says of itself): control and format characters (bidi overrides and isolates, zero-width
|
|
10
|
+
* ones, tags), the other default-ignorable ones (variation selectors, the combining grapheme joiner, Hangul fillers),
|
|
11
|
+
* line and paragraph separators, private-use and unpaired surrogate code points, and blanks other than the space
|
|
12
|
+
* (no-break and wide spaces, the braille blank).
|
|
13
|
+
*/
|
|
14
|
+
exports.UNSHOWABLE = /[\p{Cc}\p{Cf}\p{Co}\p{Cs}\p{Zl}\p{Zp}\p{Default_Ignorable_Code_Point}⠀]|(?! )\p{Zs}/u;
|
|
15
|
+
/**
|
|
16
|
+
* A zero-width joiner or non-joiner between two letters of a script that writes words with them: Arabic (as Persian
|
|
17
|
+
* does) and the Indic scripts. There it can change how the letters join, and so show; between letters of other scripts
|
|
18
|
+
* (Latin, say) it shows nothing at all, and `ad\u200dmin` reads as `admin`.
|
|
19
|
+
*/
|
|
20
|
+
const WORD_JOINERS = new RegExp(['Arabic', 'Devanagari', 'Bengali', 'Gurmukhi', 'Gujarati', 'Oriya', 'Tamil', 'Telugu', 'Kannada', 'Malayalam', 'Sinhala']
|
|
21
|
+
.map(script => `(?<=[\\p{L}\\p{M}])(?<=\\p{scx=${script}})[\\u200c\\u200d](?=[\\p{L}\\p{M}])(?=\\p{scx=${script}})`).join('|'), 'gu');
|
|
22
|
+
/** `text` without the joiners its scripts write words with (see WORD_JOINERS): what UNSHOWABLE then finds doesn't show. */
|
|
23
|
+
function withoutWordJoiners(text) {
|
|
24
|
+
return text.replace(WORD_JOINERS, '');
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Text someone else chose, as it is shown and logged: what could reorder or hide part of it (UNSHOWABLE) as U+FFFD, and
|
|
28
|
+
* at most `max` characters of it, an ellipsis saying where it was cut.
|
|
29
|
+
*/
|
|
30
|
+
function showable(text, max = Infinity) {
|
|
31
|
+
const shown = text.replace(new RegExp(exports.UNSHOWABLE.source, 'gu'), '�');
|
|
32
|
+
if (shown.length <= max) {
|
|
33
|
+
return shown;
|
|
34
|
+
}
|
|
35
|
+
// Not between the two halves of a character outside the BMP, which would leave half of one.
|
|
36
|
+
let end = Math.max(0, max - 1);
|
|
37
|
+
if (end > 0 && /[\ud800-\udbff]/.test(shown[end - 1])) {
|
|
38
|
+
end--;
|
|
39
|
+
}
|
|
40
|
+
return `${shown.slice(0, end)}…`;
|
|
41
|
+
}
|
package/dist/updates.js
CHANGED
|
@@ -9,56 +9,95 @@ var __metadata = (this && this.__metadata) || function (k, v) {
|
|
|
9
9
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
-
exports.UpdateCheck = void 0;
|
|
12
|
+
exports.UpdateCheck = exports.newer = void 0;
|
|
13
13
|
exports.installedVersion = installedVersion;
|
|
14
|
-
exports.
|
|
14
|
+
exports.latestVersion = latestVersion;
|
|
15
|
+
const fs_1 = require("fs");
|
|
16
|
+
const path_1 = require("path");
|
|
15
17
|
const core_1 = require("@angular/core");
|
|
16
18
|
const rxjs_1 = require("rxjs");
|
|
17
19
|
const tabby_core_1 = require("tabby-core");
|
|
18
20
|
const help_1 = require("./help");
|
|
19
21
|
const targets_1 = require("./targets");
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
+
const hostCompat_1 = require("./hostCompat");
|
|
23
|
+
const packageChangeDialog_1 = require("./packageChangeDialog");
|
|
24
|
+
const releases_1 = require("./releases");
|
|
25
|
+
var releases_2 = require("./releases");
|
|
26
|
+
Object.defineProperty(exports, "newer", { enumerable: true, get: function () { return releases_2.newer; } });
|
|
22
27
|
const DAY = 24 * 60 * 60 * 1000;
|
|
23
|
-
|
|
24
|
-
|
|
28
|
+
// Capture the running package before an installer replaces its files on disk.
|
|
29
|
+
const PACKAGE = require('../package.json');
|
|
30
|
+
const RUNNING = PACKAGE.version;
|
|
31
|
+
function installedVersion() { return RUNNING; }
|
|
32
|
+
/** Kept for callers needing only npm's latest version, with the same response bounds. */
|
|
33
|
+
async function latestVersion(url = 'https://registry.npmjs.org/tabby-rdp/latest', timeoutMs = 15000, maxBytes = 256 * 1024) {
|
|
34
|
+
return String((await (0, releases_1.registryJSON)(url, timeoutMs, maxBytes))?.version ?? '');
|
|
35
|
+
}
|
|
36
|
+
function localStore() {
|
|
25
37
|
try {
|
|
26
|
-
return
|
|
38
|
+
return window.localStorage;
|
|
27
39
|
}
|
|
28
40
|
catch {
|
|
29
|
-
return
|
|
41
|
+
return undefined;
|
|
30
42
|
}
|
|
31
43
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const x = parse(a), y = parse(b);
|
|
36
|
-
if (!x || !y) {
|
|
37
|
-
return false;
|
|
44
|
+
function hasPreferences(storage) {
|
|
45
|
+
try {
|
|
46
|
+
return typeof JSON.parse(storage?.getItem(releases_1.RELEASE_STATE_KEY) ?? 'null')?.paused === 'boolean';
|
|
38
47
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
return x[i] > y[i];
|
|
42
|
-
}
|
|
48
|
+
catch {
|
|
49
|
+
return false;
|
|
43
50
|
}
|
|
44
|
-
return false;
|
|
45
51
|
}
|
|
46
|
-
/**
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
/** Daily, opt-out update notes; explicit package changes are handled by ReleaseManager. */
|
|
53
|
+
let UpdateCheck = class UpdateCheck extends releases_1.ReleaseManager {
|
|
54
|
+
constructor(config, app, help, platform, zone, compat) {
|
|
55
|
+
const storage = localStore();
|
|
56
|
+
const migratePreferences = !hasPreferences(storage);
|
|
57
|
+
const dialog = new packageChangeDialog_1.PackageChangeDialog();
|
|
58
|
+
super({ running: RUNNING, policy: PACKAGE.tabbyRdp, tabby: compat.info.version ?? '', node: process.versions.node,
|
|
59
|
+
os: process.platform, cpu: process.arch, storage, fetchCatalog: releases_1.publishedVersions,
|
|
60
|
+
withLock: async (run) => {
|
|
61
|
+
if (!navigator.locks) {
|
|
62
|
+
throw new Error('This Tabby cannot coordinate version changes between windows. Use Tabby Plugins.');
|
|
63
|
+
}
|
|
64
|
+
await navigator.locks.request('tabby-rdp.package-change', run);
|
|
65
|
+
},
|
|
66
|
+
diskVersion: () => {
|
|
67
|
+
try {
|
|
68
|
+
return JSON.parse((0, fs_1.readFileSync)((0, path_1.join)(__dirname, '..', 'package.json'), 'utf8')).version;
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
if (error.code === 'ENOENT') {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
install: (name, version) => platform.installPlugin(name, version), uninstall: name => platform.uninstallPlugin(name),
|
|
78
|
+
confirm: (message, detail, action) => dialog.confirm(message, detail, action),
|
|
79
|
+
progress: state => dialog.update(state), }, !config.store || config.store.remoteDesktop?.checkUpdates === false);
|
|
54
80
|
this.config = config;
|
|
55
81
|
this.app = app;
|
|
56
82
|
this.help = help;
|
|
57
83
|
this.platform = platform;
|
|
58
84
|
this.zone = zone;
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
85
|
+
// Tabby can construct providers before ConfigService has loaded its store.
|
|
86
|
+
// Keep checks paused until ready, and migrate the old switch only without a saved local preference.
|
|
87
|
+
if (migratePreferences) {
|
|
88
|
+
const migrate = () => { void this.setPaused(config.store?.remoteDesktop?.checkUpdates === false); };
|
|
89
|
+
if (config.store) {
|
|
90
|
+
migrate();
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
config.ready$.pipe((0, rxjs_1.take)(1)).subscribe(migrate);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
window.addEventListener('storage', event => {
|
|
97
|
+
if (event.key === releases_1.RELEASE_STATE_KEY) {
|
|
98
|
+
this.zone.run(() => this.syncFromStorage());
|
|
99
|
+
}
|
|
100
|
+
});
|
|
62
101
|
}
|
|
63
102
|
start() {
|
|
64
103
|
this.zone.runOutsideAngular(() => {
|
|
@@ -66,41 +105,26 @@ let UpdateCheck = class UpdateCheck {
|
|
|
66
105
|
this.timer = setInterval(() => this.check().catch(() => null), DAY);
|
|
67
106
|
});
|
|
68
107
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
/** Asks npm for the latest version (or takes `latest`, for tests), and tells about it if it's newer. */
|
|
73
|
-
async check(latest) {
|
|
74
|
-
if (!this.enabled && latest === undefined) {
|
|
108
|
+
async check() {
|
|
109
|
+
if (this.state.paused) {
|
|
75
110
|
return null;
|
|
76
111
|
}
|
|
77
|
-
|
|
112
|
+
await this.refreshCatalog();
|
|
78
113
|
this.zone.run(() => {
|
|
79
|
-
this.available = newer(version, installedVersion()) ? version : null;
|
|
80
|
-
this.changed$.next();
|
|
81
114
|
if (this.available) {
|
|
82
115
|
this.noteOnce(this.available);
|
|
83
116
|
}
|
|
84
117
|
});
|
|
85
118
|
return this.available;
|
|
86
119
|
}
|
|
87
|
-
/**
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
this.check().catch(() => null);
|
|
120
|
+
/** Opening Updates loads published versions even when ordinary prompts are paused. */
|
|
121
|
+
refresh() { void this.refreshCatalog(); }
|
|
122
|
+
upgrade() { this.help.open('updates'); }
|
|
123
|
+
whatsNew(version = this.available ?? this.running) {
|
|
124
|
+
if ((0, releases_1.releaseVersion)(version)) {
|
|
125
|
+
this.platform.openExternal(`${releases_1.RELEASES}/tag/v${version}`);
|
|
94
126
|
}
|
|
95
127
|
}
|
|
96
|
-
/** Settings › Plugins, where Tabby's Upgrade button is. */
|
|
97
|
-
upgrade() {
|
|
98
|
-
this.help.openSettings('plugins');
|
|
99
|
-
}
|
|
100
|
-
/** The release notes of the newer version. */
|
|
101
|
-
whatsNew() {
|
|
102
|
-
this.platform.openExternal(`${RELEASES}/tag/v${this.available ?? ''}`);
|
|
103
|
-
}
|
|
104
128
|
/** A note over the active pane, once per version (then the menus and the settings page keep saying it). */
|
|
105
129
|
noteOnce(version) {
|
|
106
130
|
const store = this.config.store.remoteDesktop;
|
|
@@ -127,6 +151,5 @@ let UpdateCheck = class UpdateCheck {
|
|
|
127
151
|
exports.UpdateCheck = UpdateCheck;
|
|
128
152
|
exports.UpdateCheck = UpdateCheck = __decorate([
|
|
129
153
|
(0, core_1.Injectable)({ providedIn: 'root' }),
|
|
130
|
-
__metadata("design:paramtypes", [tabby_core_1.ConfigService, tabby_core_1.AppService, help_1.RemoteDesktopHelp, tabby_core_1.PlatformService, core_1.NgZone])
|
|
154
|
+
__metadata("design:paramtypes", [tabby_core_1.ConfigService, tabby_core_1.AppService, help_1.RemoteDesktopHelp, tabby_core_1.PlatformService, core_1.NgZone, hostCompat_1.HostCompatibility])
|
|
131
155
|
], UpdateCheck);
|
|
132
|
-
//# sourceMappingURL=updates.js.map
|
package/dist/vms.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.scanVMs = scanVMs;
|
|
4
4
|
exports.vmSpec = vmSpec;
|
|
5
|
+
const net_1 = require("net");
|
|
5
6
|
const desktops_1 = require("./desktops");
|
|
6
7
|
const hyperv_1 = require("./hyperv");
|
|
7
8
|
/**
|
|
@@ -62,17 +63,21 @@ done
|
|
|
62
63
|
* The host's VMs that have (or, being off, may have) a desktop: libvirt's running ones whose RDP answers, and Windows
|
|
63
64
|
* ones that are off; on a Windows host, Hyper-V's. `host.windows` says the host is known to run Windows, and is set
|
|
64
65
|
* when the scan finds out: such a host isn't asked for libvirt again (its shell takes a while to say it has no sh).
|
|
66
|
+
* `host.cut` says whether the list stopped at MAX_VMS (each VM is a menu item) with more there, so that the menu can
|
|
67
|
+
* say only so many are listed.
|
|
65
68
|
*/
|
|
66
69
|
async function scanVMs(target, host = {}) {
|
|
67
70
|
const out = host.windows ? '' : await target.exec('sh -s', SCAN_SCRIPT);
|
|
68
71
|
// No sh answered: a Windows host (cmd.exe or PowerShell as its SSH shell), which is where Hyper-V is. Asked only
|
|
69
72
|
// then, so that a Linux host costs nothing extra.
|
|
70
73
|
if (!/^TRD_SH$/m.test(out)) {
|
|
71
|
-
const vms = await (0, hyperv_1.scanHyperV)(target);
|
|
74
|
+
const vms = await (0, hyperv_1.scanHyperV)(target, host);
|
|
72
75
|
host.windows = true;
|
|
73
76
|
return vms.map(vm => ({ hyperv: vm.id, name: vm.name, state: vm.state, windows: true, address: '', rdp: false }));
|
|
74
77
|
}
|
|
75
78
|
const found = [];
|
|
79
|
+
// Names seen so far: looked up, not searched for, so that the time taken grows with the lines, not their square.
|
|
80
|
+
const names = new Set();
|
|
76
81
|
for (const line of out.split('\n')) {
|
|
77
82
|
const m = /^TRD_VM ([^|]+)\|([^|]*)\|([^|]*)\|([^|]*)\|(\d*)\s*$/.exec(line.trim());
|
|
78
83
|
if (!m) {
|
|
@@ -83,10 +88,20 @@ async function scanVMs(target, host = {}) {
|
|
|
83
88
|
// Windows: RDP answering, or off (started when opened). Others: only xrdp (see XRDP_LIKE).
|
|
84
89
|
const rdp = windows ? protocol !== '' : protocol === XRDP_LIKE;
|
|
85
90
|
const vm = { name, state: state === 'running' ? 'running' : 'off', windows, address, rdp };
|
|
86
|
-
|
|
91
|
+
// An IPv4 address, as the scan only ever finds: the address is part of the VM's session key, which is also the
|
|
92
|
+
// key its saved password is kept under. Anything else (`10.0.0.5:3389@gateway#gw.example`) could make that the
|
|
93
|
+
// key of a desktop configured behind this host, a password saved for its gateway included (see keyParts).
|
|
94
|
+
if ((0, net_1.isIPv4)(vm.address) && (vm.rdp || vm.state === 'off' && vm.windows) && !names.has(vm.name)) {
|
|
95
|
+
// One more than are listed: the list is cut short.
|
|
96
|
+
if (found.length === hyperv_1.MAX_VMS) {
|
|
97
|
+
host.cut = true;
|
|
98
|
+
return found;
|
|
99
|
+
}
|
|
100
|
+
names.add(vm.name);
|
|
87
101
|
found.push(vm);
|
|
88
102
|
}
|
|
89
103
|
}
|
|
104
|
+
host.cut = false;
|
|
90
105
|
return found;
|
|
91
106
|
}
|
|
92
107
|
/** A found VM as a desktop behind its host: started by name when it's off (see wake.ts). */
|
|
@@ -104,4 +119,3 @@ function vmSpec(vm) {
|
|
|
104
119
|
found: vm.state,
|
|
105
120
|
};
|
|
106
121
|
}
|
|
107
|
-
//# sourceMappingURL=vms.js.map
|
package/dist/wake.js
CHANGED
|
@@ -9,6 +9,7 @@ exports.wakeDesktop = wakeDesktop;
|
|
|
9
9
|
exports.waitForRdp = waitForRdp;
|
|
10
10
|
const deskScript_1 = require("./deskScript");
|
|
11
11
|
const hyperv_1 = require("./hyperv");
|
|
12
|
+
const osd_1 = require("./osd");
|
|
12
13
|
const MAC = /^[0-9a-f]{2}([:-]?)[0-9a-f]{2}(\1[0-9a-f]{2}){4}$/i;
|
|
13
14
|
const IPV4 = /^\d{1,3}(\.\d{1,3}){3}$/;
|
|
14
15
|
/** A `wake` config value, checked; undefined when absent or unusable. */
|
|
@@ -21,7 +22,7 @@ function parseWake(raw) {
|
|
|
21
22
|
}
|
|
22
23
|
const mac = typeof raw?.mac === 'string' ? raw.mac.trim() : '';
|
|
23
24
|
if (MAC.test(mac)) {
|
|
24
|
-
const port =
|
|
25
|
+
const port = (0, osd_1.configNumber)(raw.port ?? 9);
|
|
25
26
|
return {
|
|
26
27
|
mac,
|
|
27
28
|
...typeof raw.broadcast === 'string' && IPV4.test(raw.broadcast.trim()) ? { broadcast: raw.broadcast.trim() } : {},
|
|
@@ -98,11 +99,13 @@ const IDLE_SCRIPT = String.raw `
|
|
|
98
99
|
command -v virsh >/dev/null 2>&1 || { echo "RD_ERR virsh is not installed on the SSH host"; exit 0; }
|
|
99
100
|
command -v ss >/dev/null 2>&1 || { echo "RD_ERR ss (iproute2) is not installed on the SSH host"; exit 0; }
|
|
100
101
|
# The pid file: with the plugin's other files, or where the home folder isn't writable, in a folder of its own in /tmp.
|
|
102
|
+
# Private either way, as the desktop's setup keeps it (this may be the first to make it).
|
|
101
103
|
D="$HOME/.local/share/tabby-rdp"
|
|
102
|
-
if ! mkdir -p "$D" 2>/dev/null; then
|
|
104
|
+
if ! { mkdir -p "$D" && chmod 700 "$D"; } 2>/dev/null; then
|
|
103
105
|
D="/tmp/tabby-rdp-$(id -u)"
|
|
104
106
|
mkdir -p -m 700 "$D" 2>/dev/null
|
|
105
|
-
|
|
107
|
+
# One that was there already keeps its mode (mkdir -m only sets one it makes): made private here, or not used.
|
|
108
|
+
[ -d "$D" ] && [ -O "$D" ] && [ ! -L "$D" ] && chmod 700 "$D" 2>/dev/null || { echo "RD_ERR could not create a folder for the watcher's pid file"; exit 0; }
|
|
106
109
|
fi
|
|
107
110
|
PIDF="$D/idle-$(printf %s "$VM" | tr -c 'A-Za-z0-9._-' _).pid"
|
|
108
111
|
if [ -s "$PIDF" ] && kill -0 "$(cat "$PIDF")" 2>/dev/null; then
|
|
@@ -114,8 +117,11 @@ for u in qemu:///system qemu:///session; do
|
|
|
114
117
|
done
|
|
115
118
|
[ -n "$URI" ] || { echo "RD_ERR $VM is not running"; exit 0; }
|
|
116
119
|
A=$(getent ahostsv4 "$H" 2>/dev/null | awk 'NR == 1 { print $1 }')
|
|
120
|
+
# The watcher writes its pid file anew (umask 077): one left by an earlier version keeps its mode otherwise.
|
|
121
|
+
rm -f "$PIDF"
|
|
117
122
|
nohup setsid sh -c '
|
|
118
123
|
VM=$1 URI=$2 A=$3 P=$4 IDLE=$5 PIDF=$6
|
|
124
|
+
umask 077
|
|
119
125
|
echo $$ > "$PIDF"
|
|
120
126
|
trap "rm -f \"$PIDF\"" EXIT
|
|
121
127
|
idle=0 asked=0
|
|
@@ -143,11 +149,12 @@ echo "RD_OK $VM shuts down after $((IDLE / 60)) min without a desktop open ($URI
|
|
|
143
149
|
*/
|
|
144
150
|
async function shutDownWhenIdle(target, vm, host, port, idleSeconds) {
|
|
145
151
|
const out = await target.exec('sh -s', `VM=${(0, deskScript_1.shq)(vm)}\nH=${(0, deskScript_1.shq)(host)}\nP=${port}\nIDLE=${Math.max(1, Math.round(idleSeconds / 30)) * 30}\n${IDLE_SCRIPT}`);
|
|
146
|
-
|
|
152
|
+
// The host's words, as much of them as the log shows: the host decides how long its lines are.
|
|
153
|
+
const err = /^RD_ERR (.{0,1000})/m.exec(out);
|
|
147
154
|
if (err) {
|
|
148
155
|
throw new Error(err[1].trim());
|
|
149
156
|
}
|
|
150
|
-
return /^RD_OK (
|
|
157
|
+
return /^RD_OK (.{0,1000})/m.exec(out)?.[1] ?? 'no result';
|
|
151
158
|
}
|
|
152
159
|
/** Whether the RDP server at host:port answers, as seen from the SSH host. */
|
|
153
160
|
async function rdpAnswers(target, host, port) {
|
|
@@ -167,11 +174,12 @@ async function wakeDesktop(target, wake) {
|
|
|
167
174
|
? `VM=${(0, deskScript_1.shq)(wake.vm)}\n${VM_SCRIPT}`
|
|
168
175
|
: `MAC=${(0, deskScript_1.shq)(wake.mac)}\nBCAST=${(0, deskScript_1.shq)(wake.broadcast ?? '255.255.255.255')}\nPORT=${wake.port ?? 9}\n${WOL_SCRIPT}`;
|
|
169
176
|
const out = await target.exec('sh -s', script);
|
|
170
|
-
|
|
177
|
+
// The host's words, as much of them as a status line shows (see shutDownWhenIdle).
|
|
178
|
+
const err = /^RD_ERR (.{0,1000})/m.exec(out);
|
|
171
179
|
if (err) {
|
|
172
180
|
throw new Error(err[1].trim());
|
|
173
181
|
}
|
|
174
|
-
return /^RD_OK (
|
|
182
|
+
return /^RD_OK (.{0,1000})/m.exec(out)?.[1] ?? 'no result';
|
|
175
183
|
}
|
|
176
184
|
/**
|
|
177
185
|
* Probes until the desktop is up (true; see desktopUp), the time is up or `stopped()` (false). At most one probe
|
|
@@ -194,4 +202,3 @@ async function waitForRdp(target, host, port, timeoutMs, stopped, progress, wake
|
|
|
194
202
|
clearInterval(ticker);
|
|
195
203
|
}
|
|
196
204
|
}
|
|
197
|
-
//# sourceMappingURL=wake.js.map
|