View Javadoc
1   /*
2    * Copyright 2020-2023 The OSHI Project Contributors
3    * SPDX-License-Identifier: MIT
4    */
5   package oshi.software.os.linux;
6   
7   import static oshi.software.os.OSProcess.State.INVALID;
8   import static oshi.software.os.OSThread.ThreadFiltering.VALID_THREAD;
9   import static oshi.util.Memoizer.memoize;
10  
11  import java.io.File;
12  import java.io.FileInputStream;
13  import java.io.IOException;
14  import java.io.InputStream;
15  import java.math.BigInteger;
16  import java.nio.file.Files;
17  import java.nio.file.InvalidPathException;
18  import java.nio.file.Path;
19  import java.nio.file.Paths;
20  import java.util.Arrays;
21  import java.util.Collections;
22  import java.util.List;
23  import java.util.Locale;
24  import java.util.Map;
25  import java.util.Optional;
26  import java.util.function.Supplier;
27  import java.util.stream.Collectors;
28  
29  import com.sun.jna.platform.unix.Resource;
30  import org.slf4j.Logger;
31  import org.slf4j.LoggerFactory;
32  
33  import oshi.annotation.concurrent.ThreadSafe;
34  import oshi.driver.linux.proc.ProcessStat;
35  import oshi.jna.platform.linux.LinuxLibc;
36  import oshi.software.common.AbstractOSProcess;
37  import oshi.software.os.OSThread;
38  import oshi.util.ExecutingCommand;
39  import oshi.util.FileUtil;
40  import oshi.util.GlobalConfig;
41  import oshi.util.ParseUtil;
42  import oshi.util.UserGroupInfo;
43  import oshi.util.Util;
44  import oshi.util.platform.linux.ProcPath;
45  
46  /**
47   * OSProcess implementation
48   */
49  @ThreadSafe
50  public class LinuxOSProcess extends AbstractOSProcess {
51  
52      private static final Logger LOG = LoggerFactory.getLogger(LinuxOSProcess.class);
53  
54      private static final boolean LOG_PROCFS_WARNING = GlobalConfig.get(GlobalConfig.OSHI_OS_LINUX_PROCFS_LOGWARNING,
55              false);
56  
57      // Get a list of orders to pass to ParseUtil
58      private static final int[] PROC_PID_STAT_ORDERS = new int[ProcPidStat.values().length];
59  
60      static {
61          for (ProcPidStat stat : ProcPidStat.values()) {
62              // The PROC_PID_STAT enum indices are 1-indexed.
63              // Subtract one to get a zero-based index
64              PROC_PID_STAT_ORDERS[stat.ordinal()] = stat.getOrder() - 1;
65          }
66      }
67  
68      private final LinuxOperatingSystem os;
69  
70      private Supplier<Integer> bitness = memoize(this::queryBitness);
71      private Supplier<String> commandLine = memoize(this::queryCommandLine);
72      private Supplier<List<String>> arguments = memoize(this::queryArguments);
73      private Supplier<Map<String, String>> environmentVariables = memoize(this::queryEnvironmentVariables);
74      private Supplier<String> user = memoize(this::queryUser);
75      private Supplier<String> group = memoize(this::queryGroup);
76  
77      private String name;
78      private String path = "";
79      private String userID;
80      private String groupID;
81      private State state = INVALID;
82      private int parentProcessID;
83      private int threadCount;
84      private int priority;
85      private long virtualSize;
86      private long residentSetSize;
87      private long kernelTime;
88      private long userTime;
89      private long startTime;
90      private long upTime;
91      private long bytesRead;
92      private long bytesWritten;
93      private long minorFaults;
94      private long majorFaults;
95      private long contextSwitches;
96  
97      public LinuxOSProcess(int pid, LinuxOperatingSystem os) {
98          super(pid);
99          this.os = os;
100         updateAttributes();
101     }
102 
103     @Override
104     public String getName() {
105         return this.name;
106     }
107 
108     @Override
109     public String getPath() {
110         return this.path;
111     }
112 
113     @Override
114     public String getCommandLine() {
115         return commandLine.get();
116     }
117 
118     private String queryCommandLine() {
119         return Arrays.stream(FileUtil
120                 .getStringFromFile(String.format(Locale.ROOT, ProcPath.PID_CMDLINE, getProcessID())).split("\0"))
121                 .collect(Collectors.joining(" "));
122     }
123 
124     @Override
125     public List<String> getArguments() {
126         return arguments.get();
127     }
128 
129     private List<String> queryArguments() {
130         return Collections.unmodifiableList(ParseUtil.parseByteArrayToStrings(
131                 FileUtil.readAllBytes(String.format(Locale.ROOT, ProcPath.PID_CMDLINE, getProcessID()), LOG_PROCFS_WARNING)));
132     }
133 
134     @Override
135     public Map<String, String> getEnvironmentVariables() {
136         return environmentVariables.get();
137     }
138 
139     private Map<String, String> queryEnvironmentVariables() {
140         return Collections.unmodifiableMap(ParseUtil.parseByteArrayToStringMap(FileUtil
141                 .readAllBytes(String.format(Locale.ROOT, ProcPath.PID_ENVIRON, getProcessID()), LOG_PROCFS_WARNING)));
142     }
143 
144     @Override
145     public String getCurrentWorkingDirectory() {
146         try {
147             String cwdLink = String.format(Locale.ROOT, ProcPath.PID_CWD, getProcessID());
148             String cwd = new File(cwdLink).getCanonicalPath();
149             if (!cwd.equals(cwdLink)) {
150                 return cwd;
151             }
152         } catch (IOException e) {
153             LOG.trace("Couldn't find cwd for pid {}: {}", getProcessID(), e.getMessage());
154         }
155         return "";
156     }
157 
158     @Override
159     public String getUser() {
160         return user.get();
161     }
162 
163     private String queryUser() {
164         return UserGroupInfo.getUser(userID);
165     }
166 
167     @Override
168     public String getUserID() {
169         return this.userID;
170     }
171 
172     @Override
173     public String getGroup() {
174         return group.get();
175     }
176 
177     private String queryGroup() {
178         return UserGroupInfo.getGroupName(groupID);
179     }
180 
181     @Override
182     public String getGroupID() {
183         return this.groupID;
184     }
185 
186     @Override
187     public State getState() {
188         return this.state;
189     }
190 
191     @Override
192     public int getParentProcessID() {
193         return this.parentProcessID;
194     }
195 
196     @Override
197     public int getThreadCount() {
198         return this.threadCount;
199     }
200 
201     @Override
202     public int getPriority() {
203         return this.priority;
204     }
205 
206     @Override
207     public long getVirtualSize() {
208         return this.virtualSize;
209     }
210 
211     @Override
212     public long getResidentSetSize() {
213         return this.residentSetSize;
214     }
215 
216     @Override
217     public long getKernelTime() {
218         return this.kernelTime;
219     }
220 
221     @Override
222     public long getUserTime() {
223         return this.userTime;
224     }
225 
226     @Override
227     public long getUpTime() {
228         return this.upTime;
229     }
230 
231     @Override
232     public long getStartTime() {
233         return this.startTime;
234     }
235 
236     @Override
237     public long getBytesRead() {
238         return this.bytesRead;
239     }
240 
241     @Override
242     public long getBytesWritten() {
243         return this.bytesWritten;
244     }
245 
246     @Override
247     public List<OSThread> getThreadDetails() {
248         return ProcessStat.getThreadIds(getProcessID()).stream().parallel()
249                 .map(id -> new LinuxOSThread(getProcessID(), id)).filter(VALID_THREAD).collect(Collectors.toList());
250     }
251 
252     @Override
253     public long getMinorFaults() {
254         return this.minorFaults;
255     }
256 
257     @Override
258     public long getMajorFaults() {
259         return this.majorFaults;
260     }
261 
262     @Override
263     public long getContextSwitches() {
264         return this.contextSwitches;
265     }
266 
267     @Override
268     public long getOpenFiles() {
269         return ProcessStat.getFileDescriptorFiles(getProcessID()).length;
270     }
271 
272     @Override
273     public long getSoftOpenFileLimit() {
274         if (getProcessID() == this.os.getProcessId()) {
275             final Resource.Rlimit rlimit = new Resource.Rlimit();
276             LinuxLibc.INSTANCE.getrlimit(LinuxLibc.RLIMIT_NOFILE, rlimit);
277             return rlimit.rlim_cur;
278         } else {
279             return getProcessOpenFileLimit(getProcessID(), 1);
280         }
281     }
282 
283     @Override
284     public long getHardOpenFileLimit() {
285         if (getProcessID() == this.os.getProcessId()) {
286             final Resource.Rlimit rlimit = new Resource.Rlimit();
287             LinuxLibc.INSTANCE.getrlimit(LinuxLibc.RLIMIT_NOFILE, rlimit);
288             return rlimit.rlim_max;
289         } else {
290             return getProcessOpenFileLimit(getProcessID(), 2);
291         }
292     }
293 
294     @Override
295     public int getBitness() {
296         return this.bitness.get();
297     }
298 
299     private int queryBitness() {
300         // get 5th byte of file for 64-bit check
301         // https://en.wikipedia.org/wiki/Executable_and_Linkable_Format#File_header
302         byte[] buffer = new byte[5];
303         if (!path.isEmpty()) {
304             try (InputStream is = new FileInputStream(path)) {
305                 if (is.read(buffer) == buffer.length) {
306                     return buffer[4] == 1 ? 32 : 64;
307                 }
308             } catch (IOException e) {
309                 LOG.warn("Failed to read process file: {}", path);
310             }
311         }
312         return 0;
313     }
314 
315     @Override
316     public long getAffinityMask() {
317         // Would prefer to use native sched_getaffinity call but variable sizing is
318         // kernel-dependent and requires C macros, so we use command line instead.
319         String mask = ExecutingCommand.getFirstAnswer("taskset -p " + getProcessID());
320         // Output:
321         // pid 3283's current affinity mask: 3
322         // pid 9726's current affinity mask: f
323         String[] split = ParseUtil.whitespaces.split(mask);
324         try {
325             return new BigInteger(split[split.length - 1], 16).longValue();
326         } catch (NumberFormatException e) {
327             return 0;
328         }
329     }
330 
331     @Override
332     public boolean updateAttributes() {
333         String procPidExe = String.format(Locale.ROOT, ProcPath.PID_EXE, getProcessID());
334         try {
335             Path link = Paths.get(procPidExe);
336             this.path = Files.readSymbolicLink(link).toString();
337             // For some services the symbolic link process has terminated
338             int index = path.indexOf(" (deleted)");
339             if (index != -1) {
340                 path = path.substring(0, index);
341             }
342         } catch (InvalidPathException | IOException | UnsupportedOperationException | SecurityException e) {
343             LOG.debug("Unable to open symbolic link {}", procPidExe);
344         }
345         // Fetch all the values here
346         // check for terminated process race condition after last one.
347         Map<String, String> io = FileUtil
348                 .getKeyValueMapFromFile(String.format(Locale.ROOT, ProcPath.PID_IO, getProcessID()), ":");
349         Map<String, String> status = FileUtil
350                 .getKeyValueMapFromFile(String.format(Locale.ROOT, ProcPath.PID_STATUS, getProcessID()), ":");
351         String stat = FileUtil.getStringFromFile(String.format(Locale.ROOT, ProcPath.PID_STAT, getProcessID()));
352         if (stat.isEmpty()) {
353             this.state = INVALID;
354             return false;
355         }
356         // If some details couldn't be read from ProcPath.PID_STATUS try reading it from
357         // ProcPath.PID_STAT
358         getMissingDetails(status, stat);
359 
360         long now = System.currentTimeMillis();
361 
362         // We can get name and status more easily from /proc/pid/status which we
363         // call later, so just get the numeric bits here
364         // See man proc for how to parse /proc/[pid]/stat
365         long[] statArray = ParseUtil.parseStringToLongArray(stat, PROC_PID_STAT_ORDERS,
366                 ProcessStat.PROC_PID_STAT_LENGTH, ' ');
367 
368         // BOOTTIME is in seconds and start time from proc/pid/stat is in jiffies.
369         // Combine units to jiffies and convert to millijiffies before hz division to
370         // avoid precision loss without having to cast
371         this.startTime = (LinuxOperatingSystem.BOOTTIME * LinuxOperatingSystem.getHz()
372                 + statArray[ProcPidStat.START_TIME.ordinal()]) * 1000L / LinuxOperatingSystem.getHz();
373         // BOOT_TIME could be up to 500ms off and start time up to 5ms off. A process
374         // that has started within last 505ms could produce a future start time/negative
375         // up time, so insert a sanity check.
376         if (startTime >= now) {
377             startTime = now - 1;
378         }
379         this.parentProcessID = (int) statArray[ProcPidStat.PPID.ordinal()];
380         this.threadCount = (int) statArray[ProcPidStat.THREAD_COUNT.ordinal()];
381         this.priority = (int) statArray[ProcPidStat.PRIORITY.ordinal()];
382         this.virtualSize = statArray[ProcPidStat.VSZ.ordinal()];
383         this.residentSetSize = statArray[ProcPidStat.RSS.ordinal()] * LinuxOperatingSystem.getPageSize();
384         this.kernelTime = statArray[ProcPidStat.KERNEL_TIME.ordinal()] * 1000L / LinuxOperatingSystem.getHz();
385         this.userTime = statArray[ProcPidStat.USER_TIME.ordinal()] * 1000L / LinuxOperatingSystem.getHz();
386         this.minorFaults = statArray[ProcPidStat.MINOR_FAULTS.ordinal()];
387         this.majorFaults = statArray[ProcPidStat.MAJOR_FAULTS.ordinal()];
388         long nonVoluntaryContextSwitches = ParseUtil.parseLongOrDefault(status.get("nonvoluntary_ctxt_switches"), 0L);
389         long voluntaryContextSwitches = ParseUtil.parseLongOrDefault(status.get("voluntary_ctxt_switches"), 0L);
390         this.contextSwitches = voluntaryContextSwitches + nonVoluntaryContextSwitches;
391 
392         this.upTime = now - startTime;
393 
394         // See man proc for how to parse /proc/[pid]/io
395         this.bytesRead = ParseUtil.parseLongOrDefault(io.getOrDefault("read_bytes", ""), 0L);
396         this.bytesWritten = ParseUtil.parseLongOrDefault(io.getOrDefault("write_bytes", ""), 0L);
397 
398         // Don't set open files or bitness or currentWorkingDirectory; fetch on demand.
399 
400         this.userID = ParseUtil.whitespaces.split(status.getOrDefault("Uid", ""))[0];
401         // defer user lookup until asked
402         this.groupID = ParseUtil.whitespaces.split(status.getOrDefault("Gid", ""))[0];
403         // defer group lookup until asked
404         this.name = status.getOrDefault("Name", "");
405         this.state = ProcessStat.getState(status.getOrDefault("State", "U").charAt(0));
406         return true;
407     }
408 
409     /**
410      * If some details couldn't be read from ProcPath.PID_STATUS try reading it from ProcPath.PID_STAT
411      *
412      * @param status status map to fill.
413      * @param stat   string to read from.
414      */
415     private static void getMissingDetails(Map<String, String> status, String stat) {
416         if (status == null || stat == null) {
417             return;
418         }
419 
420         int nameStart = stat.indexOf('(');
421         int nameEnd = stat.indexOf(')');
422         if (Util.isBlank(status.get("Name")) && nameStart > 0 && nameStart < nameEnd) {
423             // remove leading and trailing parentheses
424             String statName = stat.substring(nameStart + 1, nameEnd);
425             status.put("Name", statName);
426         }
427 
428         // As per man, the next item after the name is the state
429         if (Util.isBlank(status.get("State")) && nameEnd > 0 && stat.length() > nameEnd + 2) {
430             String statState = String.valueOf(stat.charAt(nameEnd + 2));
431             status.put("State", statState);
432         }
433     }
434 
435     /**
436      * Enum used to update attributes. The order field represents the 1-indexed numeric order of the stat in
437      * /proc/pid/stat per the man file.
438      */
439     private enum ProcPidStat {
440         // The parsing implementation in ParseUtil requires these to be declared
441         // in increasing order
442         PPID(4), MINOR_FAULTS(10), MAJOR_FAULTS(12), USER_TIME(14), KERNEL_TIME(15), PRIORITY(18), THREAD_COUNT(20),
443         START_TIME(22), VSZ(23), RSS(24);
444 
445         private final int order;
446 
447         public int getOrder() {
448             return this.order;
449         }
450 
451         ProcPidStat(int order) {
452             this.order = order;
453         }
454     }
455 
456     private long getProcessOpenFileLimit(long processId, int index) {
457         final String limitsPath = String.format(Locale.ROOT, "/proc/%d/limits", processId);
458         if (!Files.exists(Paths.get(limitsPath))) {
459             return -1; // not supported
460         }
461         final List<String> lines = FileUtil.readFile(limitsPath);
462         final Optional<String> maxOpenFilesLine = lines.stream().filter(line -> line.startsWith("Max open files"))
463                 .findFirst();
464         if (!maxOpenFilesLine.isPresent()) {
465             return -1;
466         }
467 
468         // Split all non-Digits away -> ["", "{soft-limit}, "{hard-limit}"]
469         final String[] split = maxOpenFilesLine.get().split("\\D+");
470         return ParseUtil.parseLongOrDefault(split[index], -1);
471     }
472 }