فهرست منبع

示例六:使用 ProcessHandle 管理进程(4 个案例点)

yangyi 1 هفته پیش
والد
کامیت
d5d3587382
1فایلهای تغییر یافته به همراه229 افزوده شده و 0 حذف شده
  1. 229 0
      src/main/java/space/anyi/process/ProcessHandleExample.java

+ 229 - 0
src/main/java/space/anyi/process/ProcessHandleExample.java

@@ -0,0 +1,229 @@
+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 演示:
+ * <ul>
+ *   <li>handleFromProcess()     —— 从 Process 获取句柄,查询 pid / 元信息</li>
+ *   <li>resolveByPid()          —— 通过 PID 解析句柄,处理不存在的 PID</li>
+ *   <li>inspectProcessTree()    —— 遍历父子进程树,allProcesses() 统计</li>
+ *   <li>terminateViaHandle()    —— 通过句柄 destroy,onExit() 等待退出</li>
+ * </ul>
+ * 子进程为 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<ProcessHandle> 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<ProcessHandle> 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<ProcessHandle> 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;
+    }
+}