new: parse disk info via lsblk output Fixes #709 (#760)

This commit is contained in:
lollipopkit🏳️‍⚧️
2025-05-17 00:45:38 +08:00
committed by GitHub
parent d88e97e699
commit 7e16d2f159
6 changed files with 685 additions and 184 deletions

View File

@@ -30,8 +30,7 @@ enum ShellFunc {
/// Default is [scriptDirTmp]/[scriptFile], if this path is not accessible,
/// it will be changed to [scriptDirHome]/[scriptFile].
static String getScriptDir(String id) {
final customScriptDir =
ServerProvider.pick(id: id)?.value.spi.custom?.scriptDir;
final customScriptDir = ServerProvider.pick(id: id)?.value.spi.custom?.scriptDir;
if (customScriptDir != null) return customScriptDir;
return _scriptDirMap.putIfAbsent(id, () {
return scriptDirTmp;
@@ -164,9 +163,7 @@ exec 2>/dev/null
// Write each func
for (final func in values) {
final customCmdsStr = () {
if (func == ShellFunc.status &&
customCmds != null &&
customCmds.isNotEmpty) {
if (func == ShellFunc.status && customCmds != null && customCmds.isNotEmpty) {
return '$cmdDivider\n\t${customCmds.values.join(cmdDivider)}';
}
return '';
@@ -213,14 +210,13 @@ enum StatusCmdType {
cpu._('cat /proc/stat | grep cpu'),
uptime._('uptime'),
conn._('cat /proc/net/snmp'),
disk._('df'),
disk._('lsblk --bytes --json --output FSTYPE,PATH,NAME,KNAME,MOUNTPOINT,FSSIZE,FSUSED,FSAVAIL,FSUSE%,UUID'),
mem._("cat /proc/meminfo | grep -E 'Mem|Swap'"),
tempType._('cat /sys/class/thermal/thermal_zone*/type'),
tempVal._('cat /sys/class/thermal/thermal_zone*/temp'),
host._('cat /etc/hostname'),
diskio._('cat /proc/diskstats'),
battery._(
'for f in /sys/class/power_supply/*/uevent; do cat "\$f"; echo; done'),
battery._('for f in /sys/class/power_supply/*/uevent; do cat "\$f"; echo; done'),
nvidia._('nvidia-smi -q -x'),
sensors._('sensors'),
cpuBrand._('cat /proc/cpuinfo | grep "model name"'),
@@ -238,6 +234,7 @@ enum BSDStatusCmdType {
sys._('uname -or'),
cpu._('top -l 1 | grep "CPU usage"'),
uptime._('uptime'),
// Keep df -k for BSD systems as lsblk is not available on macOS/BSD
disk._('df -k'),
mem._('top -l 1 | grep PhysMem'),
//temp,

View File

@@ -1,29 +1,208 @@
import 'dart:convert';
import 'package:equatable/equatable.dart';
import 'package:fl_lib/fl_lib.dart';
import 'package:server_box/data/model/server/time_seq.dart';
import 'package:server_box/data/res/misc.dart';
class Disk {
final String fs;
class Disk with EquatableMixin {
final String path;
final String? fsTyp;
final String mount;
final int usedPercent;
final BigInt used;
final BigInt size;
final BigInt avail;
/// Device name (e.g., sda1, nvme0n1p1)
final String? name;
/// Internal kernel device name
final String? kname;
/// Filesystem UUID
final String? uuid;
/// Child disks (partitions)
final List<Disk> children;
const Disk({
required this.fs,
required this.path,
this.fsTyp,
required this.mount,
required this.usedPercent,
required this.used,
required this.size,
required this.avail,
this.name,
this.kname,
this.uuid,
this.children = const [],
});
static List<Disk> parse(String raw) {
final list = <Disk>[];
raw = raw.trim();
try {
if (raw.startsWith('{')) {
// Parse JSON output from lsblk command
final Map<String, dynamic> jsonData = json.decode(raw);
final List<dynamic> blockdevices = jsonData['blockdevices'] ?? [];
for (final device in blockdevices) {
// Process each device
_processTopLevelDevice(device, list);
}
} else {
// Fallback to the old parsing method in case of non-JSON output
return _parseWithOldMethod(raw);
}
} catch (e) {
Loggers.app.warning('Failed to parse disk info: $e', e);
}
return list;
}
/// Process a top-level device and add all valid disks to the list
static void _processTopLevelDevice(Map<String, dynamic> device, List<Disk> list) {
final disk = _processDiskDevice(device);
if (disk != null) {
list.add(disk);
}
// For devices with children (like physical disks with partitions),
// also process each child individually to ensure BTRFS RAID disks are properly handled
final List<dynamic> childDevices = device['children'] ?? [];
for (final childDevice in childDevices) {
final String childPath = childDevice['path']?.toString() ?? '';
final String childFsType = childDevice['fstype']?.toString() ?? '';
// If this is a BTRFS partition, add it directly to ensure it's properly represented
if (childFsType == 'btrfs' && childPath.isNotEmpty) {
final childDisk = _processSingleDevice(childDevice);
if (childDisk != null) {
list.add(childDisk);
}
}
}
}
/// Process a single device without recursively processing its children
static Disk? _processSingleDevice(Map<String, dynamic> device) {
final fstype = device['fstype']?.toString();
final String mountpoint = device['mountpoint']?.toString() ?? '';
final String path = device['path']?.toString() ?? '';
if (path.isEmpty || (fstype == null && mountpoint.isEmpty)) {
return null;
}
if (!_shouldCalc(fstype ?? '', mountpoint)) {
return null;
}
final sizeStr = device['fssize']?.toString() ?? '0';
final size = (BigInt.tryParse(sizeStr) ?? BigInt.zero) ~/ BigInt.from(1024);
final usedStr = device['fsused']?.toString() ?? '0';
final used = (BigInt.tryParse(usedStr) ?? BigInt.zero) ~/ BigInt.from(1024);
final availStr = device['fsavail']?.toString() ?? '0';
final avail = (BigInt.tryParse(availStr) ?? BigInt.zero) ~/ BigInt.from(1024);
// Parse fsuse% which is usually in the format "45%"
String usePercentStr = device['fsuse%']?.toString() ?? '0';
usePercentStr = usePercentStr.replaceAll('%', '');
final usedPercent = int.tryParse(usePercentStr) ?? 0;
final name = device['name']?.toString();
final kname = device['kname']?.toString();
final uuid = device['uuid']?.toString();
return Disk(
path: path,
fsTyp: fstype,
mount: mountpoint,
usedPercent: usedPercent,
used: used,
size: size,
avail: avail,
name: name,
kname: kname,
uuid: uuid,
children: const [], // No children for direct device
);
}
static Disk? _processDiskDevice(Map<String, dynamic> device) {
final fstype = device['fstype']?.toString();
final String mountpoint = device['mountpoint']?.toString() ?? '';
// For parent devices that don't have a mountpoint themselves
final String path = device['path']?.toString() ?? '';
final String mount = mountpoint;
final List<Disk> childDisks = [];
// Process children devices recursively
final List<dynamic> childDevices = device['children'] ?? [];
for (final childDevice in childDevices) {
final childDisk = _processDiskDevice(childDevice);
if (childDisk != null) {
childDisks.add(childDisk);
}
}
// Handle common filesystem cases or parent devices with children
if ((fstype != null && _shouldCalc(fstype, mount)) ||
(childDisks.isNotEmpty && path.isNotEmpty)) {
final sizeStr = device['fssize']?.toString() ?? '0';
final size = (BigInt.tryParse(sizeStr) ?? BigInt.zero) ~/ BigInt.from(1024);
final usedStr = device['fsused']?.toString() ?? '0';
final used = (BigInt.tryParse(usedStr) ?? BigInt.zero) ~/ BigInt.from(1024);
final availStr = device['fsavail']?.toString() ?? '0';
final avail = (BigInt.tryParse(availStr) ?? BigInt.zero) ~/ BigInt.from(1024);
// Parse fsuse% which is usually in the format "45%"
String usePercentStr = device['fsuse%']?.toString() ?? '0';
usePercentStr = usePercentStr.replaceAll('%', '');
final usedPercent = int.tryParse(usePercentStr) ?? 0;
final name = device['name']?.toString();
final kname = device['kname']?.toString();
final uuid = device['uuid']?.toString();
return Disk(
path: path,
fsTyp: fstype,
mount: mount,
usedPercent: usedPercent,
used: used,
size: size,
avail: avail,
name: name,
kname: kname,
uuid: uuid,
children: childDisks,
);
} else if (childDisks.isNotEmpty) {
// If this is a parent device with no filesystem but has children,
// return the first valid child instead
if (childDisks.isNotEmpty) {
return childDisks.first;
}
}
return null;
}
// Fallback to the old parsing method in case JSON parsing fails
static List<Disk> _parseWithOldMethod(String raw) {
final list = <Disk>[];
final items = raw.split('\n');
items.removeAt(0);
if (items.isNotEmpty) items.removeAt(0);
var pathCache = '';
for (var item in items) {
if (item.isEmpty) {
@@ -43,12 +222,12 @@ class Disk {
final mount = vals[5];
if (!_shouldCalc(fs, mount)) continue;
list.add(Disk(
fs: fs,
path: fs,
mount: mount,
usedPercent: int.parse(vals[4].replaceFirst('%', '')),
used: BigInt.parse(vals[2]),
size: BigInt.parse(vals[1]),
avail: BigInt.parse(vals[3]),
used: BigInt.parse(vals[2]) ~/ BigInt.from(1024),
size: BigInt.parse(vals[1]) ~/ BigInt.from(1024),
avail: BigInt.parse(vals[3]) ~/ BigInt.from(1024),
));
} catch (e) {
continue;
@@ -58,9 +237,8 @@ class Disk {
}
@override
String toString() {
return 'Disk{dev: $fs, mount: $mount, usedPercent: $usedPercent, used: $used, size: $size, avail: $avail}';
}
List<Object?> get props =>
[path, name, kname, fsTyp, mount, usedPercent, used, size, avail, uuid, children];
}
class DiskIO extends TimeSeq<List<DiskIOPiece>> {
@@ -72,9 +250,16 @@ class DiskIO extends TimeSeq<List<DiskIOPiece>> {
}
(double?, double?) _getSpeed(String dev) {
if (dev.startsWith('/dev/')) dev = dev.substring(5);
final old = pre.firstWhereOrNull((e) => e.dev == dev);
final new_ = now.firstWhereOrNull((e) => e.dev == dev);
// Extract the device name from path if needed
String searchDev = dev;
if (dev.startsWith('/dev/')) {
searchDev = dev.substring(5);
}
// Try to find by exact device name first
final old = pre.firstWhereOrNull((e) => e.dev == searchDev);
final new_ = now.firstWhereOrNull((e) => e.dev == searchDev);
if (old == null || new_ == null) return (null, null);
final sectorsRead = new_.sectorsRead - old.sectorsRead;
final sectorsWrite = new_.sectorsWrite - old.sectorsWrite;
@@ -111,6 +296,7 @@ class DiskIO extends TimeSeq<List<DiskIOPiece>> {
read += read_ ?? 0;
write += write_ ?? 0;
}
final readStr = '${read.bytes2Str}/s';
final writeStr = '${write.bytes2Str}/s';
return (readStr, writeStr);
@@ -168,7 +354,11 @@ class DiskUsage {
required this.size,
});
double get usedPercent => used / size * 100;
double get usedPercent {
// Avoid division by zero
if (size == BigInt.zero) return 0;
return used / size * 100;
}
/// Find all devs, add their used and size
static DiskUsage parse(List<Disk> disks) {
@@ -176,9 +366,12 @@ class DiskUsage {
var used = BigInt.zero;
var size = BigInt.zero;
for (var disk in disks) {
if (!_shouldCalc(disk.fs, disk.mount)) continue;
if (devs.contains(disk.fs)) continue;
devs.add(disk.fs);
if (!_shouldCalc(disk.path, disk.mount)) continue;
// Use a combination of path and kernel name to uniquely identify disks
// This helps distinguish between multiple physical disks in BTRFS RAID setups
final uniqueId = '${disk.path}:${disk.kname ?? "unknown"}';
if (devs.contains(uniqueId)) continue;
devs.add(uniqueId);
used += disk.used;
size += disk.size;
}
@@ -187,12 +380,24 @@ class DiskUsage {
}
bool _shouldCalc(String fs, String mount) {
// Skip swap partitions
// if (mount == '[SWAP]') return false;
// Include standard filesystems
if (fs.startsWith('/dev')) return true;
// Some NAS may have mounted path like this `//192.168.1.2/`
if (fs.startsWith('//')) return true;
if (mount.startsWith('/mnt')) return true;
// if (fs.startsWith('shm') ||
// fs.startsWith('overlay') ||
// fs.startsWith('tmpfs')) return false;
return false;
// Include common filesystem types
// final commonFsTypes = ['ext2', 'ext3', 'ext4', 'xfs', 'btrfs', 'zfs', 'ntfs', 'fat', 'vfat'];
// if (commonFsTypes.any((type) => fs.toLowerCase() == type)) return true;
// Skip special filesystems
// if (fs == 'LVM2_member' || fs == 'crypto_LUKS') return false;
if (fs.startsWith('shm') || fs.startsWith('overlay') || fs.startsWith('tmpfs')) {
return false;
}
return true;
}