oshi.software.os.linux.LinuxOperatingSystem Maven / Gradle / Ivy
/*
* Copyright 2016-2024 The OSHI Project Contributors
* SPDX-License-Identifier: MIT
*/
package oshi.software.os.linux;
import static oshi.software.os.OSService.State.RUNNING;
import static oshi.software.os.OSService.State.STOPPED;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.sun.jna.Native;
import com.sun.jna.platform.linux.LibC;
import com.sun.jna.platform.linux.Udev;
import oshi.annotation.concurrent.ThreadSafe;
import oshi.driver.linux.Who;
import oshi.driver.linux.proc.Auxv;
import oshi.driver.linux.proc.CpuStat;
import oshi.driver.linux.proc.ProcessStat;
import oshi.driver.linux.proc.UpTime;
import oshi.jna.Struct.CloseableSysinfo;
import oshi.jna.platform.linux.LinuxLibc;
import oshi.software.common.AbstractOperatingSystem;
import oshi.software.os.FileSystem;
import oshi.software.os.InternetProtocolStats;
import oshi.software.os.NetworkParams;
import oshi.software.os.OSProcess;
import oshi.software.os.OSProcess.State;
import oshi.software.os.OSService;
import oshi.software.os.OSSession;
import oshi.software.os.OSThread;
import oshi.util.Constants;
import oshi.util.ExecutingCommand;
import oshi.util.FileUtil;
import oshi.util.GlobalConfig;
import oshi.util.ParseUtil;
import oshi.util.platform.linux.ProcPath;
import oshi.util.tuples.Pair;
import oshi.util.tuples.Triplet;
/**
* Linux is a family of open source Unix-like operating systems based on the Linux kernel, an operating system kernel
* first released on September 17, 1991, by Linus Torvalds. Linux is typically packaged in a Linux distribution.
*/
@ThreadSafe
public class LinuxOperatingSystem extends AbstractOperatingSystem {
private static final Logger LOG = LoggerFactory.getLogger(LinuxOperatingSystem.class);
private static final String OS_RELEASE_LOG = "os-release: {}";
private static final String LSB_RELEASE_A_LOG = "lsb_release -a: {}";
private static final String LSB_RELEASE_LOG = "lsb-release: {}";
private static final String RELEASE_DELIM = " release ";
private static final String DOUBLE_QUOTES = "(?:^\")|(?:\"$)";
private static final String FILENAME_PROPERTIES = "oshi.linux.filename.properties";
/** This static field identifies if the udev library can be loaded. */
public static final boolean HAS_UDEV;
/** This static field identifies if the gettid function is in the c library. */
public static final boolean HAS_GETTID;
/** This static field identifies if the syscall for gettid returns sane results. */
public static final boolean HAS_SYSCALL_GETTID;
static {
boolean hasUdev = false;
boolean hasGettid = false;
boolean hasSyscallGettid = false;
try {
if (GlobalConfig.get(GlobalConfig.OSHI_OS_LINUX_ALLOWUDEV, true)) {
try {
@SuppressWarnings("unused")
Udev lib = Udev.INSTANCE;
hasUdev = true;
} catch (UnsatisfiedLinkError e) {
LOG.warn("Did not find udev library in operating system. Some features may not work.");
}
} else {
LOG.info("Loading of udev not allowed by configuration. Some features may not work.");
}
try {
LinuxLibc.INSTANCE.gettid();
hasGettid = true;
} catch (UnsatisfiedLinkError e) {
LOG.debug("Did not find gettid function in operating system. Using fallbacks.");
}
hasSyscallGettid = hasGettid;
if (!hasGettid) {
try {
hasSyscallGettid = LinuxLibc.INSTANCE.syscall(LinuxLibc.SYS_GETTID).intValue() > 0;
} catch (UnsatisfiedLinkError e) {
LOG.debug("Did not find working syscall gettid function in operating system. Using procfs");
}
}
} catch (NoClassDefFoundError e) {
LOG.error("Did not JNA classes. Investigate incompatible version or missing native dll.");
}
HAS_UDEV = hasUdev;
HAS_GETTID = hasGettid;
HAS_SYSCALL_GETTID = hasSyscallGettid;
}
/**
* Jiffies per second, used for process time counters.
*/
private static final long USER_HZ;
private static final long PAGE_SIZE;
static {
Map auxv = Auxv.queryAuxv();
long hz = auxv.getOrDefault(Auxv.AT_CLKTCK, 0L);
if (hz > 0) {
USER_HZ = hz;
} else {
USER_HZ = ParseUtil.parseLongOrDefault(ExecutingCommand.getFirstAnswer("getconf CLK_TCK"), 100L);
}
long pagesz = Auxv.queryAuxv().getOrDefault(Auxv.AT_PAGESZ, 0L);
if (pagesz > 0) {
PAGE_SIZE = pagesz;
} else {
PAGE_SIZE = ParseUtil.parseLongOrDefault(ExecutingCommand.getFirstAnswer("getconf PAGE_SIZE"), 4096L);
}
}
/**
* OS Name for manufacturer
*/
private static final String OS_NAME = ExecutingCommand.getFirstAnswer("uname -o");
// Package private for access from LinuxOSProcess
static final long BOOTTIME;
static {
long tempBT = CpuStat.getBootTime();
// If above fails, current time minus uptime.
if (tempBT == 0) {
tempBT = System.currentTimeMillis() / 1000L - (long) UpTime.getSystemUptimeSeconds();
}
BOOTTIME = tempBT;
}
// PPID is 4th numeric value in proc pid stat; subtract 1 for 0-index
private static final int[] PPID_INDEX = { 3 };
/**
*
* Constructor for LinuxOperatingSystem.
*
*/
public LinuxOperatingSystem() {
super.getVersionInfo();
}
@Override
public String queryManufacturer() {
return OS_NAME;
}
@Override
public Pair queryFamilyVersionInfo() {
Triplet familyVersionCodename = queryFamilyVersionCodenameFromReleaseFiles();
String buildNumber = null;
List procVersion = FileUtil.readFile(ProcPath.VERSION);
if (!procVersion.isEmpty()) {
String[] split = ParseUtil.whitespaces.split(procVersion.get(0));
for (String s : split) {
if (!"Linux".equals(s) && !"version".equals(s)) {
buildNumber = s;
break;
}
}
}
OSVersionInfo versionInfo = new OSVersionInfo(familyVersionCodename.getB(), familyVersionCodename.getC(),
buildNumber);
return new Pair<>(familyVersionCodename.getA(), versionInfo);
}
@Override
protected int queryBitness(int jvmBitness) {
if (jvmBitness < 64 && !ExecutingCommand.getFirstAnswer("uname -m").contains("64")) {
return jvmBitness;
}
return 64;
}
@Override
public FileSystem getFileSystem() {
return new LinuxFileSystem();
}
@Override
public InternetProtocolStats getInternetProtocolStats() {
return new LinuxInternetProtocolStats();
}
@Override
public List getSessions() {
return USE_WHO_COMMAND ? super.getSessions() : Who.queryUtxent();
}
@Override
public OSProcess getProcess(int pid) {
OSProcess proc = new LinuxOSProcess(pid, this);
if (!proc.getState().equals(State.INVALID)) {
return proc;
}
return null;
}
@Override
public List queryAllProcesses() {
return queryChildProcesses(-1);
}
@Override
public List queryChildProcesses(int parentPid) {
File[] pidFiles = ProcessStat.getPidFiles();
if (parentPid >= 0) {
// Only return descendants
return queryProcessList(getChildrenOrDescendants(getParentPidsFromProcFiles(pidFiles), parentPid, false));
}
Set descendantPids = new HashSet<>();
// Put everything in the "descendant" set
for (File procFile : pidFiles) {
int pid = ParseUtil.parseIntOrDefault(procFile.getName(), -2);
if (pid != -2) {
descendantPids.add(pid);
}
}
return queryProcessList(descendantPids);
}
@Override
public List queryDescendantProcesses(int parentPid) {
File[] pidFiles = ProcessStat.getPidFiles();
return queryProcessList(getChildrenOrDescendants(getParentPidsFromProcFiles(pidFiles), parentPid, true));
}
private List queryProcessList(Set descendantPids) {
List procs = new ArrayList<>();
for (int pid : descendantPids) {
OSProcess proc = new LinuxOSProcess(pid, this);
if (!proc.getState().equals(State.INVALID)) {
procs.add(proc);
}
}
return procs;
}
private static Map getParentPidsFromProcFiles(File[] pidFiles) {
Map parentPidMap = new HashMap<>();
for (File procFile : pidFiles) {
int pid = ParseUtil.parseIntOrDefault(procFile.getName(), 0);
parentPidMap.put(pid, getParentPidFromProcFile(pid));
}
return parentPidMap;
}
private static int getParentPidFromProcFile(int pid) {
String stat = FileUtil.getStringFromFile(String.format(Locale.ROOT, "/proc/%d/stat", pid));
// A race condition may leave us with an empty string
if (stat.isEmpty()) {
return 0;
}
// Grab PPID
long[] statArray = ParseUtil.parseStringToLongArray(stat, PPID_INDEX, ProcessStat.PROC_PID_STAT_LENGTH, ' ');
return (int) statArray[0];
}
@Override
public int getProcessId() {
return LinuxLibc.INSTANCE.getpid();
}
@Override
public int getProcessCount() {
return ProcessStat.getPidFiles().length;
}
@Override
public int getThreadId() {
if (HAS_SYSCALL_GETTID) {
return HAS_GETTID ? LinuxLibc.INSTANCE.gettid()
: LinuxLibc.INSTANCE.syscall(LinuxLibc.SYS_GETTID).intValue();
}
try {
return ParseUtil.parseIntOrDefault(
Files.readSymbolicLink(new File(ProcPath.THREAD_SELF).toPath()).getFileName().toString(), 0);
} catch (IOException e) {
return 0;
}
}
@Override
public OSThread getCurrentThread() {
return new LinuxOSThread(getProcessId(), getThreadId());
}
@Override
public int getThreadCount() {
try (CloseableSysinfo info = new CloseableSysinfo()) {
if (0 != LibC.INSTANCE.sysinfo(info)) {
LOG.error("Failed to get process thread count. Error code: {}", Native.getLastError());
return 0;
}
return info.procs;
} catch (UnsatisfiedLinkError | NoClassDefFoundError e) {
LOG.error("Failed to get procs from sysinfo. {}", e.getMessage());
}
return 0;
}
@Override
public long getSystemUptime() {
return (long) UpTime.getSystemUptimeSeconds();
}
@Override
public long getSystemBootTime() {
return BOOTTIME;
}
@Override
public NetworkParams getNetworkParams() {
return new LinuxNetworkParams();
}
private static Triplet queryFamilyVersionCodenameFromReleaseFiles() {
Triplet familyVersionCodename;
// There are two competing options for family/version information.
// Newer systems are adopting a standard /etc/os-release file:
// https://www.freedesktop.org/software/systemd/man/os-release.html
//
// Some systems are still using the lsb standard which parses a
// variety of /etc/*-release files and is most easily accessed via
// the commandline lsb_release -a, see here:
// https://linux.die.net/man/1/lsb_release
// In this case, the /etc/lsb-release file (if it exists) has
// optional overrides to the information in the /etc/distrib-release
// files, which show: "Distributor release x.x (Codename)"
// Attempt to read /etc/system-release which has more details than
// os-release on (CentOS and Fedora)
if ((familyVersionCodename = readDistribRelease("/etc/system-release")) != null) {
// If successful, we're done. this.family has been set and
// possibly the versionID and codeName
return familyVersionCodename;
}
// Attempt to read /etc/os-release file.
if ((familyVersionCodename = readOsRelease()) != null) {
// If successful, we're done. this.family has been set and
// possibly the versionID and codeName
return familyVersionCodename;
}
// Attempt to execute the `lsb_release` command
if ((familyVersionCodename = execLsbRelease()) != null) {
// If successful, we're done. this.family has been set and
// possibly the versionID and codeName
return familyVersionCodename;
}
// The above two options should hopefully work on most
// distributions. If not, we keep having fun.
// Attempt to read /etc/lsb-release file
if ((familyVersionCodename = readLsbRelease()) != null) {
// If successful, we're done. this.family has been set and
// possibly the versionID and codeName
return familyVersionCodename;
}
// If we're still looking, we search for any /etc/*-release (or
// similar) filename, for which the first line should be of the
// "Distributor release x.x (Codename)" format or possibly a
// "Distributor VERSION x.x (Codename)" format
String etcDistribRelease = getReleaseFilename();
if ((familyVersionCodename = readDistribRelease(etcDistribRelease)) != null) {
// If successful, we're done. this.family has been set and
// possibly the versionID and codeName
return familyVersionCodename;
}
// If we've gotten this far with no match, use the distrib-release
// filename (defaults will eventually give "Unknown")
String family = filenameToFamily(etcDistribRelease.replace("/etc/", "").replace("release", "")
.replace("version", "").replace("-", "").replace("_", ""));
return new Triplet<>(family, Constants.UNKNOWN, Constants.UNKNOWN);
}
/**
* Attempts to read /etc/os-release
*
* @return a triplet with the parsed family, versionID and codeName if file successfully read and NAME= found, null
* otherwise
*/
private static Triplet readOsRelease() {
String family = null;
String versionId = Constants.UNKNOWN;
String codeName = Constants.UNKNOWN;
List osRelease = FileUtil.readFile("/etc/os-release");
// Search for NAME=
for (String line : osRelease) {
if (line.startsWith("VERSION=")) {
LOG.debug(OS_RELEASE_LOG, line);
// remove beginning and ending '"' characters, etc from
// VERSION="14.04.4 LTS, Trusty Tahr" (Ubuntu style)
// or VERSION="17 (Beefy Miracle)" (os-release doc style)
line = line.replace("VERSION=", "").replaceAll(DOUBLE_QUOTES, "").trim();
String[] split = line.split("[()]");
if (split.length <= 1) {
// If no parentheses, check for Ubuntu's comma format
split = line.split(", ");
}
if (split.length > 0) {
versionId = split[0].trim();
}
if (split.length > 1) {
codeName = split[1].trim();
}
} else if (line.startsWith("NAME=") && family == null) {
LOG.debug(OS_RELEASE_LOG, line);
// remove beginning and ending '"' characters, etc from
// NAME="Ubuntu"
family = line.replace("NAME=", "").replaceAll(DOUBLE_QUOTES, "").trim();
} else if (line.startsWith("VERSION_ID=") && versionId.equals(Constants.UNKNOWN)) {
LOG.debug(OS_RELEASE_LOG, line);
// remove beginning and ending '"' characters, etc from
// VERSION_ID="14.04"
versionId = line.replace("VERSION_ID=", "").replaceAll(DOUBLE_QUOTES, "").trim();
}
}
return family == null ? null : new Triplet<>(family, versionId, codeName);
}
/**
* Attempts to execute `lsb_release -a`
*
* @return a triplet with the parsed family, versionID and codeName if the command successfully executed and
* Distributor ID: or Description: found, null otherwise
*/
private static Triplet execLsbRelease() {
String family = null;
String versionId = Constants.UNKNOWN;
String codeName = Constants.UNKNOWN;
// If description is of the format Distrib release x.x (Codename)
// that is primary, otherwise use Distributor ID: which returns the
// distribution concatenated, e.g., RedHat instead of Red Hat
for (String line : ExecutingCommand.runNative("lsb_release -a")) {
if (line.startsWith("Description:")) {
LOG.debug(LSB_RELEASE_A_LOG, line);
line = line.replace("Description:", "").trim();
if (line.contains(RELEASE_DELIM)) {
Triplet triplet = parseRelease(line, RELEASE_DELIM);
family = triplet.getA();
if (versionId.equals(Constants.UNKNOWN)) {
versionId = triplet.getB();
}
if (codeName.equals(Constants.UNKNOWN)) {
codeName = triplet.getC();
}
}
} else if (line.startsWith("Distributor ID:") && family == null) {
LOG.debug(LSB_RELEASE_A_LOG, line);
family = line.replace("Distributor ID:", "").trim();
} else if (line.startsWith("Release:") && versionId.equals(Constants.UNKNOWN)) {
LOG.debug(LSB_RELEASE_A_LOG, line);
versionId = line.replace("Release:", "").trim();
} else if (line.startsWith("Codename:") && codeName.equals(Constants.UNKNOWN)) {
LOG.debug(LSB_RELEASE_A_LOG, line);
codeName = line.replace("Codename:", "").trim();
}
}
return family == null ? null : new Triplet<>(family, versionId, codeName);
}
/**
* Attempts to read /etc/lsb-release
*
* @return a triplet with the parsed family, versionID and codeName if file successfully read and and DISTRIB_ID or
* DISTRIB_DESCRIPTION, null otherwise
*/
private static Triplet readLsbRelease() {
String family = null;
String versionId = Constants.UNKNOWN;
String codeName = Constants.UNKNOWN;
List osRelease = FileUtil.readFile("/etc/lsb-release");
// Search for NAME=
for (String line : osRelease) {
if (line.startsWith("DISTRIB_DESCRIPTION=")) {
LOG.debug(LSB_RELEASE_LOG, line);
line = line.replace("DISTRIB_DESCRIPTION=", "").replaceAll(DOUBLE_QUOTES, "").trim();
if (line.contains(RELEASE_DELIM)) {
Triplet triplet = parseRelease(line, RELEASE_DELIM);
family = triplet.getA();
if (versionId.equals(Constants.UNKNOWN)) {
versionId = triplet.getB();
}
if (codeName.equals(Constants.UNKNOWN)) {
codeName = triplet.getC();
}
}
} else if (line.startsWith("DISTRIB_ID=") && family == null) {
LOG.debug(LSB_RELEASE_LOG, line);
family = line.replace("DISTRIB_ID=", "").replaceAll(DOUBLE_QUOTES, "").trim();
} else if (line.startsWith("DISTRIB_RELEASE=") && versionId.equals(Constants.UNKNOWN)) {
LOG.debug(LSB_RELEASE_LOG, line);
versionId = line.replace("DISTRIB_RELEASE=", "").replaceAll(DOUBLE_QUOTES, "").trim();
} else if (line.startsWith("DISTRIB_CODENAME=") && codeName.equals(Constants.UNKNOWN)) {
LOG.debug(LSB_RELEASE_LOG, line);
codeName = line.replace("DISTRIB_CODENAME=", "").replaceAll(DOUBLE_QUOTES, "").trim();
}
}
return family == null ? null : new Triplet<>(family, versionId, codeName);
}
/**
* Attempts to read /etc/distrib-release (for some value of distrib)
*
* @param filename The /etc/distrib-release file
* @return a triplet with the parsed family, versionID and codeName if file successfully read and " release " or "
* VERSION " found, null otherwise
*/
private static Triplet readDistribRelease(String filename) {
if (new File(filename).exists()) {
List osRelease = FileUtil.readFile(filename);
// Search for Distrib release x.x (Codename)
for (String line : osRelease) {
LOG.debug("{}: {}", filename, line);
if (line.contains(RELEASE_DELIM)) {
// If this parses properly we're done
return parseRelease(line, RELEASE_DELIM);
} else if (line.contains(" VERSION ")) {
// If this parses properly we're done
return parseRelease(line, " VERSION ");
}
}
}
return null;
}
/**
* Helper method to parse version description line style
*
* @param line a String of the form "Distributor release x.x (Codename)"
* @param splitLine A regex to split on, e.g. " release "
* @return a triplet with the parsed family, versionID and codeName
*/
private static Triplet parseRelease(String line, String splitLine) {
String[] split = line.split(splitLine);
String family = split[0].trim();
String versionId = Constants.UNKNOWN;
String codeName = Constants.UNKNOWN;
if (split.length > 1) {
split = split[1].split("[()]");
if (split.length > 0) {
versionId = split[0].trim();
}
if (split.length > 1) {
codeName = split[1].trim();
}
}
return new Triplet<>(family, versionId, codeName);
}
/**
* Looks for a collection of possible distrib-release filenames
*
* @return The first valid matching filename
*/
protected static String getReleaseFilename() {
// Look for any /etc/*-release, *-version, and variants
File etc = new File("/etc");
// Find any *_input files in that path
File[] matchingFiles = etc.listFiles(//
f -> (f.getName().endsWith("-release") || //
f.getName().endsWith("-version") || //
f.getName().endsWith("_release") || //
f.getName().endsWith("_version")) //
&& !(f.getName().endsWith("os-release") || //
f.getName().endsWith("lsb-release") || //
f.getName().endsWith("system-release")));
if (matchingFiles != null && matchingFiles.length > 0) {
return matchingFiles[0].getPath();
}
if (new File("/etc/release").exists()) {
return "/etc/release";
}
// If all else fails, try this
return "/etc/issue";
}
/**
* Converts a portion of a filename (e.g. the 'redhat' in /etc/redhat-release) to a mixed case string representing
* the family (e.g., Red Hat)
*
* @param name Stripped version of filename after removing /etc and -release
* @return Mixed case family
*/
private static String filenameToFamily(String name) {
if (name.isEmpty()) {
return "Solaris";
} else if ("issue".equalsIgnoreCase(name)) {
// /etc/issue will end up here
return "Unknown";
} else {
Properties filenameProps = FileUtil.readPropertiesFromFilename(FILENAME_PROPERTIES);
String family = filenameProps.getProperty(name.toLowerCase(Locale.ROOT));
return family != null ? family : name.substring(0, 1).toUpperCase(Locale.ROOT) + name.substring(1);
}
}
@Override
public List getServices() {
// Get running services
List services = new ArrayList<>();
Set running = new HashSet<>();
for (OSProcess p : getChildProcesses(1, ProcessFiltering.ALL_PROCESSES, ProcessSorting.PID_ASC, 0)) {
OSService s = new OSService(p.getName(), p.getProcessID(), RUNNING);
services.add(s);
running.add(p.getName());
}
boolean systemctlFound = false;
List systemctl = ExecutingCommand.runNative("systemctl list-unit-files");
for (String str : systemctl) {
String[] split = ParseUtil.whitespaces.split(str);
if (split.length >= 2 && split[0].endsWith(".service") && "enabled".equals(split[1])) {
// remove .service extension
String name = split[0].substring(0, split[0].length() - 8);
int index = name.lastIndexOf('.');
String shortName = (index < 0 || index > name.length() - 2) ? name : name.substring(index + 1);
if (!running.contains(name) && !running.contains(shortName)) {
OSService s = new OSService(name, 0, STOPPED);
services.add(s);
systemctlFound = true;
}
}
}
if (!systemctlFound) {
// Get Directories for stopped services
File dir = new File("/etc/init");
if (dir.exists() && dir.isDirectory()) {
for (File f : dir.listFiles((f, name) -> name.toLowerCase(Locale.ROOT).endsWith(".conf"))) {
// remove .conf extension
String name = f.getName().substring(0, f.getName().length() - 5);
int index = name.lastIndexOf('.');
String shortName = (index < 0 || index > name.length() - 2) ? name : name.substring(index + 1);
if (!running.contains(name) && !running.contains(shortName)) {
OSService s = new OSService(name, 0, STOPPED);
services.add(s);
}
}
} else {
LOG.error("Directory: /etc/init does not exist");
}
}
return services;
}
/**
* Gets Jiffies per second, useful for converting ticks to milliseconds and vice versa.
*
* @return Jiffies per second.
*/
public static long getHz() {
return USER_HZ;
}
/**
* Gets Page Size, for converting memory stats from pages to bytes
*
* @return Page Size
*/
public static long getPageSize() {
return PAGE_SIZE;
}
}