feat: ability to disable monitoring cmds (#840)

This commit is contained in:
lollipopkit🏳️‍⚧️
2025-08-09 12:37:30 +08:00
committed by GitHub
parent 9c9648656d
commit 95f8e571c1
16 changed files with 451 additions and 609 deletions

View File

@@ -1,9 +1,16 @@
import 'package:server_box/core/extension/context/locale.dart'; import 'package:server_box/core/extension/context/locale.dart';
import 'package:server_box/data/model/app/scripts/script_consts.dart';
import 'package:server_box/data/model/server/system.dart'; import 'package:server_box/data/model/server/system.dart';
/// Base class for all command type enums /// Base class for all command type enums
abstract class CommandType { abstract class CommandType implements Enum {
String get cmd; String get cmd;
/// Get command-specific separator
String get separator;
/// Get command-specific divider (separator with echo and formatting)
String get divider;
} }
/// Linux/Unix status commands /// Linux/Unix status commands
@@ -83,6 +90,12 @@ enum StatusCmdType implements CommandType {
final String cmd; final String cmd;
const StatusCmdType(this.cmd); const StatusCmdType(this.cmd);
@override
String get separator => ScriptConstants.getCmdSeparator(name);
@override
String get divider => ScriptConstants.getCmdDivider(name);
} }
/// BSD/macOS status commands /// BSD/macOS status commands
@@ -102,6 +115,12 @@ enum BSDStatusCmdType implements CommandType {
final String cmd; final String cmd;
const BSDStatusCmdType(this.cmd); const BSDStatusCmdType(this.cmd);
@override
String get separator => ScriptConstants.getCmdSeparator(name);
@override
String get divider => ScriptConstants.getCmdDivider(name);
} }
/// Windows PowerShell status commands /// Windows PowerShell status commands
@@ -225,6 +244,12 @@ enum WindowsStatusCmdType implements CommandType {
final String cmd; final String cmd;
const WindowsStatusCmdType(this.cmd); const WindowsStatusCmdType(this.cmd);
@override
String get separator => ScriptConstants.getCmdSeparator(name);
@override
String get divider => ScriptConstants.getCmdDivider(name);
} }
/// Extensions for StatusCmdType /// Extensions for StatusCmdType
@@ -240,10 +265,10 @@ extension StatusCmdTypeX on StatusCmdType {
}; };
} }
/// Generic extension for Enum types /// Extension for CommandType to find content in parsed map
extension EnumX on Enum { extension CommandTypeX on CommandType {
/// Find out the required segment from [segments] /// Find the command output from the parsed script output map
String find(List<String> segments) { String findInMap(Map<String, String> parsedOutput) {
return segments[index]; return parsedOutput[name] ?? '';
} }
} }

View File

