tabby-rdp 0.5.1 → 0.5.3
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 +75 -0
- package/README.md +41 -11
- package/TROUBLESHOOTING.md +82 -0
- package/dist/accountRevisions.js +46 -0
- package/dist/accounts.js +5 -1
- package/dist/connectionStatus.js +21 -4
- package/dist/desktop.service.js +296 -96
- package/dist/desktopForm.js +5 -4
- package/dist/diskStorage.js +212 -0
- package/dist/fileTransfer.js +6 -53
- package/dist/help.js +10 -62
- package/dist/hostCompat.js +86 -0
- package/dist/index.js +5 -2
- package/dist/ironrdp.js +26 -0
- package/dist/keyboard.js +12 -1
- package/dist/packageChangeDialog.js +186 -0
- package/dist/rdcleanpath.js +17 -4
- package/dist/rdpFile.js +13 -4
- package/dist/rdpProfile.js +44 -14
- package/dist/releaseSettings.js +72 -0
- package/dist/releases.js +411 -0
- package/dist/settingsPage.js +110 -115
- package/dist/signin.js +73 -10
- package/dist/targets.js +9 -1
- package/dist/updates.js +77 -89
- package/docs/desk.md +157 -0
- package/package.json +11 -2
- package/vendor/SHA256SUMS +1 -0
- package/vendor/ironrdp-factory.js +3237 -0
package/dist/desktop.service.js
CHANGED
|
@@ -23,12 +23,14 @@ const url_1 = require("url");
|
|
|
23
23
|
const targets_1 = require("./targets");
|
|
24
24
|
const deskScript_1 = require("./deskScript");
|
|
25
25
|
const audio_1 = require("./audio");
|
|
26
|
+
const ironrdp_1 = require("./ironrdp");
|
|
26
27
|
const clipboard_1 = require("./clipboard");
|
|
27
28
|
const connectionStatus_1 = require("./connectionStatus");
|
|
28
29
|
const microphone_1 = require("./microphone");
|
|
29
30
|
const drives_1 = require("./drives");
|
|
30
31
|
const desktopForm_1 = require("./desktopForm");
|
|
31
32
|
const accounts_1 = require("./accounts");
|
|
33
|
+
const accountRevisions_1 = require("./accountRevisions");
|
|
32
34
|
const accountForm_1 = require("./accountForm");
|
|
33
35
|
const fileTransfer_1 = require("./fileTransfer");
|
|
34
36
|
const desktops_1 = require("./desktops");
|
|
@@ -45,6 +47,7 @@ const gateway_1 = require("./gateway");
|
|
|
45
47
|
const authorities_1 = require("./authorities");
|
|
46
48
|
const vms_1 = require("./vms");
|
|
47
49
|
const sessionLog_1 = require("./sessionLog");
|
|
50
|
+
const hostCompat_1 = require("./hostCompat");
|
|
48
51
|
const unshowable_1 = require("./unshowable");
|
|
49
52
|
/**
|
|
50
53
|
* The keychain entry that marks the passwords 0.5.0 saved for desktops reached through a gateway forgotten from this
|
|
@@ -82,6 +85,7 @@ const STYLE = `
|
|
|
82
85
|
.trd-overlay { position: absolute; inset: 0; z-index: 30; display: flex; background: #111; }
|
|
83
86
|
.trd-host { flex: auto; min-width: 0; min-height: 0; display: flex; }
|
|
84
87
|
.trd-host iron-remote-desktop { flex: auto; }
|
|
88
|
+
.trd-last-frame { max-width: 100%; max-height: 100%; object-fit: contain; align-self: center; margin: auto; }
|
|
85
89
|
.trd-status { position: absolute; inset: 0; display: flex; flex-direction: column; gap: 14px; align-items: center;
|
|
86
90
|
justify-content: center; padding: 2em; text-align: center; color: #bbb; font-size: 13px; white-space: pre-line;
|
|
87
91
|
pointer-events: none; }
|
|
@@ -105,6 +109,10 @@ const STYLE = `
|
|
|
105
109
|
.trd-overlay.trd-mic-on > .trd-toast { right: 40px; max-width: min(420px, calc(100% - 52px)); }
|
|
106
110
|
.trd-floating-mic-on .trd-overlay.trd-broadcast-on::before { top: 40px; }
|
|
107
111
|
`;
|
|
112
|
+
/** A file's name as it is shown: it comes with the file, and what could reorder or hide part of it shows as �. */
|
|
113
|
+
function showableFileName(raw) {
|
|
114
|
+
return raw.replace(new RegExp(unshowable_1.UNSHOWABLE.source, 'gu'), '�');
|
|
115
|
+
}
|
|
108
116
|
/**
|
|
109
117
|
* The name a desktop connected to directly goes by, as its certificate is checked against it (see
|
|
110
118
|
* RDCleanPathOptions.serverName): an address as it is, a host name as it resolves (lower case, punycode, without a
|
|
@@ -259,11 +267,24 @@ class DesktopSession {
|
|
|
259
267
|
/** Keys this desktop was sent and not yet let go of (by `code`): see RemoteDesktopService.broadcastKey. */
|
|
260
268
|
this.keysDown = new Map();
|
|
261
269
|
this.reclaimFocus = (event) => {
|
|
262
|
-
if (this.visible
|
|
270
|
+
if (!this.visible) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
if (!this.overlay.contains(event.target)) {
|
|
263
274
|
this.focusSoon();
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
// The focus came to the desktop while its pane isn't the one the user is in: IronRDP's canvas takes it as the
|
|
278
|
+
// pointer passes over it (its `mouseenter`), without a click, and with it the keys typed and this computer's
|
|
279
|
+
// clipboard (its monitor follows the focus). It goes back where it came from. A click hands it over still:
|
|
280
|
+
// Tabby makes the pane the one in front on the click, and the pane's focus gives the desktop the keyboard. (The
|
|
281
|
+
// layer's own fields and buttons, a sign-in form's, send nothing anywhere until they are used.)
|
|
282
|
+
if (event.target?.tagName === 'IRON-REMOTE-DESKTOP' && !this.inFront()) {
|
|
283
|
+
this.letGo(event.relatedTarget);
|
|
264
284
|
}
|
|
265
285
|
};
|
|
266
286
|
this.disposers = [];
|
|
287
|
+
this.connectionDisposers = [];
|
|
267
288
|
this.disposed = new Promise(resolve => { this.markDisposed = resolve; });
|
|
268
289
|
this.overlay = document.createElement('div');
|
|
269
290
|
this.overlay.className = 'trd-overlay';
|
|
@@ -302,7 +323,9 @@ class DesktopSession {
|
|
|
302
323
|
}
|
|
303
324
|
/**
|
|
304
325
|
* Lets go of every key still held on this desktop. For when their keyups won't reach it: view only coming on, or
|
|
305
|
-
* typing into all going off, by a shortcut whose modifiers are down at that moment
|
|
326
|
+
* typing into all going off, by a shortcut whose modifiers are down at that moment, or the user leaving its pane
|
|
327
|
+
* by one (see RemoteDesktopService.letGoUnlessInFront). They would stay down there. Sent through IronRDP's
|
|
328
|
+
* component, which takes a key sent this way whether or not it has the focus.
|
|
306
329
|
*/
|
|
307
330
|
releaseKeys() {
|
|
308
331
|
for (const [code, init] of [...this.keysDown]) {
|
|
@@ -332,6 +355,33 @@ class DesktopSession {
|
|
|
332
355
|
canvas() {
|
|
333
356
|
return this.host.querySelector('iron-remote-desktop')?.shadowRoot?.querySelector('canvas') ?? null;
|
|
334
357
|
}
|
|
358
|
+
/** Whether the focus is on the desktop: IronRDP's component, its sign-in form, a question's buttons. */
|
|
359
|
+
holdsFocus() {
|
|
360
|
+
return this.overlay.contains(document.activeElement);
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Takes the focus from the desktop, if it has it, back to `to` (where it came from) when that is still there to
|
|
364
|
+
* take it, or else to nothing. On IronRDP's component it is its canvas, in the component's shadow root, that has
|
|
365
|
+
* it, and has to let go. The keys held on the remote stay held: IronRDP lets go of them as the window loses the
|
|
366
|
+
* focus or the pointer leaves the canvas, not as the canvas loses the focus. Where the focus goes back to the
|
|
367
|
+
* desktop in front (the pointer passed over this one), their keyups still come from there, copied when typing
|
|
368
|
+
* into all; where the user left the pane, they don't, and the keys are let go of (see
|
|
369
|
+
* RemoteDesktopService.letGoUnlessInFront).
|
|
370
|
+
*/
|
|
371
|
+
letGo(to) {
|
|
372
|
+
const active = document.activeElement;
|
|
373
|
+
if (!active || !this.overlay.contains(active)) {
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
const back = to;
|
|
377
|
+
if (typeof back?.focus === 'function' && back.isConnected !== false && !this.overlay.contains(back)) {
|
|
378
|
+
back.focus({ preventScroll: true });
|
|
379
|
+
}
|
|
380
|
+
if (this.overlay.contains(document.activeElement)) {
|
|
381
|
+
;
|
|
382
|
+
(active.shadowRoot?.activeElement ?? active).blur();
|
|
383
|
+
}
|
|
384
|
+
}
|
|
335
385
|
/**
|
|
336
386
|
* Focuses the remote desktop in a moment, if it is still in the pane the user is in then (see inFront). `visible`
|
|
337
387
|
* stays true in a tab in the background, and a focus that was waiting (a retry, or Tabby's re-focus of the terminal
|
|
@@ -348,7 +398,7 @@ class DesktopSession {
|
|
|
348
398
|
setVisible(visible) {
|
|
349
399
|
this.visible = visible;
|
|
350
400
|
this.overlay.style.display = visible ? '' : 'none';
|
|
351
|
-
this.container
|
|
401
|
+
(0, hostCompat_1.terminalInputs)(this.container).forEach(input => {
|
|
352
402
|
input.disabled = visible;
|
|
353
403
|
});
|
|
354
404
|
if (visible) {
|
|
@@ -384,6 +434,58 @@ class DesktopSession {
|
|
|
384
434
|
onDispose(f) {
|
|
385
435
|
this.disposers.push(f);
|
|
386
436
|
}
|
|
437
|
+
onConnectionEnd(f) {
|
|
438
|
+
this.connectionDisposers.push(f);
|
|
439
|
+
}
|
|
440
|
+
/** Release browser resources and references to one backend, including when its WASM has trapped. */
|
|
441
|
+
endConnection(keepPicture = true) {
|
|
442
|
+
const clean = (f) => {
|
|
443
|
+
try {
|
|
444
|
+
f();
|
|
445
|
+
}
|
|
446
|
+
catch (error) {
|
|
447
|
+
this.log.push(`cleanup: ${error?.message ?? error}`);
|
|
448
|
+
}
|
|
449
|
+
};
|
|
450
|
+
let picture = keepPicture ? this.host.querySelector('.trd-last-frame') : null;
|
|
451
|
+
// Preserve the dimmed disconnect picture as plain pixels, without the component's backend or listeners.
|
|
452
|
+
clean(() => {
|
|
453
|
+
const frame = keepPicture ? this.canvas() : null;
|
|
454
|
+
if (!frame?.width || !frame.height) {
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
const copy = document.createElement('canvas');
|
|
458
|
+
copy.width = frame.width;
|
|
459
|
+
copy.height = frame.height;
|
|
460
|
+
copy.className = 'trd-last-frame';
|
|
461
|
+
const rect = frame.getBoundingClientRect();
|
|
462
|
+
copy.style.width = `${rect.width || frame.width}px`;
|
|
463
|
+
copy.style.height = `${rect.height || frame.height}px`;
|
|
464
|
+
const ctx = copy.getContext('2d');
|
|
465
|
+
if (ctx) {
|
|
466
|
+
ctx.drawImage(frame, 0, 0);
|
|
467
|
+
picture = copy;
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
this.connectionDisposers.splice(0).forEach(clean);
|
|
471
|
+
clean(() => this.indicator?.setActive(false));
|
|
472
|
+
clean(() => this.ui?.shutdown());
|
|
473
|
+
// Removing the component stops its clipboard loop and removes its global listeners.
|
|
474
|
+
clean(() => this.host.replaceChildren(...picture ? [picture] : []));
|
|
475
|
+
this.ui = null;
|
|
476
|
+
this.stopSending = null;
|
|
477
|
+
clean(() => this.audio?.close());
|
|
478
|
+
this.audio = null;
|
|
479
|
+
clean(() => this.h264?.close());
|
|
480
|
+
this.h264 = null;
|
|
481
|
+
clean(() => this.mic?.close());
|
|
482
|
+
this.mic = null;
|
|
483
|
+
clean(() => this.files?.dispose());
|
|
484
|
+
this.files = null;
|
|
485
|
+
clean(() => this.drives?.dispose());
|
|
486
|
+
this.drives = null;
|
|
487
|
+
this.remoteSize = null;
|
|
488
|
+
}
|
|
387
489
|
/** Focuses the remote desktop. IronRDP reveals its canvas asynchronously, so retry for about half a second. */
|
|
388
490
|
focusDesktop(attempts = 30) {
|
|
389
491
|
// While the sign-in form shows, it is what takes focus back (the user name, or the password once filled).
|
|
@@ -415,16 +517,7 @@ class DesktopSession {
|
|
|
415
517
|
clearTimeout(this.labelTimer);
|
|
416
518
|
this.container.removeEventListener('focusin', this.reclaimFocus, true);
|
|
417
519
|
this.disposers.forEach(f => f());
|
|
418
|
-
this.
|
|
419
|
-
try {
|
|
420
|
-
this.ui?.shutdown();
|
|
421
|
-
}
|
|
422
|
-
catch { }
|
|
423
|
-
this.audio?.close();
|
|
424
|
-
this.h264?.close();
|
|
425
|
-
this.mic?.close();
|
|
426
|
-
this.files?.dispose();
|
|
427
|
-
this.drives?.dispose();
|
|
520
|
+
this.endConnection(false);
|
|
428
521
|
this.proxy?.close();
|
|
429
522
|
this.overlay.remove();
|
|
430
523
|
}
|
|
@@ -449,7 +542,13 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
449
542
|
/** Emits whenever a pane's desktop is opened, shown, hidden, connected, ended or disconnected. */
|
|
450
543
|
this.changed$ = new rxjs_1.Subject();
|
|
451
544
|
this.sessions = new Map();
|
|
452
|
-
this.ironrdp =
|
|
545
|
+
this.ironrdp = new ironrdp_1.IronRDPLoader(async () => {
|
|
546
|
+
const [factory, bytes] = await Promise.all([
|
|
547
|
+
importESM(vendor('iron-remote-desktop.js')).then(() => importESM(vendor('ironrdp-factory.js'))),
|
|
548
|
+
fs.promises.readFile(vendorFile('ironrdp_web_bg.wasm')),
|
|
549
|
+
]);
|
|
550
|
+
return { module: await WebAssembly.compile(bytes), createBackend: factory.createBackend };
|
|
551
|
+
});
|
|
453
552
|
/** Whether the browser decodes H.264 (asked once). */
|
|
454
553
|
this.h264Support = null;
|
|
455
554
|
/** Desktops (session keys) where H.264 decoding failed: they connect without it until Tabby restarts. */
|
|
@@ -478,6 +577,7 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
478
577
|
/** VMs found on SSH hosts (see vms.ts), by target key: when, and as desktops; and what the scans learnt of the host. */
|
|
479
578
|
this.vms = new Map();
|
|
480
579
|
this.lastUsed = new Map();
|
|
580
|
+
this.accountRevisions = new accountRevisions_1.AccountRevisions();
|
|
481
581
|
/** Each pane's `desk` requests, one at a time (see DeskGate). */
|
|
482
582
|
this.deskGates = new WeakMap();
|
|
483
583
|
/**
|
|
@@ -833,6 +933,10 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
833
933
|
accounts() {
|
|
834
934
|
return (0, accounts_1.accountsOf)(this.config.store.remoteDesktop);
|
|
835
935
|
}
|
|
936
|
+
accountCurrent(key, name, revision) {
|
|
937
|
+
const account = this.accounts().find(a => (0, accounts_1.accountKey)(a.id) === key);
|
|
938
|
+
return !!account && (0, accounts_1.signInName)(account) === name && (account.credentialRevision ?? '') === revision && this.accountRevisions.current(account.id, revision);
|
|
939
|
+
}
|
|
836
940
|
/**
|
|
837
941
|
* The desktops that sign in with a saved account, or sign in to their RD Gateway with it: configured ones behind
|
|
838
942
|
* SSH hosts, and RDP profiles (their own choice, or their profile group's default when they make none). Each comes
|
|
@@ -842,10 +946,14 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
842
946
|
// Names as text whatever the config holds (see entryText): the accounts list and the question before removing
|
|
843
947
|
// one show them all.
|
|
844
948
|
const desktops = this.configuredDesktops().flatMap((d, i) => (0, desktops_1.isDesktopEntry)(d) && (d.account === id || d.gatewayAccount === id) ? [{ name: (0, desktops_1.entryText)(d).name, desktopIndex: i }] : []);
|
|
845
|
-
const groups = this.config.store.groups ?? [];
|
|
846
|
-
const inherited = (p) => p.options?.account === undefined && groups.find(g => g.id === p.group)?.defaults?.[targets_1.RDP_PROFILE_TYPE]?.options?.account === id;
|
|
847
949
|
const profiles = (this.config.store.profiles ?? [])
|
|
848
|
-
.filter((p) =>
|
|
950
|
+
.filter((p) => {
|
|
951
|
+
if (p?.type !== targets_1.RDP_PROFILE_TYPE) {
|
|
952
|
+
return false;
|
|
953
|
+
}
|
|
954
|
+
const options = this.profiles.getConfigProxyForProfile(p).options;
|
|
955
|
+
return options?.account === id || options?.gatewayAccount === id;
|
|
956
|
+
})
|
|
849
957
|
.map((p) => ({ name: (0, desktops_1.configText)(p.name) || (0, desktops_1.configText)(p.options?.host), profileId: (0, desktops_1.configText)(p.id) || undefined }));
|
|
850
958
|
return [...desktops, ...profiles];
|
|
851
959
|
}
|
|
@@ -863,14 +971,31 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
863
971
|
username: account.username.trim(),
|
|
864
972
|
...account.domain?.trim() ? { domain: account.domain.trim() } : {},
|
|
865
973
|
};
|
|
974
|
+
let revision;
|
|
975
|
+
try {
|
|
976
|
+
revision = this.accountRevisions.change(entry.id);
|
|
977
|
+
}
|
|
978
|
+
catch (error) {
|
|
979
|
+
return { id: entry.id, keychainError: error?.message ?? String(error) };
|
|
980
|
+
}
|
|
981
|
+
entry.credentialRevision = revision;
|
|
982
|
+
// Invalidate before any store await. Keep the old sign-in name until changing its password succeeds.
|
|
983
|
+
if (old) {
|
|
984
|
+
this.config.store.remoteDesktop.accounts = list.map(a => a.id === old.id ? { ...a, credentialRevision: revision } : a);
|
|
985
|
+
await this.config.save();
|
|
986
|
+
}
|
|
987
|
+
const current = () => this.accountRevisions.current(entry.id, revision) && (!old || this.accounts().some(a => a.id === entry.id && a.credentialRevision === revision));
|
|
866
988
|
// The password first: a new user name must not be written while the old password (for the old name) stays, nor
|
|
867
989
|
// a new password reported as saved when the store refused it.
|
|
868
990
|
let keychainError;
|
|
869
991
|
if (password !== undefined && password !== '') {
|
|
870
|
-
await (0, signin_1.
|
|
992
|
+
await (0, signin_1.saveCredentialsIf)((0, accounts_1.accountKey)(entry.id), { username: (0, accounts_1.signInName)(entry), password }, current, `saved account ${entry.name}`).catch(e => { keychainError = String(e?.message ?? e); });
|
|
871
993
|
}
|
|
872
994
|
else if (old && (0, accounts_1.signInName)(old) !== (0, accounts_1.signInName)(entry)) {
|
|
873
|
-
await (0, signin_1.
|
|
995
|
+
await (0, signin_1.forgetCredentialsIf)((0, accounts_1.accountKey)(entry.id), current).catch(e => { keychainError = `the old password couldn't be removed: ${e?.message ?? e}`; });
|
|
996
|
+
}
|
|
997
|
+
if (!current()) {
|
|
998
|
+
return { id: entry.id, keychainError: 'The account changed again while this edit was being saved. The newer change was kept.' };
|
|
874
999
|
}
|
|
875
1000
|
if (keychainError && !old) {
|
|
876
1001
|
// Not added at all: a retry would otherwise make a second account.
|
|
@@ -886,7 +1011,8 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
886
1011
|
delete entry.domain;
|
|
887
1012
|
}
|
|
888
1013
|
}
|
|
889
|
-
|
|
1014
|
+
const now = this.accounts();
|
|
1015
|
+
this.config.store.remoteDesktop.accounts = old ? now.map(a => a.id === old.id ? entry : a) : [...now, entry];
|
|
890
1016
|
await this.config.save();
|
|
891
1017
|
this.changed$.next();
|
|
892
1018
|
return { id: entry.id, keychainError };
|
|
@@ -923,11 +1049,18 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
923
1049
|
if (response !== 0) {
|
|
924
1050
|
return false;
|
|
925
1051
|
}
|
|
926
|
-
|
|
927
|
-
|
|
1052
|
+
try {
|
|
1053
|
+
await this.removeAccount(id);
|
|
1054
|
+
return true;
|
|
1055
|
+
}
|
|
1056
|
+
catch (error) {
|
|
1057
|
+
this.notifications.error(`The account could not be fully removed: ${error?.message ?? error}`);
|
|
1058
|
+
return false;
|
|
1059
|
+
}
|
|
928
1060
|
}
|
|
929
1061
|
/** Removes a saved account and its password; desktops that used it go back to asking. */
|
|
930
1062
|
async removeAccount(id) {
|
|
1063
|
+
const revision = this.accountRevisions.change(id);
|
|
931
1064
|
const store = this.config.store.remoteDesktop;
|
|
932
1065
|
store.accounts = this.accounts().filter(a => a.id !== id);
|
|
933
1066
|
if (this.configuredDesktops().some(d => (0, desktops_1.isDesktopEntry)(d) && (d.account === id || d.gatewayAccount === id))) {
|
|
@@ -939,9 +1072,9 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
939
1072
|
if (rest.account === id) {
|
|
940
1073
|
delete rest.account;
|
|
941
1074
|
}
|
|
942
|
-
//
|
|
1075
|
+
// Keep the gateway's credentials separate after its saved account is removed.
|
|
943
1076
|
if (rest.gatewayAccount === id) {
|
|
944
|
-
|
|
1077
|
+
rest.gatewayAccount = accounts_1.ASK_GATEWAY_ACCOUNT;
|
|
945
1078
|
}
|
|
946
1079
|
return rest;
|
|
947
1080
|
});
|
|
@@ -952,19 +1085,23 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
952
1085
|
profile.options.account = null;
|
|
953
1086
|
}
|
|
954
1087
|
if (profile?.type === targets_1.RDP_PROFILE_TYPE && profile.options?.gatewayAccount === id) {
|
|
955
|
-
profile.options.gatewayAccount =
|
|
1088
|
+
profile.options.gatewayAccount = accounts_1.ASK_GATEWAY_ACCOUNT;
|
|
956
1089
|
}
|
|
957
1090
|
}
|
|
958
|
-
//
|
|
959
|
-
|
|
960
|
-
|
|
1091
|
+
// Group and global defaults keep separate gateway authentication for their inheriting profiles, too.
|
|
1092
|
+
const defaults = [this.config.store.profileDefaults, ...(this.config.store.groups ?? []).map((group) => group?.defaults)];
|
|
1093
|
+
for (const entry of defaults) {
|
|
1094
|
+
const options = entry?.[targets_1.RDP_PROFILE_TYPE]?.options;
|
|
961
1095
|
if (options?.account === id) {
|
|
962
1096
|
options.account = '';
|
|
963
1097
|
}
|
|
1098
|
+
if (options?.gatewayAccount === id) {
|
|
1099
|
+
options.gatewayAccount = accounts_1.ASK_GATEWAY_ACCOUNT;
|
|
1100
|
+
}
|
|
964
1101
|
}
|
|
965
1102
|
await this.config.save();
|
|
966
1103
|
this.changed$.next();
|
|
967
|
-
await (0, signin_1.
|
|
1104
|
+
await (0, signin_1.forgetCredentialsIf)((0, accounts_1.accountKey)(id), () => this.accountRevisions.current(id, revision)).catch(e => this.notifications.error(`The account is removed, but its password could not be: ${e?.message ?? e}`));
|
|
968
1105
|
}
|
|
969
1106
|
/** `remoteDesktop.desktops`: the desktops configured behind SSH hosts. */
|
|
970
1107
|
configuredDesktops() {
|
|
@@ -1138,6 +1275,10 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
1138
1275
|
}
|
|
1139
1276
|
}
|
|
1140
1277
|
}
|
|
1278
|
+
/** Says that the .rdp file named `rawFileName` is too large to be one (see MAX_RDP_FILE). */
|
|
1279
|
+
refuseLargeRdp(rawFileName, size) {
|
|
1280
|
+
this.notifications.error(`${showableFileName(rawFileName)}: at ${Math.ceil(size / 1024)} KB, far larger than an .rdp file is, so it wasn't imported`);
|
|
1281
|
+
}
|
|
1141
1282
|
/** "Import an .rdp file…": picks one and adds it as a remote desktop profile (see importRdp). */
|
|
1142
1283
|
async importRdpFile() {
|
|
1143
1284
|
const file = await new Promise(resolve => {
|
|
@@ -1151,6 +1292,11 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
1151
1292
|
input.click();
|
|
1152
1293
|
});
|
|
1153
1294
|
if (file) {
|
|
1295
|
+
// Not even read when it is far larger than an .rdp file is (see importRdp).
|
|
1296
|
+
if (file.size > rdpFile_1.MAX_RDP_FILE) {
|
|
1297
|
+
this.refuseLargeRdp(file.name, file.size);
|
|
1298
|
+
return;
|
|
1299
|
+
}
|
|
1154
1300
|
// A reading or parsing failure is shown, not dropped as an unhandled rejection: this is the user's action.
|
|
1155
1301
|
try {
|
|
1156
1302
|
await this.importRdp(new Uint8Array(await file.arrayBuffer()), file.name);
|
|
@@ -1184,8 +1330,12 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
1184
1330
|
* and opens it; or opens the profile that already has that address, user and domain. Returns the profile, or null.
|
|
1185
1331
|
*/
|
|
1186
1332
|
async importRdp(data, rawFileName) {
|
|
1187
|
-
|
|
1188
|
-
|
|
1333
|
+
const fileName = showableFileName(rawFileName);
|
|
1334
|
+
// Far larger than an .rdp file is: not parsed, on the thread that runs Tabby's window (see MAX_RDP_FILE).
|
|
1335
|
+
if (data.length > rdpFile_1.MAX_RDP_FILE) {
|
|
1336
|
+
this.refuseLargeRdp(rawFileName, data.length);
|
|
1337
|
+
return null;
|
|
1338
|
+
}
|
|
1189
1339
|
const parsed = (0, rdpFile_1.parseRdpFile)(data);
|
|
1190
1340
|
if (!parsed) {
|
|
1191
1341
|
this.notifications.error(`${fileName}: no address ("full address") in it`);
|
|
@@ -1510,6 +1660,16 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
1510
1660
|
}
|
|
1511
1661
|
});
|
|
1512
1662
|
created.onDispose(() => focused.unsubscribe());
|
|
1663
|
+
// The pane is no longer the one the user is in: another tab, another pane of the split. Tabby only moves a
|
|
1664
|
+
// tab it leaves off screen, and a tab that takes no focus of its own (Settings, the welcome tab) leaves the
|
|
1665
|
+
// focus where it was: on the desktop, which would go on taking the keys typed and this computer's
|
|
1666
|
+
// clipboard. It lets go then, of the keys it holds on the remote too: the user left by a shortcut whose
|
|
1667
|
+
// modifier is still down, and its keyup goes where they went. A moment later: Tabby says so before it says
|
|
1668
|
+
// which tab is the active one.
|
|
1669
|
+
const blurred = pane.blurred$.subscribe(() => {
|
|
1670
|
+
setTimeout(() => this.letGoUnlessInFront(created));
|
|
1671
|
+
});
|
|
1672
|
+
created.onDispose(() => blurred.unsubscribe());
|
|
1513
1673
|
this.connect(pane, target, spec, created);
|
|
1514
1674
|
}
|
|
1515
1675
|
if (options.background) {
|
|
@@ -1577,10 +1737,29 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
1577
1737
|
inFront(pane) {
|
|
1578
1738
|
return (0, targets_1.desktopPaneOf)(this.app.activeTab) === pane;
|
|
1579
1739
|
}
|
|
1580
|
-
/**
|
|
1740
|
+
/**
|
|
1741
|
+
* Whether `session` is the desktop of the pane the user is in (see inFront), and not behind a pane maximized over
|
|
1742
|
+
* it (see behindMaximized).
|
|
1743
|
+
*/
|
|
1581
1744
|
showsInFront(session) {
|
|
1582
1745
|
const pane = (0, targets_1.desktopPaneOf)(this.app.activeTab);
|
|
1583
|
-
return !!pane && this.sessions.get(pane) === session;
|
|
1746
|
+
return !!pane && this.sessions.get(pane) === session && !this.behindMaximized(pane);
|
|
1747
|
+
}
|
|
1748
|
+
/**
|
|
1749
|
+
* A desktop whose pane stopped being the one the user is in (see showsInFront): the focus is taken from it, if it
|
|
1750
|
+
* has it, and the keys it holds on the remote are let go of. Their keyups go where the user went, whether the
|
|
1751
|
+
* desktop kept the focus until now (a tab that takes none) or lost it to what they went to (a terminal), and
|
|
1752
|
+
* IronRDP lets go of nothing as its canvas loses the focus (see DesktopSession.letGo). With typing broadcast to
|
|
1753
|
+
* every desktop of the tab, leaving the tab leaves all of its panes, so each of them is let go of here too.
|
|
1754
|
+
*/
|
|
1755
|
+
letGoUnlessInFront(session) {
|
|
1756
|
+
if (this.showsInFront(session)) {
|
|
1757
|
+
return;
|
|
1758
|
+
}
|
|
1759
|
+
if (session.holdsFocus()) {
|
|
1760
|
+
session.letGo();
|
|
1761
|
+
}
|
|
1762
|
+
session.releaseKeys();
|
|
1584
1763
|
}
|
|
1585
1764
|
/**
|
|
1586
1765
|
* Gives a desktop that just connected the keyboard, if it shows in the pane the user is in. Not when another pane is
|
|
@@ -2143,7 +2322,7 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
2143
2322
|
timer = setTimeout(() => this.followPane(pane, session), 300);
|
|
2144
2323
|
});
|
|
2145
2324
|
observer.observe(session.overlay);
|
|
2146
|
-
session.
|
|
2325
|
+
session.onConnectionEnd(() => {
|
|
2147
2326
|
clearTimeout(timer);
|
|
2148
2327
|
observer.disconnect();
|
|
2149
2328
|
});
|
|
@@ -2618,25 +2797,12 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
2618
2797
|
return this.sessions.get(pane)?.log ?? [];
|
|
2619
2798
|
}
|
|
2620
2799
|
loadIronRDP() {
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
fs.promises.readFile(vendorFile('ironrdp_web_bg.wasm')),
|
|
2628
|
-
]);
|
|
2629
|
-
// Troubleshooting: localStorage.trdLogLevel = 'DEBUG' (or TRACE), then restart Tabby.
|
|
2630
|
-
let level = 'INFO';
|
|
2631
|
-
try {
|
|
2632
|
-
level = localStorage.getItem('trdLogLevel') || level;
|
|
2633
|
-
}
|
|
2634
|
-
catch { }
|
|
2635
|
-
await rdp.init(level, wasm);
|
|
2636
|
-
return rdp;
|
|
2637
|
-
})();
|
|
2638
|
-
this.ironrdp.catch(() => { this.ironrdp = null; });
|
|
2639
|
-
return this.ironrdp;
|
|
2800
|
+
let level = 'INFO';
|
|
2801
|
+
try {
|
|
2802
|
+
level = localStorage.getItem('trdLogLevel') || level;
|
|
2803
|
+
}
|
|
2804
|
+
catch { }
|
|
2805
|
+
return this.ironrdp.create(level);
|
|
2640
2806
|
}
|
|
2641
2807
|
/**
|
|
2642
2808
|
* Where to connect and as whom. The host's own desktop: the setup script (grd, generated credentials), or, where
|
|
@@ -2721,6 +2887,7 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
2721
2887
|
}
|
|
2722
2888
|
session.status('');
|
|
2723
2889
|
const mark = (0, signin_1.forgetMark)();
|
|
2890
|
+
const accountRevision = account && this.accountRevisions.capture(account);
|
|
2724
2891
|
// The host's own desktops (its own, or its xrdp besides GNOME) and direct ones need no "via". A machine reached
|
|
2725
2892
|
// with ssh typed in a host's console says which host: that host is what leads there.
|
|
2726
2893
|
const hostOwn = spec.id === desktops_1.OWN_DESKTOP || spec.kind === 'xrdp' && spec.host === '127.0.0.1';
|
|
@@ -2729,8 +2896,8 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
2729
2896
|
title: spec.hyperv ? `Sign in to ${target.label}${target.via ? ` (via ${target.via})` : ''}, to open ${spec.name}`
|
|
2730
2897
|
: hostOwn && !target.via || target.direct ? `Sign in to ${spec.name}` : `Sign in to ${spec.name} (via ${via})`,
|
|
2731
2898
|
username: account ? (0, accounts_1.signInName)(account) : spec.username,
|
|
2732
|
-
error: retryError,
|
|
2733
|
-
canRemember:
|
|
2899
|
+
error: [retryError, account && accountRevision === undefined ? accountRevisions_1.ACCOUNT_REMEMBER_UNAVAILABLE : ''].filter(Boolean).join(' '),
|
|
2900
|
+
canRemember: !account || accountRevision !== undefined,
|
|
2734
2901
|
account: account?.name,
|
|
2735
2902
|
}, session.disposed);
|
|
2736
2903
|
// Only in the pane in front, as when connected (see run()): the form gets the keyboard when its pane does.
|
|
@@ -2741,7 +2908,7 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
2741
2908
|
return entered && {
|
|
2742
2909
|
host: spec.host, port: spec.port, credentials: entered, remember: entered.remember, domain: account ? account.domain ?? '' : undefined,
|
|
2743
2910
|
saveKey: account && (0, accounts_1.accountKey)(account.id), saveLabel: account && `saved account ${account.name}`, saveFor: account && (0, accounts_1.signInName)(account),
|
|
2744
|
-
hyperv, mark,
|
|
2911
|
+
hyperv, mark, accountRevision,
|
|
2745
2912
|
};
|
|
2746
2913
|
}
|
|
2747
2914
|
async connect(pane, target, spec, session) {
|
|
@@ -2886,35 +3053,35 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
2886
3053
|
}
|
|
2887
3054
|
/**
|
|
2888
3055
|
* The sign-in to a desktop's RD Gateway with the saved account it names: the account's password from the keychain,
|
|
2889
|
-
* or asked for (when none is saved, or `retryError`: the gateway just refused it).
|
|
2890
|
-
*
|
|
3056
|
+
* or asked for (when none is saved, or `retryError`: the gateway just refused it). A removed or missing account
|
|
3057
|
+
* always asks for separate credentials; null when the form was cancelled.
|
|
2891
3058
|
*/
|
|
2892
3059
|
async gatewayAccountFor(spec, session, gateway, retryError) {
|
|
2893
3060
|
const account = this.accounts().find(a => a.id === spec.gatewayAccount);
|
|
2894
3061
|
if (!account) {
|
|
2895
|
-
session.log.push('gateway:
|
|
2896
|
-
return undefined;
|
|
3062
|
+
session.log.push('gateway: asking for a separate account');
|
|
2897
3063
|
}
|
|
2898
|
-
const signIn = { domain: account.domain, saveKey: (0, accounts_1.accountKey)(account.id), saveLabel: `saved account ${account.name}`, saveFor: (0, accounts_1.signInName)(account) };
|
|
2899
|
-
const saved = retryError
|
|
2900
|
-
if (saved && saved.username === (0, accounts_1.signInName)(account)) {
|
|
3064
|
+
const signIn = account ? { domain: account.domain, saveKey: (0, accounts_1.accountKey)(account.id), saveLabel: `saved account ${account.name}`, saveFor: (0, accounts_1.signInName)(account), accountRevision: this.accountRevisions.capture(account) } : undefined;
|
|
3065
|
+
const saved = !retryError && signIn ? await (0, signin_1.loadCredentials)(signIn.saveKey) : null;
|
|
3066
|
+
if (saved && account && saved.username === (0, accounts_1.signInName)(account)) {
|
|
2901
3067
|
session.log.push(`gateway: the saved account "${account.name}"`);
|
|
2902
3068
|
return { ...signIn, credentials: { username: (0, accounts_1.signInName)(account), password: saved.password }, remember: false };
|
|
2903
3069
|
}
|
|
2904
3070
|
session.status('');
|
|
2905
3071
|
const asked = (0, signin_1.askCredentials)(session.overlay, {
|
|
2906
3072
|
title: `Sign in to the gateway ${gateway.host}, to reach ${spec.name}`,
|
|
2907
|
-
username: (0, accounts_1.signInName)(account),
|
|
2908
|
-
error: retryError,
|
|
2909
|
-
|
|
2910
|
-
|
|
3073
|
+
username: account ? (0, accounts_1.signInName)(account) : '',
|
|
3074
|
+
error: [retryError, !account && spec.gatewayAccount !== accounts_1.ASK_GATEWAY_ACCOUNT ? 'The saved gateway account no longer exists. Enter a separate account for the gateway.' : '',
|
|
3075
|
+
account && signIn?.accountRevision === undefined ? accountRevisions_1.ACCOUNT_REMEMBER_UNAVAILABLE : ''].filter(Boolean).join(' '),
|
|
3076
|
+
canRemember: !!signIn && signIn.accountRevision !== undefined,
|
|
3077
|
+
account: account?.name,
|
|
2911
3078
|
}, session.disposed);
|
|
2912
3079
|
// Only in the pane in front (see endpointFor).
|
|
2913
3080
|
if (session.visible && this.showsInFront(session)) {
|
|
2914
3081
|
setTimeout(() => session.visible && this.showsInFront(session) && session.focusDesktop());
|
|
2915
3082
|
}
|
|
2916
3083
|
const entered = await asked;
|
|
2917
|
-
return entered && { ...signIn, credentials: entered, remember: entered.remember };
|
|
3084
|
+
return entered && { ...signIn, credentials: entered, remember: !!signIn && entered.remember };
|
|
2918
3085
|
}
|
|
2919
3086
|
/**
|
|
2920
3087
|
* The proxy's stream to a desktop behind an RD Gateway (see gateway.ts): to the gateway as the SSH host (or this
|
|
@@ -2923,17 +3090,18 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
2923
3090
|
async throughGateway(target, spec, session, gateway, endpoint, signal) {
|
|
2924
3091
|
session.gatewayRefused = null;
|
|
2925
3092
|
const own = endpoint.gatewayAccount;
|
|
3093
|
+
if (spec.gatewayAccount && !own) {
|
|
3094
|
+
throw new Error('A separate gateway sign-in is required before connecting.');
|
|
3095
|
+
}
|
|
2926
3096
|
const credentials = own?.credentials ?? endpoint.credentials;
|
|
2927
|
-
session.log.push(`gateway: ${gateway.host}:${gateway.port}, signing in with ${own ? 'its
|
|
3097
|
+
session.log.push(`gateway: ${gateway.host}:${gateway.port}, signing in with ${own ? 'its separate account' : 'the desktop\'s sign-in'}`);
|
|
2928
3098
|
try {
|
|
2929
3099
|
const stream = await (0, gateway_1.openThroughGateway)(() => target.openTcp(gateway.host, gateway.port), gateway, { username: credentials.username, password: credentials.password, domain: (own ? own.domain : endpoint.domain ?? spec.domain) || undefined }, { host: endpoint.host, port: endpoint.port }, (fingerprint, valid, reason, details) => this.checkGatewayCertificate(session, gateway, fingerprint, valid, reason, details), { log: m => session.log.push(m), clientName: os.hostname(), signal });
|
|
2930
3100
|
// Entered a moment ago and taken by the gateway: kept, while the account is still what it was signed in as.
|
|
2931
|
-
if (own?.remember) {
|
|
3101
|
+
if (own?.remember && own.saveKey) {
|
|
2932
3102
|
own.remember = false;
|
|
2933
|
-
const
|
|
2934
|
-
|
|
2935
|
-
(0, signin_1.saveCredentials)(own.saveKey, own.credentials, own.saveLabel).then(() => session.log.push(`gateway: saved as the account's password in ${(0, signin_1.storeName)()}`), e => session.log.push(`gateway: ${(0, signin_1.storeName)()}: ${e?.message ?? e}`));
|
|
2936
|
-
}
|
|
3103
|
+
const saveKey = own.saveKey;
|
|
3104
|
+
(0, signin_1.saveCredentialsIf)(saveKey, own.credentials, () => this.accountCurrent(saveKey, own.saveFor, own.accountRevision), own.saveLabel).then(saved => session.log.push(saved ? `gateway: saved as the account's password in ${(0, signin_1.storeName)()}` : 'gateway: not saved: the account changed meanwhile'), e => session.log.push(`gateway: ${(0, signin_1.storeName)()}: ${e?.message ?? e}`));
|
|
2937
3105
|
}
|
|
2938
3106
|
return stream;
|
|
2939
3107
|
}
|
|
@@ -3391,8 +3559,40 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
3391
3559
|
}
|
|
3392
3560
|
/** One RDP connection attempt, until it ends. */
|
|
3393
3561
|
async run(pane, target, spec, session, rdp, endpoint) {
|
|
3562
|
+
const attempt = new AbortController();
|
|
3563
|
+
let connectedAt;
|
|
3564
|
+
let unwatch;
|
|
3565
|
+
const trapped = new Promise(resolve => {
|
|
3566
|
+
unwatch = rdp.runtime?.onTrap((error) => {
|
|
3567
|
+
session.log.push(`wasm: this desktop's instance trapped: ${error.message}`);
|
|
3568
|
+
// Drop the socket even if Rust's run() promise can no longer settle after a trap.
|
|
3569
|
+
session.proxy?.close();
|
|
3570
|
+
session.proxy = null;
|
|
3571
|
+
resolve({ connected: session.state === 'connected', error: `WebAssembly instance failed: ${error.message}` });
|
|
3572
|
+
});
|
|
3573
|
+
});
|
|
3574
|
+
try {
|
|
3575
|
+
return await Promise.race([
|
|
3576
|
+
this.runConnection(pane, target, spec, session, rdp, endpoint, attempt.signal, at => { connectedAt = at; }),
|
|
3577
|
+
trapped,
|
|
3578
|
+
session.disposed.then(() => ({ connected: false })),
|
|
3579
|
+
]);
|
|
3580
|
+
}
|
|
3581
|
+
finally {
|
|
3582
|
+
attempt.abort();
|
|
3583
|
+
unwatch?.();
|
|
3584
|
+
// Rust's run() can stay pending after an asynchronous trap. Account for the completed attempt here,
|
|
3585
|
+
// exactly once, and never update retry state belonging to a replacement session in this pane.
|
|
3586
|
+
if (connectedAt !== undefined && this.sessions.get(pane) === session) {
|
|
3587
|
+
this.reconnectedUntilNow(pane, connectedAt);
|
|
3588
|
+
}
|
|
3589
|
+
session.endConnection?.();
|
|
3590
|
+
rdp.runtime?.dispose?.();
|
|
3591
|
+
}
|
|
3592
|
+
}
|
|
3593
|
+
async runConnection(pane, target, spec, session, rdp, endpoint, signal, onConnected) {
|
|
3394
3594
|
const credentials = endpoint.credentials;
|
|
3395
|
-
const alive = () => this.sessions.get(pane) === session;
|
|
3595
|
+
const alive = () => !signal.aborted && this.sessions.get(pane) === session;
|
|
3396
3596
|
session.certificateProblem = null;
|
|
3397
3597
|
session.gatewayRefused = null;
|
|
3398
3598
|
session.withoutNla = false;
|
|
@@ -3406,7 +3606,9 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
3406
3606
|
session.host.replaceChildren(el);
|
|
3407
3607
|
// The element only gets ready in the page: one whose desktop was closed meanwhile (the layer gone) never does.
|
|
3408
3608
|
// Closed as it got ready, it isn't given the clipboard, files and the rest that closing it has ended already.
|
|
3409
|
-
session.ui = await Promise.race([ready, session.disposed.then(() => null)
|
|
3609
|
+
session.ui = await Promise.race([ready, session.disposed.then(() => null), new Promise(resolve => {
|
|
3610
|
+
signal.addEventListener('abort', () => resolve(null), { once: true });
|
|
3611
|
+
})]);
|
|
3410
3612
|
if (!session.ui || !alive()) {
|
|
3411
3613
|
return { connected: false };
|
|
3412
3614
|
}
|
|
@@ -3546,6 +3748,8 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
3546
3748
|
if (!alive()) {
|
|
3547
3749
|
return { connected: false };
|
|
3548
3750
|
}
|
|
3751
|
+
const connectedAt = Date.now();
|
|
3752
|
+
onConnected(connectedAt);
|
|
3549
3753
|
session.ui.setVisibility(true);
|
|
3550
3754
|
this.applyKeyboardMode(session);
|
|
3551
3755
|
session.remoteSize = { width, height, scale: 100 };
|
|
@@ -3564,11 +3768,11 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
3564
3768
|
this.changed$.next();
|
|
3565
3769
|
// Under a saved account only while that account is still there with the same sign-in name: the prompt may
|
|
3566
3770
|
// have been open while it was removed or changed in Settings.
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
session.log.push(`sign-in:
|
|
3771
|
+
if (endpoint.remember && endpoint.saveKey) {
|
|
3772
|
+
const key = endpoint.saveKey;
|
|
3773
|
+
(0, signin_1.saveCredentialsIf)(key, credentials, () => this.accountCurrent(key, endpoint.saveFor, endpoint.accountRevision), endpoint.saveLabel).then(saved => session.log.push(saved ? `sign-in: saved as the account's password in ${(0, signin_1.storeName)()}` : 'sign-in: not saved: the account changed meanwhile'), e => session.log.push(`sign-in: ${(0, signin_1.storeName)()}: ${e?.message ?? e}`));
|
|
3570
3774
|
}
|
|
3571
|
-
else if (endpoint.remember &&
|
|
3775
|
+
else if (endpoint.remember && endpoint.mark !== undefined && (0, signin_1.forgottenSince)(session.credentialKey, endpoint.mark)) {
|
|
3572
3776
|
// Edited (its user name, gateway or address) or removed while the server delayed the sign-in: what was
|
|
3573
3777
|
// forgotten then stays forgotten, and the desktop asks the next time.
|
|
3574
3778
|
session.log.push('sign-in: not saved: the desktop was changed or removed meanwhile');
|
|
@@ -3587,12 +3791,14 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
3587
3791
|
}, e => session.log.push(`sign-in: ${(0, signin_1.storeName)()}: ${e?.message ?? e}`));
|
|
3588
3792
|
}
|
|
3589
3793
|
this.focusIfInFront(pane, session);
|
|
3590
|
-
const connectedAt = Date.now();
|
|
3591
3794
|
let end;
|
|
3592
3795
|
try {
|
|
3593
3796
|
end = await info.run();
|
|
3594
3797
|
}
|
|
3595
3798
|
catch (e) {
|
|
3799
|
+
if (!alive()) {
|
|
3800
|
+
return { connected: false };
|
|
3801
|
+
}
|
|
3596
3802
|
if (session.h264?.failed) {
|
|
3597
3803
|
// Reconnects (automatically, like any dropped connection) without H.264, unless the browser only
|
|
3598
3804
|
// took an idle decoder back (a hidden window): then with it, as a new stream starts with a key frame.
|
|
@@ -3613,15 +3819,7 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
3613
3819
|
}
|
|
3614
3820
|
return { connected: true, error };
|
|
3615
3821
|
}
|
|
3616
|
-
|
|
3617
|
-
// The server can't close the microphone, or its files, once the connection is gone.
|
|
3618
|
-
session.mic?.close();
|
|
3619
|
-
session.drives?.dispose();
|
|
3620
|
-
if (alive()) {
|
|
3621
|
-
this.reconnectedUntilNow(pane, connectedAt);
|
|
3622
|
-
}
|
|
3623
|
-
}
|
|
3624
|
-
return { connected: true, reason: end?.reason?.() };
|
|
3822
|
+
return alive() ? { connected: true, reason: end?.reason?.() } : { connected: false };
|
|
3625
3823
|
}
|
|
3626
3824
|
catch (e) {
|
|
3627
3825
|
return { connected: false, error: e?.message ?? String(e) };
|
|
@@ -3647,7 +3845,7 @@ exports.RemoteDesktopService = RemoteDesktopService = RemoteDesktopService_1 = _
|
|
|
3647
3845
|
tabby_core_1.PlatformService])
|
|
3648
3846
|
], RemoteDesktopService);
|
|
3649
3847
|
let styleInstalled = false;
|
|
3650
|
-
function installStyle() {
|
|
3848
|
+
function installStyle(compat) {
|
|
3651
3849
|
if (styleInstalled) {
|
|
3652
3850
|
return;
|
|
3653
3851
|
}
|
|
@@ -3655,7 +3853,7 @@ function installStyle() {
|
|
|
3655
3853
|
const style = document.createElement('style');
|
|
3656
3854
|
style.textContent = STYLE + signin_1.STYLE + fileTransfer_1.STYLE + connectionStatus_1.STYLE + osd_1.OSD_STYLE + DRAGBAR_STYLE + accountForm_1.STYLE;
|
|
3657
3855
|
document.head.appendChild(style);
|
|
3658
|
-
keepWindowDraggable();
|
|
3856
|
+
keepWindowDraggable(compat);
|
|
3659
3857
|
}
|
|
3660
3858
|
/** A strip the height of the tab bar that stays a window drag region over a dialog (the plugin's, and Tabby's). */
|
|
3661
3859
|
const DRAGBAR_STYLE = `
|
|
@@ -3663,12 +3861,14 @@ const DRAGBAR_STYLE = `
|
|
|
3663
3861
|
.modal > .trd-form-dragbar { display: block; position: fixed; left: 0; right: 0; top: 0; height: var(--tabs-height, 38px); -webkit-app-region: drag; }
|
|
3664
3862
|
`;
|
|
3665
3863
|
/**
|
|
3666
|
-
* Tabby
|
|
3667
|
-
*
|
|
3668
|
-
* is below it. The plugin's own dialogs do the same (see settingsPage.ts).
|
|
3864
|
+
* Older Tabby dialogs cover the tab bar without a drag region. Supply their fallback strip, deferring to the host's
|
|
3865
|
+
* modal pseudo-element when it provides one. The plugin's own dialogs still need their strips (see settingsPage.ts).
|
|
3669
3866
|
*/
|
|
3670
|
-
function keepWindowDraggable() {
|
|
3867
|
+
function keepWindowDraggable(compat) {
|
|
3671
3868
|
const strip = (modal) => {
|
|
3869
|
+
if (compat.hasNativeModalDragRegion(modal)) {
|
|
3870
|
+
return;
|
|
3871
|
+
}
|
|
3672
3872
|
if (!modal.querySelector(':scope > .trd-form-dragbar')) {
|
|
3673
3873
|
const bar = document.createElement('div');
|
|
3674
3874
|
bar.className = 'trd-form-dragbar';
|