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.
Files changed (87) hide show
  1. package/CHANGELOG.md +516 -0
  2. package/README.md +216 -58
  3. package/TROUBLESHOOTING.md +77 -0
  4. package/dist/accountForm.js +0 -1
  5. package/dist/accountRevisions.js +46 -0
  6. package/dist/accounts.js +2 -2
  7. package/dist/audio.js +0 -1
  8. package/dist/authorities.js +331 -0
  9. package/dist/byteQueue.js +102 -0
  10. package/dist/clipboard.js +106 -0
  11. package/dist/connectionStatus.js +0 -1
  12. package/dist/desk.js +16 -47
  13. package/dist/deskScript.js +196 -6
  14. package/dist/desktop.service.js +1499 -269
  15. package/dist/desktopForm.js +53 -18
  16. package/dist/desktops.js +232 -24
  17. package/dist/diskStorage.js +212 -0
  18. package/dist/drives.js +271 -30
  19. package/dist/fileTransfer.js +960 -132
  20. package/dist/gateway.js +216 -57
  21. package/dist/header.js +76 -5
  22. package/dist/help.js +16 -47
  23. package/dist/hostCompat.js +86 -0
  24. package/dist/hyperv.js +65 -15
  25. package/dist/index.js +7 -4
  26. package/dist/keyboard.js +0 -1
  27. package/dist/microphone.js +57 -13
  28. package/dist/ntlm.js +0 -1
  29. package/dist/osd.js +56 -15
  30. package/dist/packageChangeDialog.js +186 -0
  31. package/dist/probe.js +15 -3
  32. package/dist/rdcleanpath.js +319 -44
  33. package/dist/rdpFile.js +22 -7
  34. package/dist/rdpProfile.js +39 -21
  35. package/dist/releaseSettings.js +72 -0
  36. package/dist/releases.js +408 -0
  37. package/dist/remoteSetup.js +103 -23
  38. package/dist/sessionLog.js +53 -0
  39. package/dist/settingsPage.js +148 -122
  40. package/dist/signin.js +478 -48
  41. package/dist/ssh.js +99 -18
  42. package/dist/sshChannelStream.js +40 -6
  43. package/dist/targets.js +194 -40
  44. package/dist/ui.js +88 -20
  45. package/dist/unshowable.js +41 -0
  46. package/dist/updates.js +79 -56
  47. package/dist/vms.js +17 -3
  48. package/dist/wake.js +15 -8
  49. package/docs/desk.md +157 -0
  50. package/package.json +14 -4
  51. package/remote/trd-pty.py +74 -13
  52. package/vendor/SHA256SUMS +26 -0
  53. package/vendor/ironrdp_web_bg.wasm +0 -0
  54. package/dist/accountForm.js.map +0 -1
  55. package/dist/accounts.js.map +0 -1
  56. package/dist/audio.js.map +0 -1
  57. package/dist/connectionStatus.js.map +0 -1
  58. package/dist/desk.js.map +0 -1
  59. package/dist/deskScript.js.map +0 -1
  60. package/dist/desktop.service.js.map +0 -1
  61. package/dist/desktopForm.js.map +0 -1
  62. package/dist/desktops.js.map +0 -1
  63. package/dist/drives.js.map +0 -1
  64. package/dist/fileTransfer.js.map +0 -1
  65. package/dist/gateway.js.map +0 -1
  66. package/dist/header.js.map +0 -1
  67. package/dist/help.js.map +0 -1
  68. package/dist/hyperv.js.map +0 -1
  69. package/dist/index.js.map +0 -1
  70. package/dist/keyboard.js.map +0 -1
  71. package/dist/microphone.js.map +0 -1
  72. package/dist/ntlm.js.map +0 -1
  73. package/dist/osd.js.map +0 -1
  74. package/dist/probe.js.map +0 -1
  75. package/dist/rdcleanpath.js.map +0 -1
  76. package/dist/rdpFile.js.map +0 -1
  77. package/dist/rdpProfile.js.map +0 -1
  78. package/dist/remoteSetup.js.map +0 -1
  79. package/dist/settingsPage.js.map +0 -1
  80. package/dist/signin.js.map +0 -1
  81. package/dist/ssh.js.map +0 -1
  82. package/dist/sshChannelStream.js.map +0 -1
  83. package/dist/targets.js.map +0 -1
  84. package/dist/ui.js.map +0 -1
  85. package/dist/updates.js.map +0 -1
  86. package/dist/vms.js.map +0 -1
  87. 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 the remote desktop while an application there records (applies on the next connect).
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 of desktops behind hosts, trusted on first use: [{ desktop: <session key>, sha256 }].
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, replacing the current one.
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, current } = desktop.choicesOf(pane);
122
- const items = specs.filter(spec => spec.id !== current?.id).map(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
- /** What can be done with the pane's connected desktop: send keys, view only, a screenshot. Shared by the menus. */
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.desktopOf(pane)?.found) {
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 while an app on the remote desktop records (applies on the next connect)',
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) && desktop.configuredDesktops().length > 0,
335
- submenu: desktop.configuredDesktops().map((d, i) => ({
336
- label: `${d.name ?? `${d.host}:${d.port}`} (behind ${d.via})…`,
337
- click: () => pane && desktop.editDesktop(pane, i),
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: desktop.configuredDesktops().length > 0,
343
- submenu: desktop.configuredDesktops().map((d, i) => ({
344
- label: `${d.name ?? `${d.host}:${d.port}`} (behind ${d.via})…`,
345
- click: () => desktop.confirmRemoveDesktop(i),
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.newer = newer;
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 REGISTRY = 'https://registry.npmjs.org/tabby-rdp/latest';
21
- const RELEASES = 'https://github.com/meanaverage/tabby-rdp/releases';
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
- /** This package's version. */
24
- function installedVersion() {
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 require('../package.json').version;
38
+ return window.localStorage;
27
39
  }
28
40
  catch {
29
- return '0.0.0';
41
+ return undefined;
30
42
  }
31
43
  }
32
- /** Whether `a` is a later release than `b` (x.y.z; a version with a pre-release part is never offered). */
33
- function newer(a, b) {
34
- const parse = (v) => /^(\d+)\.(\d+)\.(\d+)$/.exec(v.trim())?.slice(1).map(Number);
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
- for (let i = 0; i < 3; i++) {
40
- if (x[i] !== y[i]) {
41
- return x[i] > y[i];
42
- }
48
+ catch {
49
+ return false;
43
50
  }
44
- return false;
45
51
  }
46
- /**
47
- * Tells about a newer tabby-rdp: Tabby only shows plugin upgrades while its Plugins page is open, and never on its own.
48
- * Once a day (and a little after Tabby starts) it asks npm for the latest version, and nothing else: no identifier,
49
- * no usage. A newer one gets a note, once per version, over the active pane; an "Update available" item in the
50
- * menus and a line on the settings page until it's installed. `remoteDesktop.checkUpdates` turns it off.
51
- */
52
- let UpdateCheck = class UpdateCheck {
53
- constructor(config, app, help, platform, zone) {
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
- /** The newer version, once one is known. */
60
- this.available = null;
61
- this.changed$ = new rxjs_1.Subject();
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
- get enabled() {
70
- return this.config.store.remoteDesktop?.checkUpdates !== false;
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
- const version = latest ?? String((await (await fetch(REGISTRY, { cache: 'no-store' })).json())?.version ?? '');
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
- * Asks again now, when a version is known already: the settings page is where it's shown, and a Tabby left open for
89
- * days would otherwise offer the version it found first, not the latest (meanaverage/tabby-rdp#17).
90
- */
91
- refresh() {
92
- if (this.available && this.enabled) {
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
- if (vm.address && (vm.rdp || vm.state === 'off' && vm.windows) && !found.some(f => f.name === vm.name)) {
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 = Number(raw.port ?? 9);
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
- [ -d "$D" ] && [ -O "$D" ] && [ ! -L "$D" ] || { echo "RD_ERR could not create a folder for the watcher's pid file"; exit 0; }
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
- const err = /^RD_ERR (.*)$/m.exec(out);
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 (.*)$/m.exec(out)?.[1] ?? 'no result';
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
- const err = /^RD_ERR (.*)$/m.exec(out);
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 (.*)$/m.exec(out)?.[1] ?? 'no result';
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