diff --git a/web-api/src/main/java/com/jeesite/modules/utils/DockerUtil.java b/web-api/src/main/java/com/jeesite/modules/utils/DockerUtil.java index 0d6993b..4dcf31a 100644 --- a/web-api/src/main/java/com/jeesite/modules/utils/DockerUtil.java +++ b/web-api/src/main/java/com/jeesite/modules/utils/DockerUtil.java @@ -1,6 +1,8 @@ package com.jeesite.modules.utils; -import com.jcraft.jsch.*; +import com.jcraft.jsch.ChannelExec; +import com.jcraft.jsch.JSch; +import com.jcraft.jsch.Session; import com.jeesite.modules.apps.Module.ContainerInfo; import com.jeesite.modules.apps.Module.DockerResult; import com.jeesite.modules.apps.Module.SystemInfo; @@ -14,135 +16,104 @@ 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 final int SSH_TIMEOUT = 10000; + private static final Map sessionCache = new ConcurrentHashMap<>(); - /** - * 连接池:accountId -> Session - */ - private static final Map sessionPool = new ConcurrentHashMap<>(); - - /** - * 被池化的 Session,包含创建时间和活跃标记 - */ - private static class PooledSession { - Session session; - long createdAt; - volatile boolean inUse; // 防止并发误删 - - PooledSession(Session session) { - this.session = session; - this.createdAt = System.currentTimeMillis(); - this.inUse = false; - } - } - - /** - * 获取或创建 Session(线程安全,复用已有连接) - */ - private static PooledSession getSession(MySftpAccounts account) throws JSchException { + private static Session getSession(MySftpAccounts account) throws Exception { 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 { - return pooled; - } + + // 1. 从缓存取 + Session session = sessionCache.get(key); + if (session != null && session.isConnected()) { + return session; } - // 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; - }); + // 2. 无效则移除旧的 + if (session != null) { + session.disconnect(); + sessionCache.remove(key); } - // 3. 新建连接 + // 3. 创建新连接 JSch jsch = new JSch(); int port = account.getHostPort() == null ? 22 : account.getHostPort(); - Session session = jsch.getSession(account.getUsername(), account.getHostIp(), port); + 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); + byte[] prvKey = account.getPrivateKey().getBytes(StandardCharsets.UTF_8); + jsch.addIdentity("id_" + key, prvKey, null, null); } else { session.setPassword(account.getPassword()); } - Hashtable config = new Hashtable<>(); + Properties config = new Properties(); config.put("StrictHostKeyChecking", "no"); session.setConfig(config); session.connect(SSH_TIMEOUT); - pooled = new PooledSession(session); - sessionPool.put(key, pooled); - return pooled; + sessionCache.put(key, session); + return session; } - /** - * 执行单条命令(通过池化 Session) - */ + // ====================== 执行命令(最稳定)====================== private static String runCommand(MySftpAccounts account, String cmd) { - PooledSession pooled = null; + Session session = null; ChannelExec channel = null; + InputStream in = null; + ByteArrayOutputStream out = new ByteArrayOutputStream(); + try { - pooled = getSession(account); - pooled.inUse = true; + session = getSession(account); + + // 拼接命令 + String command = cmd; + if (StringUtils.isNotBlank(account.getRootPath())) { + command = "cd " + account.getRootPath() + " && " + cmd; + } - 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.setErrStream(System.err, true); // 把错误流也读出来 + in = channel.getInputStream(); 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(); - } + // 执行失败,清理坏连接 + if (session != null) { + session.disconnect(); + sessionCache.remove(account.getAccountId()); } return null; } finally { - if (channel != null) channel.disconnect(); - if (pooled != null) pooled.inUse = false; + try { + if (in != null) in.close(); + if (channel != null) channel.disconnect(); + out.close(); + } catch (Exception ignored) {} } } - /** - * 列出容器(一次 SSH 连接获取所有信息) - */ public static List listContainers(MySftpAccounts accounts, boolean all) { 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 Collections.emptyList(); } + return Arrays.stream(output.split("\\R")) .filter(StringUtils::isNotBlank) .map(line -> { @@ -163,17 +134,17 @@ public class DockerUtil { public static DockerResult start(MySftpAccounts accounts, String containerId) { String res = runCommand(accounts, "docker start " + containerId); - return res != null ? DockerResult.ok(res) : DockerResult.fail("执行失败"); + return res != null ? DockerResult.ok(res) : DockerResult.fail("启动失败"); } public static DockerResult stop(MySftpAccounts accounts, String containerId) { String res = runCommand(accounts, "docker stop " + containerId); - return res != null ? DockerResult.ok(res) : DockerResult.fail("执行失败"); + return res != null ? DockerResult.ok(res) : DockerResult.fail("停止失败"); } public static DockerResult restart(MySftpAccounts accounts, String containerId) { String res = runCommand(accounts, "docker restart " + containerId); - return res != null ? DockerResult.ok(res) : DockerResult.fail("执行失败"); + return res != null ? DockerResult.ok(res) : DockerResult.fail("重启失败"); } public static DockerResult getLogs(MySftpAccounts accounts, String containerId, int tail, boolean timestamps) { @@ -184,64 +155,35 @@ public class DockerUtil { 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("获取列表失败"); + return res != null ? DockerResult.ok(res) : DockerResult.fail("获取容器列表失败"); } public static DockerResult inspect(MySftpAccounts accounts, String containerId) { String res = runCommand(accounts, "docker inspect " + containerId); return res != null ? DockerResult.ok(res) : DockerResult.fail("查询详情失败"); } - - /** - * 系统状态:CPU + 内存 + 磁盘,三项一次 SSH 连接搞定 - */ public static SystemInfo systemInfo(MySftpAccounts accounts) { - 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; - } + String output = runCommand(accounts, + "top -bn1 | grep 'Cpu(s)' | awk '{printf \"%.1f\", 100-$8}'; echo; " + + "free | grep Mem | awk '{printf \"%.1f\", $3/$2*100}'; echo; " + + "df -h / | grep / | awk '{gsub(/%/,\"\"); print $5}'" + ); - 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()); - } + if (output == null) return info; + String[] lines = output.split("\\R"); + if (lines.length >= 1) info.setCpu(lines[0].trim()); + if (lines.length >= 2) info.setMemory(lines[1].trim()); + if (lines.length >= 3) info.setDisk(lines[2].trim()); return info; } - - /** - * 关闭指定账号的连接(账号删除或更新密码时调用) - */ public static void closeSession(String accountId) { - PooledSession pooled = sessionPool.remove(accountId); - if (pooled != null && pooled.session.isConnected()) { - pooled.session.disconnect(); - } + Session s = sessionCache.remove(accountId); + if (s != null && s.isConnected()) s.disconnect(); } - /** - * 清空所有连接池 - */ public static void closeAll() { - sessionPool.values().forEach(p -> { - if (p.session.isConnected()) p.session.disconnect(); - }); - sessionPool.clear(); + sessionCache.values().forEach(s -> { if (s.isConnected()) s.disconnect(); }); + sessionCache.clear(); } - - /** - * 获取当前池大小(调试用) - */ - public static int poolSize() { - return sessionPool.size(); - } -} +} \ No newline at end of file