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/help.js
CHANGED
|
@@ -9,7 +9,7 @@ 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.RemoteDesktopHelp = exports.esc = exports.TROUBLESHOOTING = exports.SETTINGS_TAB_ID = exports.HOTKEYS = exports.TOGGLE_HOTKEY = void 0;
|
|
12
|
+
exports.RemoteDesktopHelp = exports.esc = exports.TROUBLESHOOTING = exports.TROUBLESHOOTING_URL = exports.SETTINGS_TAB_ID = exports.HOTKEYS = exports.TOGGLE_HOTKEY = void 0;
|
|
13
13
|
exports.entryFor = entryFor;
|
|
14
14
|
exports.hotkeyLabel = hotkeyLabel;
|
|
15
15
|
const core_1 = require("@angular/core");
|
|
@@ -33,88 +33,50 @@ exports.HOTKEYS = [
|
|
|
33
33
|
];
|
|
34
34
|
/** The plugin's page in Tabby's settings (see settingsPage.ts). */
|
|
35
35
|
exports.SETTINGS_TAB_ID = 'remote-desktop';
|
|
36
|
+
exports.TROUBLESHOOTING_URL = 'https://github.com/meanaverage/tabby-rdp/blob/main/TROUBLESHOOTING.md';
|
|
36
37
|
exports.TROUBLESHOOTING = [
|
|
37
38
|
{
|
|
38
39
|
id: 'certificate',
|
|
39
|
-
title: '
|
|
40
|
+
title: 'A certificate this computer can\'t verify, or one that has changed',
|
|
40
41
|
match: /certificate/i,
|
|
41
|
-
body: `The first connection to a Windows or xrdp desktop remembers its certificate. When a later one differs, the
|
|
42
|
-
plugin stops before signing in, so nothing (no password) has been sent. Reinstalling the machine or renewing its
|
|
43
|
-
certificate changes it: then choose <b>Trust the new certificate</b>. If neither happened, something else is
|
|
44
|
-
answering at that address. A GNOME desktop only ever accepts the certificate the plugin made for it; a
|
|
45
|
-
different one there means another program listens on its port. Remembered certificates are listed above.`,
|
|
46
42
|
},
|
|
47
43
|
{
|
|
48
44
|
id: 'sign-in',
|
|
49
45
|
title: 'Wrong user name or password',
|
|
50
46
|
match: /sign-in|sign in|password|user name/i,
|
|
51
|
-
body: `Windows and xrdp desktops sign in with an account on that machine (<code>DOMAIN\\user</code> works). A saved
|
|
52
|
-
password that no longer works is asked for again; <b>Sign in again…</b> in the tab's menu replaces it on
|
|
53
|
-
purpose. xrdp doesn't refuse a wrong password: it shows its own login window instead. GNOME desktops never
|
|
54
|
-
ask: the plugin manages their sign-in.`,
|
|
55
47
|
},
|
|
56
48
|
{
|
|
57
49
|
id: 'no-desktop',
|
|
58
50
|
title: 'No desktop found on a Linux host',
|
|
59
51
|
match: /GNOME|xrdp|grdctl|headless/i,
|
|
60
|
-
body: `The plugin uses GNOME Remote Desktop 46 or newer (Ubuntu 24.04, for example), with no root and no login
|
|
61
|
-
screen. For KDE, XFCE, MATE and others, install xrdp and a desktop for its sessions: on Debian and Ubuntu,
|
|
62
|
-
<code>sudo apt install xrdp xfce4</code>. A GNOME session already shared from the machine's own screen (GNOME's
|
|
63
|
-
Desktop Sharing) has to be turned off first.`,
|
|
64
52
|
},
|
|
65
53
|
{
|
|
66
54
|
id: 'unreachable',
|
|
67
55
|
title: 'Can\'t reach a Windows desktop',
|
|
68
56
|
match: /refused|timed out|timeout|unreachable|ECONN|EHOST|no route|could not connect|couldn't connect/i,
|
|
69
|
-
body: `Windows needs Remote Desktop turned on (Settings › System › Remote Desktop; Pro, Enterprise or Server).
|
|
70
|
-
Behind an SSH host, the address is as that host sees it: from there, <code>nc -z <address> 3389</code>
|
|
71
|
-
should succeed. For a VM or machine that may be off, give the desktop a VM name or MAC address in its edit form
|
|
72
|
-
and it is started when opened.`,
|
|
73
57
|
},
|
|
74
58
|
{
|
|
75
59
|
id: 'reconnecting',
|
|
76
60
|
title: 'It keeps reconnecting',
|
|
77
61
|
match: /reconnect|connection lost/i,
|
|
78
|
-
body: `After sleep or a network change the desktop reconnects by itself, with growing pauses, and waits while the
|
|
79
|
-
SSH connection is down (<b>Reconnect SSH</b> hurries that along). It stops after a few tries or on
|
|
80
|
-
<b>Stop</b>. If it drops again right after connecting, check the host's free memory and that the desktop's
|
|
81
|
-
session is still running there.`,
|
|
82
62
|
},
|
|
83
63
|
{
|
|
84
64
|
id: 'picture',
|
|
85
65
|
title: 'The picture is blurry or slow',
|
|
86
66
|
match: /H\.264|decod|video/i,
|
|
87
|
-
body: `<b>Sharpness › Retina</b> renders at your screen's device pixels, with the remote's scale set to match. Video
|
|
88
|
-
(H.264) is used when the remote sends it: Windows does, GNOME only with a hardware encoder (VA-API or NVENC).
|
|
89
|
-
<b>Show connection status</b> shows the frame rate, the round trip and how the picture comes.`,
|
|
90
67
|
},
|
|
91
68
|
{
|
|
92
69
|
id: 'sound',
|
|
93
70
|
title: 'No sound, or the microphone isn\'t heard',
|
|
94
|
-
body: `Sound and microphone changes apply on the next connection. xrdp needs <code>pipewire-module-xrdp</code> or
|
|
95
|
-
<code>pulseaudio-module-xrdp</code>. The microphone needs the system's permission here (macOS: System Settings ›
|
|
96
|
-
Privacy & Security › Microphone, then restart Tabby). On GNOME, apps record from "Remoteaudio Source"; on
|
|
97
|
-
Windows, the machine must allow audio recording redirection.`,
|
|
98
71
|
},
|
|
99
72
|
{
|
|
100
73
|
id: 'nested-ssh',
|
|
101
74
|
title: 'I typed ssh to another machine in a terminal',
|
|
102
|
-
body: `In an SSH tab (or a split pane) where you typed <code>ssh</code> on to another machine, <b>Desktop</b> opens that
|
|
103
|
-
machine's desktop, going through the first one, and <code>desk</code> works there too. The first machine has to
|
|
104
|
-
log in to the other by itself for this: with a key there, or agent forwarding, as <code>ssh -o BatchMode=yes</code>
|
|
105
|
-
would. Tabby's <b>Reconnect</b> belongs to the tab: it reconnects the first machine and ends the ssh typed in it.
|
|
106
|
-
To have both desktops side by side, open the first machine's desktop in one pane, and ssh on in the other.`,
|
|
107
75
|
},
|
|
108
76
|
{
|
|
109
77
|
id: 'extra-monitor',
|
|
110
78
|
title: 'The desktop is open somewhere else',
|
|
111
79
|
match: /open somewhere else|opened somewhere else/,
|
|
112
|
-
body: `GNOME Remote Desktop's headless mode gives each connection a screen of its own: a second computer, or another
|
|
113
|
-
Tabby window, connected to the same account gets an empty extra screen rather than the same one. So when the
|
|
114
|
-
desktop is open somewhere else already, you choose. <b>Take it over</b> disconnects the other connection, as
|
|
115
|
-
Windows does; the session and its apps carry on, on this screen. <b>Open a second screen</b> keeps both, each
|
|
116
|
-
with a screen of its own. A desktop taken over from here doesn't take itself back: it says so, and offers
|
|
117
|
-
the same two choices.`,
|
|
118
80
|
},
|
|
119
81
|
];
|
|
120
82
|
/** The troubleshooting entry that explains a message, if one does. */
|
|
@@ -127,13 +89,15 @@ const esc = (s) => String(s ?? '').replace(/[&<>"']/g, c => `&#${c.charCodeAt(0)
|
|
|
127
89
|
exports.esc = esc;
|
|
128
90
|
function hotkeyLabel(binding) {
|
|
129
91
|
const first = Array.isArray(binding) ? binding[0] : null;
|
|
130
|
-
|
|
92
|
+
// Text only: the config may hold anything, and this runs for the settings page and the first-connect tip.
|
|
93
|
+
const strokes = (Array.isArray(first) ? first : [first]).filter((s) => typeof s === 'string');
|
|
131
94
|
if (!strokes.length) {
|
|
132
95
|
return null;
|
|
133
96
|
}
|
|
134
97
|
const mac = process.platform === 'darwin';
|
|
135
|
-
|
|
136
|
-
|
|
98
|
+
// A Map, not an object literal: a part named 'constructor' or 'toString' would find what objects inherit.
|
|
99
|
+
const MAC = new Map([['Shift', '⇧'], ['Alt', '⌥'], ['Ctrl', '⌃'], ['Meta', '⌘'], ['Cmd', '⌘']]);
|
|
100
|
+
return strokes.map(s => s.split('-').map(k => mac ? MAC.get(k) ?? k : k).join(mac ? '' : '+')).join(', ');
|
|
137
101
|
}
|
|
138
102
|
const TIP_STYLE = `
|
|
139
103
|
.trd-tip { position: absolute; left: 50%; bottom: 16px; transform: translateX(-50%); z-index: 2; max-width: min(560px, 90%);
|
|
@@ -151,10 +115,11 @@ const TIP_STYLE = `
|
|
|
151
115
|
* connects. Nothing else pops up.
|
|
152
116
|
*/
|
|
153
117
|
let RemoteDesktopHelp = class RemoteDesktopHelp {
|
|
154
|
-
constructor(app, config, zone) {
|
|
118
|
+
constructor(app, config, zone, platform) {
|
|
155
119
|
this.app = app;
|
|
156
120
|
this.config = config;
|
|
157
121
|
this.zone = zone;
|
|
122
|
+
this.platform = platform;
|
|
158
123
|
/** A section to show; the open settings page follows it (see settingsPage.ts). */
|
|
159
124
|
this.show$ = new rxjs_1.Subject();
|
|
160
125
|
/** For a settings page not yet made: where to scroll once it is. */
|
|
@@ -165,6 +130,11 @@ let RemoteDesktopHelp = class RemoteDesktopHelp {
|
|
|
165
130
|
}
|
|
166
131
|
/** Opens Tabby's settings at the plugin's page, at `topic` (and a troubleshooting entry). */
|
|
167
132
|
open(topic = 'start', entry) {
|
|
133
|
+
if (topic === 'troubleshooting') {
|
|
134
|
+
const anchor = exports.TROUBLESHOOTING.some(e => e.id === entry) ? `#${entry}` : '';
|
|
135
|
+
this.platform.openExternal(exports.TROUBLESHOOTING_URL + anchor);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
168
138
|
this.zone.run(() => {
|
|
169
139
|
this.pending = { topic, entry };
|
|
170
140
|
this.openSettings(exports.SETTINGS_TAB_ID);
|
|
@@ -258,6 +228,5 @@ let RemoteDesktopHelp = class RemoteDesktopHelp {
|
|
|
258
228
|
exports.RemoteDesktopHelp = RemoteDesktopHelp;
|
|
259
229
|
exports.RemoteDesktopHelp = RemoteDesktopHelp = __decorate([
|
|
260
230
|
(0, core_1.Injectable)({ providedIn: 'root' }),
|
|
261
|
-
__metadata("design:paramtypes", [tabby_core_1.AppService, tabby_core_1.ConfigService, core_1.NgZone])
|
|
231
|
+
__metadata("design:paramtypes", [tabby_core_1.AppService, tabby_core_1.ConfigService, core_1.NgZone, tabby_core_1.PlatformService])
|
|
262
232
|
], RemoteDesktopHelp);
|
|
263
|
-
//# sourceMappingURL=help.js.map
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HostCompatibility = void 0;
|
|
4
|
+
exports.terminalInputs = terminalInputs;
|
|
5
|
+
exports.createHostCompatibility = createHostCompatibility;
|
|
6
|
+
function text(value) {
|
|
7
|
+
return typeof value === 'string' && value.trim() ? value.replace(/[\x00-\x1f\x7f]/g, ' ').trim().slice(0, 256) || null : null;
|
|
8
|
+
}
|
|
9
|
+
function read(source) {
|
|
10
|
+
try {
|
|
11
|
+
return source?.();
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/** Ranges spanning multiple majors, URLs and workspace aliases cannot establish a single major. */
|
|
18
|
+
function declaredMajor(declaration) {
|
|
19
|
+
const match = declaration?.match(/^[~^]?(\d+)(?:\.(?:\d+|x|\*)(?:\.(?:\d+|x|\*))?)?(?:-[\da-z.-]+)?(?:\+[\da-z.-]+)?$/i);
|
|
20
|
+
const major = match ? Number(match[1]) : NaN;
|
|
21
|
+
return Number.isSafeInteger(major) ? major : null;
|
|
22
|
+
}
|
|
23
|
+
/** Shared terminal DOM adapter; keep desktop layers and forms in agreement about the input they cover. */
|
|
24
|
+
function terminalInputs(container) {
|
|
25
|
+
return Array.from(container.querySelectorAll('textarea.xterm-helper-textarea'));
|
|
26
|
+
}
|
|
27
|
+
/** An Angular factory provides one instance per plugin window; this module itself needs no Angular runtime. */
|
|
28
|
+
class HostCompatibility {
|
|
29
|
+
constructor(sources) {
|
|
30
|
+
this.nativeModalDragRegion = null;
|
|
31
|
+
const xtermDeclaration = text(read(() => {
|
|
32
|
+
const terminal = sources.terminalPackage?.();
|
|
33
|
+
return text(terminal?.dependencies?.['@xterm/xterm']) ?? text(terminal?.devDependencies?.['@xterm/xterm']);
|
|
34
|
+
}));
|
|
35
|
+
this.info = Object.freeze({
|
|
36
|
+
name: text(read(sources.name)), version: text(read(sources.version)), platform: text(sources.platform),
|
|
37
|
+
electron: text(sources.electron), node: text(sources.node), xtermDeclaration,
|
|
38
|
+
xtermMajor: declaredMajor(xtermDeclaration),
|
|
39
|
+
});
|
|
40
|
+
this.modalStyle = sources.modalStyle ?? ((element, pseudo) => getComputedStyle(element, pseudo));
|
|
41
|
+
}
|
|
42
|
+
/** Probe each modal; themes and UI scale can change while the application is running. */
|
|
43
|
+
hasNativeModalDragRegion(modal) {
|
|
44
|
+
let supported = false;
|
|
45
|
+
try {
|
|
46
|
+
const style = this.modalStyle(modal, '::before');
|
|
47
|
+
supported = style.content !== 'none' && style.content !== 'normal' && style.display !== 'none' &&
|
|
48
|
+
style.getPropertyValue('-webkit-app-region') === 'drag' && parseFloat(style.height) > 0;
|
|
49
|
+
}
|
|
50
|
+
catch { }
|
|
51
|
+
this.nativeModalDragRegion = supported;
|
|
52
|
+
return supported;
|
|
53
|
+
}
|
|
54
|
+
snapshot(pane) {
|
|
55
|
+
return {
|
|
56
|
+
...this.info,
|
|
57
|
+
capabilities: {
|
|
58
|
+
nativeModalDragRegion: this.nativeModalDragRegion,
|
|
59
|
+
terminalInput: pane ? terminalInputs(pane).length > 0 : null,
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/** Local build information and observed capabilities only; no config, accounts, paths or connection details. */
|
|
64
|
+
diagnosticLines(pane) {
|
|
65
|
+
const info = this.snapshot(pane);
|
|
66
|
+
const { nativeModalDragRegion, terminalInput } = info.capabilities;
|
|
67
|
+
const drag = nativeModalDragRegion === null ? 'unobserved' : nativeModalDragRegion ? 'host' : 'plugin fallback';
|
|
68
|
+
const input = terminalInput === null ? 'unobserved' : terminalInput ? 'available' : 'unavailable';
|
|
69
|
+
return [
|
|
70
|
+
`Host: ${info.name ?? 'unknown'} ${info.version ?? 'unknown'} on ${info.platform ?? 'unknown'}`,
|
|
71
|
+
`Runtime: Electron ${info.electron ?? 'unknown'}, Node ${info.node ?? 'unknown'}`,
|
|
72
|
+
`Terminal: xterm ${info.xtermDeclaration ?? 'unknown'} (host declaration)`,
|
|
73
|
+
`Compatibility: modal drag region ${drag}; terminal input ${input}`,
|
|
74
|
+
];
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
exports.HostCompatibility = HostCompatibility;
|
|
78
|
+
/** Prefer Tabby's public version API; optional metadata reads never become requirements for operating a desktop. */
|
|
79
|
+
function createHostCompatibility(platform) {
|
|
80
|
+
return new HostCompatibility({
|
|
81
|
+
name: () => require('@electron/remote').app.getName(),
|
|
82
|
+
version: () => platform.getAppVersion(),
|
|
83
|
+
platform: process.platform, electron: process.versions.electron, node: process.versions.node,
|
|
84
|
+
terminalPackage: () => require('tabby-terminal/package.json'),
|
|
85
|
+
});
|
|
86
|
+
}
|
package/dist/hyperv.js
CHANGED
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.HYPERV_PORT = void 0;
|
|
3
|
+
exports.MAX_VMS = exports.HYPERV_PORT = void 0;
|
|
4
4
|
exports.vmId = vmId;
|
|
5
5
|
exports.scanHyperV = scanHyperV;
|
|
6
6
|
exports.hyperVState = hyperVState;
|
|
7
7
|
exports.startHyperV = startHyperV;
|
|
8
8
|
/** VMConnect's port on a Hyper-V host. */
|
|
9
9
|
exports.HYPERV_PORT = 2179;
|
|
10
|
+
/**
|
|
11
|
+
* How many VMs a host's scan lists at most (see vms.ts too). Each is a menu item, and an answer of the host's making
|
|
12
|
+
* could otherwise make thousands of them. A host can have more (a Hyper-V server can run 1,024): the first ones are
|
|
13
|
+
* listed, and the menu says that only so many are (see RemoteDesktopService.vmsCut).
|
|
14
|
+
*/
|
|
15
|
+
exports.MAX_VMS = 256;
|
|
10
16
|
const GUID = /^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i;
|
|
11
17
|
/** A VM id as Hyper-V prints it, or '' if this isn't one. */
|
|
12
18
|
function vmId(value) {
|
|
13
|
-
|
|
19
|
+
// Text only: String() of an object from the config runs its own conversions, which can throw.
|
|
20
|
+
const id = (typeof value === 'string' ? value : '').trim().replace(/^\{|\}$/g, '');
|
|
14
21
|
return GUID.test(id) ? id.toLowerCase() : '';
|
|
15
22
|
}
|
|
16
23
|
/**
|
|
@@ -27,19 +34,63 @@ const SCAN = String.raw `
|
|
|
27
34
|
$ErrorActionPreference = 'SilentlyContinue'
|
|
28
35
|
if (Get-Command Get-VM) { Get-VM | ForEach-Object { 'TRD_HV ' + $_.Id + '|' + $_.State + '|' + $_.Name } }
|
|
29
36
|
`;
|
|
30
|
-
/**
|
|
31
|
-
|
|
37
|
+
/**
|
|
38
|
+
* A line of the scan's answer longer than this is none of its own: its id and state take some 50 characters, and
|
|
39
|
+
* Hyper-V keeps a VM's name to 100.
|
|
40
|
+
*/
|
|
41
|
+
const MAX_LINE = 1024;
|
|
42
|
+
/**
|
|
43
|
+
* A line of the scan's answer, `TRD_HV <id>|<state>|<name>` (the name may hold bars), as a VM; null for any other line.
|
|
44
|
+
* Read by where its bars are, not with a pattern whose parts can take the same characters: a long line of the host's
|
|
45
|
+
* making could make that cost the square of its length.
|
|
46
|
+
*/
|
|
47
|
+
function scannedVM(raw) {
|
|
48
|
+
const line = raw.length > MAX_LINE ? '' : raw.trim();
|
|
49
|
+
const first = line.indexOf('|');
|
|
50
|
+
const second = first < 0 ? -1 : line.indexOf('|', first + 1);
|
|
51
|
+
if (!line.startsWith('TRD_HV ') || second < 0) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
const id = vmId(line.slice('TRD_HV '.length, first));
|
|
55
|
+
const name = line.slice(second + 1);
|
|
56
|
+
// A name on one line, as PowerShell prints it.
|
|
57
|
+
if (!id || !name || /[\r\u2028\u2029]/.test(name)) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
return { id, name, state: line.slice(first + 1, second) === 'Running' ? 'running' : 'off' };
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The host's Hyper-V VMs, running or not (one that is off is started when opened), MAX_VMS at most; `listed.cut` says
|
|
64
|
+
* whether the host has more. Read-only.
|
|
65
|
+
*/
|
|
66
|
+
async function scanHyperV(target, listed = {}) {
|
|
32
67
|
const out = await powershell(target, SCAN);
|
|
33
68
|
const found = [];
|
|
69
|
+
listed.cut = false;
|
|
70
|
+
// Ids seen so far: looked up, not searched for, so that the time taken grows with the lines, not their square.
|
|
71
|
+
const ids = new Set();
|
|
34
72
|
for (const line of out.split('\n')) {
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
found.
|
|
73
|
+
const vm = scannedVM(line);
|
|
74
|
+
if (vm && !ids.has(vm.id)) {
|
|
75
|
+
// One more than are listed: the list is cut short.
|
|
76
|
+
if (found.length === exports.MAX_VMS) {
|
|
77
|
+
listed.cut = true;
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
ids.add(vm.id);
|
|
81
|
+
found.push(vm);
|
|
39
82
|
}
|
|
40
83
|
}
|
|
41
84
|
return found;
|
|
42
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* What follows `prefix` on each line of the host's answer that starts with it, trimmed, and MAX_LINE characters of it
|
|
88
|
+
* at most (the host's reason for an error is shown as it is). Line by line: a pattern run over the whole answer can
|
|
89
|
+
* match across its lines, which a long answer of the host's making can make cost the square of its length.
|
|
90
|
+
*/
|
|
91
|
+
function linesAfter(out, prefix) {
|
|
92
|
+
return out.split('\n').map(line => line.trim()).filter(line => line.startsWith(prefix)).map(line => line.slice(prefix.length).trim().slice(0, MAX_LINE));
|
|
93
|
+
}
|
|
43
94
|
// TRD_HV_STATE <state>|<enhanced session mode state>, or TRD_HV_ERR <why>. The enhanced state is
|
|
44
95
|
// Msvm_ComputerSystem's: 2 when the guest takes enhanced sessions right now (the host allows them, and the guest's
|
|
45
96
|
// Remote Desktop Services answer on its bus), anything else when only the basic console is there.
|
|
@@ -53,9 +104,9 @@ try {
|
|
|
53
104
|
/** How a VM is now: running or not, and whether it takes an enhanced session. Throws with the host's reason when it can't say. */
|
|
54
105
|
async function hyperVState(target, id) {
|
|
55
106
|
const out = await powershell(target, STATE.replace('@@ID@@', vmId(id)));
|
|
56
|
-
const state =
|
|
107
|
+
const state = linesAfter(out, 'TRD_HV_STATE ').map(line => /^([^|]*)\|(\d*)$/.exec(line)).find(Boolean);
|
|
57
108
|
if (!state) {
|
|
58
|
-
throw new Error(
|
|
109
|
+
throw new Error(linesAfter(out, 'TRD_HV_ERR ')[0] || 'the host didn\'t answer about the VM (is PowerShell\'s Hyper-V module there, and may this account use it?)');
|
|
59
110
|
}
|
|
60
111
|
return { running: state[1] === 'Running', enhanced: state[2] === '2' };
|
|
61
112
|
}
|
|
@@ -70,10 +121,9 @@ try {
|
|
|
70
121
|
/** Starts (or resumes) the VM. Resolves with what was done; throws with the host's reason when it couldn't. */
|
|
71
122
|
async function startHyperV(target, id) {
|
|
72
123
|
const out = await powershell(target, START.replace('@@ID@@', vmId(id)));
|
|
73
|
-
const ok =
|
|
74
|
-
if (!ok) {
|
|
75
|
-
throw new Error(
|
|
124
|
+
const ok = linesAfter(out, 'TRD_HV_OK ');
|
|
125
|
+
if (!ok.length) {
|
|
126
|
+
throw new Error(linesAfter(out, 'TRD_HV_ERR ')[0] || 'the host didn\'t answer');
|
|
76
127
|
}
|
|
77
|
-
return ok[
|
|
128
|
+
return ok[0];
|
|
78
129
|
}
|
|
79
|
-
//# sourceMappingURL=hyperv.js.map
|
package/dist/index.js
CHANGED
|
@@ -18,6 +18,7 @@ const desk_1 = require("./desk");
|
|
|
18
18
|
const header_1 = require("./header");
|
|
19
19
|
const keyboard_1 = require("./keyboard");
|
|
20
20
|
const help_1 = require("./help");
|
|
21
|
+
const hostCompat_1 = require("./hostCompat");
|
|
21
22
|
const signin_1 = require("./signin");
|
|
22
23
|
const rdpFile_1 = require("./rdpFile");
|
|
23
24
|
const rdpProfile_1 = require("./rdpProfile");
|
|
@@ -28,7 +29,8 @@ const targets_1 = require("./targets");
|
|
|
28
29
|
const ui_1 = require("./ui");
|
|
29
30
|
let RemoteDesktopModule = class RemoteDesktopModule {
|
|
30
31
|
constructor(app, hotkeys, desktop, header, targets, keyboard, injector) {
|
|
31
|
-
|
|
32
|
+
const compat = injector.get(hostCompat_1.HostCompatibility);
|
|
33
|
+
(0, desktop_service_1.installStyle)(compat);
|
|
32
34
|
header.install();
|
|
33
35
|
// Passwords go to Tabby's Vault while it is enabled (as Tabby's SSH passwords do), else to the system keychain.
|
|
34
36
|
try {
|
|
@@ -49,7 +51,8 @@ let RemoteDesktopModule = class RemoteDesktopModule {
|
|
|
49
51
|
'remote-desktop-disconnect': pane => desktop.disconnect(pane),
|
|
50
52
|
};
|
|
51
53
|
hotkeys.hotkey$.subscribe((id) => {
|
|
52
|
-
|
|
54
|
+
// Tabby fires any key bound under `hotkeys` in the config ('constructor' too): the table's own entries only.
|
|
55
|
+
const action = Object.prototype.hasOwnProperty.call(actions, id) ? actions[id] : undefined;
|
|
53
56
|
const pane = action && (0, targets_1.desktopPaneOf)(app.activeTab);
|
|
54
57
|
if (!action || !pane) {
|
|
55
58
|
return;
|
|
@@ -61,12 +64,13 @@ let RemoteDesktopModule = class RemoteDesktopModule {
|
|
|
61
64
|
});
|
|
62
65
|
// Handle for tests and troubleshooting from DevTools.
|
|
63
66
|
const w = window;
|
|
64
|
-
w.__remoteDesktop = { app, desktop, targets, desktopPaneOf: targets_1.desktopPaneOf, injector, execRemote: ssh_1.execRemote, parseRdpFile: rdpFile_1.parseRdpFile, help: injector.get(help_1.RemoteDesktopHelp), updates: injector.get(updates_1.UpdateCheck) };
|
|
67
|
+
w.__remoteDesktop = { app, desktop, targets, desktopPaneOf: targets_1.desktopPaneOf, injector, compat, execRemote: ssh_1.execRemote, parseRdpFile: rdpFile_1.parseRdpFile, help: injector.get(help_1.RemoteDesktopHelp), updates: injector.get(updates_1.UpdateCheck) };
|
|
65
68
|
}
|
|
66
69
|
};
|
|
67
70
|
RemoteDesktopModule = __decorate([
|
|
68
71
|
(0, core_1.NgModule)({
|
|
69
72
|
providers: [
|
|
73
|
+
{ provide: hostCompat_1.HostCompatibility, useFactory: hostCompat_1.createHostCompatibility, deps: [tabby_core_1.PlatformService] },
|
|
70
74
|
{ provide: tabby_core_1.HotkeyProvider, useClass: ui_1.RemoteDesktopHotkeys, multi: true },
|
|
71
75
|
{ provide: tabby_core_1.ConfigProvider, useClass: ui_1.RemoteDesktopConfig, multi: true },
|
|
72
76
|
{ provide: tabby_core_1.TabContextMenuItemProvider, useClass: ui_1.RemoteDesktopContextMenu, multi: true },
|
|
@@ -85,4 +89,3 @@ RemoteDesktopModule = __decorate([
|
|
|
85
89
|
__metadata("design:paramtypes", [tabby_core_1.AppService, tabby_core_1.HotkeysService, desktop_service_1.RemoteDesktopService, header_1.HeaderControls, targets_1.RemoteTargets, keyboard_1.DesktopKeyboard, core_1.Injector])
|
|
86
90
|
], RemoteDesktopModule);
|
|
87
91
|
exports.default = RemoteDesktopModule;
|
|
88
|
-
//# sourceMappingURL=index.js.map
|
package/dist/keyboard.js
CHANGED
package/dist/microphone.js
CHANGED
|
@@ -2,14 +2,16 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.Resampler = exports.Microphone = void 0;
|
|
4
4
|
/**
|
|
5
|
-
* Redirects the local microphone to a remote desktop (IronRDP's `audioInput` extension). The server asks for it
|
|
6
|
-
*
|
|
7
|
-
* the microphone is captured only in between. The browser's capture runs at the audio device's
|
|
8
|
-
* and converted to interleaved 16-bit PCM here, in blocks of about 20 ms.
|
|
5
|
+
* Redirects the local microphone to a remote desktop (IronRDP's `audioInput` extension). The server asks for it when it
|
|
6
|
+
* likes (by its own account, while an application there records): IronRDP says `open` (with the format the server
|
|
7
|
+
* chose) and `close`, and the microphone is captured only in between. The browser's capture runs at the audio device's
|
|
8
|
+
* rate; it is resampled and converted to interleaved 16-bit PCM here, in blocks of about 20 ms.
|
|
9
9
|
*
|
|
10
10
|
* It keeps capturing while the desktop layer is hidden: the remote application is still recording (a call, say), and
|
|
11
|
-
* the console being in front doesn't mean the user stopped talking to it.
|
|
12
|
-
*
|
|
11
|
+
* the console being in front doesn't mean the user stopped talking to it. What the remote does with it is out of
|
|
12
|
+
* sight then, so it shows elsewhere (see RemoteDesktopService.microphoneChanged): a red microphone in Tabby's header
|
|
13
|
+
* for as long as it lasts, and a note when a capture starts on a desktop that isn't showing. turnOff() stops it here,
|
|
14
|
+
* and turnOn() takes the remote's requests again.
|
|
13
15
|
*/
|
|
14
16
|
class Microphone {
|
|
15
17
|
/**
|
|
@@ -37,6 +39,8 @@ class Microphone {
|
|
|
37
39
|
this.starting = false;
|
|
38
40
|
this.resampler = null;
|
|
39
41
|
this.reportedError = false;
|
|
42
|
+
/** Turned off here (see turnOff): the remote's requests are refused for the rest of the connection. */
|
|
43
|
+
this.off = false;
|
|
40
44
|
/** The callback for IronRDP's audioInput extension. */
|
|
41
45
|
this.callback = (message) => {
|
|
42
46
|
try {
|
|
@@ -49,6 +53,10 @@ class Microphone {
|
|
|
49
53
|
}
|
|
50
54
|
this.format = { sampleRate, channels };
|
|
51
55
|
this.log(`microphone: the remote opened it (${sampleRate} Hz, ${channels === 1 ? 'mono' : 'stereo'})`);
|
|
56
|
+
if (this.off) {
|
|
57
|
+
this.log('microphone: not sent: turned off here');
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
52
60
|
this.start().catch(e => this.failed(e));
|
|
53
61
|
}
|
|
54
62
|
else if (message?.type === 'close') {
|
|
@@ -74,8 +82,8 @@ class Microphone {
|
|
|
74
82
|
finally {
|
|
75
83
|
this.starting = false;
|
|
76
84
|
}
|
|
77
|
-
// Closed and opened again while starting: that start was dropped, so start over.
|
|
78
|
-
if (!this.stream && this.format) {
|
|
85
|
+
// Closed and opened again while starting: that start was dropped, so start over (unless turned off meanwhile).
|
|
86
|
+
if (!this.stream && this.format && !this.off) {
|
|
79
87
|
await this.start();
|
|
80
88
|
}
|
|
81
89
|
}
|
|
@@ -100,28 +108,42 @@ class Microphone {
|
|
|
100
108
|
let capture;
|
|
101
109
|
try {
|
|
102
110
|
await context.audioWorklet.addModule(workletURL());
|
|
111
|
+
if (generation !== this.generation) {
|
|
112
|
+
return; // closed meanwhile: stop() released the stream and closed the context, which takes no new nodes
|
|
113
|
+
}
|
|
103
114
|
const node = new AudioWorkletNode(context, 'trd-microphone', { numberOfInputs: 1, numberOfOutputs: 1, outputChannelCount: [1] });
|
|
104
|
-
node.port.onmessage = event =>
|
|
115
|
+
node.port.onmessage = event => {
|
|
116
|
+
// A block the worklet posted before the capture stopped can still arrive after it: dropped, not sent.
|
|
117
|
+
if (generation === this.generation) {
|
|
118
|
+
this.captured(event.data, context.sampleRate);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
105
121
|
capture = node;
|
|
106
122
|
}
|
|
107
123
|
catch (e) {
|
|
124
|
+
if (generation !== this.generation) {
|
|
125
|
+
return; // closed meanwhile, which may be why the module was refused: as above, and not an error
|
|
126
|
+
}
|
|
108
127
|
// No AudioWorklet (or its module was refused): the deprecated ScriptProcessor does the same on the main thread.
|
|
109
128
|
this.log(`microphone: AudioWorklet unavailable (${e?.message ?? e}), using ScriptProcessor`);
|
|
110
129
|
const node = context.createScriptProcessor(1024, 2, 1);
|
|
111
130
|
node.onaudioprocess = event => {
|
|
131
|
+
if (generation !== this.generation) {
|
|
132
|
+
return; // stopped: as above
|
|
133
|
+
}
|
|
112
134
|
const input = event.inputBuffer;
|
|
113
135
|
this.captured(Array.from({ length: input.numberOfChannels }, (_, i) => input.getChannelData(i).slice()), context.sampleRate);
|
|
114
136
|
};
|
|
115
137
|
capture = node;
|
|
116
138
|
}
|
|
117
|
-
if (generation !== this.generation) {
|
|
118
|
-
return; // closed meanwhile: stop() released the stream and the context
|
|
119
|
-
}
|
|
120
139
|
source.connect(capture);
|
|
121
140
|
capture.connect(mute);
|
|
122
141
|
this.nodes = [source, capture, mute];
|
|
123
142
|
if (context.state === 'suspended') {
|
|
124
143
|
await context.resume().catch(() => null);
|
|
144
|
+
if (generation !== this.generation) {
|
|
145
|
+
return; // closed or turned off while resuming: stop() released everything, and it isn't capturing
|
|
146
|
+
}
|
|
125
147
|
}
|
|
126
148
|
const track = stream.getAudioTracks()[0];
|
|
127
149
|
this.log(`microphone: capturing from ${track?.label || 'the default input'} at ${context.sampleRate} Hz`);
|
|
@@ -188,6 +210,29 @@ class Microphone {
|
|
|
188
210
|
this.changed();
|
|
189
211
|
}
|
|
190
212
|
}
|
|
213
|
+
/**
|
|
214
|
+
* Turned off here (the setting, or Stop for this desktop): stops capturing at once, also a capture still starting,
|
|
215
|
+
* and refuses the remote's later requests on this connection. Whether the next connection gets the microphone is
|
|
216
|
+
* the service's to say (see RemoteDesktopService.stopMicrophone).
|
|
217
|
+
*/
|
|
218
|
+
turnOff() {
|
|
219
|
+
if (!this.off) {
|
|
220
|
+
this.off = true;
|
|
221
|
+
this.log('microphone: turned off here');
|
|
222
|
+
}
|
|
223
|
+
this.stop();
|
|
224
|
+
}
|
|
225
|
+
/** Turned on again here (the desktop's menu): the remote's requests are taken again, one open now included. */
|
|
226
|
+
turnOn() {
|
|
227
|
+
if (!this.off) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
this.off = false;
|
|
231
|
+
this.log('microphone: turned on again here');
|
|
232
|
+
if (this.format) {
|
|
233
|
+
this.start().catch(e => this.failed(e));
|
|
234
|
+
}
|
|
235
|
+
}
|
|
191
236
|
/** The connection ended. */
|
|
192
237
|
close() {
|
|
193
238
|
this.format = null;
|
|
@@ -292,4 +337,3 @@ function workletURL() {
|
|
|
292
337
|
workletBlobURL ??= URL.createObjectURL(new Blob([WORKLET], { type: 'application/javascript' }));
|
|
293
338
|
return workletBlobURL;
|
|
294
339
|
}
|
|
295
|
-
//# sourceMappingURL=microphone.js.map
|
package/dist/ntlm.js
CHANGED