mirror of
https://github.com/tale/headplane.git
synced 2026-07-28 08:38:57 +00:00
fix: better ssh resilience and customizable timeout for future backoff
This commit is contained in:
@@ -156,7 +156,7 @@ export default function Page() {
|
||||
<div className="w-screen h-screen bg-headplane-900">
|
||||
{ipn === null ? (
|
||||
<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 className="flex flex-col h-screen">
|
||||
|
||||
@@ -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<HTMLDivElement>(null);
|
||||
const term = useRef<xterm.Terminal>(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 (
|
||||
<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 ? (
|
||||
<div
|
||||
className={cn(
|
||||
@@ -135,6 +149,16 @@ export default function XTerm({ ipn, username, hostname }: XTermProps) {
|
||||
{term.current.cols}x{term.current.rows}
|
||||
</div>
|
||||
) : 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>
|
||||
);
|
||||
}
|
||||
|
||||
+16
-22
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user