tabby-rdp 0.5.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/CHANGELOG.md +516 -0
  2. package/README.md +216 -58
  3. package/TROUBLESHOOTING.md +77 -0
  4. package/dist/accountForm.js +0 -1
  5. package/dist/accountRevisions.js +46 -0
  6. package/dist/accounts.js +2 -2
  7. package/dist/audio.js +0 -1
  8. package/dist/authorities.js +331 -0
  9. package/dist/byteQueue.js +102 -0
  10. package/dist/clipboard.js +106 -0
  11. package/dist/connectionStatus.js +0 -1
  12. package/dist/desk.js +16 -47
  13. package/dist/deskScript.js +196 -6
  14. package/dist/desktop.service.js +1499 -269
  15. package/dist/desktopForm.js +53 -18
  16. package/dist/desktops.js +232 -24
  17. package/dist/diskStorage.js +212 -0
  18. package/dist/drives.js +271 -30
  19. package/dist/fileTransfer.js +960 -132
  20. package/dist/gateway.js +216 -57
  21. package/dist/header.js +76 -5
  22. package/dist/help.js +16 -47
  23. package/dist/hostCompat.js +86 -0
  24. package/dist/hyperv.js +65 -15
  25. package/dist/index.js +7 -4
  26. package/dist/keyboard.js +0 -1
  27. package/dist/microphone.js +57 -13
  28. package/dist/ntlm.js +0 -1
  29. package/dist/osd.js +56 -15
  30. package/dist/packageChangeDialog.js +186 -0
  31. package/dist/probe.js +15 -3
  32. package/dist/rdcleanpath.js +319 -44
  33. package/dist/rdpFile.js +22 -7
  34. package/dist/rdpProfile.js +39 -21
  35. package/dist/releaseSettings.js +72 -0
  36. package/dist/releases.js +408 -0
  37. package/dist/remoteSetup.js +103 -23
  38. package/dist/sessionLog.js +53 -0
  39. package/dist/settingsPage.js +148 -122
  40. package/dist/signin.js +478 -48
  41. package/dist/ssh.js +99 -18
  42. package/dist/sshChannelStream.js +40 -6
  43. package/dist/targets.js +194 -40
  44. package/dist/ui.js +88 -20
  45. package/dist/unshowable.js +41 -0
  46. package/dist/updates.js +79 -56
  47. package/dist/vms.js +17 -3
  48. package/dist/wake.js +15 -8
  49. package/docs/desk.md +157 -0
  50. package/package.json +14 -4
  51. package/remote/trd-pty.py +74 -13
  52. package/vendor/SHA256SUMS +26 -0
  53. package/vendor/ironrdp_web_bg.wasm +0 -0
  54. package/dist/accountForm.js.map +0 -1
  55. package/dist/accounts.js.map +0 -1
  56. package/dist/audio.js.map +0 -1
  57. package/dist/connectionStatus.js.map +0 -1
  58. package/dist/desk.js.map +0 -1
  59. package/dist/deskScript.js.map +0 -1
  60. package/dist/desktop.service.js.map +0 -1
  61. package/dist/desktopForm.js.map +0 -1
  62. package/dist/desktops.js.map +0 -1
  63. package/dist/drives.js.map +0 -1
  64. package/dist/fileTransfer.js.map +0 -1
  65. package/dist/gateway.js.map +0 -1
  66. package/dist/header.js.map +0 -1
  67. package/dist/help.js.map +0 -1
  68. package/dist/hyperv.js.map +0 -1
  69. package/dist/index.js.map +0 -1
  70. package/dist/keyboard.js.map +0 -1
  71. package/dist/microphone.js.map +0 -1
  72. package/dist/ntlm.js.map +0 -1
  73. package/dist/osd.js.map +0 -1
  74. package/dist/probe.js.map +0 -1
  75. package/dist/rdcleanpath.js.map +0 -1
  76. package/dist/rdpFile.js.map +0 -1
  77. package/dist/rdpProfile.js.map +0 -1
  78. package/dist/remoteSetup.js.map +0 -1
  79. package/dist/settingsPage.js.map +0 -1
  80. package/dist/signin.js.map +0 -1
  81. package/dist/ssh.js.map +0 -1
  82. package/dist/sshChannelStream.js.map +0 -1
  83. package/dist/targets.js.map +0 -1
  84. package/dist/ui.js.map +0 -1
  85. package/dist/updates.js.map +0 -1
  86. package/dist/vms.js.map +0 -1
  87. package/dist/wake.js.map +0 -1
@@ -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, and
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
- // (grdctl takes the credentials as arguments, so they are briefly visible in the remote's process list.)
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
- for rc in "$HOME/.bashrc" "$HOME/.zshrc"; do
63
- if [ -f "$rc" ] && grep -qF '# tabby-remote-desktop' "$rc"; then
64
- sed -i.trd-bak '/# tabby-remote-desktop$/d' "$rc" && rm -f "$rc.trd-bak"
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
- mkdir -p "$D" && chmod 700 "$D" || fail "could not create $D"
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
- SEQ=$(printf '\033]7777;desk;%s;%s;%s;%s;%s;%s\007' "$(b64 "$SESSION")" "$(b64 "$SOCKET")" "$(b64 "$PWD")" "$(b64 "$KIND")" "$(b64 "$MACHINE")" "$(b64 "$(hostname)")")
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 -qF '# tabby-rdp' "$rc" && continue
147
- # Only one line: turning desk off deletes exactly it, so on/off cycles leave the file as it was.
148
- [ -z "$(tail -c1 "$rc")" ] || echo >> "$rc"
149
- printf '[ -f "$HOME/.local/share/tabby-rdp/login.sh" ] && . "$HOME/.local/share/tabby-rdp/login.sh" # tabby-rdp\n' >> "$rc"
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
- if [ -f "$rc" ] && grep -qF '# tabby-rdp' "$rc"; then
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
- has () { printf '%s\n' "$STATUS" | grep -qxF "$(printf '\t%s' "$1")"; }
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
- g set-credentials "$RD_USER" "$RD_PASS" || fail "grdctl could not set credentials"
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.replace(/@[^@]*$/, ''), password: '', certificate: '' };
424
+ return { kind: 'windows', port: 3389, username: (0, desktops_1.keyUser)(target.key), password: '', certificate: '' };
347
425
  }
348
- const err = /^RD_ERR (.*)$/m.exec(out);
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
- throw new Error(`Remote setup reported no usable certificate fingerprint (${ok[4]})`);
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;