View Javadoc
1   /*
2    * Copyright 2016-2024 The OSHI Project Contributors
3    * SPDX-License-Identifier: MIT
4    */
5   package oshi.software.os.linux;
6   
7   import java.io.File;
8   import java.io.IOException;
9   import java.nio.file.Files;
10  import java.nio.file.Path;
11  import java.nio.file.PathMatcher;
12  import java.nio.file.Paths;
13  import java.util.ArrayList;
14  import java.util.HashMap;
15  import java.util.List;
16  import java.util.Locale;
17  import java.util.Map;
18  
19  import org.slf4j.Logger;
20  import org.slf4j.LoggerFactory;
21  
22  import com.sun.jna.Native;
23  import com.sun.jna.platform.linux.LibC;
24  
25  import oshi.annotation.concurrent.ThreadSafe;
26  import oshi.software.common.AbstractFileSystem;
27  import oshi.software.os.OSFileStore;
28  import oshi.util.ExecutingCommand;
29  import oshi.util.FileSystemUtil;
30  import oshi.util.FileUtil;
31  import oshi.util.ParseUtil;
32  import oshi.util.platform.linux.DevPath;
33  import oshi.util.platform.linux.ProcPath;
34  
35  /**
36   * The Linux File System contains {@link oshi.software.os.OSFileStore}s which are a storage pool, device, partition,
37   * volume, concrete file system or other implementation specific means of file storage. In Linux, these are found in the
38   * /proc/mount filesystem, excluding temporary and kernel mounts.
39   */
40  @ThreadSafe
41  public class LinuxFileSystem extends AbstractFileSystem {
42  
43      private static final Logger LOG = LoggerFactory.getLogger(LinuxFileSystem.class);
44  
45      public static final String OSHI_LINUX_FS_PATH_EXCLUDES = "oshi.os.linux.filesystem.path.excludes";
46      public static final String OSHI_LINUX_FS_PATH_INCLUDES = "oshi.os.linux.filesystem.path.includes";
47      public static final String OSHI_LINUX_FS_VOLUME_EXCLUDES = "oshi.os.linux.filesystem.volume.excludes";
48      public static final String OSHI_LINUX_FS_VOLUME_INCLUDES = "oshi.os.linux.filesystem.volume.includes";
49  
50      private static final List<PathMatcher> FS_PATH_EXCLUDES = FileSystemUtil
51              .loadAndParseFileSystemConfig(OSHI_LINUX_FS_PATH_EXCLUDES);
52      private static final List<PathMatcher> FS_PATH_INCLUDES = FileSystemUtil
53              .loadAndParseFileSystemConfig(OSHI_LINUX_FS_PATH_INCLUDES);
54      private static final List<PathMatcher> FS_VOLUME_EXCLUDES = FileSystemUtil
55              .loadAndParseFileSystemConfig(OSHI_LINUX_FS_VOLUME_EXCLUDES);
56      private static final List<PathMatcher> FS_VOLUME_INCLUDES = FileSystemUtil
57              .loadAndParseFileSystemConfig(OSHI_LINUX_FS_VOLUME_INCLUDES);
58  
59      private static final String UNICODE_SPACE = "\\040";
60  
61      @Override
62      public List<OSFileStore> getFileStores(boolean localOnly) {
63          // Map of volume with device path as key
64          Map<String, String> volumeDeviceMap = new HashMap<>();
65          File devMapper = new File(DevPath.MAPPER);
66          File[] volumes = devMapper.listFiles();
67          if (volumes != null) {
68              for (File volume : volumes) {
69                  try {
70                      volumeDeviceMap.put(volume.getCanonicalPath(), volume.getAbsolutePath());
71                  } catch (IOException e) {
72                      LOG.error("Couldn't get canonical path for {}. {}", volume.getName(), e.getMessage());
73                  }
74              }
75          }
76          // Map uuids with device path as key
77          Map<String, String> uuidMap = new HashMap<>();
78          File uuidDir = new File(DevPath.DISK_BY_UUID);
79          File[] uuids = uuidDir.listFiles();
80          if (uuids != null) {
81              for (File uuid : uuids) {
82                  try {
83                      // Store UUID as value with path (e.g., /dev/sda1) and volumes as key
84                      String canonicalPath = uuid.getCanonicalPath();
85                      uuidMap.put(canonicalPath, uuid.getName().toLowerCase(Locale.ROOT));
86                      if (volumeDeviceMap.containsKey(canonicalPath)) {
87                          uuidMap.put(volumeDeviceMap.get(canonicalPath), uuid.getName().toLowerCase(Locale.ROOT));
88                      }
89                  } catch (IOException e) {
90                      LOG.error("Couldn't get canonical path for {}. {}", uuid.getName(), e.getMessage());
91                  }
92              }
93          }
94  
95          // List file systems
96          return getFileStoreMatching(null, uuidMap, localOnly);
97      }
98  
99      // called from LinuxOSFileStore
100     static List<OSFileStore> getFileStoreMatching(String nameToMatch, Map<String, String> uuidMap) {
101         return getFileStoreMatching(nameToMatch, uuidMap, false);
102     }
103 
104     private static List<OSFileStore> getFileStoreMatching(String nameToMatch, Map<String, String> uuidMap,
105             boolean localOnly) {
106         List<OSFileStore> fsList = new ArrayList<>();
107 
108         Map<String, String> labelMap = queryLabelMap();
109 
110         // Parse /proc/mounts to get fs types
111         List<String> mounts = FileUtil.readFile(ProcPath.MOUNTS);
112         for (String mount : mounts) {
113             String[] split = mount.split(" ");
114             // As reported in fstab(5) manpage, struct is:
115             // 1st field is volume name
116             // 2nd field is path with spaces escaped as \040
117             // 3rd field is fs type
118             // 4th field is mount options
119             // 5th field is used by dump(8) (ignored)
120             // 6th field is fsck order (ignored)
121             if (split.length < 6) {
122                 continue;
123             }
124 
125             // Exclude pseudo file systems
126             String volume = split[0].replace(UNICODE_SPACE, " ");
127             String name = volume;
128             String path = split[1].replace(UNICODE_SPACE, " ");
129             if (path.equals("/")) {
130                 name = "/";
131             }
132             String type = split[2];
133 
134             // Skip non-local drives if requested, and exclude pseudo file systems
135             if ((localOnly && NETWORK_FS_TYPES.contains(type))
136                     || !path.equals("/") && (PSEUDO_FS_TYPES.contains(type) || FileSystemUtil.isFileStoreExcluded(path,
137                             volume, FS_PATH_INCLUDES, FS_PATH_EXCLUDES, FS_VOLUME_INCLUDES, FS_VOLUME_EXCLUDES))) {
138                 continue;
139             }
140 
141             String options = split[3];
142 
143             // If only updating for one name, skip others
144             if (nameToMatch != null && !nameToMatch.equals(name)) {
145                 continue;
146             }
147 
148             String uuid = uuidMap != null ? uuidMap.getOrDefault(split[0], "") : "";
149 
150             String description;
151             if (volume.startsWith(DevPath.DEV)) {
152                 description = "Local Disk";
153             } else if (volume.equals("tmpfs")) {
154                 description = "Ram Disk";
155             } else if (NETWORK_FS_TYPES.contains(type)) {
156                 description = "Network Disk";
157             } else {
158                 description = "Mount Point";
159             }
160 
161             // Add in logical volume found at /dev/mapper, useful when linking
162             // file system with drive.
163             String logicalVolume = "";
164             Path link = Paths.get(volume);
165             if (link.toFile().exists() && Files.isSymbolicLink(link)) {
166                 try {
167                     Path slink = Files.readSymbolicLink(link);
168                     Path full = Paths.get(DevPath.MAPPER + slink.toString());
169                     if (full.toFile().exists()) {
170                         logicalVolume = full.normalize().toString();
171                     }
172                 } catch (IOException e) {
173                     LOG.warn("Couldn't access symbolic path  {}. {}", link, e.getMessage());
174                 }
175             }
176 
177             long totalInodes = 0L;
178             long freeInodes = 0L;
179             long totalSpace = 0L;
180             long usableSpace = 0L;
181             long freeSpace = 0L;
182 
183             try {
184                 LibC.Statvfs vfsStat = new LibC.Statvfs();
185                 if (0 == LibC.INSTANCE.statvfs(path, vfsStat)) {
186                     totalInodes = vfsStat.f_files.longValue();
187                     freeInodes = vfsStat.f_ffree.longValue();
188                     // Per stavfs, these units are in fragments
189                     totalSpace = vfsStat.f_blocks.longValue() * vfsStat.f_frsize.longValue();
190                     usableSpace = vfsStat.f_bavail.longValue() * vfsStat.f_frsize.longValue();
191                     freeSpace = vfsStat.f_bfree.longValue() * vfsStat.f_frsize.longValue();
192                 } else {
193                     LOG.warn("Failed to get information to use statvfs. path: {}, Error code: {}", path,
194                             Native.getLastError());
195                 }
196             } catch (UnsatisfiedLinkError | NoClassDefFoundError e) {
197                 LOG.error("Failed to get file counts from statvfs. {}", e.getMessage());
198             }
199             // If native methods failed use JVM methods
200             if (totalSpace == 0L) {
201                 File tmpFile = new File(path);
202                 totalSpace = tmpFile.getTotalSpace();
203                 usableSpace = tmpFile.getUsableSpace();
204                 freeSpace = tmpFile.getFreeSpace();
205             }
206 
207             fsList.add(new LinuxOSFileStore(name, volume, labelMap.getOrDefault(path, name), path, options, uuid,
208                     logicalVolume, description, type, freeSpace, usableSpace, totalSpace, freeInodes, totalInodes));
209         }
210         return fsList;
211     }
212 
213     private static Map<String, String> queryLabelMap() {
214         Map<String, String> labelMap = new HashMap<>();
215         for (String line : ExecutingCommand.runNative("lsblk -o mountpoint,label")) {
216             String[] split = ParseUtil.whitespaces.split(line, 2);
217             if (split.length == 2) {
218                 labelMap.put(split[0], split[1]);
219             }
220         }
221         return labelMap;
222     }
223 
224     @Override
225     public long getOpenFileDescriptors() {
226         return getFileDescriptors(0);
227     }
228 
229     @Override
230     public long getMaxFileDescriptors() {
231         return getFileDescriptors(2);
232     }
233 
234     @Override
235     public long getMaxFileDescriptorsPerProcess() {
236         return getFileDescriptorsPerProcess();
237     }
238 
239     /**
240      * Returns a value from the Linux system file /proc/sys/fs/file-nr.
241      *
242      * @param index The index of the value to retrieve. 0 returns the total allocated file descriptors. 1 returns the
243      *              number of used file descriptors for kernel 2.4, or the number of unused file descriptors for kernel
244      *              2.6. 2 returns the maximum number of file descriptors that can be allocated.
245      * @return Corresponding file descriptor value from the Linux system file.
246      */
247     private static long getFileDescriptors(int index) {
248         String filename = ProcPath.SYS_FS_FILE_NR;
249         if (index < 0 || index > 2) {
250             throw new IllegalArgumentException("Index must be between 0 and 2.");
251         }
252         List<String> osDescriptors = FileUtil.readFile(filename);
253         if (!osDescriptors.isEmpty()) {
254             String[] splittedLine = osDescriptors.get(0).split("\\D+");
255             return ParseUtil.parseLongOrDefault(splittedLine[index], 0L);
256         }
257         return 0L;
258     }
259 
260     private static long getFileDescriptorsPerProcess() {
261         return FileUtil.getLongFromFile(ProcPath.SYS_FS_FILE_MAX);
262     }
263 }