бла бла бла
This commit is contained in:
@@ -1,66 +1,89 @@
|
||||
|
||||
package com.htopweb.system;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import oshi.SystemInfo;
|
||||
import oshi.hardware.CentralProcessor;
|
||||
import oshi.hardware.GlobalMemory;
|
||||
import oshi.hardware.HardwareAbstractionLayer;
|
||||
import oshi.software.os.OperatingSystem;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
public class SystemMetricsService {
|
||||
|
||||
private final SystemInfo _systemInfo;
|
||||
private final HardwareAbstractionLayer _hardware;
|
||||
private static final Path HOST_ROOT = Path.of("/host/root");
|
||||
private static final Path HOST_PROC = HOST_ROOT.resolve("proc");
|
||||
|
||||
private final OperatingSystem _operatingSystem;
|
||||
private final CentralProcessor _processor;
|
||||
private final GlobalMemory _memory;
|
||||
|
||||
private long[] _previousCpuTicks;
|
||||
private CpuTicks _previousHostCpuTicks;
|
||||
|
||||
private static final Logger log =
|
||||
LoggerFactory.getLogger(SystemMetricsService.class);
|
||||
|
||||
public SystemMetricsService() {
|
||||
_systemInfo = new SystemInfo();
|
||||
_hardware = _systemInfo.getHardware();
|
||||
_operatingSystem = _systemInfo.getOperatingSystem();
|
||||
_processor = _hardware.getProcessor();
|
||||
_memory = _hardware.getMemory();
|
||||
SystemInfo systemInfo = new SystemInfo();
|
||||
HardwareAbstractionLayer hardware = systemInfo.getHardware();
|
||||
|
||||
_previousCpuTicks = _processor.getSystemCpuLoadTicks();
|
||||
_operatingSystem = systemInfo.getOperatingSystem();
|
||||
_processor = hardware.getProcessor();
|
||||
_memory = hardware.getMemory();
|
||||
|
||||
_previousHostCpuTicks = readHostCpuTicks();
|
||||
}
|
||||
|
||||
public synchronized SystemMetricsResponse getMetrics() {
|
||||
long[] currentCpuTicks = _processor.getSystemCpuLoadTicks();
|
||||
CpuTicks currentTicks = readHostCpuTicks();
|
||||
double cpuUsage = 0.0;
|
||||
|
||||
double cpuUsage = _processor.getSystemCpuLoadBetweenTicks(
|
||||
_previousCpuTicks,
|
||||
currentCpuTicks
|
||||
);
|
||||
if (currentTicks != null && _previousHostCpuTicks != null) {
|
||||
long totalDelta = currentTicks.total() - _previousHostCpuTicks.total();
|
||||
long idleDelta = currentTicks.idle() - _previousHostCpuTicks.idle();
|
||||
|
||||
_previousCpuTicks = currentCpuTicks;
|
||||
|
||||
if (Double.isNaN(cpuUsage) || Double.isInfinite(cpuUsage)) {
|
||||
cpuUsage = 0.0;
|
||||
if (totalDelta > 0 && idleDelta >= 0 && idleDelta <= totalDelta) {
|
||||
cpuUsage = (double) (totalDelta - idleDelta) / totalDelta;
|
||||
}
|
||||
}
|
||||
|
||||
// Task Manager displays system CPU usage in the 0–100% range.
|
||||
cpuUsage = Math.max(0.0, Math.min(1.0, cpuUsage));
|
||||
if (currentTicks != null) {
|
||||
_previousHostCpuTicks = currentTicks;
|
||||
}
|
||||
|
||||
Map<String, Long> hostMemory = readHostMemory();
|
||||
|
||||
long totalMemory = hostMemory.getOrDefault("MemTotal", _memory.getTotal());
|
||||
long availableMemory = hostMemory.getOrDefault(
|
||||
"MemAvailable",
|
||||
_memory.getAvailable()
|
||||
);
|
||||
|
||||
availableMemory = Math.max(0L, Math.min(totalMemory, availableMemory));
|
||||
|
||||
long totalMemory = _memory.getTotal();
|
||||
long availableMemory = _memory.getAvailable();
|
||||
long usedMemory = Math.max(0L, totalMemory - availableMemory);
|
||||
|
||||
double memoryUsage = totalMemory > 0
|
||||
? (double) usedMemory / totalMemory * 100.0
|
||||
: 0.0;
|
||||
|
||||
Map<String, String> osRelease = readHostOsRelease();
|
||||
|
||||
return new SystemMetricsResponse(
|
||||
_operatingSystem.getFamily(),
|
||||
_operatingSystem.getVersionInfo().getVersion(),
|
||||
_operatingSystem.getNetworkParams().getHostName(),
|
||||
getHostOsFamily(osRelease),
|
||||
getHostOsVersion(osRelease),
|
||||
readHostHostname(),
|
||||
_processor.getProcessorIdentifier().getName(),
|
||||
_processor.getLogicalProcessorCount(),
|
||||
getLogicalProcessorCount(),
|
||||
round(cpuUsage * 100.0),
|
||||
totalMemory,
|
||||
availableMemory,
|
||||
@@ -69,7 +92,187 @@ public class SystemMetricsService {
|
||||
);
|
||||
}
|
||||
|
||||
private CpuTicks readHostCpuTicks() {
|
||||
Path statFile = HOST_PROC.resolve("stat");
|
||||
|
||||
try {
|
||||
if (!Files.isRegularFile(statFile)
|
||||
|| !Files.isReadable(statFile)) {
|
||||
log.warn("Host CPU statistics are unavailable: {}", statFile);
|
||||
return null;
|
||||
}
|
||||
|
||||
for (String line : Files.readAllLines(statFile)) {
|
||||
if (!line.startsWith("cpu ")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String[] fields = line.trim().split("\\s+");
|
||||
if (fields.length < 5) {
|
||||
log.warn("Invalid host CPU statistics in {}", statFile);
|
||||
return null;
|
||||
}
|
||||
|
||||
long total = 0L;
|
||||
|
||||
for (int i = 1; i < fields.length; i++) {
|
||||
total += Long.parseLong(fields[i]);
|
||||
}
|
||||
|
||||
long idle = Long.parseLong(fields[4]);
|
||||
|
||||
if (fields.length > 5) {
|
||||
idle += Long.parseLong(fields[5]);
|
||||
}
|
||||
|
||||
return new CpuTicks(total, idle);
|
||||
}
|
||||
|
||||
log.warn("Aggregate CPU line not found in {}", statFile);
|
||||
} catch (IOException | NumberFormatException e) {
|
||||
log.warn("Failed to read host CPU statistics: {}", e.getMessage());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private Map<String, Long> readHostMemory() {
|
||||
Map<String, Long> values = new HashMap<>();
|
||||
Path meminfoFile = HOST_PROC.resolve("meminfo");
|
||||
|
||||
try {
|
||||
if (!Files.isRegularFile(meminfoFile)) {
|
||||
return values;
|
||||
}
|
||||
|
||||
for (String line : Files.readAllLines(meminfoFile)) {
|
||||
int separator = line.indexOf(':');
|
||||
|
||||
if (separator < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String key = line.substring(0, separator).trim();
|
||||
|
||||
if (!key.equals("MemTotal") && !key.equals("MemAvailable")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String[] parts = line.substring(separator + 1).trim().split("\\s+");
|
||||
|
||||
if (parts.length > 0) {
|
||||
// Linux meminfo values are reported in kB.
|
||||
values.put(key, Long.parseLong(parts[0]) * 1024L);
|
||||
}
|
||||
}
|
||||
} catch (IOException | NumberFormatException ignored) {
|
||||
// Use OSHI values when host meminfo is unavailable.
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
private int getLogicalProcessorCount() {
|
||||
Path statFile = HOST_PROC.resolve("stat");
|
||||
|
||||
try {
|
||||
if (Files.isRegularFile(statFile)) {
|
||||
int count = 0;
|
||||
|
||||
for (String line : Files.readAllLines(statFile)) {
|
||||
if (line.matches("^cpu\\d+\\s+.*")) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
if (count > 0) {
|
||||
return count;
|
||||
}
|
||||
}
|
||||
} catch (IOException ignored) {
|
||||
// Fall back to OSHI.
|
||||
}
|
||||
|
||||
return _processor.getLogicalProcessorCount();
|
||||
}
|
||||
|
||||
private String readHostHostname() {
|
||||
try {
|
||||
Path file = HOST_ROOT.resolve("etc/hostname");
|
||||
|
||||
if (Files.isRegularFile(file)) {
|
||||
String hostname = Files.readString(file).trim();
|
||||
|
||||
if (!hostname.isBlank()) {
|
||||
return hostname;
|
||||
}
|
||||
}
|
||||
} catch (IOException ignored) {
|
||||
// Fall back to OSHI.
|
||||
}
|
||||
|
||||
return _operatingSystem.getNetworkParams().getHostName();
|
||||
}
|
||||
|
||||
private Map<String, String> readHostOsRelease() {
|
||||
Map<String, String> values = new HashMap<>();
|
||||
|
||||
try {
|
||||
Path file = HOST_ROOT.resolve("etc/os-release");
|
||||
|
||||
if (!Files.isRegularFile(file)) {
|
||||
return values;
|
||||
}
|
||||
|
||||
for (String line : Files.readAllLines(file)) {
|
||||
int separator = line.indexOf('=');
|
||||
|
||||
if (separator <= 0 || line.startsWith("#")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String key = line.substring(0, separator).trim();
|
||||
String value = line.substring(separator + 1).trim();
|
||||
|
||||
if (value.length() >= 2
|
||||
&& ((value.startsWith("\"") && value.endsWith("\""))
|
||||
|| (value.startsWith("'") && value.endsWith("'")))) {
|
||||
value = value.substring(1, value.length() - 1);
|
||||
}
|
||||
|
||||
values.put(key, value);
|
||||
}
|
||||
} catch (IOException ignored) {
|
||||
// Fall back to OSHI.
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
private String getHostOsFamily(Map<String, String> osRelease) {
|
||||
String name = osRelease.get("NAME");
|
||||
|
||||
return name != null && !name.isBlank()
|
||||
? name
|
||||
: _operatingSystem.getFamily();
|
||||
}
|
||||
|
||||
private String getHostOsVersion(Map<String, String> osRelease) {
|
||||
String version = osRelease.get("VERSION");
|
||||
|
||||
if (version == null || version.isBlank()) {
|
||||
version = osRelease.get("VERSION_ID");
|
||||
}
|
||||
|
||||
return version != null && !version.isBlank()
|
||||
? version
|
||||
: _operatingSystem.getVersionInfo().getVersion();
|
||||
}
|
||||
|
||||
private double round(double value) {
|
||||
return Math.round(value * 10.0) / 10.0;
|
||||
}
|
||||
|
||||
private record CpuTicks(long total, long idle) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,14 @@ package com.htopweb.system;
|
||||
import oshi.SystemInfo;
|
||||
import oshi.software.os.OSProcess;
|
||||
import oshi.software.os.OperatingSystem;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
@@ -15,15 +20,24 @@ import java.util.Map;
|
||||
@Service
|
||||
public class SystemProcessesService {
|
||||
|
||||
private static final Logger log =
|
||||
LoggerFactory.getLogger(SystemProcessesService.class);
|
||||
|
||||
private static final Path HOST_PROC = Path.of("/host/root/proc");
|
||||
private static final Path PROC = Path.of("/proc");
|
||||
|
||||
private final OperatingSystem _operatingSystem;
|
||||
private final int _logicalProcessorCount;
|
||||
private final long _clockTicksPerSecond;
|
||||
private final long _pageSizeBytes;
|
||||
|
||||
private final Map<Integer, OSProcess> _previousProcesses =
|
||||
private final Map<Integer, ProcessSnapshot> _previousProcesses =
|
||||
new HashMap<>();
|
||||
|
||||
private long _previousSampleNanos = System.nanoTime();
|
||||
|
||||
public SystemProcessesService() {
|
||||
SystemInfo systemInfo = new SystemInfo();
|
||||
|
||||
_operatingSystem = systemInfo.getOperatingSystem();
|
||||
_logicalProcessorCount = Math.max(
|
||||
1,
|
||||
@@ -31,55 +45,246 @@ public class SystemProcessesService {
|
||||
.getProcessor()
|
||||
.getLogicalProcessorCount()
|
||||
);
|
||||
|
||||
_clockTicksPerSecond = readPositiveSystemValue(
|
||||
"getconf CLK_TCK", 100L
|
||||
);
|
||||
_pageSizeBytes = readPositiveSystemValue(
|
||||
"getconf PAGESIZE", 4096L
|
||||
);
|
||||
}
|
||||
|
||||
public synchronized List<ProcessInfoResponse> getProcesses() {
|
||||
List<OSProcess> processes = _operatingSystem.getProcesses();
|
||||
Map<Integer, OSProcess> currentProcesses = new HashMap<>();
|
||||
Path procRoot = Files.isDirectory(HOST_PROC) ? HOST_PROC : PROC;
|
||||
|
||||
List<ProcessInfoResponse> result = processes.stream()
|
||||
.map(process -> {
|
||||
int pid = process.getProcessID();
|
||||
currentProcesses.put(pid, process);
|
||||
if (!Files.isReadable(procRoot)) {
|
||||
log.warn("Process information is unavailable: {}", procRoot);
|
||||
return getProcessesFromOshi();
|
||||
}
|
||||
|
||||
OSProcess previous = _previousProcesses.get(pid);
|
||||
double cpuUsage = 0.0;
|
||||
long nowNanos = System.nanoTime();
|
||||
double elapsedSeconds =
|
||||
(nowNanos - _previousSampleNanos) / 1_000_000_000.0;
|
||||
_previousSampleNanos = nowNanos;
|
||||
|
||||
if (pid != 0
|
||||
&& previous != null
|
||||
&& previous.getStartTime() == process.getStartTime()) {
|
||||
long bootTimeSeconds = readBootTime(procRoot);
|
||||
Map<Integer, ProcessSnapshot> currentProcesses = new HashMap<>();
|
||||
List<ProcessInfoResponse> result = new ArrayList<>();
|
||||
|
||||
cpuUsage = process.getProcessCpuLoadBetweenTicks(previous)
|
||||
* 100.0 / _logicalProcessorCount;
|
||||
try (var entries = Files.list(procRoot)) {
|
||||
entries.filter(path ->
|
||||
path.getFileName().toString().matches("\\d+"))
|
||||
.forEach(path -> {
|
||||
try {
|
||||
int pid = Integer.parseInt(
|
||||
path.getFileName().toString()
|
||||
);
|
||||
|
||||
cpuUsage = Math.max(0.0, Math.min(100.0, cpuUsage));
|
||||
}
|
||||
ProcessSnapshot current = readProcess(
|
||||
path, pid, bootTimeSeconds
|
||||
);
|
||||
|
||||
return new ProcessInfoResponse(
|
||||
pid,
|
||||
process.getName(),
|
||||
process.getState().name(),
|
||||
Math.round(cpuUsage * 10.0) / 10.0,
|
||||
process.getResidentMemory(),
|
||||
process.getVirtualSize(),
|
||||
Math.max(
|
||||
0L,
|
||||
(System.currentTimeMillis()
|
||||
- process.getStartTime()) / 1000
|
||||
),
|
||||
process.getThreadCount()
|
||||
);
|
||||
})
|
||||
.sorted(
|
||||
Comparator.comparingDouble(
|
||||
ProcessInfoResponse::cpuUsagePercent
|
||||
).reversed()
|
||||
)
|
||||
.toList();
|
||||
if (current == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
currentProcesses.put(pid, current);
|
||||
|
||||
ProcessSnapshot previous =
|
||||
_previousProcesses.get(pid);
|
||||
|
||||
double cpuUsage = 0.0;
|
||||
|
||||
if (previous != null
|
||||
&& previous.startTimeTicks()
|
||||
== current.startTimeTicks()
|
||||
&& elapsedSeconds > 0.0) {
|
||||
long deltaTicks =
|
||||
current.cpuTicks() - previous.cpuTicks();
|
||||
|
||||
if (deltaTicks >= 0) {
|
||||
cpuUsage = deltaTicks
|
||||
/ (double) _clockTicksPerSecond
|
||||
/ elapsedSeconds
|
||||
* 100.0
|
||||
/ _logicalProcessorCount;
|
||||
}
|
||||
}
|
||||
|
||||
cpuUsage = Math.max(
|
||||
0.0, Math.min(100.0, cpuUsage)
|
||||
);
|
||||
|
||||
result.add(new ProcessInfoResponse(
|
||||
pid,
|
||||
current.name(),
|
||||
current.state(),
|
||||
Math.round(cpuUsage * 10.0) / 10.0,
|
||||
current.residentMemoryBytes(),
|
||||
current.virtualMemoryBytes(),
|
||||
current.uptimeSeconds(),
|
||||
current.threadCount()
|
||||
));
|
||||
} catch (IOException | NumberFormatException
|
||||
| SecurityException ignored) {
|
||||
// A process may exit or become inaccessible.
|
||||
}
|
||||
});
|
||||
} catch (IOException e) {
|
||||
log.warn("Cannot read process list from {}: {}",
|
||||
procRoot, e.getMessage());
|
||||
return getProcessesFromOshi();
|
||||
}
|
||||
|
||||
_previousProcesses.clear();
|
||||
_previousProcesses.putAll(currentProcesses);
|
||||
|
||||
result.sort(Comparator.comparingDouble(
|
||||
ProcessInfoResponse::cpuUsagePercent
|
||||
).reversed());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private ProcessSnapshot readProcess(
|
||||
Path processPath,
|
||||
int pid,
|
||||
long bootTimeSeconds
|
||||
) throws IOException {
|
||||
String stat = Files.readString(processPath.resolve("stat")).trim();
|
||||
|
||||
int open = stat.indexOf('(');
|
||||
int close = stat.lastIndexOf(')');
|
||||
|
||||
if (open < 0 || close <= open) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String name = stat.substring(open + 1, close);
|
||||
String[] fields = stat.substring(close + 2).trim().split("\\s+");
|
||||
|
||||
// Array begins at Linux stat field 3 (state).
|
||||
if (fields.length < 22) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String state = fields[0];
|
||||
long userTicks = Long.parseLong(fields[11]); // field 14
|
||||
long systemTicks = Long.parseLong(fields[12]); // field 15
|
||||
long startTicks = Long.parseLong(fields[19]); // field 22
|
||||
long virtualBytes = Long.parseLong(fields[20]); // field 23
|
||||
long residentPages = Long.parseLong(fields[21]); // field 24
|
||||
|
||||
int threadCount = readThreadCount(processPath.resolve("status"));
|
||||
|
||||
long uptimeSeconds = 0L;
|
||||
|
||||
if (bootTimeSeconds > 0L) {
|
||||
long processStartSeconds = bootTimeSeconds
|
||||
+ startTicks / _clockTicksPerSecond;
|
||||
uptimeSeconds = Math.max(
|
||||
0L, System.currentTimeMillis() / 1000L
|
||||
- processStartSeconds
|
||||
);
|
||||
}
|
||||
|
||||
return new ProcessSnapshot(
|
||||
pid,
|
||||
name,
|
||||
state,
|
||||
userTicks + systemTicks,
|
||||
startTicks,
|
||||
Math.max(0L, virtualBytes),
|
||||
Math.max(0L, residentPages) * _pageSizeBytes,
|
||||
uptimeSeconds,
|
||||
threadCount
|
||||
);
|
||||
}
|
||||
|
||||
private int readThreadCount(Path statusFile) {
|
||||
try {
|
||||
for (String line : Files.readAllLines(statusFile)) {
|
||||
if (line.startsWith("Threads:")) {
|
||||
return Integer.parseInt(
|
||||
line.substring("Threads:".length()).trim()
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (IOException | NumberFormatException ignored) {
|
||||
// The process may exit while status is being read.
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private long readBootTime(Path procRoot) {
|
||||
try {
|
||||
for (String line : Files.readAllLines(procRoot.resolve("stat"))) {
|
||||
if (line.startsWith("btime ")) {
|
||||
return Long.parseLong(line.substring(6).trim());
|
||||
}
|
||||
}
|
||||
} catch (IOException | NumberFormatException ignored) {
|
||||
// Boot time unavailable.
|
||||
}
|
||||
|
||||
return 0L;
|
||||
}
|
||||
|
||||
private long readPositiveSystemValue(String command, long fallback) {
|
||||
try {
|
||||
Process process = new ProcessBuilder(
|
||||
"/bin/sh", "-c", command
|
||||
).redirectErrorStream(true).start();
|
||||
|
||||
String output = new String(
|
||||
process.getInputStream().readAllBytes()
|
||||
).trim();
|
||||
|
||||
if (process.waitFor() == 0) {
|
||||
long value = Long.parseLong(output);
|
||||
|
||||
if (value > 0L) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.debug("Cannot read system value for {}: {}", command,
|
||||
e.getMessage());
|
||||
}
|
||||
|
||||
return fallback;
|
||||
}
|
||||
|
||||
private List<ProcessInfoResponse> getProcessesFromOshi() {
|
||||
return _operatingSystem.getProcesses().stream()
|
||||
.map(process -> new ProcessInfoResponse(
|
||||
process.getProcessID(),
|
||||
process.getName(),
|
||||
process.getState().name(),
|
||||
0.0,
|
||||
process.getResidentMemory(),
|
||||
process.getVirtualSize(),
|
||||
Math.max(0L, (System.currentTimeMillis()
|
||||
- process.getStartTime()) / 1000L),
|
||||
process.getThreadCount()
|
||||
))
|
||||
.sorted(Comparator.comparingLong(
|
||||
ProcessInfoResponse::residentMemoryBytes
|
||||
).reversed())
|
||||
.toList();
|
||||
}
|
||||
|
||||
private record ProcessSnapshot(
|
||||
int pid,
|
||||
String name,
|
||||
String state,
|
||||
long cpuTicks,
|
||||
long startTimeTicks,
|
||||
long virtualMemoryBytes,
|
||||
long residentMemoryBytes,
|
||||
long uptimeSeconds,
|
||||
int threadCount
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,54 +1,131 @@
|
||||
|
||||
package com.htopweb.system.linux;
|
||||
|
||||
import oshi.SystemInfo;
|
||||
import oshi.software.os.OSFileStore;
|
||||
import oshi.software.os.OperatingSystem;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.FileStore;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
@Service
|
||||
public class LinuxDiskService {
|
||||
|
||||
private final OperatingSystem _operatingSystem;
|
||||
private static final Logger log =
|
||||
LoggerFactory.getLogger(LinuxDiskService.class);
|
||||
|
||||
public LinuxDiskService() {
|
||||
SystemInfo systemInfo = new SystemInfo();
|
||||
_operatingSystem = systemInfo.getOperatingSystem();
|
||||
}
|
||||
private static final Path HOST_ROOT = Path.of("/host/root");
|
||||
private static final Path HOST_MOUNTS =
|
||||
HOST_ROOT.resolve("proc/1/mounts");
|
||||
|
||||
private static final Set<String> EXCLUDED_TYPES = Set.of(
|
||||
"proc", "sysfs", "devtmpfs", "devpts", "tmpfs",
|
||||
"cgroup", "cgroup2", "overlay", "squashfs",
|
||||
"tracefs", "securityfs", "debugfs", "pstore",
|
||||
"configfs", "fusectl", "mqueue", "hugetlbfs",
|
||||
"rpc_pipefs", "autofs"
|
||||
);
|
||||
|
||||
public List<LinuxDiskResponse> getDisks() {
|
||||
return _operatingSystem.getFileSystem()
|
||||
.getFileStores()
|
||||
.stream()
|
||||
.filter(store -> store.getMount().startsWith("/"))
|
||||
.filter(store -> store.getTotalSpace() > 0)
|
||||
.map(this::toResponse)
|
||||
.sorted(Comparator.comparing(LinuxDiskResponse::mountPoint))
|
||||
.toList();
|
||||
if (!Files.isReadable(HOST_MOUNTS)) {
|
||||
log.error("Host mounts file is not readable: {}", HOST_MOUNTS);
|
||||
return List.of();
|
||||
}
|
||||
|
||||
try {
|
||||
List<LinuxDiskResponse> disks = Files.readAllLines(HOST_MOUNTS)
|
||||
.stream()
|
||||
.map(line -> line.trim().split("\\s+"))
|
||||
.filter(fields -> fields.length >= 3)
|
||||
.filter(fields -> fields[0].startsWith("/dev/"))
|
||||
.filter(fields -> !EXCLUDED_TYPES.contains(fields[2]))
|
||||
.map(this::toHostResponse)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(java.util.stream.Collectors.toMap(
|
||||
LinuxDiskResponse::mountPoint,
|
||||
disk -> disk,
|
||||
(first, second) -> first
|
||||
))
|
||||
.values()
|
||||
.stream()
|
||||
.sorted(Comparator.comparing(
|
||||
LinuxDiskResponse::mountPoint
|
||||
))
|
||||
.toList();
|
||||
|
||||
log.debug("Found {} host disks", disks.size());
|
||||
return disks;
|
||||
} catch (IOException e) {
|
||||
log.error("Failed to read host mounts: {}", e.getMessage(), e);
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
private LinuxDiskResponse toResponse(OSFileStore store) {
|
||||
long total = Math.max(0L, store.getTotalSpace());
|
||||
long usable = Math.max(
|
||||
0L,
|
||||
Math.min(total, store.getUsableSpace())
|
||||
);
|
||||
long used = Math.max(0L, total - usable);
|
||||
private LinuxDiskResponse toHostResponse(String[] fields) {
|
||||
String device = unescapeMountField(fields[0]);
|
||||
String hostMount = unescapeMountField(fields[1]);
|
||||
String type = fields[2];
|
||||
|
||||
double usage = total > 0
|
||||
? Math.round((double) used / total * 1000.0) / 10.0
|
||||
: 0.0;
|
||||
Path accessiblePath = HOST_ROOT.resolve(
|
||||
hostMount.startsWith("/")
|
||||
? hostMount.substring(1)
|
||||
: hostMount
|
||||
).normalize();
|
||||
|
||||
return new LinuxDiskResponse(
|
||||
store.getName(),
|
||||
store.getMount(),
|
||||
store.getType(),
|
||||
total,
|
||||
usable,
|
||||
used,
|
||||
usage
|
||||
);
|
||||
if (!accessiblePath.startsWith(HOST_ROOT)
|
||||
|| !Files.exists(accessiblePath)) {
|
||||
log.warn("Host mount path is unavailable: {}", accessiblePath);
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
FileStore store = Files.getFileStore(accessiblePath);
|
||||
|
||||
long total = Math.max(0L, store.getTotalSpace());
|
||||
long usable = Math.max(
|
||||
0L,
|
||||
Math.min(total, store.getUsableSpace())
|
||||
);
|
||||
|
||||
if (total == 0L) {
|
||||
log.warn("Host disk has zero capacity: {}", hostMount);
|
||||
return null;
|
||||
}
|
||||
|
||||
long used = total - usable;
|
||||
double usage = Math.round(
|
||||
(double) used / total * 1000.0
|
||||
) / 10.0;
|
||||
|
||||
return new LinuxDiskResponse(
|
||||
device,
|
||||
hostMount,
|
||||
type,
|
||||
total,
|
||||
usable,
|
||||
used,
|
||||
usage
|
||||
);
|
||||
} catch (IOException | SecurityException e) {
|
||||
log.warn(
|
||||
"Cannot read capacity for host mount {}: {}",
|
||||
hostMount,
|
||||
e.getMessage()
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String unescapeMountField(String value) {
|
||||
return value.replace("\\040", " ")
|
||||
.replace("\\011", "\t")
|
||||
.replace("\\012", "\n")
|
||||
.replace("\\134", "\\");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
|
||||
package com.htopweb.system.linux;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record LinuxNetworkResponse(
|
||||
String name,
|
||||
String displayName,
|
||||
String macAddress,
|
||||
List<String> ipv4Addresses,
|
||||
List<String> ipv6Addresses,
|
||||
long bytesReceived,
|
||||
long bytesSent,
|
||||
long packetsReceived,
|
||||
|
||||
@@ -1,86 +1,186 @@
|
||||
|
||||
package com.htopweb.system.linux;
|
||||
|
||||
import oshi.SystemInfo;
|
||||
import oshi.hardware.NetworkIF;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@Service
|
||||
public class LinuxNetworkService {
|
||||
|
||||
private final SystemInfo _systemInfo;
|
||||
private static final Logger log =
|
||||
LoggerFactory.getLogger(LinuxNetworkService.class);
|
||||
|
||||
private static final Path HOST_ROOT = Path.of("/host/root");
|
||||
|
||||
private static final Path HOST_NET_DEV =
|
||||
HOST_ROOT.resolve("proc/1/net/dev");
|
||||
|
||||
private static final Path HOST_NET_CLASS =
|
||||
HOST_ROOT.resolve("sys/class/net");
|
||||
|
||||
private static final Set<String> EXCLUDED_INTERFACES =
|
||||
Set.of("lo", "docker0");
|
||||
|
||||
private final ConcurrentMap<String, NetworkSnapshot> _previousSnapshots =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
public LinuxNetworkService() {
|
||||
_systemInfo = new SystemInfo();
|
||||
}
|
||||
|
||||
public List<LinuxNetworkResponse> getInterfaces() {
|
||||
return _systemInfo.getHardware()
|
||||
.getNetworkIFs()
|
||||
.stream()
|
||||
.map(this::toResponse)
|
||||
.toList();
|
||||
if (!Files.isReadable(HOST_NET_DEV)) {
|
||||
log.error("Host network counters unavailable: {}", HOST_NET_DEV);
|
||||
return List.of();
|
||||
}
|
||||
|
||||
try {
|
||||
List<LinuxNetworkResponse> result = new ArrayList<>();
|
||||
|
||||
for (String line : Files.readAllLines(HOST_NET_DEV)) {
|
||||
int separator = line.indexOf(':');
|
||||
|
||||
if (separator < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String name = line.substring(0, separator).trim();
|
||||
|
||||
if (name.isBlank() || !shouldIncludeInterface(name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String[] values = line.substring(separator + 1)
|
||||
.trim()
|
||||
.split("\\s+");
|
||||
|
||||
if (values.length < 16) {
|
||||
continue;
|
||||
}
|
||||
|
||||
result.add(toResponse(
|
||||
name,
|
||||
parseCounter(values[0]),
|
||||
parseCounter(values[1]),
|
||||
parseCounter(values[2]),
|
||||
parseCounter(values[8]),
|
||||
parseCounter(values[9]),
|
||||
parseCounter(values[10])
|
||||
));
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (IOException | NumberFormatException e) {
|
||||
log.error("Failed to read host network counters: {}",
|
||||
e.getMessage(), e);
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
private LinuxNetworkResponse toResponse(NetworkIF networkIF) {
|
||||
networkIF.updateAttributes();
|
||||
private boolean shouldIncludeInterface(String name) {
|
||||
if (EXCLUDED_INTERFACES.contains(name)
|
||||
|| name.startsWith("veth")
|
||||
|| name.startsWith("br-")
|
||||
|| name.startsWith("docker")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String name = networkIF.getName();
|
||||
long received = Math.max(0L, networkIF.getBytesRecv());
|
||||
long sent = Math.max(0L, networkIF.getBytesSent());
|
||||
Path interfacePath = HOST_NET_CLASS.resolve(name);
|
||||
|
||||
// Physical network interfaces normally expose a "device" symlink.
|
||||
if (Files.exists(interfacePath.resolve("device"))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Keep useful virtual interfaces such as WireGuard and tunnels.
|
||||
return Files.exists(interfacePath.resolve("wireless"))
|
||||
|| Files.exists(interfacePath.resolve("tun_flags"))
|
||||
|| name.startsWith("wg")
|
||||
|| name.startsWith("tun")
|
||||
|| name.startsWith("tap")
|
||||
|| name.startsWith("bond")
|
||||
|| name.startsWith("br");
|
||||
}
|
||||
|
||||
private LinuxNetworkResponse toResponse(
|
||||
String name,
|
||||
long received,
|
||||
long packetsReceived,
|
||||
long inboundErrors,
|
||||
long sent,
|
||||
long packetsSent,
|
||||
long outboundErrors
|
||||
) {
|
||||
long now = System.nanoTime();
|
||||
|
||||
NetworkSnapshot current = new NetworkSnapshot(received, sent, now);
|
||||
NetworkSnapshot previous = _previousSnapshots.put(name, current);
|
||||
NetworkSnapshot current =
|
||||
new NetworkSnapshot(received, sent, now);
|
||||
|
||||
NetworkSnapshot previous =
|
||||
_previousSnapshots.put(name, current);
|
||||
|
||||
double receiveBytesPerSecond = 0.0;
|
||||
double sendBytesPerSecond = 0.0;
|
||||
|
||||
if (previous != null && now > previous.timestampNanos()) {
|
||||
double elapsedSeconds =
|
||||
double elapsed =
|
||||
(now - previous.timestampNanos()) / 1_000_000_000.0;
|
||||
|
||||
if (received >= previous.bytesReceived()) {
|
||||
receiveBytesPerSecond =
|
||||
(received - previous.bytesReceived()) / elapsedSeconds;
|
||||
(received - previous.bytesReceived()) / elapsed;
|
||||
}
|
||||
|
||||
if (sent >= previous.bytesSent()) {
|
||||
sendBytesPerSecond =
|
||||
(sent - previous.bytesSent()) / elapsedSeconds;
|
||||
(sent - previous.bytesSent()) / elapsed;
|
||||
}
|
||||
}
|
||||
|
||||
double receiveMbps = receiveBytesPerSecond * 8.0 / 1_000_000.0;
|
||||
double sendMbps = sendBytesPerSecond * 8.0 / 1_000_000.0;
|
||||
Path interfacePath = HOST_NET_CLASS.resolve(name);
|
||||
String alias = readFirstLine(interfacePath.resolve("ifalias"));
|
||||
String displayName = alias.isBlank() ? name : alias;
|
||||
|
||||
return new LinuxNetworkResponse(
|
||||
name,
|
||||
networkIF.getDisplayName(),
|
||||
networkIF.getMacaddr(),
|
||||
Arrays.asList(networkIF.getIPv4addr()),
|
||||
Arrays.asList(networkIF.getIPv6addr()),
|
||||
displayName,
|
||||
received,
|
||||
sent,
|
||||
Math.max(0L, networkIF.getPacketsRecv()),
|
||||
Math.max(0L, networkIF.getPacketsSent()),
|
||||
Math.max(0L, networkIF.getInErrors()),
|
||||
Math.max(0L, networkIF.getOutErrors()),
|
||||
packetsReceived,
|
||||
packetsSent,
|
||||
inboundErrors,
|
||||
outboundErrors,
|
||||
roundOne(receiveBytesPerSecond),
|
||||
roundOne(sendBytesPerSecond),
|
||||
roundOne(receiveMbps),
|
||||
roundOne(sendMbps)
|
||||
roundOne(receiveBytesPerSecond * 8.0 / 1_000_000.0),
|
||||
roundOne(sendBytesPerSecond * 8.0 / 1_000_000.0)
|
||||
);
|
||||
}
|
||||
|
||||
private long parseCounter(String value) {
|
||||
return Math.max(0L, Long.parseLong(value));
|
||||
}
|
||||
|
||||
private String readFirstLine(Path path) {
|
||||
if (!Files.isReadable(path)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
try (Stream<String> lines = Files.lines(path)) {
|
||||
return lines.findFirst().orElse("").trim();
|
||||
} catch (IOException e) {
|
||||
log.debug("Cannot read {}: {}", path, e.getMessage());
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private double roundOne(double value) {
|
||||
return Math.round(value * 10.0) / 10.0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user