mirror of
https://github.com/lollipopkit/flutter_server_box.git
synced 2025-12-16 23:04:22 +01:00
fix: fallback to df on incompatible system (#880)
This commit is contained in:
@@ -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'),
|
||||
|
||||
@@ -44,22 +44,49 @@ class Disk with EquatableMixin {
|
||||
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'] ?? [];
|
||||
|
||||
if (raw.isEmpty) {
|
||||
dprint('Empty disk info data received');
|
||||
return list;
|
||||
}
|
||||
|
||||
for (final device in blockdevices) {
|
||||
// Process each device
|
||||
_processTopLevelDevice(device, list);
|
||||
try {
|
||||
// Check if we have lsblk JSON output with success marker
|
||||
if (raw.startsWith('{')) {
|
||||
// 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);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
} else {
|
||||
// Fallback to the old parsing method in case of non-JSON output
|
||||
}
|
||||
|
||||
// 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,
|
||||
|
||||
@@ -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
|
||||
''';
|
||||
|
||||
Reference in New Issue
Block a user