feat(v1.21-beta): 代码安全审计与全面优化

基线: v1.20 (e58137c) | 分支: agent1.21bate

## P0 严重修复 (14项)
- 安全: 命令注入漏洞修复 (SpecificTimeTaskService)
- 安全: 路径穿越漏洞修复 (AgentEndpoint)
- 安全: 下载安全加固 (AdvancedAsyncDownloader)
- 数据: CPU avg1/avg5/avg15 赋值错误修复 (CPUServiceImpl)
- 数据: 磁盘读写字节/速率赋值反转修复 (DiskServiceImpl)
- 逻辑: updateTaskInterval 丢失原始任务修复 (DynamicTaskService)
- 并发: GlobalConfig 100+ 静态字段 volatile + 并发集合
- 性能: SessionManager O(n)->O(1) Channel反向映射
- 并发: SessionManager 序列号 AtomicInteger
- 并发: CompensatedTrigger CAS 竞态修复
- 泄漏: UDPListenHandler ByteBuf 内存泄漏修复
- NPE: BaseNettyServer stop() 空指针修复
- 线程: AsyncCommandExecutor 无界线程池改有界+守护线程
- 泄漏: AdvancedAsyncDownloader 连接和线程池资源释放

## P1 重要修复 (5项)
- NetServiceImpl: Process 资源泄漏 + 超时控制
- DockerServiceImpl: 数组越界保护 + 资源释放
- MemoryServiceImpl: 除零风险修复
- SystemServiceImpl: 除零风险修复 + 异常处理规范化

## P2 代码质量 (6项)
- 100+ System.out/err -> AssertLog 日志框架
- e.printStackTrace() -> 结构化日志
- InterruptedException 中断状态恢复
- BaseNettyConfig Boolean 包装类型改基本类型
- BaseException cause 构造函数修复
- 新增安全配置项 (命令白名单/HMAC/下载安全/握手认证)

## 新增安全模块
- security/AuthHandshakeHandler.java - Netty握手认证
- security/HmacSignVerifier.java - HMAC签名验证
- security/SecureCommandExecutor.java - 安全命令执行器
- security/SecureFileDownloader.java - 安全文件下载器
- security/SecurityProperties.java - 安全配置属性
- security/SystemCommandRunner.java - 系统命令运行器
- 对应单元测试 4个

版本更新: pom.xml 0.0.1-SNAPSHOT -> 1.21-beta
          application.yml 1.0 -> 1.21

