diff --git a/app/routes/ssh/console.tsx b/app/routes/ssh/console.tsx index 9588f9d..a5ad6e4 100644 --- a/app/routes/ssh/console.tsx +++ b/app/routes/ssh/console.tsx @@ -23,6 +23,13 @@ export async function loader({ request, context, }: LoaderFunctionArgs) { + if (!context.agents?.agentID()) { + throw data( + 'WebSSH is only available with the Headplane agent integration', + 400, + ); + } + const session = await context.sessions.auth(request); const user = session.get('user'); if (!user) { @@ -35,6 +42,7 @@ export async function loader({ 'v1/user', session.get('api_key')!, ); + // MARK: This assumes that a user has authenticated with Headscale first // Since the only way to enforce permissions via ACLs is to generate a // pre-authkey which REQUIRES a user ID, meaning the user has to have diff --git a/app/routes/ssh/hp_ssh.d.ts b/app/routes/ssh/hp_ssh.d.ts index 46b7df9..72bcb33 100644 --- a/app/routes/ssh/hp_ssh.d.ts +++ b/app/routes/ssh/hp_ssh.d.ts @@ -46,18 +46,6 @@ interface XtermConfig { OnDisconnect: () => void; } -// interface SSHTerminalConfig { -// writeFn: (data: string) => void; -// writeErrorFn: (error: string) => void; -// setReadFn: (cb: (input: string) => void) => void; -// rows: number; -// cols: number; -// timeoutSeconds?: number; -// onConnectionProgress: (message: string) => void; -// onConnected: () => void; -// onDone: () => void; -// } - interface SSHSession { Close(): boolean; Resize(rows: number, cols: number): boolean; diff --git a/app/routes/ssh/xterm.client.tsx b/app/routes/ssh/xterm.client.tsx index 566b9d8..3c152e9 100644 --- a/app/routes/ssh/xterm.client.tsx +++ b/app/routes/ssh/xterm.client.tsx @@ -30,6 +30,17 @@ export default function XTerm({ ipn, username, hostname }: XTermProps) { useEffect(() => { pause(); + if (!container.current) { + console.error('Container ref is not set'); + return; + } + + // Don't create a new terminal if one already exists + if (term.current) { + console.warn('Terminal already exists, skipping initialization'); + return; + } + const terminal = new xterm.Terminal({ allowProposedApi: true, cursorBlink: true, @@ -52,24 +63,23 @@ export default function XTerm({ ipn, username, hostname }: XTermProps) { ); terminal.unicode.activeVersion = '11'; - const gl = new WebglAddon(); - terminal.loadAddon(gl); - const fit = new FitAddon(); terminal.loadAddon(fit); + const gl = new WebglAddon(); + terminal.loadAddon(gl); + gl.onContextLoss(() => { console.warn('WebGL context lost, falling back to canvas rendering'); gl.dispose(); }); + fit.fit(); + term.current = terminal; terminal.open(container.current!); terminal.focus(); - term.current = terminal; - let ro: ResizeObserver | null = null; let onUnload: ((e: Event) => void) | null = null; - const session = ipn.OpenSSH(hostname, username, { Rows: terminal.rows, Cols: terminal.cols, @@ -103,8 +113,7 @@ export default function XTerm({ ipn, username, hostname }: XTermProps) { }, }); - const parent = container.current?.ownerDocument.defaultView ?? window; - ro = new parent.ResizeObserver(() => { + const ro = new ResizeObserver(() => { if (term.current) { setIsResizing(true); fit.fit(); @@ -112,12 +121,10 @@ export default function XTerm({ ipn, username, hostname }: XTermProps) { } }); - if (container.current) { - ro.observe(container.current); - } - + ro.observe(container.current); terminal.onResize(({ cols, rows }) => { - session.Resize(rows, cols); + console.log(`Terminal resized to ${cols}x${rows}`); + session.Resize(cols, rows); }); terminal.onData((data) => { @@ -126,12 +133,25 @@ export default function XTerm({ ipn, username, hostname }: XTermProps) { onUnload = (_) => session.Close(); parent.addEventListener('unload', onUnload); + + return () => { + if (onUnload) { + parent.removeEventListener('unload', onUnload); + } + + session.Close(); + ro?.disconnect(); + terminal.dispose(); + term.current = null; + inputRef.current = null; + console.log('SSH session closed and terminal disposed'); + }; }, []); return ( -
+ <> {isLoading ? ( -
+
) : undefined} @@ -159,6 +179,6 @@ export default function XTerm({ ipn, username, hostname }: XTermProps) { Failed to connect to SSH session
) : undefined} -
+ ); } diff --git a/internal/hp_ipn/ssh.go b/internal/hp_ipn/ssh.go index a03e3cb..1728799 100644 --- a/internal/hp_ipn/ssh.go +++ b/internal/hp_ipn/ssh.go @@ -150,7 +150,7 @@ func (s *SSHSession) Resize(rows, cols int) error { return nil } - return s.Pty.WindowChange(rows, cols) + return s.Pty.WindowChange(cols, rows) } // Closes the SSH session.