diff --git a/app/routes/ssh/console.tsx b/app/routes/ssh/console.tsx index 010bdbc..0741224 100644 --- a/app/routes/ssh/console.tsx +++ b/app/routes/ssh/console.tsx @@ -156,7 +156,7 @@ export default function Page() {
{ipn === null ? (
- +
) : (
diff --git a/app/routes/ssh/xterm.client.tsx b/app/routes/ssh/xterm.client.tsx index d726fed..566b9d8 100644 --- a/app/routes/ssh/xterm.client.tsx +++ b/app/routes/ssh/xterm.client.tsx @@ -5,9 +5,11 @@ import { WebLinksAddon } from '@xterm/addon-web-links'; import { WebglAddon } from '@xterm/addon-webgl'; import * as xterm from '@xterm/xterm'; import '@xterm/xterm/css/xterm.css'; +import { Loader2 } from 'lucide-react'; import { useEffect, useRef, useState } from 'react'; import cn from '~/utils/cn'; import { useLiveData } from '~/utils/live-data'; +import toast from '~/utils/toast'; interface XTermProps { ipn: TsWasmNet; @@ -15,17 +17,17 @@ interface XTermProps { hostname: string; } -const RED = new TextEncoder().encode('\x1b[31m'); -const RESET = new TextEncoder().encode('\x1b[0m'); - export default function XTerm({ ipn, username, hostname }: XTermProps) { const { pause } = useLiveData(); const container = useRef(null); const term = useRef(null); - const [isResizing, setIsResizing] = useState(false); const inputRef = useRef<((input: string) => void) | null>(null); + const [isResizing, setIsResizing] = useState(false); + const [isLoading, setIsLoading] = useState(true); + const [isFailed, setIsFailed] = useState(false); + useEffect(() => { pause(); const terminal = new xterm.Terminal({ @@ -76,12 +78,14 @@ export default function XTerm({ ipn, username, hostname }: XTermProps) { OnStderr: (data) => { terminal.write(data); console.log('SSH stderr:', data); + toast(data); }, OnStdin: (func) => { inputRef.current = func; }, OnConnect: () => { console.log('SSH session connected'); + setIsLoading(false); }, OnDisconnect: () => { @@ -92,6 +96,9 @@ export default function XTerm({ ipn, username, hostname }: XTermProps) { } console.log('SSH session disconnected'); + terminal.writeln('Disconnected from SSH session'); + setIsLoading(false); + setIsFailed(true); term.current = null; }, }); @@ -123,8 +130,15 @@ export default function XTerm({ ipn, username, hostname }: XTermProps) { return (
-
- + {isLoading ? ( +
+ +
+ ) : undefined} +
{term.current && isResizing ? (
) : undefined} + {isFailed ? ( +
+ Failed to connect to SSH session +
+ ) : undefined}
); } diff --git a/internal/hp_ipn/jsmap.go b/internal/hp_ipn/jsmap.go index bc47a7c..d0c8940 100644 --- a/internal/hp_ipn/jsmap.go +++ b/internal/hp_ipn/jsmap.go @@ -43,26 +43,14 @@ func ParseTsWasmNetOptions(obj js.Value) (*TsWasmNetOptions, error) { // Options passed from the JS side to pass data to xterm.js. type SSHXtermConfig struct { - // Number of rows in the PTY. - Rows int - - // Number of columns in the PTY. - Cols int - - // Fires when the PTY has output. - OnStdout func(data string) - - // 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() + Timeout int // Timeout in seconds for the PTY connection. + Rows int // Number of rows in the PTY. + Cols int // Number of columns in the PTY. + OnStdout func(data string) // Fires when the PTY has output. + OnStderr func(error string) // Fires when the PTY has an error. + OnStdin js.Value // Passes a function to the JS side to provide input. + OnConnect func() // Fires when the PTY is opened. + OnDisconnect func() // Fires when the PTY is closed. } // 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") } + timeout := safeInt("Timeout", obj) rows := safeInt("Rows", 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") } + if timeout <= 0 { + timeout = 30 // Default timeout to 30 seconds if not specified + } + config := &SSHXtermConfig{ - Rows: rows, - Cols: cols, + Timeout: timeout, + Rows: rows, + Cols: cols, } onStdout := obj.Get("OnStdout") diff --git a/internal/hp_ipn/ssh.go b/internal/hp_ipn/ssh.go index 8db62b2..a03e3cb 100644 --- a/internal/hp_ipn/ssh.go +++ b/internal/hp_ipn/ssh.go @@ -50,8 +50,7 @@ func (i *TsWasmIpn) NewSSHSession(hostname, username string, termConfig *SSHXter func (s *SSHSession) ConnectAndRun() { defer s.TermConfig.OnDisconnect() - // Default to a 5 second timeout for the connection AFTER dial. - ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + ctx, cancel := context.WithTimeout(context.Background(), time.Duration(s.TermConfig.Timeout)*time.Second) defer cancel() // TODO: Log here