This commit is contained in:
Baobhan Sith
2025-04-20 19:32:51 +08:00
parent 7940044512
commit 1ebf05940a
3 changed files with 503 additions and 323 deletions
+102 -56
View File
@@ -8,24 +8,37 @@ const execAsync = promisify(exec);
// --- Interfaces (与前端 DockerManager.vue 中的定义保持一致) ---
// 理想情况下,这些类型应该放在共享的 types 包中
interface PortInfo {
IP?: string;
PrivatePort: number;
PublicPort?: number;
Type: 'tcp' | 'udp' | string;
IP?: string;
PrivatePort: number;
PublicPort?: number;
Type: 'tcp' | 'udp' | string;
}
// 与前端一致的 Stats 接口
interface DockerStats {
ID: string; // Docker stats 返回的是 ID
Name: string;
CPUPerc: string;
MemUsage: string;
MemPerc: string;
NetIO: string;
BlockIO: string;
PIDs: 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 .}}'` 的输出添加其他需要的字段
Id: string; // docker ps 返回的是 Id
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>;
stats?: DockerStats | null; // 添加可选的 stats 字段
// 根据 `docker ps --format '{{json .}}'` 的输出添加其他需要的字段
}
// 定义命令类型
@@ -46,7 +59,7 @@ export class DockerService {
try {
// 尝试执行一个简单的 docker 命令,如 docker version
await execAsync('docker version', { timeout: 5000 }); // 5秒超时
await execAsync('docker version', { timeout: 2000 }); // 5秒超时
console.log('[DockerService] Docker is available.'); // Use console.log
this.isDockerAvailableCache = true;
return true;
@@ -61,51 +74,84 @@ export class DockerService {
* 获取所有 Docker 容器的状态 (包括已停止的)。
*/
async getContainerStatus(): Promise<{ available: boolean; containers: DockerContainer[] }> {
const available = await this.checkDockerAvailability();
if (!available) {
return { available: false, containers: [] };
}
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 });
let allContainers: DockerContainer[] = [];
const statsMap = new Map<string, DockerStats>();
// 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 不是数组,置为空数组
}
// 1. 获取所有容器的基本信息
try {
const { stdout: psStdout } = await execAsync("docker ps -a --no-trunc --format '{{json .}}'", { timeout: this.commandTimeout });
const lines = psStdout.trim().split('\n');
allContainers = lines
.map(line => {
try {
const data = JSON.parse(line);
if (typeof data.Names === 'string') {
data.Names = data.Names.split(',');
}
if (!Array.isArray(data.Ports)) {
data.Ports = [];
}
// 初始化 stats 为 null
data.stats = null;
return data as DockerContainer;
} catch (parseError) {
console.error(`[DockerService] Failed to parse container JSON line: ${line}`, { error: parseError });
return null;
}
})
.filter((container): container is DockerContainer => container !== null);
} catch (error: any) {
console.error('[DockerService] Failed to execute "docker ps"', { error: error.message, stderr: error.stderr });
this.isDockerAvailableCache = false;
return { available: false, containers: [] };
}
return data as DockerContainer;
} catch (parseError) {
console.error(`[DockerService] Failed to parse container JSON line: ${line}`, { error: parseError }); // Use console.error
return null;
// 2. 获取正在运行容器的统计信息
try {
// --no-stream 获取一次性快照
const { stdout: statsStdout } = await execAsync("docker stats --no-stream --format '{{json .}}'", { timeout: this.commandTimeout });
const statsLines = statsStdout.trim().split('\n');
statsLines.forEach(line => {
try {
const statsData = JSON.parse(line) as DockerStats;
// docker stats 返回的 ID 可能与 docker ps 的 Id 字段匹配
// 注意:docker stats 可能返回短 ID,而 docker ps -a --no-trunc 返回长 ID
// 实际应用中可能需要处理 ID 匹配问题,这里假设它们能直接匹配或通过 Name 匹配
// 为了简化,我们优先使用 ID 匹配
if (statsData.ID) {
// 尝试直接用 ID 作为 key (可能是短 ID)
// 如果 statsData.ID 是短 ID,而 allContainers 的 Id 是长 ID,这里可能匹配不上
// 一个更健壮的方法是先从 allContainers 构建一个 Name -> ID 的映射
// 但这里我们先简化处理,假设 ID 能匹配上
statsMap.set(statsData.ID, statsData);
// 也可以考虑用 Name 匹配作为备选
// if (statsData.Name) statsMap.set(statsData.Name, statsData);
}
} catch (parseError) {
console.error(`[DockerService] Failed to parse stats JSON line: ${line}`, { error: parseError });
}
});
} catch (error: any) {
// 获取 stats 失败不应阻止返回容器列表,只是 stats 会是 null
console.warn('[DockerService] Failed to execute "docker stats"', { error: error.message, stderr: error.stderr });
}
// 3. 合并统计信息到容器列表
allContainers.forEach(container => {
// 尝试用容器的长 ID 或短 ID (前12位) 或 Name 去匹配 statsMap
const shortId = container.Id.substring(0, 12);
const stats = statsMap.get(container.Id) || statsMap.get(shortId) || statsMap.get(container.Names[0]); // 尝试多种匹配方式
if (stats) {
container.stats = stats;
}
})
.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: [] };
}
return { available: true, containers: allContainers };
}
/**