fix: better ssh resilience and customizable timeout for future backoff

This commit is contained in:
Aarnav Tale
2025-06-16 11:23:44 -04:00
parent 6a0e097412
commit 7f376c3f70
4 changed files with 48 additions and 31 deletions
+1 -1
View File
@@ -156,7 +156,7 @@ export default function Page() {
<div className="w-screen h-screen bg-headplane-900"> <div className="w-screen h-screen bg-headplane-900">
{ipn === null ? ( {ipn === null ? (
<div className="mx-auto h-screen flex items-center justify-center"> <div className="mx-auto h-screen flex items-center justify-center">
<Loader2 className="animate-spin size-10" /> <Loader2 className="animate-spin size-10 text-headplane-50" />
</div> </div>
) : ( ) : (
<div className="flex flex-col h-screen"> <div className="flex flex-col h-screen">
+30 -6
View File
@@ -5,9 +5,11 @@ import { WebLinksAddon } from '@xterm/addon-web-links';
import { WebglAddon } from '@xterm/addon-webgl'; import { WebglAddon } from '@xterm/addon-webgl';
import * as xterm from '@xterm/xterm'; import * as xterm from '@xterm/xterm';
import '@xterm/xterm/css/xterm.css'; import '@xterm/xterm/css/xterm.css';
import { Loader2 } from 'lucide-react';
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import cn from '~/utils/cn'; import cn from '~/utils/cn';
import { useLiveData } from '~/utils/live-data'; import { useLiveData } from '~/utils/live-data';
import toast from '~/utils/toast';
interface XTermProps { interface XTermProps {
ipn: TsWasmNet; ipn: TsWasmNet;
@@ -15,17 +17,17 @@ interface XTermProps {
hostname: string; hostname: string;
} }
const RED = new TextEncoder().encode('\x1b[31m');
const RESET = new TextEncoder().encode('\x1b[0m');
export default function XTerm({ ipn, username, hostname }: XTermProps) { export default function XTerm({ ipn, username, hostname }: XTermProps) {
const { pause } = useLiveData(); const { pause } = useLiveData();
const container = useRef<HTMLDivElement>(null); const container = useRef<HTMLDivElement>(null);
const term = useRef<xterm.Terminal>(null); const term = useRef<xterm.Terminal>(null);
const [isResizing, setIsResizing] = useState(false);
const inputRef = useRef<((input: string) => void) | null>(null); const inputRef = useRef<((input: string) => void) | null>(null);
const [isResizing, setIsResizing] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [isFailed, setIsFailed] = useState(false);
useEffect(() => { useEffect(() => {
pause(); pause();
const terminal = new xterm.Terminal({ const terminal = new xterm.Terminal({
@@ -76,12 +78,14 @@ export default function XTerm({ ipn, username, hostname }: XTermProps) {
OnStderr: (data) => { OnStderr: (data) => {
terminal.write(data); terminal.write(data);
console.log('SSH stderr:', data); console.log('SSH stderr:', data);
toast(data);
}, },
OnStdin: (func) => { OnStdin: (func) => {
inputRef.current = func; inputRef.current = func;
}, },
OnConnect: () => { OnConnect: () => {
console.log('SSH session connected'); console.log('SSH session connected');
setIsLoading(false);
}, },
OnDisconnect: () => { OnDisconnect: () => {
@@ -92,6 +96,9 @@ export default function XTerm({ ipn, username, hostname }: XTermProps) {
} }
console.log('SSH session disconnected'); console.log('SSH session disconnected');
terminal.writeln('Disconnected from SSH session');
setIsLoading(false);
setIsFailed(true);
term.current = null; term.current = null;
}, },
}); });
@@ -123,8 +130,15 @@ export default function XTerm({ ipn, username, hostname }: XTermProps) {
return ( return (
<div className="relative w-full h-full group"> <div className="relative w-full h-full group">
<div ref={container} className="w-full h-full" /> {isLoading ? (
<div className="mx-auto h-screen flex items-center justify-center">
<Loader2 className="animate-spin size-10 text-headplane-50" />
</div>
) : undefined}
<div
ref={container}
className={cn('w-full h-full', isLoading ? 'opacity-0' : 'opacity-100')}
/>
{term.current && isResizing ? ( {term.current && isResizing ? (
<div <div
className={cn( className={cn(
@@ -135,6 +149,16 @@ export default function XTerm({ ipn, username, hostname }: XTermProps) {
{term.current.cols}x{term.current.rows} {term.current.cols}x{term.current.rows}
</div> </div>
) : undefined} ) : undefined}
{isFailed ? (
<div
className={cn(
'absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2',
'px-4 py-2 bg-headplane-800 text-white rounded-full shadow z-50',
)}
>
Failed to connect to SSH session
</div>
) : undefined}
</div> </div>
); );
} }
+16 -22
View File
@@ -43,26 +43,14 @@ func ParseTsWasmNetOptions(obj js.Value) (*TsWasmNetOptions, error) {
// Options passed from the JS side to pass data to xterm.js. // Options passed from the JS side to pass data to xterm.js.
type SSHXtermConfig struct { type SSHXtermConfig struct {
// Number of rows in the PTY. Timeout int // Timeout in seconds for the PTY connection.
Rows int Rows int // Number of rows in the PTY.
Cols int // Number of columns in the PTY.
// Number of columns in the PTY. OnStdout func(data string) // Fires when the PTY has output.
Cols int OnStderr func(error string) // Fires when the PTY has an error.
OnStdin js.Value // Passes a function to the JS side to provide input.
// Fires when the PTY has output. OnConnect func() // Fires when the PTY is opened.
OnStdout func(data string) OnDisconnect func() // Fires when the PTY is closed.
// Fires when the PTY has an error.
OnStderr func(error string)
// Passes a function to the JS side to provide input.
OnStdin js.Value
// Fires when the PTY is opened.
OnConnect func()
// Fires when the PTY is closed.
OnDisconnect func()
} }
// Parses the provided JS object to validate and extract SSHXtermConfig. // Parses the provided JS object to validate and extract SSHXtermConfig.
@@ -71,6 +59,7 @@ func ParseSSHXtermConfig(obj js.Value) (*SSHXtermConfig, error) {
return nil, errors.New("SSHXtermConfig cannot be undefined or null") return nil, errors.New("SSHXtermConfig cannot be undefined or null")
} }
timeout := safeInt("Timeout", obj)
rows := safeInt("Rows", obj) rows := safeInt("Rows", obj)
cols := safeInt("Cols", obj) cols := safeInt("Cols", obj)
@@ -78,9 +67,14 @@ func ParseSSHXtermConfig(obj js.Value) (*SSHXtermConfig, error) {
return nil, errors.New("Rows and Cols must be positive integers") return nil, errors.New("Rows and Cols must be positive integers")
} }
if timeout <= 0 {
timeout = 30 // Default timeout to 30 seconds if not specified
}
config := &SSHXtermConfig{ config := &SSHXtermConfig{
Rows: rows, Timeout: timeout,
Cols: cols, Rows: rows,
Cols: cols,
} }
onStdout := obj.Get("OnStdout") onStdout := obj.Get("OnStdout")
+1 -2
View File
@@ -50,8 +50,7 @@ func (i *TsWasmIpn) NewSSHSession(hostname, username string, termConfig *SSHXter
func (s *SSHSession) ConnectAndRun() { func (s *SSHSession) ConnectAndRun() {
defer s.TermConfig.OnDisconnect() defer s.TermConfig.OnDisconnect()
// Default to a 5 second timeout for the connection AFTER dial. ctx, cancel := context.WithTimeout(context.Background(), time.Duration(s.TermConfig.Timeout)*time.Second)
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel() defer cancel()
// TODO: Log here // TODO: Log here