Files
flutter_server_box/lib/data/provider/pve.dart
lollipopkit🏳️‍⚧️ 3a615449e3 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>
2025-08-08 16:56:36 +08:00

218 lines
6.0 KiB
Dart

import 'dart:async';
import 'dart:io';
import 'package:computer/computer.dart';
import 'package:dartssh2/dartssh2.dart';
import 'package:dio/dio.dart';
import 'package:dio/io.dart';
import 'package:fl_lib/fl_lib.dart';
import 'package:flutter/material.dart';
import 'package:server_box/core/extension/context/locale.dart';
import 'package:server_box/data/model/app/error.dart';
import 'package:server_box/data/model/server/pve.dart';
import 'package:server_box/data/model/server/server_private_info.dart';
typedef PveCtrlFunc = Future<bool> Function(String node, String id);
final class PveProvider extends ChangeNotifier {
final Spi spi;
late String addr;
late final SSHClient _client;
late final ServerSocket _serverSocket;
final List<SSHForwardChannel> _forwards = [];
int _localPort = 0;
PveProvider({required this.spi}) {
final client = spi.server?.value.client;
if (client == null) {
throw Exception('Server client is null');
}
_client = client;
final addr = spi.custom?.pveAddr;
if (addr == null) {
err.value = 'PVE address is null';
return;
}
this.addr = addr;
_init();
}
final err = ValueNotifier<String?>(null);
final connected = Completer<void>();
late final _ignoreCert = spi.custom?.pveIgnoreCert ?? false;
late final session = Dio()
..httpClientAdapter = IOHttpClientAdapter(
createHttpClient: () {
final client = HttpClient();
client.connectionFactory = cf;
if (_ignoreCert) {
client.badCertificateCallback = (_, _, _) => true;
}
return client;
},
validateCertificate: _ignoreCert ? (_, _, _) => true : null,
);
final data = ValueNotifier<PveRes?>(null);
bool get onlyOneNode => data.value?.nodes.length == 1;
String? release;
bool isBusy = false;
Future<void> _init() async {
try {
await _forward();
await _login();
await _getRelease();
} on PveErr {
err.value = l10n.pveLoginFailed;
} catch (e, s) {
Loggers.app.warning('PVE init failed', e, s);
err.value = e.toString();
} finally {
connected.complete();
}
}
Future<void> _forward() async {
final url = Uri.parse(addr);
if (_localPort == 0) {
_serverSocket = await ServerSocket.bind('localhost', 0);
_localPort = _serverSocket.port;
_serverSocket.listen((socket) async {
final forward = await _client.forwardLocal(url.host, url.port);
_forwards.add(forward);
forward.stream.cast<List<int>>().pipe(socket);
socket.cast<List<int>>().pipe(forward.sink);
});
final newUrl = Uri.parse(
addr,
).replace(host: 'localhost', port: _localPort).toString();
debugPrint('Forwarding $newUrl to $addr');
}
}
Future<ConnectionTask<Socket>> cf(
Uri url,
String? proxyHost,
int? proxyPort,
) async {
/* final serverSocket = await ServerSocket.bind(InternetAddress.anyIPv4, 0);
final _localPort = serverSocket.port;
serverSocket.listen((socket) async {
final forward = await _client.forwardLocal(url.host, url.port);
forwards.add(forward);
forward.stream.cast<List<int>>().pipe(socket);
socket.cast<List<int>>().pipe(forward.sink);
});*/
if (url.isScheme('https')) {
return SecureSocket.startConnect(
'localhost',
_localPort,
onBadCertificate: (_) => true,
);
} else {
return Socket.startConnect('localhost', _localPort);
}
}
Future<void> _login() async {
final resp = await session.post(
'$addr/api2/extjs/access/ticket',
data: {
'username': spi.user,
'password': spi.pwd,
'realm': 'pam',
'new-format': '1',
},
options: Options(
headers: {HttpHeaders.contentTypeHeader: Headers.jsonContentType},
),
);
try {
final ticket = resp.data['data']['ticket'];
session.options.headers['CSRFPreventionToken'] =
resp.data['data']['CSRFPreventionToken'];
session.options.headers['Cookie'] = 'PVEAuthCookie=$ticket';
} catch (e) {
throw PveErr(type: PveErrType.loginFailed, message: e.toString());
}
}
/// Returns true if the PVE version is 8.0 or later
Future<void> _getRelease() async {
final resp = await session.get('$addr/api2/extjs/version');
final version = resp.data['data']['release'] as String?;
if (version != null) {
release = version;
}
}
Future<void> list() async {
await connected.future;
if (isBusy) return;
isBusy = true;
try {
final resp = await session.get('$addr/api2/json/cluster/resources');
final res = resp.data['data'] as List;
final result = await Computer.shared.start(PveRes.parse, (
res,
data.value,
));
data.value = result;
} catch (e) {
Loggers.app.warning('PVE list failed', e);
err.value = e.toString();
} finally {
isBusy = false;
}
}
Future<bool> reboot(String node, String id) async {
await connected.future;
final resp = await session.post(
'$addr/api2/json/nodes/$node/$id/status/reboot',
);
return _isCtrlSuc(resp);
}
Future<bool> start(String node, String id) async {
await connected.future;
final resp = await session.post(
'$addr/api2/json/nodes/$node/$id/status/start',
);
return _isCtrlSuc(resp);
}
Future<bool> stop(String node, String id) async {
await connected.future;
final resp = await session.post(
'$addr/api2/json/nodes/$node/$id/status/stop',
);
return _isCtrlSuc(resp);
}
Future<bool> shutdown(String node, String id) async {
await connected.future;
final resp = await session.post(
'$addr/api2/json/nodes/$node/$id/status/shutdown',
);
return _isCtrlSuc(resp);
}
bool _isCtrlSuc(Response resp) {
return resp.statusCode == 200;
}
@override
Future<void> dispose() async {
super.dispose();
await _serverSocket.close();
for (final forward in _forwards) {
forward.close();
}
}
}