mirror of
https://github.com/lollipopkit/flutter_server_box.git
synced 2025-12-17 23:34:24 +01:00
* 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>
190 lines
5.4 KiB
Dart
190 lines
5.4 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:fl_lib/fl_lib.dart';
|
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
|
import 'package:server_box/data/model/server/server_private_info.dart';
|
|
import 'package:xterm/core.dart';
|
|
|
|
part 'snippet.g.dart';
|
|
part 'snippet.freezed.dart';
|
|
|
|
@freezed
|
|
abstract class Snippet with _$Snippet {
|
|
const factory Snippet({
|
|
required String name,
|
|
required String script,
|
|
List<String>? tags,
|
|
String? note,
|
|
|
|
/// List of server id that this snippet should be auto run on
|
|
List<String>? autoRunOn,
|
|
}) = _Snippet;
|
|
|
|
factory Snippet.fromJson(Map<String, dynamic> json) => _$SnippetFromJson(json);
|
|
|
|
static const example = Snippet(
|
|
name: 'example',
|
|
script: 'echo hello',
|
|
tags: ['tag'],
|
|
note: 'note',
|
|
autoRunOn: ['server_id'],
|
|
);
|
|
}
|
|
|
|
extension SnippetX on Snippet {
|
|
static final fmtFinder = RegExp(r'\$\{[^{}]+\}');
|
|
|
|
String fmtWithSpi(Spi spi) {
|
|
return script.replaceAllMapped(fmtFinder, (match) {
|
|
final key = match.group(0);
|
|
final func = fmtArgs[key];
|
|
if (func != null) return func(spi);
|
|
// If not found, return the original content for further processing
|
|
return key ?? '';
|
|
});
|
|
}
|
|
|
|
Future<void> runInTerm(Terminal terminal, Spi spi, {bool autoEnter = false}) async {
|
|
final argsFmted = fmtWithSpi(spi);
|
|
final matches = fmtFinder.allMatches(argsFmted);
|
|
|
|
/// There is no [TerminalKey] in the script
|
|
if (matches.isEmpty) {
|
|
terminal.textInput(argsFmted);
|
|
if (autoEnter) terminal.keyInput(TerminalKey.enter);
|
|
return;
|
|
}
|
|
|
|
// Records all start and end indexes of the matches
|
|
final (starts, ends) = matches.fold((<int>[], <int>[]), (pre, e) {
|
|
pre.$1.add(e.start);
|
|
pre.$2.add(e.end);
|
|
return pre;
|
|
});
|
|
|
|
// Check all indexes, `(idx + 1).start` must >= `idx.end`
|
|
for (var i = 0; i < starts.length - 1; i++) {
|
|
final lastEnd = ends[i];
|
|
final nextStart = starts[i + 1];
|
|
if (nextStart < lastEnd) {
|
|
throw 'Invalid format: $nextStart < $lastEnd';
|
|
}
|
|
}
|
|
|
|
// Start term input
|
|
if (starts.first > 0) {
|
|
terminal.textInput(argsFmted.substring(0, starts.first));
|
|
}
|
|
|
|
// Process matched
|
|
for (var idx = 0; idx < starts.length; idx++) {
|
|
final start = starts[idx];
|
|
final end = ends[idx];
|
|
final key = argsFmted.substring(start, end).toLowerCase();
|
|
|
|
// Special funcs
|
|
final special = _find(SnippetFuncs.specialCtrl, key);
|
|
if (special != null) {
|
|
final raw = key.substring(special.key.length + 1, key.length - 1);
|
|
await special.value((term: terminal, raw: raw));
|
|
} else {
|
|
// Term keys
|
|
final termKey = _find(fmtTermKeys, key);
|
|
if (termKey != null) {
|
|
await _doTermKeys(terminal, termKey, key);
|
|
} else {
|
|
// Normal input
|
|
terminal.textInput(key);
|
|
}
|
|
}
|
|
|
|
// Text between this and next match
|
|
if (idx < starts.length - 1) {
|
|
terminal.textInput(argsFmted.substring(end, starts[idx + 1]));
|
|
}
|
|
}
|
|
|
|
// End term input
|
|
if (ends.last < argsFmted.length) {
|
|
terminal.textInput(argsFmted.substring(ends.last));
|
|
}
|
|
|
|
if (autoEnter) terminal.keyInput(TerminalKey.enter);
|
|
}
|
|
|
|
Future<void> _doTermKeys(Terminal terminal, MapEntry<String, TerminalKey> termKey, String key) async {
|
|
// if (termKey.value == TerminalKey.enter) {
|
|
// terminal.keyInput(TerminalKey.enter);
|
|
// return;
|
|
// }
|
|
|
|
final ctrlAlt = switch (termKey.value) {
|
|
TerminalKey.control => (ctrl: true, alt: false),
|
|
TerminalKey.alt => (ctrl: false, alt: true),
|
|
_ => (ctrl: false, alt: false),
|
|
};
|
|
|
|
if (!key.contains('+')) return;
|
|
|
|
// `${ctrl+ad}` -> `ctrla + d`
|
|
final chars = key.substring(termKey.key.length + 1, key.length - 1);
|
|
if (chars.isEmpty) return;
|
|
final ok = terminal.charInput(chars.codeUnitAt(0), ctrl: ctrlAlt.ctrl, alt: ctrlAlt.alt);
|
|
if (!ok) {
|
|
Loggers.app.warning('Failed to input: $key');
|
|
}
|
|
|
|
terminal.textInput(chars.substring(1));
|
|
}
|
|
|
|
MapEntry<String, T>? _find<T>(Map<String, T> map, String key) {
|
|
return map.entries.firstWhereOrNull((e) => key.startsWith(e.key));
|
|
}
|
|
|
|
static final fmtArgs = {
|
|
r'${host}': (Spi spi) => spi.ip,
|
|
r'${port}': (Spi spi) => spi.port.toString(),
|
|
r'${user}': (Spi spi) => spi.user,
|
|
r'${pwd}': (Spi spi) => spi.pwd ?? '',
|
|
r'${id}': (Spi spi) => spi.id,
|
|
r'${name}': (Spi spi) => spi.name,
|
|
};
|
|
|
|
/// r'${ctrl+ad}' -> TerminalKey.control, a, d
|
|
static final fmtTermKeys = {r'${ctrl': TerminalKey.control, r'${alt': TerminalKey.alt};
|
|
}
|
|
|
|
class SnippetResult {
|
|
final String? dest;
|
|
final String result;
|
|
final Duration time;
|
|
|
|
SnippetResult({required this.dest, required this.result, required this.time});
|
|
}
|
|
|
|
typedef SnippetFuncCtx = ({Terminal term, String raw});
|
|
|
|
abstract final class SnippetFuncs {
|
|
static final specialCtrl = {
|
|
// `${sleep 3}` -> sleep 3 seconds
|
|
r'${sleep': SnippetFuncs.sleep,
|
|
r'${enter': SnippetFuncs.enter,
|
|
};
|
|
|
|
static const help = {'sleep': 'Sleep for a few seconds', 'enter': 'Enter a few times'};
|
|
|
|
static FutureOr<void> sleep(SnippetFuncCtx ctx) async {
|
|
final seconds = int.tryParse(ctx.raw);
|
|
if (seconds == null) return;
|
|
final duration = Duration(seconds: seconds);
|
|
await Future.delayed(duration);
|
|
}
|
|
|
|
static FutureOr<void> enter(SnippetFuncCtx ctx) async {
|
|
final times = int.tryParse(ctx.raw) ?? 1;
|
|
for (var i = 0; i < times; i++) {
|
|
ctx.term.keyInput(TerminalKey.enter);
|
|
}
|
|
}
|
|
}
|