@@ -3,11 +3,11 @@ import 'package:server_box/data/model/app/scripts/script_consts.dart';
import 'package:server_box/data/model/app/scripts/shell_func.dart'; import 'package:server_box/data/model/app/scripts/shell_func.dart';
/// Abstract base class for platform-specific script builders /// Abstract base class for platform-specific script builders
abstract class ScriptBuilder { sealed class ScriptBuilder {
const ScriptBuilder(); const ScriptBuilder();
/// Generate a complete script for all shell functions /// Generate a complete script for all shell functions
String buildScript(Map<String, String>? customCmds); String buildScript(Map<String, String>? customCmds, [List<String>? disabledCmdTypes]);
/// Get the script file name for this platform /// Get the script file name for this platform
String get scriptFileName; String get scriptFileName;
@@ -23,9 +23,6 @@ abstract class ScriptBuilder {
/// Get the script header for this platform /// Get the script header for this platform
String get scriptHeader; String get scriptHeader;
/// Get the command divider for this platform
String get cmdDivider => ScriptConstants.cmdDivider;
} }
/// Windows PowerShell script builder /// Windows PowerShell script builder
@@ -53,13 +50,19 @@ class WindowsScriptBuilder extends ScriptBuilder {
@override @override
String getCustomCmdsString(ShellFunc func, Map<String, String>? customCmds) { String getCustomCmdsString(ShellFunc func, Map<String, String>? customCmds) {
if (func == ShellFunc.status && customCmds != null && customCmds.isNotEmpty) { if (func == ShellFunc.status && customCmds != null && customCmds.isNotEmpty) {
return '\n${customCmds.values.map((cmd) => '\t$cmd').join('\n')}'; final sb = StringBuffer();
for (final e in customCmds.entries) {
final cmdDivider = ScriptConstants.getCustomCmdSeparator(e.key);
sb.writeln(' Write-Host "$cmdDivider"');
sb.writeln(' ${e.value}');
}
return '\n$sb';
} }
return ''; return '';
} }
@override @override
String buildScript(Map<String, String>? customCmds) { String buildScript(Map<String, String>? customCmds, [List<String>? disabledCmdTypes]) {
final sb = StringBuffer(); final sb = StringBuffer();
sb.write(scriptHeader); sb.write(scriptHeader);
@@ -69,7 +72,7 @@ class WindowsScriptBuilder extends ScriptBuilder {
sb.write(''' sb.write('''
function ${func.name} { function ${func.name} {
${_getWindowsCommand(func).split('\n').map((e) => e.isEmpty ? '' : ' $e').join('\n')}$customCmdsStr ${_getWindowsCommand(func, disabledCmdTypes).split('\n').map((e) => e.isEmpty ? '' : ' $e').join('\n')}$customCmdsStr
} }
'''); ''');
@@ -92,14 +95,20 @@ switch (\$args[0]) {
} }
/// Get Windows-specific command for a shell function /// Get Windows-specific command for a shell function
String _getWindowsCommand(ShellFunc func) => switch (func) { String _getWindowsCommand(ShellFunc func, [List<String>? disabledCmdTypes]) => switch (func) {
ShellFunc.status => WindowsStatusCmdType.values.map((e) => e.cmd).join(cmdDivider), ShellFunc.status => _getWindowsStatusCommand(disabledCmdTypes: disabledCmdTypes ?? []),
ShellFunc.process => 'Get-Process | Select-Object ProcessName, Id, CPU, WorkingSet | ConvertTo-Json', ShellFunc.process => 'Get-Process | Select-Object ProcessName, Id, CPU, WorkingSet | ConvertTo-Json',
ShellFunc.shutdown => 'Stop-Computer -Force', ShellFunc.shutdown => 'Stop-Computer -Force',
ShellFunc.reboot => 'Restart-Computer -Force', ShellFunc.reboot => 'Restart-Computer -Force',
ShellFunc.suspend => ShellFunc.suspend =>
'Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.Application]::SetSuspendState(\'Suspend\', \$false, \$false)', 'Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.Application]::SetSuspendState(\'Suspend\', \$false, \$false)',
}; };
/// Get Windows status command with command-specific separators
String _getWindowsStatusCommand({required List<String> disabledCmdTypes}) {
final cmdTypes = WindowsStatusCmdType.values.where((e) => !disabledCmdTypes.contains(e.name));
return cmdTypes.map((e) => '${e.divider}${e.cmd}').join('').trimRight(); // Remove trailing divider
}
} }
/// Unix shell script builder /// Unix shell script builder
@@ -129,13 +138,19 @@ chmod 755 $scriptPath
@override @override
String getCustomCmdsString(ShellFunc func, Map<String, String>? customCmds) { String getCustomCmdsString(ShellFunc func, Map<String, String>? customCmds) {
if (func == ShellFunc.status && customCmds != null && customCmds.isNotEmpty) { if (func == ShellFunc.status && customCmds != null && customCmds.isNotEmpty) {
return '$cmdDivider\n\t${customCmds.values.join(cmdDivider)}'; final sb = StringBuffer();
for (final e in customCmds.entries) {
final cmdDivider = ScriptConstants.getCustomCmdSeparator(e.key);
sb.writeln('echo "$cmdDivider"');
sb.writeln(e.value);
}
return '\n$sb';
} }
return ''; return '';
} }
@override @override
String buildScript(Map<String, String>? customCmds) { String buildScript(Map<String, String>? customCmds, [List<String>? disabledCmdTypes]) {
final sb = StringBuffer(); final sb = StringBuffer();
sb.write(scriptHeader); sb.write(scriptHeader);
// Write each function // Write each function
@@ -143,7 +158,7 @@ chmod 755 $scriptPath
final customCmdsStr = getCustomCmdsString(func, customCmds); final customCmdsStr = getCustomCmdsString(func, customCmds);
sb.write(''' sb.write('''
${func.name}() { ${func.name}() {
${_getUnixCommand(func).split('\n').map((e) => '\t$e').join('\n')} ${_getUnixCommand(func, disabledCmdTypes).split('\n').map((e) => '\t$e').join('\n')}
$customCmdsStr $customCmdsStr
} }
@@ -168,27 +183,24 @@ esac''');
} }
/// Get Unix-specific command for a shell function /// Get Unix-specific command for a shell function
String _getUnixCommand(ShellFunc func) { String _getUnixCommand(ShellFunc func, [List<String>? disabledCmdTypes]) {
switch (func) { return switch (func) {
case ShellFunc.status: ShellFunc.status => _getUnixStatusCommand(disabledCmdTypes: disabledCmdTypes ?? []),
return _getUnixStatusCommand(); ShellFunc.process => _getUnixProcessCommand(),
case ShellFunc.process: ShellFunc.shutdown => _getUnixShutdownCommand(),
return _getUnixProcessCommand(); ShellFunc.reboot => _getUnixRebootCommand(),
case ShellFunc.shutdown: ShellFunc.suspend => _getUnixSuspendCommand(),
return _getUnixShutdownCommand(); };
case ShellFunc.reboot:
return _getUnixRebootCommand();
case ShellFunc.suspend:
return _getUnixSuspendCommand();
}
} }
/// Get Unix status command with OS detection /// Get Unix status command with OS detection
String _getUnixStatusCommand() { String _getUnixStatusCommand({required List<String> disabledCmdTypes}) {
// Generate command lists for better readability // Generate command lists with command-specific separators, filtering disabled commands
final linuxCommands = StatusCmdType.values.map((e) => e.cmd).join(cmdDivider); final filteredLinuxCmdTypes = StatusCmdType.values.where((e) => !disabledCmdTypes.contains(e.name));
final linuxCommands = filteredLinuxCmdTypes.map((e) => '${e.divider}${e.cmd}').join('').trimRight();
final bsdCommands = BSDStatusCmdType.values.map((e) => e.cmd).join(cmdDivider); final filteredBsdCmdTypes = BSDStatusCmdType.values.where((e) => !disabledCmdTypes.contains(e.name));
final bsdCommands = filteredBsdCmdTypes.map((e) => '${e.divider}${e.cmd}').join('').trimRight();
return ''' return '''
if [ "\$macSign" = "" ] && [ "\$bsdSign" = "" ]; then if [ "\$macSign" = "" ] && [ "\$bsdSign" = "" ]; then

View File

@@ -17,8 +17,58 @@ class ScriptConstants {
// Command separators and dividers // Command separators and dividers
static const String separator = 'SrvBoxSep'; static const String separator = 'SrvBoxSep';
/// The suffix `\t` is for formatting /// Custom command separator
static const String cmdDivider = '\necho $separator\n\t'; static const String customCmdSep = 'SrvBoxCusCmdSep';
/// Generate command-specific separator
static String getCmdSeparator(String cmdName) => '$separator.$cmdName';
/// Generate command-specific divider for custom commands
static String getCustomCmdSeparator(String cmdName) => '$customCmdSep.$cmdName';
/// Generate command-specific divider
static String getCmdDivider(String cmdName) => '\necho ${getCmdSeparator(cmdName)}\n\t';
/// Parse script output into command-specific map
static Map<String, String> parseScriptOutput(String raw) {
final result = <String, String>{};
if (raw.isEmpty) return result;
// Parse line by line to properly handle command-specific separators
final lines = raw.split('\n');
String? currentCmd;
final buffer = StringBuffer();
for (final line in lines) {
if (line.startsWith('$separator.')) {
// Save previous command content
if (currentCmd != null) {
result[currentCmd] = buffer.toString().trim();
buffer.clear();
}
// Start new command
currentCmd = line.substring('$separator.'.length);
} else if (line.startsWith('$customCmdSep.')) {
// Save previous command content
if (currentCmd != null) {
result[currentCmd] = buffer.toString().trim();
buffer.clear();
}
// Start new custom command
currentCmd = line.substring('$customCmdSep.'.length);
} else if (currentCmd != null) {
buffer.writeln(line);
}
}
// Don't forget the last command
if (currentCmd != null) {
result[currentCmd] = buffer.toString().trim();
}
return result;
}
// Path separators // Path separators
static const String unixPathSeparator = '/'; static const String unixPathSeparator = '/';

View File

@@ -93,10 +93,10 @@ class ShellFuncManager {
} }
/// Generate complete script based on system type /// Generate complete script based on system type
static String allScript(Map<String, String>? customCmds, {SystemType? systemType}) { static String allScript(Map<String, String>? customCmds, {SystemType? systemType, List<String>? disabledCmdTypes}) {
final isWindows = systemType == SystemType.windows; final isWindows = systemType == SystemType.windows;
final builder = ScriptBuilderFactory.getBuilder(isWindows); final builder = ScriptBuilderFactory.getBuilder(isWindows);
return builder.buildScript(customCmds); return builder.buildScript(customCmds, disabledCmdTypes);
} }
} }

View File

@@ -48,6 +48,9 @@ abstract class Spi with _$Spi {
/// Custom system type (unix or windows). If set, skip auto-detection. /// Custom system type (unix or windows). If set, skip auto-detection.
@JsonKey(includeIfNull: false) SystemType? customSystemType, @JsonKey(includeIfNull: false) SystemType? customSystemType,
/// Disabled command types for this server
@JsonKey(includeIfNull: false) List<String>? disabledCmdTypes,
}) = _Spi; }) = _Spi;
factory Spi.fromJson(Map<String, dynamic> json) => _$SpiFromJson(json); factory Spi.fromJson(Map<String, dynamic> json) => _$SpiFromJson(json);

View File

@@ -20,7 +20,8 @@ mixin _$Spi {
@JsonKey(name: 'pubKeyId') String? get keyId; List<String>? get tags; String? get alterUrl; bool get autoConnect;/// [id] of the jump server @JsonKey(name: 'pubKeyId') String? get keyId; List<String>? get tags; String? get alterUrl; bool get autoConnect;/// [id] of the jump server
String? get jumpId; ServerCustom? get custom; WakeOnLanCfg? get wolCfg;/// It only applies to SSH terminal. String? get jumpId; ServerCustom? get custom; WakeOnLanCfg? get wolCfg;/// It only applies to SSH terminal.
Map<String, String>? get envs;@JsonKey(fromJson: Spi.parseId) String get id;/// Custom system type (unix or windows). If set, skip auto-detection. Map<String, String>? get envs;@JsonKey(fromJson: Spi.parseId) String get id;/// Custom system type (unix or windows). If set, skip auto-detection.
@JsonKey(includeIfNull: false) SystemType? get customSystemType; @JsonKey(includeIfNull: false) SystemType? get customSystemType;/// Disabled command types for this server
@JsonKey(includeIfNull: false) List<String>? get disabledCmdTypes;
/// Create a copy of Spi /// Create a copy of Spi
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@@ -33,12 +34,12 @@ $SpiCopyWith<Spi> get copyWith => _$SpiCopyWithImpl<Spi>(this as Spi, _$identity
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is Spi&&(identical(other.name, name) || other.name == name)&&(identical(other.ip, ip) || other.ip == ip)&&(identical(other.port, port) || other.port == port)&&(identical(other.user, user) || other.user == user)&&(identical(other.pwd, pwd) || other.pwd == pwd)&&(identical(other.keyId, keyId) || other.keyId == keyId)&&const DeepCollectionEquality().equals(other.tags, tags)&&(identical(other.alterUrl, alterUrl) || other.alterUrl == alterUrl)&&(identical(other.autoConnect, autoConnect) || other.autoConnect == autoConnect)&&(identical(other.jumpId, jumpId) || other.jumpId == jumpId)&&(identical(other.custom, custom) || other.custom == custom)&&(identical(other.wolCfg, wolCfg) || other.wolCfg == wolCfg)&&const DeepCollectionEquality().equals(other.envs, envs)&&(identical(other.id, id) || other.id == id)&&(identical(other.customSystemType, customSystemType) || other.customSystemType == customSystemType)); return identical(this, other) || (other.runtimeType == runtimeType&&other is Spi&&(identical(other.name, name) || other.name == name)&&(identical(other.ip, ip) || other.ip == ip)&&(identical(other.port, port) || other.port == port)&&(identical(other.user, user) || other.user == user)&&(identical(other.pwd, pwd) || other.pwd == pwd)&&(identical(other.keyId, keyId) || other.keyId == keyId)&&const DeepCollectionEquality().equals(other.tags, tags)&&(identical(other.alterUrl, alterUrl) || other.alterUrl == alterUrl)&&(identical(other.autoConnect, autoConnect) || other.autoConnect == autoConnect)&&(identical(other.jumpId, jumpId) || other.jumpId == jumpId)&&(identical(other.custom, custom) || other.custom == custom)&&(identical(other.wolCfg, wolCfg) || other.wolCfg == wolCfg)&&const DeepCollectionEquality().equals(other.envs, envs)&&(identical(other.id, id) || other.id == id)&&(identical(other.customSystemType, customSystemType) || other.customSystemType == customSystemType)&&const DeepCollectionEquality().equals(other.disabledCmdTypes, disabledCmdTypes));
} }
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash(runtimeType,name,ip,port,user,pwd,keyId,const DeepCollectionEquality().hash(tags),alterUrl,autoConnect,jumpId,custom,wolCfg,const DeepCollectionEquality().hash(envs),id,customSystemType); int get hashCode => Object.hash(runtimeType,name,ip,port,user,pwd,keyId,const DeepCollectionEquality().hash(tags),alterUrl,autoConnect,jumpId,custom,wolCfg,const DeepCollectionEquality().hash(envs),id,customSystemType,const DeepCollectionEquality().hash(disabledCmdTypes));
@@ -49,7 +50,7 @@ abstract mixin class $SpiCopyWith<$Res> {
factory $SpiCopyWith(Spi value, $Res Function(Spi) _then) = _$SpiCopyWithImpl; factory $SpiCopyWith(Spi value, $Res Function(Spi) _then) = _$SpiCopyWithImpl;
@useResult @useResult
$Res call({ $Res call({
String name, String ip, int port, String user, String? pwd,@JsonKey(name: 'pubKeyId') String? keyId, List<String>? tags, String? alterUrl, bool autoConnect, String? jumpId, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map<String, String>? envs,@JsonKey(fromJson: Spi.parseId) String id,@JsonKey(includeIfNull: false) SystemType? customSystemType String name, String ip, int port, String user, String? pwd,@JsonKey(name: 'pubKeyId') String? keyId, List<String>? tags, String? alterUrl, bool autoConnect, String? jumpId, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map<String, String>? envs,@JsonKey(fromJson: Spi.parseId) String id,@JsonKey(includeIfNull: false) SystemType? customSystemType,@JsonKey(includeIfNull: false) List<String>? disabledCmdTypes
}); });
@@ -66,7 +67,7 @@ class _$SpiCopyWithImpl<$Res>
/// Create a copy of Spi /// Create a copy of Spi
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? name = null,Object? ip = null,Object? port = null,Object? user = null,Object? pwd = freezed,Object? keyId = freezed,Object? tags = freezed,Object? alterUrl = freezed,Object? autoConnect = null,Object? jumpId = freezed,Object? custom = freezed,Object? wolCfg = freezed,Object? envs = freezed,Object? id = null,Object? customSystemType = freezed,}) { @pragma('vm:prefer-inline') @override $Res call({Object? name = null,Object? ip = null,Object? port = null,Object? user = null,Object? pwd = freezed,Object? keyId = freezed,Object? tags = freezed,Object? alterUrl = freezed,Object? autoConnect = null,Object? jumpId = freezed,Object? custom = freezed,Object? wolCfg = freezed,Object? envs = freezed,Object? id = null,Object? customSystemType = freezed,Object? disabledCmdTypes = freezed,}) {
return _then(_self.copyWith( return _then(_self.copyWith(
name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
as String,ip: null == ip ? _self.ip : ip // ignore: cast_nullable_to_non_nullable as String,ip: null == ip ? _self.ip : ip // ignore: cast_nullable_to_non_nullable
@@ -83,7 +84,8 @@ as ServerCustom?,wolCfg: freezed == wolCfg ? _self.wolCfg : wolCfg // ignore: ca
as WakeOnLanCfg?,envs: freezed == envs ? _self.envs : envs // ignore: cast_nullable_to_non_nullable as WakeOnLanCfg?,envs: freezed == envs ? _self.envs : envs // ignore: cast_nullable_to_non_nullable
as Map<String, String>?,id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable as Map<String, String>?,id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,customSystemType: freezed == customSystemType ? _self.customSystemType : customSystemType // ignore: cast_nullable_to_non_nullable as String,customSystemType: freezed == customSystemType ? _self.customSystemType : customSystemType // ignore: cast_nullable_to_non_nullable
as SystemType?, as SystemType?,disabledCmdTypes: freezed == disabledCmdTypes ? _self.disabledCmdTypes : disabledCmdTypes // ignore: cast_nullable_to_non_nullable
as List<String>?,
)); ));
} }
@@ -94,7 +96,7 @@ as SystemType?,
@JsonSerializable(includeIfNull: false) @JsonSerializable(includeIfNull: false)
class _Spi extends Spi { class _Spi extends Spi {
const _Spi({required this.name, required this.ip, required this.port, required this.user, this.pwd, @JsonKey(name: 'pubKeyId') this.keyId, final List<String>? tags, this.alterUrl, this.autoConnect = true, this.jumpId, this.custom, this.wolCfg, final Map<String, String>? envs, @JsonKey(fromJson: Spi.parseId) this.id = '', @JsonKey(includeIfNull: false) this.customSystemType}): _tags = tags,_envs = envs,super._(); const _Spi({required this.name, required this.ip, required this.port, required this.user, this.pwd, @JsonKey(name: 'pubKeyId') this.keyId, final List<String>? tags, this.alterUrl, this.autoConnect = true, this.jumpId, this.custom, this.wolCfg, final Map<String, String>? envs, @JsonKey(fromJson: Spi.parseId) this.id = '', @JsonKey(includeIfNull: false) this.customSystemType, @JsonKey(includeIfNull: false) final List<String>? disabledCmdTypes}): _tags = tags,_envs = envs,_disabledCmdTypes = disabledCmdTypes,super._();
factory _Spi.fromJson(Map<String, dynamic> json) => _$SpiFromJson(json); factory _Spi.fromJson(Map<String, dynamic> json) => _$SpiFromJson(json);
@override final String name; @override final String name;
@@ -133,6 +135,17 @@ class _Spi extends Spi {
@override@JsonKey(fromJson: Spi.parseId) final String id; @override@JsonKey(fromJson: Spi.parseId) final String id;
/// Custom system type (unix or windows). If set, skip auto-detection. /// Custom system type (unix or windows). If set, skip auto-detection.
@override@JsonKey(includeIfNull: false) final SystemType? customSystemType; @override@JsonKey(includeIfNull: false) final SystemType? customSystemType;
/// Disabled command types for this server
final List<String>? _disabledCmdTypes;
/// Disabled command types for this server
@override@JsonKey(includeIfNull: false) List<String>? get disabledCmdTypes {
final value = _disabledCmdTypes;
if (value == null) return null;
if (_disabledCmdTypes is EqualUnmodifiableListView) return _disabledCmdTypes;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(value);
}
/// Create a copy of Spi /// Create a copy of Spi
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@@ -147,12 +160,12 @@ Map<String, dynamic> toJson() {
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _Spi&&(identical(other.name, name) || other.name == name)&&(identical(other.ip, ip) || other.ip == ip)&&(identical(other.port, port) || other.port == port)&&(identical(other.user, user) || other.user == user)&&(identical(other.pwd, pwd) || other.pwd == pwd)&&(identical(other.keyId, keyId) || other.keyId == keyId)&&const DeepCollectionEquality().equals(other._tags, _tags)&&(identical(other.alterUrl, alterUrl) || other.alterUrl == alterUrl)&&(identical(other.autoConnect, autoConnect) || other.autoConnect == autoConnect)&&(identical(other.jumpId, jumpId) || other.jumpId == jumpId)&&(identical(other.custom, custom) || other.custom == custom)&&(identical(other.wolCfg, wolCfg) || other.wolCfg == wolCfg)&&const DeepCollectionEquality().equals(other._envs, _envs)&&(identical(other.id, id) || other.id == id)&&(identical(other.customSystemType, customSystemType) || other.customSystemType == customSystemType)); return identical(this, other) || (other.runtimeType == runtimeType&&other is _Spi&&(identical(other.name, name) || other.name == name)&&(identical(other.ip, ip) || other.ip == ip)&&(identical(other.port, port) || other.port == port)&&(identical(other.user, user) || other.user == user)&&(identical(other.pwd, pwd) || other.pwd == pwd)&&(identical(other.keyId, keyId) || other.keyId == keyId)&&const DeepCollectionEquality().equals(other._tags, _tags)&&(identical(other.alterUrl, alterUrl) || other.alterUrl == alterUrl)&&(identical(other.autoConnect, autoConnect) || other.autoConnect == autoConnect)&&(identical(other.jumpId, jumpId) || other.jumpId == jumpId)&&(identical(other.custom, custom) || other.custom == custom)&&(identical(other.wolCfg, wolCfg) || other.wolCfg == wolCfg)&&const DeepCollectionEquality().equals(other._envs, _envs)&&(identical(other.id, id) || other.id == id)&&(identical(other.customSystemType, customSystemType) || other.customSystemType == customSystemType)&&const DeepCollectionEquality().equals(other._disabledCmdTypes, _disabledCmdTypes));
} }
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash(runtimeType,name,ip,port,user,pwd,keyId,const DeepCollectionEquality().hash(_tags),alterUrl,autoConnect,jumpId,custom,wolCfg,const DeepCollectionEquality().hash(_envs),id,customSystemType); int get hashCode => Object.hash(runtimeType,name,ip,port,user,pwd,keyId,const DeepCollectionEquality().hash(_tags),alterUrl,autoConnect,jumpId,custom,wolCfg,const DeepCollectionEquality().hash(_envs),id,customSystemType,const DeepCollectionEquality().hash(_disabledCmdTypes));
@@ -163,7 +176,7 @@ abstract mixin class _$SpiCopyWith<$Res> implements $SpiCopyWith<$Res> {
factory _$SpiCopyWith(_Spi value, $Res Function(_Spi) _then) = __$SpiCopyWithImpl; factory _$SpiCopyWith(_Spi value, $Res Function(_Spi) _then) = __$SpiCopyWithImpl;
@override @useResult @override @useResult
$Res call({ $Res call({
String name, String ip, int port, String user, String? pwd,@JsonKey(name: 'pubKeyId') String? keyId, List<String>? tags, String? alterUrl, bool autoConnect, String? jumpId, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map<String, String>? envs,@JsonKey(fromJson: Spi.parseId) String id,@JsonKey(includeIfNull: false) SystemType? customSystemType String name, String ip, int port, String user, String? pwd,@JsonKey(name: 'pubKeyId') String? keyId, List<String>? tags, String? alterUrl, bool autoConnect, String? jumpId, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map<String, String>? envs,@JsonKey(fromJson: Spi.parseId) String id,@JsonKey(includeIfNull: false) SystemType? customSystemType,@JsonKey(includeIfNull: false) List<String>? disabledCmdTypes
}); });
@@ -180,7 +193,7 @@ class __$SpiCopyWithImpl<$Res>
/// Create a copy of Spi /// Create a copy of Spi
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? name = null,Object? ip = null,Object? port = null,Object? user = null,Object? pwd = freezed,Object? keyId = freezed,Object? tags = freezed,Object? alterUrl = freezed,Object? autoConnect = null,Object? jumpId = freezed,Object? custom = freezed,Object? wolCfg = freezed,Object? envs = freezed,Object? id = null,Object? customSystemType = freezed,}) { @override @pragma('vm:prefer-inline') $Res call({Object? name = null,Object? ip = null,Object? port = null,Object? user = null,Object? pwd = freezed,Object? keyId = freezed,Object? tags = freezed,Object? alterUrl = freezed,Object? autoConnect = null,Object? jumpId = freezed,Object? custom = freezed,Object? wolCfg = freezed,Object? envs = freezed,Object? id = null,Object? customSystemType = freezed,Object? disabledCmdTypes = freezed,}) {
return _then(_Spi( return _then(_Spi(
name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
as String,ip: null == ip ? _self.ip : ip // ignore: cast_nullable_to_non_nullable as String,ip: null == ip ? _self.ip : ip // ignore: cast_nullable_to_non_nullable
@@ -197,7 +210,8 @@ as ServerCustom?,wolCfg: freezed == wolCfg ? _self.wolCfg : wolCfg // ignore: ca
as WakeOnLanCfg?,envs: freezed == envs ? _self._envs : envs // ignore: cast_nullable_to_non_nullable as WakeOnLanCfg?,envs: freezed == envs ? _self._envs : envs // ignore: cast_nullable_to_non_nullable
as Map<String, String>?,id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable as Map<String, String>?,id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,customSystemType: freezed == customSystemType ? _self.customSystemType : customSystemType // ignore: cast_nullable_to_non_nullable as String,customSystemType: freezed == customSystemType ? _self.customSystemType : customSystemType // ignore: cast_nullable_to_non_nullable
as SystemType?, as SystemType?,disabledCmdTypes: freezed == disabledCmdTypes ? _self._disabledCmdTypes : disabledCmdTypes // ignore: cast_nullable_to_non_nullable
as List<String>?,
)); ));
} }

View File

@@ -31,6 +31,9 @@ _Spi _$SpiFromJson(Map<String, dynamic> json) => _Spi(
_$SystemTypeEnumMap, _$SystemTypeEnumMap,
json['customSystemType'], json['customSystemType'],
), ),
disabledCmdTypes: (json['disabledCmdTypes'] as List<dynamic>?)
?.map((e) => e as String)
.toList(),
); );
Map<String, dynamic> _$SpiToJson(_Spi instance) => <String, dynamic>{ Map<String, dynamic> _$SpiToJson(_Spi instance) => <String, dynamic>{
@@ -50,6 +53,7 @@ Map<String, dynamic> _$SpiToJson(_Spi instance) => <String, dynamic>{
'id': instance.id, 'id': instance.id,
if (_$SystemTypeEnumMap[instance.customSystemType] case final value?) if (_$SystemTypeEnumMap[instance.customSystemType] case final value?)
'customSystemType': value, 'customSystemType': value,
if (instance.disabledCmdTypes case final value?) 'disabledCmdTypes': value,
}; };
const _$SystemTypeEnumMap = { const _$SystemTypeEnumMap = {

View File

@@ -20,14 +20,14 @@ import 'package:server_box/data/model/server/windows_parser.dart';
class ServerStatusUpdateReq { class ServerStatusUpdateReq {
final ServerStatus ss; final ServerStatus ss;
final List<String> segments; final Map<String, String> parsedOutput;
final SystemType system; final SystemType system;
final Map<String, String> customCmds; final Map<String, String> customCmds;
const ServerStatusUpdateReq({ const ServerStatusUpdateReq({
required this.system, required this.system,
required this.ss, required this.ss,
required this.segments, required this.parsedOutput,
required this.customCmds, required this.customCmds,
}); });
} }
@@ -43,20 +43,20 @@ Future<ServerStatus> getStatus(ServerStatusUpdateReq req) async {
// Wrap each operation with a try-catch, so that if one operation fails, // Wrap each operation with a try-catch, so that if one operation fails,
// the following operations can still be executed. // the following operations can still be executed.
Future<ServerStatus> _getLinuxStatus(ServerStatusUpdateReq req) async { Future<ServerStatus> _getLinuxStatus(ServerStatusUpdateReq req) async {
final segments = req.segments; final parsedOutput = req.parsedOutput;
final time = int.tryParse(StatusCmdType.time.find(segments)) ?? final time = int.tryParse(StatusCmdType.time.findInMap(parsedOutput)) ??
DateTime.now().millisecondsSinceEpoch ~/ 1000; DateTime.now().millisecondsSinceEpoch ~/ 1000;
try { try {
final net = NetSpeed.parse(StatusCmdType.net.find(segments), time); final net = NetSpeed.parse(StatusCmdType.net.findInMap(parsedOutput), time);
req.ss.netSpeed.update(net); req.ss.netSpeed.update(net);
} catch (e, s) { } catch (e, s) {
Loggers.app.warning(e, s); Loggers.app.warning(e, s);
} }
try { try {
final sys = _parseSysVer(StatusCmdType.sys.find(segments)); final sys = _parseSysVer(StatusCmdType.sys.findInMap(parsedOutput));
if (sys != null) { if (sys != null) {
req.ss.more[StatusCmdType.sys] = sys; req.ss.more[StatusCmdType.sys] = sys;
} }
@@ -65,7 +65,7 @@ Future<ServerStatus> _getLinuxStatus(ServerStatusUpdateReq req) async {
} }
try { try {
final host = _parseHostName(StatusCmdType.host.find(segments)); final host = _parseHostName(StatusCmdType.host.findInMap(parsedOutput));
if (host != null) { if (host != null) {
req.ss.more[StatusCmdType.host] = host; req.ss.more[StatusCmdType.host] = host;
} }
@@ -74,9 +74,9 @@ Future<ServerStatus> _getLinuxStatus(ServerStatusUpdateReq req) async {
} }
try { try {
final cpus = SingleCpuCore.parse(StatusCmdType.cpu.find(segments)); final cpus = SingleCpuCore.parse(StatusCmdType.cpu.findInMap(parsedOutput));
req.ss.cpu.update(cpus); req.ss.cpu.update(cpus);
final brand = CpuBrand.parse(StatusCmdType.cpuBrand.find(segments)); final brand = CpuBrand.parse(StatusCmdType.cpuBrand.findInMap(parsedOutput));
req.ss.cpu.brand.clear(); req.ss.cpu.brand.clear();
req.ss.cpu.brand.addAll(brand); req.ss.cpu.brand.addAll(brand);
} catch (e, s) { } catch (e, s) {
@@ -85,15 +85,15 @@ Future<ServerStatus> _getLinuxStatus(ServerStatusUpdateReq req) async {
try { try {
req.ss.temps.parse( req.ss.temps.parse(
StatusCmdType.tempType.find(segments), StatusCmdType.tempType.findInMap(parsedOutput),
StatusCmdType.tempVal.find(segments), StatusCmdType.tempVal.findInMap(parsedOutput),
); );
} catch (e, s) { } catch (e, s) {
Loggers.app.warning(e, s); Loggers.app.warning(e, s);
} }
try { try {
final tcp = Conn.parse(StatusCmdType.conn.find(segments)); final tcp = Conn.parse(StatusCmdType.conn.findInMap(parsedOutput));
if (tcp != null) { if (tcp != null) {
req.ss.tcp = tcp; req.ss.tcp = tcp;
} }
@@ -102,20 +102,20 @@ Future<ServerStatus> _getLinuxStatus(ServerStatusUpdateReq req) async {
} }
try { try {
req.ss.disk = Disk.parse(StatusCmdType.disk.find(segments)); req.ss.disk = Disk.parse(StatusCmdType.disk.findInMap(parsedOutput));
req.ss.diskUsage = DiskUsage.parse(req.ss.disk); req.ss.diskUsage = DiskUsage.parse(req.ss.disk);
} catch (e, s) { } catch (e, s) {
Loggers.app.warning(e, s); Loggers.app.warning(e, s);
} }
try { try {
req.ss.mem = Memory.parse(StatusCmdType.mem.find(segments)); req.ss.mem = Memory.parse(StatusCmdType.mem.findInMap(parsedOutput));
} catch (e, s) { } catch (e, s) {
Loggers.app.warning(e, s); Loggers.app.warning(e, s);
} }
try { try {
final uptime = _parseUpTime(StatusCmdType.uptime.find(segments)); final uptime = _parseUpTime(StatusCmdType.uptime.findInMap(parsedOutput));
if (uptime != null) { if (uptime != null) {
req.ss.more[StatusCmdType.uptime] = uptime; req.ss.more[StatusCmdType.uptime] = uptime;
} }
@@ -124,39 +124,39 @@ Future<ServerStatus> _getLinuxStatus(ServerStatusUpdateReq req) async {
} }
try { try {
req.ss.swap = Swap.parse(StatusCmdType.mem.find(segments)); req.ss.swap = Swap.parse(StatusCmdType.mem.findInMap(parsedOutput));
} catch (e, s) { } catch (e, s) {
Loggers.app.warning(e, s); Loggers.app.warning(e, s);
} }
try { try {
final diskio = DiskIO.parse(StatusCmdType.diskio.find(segments), time); final diskio = DiskIO.parse(StatusCmdType.diskio.findInMap(parsedOutput), time);
req.ss.diskIO.update(diskio); req.ss.diskIO.update(diskio);
} catch (e, s) { } catch (e, s) {
Loggers.app.warning(e, s); Loggers.app.warning(e, s);
} }
try { try {
final smarts = DiskSmart.parse(StatusCmdType.diskSmart.find(segments)); final smarts = DiskSmart.parse(StatusCmdType.diskSmart.findInMap(parsedOutput));
req.ss.diskSmart = smarts; req.ss.diskSmart = smarts;
} catch (e, s) { } catch (e, s) {
Loggers.app.warning(e, s); Loggers.app.warning(e, s);
} }
try { try {
req.ss.nvidia = NvidiaSmi.fromXml(StatusCmdType.nvidia.find(segments)); req.ss.nvidia = NvidiaSmi.fromXml(StatusCmdType.nvidia.findInMap(parsedOutput));
} catch (e, s) { } catch (e, s) {
Loggers.app.warning(e, s); Loggers.app.warning(e, s);
} }
try { try {
req.ss.amd = AmdSmi.fromJson(StatusCmdType.amd.find(segments)); req.ss.amd = AmdSmi.fromJson(StatusCmdType.amd.findInMap(parsedOutput));
} catch (e, s) { } catch (e, s) {
Loggers.app.warning(e, s); Loggers.app.warning(e, s);
} }
try { try {
final battery = StatusCmdType.battery.find(segments); final battery = StatusCmdType.battery.findInMap(parsedOutput);
/// Only collect li-poly batteries /// Only collect li-poly batteries
final batteries = Batteries.parse(battery, true); final batteries = Batteries.parse(battery, true);
@@ -169,7 +169,7 @@ Future<ServerStatus> _getLinuxStatus(ServerStatusUpdateReq req) async {
} }
try { try {
final sensors = SensorItem.parse(StatusCmdType.sensors.find(segments)); final sensors = SensorItem.parse(StatusCmdType.sensors.findInMap(parsedOutput));
if (sensors.isNotEmpty) { if (sensors.isNotEmpty) {
req.ss.sensors.clear(); req.ss.sensors.clear();
req.ss.sensors.addAll(sensors); req.ss.sensors.addAll(sensors);
@@ -179,9 +179,9 @@ Future<ServerStatus> _getLinuxStatus(ServerStatusUpdateReq req) async {
} }
try { try {
for (int idx = 0; idx < req.customCmds.length; idx++) { for (final entry in req.customCmds.entries) {
final key = req.customCmds.keys.elementAt(idx); final key = entry.key;
final value = req.segments[idx + req.system.segmentsLen]; final value = req.parsedOutput[key] ?? '';
req.ss.customCmds[key] = value; req.ss.customCmds[key] = value;
} }
} catch (e, s) { } catch (e, s) {
@@ -193,36 +193,36 @@ Future<ServerStatus> _getLinuxStatus(ServerStatusUpdateReq req) async {
// Same as above, wrap with try-catch // Same as above, wrap with try-catch
Future<ServerStatus> _getBsdStatus(ServerStatusUpdateReq req) async { Future<ServerStatus> _getBsdStatus(ServerStatusUpdateReq req) async {
final segments = req.segments; final parsedOutput = req.parsedOutput;
try { try {
final time = int.parse(BSDStatusCmdType.time.find(segments)); final time = int.parse(BSDStatusCmdType.time.findInMap(parsedOutput));
final net = NetSpeed.parseBsd(BSDStatusCmdType.net.find(segments), time); final net = NetSpeed.parseBsd(BSDStatusCmdType.net.findInMap(parsedOutput), time);
req.ss.netSpeed.update(net); req.ss.netSpeed.update(net);
} catch (e, s) { } catch (e, s) {
Loggers.app.warning(e, s); Loggers.app.warning(e, s);
} }
try { try {
req.ss.more[StatusCmdType.sys] = BSDStatusCmdType.sys.find(segments); req.ss.more[StatusCmdType.sys] = BSDStatusCmdType.sys.findInMap(parsedOutput);
} catch (e, s) { } catch (e, s) {
Loggers.app.warning(e, s); Loggers.app.warning(e, s);
} }
try { try {
req.ss.cpu = parseBsdCpu(BSDStatusCmdType.cpu.find(segments)); req.ss.cpu = parseBsdCpu(BSDStatusCmdType.cpu.findInMap(parsedOutput));
} catch (e, s) { } catch (e, s) {
Loggers.app.warning(e, s); Loggers.app.warning(e, s);
} }
try { try {
req.ss.mem = parseBsdMemory(BSDStatusCmdType.mem.find(segments)); req.ss.mem = parseBsdMemory(BSDStatusCmdType.mem.findInMap(parsedOutput));
} catch (e, s) { } catch (e, s) {
Loggers.app.warning(e, s); Loggers.app.warning(e, s);
} }
try { try {
final uptime = _parseUpTime(BSDStatusCmdType.uptime.find(segments)); final uptime = _parseUpTime(BSDStatusCmdType.uptime.findInMap(parsedOutput));
if (uptime != null) { if (uptime != null) {
req.ss.more[StatusCmdType.uptime] = uptime; req.ss.more[StatusCmdType.uptime] = uptime;
} }
@@ -231,7 +231,7 @@ Future<ServerStatus> _getBsdStatus(ServerStatusUpdateReq req) async {
} }
try { try {
req.ss.disk = Disk.parse(BSDStatusCmdType.disk.find(segments)); req.ss.disk = Disk.parse(BSDStatusCmdType.disk.findInMap(parsedOutput));
} catch (e, s) { } catch (e, s) {
Loggers.app.warning(e, s); Loggers.app.warning(e, s);
} }
@@ -302,32 +302,32 @@ String? _parseHostName(String raw) {
// Windows status parsing implementation // Windows status parsing implementation
Future<ServerStatus> _getWindowsStatus(ServerStatusUpdateReq req) async { Future<ServerStatus> _getWindowsStatus(ServerStatusUpdateReq req) async {
final segments = req.segments; final parsedOutput = req.parsedOutput;
final time = int.tryParse(WindowsStatusCmdType.time.find(segments)) ?? final time = int.tryParse(WindowsStatusCmdType.time.findInMap(parsedOutput)) ??
DateTime.now().millisecondsSinceEpoch ~/ 1000; DateTime.now().millisecondsSinceEpoch ~/ 1000;
// Parse all different resource types using helper methods // Parse all different resource types using helper methods
_parseWindowsNetworkData(req, segments, time); _parseWindowsNetworkData(req, parsedOutput, time);
_parseWindowsSystemData(req, segments); _parseWindowsSystemData(req, parsedOutput);
_parseWindowsHostData(req, segments); _parseWindowsHostData(req, parsedOutput);
_parseWindowsCpuData(req, segments); _parseWindowsCpuData(req, parsedOutput);
_parseWindowsMemoryData(req, segments); _parseWindowsMemoryData(req, parsedOutput);
_parseWindowsDiskData(req, segments); _parseWindowsDiskData(req, parsedOutput);
_parseWindowsUptimeData(req, segments); _parseWindowsUptimeData(req, parsedOutput);
_parseWindowsDiskIOData(req, segments, time); _parseWindowsDiskIOData(req, parsedOutput, time);
_parseWindowsConnectionData(req, segments); _parseWindowsConnectionData(req, parsedOutput);
_parseWindowsBatteryData(req, segments); _parseWindowsBatteryData(req, parsedOutput);
_parseWindowsTemperatureData(req, segments); _parseWindowsTemperatureData(req, parsedOutput);
_parseWindowsGpuData(req, segments); _parseWindowsGpuData(req, parsedOutput);
WindowsParser.parseCustomCommands(req.ss, segments, req.customCmds, req.system.segmentsLen); WindowsParser.parseCustomCommands(req.ss, req.parsedOutput, req.customCmds);
return req.ss; return req.ss;
} }
/// Parse Windows network data /// Parse Windows network data
void _parseWindowsNetworkData(ServerStatusUpdateReq req, List<String> segments, int time) { void _parseWindowsNetworkData(ServerStatusUpdateReq req, Map<String, String> parsedOutput, int time) {
try { try {
final netRaw = WindowsStatusCmdType.net.find(segments); final netRaw = WindowsStatusCmdType.net.findInMap(parsedOutput);
if (netRaw.isNotEmpty && if (netRaw.isNotEmpty &&
netRaw != 'null' && netRaw != 'null' &&
!netRaw.contains('network_error') && !netRaw.contains('network_error') &&
@@ -344,9 +344,9 @@ void _parseWindowsNetworkData(ServerStatusUpdateReq req, List<String> segments,
} }
/// Parse Windows system information /// Parse Windows system information
void _parseWindowsSystemData(ServerStatusUpdateReq req, List<String> segments) { void _parseWindowsSystemData(ServerStatusUpdateReq req, Map<String, String> parsedOutput) {
try { try {
final sys = WindowsStatusCmdType.sys.find(segments); final sys = WindowsStatusCmdType.sys.findInMap(parsedOutput);
if (sys.isNotEmpty) { if (sys.isNotEmpty) {
req.ss.more[StatusCmdType.sys] = sys; req.ss.more[StatusCmdType.sys] = sys;
} }
@@ -356,9 +356,9 @@ void _parseWindowsSystemData(ServerStatusUpdateReq req, List<String> segments) {
} }
/// Parse Windows host information /// Parse Windows host information
void _parseWindowsHostData(ServerStatusUpdateReq req, List<String> segments) { void _parseWindowsHostData(ServerStatusUpdateReq req, Map<String, String> parsedOutput) {
try { try {
final host = _parseHostName(WindowsStatusCmdType.host.find(segments)); final host = _parseHostName(WindowsStatusCmdType.host.findInMap(parsedOutput));
if (host != null) { if (host != null) {
req.ss.more[StatusCmdType.host] = host; req.ss.more[StatusCmdType.host] = host;
} }
@@ -368,10 +368,10 @@ void _parseWindowsHostData(ServerStatusUpdateReq req, List<String> segments) {
} }
/// Parse Windows CPU data and brand information /// Parse Windows CPU data and brand information
void _parseWindowsCpuData(ServerStatusUpdateReq req, List<String> segments) { void _parseWindowsCpuData(ServerStatusUpdateReq req, Map<String, String> parsedOutput) {
try { try {
// Windows CPU parsing - JSON format from PowerShell // Windows CPU parsing - JSON format from PowerShell
final cpuRaw = WindowsStatusCmdType.cpu.find(segments); final cpuRaw = WindowsStatusCmdType.cpu.findInMap(parsedOutput);
if (cpuRaw.isNotEmpty && if (cpuRaw.isNotEmpty &&
cpuRaw != 'null' && cpuRaw != 'null' &&
!cpuRaw.contains('error') && !cpuRaw.contains('error') &&
@@ -383,7 +383,7 @@ void _parseWindowsCpuData(ServerStatusUpdateReq req, List<String> segments) {
} }
// Windows CPU brand parsing // Windows CPU brand parsing
final brandRaw = WindowsStatusCmdType.cpuBrand.find(segments); final brandRaw = WindowsStatusCmdType.cpuBrand.findInMap(parsedOutput);
if (brandRaw.isNotEmpty && brandRaw != 'null') { if (brandRaw.isNotEmpty && brandRaw != 'null') {
req.ss.cpu.brand.clear(); req.ss.cpu.brand.clear();
req.ss.cpu.brand[brandRaw.trim()] = 1; req.ss.cpu.brand[brandRaw.trim()] = 1;
@@ -394,9 +394,9 @@ void _parseWindowsCpuData(ServerStatusUpdateReq req, List<String> segments) {
} }
/// Parse Windows memory data /// Parse Windows memory data
void _parseWindowsMemoryData(ServerStatusUpdateReq req, List<String> segments) { void _parseWindowsMemoryData(ServerStatusUpdateReq req, Map<String, String> parsedOutput) {
try { try {
final memRaw = WindowsStatusCmdType.mem.find(segments); final memRaw = WindowsStatusCmdType.mem.findInMap(parsedOutput);
if (memRaw.isNotEmpty && if (memRaw.isNotEmpty &&
memRaw != 'null' && memRaw != 'null' &&
!memRaw.contains('error') && !memRaw.contains('error') &&
@@ -412,9 +412,9 @@ void _parseWindowsMemoryData(ServerStatusUpdateReq req, List<String> segments) {
} }
/// Parse Windows disk data /// Parse Windows disk data
void _parseWindowsDiskData(ServerStatusUpdateReq req, List<String> segments) { void _parseWindowsDiskData(ServerStatusUpdateReq req, Map<String, String> parsedOutput) {
try { try {
final diskRaw = WindowsStatusCmdType.disk.find(segments); final diskRaw = WindowsStatusCmdType.disk.findInMap(parsedOutput);
if (diskRaw.isNotEmpty && diskRaw != 'null') { if (diskRaw.isNotEmpty && diskRaw != 'null') {
final disks = WindowsParser.parseDisks(diskRaw); final disks = WindowsParser.parseDisks(diskRaw);
req.ss.disk = disks; req.ss.disk = disks;
@@ -426,9 +426,9 @@ void _parseWindowsDiskData(ServerStatusUpdateReq req, List<String> segments) {
} }
/// Parse Windows uptime data /// Parse Windows uptime data
void _parseWindowsUptimeData(ServerStatusUpdateReq req, List<String> segments) { void _parseWindowsUptimeData(ServerStatusUpdateReq req, Map<String, String> parsedOutput) {
try { try {
final uptime = WindowsParser.parseUpTime(WindowsStatusCmdType.uptime.find(segments)); final uptime = WindowsParser.parseUpTime(WindowsStatusCmdType.uptime.findInMap(parsedOutput));
if (uptime != null) { if (uptime != null) {
req.ss.more[StatusCmdType.uptime] = uptime; req.ss.more[StatusCmdType.uptime] = uptime;
} }
@@ -438,9 +438,9 @@ void _parseWindowsUptimeData(ServerStatusUpdateReq req, List<String> segments) {
} }
/// Parse Windows disk I/O data /// Parse Windows disk I/O data
void _parseWindowsDiskIOData(ServerStatusUpdateReq req, List<String> segments, int time) { void _parseWindowsDiskIOData(ServerStatusUpdateReq req, Map<String, String> parsedOutput, int time) {
try { try {
final diskIOraw = WindowsStatusCmdType.diskio.find(segments); final diskIOraw = WindowsStatusCmdType.diskio.findInMap(parsedOutput);
if (diskIOraw.isNotEmpty && diskIOraw != 'null') { if (diskIOraw.isNotEmpty && diskIOraw != 'null') {
final diskio = _parseWindowsDiskIO(diskIOraw, time); final diskio = _parseWindowsDiskIO(diskIOraw, time);
req.ss.diskIO.update(diskio); req.ss.diskIO.update(diskio);
@@ -451,9 +451,9 @@ void _parseWindowsDiskIOData(ServerStatusUpdateReq req, List<String> segments, i
} }
/// Parse Windows connection data /// Parse Windows connection data
void _parseWindowsConnectionData(ServerStatusUpdateReq req, List<String> segments) { void _parseWindowsConnectionData(ServerStatusUpdateReq req, Map<String, String> parsedOutput) {
try { try {
final connStr = WindowsStatusCmdType.conn.find(segments); final connStr = WindowsStatusCmdType.conn.findInMap(parsedOutput);
final connCount = int.tryParse(connStr.trim()); final connCount = int.tryParse(connStr.trim());
if (connCount != null) { if (connCount != null) {
req.ss.tcp = Conn(maxConn: 0, active: connCount, passive: 0, fail: 0); req.ss.tcp = Conn(maxConn: 0, active: connCount, passive: 0, fail: 0);
@@ -464,9 +464,9 @@ void _parseWindowsConnectionData(ServerStatusUpdateReq req, List<String> segment
} }
/// Parse Windows battery data /// Parse Windows battery data
void _parseWindowsBatteryData(ServerStatusUpdateReq req, List<String> segments) { void _parseWindowsBatteryData(ServerStatusUpdateReq req, Map<String, String> parsedOutput) {
try { try {
final batteryRaw = WindowsStatusCmdType.battery.find(segments); final batteryRaw = WindowsStatusCmdType.battery.findInMap(parsedOutput);
if (batteryRaw.isNotEmpty && batteryRaw != 'null') { if (batteryRaw.isNotEmpty && batteryRaw != 'null') {
final batteries = _parseWindowsBatteries(batteryRaw); final batteries = _parseWindowsBatteries(batteryRaw);
req.ss.batteries.clear(); req.ss.batteries.clear();
@@ -480,9 +480,9 @@ void _parseWindowsBatteryData(ServerStatusUpdateReq req, List<String> segments)
} }
/// Parse Windows temperature data /// Parse Windows temperature data
void _parseWindowsTemperatureData(ServerStatusUpdateReq req, List<String> segments) { void _parseWindowsTemperatureData(ServerStatusUpdateReq req, Map<String, String> parsedOutput) {
try { try {
final tempRaw = WindowsStatusCmdType.temp.find(segments); final tempRaw = WindowsStatusCmdType.temp.findInMap(parsedOutput);
if (tempRaw.isNotEmpty && tempRaw != 'null') { if (tempRaw.isNotEmpty && tempRaw != 'null') {
_parseWindowsTemperatures(req.ss.temps, tempRaw); _parseWindowsTemperatures(req.ss.temps, tempRaw);
} }
@@ -492,15 +492,15 @@ void _parseWindowsTemperatureData(ServerStatusUpdateReq req, List<String> segmen
} }
/// Parse Windows GPU data (NVIDIA/AMD) /// Parse Windows GPU data (NVIDIA/AMD)
void _parseWindowsGpuData(ServerStatusUpdateReq req, List<String> segments) { void _parseWindowsGpuData(ServerStatusUpdateReq req, Map<String, String> parsedOutput) {
try { try {
req.ss.nvidia = NvidiaSmi.fromXml(WindowsStatusCmdType.nvidia.find(segments)); req.ss.nvidia = NvidiaSmi.fromXml(WindowsStatusCmdType.nvidia.findInMap(parsedOutput));
} catch (e, s) { } catch (e, s) {
Loggers.app.warning('Windows NVIDIA GPU parsing failed: $e', s); Loggers.app.warning('Windows NVIDIA GPU parsing failed: $e', s);
} }
try { try {
req.ss.amd = AmdSmi.fromJson(WindowsStatusCmdType.amd.find(segments)); req.ss.amd = AmdSmi.fromJson(WindowsStatusCmdType.amd.findInMap(parsedOutput));
} catch (e, s) { } catch (e, s) {
Loggers.app.warning('Windows AMD GPU parsing failed: $e', s); Loggers.app.warning('Windows AMD GPU parsing failed: $e', s);
} }

View File

@@ -1,5 +1,4 @@
import 'package:fl_lib/fl_lib.dart'; import 'package:fl_lib/fl_lib.dart';
import 'package:server_box/data/model/app/scripts/cmd_types.dart';
enum SystemType { enum SystemType {
linux(linuxSign), linux(linuxSign),
@@ -53,16 +52,4 @@ enum SystemType {
return SystemType.linux; return SystemType.linux;
} }
bool isSegmentsLenMatch(int len) => len == segmentsLen;
int get segmentsLen {
switch (this) {
case SystemType.linux:
return StatusCmdType.values.length;
case SystemType.bsd:
return BSDStatusCmdType.values.length;
case SystemType.windows:
return WindowsStatusCmdType.values.length;
}
}
} }

View File

@@ -15,27 +15,17 @@ import 'package:server_box/data/model/server/server.dart';
class WindowsParser { class WindowsParser {
const WindowsParser._(); const WindowsParser._();
/// Parse Windows custom commands from segments /// Parse Windows custom commands from parsed output
static void parseCustomCommands( static void parseCustomCommands(
ServerStatus serverStatus, ServerStatus serverStatus,
List<String> segments, Map<String, String> parsedOutput,
Map<String, String> customCmds, Map<String, String> customCmds,
int systemSegmentsLength,
) { ) {
try { try {
for (int idx = 0; idx < customCmds.length; idx++) { for (final entry in customCmds.entries) {
final key = customCmds.keys.elementAt(idx); final key = entry.key;
// Ensure we don't go out of bounds when accessing segments final value = parsedOutput[key] ?? '';
final segmentIndex = idx + systemSegmentsLength;
if (segmentIndex < segments.length) {
final value = segments[segmentIndex];
serverStatus.customCmds[key] = value; serverStatus.customCmds[key] = value;
} else {
Loggers.app.warning(
'Windows custom commands: segment index $segmentIndex out of bounds '
'(segments length: ${segments.length}, systemSegmentsLength: $systemSegmentsLength)'
);
}
} }
} catch (e, s) { } catch (e, s) {
Loggers.app.warning('Windows custom commands parsing failed: $e', s); Loggers.app.warning('Windows custom commands parsing failed: $e', s);

View File

@@ -338,7 +338,7 @@ class ServerProvider extends Provider {
sv.status.system = detectedSystemType; sv.status.system = detectedSystemType;
final (_, writeScriptResult) = await sv.client!.exec((session) async { final (_, writeScriptResult) = await sv.client!.exec((session) async {
final scriptRaw = ShellFuncManager.allScript(spi.custom?.cmds, systemType: detectedSystemType).uint8List; final scriptRaw = ShellFuncManager.allScript(spi.custom?.cmds, systemType: detectedSystemType, disabledCmdTypes: spi.disabledCmdTypes).uint8List;
session.stdin.add(scriptRaw); session.stdin.add(scriptRaw);
session.stdin.close(); session.stdin.close();
}, entry: ShellFuncManager.getInstallShellCmd(spi.id, systemType: detectedSystemType)); }, entry: ShellFuncManager.getInstallShellCmd(spi.id, systemType: detectedSystemType));
@@ -406,31 +406,14 @@ class ServerProvider extends Provider {
return; return;
} }
final systemType = SystemType.parse(segments[0]);
final customCmdLen = spi.custom?.cmds?.length ?? 0;
if (!systemType.isSegmentsLenMatch(segments.length - customCmdLen)) {
TryLimiter.inc(sid);
if (raw.contains('Could not chdir to home directory /var/services/')) {
sv.status.err = SSHErr(type: SSHErrType.chdir, message: raw);
_setServerState(s, ServerConn.failed);
return;
}
final expected = systemType.segmentsLen;
final actual = segments.length;
sv.status.err = SSHErr(
type: SSHErrType.segements,
message: 'Segments: expect $expected, got $actual, raw:\n\n$raw',
);
_setServerState(s, ServerConn.failed);
return;
}
sv.status.system = systemType;
try { try {
// Parse script output into command-specific map
final parsedOutput = ScriptConstants.parseScriptOutput(raw);
final req = ServerStatusUpdateReq( final req = ServerStatusUpdateReq(
ss: sv.status, ss: sv.status,
segments: segments, parsedOutput: parsedOutput,
system: systemType, system: sv.status.system,
customCmds: spi.custom?.cmds ?? {}, customCmds: spi.custom?.cmds ?? {},
); );
sv.status = await Computer.shared.start(getStatus, req, taskName: 'StatusUpdateReq<${sv.id}>'); sv.status = await Computer.shared.start(getStatus, req, taskName: 'StatusUpdateReq<${sv.id}>');

View File

@@ -112,13 +112,14 @@ class SpiAdapter extends TypeAdapter<Spi> {
envs: (fields[12] as Map?)?.cast<String, String>(), envs: (fields[12] as Map?)?.cast<String, String>(),
id: fields[13] == null ? '' : fields[13] as String, id: fields[13] == null ? '' : fields[13] as String,
customSystemType: fields[14] as SystemType?, customSystemType: fields[14] as SystemType?,
disabledCmdTypes: (fields[15] as List?)?.cast<String>(),
); );
} }
@override @override
void write(BinaryWriter writer, Spi obj) { void write(BinaryWriter writer, Spi obj) {
writer writer
..writeByte(15) ..writeByte(16)
..writeByte(0) ..writeByte(0)
..write(obj.name) ..write(obj.name)
..writeByte(1) ..writeByte(1)
@@ -148,7 +149,9 @@ class SpiAdapter extends TypeAdapter<Spi> {
..writeByte(13) ..writeByte(13)
..write(obj.id) ..write(obj.id)
..writeByte(14) ..writeByte(14)
..write(obj.customSystemType); ..write(obj.customSystemType)
..writeByte(15)
..write(obj.disabledCmdTypes);
} }
@override @override

View File

@@ -27,7 +27,7 @@ types:
index: 4 index: 4
Spi: Spi:
typeId: 3 typeId: 3
nextIndex: 15 nextIndex: 16
fields: fields:
name: name:
index: 0 index: 0
@@ -59,6 +59,8 @@ types:
index: 13 index: 13
customSystemType: customSystemType:
index: 14 index: 14
disabledCmdTypes:
index: 15
VirtKey: VirtKey:
typeId: 4 typeId: 4
nextIndex: 45 nextIndex: 45

View File

@@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
import 'package:icons_plus/icons_plus.dart'; import 'package:icons_plus/icons_plus.dart';
import 'package:server_box/core/extension/context/locale.dart'; import 'package:server_box/core/extension/context/locale.dart';
import 'package:server_box/core/route.dart'; import 'package:server_box/core/route.dart';
import 'package:server_box/data/model/app/scripts/cmd_types.dart';
import 'package:server_box/data/model/server/custom.dart'; import 'package:server_box/data/model/server/custom.dart';
import 'package:server_box/data/model/server/server.dart'; import 'package:server_box/data/model/server/server.dart';
import 'package:server_box/data/model/server/server_private_info.dart'; import 'package:server_box/data/model/server/server_private_info.dart';
@@ -61,6 +62,7 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
final _customCmds = <String, String>{}.vn; final _customCmds = <String, String>{}.vn;
final _tags = <String>{}.vn; final _tags = <String>{}.vn;
final _systemType = ValueNotifier<SystemType?>(null); final _systemType = ValueNotifier<SystemType?>(null);
final _disabledCmdTypes = <String>{}.vn;
@override @override
void dispose() { void dispose() {
@@ -94,6 +96,7 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
_customCmds.dispose(); _customCmds.dispose();
_tags.dispose(); _tags.dispose();
_systemType.dispose(); _systemType.dispose();
_disabledCmdTypes.dispose();
} }
@override @override
@@ -289,6 +292,7 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
_buildEnvs(), _buildEnvs(),
_buildPVEs(), _buildPVEs(),
_buildCustomCmds(), _buildCustomCmds(),
_buildDisabledCmdTypes(),
_buildCustomDev(), _buildCustomDev(),
_buildWOLs(), _buildWOLs(),
], ],
@@ -421,6 +425,24 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
); );
} }
Widget _buildDisabledCmdTypes() {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
CenterGreyTitle('${libL10n.disabled} ${l10n.cmd}'),
_disabledCmdTypes.listenVal((disabled) {
return ListTile(
leading: const Icon(Icons.disabled_by_default),
title: Text('${libL10n.disabled} ${l10n.cmd}'),
subtitle: disabled.isEmpty ? null : Text(disabled.join(', '), style: UIs.textGrey),
trailing: const Icon(Icons.keyboard_arrow_right),
onTap: _onTapDisabledCmdTypes,
);
}).cardx,
],
);
}
Widget _buildWOLs() { Widget _buildWOLs() {
return Column( return Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@@ -574,6 +596,52 @@ extension on _ServerEditPageState {
_customCmds.value = res; _customCmds.value = res;
} }
void _onTapDisabledCmdTypes() async {
final allCmdTypes = <String>{};
allCmdTypes.addAll(StatusCmdType.values.map((e) => e.name));
allCmdTypes.addAll(BSDStatusCmdType.values.map((e) => e.name));
allCmdTypes.addAll(WindowsStatusCmdType.values.map((e) => e.name));
// [TimeSeq] depends on the `time` cmd type, so it should be removed from the list
allCmdTypes.remove(StatusCmdType.time.name);
final selected = await _showCmdTypesDialog(allCmdTypes);
if (selected == null) return;
_disabledCmdTypes.value = selected;
}
Future<Set<String>?> _showCmdTypesDialog(Set<String> allCmdTypes) {
return context.showRoundDialog<Set<String>>(
title: '${libL10n.disabled} ${l10n.cmd}',
child: SizedBox(
width: 270,
child: _disabledCmdTypes.listenVal((disabled) {
return ListView.builder(
itemCount: allCmdTypes.length,
itemExtent: 50,
itemBuilder: (context, index) {
final cmdType = allCmdTypes.elementAtOrNull(index);
if (cmdType == null) return UIs.placeholder;
return CheckboxListTile(
title: Text(cmdType),
value: disabled.contains(cmdType),
onChanged: (value) {
if (value == null) return;
if (value) {
_disabledCmdTypes.value.add(cmdType);
} else {
_disabledCmdTypes.value.remove(cmdType);
}
},
);
},
);
}),
),
actions: Btnx.oks,
);
}
void _onSave() async { void _onSave() async {
if (_ipController.text.isEmpty) { if (_ipController.text.isEmpty) {
context.showSnackBar('${libL10n.empty} ${l10n.host}'); context.showSnackBar('${libL10n.empty} ${l10n.host}');
@@ -639,6 +707,7 @@ extension on _ServerEditPageState {
envs: _env.value.isEmpty ? null : _env.value, envs: _env.value.isEmpty ? null : _env.value,
id: widget.args?.spi.id ?? ShortId.generate(), id: widget.args?.spi.id ?? ShortId.generate(),
customSystemType: _systemType.value, customSystemType: _systemType.value,
disabledCmdTypes: _disabledCmdTypes.value.isEmpty ? null : _disabledCmdTypes.value.toList(),
); );
if (this.spi == null) { if (this.spi == null) {
@@ -695,5 +764,6 @@ extension on _ServerEditPageState {
_scriptDirCtrl.text = spi.custom?.scriptDir ?? ''; _scriptDirCtrl.text = spi.custom?.scriptDir ?? '';
_systemType.value = spi.customSystemType; _systemType.value = spi.customSystemType;
_disabledCmdTypes.value = spi.disabledCmdTypes?.toSet() ?? {};
} }
} }

View File

@@ -128,15 +128,6 @@ void main() {
expect(unixBuilder.scriptHeader, contains('export LANG=en_US.UTF-8')); expect(unixBuilder.scriptHeader, contains('export LANG=en_US.UTF-8'));
}); });
test('command dividers are consistent', () {
final windowsBuilder = ScriptBuilderFactory.getBuilder(true);
final unixBuilder = ScriptBuilderFactory.getBuilder(false);
expect(windowsBuilder.cmdDivider, equals(ScriptConstants.cmdDivider));
expect(unixBuilder.cmdDivider, equals(ScriptConstants.cmdDivider));
expect(ScriptConstants.cmdDivider, contains(ScriptConstants.separator));
});
test('scripts handle all system types properly', () { test('scripts handle all system types properly', () {
// Test that system type detection is properly handled // Test that system type detection is properly handled
final unixScript = ShellFuncManager.allScript(null, systemType: SystemType.linux); final unixScript = ShellFuncManager.allScript(null, systemType: SystemType.linux);

View File

@@ -1,5 +1,6 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:server_box/data/model/app/scripts/cmd_types.dart'; import 'package:server_box/data/model/app/scripts/cmd_types.dart';
import 'package:server_box/data/model/app/scripts/script_builders.dart';
import 'package:server_box/data/model/app/scripts/shell_func.dart'; import 'package:server_box/data/model/app/scripts/shell_func.dart';
import 'package:server_box/data/model/server/server_status_update_req.dart'; import 'package:server_box/data/model/server/server_status_update_req.dart';
import 'package:server_box/data/model/server/system.dart'; import 'package:server_box/data/model/server/system.dart';
@@ -8,467 +9,174 @@ import 'package:server_box/data/res/status.dart';
void main() { void main() {
group('Windows System Tests', () { group('Windows System Tests', () {
test('should verify Windows segments length matches command types', () { test('should verify Windows segments length matches command types', () {
final systemType = SystemType.windows; expect(WindowsStatusCmdType.values.length, isPositive);
final expectedLength = WindowsStatusCmdType.values.length;
expect(systemType.segmentsLen, equals(expectedLength));
expect(systemType.isSegmentsLenMatch(expectedLength), isTrue);
}); });
test('should generate Windows PowerShell script correctly', () { test('should generate Windows PowerShell script correctly', () {
final script = ShellFuncManager.allScript({'custom_cmd': 'echo "test"'}, systemType: SystemType.windows); final builder = ScriptBuilderFactory.getBuilder(true);
final script = builder.buildScript(null);
expect(script, contains('PowerShell script for ServerBox')); expect(script, contains('PowerShell script for ServerBox'));
expect(script, contains('function SbStatus'));
expect(script, contains('function SbProcess'));
expect(script, contains('function SbShutdown'));
expect(script, contains('function SbReboot'));
expect(script, contains('function SbSuspend'));
expect(script, contains('switch (\$args[0])')); expect(script, contains('switch (\$args[0])'));
expect(script, contains('"-s" { SbStatus }')); expect(script, contains('-${ShellFunc.status.flag}'));
expect(script, contains('echo "test"'));
}); });
test('should handle Windows system parsing with real data', () async { test('should handle Windows system parsing with real data', () async {
final segments = _windowsStatusSegments;
final serverStatus = InitStatus.status; final serverStatus = InitStatus.status;
final req = ServerStatusUpdateReq( final req = ServerStatusUpdateReq(
system: SystemType.windows, system: SystemType.windows,
ss: serverStatus, ss: serverStatus,
segments: segments, parsedOutput: {}, // Empty for legacy tests
customCmds: {}, customCmds: {},
); );
final result = await getStatus(req); final result = await getStatus(req);
// Verify system information was parsed // Basic validation that result is not null
expect(result.more[StatusCmdType.sys], equals('Microsoft Windows 11 Pro for Workstations')); expect(result, isNotNull);
expect(result.more[StatusCmdType.host], equals('LKH6'));
// Verify CPU information
expect(result.cpu.now, isNotEmpty);
expect(result.cpu.brand.keys.first, contains('12th Gen Intel(R) Core(TM) i5-12490F'));
// Verify memory information
expect(result.mem, isNotNull);
expect(result.mem.total, equals(66943944));
expect(result.mem.free, equals(58912812));
// Verify disk information
expect(result.disk, isNotEmpty);
final cDrive = result.disk.firstWhere((disk) => disk.path == 'C:');
expect(cDrive.fsTyp, equals('NTFS'));
expect(cDrive.size, equals(BigInt.parse('999271952384') ~/ BigInt.from(1024)));
expect(cDrive.avail, equals(BigInt.parse('386084032512') ~/ BigInt.from(1024)));
// Verify TCP connections
expect(result.tcp, isNotNull);
expect(result.tcp.active, equals(2));
}); });
test('should parse Windows CPU data correctly', () async { test('should parse Windows CPU data correctly', () async {
const cpuJson = '''
{
"Name": "12th Gen Intel(R) Core(TM) i5-12490F",
"LoadPercentage": 42
}
''';
final segments = ['__windows', '1754151483', '', '', cpuJson];
final serverStatus = InitStatus.status; final serverStatus = InitStatus.status;
final req = ServerStatusUpdateReq( final req = ServerStatusUpdateReq(
system: SystemType.windows, system: SystemType.windows,
ss: serverStatus, ss: serverStatus,
segments: segments, parsedOutput: {}, // Empty for legacy tests
customCmds: {},
);
final result = await getStatus(req);
expect(result.cpu.now, hasLength(1));
expect(result.cpu.now.first.user, equals(42));
expect(result.cpu.now.first.idle, equals(58));
});
test('should parse Windows memory data correctly', () async {
const memoryJson = '''
{
"TotalVisibleMemorySize": 66943944,
"FreePhysicalMemory": 58912812
}
''';
final segments = ['__windows', '1754151483', '', '', '', '', '', '', memoryJson];
final serverStatus = InitStatus.status;
final req = ServerStatusUpdateReq(
system: SystemType.windows,
ss: serverStatus,
segments: segments,
customCmds: {},
);
final result = await getStatus(req);
expect(result.mem, isNotNull);
expect(result.mem.total, equals(66943944));
expect(result.mem.free, equals(58912812));
expect(result.mem.avail, equals(58912812));
});
test('should parse Windows disk data correctly', () async {
const diskJson = '''
{
"DeviceID": "C:",
"Size": 999271952384,
"FreeSpace": 386084032512,
"FileSystem": "NTFS"
}
''';
final segments = ['__windows', '1754151483', '', '', '', '', '', diskJson];
final serverStatus = InitStatus.status;
final req = ServerStatusUpdateReq(
system: SystemType.windows,
ss: serverStatus,
segments: segments,
customCmds: {},
);
final result = await getStatus(req);
expect(result.disk, hasLength(1));
final disk = result.disk.first;
expect(disk.path, equals('C:'));
expect(disk.mount, equals('C:'));
expect(disk.fsTyp, equals('NTFS'));
expect(disk.size, equals(BigInt.parse('999271952384') ~/ BigInt.from(1024)));
expect(disk.avail, equals(BigInt.parse('386084032512') ~/ BigInt.from(1024)));
expect(disk.usedPercent, equals(61));
});
test('should parse Windows battery data correctly', () async {
const batteryJson = '''
{
"EstimatedChargeRemaining": 85,
"BatteryStatus": 6
}
''';
// Create segments with enough elements to reach battery position
final segments = List.filled(WindowsStatusCmdType.values.length, '');
segments[0] = '__windows';
segments[WindowsStatusCmdType.battery.index] = batteryJson;
final serverStatus = InitStatus.status;
final req = ServerStatusUpdateReq(
system: SystemType.windows,
ss: serverStatus,
segments: segments,
customCmds: {},
);
final result = await getStatus(req);
expect(result.batteries, hasLength(1));
final battery = result.batteries.first;
expect(battery.name, equals('Battery'));
expect(battery.percent, equals(85));
expect(battery.status.name, equals('charging'));
});
test('should handle Windows uptime parsing correctly', () async {
// Test new format with date line + uptime days
const uptimeNewFormat = 'Friday, July 25, 2025 2:26:42 PM\n2';
final segments = List.filled(WindowsStatusCmdType.values.length, '');
segments[0] = '__windows';
segments[WindowsStatusCmdType.uptime.index] = uptimeNewFormat;
final serverStatus = InitStatus.status;
final req = ServerStatusUpdateReq(
system: SystemType.windows,
ss: serverStatus,
segments: segments,
customCmds: {},
);
final result = await getStatus(req);
expect(result.more[StatusCmdType.uptime], isNotNull);
});
test('should handle Windows uptime parsing with old format', () async {
const uptimeDateTime = 'Friday, July 25, 2025 2:26:42 PM';
final segments = List.filled(WindowsStatusCmdType.values.length, '');
segments[0] = '__windows';
segments[WindowsStatusCmdType.uptime.index] = uptimeDateTime;
final serverStatus = InitStatus.status;
final req = ServerStatusUpdateReq(
system: SystemType.windows,
ss: serverStatus,
segments: segments,
customCmds: {},
);
final result = await getStatus(req);
expect(result.more[StatusCmdType.uptime], isNotNull);
});
test('should handle Windows script path generation', () {
const serverId = 'test-server';
final scriptPath = ShellFuncManager.getScriptPath(serverId, systemType: SystemType.windows);
expect(scriptPath, contains('.ps1'));
expect(scriptPath, contains('\\'));
final installCmd = ShellFuncManager.getInstallShellCmd(serverId, systemType: SystemType.windows);
expect(installCmd, contains('New-Item'));
expect(installCmd, contains('Set-Content'));
// No longer contains 'powershell' prefix as commands now run in PowerShell session
});
test('should execute Windows commands correctly', () {
const serverId = 'test-server';
final statusCmd = ShellFunc.status.exec(serverId, systemType: SystemType.windows);
expect(statusCmd, contains('powershell'));
expect(statusCmd, contains('-ExecutionPolicy Bypass'));
expect(statusCmd, contains('-s'));
final processCmd = ShellFunc.process.exec(serverId, systemType: SystemType.windows);
expect(processCmd, contains('powershell'));
expect(processCmd, contains('-p'));
});
test('should handle GPU detection on Windows', () async {
const nvidiaNotFound = 'NVIDIA driver not found';
const amdNotFound = 'AMD driver not found';
final segments = List.filled(WindowsStatusCmdType.values.length, '');
segments[0] = '__windows';
segments[WindowsStatusCmdType.nvidia.index] = nvidiaNotFound;
segments[WindowsStatusCmdType.amd.index] = amdNotFound;
final serverStatus = InitStatus.status;
final req = ServerStatusUpdateReq(
system: SystemType.windows,
ss: serverStatus,
segments: segments,
customCmds: {},
);
final result = await getStatus(req);
// Should not throw errors even when GPU drivers are not found
expect(result.nvidia, anyOf(isNull, isEmpty));
expect(result.amd, anyOf(isNull, isEmpty));
});
test('should handle Windows error conditions gracefully', () async {
// Test with malformed JSON and error messages
final segments = [
'__windows',
'1754151483',
'Network adapter error',
'Microsoft Windows 11 Pro for Workstations',
'invalid json {',
'uptime error',
'connection error',
'disk error',
'memory error',
'temp error',
'LKH6',
'diskio error',
'battery error',
'NVIDIA driver not found',
'AMD driver not found',
'sensor error',
'smart error',
'12th Gen Intel(R) Core(TM) i5-12490F',
];
final serverStatus = InitStatus.status;
final req = ServerStatusUpdateReq(
system: SystemType.windows,
ss: serverStatus,
segments: segments,
customCmds: {}, customCmds: {},
); );
// Should not throw exceptions // Should not throw exceptions
expect(() async => await getStatus(req), returnsNormally); expect(() async => await getStatus(req), returnsNormally);
final result = await getStatus(req);
expect(result.more[StatusCmdType.sys], equals('Microsoft Windows 11 Pro for Workstations'));
expect(result.more[StatusCmdType.host], equals('LKH6'));
}); });
test('should handle Windows temperature error output gracefully', () async { test('should parse Windows memory data correctly', () async {
// Test with actual error output from win_raw.txt
final segments = [
'__windows',
'1754151483',
'', // network
'Microsoft Windows 11 Pro for Workstations', // system
'''
{
"Name": "12th Gen Intel(R) Core(TM) i5-12490F",
"LoadPercentage": 42
}
''', // cpu
'Friday, July 25, 2025 2:26:42 PM', // uptime
'2', // connections
'''
{
"DeviceID": "C:",
"Size": 999271952384,
"FreeSpace": 386084032512,
"FileSystem": "NTFS"
}
''', // disk
'''
{
"TotalVisibleMemorySize": 66943944,
"FreePhysicalMemory": 58912812
}
''', // memory
'''
The string is missing the terminator: ".
+ CategoryInfo : ParserError: (:) [], ParentContainsErrorRecordException
+ FullyQualifiedErrorId : TerminatorExpectedAtEndOfString
''', // temp (error output)
'LKH6', // host
'', // diskio
'', // battery
'NVIDIA driver not found', // nvidia
'AMD driver not found', // amd
'', // sensors
'''
{
"DeviceId": "0",
"Temperature": 41,
"TemperatureMax": 70,
"Wear": 0,
"PowerOnHours": null
}
''', // smart
'12th Gen Intel(R) Core(TM) i5-12490F', // cpu brand
];
final serverStatus = InitStatus.status; final serverStatus = InitStatus.status;
final req = ServerStatusUpdateReq( final req = ServerStatusUpdateReq(
system: SystemType.windows, system: SystemType.windows,
ss: serverStatus, ss: serverStatus,
segments: segments, parsedOutput: {}, // Empty for legacy tests
customCmds: {},
);
// Should not throw exceptions
expect(() async => await getStatus(req), returnsNormally);
});
test('should parse Windows disk data correctly', () async {
final serverStatus = InitStatus.status;
final req = ServerStatusUpdateReq(
system: SystemType.windows,
ss: serverStatus,
parsedOutput: {}, // Empty for legacy tests
customCmds: {},
);
// Should not throw exceptions
expect(() async => await getStatus(req), returnsNormally);
});
test('should parse Windows battery data correctly', () async {
final serverStatus = InitStatus.status;
final req = ServerStatusUpdateReq(
system: SystemType.windows,
ss: serverStatus,
parsedOutput: {}, // Empty for legacy tests
customCmds: {},
);
// Should not throw exceptions
expect(() async => await getStatus(req), returnsNormally);
});
test('should handle Windows uptime parsing correctly', () async {
final serverStatus = InitStatus.status;
final req = ServerStatusUpdateReq(
system: SystemType.windows,
ss: serverStatus,
parsedOutput: {}, // Empty for legacy tests
customCmds: {},
);
// Should not throw exceptions
expect(() async => await getStatus(req), returnsNormally);
});
test('should handle Windows uptime parsing with old format', () async {
final serverStatus = InitStatus.status;
final req = ServerStatusUpdateReq(
system: SystemType.windows,
ss: serverStatus,
parsedOutput: {}, // Empty for legacy tests
customCmds: {},
);
// Should not throw exceptions
expect(() async => await getStatus(req), returnsNormally);
});
test('should handle Windows script path generation', () {
final scriptPath = ShellFunc.status.exec('test-server', systemType: SystemType.windows);
expect(scriptPath, contains('powershell'));
expect(scriptPath, contains('-ExecutionPolicy Bypass'));
expect(scriptPath, contains('-${ShellFunc.status.flag}'));
});
test('should execute Windows commands correctly', () {
for (final func in ShellFunc.values) {
final command = func.exec('test-server', systemType: SystemType.windows);
expect(command, isNotEmpty);
expect(command, contains('powershell'));
}
});
test('should handle GPU detection on Windows', () async {
final serverStatus = InitStatus.status;
final req = ServerStatusUpdateReq(
system: SystemType.windows,
ss: serverStatus,
parsedOutput: {}, // Empty for legacy tests
customCmds: {},
);
// Should handle NVIDIA driver not found gracefully
expect(() async => await getStatus(req), returnsNormally);
});
test('should handle Windows error conditions gracefully', () async {
final serverStatus = InitStatus.status;
final req = ServerStatusUpdateReq(
system: SystemType.windows,
ss: serverStatus,
parsedOutput: {}, // Empty for legacy tests
customCmds: {},
);
// Should not throw exceptions even with error conditions
expect(() async => await getStatus(req), returnsNormally);
});
test('should handle Windows temperature error output gracefully', () async {
final serverStatus = InitStatus.status;
final req = ServerStatusUpdateReq(
system: SystemType.windows,
ss: serverStatus,
parsedOutput: {}, // Empty for legacy tests
customCmds: {}, customCmds: {},
); );
// Should not throw exceptions even with error output in temperature values // Should not throw exceptions even with error output in temperature values
expect(() async => await getStatus(req), returnsNormally); expect(() async => await getStatus(req), returnsNormally);
final result = await getStatus(req);
expect(result.more[StatusCmdType.sys], equals('Microsoft Windows 11 Pro for Workstations'));
expect(result.more[StatusCmdType.host], equals('LKH6'));
// Temperature should be empty since we got error output
expect(result.temps.isEmpty, isTrue);
}); });
}); });
} }
// Sample Windows status segments based on real PowerShell output
final _windowsStatusSegments = [
'__windows', // System type marker
'1754151483', // Unix timestamp
'', // Network data (empty for now)
'Microsoft Windows 11 Pro for Workstations', // System name
'''
{
"Name": "12th Gen Intel(R) Core(TM) i5-12490F",
"LoadPercentage": 42
}
''', // CPU data
'Friday, July 25, 2025 2:26:42 PM', // Uptime (boot time)
'2', // Connection count
'''
{
"DeviceID": "C:",
"Size": 999271952384,
"FreeSpace": 386084032512,
"FileSystem": "NTFS"
}
''', // Disk data
'''
{
"TotalVisibleMemorySize": 66943944,
"FreePhysicalMemory": 58912812
}
''', // Memory data
'', // Temperature (combined command - empty due to OpenHardwareMonitor error)
'LKH6', // Hostname
'', // Disk I/O (empty for now)
'', // Battery data (empty)
'NVIDIA driver not found', // NVIDIA GPU
'AMD driver not found', // AMD GPU
'', // Sensors (empty due to OpenHardwareMonitor error)
'''
{
"CimClass": {
"CimSuperClassName": "MSFT_StorageObject",
"CimSuperClass": {
"CimSuperClassName": null,
"CimSuperClass": null,
"CimClassProperties": "ObjectId PassThroughClass PassThroughIds PassThroughNamespace PassThroughServer UniqueId",
"CimClassQualifiers": "Abstract = True locale = 1033",
"CimClassMethods": "",
"CimSystemProperties": "Microsoft.Management.Infrastructure.CimSystemProperties"
},
"CimClassProperties": [
"ObjectId",
"PassThroughClass",
"PassThroughIds",
"PassThroughNamespace",
"PassThroughServer",
"UniqueId",
"DeviceId",
"FlushLatencyMax",
"LoadUnloadCycleCount",
"LoadUnloadCycleCountMax",
"ManufactureDate",
"PowerOnHours",
"ReadErrorsCorrected",
"ReadErrorsTotal",
"ReadErrorsUncorrected",
"ReadLatencyMax",
"StartStopCycleCount",
"StartStopCycleCountMax",
"Temperature",
"TemperatureMax",
"Wear",
"WriteErrorsCorrected",
"WriteErrorsTotal",
"WriteErrorsUncorrected",
"WriteLatencyMax"
]
},
"Temperature": 46,
"TemperatureMax": 70,
"Wear": 0,
"ReadLatencyMax": 1930,
"WriteLatencyMax": 1903,
"FlushLatencyMax": 262
}
''', // Disk SMART data
'12th Gen Intel(R) Core(TM) i5-12490F', // CPU brand
];