fix: fallback to df on incompatible system (#880)

This commit is contained in:
lollipopkit🏳️‍⚧️
2025-09-01 23:32:20 +08:00
committed by GitHub
parent 4ec7f5895e
commit 8e4c2a7cde
3 changed files with 356 additions and 49 deletions

View File

@@ -55,8 +55,8 @@ enum StatusCmdType implements ShellCmdType {
uptime('uptime'),
conn('cat /proc/net/snmp'),
disk(
'lsblk --bytes --json --output '
'FSTYPE,PATH,NAME,KNAME,MOUNTPOINT,FSSIZE,FSUSED,FSAVAIL,FSUSE%,UUID',
'(lsblk --bytes --json --output '
'FSTYPE,PATH,NAME,KNAME,MOUNTPOINT,FSSIZE,FSUSED,FSAVAIL,FSUSE%,UUID 2>/dev/null && echo "LSBLK_SUCCESS") || df -k'
),
mem("cat /proc/meminfo | grep -E 'Mem|Swap'"),
tempType('cat /sys/class/thermal/thermal_zone*/type'),

View File

@@ -44,22 +44,49 @@ class Disk with EquatableMixin {
static List<Disk> parse(String raw) {
final list = <Disk>[];
raw = raw.trim();
if (raw.isEmpty) {
dprint('Empty disk info data received');
return list;
}
try {
// Check if we have lsblk JSON output with success marker
if (raw.startsWith('{')) {
// Parse JSON output from lsblk command
final Map<String, dynamic> jsonData = json.decode(raw);
// Extract JSON part (excluding the success marker if present)
final jsonEnd = raw.indexOf('\nLSBLK_SUCCESS');
final jsonPart = jsonEnd > 0 ? raw.substring(0, jsonEnd) : raw;
try {
final Map<String, dynamic> jsonData = json.decode(jsonPart);
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
// If we successfully parsed JSON and have valid disks, return them
if (list.isNotEmpty) {
return list;
}
} on FormatException catch (e) {
Loggers.app.warning('JSON parsing failed, falling back to df -k output: $e');
} catch (e) {
Loggers.app.warning('Error processing JSON disk data, falling back to df -k output: $e', e);
}
}
// Check if we have df -k output (fallback case)
if (raw.contains('Filesystem') && raw.contains('Mounted on')) {
return _parseWithOldMethod(raw);
}
// If we reach here, both parsing methods failed
Loggers.app.warning('Unable to parse disk info with any method');
} catch (e) {
Loggers.app.warning('Failed to parse disk info: $e', e);
Loggers.app.warning('Failed to parse disk info with both methods: $e', e);
}
return list;
}
@@ -88,6 +115,32 @@ class Disk with EquatableMixin {
}
}
/// Parse filesystem fields from device data
static ({BigInt size, BigInt used, BigInt avail, int usedPercent}) _parseFilesystemFields(Map<String, dynamic> device) {
// Helper function to parse size strings safely
BigInt parseSize(String? sizeStr) {
if (sizeStr == null || sizeStr.isEmpty || sizeStr == 'null' || sizeStr == '0') {
return BigInt.zero;
}
return (BigInt.tryParse(sizeStr) ?? BigInt.zero) ~/ BigInt.from(1024);
}
// Helper function to parse percentage strings
int parsePercent(String? percentStr) {
if (percentStr == null || percentStr.isEmpty || percentStr == 'null') {
return 0;
}
return int.tryParse(percentStr.replaceAll('%', '')) ?? 0;
}
return (
size: parseSize(device['fssize']?.toString()),
used: parseSize(device['fsused']?.toString()),
avail: parseSize(device['fsavail']?.toString()),
usedPercent: parsePercent(device['fsuse%']?.toString()),
);
}
/// Process a single device without recursively processing its children
static Disk? _processSingleDevice(Map<String, dynamic> device) {
final fstype = device['fstype']?.toString();
@@ -102,20 +155,7 @@ class Disk with EquatableMixin {
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 fsFields = _parseFilesystemFields(device);
final name = device['name']?.toString();
final kname = device['kname']?.toString();
final uuid = device['uuid']?.toString();
@@ -124,10 +164,10 @@ class Disk with EquatableMixin {
path: path,
fsTyp: fstype,
mount: mountpoint,
usedPercent: usedPercent,
used: used,
size: size,
avail: avail,
usedPercent: fsFields.usedPercent,
used: fsFields.used,
size: fsFields.size,
avail: fsFields.avail,
name: name,
kname: kname,
uuid: uuid,
@@ -155,20 +195,7 @@ class Disk with EquatableMixin {
// 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 fsFields = _parseFilesystemFields(device);
final name = device['name']?.toString();
final kname = device['kname']?.toString();
final uuid = device['uuid']?.toString();
@@ -177,10 +204,10 @@ class Disk with EquatableMixin {
path: path,
fsTyp: fstype,
mount: mount,
usedPercent: usedPercent,
used: used,
size: size,
avail: avail,
usedPercent: fsFields.usedPercent,
used: fsFields.used,
size: fsFields.size,
avail: fsFields.avail,
name: name,
kname: kname,
uuid: uuid,

View File

@@ -81,6 +81,138 @@ void main() {
expect(usage.usedPercent, 50);
// This would use the "unknown" fallback for kname
});
test('parse df -k output (fallback mode)', () {
final disks = Disk.parse(_dfOutput);
expect(disks, isNotEmpty);
expect(disks.length, 3); // Should find 3 valid filesystems: udev, /dev/vda3, /dev/vda2
// Verify root filesystem
final rootFs = disks.firstWhere((disk) => disk.mount == '/');
expect(rootFs.path, '/dev/vda3');
expect(rootFs.usedPercent, 47);
expect(rootFs.size, BigInt.from(40910528 ~/ 1024)); // df -k output divided by 1024 = MB
expect(rootFs.used, BigInt.from(18067948 ~/ 1024));
expect(rootFs.avail, BigInt.from(20951380 ~/ 1024));
// Verify boot/efi filesystem
final efiFs = disks.firstWhere((disk) => disk.mount == '/boot/efi');
expect(efiFs.path, '/dev/vda2');
expect(efiFs.usedPercent, 7);
expect(efiFs.size, BigInt.from(192559 ~/ 1024));
// Verify udev filesystem is included (virtual filesystem)
final udevFs = disks.firstWhere((disk) => disk.path == 'udev');
expect(udevFs.mount, '/dev');
expect(udevFs.usedPercent, 0);
expect(udevFs.size, BigInt.from(864088 ~/ 1024));
});
test('handle empty input gracefully', () {
final disks = Disk.parse('');
expect(disks, isEmpty);
});
test('handle whitespace-only input', () {
final disks = Disk.parse(' \n\t \r\n ');
expect(disks, isEmpty);
});
test('handle JSON with null filesystem fields', () {
final disks = Disk.parse(_jsonWithNullFields);
expect(disks, isNotEmpty);
// Should handle null filesystem fields gracefully
final disk = disks.firstWhere((disk) => disk.mount == '/');
expect(disk.size, BigInt.zero);
expect(disk.used, BigInt.zero);
expect(disk.avail, BigInt.zero);
expect(disk.usedPercent, 0);
});
test('handle JSON with string "null" values', () {
final disks = Disk.parse(_jsonWithStringNulls);
expect(disks, isNotEmpty);
// Should handle string "null" filesystem fields gracefully
final disk = disks.firstWhere((disk) => disk.mount == '/');
expect(disk.size, BigInt.zero);
expect(disk.used, BigInt.zero);
expect(disk.avail, BigInt.zero);
expect(disk.usedPercent, 0);
});
test('handle JSON with empty string values', () {
final disks = Disk.parse(_jsonWithEmptyStrings);
expect(disks, isNotEmpty);
// Should handle empty string filesystem fields gracefully
final disk = disks.firstWhere((disk) => disk.mount == '/');
expect(disk.size, BigInt.zero);
expect(disk.used, BigInt.zero);
expect(disk.avail, BigInt.zero);
expect(disk.usedPercent, 0);
});
test('handle JSON with invalid percentage format', () {
final disks = Disk.parse(_jsonWithInvalidPercent);
expect(disks, isNotEmpty);
// Should handle invalid percentage gracefully
final disk = disks.firstWhere((disk) => disk.mount == '/');
expect(disk.usedPercent, 0);
});
test('handle JSON with malformed numbers', () {
final disks = Disk.parse(_jsonWithMalformedNumbers);
expect(disks, isNotEmpty);
// Should handle malformed numbers gracefully
final disk = disks.firstWhere((disk) => disk.mount == '/');
expect(disk.size, BigInt.zero);
expect(disk.used, BigInt.zero);
expect(disk.avail, BigInt.zero);
});
test('handle JSON parsing errors gracefully', () {
final disks = Disk.parse(_malformedJson);
expect(disks, isEmpty); // Should fallback to legacy method, which also fails
});
test('handle df output with missing fields', () {
final disks = Disk.parse(_dfWithMissingFields);
expect(disks, isNotEmpty);
// Should handle missing fields gracefully
final disk = disks.firstWhere((disk) => disk.mount == '/');
expect(disk.usedPercent, 47);
});
test('handle df output with inconsistent formatting', () {
final disks = Disk.parse(_dfWithInconsistentFormatting);
expect(disks, isNotEmpty);
// Should handle inconsistent formatting
expect(disks.length, greaterThan(0));
});
test('handle lsblk with success marker', () {
final disks = Disk.parse(_lsblkWithSuccessMarker);
expect(disks, isNotEmpty);
// Should parse JSON and ignore success marker
final rootFs = disks.firstWhere((disk) => disk.mount == '/');
expect(rootFs.fsTyp, 'ext4');
expect(rootFs.usedPercent, 56);
});
test('handle malformed lsblk output fallback', () {
final disks = Disk.parse(_malformedLsblkWithDfFallback);
expect(disks, isNotEmpty);
// Should fallback to df -k parsing when lsblk output is malformed
expect(disks.length, 3);
});
});
}
@@ -278,3 +410,151 @@ overlay 1907116416 5470
v2000pro/pve 1906694784 125440 1906569344 1% /mnt/v2000pro/pve
v2000pro/download 1906569472 128 1906569344 1% /mnt/v2000pro/download''',
];
const _dfOutput = '''
Filesystem 1K-blocks Used Available Use% Mounted on
udev 864088 0 864088 0% /dev
tmpfs 176724 688 176036 1% /run
/dev/vda3 40910528 18067948 20951380 47% /
tmpfs 883612 0 883612 0% /dev/shm
tmpfs 5120 0 5120 0% /run/lock
/dev/vda2 192559 11807 180752 7% /boot/efi
tmpfs 176720 104 176616 1% /run/user/1000
''';
// Test data for edge cases
const _jsonWithNullFields = '''
{
"blockdevices": [
{
"fstype": "ext4",
"mountpoint": "/",
"fssize": null,
"fsused": null,
"fsavail": null,
"fsuse%": null,
"path": "/dev/sda1"
}
]
}
''';
const _jsonWithStringNulls = '''
{
"blockdevices": [
{
"fstype": "ext4",
"mountpoint": "/",
"fssize": "null",
"fsused": "null",
"fsavail": "null",
"fsuse%": "null",
"path": "/dev/sda1"
}
]
}
''';
const _jsonWithEmptyStrings = '''
{
"blockdevices": [
{
"fstype": "ext4",
"mountpoint": "/",
"fssize": "",
"fsused": "",
"fsavail": "",
"fsuse%": "",
"path": "/dev/sda1"
}
]
}
''';
const _jsonWithInvalidPercent = '''
{
"blockdevices": [
{
"fstype": "ext4",
"mountpoint": "/",
"fssize": "1000000",
"fsused": "500000",
"fsavail": "500000",
"fsuse%": "invalid_percent",
"path": "/dev/sda1"
}
]
}
''';
const _jsonWithMalformedNumbers = '''
{
"blockdevices": [
{
"fstype": "ext4",
"mountpoint": "/",
"fssize": "not_a_number",
"fsused": "invalid",
"fsavail": "broken",
"fsuse%": "50%",
"path": "/dev/sda1"
}
]
}
''';
const _malformedJson = '''
{
"blockdevices": [
{
"fstype": "ext4",
"mountpoint": "/",
"fssize": "1000000",
"fsused": "500000",
"fsavail": "500000",
"fsuse%": "50%",
"path": "/dev/sda1"
}
]
// Missing closing brace and malformed structure
''';
const _dfWithMissingFields = '''
Filesystem 1K-blocks Used Available Use% Mounted on
/dev/vda3 40910528 18067948 20951380 47% /
''';
const _dfWithInconsistentFormatting = '''
Filesystem 1K-blocks Used Available Use% Mounted on
/dev/sda1 1000000 500000 500000 50% /
/dev/sda2 2000000 1000000 1000000 50% /home
udev 864088 0 864088 0% /dev
''';
const _lsblkWithSuccessMarker = '''
{
"blockdevices": [
{
"fstype": "ext4",
"mountpoint": "/",
"fssize": 982141468672,
"fsused": 552718364672,
"fsavail": 379457622016,
"fsuse%": "56%",
"path": "/dev/sda1"
}
]
}
LSBLK_SUCCESS
''';
const _malformedLsblkWithDfFallback = '''
Filesystem 1K-blocks Used Available Use% Mounted on
udev 864088 0 864088 0% /dev
tmpfs 176724 688 176036 1% /run
/dev/vda3 40910528 18067948 20951380 47% /
tmpfs 883612 0 883612 0% /dev/shm
tmpfs 5120 0 5120 0% /run/lock
/dev/vda2 192559 11807 180752 7% /boot/efi
tmpfs 176720 104 176616 1% /run/user/1000
''';