Compare commits

...

2 Commits

Author SHA1 Message Date
3a79513e95 增加主机信息功能 2026-04-13 22:49:09 +08:00
4103f2d7ac refactor(DockerUtil): 新增SSH连接池,复用Session 2026-04-13 22:35:04 +08:00
2 changed files with 183 additions and 73 deletions

View File

@@ -8,14 +8,28 @@ import java.io.Serializable;
* Docker 容器信息
*/
@Data
public class ContainerInfo implements Serializable {
public class ContainerInfo implements Serializable {
private String containerId;
private String image;
private String command;
private String status;
private String created;
private String names;
private String status;
private String ports;
private String names;
private String accountId;
public ContainerInfo() {
}
public ContainerInfo(String containerId, String image, String command, String created, String status, String ports, String names, String accountId) {
this.containerId = containerId;
this.image = image;
this.command = command;
this.created = created;
this.status = status;
this.ports = ports;
this.names = names;
this.accountId = accountId;
}
}

View File

@@ -5,90 +5,160 @@ import com.jeesite.modules.apps.Module.ContainerInfo;
import com.jeesite.modules.apps.Module.DockerResult;
import com.jeesite.modules.apps.Module.SystemInfo;
import com.jeesite.modules.biz.entity.MySftpAccounts;
import io.micrometer.common.util.StringUtils;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
/**
* Docker 工具类(连接池版本)
*/
public class DockerUtil {
private static final int SSH_TIMEOUT = 3000;
private static final int MAX_POOL_SIZE = 50;
private static String runCommand(MySftpAccounts account, String cmd) {
JSch jsch = new JSch();
Session session = null;
ChannelExec channel = null;
/**
* 连接池accountId -> Session
*/
private static final Map<String, PooledSession> sessionPool = new ConcurrentHashMap<>();
try {
int port = account.getHostPort() == null ? 22 : account.getHostPort();
session = jsch.getSession(account.getUsername(), account.getHostIp(), port);
session.setTimeout(SSH_TIMEOUT);
/**
* 被池化的 Session包含创建时间和活跃标记
*/
private static class PooledSession {
Session session;
long createdAt;
volatile boolean inUse; // 防止并发误删
// 认证
if ("key".equalsIgnoreCase(account.getAuthType()) && account.getPrivateKey() != null) {
jsch.addIdentity("temp", account.getPrivateKey().getBytes(StandardCharsets.UTF_8), null, null);
PooledSession(Session session) {
this.session = session;
this.createdAt = System.currentTimeMillis();
this.inUse = false;
}
}
/**
* 获取或创建 Session线程安全复用已有连接
*/
private static PooledSession getSession(MySftpAccounts account) throws JSchException {
String key = account.getAccountId();
PooledSession pooled = sessionPool.get(key);
if (pooled != null && !pooled.inUse && pooled.session.isConnected()) {
if (System.currentTimeMillis() - pooled.createdAt > 30 * 60 * 1000) {
pooled.session.disconnect();
pooled.session = null;
} else {
session.setPassword(account.getPassword());
return pooled;
}
}
Hashtable<String, String> config = new Hashtable<>();
config.put("StrictHostKeyChecking", "no");
session.setConfig(config);
session.connect(SSH_TIMEOUT);
// 2. 超过容量上限,先清理一个过期连接
if (sessionPool.size() >= MAX_POOL_SIZE) {
sessionPool.entrySet().removeIf(e -> {
if (System.currentTimeMillis() - e.getValue().createdAt > 20 * 60 * 1000) {
e.getValue().session.disconnect();
return true;
}
return false;
});
}
// 执行命令
// 3. 新建连接
JSch jsch = new JSch();
int port = account.getHostPort() == null ? 22 : account.getHostPort();
Session session = jsch.getSession(account.getUsername(), account.getHostIp(), port);
session.setTimeout(SSH_TIMEOUT);
if ("key".equalsIgnoreCase(account.getAuthType()) && account.getPrivateKey() != null) {
jsch.addIdentity("key_" + key,
account.getPrivateKey().getBytes(StandardCharsets.UTF_8), null, null);
} else {
session.setPassword(account.getPassword());
}
Hashtable<String, String> config = new Hashtable<>();
config.put("StrictHostKeyChecking", "no");
session.setConfig(config);
session.connect(SSH_TIMEOUT);
pooled = new PooledSession(session);
sessionPool.put(key, pooled);
return pooled;
}
/**
* 执行单条命令(通过池化 Session
*/
private static String runCommand(MySftpAccounts account, String cmd) {
PooledSession pooled = null;
ChannelExec channel = null;
try {
pooled = getSession(account);
pooled.inUse = true;
Session session = pooled.session;
channel = (ChannelExec) session.openChannel("exec");
String command = account.getRootPath() != null && !account.getRootPath().isEmpty()
? "cd " + account.getRootPath() + " && " + cmd
: cmd;
channel.setCommand(command);
InputStream in = channel.getInputStream();
channel.connect();
channel.connect(SSH_TIMEOUT);
// 读取输出
ByteArrayOutputStream out = new ByteArrayOutputStream();
byte[] buf = new byte[4096];
int len;
while ((len = in.read(buf)) != -1) {
out.write(buf, 0, len);
}
return out.toString(StandardCharsets.UTF_8).trim();
} catch (Exception e) {
if (pooled != null) {
sessionPool.remove(account.getAccountId());
if (pooled.session.isConnected()) {
pooled.session.disconnect();
}
}
return null;
} finally {
if (channel != null) channel.disconnect();
if (session != null) session.disconnect();
if (pooled != null) pooled.inUse = false;
}
}
/**
* 列出容器(一次 SSH 连接获取所有信息)
*/
public static List<ContainerInfo> listContainers(MySftpAccounts accounts, boolean all) {
List<ContainerInfo> list = new ArrayList<>();
String cmd = (all ? "docker ps -a" : "docker ps")
+ " --format \"{{.ID}}|{{.Image}}|{{.Command}}|{{.CreatedAt}}|{{.Status}}|{{.Ports}}|{{.Names}}\"";
String FORMAT = "{{.ID}}|{{.Image}}|{{.Command}}|{{.CreatedAt}}|{{.Status}}|{{.Ports}}|{{.Names}}";
String cmd = (all ? "docker ps -a" : "docker ps") + " --format \"" + FORMAT + "\"";
String output = runCommand(accounts, cmd);
if (output == null || output.isBlank()) return list;
for (String line : output.split("\\R")) {
if (line.isBlank()) continue;
String[] arr = line.split("\\|");
ContainerInfo info = new ContainerInfo();
info.setContainerId(arr.length > 0 ? arr[0].trim() : "");
info.setImage(arr.length > 1 ? arr[1].trim() : "");
info.setCommand(arr.length > 2 ? arr[2].trim() : "");
info.setCreated(arr.length > 3 ? arr[3].trim() : "");
info.setStatus(arr.length > 4 ? arr[4].trim() : "");
info.setPorts(arr.length > 5 ? arr[5].trim() : "");
info.setNames(arr.length > 6 ? arr[6].trim() : "");
info.setAccountId(accounts.getAccountId());
list.add(info);
if (output == null || output.isBlank()) {
return Collections.emptyList();
}
return list;
return Arrays.stream(output.split("\\R"))
.filter(StringUtils::isNotBlank)
.map(line -> {
String[] arr = line.split("\\|", 8);
return new ContainerInfo(
arr.length > 0 ? arr[0].trim() : "",
arr.length > 1 ? arr[1].trim() : "",
arr.length > 2 ? arr[2].trim() : "",
arr.length > 3 ? arr[3].trim() : "",
arr.length > 4 ? arr[4].trim() : "",
arr.length > 5 ? arr[5].trim() : "",
arr.length > 6 ? arr[6].trim() : "",
accounts.getAccountId()
);
})
.collect(Collectors.toList());
}
public static DockerResult start(MySftpAccounts accounts, String containerId) {
@@ -112,7 +182,7 @@ public class DockerUtil {
return res != null ? DockerResult.ok(res) : DockerResult.fail("获取日志失败");
}
public static DockerResult list(MySftpAccounts accounts, boolean all) {
public static DockerResult listRaw(MySftpAccounts accounts, boolean all) {
String res = runCommand(accounts, all ? "docker ps -a" : "docker ps");
return res != null ? DockerResult.ok(res) : DockerResult.fail("获取列表失败");
}
@@ -122,30 +192,56 @@ public class DockerUtil {
return res != null ? DockerResult.ok(res) : DockerResult.fail("查询详情失败");
}
// 获取 CPU 使用率
public static String getCpuUsage(MySftpAccounts accounts) {
// 1秒采样输出纯数字百分比
return runCommand(accounts,
"top -bn1 | grep 'Cpu(s)' | sed -n '1p' | awk '{printf \"%.1f\", 100 - $8}'");
}
// 获取内存使用率
public static String getMemoryUsage(MySftpAccounts accounts) {
return runCommand(accounts,
"free | grep Mem | awk '{printf \"%.1f\", $3/$2*100}'");
}
// 获取磁盘使用率
public static String getDiskUsage(MySftpAccounts accounts) {
return runCommand(accounts,
"df -h / | grep / | awk '{gsub(/%/,\"\"); print $5}'");
}
/**
* 系统状态CPU + 内存 + 磁盘,三项一次 SSH 连接搞定
*/
public static SystemInfo systemInfo(MySftpAccounts accounts) {
SystemInfo systemInfo = new SystemInfo();
systemInfo.setCpu(getCpuUsage(accounts));
systemInfo.setMemory(getMemoryUsage(accounts));
systemInfo.setDisk(getDiskUsage(accounts));
return systemInfo;
String cmd =
"echo CPU:$(top -bn1 2>/dev/null | grep 'Cpu(s)' | awk '{printf \"%.1f\", 100-$8}') && " +
"echo MEM:$(free 2>/dev/null | grep Mem | awk '{printf \"%.1f\", $3/$2*100}') && " +
"echo DISK:$(df -h / 2>/dev/null | grep / | awk '{gsub(/%/,\"\"); print $5}')";
String output = runCommand(accounts, cmd);
SystemInfo info = new SystemInfo();
if (output == null) {
info.setCpu("N/A");
info.setMemory("N/A");
info.setDisk("N/A");
return info;
}
for (String line : output.split("\\R")) {
if (line.startsWith("CPU:")) info.setCpu(line.substring(4).trim());
else if (line.startsWith("MEM:")) info.setMemory(line.substring(4).trim());
else if (line.startsWith("DISK:")) info.setDisk(line.substring(5).trim());
}
return info;
}
}
/**
* 关闭指定账号的连接(账号删除或更新密码时调用)
*/
public static void closeSession(String accountId) {
PooledSession pooled = sessionPool.remove(accountId);
if (pooled != null && pooled.session.isConnected()) {
pooled.session.disconnect();
}
}
/**
* 清空所有连接池
*/
public static void closeAll() {
sessionPool.values().forEach(p -> {
if (p.session.isConnected()) p.session.disconnect();
});
sessionPool.clear();
}
/**
* 获取当前池大小(调试用)
*/
public static int poolSize() {
return sessionPool.size();
}
}