/* * Copyright 2012-present Facebook, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ package com.facebook.buck.util; import com.facebook.buck.log.Logger; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Supplier; import com.google.common.base.Suppliers; import com.sun.jna.Pointer; import com.sun.jna.platform.win32.Kernel32; import com.sun.jna.platform.win32.WinNT; import com.zaxxer.nuprocess.NuProcess; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; import javax.annotation.Nullable; import javax.lang.model.SourceVersion; import oshi.SystemInfo; import oshi.software.os.OSProcess; import oshi.software.os.OperatingSystem; /** * A helper singleton that provides facilities such as extracting the native process id of a {@link * Process} or gathering the process resource consumption. */ public class ProcessHelper { private static final Logger LOG = Logger.get(ProcessHelper.class); // Comparing with the string value to avoid a strong dependency on JDK 9 private static final boolean IS_JDK9 = SourceVersion.latest().toString().equals("RELEASE_9"); private static final SystemInfo OSHI = new SystemInfo(); private static final ProcessHelper INSTANCE = new ProcessHelper(); /** Gets the singleton instance of this class. */ public static ProcessHelper getInstance() { return INSTANCE; } private Supplier<ProcessTree> processTree = Suppliers.memoizeWithExpiration( () -> { ProcessTree tree = new ProcessTree(); try { LOG.verbose("Getting process tree..."); OperatingSystem os = OSHI.getOperatingSystem(); OSProcess[] processes = os.getProcesses(100, OperatingSystem.ProcessSort.NEWEST); for (OSProcess process : processes) { tree.add(process); } LOG.verbose("Process tree built."); } catch (Exception e) { LOG.warn(e, "Cannot get the process tree!"); } return tree; }, 1, TimeUnit.SECONDS); /** This is a helper singleton. */ @VisibleForTesting ProcessHelper() {} /** Gets resource consumption of the process subtree rooted at the process with the given pid. */ @Nullable public ProcessResourceConsumption getTotalResourceConsumption(long pid) { final ProcessResourceConsumption[] res = new ProcessResourceConsumption[] {null}; ProcessTree tree = processTree.get(); tree.visitAllDescendants( pid, (childPid, childNode) -> { ProcessResourceConsumption childRes = getProcessResourceConsumptionInternal(childNode.info); res[0] = ProcessResourceConsumption.getTotal(res[0], childRes); }); // fallback to the root process only if the above fails return (res[0] != null) ? res[0] : getProcessResourceConsumption(pid); } /** Gets resource consumption of the process with the given pid. */ @Nullable public ProcessResourceConsumption getProcessResourceConsumption(long pid) { try { OperatingSystem os = OSHI.getOperatingSystem(); OSProcess process = os.getProcess((int) pid); return getProcessResourceConsumptionInternal(process); } catch (Exception ex) { // do nothing return null; } } @Nullable static ProcessResourceConsumption getProcessResourceConsumptionInternal( @Nullable OSProcess process) { if (process == null) { return null; } return ProcessResourceConsumption.builder() .setMemResident(process.getResidentSetSize()) .setMemSize(process.getVirtualSize()) .setCpuReal(process.getUpTime()) .setCpuUser(process.getUserTime()) .setCpuSys(process.getKernelTime()) .setCpuTotal(process.getUserTime() + process.getKernelTime()) .setIoBytesRead(process.getBytesRead()) .setIoBytesWritten(process.getBytesWritten()) .setIoTotal(process.getBytesRead() + process.getBytesWritten()) .build(); } /** @return whether the process has finished executing or not. */ public boolean hasProcessFinished(Object process) { if (process instanceof NuProcess) { return !((NuProcess) process).isRunning(); } else if (process instanceof Process) { try { ((Process) process).exitValue(); return true; } catch (IllegalThreadStateException e) { return false; } } else { throw new IllegalArgumentException("Unknown process class: " + process.getClass().toString()); } } /** Gets the native process identifier for the current process. */ @Nullable public Long getPid() { try { return (long) OSHI.getOperatingSystem().getProcessId(); } catch (Exception e) { LOG.warn(e, "Cannot get the current process id!"); return null; } } /** Gets the native process identifier for the given process instance. */ @Nullable public Long getPid(Object process) { if (process instanceof NuProcess) { return (long) ((NuProcess) process).getPID(); } else if (process instanceof Process) { // Until we switch to JDK9, we will have to go with this per-platform workaround. // In JDK9, `Process` has `getPid()` method that does exactly what we need here. // http://download.java.net/java/jdk9/docs/api/java/lang/Process.html#getPid-- Long pid = jdk9ProcessId(process); if (pid == null) { pid = unixLikeProcessId(process); } if (pid == null) { pid = windowsProcessId(process); } return pid; } else { throw new IllegalArgumentException("Unknown process class: " + process.getClass().toString()); } } @Nullable private Long jdk9ProcessId(Object process) { if (IS_JDK9) { try { // Invoking via reflection to avoid a strong dependency on JDK 9 Method getPid = Process.class.getMethod("getPid"); return (Long) getPid.invoke(process); } catch (Exception e) { LOG.warn(e, "Cannot get process id!"); } } return null; } @Nullable private Long unixLikeProcessId(Object process) { Class<?> clazz = process.getClass(); try { if (clazz.getName().equals("java.lang.UNIXProcess")) { Field field = clazz.getDeclaredField("pid"); field.setAccessible(true); return (long) field.getInt(process); } } catch (Exception e) { LOG.warn(e, "Cannot get process id!"); } return null; } @Nullable private Long windowsProcessId(Object process) { Class<?> clazz = process.getClass(); if (clazz.getName().equals("java.lang.Win32Process") || clazz.getName().equals("java.lang.ProcessImpl")) { try { Field f = clazz.getDeclaredField("handle"); f.setAccessible(true); long peer = f.getLong(process); Pointer pointer = Pointer.createConstant(peer); WinNT.HANDLE handle = new WinNT.HANDLE(pointer); return (long) Kernel32.INSTANCE.GetProcessId(handle); } catch (Exception e) { LOG.warn(e, "Cannot get process id!"); } } return null; } /** A simple representation of a process tree. */ private static class ProcessTree { public interface Visitor { void visit(long pid, Node node); } public static class Node { public final long pid; // May be null if it's the parent of a process, and we sampled a subset of processes which // didn't include the parent. @Nullable private OSProcess info; private final List<Node> children = new ArrayList<>(); public Node(long pid) { this.pid = pid; } private void addChild(Node child) { children.add(child); } } private final Map<Long, Node> nodes = new HashMap<>(); private Node getOrCreate(long pid) { Node node = nodes.get(pid); if (node == null) { node = new Node(pid); nodes.put(pid, node); } return node; } public Node add(OSProcess info) { Node node = getOrCreate(info.getProcessID()); if (node.info == null) { node.info = info; } Node parent = getOrCreate(info.getParentProcessID()); parent.addChild(node); return node; } @Nullable public Node get(long pid) { return nodes.get(pid); } public void visitAllDescendants(long pid, Visitor visitor) { Node root = get(pid); if (root != null) { visitor.visit(pid, root); for (Node child : root.children) { visitAllDescendants(child.pid, visitor); } } } } }