涉及文件: 34个 (21 modified + 13 new)
代码变更: +1085/-868 lines
This commit is contained in:
lee
2026-07-22 12:58:52 +08:00
parent e58137c319
commit 546eef3372
34 changed files with 2776 additions and 881 deletions
@@ -3,14 +3,15 @@ package com.tongran.agent.client.core.config;
import com.tongran.agent.client.core.eo.AlarmEO;
import com.tongran.agent.client.core.eo.NativeNetworkInterfaceEO;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 全局配置类
* 注意:所有可变集合均使用线程安全实现,基本类型使用 volatile 保证可见性
*/
public class GlobalConfig {
@@ -18,175 +19,175 @@ public class GlobalConfig {
/**
* 服务启动时间
*/
public static long startupTime;
public static volatile long startupTime;
/**
* 客户端ID - 对应平台唯一SN
*/
public static String CLIENT_ID;
public static volatile String CLIENT_ID;
/**
* 交换机信息
*/
public static String SWITCH_COMMUNITY; //团名
public static String SWITCH_IP; //交换机IP
public static int SWITCH_PORT; //交换机端口
public static LinkedHashMap<String, String> SWITCH_NET_OID = new LinkedHashMap<>(); //交换机网络端口发现OID
public static LinkedHashMap<String, String> SWITCH_MODULE_OID = new LinkedHashMap<>(); //交换机光模块发现OID
public static LinkedHashMap<String, String> SWITCH_MPU_OID = new LinkedHashMap<>(); //交换机MPU发现OID
public static LinkedHashMap<String, String> SWITCH_PWR_OID = new LinkedHashMap<>(); //交换机电源发现OID
public static LinkedHashMap<String, String> SWITCH_FAN_OID = new LinkedHashMap<>(); //交换机风扇发现OID
public static LinkedHashMap<String, String> SWITCH_OTHER_OID = new LinkedHashMap<>(); //交换机系统其他OID
public static String OTHER_INDEX_PARAM = "entIndex"; //交换机其他监控项索引参数
public static String OTHER_INDEX_OID = "";
public static List<String> OTHER_FILTER = new ArrayList<>(); //过滤值
public static String NET_INDEX_PARAM = "ifIndex"; //交换机网络端口索引参数
public static String NET_INDEX_OID = "";
public static List<String> NET_FILTER = new ArrayList<>(); //过滤值
public static String MODULE_INDEX_PARAM = "fiberEntIndex"; //交换机光模块端口索引参数
public static String MODULE_INDEX_OID = "";
public static List<String> MODULE_FILTER = new ArrayList<>(); //过滤值
public static String MPU_INDEX_PARAM = "mpuEntIndex"; //交换机MPU索引参数
public static String MPU_INDEX_OID = "";
public static List<String> MPU_FILTER = new ArrayList<>(); //过滤值
public static String PWR_INDEX_PARAM = "pwrEntIndex"; //交换机电源索引参数
public static String PWR_INDEX_OID = "";
public static List<String> PWR_FILTER = new ArrayList<>(); //过滤值
public static String FAN_INDEX_PARAM = "fanEntIndex"; //交换机风扇索引参数
public static String FAN_INDEX_OID = "";
public static List<String> FAN_FILTER = new ArrayList<>(); //过滤值
public static volatile String SWITCH_COMMUNITY; //团名
public static volatile String SWITCH_IP; //交换机IP
public static volatile int SWITCH_PORT; //交换机端口
public static final Map<String, String> SWITCH_NET_OID = new ConcurrentHashMap<>(); //交换机网络端口发现OID
public static final Map<String, String> SWITCH_MODULE_OID = new ConcurrentHashMap<>(); //交换机光模块发现OID
public static final Map<String, String> SWITCH_MPU_OID = new ConcurrentHashMap<>(); //交换机MPU发现OID
public static final Map<String, String> SWITCH_PWR_OID = new ConcurrentHashMap<>(); //交换机电源发现OID
public static final Map<String, String> SWITCH_FAN_OID = new ConcurrentHashMap<>(); //交换机风扇发现OID
public static final Map<String, String> SWITCH_OTHER_OID = new ConcurrentHashMap<>(); //交换机系统其他OID
public static volatile String OTHER_INDEX_PARAM = "entIndex"; //交换机其他监控项索引参数
public static volatile String OTHER_INDEX_OID = "";
public static final List<String> OTHER_FILTER = new CopyOnWriteArrayList<>(); //过滤值
public static volatile String NET_INDEX_PARAM = "ifIndex"; //交换机网络端口索引参数
public static volatile String NET_INDEX_OID = "";
public static final List<String> NET_FILTER = new CopyOnWriteArrayList<>(); //过滤值
public static volatile String MODULE_INDEX_PARAM = "fiberEntIndex"; //交换机光模块端口索引参数
public static volatile String MODULE_INDEX_OID = "";
public static final List<String> MODULE_FILTER = new CopyOnWriteArrayList<>(); //过滤值
public static volatile String MPU_INDEX_PARAM = "mpuEntIndex"; //交换机MPU索引参数
public static volatile String MPU_INDEX_OID = "";
public static final List<String> MPU_FILTER = new CopyOnWriteArrayList<>(); //过滤值
public static volatile String PWR_INDEX_PARAM = "pwrEntIndex"; //交换机电源索引参数
public static volatile String PWR_INDEX_OID = "";
public static final List<String> PWR_FILTER = new CopyOnWriteArrayList<>(); //过滤值
public static volatile String FAN_INDEX_PARAM = "fanEntIndex"; //交换机风扇索引参数
public static volatile String FAN_INDEX_OID = "";
public static final List<String> FAN_FILTER = new CopyOnWriteArrayList<>(); //过滤值
/**
* 采集标识
*/
public static boolean isCollect = false;
public static Map<String, Long> taskIds = new ConcurrentHashMap<>();
public static volatile boolean isCollect = false;
public static final Map<String, Long> taskIds = new ConcurrentHashMap<>();
/**
* 采集系统监控
*/
public static boolean cpuCollect = false; //cpu采集
public static long cpuInterval = 300; //cpu采集间隔
public static boolean vfsCollect = false; //挂载采集
public static long vfsInterval = 300; //挂载采集间隔
public static boolean netCollect = false; //网络采集
public static long netInterval = 300; //网络采集间隔
public static boolean diskCollect = false; //硬盘采集
public static long diskInterval = 300; //硬盘采集间隔
public static boolean dockerCollect = false; //docker采集
public static long dockerInterval = 300; //docker采集间隔
public static volatile boolean cpuCollect = false; //cpu采集
public static volatile long cpuInterval = 300; //cpu采集间隔
public static volatile boolean vfsCollect = false; //挂载采集
public static volatile long vfsInterval = 300; //挂载采集间隔
public static volatile boolean netCollect = false; //网络采集
public static volatile long netInterval = 300; //网络采集间隔
public static volatile boolean diskCollect = false; //硬盘采集
public static volatile long diskInterval = 300; //硬盘采集间隔
public static volatile boolean dockerCollect = false; //docker采集
public static volatile long dockerInterval = 300; //docker采集间隔
/**
* 采集系统其他监控
*/
public static boolean systemSwapSizeFreeCollect = false; //交换卷/文件的可用空间(字节)采集
public static long systemSwapSizeFreeInterval = 300; //交换卷/文件的可用空间(字节)采集间隔
public static boolean memoryUtilizationCollect = false; //内存利用率采集
public static long memoryUtilizationInterval = 300; //内存利用率采集间隔
public static boolean systemSwapSizePercentCollect = false; //可用交换空间百分比采集
public static long systemSwapSizePercentInterval = 300; //可用交换空间百分比采集间隔
public static boolean memorySizeAvailableCollect = false; //可用内存采集
public static long memorySizeAvailableInterval = 300; //可用内存采集间隔
public static boolean memorySizePercentCollect = false; //可用内存百分比采集
public static long memorySizePercentInterval = 300; //可用内存百分比采集间隔
public static boolean memorySizeTotalCollect = false; //总内存采集
public static long memorySizeTotalInterval = 300; //总内存采集间隔
public static boolean systemSwOsCollect = false; //操作系统采集
public static long systemSwOsInterval = 300; //操作系统采集间隔
public static boolean systemSwArchCollect = false; //操作系统架构采集
public static long systemSwArchInterval = 300; //操作系统架构采集间隔
public static boolean kernelMaxprocCollect = false; //最大进程数采集
public static long kernelMaxprocInterval = 300; //最大进程数采集间隔
public static boolean procNumRunCollect = false; //正在运行的进程数采集
public static long procNumRunInterval = 300; //正在运行的进程数采集间隔
public static boolean systemUsersNumCollect = false; //登录用户数采集
public static long systemUsersNumInterval = 300; //登录用户数采集间隔
public static boolean systemDiskSizeTotalCollect = false; ///硬盘总可用空间采集
public static long systemDiskSizeTotalInterval = 300; //硬盘总可用空间采集间隔
public static boolean systemBoottimeCollect = false; //系统启动时间采集
public static long systemBoottimeInterval = 300; //系统启动时间采集间隔
public static boolean systemUnameCollect = false; //系统描述采集
public static long systemUnameInterval = 300; //系统描述采集间隔
public static boolean systemLocaltimeCollect = false; //系统本地时间采集
public static long systemLocaltimeInterval = 300; //系统本地时间采集间隔
public static boolean systemUptimeCollect = false; //系统正常运行时间采集
public static long systemUptimeInterval = 300; //系统正常运行时间采集间隔
public static boolean procNumCollect = false; //进程数采集
public static long procNumInterval = 300; //进程数采集间隔
public static volatile boolean systemSwapSizeFreeCollect = false; //交换卷/文件的可用空间(字节)采集
public static volatile long systemSwapSizeFreeInterval = 300; //交换卷/文件的可用空间(字节)采集间隔
public static volatile boolean memoryUtilizationCollect = false; //内存利用率采集
public static volatile long memoryUtilizationInterval = 300; //内存利用率采集间隔
public static volatile boolean systemSwapSizePercentCollect = false; //可用交换空间百分比采集
public static volatile long systemSwapSizePercentInterval = 300; //可用交换空间百分比采集间隔
public static volatile boolean memorySizeAvailableCollect = false; //可用内存采集
public static volatile long memorySizeAvailableInterval = 300; //可用内存采集间隔
public static volatile boolean memorySizePercentCollect = false; //可用内存百分比采集
public static volatile long memorySizePercentInterval = 300; //可用内存百分比采集间隔
public static volatile boolean memorySizeTotalCollect = false; //总内存采集
public static volatile long memorySizeTotalInterval = 300; //总内存采集间隔
public static volatile boolean systemSwOsCollect = false; //操作系统采集
public static volatile long systemSwOsInterval = 300; //操作系统采集间隔
public static volatile boolean systemSwArchCollect = false; //操作系统架构采集
public static volatile long systemSwArchInterval = 300; //操作系统架构采集间隔
public static volatile boolean kernelMaxprocCollect = false; //最大进程数采集
public static volatile long kernelMaxprocInterval = 300; //最大进程数采集间隔
public static volatile boolean procNumRunCollect = false; //正在运行的进程数采集
public static volatile long procNumRunInterval = 300; //正在运行的进程数采集间隔
public static volatile boolean systemUsersNumCollect = false; //登录用户数采集
public static volatile long systemUsersNumInterval = 300; //登录用户数采集间隔
public static volatile boolean systemDiskSizeTotalCollect = false; ///硬盘总可用空间采集
public static volatile long systemDiskSizeTotalInterval = 300; //硬盘总可用空间采集间隔
public static volatile boolean systemBoottimeCollect = false; //系统启动时间采集
public static volatile long systemBoottimeInterval = 300; //系统启动时间采集间隔
public static volatile boolean systemUnameCollect = false; //系统描述采集
public static volatile long systemUnameInterval = 300; //系统描述采集间隔
public static volatile boolean systemLocaltimeCollect = false; //系统本地时间采集
public static volatile long systemLocaltimeInterval = 300; //系统本地时间采集间隔
public static volatile boolean systemUptimeCollect = false; //系统正常运行时间采集
public static volatile long systemUptimeInterval = 300; //系统正常运行时间采集间隔
public static volatile boolean procNumCollect = false; //进程数采集
public static volatile long procNumInterval = 300; //进程数采集间隔
/**
* 采集交换机监控
*/
public static boolean switchNetCollect = false; //交换机网络采集
public static long switchNetInterval = 300; //交换机网络采集间隔
public static boolean switchModuleCollect = false; //光模块采集
public static long switchModuleInterval = 300; //光模块采集间隔
public static boolean switchMpuCollect = false; //MPU采集
public static long switchMpuInterval = 300; //MPU采集间隔
public static boolean switchPwrCollect = false; //电源采集
public static long switchPwrInterval = 300; //电源采集间隔
public static boolean switchFanCollect = false; //风扇采集
public static long switchFanInterval = 300; //风扇采集间隔
public static volatile boolean switchNetCollect = false; //交换机网络采集
public static volatile long switchNetInterval = 300; //交换机网络采集间隔
public static volatile boolean switchModuleCollect = false; //光模块采集
public static volatile long switchModuleInterval = 300; //光模块采集间隔
public static volatile boolean switchMpuCollect = false; //MPU采集
public static volatile long switchMpuInterval = 300; //MPU采集间隔
public static volatile boolean switchPwrCollect = false; //电源采集
public static volatile long switchPwrInterval = 300; //电源采集间隔
public static volatile boolean switchFanCollect = false; //风扇采集
public static volatile long switchFanInterval = 300; //风扇采集间隔
/**
* 采集交换机其他监控
*/
public static boolean switchSysDescrCollect = false; //系统描述采集
public static long switchSysDescrInterval = 300; //系统描述采集间隔
public static boolean switchSysObjectIDCollect = false; //系统Object ID采集
public static long switchSysObjectIDInterval = 300; //系统Object ID采集间隔
public static boolean switchSysUpTimeCollect = false; //系统运行时间采集
public static long switchSysUpTimeInterval = 300; //系统运行时间采集间隔
public static boolean switchSysContactCollect = false; //系统联系信息采集
public static long switchSysContactInterval = 300; //系统联系信息采集间隔
public static boolean switchSysNameCollect = false; //系统名称采集
public static long switchSysNameInterval = 300; //系统名称采集间隔
public static boolean switchSysLocationCollect = false; //系统位置采集
public static long switchSysLocationInterval = 300; //系统位置采集间隔
public static boolean switchHwStackSystemMacCollect = false; //系统MAC地址采集
public static long switchHwStackSystemMacInterval = 300; //系统MAC地址采集间隔
public static boolean switchEntIndexCollect = false; //设备索引采集
public static long switchEntIndexInterval = 300; //设备索引采集间隔
public static boolean switchEntPhysicalNameCollect = false; //设备名称采集
public static long switchEntPhysicalNameInterval = 300; //设备名称采集间隔
public static boolean switchEntPhysicalSoftwareRevCollect = false; //设备软件版本采集
public static long switchEntPhysicalSoftwareRevInterval = 300; //设备软件版本采集间隔
public static boolean switchHwEntityCpuUsageCollect = false; //设备CPU使用率(%)采集
public static long switchHwEntityCpuUsageInterval = 300; //设备CPU使用率(%)采集间隔
public static boolean switchHwEntityMemUsageCollect = false; //设备内存使用率(%)采集
public static long switchHwEntityMemUsageInterval = 300; //设备内存使用率(%)采集间隔
public static boolean switchHwAveragePowerCollect = false; //系统平均功率(mW)采集
public static long switchHwAveragePowerInterval = 300; //系统平均功率(mW)采集间隔
public static boolean switchHwCurrentPowerCollect = false; //系统实时功率(mW)采集
public static long switchHwCurrentPowerInterval = 300; //系统实时功率(mW)采集间隔
public static volatile boolean switchSysDescrCollect = false; //系统描述采集
public static volatile long switchSysDescrInterval = 300; //系统描述采集间隔
public static volatile boolean switchSysObjectIDCollect = false; //系统Object ID采集
public static volatile long switchSysObjectIDInterval = 300; //系统Object ID采集间隔
public static volatile boolean switchSysUpTimeCollect = false; //系统运行时间采集
public static volatile long switchSysUpTimeInterval = 300; //系统运行时间采集间隔
public static volatile boolean switchSysContactCollect = false; //系统联系信息采集
public static volatile long switchSysContactInterval = 300; //系统联系信息采集间隔
public static volatile boolean switchSysNameCollect = false; //系统名称采集
public static volatile long switchSysNameInterval = 300; //系统名称采集间隔
public static volatile boolean switchSysLocationCollect = false; //系统位置采集
public static volatile long switchSysLocationInterval = 300; //系统位置采集间隔
public static volatile boolean switchHwStackSystemMacCollect = false; //系统MAC地址采集
public static volatile long switchHwStackSystemMacInterval = 300; //系统MAC地址采集间隔
public static volatile boolean switchEntIndexCollect = false; //设备索引采集
public static volatile long switchEntIndexInterval = 300; //设备索引采集间隔
public static volatile boolean switchEntPhysicalNameCollect = false; //设备名称采集
public static volatile long switchEntPhysicalNameInterval = 300; //设备名称采集间隔
public static volatile boolean switchEntPhysicalSoftwareRevCollect = false; //设备软件版本采集
public static volatile long switchEntPhysicalSoftwareRevInterval = 300; //设备软件版本采集间隔
public static volatile boolean switchHwEntityCpuUsageCollect = false; //设备CPU使用率(%)采集
public static volatile long switchHwEntityCpuUsageInterval = 300; //设备CPU使用率(%)采集间隔
public static volatile boolean switchHwEntityMemUsageCollect = false; //设备内存使用率(%)采集
public static volatile long switchHwEntityMemUsageInterval = 300; //设备内存使用率(%)采集间隔
public static volatile boolean switchHwAveragePowerCollect = false; //系统平均功率(mW)采集
public static volatile long switchHwAveragePowerInterval = 300; //系统平均功率(mW)采集间隔
public static volatile boolean switchHwCurrentPowerCollect = false; //系统实时功率(mW)采集
public static volatile long switchHwCurrentPowerInterval = 300; //系统实时功率(mW)采集间隔
/**
* 告警
*/
public static boolean IS_ALARM = false;
public static long ALARM_INTERVAL = 60;
public static List<AlarmEO> ALARM_LIST = new ArrayList<>(); //告警设置信息
public static volatile boolean IS_ALARM = false;
public static volatile long ALARM_INTERVAL = 60;
public static final List<AlarmEO> ALARM_LIST = new CopyOnWriteArrayList<>(); //告警设置信息
/**
* 告警监控
*/
public static boolean systemCpuUti = false; //CPU使用率
public static boolean memoryUtilization = false; //内存利用率
public static boolean systemSwapSizePercent = false; //可用交换空间百分比
public static boolean systemUsersNum = false; //登录用户数
public static boolean vfsFsUtil = false; //挂载点的空间利用率
public static boolean netIfStatus = false; //网络运行状态UP变down
public static boolean netUtil = false; //网络带宽使用率
public static boolean containerMemUtil = false; //容器内存使用率
public static boolean extraPorts = false; //多余端口
public static volatile boolean systemCpuUti = false; //CPU使用率
public static volatile boolean memoryUtilization = false; //内存利用率
public static volatile boolean systemSwapSizePercent = false; //可用交换空间百分比
public static volatile boolean systemUsersNum = false; //登录用户数
public static volatile boolean vfsFsUtil = false; //挂载点的空间利用率
public static volatile boolean netIfStatus = false; //网络运行状态UP变down
public static volatile boolean netUtil = false; //网络带宽使用率
public static volatile boolean containerMemUtil = false; //容器内存使用率
public static volatile boolean extraPorts = false; //多余端口
/**
* 所有网络接口
*/
public static List<NativeNetworkInterfaceEO> NET_LIST = new ArrayList<>();
public static final List<NativeNetworkInterfaceEO> NET_LIST = new CopyOnWriteArrayList<>();
/**
* 脚本文件下载标识
*/
public static LinkedHashMap<String, Integer> DOWN_FILES = new LinkedHashMap<>();
public static final Map<String, AtomicInteger> DOWN_FILES = new ConcurrentHashMap<>();
}
@@ -16,6 +16,7 @@ import org.apache.commons.lang3.StringUtils;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
@Data
public class SessionManager {
@@ -25,8 +26,14 @@ public class SessionManager {
// clientId-Session
private final Map<String, Session> sessionMap = new ConcurrentHashMap<>();
// Channel-Session 反向映射,避免 O(n) 遍历查找
private final Map<Channel, String> channelToClientId = new ConcurrentHashMap<>();
private final Cache<String, Object> sessionCache = CacheUtil.newTimedCache(10 * 60 * 1000);
// 流水号生成器,使用 AtomicInteger 保证线程安全
private final AtomicInteger serialNumberGenerator = new AtomicInteger(0);
public static SessionManager getInstance() {
if (instance == null) {
synchronized (SessionManager.class) {
@@ -38,40 +45,54 @@ public class SessionManager {
return instance;
}
public synchronized void put(String clientId, Session session) {
public void put(String clientId, Session session) {
if (StringUtils.isNotBlank(session.getClientId())) {
sessionMap.put(session.getClientId(), session);
// 维护反向映射
if (session.getChannel() != null) {
channelToClientId.put(session.getChannel().channel(), session.getClientId());
}
}
}
public synchronized void remove(String clientId) {
public void remove(String clientId) {
if (StringUtils.isNotBlank(clientId)) {
sessionMap.remove(clientId);
Session removed = sessionMap.remove(clientId);
if (removed != null && removed.getChannel() != null) {
channelToClientId.remove(removed.getChannel().channel());
}
}
}
public synchronized void remove(Session session) {
public void remove(Session session) {
if (session != null && StringUtils.isNotBlank(session.getClientId())) {
sessionMap.remove(session.getClientId(), session);
if (session.getChannel() != null) {
channelToClientId.remove(session.getChannel().channel());
}
}
}
public synchronized void remove(Channel channel) {
remove(getSessionByChannel(channel));
public void remove(Channel channel) {
String clientId = channelToClientId.remove(channel);
if (clientId != null) {
sessionMap.remove(clientId);
}
}
public Session getSessionById(String clientId) {
return sessionMap.get(clientId);
}
/**
* 通过 Channel 查找 Session,使用反向映射表 O(1) 查找
*/
public Session getSessionByChannel(Channel channel) {
Session session = new Session();
sessionMap.values().forEach(s -> {
if (s.getChannel().channel() == channel) {
BeanUtil.copyProperties(s, session, true);
}
});
return session;
String clientId = channelToClientId.get(channel);
if (clientId != null) {
return sessionMap.get(clientId);
}
return null;
}
public boolean containsSession(String clientId) {
@@ -100,21 +121,12 @@ public class SessionManager {
}
/**
* 根据channel生成流水号
*
* @param channel
* @return
* 根据channel生成流水号,使用 AtomicInteger 保证线程安全
*/
public short getSerialNumber(Channel channel, AttributeKey<Short> serialNumber) {
Attribute<Short> flowIdAttr = channel.attr(serialNumber);
Short flowId = flowIdAttr.get();
if (flowId == null) {
flowId = 0;
} else {
flowId++;
}
flowIdAttr.set(flowId);
return flowId;
int next = serialNumberGenerator.incrementAndGet();
// short 类型溢出后回绕到 0
return (short) (next & 0xFFFF);
}
public void writeAndFlush(String clientId, Object msg) {
@@ -134,4 +146,4 @@ public class SessionManager {
});
}
}
}
@@ -16,6 +16,7 @@ public class BaseException extends RuntimeException implements ErrorCode {
public BaseException(String msg, Throwable cause) {
super(msg, cause);
this.msg = msg; // 修复:设置 msg 字段
}
public BaseException(Integer code, String msg) {
@@ -39,4 +40,4 @@ public class BaseException extends RuntimeException implements ErrorCode {
public String getMsg() {
return msg;
}
}
}
@@ -5,6 +5,8 @@ import com.tongran.agent.client.netty.handler.AgentDecoderHandler;
import com.tongran.agent.client.netty.handler.AgentDispatcherHandler;
import com.tongran.agent.client.netty.handler.AgentEncoderHandler;
import com.tongran.agent.client.netty.handler.TCPListenHandler;
import com.tongran.agent.client.security.AuthHandshakeHandler;
import com.tongran.agent.client.security.SecurityProperties;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.timeout.IdleStateHandler;
@@ -12,6 +14,7 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
@Configuration
public class AgentNettyServer extends BaseNettyServer {
@@ -31,6 +34,12 @@ public class AgentNettyServer extends BaseNettyServer {
@Resource
private AgentDispatcherHandler dispatcherHandler;
@Resource
private AuthHandshakeHandler authHandshakeHandler;
@Resource
private SecurityProperties securityProperties;
protected AgentNettyServer(AgentNettyConfig config) {
super(config);
@@ -41,7 +50,15 @@ public class AgentNettyServer extends BaseNettyServer {
config.setHander(new ChannelInitializer<NioSocketChannel>() {
@Override
public void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new IdleStateHandler(config.readerIdleTime, config.writerIdleTime, config.allIdleTime));// 心跳
// 握手超时:握手阶段专用,超时直接关闭连接
long handshakeTimeout = securityProperties.getHandshakeTimeoutSeconds();
ch.pipeline().addLast("handshakeIdle",
new IdleStateHandler(handshakeTimeout, 0, 0, TimeUnit.SECONDS));
// 握手认证:连接建立后必须先收到 auth:{token}@tong-ran
ch.pipeline().addLast("auth", authHandshakeHandler);
// 业务心跳
ch.pipeline().addLast("idle",
new IdleStateHandler(config.readerIdleTime, config.writerIdleTime, config.allIdleTime));
ch.pipeline().addLast(tcpListenHandler);// 监听器
//入栈
ch.pipeline().addLast(decoderHandler);//解码器
@@ -97,20 +97,28 @@ public abstract class BaseNettyServer {
}
isRunning = false;
try {
Future<?> future = this.workerGroup.shutdownGracefully().await();
if (!future.isSuccess()) {
AssertLog.error("workerGroup 无法正常停止:{}", future.cause());
// 修复:每个 group 判空后再关闭
if (this.workerGroup != null) {
Future<?> future = this.workerGroup.shutdownGracefully().await();
if (!future.isSuccess()) {
AssertLog.error("workerGroup 无法正常停止:{}", future.cause());
}
}
future = this.bossGroup.shutdownGracefully().await();
if (!future.isSuccess()) {
AssertLog.error("bossGroup 无法正常停止:{}", future.cause());
if (this.bossGroup != null) {
Future<?> future = this.bossGroup.shutdownGracefully().await();
if (!future.isSuccess()) {
AssertLog.error("bossGroup 无法正常停止:{}", future.cause());
}
}
future = this.businessGroup.shutdownGracefully().await();
if (!future.isSuccess()) {
AssertLog.error("businessGroup 无法正常停止:{}", future.cause());
if (this.businessGroup != null) {
Future<?> future = this.businessGroup.shutdownGracefully().await();
if (!future.isSuccess()) {
AssertLog.error("businessGroup 无法正常停止:{}", future.cause());
}
}
} catch (InterruptedException e) {
e.printStackTrace();
Thread.currentThread().interrupt();
AssertLog.error("Netty服务停止被中断", e);
}
AssertLog.info("\n\n\t\t\t\t\t\t\t\t======{} 已经停止,port:{}======\n", config.name, config.port);
}
@@ -18,12 +18,12 @@ public class BaseNettyConfig implements Serializable {
/**
* 是否开启
*/
public Boolean enable = false;
public boolean enable = false;
/**
* 是否TCP
*/
public Boolean isTcp = true;
public boolean isTcp = true;
/**
* 服务名称
@@ -24,8 +24,9 @@ import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.nio.file.Paths;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
@Component
public class AgentEndpoint {
@@ -347,74 +348,60 @@ public class AgentEndpoint {
if(AdvancedAsyncDownloader.createSingleDirectoryIfNotExists(policy.getFilePath())){
if(CollectionUtil.isNotEmpty(policy.getFiles())){
isFalse = true;
GlobalConfig.DOWN_FILES.put(policy.getPolicyName(), policy.getFiles().size());
// 修复:使用 AtomicInteger 替代 Integer,线程安全
GlobalConfig.DOWN_FILES.put(policy.getPolicyName(), new AtomicInteger(policy.getFiles().size()));
for (ScriptPolicyEO.ScriptFile f : policy.getFiles()) {
String finalSaveDir = policy.getFilePath();
//文件类型:0、平台文件地址;1、外网HTTP(S)
if(f.getFileType() == 0){
String filePath = policy.getFilePath() + "/" + f.getFileName();
// 移除Base64 URL前缀(如果存在)
// if (f.getFileData().contains(",")) {
// f.setFileData(f.getFileData().split(",")[1]);
// }
// 修复:路径穿越防护
String fileName = f.getFileName();
java.nio.file.Path baseDir = Paths.get(finalSaveDir).toAbsolutePath().normalize();
java.nio.file.Path targetPath = baseDir.resolve(fileName).normalize();
if (!targetPath.startsWith(baseDir)) {
AssertLog.error("检测到路径穿越攻击,拒绝写入文件: {}", fileName);
continue;
}
String filePath = targetPath.toString();
try {
AgentUtil.base64ToFile(f.getFileData(), filePath);
System.out.println("文件保存成功: " + filePath);
AssertLog.info("文件保存成功: {}", filePath);
isFalse = true;
try {
AgentDataUtil.chmod(filePath,"+x");
} catch (IOException e) {
e.printStackTrace();
AssertLog.error("设置文件权限失败", e);
} catch (InterruptedException e) {
e.printStackTrace();
Thread.currentThread().interrupt();
AssertLog.error("设置文件权限被中断", e);
}
} catch (IOException e) {
isFalse = false;
System.err.println("文件保存失败: " + e.getMessage());
AssertLog.error("文件保存失败: {}", e.getMessage());
}
}else{
AdvancedAsyncDownloader.downloadWithProgress(f.getFileUrl(), finalSaveDir, f.getFileName(), progress -> {
System.out.printf("下载进度: %.1f%%\n", progress);
AssertLog.info("下载进度: {}%", String.format("%.1f", progress));
}).thenAccept(filePath -> {
System.out.println("下载完成: " + filePath);
AssertLog.info("下载完成: {}", filePath);
try {
AgentDataUtil.chmod(finalSaveDir+"/"+f.getFileName(),"775");
int count = GlobalConfig.DOWN_FILES.get(policy.getPolicyName());
if(count > 1){
GlobalConfig.DOWN_FILES.put(policy.getPolicyName(), count--);
}else{
// 修复:使用 AtomicInteger decrementAndGet 线程安全递减
AtomicInteger counter = GlobalConfig.DOWN_FILES.get(policy.getPolicyName());
if (counter != null && counter.decrementAndGet() <= 0) {
//所有文件下载完成,执行脚本命令
agentService.command(policy, clientId,MsgEnum.执行脚本策略应答.getValue());
}
} catch (IOException e) {
e.printStackTrace();
AssertLog.error("设置文件权限失败", e);
} catch (InterruptedException e) {
e.printStackTrace();
Thread.currentThread().interrupt();
AssertLog.error("设置文件权限被中断", e);
}
}).exceptionally(ex -> {
System.err.println("下载错误: " + ex.getMessage());
AssertLog.error("下载错误: {}", ex.getMessage());
return null;
});
// Java8FileDownloader.DownloadResult result = Java8FileDownloader.downloadFile(f.getFileUrl(), finalSaveDir, f.getFileName());
// // 验证下载是否完成
// if (result.isSuccess()) {
// boolean isComplete = Java8FileDownloader.verifyFileCompletion(
// result.getFilePath(), result.getExpectedSize());
// if(isComplete){
// isFalse = true;
// System.out.println("下载完成: " + finalSaveDir+"/"+f.getFileName());
// try {
// AgentDataUtil.chmod(finalSaveDir+"/"+f.getFileName(),"775");
// } catch (IOException e) {
// e.printStackTrace();
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// }else{
// isFalse = false;
// System.err.println("下载失败");
// }
// }
}
}
}
@@ -453,9 +440,9 @@ public class AgentEndpoint {
isFalse = true;
String finalSaveDir = versionUpdateEO.getFilePath();
AdvancedAsyncDownloader.downloadWithProgress(versionUpdateEO.getFileUrl(), finalSaveDir, null, progress -> {
System.out.printf("下载进度: %.1f%%\n", progress);
AssertLog.info("版本更新下载进度: {}%", String.format("%.1f", progress));
}).thenAccept(filePath -> {
System.out.println("下载完成: " + filePath);
AssertLog.info("版本更新下载完成: {}", filePath);
try {
//更改全局变量
GlobalConfig.isCollect = false;
@@ -464,10 +451,10 @@ public class AgentEndpoint {
//所有文件下载完成,执行脚本命令
agentService.command(policy, clientId,MsgEnum.Agent版本更新应答.getValue());
} catch (Exception e) {
e.printStackTrace();
AssertLog.error("版本更新执行异常", e);
}
}).exceptionally(ex -> {
System.err.println("下载错误: " + ex.getMessage());
AssertLog.error("版本更新下载错误: {}", ex.getMessage());
return null;
});
}
@@ -1,5 +1,6 @@
package com.tongran.agent.client.netty.handler;
import com.tongran.agent.client.utils.AssertLog;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
@@ -16,9 +17,24 @@ public class UDPListenHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
if (!(msg instanceof DatagramPacket)) {
return;
}
DatagramPacket packet = (DatagramPacket) msg;
ByteBuf buf = packet.content();
buf.clear();// 暂未实现
try {
ByteBuf buf = packet.content();
byte[] data = new byte[buf.readableBytes()];
buf.readBytes(data);
AssertLog.info("收到UDP数据: {} bytes, 来自: {}", data.length, packet.sender());
// TODO: 实现UDP消息处理逻辑
} finally {
// 修复:确保 DatagramPacket 被释放,避免内存泄漏
packet.release();
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
AssertLog.error("UDP处理异常", cause);
}
}
@@ -1,5 +1,7 @@
package com.tongran.agent.client.scheduler.service;
import com.tongran.agent.client.utils.AssertLog;
import java.io.BufferedInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
@@ -8,114 +10,162 @@ import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
/**
* 异步文件下载器
* 安全增强:协议白名单、域名白名单、文件大小上限、路径穿越防护
*/
public class AdvancedAsyncDownloader {
private static final ExecutorService downloadExecutor =
Executors.newFixedThreadPool(5);
private static final ExecutorService downloadExecutor =
Executors.newFixedThreadPool(5, r -> {
Thread t = new Thread(r, "async-downloader");
t.setDaemon(true);
return t;
});
/** 允许的下载协议 */
private static final Set<String> ALLOWED_PROTOCOLS = new HashSet<>();
static {
ALLOWED_PROTOCOLS.add("http");
ALLOWED_PROTOCOLS.add("https");
}
/** 最大下载文件大小 (200MB) */
private static final long MAX_FILE_SIZE = 200L * 1024 * 1024;
/** 连接超时 (10秒) */
private static final int CONNECT_TIMEOUT = 10_000;
/** 读取超时 (60秒) */
private static final int READ_TIMEOUT = 60_000;
/**
* 带进度回调的异步下载
* 带进度回调的异步下载(安全增强版)
*/
public static CompletableFuture<String> downloadWithProgress(
String fileUrl,
String saveDir,
String fileUrl,
String saveDir,
String fileName,
Consumer<Double> progressCallback) {
return CompletableFuture.supplyAsync(() -> {
HttpURLConnection connection = null;
try {
// 安全校验:协议白名单
URL url = new URL(fileUrl);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
// 获取文件大小
if (!ALLOWED_PROTOCOLS.contains(url.getProtocol().toLowerCase())) {
throw new SecurityException("不允许的协议: " + url.getProtocol());
}
connection = (HttpURLConnection) url.openConnection();
connection.setConnectTimeout(CONNECT_TIMEOUT);
connection.setReadTimeout(READ_TIMEOUT);
// 获取文件大小并校验
long fileSize = connection.getContentLengthLong();
if (fileSize > MAX_FILE_SIZE) {
throw new SecurityException("文件大小超过限制: " + fileSize + " bytes");
}
// 创建保存目录
Path directory = Paths.get(saveDir);
if (!Files.exists(directory)) {
Files.createDirectories(directory);
}
String filePath = directory.resolve(fileName != null ? fileName :
extractFileNameFromUrl(fileUrl)).toString();
// 确定文件名
String resolvedFileName = fileName != null ? fileName : extractFileNameFromUrl(fileUrl);
Path filePath = directory.resolve(resolvedFileName).normalize();
// 路径穿越防护:确保最终路径在允许的目录内
if (!filePath.startsWith(directory)) {
throw new SecurityException("检测到路径穿越攻击: " + resolvedFileName);
}
try (BufferedInputStream in = new BufferedInputStream(connection.getInputStream());
FileOutputStream out = new FileOutputStream(filePath)) {
FileOutputStream out = new FileOutputStream(filePath.toFile())) {
byte[] buffer = new byte[8192];
int bytesRead;
long totalRead = 0;
while ((bytesRead = in.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
totalRead += bytesRead;
// 回调进度
// 文件大小运行时校验
if (totalRead > MAX_FILE_SIZE) {
throw new SecurityException("下载文件大小超过运行时限制");
}
if (progressCallback != null && fileSize > 0) {
double progress = (double) totalRead / fileSize * 100;
progressCallback.accept(progress);
}
}
}
return filePath;
return filePath.toString();
} catch (Exception e) {
throw new RuntimeException("下载失败", e);
AssertLog.error("文件下载失败: {} - {}", fileUrl, e.getMessage());
throw new RuntimeException("下载失败: " + e.getMessage(), e);
} finally {
// 修复:确保 connection 被释放
if (connection != null) {
connection.disconnect();
}
}
}, downloadExecutor);
}
public static String extractFileNameFromUrl(String fileUrl) {
// 实现文件名提取逻辑
return fileUrl.substring(fileUrl.lastIndexOf('/') + 1);
String result = fileUrl.substring(fileUrl.lastIndexOf('/') + 1);
// 去除查询参数
int queryIndex = result.indexOf('?');
if (queryIndex > 0) {
result = result.substring(0, queryIndex);
}
return result.isEmpty() ? "download_file" : result;
}
/**
* 检查目录是否存在,不存在则创建(不创建父目录)
* @param directoryPath 目录路径
* @return 创建成功返回true,否则返回false
*/
public static boolean createSingleDirectoryIfNotExists(String directoryPath) {
try {
Path path = Paths.get(directoryPath);
if (!Files.exists(path)) {
// 只创建单级目录(父目录必须存在)
Files.createDirectory(path);
System.out.println("目录创建成功: " + directoryPath);
return true;
} else {
System.out.println("目录已存在: " + directoryPath);
AssertLog.info("目录创建成功: {}", directoryPath);
return true;
}
return true;
} catch (IOException e) {
System.err.println("创建目录失败: " + directoryPath);
System.err.println("错误信息: " + e.getMessage());
AssertLog.error("创建目录失败: {} - {}", directoryPath, e.getMessage());
return false;
}
}
/**
* 使用示例
* 优雅关闭线程池
*/
public static void main(String[] args) {
String fileUrl = "https://www.tzdsp.net/ksc-andromedae";
String saveDir = "D:/down";
downloadWithProgress(fileUrl, saveDir, null, progress -> {
System.out.printf("下载进度: %.1f%%\n", progress);
}).thenAccept(filePath -> {
System.out.println("下载完成: " + filePath);
}).exceptionally(ex -> {
System.err.println("下载错误: " + ex.getMessage());
return null;
});
// 主线程继续执行
System.out.println("异步下载已启动...");
public static void shutdown() {
downloadExecutor.shutdown();
try {
if (!downloadExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
downloadExecutor.shutdownNow();
}
} catch (InterruptedException e) {
downloadExecutor.shutdownNow();
Thread.currentThread().interrupt();
}
}
}
}
@@ -1,5 +1,7 @@
package com.tongran.agent.client.scheduler.service;
import com.tongran.agent.client.utils.AssertLog;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
@@ -13,12 +15,24 @@ import java.util.Set;
import java.util.concurrent.*;
import java.util.function.Consumer;
/**
* 异步命令执行器
* 修复:使用有界线程池替代无界 CachedThreadPool,使用守护线程
*/
public class AsyncCommandExecutor {
// 使用线程池管理异步任务
private static final ExecutorService executorService = Executors.newCachedThreadPool();
private static final ScheduledExecutorService timeoutExecutor = Executors.newScheduledThreadPool(5);
// 修复:使用有界线程池替代无界 CachedThreadPool
private static final ExecutorService executorService = Executors.newFixedThreadPool(20, r -> {
Thread t = new Thread(r, "async-cmd-executor");
t.setDaemon(true);
return t;
});
private static final ScheduledExecutorService timeoutExecutor = Executors.newScheduledThreadPool(5, r -> {
Thread t = new Thread(r, "async-timeout-monitor");
t.setDaemon(true);
return t;
});
/**
* 异步为文件添加可执行权限
*/
@@ -26,25 +40,26 @@ public class AsyncCommandExecutor {
return CompletableFuture.supplyAsync(() -> {
try {
Path path = Paths.get(filePath);
if (!Files.exists(path)) {
throw new RuntimeException("文件不存在: " + filePath);
}
Set<PosixFilePermission> permissions = Files.getPosixFilePermissions(path);
permissions.add(PosixFilePermission.OWNER_EXECUTE);
permissions.add(PosixFilePermission.GROUP_EXECUTE);
permissions.add(PosixFilePermission.OTHERS_EXECUTE);
Files.setPosixFilePermissions(path, permissions);
return true;
} catch (Exception e) {
AssertLog.error("设置执行权限失败: {}", e.getMessage());
throw new RuntimeException("设置执行权限失败: " + e.getMessage(), e);
}
}, executorService);
}
/**
* 异步执行命令(基础版本)
*/
@@ -52,55 +67,54 @@ public class AsyncCommandExecutor {
List<String> command,
long timeout,
TimeUnit timeUnit) {
return CompletableFuture.supplyAsync(() -> {
Process process = null;
try {
ProcessBuilder processBuilder = new ProcessBuilder(command);
processBuilder.redirectErrorStream(true);
// processBuilder.directory(new File("/data/agent-server")); // 设置工作目录
// 不合并错误流,分别读取
process = processBuilder.start();
// 异步读取输出
Process finalProcess = process;
Future<String> outputFuture = executorService.submit(() -> {
StringBuilder output = new StringBuilder();
// String output = "";
try (BufferedReader reader = new BufferedReader(new InputStreamReader(finalProcess.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
output.append(line).append("\n");
// output = line;
}
} catch (IOException e) {
AssertLog.error("读取命令输出异常", e);
}
return output.toString();
});
// 设置超时
if (timeout > 0) {
boolean finished = process.waitFor(timeout, timeUnit);
if (!finished) {
process.destroy();
process.destroyForcibly();
throw new TimeoutException("命令执行超时");
}
} else {
process.waitFor();
}
String output = outputFuture.get(1, TimeUnit.SECONDS);
int exitCode = process.exitValue();
return new CommandResult(exitCode, output, "");
} catch (Exception e) {
AssertLog.error("命令执行失败: {}", e.getMessage());
return new CommandResult(-1, "", "执行失败: " + e.getMessage());
} finally {
if (process != null) {
process.destroy();
process.destroyForcibly();
}
}
}, executorService);
}
/**
* 带实时输出的异步命令执行
*/
@@ -110,66 +124,67 @@ public class AsyncCommandExecutor {
TimeUnit timeUnit,
Consumer<String> outputConsumer,
Consumer<String> errorConsumer) {
return CompletableFuture.supplyAsync(() -> {
Process process = null;
try {
ProcessBuilder processBuilder = new ProcessBuilder(command);
process = processBuilder.start();
// 启动输出读取线程
CompletableFuture<Void> outputFuture = readStreamAsync(
process.getInputStream(), outputConsumer, "OUTPUT");
process.getInputStream(), outputConsumer, "OUTPUT");
CompletableFuture<Void> errorFuture = readStreamAsync(
process.getErrorStream(), errorConsumer, "ERROR");
process.getErrorStream(), errorConsumer, "ERROR");
// 设置超时监控
ScheduledFuture<?> timeoutFuture = null;
if (timeout > 0) {
Process finalProcess = process;
timeoutFuture = timeoutExecutor.schedule(() -> {
if (finalProcess.isAlive()) {
finalProcess.destroy();
finalProcess.destroyForcibly();
}
}, timeout, timeUnit);
}
// 等待进程完成
int exitCode = process.waitFor();
// 取消超时任务
if (timeoutFuture != null) {
timeoutFuture.cancel(false);
}
// 等待输出读取完成
CompletableFuture.allOf(outputFuture, errorFuture).get(2, TimeUnit.SECONDS);
return new CommandResult(exitCode, "", "");
} catch (Exception e) {
AssertLog.error("实时输出命令执行异常: {}", e.getMessage());
return new CommandResult(-1, "", "执行异常: " + e.getMessage());
} finally {
if (process != null) {
process.destroy();
process.destroyForcibly();
}
}
}, executorService);
}
/**
* 异步读取流数据
*/
private static CompletableFuture<Void> readStreamAsync(
InputStream inputStream,
InputStream inputStream,
Consumer<String> lineConsumer,
String streamType) {
return CompletableFuture.runAsync(() -> {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(inputStream))) {
new InputStreamReader(inputStream))) {
String line;
while ((line = reader.readLine()) != null) {
if (lineConsumer != null) {
@@ -183,7 +198,7 @@ public class AsyncCommandExecutor {
}
}, executorService);
}
/**
* 异步执行脚本文件
*/
@@ -193,38 +208,25 @@ public class AsyncCommandExecutor {
long timeout,
TimeUnit timeUnit,
Consumer<String> realtimeOutputConsumer) {
return makeFileExecutableAsync(scriptPath)
.thenCompose(permissionSuccess -> {
if (!permissionSuccess) {
return CompletableFuture.completedFuture(
new CommandResult(-1, "", "设置执行权限失败"));
}
String[] command = new String[args.length + 1];
command[0] = scriptPath;
System.arraycopy(args, 0, command, 1, args.length);
return executeCommandWithRealtimeOutput(
command, timeout, timeUnit,
realtimeOutputConsumer,
error -> System.err.println(error));
});
.thenCompose(permissionSuccess -> {
if (!permissionSuccess) {
return CompletableFuture.completedFuture(
new CommandResult(-1, "", "设置执行权限失败"));
}
String[] command = new String[args.length + 1];
command[0] = scriptPath;
System.arraycopy(args, 0, command, 1, args.length);
return executeCommandWithRealtimeOutput(
command, timeout, timeUnit,
realtimeOutputConsumer,
error -> AssertLog.error("脚本执行错误输出: {}", error));
});
}
/**
* 批量异步执行命令
*/
// public static List<CompletableFuture<CommandResult>> executeCommandsAsync(
// List<String[]> commands,
// long timeout,
// TimeUnit timeUnit) {
//
// return commands.stream()
// .map(command -> executeCommandAsync(command, timeout, timeUnit))
// .collect(java.util.stream.Collectors.toList());
// }
/**
* 关闭执行器
*/
@@ -244,7 +246,7 @@ public class AsyncCommandExecutor {
Thread.currentThread().interrupt();
}
}
/**
* 执行结果封装类
*/
@@ -252,22 +254,22 @@ public class AsyncCommandExecutor {
private final int exitCode;
private final String output;
private final String error;
public CommandResult(int exitCode, String output, String error) {
this.exitCode = exitCode;
this.output = output;
this.error = error;
}
public int getExitCode() { return exitCode; }
public String getOutput() { return output; }
public String getError() { return error; }
public boolean isSuccess() { return exitCode == 0; }
@Override
public String toString() {
return String.format("Exit Code: %d\nOutput: %s\nError: %s",
exitCode, output, error);
return String.format("Exit Code: %d\nOutput: %s\nError: %s",
exitCode, output, error);
}
}
}
}
@@ -7,6 +7,10 @@ import java.util.Date;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
/**
* 补偿触发器
* 修复:使用 CAS 操作替代 get-then-set,消除竞态条件
*/
public class CompensatedTrigger implements Trigger {
private final long period;
@@ -27,31 +31,35 @@ public class CompensatedTrigger implements Trigger {
@Override
public Date nextExecutionTime(TriggerContext triggerContext) {
long lastCompletionTime = triggerContext.lastCompletionTime() != null
? triggerContext.lastCompletionTime().getTime()
: System.currentTimeMillis();
// 计算下一次执行时间(考虑补偿)
long currentNextTime = nextExecutionTime.get();
long periodMillis = timeUnit.toMillis(period);
long now = System.currentTimeMillis();
// 如果当前时间已经超过计划时间,立即执行
if (now >= currentNextTime) {
nextExecutionTime.set(now + timeUnit.toMillis(period));
return new Date(now);
// 修复:使用 CAS 循环确保原子性
while (true) {
long currentNextTime = nextExecutionTime.get();
// 如果当前时间已经超过计划时间,立即执行
if (now >= currentNextTime) {
if (nextExecutionTime.compareAndSet(currentNextTime, now + periodMillis)) {
return new Date(now);
}
// CAS 失败,重试
continue;
}
// 否则按计划时间执行
if (nextExecutionTime.compareAndSet(currentNextTime, currentNextTime + periodMillis)) {
return new Date(currentNextTime);
}
// CAS 失败,重试
}
// 否则按计划时间执行
nextExecutionTime.set(currentNextTime + timeUnit.toMillis(period));
return new Date(currentNextTime);
}
public void updatePeriod(long newPeriod) {
// 更新执行间隔
long currentTime = System.currentTimeMillis();
long remaining = nextExecutionTime.get() - currentTime;
if (remaining > 0) {
nextExecutionTime.set(currentTime + timeUnit.toMillis(newPeriod));
}
}
}
}
@@ -21,6 +21,8 @@ public class DynamicTaskService {
private final Map<String, ScheduledFuture<?>> taskFutures = new ConcurrentHashMap<>();
private final Map<String, TaskConfig> taskConfigs = new ConcurrentHashMap<>();
// 修复:保存原始任务引用,updateTaskInterval 时可以恢复
private final Map<String, Runnable> originalTasks = new ConcurrentHashMap<>();
private final Map<String, AtomicLong> taskExecutions = new ConcurrentHashMap<>();
private final Map<String, Long> lastExecutionTimes = new ConcurrentHashMap<>();
@@ -34,6 +36,7 @@ public class DynamicTaskService {
TaskConfig config = new TaskConfig(initialDelay, period, System.currentTimeMillis());
taskConfigs.put(taskId, config);
taskExecutions.put(taskId, new AtomicLong(0));
originalTasks.put(taskId, task); // 保存原始任务引用
// 使用补偿机制调度任务
ScheduledFuture<?> future = taskScheduler.schedule(
@@ -43,7 +46,7 @@ public class DynamicTaskService {
taskFutures.put(taskId, future);
GlobalConfig.taskIds.put(taskId, 0L);
AssertLog.info("添加或更新定时任务taskId={}",taskId);
AssertLog.info("添加或更新定时任务taskId={}", taskId);
}
/**
@@ -53,27 +56,22 @@ public class DynamicTaskService {
return () -> {
long startTime = System.currentTimeMillis();
lastExecutionTimes.put(taskId, startTime);
try {
System.out.printf("开始执行任务: %s, 线程: %s%n",
taskId, Thread.currentThread().getName());
// 添加前置检查
AssertLog.info("开始执行任务: {}, 线程: {}", taskId, Thread.currentThread().getName());
if (originalTask == null) {
throw new IllegalArgumentException("原始任务不能为null");
}
originalTask.run();
System.out.printf("任务 %s 执行成功%n", taskId);
} catch (Exception e) {
System.err.printf("任务 %s 执行失败: %s%n", taskId, e.getMessage());
// System.err.println("Task " + taskId + " execution failed: " + e.getMessage());
AssertLog.error("任务 {} 执行失败: {}", taskId, e.getMessage());
} finally {
long endTime = System.currentTimeMillis();
long executionTime = endTime - startTime;
taskExecutions.get(taskId).incrementAndGet();
// System.out.printf("Task %s executed in %dms, total executions: %d%n",
// taskId, executionTime, taskExecutions.get(taskId).get());
System.out.printf("任务 %s 执行耗时: %dms, 总执行次数: %d%n",
taskId, executionTime, taskExecutions.get(taskId).get());
AtomicLong execCount = taskExecutions.get(taskId);
if (execCount != null) {
AssertLog.info("任务 {} 执行耗时: {}ms, 总执行次数: {}", taskId, executionTime, execCount.incrementAndGet());
}
}
};
}
@@ -88,6 +86,7 @@ public class DynamicTaskService {
taskConfigs.remove(taskId);
taskExecutions.remove(taskId);
lastExecutionTimes.remove(taskId);
originalTasks.remove(taskId); // 清理原始任务引用
GlobalConfig.taskIds.remove(taskId);
return true;
}
@@ -95,13 +94,15 @@ public class DynamicTaskService {
}
/**
* 更新任务间隔
* 更新任务间隔 - 修复:使用保存的原始任务重新调度
*/
public void updateTaskInterval(String taskId, long newPeriod) {
TaskConfig config = taskConfigs.get(taskId);
if (config != null) {
Runnable task = () -> {}; // 这里需要根据实际情况获取原始任务
scheduleTask(taskId, task, 0, newPeriod); // 立即重新调度
Runnable task = originalTasks.get(taskId); // 获取原始任务
if (config != null && task != null) {
scheduleTask(taskId, task, 0, newPeriod);
} else {
AssertLog.warn("更新任务间隔失败,任务不存在或原始任务已丢失: taskId={}", taskId);
}
}
@@ -112,14 +113,15 @@ public class DynamicTaskService {
TaskConfig config = taskConfigs.get(taskId);
AtomicLong executions = taskExecutions.get(taskId);
Long lastTime = lastExecutionTimes.get(taskId);
if (config != null && executions != null) {
ScheduledFuture<?> future = taskFutures.get(taskId);
return new TaskStatus(
taskId,
config.getPeriod(),
executions.get(),
lastTime,
taskFutures.get(taskId) != null && !taskFutures.get(taskId).isCancelled()
future != null && !future.isCancelled()
);
}
return null;
@@ -155,4 +157,4 @@ public class DynamicTaskService {
private Long lastExecutionTime;
private boolean isRunning;
}
}
}
@@ -5,6 +5,7 @@ import com.tongran.agent.client.core.enums.MsgEnum;
import com.tongran.agent.client.core.session.SessionManager;
import com.tongran.agent.client.netty.model.Message;
import com.tongran.agent.client.scheduler.service.AsyncCommandExecutor;
import com.tongran.agent.client.utils.AssertLog;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
@@ -13,19 +14,41 @@ import java.io.IOException;
import java.time.Instant;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
@Service
public class SpecificTimeTaskService {
protected final SessionManager sessionManager;
@Resource
private SpecificTimeTaskConfig taskConfig;
/**
* 允许执行的命令白名单前缀
*/
private static final Set<String> ALLOWED_COMMAND_PREFIXES = new HashSet<>();
static {
ALLOWED_COMMAND_PREFIXES.add("/data/");
ALLOWED_COMMAND_PREFIXES.add("/opt/");
ALLOWED_COMMAND_PREFIXES.add("/usr/local/");
ALLOWED_COMMAND_PREFIXES.add("./");
}
/**
* 禁止的命令字符(防止 shell 注入)
*/
private static final Pattern DANGEROUS_PATTERN = Pattern.compile(
"[;&|`$(){}\\[\\]<>\\\\\"'\\n\\r]");
private static final Pattern SAFE_SCRIPT_PATTERN = Pattern.compile(
"^[a-zA-Z0-9_./\\-]+$");
public SpecificTimeTaskService() {
this.sessionManager = SessionManager.getInstance();
}
@@ -34,11 +57,11 @@ public class SpecificTimeTaskService {
* 创建指定时间任务
*/
public boolean createSpecificTimeTask(SpecificTimeRequest request) {
String taskId = request.getTaskId() != null ?
String taskId = request.getTaskId() != null ?
request.getTaskId() : "task-" + System.currentTimeMillis();
Runnable task = createTaskRunnable(request);
try {
if (request.getCronExpression() != null) {
taskConfig.addCronTask(taskId, task, request.getCronExpression());
@@ -53,40 +76,50 @@ public class SpecificTimeTaskService {
}
return true;
} catch (Exception e) {
AssertLog.error("创建定时任务失败: {}", e.getMessage());
throw new RuntimeException("创建任务失败: " + e.getMessage(), e);
}
}
private Runnable createTaskRunnable(SpecificTimeRequest request) {
return () -> {
System.out.println("执行定时任务: " + request.getTaskName());
System.out.println("任务数据: " + request.getTaskData());
System.out.println("执行时间: " + LocalDateTime.now());
System.out.println("-----------------------------------");
// 具体的业务逻辑
AssertLog.info("执行定时任务: {}, 执行时间: {}", request.getTaskName(), LocalDateTime.now());
executeBusinessLogic(request);
};
}
/**
* 执行业务逻辑 - 安全增强版
*/
private void executeBusinessLogic(SpecificTimeRequest request) {
// 实现你的业务逻辑
try {
System.out.println("处理业务: " + request.getTaskData());
String key = request.getTaskName()+"-"+System.currentTimeMillis();
AssertLog.info("处理业务: taskName={}, dataType={}", request.getTaskName(), request.getDataType());
for (String command : request.getTaskData()) {
if(StringUtils.equals(request.getDataType(), MsgEnum.Agent版本更新应答.getValue())){
if (StringUtils.equals(request.getDataType(), MsgEnum.Agent版本更新应答.getValue())) {
// 版本更新:使用安全校验
try {
System.out.println("重启进程已启动,当前服务退出");
System.out.println("command="+command);
ProcessBuilder pb = new ProcessBuilder("/bin/sh", "-c",
command);
AssertLog.info("重启进程已启动,当前服务退出");
// 修复:不直接使用 /bin/sh -c,而是安全校验后执行
if (!isSafeCommand(command)) {
AssertLog.error("拒绝执行不安全的命令: {}", command);
continue;
}
List<String> safeCmd = Arrays.asList(command.split("\\s+"));
ProcessBuilder pb = new ProcessBuilder(safeCmd);
pb.start();
System.exit(0);
} catch (IOException e) {
e.printStackTrace();
AssertLog.error("版本更新执行异常", e);
}
}else{
} else {
// 普通命令执行:安全校验
if (!isSafeCommand(command)) {
AssertLog.error("拒绝执行不安全的命令: {}", command);
sendErrorResponse(request, "拒绝执行不安全的命令");
continue;
}
List<String> cmd = Arrays.asList(command.split("\\s+"));
CompletableFuture<AsyncCommandExecutor.CommandResult> future =
AsyncCommandExecutor.executeCommandAsync(
@@ -96,35 +129,79 @@ public class SpecificTimeTaskService {
JSONObject jsonObject = new JSONObject();
jsonObject.put("command", command);
jsonObject.put("resOut", result.getOutput());
System.out.println("JSON: " + jsonObject.toJSONString()); // 注意:toJSONString()
JSONObject json = new JSONObject();
json.put("resCode",1);
json.put("resCode", 1);
json.put("resMsg", "");
json.put("result", jsonObject.toJSONString());
json.put("timestamp", Instant.now().getEpochSecond());
Message message = Message.builder().clientId(request.getClientId()).dataType(request.getDataType()).data(json.toJSONString()).build();
Message message = Message.builder()
.clientId(request.getClientId())
.dataType(request.getDataType())
.data(json.toJSONString())
.build();
if (Objects.nonNull(sessionManager.getSessionById(request.getClientId()))) {
System.out.println("发送执行结果: " + json.toJSONString()); // 注意:toJSONString()
sessionManager.writeAndFlush(sessionManager.getSessionById(request.getClientId()).getChannel(), message);
AssertLog.info("发送执行结果: taskId={}", request.getTaskName());
sessionManager.writeAndFlush(
sessionManager.getSessionById(request.getClientId()).getChannel(), message);
}
}).exceptionally(ex -> {
System.err.println("执行失败: " + ex.getMessage());
JSONObject json = new JSONObject();
json.put("resCode",0);
json.put("resMsg", "执行失败:Policy execute filed");
json.put("result", "");
Message message = Message.builder().clientId(request.getClientId()).dataType(request.getDataType()).data(json.toJSONString()).build();
if (Objects.nonNull(sessionManager.getSessionById(request.getClientId()))) {
sessionManager.writeAndFlush(sessionManager.getSessionById(request.getClientId()).getChannel(), message);
}
AssertLog.error("命令执行失败: {}", ex.getMessage());
sendErrorResponse(request, "执行失败:Policy execute failed");
return null;
});
}
}
// 调用其他服务等
} catch (Exception e) {
System.err.println("任务执行异常: " + e.getMessage());
AssertLog.error("任务执行异常", e);
}
}
}
/**
* 安全校验:检查命令是否安全可执行
*/
private boolean isSafeCommand(String command) {
if (StringUtils.isBlank(command)) {
return false;
}
// 检查是否包含危险字符(shell 注入防护)
if (DANGEROUS_PATTERN.matcher(command).find()) {
return false;
}
// 检查命令路径是否在白名单内
boolean pathAllowed = false;
for (String prefix : ALLOWED_COMMAND_PREFIXES) {
if (command.startsWith(prefix) || command.startsWith("./")) {
pathAllowed = true;
break;
}
}
// 如果不在白名单路径,检查是否是安全的脚本/命令名
if (!pathAllowed) {
String firstToken = command.split("\\s+")[0];
if (!SAFE_SCRIPT_PATTERN.matcher(firstToken).matches()) {
return false;
}
}
return true;
}
/**
* 发送错误响应
*/
private void sendErrorResponse(SpecificTimeRequest request, String errorMsg) {
JSONObject json = new JSONObject();
json.put("resCode", 0);
json.put("resMsg", errorMsg);
json.put("result", "");
Message message = Message.builder()
.clientId(request.getClientId())
.dataType(request.getDataType())
.data(json.toJSONString())
.build();
if (Objects.nonNull(sessionManager.getSessionById(request.getClientId()))) {
sessionManager.writeAndFlush(
sessionManager.getSessionById(request.getClientId()).getChannel(), message);
}
}
}
@@ -0,0 +1,164 @@
package com.tongran.agent.client.security;
import com.tongran.agent.client.utils.AssertLog;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.AttributeKey;
import org.springframework.stereotype.Component;
import java.nio.charset.StandardCharsets;
/**
* Netty 握手认证处理器
*
* <p>放在 pipeline 最前面(在 {@code AgentDecoderHandler} 之前)。
* 连接建立后,客户端必须先发一个握手包:
* <pre>
* auth:{token}@tong-ran
* </pre>
* 服务端校验通过后,移除自身,放行后续业务消息。失败则关闭连接。
*
* <p>同时配合 {@link io.netty.handler.timeout.IdleStateHandler} 做握手超时:
* 如果在 {@code handshake-timeout-seconds} 内未完成握手,IdleStateHandler 会触发
* readerIdle 事件,本 handler 在 userEventTriggered 中关闭连接。
*
* <p>注意:本 handler 必须是 {@code @Sharable} 的,因为所有连接共用一个实例。
*
* @author Senior Developer
*/
@Component
@io.netty.channel.ChannelHandler.Sharable
public class AuthHandshakeHandler extends ChannelInboundHandlerAdapter {
/** 握手包前缀 */
public static final String HANDSHAKE_PREFIX = "auth:";
/** 握手包后缀(与业务消息一致,方便客户端复用分隔符) */
public static final String HANDSHAKE_SUFFIX = "@tong-ran";
/** Channel 属性:标记是否已通过认证 */
public static final AttributeKey<Boolean> AUTHENTICATED =
AttributeKey.valueOf("agent-authenticated");
/** Channel 属性:记录客户端 IP,便于审计 */
public static final AttributeKey<String> CLIENT_IP =
AttributeKey.valueOf("agent-client-ip");
private final SecurityProperties properties;
public AuthHandshakeHandler(SecurityProperties properties) {
this.properties = properties;
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
// 连接建立:记录 IP
String clientIp = ctx.channel().remoteAddress() != null
? ctx.channel().remoteAddress().toString()
: "unknown";
ctx.channel().attr(CLIENT_IP).set(clientIp);
ctx.channel().attr(AUTHENTICATED).set(false);
AssertLog.info("[AUTH] 新连接 ip={}", clientIp);
super.channelActive(ctx);
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// 1. 已认证:直接放行,移除自身(后续消息走业务 handler)
Boolean authenticated = ctx.channel().attr(AUTHENTICATED).get();
if (Boolean.TRUE.equals(authenticated)) {
ctx.fireChannelRead(msg);
return;
}
// 2. 未认证:必须是 ByteBuf,且内容是握手包
if (!(msg instanceof ByteBuf)) {
rejectAndClose(ctx, "握手阶段收到非 ByteBuf 消息");
return;
}
ByteBuf buf = (ByteBuf) msg;
String content = buf.toString(StandardCharsets.UTF_8);
// 握手包格式:auth:{token}@tong-ran
if (!content.startsWith(HANDSHAKE_PREFIX) || !content.endsWith(HANDSHAKE_SUFFIX)) {
rejectAndClose(ctx, "握手包格式错误: " + truncate(content));
buf.release();
return;
}
// 提取 token
String token = content.substring(
HANDSHAKE_PREFIX.length(),
content.length() - HANDSHAKE_SUFFIX.length()
).trim();
// 3. token 校验
String expectedToken = properties.getNettyAuthToken();
if (expectedToken == null || expectedToken.isEmpty()) {
rejectAndClose(ctx, "服务端未配置握手 token");
buf.release();
return;
}
if (!constantTimeEquals(token, expectedToken)) {
rejectAndClose(ctx, "握手 token 不匹配");
buf.release();
return;
}
// 4. 认证通过:标记、移除自身、释放 ByteBuf
ctx.channel().attr(AUTHENTICATED).set(true);
buf.release();
AssertLog.info("[AUTH] 认证通过 ip={}",
ctx.channel().attr(CLIENT_IP).get());
// 移除自身,后续消息直接走 AgentDecoderHandler
ctx.pipeline().remove(this);
}
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
// IdleStateHandler 触发的超时事件:握手阶段超时直接关闭
if (evt instanceof io.netty.handler.timeout.IdleStateEvent) {
Boolean authenticated = ctx.channel().attr(AUTHENTICATED).get();
if (!Boolean.TRUE.equals(authenticated)) {
rejectAndClose(ctx, "握手超时");
return;
}
}
super.userEventTriggered(ctx, evt);
}
/** 拒绝并关闭连接 */
private void rejectAndClose(ChannelHandlerContext ctx, String reason) {
String clientIp = ctx.channel().attr(CLIENT_IP).get();
AssertLog.error("[AUTH] 认证失败 ip={} reason={}", clientIp, reason);
// 可选:发送拒绝消息(便于客户端排查)
String rejectMsg = "auth-failed:" + reason + HANDSHAKE_SUFFIX;
ctx.writeAndFlush(Unpooled.copiedBuffer(rejectMsg, StandardCharsets.UTF_8))
.addListener(future -> ctx.close());
}
/** 常量时间字符串比较(防时序攻击) */
private boolean constantTimeEquals(String a, String b) {
if (a == null || b == null) return false;
if (a.length() != b.length()) return false;
int result = 0;
for (int i = 0; i < a.length(); i++) {
result |= a.charAt(i) ^ b.charAt(i);
}
return result == 0;
}
/** 截断日志内容(防止超长日志) */
private String truncate(String s) {
if (s == null) return "null";
return s.length() > 100 ? s.substring(0, 100) + "..." : s;
}
}
@@ -0,0 +1,161 @@
package com.tongran.agent.client.security;
import com.tongran.agent.client.utils.AssertLog;
import org.springframework.stereotype.Component;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.HexFormat;
/**
* HMAC-SHA256 签名校验器
*
* <p>用于校验服务端下发的命令、下载任务等是否被篡改,以及是否过期(防重放)。
*
* <p>签名规则:
* <pre>
* payload = timestamp + "\n" + body
* signature = HMAC_SHA256(secret, payload)
* 下发格式: sign = "v1:{timestamp}:{hexSignature}"
* </pre>
*
* <p>校验流程:
* <ol>
* <li>解析 sign,取出 timestamp 和 signature</li>
* <li>校验 timestamp 是否在 TTL 内(防重放)</li>
* <li>用本地 secret 重新计算签名,常量时间比较</li>
* </ol>
*
* @author Senior Developer
*/
@Component
public class HmacSignVerifier {
private static final String HMAC_ALGORITHM = "HmacSHA256";
private static final String SIGN_PREFIX = "v1:";
private final SecurityProperties properties;
public HmacSignVerifier(SecurityProperties properties) {
this.properties = properties;
}
/**
* 校验签名
*
* @param sign 服务端下发的签名,格式 "v1:{timestamp}:{hexSig}"
* @param body 业务内容(命令字符串、URL 等)
* @param nowMillis 当前时间戳(毫秒),由调用方传入方便测试
* @return true 校验通过
*/
public VerifyResult verify(String sign, String body, long nowMillis) {
// 1. 配置校验:未配置 secret 直接拒绝
String secret = properties.getSignSecret();
if (secret == null || secret.isEmpty()) {
AssertLog.error("签名校验失败:未配置 agent.security.sign-secret");
return VerifyResult.fail("签名密钥未配置");
}
// 2. 格式校验
if (sign == null || !sign.startsWith(SIGN_PREFIX)) {
return VerifyResult.fail("签名格式错误");
}
String[] parts = sign.substring(SIGN_PREFIX.length()).split(":", 2);
if (parts.length != 2) {
return VerifyResult.fail("签名格式错误");
}
long timestamp;
String receivedSig;
try {
timestamp = Long.parseLong(parts[0]);
} catch (NumberFormatException e) {
return VerifyResult.fail("签名时间戳非法");
}
receivedSig = parts[1];
// 3. 时间窗口校验(防重放)
long ageSeconds = (nowMillis - timestamp) / 1000;
if (ageSeconds < 0) {
// 允许 60 秒时钟偏差
if (ageSeconds < -60) {
return VerifyResult.fail("签名时间戳超前过多");
}
} else if (ageSeconds > properties.getSignTtlSeconds()) {
AssertLog.error("签名过期:age={}s,ttl={}s", ageSeconds, properties.getSignTtlSeconds());
return VerifyResult.fail("签名已过期");
}
// 4. 重新计算签名
String payload = timestamp + "\n" + (body == null ? "" : body);
String computedSig;
try {
computedSig = computeHmac(secret, payload);
} catch (Exception e) {
AssertLog.error("签名计算异常:{}", e.getMessage());
return VerifyResult.fail("签名计算异常");
}
// 5. 常量时间比较(防时序攻击)
if (!constantTimeEquals(receivedSig, computedSig)) {
AssertLog.error("签名不匹配:received={},computed={}", receivedSig, computedSig);
return VerifyResult.fail("签名不匹配");
}
return VerifyResult.ok();
}
/** 便捷重载:用系统当前时间 */
public VerifyResult verify(String sign, String body) {
return verify(sign, body, System.currentTimeMillis());
}
/** 计算 HMAC-SHA256,返回 hex 字符串 */
private String computeHmac(String secret, String payload) throws Exception {
Mac mac = Mac.getInstance(HMAC_ALGORITHM);
SecretKeySpec keySpec = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), HMAC_ALGORITHM);
mac.init(keySpec);
byte[] raw = mac.doFinal(payload.getBytes(StandardCharsets.UTF_8));
return HexFormat.of().formatHex(raw);
}
/** 常量时间字符串比较 */
private boolean constantTimeEquals(String a, String b) {
if (a == null || b == null) return false;
if (a.length() != b.length()) return false;
int result = 0;
for (int i = 0; i < a.length(); i++) {
result |= a.charAt(i) ^ b.charAt(i);
}
return result == 0;
}
/** SHA-256 工具(供下载校验使用) */
public static String sha256Hex(byte[] data) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
return HexFormat.of().formatHex(md.digest(data));
} catch (Exception e) {
throw new RuntimeException("SHA-256 计算失败", e);
}
}
/** 校验结果 */
public static class VerifyResult {
private final boolean success;
private final String reason;
private VerifyResult(boolean success, String reason) {
this.success = success;
this.reason = reason;
}
public static VerifyResult ok() { return new VerifyResult(true, null); }
public static VerifyResult fail(String reason) { return new VerifyResult(false, reason); }
public boolean isSuccess() { return success; }
public String getReason() { return reason; }
}
}
@@ -0,0 +1,144 @@
package com.tongran.agent.client.security;
import com.tongran.agent.client.utils.AssertLog;
import org.springframework.stereotype.Component;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
/**
* 安全命令执行器
*
* <p>替代原 {@code AgentServiceImpl} 中直接 {@code /bin/sh -c command} 的危险写法。
*
* <p>核心策略:
* <ol>
* <li><b>白名单</b>:只允许执行 {@code agent.security.allowed-scripts-dir} 目录下、
* 且文件名在 {@code allowed-script-names} 列表中的脚本</li>
* <li><b>路径穿越防护</b>:解析后路径必须以白名单目录开头</li>
* <li><b>签名校验</b>:服务端下发必须携带 HMAC 签名,客户端用 {@link HmacSignVerifier} 校验</li>
* <li><b>参数隔离</b>:脚本路径和参数分开传递给 {@link ProcessBuilder},
* 不经过 shell 解析,杜绝注入</li>
* <li><b>审计日志</b>:所有执行请求(无论通过与否)都记入日志</li>
* </ol>
*
* <p>调用约定:服务端不再下发 shell 字符串,而是下发 JSON:
* <pre>
* {
* "script": "restart.sh", // 必须在白名单内
* "args": ["--service", "agent"], // 参数数组,不经过 shell
* "sign": "v1:1735724123:abc..." // HMAC 签名
* }
* </pre>
*
* @author Senior Developer
*/
@Component
public class SecureCommandExecutor {
private final SecurityProperties properties;
private final HmacSignVerifier signVerifier;
private final SystemCommandRunner commandRunner;
public SecureCommandExecutor(SecurityProperties properties,
HmacSignVerifier signVerifier,
SystemCommandRunner commandRunner) {
this.properties = properties;
this.signVerifier = signVerifier;
this.commandRunner = commandRunner;
}
/**
* 执行脚本(安全入口)
*
* @param scriptName 脚本名(必须存在于白名单)
* @param args 参数数组(不经过 shell 解析)
* @param sign HMAC 签名,签名内容为 {@code scriptName + "\n" + String.join(" ", args)}
* @return 执行结果
*/
public CompletableFuture<CommandResult> execute(String scriptName,
List<String> args,
String sign) {
// 1. 入参非空校验
if (scriptName == null || scriptName.isEmpty()) {
audit("REJECT", scriptName, args, "脚本名为空");
return CompletableFuture.completedFuture(CommandResult.fail("脚本名为空"));
}
// 2. 签名校验
String signPayload = scriptName + "\n" + (args == null ? "" : String.join(" ", args));
HmacSignVerifier.VerifyResult signResult = signVerifier.verify(sign, signPayload);
if (!signResult.isSuccess()) {
audit("REJECT", scriptName, args, "签名校验失败: " + signResult.getReason());
return CompletableFuture.completedFuture(CommandResult.fail("签名校验失败"));
}
// 3. 白名单校验:脚本名必须在 allowed-script-names 中
if (!properties.getAllowedScriptNames().contains(scriptName)) {
audit("REJECT", scriptName, args, "脚本不在白名单");
return CompletableFuture.completedFuture(CommandResult.fail("脚本不在白名单: " + scriptName));
}
// 4. 解析路径并防穿越
Path scriptDir = Paths.get(properties.getAllowedScriptsDir()).normalize();
Path scriptPath = scriptDir.resolve(scriptName).normalize();
if (!scriptPath.startsWith(scriptDir)) {
audit("REJECT", scriptName, args, "路径穿越: " + scriptPath);
return CompletableFuture.completedFuture(CommandResult.fail("路径非法"));
}
// 5. 文件存在性 + 普通文件校验
if (!Files.isRegularFile(scriptPath)) {
audit("REJECT", scriptName, args, "脚本文件不存在: " + scriptPath);
return CompletableFuture.completedFuture(CommandResult.fail("脚本不存在"));
}
// 6. 审计:通过校验,准备执行
audit("ACCEPT", scriptName, args, "path=" + scriptPath);
// 7. 委托给底层 runner 执行(参数隔离,不经过 shell)
long timeout = properties.getCommandTimeoutSeconds();
return commandRunner.execute(scriptPath, args == null ? List.of() : args, timeout, TimeUnit.SECONDS);
}
/** 审计日志(统一格式,便于事后追溯) */
private void audit(String decision, String scriptName, List<String> args, String detail) {
AssertLog.info("[CMD-AUDIT] decision={} script={} args={} detail={}",
decision, scriptName, args, detail);
}
/** 执行结果 */
public static class CommandResult {
private final boolean success;
private final int exitCode;
private final String output;
private final String error;
private final String reason; // 失败原因(校验失败时)
public CommandResult(boolean success, int exitCode, String output, String error, String reason) {
this.success = success;
this.exitCode = exitCode;
this.output = output;
this.error = error;
this.reason = reason;
}
public static CommandResult ok(int exitCode, String output, String error) {
return new CommandResult(exitCode == 0, exitCode, output, error, null);
}
public static CommandResult fail(String reason) {
return new CommandResult(false, -1, "", "", reason);
}
public boolean isSuccess() { return success; }
public int getExitCode() { return exitCode; }
public String getOutput() { return output; }
public String getError() { return error; }
public String getReason() { return reason; }
}
}
@@ -0,0 +1,223 @@
package com.tongran.agent.client.security;
import com.tongran.agent.client.utils.AssertLog;
import org.springframework.stereotype.Component;
import javax.net.ssl.HttpsURLConnection;
import java.io.BufferedInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.function.Consumer;
/**
* 安全文件下载器
*
* <p>替代原 {@code AdvancedAsyncDownloader} 中无防护的下载逻辑。
*
* <p>安全策略:
* <ol>
* <li><b>协议白名单</b>:只允许 HTTPS(可配置)</li>
* <li><b>域名白名单</b>:URL host 必须在 {@code allowed-hosts} 列表内</li>
* <li><b>大小上限</b>:Content-Length 超过 {@code max-size-bytes} 直接拒绝</li>
* <li><b>路径穿越防护</b>:savePath 必须在 {@code allowed-save-dir} 内</li>
* <li><b>SHA-256 校验</b>:下载完成计算摘要,与预期值(若提供)对比</li>
* <li><b>HTTPS 证书校验</b>:不绕过,严格校验(开发环境可通过 system property 临时关闭)</li>
* </ol>
*
* @author Senior Developer
*/
@Component
public class SecureFileDownloader {
private static final int BUFFER_SIZE = 8192;
private final SecurityProperties properties;
private final ExecutorService executor;
public SecureFileDownloader(SecurityProperties properties) {
this.properties = properties;
this.executor = Executors.newFixedThreadPool(5, r -> {
Thread t = new Thread(r, "agent-downloader");
t.setDaemon(true);
return t;
});
}
/**
* 异步下载文件(安全入口)
*
* @param fileUrl 下载 URL(必须 HTTPS,host 在白名单内)
* @param savePath 保存路径(必须在 allowed-save-dir 内)
* @param expectedSha256 预期 SHA-256(可为 null,但开启 sha256-required 时必传)
* @param progressCallback 进度回调(可为 null)
* @return 下载结果
*/
public CompletableFuture<DownloadResult> download(
String fileUrl,
String savePath,
String expectedSha256,
Consumer<Double> progressCallback) {
return CompletableFuture.supplyAsync(() -> {
try {
// 1. URL 解析与协议白名单
URL url = new URL(fileUrl);
String protocol = url.getProtocol().toLowerCase();
List<String> allowedProtocols = properties.getDownload().getAllowedProtocols();
if (allowedProtocols.isEmpty() || !allowedProtocols.contains(protocol)) {
return DownloadResult.fail("协议不在白名单: " + protocol);
}
// 2. 域名白名单
String host = url.getHost();
List<String> allowedHosts = properties.getDownload().getAllowedHosts();
if (allowedHosts.isEmpty() || !allowedHosts.contains(host)) {
return DownloadResult.fail("域名不在白名单: " + host);
}
// 3. 保存路径穿越防护
Path allowedDir = Paths.get(properties.getDownload().getAllowedSaveDir()).normalize();
Path targetPath = Paths.get(savePath).normalize();
if (!targetPath.startsWith(allowedDir)) {
return DownloadResult.fail("保存路径非法: " + targetPath);
}
// 4. SHA-256 必传校验
if (properties.getDownload().isSha256Required()
&& (expectedSha256 == null || expectedSha256.isEmpty())) {
return DownloadResult.fail("未提供 SHA-256 校验值");
}
// 5. 创建目录
Files.createDirectories(targetPath.getParent());
// 6. 建立连接
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
if (connection instanceof HttpsURLConnection) {
// HTTPS:严格证书校验,不做任何绕过
((HttpsURLConnection) connection).setSSLSocketFactory(
javax.net.ssl.HttpsURLConnection.getDefaultSSLSocketFactory());
((HttpsURLConnection) connection).setHostnameVerifier(
javax.net.ssl.HttpsURLConnection.getDefaultHostnameVerifier());
}
connection.setConnectTimeout(10_000);
connection.setReadTimeout(60_000);
connection.setRequestMethod("GET");
int responseCode = connection.getResponseCode();
if (responseCode != 200) {
return DownloadResult.fail("HTTP 响应码异常: " + responseCode);
}
// 7. 大小上限校验
long fileSize = connection.getContentLengthLong();
long maxSize = properties.getDownload().getMaxSizeBytes();
if (fileSize > 0 && fileSize > maxSize) {
return DownloadResult.fail("文件过大: " + fileSize + " > " + maxSize);
}
// 8. 下载 + SHA-256 计算
java.security.MessageDigest shaDigest = java.security.MessageDigest.getInstance("SHA-256");
long totalRead = 0;
try (BufferedInputStream in = new BufferedInputStream(connection.getInputStream());
FileOutputStream out = new FileOutputStream(targetPath.toFile())) {
byte[] buffer = new byte[BUFFER_SIZE];
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
shaDigest.update(buffer, 0, bytesRead);
totalRead += bytesRead;
// 实时大小校验(防止服务端不返回 Content-Length 时被绕过)
if (totalRead > maxSize) {
Files.deleteIfExists(targetPath);
return DownloadResult.fail("下载超过大小上限: " + totalRead);
}
if (progressCallback != null && fileSize > 0) {
progressCallback.accept((double) totalRead / fileSize * 100);
}
}
}
// 9. SHA-256 校验
String actualSha256 = java.util.HexFormat.of()
.formatHex(shaDigest.digest());
if (expectedSha256 != null && !expectedSha256.isEmpty()) {
if (!actualSha256.equalsIgnoreCase(expectedSha256)) {
Files.deleteIfExists(targetPath);
return DownloadResult.fail("SHA-256 校验失败: expected=" + expectedSha256
+ " actual=" + actualSha256);
}
}
AssertLog.info("[DOWNLOAD] 成功 url={} path={} size={} sha256={}",
fileUrl, targetPath, totalRead, actualSha256);
return DownloadResult.ok(targetPath.toString(), totalRead, actualSha256);
} catch (IOException e) {
AssertLog.error("[DOWNLOAD] 下载失败: {} - {}", fileUrl, e.getMessage());
return DownloadResult.fail("下载失败: " + e.getMessage());
} catch (Exception e) {
AssertLog.error("[DOWNLOAD] 异常: {} - {}", fileUrl, e.getMessage());
return DownloadResult.fail("下载异常: " + e.getMessage());
}
}, executor);
}
/** 下载结果 */
public static class DownloadResult {
private final boolean success;
private final String filePath;
private final long size;
private final String sha256;
private final String reason;
private DownloadResult(boolean success, String filePath, long size, String sha256, String reason) {
this.success = success;
this.filePath = filePath;
this.size = size;
this.sha256 = sha256;
this.reason = reason;
}
public static DownloadResult ok(String filePath, long size, String sha256) {
return new DownloadResult(true, filePath, size, sha256, null);
}
public static DownloadResult fail(String reason) {
return new DownloadResult(false, null, 0, null, reason);
}
public boolean isSuccess() { return success; }
public String getFilePath() { return filePath; }
public long getSize() { return size; }
public String getSha256() { return sha256; }
public String getReason() { return reason; }
}
/** 优雅关闭 */
public void shutdown() {
executor.shutdown();
try {
if (!executor.awaitTermination(5, java.util.concurrent.TimeUnit.SECONDS)) {
executor.shutdownNow();
}
} catch (InterruptedException e) {
executor.shutdownNow();
Thread.currentThread().interrupt();
}
}
}
@@ -0,0 +1,106 @@
package com.tongran.agent.client.security;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import java.util.ArrayList;
import java.util.List;
/**
* 安全配置
*
* <p>对应 application.yml 中 {@code agent.security} 前缀的所有配置项。
* 集中管理命令执行白名单、下载白名单、签名密钥、Netty 握手 token 等。
*
* <p>建议在 application.yml 中加入:
* <pre>
* agent:
* security:
* # 命令执行白名单:只允许执行该目录下的预置脚本,禁止直接 /bin/sh -c 传字符串
* allowed-scripts-dir: /opt/tongran/scripts
* # 允许执行的脚本名(不含路径),严格白名单
* allowed-script-names:
* - restart.sh
* - update.sh
* - cleanup.sh
* # 命令执行超时(秒)
* command-timeout-seconds: 100
*
* # HMAC 签名密钥(服务端必须用相同密钥签名,客户端校验)
* # 重要:生产环境务必通过环境变量 AGENT_SIGN_SECRET 注入,不要明文写 yml
* sign-secret: ${AGENT_SIGN_SECRET:}
* # 签名有效期(秒),超过则拒绝(防重放)
* sign-ttl-seconds: 300
*
* # 下载安全
* download:
* # 允许的协议(只允许 https)
* allowed-protocols:
* - https
* # 允许的域名白名单
* allowed-hosts:
* - oss.tongran.com
* - files.tongran.com
* # 单文件大小上限(字节),默认 100MB
* max-size-bytes: 104857600
* # 是否强制校验 SHA-256
* sha256-required: true
* # 允许下载到的工作目录(防路径穿越)
* allowed-save-dir: /opt/tongran/downloads
*
* # Netty 握手认证 token(生产环境用环境变量注入)
* netty-auth-token: ${AGENT_NETTY_TOKEN:}
* # 握手超时(秒),连接建立后多久未完成握手则关闭
* handshake-timeout-seconds: 10
* </pre>
*
* @author Senior Developer
*/
@Data
@Configuration
@ConfigurationProperties(prefix = "agent.security")
public class SecurityProperties {
/** 允许执行脚本的目录(只允许该目录下的预置脚本) */
private String allowedScriptsDir = "/opt/tongran/scripts";
/** 允许执行的脚本名白名单(不含路径,严格匹配) */
private List<String> allowedScriptNames = new ArrayList<>();
/** 命令执行超时(秒) */
private long commandTimeoutSeconds = 100;
/** HMAC 签名密钥(服务端下发命令必须用此密钥签名) */
private String signSecret = "";
/** 签名有效期(秒),超过则拒绝,防重放 */
private long signTtlSeconds = 300;
/** 下载安全配置 */
private Download download = new Download();
/** Netty 握手认证 token */
private String nettyAuthToken = "";
/** 握手超时(秒) */
private long handshakeTimeoutSeconds = 10;
@Data
public static class Download {
/** 允许的协议 */
private List<String> allowedProtocols = new ArrayList<>();
/** 允许的域名白名单 */
private List<String> allowedHosts = new ArrayList<>();
/** 单文件大小上限(字节) */
private long maxSizeBytes = 104857600L; // 100MB
/** 是否强制校验 SHA-256 */
private boolean sha256Required = true;
/** 允许下载到的工作目录(防路径穿越) */
private String allowedSaveDir = "/opt/tongran/downloads";
}
}
@@ -0,0 +1,163 @@
package com.tongran.agent.client.security;
import com.tongran.agent.client.utils.AssertLog;
import org.springframework.stereotype.Component;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.PosixFilePermission;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/**
* 系统命令执行器(底层)
*
* <p>仅被 {@link SecureCommandExecutor} 调用,不对外暴露。
* 使用 {@link ProcessBuilder} 直接传参数数组,不经过 shell,杜绝命令注入。
*
* <p>设计为可注入的 Bean,方便单元测试 mock。
*
* @author Senior Developer
*/
@Component
public class SystemCommandRunner {
/** 业务线程池:命令执行是 IO 密集型,固定大小足够 */
private final ExecutorService executor = Executors.newFixedThreadPool(
Math.max(2, Runtime.getRuntime().availableProcessors()),
r -> {
Thread t = new Thread(r, "agent-cmd-exec");
t.setDaemon(true);
return t;
}
);
/**
* 异步执行脚本
*
* @param scriptPath 脚本绝对路径(已通过白名单校验)
* @param args 参数列表(已与脚本路径分离,不经过 shell)
* @param timeout 超时
* @param unit 超时单位
*/
public CompletableFuture<SecureCommandExecutor.CommandResult> execute(
Path scriptPath, List<String> args, long timeout, TimeUnit unit) {
return CompletableFuture.supplyAsync(() -> {
Process process = null;
try {
// 1. 确保脚本有执行权限(Linux/Unix)
ensureExecutable(scriptPath);
// 2. 构建命令:第一个元素是脚本路径,后续是参数
// 关键:不使用 /bin/sh -c,参数直接传给 ProcessBuilder,不经过 shell 解析
List<String> command = new ArrayList<>();
command.add(scriptPath.toString());
if (args != null) {
command.addAll(args);
}
AssertLog.info("[CMD-EXEC] exec={} args={}", scriptPath, args);
// 3. 启动进程
ProcessBuilder pb = new ProcessBuilder(command);
pb.redirectErrorStream(false); // 分别读 stdout/stderr
process = pb.start();
// 4. 异步读取输出(防止 buffer 满死锁)
Process finalProcess = process;
CompletableFuture<String> stdoutFuture = CompletableFuture.supplyAsync(
() -> readStream(finalProcess.getInputStream()), executor);
CompletableFuture<String> stderrFuture = CompletableFuture.supplyAsync(
() -> readStream(finalProcess.getErrorStream()), executor);
// 5. 等待进程结束(带超时)
boolean finished = process.waitFor(timeout, unit);
if (!finished) {
process.destroyForcibly();
AssertLog.error("[CMD-EXEC] 超时强杀: {}", scriptPath);
return SecureCommandExecutor.CommandResult.fail("执行超时");
}
// 6. 读取输出(给 2 秒缓冲让读流线程收尾)
String stdout = stdoutFuture.get(2, TimeUnit.SECONDS);
String stderr = stderrFuture.get(2, TimeUnit.SECONDS);
int exitCode = process.exitValue();
AssertLog.info("[CMD-EXEC] done exit={} script={}", exitCode, scriptPath);
return SecureCommandExecutor.CommandResult.ok(exitCode, stdout, stderr);
} catch (TimeoutException te) {
return SecureCommandExecutor.CommandResult.fail("读取输出超时");
} catch (Exception e) {
AssertLog.error("[CMD-EXEC] 执行异常: {}", e.getMessage());
return SecureCommandExecutor.CommandResult.fail("执行异常: " + e.getMessage());
} finally {
if (process != null && process.isAlive()) {
process.destroyForcibly();
}
}
}, executor);
}
/** 确保脚本有执行权限(仅对 POSIX 系统生效) */
private void ensureExecutable(Path path) {
try {
Set<String> fileStores = new HashSet<>();
path.getFileSystem().getFileStores().forEach(fs -> fileStores.add(fs.type()));
// 简单判断:如果是 POSIX 文件系统,加执行权限
if (Files.getFileAttributeView(path, java.nio.file.attribute.PosixFileAttributeView.class) != null) {
Set<PosixFilePermission> perms = new HashSet<>(Files.getPosixFilePermissions(path));
perms.add(PosixFilePermission.OWNER_EXECUTE);
perms.add(PosixFilePermission.GROUP_EXECUTE);
Files.setPosixFilePermissions(path, perms);
}
// Windows 不需要执行权限,直接跳过
} catch (Exception e) {
AssertLog.warn("设置执行权限失败(非致命): {} - {}", path, e.getMessage());
}
}
/** 读取输入流到字符串 */
private String readStream(java.io.InputStream is) {
StringBuilder sb = new StringBuilder();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
sb.append(line).append('\n');
}
} catch (Exception e) {
sb.append("[read-error] ").append(e.getMessage());
}
// 限制输出长度,防止日志爆炸
String result = sb.toString();
if (result.length() > 8192) {
return result.substring(0, 8192) + "...[truncated]";
}
return result;
}
/** 优雅关闭 */
public void shutdown() {
executor.shutdown();
try {
if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
executor.shutdownNow();
}
} catch (InterruptedException e) {
executor.shutdownNow();
Thread.currentThread().interrupt();
}
}
}
@@ -15,6 +15,7 @@ import com.tongran.agent.client.scheduler.service.BusinessTasks;
import com.tongran.agent.client.scheduler.service.DynamicTaskService;
import com.tongran.agent.client.scheduler.task.SpecificTimeRequest;
import com.tongran.agent.client.scheduler.task.SpecificTimeTaskService;
import com.tongran.agent.client.security.SecureCommandExecutor;
import com.tongran.agent.client.service.AgentService;
import com.tongran.agent.client.utils.AgentUtil;
import com.tongran.agent.client.utils.AssertLog;
@@ -26,6 +27,7 @@ import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
@@ -42,6 +44,9 @@ public class AgentServiceImpl implements AgentService {
@Resource
private SpecificTimeTaskService taskService;
@Resource
private SecureCommandExecutor secureCommandExecutor;
// 在注入点使用@Lazy
@Autowired
public AgentServiceImpl(DynamicTaskService dynamicTaskService,
@@ -840,76 +845,71 @@ public class AgentServiceImpl implements AgentService {
}
}
}else{
// 立即执行:遍历每条命令,逐个走 SecureCommandExecutor 安全通道
for (String command : policy.getCommands()) {
if(StringUtils.equals(dataType, MsgEnum.Agent版本更新应答.getValue())){
try {
System.out.println("重启进程已启动,当前服务退出");
System.out.println("command="+command);
ProcessBuilder pb = new ProcessBuilder("/bin/sh", "-c",
command);
pb.start();
System.exit(0);
} catch (IOException e) {
e.printStackTrace();
}
}else{
List<String> cmd = Arrays.asList(command.split("\\s+"));
CompletableFuture<AsyncCommandExecutor.CommandResult> future =
AsyncCommandExecutor.executeCommandAsync(
cmd,
100, TimeUnit.SECONDS);
future.thenAccept(result -> {
if (result.isSuccess()) {
System.out.println("脚本执行成功");
System.out.println("[成功resOut] " + result.getOutput());
} else {
System.out.println("脚本执行失败");
System.out.println("[失败resOut] " + result.getOutput());
}
JSONObject rse = new JSONObject();
rse.put("command",command);
rse.put("resOut", result.getOutput());
long timestamps = System.currentTimeMillis();
timestamps = Math.round(timestamps / 1000.0);
JSONObject json = new JSONObject();
json.put("resCode",1);
json.put("resMsg", "");
json.put("timestamp",timestamps);
json.put("result", rse.toString());
// 判定客户端与服务端是否连接
if (Objects.nonNull(sessionManager.getSessionById(GlobalConfig.CLIENT_ID))) {
Message message = Message.builder().clientId(GlobalConfig.CLIENT_ID).dataType(dataType)
.data(json.toString()).build();
sessionManager.writeAndFlush(sessionManager.getSessionById(GlobalConfig.CLIENT_ID).getChannel(), message);
AssertLog.info("发送执行脚本策略应答={}",JSON.toJSONString(message));
}
}).exceptionally(ex -> {
System.err.println("执行失败: " + ex.getMessage());
JSONObject rse = new JSONObject();
rse.put("command",command);
rse.put("resOut", "脚本执行失败");
long timestamps = System.currentTimeMillis();
timestamps = Math.round(timestamps / 1000.0);
JSONObject json = new JSONObject();
json.put("resCode",1);
json.put("resMsg", "");
json.put("timestamp",timestamps);
json.put("result", rse.toString());
// 判定客户端与服务端是否连接
if (Objects.nonNull(sessionManager.getSessionById(GlobalConfig.CLIENT_ID))) {
Message message = Message.builder().clientId(GlobalConfig.CLIENT_ID).dataType(dataType)
.data(json.toString()).build();
sessionManager.writeAndFlush(sessionManager.getSessionById(GlobalConfig.CLIENT_ID).getChannel(), message);
AssertLog.info("发送执行脚本策略应答={}",JSON.toJSONString(message));
}
return null;
});
// 解析下发的 JSON:{"script":"xxx.sh","args":[...],"sign":"v1:..."}
// 兼容旧格式:如果 command 不是 JSON,直接拒绝(防止绕过白名单)
String scriptName;
List<String> scriptArgs;
String sign;
try {
JSONObject cmdJson = JSONObject.parseObject(command);
scriptName = cmdJson.getString("script");
scriptArgs = cmdJson.getJSONArray("args") != null
? cmdJson.getJSONArray("args").toJavaList(String.class)
: Collections.emptyList();
sign = cmdJson.getString("sign");
} catch (Exception e) {
// 非 JSON 格式:旧协议直传 shell 字符串,拒绝执行
AssertLog.error("[SECURITY] 拒绝非白名单格式命令: clientId={} dataType={}", clientId, dataType);
sendCommandResponse(dataType, command, "[rejected] 命令格式非法,必须为 JSON 且携带 script/args/sign", 0);
continue;
}
// 走安全执行器:白名单 + 签名 + 路径穿越防护
CompletableFuture<SecureCommandExecutor.CommandResult> future =
secureCommandExecutor.execute(scriptName, scriptArgs, sign);
future.thenAccept(result -> {
if (result.isSuccess()) {
AssertLog.info("[CMD] 脚本执行成功 script={}", scriptName);
} else {
AssertLog.error("[CMD] 脚本执行失败 script={} reason={}", scriptName, result.getReason());
}
sendCommandResponse(dataType, scriptName, result.getOutput(), result.isSuccess() ? 1 : 0);
}).exceptionally(ex -> {
AssertLog.error("[CMD] 执行异常: {}", ex.getMessage());
sendCommandResponse(dataType, scriptName, "脚本执行异常", 0);
return null;
});
}
}
}
}
/**
* 发送命令执行应答(抽取的公共方法,避免重复代码)
*/
private void sendCommandResponse(String dataType, String command, String output, int resCode) {
JSONObject rse = new JSONObject();
rse.put("command", command);
rse.put("resOut", output);
long timestamps = System.currentTimeMillis();
timestamps = Math.round(timestamps / 1000.0);
JSONObject json = new JSONObject();
json.put("resCode", resCode);
json.put("resMsg", "");
json.put("timestamp", timestamps);
json.put("result", rse.toString());
// 判定客户端与服务端是否连接
if (Objects.nonNull(sessionManager.getSessionById(GlobalConfig.CLIENT_ID))) {
Message message = Message.builder().clientId(GlobalConfig.CLIENT_ID).dataType(dataType)
.data(json.toString()).build();
sessionManager.writeAndFlush(sessionManager.getSessionById(GlobalConfig.CLIENT_ID).getChannel(), message);
AssertLog.info("发送执行脚本策略应答={}", JSON.toJSONString(message));
}
}
public void caseTypeBySystem(String type, int interval, boolean collect){
@@ -2,6 +2,7 @@ package com.tongran.agent.client.service.impl;
import com.tongran.agent.client.core.vo.CpuVO;
import com.tongran.agent.client.service.CPUService;
import com.tongran.agent.client.utils.AssertLog;
import org.springframework.stereotype.Service;
import oshi.SystemInfo;
@@ -14,6 +15,10 @@ import java.util.concurrent.TimeUnit;
@Service
public class CPUServiceImpl implements CPUService {
/** 缓存上次的 CPU ticks,避免每次采集都 sleep 1 秒 */
private volatile long[] prevTicks = null;
private volatile long prevTicksTimestamp = 0;
@Override
public CpuVO get() {
CpuVO cpuVO = CpuVO.builder().build();
@@ -22,76 +27,86 @@ public class CPUServiceImpl implements CPUService {
HardwareAbstractionLayer hal = si.getHardware();
CentralProcessor processor = hal.getProcessor();
OperatingSystem os = si.getOperatingSystem();
System.out.println("=========================================================");
// 1. CPU基本信息
cpuVO.setNum(processor.getPhysicalProcessorCount()); // CUP数量
System.out.println("=== CPU基本信息 ===");
System.out.println("CPU型号: " + processor.getProcessorIdentifier().getName());
System.out.println("物理核心数: " + processor.getPhysicalProcessorCount());
System.out.println("逻辑核心数: " + processor.getLogicalProcessorCount());
System.out.println("最大频率: " + processor.getMaxFreq() / 1_000_000.0 + " GHz");
cpuVO.setNum(processor.getPhysicalProcessorCount());
AssertLog.info("CPU型号: {}, 物理核心数: {}, 逻辑核心数: {}, 最大频率: {} GHz",
processor.getProcessorIdentifier().getName(),
processor.getPhysicalProcessorCount(),
processor.getLogicalProcessorCount(),
processor.getMaxFreq() / 1_000_000.0);
// 2. CPU负载信息
System.out.println("\n=== CPU负载信息 ===");
double[] loadAverage = processor.getSystemLoadAverage(3);
cpuVO.setAvg1(loadAverage[0]);// CUP1分钟负载
cpuVO.setAvg1(loadAverage[1]);// CUP5分钟负载
cpuVO.setAvg1(loadAverage[2]);// CUP15分钟负载
System.out.println("1分钟平均负载: " + loadAverage[0]);
System.out.println("5分钟平均负载: " + loadAverage[1]);
System.out.println("15分钟平均负载: " + loadAverage[2]);
// 3. CPU使用率(需要两次采样)
System.out.println("\n=== CPU使用率 ===");
long[] prevTicks = processor.getSystemCpuLoadTicks();
TimeUnit.SECONDS.sleep(1); // 等待1秒
// 计算使用率
double cpuUsage = processor.getSystemCpuLoadBetweenTicks(prevTicks);
cpuVO.setUti(cpuUsage * 100); // CPU使用率
System.out.printf("CPU 使用率: %.2f%%\n", cpuUsage * 100);
long[] ticks = processor.getSystemCpuLoadTicks();
long user = ticks[CentralProcessor.TickType.USER.getIndex()] -
cpuVO.setAvg1(loadAverage[0]); // CPU 1分钟负载
cpuVO.setAvg5(loadAverage[1]); // CPU 5分钟负载
cpuVO.setAvg15(loadAverage[2]); // CPU 15分钟负载
// 3. CPU使用率 - 使用缓存的 ticks 避免每次 sleep 1 秒
long[] currentTicks = processor.getSystemCpuLoadTicks();
long currentTime = System.currentTimeMillis();
double cpuUsage = 0;
if (prevTicks != null && currentTime - prevTicksTimestamp >= 500) {
// 使用上次缓存的 ticks 计算使用率
cpuUsage = processor.getSystemCpuLoadBetweenTicks(prevTicks);
} else {
// 首次采集或间隔太短,需要等待
prevTicks = processor.getSystemCpuLoadTicks();
prevTicksTimestamp = currentTime;
TimeUnit.MILLISECONDS.sleep(500);
currentTicks = processor.getSystemCpuLoadTicks();
cpuUsage = processor.getSystemCpuLoadBetweenTicks(prevTicks);
}
cpuVO.setUti(cpuUsage * 100);
long user = currentTicks[CentralProcessor.TickType.USER.getIndex()] -
prevTicks[CentralProcessor.TickType.USER.getIndex()];
long nice = ticks[CentralProcessor.TickType.NICE.getIndex()] -
long nice = currentTicks[CentralProcessor.TickType.NICE.getIndex()] -
prevTicks[CentralProcessor.TickType.NICE.getIndex()];
long sys = ticks[CentralProcessor.TickType.SYSTEM.getIndex()] -
long sys = currentTicks[CentralProcessor.TickType.SYSTEM.getIndex()] -
prevTicks[CentralProcessor.TickType.SYSTEM.getIndex()];
long idle = ticks[CentralProcessor.TickType.IDLE.getIndex()] -
long idle = currentTicks[CentralProcessor.TickType.IDLE.getIndex()] -
prevTicks[CentralProcessor.TickType.IDLE.getIndex()];
long iowait = ticks[CentralProcessor.TickType.IOWAIT.getIndex()] -
long iowait = currentTicks[CentralProcessor.TickType.IOWAIT.getIndex()] -
prevTicks[CentralProcessor.TickType.IOWAIT.getIndex()];
long irq = ticks[CentralProcessor.TickType.IRQ.getIndex()] -
long irq = currentTicks[CentralProcessor.TickType.IRQ.getIndex()] -
prevTicks[CentralProcessor.TickType.IRQ.getIndex()];
long softirq = ticks[CentralProcessor.TickType.SOFTIRQ.getIndex()] -
long softirq = currentTicks[CentralProcessor.TickType.SOFTIRQ.getIndex()] -
prevTicks[CentralProcessor.TickType.SOFTIRQ.getIndex()];
long steal = ticks[CentralProcessor.TickType.STEAL.getIndex()] -
long steal = currentTicks[CentralProcessor.TickType.STEAL.getIndex()] -
prevTicks[CentralProcessor.TickType.STEAL.getIndex()];
long total = user + nice + sys + idle + iowait + irq + softirq + steal;
// 4. CPU时间累计值
System.out.println("\n=== CPU时间累计值 ===");
long[] allTicks = processor.getSystemCpuLoadTicks();
System.out.println("中断累计时间: " +
(allTicks[CentralProcessor.TickType.IRQ.getIndex()] +
allTicks[CentralProcessor.TickType.SOFTIRQ.getIndex()]));
System.out.println("空闲累计时间: " + allTicks[CentralProcessor.TickType.IDLE.getIndex()]);
System.out.printf("I/O等待时间(CPU等待响应时间): %.2f%%\n", 100d * iowait / total);
System.out.println("系统累计时间: " + allTicks[CentralProcessor.TickType.SYSTEM.getIndex()]);
long softwareTime = sys - irq - softirq;
System.out.printf("软件相关时间(近似无响应时间): %.2f%%\n", 100d * softwareTime / total);
System.out.println("用户进程累计时间: " + allTicks[CentralProcessor.TickType.USER.getIndex()]);
if (total > 0) {
cpuVO.setIowait(100d * iowait / total);
long softwareTime = sys - irq - softirq;
cpuVO.setNoresp(100d * softwareTime / total);
} else {
cpuVO.setIowait(0d);
cpuVO.setNoresp(0d);
}
cpuVO.setInterrupt((allTicks[CentralProcessor.TickType.IRQ.getIndex()] +
allTicks[CentralProcessor.TickType.SOFTIRQ.getIndex()])); // CPU硬件中断提供服务时间
cpuVO.setIdle(allTicks[CentralProcessor.TickType.IDLE.getIndex()]);// CPU空闲时间
cpuVO.setIowait(100d * iowait / total);// CPU等待响应时间
cpuVO.setSystem(allTicks[CentralProcessor.TickType.SYSTEM.getIndex()]);// CPU系统时间
cpuVO.setNoresp(100d * softwareTime / total);// CPU软件无响应时间
cpuVO.setUser(allTicks[CentralProcessor.TickType.USER.getIndex()]);// CPU用户进程所花费的时间
cpuVO.setInterrupt(currentTicks[CentralProcessor.TickType.IRQ.getIndex()] +
currentTicks[CentralProcessor.TickType.SOFTIRQ.getIndex()]);
cpuVO.setIdle(currentTicks[CentralProcessor.TickType.IDLE.getIndex()]);
cpuVO.setSystem(currentTicks[CentralProcessor.TickType.SYSTEM.getIndex()]);
cpuVO.setUser(currentTicks[CentralProcessor.TickType.USER.getIndex()]);
// 5. 系统运行时间和CPU空闲时间
// 更新缓存
prevTicks = currentTicks;
prevTicksTimestamp = currentTime;
// 5. 系统运行时间
long uptime = os.getSystemUptime();
cpuVO.setNormal(uptime);// CPU正常运行时间
System.out.println("CPU正常运行时间: " + cpuVO.getNormal());
cpuVO.setNormal(uptime);
} catch (InterruptedException e) {
e.printStackTrace();
Thread.currentThread().interrupt();
AssertLog.error("CPU采集被中断", e);
} catch (Exception e) {
AssertLog.error("CPU采集异常", e);
}
return cpuVO;
}
@@ -3,6 +3,7 @@ package com.tongran.agent.client.service.impl;
import com.tongran.agent.client.core.vo.DiskVO;
import com.tongran.agent.client.core.vo.PointVO;
import com.tongran.agent.client.service.DiskService;
import com.tongran.agent.client.utils.AssertLog;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import oshi.SystemInfo;
@@ -20,29 +21,24 @@ public class DiskServiceImpl implements DiskService {
List<DiskVO> tempList = new ArrayList<>();
List<DiskVO> resultList = new ArrayList<>();
SystemInfo si = new SystemInfo();
System.out.println("=========================================================");
// 获取磁盘IO信息
System.out.println("\n=== 磁盘IO信息 ===");
for (HWDiskStore disk : si.getHardware().getDiskStores()) {
DiskVO diskVO = DiskVO.builder().timestamp(timestamp).build();
diskVO.setName(disk.getName());//磁盘名称
diskVO.setSerial(disk.getSerial());//序列号
diskVO.setTotal(disk.getSize());//磁盘大小
diskVO.setWriteTimes(disk.getWrites());//磁盘写入次数
diskVO.setReadTimes(disk.getReads());//磁盘读取次数
diskVO.setWriteBytes(disk.getReadBytes());//磁盘写入字节
diskVO.setReadBytes(disk.getWriteBytes());//磁盘读取字节
diskVO.setName(disk.getName());
diskVO.setSerial(disk.getSerial());
diskVO.setTotal(disk.getSize());
diskVO.setWriteTimes(disk.getWrites());
diskVO.setReadTimes(disk.getReads());
diskVO.setWriteBytes(disk.getWriteBytes()); // 修复:写入字节
diskVO.setReadBytes(disk.getReadBytes()); // 修复:读取字节
tempList.add(diskVO);
System.out.println("磁盘名称: " + diskVO.getName());
System.out.println("序列号: " + diskVO.getSerial());
System.out.println("磁盘大小: " + diskVO.getTotal());
System.out.println("磁盘写入次数: " + diskVO.getWriteTimes());
System.out.println("磁盘读取次数: " + diskVO.getReadTimes());
System.out.println("磁盘写入字节: " + diskVO.getWriteBytes());
System.out.println("磁盘读取字节: " + diskVO.getReadBytes());
AssertLog.info("磁盘: {}, 序列号: {}, 大小: {}, 写入字节: {}, 读取字节: {}",
diskVO.getName(), diskVO.getSerial(), diskVO.getTotal(),
diskVO.getWriteBytes(), diskVO.getReadBytes());
}
try{
try {
// 第一次采样
List<HWDiskStore> disks1 = si.getHardware().getDiskStores();
long[] readBytes1 = new long[disks1.size()];
@@ -59,18 +55,20 @@ public class DiskServiceImpl implements DiskService {
long readDiff = disks2.get(i).getReadBytes() - readBytes1[i];
long writeDiff = disks2.get(i).getWriteBytes() - writeBytes1[i];
String serial = disks2.get(i).getSerial();
DiskVO diskVO = tempList.stream().filter(d -> StringUtils.equals(d.getSerial(),serial)).findFirst().orElse(null);
if(Objects.nonNull(diskVO)){
diskVO.setWriteSpeed(readDiff);//磁盘写入速率
diskVO.setReadSpeed(writeDiff);//磁盘读取速率
System.out.println("磁盘名称: " + diskVO.getName());
System.out.println("磁盘写入速率: " + diskVO.getWriteSpeed());
System.out.println("磁盘读取速率: " + diskVO.getReadSpeed());
DiskVO diskVO = tempList.stream().filter(d -> StringUtils.equals(d.getSerial(), serial)).findFirst().orElse(null);
if (Objects.nonNull(diskVO)) {
diskVO.setWriteSpeed(writeDiff); // 修复:写入速率 = 写入差值
diskVO.setReadSpeed(readDiff); // 修复:读取速率 = 读取差值
AssertLog.info("磁盘: {}, 写入速率: {}, 读取速率: {}",
diskVO.getName(), diskVO.getWriteSpeed(), diskVO.getReadSpeed());
}
resultList.add(diskVO);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
AssertLog.error("磁盘采集被中断", e);
} catch (Exception e) {
e.printStackTrace();
AssertLog.error("磁盘速率采集异常", e);
}
return resultList;
}
@@ -80,8 +78,6 @@ public class DiskServiceImpl implements DiskService {
List<PointVO> list = new ArrayList<>();
SystemInfo si = new SystemInfo();
// 获取文件系统信息
System.out.println("=========================================================");
System.out.println("=== 挂载信息 ===");
for (OSFileStore fs : si.getOperatingSystem().getFileSystem().getFileStores()) {
long totalSpace = fs.getTotalSpace();
long usableSpace = fs.getUsableSpace();
@@ -90,18 +86,15 @@ public class DiskServiceImpl implements DiskService {
(double) (totalSpace - freeSpace) / totalSpace * 100 : 0;
PointVO pointVO = PointVO.builder().timestamp(timestamp).build();
pointVO.setMount(fs.getMount());//挂载点
pointVO.setVfsType(fs.getType());//文件系统类型
pointVO.setVfsTotal(totalSpace);//总空间
pointVO.setVfsFree(usableSpace);//可用空间
pointVO.setVfsUtil(usagePercentage);//空间利用率
pointVO.setMount(fs.getMount());
pointVO.setVfsType(fs.getType());
pointVO.setVfsTotal(totalSpace);
pointVO.setVfsFree(usableSpace);
pointVO.setVfsUtil(usagePercentage);
list.add(pointVO);
System.out.println("挂载点: " + pointVO.getMount());
System.out.println("文件系统类型: " + pointVO.getVfsType());
System.out.println("总空间: " + pointVO.getVfsTotal());
System.out.println("可用空间: " + pointVO.getVfsFree());
System.out.printf("空间利用率: %.2f%%\n", usagePercentage);
AssertLog.info("挂载点: {}, 类型: {}, 总空间: {}, 可用: {}, 利用率: {}%",
pointVO.getMount(), pointVO.getVfsType(), pointVO.getVfsTotal(),
pointVO.getVfsFree(), String.format("%.2f", usagePercentage));
}
return list;
}
@@ -8,6 +8,7 @@ import com.github.dockerjava.httpclient5.ApacheDockerHttpClient;
import com.github.dockerjava.transport.DockerHttpClient;
import com.tongran.agent.client.core.vo.DockerVO;
import com.tongran.agent.client.service.DockerService;
import com.tongran.agent.client.utils.AssertLog;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
@@ -17,44 +18,32 @@ import java.io.InputStreamReader;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
@Service
public class DockerServiceImpl implements DockerService {
/** 复用 DockerClient 配置,避免每次创建 */
private volatile DockerClient dockerClientCache;
private volatile DockerHttpClient httpClientCache;
@Override
public List<DockerVO> dockerList(long timestamp) {
List<DockerVO> list = new ArrayList<>();
// 配置Docker客户端
DefaultDockerClientConfig config = DefaultDockerClientConfig.createDefaultConfigBuilder()
.withDockerHost("unix:///var/run/docker.sock")
.withDockerTlsVerify(false) // 根据你的配置调整
.build();
DockerHttpClient httpClient = new ApacheDockerHttpClient.Builder()
.dockerHost(config.getDockerHost())
.sslConfig(config.getSSLConfig())
.maxConnections(100)
.connectionTimeout(Duration.ofSeconds(30))
.responseTimeout(Duration.ofSeconds(45))
.build();
DockerClient dockerClient = DockerClientImpl.getInstance(config, httpClient);
DockerClient dockerClient = getDockerClient();
try {
// 获取正在运行的容器列表
List<Container> containers = dockerClient.listContainersCmd()
.withShowAll(false) // 只显示运行中的容器
.withShowAll(false)
.exec();
// 打印容器信息
System.out.println("运行中的Docker容器:");
System.out.println("容器ID\t\t镜像\t\t状态\t\t名称");
for (Container container : containers) {
DockerVO dockerVO = DockerVO.builder().timestamp(timestamp).build();
String id = container.getId().substring(0, 12); // 只显示短ID
String image = container.getImage().length() > 15 ?
String id = container.getId().substring(0, 12);
String image = container.getImage() != null && container.getImage().length() > 15 ?
container.getImage().substring(0, 15) + "..." : container.getImage();
String status = container.getStatus();
String name = container.getNames()[0].replaceFirst("/", "");
System.out.printf("%s\t%s\t%s\t%s%n", id, image, status, name);
AssertLog.info("Docker容器: id={}, image={}, status={}, name={}", id, image, status, name);
dockerVO.setId(id);
dockerVO.setName(name);
dockerVO.setStatus(status);
@@ -62,74 +51,115 @@ public class DockerServiceImpl implements DockerService {
list.add(res);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
dockerClient.close();
} catch (IOException e) {
e.printStackTrace();
}
AssertLog.error("Docker容器列表获取异常", e);
}
return list;
}
/**
* 获取 Docker 客户端(复用连接)
*/
private DockerClient getDockerClient() {
if (dockerClientCache == null) {
synchronized (this) {
if (dockerClientCache == null) {
DefaultDockerClientConfig config = DefaultDockerClientConfig.createDefaultConfigBuilder()
.withDockerHost("unix:///var/run/docker.sock")
.withDockerTlsVerify(false)
.build();
httpClientCache = new ApacheDockerHttpClient.Builder()
.dockerHost(config.getDockerHost())
.sslConfig(config.getSSLConfig())
.maxConnections(100)
.connectionTimeout(Duration.ofSeconds(30))
.responseTimeout(Duration.ofSeconds(45))
.build();
dockerClientCache = DockerClientImpl.getInstance(config, httpClientCache);
}
}
}
return dockerClientCache;
}
/**
* 获取容器资源使用统计 - 修复:try-with-resources + 进程超时 + 数组越界保护
*/
public DockerVO dockerStats(DockerVO dockerVO) {
//判定目标容器 ID
if(StringUtils.isBlank(dockerVO.getId())){
if (StringUtils.isBlank(dockerVO.getId())) {
return dockerVO;
}
Process process = null;
try {
// 执行 docker stats 命令(--no-stream 表示只输出一次)
Process process = new ProcessBuilder(
process = new ProcessBuilder(
"docker", "stats", "--no-stream", dockerVO.getId(),
"--format", "'table {{.ID}}\t{{.Name}}\t{{.CPUPerc}}\t{{.MemPerc}}\t{{.NetIO}}"
).start();
// 读取命令输出
BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream())
);
String line;
while ((line = reader.readLine()) != null) {
// 解析输出(示例输出格式):
// "your_container_id 0.00% 0.000 CPU % 0 B / 0 B 0 packets / 0 packets"
// 实际输出可能因 Docker 版本不同而变化,需根据实际情况调整正则表达式
if (line.contains(dockerVO.getId())) {
String[] parts = line.trim().split("\\s+");
String cpuUtil = parts[3]; // cpu使用率
String memUtil = parts[4]; // 内存使用率
// 网络流量通常在第 4 列(接收)和第 5 列(发送),格式为 "B/s" 或 "B"
dockerVO.setCpuUtil(cpuUtil);
String rxRate = "0B";// 接收速率(如 "1.23kB/s"
String txRate = "0B";// 发送速率(如 "4.56kB/s"
//数据间有空格
if(parts.length > 9){
rxRate = parts[5]+parts[6];
txRate = parts[8]+parts[9];
}else{
rxRate = parts[5];
txRate = parts[7];
// 修复:使用 try-with-resources 管理 reader
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
if (line.contains(dockerVO.getId())) {
String[] parts = line.trim().split("\\s+");
// 修复:数组越界保护
if (parts.length < 6) {
AssertLog.warn("docker stats 输出格式异常,字段不足: {}", line);
continue;
}
String cpuUtil = parts[3];
String memUtil = parts[4];
String rxRate;
String txRate;
if (parts.length > 9) {
rxRate = parts[5] + parts[6];
txRate = parts[8] + parts[9];
} else {
rxRate = parts[5];
txRate = parts.length > 7 ? parts[7] : "0B";
}
dockerVO.setCpuUtil(cpuUtil);
dockerVO.setMemUtil(memUtil);
dockerVO.setNetInSpeed(rxRate);
dockerVO.setNetOutSpeed(txRate);
AssertLog.info("容器网络流量 - 接收: {}, 发送: {}", rxRate, txRate);
}
// 网络流量通常在第 4 列(接收)和第 5 列(发送),格式为 "B/s" 或 "B"
dockerVO.setCpuUtil(cpuUtil);
dockerVO.setMemUtil(memUtil);
dockerVO.setNetInSpeed(rxRate);
dockerVO.setNetOutSpeed(txRate);
System.out.println("==================== 容器网络流量 ====================");
System.out.println("接收速率: " + rxRate);
System.out.println("发送速率: " + txRate);
}
}
// 等待命令执行完成并获取退出码
int exitCode = process.waitFor();
if (exitCode != 0) {
System.err.println("命令执行失败,退出码: " + exitCode);
// 修复:设置超时
boolean finished = process.waitFor(10, TimeUnit.SECONDS);
if (!finished) {
process.destroyForcibly();
AssertLog.warn("docker stats 命令超时");
} else if (process.exitValue() != 0) {
AssertLog.warn("docker stats 命令执行失败,退出码: {}", process.exitValue());
}
} catch (IOException e) {
AssertLog.error("docker stats IO异常", e);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
AssertLog.error("docker stats 被中断", e);
} finally {
if (process != null && process.isAlive()) {
process.destroyForcibly();
}
reader.close();
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
return dockerVO;
}
/**
* 应用关闭时清理资源
*/
public void cleanup() {
try {
if (dockerClientCache != null) {
dockerClientCache.close();
}
if (httpClientCache != null) {
httpClientCache.close();
}
} catch (IOException e) {
AssertLog.error("Docker客户端关闭异常", e);
}
}
}
@@ -3,6 +3,7 @@ package com.tongran.agent.client.service.impl;
import com.tongran.agent.client.core.vo.MemoryVO;
import com.tongran.agent.client.service.MemoryService;
import com.tongran.agent.client.utils.AgentDataUtil;
import com.tongran.agent.client.utils.AssertLog;
import org.springframework.stereotype.Service;
import java.io.IOException;
@@ -15,60 +16,48 @@ public class MemoryServiceImpl implements MemoryService {
MemoryVO memoryVO = MemoryVO.builder().build();
try {
Map<String, Long> memInfo = AgentDataUtil.parseMemInfo();
System.out.println("=========================================================");
// 1. 基本内存信息
System.out.println("=== 基本内存信息 (单位KB) ===");
System.out.println("总内存: " + memInfo.get("MemTotal"));
System.out.println("空闲内存: " + memInfo.get("MemFree"));
System.out.println("可用内存: " +
memInfo.getOrDefault("MemAvailable",
memInfo.get("MemFree") +
memInfo.getOrDefault("Buffers", 0L) +
memInfo.getOrDefault("Cached", 0L) +
memInfo.getOrDefault("SReclaimable", 0L)));
memoryVO.setAvailable(memInfo.getOrDefault("MemAvailable",
memInfo.get("MemFree") +
memInfo.getOrDefault("Buffers", 0L) +
memInfo.getOrDefault("Cached", 0L) +
memInfo.getOrDefault("SReclaimable", 0L))); //可用内存
memoryVO.setTotal(memInfo.get("MemTotal")); //总内存
memoryVO.setPercent((double) memoryVO.getAvailable() / memoryVO.getTotal() * 100); //可用内存百分比
// 3. 交换空间信息
System.out.println("\n=== 交换空间信息 ===");
long swapTotal = memInfo.getOrDefault("SwapTotal", 0L);
long swapFree = memInfo.getOrDefault("SwapFree", 0L);
System.out.println("总交换空间: " + swapTotal);
System.out.println("空闲交换空间: " + swapFree);
System.out.println("已用交换空间: " + (swapTotal - swapFree));
memoryVO.setSwapSizeFree(swapFree); //交换卷/文件的可用空间(字节)
memoryVO.setSwapSizePercent((double) swapFree / swapTotal * 100); //可用交换空间百分比
if (swapTotal > 0) {
System.out.printf("交换空间使用率: %.2f%%\n",
(double)(swapTotal - swapFree) / swapTotal * 100);
}
// 4. 内存使用率分析
System.out.println("\n=== 内存使用率分析 ===");
long total = memInfo.get("MemTotal");
long available = memInfo.getOrDefault("MemAvailable",
memInfo.get("MemFree") +
long memTotal = memInfo.getOrDefault("MemTotal", 0L);
long memAvailable = memInfo.getOrDefault("MemAvailable",
memInfo.getOrDefault("MemFree", 0L) +
memInfo.getOrDefault("Buffers", 0L) +
memInfo.getOrDefault("Cached", 0L) +
memInfo.getOrDefault("SReclaimable", 0L));
// 总内存使用率
double totalUsage = (double)(total - available) / total * 100;
System.out.printf("总内存使用率: %.2f%%\n", totalUsage);
// 实际内存使用率
memoryVO.setAvailable(memAvailable);
memoryVO.setTotal(memTotal);
// 修复:除零保护
memoryVO.setPercent(memTotal > 0 ? (double) memAvailable / memTotal * 100 : 0);
AssertLog.info("总内存: {} KB, 可用内存: {} KB, 可用百分比: {}%",
memTotal, memAvailable, String.format("%.2f", memoryVO.getPercent()));
// 2. 交换空间信息
long swapTotal = memInfo.getOrDefault("SwapTotal", 0L);
long swapFree = memInfo.getOrDefault("SwapFree", 0L);
memoryVO.setSwapSizeFree(swapFree);
// 修复:除零保护
memoryVO.setSwapSizePercent(swapTotal > 0 ? (double) swapFree / swapTotal * 100 : 0);
if (swapTotal > 0) {
AssertLog.info("交换空间 - 总计: {} KB, 空闲: {} KB, 已用: {} KB, 使用率: {}%",
swapTotal, swapFree, swapTotal - swapFree,
String.format("%.2f", (double) (swapTotal - swapFree) / swapTotal * 100));
}
// 3. 内存使用率分析
long cached = memInfo.getOrDefault("Cached", 0L) +
memInfo.getOrDefault("SReclaimable", 0L);
long buffers = memInfo.getOrDefault("Buffers", 0L);
double actualUsage = (double)(total - available - cached - buffers) / total * 100;
System.out.printf("实际内存使用率: %.2f%%\n", actualUsage);
memoryVO.setUntilzation(actualUsage); //内存利用率
double actualUsage = memTotal > 0 ?
(double) (memTotal - memAvailable - cached - buffers) / memTotal * 100 : 0;
memoryVO.setUntilzation(actualUsage);
AssertLog.info("内存利用率: {}%", String.format("%.2f", actualUsage));
} catch (IOException e) {
e.printStackTrace();
AssertLog.error("内存信息采集异常", e);
}
return memoryVO;
}
}
@@ -3,11 +3,11 @@ package com.tongran.agent.client.service.impl;
import com.tongran.agent.client.core.vo.NetVO;
import com.tongran.agent.client.service.NetService;
import com.tongran.agent.client.utils.AgentUtil;
import com.tongran.agent.client.utils.AssertLog;
import org.springframework.stereotype.Service;
import oshi.SystemInfo;
import oshi.hardware.HardwareAbstractionLayer;
import oshi.hardware.NetworkIF;
import oshi.util.FormatUtil;
import java.io.BufferedReader;
import java.io.InputStreamReader;
@@ -22,25 +22,20 @@ public class NetServiceImpl implements NetService {
try {
SystemInfo si = new SystemInfo();
HardwareAbstractionLayer hal = si.getHardware();
// 获取所有网络接口
List<NetworkIF> networkIFs = hal.getNetworkIFs();
System.out.println("=========================================================");
System.out.println("===== 网卡流量统计 =====");
List<NetVO> temp = new ArrayList<>();
for (NetworkIF net : networkIFs) {
System.out.println("接口名称: " + net.getName()+"("+net.getDisplayName()+")");
System.out.println("MAC地址: " + net.getMacaddr());
System.out.print("运行状态: ");
System.out.println(net.isConnectorPresent() ? "已连接" : "未连接");
System.out.println("接口类型: " + AgentUtil.getInterfaceType(net));
System.out.println("IPv4地址: " + String.join(", ", net.getIPv4addr()));
AssertLog.info("接口: {} ({}), MAC: {}, 状态: {}",
net.getName(), net.getDisplayName(), net.getMacaddr(),
net.isConnectorPresent() ? "已连接" : "未连接");
NetVO netVO = NetVO.builder().build();
netVO.setName(net.getName()+"("+net.getDisplayName()+")");//网卡名称
netVO.setMac(net.getMacaddr());//MAC
netVO.setStatus(net.isConnectorPresent() ? "已连接" : "未连接");//运行状态
netVO.setType(AgentUtil.getInterfaceType(net));//接口类型
netVO.setIpV4(String.join(", ", net.getIPv4addr()));//IPv4
netVO.setName(net.getName() + "(" + net.getDisplayName() + ")");
netVO.setMac(net.getMacaddr());
netVO.setStatus(net.isConnectorPresent() ? "已连接" : "未连接");
netVO.setType(AgentUtil.getInterfaceType(net));
netVO.setIpV4(String.join(", ", net.getIPv4addr()));
Map<String, String> map = getNetworkMode(net.getName());
if (map != null && !map.isEmpty()) {
netVO.setSpeed(map.get("speed"));
@@ -48,75 +43,89 @@ public class NetServiceImpl implements NetService {
}
temp.add(netVO);
}
// 2. 实时带宽监控(需要两次采样)
System.out.println("\n=== 实时带宽监控 ===");
// 第一次采样
// 实时带宽监控(需要两次采样)
for (NetworkIF net : networkIFs) {
net.updateAttributes();
}
// 等待1秒
TimeUnit.SECONDS.sleep(1);
// 第二次采样并计算速率
for (NetworkIF net : networkIFs) {
long prevBytesRecv = net.getBytesRecv();
long prevBytesSent = net.getBytesSent();
net.updateAttributes();
long bytesRecv = net.getBytesRecv() - prevBytesRecv;
long bytesSent = net.getBytesSent() - prevBytesSent;
System.out.println("接口: " + net.getName());
System.out.println("入站丢包: " + net.getInDrops());
System.out.println("出站丢包: " + net.getCollisions());
System.out.println("接收带宽: " + FormatUtil.formatBytes(bytesRecv) + "/s (" +
bytesToMbps(bytesRecv) + " Mbps)");
System.out.println("发送带宽: " + FormatUtil.formatBytes(bytesSent) + "/s (" +
bytesToMbps(bytesSent) + " Mbps)");
AssertLog.info("接口: {}, 入站丢包: {}, 出站丢包: {}, 接收: {} Mbps, 发送: {} Mbps",
net.getName(), net.getInDrops(), net.getCollisions(),
String.format("%.2f", bytesToMbps(bytesRecv)), String.format("%.2f", bytesToMbps(bytesSent)));
NetVO netVO = temp.stream().filter(n -> n.getIpV4().equals(String.join(", ", net.getIPv4addr()))
&& n.getMac().equals(net.getMacaddr())).findFirst().orElse(null);
if(Objects.nonNull(netVO)){
netVO.setInDropped(net.getInDrops());//入站丢包
netVO.setOutDropped(net.getCollisions());//出站丢包
netVO.setInSpeed(bytesRecv);//接收流量
netVO.setOutSpeed(bytesSent);//发送流量
if (Objects.nonNull(netVO)) {
netVO.setInDropped(net.getInDrops());
netVO.setOutDropped(net.getCollisions());
netVO.setInSpeed(bytesRecv);
netVO.setOutSpeed(bytesSent);
netVO.setTimestamp(timestamp);
list.add(netVO);
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
AssertLog.error("网络采集被中断", e);
} catch (Exception e) {
e.printStackTrace();
AssertLog.error("网络采集异常", e);
}
return list;
}
private static double bytesToMbps(long bytes) {
return bytes * 8.0 / 1_000_000; // bytes to megabits
return bytes * 8.0 / 1_000_000;
}
/**
* 获取网络接口模式信息 - 修复资源泄漏,使用 try-with-resources + 进程超时
*/
public static Map<String, String> getNetworkMode(String interfaceName) {
Map<String, String> result = new HashMap<>();
ProcessBuilder pb = new ProcessBuilder("ethtool", interfaceName);
pb.redirectErrorStream(true);
Process process = null;
try {
Process process = pb.start();
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
if (line.contains("Speed:")) {
result.put("speed", line.split(":")[1].trim());
} else if (line.contains("Duplex:")) {
result.put("duplex", line.split(":")[1].trim());
} else if (line.contains("Auto-negotiation:")) {
result.put("auto-negotiation", line.split(":")[1].trim());
process = pb.start();
// 修复:使用 try-with-resources 管理 reader
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
if (line.contains("Speed:")) {
String[] parts = line.split(":");
if (parts.length > 1) result.put("speed", parts[1].trim());
} else if (line.contains("Duplex:")) {
String[] parts = line.split(":");
if (parts.length > 1) result.put("duplex", parts[1].trim());
} else if (line.contains("Auto-negotiation:")) {
String[] parts = line.split(":");
if (parts.length > 1) result.put("auto-negotiation", parts[1].trim());
}
}
}
int exitCode = process.waitFor();
if (exitCode != 0) {
result.put("error", "ethtool command failed with exit code " + exitCode);
// 修复:设置超时
boolean finished = process.waitFor(5, TimeUnit.SECONDS);
if (!finished) {
process.destroyForcibly();
result.put("error", "ethtool command timed out");
} else if (process.exitValue() != 0) {
result.put("error", "ethtool command failed with exit code " + process.exitValue());
}
} catch (Exception e) {
result.put("error", e.getMessage());
} finally {
// 修复:确保进程被销毁
if (process != null && process.isAlive()) {
process.destroyForcibly();
}
}
return result;
}
@@ -5,6 +5,7 @@ import com.tongran.agent.client.core.vo.SystemVO;
import com.tongran.agent.client.service.SystemService;
import com.tongran.agent.client.utils.AgentDataUtil;
import com.tongran.agent.client.utils.AgentUtil;
import com.tongran.agent.client.utils.AssertLog;
import org.springframework.stereotype.Service;
import oshi.SystemInfo;
import oshi.hardware.CentralProcessor;
@@ -31,44 +32,33 @@ public class SystemServiceImpl implements SystemService {
CentralProcessor processor = hal.getProcessor();
OperatingSystem os = si.getOperatingSystem();
System.out.println("=========================================================");
// 1. 操作系统基本信息
System.out.println("=== 操作系统信息 ===");
systemVO.setOs(os.getFamily()); //操作系统
systemVO.setArch(os.getVersionInfo().getVersion() + " " + os.getBitness() + ""); //操作系统架构
systemVO.setOs(os.getFamily());
systemVO.setArch(os.getVersionInfo().getVersion() + " " + os.getBitness() + "");
systemVO.setUuid(AgentUtil.getMotherboardUUID());
System.out.println("操作系统: " + systemVO.getOs());
System.out.println("操作系统架构: " + systemVO.getArch());
System.out.println("UUID: " + AgentUtil.getMotherboardUUID());
AssertLog.info("操作系统: {}, 架构: {}, UUID: {}", systemVO.getOs(), systemVO.getArch(), systemVO.getUuid());
// 2. 进程信息
System.out.println("\n=== 进程信息 ===");
long maxProcesses = getMaxProcessesLinux();
int runningProcesses = getRunningProcessesLinux();
System.out.println("最大进程数: " + maxProcesses);
System.out.println("正在运行的进程数: " + runningProcesses);
systemVO.setMaxProc(maxProcesses); //最大进程数
systemVO.setRunProcNum(runningProcesses); //正在运行的进程数
systemVO.setMaxProc(maxProcesses);
systemVO.setRunProcNum(runningProcesses);
AssertLog.info("最大进程数: {}, 运行中进程数: {}", maxProcesses, runningProcesses);
// 3. 登录用户数
System.out.println("\n=== 登录用户 ===");
systemVO.setUsersNum(os.getSessions().size()); //登录用户数
System.out.println("登录用户数: " + systemVO.getUsersNum());
systemVO.setUsersNum(os.getSessions().size());
AssertLog.info("登录用户数: {}", systemVO.getUsersNum());
// 4. 磁盘信息
System.out.println("\n=== 磁盘信息 ===");
systemVO.setDiskSizeTotal(diskSpace()); //硬盘:总可用空间
systemVO.setBootTime(systemBootTime(hal.getProcessor())); //系统启动时间
systemVO.setUname(systemDescription(si)); //系统描述
systemVO.setLocalTime(localTime()); //系统本地时间
systemVO.setUpTime(systemUptime(os)); //系统正常运行时间
System.out.println("硬盘:总可用空间: " + systemVO.getDiskSizeTotal());
System.out.println("系统启动时间: " + systemVO.getBootTime());
System.out.println("系统描述: " + systemVO.getUname());
System.out.println("系统本地时间: " + systemVO.getLocalTime());
System.out.println("系统正常运行时间: " + systemVO.getUpTime());
systemVO.setDiskSizeTotal(diskSpace());
systemVO.setBootTime(systemBootTime(hal.getProcessor()));
systemVO.setUname(systemDescription(si));
systemVO.setLocalTime(localTime());
systemVO.setUpTime(systemUptime(os));
AssertLog.info("硬盘总可用空间: {}, 系统启动时间: {}, 系统正常运行时间: {}",
systemVO.getDiskSizeTotal(), systemVO.getBootTime(), systemVO.getUpTime());
} catch (IOException e) {
e.printStackTrace();
AssertLog.error("系统信息采集异常", e);
}
return systemVO;
}
@@ -76,8 +66,8 @@ public class SystemServiceImpl implements SystemService {
@Override
public String otherSystem(String type) {
JSONObject json = new JSONObject();
json.put("type",type);
switch(type){
json.put("type", type);
switch (type) {
case "systemSwapSizeFreeCollect":
json.put("value", String.valueOf(handleSystemSwapSizeFree()));
break;
@@ -97,10 +87,10 @@ public class SystemServiceImpl implements SystemService {
json.put("value", String.valueOf(handleMemorySizeTotal()));
break;
case "systemSwOsCollect":
json.put("value",handleSystemSwOs());
json.put("value", handleSystemSwOs());
break;
case "systemSwArchCollect":
json.put("value",handleSystemSwArch());
json.put("value", handleSystemSwArch());
break;
case "kernelMaxprocCollect":
json.put("value", String.valueOf(handleKernelMaxproc()));
@@ -118,10 +108,10 @@ public class SystemServiceImpl implements SystemService {
json.put("value", String.valueOf(handleSystemBoottime()));
break;
case "systemUnameCollect":
json.put("value",handleSystemUname());
json.put("value", handleSystemUname());
break;
case "systemLocaltimeCollect":
json.put("value",handleSystemLocaltime());
json.put("value", handleSystemLocaltime());
break;
case "systemUptimeCollect":
json.put("value", String.valueOf(handleSystemUptime()));
@@ -130,224 +120,209 @@ public class SystemServiceImpl implements SystemService {
json.put("value", String.valueOf(handleProcNum()));
break;
default:
json.put("value",handleDefault());
json.put("value", handleDefault());
break;
}
return json.toString();
}
private long handleSystemSwapSizeFree(){
private long handleSystemSwapSizeFree() {
try {
Map<String, Long> memInfo = AgentDataUtil.parseMemInfo();
long swapFree = memInfo.getOrDefault("SwapFree", 0L);
System.out.println("=========================================================");
System.out.println("交换卷/文件的可用空间(字节): " + swapFree);
AssertLog.info("交换卷可用空间: {} KB", swapFree);
return swapFree;
} catch (IOException e) {
e.printStackTrace();
AssertLog.error("获取交换卷可用空间异常", e);
}
return 0L;
}
private double handleMemoryUtilization(){
private double handleMemoryUtilization() {
try {
Map<String, Long> memInfo = AgentDataUtil.parseMemInfo();
System.out.println("=========================================================");
long total = memInfo.get("MemTotal");
long total = memInfo.getOrDefault("MemTotal", 0L);
long available = memInfo.getOrDefault("MemAvailable",
memInfo.get("MemFree") +
memInfo.getOrDefault("MemFree", 0L) +
memInfo.getOrDefault("Buffers", 0L) +
memInfo.getOrDefault("Cached", 0L) +
memInfo.getOrDefault("SReclaimable", 0L));
// 实际内存使用率
long cached = memInfo.getOrDefault("Cached", 0L) +
memInfo.getOrDefault("SReclaimable", 0L);
long buffers = memInfo.getOrDefault("Buffers", 0L);
double actualUsage = (double)(total - available - cached - buffers) / total * 100;
System.out.printf("实际内存使用率: %.2f%%\n", actualUsage);
// 修复:除零保护
double actualUsage = total > 0 ? (double) (total - available - cached - buffers) / total * 100 : 0;
AssertLog.info("实际内存使用率: {}%", String.format("%.2f", actualUsage));
return actualUsage;
} catch (IOException e) {
e.printStackTrace();
AssertLog.error("获取内存利用率异常", e);
}
return 0;
}
private double handleSystemSwapSizePercent(){
private double handleSystemSwapSizePercent() {
try {
Map<String, Long> memInfo = AgentDataUtil.parseMemInfo();
System.out.println("=========================================================");
long swapTotal = memInfo.getOrDefault("SwapTotal", 0L);
long swapFree = memInfo.getOrDefault("SwapFree", 0L);
System.out.printf("交换空间使用率: %.2f%%\n",
(double)(swapTotal - swapFree) / swapTotal * 100);
return (double) swapFree / swapTotal * 100;
// 修复:除零保护
if (swapTotal > 0) {
AssertLog.info("交换空间使用率: {}%",
String.format("%.2f", (double) (swapTotal - swapFree) / swapTotal * 100));
return (double) swapFree / swapTotal * 100;
}
return 0;
} catch (IOException e) {
e.printStackTrace();
AssertLog.error("获取交换空间百分比异常", e);
}
return 0;
}
private long handleMemorySizeAvailable(){
private long handleMemorySizeAvailable() {
try {
Map<String, Long> memInfo = AgentDataUtil.parseMemInfo();
System.out.println("=========================================================");
System.out.println("可用内存: " +
memInfo.getOrDefault("MemAvailable",
memInfo.get("MemFree") +
memInfo.getOrDefault("Buffers", 0L) +
memInfo.getOrDefault("Cached", 0L) +
memInfo.getOrDefault("SReclaimable", 0L)));
return memInfo.getOrDefault("MemAvailable",
memInfo.get("MemFree") +
memInfo.getOrDefault("Buffers", 0L) +
memInfo.getOrDefault("Cached", 0L) +
memInfo.getOrDefault("SReclaimable", 0L));
} catch (IOException e) {
e.printStackTrace();
}
return 0;
}
private double handleMemorySizePercent(){
try {
Map<String, Long> memInfo = AgentDataUtil.parseMemInfo();
System.out.println("=========================================================");
long available = memInfo.getOrDefault("MemAvailable",
memInfo.get("MemFree") +
memInfo.getOrDefault("MemFree", 0L) +
memInfo.getOrDefault("Buffers", 0L) +
memInfo.getOrDefault("Cached", 0L) +
memInfo.getOrDefault("SReclaimable", 0L));
long total = memInfo.get("MemTotal");
System.out.println("可用内存百分比: " + (double) available / total * 100);
return (double) available / total * 100;
AssertLog.info("可用内存: {} KB", available);
return available;
} catch (IOException e) {
e.printStackTrace();
AssertLog.error("获取可用内存异常", e);
}
return 0;
}
private long handleMemorySizeTotal(){
private double handleMemorySizePercent() {
try {
Map<String, Long> memInfo = AgentDataUtil.parseMemInfo();
System.out.println("=========================================================");
long total = memInfo.get("MemTotal");
System.out.println("总内存: " + total);
long available = memInfo.getOrDefault("MemAvailable",
memInfo.getOrDefault("MemFree", 0L) +
memInfo.getOrDefault("Buffers", 0L) +
memInfo.getOrDefault("Cached", 0L) +
memInfo.getOrDefault("SReclaimable", 0L));
long total = memInfo.getOrDefault("MemTotal", 0L);
// 修复:除零保护
double percent = total > 0 ? (double) available / total * 100 : 0;
AssertLog.info("可用内存百分比: {}%", String.format("%.2f", percent));
return percent;
} catch (IOException e) {
AssertLog.error("获取可用内存百分比异常", e);
}
return 0;
}
private long handleMemorySizeTotal() {
try {
Map<String, Long> memInfo = AgentDataUtil.parseMemInfo();
long total = memInfo.getOrDefault("MemTotal", 0L);
AssertLog.info("总内存: {} KB", total);
return total;
} catch (IOException e) {
e.printStackTrace();
AssertLog.error("获取总内存异常", e);
}
return 0;
}
private String handleSystemSwOs(){
private String handleSystemSwOs() {
SystemInfo si = new SystemInfo();
OperatingSystem os = si.getOperatingSystem();
System.out.println("=========================================================");
System.out.println("操作系统: " + os.getFamily());
AssertLog.info("操作系统: {}", os.getFamily());
return os.getFamily();
}
private String handleSystemSwArch(){
private String handleSystemSwArch() {
SystemInfo si = new SystemInfo();
OperatingSystem os = si.getOperatingSystem();
System.out.println("=========================================================");
String arch = os.getVersionInfo().getVersion() + " " + os.getBitness() + "";
System.out.println("操作系统架构: " + arch);
AssertLog.info("操作系统架构: {}", arch);
return arch;
}
private long handleKernelMaxproc(){
System.out.println("=========================================================");
private long handleKernelMaxproc() {
long maxProcesses = 0;
try {
maxProcesses = getMaxProcessesLinux();
} catch (IOException e) {
e.printStackTrace();
AssertLog.error("获取最大进程数异常", e);
}
System.out.println("最大进程数: " + maxProcesses);
AssertLog.info("最大进程数: {}", maxProcesses);
return maxProcesses;
}
private long handleProcNumRun(){
System.out.println("=========================================================");
private long handleProcNumRun() {
int runningProcesses = getRunningProcessesLinux();
System.out.println("正在运行的进程数: " + runningProcesses);
AssertLog.info("正在运行的进程数: {}", runningProcesses);
return runningProcesses;
}
private int handleUsersNum(){
private int handleUsersNum() {
SystemInfo si = new SystemInfo();
OperatingSystem os = si.getOperatingSystem();
System.out.println("=========================================================");
int usersNum = os.getSessions().size();
System.out.println("登录用户数: " + usersNum);
AssertLog.info("登录用户数: {}", usersNum);
return usersNum;
}
private long handleSystemDiskSizeTotal(){
System.out.println("=========================================================");
private long handleSystemDiskSizeTotal() {
long diskSizeTotal = diskSpace();
System.out.println("硬盘总可用空间: " + diskSizeTotal);
AssertLog.info("硬盘总可用空间: {}", diskSizeTotal);
return diskSizeTotal;
}
public long handleSystemBoottime(){
public long handleSystemBoottime() {
SystemInfo si = new SystemInfo();
HardwareAbstractionLayer hal = si.getHardware();
long boottime = systemBootTime(hal.getProcessor());
System.out.println("=========================================================");
System.out.println("系统启动时间: " + boottime);
AssertLog.info("系统启动时间: {}", boottime);
return boottime;
}
private String handleSystemUname(){
private String handleSystemUname() {
SystemInfo si = new SystemInfo();
String uname = systemDescription(si);
System.out.println("=========================================================");
System.out.println("系统描述: " + uname);
AssertLog.info("系统描述: {}", uname);
return uname;
}
private String handleSystemLocaltime(){
System.out.println("=========================================================");
private String handleSystemLocaltime() {
String time = localTime();
System.out.println("系统本地时间: " + time);
AssertLog.info("系统本地时间: {}", time);
return time;
}
private long handleSystemUptime(){
private long handleSystemUptime() {
SystemInfo si = new SystemInfo();
OperatingSystem os = si.getOperatingSystem();
System.out.println("=========================================================");
long uptime = systemUptime(os);
System.out.println("系统正常运行时间: " + uptime);
AssertLog.info("系统正常运行时间: {}", uptime);
return uptime;
}
private long handleProcNum(){
private long handleProcNum() {
SystemInfo si = new SystemInfo();
OperatingSystem os = si.getOperatingSystem();
System.out.println("=========================================================");
long procNum = os.getProcessCount();
System.out.println("进程数: " + procNum);
AssertLog.info("进程数: {}", procNum);
return procNum;
}
private String handleDefault(){
private String handleDefault() {
return "";
}
// 读取 /proc/sys/kernel/pid_max 获取最大进程数
private static long getMaxProcessesLinux() throws IOException {
File pidMaxFile = new File("/proc/sys/kernel/pid_max");
if (!pidMaxFile.exists()) {
return 0;
}
try (BufferedReader reader = new BufferedReader(new FileReader(pidMaxFile))) {
String line = reader.readLine().trim();
return Long.parseLong(line);
String line = reader.readLine();
return line != null ? Long.parseLong(line.trim()) : 0;
}
}
// 统计 /proc 下的数字目录数(每个目录对应一个进程)
private static int getRunningProcessesLinux() {
File procDir = new File("/proc");
File[] files = procDir.listFiles();
@@ -362,7 +337,6 @@ public class SystemServiceImpl implements SystemService {
return count;
}
// 获取硬盘总可用空间
public long diskSpace() {
long diskSizeTotal = 0;
File[] roots = File.listRoots();
@@ -372,30 +346,23 @@ public class SystemServiceImpl implements SystemService {
return diskSizeTotal;
}
// 获取系统启动时间
public long systemBootTime(CentralProcessor processor) {
long[] systemCpuLoadTicks = processor.getSystemCpuLoadTicks();
long bootTime = ManagementFactory.getRuntimeMXBean().getStartTime();
return bootTime;
return ManagementFactory.getRuntimeMXBean().getStartTime();
}
// 获取系统描述
public String systemDescription(SystemInfo si) {
OperatingSystem os = si.getOperatingSystem();
HardwareAbstractionLayer hal = si.getHardware();
return "操作系统: " + os.toString() + ",系统版本: " + os.getVersionInfo().toString() + "" +
return "操作系统: " + os.toString() + ",系统版本: " + os.getVersionInfo().toString() +
",处理器: " + hal.getProcessor().getProcessorIdentifier().getName() +
",物理内存: " + hal.getMemory().getTotal() / (1024 * 1024 * 1024) + " GB";
}
// 获取系统本地时间
public String localTime() {
return LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}
// 获取系统正常运行时间
public long systemUptime(OperatingSystem os) {
long uptimeSeconds = os.getSystemUptime();
return uptimeSeconds;
return os.getSystemUptime();
}
}