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:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+102
-52
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+79
-77
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+116
-39
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user