From 01e6565e8a0193ec940a8eca9129dadb2c05e29d Mon Sep 17 00:00:00 2001 From: Mihai Parparita Date: Wed, 7 Sep 2022 18:23:37 -0700 Subject: [PATCH] cmd/tsconnect: temporarily switch to xterm.js fork that handles popup windows Allows other work to be unblocked while xtermjs/xterm.js#4069 is worked through. To enable testing the popup window handling, the standalone app allows opening of SSH sessions in new windows by holding down the alt key while pressing the SSH button. Signed-off-by: Mihai Parparita --- cmd/tsconnect/package.json | 2 +- cmd/tsconnect/src/app/ssh.tsx | 84 +++++++++++++++++++++++++++-------- cmd/tsconnect/src/lib/ssh.ts | 5 ++- cmd/tsconnect/yarn.lock | 7 ++- 4 files changed, 74 insertions(+), 24 deletions(-) diff --git a/cmd/tsconnect/package.json b/cmd/tsconnect/package.json index 28ebd6762..fabf9910a 100644 --- a/cmd/tsconnect/package.json +++ b/cmd/tsconnect/package.json @@ -10,7 +10,7 @@ "qrcode": "^1.5.0", "tailwindcss": "^3.1.6", "typescript": "^4.7.4", - "xterm": "^4.18.0", + "xterm": "mihaip/xterm.js#95100e3c870b59348bb4fa5504bab9f9317bac67", "xterm-addon-fit": "^0.5.0" }, "scripts": { diff --git a/cmd/tsconnect/src/app/ssh.tsx b/cmd/tsconnect/src/app/ssh.tsx index 36c85dddc..bd8ee4690 100644 --- a/cmd/tsconnect/src/app/ssh.tsx +++ b/cmd/tsconnect/src/app/ssh.tsx @@ -2,16 +2,24 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -import { useState, useCallback } from "preact/hooks" +import { useState, useCallback, useMemo, useEffect, useRef } from "preact/hooks" +import { createPortal } from "preact/compat" +import type { VNode } from "preact" import { runSSHSession, SSHSessionDef } from "../lib/ssh" export function SSH({ netMap, ipn }: { netMap: IPNNetMap; ipn: IPN }) { - const [sshSessionDef, setSSHSessionDef] = useState(null) + const [sshSessionDef, setSSHSessionDef] = useState( + null + ) const clearSSHSessionDef = useCallback(() => setSSHSessionDef(null), []) if (sshSessionDef) { - return ( + const sshSession = ( ) + if (sshSessionDef.newWindow) { + return {sshSession} + } + return sshSession } const sshPeers = netMap.peers.filter( (p) => p.tailscaleSSHEnabled && p.online !== false @@ -24,6 +32,8 @@ export function SSH({ netMap, ipn }: { netMap: IPNNetMap; ipn: IPN }) { return } +type SSHFormSessionDef = SSHSessionDef & { newWindow?: boolean } + function SSHSession({ def, ipn, @@ -33,20 +43,14 @@ function SSHSession({ ipn: IPN onDone: () => void }) { - return ( -
{ - if (node) { - // Run the SSH session aysnchronously, so that the React render - // loop is complete (otherwise the SSH form may still be visible, - // which affects the size of the terminal, leading to a spurious - // initial resize). - setTimeout(() => runSSHSession(node, def, ipn, onDone), 0) - } - }} - /> - ) + const ref = useRef(null) + useEffect(() => { + if (ref.current) { + runSSHSession(ref.current, def, ipn, onDone) + } + }, [ref]) + + return
} function NoSSHPeers() { @@ -66,7 +70,7 @@ function SSHForm({ onSubmit, }: { sshPeers: IPNNetMapPeerNode[] - onSubmit: (def: SSHSessionDef) => void + onSubmit: (def: SSHFormSessionDef) => void }) { sshPeers = sshPeers.slice().sort((a, b) => a.name.localeCompare(b.name)) const [username, setUsername] = useState("") @@ -99,7 +103,51 @@ function SSHForm({ type="submit" class="button bg-green-500 border-green-500 text-white hover:bg-green-600 hover:border-green-600" value="SSH" + onClick={(e) => { + if (e.altKey) { + e.preventDefault() + e.stopPropagation() + onSubmit({ username, hostname, newWindow: true }) + } + }} /> ) } + +const NewWindow = ({ + children, + close, +}: { + children: VNode + close: () => void +}) => { + const newWindow = useMemo(() => { + const newWindow = window.open(undefined, undefined, "width=600,height=400") + if (newWindow) { + const containerNode = newWindow.document.createElement("div") + containerNode.className = "h-screen flex flex-col overflow-hidden" + newWindow.document.body.appendChild(containerNode) + + for (const linkNode of document.querySelectorAll( + "head link[rel=stylesheet]" + )) { + const newLink = document.createElement("link") + newLink.rel = "stylesheet" + newLink.href = (linkNode as HTMLLinkElement).href + newWindow.document.head.appendChild(newLink) + } + } + return newWindow + }, []) + if (!newWindow) { + console.error("Could not open window") + return null + } + newWindow.onbeforeunload = () => { + close() + } + + useEffect(() => () => newWindow.close(), []) + return createPortal(children, newWindow.document.body.lastChild as Element) +} diff --git a/cmd/tsconnect/src/lib/ssh.ts b/cmd/tsconnect/src/lib/ssh.ts index 0735c860a..bbc26eacb 100644 --- a/cmd/tsconnect/src/lib/ssh.ts +++ b/cmd/tsconnect/src/lib/ssh.ts @@ -54,7 +54,10 @@ export function runSSHSession( }) // Make terminal and SSH session track the size of the containing DOM node. - resizeObserver = new ResizeObserver(() => fitAddon.fit()) + resizeObserver = + new termContainerNode.ownerDocument.defaultView!.ResizeObserver(() => + fitAddon.fit() + ) resizeObserver.observe(termContainerNode) term.onResize(({ rows, cols }) => sshSession.resize(rows, cols)) diff --git a/cmd/tsconnect/yarn.lock b/cmd/tsconnect/yarn.lock index cb311ada9..b8b1e9fd7 100644 --- a/cmd/tsconnect/yarn.lock +++ b/cmd/tsconnect/yarn.lock @@ -644,10 +644,9 @@ xterm-addon-fit@^0.5.0: resolved "https://registry.yarnpkg.com/xterm-addon-fit/-/xterm-addon-fit-0.5.0.tgz#2d51b983b786a97dcd6cde805e700c7f913bc596" integrity sha512-DsS9fqhXHacEmsPxBJZvfj2la30Iz9xk+UKjhQgnYNkrUIN5CYLbw7WEfz117c7+S86S/tpHPfvNxJsF5/G8wQ== -xterm@^4.18.0: - version "4.18.0" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.18.0.tgz#a1f6ab2c330c3918fb094ae5f4c2562987398ea1" - integrity sha512-JQoc1S0dti6SQfI0bK1AZvGnAxH4MVw45ZPFSO6FHTInAiau3Ix77fSxNx3mX4eh9OL4AYa8+4C8f5UvnSfppQ== +xterm@mihaip/xterm.js#95100e3c870b59348bb4fa5504bab9f9317bac67: + version "4.19.0" + resolved "https://codeload.github.com/mihaip/xterm.js/tar.gz/95100e3c870b59348bb4fa5504bab9f9317bac67" y18n@^4.0.0: version "4.0.3"