feat: Windows compatibility (#836)

* feat: win compatibility

* fix

* fix: uptime parse

* opt.: linux uptime accuracy

* fix: windows temperature fetching

* opt.

* opt.: powershell exec

* refactor: address PR review feedback and improve code quality

### Major Improvements:
- **Refactored Windows status parsing**: Broke down large `_getWindowsStatus` method into 13 smaller, focused helper methods for better maintainability and readability
- **Extracted system detection logic**: Created dedicated `SystemDetector` helper class to separate OS detection concerns from ServerProvider
- **Improved concurrency handling**: Implemented proper synchronization for server updates using Future-based locks to prevent race conditions

### Bug Fixes:
- **Fixed CPU percentage parsing**: Removed incorrect '*100' multiplication in BSD CPU parsing (values were already percentages)
- **Enhanced memory parsing**: Added validation and error handling to BSD memory fallback parsing with proper logging
- **Improved uptime parsing**: Added support for multiple Windows date formats and robust error handling with validation
- **Fixed division by zero**: Added safety checks in Swap.usedPercent getter

### Code Quality Enhancements:
- **Added comprehensive documentation**: Documented Windows CPU counter limitations and approach
- **Strengthened error handling**: Added detailed logging and validation throughout parsing methods
- **Improved robustness**: Enhanced BSD CPU parsing with percentage validation and warnings
- **Better separation of concerns**: Each parsing method now has single responsibility

### Files Changed:
- `lib/data/helper/system_detector.dart` (new): System detection helper
- `lib/data/model/server/cpu.dart`: Fixed percentage parsing and added validation
- `lib/data/model/server/memory.dart`: Enhanced fallback parsing and division-by-zero protection
- `lib/data/model/server/server_status_update_req.dart`: Refactored into 13 focused parsing methods
- `lib/data/provider/server.dart`: Improved synchronization and extracted system detection

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor: parse & shell fn struct

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
lollipopkit🏳️‍⚧️
2025-08-08 16:56:36 +08:00
committed by GitHub
parent 46a12bc844
commit 3a615449e3
103 changed files with 9591 additions and 1906 deletions

View File

@@ -12,8 +12,7 @@ import 'package:server_box/data/model/container/ps.dart';
import 'package:server_box/data/model/container/type.dart';
import 'package:server_box/data/res/store.dart';
final _dockerNotFound =
RegExp(r"command not found|Unknown command|Command '\w+' not found");
final _dockerNotFound = RegExp(r"command not found|Unknown command|Command '\w+' not found");
class ContainerProvider extends ChangeNotifier {
final SSHClient? client;
@@ -90,11 +89,7 @@ class ContainerProvider extends ChangeNotifier {
final includeStats = Stores.setting.containerParseStat.fetch();
var raw = '';
final cmd = _wrap(ContainerCmdType.execAll(
type,
sudo: sudo,
includeStats: includeStats,
));
final cmd = _wrap(ContainerCmdType.execAll(type, sudo: sudo, includeStats: includeStats));
final code = await client?.execWithPwd(
cmd,
context: context,
@@ -130,10 +125,7 @@ class ContainerProvider extends ChangeNotifier {
try {
version = json.decode(verRaw)['Client']['Version'];
} catch (e, trace) {
error = ContainerErr(
type: ContainerErrType.invalidVersion,
message: '$e',
);
error = ContainerErr(type: ContainerErrType.invalidVersion, message: '$e');
Loggers.app.warning('Container version failed', e, trace);
} finally {
notifyListeners();
@@ -150,10 +142,7 @@ class ContainerProvider extends ChangeNotifier {
lines.removeWhere((element) => element.isEmpty);
items = lines.map((e) => ContainerPs.fromRaw(e, type)).toList();
} catch (e, trace) {
error = ContainerErr(
type: ContainerErrType.parsePs,
message: '$e',
);
error = ContainerErr(type: ContainerErrType.parsePs, message: '$e');
Loggers.app.warning('Container ps failed', e, trace);
} finally {
notifyListeners();
@@ -173,10 +162,7 @@ class ContainerProvider extends ChangeNotifier {
images = lines.map((e) => ContainerImg.fromRawJson(e, type)).toList();
}
} catch (e, trace) {
error = ContainerErr(
type: ContainerErrType.parseImages,
message: '$e',
);
error = ContainerErr(type: ContainerErrType.parseImages, message: '$e');
Loggers.app.warning('Container images failed', e, trace);
} finally {
notifyListeners();
@@ -199,10 +185,7 @@ class ContainerProvider extends ChangeNotifier {
item.parseStats(statsLine);
}
} catch (e, trace) {
error = ContainerErr(
type: ContainerErrType.parseStats,
message: '$e',
);
error = ContainerErr(type: ContainerErrType.parseStats, message: '$e');
Loggers.app.warning('Parse docker stats: $statsRaw', e, trace);
} finally {
notifyListeners();
@@ -261,10 +244,7 @@ class ContainerProvider extends ChangeNotifier {
notifyListeners();
if (code != 0) {
return ContainerErr(
type: ContainerErrType.unknown,
message: errs.join('\n').trim(),
);
return ContainerErr(type: ContainerErrType.unknown, message: errs.join('\n').trim());
}
if (autoRefresh) await refresh();
return null;
@@ -288,40 +268,32 @@ enum ContainerCmdType {
version,
ps,
stats,
images,
images
// No specific commands needed for prune actions as they are simple
// and don't require splitting output with ShellFunc.seperator
;
String exec(
ContainerType type, {
bool sudo = false,
bool includeStats = false,
}) {
String exec(ContainerType type, {bool sudo = false, bool includeStats = false}) {
final prefix = sudo ? 'sudo -S ${type.name}' : type.name;
return switch (this) {
ContainerCmdType.version => '$prefix version $_jsonFmt',
ContainerCmdType.ps => switch (type) {
/// TODO: Rollback to json format when permformance recovers.
/// Use [_jsonFmt] in Docker will cause the operation to slow down.
ContainerType.docker => '$prefix ps -a --format "table {{printf \\"'
/// TODO: Rollback to json format when permformance recovers.
/// Use [_jsonFmt] in Docker will cause the operation to slow down.
ContainerType.docker =>
'$prefix ps -a --format "table {{printf \\"'
'%-15.15s '
'%-30.30s '
'${"%-50.50s " * 2}\\"'
' .ID .Status .Names .Image}}"',
ContainerType.podman => '$prefix ps -a $_jsonFmt',
},
ContainerCmdType.stats =>
includeStats ? '$prefix stats --no-stream $_jsonFmt' : 'echo PASS',
ContainerType.podman => '$prefix ps -a $_jsonFmt',
},
ContainerCmdType.stats => includeStats ? '$prefix stats --no-stream $_jsonFmt' : 'echo PASS',
ContainerCmdType.images => '$prefix image ls $_jsonFmt',
};
}
static String execAll(
ContainerType type, {
bool sudo = false,
bool includeStats = false,
}) {
static String execAll(ContainerType type, {bool sudo = false, bool includeStats = false}) {
return ContainerCmdType.values
.map((e) => e.exec(type, sudo: sudo, includeStats: includeStats))
.join('\necho ${ShellFunc.seperator}\n');