fix: Unable to obtain Windows server information (#963)

* fix: FormatException: Unexpected extension byte (at offset 8) error

* fix: PowerShell script error repair, Windows data parsing repair

* fix: Unable to obtain network card information

* fix: Unable to obtain system startup time

* fix conversation as resolved.
This commit is contained in:
lxdklp
2025-11-22 19:17:40 +08:00
committed by GitHub
parent ca4e65d7a5
commit 75d1a59e77
12 changed files with 455 additions and 215 deletions

View File

@@ -6,7 +6,7 @@ import 'package:server_box/data/res/status.dart';
/// Capacity of the FIFO queue
const _kCap = 30;
class Cpus extends TimeSeq<List<SingleCpuCore>> {
class Cpus extends TimeSeq<SingleCpuCore> {
Cpus(super.init1, super.init2);
final Map<String, int> brand = {};
@@ -14,13 +14,20 @@ class Cpus extends TimeSeq<List<SingleCpuCore>> {
@override
void onUpdate() {
_coresCount = now.length;
if (pre.isEmpty || now.isEmpty || pre.length != now.length) {
_totalDelta = 0;
_user = 0;
_sys = 0;
_iowait = 0;
_idle = 0;
return;
}
_totalDelta = now[0].total - pre[0].total;
_user = _getUser();
_sys = _getSys();
_iowait = _getIowait();
_idle = _getIdle();
_updateSpots();
//_updateRange();
}
double usedPercent({int coreIdx = 0}) {

View File

@@ -280,7 +280,7 @@ class Disk with EquatableMixin {
];
}
class DiskIO extends TimeSeq<List<DiskIOPiece>> {
class DiskIO extends TimeSeq<DiskIOPiece> {
DiskIO(super.init1, super.init2);
@override

View File

@@ -18,7 +18,7 @@ class NetSpeedPart extends TimeSeqIface<NetSpeedPart> {
typedef CachedNetVals = ({String sizeIn, String sizeOut, String speedIn, String speedOut});
class NetSpeed extends TimeSeq<List<NetSpeedPart>> {
class NetSpeed extends TimeSeq<NetSpeedPart> {
NetSpeed(super.init1, super.init2);
@override

View File

@@ -378,18 +378,27 @@ void _parseWindowsCpuData(ServerStatusUpdateReq req, Map<String, String> parsedO
// Windows CPU parsing - JSON format from PowerShell
final cpuRaw = WindowsStatusCmdType.cpu.findInMap(parsedOutput);
if (cpuRaw.isNotEmpty && cpuRaw != 'null' && !cpuRaw.contains('error') && !cpuRaw.contains('Exception')) {
final cpus = WindowsParser.parseCpu(cpuRaw, req.ss);
if (cpus.isNotEmpty) {
req.ss.cpu.update(cpus);
final cpuResult = WindowsParser.parseCpu(cpuRaw, req.ss);
if (cpuResult.cores.isNotEmpty) {
req.ss.cpu.update(cpuResult.cores);
final brandRaw = WindowsStatusCmdType.cpuBrand.findInMap(parsedOutput);
if (brandRaw.isNotEmpty && brandRaw != 'null') {
req.ss.cpu.brand.clear();
final brandLines = brandRaw.trim().split('\n');
final uniqueBrands = <String>{};
for (final line in brandLines) {
final trimmedLine = line.trim();
if (trimmedLine.isNotEmpty) {
uniqueBrands.add(trimmedLine);
}
}
if (uniqueBrands.isNotEmpty) {
final brandName = uniqueBrands.first;
req.ss.cpu.brand[brandName] = cpuResult.coreCount;
}
}
}
}
// Windows CPU brand parsing
final brandRaw = WindowsStatusCmdType.cpuBrand.findInMap(parsedOutput);
if (brandRaw.isNotEmpty && brandRaw != 'null') {
req.ss.cpu.brand.clear();
req.ss.cpu.brand[brandRaw.trim()] = 1;
}
} catch (e, s) {
Loggers.app.warning('Windows CPU parsing failed: $e', s);
}
@@ -427,8 +436,11 @@ void _parseWindowsDiskData(ServerStatusUpdateReq req, Map<String, String> parsed
/// Parse Windows uptime data
void _parseWindowsUptimeData(ServerStatusUpdateReq req, Map<String, String> parsedOutput) {
try {
final uptime = WindowsParser.parseUpTime(WindowsStatusCmdType.uptime.findInMap(parsedOutput));
if (uptime != null) {
final uptimeRaw = WindowsStatusCmdType.uptime.findInMap(parsedOutput);
if (uptimeRaw.isNotEmpty && uptimeRaw != 'null') {
// PowerShell now returns pre-formatted uptime string (e.g., "28 days, 5:00" or "5:00")
// No parsing needed - use it directly
final uptime = uptimeRaw.trim();
req.ss.more[StatusCmdType.uptime] = uptime;
}
} catch (e, s) {
@@ -541,38 +553,36 @@ List<NetSpeedPart> _parseWindowsNetwork(String raw, int currentTime) {
final dynamic jsonData = json.decode(raw);
final List<NetSpeedPart> netParts = [];
// PowerShell Get-Counter returns a structure with CounterSamples
if (jsonData is Map && jsonData.containsKey('CounterSamples')) {
final samples = jsonData['CounterSamples'] as List?;
if (samples != null && samples.length >= 2) {
// We need 2 samples to calculate speed (interval between them)
final Map<String, double> interfaceRx = {};
final Map<String, double> interfaceTx = {};
for (final sample in samples) {
final path = sample['Path']?.toString() ?? '';
final cookedValue = sample['CookedValue'] as num? ?? 0;
if (path.contains('Bytes Received/sec')) {
final interfaceName = _extractInterfaceName(path);
if (interfaceName.isNotEmpty) {
interfaceRx[interfaceName] = cookedValue.toDouble();
}
} else if (path.contains('Bytes Sent/sec')) {
final interfaceName = _extractInterfaceName(path);
if (interfaceName.isNotEmpty) {
interfaceTx[interfaceName] = cookedValue.toDouble();
}
}
}
// Create NetSpeedPart for each interface
for (final interfaceName in interfaceRx.keys) {
final rx = interfaceRx[interfaceName] ?? 0;
final tx = interfaceTx[interfaceName] ?? 0;
if (jsonData is List && jsonData.length >= 2) {
var sample1 = jsonData[jsonData.length - 2];
var sample2 = jsonData[jsonData.length - 1];
if (sample1 is Map && sample1.containsKey('value')) {
sample1 = sample1['value'];
}
if (sample2 is Map && sample2.containsKey('value')) {
sample2 = sample2['value'];
}
if (sample1 is List && sample2 is List && sample1.length == sample2.length) {
for (int i = 0; i < sample1.length; i++) {
final s1 = sample1[i];
final s2 = sample2[i];
final name = s1['Name']?.toString() ?? '';
if (name.isEmpty || name == '_Total') continue;
final rx1 = (s1['BytesReceivedPersec'] as num?)?.toDouble() ?? 0;
final rx2 = (s2['BytesReceivedPersec'] as num?)?.toDouble() ?? 0;
final tx1 = (s1['BytesSentPersec'] as num?)?.toDouble() ?? 0;
final tx2 = (s2['BytesSentPersec'] as num?)?.toDouble() ?? 0;
final time1 = (s1['Timestamp_Sys100NS'] as num?)?.toDouble() ?? 0;
final time2 = (s2['Timestamp_Sys100NS'] as num?)?.toDouble() ?? 0;
final timeDelta = (time2 - time1) / 10000000;
if (timeDelta <= 0) continue;
final rxDelta = rx2 - rx1;
final txDelta = tx2 - tx1;
if (rxDelta < 0 || txDelta < 0) continue;
final rxSpeed = rxDelta / timeDelta;
final txSpeed = txDelta / timeDelta;
netParts.add(
NetSpeedPart(interfaceName, BigInt.from(rx.toInt()), BigInt.from(tx.toInt()), currentTime),
NetSpeedPart(name, BigInt.from(rxSpeed.toInt()), BigInt.from(txSpeed.toInt()), currentTime),
);
}
}
@@ -584,53 +594,45 @@ List<NetSpeedPart> _parseWindowsNetwork(String raw, int currentTime) {
}
}
String _extractInterfaceName(String path) {
// Extract interface name from path like
// "\\Computer\\NetworkInterface(Interface Name)\\..."
final match = RegExp(r'\\NetworkInterface\(([^)]+)\)\\').firstMatch(path);
return match?.group(1) ?? '';
}
List<DiskIOPiece> _parseWindowsDiskIO(String raw, int currentTime) {
try {
final dynamic jsonData = json.decode(raw);
final List<DiskIOPiece> diskParts = [];
// PowerShell Get-Counter returns a structure with CounterSamples
if (jsonData is Map && jsonData.containsKey('CounterSamples')) {
final samples = jsonData['CounterSamples'] as List?;
if (samples != null) {
final Map<String, double> diskReads = {};
final Map<String, double> diskWrites = {};
for (final sample in samples) {
final path = sample['Path']?.toString() ?? '';
final cookedValue = sample['CookedValue'] as num? ?? 0;
if (path.contains('Disk Read Bytes/sec')) {
final diskName = _extractDiskName(path);
if (diskName.isNotEmpty) {
diskReads[diskName] = cookedValue.toDouble();
}
} else if (path.contains('Disk Write Bytes/sec')) {
final diskName = _extractDiskName(path);
if (diskName.isNotEmpty) {
diskWrites[diskName] = cookedValue.toDouble();
}
}
}
// Create DiskIOPiece for each disk - convert bytes to sectors
// (assuming 512 bytes per sector)
for (final diskName in diskReads.keys) {
final readBytes = diskReads[diskName] ?? 0;
final writeBytes = diskWrites[diskName] ?? 0;
final sectorsRead = (readBytes / 512).round();
final sectorsWrite = (writeBytes / 512).round();
if (jsonData is List && jsonData.length >= 2) {
var sample1 = jsonData[jsonData.length - 2];
var sample2 = jsonData[jsonData.length - 1];
if (sample1 is Map && sample1.containsKey('value')) {
sample1 = sample1['value'];
}
if (sample2 is Map && sample2.containsKey('value')) {
sample2 = sample2['value'];
}
if (sample1 is List && sample2 is List && sample1.length == sample2.length) {
for (int i = 0; i < sample1.length; i++) {
final s1 = sample1[i];
final s2 = sample2[i];
final name = s1['Name']?.toString() ?? '';
if (name.isEmpty || name == '_Total') continue;
final read1 = (s1['DiskReadBytesPersec'] as num?)?.toDouble() ?? 0;
final read2 = (s2['DiskReadBytesPersec'] as num?)?.toDouble() ?? 0;
final write1 = (s1['DiskWriteBytesPersec'] as num?)?.toDouble() ?? 0;
final write2 = (s2['DiskWriteBytesPersec'] as num?)?.toDouble() ?? 0;
final time1 = (s1['Timestamp_Sys100NS'] as num?)?.toDouble() ?? 0;
final time2 = (s2['Timestamp_Sys100NS'] as num?)?.toDouble() ?? 0;
final timeDelta = (time2 - time1) / 10000000;
if (timeDelta <= 0) continue;
final readDelta = read2 - read1;
final writeDelta = write2 - write1;
if (readDelta < 0 || writeDelta < 0) continue;
final readSpeed = readDelta / timeDelta;
final writeSpeed = writeDelta / timeDelta;
final sectorsRead = (readSpeed / 512).round();
final sectorsWrite = (writeSpeed / 512).round();
diskParts.add(
DiskIOPiece(
dev: diskName,
dev: name,
sectorsRead: sectorsRead,
sectorsWrite: sectorsWrite,
time: currentTime,
@@ -646,13 +648,6 @@ List<DiskIOPiece> _parseWindowsDiskIO(String raw, int currentTime) {
}
}
String _extractDiskName(String path) {
// Extract disk name from path like
// "\\Computer\\PhysicalDisk(Disk Name)\\..."
final match = RegExp(r'\\PhysicalDisk\(([^)]+)\)\\').firstMatch(path);
return match?.group(1) ?? '';
}
void _parseWindowsTemperatures(Temperatures temps, String raw) {
try {
// Handle error output

View File

@@ -37,27 +37,39 @@ class Fifo<T> extends ListBase<T> {
}
}
abstract class TimeSeq<T extends List<TimeSeqIface>> extends Fifo<T> {
abstract class TimeSeq<T extends TimeSeqIface<T>> extends Fifo<List<T>> {
/// Due to the design, at least two elements are required, otherwise [pre] /
/// [now] will throw.
TimeSeq(T init1, T init2, {super.capacity}) : super(list: [init1, init2]);
TimeSeq(List<T> init1, List<T> init2, {super.capacity}) : super(list: [init1, init2]);
T get pre {
List<T> get pre {
return _list[length - 2];
}
T get now {
List<T> get now {
return _list[length - 1];
}
void onUpdate();
void update(T new_) {
void update(List<T> new_) {
add(new_);
if (pre.length != now.length) {
pre.removeWhere((e) => now.any((el) => e.same(el)));
pre.addAll(now.where((e) => pre.every((el) => !e.same(el))));
final previous = pre.toList(growable: false);
final remaining = previous.toList(growable: true);
final aligned = <T>[];
for (final current in now) {
final matchIndex = remaining.indexWhere((item) => item.same(current));
if (matchIndex >= 0) {
aligned.add(remaining.removeAt(matchIndex));
} else {
aligned.add(current);
}
}
_list[length - 2] = aligned;
}
onUpdate();

View File

@@ -7,6 +7,13 @@ import 'package:server_box/data/model/server/disk.dart';
import 'package:server_box/data/model/server/memory.dart';
import 'package:server_box/data/model/server/server.dart';
/// Windows CPU parse result
class WindowsCpuResult {
final List<SingleCpuCore> cores;
final int coreCount;
const WindowsCpuResult(this.cores, this.coreCount);
}
/// Windows-specific status parsing utilities
///
/// This module handles parsing of Windows PowerShell command outputs
@@ -94,30 +101,75 @@ class WindowsParser {
}
/// Parse Windows CPU information from PowerShell output
static List<SingleCpuCore> parseCpu(String raw, ServerStatus serverStatus) {
/// Returns WindowsCpuResult containing CPU cores and total core count
static WindowsCpuResult parseCpu(String raw, ServerStatus serverStatus) {
try {
final dynamic jsonData = json.decode(raw);
final List<SingleCpuCore> cpus = [];
int totalCoreCount = 1;
if (jsonData is List) {
for (int i = 0; i < jsonData.length; i++) {
final cpu = jsonData[i];
final loadPercentage = cpu['LoadPercentage'] ?? 0;
final usage = loadPercentage as int;
// Multiple physical processors
totalCoreCount = 0; // Reset to sum up
var logicalProcessorOffset = 0;
final prevCpus = serverStatus.cpu.now;
for (int procIdx = 0; procIdx < jsonData.length; procIdx++) {
final processor = jsonData[procIdx];
final loadPercentage = (processor['LoadPercentage'] as num?) ?? 0;
final numberOfCores = (processor['NumberOfCores'] as int?) ?? 1;
final numberOfLogicalProcessors = (processor['NumberOfLogicalProcessors'] as int?) ?? numberOfCores;
totalCoreCount += numberOfCores;
final usage = loadPercentage.toInt();
final idle = 100 - usage;
// Get previous CPU data to calculate cumulative values
final prevCpus = serverStatus.cpu.now;
final prevCpu = i < prevCpus.length ? prevCpus[i] : null;
// Create a SingleCpuCore entry for each logical processor
// Windows only reports overall CPU load, so we distribute it evenly
for (int i = 0; i < numberOfLogicalProcessors; i++) {
final coreId = logicalProcessorOffset + i;
// Skip summary entry at index 0 when looking up previous samples
final prevIndex = coreId + 1;
final prevCpu = prevIndex < prevCpus.length ? prevCpus[prevIndex] : null;
// LIMITATION: Windows CPU counters approach
// PowerShell provides LoadPercentage as instantaneous percentage, not cumulative time.
// We simulate cumulative counters by adding current percentages to previous totals.
// This approach has limitations:
// 1. Not as accurate as true cumulative time counters (Linux /proc/stat)
// 2. May drift over time with variable polling intervals
// 3. Results depend on consistent polling frequency
// However, this allows compatibility with existing delta-based CPU calculation logic.
// LIMITATION: Windows CPU counters approach
// PowerShell provides LoadPercentage as instantaneous percentage, not cumulative time.
// We simulate cumulative counters by adding current percentages to previous totals.
// Additionally, Windows only provides overall CPU load, not per-core load.
// We distribute the load evenly across all logical processors.
final newUser = (prevCpu?.user ?? 0) + usage;
final newIdle = (prevCpu?.idle ?? 0) + idle;
cpus.add(
SingleCpuCore(
'cpu$coreId',
newUser, // cumulative user time
0, // sys (not available)
0, // nice (not available)
newIdle, // cumulative idle time
0, // iowait (not available)
0, // irq (not available)
0, // softirq (not available)
),
);
}
logicalProcessorOffset += numberOfLogicalProcessors;
}
} else if (jsonData is Map) {
// Single physical processor
final loadPercentage = (jsonData['LoadPercentage'] as num?) ?? 0;
final numberOfCores = (jsonData['NumberOfCores'] as int?) ?? 1;
final numberOfLogicalProcessors = (jsonData['NumberOfLogicalProcessors'] as int?) ?? numberOfCores;
totalCoreCount = numberOfCores;
final usage = loadPercentage.toInt();
final idle = 100 - usage;
// Create a SingleCpuCore entry for each logical processor
final prevCpus = serverStatus.cpu.now;
for (int i = 0; i < numberOfLogicalProcessors; i++) {
// Skip summary entry at index 0 when looking up previous samples
final prevIndex = i + 1;
final prevCpu = prevIndex < prevCpus.length ? prevCpus[prevIndex] : null;
// LIMITATION: See comment above for Windows CPU counter limitations
final newUser = (prevCpu?.user ?? 0) + usage;
final newIdle = (prevCpu?.idle ?? 0) + idle;
@@ -125,46 +177,43 @@ class WindowsParser {
SingleCpuCore(
'cpu$i',
newUser, // cumulative user time
0, // sys (not available)
0, // nice (not available)
0, // sys
0, // nice
newIdle, // cumulative idle time
0, // iowait (not available)
0, // irq (not available)
0, // softirq (not available)
0, // iowait
0, // irq
0, // softirq
),
);
}
} else if (jsonData is Map) {
// Single CPU core
final loadPercentage = jsonData['LoadPercentage'] ?? 0;
final usage = loadPercentage as int;
final idle = 100 - usage;
// Get previous CPU data to calculate cumulative values
final prevCpus = serverStatus.cpu.now;
final prevCpu = prevCpus.isNotEmpty ? prevCpus[0] : null;
// LIMITATION: See comment above for Windows CPU counter limitations
final newUser = (prevCpu?.user ?? 0) + usage;
final newIdle = (prevCpu?.idle ?? 0) + idle;
cpus.add(
SingleCpuCore(
'cpu0',
newUser, // cumulative user time
0, // sys
0, // nice
newIdle, // cumulative idle time
0, // iowait
0, // irq
0, // softirq
),
);
}
return cpus;
} catch (e) {
return [];
// Add a summary entry at the beginning (like Linux 'cpu' line)
// This is the aggregate of all logical processors
if (cpus.isNotEmpty) {
int totalUser = 0;
int totalIdle = 0;
for (final core in cpus) {
totalUser += core.user;
totalIdle += core.idle;
}
// Insert at the beginning with ID 'cpu' (matching Linux format)
cpus.insert(0, SingleCpuCore(
'cpu', // Summary entry, like Linux
totalUser,
0,
0,
totalIdle,
0,
0,
0,
));
}
return WindowsCpuResult(cpus, totalCoreCount);
} catch (e, s) {
Loggers.app.warning('Windows CPU parsing failed: $e', s);
return WindowsCpuResult([], 1);
}
}