View Javadoc
1   /*
2    * Copyright 2020-2024 The OSHI Project Contributors
3    * SPDX-License-Identifier: MIT
4    */
5   package oshi.hardware.platform.linux;
6   
7   import static oshi.software.os.linux.LinuxOperatingSystem.HAS_UDEV;
8   
9   import java.io.File;
10  import java.util.ArrayList;
11  import java.util.Arrays;
12  import java.util.Collections;
13  import java.util.Comparator;
14  import java.util.HashMap;
15  import java.util.List;
16  import java.util.Map;
17  import java.util.stream.Collectors;
18  
19  import org.slf4j.Logger;
20  import org.slf4j.LoggerFactory;
21  
22  import com.sun.jna.platform.linux.Udev;
23  import com.sun.jna.platform.linux.Udev.UdevContext;
24  import com.sun.jna.platform.linux.Udev.UdevDevice;
25  import com.sun.jna.platform.linux.Udev.UdevEnumerate;
26  import com.sun.jna.platform.linux.Udev.UdevListEntry;
27  
28  import oshi.annotation.concurrent.ThreadSafe;
29  import oshi.hardware.HWDiskStore;
30  import oshi.hardware.HWPartition;
31  import oshi.hardware.common.AbstractHWDiskStore;
32  import oshi.util.Constants;
33  import oshi.util.FileUtil;
34  import oshi.util.ParseUtil;
35  import oshi.util.platform.linux.DevPath;
36  import oshi.util.platform.linux.ProcPath;
37  
38  /**
39   * Linux hard disk implementation.
40   */
41  @ThreadSafe
42  public final class LinuxHWDiskStore extends AbstractHWDiskStore {
43  
44      private static final Logger LOG = LoggerFactory.getLogger(LinuxHWDiskStore.class);
45  
46      private static final String BLOCK = "block";
47      private static final String DISK = "disk";
48      private static final String PARTITION = "partition";
49  
50      private static final String STAT = "stat";
51      private static final String SIZE = "size";
52      private static final String MINOR = "MINOR";
53      private static final String MAJOR = "MAJOR";
54  
55      private static final String ID_FS_TYPE = "ID_FS_TYPE";
56      private static final String ID_FS_UUID = "ID_FS_UUID";
57      private static final String ID_MODEL = "ID_MODEL";
58      private static final String ID_SERIAL_SHORT = "ID_SERIAL_SHORT";
59  
60      private static final String DM_UUID = "DM_UUID";
61      private static final String DM_VG_NAME = "DM_VG_NAME";
62      private static final String DM_LV_NAME = "DM_LV_NAME";
63      private static final String LOGICAL_VOLUME_GROUP = "Logical Volume Group";
64  
65      private static final int SECTORSIZE = 512;
66  
67      // Get a list of orders to pass to ParseUtil
68      private static final int[] UDEV_STAT_ORDERS = new int[UdevStat.values().length];
69      static {
70          for (UdevStat stat : UdevStat.values()) {
71              UDEV_STAT_ORDERS[stat.ordinal()] = stat.getOrder();
72          }
73      }
74  
75      // There are at least 11 elements in udev stat output or sometimes 15. We want
76      // the rightmost 11 or 15 if there is leading text.
77      private static final int UDEV_STAT_LENGTH;
78      static {
79          String stat = FileUtil.getStringFromFile(ProcPath.DISKSTATS);
80          int statLength = 11;
81          if (!stat.isEmpty()) {
82              statLength = ParseUtil.countStringToLongArray(stat, ' ');
83          }
84          UDEV_STAT_LENGTH = statLength;
85      }
86  
87      private long reads = 0L;
88      private long readBytes = 0L;
89      private long writes = 0L;
90      private long writeBytes = 0L;
91      private long currentQueueLength = 0L;
92      private long transferTime = 0L;
93      private long timeStamp = 0L;
94      private List<HWPartition> partitionList = new ArrayList<>();
95  
96      private LinuxHWDiskStore(String name, String model, String serial, long size) {
97          super(name, model, serial, size);
98      }
99  
100     @Override
101     public long getReads() {
102         return reads;
103     }
104 
105     @Override
106     public long getReadBytes() {
107         return readBytes;
108     }
109 
110     @Override
111     public long getWrites() {
112         return writes;
113     }
114 
115     @Override
116     public long getWriteBytes() {
117         return writeBytes;
118     }
119 
120     @Override
121     public long getCurrentQueueLength() {
122         return currentQueueLength;
123     }
124 
125     @Override
126     public long getTransferTime() {
127         return transferTime;
128     }
129 
130     @Override
131     public long getTimeStamp() {
132         return timeStamp;
133     }
134 
135     @Override
136     public List<HWPartition> getPartitions() {
137         return this.partitionList;
138     }
139 
140     /**
141      * Gets the disks on this machine
142      *
143      * @return a list of {@link HWDiskStore} objects representing the disks
144      */
145     public static List<HWDiskStore> getDisks() {
146         return getDisks(null);
147     }
148 
149     private static List<HWDiskStore> getDisks(LinuxHWDiskStore storeToUpdate) {
150         if (!HAS_UDEV) {
151             LOG.warn("Disk Store information requires libudev, which is not present.");
152             return Collections.emptyList();
153         }
154         LinuxHWDiskStore store = null;
155         List<HWDiskStore> result = new ArrayList<>();
156 
157         Map<String, String> mountsMap = readMountsMap();
158 
159         UdevContext udev = Udev.INSTANCE.udev_new();
160         try {
161             UdevEnumerate enumerate = udev.enumerateNew();
162             try {
163                 enumerate.addMatchSubsystem(BLOCK);
164                 enumerate.scanDevices();
165                 for (UdevListEntry entry = enumerate.getListEntry(); entry != null; entry = entry.getNext()) {
166                     String syspath = entry.getName();
167                     UdevDevice device = udev.deviceNewFromSyspath(syspath);
168                     if (device != null) {
169                         try {
170                             // devnode is what we use as name, like /dev/sda
171                             String devnode = device.getDevnode();
172                             // Ignore loopback and ram disks; do nothing
173                             if (devnode != null && !devnode.startsWith(DevPath.LOOP)
174                                     && !devnode.startsWith(DevPath.RAM)) {
175                                 if (DISK.equals(device.getDevtype())) {
176                                     // Null model and serial in virtual environments
177                                     String devModel = device.getPropertyValue(ID_MODEL);
178                                     String devSerial = device.getPropertyValue(ID_SERIAL_SHORT);
179                                     long devSize = ParseUtil.parseLongOrDefault(device.getSysattrValue(SIZE), 0L)
180                                             * SECTORSIZE;
181                                     if (devnode.startsWith(DevPath.DM)) {
182                                         devModel = LOGICAL_VOLUME_GROUP;
183                                         devSerial = device.getPropertyValue(DM_UUID);
184                                         store = new LinuxHWDiskStore(devnode, devModel,
185                                                 devSerial == null ? Constants.UNKNOWN : devSerial, devSize);
186                                         String vgName = device.getPropertyValue(DM_VG_NAME);
187                                         String lvName = device.getPropertyValue(DM_LV_NAME);
188                                         store.partitionList.add(new HWPartition(
189                                                 getPartitionNameForDmDevice(vgName, lvName), device.getSysname(),
190                                                 device.getPropertyValue(ID_FS_TYPE) == null ? PARTITION
191                                                         : device.getPropertyValue(ID_FS_TYPE),
192                                                 device.getPropertyValue(ID_FS_UUID) == null ? ""
193                                                         : device.getPropertyValue(ID_FS_UUID),
194                                                 ParseUtil.parseLongOrDefault(device.getSysattrValue(SIZE), 0L)
195                                                         * SECTORSIZE,
196                                                 ParseUtil.parseIntOrDefault(device.getPropertyValue(MAJOR), 0),
197                                                 ParseUtil.parseIntOrDefault(device.getPropertyValue(MINOR), 0),
198                                                 getMountPointOfDmDevice(vgName, lvName)));
199                                     } else {
200                                         store = new LinuxHWDiskStore(devnode,
201                                                 devModel == null ? Constants.UNKNOWN : devModel,
202                                                 devSerial == null ? Constants.UNKNOWN : devSerial, devSize);
203                                     }
204                                     if (storeToUpdate == null) {
205                                         // If getting all stores, add to the list with stats
206                                         computeDiskStats(store, device.getSysattrValue(STAT));
207                                         result.add(store);
208                                     } else if (store.getName().equals(storeToUpdate.getName())
209                                             && store.getModel().equals(storeToUpdate.getModel())
210                                             && store.getSerial().equals(storeToUpdate.getSerial())
211                                             && store.getSize() == storeToUpdate.getSize()) {
212                                         // If we are only updating a single disk, the name, model, serial, and size are
213                                         // sufficient to test if this is a match. Add the (old) object, release handle
214                                         // and return.
215                                         computeDiskStats(storeToUpdate, device.getSysattrValue(STAT));
216                                         result.add(storeToUpdate);
217                                         break;
218                                     }
219                                 } else if (storeToUpdate == null && store != null // only add if getting new list
220                                         && PARTITION.equals(device.getDevtype())) {
221                                     // udev_device_get_parent_*() does not take a reference on the returned device,
222                                     // it is automatically unref'd with the parent
223                                     UdevDevice parent = device.getParentWithSubsystemDevtype(BLOCK, DISK);
224                                     if (parent != null && store.getName().equals(parent.getDevnode())) {
225                                         // `store` should still point to the parent HWDiskStore this partition is
226                                         // attached to. If not, it's an error, so skip.
227                                         String name = device.getDevnode();
228                                         store.partitionList.add(new HWPartition(name, device.getSysname(),
229                                                 device.getPropertyValue(ID_FS_TYPE) == null ? PARTITION
230                                                         : device.getPropertyValue(ID_FS_TYPE),
231                                                 device.getPropertyValue(ID_FS_UUID) == null ? ""
232                                                         : device.getPropertyValue(ID_FS_UUID),
233                                                 ParseUtil.parseLongOrDefault(device.getSysattrValue(SIZE), 0L)
234                                                         * SECTORSIZE,
235                                                 ParseUtil.parseIntOrDefault(device.getPropertyValue(MAJOR), 0),
236                                                 ParseUtil.parseIntOrDefault(device.getPropertyValue(MINOR), 0),
237                                                 mountsMap.getOrDefault(name,
238                                                         getDependentNamesFromHoldersDirectory(device.getSysname()))));
239                                     }
240                                 }
241                             }
242                         } finally {
243                             device.unref();
244                         }
245                     }
246                 }
247             } finally {
248                 enumerate.unref();
249             }
250         } finally {
251             udev.unref();
252         }
253         // Iterate the list and make the partitions unmodifiable
254         for (HWDiskStore hwds : result) {
255             ((LinuxHWDiskStore) hwds).partitionList = Collections.unmodifiableList(hwds.getPartitions().stream()
256                     .sorted(Comparator.comparing(HWPartition::getName)).collect(Collectors.toList()));
257         }
258         return result;
259     }
260 
261     @Override
262     public boolean updateAttributes() {
263         // If this returns non-empty (the same store, but updated) then we were
264         // successful in the update
265         return !getDisks(this).isEmpty();
266     }
267 
268     private static Map<String, String> readMountsMap() {
269         Map<String, String> mountsMap = new HashMap<>();
270         List<String> mounts = FileUtil.readFile(ProcPath.MOUNTS);
271         for (String mount : mounts) {
272             String[] split = ParseUtil.whitespaces.split(mount);
273             if (split.length < 2 || !split[0].startsWith(DevPath.DEV)) {
274                 continue;
275             }
276             mountsMap.put(split[0], split[1]);
277         }
278         return mountsMap;
279     }
280 
281     private static void computeDiskStats(LinuxHWDiskStore store, String devstat) {
282         long[] devstatArray = ParseUtil.parseStringToLongArray(devstat, UDEV_STAT_ORDERS, UDEV_STAT_LENGTH, ' ');
283         store.timeStamp = System.currentTimeMillis();
284 
285         // Reads and writes are converted in bytes
286         store.reads = devstatArray[UdevStat.READS.ordinal()];
287         store.readBytes = devstatArray[UdevStat.READ_BYTES.ordinal()] * SECTORSIZE;
288         store.writes = devstatArray[UdevStat.WRITES.ordinal()];
289         store.writeBytes = devstatArray[UdevStat.WRITE_BYTES.ordinal()] * SECTORSIZE;
290         store.currentQueueLength = devstatArray[UdevStat.QUEUE_LENGTH.ordinal()];
291         store.transferTime = devstatArray[UdevStat.ACTIVE_MS.ordinal()];
292     }
293 
294     private static String getPartitionNameForDmDevice(String vgName, String lvName) {
295         return new StringBuilder().append(DevPath.DEV).append(vgName).append('/').append(lvName).toString();
296     }
297 
298     private static String getMountPointOfDmDevice(String vgName, String lvName) {
299         return new StringBuilder().append(DevPath.MAPPER).append(vgName).append('-').append(lvName).toString();
300     }
301 
302     private static String getDependentNamesFromHoldersDirectory(String sysPath) {
303         File holdersDir = new File(sysPath + "/holders");
304         File[] holders = holdersDir.listFiles();
305         if (holders != null) {
306             return Arrays.stream(holders).map(File::getName).collect(Collectors.joining(" "));
307         }
308         return "";
309     }
310 
311     // Order the field is in udev stats
312     enum UdevStat {
313         // The parsing implementation in ParseUtil requires these to be declared
314         // in increasing order. Use 0-ordered index here
315         READS(0), READ_BYTES(2), WRITES(4), WRITE_BYTES(6), QUEUE_LENGTH(8), ACTIVE_MS(9);
316 
317         private int order;
318 
319         public int getOrder() {
320             return this.order;
321         }
322 
323         UdevStat(int order) {
324             this.order = order;
325         }
326     }
327 }