Files
tr-agent-client/src/main/java/com/tongran/agent/client/utils/AgentUtil.java
T
2025-11-07 18:48:11 +08:00

781 lines
28 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package com.tongran.agent.client.utils;
import cn.hutool.json.JSONObject;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.tongran.agent.client.core.config.GlobalConfig;
import com.tongran.agent.client.core.vo.NetworkInterfaceInfo;
import org.apache.commons.lang3.StringUtils;
import org.snmp4j.smi.OID;
import oshi.hardware.NetworkIF;
import javax.annotation.Resource;
import java.io.*;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
public class AgentUtil {
@Resource
private GlobalConfig globalConfig;
public static String getMotherboardUUID() {
String hostName = getHostname();
String primaryIp = getPrimaryIp();
// SystemInfo si = new SystemInfo();
// HardwareAbstractionLayer hal = si.getHardware();
// Baseboard baseboard = hal.getComputerSystem().getBaseboard();
// if(StringUtils.isNotBlank(baseboard.getSerialNumber())){
// return baseboard.getSerialNumber();
// }
return hostName+":"+primaryIp;
}
public static String getHostname() {
try {
return InetAddress.getLocalHost().getHostName();
} catch (UnknownHostException e) {
return "unknown-host";
}
}
/**
* 获取本机首选 IP 地址(通常是第一个非回环、非虚拟网卡的 IPv4 地址)
*/
public static String getPrimaryIp() {
try {
InetAddress localHost = InetAddress.getLocalHost();
String ip = localHost.getHostAddress();
// 如果是 127.x.x.x,说明可能 hosts 配置有问题,需要手动查找
if (ip.startsWith("127.")) {
return getExternalIp();
}
return ip;
} catch (UnknownHostException e) {
return "127.0.0.1";
}
}
/**
* 获取第一个非回环、非虚拟网卡的 IPv4 地址
*/
public static String getExternalIp() {
try {
Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
while (interfaces.hasMoreElements()) {
NetworkInterface iface = interfaces.nextElement();
// 跳过虚拟网卡(如 docker, veth, lo
if (iface.isLoopback() || iface.isVirtual() || !iface.isUp()) {
continue;
}
Enumeration<InetAddress> addresses = iface.getInetAddresses();
while (addresses.hasMoreElements()) {
InetAddress addr = addresses.nextElement();
if (addr.isLoopbackAddress()) {
continue; // 跳过 127.0.0.1
}
if (addr.getHostAddress().contains(":")) {
continue; // 跳过 IPv6
}
return addr.getHostAddress();
}
}
} catch (SocketException e) {
e.printStackTrace();
}
return "127.0.0.1";
}
public static String toJsonString(List<Map<String, String>> list) {
try {
ObjectMapper mapper = new ObjectMapper();
return mapper.writeValueAsString(list);
} catch (Exception e) {
throw new RuntimeException("JSON转换失败", e);
}
}
public static void base64ToFile(String base64String, String filePath) throws IOException {
// 解码Base64字符串
byte[] decodedBytes = Base64.getDecoder().decode(base64String);
// 写入文件
try (FileOutputStream fos = new FileOutputStream(filePath)) {
fos.write(decodedBytes);
}
}
public static String getInterfaceType(NetworkIF net) {
String name = net.getName().toLowerCase();
String displayName = net.getDisplayName().toLowerCase();
if (name.startsWith("eth") || name.startsWith("en") || displayName.contains("ethernet")) {
return "Ethernet";
} else if (name.startsWith("wlan") || name.startsWith("wl") || displayName.contains("wireless")) {
return "Wi-Fi";
} else if (name.startsWith("lo") || displayName.contains("loopback")) {
return "Loopback";
} else if (name.startsWith("ppp") || displayName.contains("point-to-point")) {
return "PPP";
} else if (name.startsWith("vmnet") || displayName.contains("virtual")) {
return "Virtual";
} else if (name.startsWith("tun") || name.startsWith("tap")) {
return "TUN/TAP";
} else if (name.startsWith("br") || displayName.contains("bridge")) {
return "Bridge";
} else if (name.startsWith("bond") || displayName.contains("bond")) {
return "Bond";
} else {
return "Unknown";
}
}
/**
* 获取当前时间距离下一个“分钟为 0 或 5”的时间点还差多少毫秒
* 例如:xx:00, xx:05, xx:10, ..., xx:55
*/
public static long millisecondsToNext5Minute() {
LocalDateTime now = LocalDateTime.now();
// 当前分钟
int minute = now.getMinute();
// 计算下一个 5 分钟整点(向上取整)
int nextMinute = ((minute / 5) + 1) * 5;
LocalDateTime nextTime;
if (nextMinute < 60) {
// 在当前小时内
nextTime = now.withMinute(nextMinute).withSecond(0).withNano(0);
} else {
// 跨小时,如 10:58 → 11:00
nextTime = now.plusHours(1).withMinute(0).withSecond(0).withNano(0);
}
// 计算相差的毫秒数
return ChronoUnit.MILLIS.between(now, nextTime);
}
/**
* 获取距离下一个分钟整点的毫秒数
*/
public static long getMillisToNextMinute() {
LocalDateTime now = LocalDateTime.now();
// 获取下一个分钟整点时间
LocalDateTime nextMinute = now
.truncatedTo(ChronoUnit.MINUTES) // 截断到当前分钟
.plusMinutes(1); // 加1分钟
// 计算时间差(毫秒)
return ChronoUnit.MILLIS.between(now, nextMinute);
}
/**
* 获取距离下一个指定分钟整点的毫秒数
*/
public static long getMillisToNextMinuteInterval(int intervalMinutes) {
if (intervalMinutes <= 0) {
throw new IllegalArgumentException("Interval must be positive");
}
LocalDateTime now = LocalDateTime.now();
// 计算当前分钟在间隔中的位置
int currentMinute = now.getMinute();
int remainder = currentMinute % intervalMinutes;
// 计算需要增加的分钟数
int minutesToAdd = remainder == 0 ? intervalMinutes : intervalMinutes - remainder;
// 获取下一个间隔整点时间
LocalDateTime nextInterval = now
.truncatedTo(ChronoUnit.MINUTES) // 截断到当前分钟
.plusMinutes(minutesToAdd) // 增加到下一个整点
.withSecond(0) // 秒设为0
.withNano(0); // 纳秒设为0
return ChronoUnit.MILLIS.between(now, nextInterval);
}
public static long roundMinutes(){
LocalDateTime now = LocalDateTime.now();
int minute = now.getMinute();
int roundedMinute = (int) (Math.round(minute / 5.0) * 5);
LocalDateTime roundedTime ;
if (roundedMinute == 60) {
roundedTime = now.plusHours(1).withMinute(0).withSecond(0).withNano(0);
} else {
roundedTime = now.withMinute(roundedMinute).withSecond(0).withNano(0);
}
// 转换为10位时间戳
long timestamp = roundedTime.atZone(ZoneId.systemDefault()).toEpochSecond();
return timestamp;
}
public static LocalDateTime toLocalDateTime(long timestamp, boolean isSeconds) {
java.time.Instant instant = isSeconds ?
java.time.Instant.ofEpochSecond(timestamp) :
java.time.Instant.ofEpochMilli(timestamp);
return LocalDateTime.ofInstant(instant, ZoneId.systemDefault());
}
public static String getLastOid(OID oid){
if(oid.size() > 0){
int lastValue = oid.get(oid.size() - 1);
return String.valueOf(lastValue);
}
return null;
}
public static LinkedHashMap<String, String> swapMap(LinkedHashMap<String, String> original) {
LinkedHashMap<String, String> swapped = new LinkedHashMap<>();
for (Map.Entry<String, String> entry : original.entrySet()) {
if (entry.getValue() != null) { // 避免 null key
swapped.put(entry.getValue(), entry.getKey());
}
}
return swapped;
}
// 存储网关信息:接口名 -> 网关IP
private static Map<String, String> gatewayMap = new HashMap<>();
// 正则表达式匹配:当前 IPxxx.xxx.xxx.xxx 来自于:中国 江苏省 南京市 电信
private static final String REGEX = "来自于:(\\S+)\\s+(\\S+)\\s+(\\S+)\\s+(\\S+)";
/**
* 收集所有 Ethernet 类型网卡信息
*/
public static List<NetworkInterfaceInfo> collectNetworkInfo() throws Exception {
List<NetworkInterfaceInfo> result = new ArrayList<>();
// String publicIp = getPublicIp(); // 获取公网 IP(全局出口)
// String ipInfo = PublicIpFetcher.getPublicIp();
// String publicIp = PublicIpFetcher.extractIp(ipInfo);
// if (publicIp != null) {
// System.out.println("公网 IP: " + publicIp);
// } else {
// System.out.println("未能提取 IP 地址");
// }
Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
while (interfaces.hasMoreElements()) {
NetworkInterface ni = interfaces.nextElement();
String ipv4 = getIPv4Address(ni);
AssertLog.info("ipv4={},接口状态={}",ipv4,ni.isUp());
// 跳过回环、虚拟、关闭的接口
if (ni.isLoopback() || ni.isVirtual() || !ni.isUp()) continue;
// 判断是否为 Ethernet(通过名称约定:eth*, en*, 等)
String name = ni.getName();
AssertLog.info("ipv4={},名称={}",ipv4,name);
if (!isEthernetInterface(name)) continue;
AssertLog.info("ipv4={},物理链路状态={}",ipv4,isInterfaceConnected(name));
// 检查是否“已连接”(物理链路状态)
if (!isInterfaceConnected(name)) continue;
// 检查IP4信息
if (ipv4.equals("N/A")) continue;
String gateway = getGatewayAddress();
AssertLog.info("ipv4={},网关={}",ipv4,gateway);
// 检查IP4信息
if (gateway.equals("N/A")) continue;
// 添加临时路由
boolean flag = addTempRoute(ipv4, name, gateway);
if (!flag) continue;
String ipInfo = PublicIpFetcher.getPublicIp();
String publicIp = PublicIpFetcher.extractIp(ipInfo);
if (publicIp != null) {
System.out.println("公网 IP: " + publicIp);
} else {
System.out.println("未能提取 IP 地址");
}
if (StringUtils.isBlank(publicIp)) continue;
// 删除临时路由
delTempRoute(ipv4, name, gateway);
NetworkInterfaceInfo info = NetworkInterfaceInfo.builder()
.name(name)
.type("Ethernet")
.mac(getMacAddress(ni))
.ipv4(ipv4)
.gateway(gateway) // 网关通常是默认路由,全局一致
.publicIp(publicIp)
.build();
// 如果有公网 IP,查询归属地
if (publicIp != null && !publicIp.isEmpty()) {
Pattern pattern = Pattern.compile(REGEX);
Matcher matcher = pattern.matcher(ipInfo);
if (matcher.find()) {
// group(1): 国家(通常是“中国”)
// group(2): 省份
// group(3): 城市
// group(4): 运营商
String province = matcher.group(2);
String city = matcher.group(3);
String isp = matcher.group(4);
// 去掉可能的标点符号(如句号)
province = province.replaceAll("[。]", "");
city = city.replaceAll("[。]", "");
isp = isp.replaceAll("[。]", "");
info.setCarrier(isp);
info.setProvince(province);
info.setCity(city);
}
// ipInfo.substring(ipInfo.indexOf("来自于:")+4,ipInfo.length())
// JSONObject location = queryIpLocation(publicIp);
// if (location != null) {
// info.setCarrier(location.getStr("org", "未知").replaceAll("^AS\\d+\\s*", ""));
// info.setProvince(location.getStr("region", "未知"));
// info.setCity(location.getStr("city", "未知"));
// } else {
// info.setCarrier("查询失败");
// info.setProvince("查询失败");
// info.setCity("查询失败");
// }
}
result.add(info);
}
return result;
}
/**
* 添加临时路由
*/
private static boolean addTempRoute(String ipv4, String name, String gateway) {
String prefix = "32";
AssertLog.info("添加临时路由,ipv4{}",ipv4);
boolean flag = NetworkUtil.addRoute(ipv4,prefix,gateway,name);
if(flag){
AssertLog.info("添加临时路由成功,ipv4{}",ipv4);
}else{
AssertLog.info("添加临时路由失败,ipv4{}",ipv4);
}
return flag;
}
/**
* 删除临时路由
*/
private static boolean delTempRoute(String ipv4, String name, String gateway) {
String prefix = "32";
AssertLog.info("删除临时路由,ipv4{}",ipv4);
boolean flag = NetworkUtil.deleteRoute(ipv4,prefix,gateway,name);
if(flag){
AssertLog.info("删除临时路由成功,ipv4{}",ipv4);
}else{
AssertLog.info("删除临时路由失败,ipv4{}",ipv4);
}
return flag;
}
/**
* 判断是否为 Ethernet 类型网卡(基于常见命名)
*/
private static boolean isEthernetInterface(String name) {
return name.startsWith("eth") || // Linux 传统
name.startsWith("en") || // systemd 命名 (enp3s0)
name.startsWith("em"); // 有些主板网卡
}
/**
* 获取 MAC 地址
*/
private static String getMacAddress(NetworkInterface ni) {
try {
byte[] mac = ni.getHardwareAddress();
if (mac == null) return "N/A";
StringBuilder sb = new StringBuilder();
for (int i = 0; i < mac.length; i++) {
sb.append(String.format("%02X", mac[i]));
if (i < mac.length - 1) sb.append(":");
}
return sb.toString();
} catch (SocketException e) {
e.printStackTrace();
}
return null;
}
/**
* 获取第一个 IPv4 地址
*/
private static String getIPv4Address(NetworkInterface ni) {
Enumeration<InetAddress> addresses = ni.getInetAddresses();
while (addresses.hasMoreElements()) {
InetAddress addr = addresses.nextElement();
if (addr instanceof Inet4Address) {
return addr.getHostAddress();
}
}
return "N/A";
}
/**
* 获取默认网关(调用 shell 命令)
*/
private static String getGatewayAddress() {
try {
Process process = Runtime.getRuntime().exec("ip route");
java.util.Scanner scanner = new java.util.Scanner(process.getInputStream());
while (scanner.hasNextLine()) {
String line = scanner.nextLine();
if (line.startsWith("default")) {
String[] parts = line.split(" ");
return parts[2]; // default via <gateway> dev ...
}
}
scanner.close();
} catch (Exception e) {
e.printStackTrace();
}
return "N/A";
}
// 检查接口是否“已连接”(物理链路状态)
private static boolean isInterfaceConnected(String interfaceName) {
try {
String os = System.getProperty("os.name").toLowerCase();
Process process;
if (os.contains("win")) {
// Windows: 使用 wmic 检查网卡是否启用
process = Runtime.getRuntime().exec(
"wmic nic where \"NetEnabled=true\" get Name");
} else {
// Linux: 检查 operstate
process = Runtime.getRuntime().exec(
"cat /sys/class/net/" + interfaceName + "/operstate");
}
BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
if (os.contains("win")) {
if (line.trim().equalsIgnoreCase(interfaceName)) {
return true;
}
} else {
return "up".equals(line.trim());
}
}
reader.close();
return false;
} catch (Exception e) {
System.err.println("无法检查接口状态: " + interfaceName);
return false; // 保守处理
}
}
/**
* 获取公网 IP
*/
private static String getPublicIp() {
return sendHttpGet("https://ifconfig.me");
}
/**
* 查询 IP 归属地(使用 ipinfo.io
*/
private static JSONObject queryIpLocation(String ip) {
String url = "https://ipinfo.io/" + ip + "/json";
String response = sendHttpGet(url);
AssertLog.info("查询 IP 归属地={}", response);
if (response != null) {
try {
return new JSONObject(response);
} catch (Exception e) {
e.printStackTrace();
}
}
return null;
}
/**
* 发送 HTTP GET 请求
*/
private static String sendHttpGet(String urlString) {
try {
URL url = new URL(urlString);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(5000);
conn.setReadTimeout(5000);
if (conn.getResponseCode() == 200) {
java.util.Scanner scanner = new java.util.Scanner(conn.getInputStream());
String result = scanner.useDelimiter("\\A").next();
scanner.close();
return result;
}
} catch (Exception e) {
System.err.println("HTTP 请求失败: " + e.getMessage());
}
return null;
}
public static void bufferedWriter(String filePath, String[] lines){
try (BufferedWriter writer = Files.newBufferedWriter(
Paths.get(filePath),
StandardCharsets.UTF_8,
StandardOpenOption.CREATE, // 创建文件
StandardOpenOption.TRUNCATE_EXISTING // 覆盖写入
)) {
for (String line : lines) {
writer.write(line);
writer.newLine(); // 换行
}
System.out.println("文件写入完成!");
} catch (IOException e) {
System.err.println("写入异常:" + e.getMessage());
e.printStackTrace();
}
}
/**
* 获取 /etc/issue 文件的第二行内容
* @return 第二行字符串,如果不存在则返回 null
*/
public static String getDeviceSN() {
Path issuePath = Paths.get("/etc/issue");
if (!Files.exists(issuePath)) {
System.err.println("文件不存在: /etc/issue");
return null;
}
if (!Files.isReadable(issuePath)) {
System.err.println("无读取权限: /etc/issue");
return null;
}
try (BufferedReader reader = Files.newBufferedReader(issuePath)) {
String line;
int lineNumber = 0;
while ((line = reader.readLine()) != null) {
line = line.trim();
if (!line.isEmpty()) { // 只计数非空行
lineNumber++;
if (lineNumber == 2) {
return line;
}
}
}
// 如果文件少于两行
System.err.println("文件行数不足,只有 " + lineNumber + "");
return null;
} catch (NoSuchFileException e) {
System.err.println("文件未找到: " + e.getMessage());
return null;
} catch (IOException e) {
System.err.println("读取文件异常: " + e.getMessage());
return null;
}
}
public static void main(String[] args) throws Exception {
List<NetworkInterfaceInfo> infos = collectNetworkInfo();
for (NetworkInterfaceInfo info : infos) {
System.out.println(info);
}
}
public static String getFileMD5(String filePath) {
// 检查文件是否存在
Path path = Paths.get(filePath);
if (!Files.exists(path)) {
System.out.println("文件不存在: " + filePath);
return null;
}
// 检查是否是文件(不是目录)
if (!Files.isRegularFile(path)) {
System.out.println("路径不是文件: " + filePath);
return null;
}
// 检查文件是否可读
if (!Files.isReadable(path)) {
System.out.println("文件不可读: " + filePath);
return null;
}
try {
MessageDigest md = MessageDigest.getInstance("MD5");
try (FileInputStream fis = new FileInputStream(filePath)) {
byte[] buffer = new byte[4096];
int length;
while ((length = fis.read(buffer)) != -1) {
md.update(buffer, 0, length);
}
}
byte[] digest = md.digest();
StringBuilder sb = new StringBuilder();
for (byte b : digest) {
sb.append(String.format("%02x", b));
}
return sb.toString();
} catch (NoSuchAlgorithmException | IOException e) {
System.out.println("计算MD5失败: " + e.getMessage());
return null;
}
}
/**
* 安排脚本在 3 分钟后执行
*/
public static void scheduleScriptIn3Minutes(String SCRIPT_PATH) {
// 使用 at 命令:3 minutes from now
String atCommand = String.format("echo '%s' | at now + 3 minutes", SCRIPT_PATH);
ProcessBuilder pb = new ProcessBuilder("bash", "-c", atCommand);
pb.redirectErrorStream(true); // 合并 stdout 和 stderr
try {
Process process = pb.start();
// 读取命令输出(at 通常会打印任务编号)
java.io.BufferedReader reader = new java.io.BufferedReader(
new java.io.InputStreamReader(process.getInputStream())
);
String line;
StringBuilder output = new StringBuilder();
while ((line = reader.readLine()) != null) {
output.append(line).append("\n");
}
reader.close();
int exitCode = process.waitFor();
if (exitCode == 0) {
System.out.println("✅ 成功安排脚本在 3 分钟后执行:");
System.out.println(" 脚本: " + SCRIPT_PATH);
if (output.length() > 0) {
System.out.println(" at 响应: " + output.toString().trim());
}
} else {
System.err.println("❌ 调度失败,exit code: " + exitCode);
System.err.println(" 输出: " + output.toString().trim());
}
} catch (IOException | InterruptedException e) {
System.err.println("执行 at 命令时出错: " + e.getMessage());
e.printStackTrace();
}
}
public static String getNetworkParam(String filePath, String key){
Properties props = new Properties();
String result = "";
try (InputStream input = Files.newInputStream(Paths.get(filePath))) {
// 加载配置文件
props.load(input);
result = props.getProperty(key, "");
System.out.println("配置文件加载成功");
} catch (IOException e) {
System.err.println("无法加载配置文件,使用默认值");
} catch (NumberFormatException e) {
System.err.println("配置文件格式错误: " + e.getMessage());
}
return result;
}
/**
* 读取文件,跳过空行,将所有非空行拼接成一个字符串
* @param filePath 文件路径
* @return 拼接后的字符串(空行已去除)
*/
public static String readNonEmptyLinesAsString(String filePath) throws Exception {
return Files.lines(Paths.get(filePath))
.filter(line -> !line.trim().isEmpty()) // 去除 null 和 空/空白行
.collect(Collectors.joining("\n")); // 用换行符连接
}
/**
* 获取文件最后修改时间
* @param filePath
* @return
*/
public static LocalDateTime getLastModifiedTime(String filePath) {
try {
BasicFileAttributes attrs = Files.readAttributes(Paths.get(filePath), BasicFileAttributes.class);
return attrs.lastModifiedTime()
.toInstant()
.atZone(ZoneId.systemDefault())
.toLocalDateTime();
} catch (IOException e) {
System.err.println("无法读取文件时间: " + filePath);
return null;
}
// LocalDateTime mtime = FileUtils.getLastModifiedTime("/tmp/data.log");
// if (mtime != null) {
// System.out.println("修改时间: " + mtime);
// }
}
/**
* 获取系统 HZ(每秒 tick 数)
* 典型值:100, 250, 300, 1000
*/
public static int getSystemHz() {
if (GlobalConfig.systemHz != null) {
return GlobalConfig.systemHz;
}
try {
Process process = Runtime.getRuntime().exec(
"grep CONFIG_HZ /boot/config-$(uname -r)"
);
BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream())
);
String line;
while ((line = reader.readLine()) != null) {
if (line.contains("CONFIG_HZ=")) {
String[] parts = line.split("=");
int hz = Integer.parseInt(parts[1].trim());
GlobalConfig.systemHz = hz;
return hz;
}
}
reader.close();
} catch (Exception e) {
System.err.println("无法读取系统 HZ,使用默认值 1000");
}
// 默认值(大多数现代 Linux 发行版使用 1000 Hz
GlobalConfig.systemHz = 1000;
return GlobalConfig.systemHz;
}
/**
* 将 ticks 转换为秒
*/
public static double ticksToSeconds(long ticks) {
int hz = getSystemHz();
return (double) ticks / hz;
}
}