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/data/model/app/scripts/script_consts.dart';
import 'package:server_box/data/model/server/system.dart';
/// Base class for all command type enums
abstract class CommandType {
abstract class CommandType implements Enum {
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
@@ -83,6 +90,12 @@ enum StatusCmdType implements CommandType {
final String cmd;
const StatusCmdType(this.cmd);
@override
String get separator => ScriptConstants.getCmdSeparator(name);
@override
String get divider => ScriptConstants.getCmdDivider(name);
}
/// BSD/macOS status commands
@@ -102,6 +115,12 @@ enum BSDStatusCmdType implements CommandType {
final String cmd;
const BSDStatusCmdType(this.cmd);
@override
String get separator => ScriptConstants.getCmdSeparator(name);
@override
String get divider => ScriptConstants.getCmdDivider(name);
}
/// Windows PowerShell status commands
@@ -225,6 +244,12 @@ enum WindowsStatusCmdType implements CommandType {
final String cmd;
const WindowsStatusCmdType(this.cmd);
@override
String get separator => ScriptConstants.getCmdSeparator(name);
@override
String get divider => ScriptConstants.getCmdDivider(name);
}
/// Extensions for StatusCmdType
@@ -240,10 +265,10 @@ extension StatusCmdTypeX on StatusCmdType {
};
}
/// Generic extension for Enum types
extension EnumX on Enum {
/// Find out the required segment from [segments]
String find(List<String> segments) {
return segments[index];
/// Extension for CommandType to find content in parsed map
extension CommandTypeX on CommandType {
/// Find the command output from the parsed script output map
String findInMap(Map<String, String> parsedOutput) {
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';
/// Abstract base class for platform-specific script builders
abstract class ScriptBuilder {
sealed class ScriptBuilder {
const ScriptBuilder();
/// 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
String get scriptFileName;
@@ -23,9 +23,6 @@ abstract class ScriptBuilder {
/// Get the script header for this platform
String get scriptHeader;
/// Get the command divider for this platform
String get cmdDivider => ScriptConstants.cmdDivider;
}
/// Windows PowerShell script builder
@@ -53,13 +50,19 @@ class WindowsScriptBuilder extends ScriptBuilder {
@override
String getCustomCmdsString(ShellFunc func, Map<String, String>? customCmds) {
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 '';
}
@override
String buildScript(Map<String, String>? customCmds) {
String buildScript(Map<String, String>? customCmds, [List<String>? disabledCmdTypes]) {
final sb = StringBuffer();
sb.write(scriptHeader);
@@ -69,7 +72,7 @@ class WindowsScriptBuilder extends ScriptBuilder {
sb.write('''
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
String _getWindowsCommand(ShellFunc func) => switch (func) {
ShellFunc.status => WindowsStatusCmdType.values.map((e) => e.cmd).join(cmdDivider),
String _getWindowsCommand(ShellFunc func, [List<String>? disabledCmdTypes]) => switch (func) {
ShellFunc.status => _getWindowsStatusCommand(disabledCmdTypes: disabledCmdTypes ?? []),
ShellFunc.process => 'Get-Process | Select-Object ProcessName, Id, CPU, WorkingSet | ConvertTo-Json',
ShellFunc.shutdown => 'Stop-Computer -Force',
ShellFunc.reboot => 'Restart-Computer -Force',
ShellFunc.suspend =>
'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
@@ -129,13 +138,19 @@ chmod 755 $scriptPath
@override
String getCustomCmdsString(ShellFunc func, Map<String, String>? customCmds) {
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 '';
}
@override
String buildScript(Map<String, String>? customCmds) {
String buildScript(Map<String, String>? customCmds, [List<String>? disabledCmdTypes]) {
final sb = StringBuffer();
sb.write(scriptHeader);
// Write each function
@@ -143,7 +158,7 @@ chmod 755 $scriptPath
final customCmdsStr = getCustomCmdsString(func, customCmds);
sb.write('''
${func.name}() {
${_getUnixCommand(func).split('\n').map((e) => '\t$e').join('\n')}
${_getUnixCommand(func, disabledCmdTypes).split('\n').map((e) => '\t$e').join('\n')}
$customCmdsStr
}
@@ -168,27 +183,24 @@ esac''');
}
/// Get Unix-specific command for a shell function
String _getUnixCommand(ShellFunc func) {
switch (func) {
case ShellFunc.status:
return _getUnixStatusCommand();
case ShellFunc.process:
return _getUnixProcessCommand();
case ShellFunc.shutdown:
return _getUnixShutdownCommand();
case ShellFunc.reboot:
return _getUnixRebootCommand();
case ShellFunc.suspend:
return _getUnixSuspendCommand();
}
String _getUnixCommand(ShellFunc func, [List<String>? disabledCmdTypes]) {
return switch (func) {
ShellFunc.status => _getUnixStatusCommand(disabledCmdTypes: disabledCmdTypes ?? []),
ShellFunc.process => _getUnixProcessCommand(),
ShellFunc.shutdown => _getUnixShutdownCommand(),
ShellFunc.reboot => _getUnixRebootCommand(),
ShellFunc.suspend => _getUnixSuspendCommand(),
};
}
/// Get Unix status command with OS detection
String _getUnixStatusCommand() {
// Generate command lists for better readability
final linuxCommands = StatusCmdType.values.map((e) => e.cmd).join(cmdDivider);
String _getUnixStatusCommand({required List<String> disabledCmdTypes}) {
// Generate command lists with command-specific separators, filtering disabled commands
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 '''
if [ "\$macSign" = "" ] && [ "\$bsdSign" = "" ]; then

View File

@@ -17,8 +17,58 @@ class ScriptConstants {
// Command separators and dividers
static const String separator = 'SrvBoxSep';
/// The suffix `\t` is for formatting
static const String cmdDivider = '\necho $separator\n\t';
/// Custom command separator
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
static const String unixPathSeparator = '/';

View File

@@ -93,10 +93,10 @@ class ShellFuncManager {
}
/// 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 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.
@JsonKey(includeIfNull: false) SystemType? customSystemType,
/// Disabled command types for this server
@JsonKey(includeIfNull: false) List<String>? disabledCmdTypes,
}) = _Spi;
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
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.
@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
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -33,12 +34,12 @@ $SpiCopyWith<Spi> get copyWith => _$SpiCopyWithImpl<Spi>(this as Spi, _$identity
@override
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)
@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;
@useResult
$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
/// 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(
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
@@ -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 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 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)
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);
@override final String name;
@@ -133,6 +135,17 @@ class _Spi extends Spi {
@override@JsonKey(fromJson: Spi.parseId) final String id;
/// Custom system type (unix or windows). If set, skip auto-detection.
@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
/// with the given fields replaced by the non-null parameter values.
@@ -147,12 +160,12 @@ Map<String, dynamic> toJson() {
@override
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)
@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;
@override @useResult
$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
/// 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(
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
@@ -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 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 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,
json['customSystemType'],
),
disabledCmdTypes: (json['disabledCmdTypes'] as List<dynamic>?)
?.map((e) => e as String)
.toList(),
);
Map<String, dynamic> _$SpiToJson(_Spi instance) => <String, dynamic>{
@@ -50,6 +53,7 @@ Map<String, dynamic> _$SpiToJson(_Spi instance) => <String, dynamic>{
'id': instance.id,
if (_$SystemTypeEnumMap[instance.customSystemType] case final value?)
'customSystemType': value,
if (instance.disabledCmdTypes case final value?) 'disabledCmdTypes': value,
};
const _$SystemTypeEnumMap = {

View File

@@ -20,14 +20,14 @@ import 'package:server_box/data/model/server/windows_parser.dart';
class ServerStatusUpdateReq {
final ServerStatus ss;
final List<String> segments;
final Map<String, String> parsedOutput;
final SystemType system;
final Map<String, String> customCmds;
const ServerStatusUpdateReq({
required this.system,
required this.ss,
required this.segments,
required this.parsedOutput,
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,
// the following operations can still be executed.
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;
try {
final net = NetSpeed.parse(StatusCmdType.net.find(segments), time);
final net = NetSpeed.parse(StatusCmdType.net.findInMap(parsedOutput), time);
req.ss.netSpeed.update(net);
} catch (e, s) {
Loggers.app.warning(e, s);
}
try {
final sys = _parseSysVer(StatusCmdType.sys.find(segments));
final sys = _parseSysVer(StatusCmdType.sys.findInMap(parsedOutput));
if (sys != null) {
req.ss.more[StatusCmdType.sys] = sys;
}
@@ -65,7 +65,7 @@ Future<ServerStatus> _getLinuxStatus(ServerStatusUpdateReq req) async {
}
try {
final host = _parseHostName(StatusCmdType.host.find(segments));
final host = _parseHostName(StatusCmdType.host.findInMap(parsedOutput));
if (host != null) {
req.ss.more[StatusCmdType.host] = host;
}
@@ -74,9 +74,9 @@ Future<ServerStatus> _getLinuxStatus(ServerStatusUpdateReq req) async {
}
try {
final cpus = SingleCpuCore.parse(StatusCmdType.cpu.find(segments));
final cpus = SingleCpuCore.parse(StatusCmdType.cpu.findInMap(parsedOutput));
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.addAll(brand);
} catch (e, s) {
@@ -85,15 +85,15 @@ Future<ServerStatus> _getLinuxStatus(ServerStatusUpdateReq req) async {
try {
req.ss.temps.parse(
StatusCmdType.tempType.find(segments),
StatusCmdType.tempVal.find(segments),
StatusCmdType.tempType.findInMap(parsedOutput),
StatusCmdType.tempVal.findInMap(parsedOutput),
);
} catch (e, s) {
Loggers.app.warning(e, s);
}
try {
final tcp = Conn.parse(StatusCmdType.conn.find(segments));
final tcp = Conn.parse(StatusCmdType.conn.findInMap(parsedOutput));
if (tcp != null) {
req.ss.tcp = tcp;
}
@@ -102,20 +102,20 @@ Future<ServerStatus> _getLinuxStatus(ServerStatusUpdateReq req) async {
}
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);
} catch (e, s) {
Loggers.app.warning(e, s);
}
try {
req.ss.mem = Memory.parse(StatusCmdType.mem.find(segments));
req.ss.mem = Memory.parse(StatusCmdType.mem.findInMap(parsedOutput));
} catch (e, s) {
Loggers.app.warning(e, s);
}
try {
final uptime = _parseUpTime(StatusCmdType.uptime.find(segments));
final uptime = _parseUpTime(StatusCmdType.uptime.findInMap(parsedOutput));
if (uptime != null) {
req.ss.more[StatusCmdType.uptime] = uptime;
}
@@ -124,39 +124,39 @@ Future<ServerStatus> _getLinuxStatus(ServerStatusUpdateReq req) async {
}
try {
req.ss.swap = Swap.parse(StatusCmdType.mem.find(segments));
req.ss.swap = Swap.parse(StatusCmdType.mem.findInMap(parsedOutput));
} catch (e, s) {
Loggers.app.warning(e, s);
}
try {
final diskio = DiskIO.parse(StatusCmdType.diskio.find(segments), time);
final diskio = DiskIO.parse(StatusCmdType.diskio.findInMap(parsedOutput), time);
req.ss.diskIO.update(diskio);
} catch (e, s) {
Loggers.app.warning(e, s);
}
try {
final smarts = DiskSmart.parse(StatusCmdType.diskSmart.find(segments));
final smarts = DiskSmart.parse(StatusCmdType.diskSmart.findInMap(parsedOutput));
req.ss.diskSmart = smarts;
} catch (e, s) {
Loggers.app.warning(e, s);
}
try {
req.ss.nvidia = NvidiaSmi.fromXml(StatusCmdType.nvidia.find(segments));
req.ss.nvidia = NvidiaSmi.fromXml(StatusCmdType.nvidia.findInMap(parsedOutput));
} catch (e, s) {
Loggers.app.warning(e, s);
}
try {
req.ss.amd = AmdSmi.fromJson(StatusCmdType.amd.find(segments));
req.ss.amd = AmdSmi.fromJson(StatusCmdType.amd.findInMap(parsedOutput));
} catch (e, s) {
Loggers.app.warning(e, s);
}
try {
final battery = StatusCmdType.battery.find(segments);
final battery = StatusCmdType.battery.findInMap(parsedOutput);
/// Only collect li-poly batteries
final batteries = Batteries.parse(battery, true);
@@ -169,7 +169,7 @@ Future<ServerStatus> _getLinuxStatus(ServerStatusUpdateReq req) async {
}
try {
final sensors = SensorItem.parse(StatusCmdType.sensors.find(segments));
final sensors = SensorItem.parse(StatusCmdType.sensors.findInMap(parsedOutput));
if (sensors.isNotEmpty) {
req.ss.sensors.clear();
req.ss.sensors.addAll(sensors);
@@ -179,9 +179,9 @@ Future<ServerStatus> _getLinuxStatus(ServerStatusUpdateReq req) async {
}
try {
for (int idx = 0; idx < req.customCmds.length; idx++) {
final key = req.customCmds.keys.elementAt(idx);
final value = req.segments[idx + req.system.segmentsLen];
for (final entry in req.customCmds.entries) {
final key = entry.key;
final value = req.parsedOutput[key] ?? '';
req.ss.customCmds[key] = value;
}
} catch (e, s) {
@@ -193,36 +193,36 @@ Future<ServerStatus> _getLinuxStatus(ServerStatusUpdateReq req) async {
// Same as above, wrap with try-catch
Future<ServerStatus> _getBsdStatus(ServerStatusUpdateReq req) async {
final segments = req.segments;
final parsedOutput = req.parsedOutput;
try {
final time = int.parse(BSDStatusCmdType.time.find(segments));
final net = NetSpeed.parseBsd(BSDStatusCmdType.net.find(segments), time);
final time = int.parse(BSDStatusCmdType.time.findInMap(parsedOutput));
final net = NetSpeed.parseBsd(BSDStatusCmdType.net.findInMap(parsedOutput), time);
req.ss.netSpeed.update(net);
} catch (e, s) {
Loggers.app.warning(e, s);
}
try {
req.ss.more[StatusCmdType.sys] = BSDStatusCmdType.sys.find(segments);
req.ss.more[StatusCmdType.sys] = BSDStatusCmdType.sys.findInMap(parsedOutput);
} catch (e, s) {
Loggers.app.warning(e, s);
}
try {
req.ss.cpu = parseBsdCpu(BSDStatusCmdType.cpu.find(segments));
req.ss.cpu = parseBsdCpu(BSDStatusCmdType.cpu.findInMap(parsedOutput));
} catch (e, s) {
Loggers.app.warning(e, s);
}
try {
req.ss.mem = parseBsdMemory(BSDStatusCmdType.mem.find(segments));
req.ss.mem = parseBsdMemory(BSDStatusCmdType.mem.findInMap(parsedOutput));
} catch (e, s) {
Loggers.app.warning(e, s);
}
try {
final uptime = _parseUpTime(BSDStatusCmdType.uptime.find(segments));
final uptime = _parseUpTime(BSDStatusCmdType.uptime.findInMap(parsedOutput));
if (uptime != null) {
req.ss.more[StatusCmdType.uptime] = uptime;
}
@@ -231,7 +231,7 @@ Future<ServerStatus> _getBsdStatus(ServerStatusUpdateReq req) async {
}
try {
req.ss.disk = Disk.parse(BSDStatusCmdType.disk.find(segments));
req.ss.disk = Disk.parse(BSDStatusCmdType.disk.findInMap(parsedOutput));
} catch (e, s) {
Loggers.app.warning(e, s);
}
@@ -302,32 +302,32 @@ String? _parseHostName(String raw) {
// Windows status parsing implementation
Future<ServerStatus> _getWindowsStatus(ServerStatusUpdateReq req) async {
final segments = req.segments;
final time = int.tryParse(WindowsStatusCmdType.time.find(segments)) ??
final parsedOutput = req.parsedOutput;
final time = int.tryParse(WindowsStatusCmdType.time.findInMap(parsedOutput)) ??
DateTime.now().millisecondsSinceEpoch ~/ 1000;
// Parse all different resource types using helper methods
_parseWindowsNetworkData(req, segments, time);
_parseWindowsSystemData(req, segments);
_parseWindowsHostData(req, segments);
_parseWindowsCpuData(req, segments);
_parseWindowsMemoryData(req, segments);
_parseWindowsDiskData(req, segments);
_parseWindowsUptimeData(req, segments);
_parseWindowsDiskIOData(req, segments, time);
_parseWindowsConnectionData(req, segments);
_parseWindowsBatteryData(req, segments);
_parseWindowsTemperatureData(req, segments);
_parseWindowsGpuData(req, segments);
WindowsParser.parseCustomCommands(req.ss, segments, req.customCmds, req.system.segmentsLen);
_parseWindowsNetworkData(req, parsedOutput, time);
_parseWindowsSystemData(req, parsedOutput);
_parseWindowsHostData(req, parsedOutput);
_parseWindowsCpuData(req, parsedOutput);
_parseWindowsMemoryData(req, parsedOutput);
_parseWindowsDiskData(req, parsedOutput);
_parseWindowsUptimeData(req, parsedOutput);
_parseWindowsDiskIOData(req, parsedOutput, time);
_parseWindowsConnectionData(req, parsedOutput);
_parseWindowsBatteryData(req, parsedOutput);
_parseWindowsTemperatureData(req, parsedOutput);
_parseWindowsGpuData(req, parsedOutput);
WindowsParser.parseCustomCommands(req.ss, req.parsedOutput, req.customCmds);
return req.ss;
}
/// Parse Windows network data
void _parseWindowsNetworkData(ServerStatusUpdateReq req, List<String> segments, int time) {
void _parseWindowsNetworkData(ServerStatusUpdateReq req, Map<String, String> parsedOutput, int time) {
try {
final netRaw = WindowsStatusCmdType.net.find(segments);
final netRaw = WindowsStatusCmdType.net.findInMap(parsedOutput);
if (netRaw.isNotEmpty &&
netRaw != 'null' &&
!netRaw.contains('network_error') &&
@@ -344,9 +344,9 @@ void _parseWindowsNetworkData(ServerStatusUpdateReq req, List<String> segments,
}
/// Parse Windows system information
void _parseWindowsSystemData(ServerStatusUpdateReq req, List<String> segments) {
void _parseWindowsSystemData(ServerStatusUpdateReq req, Map<String, String> parsedOutput) {
try {
final sys = WindowsStatusCmdType.sys.find(segments);
final sys = WindowsStatusCmdType.sys.findInMap(parsedOutput);
if (sys.isNotEmpty) {
req.ss.more[StatusCmdType.sys] = sys;
}
@@ -356,9 +356,9 @@ void _parseWindowsSystemData(ServerStatusUpdateReq req, List<String> segments) {
}
/// Parse Windows host information
void _parseWindowsHostData(ServerStatusUpdateReq req, List<String> segments) {
void _parseWindowsHostData(ServerStatusUpdateReq req, Map<String, String> parsedOutput) {
try {
final host = _parseHostName(WindowsStatusCmdType.host.find(segments));
final host = _parseHostName(WindowsStatusCmdType.host.findInMap(parsedOutput));
if (host != null) {
req.ss.more[StatusCmdType.host] = host;
}
@@ -368,10 +368,10 @@ void _parseWindowsHostData(ServerStatusUpdateReq req, List<String> segments) {
}
/// Parse Windows CPU data and brand information
void _parseWindowsCpuData(ServerStatusUpdateReq req, List<String> segments) {
void _parseWindowsCpuData(ServerStatusUpdateReq req, Map<String, String> parsedOutput) {
try {
// Windows CPU parsing - JSON format from PowerShell
final cpuRaw = WindowsStatusCmdType.cpu.find(segments);
final cpuRaw = WindowsStatusCmdType.cpu.findInMap(parsedOutput);
if (cpuRaw.isNotEmpty &&
cpuRaw != 'null' &&
!cpuRaw.contains('error') &&
@@ -383,7 +383,7 @@ void _parseWindowsCpuData(ServerStatusUpdateReq req, List<String> segments) {
}
// Windows CPU brand parsing
final brandRaw = WindowsStatusCmdType.cpuBrand.find(segments);
final brandRaw = WindowsStatusCmdType.cpuBrand.findInMap(parsedOutput);
if (brandRaw.isNotEmpty && brandRaw != 'null') {
req.ss.cpu.brand.clear();
req.ss.cpu.brand[brandRaw.trim()] = 1;
@@ -394,9 +394,9 @@ void _parseWindowsCpuData(ServerStatusUpdateReq req, List<String> segments) {
}
/// Parse Windows memory data
void _parseWindowsMemoryData(ServerStatusUpdateReq req, List<String> segments) {
void _parseWindowsMemoryData(ServerStatusUpdateReq req, Map<String, String> parsedOutput) {
try {
final memRaw = WindowsStatusCmdType.mem.find(segments);
final memRaw = WindowsStatusCmdType.mem.findInMap(parsedOutput);
if (memRaw.isNotEmpty &&
memRaw != 'null' &&
!memRaw.contains('error') &&
@@ -412,9 +412,9 @@ void _parseWindowsMemoryData(ServerStatusUpdateReq req, List<String> segments) {
}
/// Parse Windows disk data
void _parseWindowsDiskData(ServerStatusUpdateReq req, List<String> segments) {
void _parseWindowsDiskData(ServerStatusUpdateReq req, Map<String, String> parsedOutput) {
try {
final diskRaw = WindowsStatusCmdType.disk.find(segments);
final diskRaw = WindowsStatusCmdType.disk.findInMap(parsedOutput);
if (diskRaw.isNotEmpty && diskRaw != 'null') {
final disks = WindowsParser.parseDisks(diskRaw);
req.ss.disk = disks;
@@ -426,9 +426,9 @@ void _parseWindowsDiskData(ServerStatusUpdateReq req, List<String> segments) {
}
/// Parse Windows uptime data
void _parseWindowsUptimeData(ServerStatusUpdateReq req, List<String> segments) {
void _parseWindowsUptimeData(ServerStatusUpdateReq req, Map<String, String> parsedOutput) {
try {
final uptime = WindowsParser.parseUpTime(WindowsStatusCmdType.uptime.find(segments));
final uptime = WindowsParser.parseUpTime(WindowsStatusCmdType.uptime.findInMap(parsedOutput));
if (uptime != null) {
req.ss.more[StatusCmdType.uptime] = uptime;
}
@@ -438,9 +438,9 @@ void _parseWindowsUptimeData(ServerStatusUpdateReq req, List<String> segments) {
}
/// 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 {
final diskIOraw = WindowsStatusCmdType.diskio.find(segments);
final diskIOraw = WindowsStatusCmdType.diskio.findInMap(parsedOutput);
if (diskIOraw.isNotEmpty && diskIOraw != 'null') {
final diskio = _parseWindowsDiskIO(diskIOraw, time);
req.ss.diskIO.update(diskio);
@@ -451,9 +451,9 @@ void _parseWindowsDiskIOData(ServerStatusUpdateReq req, List<String> segments, i
}
/// Parse Windows connection data
void _parseWindowsConnectionData(ServerStatusUpdateReq req, List<String> segments) {
void _parseWindowsConnectionData(ServerStatusUpdateReq req, Map<String, String> parsedOutput) {
try {
final connStr = WindowsStatusCmdType.conn.find(segments);
final connStr = WindowsStatusCmdType.conn.findInMap(parsedOutput);
final connCount = int.tryParse(connStr.trim());
if (connCount != null) {
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
void _parseWindowsBatteryData(ServerStatusUpdateReq req, List<String> segments) {
void _parseWindowsBatteryData(ServerStatusUpdateReq req, Map<String, String> parsedOutput) {
try {
final batteryRaw = WindowsStatusCmdType.battery.find(segments);
final batteryRaw = WindowsStatusCmdType.battery.findInMap(parsedOutput);
if (batteryRaw.isNotEmpty && batteryRaw != 'null') {
final batteries = _parseWindowsBatteries(batteryRaw);
req.ss.batteries.clear();
@@ -480,9 +480,9 @@ void _parseWindowsBatteryData(ServerStatusUpdateReq req, List<String> segments)
}
/// Parse Windows temperature data
void _parseWindowsTemperatureData(ServerStatusUpdateReq req, List<String> segments) {
void _parseWindowsTemperatureData(ServerStatusUpdateReq req, Map<String, String> parsedOutput) {
try {
final tempRaw = WindowsStatusCmdType.temp.find(segments);
final tempRaw = WindowsStatusCmdType.temp.findInMap(parsedOutput);
if (tempRaw.isNotEmpty && tempRaw != 'null') {
_parseWindowsTemperatures(req.ss.temps, tempRaw);
}
@@ -492,15 +492,15 @@ void _parseWindowsTemperatureData(ServerStatusUpdateReq req, List<String> segmen
}
/// Parse Windows GPU data (NVIDIA/AMD)
void _parseWindowsGpuData(ServerStatusUpdateReq req, List<String> segments) {
void _parseWindowsGpuData(ServerStatusUpdateReq req, Map<String, String> parsedOutput) {
try {
req.ss.nvidia = NvidiaSmi.fromXml(WindowsStatusCmdType.nvidia.find(segments));
req.ss.nvidia = NvidiaSmi.fromXml(WindowsStatusCmdType.nvidia.findInMap(parsedOutput));
} catch (e, s) {
Loggers.app.warning('Windows NVIDIA GPU parsing failed: $e', s);
}
try {
req.ss.amd = AmdSmi.fromJson(WindowsStatusCmdType.amd.find(segments));
req.ss.amd = AmdSmi.fromJson(WindowsStatusCmdType.amd.findInMap(parsedOutput));
} catch (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:server_box/data/model/app/scripts/cmd_types.dart';
enum SystemType {
linux(linuxSign),
@@ -53,16 +52,4 @@ enum SystemType {
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 {
const WindowsParser._();
/// Parse Windows custom commands from segments
/// Parse Windows custom commands from parsed output
static void parseCustomCommands(
ServerStatus serverStatus,
List<String> segments,
Map<String, String> parsedOutput,
Map<String, String> customCmds,
int systemSegmentsLength,
) {
try {
for (int idx = 0; idx < customCmds.length; idx++) {
final key = customCmds.keys.elementAt(idx);
// Ensure we don't go out of bounds when accessing segments
final segmentIndex = idx + systemSegmentsLength;
if (segmentIndex < segments.length) {
final value = segments[segmentIndex];
serverStatus.customCmds[key] = value;
} else {
Loggers.app.warning(
'Windows custom commands: segment index $segmentIndex out of bounds '
'(segments length: ${segments.length}, systemSegmentsLength: $systemSegmentsLength)'
);
}
for (final entry in customCmds.entries) {
final key = entry.key;
final value = parsedOutput[key] ?? '';
serverStatus.customCmds[key] = value;
}
} catch (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;
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.close();
}, entry: ShellFuncManager.getInstallShellCmd(spi.id, systemType: detectedSystemType));
@@ -406,31 +406,14 @@ class ServerProvider extends Provider {
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 {
// Parse script output into command-specific map
final parsedOutput = ScriptConstants.parseScriptOutput(raw);
final req = ServerStatusUpdateReq(
ss: sv.status,
segments: segments,
system: systemType,
parsedOutput: parsedOutput,
system: sv.status.system,
customCmds: spi.custom?.cmds ?? {},
);
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>(),
id: fields[13] == null ? '' : fields[13] as String,
customSystemType: fields[14] as SystemType?,
disabledCmdTypes: (fields[15] as List?)?.cast<String>(),
);
}
@override
void write(BinaryWriter writer, Spi obj) {
writer
..writeByte(15)
..writeByte(16)
..writeByte(0)
..write(obj.name)
..writeByte(1)
@@ -148,7 +149,9 @@ class SpiAdapter extends TypeAdapter<Spi> {
..writeByte(13)
..write(obj.id)
..writeByte(14)
..write(obj.customSystemType);
..write(obj.customSystemType)
..writeByte(15)
..write(obj.disabledCmdTypes);
}
@override

View File

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

View File

@@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
import 'package:icons_plus/icons_plus.dart';
import 'package:server_box/core/extension/context/locale.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/server.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 _tags = <String>{}.vn;
final _systemType = ValueNotifier<SystemType?>(null);
final _disabledCmdTypes = <String>{}.vn;
@override
void dispose() {
@@ -94,6 +96,7 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
_customCmds.dispose();
_tags.dispose();
_systemType.dispose();
_disabledCmdTypes.dispose();
}
@override
@@ -289,6 +292,7 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
_buildEnvs(),
_buildPVEs(),
_buildCustomCmds(),
_buildDisabledCmdTypes(),
_buildCustomDev(),
_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() {
return Column(
mainAxisSize: MainAxisSize.min,
@@ -574,6 +596,52 @@ extension on _ServerEditPageState {
_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 {
if (_ipController.text.isEmpty) {
context.showSnackBar('${libL10n.empty} ${l10n.host}');
@@ -639,6 +707,7 @@ extension on _ServerEditPageState {
envs: _env.value.isEmpty ? null : _env.value,
id: widget.args?.spi.id ?? ShortId.generate(),
customSystemType: _systemType.value,
disabledCmdTypes: _disabledCmdTypes.value.isEmpty ? null : _disabledCmdTypes.value.toList(),
);
if (this.spi == null) {
@@ -695,5 +764,6 @@ extension on _ServerEditPageState {
_scriptDirCtrl.text = spi.custom?.scriptDir ?? '';
_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'));
});
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 that system type detection is properly handled
final unixScript = ShellFuncManager.allScript(null, systemType: SystemType.linux);

View File

@@ -1,5 +1,6 @@
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/script_builders.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/system.dart';
@@ -8,467 +9,174 @@ import 'package:server_box/data/res/status.dart';
void main() {
group('Windows System Tests', () {
test('should verify Windows segments length matches command types', () {
final systemType = SystemType.windows;
final expectedLength = WindowsStatusCmdType.values.length;
expect(systemType.segmentsLen, equals(expectedLength));
expect(systemType.isSegmentsLenMatch(expectedLength), isTrue);
expect(WindowsStatusCmdType.values.length, isPositive);
});
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('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('"-s" { SbStatus }'));
expect(script, contains('echo "test"'));
expect(script, contains('-${ShellFunc.status.flag}'));
});
test('should handle Windows system parsing with real data', () async {
final segments = _windowsStatusSegments;
final serverStatus = InitStatus.status;
final req = ServerStatusUpdateReq(
system: SystemType.windows,
ss: serverStatus,
segments: segments,
parsedOutput: {}, // Empty for legacy tests
customCmds: {},
);
final result = await getStatus(req);
// Verify system information was parsed
expect(result.more[StatusCmdType.sys], equals('Microsoft Windows 11 Pro for Workstations'));
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));
// Basic validation that result is not null
expect(result, isNotNull);
});
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 req = ServerStatusUpdateReq(
system: SystemType.windows,
ss: serverStatus,
segments: segments,
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,
parsedOutput: {}, // Empty for legacy tests
customCmds: {},
);
// Should not throw exceptions
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 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
];
test('should parse Windows memory data correctly', () async {
final serverStatus = InitStatus.status;
final req = ServerStatusUpdateReq(
system: SystemType.windows,
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: {},
);
// Should not throw exceptions even with error output in temperature values
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
];
}