package space.anyi.process; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; import java.nio.file.Paths; import java.util.Arrays; import java.util.Optional; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * 示例六:使用 ProcessHandle(JDK 9+)管理进程。 * * 四个案例点各用一个 method 演示: * * 子进程为 src/main/resources/code/processHandle/Main.java(打印 pid/args 后睡眠 30 秒)。 */ public class ProcessHandleExample { private static final Logger log = LoggerFactory.getLogger(ProcessHandleExample.class); private static final File WORK_DIR = Paths.get(System.getProperty("user.dir"), "src", "main", "resources", "code", "processHandle").toFile(); public static void main(String[] args) throws Exception { Jdk.compileFixture(WORK_DIR); handleFromProcess(); resolveByPid(); inspectProcessTree(); terminateViaHandle(); } /** 启动一个 processHandle 子进程程序(睡眠 30 秒,等待父进程检查/销毁) */ private static Process startChild() throws IOException { return new ProcessBuilder(Jdk.java(), "-Dfile.encoding=UTF-8", "Main") .directory(WORK_DIR) .start(); } /** * 案例点:从 Process 获取 ProcessHandle,查询 pid、元信息。 * handle.pid() 应与 process.pid() 完全一致; * info() 返回 command / commandLine / startInstant / totalCpuDuration / user。 */ private static void handleFromProcess() throws Exception { log.info("== 从 Process 获取 ProcessHandle =="); Process process = startChild(); Thread stdoutDrain = drain("stdout", process.getInputStream()); Thread stderrDrain = drain("stderr", process.getErrorStream()); // 稍等片刻,让 drain 线程先把子进程的启动横幅读走,再开始检查 Thread.sleep(300); // 从 Process 取出进程句柄 ProcessHandle handle = process.toHandle(); // pid() 应与 process.pid() 一致 log.info("process.pid() = {}", process.pid()); log.info("handle.pid() = {}", handle.pid()); log.info("pid 一致: {}", process.pid() == handle.pid()); // isAlive —— 子进程此时正在睡眠,应为 true log.info("handle.isAlive() = {}", handle.isAlive()); // info() 返回进程元信息(均为 Optional,部分字段在进程退出后不再可读) ProcessHandle.Info info = handle.info(); log.info("command() = {}", info.command()); log.info("commandLine() = {}", info.commandLine()); log.info("startInstant() = {}", info.startInstant()); log.info("totalCpuDuration()= {}", info.totalCpuDuration()); log.info("user() = {}", info.user()); // supportsNormalTermination() —— Linux 返回 true,Windows 返回 false log.info("supportsNormalTermination() = {}", handle.supportsNormalTermination()); // 销毁子进程,恢复环境 handle.destroy(); stdoutDrain.join(); stderrDrain.join(); process.waitFor(); log.info("handleFromProcess 完成,退出码: {}", process.exitValue()); } /** * 案例点:通过 PID 解析进程句柄。 * ProcessHandle.of(pid) 返回 Optional,进程存在时有值,PID 不存在时为空。 */ private static void resolveByPid() throws Exception { log.info("== 通过 PID 解析 ProcessHandle =="); Process process = startChild(); Thread stdoutDrain = drain("stdout", process.getInputStream()); Thread stderrDrain = drain("stderr", process.getErrorStream()); Thread.sleep(300); long pid = process.pid(); log.info("子进程 PID: {}", pid); // 通过 PID 解析——应成功 Optional child = ProcessHandle.of(pid); log.info("ProcessHandle.of({}) 存在: {}", pid, child.isPresent()); child.ifPresent(h -> { log.info(" pid = {}", h.pid()); log.info(" command = {}", h.info().command()); }); // 不存在的 PID——应返回空 Optional bogus = ProcessHandle.of(99999999L); log.info("ProcessHandle.of(99999999) 存在: {}", bogus.isPresent()); // 恢复环境 process.destroy(); stdoutDrain.join(); stderrDrain.join(); process.waitFor(); log.info("resolveByPid 完成,退出码: {}", process.exitValue()); } /** * 案例点:遍历父子进程树。 * current.children() 是当前 JVM 的直接子进程(应包含我们启动的子进程); * child.parent() 应回溯到当前 JVM; * allProcesses() 统计当前用户可见的所有活跃进程。 */ private static void inspectProcessTree() throws Exception { log.info("== 进程树与 allProcesses =="); Process process = startChild(); Thread stdoutDrain = drain("stdout", process.getInputStream()); Thread stderrDrain = drain("stderr", process.getErrorStream()); Thread.sleep(300); // 当前 JVM 进程句柄 ProcessHandle current = ProcessHandle.current(); log.info("当前 JVM pid: {}", current.pid()); // 直接子进程——应包含我们刚启动的子进程 log.info("当前 JVM 的直接子进程:"); long[] childPids = current.children() .peek(h -> log.info(" 子进程 pid: {}", h.pid())) .mapToLong(h -> h.pid()) .toArray(); log.info("子进程 PID {} 在 children() 中: {}", process.pid(), Arrays.binarySearch(childPids, process.pid()) >= 0); // 子进程的 parent——应指向当前 JVM ProcessHandle.of(process.pid()).ifPresent(child -> { Optional parent = child.parent(); log.info("子进程 parent 存在: {}", parent.isPresent()); parent.ifPresent(p -> log.info(" parent pid = {}", p.pid())); log.info(" parent 就是当前 JVM: {}", parent.isPresent() && parent.get().pid() == current.pid()); }); // descendants() 当前示例深度为 1,与 children() 内容一致 log.info("descendants() 数量: {}", current.descendants().count()); // allProcesses() 是静态方法,用接口名直接调用;统计当前用户可见的所有活跃进程 try { long total = ProcessHandle.allProcesses().count(); log.info("当前用户可见活跃进程总数: {}", total); } catch (Exception e) { // 部分平台/权限下可能受限 log.info("allProcesses() 调用失败(权限受限): {}", e.getMessage()); } // 恢复环境 process.destroy(); stdoutDrain.join(); stderrDrain.join(); process.waitFor(); log.info("inspectProcessTree 完成,退出码: {}", process.exitValue()); } /** * 案例点:通过句柄销毁进程,对比 Process.destroy()。 * handle.destroy() 发 SIGTERM(Linux); * handle.onExit() 返回 CompletableFuture,join() 阻塞到进程真正退出; * 退出后通过 process.exitValue() 读取退出码。 */ private static void terminateViaHandle() throws Exception { log.info("== 通过句柄销毁进程 =="); Process process = startChild(); Thread stdoutDrain = drain("stdout", process.getInputStream()); Thread stderrDrain = drain("stderr", process.getErrorStream()); Thread.sleep(300); ProcessHandle handle = process.toHandle(); log.info("进程存活: {}", handle.isAlive()); // 通过句柄发起终止(Linux 发 SIGTERM,Windows 走 TerminateProcess) boolean initiated = handle.destroy(); log.info("destroy() 已发起: {}", initiated); // onExit() 阻塞到进程真正退出 handle.onExit().join(); log.info("进程已退出,handle.isAlive() = {}", handle.isAlive()); // 回收输出流线程 stdoutDrain.join(); stderrDrain.join(); // 读取退出码:被 SIGTERM 终止时通常为 143,Windows 为 1 log.info("退出码 (process.exitValue()): {}", process.exitValue()); log.info("terminateViaHandle 完成"); } /** 起一个后台线程把给定输入流按行读取并打日志,读到 EOF(子进程退出)后结束 */ private static Thread drain(String tag, InputStream in) { Thread thread = new Thread(() -> { try { new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8)) .lines() .forEach(line -> log.info("[{}] {}", tag, line)); } catch (java.io.UncheckedIOException e) { // 子进程被 destroy() 销毁时管道会立即关闭,读到一半抛 IOException 属预期行为 log.info("[{}] 子进程被销毁,管道关闭(预期行为)", tag); } }, "drain-" + tag); thread.setDaemon(true); thread.start(); return thread; } }