mirror of
https://github.com/tale/headplane.git
synced 2026-08-28 16:07:07 +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">
|
<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">
|
||||||
|
|||||||
@@ -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
@@ -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")
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user