diff --git a/packages/backend/src/services/ssh-suspend.service.ts b/packages/backend/src/services/ssh-suspend.service.ts index 121cb4e..b72d36b 100644 --- a/packages/backend/src/services/ssh-suspend.service.ts +++ b/packages/backend/src/services/ssh-suspend.service.ts @@ -62,12 +62,14 @@ export class SshSuspendService extends EventEmitter { logIdentifier, customSuspendName, } = details; + console.log(`[SshSuspendService DEBUG] takeOverMarkedSession: Called for userId=${userId}, originalSessionId=${originalSessionId}`); // 检查 SSH client 和 channel 是否仍然可用 // ClientChannel 有 readable 和 writable, Client 本身没有直接的此类属性 // 如果 channel 不可读写,通常意味着底层连接有问题。 + console.log(`[SshSuspendService DEBUG] takeOverMarkedSession: Checking channel for originalSessionId=${originalSessionId}. Readable: ${channel?.readable}, Writable: ${channel?.writable}`); if (!channel || !channel.readable || !channel.writable) { - console.warn(`[用户: ${userId}] 尝试接管会话 ${originalSessionId} 时,SSH channel 已不可用 (readable: ${channel?.readable}, writable: ${channel?.writable})。将标记为已断开。`); + console.warn(`[SshSuspendService WARN] takeOverMarkedSession: userId=${userId}, originalSessionId=${originalSessionId}. SSH channel is not usable. readable=${channel?.readable}, writable=${channel?.writable}. Cannot take over.`); // 确保如果 SSH 连接已经关闭,日志文件仍然保留,但不创建挂起条目。 // SshSuspendService 不会管理这个“已经断开”的会话,但日志保留供用户清理。 try { channel?.end(); } catch (e) { /* ignore */ } @@ -101,50 +103,72 @@ export class SshSuspendService extends EventEmitter { }; userSessions.set(suspendSessionId, sessionDetails); - console.log(`[用户: ${userId}] SSH会话 ${originalSessionId} 已被 SshSuspendService 接管 (新挂起ID: ${suspendSessionId})。日志文件标识: ${logIdentifier}`); + console.log(`[SshSuspendService INFO] takeOverMarkedSession: userId=${userId}, originalSessionId=${originalSessionId} taken over. New suspendSessionId=${suspendSessionId}, initial status=${sessionDetails.backendSshStatus}. Log identifier=${logIdentifier}`); await this.logStorageService.ensureLogDirectoryExists(); + console.log(`[SshSuspendService DEBUG] takeOverMarkedSession: Setting up channel 'data' listener for suspendSessionId=${suspendSessionId}`); channel.on('data', (data: Buffer) => { - if (userSessions.get(suspendSessionId)?.backendSshStatus === 'hanging') { + const currentDetails = userSessions.get(suspendSessionId); + if (currentDetails?.backendSshStatus === 'hanging') { + // console.log(`[SshSuspendService DEBUG] channel.on('data') for suspendSessionId=${suspendSessionId}: Writing to log ${logIdentifier}`); this.logStorageService.writeToLog(logIdentifier, data.toString('utf-8')).catch(err => { - console.error(`[用户: ${userId}, 挂起ID: ${suspendSessionId}, 日志: ${logIdentifier}] 写入挂起日志失败:`, err); + console.error(`[SshSuspendService ERROR] channel.on('data') for suspendSessionId=${suspendSessionId}, log=${logIdentifier}: Failed to write to log:`, err); }); + } else { + // console.log(`[SshSuspendService DEBUG] channel.on('data') for suspendSessionId=${suspendSessionId}: Backend status is ${currentDetails?.backendSshStatus}, not writing to log.`); } }); const handleSessionTermination = (reasonSuffix: string) => { const currentSession = userSessions.get(suspendSessionId); + console.log(`[SshSuspendService DEBUG] handleSessionTermination: Called for suspendSessionId=${suspendSessionId}, reasonSuffix='${reasonSuffix}'. Session found: ${!!currentSession}. Current status: ${currentSession?.backendSshStatus}`); if (currentSession && currentSession.backendSshStatus === 'hanging') { const reason = `SSH connection ${reasonSuffix}.`; - console.warn(`[用户: ${userId}, 挂起ID: ${suspendSessionId}, 日志: ${logIdentifier}] SSH 连接在挂起期间终止。原因: ${reason}`); + console.warn(`[SshSuspendService WARN] handleSessionTermination: userId=${currentSession.userId}, suspendSessionId=${suspendSessionId}. SSH connection terminated during suspension. Reason: ${reason}`); currentSession.backendSshStatus = 'disconnected_by_backend'; currentSession.disconnectionTimestamp = new Date().toISOString(); this.removeChannelListeners(channel, sshClient); + console.log(`[SshSuspendService DEBUG] handleSessionTermination: Listeners removed for suspendSessionId=${suspendSessionId}.`); this.emit('sessionAutoTerminated', { userId: currentSession.userId, suspendSessionId, reason }); + console.log(`[SshSuspendService INFO] handleSessionTermination: Emitted 'sessionAutoTerminated' for suspendSessionId=${suspendSessionId}, userId=${currentSession.userId}.`); + } else if (currentSession) { + console.log(`[SshSuspendService DEBUG] handleSessionTermination: Condition not met for suspendSessionId=${suspendSessionId}. Status was '${currentSession.backendSshStatus}', not 'hanging'. No action taken.`); + } else { + console.warn(`[SshSuspendService WARN] handleSessionTermination: Session not found for suspendSessionId=${suspendSessionId} when event '${reasonSuffix}' occurred.`); } }; - channel.on('close', () => handleSessionTermination('closed')); - channel.on('error', (err: Error) => { - console.error(`[用户: ${userId}, 挂起ID: ${suspendSessionId}, 日志: ${logIdentifier}] 挂起会话的通道错误:`, err); - handleSessionTermination('errored'); + console.log(`[SshSuspendService DEBUG] takeOverMarkedSession: Setting up channel/client event listeners for suspendSessionId=${suspendSessionId}`); + channel.on('close', () => { + console.log(`[SshSuspendService DEBUG] channel.on('close') triggered for suspendSessionId=${suspendSessionId}`); + handleSessionTermination('channel closed'); + }); + channel.on('error', (err: Error) => { + console.error(`[SshSuspendService ERROR] channel.on('error') for suspendSessionId=${suspendSessionId}:`, err); + handleSessionTermination('channel errored'); + }); + channel.on('end', () => { + console.log(`[SshSuspendService DEBUG] channel.on('end') triggered for suspendSessionId=${suspendSessionId}`); + handleSessionTermination('channel ended'); + }); + channel.on('exit', (code: number | null, signalName: string | null) => { + console.log(`[SshSuspendService DEBUG] channel.on('exit') triggered for suspendSessionId=${suspendSessionId}. Code: ${code}, Signal: ${signalName}`); + handleSessionTermination(`channel exited with code ${code}, signal ${signalName}`); }); - channel.on('end', () => handleSessionTermination('ended')); - channel.on('exit', (code: number | null, signalName: string | null) => handleSessionTermination(`exited with code ${code}, signal ${signalName}`)); sshClient.on('error', (err: Error) => { - console.error(`[用户: ${userId}, 挂起ID: ${suspendSessionId}, 日志: ${logIdentifier}] 挂起会话的SSH客户端错误:`, err); + console.error(`[SshSuspendService ERROR] sshClient.on('error') for suspendSessionId=${suspendSessionId}:`, err); handleSessionTermination('client errored'); }); sshClient.on('end', () => { - console.log(`[用户: ${userId}, 挂起ID: ${suspendSessionId}, 日志: ${logIdentifier}] 挂起会话的SSH客户端连接结束。`); + console.log(`[SshSuspendService DEBUG] sshClient.on('end') triggered for suspendSessionId=${suspendSessionId}`); handleSessionTermination('client ended'); }); @@ -167,11 +191,13 @@ export class SshSuspendService extends EventEmitter { * @param userId 用户ID。 * @returns Promise 挂起会话信息的数组。 */ - async listSuspendedSessions(userId: number): Promise { // userId: string -> number + async listSuspendedSessions(userId: number): Promise { + console.log(`[SshSuspendService DEBUG] listSuspendedSessions: Called for userId=${userId}`); const userSessions = this.getUserSessions(userId); const sessionsInfo: SuspendedSessionInfo[] = []; for (const [suspendSessionId, details] of userSessions.entries()) { + console.log(`[SshSuspendService DEBUG] listSuspendedSessions: Processing suspendSessionId=${suspendSessionId}, status=${details.backendSshStatus}`); sessionsInfo.push({ suspendSessionId, connectionName: details.connectionName, diff --git a/packages/backend/src/services/ssh.service.ts b/packages/backend/src/services/ssh.service.ts index 8b068a2..469aa23 100644 --- a/packages/backend/src/services/ssh.service.ts +++ b/packages/backend/src/services/ssh.service.ts @@ -145,8 +145,8 @@ export const establishSshConnection = ( privateKey: connDetails.privateKey, passphrase: connDetails.passphrase, readyTimeout: timeout, - keepaliveInterval: 10000, // 保持连接 - keepaliveCountMax: 10, + keepaliveInterval: 5000, // 修改:每 5 秒发送一次 keepalive + keepaliveCountMax: 5, // 修改:最多尝试 5 次 (总超时约 5*5=10 秒) }; const readyHandler = async () => { // 改为 async 函数 diff --git a/packages/backend/src/ssh-suspend/ssh-suspend.controller.ts b/packages/backend/src/ssh-suspend/ssh-suspend.controller.ts index ebff402..91dbcb9 100644 --- a/packages/backend/src/ssh-suspend/ssh-suspend.controller.ts +++ b/packages/backend/src/ssh-suspend/ssh-suspend.controller.ts @@ -11,6 +11,7 @@ export class SshSuspendController { this.getSuspendedSshSessions = this.getSuspendedSshSessions.bind(this); this.terminateAndRemoveSession = this.terminateAndRemoveSession.bind(this); this.removeSessionEntry = this.removeSessionEntry.bind(this); + this.editSessionNameHttp = this.editSessionNameHttp.bind(this); // 绑定新方法 } public async getSuspendedSshSessions(req: Request, res: Response): Promise { @@ -104,4 +105,42 @@ export class SshSuspendController { } } } - } \ No newline at end of file + + public async editSessionNameHttp(req: Request, res: Response): Promise { + try { + const userId = req.session.userId; + const { suspendSessionId } = req.params; + const { customName } = req.body; // 从请求体获取新名称 + + if (!userId) { + res.status(401).json({ message: 'Unauthorized. User ID not found in session.' }); + return; + } + if (!suspendSessionId) { + res.status(400).json({ message: 'Bad Request. suspendSessionId parameter is missing.' }); + return; + } + if (typeof customName !== 'string') { // 验证 customName + res.status(400).json({ message: 'Bad Request. customName must be a string and is missing or invalid.' }); + return; + } + + console.log(`[SshSuspendController] editSessionNameHttp called for user ID: ${userId}, suspendSessionId: ${suspendSessionId}, newName: "${customName}"`); + + const success = await sshSuspendService.editSuspendedSessionName(userId, suspendSessionId, customName); + if (success) { + res.status(200).json({ message: `Suspended session ${suspendSessionId} name updated to "${customName}".`, customName }); + } else { + // 假设服务层在找不到会话时返回 false + res.status(404).json({ message: `Failed to update name for session ${suspendSessionId}. It might not exist.` }); + } + } catch (error) { + console.error(`[SshSuspendController] Error editing session name for user ID: ${req.session.userId}, suspendSessionId: ${req.params.suspendSessionId}:`, error); + if (error instanceof Error) { + res.status(500).json({ message: 'Failed to edit suspended session name', error: error.message }); + } else { + res.status(500).json({ message: 'Failed to edit suspended session name', error: 'Unknown error' }); + } + } + } +} \ No newline at end of file diff --git a/packages/backend/src/ssh-suspend/ssh-suspend.routes.ts b/packages/backend/src/ssh-suspend/ssh-suspend.routes.ts index e6b4d94..4b422db 100644 --- a/packages/backend/src/ssh-suspend/ssh-suspend.routes.ts +++ b/packages/backend/src/ssh-suspend/ssh-suspend.routes.ts @@ -27,4 +27,11 @@ router.delete( sshSuspendController.removeSessionEntry ); +// Route to edit a suspended session's custom name +router.put( + '/name/:suspendSessionId', + isAuthenticated, + sshSuspendController.editSessionNameHttp // 新的控制器方法 +); + export default router; \ No newline at end of file diff --git a/packages/backend/src/websocket/connection.ts b/packages/backend/src/websocket/connection.ts index 20bd945..2301e62 100644 --- a/packages/backend/src/websocket/connection.ts +++ b/packages/backend/src/websocket/connection.ts @@ -7,14 +7,14 @@ import { SshSuspendResumeRequest, SshSuspendTerminateRequest, SshSuspendRemoveEntryRequest, - SshSuspendEditNameRequest, + // SshSuspendEditNameRequest, // Removed as it's now HTTP SshSuspendStartedResponse, SshSuspendListResponse, SshSuspendResumedNotification, SshOutputCachedChunk, SshSuspendTerminatedResponse, SshSuspendEntryRemovedResponse, - SshSuspendNameEditedResponse, + // SshSuspendNameEditedResponse, // Removed as it's now HTTP SshSuspendAutoTerminatedNotification, SshMarkForSuspendRequest, SshMarkedForSuspendAck, @@ -296,26 +296,7 @@ export function initializeConnectionHandler(wss: WebSocketServer, sshSuspendServ } break; } - case 'SSH_SUSPEND_EDIT_NAME': { - const { suspendSessionId, customName } = payload as SshSuspendEditNameRequest['payload']; - if (!ws.userId) { - console.error(`[SSH_SUSPEND_EDIT_NAME] 用户 ID 未定义。`); - if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: 'SSH_SUSPEND_NAME_EDITED_RESP', payload: { suspendSessionId, success: false, error: '用户认证失败' } })); - break; - } - try { - const success = await sshSuspendService.editSuspendedSessionName(ws.userId, suspendSessionId, customName); - const response: SshSuspendNameEditedResponse = { - type: 'SSH_SUSPEND_NAME_EDITED', - payload: { suspendSessionId, success, customName: success ? customName : undefined } - }; - if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify(response)); - } catch (error: any) { - console.error(`[SSH_SUSPEND_EDIT_NAME] 编辑名称 ${suspendSessionId} 失败:`, error); - if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: 'SSH_SUSPEND_NAME_EDITED_RESP', payload: { suspendSessionId, success: false, error: error.message || '编辑名称失败' } })); - } - break; - } + // SSH_SUSPEND_EDIT_NAME case removed, handled by HTTP API now case 'SSH_MARK_FOR_SUSPEND': { const markPayload = payload as SshMarkForSuspendRequest['payload']; const sessionToMarkId = markPayload.sessionId; diff --git a/packages/frontend/src/stores/session/actions/sshSuspendActions.ts b/packages/frontend/src/stores/session/actions/sshSuspendActions.ts index 43c41c4..d5aacf8 100644 --- a/packages/frontend/src/stores/session/actions/sshSuspendActions.ts +++ b/packages/frontend/src/stores/session/actions/sshSuspendActions.ts @@ -8,7 +8,7 @@ import type { SshSuspendResumeReqMessage, SshSuspendTerminateReqMessage, SshSuspendRemoveEntryReqMessage, - SshSuspendEditNameReqMessage, + // SshSuspendEditNameReqMessage, // Removed, using HTTP API // S2C Payloads SshMarkedForSuspendAckPayload, SshUnmarkedForSuspendAckPayload, // +++ 新增导入 +++ @@ -17,10 +17,10 @@ import type { SshOutputCachedChunkPayload, SshSuspendTerminatedRespPayload, SshSuspendEntryRemovedRespPayload, - SshSuspendNameEditedRespPayload, + // SshSuspendNameEditedRespPayload, // Removed, using HTTP API SshSuspendAutoTerminatedNotifPayload, } from '../../../types/websocket.types'; // 路径: packages/frontend/src/types/websocket.types.ts -import type { WsManagerInstance, SessionState } from '../types'; // 路径: packages/frontend/src/stores/session/types.ts +import type { WsManagerInstance, SessionState } from '../types'; // 路径: packages/frontend/src/stores/session/types.ts // Re-add WsManagerInstance import { closeSession as closeSessionAction, activateSession as activateSessionAction, openNewSession, closeSession } from './sessionActions'; // 使用 openNewSession 和 closeSession import { useConnectionsStore } from '../../connections.store'; // 用于获取连接信息 import { useUiNotificationsStore } from '../../uiNotifications.store'; // 用于显示通知 @@ -325,21 +325,43 @@ export const removeSshSessionEntry = async (suspendSessionId: string): Promise { - const wsManager = getActiveWsManager(); - if (wsManager) { - const message: SshSuspendEditNameReqMessage = { - type: 'SSH_SUSPEND_EDIT_NAME', - payload: { suspendSessionId, customName }, - }; - wsManager.sendMessage(message); - console.log(`[${t('term.sshSuspend')}] 已发送 SSH_SUSPEND_EDIT_NAME_REQ (挂起 ID: ${suspendSessionId}, 名称: "${customName}")`); - } else { - console.warn(`[${t('term.sshSuspend')}] 编辑挂起名称失败 (挂起 ID: ${suspendSessionId}):无可用 WebSocket 连接。`); +export const editSshSessionName = async (suspendSessionId: string, newCustomName: string): Promise => { + console.log(`[${t('term.sshSuspend')}] 请求通过 HTTP API 编辑挂起会话名称 (ID: ${suspendSessionId}, 新名称: "${newCustomName}")`); + const uiNotificationsStore = useUiNotificationsStore(); + try { + // 假设后端 API 端点为 /api/ssh-suspend/name/:suspendSessionId + // 并且它接受一个包含 { customName: string } 的 PUT 请求体 + // 并返回包含 { message: string, customName: string } 的成功响应 + const response = await apiClient.put<{ message: string, customName: string }>( + `ssh-suspend/name/${suspendSessionId}`, + { customName: newCustomName } + ); + + console.log(`[${t('term.sshSuspend')}] HTTP API 编辑名称 ${suspendSessionId} 成功:`, response.data); + + // 更新前端状态 + const session = suspendedSshSessions.value.find(s => s.suspendSessionId === suspendSessionId); + if (session) { + session.customSuspendName = response.data.customName; // 使用后端返回的名称确保一致性 + uiNotificationsStore.addNotification({ + type: 'success', + message: t('sshSuspend.notifications.nameEditedSuccess', { name: response.data.customName }), + }); + } else { + // 如果会话在前端列表中找不到了(理论上不应该发生,因为是先找到再编辑的) + // 也可以选择重新获取列表 + fetchSuspendedSshSessions(); + } + } catch (error: any) { + console.error(`[${t('term.sshSuspend')}] 通过 HTTP API 编辑名称 ${suspendSessionId} 失败:`, error); + uiNotificationsStore.addNotification({ + type: 'error', + message: t('sshSuspend.notifications.nameEditedError', { error: error.response?.data?.message || error.message || t('term.unknownError') }), + }); } }; @@ -561,26 +583,7 @@ const handleSshSuspendEntryRemovedResp = (payload: SshSuspendEntryRemovedRespPay } }; -const handleSshSuspendNameEditedResp = (payload: SshSuspendNameEditedRespPayload): void => { - const uiNotificationsStore = useUiNotificationsStore(); - console.log(`[${t('term.sshSuspend')}] 接到 SSH_SUSPEND_NAME_EDITED_RESP:`, payload); - if (payload.success && payload.customName !== undefined) { - const session = suspendedSshSessions.value.find(s => s.suspendSessionId === payload.suspendSessionId); - if (session) { - session.customSuspendName = payload.customName; - uiNotificationsStore.addNotification({ - type: 'success', - message: t('sshSuspend.notifications.nameEditedSuccess', { name: payload.customName }), - }); - } - } else { - uiNotificationsStore.addNotification({ - type: 'error', - message: t('sshSuspend.notifications.nameEditedError', { error: payload.error || t('term.unknownError') }), - }); - console.error(`[${t('term.sshSuspend')}] 编辑挂起名称失败 (ID: ${payload.suspendSessionId}): ${payload.error}`); - } -}; +// handleSshSuspendNameEditedResp removed as edit is now via HTTP const handleSshSuspendAutoTerminatedNotif = (payload: SshSuspendAutoTerminatedNotifPayload): void => { const uiNotificationsStore = useUiNotificationsStore(); @@ -621,10 +624,10 @@ export const registerSshSuspendHandlers = (wsManager: WsManagerInstance): void = wsManager.onMessage('SSH_OUTPUT_CACHED_CHUNK', (p: MessagePayload) => handleSshOutputCachedChunk(p as SshOutputCachedChunkPayload)); wsManager.onMessage('SSH_SUSPEND_TERMINATED_RESP', (p: MessagePayload) => handleSshSuspendTerminatedResp(p as SshSuspendTerminatedRespPayload)); wsManager.onMessage('SSH_SUSPEND_ENTRY_REMOVED_RESP', (p: MessagePayload) => handleSshSuspendEntryRemovedResp(p as SshSuspendEntryRemovedRespPayload)); - wsManager.onMessage('SSH_SUSPEND_NAME_EDITED_RESP', (p: MessagePayload) => handleSshSuspendNameEditedResp(p as SshSuspendNameEditedRespPayload)); + // SSH_SUSPEND_NAME_EDITED_RESP handler removed wsManager.onMessage('SSH_SUSPEND_AUTO_TERMINATED_NOTIF', (p: MessagePayload) => handleSshSuspendAutoTerminatedNotif(p as SshSuspendAutoTerminatedNotifPayload)); - console.log(`[${t('term.sshSuspend')}] SSH 挂起模式的 WebSocket 消息处理器已注册。`); + console.log(`[${t('term.sshSuspend')}] SSH 挂起模式的 WebSocket 消息处理器已注册 (移除了名称编辑相关的处理器)。`); // 连接建立后,主动获取一次挂起列表 // 考虑:是否应该在这里做,或者在应用启动时做一次? diff --git a/packages/frontend/src/views/SuspendedSshSessionsView.vue b/packages/frontend/src/views/SuspendedSshSessionsView.vue index 39ad9ea..e453907 100644 --- a/packages/frontend/src/views/SuspendedSshSessionsView.vue +++ b/packages/frontend/src/views/SuspendedSshSessionsView.vue @@ -38,7 +38,7 @@
@@ -107,87 +107,43 @@