View Javadoc
1   /*
2    * Copyright 2020-2023 The OSHI Project Contributors
3    * SPDX-License-Identifier: MIT
4    */
5   package oshi.software.os.unix.freebsd;
6   
7   import static oshi.software.os.OSProcess.State.INVALID;
8   import static oshi.software.os.OSProcess.State.OTHER;
9   import static oshi.software.os.OSProcess.State.RUNNING;
10  import static oshi.software.os.OSProcess.State.SLEEPING;
11  import static oshi.software.os.OSProcess.State.STOPPED;
12  import static oshi.software.os.OSProcess.State.WAITING;
13  import static oshi.software.os.OSProcess.State.ZOMBIE;
14  import static oshi.software.os.OSThread.ThreadFiltering.VALID_THREAD;
15  import static oshi.util.Memoizer.memoize;
16  
17  import java.nio.file.Files;
18  import java.nio.file.Paths;
19  import java.util.Arrays;
20  import java.util.Collections;
21  import java.util.List;
22  import java.util.Locale;
23  import java.util.Map;
24  import java.util.Optional;
25  import java.util.function.Predicate;
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 com.sun.jna.Memory;
34  import com.sun.jna.Native;
35  import com.sun.jna.platform.unix.LibCAPI.size_t;
36  
37  import oshi.annotation.concurrent.ThreadSafe;
38  import oshi.jna.ByRef.CloseableSizeTByReference;
39  import oshi.jna.platform.unix.FreeBsdLibc;
40  import oshi.software.common.AbstractOSProcess;
41  import oshi.software.os.OSThread;
42  import oshi.software.os.unix.freebsd.FreeBsdOperatingSystem.PsKeywords;
43  import oshi.util.ExecutingCommand;
44  import oshi.util.FileUtil;
45  import oshi.util.ParseUtil;
46  import oshi.util.platform.unix.freebsd.BsdSysctlUtil;
47  import oshi.util.platform.unix.freebsd.ProcstatUtil;
48  
49  /**
50   * OSProcess implementation
51   */
52  @ThreadSafe
53  public class FreeBsdOSProcess extends AbstractOSProcess {
54  
55      private static final Logger LOG = LoggerFactory.getLogger(FreeBsdOSProcess.class);
56  
57      private static final int ARGMAX = BsdSysctlUtil.sysctl("kern.argmax", 0);
58  
59      private final FreeBsdOperatingSystem os;
60  
61      /*
62       * Package-private for use by FreeBsdOSThread
63       */
64      enum PsThreadColumns {
65          TDNAME, LWP, STATE, ETIMES, SYSTIME, TIME, TDADDR, NIVCSW, NVCSW, MAJFLT, MINFLT, PRI;
66      }
67  
68      static final String PS_THREAD_COLUMNS = Arrays.stream(PsThreadColumns.values()).map(Enum::name)
69              .map(name -> name.toLowerCase(Locale.ROOT)).collect(Collectors.joining(","));
70  
71      private Supplier<Integer> bitness = memoize(this::queryBitness);
72      private Supplier<String> commandLine = memoize(this::queryCommandLine);
73      private Supplier<List<String>> arguments = memoize(this::queryArguments);
74      private Supplier<Map<String, String>> environmentVariables = memoize(this::queryEnvironmentVariables);
75  
76      private String name;
77      private String path = "";
78      private String user;
79      private String userID;
80      private String group;
81      private String groupID;
82      private State state = INVALID;
83      private int parentProcessID;
84      private int threadCount;
85      private int priority;
86      private long virtualSize;
87      private long residentSetSize;
88      private long kernelTime;
89      private long userTime;
90      private long startTime;
91      private long upTime;
92      private long bytesRead;
93      private long bytesWritten;
94      private long minorFaults;
95      private long majorFaults;
96      private long contextSwitches;
97      private String commandLineBackup;
98  
99      public FreeBsdOSProcess(int pid, Map<PsKeywords, String> psMap, FreeBsdOperatingSystem os) {
100         super(pid);
101         this.os = os;
102         updateAttributes(psMap);
103     }
104 
105     @Override
106     public String getName() {
107         return this.name;
108     }
109 
110     @Override
111     public String getPath() {
112         return this.path;
113     }
114 
115     @Override
116     public String getCommandLine() {
117         return this.commandLine.get();
118     }
119 
120     private String queryCommandLine() {
121         String cl = String.join(" ", getArguments());
122         return cl.isEmpty() ? this.commandLineBackup : cl;
123     }
124 
125     @Override
126     public List<String> getArguments() {
127         return arguments.get();
128     }
129 
130     private List<String> queryArguments() {
131         if (ARGMAX > 0) {
132             // Get arguments via sysctl(3)
133             int[] mib = new int[4];
134             mib[0] = 1; // CTL_KERN
135             mib[1] = 14; // KERN_PROC
136             mib[2] = 7; // KERN_PROC_ARGS
137             mib[3] = getProcessID();
138             // Allocate memory for arguments
139             try (Memory m = new Memory(ARGMAX);
140                     CloseableSizeTByReference size = new CloseableSizeTByReference(ARGMAX)) {
141                 // Fetch arguments
142                 if (FreeBsdLibc.INSTANCE.sysctl(mib, mib.length, m, size, null, size_t.ZERO) == 0) {
143                     return Collections.unmodifiableList(
144                             ParseUtil.parseByteArrayToStrings(m.getByteArray(0, size.getValue().intValue())));
145                 } else {
146                     LOG.warn(
147                             "Failed sysctl call for process arguments (kern.proc.args), process {} may not exist. Error code: {}",
148                             getProcessID(), Native.getLastError());
149                 }
150             }
151         }
152         return Collections.emptyList();
153     }
154 
155     @Override
156     public Map<String, String> getEnvironmentVariables() {
157         return environmentVariables.get();
158     }
159 
160     private Map<String, String> queryEnvironmentVariables() {
161         if (ARGMAX > 0) {
162             // Get environment variables via sysctl(3)
163             int[] mib = new int[4];
164             mib[0] = 1; // CTL_KERN
165             mib[1] = 14; // KERN_PROC
166             mib[2] = 35; // KERN_PROC_ENV
167             mib[3] = getProcessID();
168             // Allocate memory for environment variables
169             try (Memory m = new Memory(ARGMAX);
170                     CloseableSizeTByReference size = new CloseableSizeTByReference(ARGMAX)) {
171                 // Fetch environment variables
172                 if (FreeBsdLibc.INSTANCE.sysctl(mib, mib.length, m, size, null, size_t.ZERO) == 0) {
173                     return Collections.unmodifiableMap(
174                             ParseUtil.parseByteArrayToStringMap(m.getByteArray(0, size.getValue().intValue())));
175                 } else {
176                     LOG.warn(
177                             "Failed sysctl call for process environment variables (kern.proc.env), process {} may not exist. Error code: {}",
178                             getProcessID(), Native.getLastError());
179                 }
180             }
181         }
182         return Collections.emptyMap();
183     }
184 
185     @Override
186     public String getCurrentWorkingDirectory() {
187         return ProcstatUtil.getCwd(getProcessID());
188     }
189 
190     @Override
191     public String getUser() {
192         return this.user;
193     }
194 
195     @Override
196     public String getUserID() {
197         return this.userID;
198     }
199 
200     @Override
201     public String getGroup() {
202         return this.group;
203     }
204 
205     @Override
206     public String getGroupID() {
207         return this.groupID;
208     }
209 
210     @Override
211     public State getState() {
212         return this.state;
213     }
214 
215     @Override
216     public int getParentProcessID() {
217         return this.parentProcessID;
218     }
219 
220     @Override
221     public int getThreadCount() {
222         return this.threadCount;
223     }
224 
225     @Override
226     public int getPriority() {
227         return this.priority;
228     }
229 
230     @Override
231     public long getVirtualSize() {
232         return this.virtualSize;
233     }
234 
235     @Override
236     public long getResidentSetSize() {
237         return this.residentSetSize;
238     }
239 
240     @Override
241     public long getKernelTime() {
242         return this.kernelTime;
243     }
244 
245     @Override
246     public long getUserTime() {
247         return this.userTime;
248     }
249 
250     @Override
251     public long getUpTime() {
252         return this.upTime;
253     }
254 
255     @Override
256     public long getStartTime() {
257         return this.startTime;
258     }
259 
260     @Override
261     public long getBytesRead() {
262         return this.bytesRead;
263     }
264 
265     @Override
266     public long getBytesWritten() {
267         return this.bytesWritten;
268     }
269 
270     @Override
271     public long getOpenFiles() {
272         return ProcstatUtil.getOpenFiles(getProcessID());
273     }
274 
275     @Override
276     public long getSoftOpenFileLimit() {
277         if (getProcessID() == this.os.getProcessId()) {
278             final Resource.Rlimit rlimit = new Resource.Rlimit();
279             FreeBsdLibc.INSTANCE.getrlimit(FreeBsdLibc.RLIMIT_NOFILE, rlimit);
280             return rlimit.rlim_cur;
281         } else {
282             return getProcessOpenFileLimit(getProcessID(), 1);
283         }
284     }
285 
286     @Override
287     public long getHardOpenFileLimit() {
288         if (getProcessID() == this.os.getProcessId()) {
289             final Resource.Rlimit rlimit = new Resource.Rlimit();
290             FreeBsdLibc.INSTANCE.getrlimit(FreeBsdLibc.RLIMIT_NOFILE, rlimit);
291             return rlimit.rlim_max;
292         } else {
293             return getProcessOpenFileLimit(getProcessID(), 2);
294         }
295     }
296 
297     @Override
298     public int getBitness() {
299         return this.bitness.get();
300     }
301 
302     @Override
303     public long getAffinityMask() {
304         long bitMask = 0L;
305         // Would prefer to use native cpuset_getaffinity call but variable sizing is
306         // kernel-dependent and requires C macros, so we use commandline instead.
307         String cpuset = ExecutingCommand.getFirstAnswer("cpuset -gp " + getProcessID());
308         // Sample output:
309         // pid 8 mask: 0, 1
310         // cpuset: getaffinity: No such process
311         String[] split = cpuset.split(":");
312         if (split.length > 1) {
313             String[] bits = split[1].split(",");
314             for (String bit : bits) {
315                 int bitToSet = ParseUtil.parseIntOrDefault(bit.trim(), -1);
316                 if (bitToSet >= 0) {
317                     bitMask |= 1L << bitToSet;
318                 }
319             }
320         }
321         return bitMask;
322     }
323 
324     private int queryBitness() {
325         // Get process abi vector
326         int[] mib = new int[4];
327         mib[0] = 1; // CTL_KERN
328         mib[1] = 14; // KERN_PROC
329         mib[2] = 9; // KERN_PROC_SV_NAME
330         mib[3] = getProcessID();
331         // Allocate memory for arguments
332         try (Memory abi = new Memory(32); CloseableSizeTByReference size = new CloseableSizeTByReference(32)) {
333             // Fetch abi vector
334             if (0 == FreeBsdLibc.INSTANCE.sysctl(mib, mib.length, abi, size, null, size_t.ZERO)) {
335                 String elf = abi.getString(0);
336                 if (elf.contains("ELF32")) {
337                     return 32;
338                 } else if (elf.contains("ELF64")) {
339                     return 64;
340                 }
341             }
342         }
343         return 0;
344     }
345 
346     @Override
347     public List<OSThread> getThreadDetails() {
348         String psCommand = "ps -awwxo " + PS_THREAD_COLUMNS + " -H";
349         if (getProcessID() >= 0) {
350             psCommand += " -p " + getProcessID();
351         }
352         Predicate<Map<PsThreadColumns, String>> hasColumnsPri = threadMap -> threadMap.containsKey(PsThreadColumns.PRI);
353         return ExecutingCommand.runNative(psCommand).stream().skip(1).parallel()
354                 .map(thread -> ParseUtil.stringToEnumMap(PsThreadColumns.class, thread.trim(), ' '))
355                 .filter(hasColumnsPri).map(threadMap -> new FreeBsdOSThread(getProcessID(), threadMap))
356                 .filter(VALID_THREAD).collect(Collectors.toList());
357     }
358 
359     @Override
360     public long getMinorFaults() {
361         return this.minorFaults;
362     }
363 
364     @Override
365     public long getMajorFaults() {
366         return this.majorFaults;
367     }
368 
369     @Override
370     public long getContextSwitches() {
371         return this.contextSwitches;
372     }
373 
374     @Override
375     public boolean updateAttributes() {
376         String psCommand = "ps -awwxo " + FreeBsdOperatingSystem.PS_COMMAND_ARGS + " -p " + getProcessID();
377         List<String> procList = ExecutingCommand.runNative(psCommand);
378         if (procList.size() > 1) {
379             // skip header row
380             Map<PsKeywords, String> psMap = ParseUtil.stringToEnumMap(PsKeywords.class, procList.get(1).trim(), ' ');
381             // Check if last (thus all) value populated
382             if (psMap.containsKey(PsKeywords.ARGS)) {
383                 return updateAttributes(psMap);
384             }
385         }
386         this.state = INVALID;
387         return false;
388     }
389 
390     private boolean updateAttributes(Map<PsKeywords, String> psMap) {
391         long now = System.currentTimeMillis();
392         switch (psMap.get(PsKeywords.STATE).charAt(0)) {
393         case 'R':
394             this.state = RUNNING;
395             break;
396         case 'I':
397         case 'S':
398             this.state = SLEEPING;
399             break;
400         case 'D':
401         case 'L':
402         case 'U':
403             this.state = WAITING;
404             break;
405         case 'Z':
406             this.state = ZOMBIE;
407             break;
408         case 'T':
409             this.state = STOPPED;
410             break;
411         default:
412             this.state = OTHER;
413             break;
414         }
415         this.parentProcessID = ParseUtil.parseIntOrDefault(psMap.get(PsKeywords.PPID), 0);
416         this.user = psMap.get(PsKeywords.USER);
417         this.userID = psMap.get(PsKeywords.UID);
418         this.group = psMap.get(PsKeywords.GROUP);
419         this.groupID = psMap.get(PsKeywords.GID);
420         this.threadCount = ParseUtil.parseIntOrDefault(psMap.get(PsKeywords.NLWP), 0);
421         this.priority = ParseUtil.parseIntOrDefault(psMap.get(PsKeywords.PRI), 0);
422         // These are in KB, multiply
423         this.virtualSize = ParseUtil.parseLongOrDefault(psMap.get(PsKeywords.VSZ), 0) * 1024;
424         this.residentSetSize = ParseUtil.parseLongOrDefault(psMap.get(PsKeywords.RSS), 0) * 1024;
425         // Avoid divide by zero for processes up less than a second
426         long elapsedTime = ParseUtil.parseDHMSOrDefault(psMap.get(PsKeywords.ETIMES), 0L);
427         this.upTime = elapsedTime < 1L ? 1L : elapsedTime;
428         this.startTime = now - this.upTime;
429         this.kernelTime = ParseUtil.parseDHMSOrDefault(psMap.get(PsKeywords.SYSTIME), 0L);
430         this.userTime = ParseUtil.parseDHMSOrDefault(psMap.get(PsKeywords.TIME), 0L) - this.kernelTime;
431         this.path = psMap.get(PsKeywords.COMM);
432         this.name = this.path.substring(this.path.lastIndexOf('/') + 1);
433         this.minorFaults = ParseUtil.parseLongOrDefault(psMap.get(PsKeywords.MAJFLT), 0L);
434         this.majorFaults = ParseUtil.parseLongOrDefault(psMap.get(PsKeywords.MINFLT), 0L);
435         long nonVoluntaryContextSwitches = ParseUtil.parseLongOrDefault(psMap.get(PsKeywords.NVCSW), 0L);
436         long voluntaryContextSwitches = ParseUtil.parseLongOrDefault(psMap.get(PsKeywords.NIVCSW), 0L);
437         this.contextSwitches = voluntaryContextSwitches + nonVoluntaryContextSwitches;
438         this.commandLineBackup = psMap.get(PsKeywords.ARGS);
439         return true;
440     }
441 
442     private long getProcessOpenFileLimit(long processId, int index) {
443         final String limitsPath = String.format(Locale.ROOT, "/proc/%d/limits", processId);
444         if (!Files.exists(Paths.get(limitsPath))) {
445             return -1; // not supported
446         }
447         final List<String> lines = FileUtil.readFile(limitsPath);
448         final Optional<String> maxOpenFilesLine = lines.stream().filter(line -> line.startsWith("Max open files"))
449                 .findFirst();
450         if (!maxOpenFilesLine.isPresent()) {
451             return -1;
452         }
453 
454         // Split all non-Digits away -> ["", "{soft-limit}, "{hard-limit}"]
455         final String[] split = maxOpenFilesLine.get().split("\\D+");
456         return ParseUtil.parseLongOrDefault(split[index], -1);
457     }
458 }