[util] use method handles to access internal Signal class in UnixProcessManager
[idea/community.git] / platform / util / src / com / intellij / execution / process / UnixProcessManager.java
1 // Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
2 package com.intellij.execution.process;
3
4 import com.intellij.jna.JnaLoader;
5 import com.intellij.openapi.diagnostic.Logger;
6 import com.intellij.openapi.util.Ref;
7 import com.intellij.openapi.util.SystemInfo;
8 import com.intellij.openapi.util.SystemInfoRt;
9 import com.intellij.util.Processor;
10 import com.intellij.util.ReflectionUtil;
11 import com.sun.jna.Library;
12 import com.sun.jna.Native;
13 import org.jetbrains.annotations.NotNull;
14
15 import java.io.BufferedReader;
16 import java.io.File;
17 import java.io.IOException;
18 import java.io.InputStreamReader;
19 import java.lang.invoke.MethodHandle;
20 import java.lang.invoke.MethodHandles;
21 import java.lang.invoke.MethodType;
22 import java.nio.charset.StandardCharsets;
23 import java.util.*;
24
25 /**
26  * Use {@link com.intellij.execution.process.OSProcessUtil} wherever possible.
27  */
28 public final class UnixProcessManager {
29   private static final Logger LOG = Logger.getInstance(UnixProcessManager.class);
30
31   private static final MethodHandle signalStringToIntConverter;
32   static {
33     try {
34       Class<?> signalClass = Class.forName("sun.misc.Signal");
35       MethodHandle signalConstructor = MethodHandles.publicLookup().findConstructor(signalClass, MethodType.methodType(void.class, String.class));
36       MethodHandle getNumber = MethodHandles.publicLookup().findVirtual(signalClass, "getNumber", MethodType.methodType(int.class));
37       signalStringToIntConverter = MethodHandles.filterReturnValue(signalConstructor, getNumber);
38     }
39     catch (Exception e) {
40       throw new RuntimeException(e);
41     }
42   }
43
44   // https://en.wikipedia.org/wiki/Signal_(IPC)#POSIX_signals
45   public static final int SIGINT = 2;
46   public static final int SIGABRT = 6;
47   public static final int SIGKILL = 9;
48   public static final int SIGTERM = 15;
49   public static final int SIGPIPE = getSignalNumber("PIPE");
50
51   private UnixProcessManager() { }
52
53   public static int getProcessId(@NotNull Process process) {
54     try {
55       if (SystemInfoRt.IS_AT_LEAST_JAVA9 && "java.lang.ProcessImpl".equals(process.getClass().getName())) {
56         //noinspection JavaReflectionMemberAccess
57         return ((Long)Process.class.getMethod("pid").invoke(process)).intValue();
58       }
59
60       return Objects.requireNonNull(ReflectionUtil.getField(process.getClass(), process, int.class, "pid"));
61     }
62     catch (Throwable t) {
63       throw new IllegalStateException("Failed to get PID from an instance of " + process.getClass() + ", OS: " + SystemInfo.OS_NAME, t);
64     }
65   }
66
67   public static int getCurrentProcessId() {
68     return Java8Helper.C_LIB != null ? Java8Helper.C_LIB.getpid() : 0;
69   }
70
71   /**
72    * @param signalName without the 'SIG' prefix ('INT', not 'SIGINT')
73    * @return -1 for unknown signal
74    */
75   public static int getSignalNumber(@NotNull String signalName) {
76     try {
77       return (int)signalStringToIntConverter.invokeExact(signalName);
78     }
79     catch (IllegalArgumentException e) {
80       return -1;
81     }
82     catch (Throwable t) {
83       throw new RuntimeException(t);
84     }
85   }
86
87   public static int sendSignal(int pid, @NotNull String signalName) {
88     final int signalNumber = getSignalNumber(signalName);
89     return (signalNumber == -1) ? -1 : sendSignal(pid, signalNumber);
90   }
91
92   public static int sendSignal(int pid, int signal) {
93     checkCLib();
94     return Java8Helper.C_LIB.kill(pid, signal);
95   }
96
97   private static void checkCLib() {
98     if (Java8Helper.C_LIB == null) {
99       throw new IllegalStateException("Couldn't load c library, OS: " + SystemInfo.OS_NAME + ", isUnix: " + SystemInfo.isUnix);
100     }
101   }
102
103   public static boolean sendSigIntToProcessTree(@NotNull Process process) {
104     return sendSignalToProcessTree(process, SIGINT);
105   }
106
107   public static boolean sendSigKillToProcessTree(@NotNull Process process) {
108     return sendSignalToProcessTree(process, SIGKILL);
109   }
110
111   public static boolean sendSignalToProcessTree(@NotNull Process process, int signal) {
112     try {
113       return sendSignalToProcessTree(getProcessId(process), signal);
114     }
115     catch (Exception e) {
116       LOG.warn("Error killing the process", e);
117       return false;
118     }
119   }
120
121   public static boolean sendSignalToProcessTree(int processId, int signal) {
122     checkCLib();
123
124     int ourPid = Java8Helper.C_LIB.getpid();
125     return sendSignalToProcessTree(processId, signal, ourPid);
126   }
127
128   public static boolean sendSignalToProcessTree(int processId, int signal, int ourPid) {
129     if (LOG.isDebugEnabled()) {
130       LOG.debug("Sending signal " + signal + " to process tree with root PID " + processId);
131     }
132
133     final Ref<Integer> foundPid = new Ref<>();
134     final ProcessInfo processInfo = new ProcessInfo();
135     final List<Integer> childrenPids = new ArrayList<>();
136
137     findChildProcesses(ourPid, processId, foundPid, processInfo, childrenPids);
138
139     // result is true if signal was sent to at least one process
140     final boolean result;
141     if (!foundPid.isNull()) {
142       processInfo.killProcTree(foundPid.get(), signal);
143       result = true;
144     }
145     else {
146       for (Integer pid : childrenPids) {
147         processInfo.killProcTree(pid, signal);
148       }
149       result = !childrenPids.isEmpty(); //we've tried to kill at least one process
150     }
151     if (LOG.isDebugEnabled()) {
152       LOG.debug("Done sending signal " + signal + "; found: " + foundPid.get() + ", children: " + childrenPids + ", result: " + result);
153     }
154
155     return result;
156   }
157
158   private static void findChildProcesses(final int our_pid,
159                                          final int process_pid,
160                                          final Ref<? super Integer> foundPid,
161                                          final ProcessInfo processInfo,
162                                          final List<? super Integer> childrenPids) {
163     final Ref<Boolean> ourPidFound = Ref.create(false);
164     processPSOutput(getPSCmd(false), s -> {
165       StringTokenizer st = new StringTokenizer(s, " ");
166
167       int parent_pid = Integer.parseInt(st.nextToken());
168       int pid = Integer.parseInt(st.nextToken());
169
170       processInfo.register(pid, parent_pid);
171
172       if (parent_pid == process_pid) {
173         childrenPids.add(pid);
174       }
175
176       if (pid == our_pid) {
177         ourPidFound.set(true);
178       }
179       else if (pid == process_pid) {
180         if (parent_pid == our_pid || our_pid == -1) {
181           foundPid.set(pid);
182         }
183         else {
184           throw new IllegalStateException("Process (pid=" + process_pid + ") is not our child(our pid = " + our_pid + ")");
185         }
186       }
187       return false;
188     });
189     if (our_pid != -1 && !ourPidFound.get()) {
190       throw new IllegalStateException("IDE pid is not found in ps list(" + our_pid + ")");
191     }
192   }
193
194   public static void processPSOutput(String[] cmd, Processor<? super String> processor) {
195     processCommandOutput(cmd, processor, true, true);
196   }
197
198   public static void processCommandOutput(String[] cmd, Processor<? super String> processor, boolean skipFirstLine, boolean throwOnError) {
199     try {
200       Process p = Runtime.getRuntime().exec(cmd);
201       processCommandOutput(p, processor, skipFirstLine, throwOnError);
202     }
203     catch (IOException e) {
204       throw new IllegalStateException(e);
205     }
206   }
207
208   private static void processCommandOutput(Process process, Processor<? super String> processor, boolean skipFirstLine, boolean throwOnError) throws IOException {
209     try (BufferedReader stdOutput = new BufferedReader(new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) {
210       try (BufferedReader stdError = new BufferedReader(new InputStreamReader(process.getErrorStream(), StandardCharsets.UTF_8))) {
211         if (skipFirstLine) {
212           stdOutput.readLine(); //ps output header
213         }
214         String s;
215         while ((s = stdOutput.readLine()) != null) {
216           processor.process(s);
217         }
218
219         StringBuilder errorStr = new StringBuilder();
220         while ((s = stdError.readLine()) != null) {
221           if (s.contains("environment variables being ignored")) {  // PY-8160
222             continue;
223           }
224           errorStr.append(s).append("\n");
225         }
226         if (throwOnError && errorStr.length() > 0) {
227           throw new IOException("Error reading ps output:" + errorStr);
228         }
229       }
230     }
231   }
232
233   public static String[] getPSCmd(boolean commandLineOnly) {
234     return getPSCmd(commandLineOnly, false);
235   }
236
237   public static String[] getPSCmd(boolean commandLineOnly, boolean isShortenCommand) {
238     String psCommand = "/bin/ps";
239     if (!new File(psCommand).isFile()) {
240       psCommand = "ps";
241     }
242     if (SystemInfo.isLinux) {
243       return new String[]{psCommand, "-e", "--format", commandLineOnly ? "%a" : "%P%p%a"};
244     }
245     else if (SystemInfo.isMac || SystemInfo.isFreeBSD) {
246       final String command = isShortenCommand ? "comm" : "command";
247       return new String[]{psCommand, "-ax", "-o", commandLineOnly ? command : "ppid,pid," + command};
248     }
249     else {
250       throw new IllegalStateException(System.getProperty("os.name") + " is not supported.");
251     }
252   }
253
254   private static class ProcessInfo {
255     private final Map<Integer, List<Integer>> BY_PARENT = new TreeMap<>(); // pid -> list of children pids
256
257     public void register(Integer pid, Integer parentPid) {
258       List<Integer> children = BY_PARENT.get(parentPid);
259       if (children == null) BY_PARENT.put(parentPid, children = new LinkedList<>());
260       children.add(pid);
261     }
262
263     public void killProcTree(int pid, int signal) {
264       List<Integer> children = BY_PARENT.get(pid);
265       if (children != null) {
266         for (int child : children) {
267           killProcTree(child, signal);
268         }
269       }
270       if (LOG.isDebugEnabled()) {
271         LOG.debug("Sending signal " + signal + " to PID " + pid);
272       }
273       sendSignal(pid, signal);
274     }
275   }
276 }
277
278 final class Java8Helper {
279   interface CLib extends Library {
280     int getpid();
281
282     int kill(int pid, int signal);
283   }
284
285   static final CLib C_LIB;
286
287   static {
288     CLib lib = null;
289     try {
290       if (SystemInfoRt.isUnix && JnaLoader.isLoaded()) {
291         lib = Native.load("c", CLib.class);
292       }
293     }
294     catch (Throwable t) {
295       Logger.getInstance(UnixProcessManager.class).warn("Can't load standard library", t);
296     }
297     C_LIB = lib;
298   }
299 }