View Javadoc
1   /*
2    * Copyright 2020-2023 The OSHI Project Contributors
3    * SPDX-License-Identifier: MIT
4    */
5   package oshi.software.os.mac;
6   
7   import static oshi.software.os.OSProcess.State.INVALID;
8   import static oshi.software.os.OSProcess.State.NEW;
9   import static oshi.software.os.OSProcess.State.OTHER;
10  import static oshi.software.os.OSProcess.State.RUNNING;
11  import static oshi.software.os.OSProcess.State.SLEEPING;
12  import static oshi.software.os.OSProcess.State.STOPPED;
13  import static oshi.software.os.OSProcess.State.WAITING;
14  import static oshi.software.os.OSProcess.State.ZOMBIE;
15  import static oshi.util.Memoizer.memoize;
16  
17  import java.nio.charset.StandardCharsets;
18  import java.util.ArrayList;
19  import java.util.Collections;
20  import java.util.LinkedHashMap;
21  import java.util.List;
22  import java.util.Locale;
23  import java.util.Map;
24  import java.util.function.Supplier;
25  import java.util.stream.Collectors;
26  
27  import org.slf4j.Logger;
28  import org.slf4j.LoggerFactory;
29  
30  import com.sun.jna.Memory;
31  import com.sun.jna.Native;
32  import com.sun.jna.platform.mac.IOKit.IOIterator;
33  import com.sun.jna.platform.mac.IOKit.IORegistryEntry;
34  import com.sun.jna.platform.mac.IOKitUtil;
35  import com.sun.jna.platform.mac.SystemB;
36  import com.sun.jna.platform.mac.SystemB.Group;
37  import com.sun.jna.platform.mac.SystemB.Passwd;
38  import com.sun.jna.platform.unix.LibCAPI.size_t;
39  import com.sun.jna.platform.unix.Resource;
40  
41  import oshi.annotation.concurrent.ThreadSafe;
42  import oshi.driver.mac.ThreadInfo;
43  import oshi.jna.Struct.CloseableProcTaskAllInfo;
44  import oshi.jna.Struct.CloseableRUsageInfoV2;
45  import oshi.jna.Struct.CloseableVnodePathInfo;
46  import oshi.software.common.AbstractOSProcess;
47  import oshi.software.os.OSThread;
48  import oshi.util.GlobalConfig;
49  import oshi.util.ParseUtil;
50  import oshi.util.platform.mac.SysctlUtil;
51  import oshi.util.tuples.Pair;
52  
53  /**
54   * OSProcess implementation
55   */
56  @ThreadSafe
57  public class MacOSProcess extends AbstractOSProcess {
58  
59      private static final Logger LOG = LoggerFactory.getLogger(MacOSProcess.class);
60  
61      private static final int ARGMAX = SysctlUtil.sysctl("kern.argmax", 0);
62      private static final long TICKS_PER_MS;
63      static {
64          // default to 1 tick per nanosecond
65          long ticksPerSec = 1_000_000_000L;
66          IOIterator iter = IOKitUtil.getMatchingServices("IOPlatformDevice");
67          if (iter != null) {
68              IORegistryEntry cpu = iter.next();
69              while (cpu != null) {
70                  try {
71                      String s = cpu.getName().toLowerCase(Locale.ROOT);
72                      if (s.startsWith("cpu") && s.length() > 3) {
73                          byte[] data = cpu.getByteArrayProperty("timebase-frequency");
74                          if (data != null) {
75                              ticksPerSec = ParseUtil.byteArrayToLong(data, 4, false);
76                              break;
77                          }
78                      }
79                  } finally {
80                      cpu.release();
81                  }
82                  cpu = iter.next();
83              }
84              iter.release();
85          }
86          // Convert to ticks per millisecond
87          TICKS_PER_MS = ticksPerSec / 1000L;
88      }
89  
90      private static final boolean LOG_MAC_SYSCTL_WARNING = GlobalConfig.get(GlobalConfig.OSHI_OS_MAC_SYSCTL_LOGWARNING,
91              false);
92  
93      private static final int MAC_RLIMIT_NOFILE = 8;
94  
95      // 64-bit flag
96      private static final int P_LP64 = 0x4;
97      /*
98       * macOS States:
99       */
100     private static final int SSLEEP = 1; // sleeping on high priority
101     private static final int SWAIT = 2; // sleeping on low priority
102     private static final int SRUN = 3; // running
103     private static final int SIDL = 4; // intermediate state in process creation
104     private static final int SZOMB = 5; // intermediate state in process termination
105     private static final int SSTOP = 6; // process being traced
106 
107     private int majorVersion;
108     private int minorVersion;
109     private final MacOperatingSystem os;
110 
111     private Supplier<String> commandLine = memoize(this::queryCommandLine);
112     private Supplier<Pair<List<String>, Map<String, String>>> argsEnviron = memoize(this::queryArgsAndEnvironment);
113 
114     private String name = "";
115     private String path = "";
116     private String currentWorkingDirectory;
117     private String user;
118     private String userID;
119     private String group;
120     private String groupID;
121     private State state = INVALID;
122     private int parentProcessID;
123     private int threadCount;
124     private int priority;
125     private long virtualSize;
126     private long residentSetSize;
127     private long kernelTime;
128     private long userTime;
129     private long startTime;
130     private long upTime;
131     private long bytesRead;
132     private long bytesWritten;
133     private long openFiles;
134     private int bitness;
135     private long minorFaults;
136     private long majorFaults;
137     private long contextSwitches;
138 
139     public MacOSProcess(int pid, int major, int minor, MacOperatingSystem os) {
140         super(pid);
141         this.majorVersion = major;
142         this.minorVersion = minor;
143         this.os = os;
144         updateAttributes();
145     }
146 
147     @Override
148     public String getName() {
149         return this.name;
150     }
151 
152     @Override
153     public String getPath() {
154         return this.path;
155     }
156 
157     @Override
158     public String getCommandLine() {
159         return this.commandLine.get();
160     }
161 
162     private String queryCommandLine() {
163         return String.join(" ", getArguments());
164     }
165 
166     @Override
167     public List<String> getArguments() {
168         return argsEnviron.get().getA();
169     }
170 
171     @Override
172     public Map<String, String> getEnvironmentVariables() {
173         return argsEnviron.get().getB();
174     }
175 
176     private Pair<List<String>, Map<String, String>> queryArgsAndEnvironment() {
177         int pid = getProcessID();
178         // Set up return objects
179         List<String> args = new ArrayList<>();
180         // API does not specify any particular order of entries, but it is reasonable to
181         // maintain whatever order the OS provided to the end user
182         Map<String, String> env = new LinkedHashMap<>();
183 
184         // Get command line via sysctl
185         int[] mib = new int[3];
186         mib[0] = 1; // CTL_KERN
187         mib[1] = 49; // KERN_PROCARGS2
188         mib[2] = pid;
189         // Allocate memory for arguments
190         try (Memory procargs = new Memory(ARGMAX)) {
191             procargs.clear();
192             size_t.ByReference size = new size_t.ByReference(ARGMAX);
193             // Fetch arguments
194             if (0 == SystemB.INSTANCE.sysctl(mib, mib.length, procargs, size, null, size_t.ZERO)) {
195                 // Procargs contains an int representing total # of args, followed by a
196                 // null-terminated execpath string and then the arguments, each
197                 // null-terminated (possible multiple consecutive nulls),
198                 // The execpath string is also the first arg.
199                 // Following this is an int representing total # of env, followed by
200                 // null-terminated envs in similar format
201                 int nargs = procargs.getInt(0);
202                 // Sanity check
203                 if (nargs > 0 && nargs <= 1024) {
204                     // Skip first int (containing value of nargs)
205                     long offset = SystemB.INT_SIZE;
206                     // Skip exec_command, as
207                     offset += procargs.getString(offset).length();
208                     // Iterate character by character using offset
209                     // Build each arg and add to list
210                     while (offset < size.longValue()) {
211                         // Advance through additional nulls
212                         while (procargs.getByte(offset) == 0) {
213                             if (++offset >= size.longValue()) {
214                                 break;
215                             }
216                         }
217                         // Grab a string. This should go until the null terminator
218                         String arg = procargs.getString(offset);
219                         if (nargs-- > 0) {
220                             // If we havent found nargs yet, it's an arg
221                             args.add(arg);
222                         } else {
223                             // otherwise it's an env
224                             int idx = arg.indexOf('=');
225                             if (idx > 0) {
226                                 env.put(arg.substring(0, idx), arg.substring(idx + 1));
227                             }
228                         }
229                         // Advance offset to next null
230                         offset += arg.length();
231                     }
232                 }
233             } else {
234                 // Don't warn for pid 0
235                 if (pid > 0 && LOG_MAC_SYSCTL_WARNING) {
236                     LOG.warn(
237                             "Failed sysctl call for process arguments (kern.procargs2), process {} may not exist. Error code: {}",
238                             pid, Native.getLastError());
239                 }
240             }
241         }
242         return new Pair<>(Collections.unmodifiableList(args), Collections.unmodifiableMap(env));
243     }
244 
245     @Override
246     public String getCurrentWorkingDirectory() {
247         return this.currentWorkingDirectory;
248     }
249 
250     @Override
251     public String getUser() {
252         return this.user;
253     }
254 
255     @Override
256     public String getUserID() {
257         return this.userID;
258     }
259 
260     @Override
261     public String getGroup() {
262         return this.group;
263     }
264 
265     @Override
266     public String getGroupID() {
267         return this.groupID;
268     }
269 
270     @Override
271     public State getState() {
272         return this.state;
273     }
274 
275     @Override
276     public int getParentProcessID() {
277         return this.parentProcessID;
278     }
279 
280     @Override
281     public int getThreadCount() {
282         return this.threadCount;
283     }
284 
285     @Override
286     public List<OSThread> getThreadDetails() {
287         long now = System.currentTimeMillis();
288         return ThreadInfo.queryTaskThreads(getProcessID()).stream().parallel().map(stat -> {
289             // For long running threads the start time calculation can overestimate
290             long start = Math.max(now - stat.getUpTime(), getStartTime());
291             return new MacOSThread(getProcessID(), stat.getThreadId(), stat.getState(), stat.getSystemTime(),
292                     stat.getUserTime(), start, now - start, stat.getPriority());
293         }).collect(Collectors.toList());
294     }
295 
296     @Override
297     public int getPriority() {
298         return this.priority;
299     }
300 
301     @Override
302     public long getVirtualSize() {
303         return this.virtualSize;
304     }
305 
306     @Override
307     public long getResidentSetSize() {
308         return this.residentSetSize;
309     }
310 
311     @Override
312     public long getKernelTime() {
313         return this.kernelTime;
314     }
315 
316     @Override
317     public long getUserTime() {
318         return this.userTime;
319     }
320 
321     @Override
322     public long getUpTime() {
323         return this.upTime;
324     }
325 
326     @Override
327     public long getStartTime() {
328         return this.startTime;
329     }
330 
331     @Override
332     public long getBytesRead() {
333         return this.bytesRead;
334     }
335 
336     @Override
337     public long getBytesWritten() {
338         return this.bytesWritten;
339     }
340 
341     @Override
342     public long getOpenFiles() {
343         return this.openFiles;
344     }
345 
346     @Override
347     public long getSoftOpenFileLimit() {
348         if (getProcessID() == this.os.getProcessId()) {
349             final Resource.Rlimit rlimit = new Resource.Rlimit();
350             SystemB.INSTANCE.getrlimit(MAC_RLIMIT_NOFILE, rlimit);
351             return rlimit.rlim_cur;
352         } else {
353             return -1L; // not supported
354         }
355     }
356 
357     @Override
358     public long getHardOpenFileLimit() {
359         if (getProcessID() == this.os.getProcessId()) {
360             final Resource.Rlimit rlimit = new Resource.Rlimit();
361             SystemB.INSTANCE.getrlimit(MAC_RLIMIT_NOFILE, rlimit);
362             return rlimit.rlim_max;
363         } else {
364             return -1L; // not supported
365         }
366     }
367 
368     @Override
369     public int getBitness() {
370         return this.bitness;
371     }
372 
373     @Override
374     public long getAffinityMask() {
375         // macOS doesn't do affinity. Return a bitmask of the current processors.
376         int logicalProcessorCount = SysctlUtil.sysctl("hw.logicalcpu", 1);
377         return logicalProcessorCount < 64 ? (1L << logicalProcessorCount) - 1 : -1L;
378     }
379 
380     @Override
381     public long getMinorFaults() {
382         return this.minorFaults;
383     }
384 
385     @Override
386     public long getMajorFaults() {
387         return this.majorFaults;
388     }
389 
390     @Override
391     public long getContextSwitches() {
392         return this.contextSwitches;
393     }
394 
395     @Override
396     public boolean updateAttributes() {
397         long now = System.currentTimeMillis();
398         try (CloseableProcTaskAllInfo taskAllInfo = new CloseableProcTaskAllInfo()) {
399             if (0 > SystemB.INSTANCE.proc_pidinfo(getProcessID(), SystemB.PROC_PIDTASKALLINFO, 0, taskAllInfo,
400                     taskAllInfo.size()) || taskAllInfo.ptinfo.pti_threadnum < 1) {
401                 this.state = INVALID;
402                 return false;
403             }
404             try (Memory buf = new Memory(SystemB.PROC_PIDPATHINFO_MAXSIZE)) {
405                 if (0 < SystemB.INSTANCE.proc_pidpath(getProcessID(), buf, SystemB.PROC_PIDPATHINFO_MAXSIZE)) {
406                     this.path = buf.getString(0).trim();
407                     // Overwrite name with last part of path
408                     String[] pathSplit = this.path.split("/");
409                     if (pathSplit.length > 0) {
410                         this.name = pathSplit[pathSplit.length - 1];
411                     }
412                 }
413             }
414             if (this.name.isEmpty()) {
415                 // pbi_comm contains first 16 characters of name
416                 this.name = Native.toString(taskAllInfo.pbsd.pbi_comm, StandardCharsets.UTF_8);
417             }
418 
419             switch (taskAllInfo.pbsd.pbi_status) {
420             case SSLEEP:
421                 this.state = SLEEPING;
422                 break;
423             case SWAIT:
424                 this.state = WAITING;
425                 break;
426             case SRUN:
427                 this.state = RUNNING;
428                 break;
429             case SIDL:
430                 this.state = NEW;
431                 break;
432             case SZOMB:
433                 this.state = ZOMBIE;
434                 break;
435             case SSTOP:
436                 this.state = STOPPED;
437                 break;
438             default:
439                 this.state = OTHER;
440                 break;
441             }
442             this.parentProcessID = taskAllInfo.pbsd.pbi_ppid;
443             this.userID = Integer.toString(taskAllInfo.pbsd.pbi_uid);
444             Passwd pwuid = SystemB.INSTANCE.getpwuid(taskAllInfo.pbsd.pbi_uid);
445             this.user = pwuid == null ? Integer.toString(taskAllInfo.pbsd.pbi_uid) : pwuid.pw_name;
446             this.groupID = Integer.toString(taskAllInfo.pbsd.pbi_gid);
447             Group grgid = SystemB.INSTANCE.getgrgid(taskAllInfo.pbsd.pbi_gid);
448             this.group = grgid == null ? Integer.toString(taskAllInfo.pbsd.pbi_gid) : grgid.gr_name;
449             this.threadCount = taskAllInfo.ptinfo.pti_threadnum;
450             this.priority = taskAllInfo.ptinfo.pti_priority;
451             this.virtualSize = taskAllInfo.ptinfo.pti_virtual_size;
452             this.residentSetSize = taskAllInfo.ptinfo.pti_resident_size;
453             this.kernelTime = taskAllInfo.ptinfo.pti_total_system / TICKS_PER_MS;
454             this.userTime = taskAllInfo.ptinfo.pti_total_user / TICKS_PER_MS;
455             this.startTime = taskAllInfo.pbsd.pbi_start_tvsec * 1000L + taskAllInfo.pbsd.pbi_start_tvusec / 1000L;
456             this.upTime = now - this.startTime;
457             this.openFiles = taskAllInfo.pbsd.pbi_nfiles;
458             this.bitness = (taskAllInfo.pbsd.pbi_flags & P_LP64) == 0 ? 32 : 64;
459             this.majorFaults = taskAllInfo.ptinfo.pti_pageins;
460             // testing using getrusage confirms pti_faults includes both major and minor
461             this.minorFaults = taskAllInfo.ptinfo.pti_faults - taskAllInfo.ptinfo.pti_pageins; // NOSONAR squid:S2184
462             this.contextSwitches = taskAllInfo.ptinfo.pti_csw;
463         }
464         if (this.majorVersion > 10 || this.minorVersion >= 9) {
465             try (CloseableRUsageInfoV2 rUsageInfoV2 = new CloseableRUsageInfoV2()) {
466                 if (0 == SystemB.INSTANCE.proc_pid_rusage(getProcessID(), SystemB.RUSAGE_INFO_V2, rUsageInfoV2)) {
467                     this.bytesRead = rUsageInfoV2.ri_diskio_bytesread;
468                     this.bytesWritten = rUsageInfoV2.ri_diskio_byteswritten;
469                 }
470             }
471         }
472         try (CloseableVnodePathInfo vpi = new CloseableVnodePathInfo()) {
473             if (0 < SystemB.INSTANCE.proc_pidinfo(getProcessID(), SystemB.PROC_PIDVNODEPATHINFO, 0, vpi, vpi.size())) {
474                 this.currentWorkingDirectory = Native.toString(vpi.pvi_cdir.vip_path, StandardCharsets.US_ASCII);
475             }
476         }
477         return true;
478     }
479 }