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/remoteSetup.js
CHANGED
|
@@ -3,14 +3,17 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.prepareRemoteDesktop = prepareRemoteDesktop;
|
|
4
4
|
const fs = require("fs");
|
|
5
5
|
const path = require("path");
|
|
6
|
+
const desktops_1 = require("./desktops");
|
|
6
7
|
const rdcleanpath_1 = require("./rdcleanpath");
|
|
7
8
|
// Runs as the SSH user; needs no root. Prints exactly one `RD_OK port=N user=U pass=P cert=SHA256`,
|
|
8
9
|
// `RD_XRDP port=N user=U` (no GNOME, but xrdp), `RD_WINDOWS` (a Windows host's POSIX sh) or `RD_ERR <reason>` line;
|
|
9
|
-
// with RD_OK, also an RD_XRDP line when xrdp runs besides GNOME, `RD_CLIENTS N`, the clients connected already,
|
|
10
|
-
// `RD_TAKEN_OVER <client> <seconds ago>` after a recent take-over
|
|
10
|
+
// with RD_OK, also an RD_XRDP line when xrdp runs besides GNOME, `RD_CLIENTS N`, the clients connected already,
|
|
11
|
+
// `RD_TAKEN_OVER <client> <seconds ago>` after a recent take-over, `RD_DESK <key>` with 'desk' on, and an `RD_NOTE <text>`
|
|
12
|
+
// for each rc file it couldn't change.
|
|
11
13
|
// The RDP password is generated once per remote user and kept in a 0600 file; grd reads credentials only
|
|
12
14
|
// at startup, so grd is restarted only when its configuration actually changes (that drops live sessions).
|
|
13
|
-
//
|
|
15
|
+
// Secrets never go on a command line, which any user there can see in the process list, except where grdctl can't
|
|
16
|
+
// read the password from its input: then it is an argument once, when the credentials are set.
|
|
14
17
|
const SETUP_SCRIPT = String.raw `
|
|
15
18
|
say () { printf '%s\n' "$*"; }
|
|
16
19
|
fail () { say "RD_ERR $*"; exit 0; }
|
|
@@ -59,12 +62,49 @@ OLD="$HOME/.local/share/tabby-remote-desktop"
|
|
|
59
62
|
if [ -d "$OLD" ] && [ ! -e "$D" ]; then
|
|
60
63
|
mv "$OLD" "$D" || fail "could not move $OLD to $D"
|
|
61
64
|
fi
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
+
mkdir -p "$D" && chmod 700 "$D" || fail "could not create $D"
|
|
66
|
+
# The login hook ('desk'): this one line in ~/.bashrc and ~/.zshrc, added and removed exactly, so that a line of the
|
|
67
|
+
# user's own that only looks like it stays.
|
|
68
|
+
HOOK='[ -f "$HOME/.local/share/tabby-rdp/login.sh" ] && . "$HOME/.local/share/tabby-rdp/login.sh" # tabby-rdp'
|
|
69
|
+
# Takes the lines that are exactly $2 out of rc file $1 and changes nothing else. grep reads it byte by byte, as text
|
|
70
|
+
# (LC_ALL=C, -a): otherwise a NUL byte, or a byte the host's locale can't read, has it leave lines out or print "Binary
|
|
71
|
+
# file … matches" in their place. What it leaves must be the file less those lines, to the byte, and is written into
|
|
72
|
+
# the file itself, so that a symbolic link to it, another name for it (a hard link), its mode and its owner all stay.
|
|
73
|
+
# A file that can't be written (read-only) or doesn't add up is left as it is, and the setup says so (RD_NOTE).
|
|
74
|
+
unhook () {
|
|
75
|
+
[ -f "$1" ] || return 0
|
|
76
|
+
LC_ALL=C grep -a -q -xF -- "$2" "$1" 2>/dev/null
|
|
77
|
+
case $? in 0) ;; 1) return 0 ;; *) say "RD_NOTE desk's line may be in $1, which can't be read"; return 0 ;; esac
|
|
78
|
+
uh_tmp=$(mktemp "$D/rc.XXXXXX") || { say "RD_NOTE desk's line stays in $1: no temporary file could be made in $D"; return 0; }
|
|
79
|
+
LC_ALL=C grep -a -v -xF -- "$2" "$1" > "$uh_tmp"
|
|
80
|
+
# 1: no line left, the file held only the hook. Each line taken out goes with its newline, and grep ends a last line
|
|
81
|
+
# that has none with one: so what is left is shorter by exactly that, or one byte longer than that. A step that fails
|
|
82
|
+
# counts as a mismatch (rather than ending the script, as a failed sum would).
|
|
83
|
+
if [ $? -le 1 ] && uh_was=$(wc -c < "$1") && uh_lines=$(LC_ALL=C grep -a -c -xF -- "$2" "$1") && uh_now=$(wc -c < "$uh_tmp"); then
|
|
84
|
+
uh_more=$(( $uh_now - ($uh_was - $uh_lines * (${"$"}{#2} + 1)) ))
|
|
85
|
+
else
|
|
86
|
+
uh_more=-1
|
|
65
87
|
fi
|
|
88
|
+
if [ "$uh_more" != 0 ] && [ "$uh_more" != 1 ]; then
|
|
89
|
+
say "RD_NOTE desk's line stays in $1: the rest of it didn't copy exactly"
|
|
90
|
+
elif ! cat "$uh_tmp" 2>/dev/null > "$1"; then
|
|
91
|
+
# Not opened (read-only), the file is as it was, its line in it. Otherwise writing it failed on the way (a full
|
|
92
|
+
# disk): what it is to hold stays where the user can find it.
|
|
93
|
+
if ! LC_ALL=C grep -a -q -xF -- "$2" "$1" 2>/dev/null; then
|
|
94
|
+
say "RD_NOTE writing $1 failed; what it should hold (without desk's line) is in $uh_tmp"
|
|
95
|
+
return 0
|
|
96
|
+
fi
|
|
97
|
+
say "RD_NOTE desk's line stays in $1: it can't be written (read-only?)"
|
|
98
|
+
fi
|
|
99
|
+
rm -f "$uh_tmp"
|
|
100
|
+
}
|
|
101
|
+
# The hook from before 0.2, as those versions wrote it, and what it sources, where the old folder is still there (it
|
|
102
|
+
# couldn't be moved over): so that a copy of the line that was changed since, and stays, does nothing either.
|
|
103
|
+
OLD_HOOK='[ -f "$HOME/.local/share/tabby-remote-desktop/login.sh" ] && . "$HOME/.local/share/tabby-remote-desktop/login.sh" # tabby-remote-desktop'
|
|
104
|
+
for rc in "$HOME/.bashrc" "$HOME/.zshrc"; do
|
|
105
|
+
unhook "$rc" "$OLD_HOOK"
|
|
66
106
|
done
|
|
67
|
-
|
|
107
|
+
rm -f "$OLD/login.sh"
|
|
68
108
|
NEW_CERT=0
|
|
69
109
|
if [ ! -s "$D/tls.key" ]; then
|
|
70
110
|
openssl req -new -newkey rsa:3072 -days 3650 -nodes -x509 -subj "/CN=$(hostname)" \
|
|
@@ -77,8 +117,29 @@ CERT=$(openssl x509 -in "$D/tls.crt" -noout -fingerprint -sha256 2>/dev/null | s
|
|
|
77
117
|
# 'desk' support (setting, off by default): interactive SSH logins start inside a shareable session (trd-pty,
|
|
78
118
|
# or tmux), so 'desk' can attach this exact console to a terminal on the desktop. Rewritten on every connect;
|
|
79
119
|
# opt out per login with TABBY_NO_SESSION=1. When the setting is off, the hook and helpers are removed.
|
|
120
|
+
DESK_KEY=
|
|
80
121
|
if [ "${"$"}{TRD_DESK:-0}" = 1 ]; then
|
|
81
122
|
mkdir -p "$D/bin"
|
|
123
|
+
# 'desk' sends this key with each request, and the plugin (told it below) ignores requests without it: anything shown
|
|
124
|
+
# in a terminal could print the rest. Only this user can read it. Made once, like the RDP password: a key is a file of
|
|
125
|
+
# this user's own (not a link), private, holding 128 bits in hex.
|
|
126
|
+
desk_key () {
|
|
127
|
+
DESK_KEY=
|
|
128
|
+
[ -f "$D/desk-key" ] && [ ! -L "$D/desk-key" ] && [ -O "$D/desk-key" ] || return 1
|
|
129
|
+
DESK_KEY=$(cat "$D/desk-key")
|
|
130
|
+
case $DESK_KEY in *[!0-9a-f]*) DESK_KEY= ;; esac
|
|
131
|
+
[ ${"$"}{#DESK_KEY} = 32 ] || { DESK_KEY=; return 1; }
|
|
132
|
+
chmod 600 "$D/desk-key"
|
|
133
|
+
}
|
|
134
|
+
if ! desk_key; then
|
|
135
|
+
# Put in place in one step, where there is none: of two setups making one at once (two panes, or two computers),
|
|
136
|
+
# the first one's stays, and both report it.
|
|
137
|
+
T=$(mktemp "$D/desk-key.XXXXXX") && od -An -N16 -tx1 /dev/urandom | tr -d ' \n' > "$T" || fail "could not create a key for desk"
|
|
138
|
+
{ [ -e "$D/desk-key" ] || [ -L "$D/desk-key" ]; } && ! desk_key && rm -f "$D/desk-key"
|
|
139
|
+
ln "$T" "$D/desk-key" 2>/dev/null || desk_key || mv -f "$T" "$D/desk-key"
|
|
140
|
+
rm -f "$T"
|
|
141
|
+
desk_key || fail "could not create a key for desk"
|
|
142
|
+
fi
|
|
82
143
|
cat > "$D/login.sh" <<'TRD_EOF'
|
|
83
144
|
# tabby-rdp ('desk' setting): interactive SSH logins run inside a shareable session, so 'desk' in Tabby can
|
|
84
145
|
# bring this console to the remote desktop. Backend ("native" trd-pty or "tmux") is in $TRD_HOME/backend.
|
|
@@ -128,7 +189,9 @@ fi
|
|
|
128
189
|
# Which machine this is: a shared home folder puts this script on others too, and the tab may be connected to another
|
|
129
190
|
# machine than the one this runs on (ssh typed in its console).
|
|
130
191
|
MACHINE=$(cat /etc/machine-id 2>/dev/null || hostname)
|
|
131
|
-
|
|
192
|
+
# What Tabby knows a real request by: other output can't read this.
|
|
193
|
+
KEY=$(cat "$HOME/.local/share/tabby-rdp/desk-key" 2>/dev/null)
|
|
194
|
+
SEQ=$(printf '\033]7777;desk;%s;%s;%s;%s;%s;%s;%s\007' "$(b64 "$SESSION")" "$(b64 "$SOCKET")" "$(b64 "$PWD")" "$(b64 "$KIND")" "$(b64 "$MACHINE")" "$(b64 "$(hostname)")" "$(b64 "$KEY")")
|
|
132
195
|
if [ "$KIND" = tmux ]; then
|
|
133
196
|
printf '\033Ptmux;\033%s\033\\' "$SEQ" # tmux only lets it through as passthrough
|
|
134
197
|
else
|
|
@@ -143,19 +206,18 @@ chmod 755 "$D/bin/trd-pty"
|
|
|
143
206
|
printf '%s\n' "${"$"}{TRD_BACKEND:-native}" > "$D/backend"
|
|
144
207
|
for rc in "$HOME/.bashrc" "$HOME/.zshrc"; do
|
|
145
208
|
[ -f "$rc" ] || continue
|
|
146
|
-
grep -
|
|
147
|
-
# Only one line: turning desk off deletes exactly it, so on/off cycles leave the file as it was.
|
|
148
|
-
|
|
149
|
-
|
|
209
|
+
LC_ALL=C grep -a -q -xF -- "$HOOK" "$rc" && continue
|
|
210
|
+
# Only one line: turning desk off deletes exactly it, so on/off cycles leave the file as it was. A line of its own:
|
|
211
|
+
# a newline first, unless the file is empty or its last byte is one (counted, as a NUL there reads as nothing).
|
|
212
|
+
{ [ ! -s "$rc" ] || [ $(( $(tail -c1 "$rc" | wc -l) )) = 1 ] || echo; printf '%s\n' "$HOOK"; } 2>/dev/null >> "$rc" \
|
|
213
|
+
|| say "RD_NOTE desk's line couldn't be added to $rc (read-only?): logins there don't start in a shared session"
|
|
150
214
|
done
|
|
151
215
|
else
|
|
152
216
|
for rc in "$HOME/.bashrc" "$HOME/.zshrc"; do
|
|
153
|
-
|
|
154
|
-
sed -i.trd-bak '/# tabby-rdp$/d' "$rc" && rm -f "$rc.trd-bak"
|
|
155
|
-
fi
|
|
217
|
+
unhook "$rc" "$HOOK"
|
|
156
218
|
done
|
|
157
219
|
# Sessions already running keep going; new logins are plain again.
|
|
158
|
-
rm -f "$D/login.sh" "$D/tmux.conf" "$D/backend" "$D/bin/desk" "$D/bin/trd-pty"
|
|
220
|
+
rm -f "$D/login.sh" "$D/tmux.conf" "$D/backend" "$D/bin/desk" "$D/bin/trd-pty" "$D/desk-key"
|
|
159
221
|
fi
|
|
160
222
|
if [ ! -s "$D/rdp-password" ]; then
|
|
161
223
|
( umask 077; od -An -N18 -tx1 /dev/urandom | tr -d ' \n' > "$D/rdp-password" ) || fail "could not create credentials"
|
|
@@ -164,7 +226,11 @@ RD_USER=tabby
|
|
|
164
226
|
RD_PASS=$(cat "$D/rdp-password")
|
|
165
227
|
|
|
166
228
|
STATUS=$(grdctl --headless status --show-credentials 2>/dev/null)
|
|
167
|
-
|
|
229
|
+
TAB=$(printf '\t') NL='
|
|
230
|
+
'
|
|
231
|
+
# Whether grd's status has this line. Compared in the shell: as a command's argument, the password would show in the
|
|
232
|
+
# process list on every connect.
|
|
233
|
+
has () { case "$NL$STATUS$NL" in *"$NL$TAB$1$NL"*) return 0 ;; esac; return 1; }
|
|
168
234
|
g () { grdctl --headless rdp "$@" >/dev/null 2>&1; }
|
|
169
235
|
RESTART=0
|
|
170
236
|
# grd reads the certificate at startup: a new one at the same path needs a restart too, or grd would keep serving
|
|
@@ -176,7 +242,18 @@ fi
|
|
|
176
242
|
if ! has "View-only: no"; then g disable-view-only || fail "grdctl could not disable view-only"; RESTART=1; fi
|
|
177
243
|
if ! has "Status: enabled"; then g enable || fail "grdctl could not enable RDP"; RESTART=1; fi
|
|
178
244
|
if ! has "Username: $RD_USER" || ! has "Password: $RD_PASS"; then
|
|
179
|
-
|
|
245
|
+
# Left off the command line, the password stays out of the process list: grdctl versions that ask for it then read
|
|
246
|
+
# it from their input. Where that didn't take, it goes as an argument. Without timeout(1) on the host, grdctl runs
|
|
247
|
+
# as it is: its input is a pipe that ends after the password, so it doesn't wait for more.
|
|
248
|
+
if command -v timeout >/dev/null 2>&1; then
|
|
249
|
+
printf '%s\n' "$RD_PASS" | timeout 10 grdctl --headless rdp set-credentials "$RD_USER" >/dev/null 2>&1
|
|
250
|
+
else
|
|
251
|
+
printf '%s\n' "$RD_PASS" | grdctl --headless rdp set-credentials "$RD_USER" >/dev/null 2>&1
|
|
252
|
+
fi
|
|
253
|
+
STATUS=$(grdctl --headless status --show-credentials 2>/dev/null)
|
|
254
|
+
if ! has "Username: $RD_USER" || ! has "Password: $RD_PASS"; then
|
|
255
|
+
g set-credentials "$RD_USER" "$RD_PASS" || fail "grdctl could not set credentials"
|
|
256
|
+
fi
|
|
180
257
|
RESTART=1
|
|
181
258
|
fi
|
|
182
259
|
|
|
@@ -309,6 +386,7 @@ if [ -s "$D/taken-over" ]; then
|
|
|
309
386
|
fi
|
|
310
387
|
# xrdp besides GNOME (on a port of its own; on grd's, one of them couldn't listen): offered as another desktop.
|
|
311
388
|
[ -z "$XRDP_PORT" ] || [ "$XRDP_PORT" = "$PORT" ] || say "RD_XRDP port=$XRDP_PORT user=$(id -un)"
|
|
389
|
+
[ -z "$DESK_KEY" ] || say "RD_DESK $DESK_KEY"
|
|
312
390
|
say "RD_OK port=$PORT user=$RD_USER pass=$RD_PASS cert=$CERT"
|
|
313
391
|
`;
|
|
314
392
|
let trdPtySource = null;
|
|
@@ -343,9 +421,10 @@ async function prepareRemoteDesktop(target, desk, backend = 'native', client = '
|
|
|
343
421
|
// it worth asking, so a Linux host costs nothing extra.
|
|
344
422
|
if (/^RD_WINDOWS$/m.test(out) || !/^RD_(OK|XRDP|ERR) /m.test(out) && await isWindows(target)) {
|
|
345
423
|
// Its RDP server makes its own certificate: trusted on first use, like a desktop behind the host.
|
|
346
|
-
return { kind: 'windows', port: 3389, username: target.key
|
|
424
|
+
return { kind: 'windows', port: 3389, username: (0, desktops_1.keyUser)(target.key), password: '', certificate: '' };
|
|
347
425
|
}
|
|
348
|
-
|
|
426
|
+
// The host's reason, as much of it as a status line shows: the host decides how long the line is.
|
|
427
|
+
const err = /^RD_ERR (.{0,1000})/m.exec(out);
|
|
349
428
|
if (err) {
|
|
350
429
|
throw new Error(err[1]);
|
|
351
430
|
}
|
|
@@ -354,13 +433,15 @@ async function prepareRemoteDesktop(target, desk, backend = 'native', client = '
|
|
|
354
433
|
if (ok) {
|
|
355
434
|
const certificate = (0, rdcleanpath_1.normalizeFingerprint)(ok[4]);
|
|
356
435
|
if (!certificate) {
|
|
357
|
-
|
|
436
|
+
// What the host put there, as long as it likes: as much of it as a fingerprint takes, for a status line.
|
|
437
|
+
throw new Error(`Remote setup reported no usable certificate fingerprint (${ok[4].slice(0, 100)})`);
|
|
358
438
|
}
|
|
359
439
|
const clients = Number(/^RD_CLIENTS (\d+)$/m.exec(out)?.[1] ?? 0);
|
|
360
440
|
const taken = /^RD_TAKEN_OVER (\S+) (\d+)$/m.exec(out);
|
|
361
441
|
return {
|
|
362
442
|
kind: 'gnome', port: Number(ok[1]), username: ok[2], password: ok[3], certificate, xrdpPort: xrdp ? Number(xrdp[1]) : undefined, xrdpUser: xrdp?.[2],
|
|
363
|
-
clients, takenOver: taken && taken[1] !== client ? Number(taken[2]) : undefined,
|
|
443
|
+
clients, takenOver: taken && taken[1] !== client ? Number(taken[2]) : undefined, deskKey: /^RD_DESK ([0-9a-f]{32})$/m.exec(out)?.[1],
|
|
444
|
+
notes: [...out.matchAll(/^RD_NOTE (.+)$/gm)].map(m => m[1]),
|
|
364
445
|
};
|
|
365
446
|
}
|
|
366
447
|
if (xrdp) {
|
|
@@ -369,4 +450,3 @@ async function prepareRemoteDesktop(target, desk, backend = 'native', client = '
|
|
|
369
450
|
}
|
|
370
451
|
throw new Error('Remote setup gave no result');
|
|
371
452
|
}
|
|
372
|
-
//# sourceMappingURL=remoteSetup.js.map
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SessionLog = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* A line as a string of its own. V8 keeps a part cut out of a string (a slice, a pattern's match; 13 characters or
|
|
6
|
+
* more) as a view of the string it came from, and joining strings as a list of them: a short line made from a host's
|
|
7
|
+
* answer of megabytes would keep all of it in memory for as long as the line is kept. Joined to a character and cut
|
|
8
|
+
* from it again: V8 copies a joined string's characters into one string before it cuts a part out of it, so the line
|
|
9
|
+
* is a part of that copy, every character as it was, unpaired surrogates included. A copy through UTF-16 and back
|
|
10
|
+
* does the same at several times the cost, which every line logged pays.
|
|
11
|
+
*/
|
|
12
|
+
function copied(line) {
|
|
13
|
+
return (' ' + line).slice(1);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* A desktop's connection log (DesktopSession.log): what the settings page's Copy log hands out. Much of it is of the
|
|
17
|
+
* remote's making (an RD Gateway's administrator's messages, a server's), which decides how many lines there are and
|
|
18
|
+
* when, for as long as the desktop stays open: kept whole, they could fill Tabby's memory and crash its window. So the
|
|
19
|
+
* log keeps the first lines (how the connection was set up) and the latest ones, with a line between them saying how
|
|
20
|
+
* many were left out, and each line is cut at MAX_LINE characters and kept as a copy of its own (see copied), which
|
|
21
|
+
* the text it was made from doesn't stay in memory with.
|
|
22
|
+
*/
|
|
23
|
+
class SessionLog extends Array {
|
|
24
|
+
constructor() {
|
|
25
|
+
super(...arguments);
|
|
26
|
+
/** How many lines were left out so far. */
|
|
27
|
+
this.dropped = 0;
|
|
28
|
+
}
|
|
29
|
+
// What is made from the log (filter, slice, map) is a plain array.
|
|
30
|
+
static get [Symbol.species]() {
|
|
31
|
+
return Array;
|
|
32
|
+
}
|
|
33
|
+
push(...lines) {
|
|
34
|
+
const { FIRST, LATEST, MAX_LINE } = SessionLog;
|
|
35
|
+
super.push(...lines.map(line => typeof line === 'string' ? copied(line.length > MAX_LINE ? `${line.slice(0, MAX_LINE)}…` : line) : line));
|
|
36
|
+
// Once the latest lines are twice as many as are kept, the older half of them goes in one step: each line then
|
|
37
|
+
// costs the same however many come.
|
|
38
|
+
const said = this.dropped ? 1 : 0;
|
|
39
|
+
const over = this.length - FIRST - said - LATEST;
|
|
40
|
+
if (over >= LATEST) {
|
|
41
|
+
this.dropped += over;
|
|
42
|
+
this.splice(FIRST, said + over, `(${this.dropped} lines left out here)`);
|
|
43
|
+
}
|
|
44
|
+
return this.length;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
exports.SessionLog = SessionLog;
|
|
48
|
+
/** The first lines, kept whatever follows. */
|
|
49
|
+
SessionLog.FIRST = 200;
|
|
50
|
+
/** At least this many of the latest lines are kept (and at most twice as many, see push). */
|
|
51
|
+
SessionLog.LATEST = 2000;
|
|
52
|
+
/** The longest a line is kept. */
|
|
53
|
+
SessionLog.MAX_LINE = 4096;
|