oshi.software.os.linux.LinuxOperatingSystem Maven / Gradle / Ivy
/**
* MIT License
*
* Copyright (c) 2010 - 2020 The OSHI Project Contributors: https://github.com/oshi/oshi/graphs/contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
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.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.sun.jna.Native; // NOSONAR squid:S1191
import com.sun.jna.platform.linux.LibC;
import com.sun.jna.platform.linux.LibC.Sysinfo;
import oshi.annotation.concurrent.ThreadSafe;
import oshi.driver.linux.Who;
import oshi.driver.linux.proc.CpuStat;
import oshi.driver.linux.proc.ProcessStat;
import oshi.driver.linux.proc.UpTime;
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.util.Constants;
import oshi.util.ExecutingCommand;
import oshi.util.FileUtil;
import oshi.util.ParseUtil;
import oshi.util.platform.linux.ProcPath;
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";
/**
* Jiffies per second, used for process time counters.
*/
private static final long USER_HZ = ParseUtil.parseLongOrDefault(ExecutingCommand.getFirstAnswer("getconf CLK_TCK"),
100L);
// 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 "GNU/Linux";
}
@Override
public FamilyVersionInfo 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 FamilyVersionInfo(familyVersionCodename.getA(), versionInfo);
}
@Override
protected int queryBitness(int jvmBitness) {
if (jvmBitness < 64 && ExecutingCommand.getFirstAnswer("uname -m").indexOf("64") == -1) {
return jvmBitness;
}
return 64;
}
@Override
protected boolean queryElevated() {
return System.getenv("SUDO_COMMAND") != null;
}
@Override
public FileSystem getFileSystem() {
return new LinuxFileSystem();
}
@Override
public InternetProtocolStats getInternetProtocolStats() {
return new LinuxInternetProtocolStats();
}
@Override
public List getSessions() {
return Collections.unmodifiableList(USE_WHO_COMMAND ? super.getSessions() : Who.queryUtxent());
}
@Override
public List getProcesses(int limit, ProcessSort sort) {
List procs = new ArrayList<>();
File[] pids = ProcessStat.getPidFiles();
// now for each file (with digit name) get process info
for (File pidFile : pids) {
int pid = ParseUtil.parseIntOrDefault(pidFile.getName(), 0);
OSProcess proc = new LinuxOSProcess(pid);
if (!proc.getState().equals(State.INVALID)) {
procs.add(proc);
}
}
// Sort
List sorted = processSort(procs, limit, sort);
return Collections.unmodifiableList(sorted);
}
@Override
public OSProcess getProcess(int pid) {
OSProcess proc = new LinuxOSProcess(pid);
if (!proc.getState().equals(State.INVALID)) {
return proc;
}
return null;
}
@Override
public List getChildProcesses(int parentPid, int limit, ProcessSort sort) {
List procs = new ArrayList<>();
File[] procFiles = ProcessStat.getPidFiles();
// now for each file (with digit name) get process info
for (File procFile : procFiles) {
int pid = ParseUtil.parseIntOrDefault(procFile.getName(), 0);
if (parentPid == getParentPidFromProcFile(pid)) {
OSProcess proc = new LinuxOSProcess(pid);
if (!proc.getState().equals(State.INVALID)) {
procs.add(proc);
}
}
}
List sorted = processSort(procs, limit, sort);
return Collections.unmodifiableList(sorted);
}
private static int getParentPidFromProcFile(int pid) {
String stat = FileUtil.getStringFromFile(String.format("/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 getThreadCount() {
try {
Sysinfo info = new Sysinfo();
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());
return family != null ? family : name.substring(0, 1).toUpperCase() + name.substring(1);
}
}
@Override
public OSService[] getServices() {
// Get running services
List services = new ArrayList<>();
Set running = new HashSet<>();
for (OSProcess p : getChildProcesses(1, 0, ProcessSort.PID)) {
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().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.toArray(new OSService[0]);
}
/**
* Gets Jiffies per second, useful for converting ticks to milliseconds and vice
* versa.
*
* @return Jiffies per second.
*/
public static long getHz() {
return USER_HZ;
}
}