mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-11-10 16:21:22 +00:00
* fix: resolve SSH chain connection hang and improve connection progress - Fix Promise never settling when conn 'close' fires before 'ready' during chain connections, which caused "reply was never sent" error - Replace fake timed progress animation with real backend events - Send granular connection progress for all SSH connections (not just chain), including: connecting, key exchange, auth attempts, forwarding, shell opening - Surface auth method attempts (SSH agent, key names, password) in progress logs so users can diagnose authentication failures - Include error details in progress events for better error visibility Closes #463 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: scope progress events by sessionId, prevent duplicate errors, hide chain UI for direct SSH - Add sessionId to chain progress payload so events are scoped per session (P1) - Set settled=true in error/timeout handlers to prevent close handler from emitting a second misleading 'closed unexpectedly' error (P2) - Only show chain progress UI when total > 1 so direct SSH connections don't render as 'Chain 1/1' (P3) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: mark shell-open failure as settled before closing connection The conn.shell() error branch calls conn.end() which triggers the close handler, but settled was not set yet, causing a duplicate 'closed unexpectedly' error to overwrite the real shell-open failure message. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
164 lines
5.8 KiB
TypeScript
164 lines
5.8 KiB
TypeScript
import { useCallback } from "react";
|
|
import { netcattyBridge } from "../../infrastructure/services/netcattyBridge";
|
|
|
|
export const useTerminalBackend = () => {
|
|
const telnetAvailable = useCallback(() => {
|
|
const bridge = netcattyBridge.get();
|
|
return !!bridge?.startTelnetSession;
|
|
}, []);
|
|
|
|
const moshAvailable = useCallback(() => {
|
|
const bridge = netcattyBridge.get();
|
|
return !!bridge?.startMoshSession;
|
|
}, []);
|
|
|
|
const localAvailable = useCallback(() => {
|
|
const bridge = netcattyBridge.get();
|
|
return !!bridge?.startLocalSession;
|
|
}, []);
|
|
|
|
const serialAvailable = useCallback(() => {
|
|
const bridge = netcattyBridge.get();
|
|
return !!bridge?.startSerialSession;
|
|
}, []);
|
|
|
|
const execAvailable = useCallback(() => {
|
|
const bridge = netcattyBridge.get();
|
|
return !!bridge?.execCommand;
|
|
}, []);
|
|
|
|
const startSSHSession = useCallback(async (options: NetcattySSHOptions) => {
|
|
const bridge = netcattyBridge.get();
|
|
if (!bridge?.startSSHSession) throw new Error("startSSHSession unavailable");
|
|
return bridge.startSSHSession(options);
|
|
}, []);
|
|
|
|
const startTelnetSession = useCallback(async (options: Parameters<NonNullable<NetcattyBridge["startTelnetSession"]>>[0]) => {
|
|
const bridge = netcattyBridge.get();
|
|
if (!bridge?.startTelnetSession) throw new Error("startTelnetSession unavailable");
|
|
return bridge.startTelnetSession(options);
|
|
}, []);
|
|
|
|
const startMoshSession = useCallback(async (options: Parameters<NonNullable<NetcattyBridge["startMoshSession"]>>[0]) => {
|
|
const bridge = netcattyBridge.get();
|
|
if (!bridge?.startMoshSession) throw new Error("startMoshSession unavailable");
|
|
return bridge.startMoshSession(options);
|
|
}, []);
|
|
|
|
const startLocalSession = useCallback(async (options: Parameters<NonNullable<NetcattyBridge["startLocalSession"]>>[0]) => {
|
|
const bridge = netcattyBridge.get();
|
|
if (!bridge?.startLocalSession) throw new Error("startLocalSession unavailable");
|
|
return bridge.startLocalSession(options);
|
|
}, []);
|
|
|
|
const startSerialSession = useCallback(async (options: Parameters<NonNullable<NetcattyBridge["startSerialSession"]>>[0]) => {
|
|
const bridge = netcattyBridge.get();
|
|
if (!bridge?.startSerialSession) throw new Error("startSerialSession unavailable");
|
|
return bridge.startSerialSession(options);
|
|
}, []);
|
|
|
|
const execCommand = useCallback(async (options: Parameters<NetcattyBridge["execCommand"]>[0]) => {
|
|
const bridge = netcattyBridge.get();
|
|
if (!bridge?.execCommand) throw new Error("execCommand unavailable");
|
|
return bridge.execCommand(options);
|
|
}, []);
|
|
|
|
const writeToSession = useCallback((sessionId: string, data: string) => {
|
|
const bridge = netcattyBridge.get();
|
|
bridge?.writeToSession?.(sessionId, data);
|
|
}, []);
|
|
|
|
const resizeSession = useCallback((sessionId: string, cols: number, rows: number) => {
|
|
const bridge = netcattyBridge.get();
|
|
bridge?.resizeSession?.(sessionId, cols, rows);
|
|
}, []);
|
|
|
|
const closeSession = useCallback((sessionId: string) => {
|
|
const bridge = netcattyBridge.get();
|
|
bridge?.closeSession?.(sessionId);
|
|
}, []);
|
|
|
|
const setSessionEncoding = useCallback(async (sessionId: string, encoding: string) => {
|
|
const bridge = netcattyBridge.get();
|
|
if (!bridge?.setSessionEncoding) return { ok: false, encoding };
|
|
return bridge.setSessionEncoding(sessionId, encoding);
|
|
}, []);
|
|
|
|
const onSessionData = useCallback((sessionId: string, cb: (data: string) => void) => {
|
|
const bridge = netcattyBridge.get();
|
|
if (!bridge?.onSessionData) throw new Error("onSessionData unavailable");
|
|
return bridge.onSessionData(sessionId, cb);
|
|
}, []);
|
|
|
|
const onSessionExit = useCallback((sessionId: string, cb: (evt: { exitCode?: number; signal?: number; error?: string; reason?: "exited" | "error" | "timeout" | "closed" }) => void) => {
|
|
const bridge = netcattyBridge.get();
|
|
if (!bridge?.onSessionExit) throw new Error("onSessionExit unavailable");
|
|
return bridge.onSessionExit(sessionId, cb);
|
|
}, []);
|
|
|
|
const onChainProgress = useCallback((cb: (sessionId: string, hop: number, total: number, label: string, status: string, error?: string) => void) => {
|
|
const bridge = netcattyBridge.get();
|
|
return bridge?.onChainProgress?.(cb);
|
|
}, []);
|
|
|
|
const openExternal = useCallback(async (url: string) => {
|
|
const bridge = netcattyBridge.get();
|
|
await bridge?.openExternal?.(url);
|
|
}, []);
|
|
|
|
const openExternalAvailable = useCallback(() => {
|
|
const bridge = netcattyBridge.get();
|
|
return !!bridge?.openExternal;
|
|
}, []);
|
|
|
|
const backendAvailable = useCallback(() => {
|
|
const bridge = netcattyBridge.get();
|
|
return !!bridge?.startSSHSession;
|
|
}, []);
|
|
|
|
const listSerialPorts = useCallback(async () => {
|
|
const bridge = netcattyBridge.get();
|
|
if (!bridge?.listSerialPorts) return [];
|
|
return bridge.listSerialPorts();
|
|
}, []);
|
|
|
|
const getSessionPwd = useCallback(async (sessionId: string) => {
|
|
const bridge = netcattyBridge.get();
|
|
if (!bridge?.getSessionPwd) return { success: false, error: 'getSessionPwd unavailable' };
|
|
return bridge.getSessionPwd(sessionId);
|
|
}, []);
|
|
|
|
const getServerStats = useCallback(async (sessionId: string) => {
|
|
const bridge = netcattyBridge.get();
|
|
if (!bridge?.getServerStats) return { success: false, error: 'getServerStats unavailable' };
|
|
return bridge.getServerStats(sessionId);
|
|
}, []);
|
|
|
|
return {
|
|
backendAvailable,
|
|
telnetAvailable,
|
|
moshAvailable,
|
|
localAvailable,
|
|
serialAvailable,
|
|
execAvailable,
|
|
openExternalAvailable,
|
|
startSSHSession,
|
|
startTelnetSession,
|
|
startMoshSession,
|
|
startLocalSession,
|
|
startSerialSession,
|
|
listSerialPorts,
|
|
execCommand,
|
|
getSessionPwd,
|
|
getServerStats,
|
|
writeToSession,
|
|
resizeSession,
|
|
closeSession,
|
|
setSessionEncoding,
|
|
onSessionData,
|
|
onSessionExit,
|
|
onChainProgress,
|
|
openExternal,
|
|
};
|
|
};
|