diff --git a/src/backend/ssh/file-manager.ts b/src/backend/ssh/file-manager.ts index 8eae5798..9f5edf5b 100644 --- a/src/backend/ssh/file-manager.ts +++ b/src/backend/ssh/file-manager.ts @@ -93,11 +93,11 @@ function scheduleSessionCleanup(sessionId: string) { // Clear existing timeout if (session.timeout) clearTimeout(session.timeout); - // Set new timeout for 10 minutes of inactivity + // Increase timeout to 30 minutes of inactivity session.timeout = setTimeout(() => { fileLogger.info(`Cleaning up inactive SSH session: ${sessionId}`); cleanupSession(sessionId); - }, 10 * 60 * 1000); // 10 minutes + }, 30 * 60 * 1000); // 30 minutes - increased from 10 minutes } } @@ -321,6 +321,41 @@ app.get("/ssh/file_manager/ssh/status", (req, res) => { res.json({ status: "success", connected: isConnected }); }); +// SSH keepalive endpoint - extends session timeout and verifies connection +app.post("/ssh/file_manager/ssh/keepalive", (req, res) => { + const { sessionId } = req.body; + + if (!sessionId) { + return res.status(400).json({ error: "Session ID is required" }); + } + + const session = sshSessions[sessionId]; + + if (!session || !session.isConnected) { + return res.status(400).json({ + error: "SSH session not found or not connected", + connected: false + }); + } + + // Update last active time and reschedule cleanup + session.lastActive = Date.now(); + scheduleSessionCleanup(sessionId); + + fileLogger.debug(`SSH session keepalive: ${sessionId}`, { + operation: "ssh_keepalive", + sessionId, + lastActive: session.lastActive, + }); + + res.json({ + status: "success", + connected: true, + message: "Session keepalive successful", + lastActive: session.lastActive + }); +}); + app.get("/ssh/file_manager/ssh/listFiles", (req, res) => { const sessionId = req.query.sessionId as string; const sshConn = sshSessions[sessionId]; diff --git a/src/ui/Desktop/Apps/File Manager/FileManagerModern.tsx b/src/ui/Desktop/Apps/File Manager/FileManagerModern.tsx index a804118a..a3007715 100644 --- a/src/ui/Desktop/Apps/File Manager/FileManagerModern.tsx +++ b/src/ui/Desktop/Apps/File Manager/FileManagerModern.tsx @@ -38,6 +38,7 @@ import { moveSSHItem, connectSSH, getSSHStatus, + keepSSHAlive, identifySSHSymlink, addRecentFile, addPinnedFile, @@ -144,6 +145,36 @@ function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) { sshHost: currentHost!, }); + // SSH keepalive function + const startKeepalive = useCallback(() => { + if (!sshSessionId) return; + + // Clear existing timer + if (keepaliveTimerRef.current) { + clearInterval(keepaliveTimerRef.current); + } + + // Send keepalive every 5 minutes (300000ms) + keepaliveTimerRef.current = setInterval(async () => { + if (sshSessionId) { + try { + await keepSSHAlive(sshSessionId); + console.log("SSH keepalive sent successfully"); + } catch (error) { + console.error("SSH keepalive failed:", error); + // If keepalive fails, session might be dead - could trigger reconnect here + } + } + }, 5 * 60 * 1000); // 5 minutes + }, [sshSessionId]); + + const stopKeepalive = useCallback(() => { + if (keepaliveTimerRef.current) { + clearInterval(keepaliveTimerRef.current); + keepaliveTimerRef.current = null; + } + }, []); + // Initialize SSH connection useEffect(() => { if (currentHost) { @@ -151,6 +182,20 @@ function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) { } }, [currentHost]); + // Start/stop keepalive based on SSH session + useEffect(() => { + if (sshSessionId) { + startKeepalive(); + } else { + stopKeepalive(); + } + + // Cleanup on unmount + return () => { + stopKeepalive(); + }; + }, [sshSessionId, startKeepalive, stopKeepalive]); + // Track if initial directory load is done to prevent duplicate loading const initialLoadDoneRef = useRef(false); // Track last path change to prevent rapid navigation issues @@ -158,6 +203,8 @@ function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) { const pathChangeTimerRef = useRef(null); // Track current loading request to handle cancellation const currentLoadingPathRef = useRef(""); + // SSH keepalive timer + const keepaliveTimerRef = useRef(null); // Debounced directory loading for path changes const debouncedLoadDirectory = useCallback((path: string) => { diff --git a/src/ui/main-axios.ts b/src/ui/main-axios.ts index cbf6e90d..52486737 100644 --- a/src/ui/main-axios.ts +++ b/src/ui/main-axios.ts @@ -1002,6 +1002,17 @@ export async function getSSHStatus( } } +export async function keepSSHAlive(sessionId: string): Promise { + try { + const response = await fileManagerApi.post("/ssh/keepalive", { + sessionId, + }); + return response.data; + } catch (error) { + handleApiError(error, "SSH keepalive"); + } +} + export async function listSSHFiles( sessionId: string, path: string,