ProcessHandleExample.java 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. package space.anyi.process;
  2. import java.io.BufferedReader;
  3. import java.io.File;
  4. import java.io.IOException;
  5. import java.io.InputStream;
  6. import java.io.InputStreamReader;
  7. import java.nio.charset.StandardCharsets;
  8. import java.nio.file.Paths;
  9. import java.util.Arrays;
  10. import java.util.Optional;
  11. import org.slf4j.Logger;
  12. import org.slf4j.LoggerFactory;
  13. /**
  14. * 示例六:使用 ProcessHandle(JDK 9+)管理进程。
  15. *
  16. * 四个案例点各用一个 method 演示:
  17. * <ul>
  18. * <li>handleFromProcess() —— 从 Process 获取句柄,查询 pid / 元信息</li>
  19. * <li>resolveByPid() —— 通过 PID 解析句柄,处理不存在的 PID</li>
  20. * <li>inspectProcessTree() —— 遍历父子进程树,allProcesses() 统计</li>
  21. * <li>terminateViaHandle() —— 通过句柄 destroy,onExit() 等待退出</li>
  22. * </ul>
  23. * 子进程为 src/main/resources/code/processHandle/Main.java(打印 pid/args 后睡眠 30 秒)。
  24. */
  25. public class ProcessHandleExample {
  26. private static final Logger log = LoggerFactory.getLogger(ProcessHandleExample.class);
  27. private static final File WORK_DIR = Paths.get(System.getProperty("user.dir"),
  28. "src", "main", "resources", "code", "processHandle").toFile();
  29. public static void main(String[] args) throws Exception {
  30. Jdk.compileFixture(WORK_DIR);
  31. handleFromProcess();
  32. resolveByPid();
  33. inspectProcessTree();
  34. terminateViaHandle();
  35. }
  36. /** 启动一个 processHandle 子进程程序(睡眠 30 秒,等待父进程检查/销毁) */
  37. private static Process startChild() throws IOException {
  38. return new ProcessBuilder(Jdk.java(), "-Dfile.encoding=UTF-8", "Main")
  39. .directory(WORK_DIR)
  40. .start();
  41. }
  42. /**
  43. * 案例点:从 Process 获取 ProcessHandle,查询 pid、元信息。
  44. * handle.pid() 应与 process.pid() 完全一致;
  45. * info() 返回 command / commandLine / startInstant / totalCpuDuration / user。
  46. */
  47. private static void handleFromProcess() throws Exception {
  48. log.info("== 从 Process 获取 ProcessHandle ==");
  49. Process process = startChild();
  50. Thread stdoutDrain = drain("stdout", process.getInputStream());
  51. Thread stderrDrain = drain("stderr", process.getErrorStream());
  52. // 稍等片刻,让 drain 线程先把子进程的启动横幅读走,再开始检查
  53. Thread.sleep(300);
  54. // 从 Process 取出进程句柄
  55. ProcessHandle handle = process.toHandle();
  56. // pid() 应与 process.pid() 一致
  57. log.info("process.pid() = {}", process.pid());
  58. log.info("handle.pid() = {}", handle.pid());
  59. log.info("pid 一致: {}", process.pid() == handle.pid());
  60. // isAlive —— 子进程此时正在睡眠,应为 true
  61. log.info("handle.isAlive() = {}", handle.isAlive());
  62. // info() 返回进程元信息(均为 Optional,部分字段在进程退出后不再可读)
  63. ProcessHandle.Info info = handle.info();
  64. log.info("command() = {}", info.command());
  65. log.info("commandLine() = {}", info.commandLine());
  66. log.info("startInstant() = {}", info.startInstant());
  67. log.info("totalCpuDuration()= {}", info.totalCpuDuration());
  68. log.info("user() = {}", info.user());
  69. // supportsNormalTermination() —— Linux 返回 true,Windows 返回 false
  70. log.info("supportsNormalTermination() = {}", handle.supportsNormalTermination());
  71. // 销毁子进程,恢复环境
  72. handle.destroy();
  73. stdoutDrain.join();
  74. stderrDrain.join();
  75. process.waitFor();
  76. log.info("handleFromProcess 完成,退出码: {}", process.exitValue());
  77. }
  78. /**
  79. * 案例点:通过 PID 解析进程句柄。
  80. * ProcessHandle.of(pid) 返回 Optional,进程存在时有值,PID 不存在时为空。
  81. */
  82. private static void resolveByPid() throws Exception {
  83. log.info("== 通过 PID 解析 ProcessHandle ==");
  84. Process process = startChild();
  85. Thread stdoutDrain = drain("stdout", process.getInputStream());
  86. Thread stderrDrain = drain("stderr", process.getErrorStream());
  87. Thread.sleep(300);
  88. long pid = process.pid();
  89. log.info("子进程 PID: {}", pid);
  90. // 通过 PID 解析——应成功
  91. Optional<ProcessHandle> child = ProcessHandle.of(pid);
  92. log.info("ProcessHandle.of({}) 存在: {}", pid, child.isPresent());
  93. child.ifPresent(h -> {
  94. log.info(" pid = {}", h.pid());
  95. log.info(" command = {}", h.info().command());
  96. });
  97. // 不存在的 PID——应返回空
  98. Optional<ProcessHandle> bogus = ProcessHandle.of(99999999L);
  99. log.info("ProcessHandle.of(99999999) 存在: {}", bogus.isPresent());
  100. // 恢复环境
  101. process.destroy();
  102. stdoutDrain.join();
  103. stderrDrain.join();
  104. process.waitFor();
  105. log.info("resolveByPid 完成,退出码: {}", process.exitValue());
  106. }
  107. /**
  108. * 案例点:遍历父子进程树。
  109. * current.children() 是当前 JVM 的直接子进程(应包含我们启动的子进程);
  110. * child.parent() 应回溯到当前 JVM;
  111. * allProcesses() 统计当前用户可见的所有活跃进程。
  112. */
  113. private static void inspectProcessTree() throws Exception {
  114. log.info("== 进程树与 allProcesses ==");
  115. Process process = startChild();
  116. Thread stdoutDrain = drain("stdout", process.getInputStream());
  117. Thread stderrDrain = drain("stderr", process.getErrorStream());
  118. Thread.sleep(300);
  119. // 当前 JVM 进程句柄
  120. ProcessHandle current = ProcessHandle.current();
  121. log.info("当前 JVM pid: {}", current.pid());
  122. // 直接子进程——应包含我们刚启动的子进程
  123. log.info("当前 JVM 的直接子进程:");
  124. long[] childPids = current.children()
  125. .peek(h -> log.info(" 子进程 pid: {}", h.pid()))
  126. .mapToLong(h -> h.pid())
  127. .toArray();
  128. log.info("子进程 PID {} 在 children() 中: {}", process.pid(),
  129. Arrays.binarySearch(childPids, process.pid()) >= 0);
  130. // 子进程的 parent——应指向当前 JVM
  131. ProcessHandle.of(process.pid()).ifPresent(child -> {
  132. Optional<ProcessHandle> parent = child.parent();
  133. log.info("子进程 parent 存在: {}", parent.isPresent());
  134. parent.ifPresent(p -> log.info(" parent pid = {}", p.pid()));
  135. log.info(" parent 就是当前 JVM: {}", parent.isPresent()
  136. && parent.get().pid() == current.pid());
  137. });
  138. // descendants() 当前示例深度为 1,与 children() 内容一致
  139. log.info("descendants() 数量: {}", current.descendants().count());
  140. // allProcesses() 是静态方法,用接口名直接调用;统计当前用户可见的所有活跃进程
  141. try {
  142. long total = ProcessHandle.allProcesses().count();
  143. log.info("当前用户可见活跃进程总数: {}", total);
  144. } catch (Exception e) {
  145. // 部分平台/权限下可能受限
  146. log.info("allProcesses() 调用失败(权限受限): {}", e.getMessage());
  147. }
  148. // 恢复环境
  149. process.destroy();
  150. stdoutDrain.join();
  151. stderrDrain.join();
  152. process.waitFor();
  153. log.info("inspectProcessTree 完成,退出码: {}", process.exitValue());
  154. }
  155. /**
  156. * 案例点:通过句柄销毁进程,对比 Process.destroy()。
  157. * handle.destroy() 发 SIGTERM(Linux);
  158. * handle.onExit() 返回 CompletableFuture,join() 阻塞到进程真正退出;
  159. * 退出后通过 process.exitValue() 读取退出码。
  160. */
  161. private static void terminateViaHandle() throws Exception {
  162. log.info("== 通过句柄销毁进程 ==");
  163. Process process = startChild();
  164. Thread stdoutDrain = drain("stdout", process.getInputStream());
  165. Thread stderrDrain = drain("stderr", process.getErrorStream());
  166. Thread.sleep(300);
  167. ProcessHandle handle = process.toHandle();
  168. log.info("进程存活: {}", handle.isAlive());
  169. // 通过句柄发起终止(Linux 发 SIGTERM,Windows 走 TerminateProcess)
  170. boolean initiated = handle.destroy();
  171. log.info("destroy() 已发起: {}", initiated);
  172. // onExit() 阻塞到进程真正退出
  173. handle.onExit().join();
  174. log.info("进程已退出,handle.isAlive() = {}", handle.isAlive());
  175. // 回收输出流线程
  176. stdoutDrain.join();
  177. stderrDrain.join();
  178. // 读取退出码:被 SIGTERM 终止时通常为 143,Windows 为 1
  179. log.info("退出码 (process.exitValue()): {}", process.exitValue());
  180. log.info("terminateViaHandle 完成");
  181. }
  182. /** 起一个后台线程把给定输入流按行读取并打日志,读到 EOF(子进程退出)后结束 */
  183. private static Thread drain(String tag, InputStream in) {
  184. Thread thread = new Thread(() -> {
  185. try {
  186. new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))
  187. .lines()
  188. .forEach(line -> log.info("[{}] {}", tag, line));
  189. } catch (java.io.UncheckedIOException e) {
  190. // 子进程被 destroy() 销毁时管道会立即关闭,读到一半抛 IOException 属预期行为
  191. log.info("[{}] 子进程被销毁,管道关闭(预期行为)", tag);
  192. }
  193. }, "drain-" + tag);
  194. thread.setDaemon(true);
  195. thread.start();
  196. return thread;
  197. }
  198. }