update
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { DockerService, DockerCommand } from '../services/docker.service'; // 导入服务和命令类型
|
||||
|
||||
// 由于没有 typedi,我们将手动实例化服务或通过其他方式获取实例
|
||||
// 简单起见,这里直接 new 一个实例。在实际项目中,可能需要更复杂的实例管理。
|
||||
const dockerService = new DockerService();
|
||||
|
||||
export class DockerController {
|
||||
|
||||
/**
|
||||
* 处理获取 Docker 容器状态的请求 (GET /docker/status)
|
||||
*/
|
||||
async getStatus(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const status = await dockerService.getContainerStatus();
|
||||
res.json(status); // 直接返回 { available: boolean, containers: DockerContainer[] }
|
||||
} catch (error) {
|
||||
// 将错误传递给 Express 的错误处理中间件
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理执行 Docker 命令的请求 (POST /docker/command)
|
||||
*/
|
||||
async executeCommand(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
// 从请求体中获取参数
|
||||
const { containerId, command } = req.body;
|
||||
|
||||
// 基本的输入验证
|
||||
if (!containerId || typeof containerId !== 'string') {
|
||||
res.status(400).json({ message: 'Missing or invalid containerId in request body.' });
|
||||
return;
|
||||
}
|
||||
// 验证 command 是否是允许的类型
|
||||
const allowedCommands: DockerCommand[] = ['start', 'stop', 'restart', 'remove'];
|
||||
if (!command || !allowedCommands.includes(command)) {
|
||||
res.status(400).json({ message: `Invalid command. Must be one of: ${allowedCommands.join(', ')}.` });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 调用服务执行命令
|
||||
await dockerService.executeContainerCommand(containerId, command as DockerCommand);
|
||||
// 成功执行,返回 200 OK 或 204 No Content
|
||||
res.status(200).json({ message: `Command '${command}' executed successfully for container ${containerId}.` });
|
||||
// 或者 res.status(204).send();
|
||||
} catch (error: any) {
|
||||
// 根据错误类型返回不同的状态码
|
||||
if (error.message.includes('Docker is not available')) {
|
||||
res.status(503).json({ message: error.message }); // Service Unavailable
|
||||
} else if (error.message.includes('Invalid container ID') || error.message.includes('Unsupported Docker command')) {
|
||||
res.status(400).json({ message: error.message }); // Bad Request
|
||||
} else {
|
||||
// 其他执行错误,可能是 Docker 守护进程错误等
|
||||
res.status(500).json({ message: error.message || 'Failed to execute Docker command.' }); // Internal Server Error
|
||||
}
|
||||
// 注意:这里没有调用 next(error),因为我们已经处理了响应。
|
||||
// 如果希望使用统一的错误处理中间件,则应该调用 next(error)。
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Router } from 'express';
|
||||
import { DockerController } from './docker.controller';
|
||||
// 修改导入路径
|
||||
import { isAuthenticated } from '../auth/auth.middleware';
|
||||
|
||||
const router = Router();
|
||||
const dockerController = new DockerController(); // 同样,手动实例化
|
||||
|
||||
// 应用认证中间件,确保只有登录用户才能访问 Docker 相关接口
|
||||
router.use(isAuthenticated);
|
||||
|
||||
// GET /api/docker/status - 获取 Docker 容器状态
|
||||
router.get('/status', (req, res, next) => dockerController.getStatus(req, res, next));
|
||||
|
||||
// POST /api/docker/command - 执行 Docker 命令
|
||||
router.post('/command', (req, res, next) => dockerController.executeCommand(req, res, next));
|
||||
|
||||
export default router;
|
||||
@@ -21,6 +21,7 @@ import commandHistoryRoutes from './command-history/command-history.routes'; //
|
||||
import quickCommandsRoutes from './quick-commands/quick-commands.routes'; // 导入快捷指令路由
|
||||
import terminalThemeRoutes from './terminal-themes/terminal-theme.routes'; // 导入终端主题路由
|
||||
import appearanceRoutes from './appearance/appearance.routes'; // 导入外观设置路由
|
||||
// import dockerRouter from './docker/docker.routes'; // <--- 移除 Docker 路由导入
|
||||
import { initializeWebSocket } from './websocket';
|
||||
import { ipWhitelistMiddleware } from './auth/ipWhitelist.middleware'; // 导入 IP 白名单中间件
|
||||
|
||||
@@ -162,8 +163,9 @@ const startServer = () => {
|
||||
app.use('/api/v1/quick-commands', quickCommandsRoutes);
|
||||
app.use('/api/v1/terminal-themes', terminalThemeRoutes);
|
||||
app.use('/api/v1/appearance', appearanceRoutes);
|
||||
// app.use('/api/v1/docker', dockerRouter); // <--- 移除 Docker 路由注册
|
||||
|
||||
// 状态检查接口 (如果不需要 session 可以保留在外面,但移入更安全)
|
||||
// 状态检查接口
|
||||
app.get('/api/v1/status', (req: Request, res: Response) => {
|
||||
res.json({ status: '后端服务运行中!' });
|
||||
});
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
// import { Service } from 'typedi'; // Removed typedi import
|
||||
// import { logger } from '../utils/logger'; // Removed logger import
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
// --- Interfaces (与前端 DockerManager.vue 中的定义保持一致) ---
|
||||
// 理想情况下,这些类型应该放在共享的 types 包中
|
||||
interface PortInfo {
|
||||
IP?: string;
|
||||
PrivatePort: number;
|
||||
PublicPort?: number;
|
||||
Type: 'tcp' | 'udp' | string;
|
||||
}
|
||||
|
||||
interface DockerContainer {
|
||||
Id: string;
|
||||
Names: string[];
|
||||
Image: string;
|
||||
ImageID: string;
|
||||
Command: string;
|
||||
Created: number;
|
||||
State: 'created' | 'restarting' | 'running' | 'removing' | 'paused' | 'exited' | 'dead' | string;
|
||||
Status: string;
|
||||
Ports: PortInfo[];
|
||||
Labels: Record<string, string>;
|
||||
// 根据 `docker ps --format '{{json .}}'` 的输出添加其他需要的字段
|
||||
}
|
||||
|
||||
// 定义命令类型
|
||||
export type DockerCommand = 'start' | 'stop' | 'restart' | 'remove'; // 使用实际的 docker 命令
|
||||
|
||||
// @Service() // Removed typedi decorator
|
||||
export class DockerService {
|
||||
private isDockerAvailableCache: boolean | null = null;
|
||||
private readonly commandTimeout = 15000; // 15 秒超时
|
||||
|
||||
/**
|
||||
* 检查 Docker CLI 是否可用。包含缓存以避免重复检查。
|
||||
*/
|
||||
async checkDockerAvailability(): Promise<boolean> {
|
||||
if (this.isDockerAvailableCache !== null) {
|
||||
return this.isDockerAvailableCache;
|
||||
}
|
||||
|
||||
try {
|
||||
// 尝试执行一个简单的 docker 命令,如 docker version
|
||||
await execAsync('docker version', { timeout: 5000 }); // 5秒超时
|
||||
console.log('[DockerService] Docker is available.'); // Use console.log
|
||||
this.isDockerAvailableCache = true;
|
||||
return true;
|
||||
} catch (error: any) {
|
||||
console.warn('[DockerService] Docker check failed. Docker might not be installed or running.', { error: error.message }); // Use console.warn
|
||||
this.isDockerAvailableCache = false;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有 Docker 容器的状态 (包括已停止的)。
|
||||
*/
|
||||
async getContainerStatus(): Promise<{ available: boolean; containers: DockerContainer[] }> {
|
||||
const available = await this.checkDockerAvailability();
|
||||
if (!available) {
|
||||
return { available: false, containers: [] };
|
||||
}
|
||||
|
||||
try {
|
||||
// 使用 --format '{{json .}}' 获取每个容器的 JSON 输出
|
||||
// 使用 --no-trunc 避免 ID 被截断
|
||||
const { stdout } = await execAsync("docker ps -a --no-trunc --format '{{json .}}'", { timeout: this.commandTimeout });
|
||||
|
||||
// stdout 包含多行 JSON,每行一个容器
|
||||
const lines = stdout.trim().split('\n');
|
||||
const containers: DockerContainer[] = lines
|
||||
.map(line => {
|
||||
try {
|
||||
// Docker 的 JSON 输出有时可能不是严格的 JSON (例如 Names 字段),需要处理
|
||||
// 尝试更健壮的解析或预处理
|
||||
const data = JSON.parse(line);
|
||||
// 手动解析 Names 字段 (docker ps format 的 Names 是逗号分隔的)
|
||||
if (typeof data.Names === 'string') {
|
||||
data.Names = data.Names.split(',');
|
||||
}
|
||||
// 解析 Ports 字段 (可能需要更复杂的逻辑来匹配前端接口)
|
||||
// docker ps format 的 Ports 字段格式比较复杂,直接用 JSON 可能不包含所有信息
|
||||
// 这里暂时依赖 JSON 输出,如果需要更详细的端口信息,可能需要 `docker inspect`
|
||||
// 假设 JSON 输出的 Ports 字段是符合我们接口的数组 (这可能需要调整命令或后端处理)
|
||||
if (!Array.isArray(data.Ports)) {
|
||||
data.Ports = []; // 如果 Ports 不是数组,置为空数组
|
||||
}
|
||||
|
||||
return data as DockerContainer;
|
||||
} catch (parseError) {
|
||||
console.error(`[DockerService] Failed to parse container JSON line: ${line}`, { error: parseError }); // Use console.error
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter((container): container is DockerContainer => container !== null); // 过滤掉解析失败的行
|
||||
|
||||
return { available: true, containers };
|
||||
} catch (error: any) {
|
||||
console.error('[DockerService] Failed to execute "docker ps"', { error: error.message, stderr: error.stderr }); // Use console.error
|
||||
// 如果执行 docker ps 失败,可能意味着 Docker 服务出问题了
|
||||
this.isDockerAvailableCache = false; // 重置可用性缓存
|
||||
return { available: false, containers: [] };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 对指定的容器执行命令。
|
||||
* @param containerId 容器 ID
|
||||
* @param command 命令 ('start', 'stop', 'restart', 'remove')
|
||||
*/
|
||||
async executeContainerCommand(containerId: string, command: DockerCommand): Promise<void> {
|
||||
const available = await this.checkDockerAvailability();
|
||||
if (!available) {
|
||||
throw new Error('Docker is not available.');
|
||||
}
|
||||
|
||||
// 参数校验和清理,防止命令注入
|
||||
const cleanContainerId = containerId.replace(/[^a-zA-Z0-9_-]/g, '');
|
||||
if (!cleanContainerId) {
|
||||
throw new Error('Invalid container ID format.');
|
||||
}
|
||||
|
||||
let dockerCliCommand: string;
|
||||
switch (command) {
|
||||
case 'start':
|
||||
dockerCliCommand = `docker start ${cleanContainerId}`;
|
||||
break;
|
||||
case 'stop':
|
||||
dockerCliCommand = `docker stop ${cleanContainerId}`;
|
||||
break;
|
||||
case 'restart':
|
||||
dockerCliCommand = `docker restart ${cleanContainerId}`;
|
||||
break;
|
||||
case 'remove':
|
||||
// 使用 -f 强制删除正在运行的容器,对应前端的 'down' 意图
|
||||
dockerCliCommand = `docker rm -f ${cleanContainerId}`;
|
||||
break;
|
||||
default:
|
||||
// 防止未知的命令类型
|
||||
console.error(`[DockerService] Received unknown command type: ${command}`); // Use console.error
|
||||
throw new Error(`Unsupported Docker command: ${command}`);
|
||||
}
|
||||
|
||||
console.log(`[DockerService] Executing command: ${dockerCliCommand}`); // Use console.log
|
||||
try {
|
||||
const { stdout, stderr } = await execAsync(dockerCliCommand, { timeout: this.commandTimeout });
|
||||
if (stderr) {
|
||||
// Docker 命令有时会将正常信息输出到 stderr (例如 rm 返回容器 ID)
|
||||
// 但也可能包含错误信息
|
||||
console.warn(`[DockerService] Command "${dockerCliCommand}" produced stderr:`, { stderr }); // Use console.warn
|
||||
// 可以根据 stderr 内容判断是否真的是错误
|
||||
if (stderr.toLowerCase().includes('error') || stderr.toLowerCase().includes('failed')) {
|
||||
throw new Error(`Docker command failed: ${stderr}`);
|
||||
}
|
||||
}
|
||||
console.log(`[DockerService] Command "${dockerCliCommand}" executed successfully.`, { stdout }); // Use console.log
|
||||
} catch (error: any) {
|
||||
console.error(`[DockerService] Failed to execute command "${dockerCliCommand}"`, { error: error.message, stderr: error.stderr }); // Use console.error
|
||||
// 抛出错误,让 Controller 层处理并返回给前端
|
||||
throw new Error(`Failed to execute Docker command "${command}": ${error.stderr || error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,77 @@ export interface ClientState { // 导出以便 Service 可以导入
|
||||
ipAddress?: string; // 添加 IP 地址字段
|
||||
}
|
||||
|
||||
// --- Interfaces (保持与前端一致) ---
|
||||
// --- FIX: Move PortInfo definition before its usage ---
|
||||
interface PortInfo {
|
||||
IP?: string;
|
||||
PrivatePort: number;
|
||||
PublicPort?: number;
|
||||
Type: 'tcp' | 'udp' | string;
|
||||
}
|
||||
// --- End FIX ---
|
||||
|
||||
// --- 新增:解析 Ports 字符串的辅助函数 ---
|
||||
function parsePortsString(portsString: string | undefined | null): PortInfo[] { // Now PortInfo is defined
|
||||
if (!portsString) {
|
||||
return [];
|
||||
}
|
||||
const ports: PortInfo[] = []; // Now PortInfo is defined
|
||||
// 示例格式: "0.0.0.0:8080->80/tcp, :::8080->80/tcp", "127.0.0.1:5432->5432/tcp", "6379/tcp"
|
||||
const entries = portsString.split(', ');
|
||||
|
||||
for (const entry of entries) {
|
||||
const parts = entry.split('->');
|
||||
let publicPart = '';
|
||||
let privatePart = '';
|
||||
|
||||
if (parts.length === 2) { // Format like "IP:PublicPort->PrivatePort/Type" or "PublicPort->PrivatePort/Type"
|
||||
publicPart = parts[0];
|
||||
privatePart = parts[1];
|
||||
} else if (parts.length === 1) { // Format like "PrivatePort/Type"
|
||||
privatePart = parts[0];
|
||||
} else {
|
||||
console.warn(`[WebSocket] Skipping unparsable port entry: ${entry}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse Private Part (e.g., "80/tcp")
|
||||
const privateMatch = privatePart.match(/^(\d+)\/(tcp|udp|\w+)$/);
|
||||
if (!privateMatch) {
|
||||
console.warn(`[WebSocket] Skipping unparsable private port part: ${privatePart}`);
|
||||
continue;
|
||||
}
|
||||
const privatePort = parseInt(privateMatch[1], 10);
|
||||
const type = privateMatch[2];
|
||||
|
||||
let ip: string | undefined = undefined;
|
||||
let publicPort: number | undefined = undefined;
|
||||
|
||||
// Parse Public Part (e.g., "0.0.0.0:8080" or ":::8080" or just "8080")
|
||||
if (publicPart) {
|
||||
const publicMatch = publicPart.match(/^(?:([\d.:a-fA-F]+):)?(\d+)$/); // Supports IPv4, IPv6, or just port
|
||||
if (publicMatch) {
|
||||
ip = publicMatch[1] || undefined; // IP might be undefined if only port is specified
|
||||
publicPort = parseInt(publicMatch[2], 10);
|
||||
} else {
|
||||
console.warn(`[WebSocket] Skipping unparsable public port part: ${publicPart}`);
|
||||
// Continue processing with only private port info if public part is weird
|
||||
}
|
||||
}
|
||||
|
||||
if (!isNaN(privatePort)) {
|
||||
ports.push({
|
||||
IP: ip,
|
||||
PrivatePort: privatePort,
|
||||
PublicPort: publicPort,
|
||||
Type: type
|
||||
});
|
||||
}
|
||||
}
|
||||
return ports;
|
||||
}
|
||||
// --- 结束辅助函数 ---
|
||||
|
||||
// 存储所有活动客户端的状态 (key: sessionId)
|
||||
export const clientStates = new Map<string, ClientState>(); // Export clientStates
|
||||
|
||||
@@ -310,7 +381,191 @@ export const initializeWebSocket = async (server: http.Server, sessionParser: Re
|
||||
break;
|
||||
}
|
||||
|
||||
// --- SFTP 操作 (委托给 SftpService) ---
|
||||
// --- NEW: Handle Docker Status Request ---
|
||||
case 'docker:get_status': {
|
||||
if (!state || !state.sshClient) {
|
||||
console.warn(`WebSocket: 收到来自 ${ws.username} (会话: ${sessionId}) 的 ${type} 请求,但无活动 SSH 连接。`);
|
||||
ws.send(JSON.stringify({ type: 'docker:status:error', payload: { message: 'SSH connection not active.' } }));
|
||||
return;
|
||||
}
|
||||
console.log(`WebSocket: 处理来自 ${ws.username} (会话: ${sessionId}) 的 ${type} 请求...`);
|
||||
try {
|
||||
// Execute docker ps command remotely
|
||||
const command = "docker ps -a --no-trunc --format '{{json .}}'";
|
||||
const execResult = await new Promise<{ stdout: string; stderr: string }>((resolve, reject) => {
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
state.sshClient.exec(command, { pty: false }, (err, stream) => { // pty: false might be better for non-interactive commands
|
||||
if (err) return reject(err);
|
||||
stream.on('data', (data: Buffer) => { stdout += data.toString(); });
|
||||
stream.stderr.on('data', (data: Buffer) => { stderr += data.toString(); });
|
||||
stream.on('close', (code: number | null, signal: string | null) => {
|
||||
if (code === 0) {
|
||||
resolve({ stdout, stderr });
|
||||
} else {
|
||||
// Check if stderr indicates docker not found or cannot connect
|
||||
if (stderr.includes('command not found') || stderr.includes('Cannot connect to the Docker daemon')) {
|
||||
console.warn(`WebSocket: 远程 Docker 命令 (${command}) 执行失败 (可能未安装或未运行) on session ${sessionId}. Stderr: ${stderr}`);
|
||||
// Send specific 'unavailable' status back
|
||||
ws.send(JSON.stringify({ type: 'docker:status:update', payload: { available: false, containers: [] } }));
|
||||
// Resolve normally here as we handled the 'unavailable' case
|
||||
resolve({ stdout: '', stderr });
|
||||
} else {
|
||||
reject(new Error(`Command failed with code ${code}. Stderr: ${stderr}`));
|
||||
}
|
||||
}
|
||||
});
|
||||
// Add type annotation for execErr
|
||||
stream.on('error', (execErr: Error) => reject(execErr));
|
||||
});
|
||||
});
|
||||
|
||||
// If stdout is empty, it means the command failed in a way we handled (like docker unavailable)
|
||||
if (!execResult.stdout.trim()) {
|
||||
// Response already sent if docker was unavailable
|
||||
if (!execResult.stderr.includes('command not found') && !execResult.stderr.includes('Cannot connect to the Docker daemon')) {
|
||||
console.warn(`WebSocket: Docker ps command for session ${sessionId} produced no output, but no specific error detected. Assuming available but no containers.`);
|
||||
ws.send(JSON.stringify({ type: 'docker:status:update', payload: { available: true, containers: [] } }));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Parse the multi-line JSON output
|
||||
const lines = execResult.stdout.trim().split('\n');
|
||||
const containers = lines.map(line => {
|
||||
try {
|
||||
const data = JSON.parse(line);
|
||||
// --- FIX: Use parsePortsString ---
|
||||
const containerData = {
|
||||
id: data.ID, // Assume original field is uppercase ID
|
||||
Names: typeof data.Names === 'string' ? data.Names.split(',') : (data.Names || []),
|
||||
Image: data.Image || '',
|
||||
ImageID: data.ImageID || '',
|
||||
Command: data.Command || '',
|
||||
Created: data.CreatedAt || 0, // Check if CreatedAt exists
|
||||
State: data.State || 'unknown',
|
||||
Status: data.Status || '',
|
||||
Ports: parsePortsString(data.Ports), // <--- Use the parser here
|
||||
Labels: data.Labels || {}
|
||||
// Add other fields as needed, mapping from data.*
|
||||
};
|
||||
// --- End FIX ---
|
||||
|
||||
// --- Add Log to verify parsed container ---
|
||||
// console.log(`Parsed Container Data (Session: ${sessionId}):`, containerData);
|
||||
// --- End Log ---
|
||||
|
||||
return containerData;
|
||||
} catch (parseError) {
|
||||
console.error(`WebSocket: Failed to parse remote docker ps JSON line for session ${sessionId}: ${line}`, parseError);
|
||||
return null;
|
||||
}
|
||||
}).filter(Boolean); // Filter out nulls from parse errors
|
||||
|
||||
// --- Add Log to verify final containers array ---
|
||||
// console.log(`Final Containers Array to Send (Session: ${sessionId}):`, containers);
|
||||
// --- End Log ---
|
||||
|
||||
ws.send(JSON.stringify({ type: 'docker:status:update', payload: { available: true, containers } }));
|
||||
|
||||
} catch (error: any) {
|
||||
console.error(`WebSocket: 执行远程 Docker 状态命令失败 for session ${sessionId}:`, error);
|
||||
// Check if error indicates docker not found or cannot connect
|
||||
const errorMessage = error.message || '';
|
||||
if (errorMessage.includes('command not found') || errorMessage.includes('Cannot connect to the Docker daemon')) {
|
||||
ws.send(JSON.stringify({ type: 'docker:status:update', payload: { available: false, containers: [] } }));
|
||||
} else {
|
||||
ws.send(JSON.stringify({ type: 'docker:status:error', payload: { message: `Failed to get remote Docker status: ${errorMessage}` } }));
|
||||
}
|
||||
}
|
||||
break;
|
||||
} // end case 'docker:get_status'
|
||||
|
||||
// --- NEW: Handle Docker Command Execution ---
|
||||
case 'docker:command': {
|
||||
if (!state || !state.sshClient) {
|
||||
console.warn(`WebSocket: 收到来自 ${ws.username} (会话: ${sessionId}) 的 ${type} 请求,但无活动 SSH 连接。`);
|
||||
ws.send(JSON.stringify({ type: 'docker:command:error', payload: { command: payload?.command, message: 'SSH connection not active.' } }));
|
||||
return;
|
||||
}
|
||||
const { containerId, command } = payload || {};
|
||||
if (!containerId || typeof containerId !== 'string' || !command || !['start', 'stop', 'restart', 'remove'].includes(command)) {
|
||||
console.error(`WebSocket: 收到来自 ${ws.username} (会话: ${sessionId}) 的无效 ${type} 请求。Payload:`, payload);
|
||||
ws.send(JSON.stringify({ type: 'docker:command:error', payload: { command: command, message: 'Invalid containerId or command.' } }));
|
||||
return;
|
||||
}
|
||||
|
||||
// --- 添加日志 ---
|
||||
console.log(`WebSocket: Received docker:command. Raw Payload:`, payload);
|
||||
console.log(`WebSocket: Validating containerId: "${containerId}" (Type: ${typeof containerId}), Command: "${command}" (Type: ${typeof command})`);
|
||||
// --- 结束日志 ---
|
||||
|
||||
// 验证逻辑:
|
||||
if (!containerId || typeof containerId !== 'string' || !command || !['start', 'stop', 'restart', 'remove'].includes(command)) {
|
||||
console.error(`WebSocket: Validation FAILED for docker:command. Payload:`, payload); // 增加失败日志
|
||||
ws.send(JSON.stringify({ type: 'docker:command:error', payload: { command: command, message: 'Invalid containerId or command.' } }));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`WebSocket: Validation PASSED for docker:command.`); // 增加成功日志
|
||||
console.log(`WebSocket: Processing command '${command}' for container '${containerId}' on session ${sessionId}...`);
|
||||
try {
|
||||
// Sanitize containerId (basic) - more robust validation might be needed
|
||||
const cleanContainerId = containerId.replace(/[^a-zA-Z0-9_-]/g, '');
|
||||
if (!cleanContainerId) throw new Error('Invalid container ID format after sanitization.');
|
||||
|
||||
let dockerCliCommand: string;
|
||||
switch (command) {
|
||||
case 'start': dockerCliCommand = `docker start ${cleanContainerId}`; break;
|
||||
case 'stop': dockerCliCommand = `docker stop ${cleanContainerId}`; break;
|
||||
case 'restart': dockerCliCommand = `docker restart ${cleanContainerId}`; break;
|
||||
case 'remove': dockerCliCommand = `docker rm -f ${cleanContainerId}`; break; // Use -f for remove
|
||||
default: throw new Error(`Unsupported command: ${command}`); // Should be caught by earlier validation
|
||||
}
|
||||
|
||||
// Execute command remotely
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
state.sshClient.exec(dockerCliCommand, { pty: false }, (err, stream) => {
|
||||
if (err) return reject(err);
|
||||
let stderr = '';
|
||||
stream.stderr.on('data', (data: Buffer) => { stderr += data.toString(); });
|
||||
stream.on('close', (code: number | null) => {
|
||||
if (code === 0) {
|
||||
console.log(`WebSocket: 远程 Docker 命令 (${dockerCliCommand}) on session ${sessionId} 执行成功。`);
|
||||
resolve();
|
||||
} else {
|
||||
console.error(`WebSocket: 远程 Docker 命令 (${dockerCliCommand}) on session ${sessionId} 执行失败 (Code: ${code}). Stderr: ${stderr}`);
|
||||
reject(new Error(`Command failed with code ${code}. ${stderr || 'No stderr output.'}`));
|
||||
}
|
||||
});
|
||||
// Add type annotation for execErr
|
||||
stream.on('error', (execErr: Error) => reject(execErr));
|
||||
});
|
||||
});
|
||||
// Optionally send a success confirmation back? Not strictly needed if status updates quickly.
|
||||
// ws.send(JSON.stringify({ type: 'docker:command:success', payload: { command, containerId } }));
|
||||
|
||||
// Trigger a status update after command execution
|
||||
// Use a small delay to allow Docker daemon to potentially update state
|
||||
setTimeout(() => {
|
||||
if (clientStates.has(sessionId!)) { // Check if session still exists
|
||||
ws.send(JSON.stringify({ type: 'request_docker_status_update' })); // Ask frontend to re-request
|
||||
// Or directly trigger backend fetch and push:
|
||||
// handleDockerGetStatus(ws, state); // Need to refactor get_status logic into a reusable function
|
||||
}
|
||||
}, 500);
|
||||
|
||||
|
||||
} catch (error: any) {
|
||||
console.error(`WebSocket: 执行远程 Docker 命令 (${command} for ${containerId}) 失败 for session ${sessionId}:`, error);
|
||||
ws.send(JSON.stringify({ type: 'docker:command:error', payload: { command, containerId, message: `Failed to execute remote command: ${error.message}` } }));
|
||||
}
|
||||
break;
|
||||
} // end case 'docker:command'
|
||||
|
||||
|
||||
// --- SFTP Cases ---
|
||||
case 'sftp:readdir':
|
||||
case 'sftp:stat':
|
||||
case 'sftp:readfile':
|
||||
|
||||
Reference in New Issue
Block a user