feat: 完成修改挂起会话名称的功能
This commit is contained in:
@@ -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<SuspendedSessionInfo[]> 挂起会话信息的数组。
|
||||
*/
|
||||
async listSuspendedSessions(userId: number): Promise<SuspendedSessionInfo[]> { // userId: string -> number
|
||||
async listSuspendedSessions(userId: number): Promise<SuspendedSessionInfo[]> {
|
||||
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,
|
||||
|
||||
@@ -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 函数
|
||||
|
||||
@@ -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<void> {
|
||||
@@ -104,4 +105,42 @@ export class SshSuspendController {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async editSessionNameHttp(req: Request, res: Response): Promise<void> {
|
||||
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' });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user