View Javadoc
1   /*
2    * Copyright 2016-2023 The OSHI Project Contributors
3    * SPDX-License-Identifier: MIT
4    */
5   package oshi.software.os.mac;
6   
7   import java.io.File;
8   import java.nio.charset.StandardCharsets;
9   import java.nio.file.PathMatcher;
10  import java.util.ArrayList;
11  import java.util.HashMap;
12  import java.util.List;
13  import java.util.Locale;
14  import java.util.Map;
15  import java.util.regex.Pattern;
16  import java.util.stream.Collectors;
17  
18  import org.slf4j.Logger;
19  import org.slf4j.LoggerFactory;
20  
21  import com.sun.jna.Native;
22  import com.sun.jna.Pointer;
23  import com.sun.jna.platform.mac.CoreFoundation;
24  import com.sun.jna.platform.mac.CoreFoundation.CFDictionaryRef;
25  import com.sun.jna.platform.mac.CoreFoundation.CFMutableDictionaryRef;
26  import com.sun.jna.platform.mac.CoreFoundation.CFStringRef;
27  import com.sun.jna.platform.mac.DiskArbitration;
28  import com.sun.jna.platform.mac.DiskArbitration.DADiskRef;
29  import com.sun.jna.platform.mac.DiskArbitration.DASessionRef;
30  import com.sun.jna.platform.mac.IOKit.IOIterator;
31  import com.sun.jna.platform.mac.IOKit.IORegistryEntry;
32  import com.sun.jna.platform.mac.IOKitUtil;
33  import com.sun.jna.platform.mac.SystemB;
34  import com.sun.jna.platform.mac.SystemB.Statfs;
35  
36  import oshi.annotation.concurrent.ThreadSafe;
37  import oshi.software.common.AbstractFileSystem;
38  import oshi.software.os.OSFileStore;
39  import oshi.util.FileSystemUtil;
40  import oshi.util.platform.mac.CFUtil;
41  import oshi.util.platform.mac.SysctlUtil;
42  
43  /**
44   * The Mac File System contains {@link oshi.software.os.OSFileStore}s which are a storage pool, device, partition,
45   * volume, concrete file system or other implementation specific means of file storage. In macOS, these are found in the
46   * /Volumes directory.
47   */
48  @ThreadSafe
49  public class MacFileSystem extends AbstractFileSystem {
50  
51      private static final Logger LOG = LoggerFactory.getLogger(MacFileSystem.class);
52  
53      public static final String OSHI_MAC_FS_PATH_EXCLUDES = "oshi.os.mac.filesystem.path.excludes";
54      public static final String OSHI_MAC_FS_PATH_INCLUDES = "oshi.os.mac.filesystem.path.includes";
55      public static final String OSHI_MAC_FS_VOLUME_EXCLUDES = "oshi.os.mac.filesystem.volume.excludes";
56      public static final String OSHI_MAC_FS_VOLUME_INCLUDES = "oshi.os.mac.filesystem.volume.includes";
57  
58      private static final List<PathMatcher> FS_PATH_EXCLUDES = FileSystemUtil
59              .loadAndParseFileSystemConfig(OSHI_MAC_FS_PATH_EXCLUDES);
60      private static final List<PathMatcher> FS_PATH_INCLUDES = FileSystemUtil
61              .loadAndParseFileSystemConfig(OSHI_MAC_FS_PATH_INCLUDES);
62      private static final List<PathMatcher> FS_VOLUME_EXCLUDES = FileSystemUtil
63              .loadAndParseFileSystemConfig(OSHI_MAC_FS_VOLUME_EXCLUDES);
64      private static final List<PathMatcher> FS_VOLUME_INCLUDES = FileSystemUtil
65              .loadAndParseFileSystemConfig(OSHI_MAC_FS_VOLUME_INCLUDES);
66  
67      // Regexp matcher for /dev/disk1 etc.
68      private static final Pattern LOCAL_DISK = Pattern.compile("/dev/disk\\d");
69  
70      // User specifiable flags.
71      private static final int MNT_RDONLY = 0x00000001;
72      private static final int MNT_SYNCHRONOUS = 0x00000002;
73      private static final int MNT_NOEXEC = 0x00000004;
74      private static final int MNT_NOSUID = 0x00000008;
75      private static final int MNT_NODEV = 0x00000010;
76      private static final int MNT_UNION = 0x00000020;
77      private static final int MNT_ASYNC = 0x00000040;
78      private static final int MNT_CPROTECT = 0x00000080;
79      private static final int MNT_EXPORTED = 0x00000100;
80      private static final int MNT_QUARANTINE = 0x00000400;
81      private static final int MNT_LOCAL = 0x00001000;
82      private static final int MNT_QUOTA = 0x00002000;
83      private static final int MNT_ROOTFS = 0x00004000;
84      private static final int MNT_DOVOLFS = 0x00008000;
85      private static final int MNT_DONTBROWSE = 0x00100000;
86      private static final int MNT_IGNORE_OWNERSHIP = 0x00200000;
87      private static final int MNT_AUTOMOUNTED = 0x00400000;
88      private static final int MNT_JOURNALED = 0x00800000;
89      private static final int MNT_NOUSERXATTR = 0x01000000;
90      private static final int MNT_DEFWRITE = 0x02000000;
91      private static final int MNT_MULTILABEL = 0x04000000;
92      private static final int MNT_NOATIME = 0x10000000;
93  
94      private static final Map<Integer, String> OPTIONS_MAP = new HashMap<>();
95      static {
96          OPTIONS_MAP.put(MNT_SYNCHRONOUS, "synchronous");
97          OPTIONS_MAP.put(MNT_NOEXEC, "noexec");
98          OPTIONS_MAP.put(MNT_NOSUID, "nosuid");
99          OPTIONS_MAP.put(MNT_NODEV, "nodev");
100         OPTIONS_MAP.put(MNT_UNION, "union");
101         OPTIONS_MAP.put(MNT_ASYNC, "asynchronous");
102         OPTIONS_MAP.put(MNT_CPROTECT, "content-protection");
103         OPTIONS_MAP.put(MNT_EXPORTED, "exported");
104         OPTIONS_MAP.put(MNT_QUARANTINE, "quarantined");
105         OPTIONS_MAP.put(MNT_LOCAL, "local");
106         OPTIONS_MAP.put(MNT_QUOTA, "quotas");
107         OPTIONS_MAP.put(MNT_ROOTFS, "rootfs");
108         OPTIONS_MAP.put(MNT_DOVOLFS, "volfs");
109         OPTIONS_MAP.put(MNT_DONTBROWSE, "nobrowse");
110         OPTIONS_MAP.put(MNT_IGNORE_OWNERSHIP, "noowners");
111         OPTIONS_MAP.put(MNT_AUTOMOUNTED, "automounted");
112         OPTIONS_MAP.put(MNT_JOURNALED, "journaled");
113         OPTIONS_MAP.put(MNT_NOUSERXATTR, "nouserxattr");
114         OPTIONS_MAP.put(MNT_DEFWRITE, "defwrite");
115         OPTIONS_MAP.put(MNT_MULTILABEL, "multilabel");
116         OPTIONS_MAP.put(MNT_NOATIME, "noatime");
117     }
118 
119     @Override
120     public List<OSFileStore> getFileStores(boolean localOnly) {
121         // List of file systems
122         return getFileStoreMatching(null, localOnly);
123     }
124 
125     // Called by MacOSFileStore
126     static List<OSFileStore> getFileStoreMatching(String nameToMatch) {
127         return getFileStoreMatching(nameToMatch, false);
128     }
129 
130     private static List<OSFileStore> getFileStoreMatching(String nameToMatch, boolean localOnly) {
131         List<OSFileStore> fsList = new ArrayList<>();
132 
133         // Use getfsstat to find fileSystems
134         // Query with null to get total # required
135         int numfs = SystemB.INSTANCE.getfsstat64(null, 0, 0);
136         if (numfs > 0) {
137             // Open a DiskArbitration session to get VolumeName of file systems
138             // with bsd names
139             DASessionRef session = DiskArbitration.INSTANCE
140                     .DASessionCreate(CoreFoundation.INSTANCE.CFAllocatorGetDefault());
141             if (session == null) {
142                 LOG.error("Unable to open session to DiskArbitration framework.");
143             } else {
144                 CFStringRef daVolumeNameKey = CFStringRef.createCFString("DAVolumeName");
145 
146                 // Create array to hold results
147                 Statfs s = new Statfs();
148                 Statfs[] fs = (Statfs[]) s.toArray(numfs);
149                 // Fill array with results
150                 numfs = SystemB.INSTANCE.getfsstat64(fs, fs[0].size() * fs.length, SystemB.MNT_NOWAIT);
151                 for (int f = 0; f < numfs; f++) {
152                     // Mount on name will match mounted path, e.g. /Volumes/foo
153                     // Mount to name will match canonical path., e.g., /dev/disk0s2
154                     // Byte arrays are null-terminated strings
155 
156                     // Get volume and path name, and type
157                     String volume = Native.toString(fs[f].f_mntfromname, StandardCharsets.UTF_8);
158                     String path = Native.toString(fs[f].f_mntonname, StandardCharsets.UTF_8);
159                     String type = Native.toString(fs[f].f_fstypename, StandardCharsets.UTF_8);
160                     // Skip non-local drives if requested, skip system types
161                     final int flags = fs[f].f_flags;
162 
163                     // Skip non-local drives if requested, and exclude pseudo file systems
164                     if ((localOnly && (flags & MNT_LOCAL) == 0) || !path.equals("/")
165                             && (PSEUDO_FS_TYPES.contains(type) || FileSystemUtil.isFileStoreExcluded(path, volume,
166                                     FS_PATH_INCLUDES, FS_PATH_EXCLUDES, FS_VOLUME_INCLUDES, FS_VOLUME_EXCLUDES))) {
167                         continue;
168                     }
169 
170                     String description = "Volume";
171                     if (LOCAL_DISK.matcher(volume).matches()) {
172                         description = "Local Disk";
173                     } else if (volume.startsWith("localhost:") || volume.startsWith("//") || volume.startsWith("smb://")
174                             || NETWORK_FS_TYPES.contains(type)) {
175                         description = "Network Drive";
176                     }
177                     File file = new File(path);
178                     String name = file.getName();
179                     // getName() for / is still blank, so:
180                     if (name.isEmpty()) {
181                         name = file.getPath();
182                     }
183                     if (nameToMatch != null && !nameToMatch.equals(name)) {
184                         continue;
185                     }
186 
187                     StringBuilder options = new StringBuilder((MNT_RDONLY & flags) == 0 ? "rw" : "ro");
188                     String moreOptions = OPTIONS_MAP.entrySet().stream().filter(e -> (e.getKey() & flags) > 0)
189                             .map(Map.Entry::getValue).collect(Collectors.joining(","));
190                     if (!moreOptions.isEmpty()) {
191                         options.append(',').append(moreOptions);
192                     }
193 
194                     String uuid = "";
195                     // Use volume to find DiskArbitration volume name and search for
196                     // the registry entry for UUID
197                     String bsdName = volume.replace("/dev/disk", "disk");
198                     if (bsdName.startsWith("disk")) {
199                         // Get the DiskArbitration dictionary for this disk,
200                         // which has volumename
201                         DADiskRef disk = DiskArbitration.INSTANCE.DADiskCreateFromBSDName(
202                                 CoreFoundation.INSTANCE.CFAllocatorGetDefault(), session, volume);
203                         if (disk != null) {
204                             CFDictionaryRef diskInfo = DiskArbitration.INSTANCE.DADiskCopyDescription(disk);
205                             if (diskInfo != null) {
206                                 // get volume name from its key
207                                 Pointer result = diskInfo.getValue(daVolumeNameKey);
208                                 name = CFUtil.cfPointerToString(result);
209                                 diskInfo.release();
210                             }
211                             disk.release();
212                         }
213                         // Search for bsd name in IOKit registry for UUID
214                         CFMutableDictionaryRef matchingDict = IOKitUtil.getBSDNameMatchingDict(bsdName);
215                         if (matchingDict != null) {
216                             // search for all IOservices that match the bsd name
217                             IOIterator fsIter = IOKitUtil.getMatchingServices(matchingDict);
218                             if (fsIter != null) {
219                                 // getMatchingServices releases matchingDict
220                                 // Should only match one logical drive
221                                 IORegistryEntry fsEntry = fsIter.next();
222                                 if (fsEntry != null && fsEntry.conformsTo("IOMedia")) {
223                                     // Now get the UUID
224                                     uuid = fsEntry.getStringProperty("UUID");
225                                     if (uuid != null) {
226                                         uuid = uuid.toLowerCase(Locale.ROOT);
227                                     }
228                                     fsEntry.release();
229                                 }
230                                 fsIter.release();
231                             }
232                         }
233                     }
234 
235                     fsList.add(new MacOSFileStore(name, volume, name, path, options.toString(),
236                             uuid == null ? "" : uuid, "", description, type, file.getFreeSpace(), file.getUsableSpace(),
237                             file.getTotalSpace(), fs[f].f_ffree, fs[f].f_files));
238                 }
239                 daVolumeNameKey.release();
240                 // Close DA session
241                 session.release();
242             }
243         }
244         return fsList;
245     }
246 
247     @Override
248     public long getOpenFileDescriptors() {
249         return SysctlUtil.sysctl("kern.num_files", 0);
250     }
251 
252     @Override
253     public long getMaxFileDescriptors() {
254         return SysctlUtil.sysctl("kern.maxfiles", 0);
255     }
256 
257     @Override
258     public long getMaxFileDescriptorsPerProcess() {
259         return SysctlUtil.sysctl("kern.maxfilesperproc", 0);
260     }
261 }