mirror of
https://github.com/lollipopkit/flutter_server_box.git
synced 2025-12-17 07:14:28 +01:00
feat: amd gpu (#831)
This commit is contained in:
@@ -16,6 +16,12 @@ enum ShellFunc {
|
|||||||
/// The suffix `\t` is for formatting
|
/// The suffix `\t` is for formatting
|
||||||
static const cmdDivider = '\necho $seperator\n\t';
|
static const cmdDivider = '\necho $seperator\n\t';
|
||||||
|
|
||||||
|
/// Cached Linux status commands string
|
||||||
|
static final _linuxStatusCmds = StatusCmdType.values.map((e) => e.cmd).join(cmdDivider);
|
||||||
|
|
||||||
|
/// Cached BSD status commands string
|
||||||
|
static final _bsdStatusCmds = BSDStatusCmdType.values.map((e) => e.cmd).join(cmdDivider);
|
||||||
|
|
||||||
/// srvboxm -> ServerBox Mobile
|
/// srvboxm -> ServerBox Mobile
|
||||||
static const scriptFile = 'srvboxm_v${BuildData.script}.sh';
|
static const scriptFile = 'srvboxm_v${BuildData.script}.sh';
|
||||||
static const scriptDirHome = '~/.config/server_box';
|
static const scriptDirHome = '~/.config/server_box';
|
||||||
@@ -28,13 +34,10 @@ enum ShellFunc {
|
|||||||
/// Default is [scriptDirTmp]/[scriptFile], if this path is not accessible,
|
/// Default is [scriptDirTmp]/[scriptFile], if this path is not accessible,
|
||||||
/// it will be changed to [scriptDirHome]/[scriptFile].
|
/// it will be changed to [scriptDirHome]/[scriptFile].
|
||||||
static String getScriptDir(String id) {
|
static String getScriptDir(String id) {
|
||||||
final customScriptDir = ServerProvider.pick(
|
final customScriptDir = ServerProvider.pick(id: id)?.value.spi.custom?.scriptDir;
|
||||||
id: id,
|
|
||||||
)?.value.spi.custom?.scriptDir;
|
|
||||||
if (customScriptDir != null) return customScriptDir;
|
if (customScriptDir != null) return customScriptDir;
|
||||||
return _scriptDirMap.putIfAbsent(id, () {
|
_scriptDirMap[id] ??= scriptDirTmp;
|
||||||
return scriptDirTmp;
|
return _scriptDirMap[id]!;
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void switchScriptDir(String id) => switch (_scriptDirMap[id]) {
|
static void switchScriptDir(String id) => switch (_scriptDirMap[id]) {
|
||||||
@@ -68,43 +71,24 @@ chmod 755 $scriptPath
|
|||||||
|
|
||||||
String exec(String id) => 'sh ${getScriptPath(id)} -$flag';
|
String exec(String id) => 'sh ${getScriptPath(id)} -$flag';
|
||||||
|
|
||||||
String get name {
|
String get name => switch (this) {
|
||||||
switch (this) {
|
ShellFunc.status => 'status',
|
||||||
case ShellFunc.status:
|
ShellFunc.process => 'process',
|
||||||
return 'status';
|
ShellFunc.shutdown => 'ShutDown',
|
||||||
// case ShellFunc.docker:
|
ShellFunc.reboot => 'Reboot',
|
||||||
// // `dockeR` -> avoid conflict with `docker` command
|
ShellFunc.suspend => 'Suspend',
|
||||||
// return 'dockeR';
|
};
|
||||||
case ShellFunc.process:
|
|
||||||
return 'process';
|
|
||||||
case ShellFunc.shutdown:
|
|
||||||
return 'ShutDown';
|
|
||||||
case ShellFunc.reboot:
|
|
||||||
return 'Reboot';
|
|
||||||
case ShellFunc.suspend:
|
|
||||||
return 'Suspend';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String get _cmd {
|
String get _cmd => switch (this) {
|
||||||
switch (this) {
|
ShellFunc.status =>
|
||||||
case ShellFunc.status:
|
'''
|
||||||
return '''
|
|
||||||
if [ "\$macSign" = "" ] && [ "\$bsdSign" = "" ]; then
|
if [ "\$macSign" = "" ] && [ "\$bsdSign" = "" ]; then
|
||||||
\t${StatusCmdType.values.map((e) => e.cmd).join(cmdDivider)}
|
\t$_linuxStatusCmds
|
||||||
else
|
else
|
||||||
\t${BSDStatusCmdType.values.map((e) => e.cmd).join(cmdDivider)}
|
\t$_bsdStatusCmds
|
||||||
fi''';
|
fi''',
|
||||||
// case ShellFunc.docker:
|
ShellFunc.process =>
|
||||||
// return '''
|
'''
|
||||||
// result=\$(docker version 2>&1 | grep "permission denied")
|
|
||||||
// if [ "\$result" != "" ]; then
|
|
||||||
// \t${_dockerCmds.join(_cmdDivider)}
|
|
||||||
// else
|
|
||||||
// \t${_dockerCmds.map((e) => "sudo -S $e").join(_cmdDivider)}
|
|
||||||
// fi''';
|
|
||||||
case ShellFunc.process:
|
|
||||||
return '''
|
|
||||||
if [ "\$macSign" = "" ] && [ "\$bsdSign" = "" ]; then
|
if [ "\$macSign" = "" ] && [ "\$bsdSign" = "" ]; then
|
||||||
\tif [ "\$isBusybox" != "" ]; then
|
\tif [ "\$isBusybox" != "" ]; then
|
||||||
\t\tps w
|
\t\tps w
|
||||||
@@ -114,30 +98,29 @@ if [ "\$macSign" = "" ] && [ "\$bsdSign" = "" ]; then
|
|||||||
else
|
else
|
||||||
\tps -ax
|
\tps -ax
|
||||||
fi
|
fi
|
||||||
''';
|
''',
|
||||||
case ShellFunc.shutdown:
|
ShellFunc.shutdown =>
|
||||||
return '''
|
'''
|
||||||
if [ "\$userId" = "0" ]; then
|
if [ "\$userId" = "0" ]; then
|
||||||
\tshutdown -h now
|
\tshutdown -h now
|
||||||
else
|
else
|
||||||
\tsudo -S shutdown -h now
|
\tsudo -S shutdown -h now
|
||||||
fi''';
|
fi''',
|
||||||
case ShellFunc.reboot:
|
ShellFunc.reboot =>
|
||||||
return '''
|
'''
|
||||||
if [ "\$userId" = "0" ]; then
|
if [ "\$userId" = "0" ]; then
|
||||||
\treboot
|
\treboot
|
||||||
else
|
else
|
||||||
\tsudo -S reboot
|
\tsudo -S reboot
|
||||||
fi''';
|
fi''',
|
||||||
case ShellFunc.suspend:
|
ShellFunc.suspend =>
|
||||||
return '''
|
'''
|
||||||
if [ "\$userId" = "0" ]; then
|
if [ "\$userId" = "0" ]; then
|
||||||
\tsystemctl suspend
|
\tsystemctl suspend
|
||||||
else
|
else
|
||||||
\tsudo -S systemctl suspend
|
\tsudo -S systemctl suspend
|
||||||
fi''';
|
fi''',
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
static String allScript(Map<String, String>? customCmds) {
|
static String allScript(Map<String, String>? customCmds) {
|
||||||
final sb = StringBuffer();
|
final sb = StringBuffer();
|
||||||
@@ -163,9 +146,7 @@ exec 2>/dev/null
|
|||||||
// Write each func
|
// Write each func
|
||||||
for (final func in values) {
|
for (final func in values) {
|
||||||
final customCmdsStr = () {
|
final customCmdsStr = () {
|
||||||
if (func == ShellFunc.status &&
|
if (func == ShellFunc.status && customCmds != null && customCmds.isNotEmpty) {
|
||||||
customCmds != null &&
|
|
||||||
customCmds.isNotEmpty) {
|
|
||||||
return '$cmdDivider\n\t${customCmds.values.join(cmdDivider)}';
|
return '$cmdDivider\n\t${customCmds.values.join(cmdDivider)}';
|
||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
@@ -212,18 +193,15 @@ enum StatusCmdType {
|
|||||||
cpu._('cat /proc/stat | grep cpu'),
|
cpu._('cat /proc/stat | grep cpu'),
|
||||||
uptime._('uptime'),
|
uptime._('uptime'),
|
||||||
conn._('cat /proc/net/snmp'),
|
conn._('cat /proc/net/snmp'),
|
||||||
disk._(
|
disk._('lsblk --bytes --json --output FSTYPE,PATH,NAME,KNAME,MOUNTPOINT,FSSIZE,FSUSED,FSAVAIL,FSUSE%,UUID'),
|
||||||
'lsblk --bytes --json --output FSTYPE,PATH,NAME,KNAME,MOUNTPOINT,FSSIZE,FSUSED,FSAVAIL,FSUSE%,UUID',
|
|
||||||
),
|
|
||||||
mem._("cat /proc/meminfo | grep -E 'Mem|Swap'"),
|
mem._("cat /proc/meminfo | grep -E 'Mem|Swap'"),
|
||||||
tempType._('cat /sys/class/thermal/thermal_zone*/type'),
|
tempType._('cat /sys/class/thermal/thermal_zone*/type'),
|
||||||
tempVal._('cat /sys/class/thermal/thermal_zone*/temp'),
|
tempVal._('cat /sys/class/thermal/thermal_zone*/temp'),
|
||||||
host._('cat /etc/hostname'),
|
host._('cat /etc/hostname'),
|
||||||
diskio._('cat /proc/diskstats'),
|
diskio._('cat /proc/diskstats'),
|
||||||
battery._(
|
battery._('for f in /sys/class/power_supply/*/uevent; do cat "\$f"; echo; done'),
|
||||||
'for f in /sys/class/power_supply/*/uevent; do cat "\$f"; echo; done',
|
|
||||||
),
|
|
||||||
nvidia._('nvidia-smi -q -x'),
|
nvidia._('nvidia-smi -q -x'),
|
||||||
|
amd._('if command -v amd-smi >/dev/null 2>&1; then amd-smi list --json && amd-smi metric --json; elif command -v rocm-smi >/dev/null 2>&1; then rocm-smi --json || rocm-smi --showunique --showuse --showtemp --showfan --showclocks --showmemuse --showpower; elif command -v radeontop >/dev/null 2>&1; then timeout 2s radeontop -d - -l 1 | tail -n +2; else echo "No AMD GPU monitoring tools found"; fi'),
|
||||||
sensors._('sensors'),
|
sensors._('sensors'),
|
||||||
diskSmart._('for d in \$(lsblk -dn -o KNAME); do smartctl -a -j /dev/\$d; echo; done'),
|
diskSmart._('for d in \$(lsblk -dn -o KNAME); do smartctl -a -j /dev/\$d; echo; done'),
|
||||||
cpuBrand._('cat /proc/cpuinfo | grep "model name"');
|
cpuBrand._('cat /proc/cpuinfo | grep "model name"');
|
||||||
@@ -258,6 +236,8 @@ extension StatusCmdTypeX on StatusCmdType {
|
|||||||
StatusCmdType.host => l10n.host,
|
StatusCmdType.host => l10n.host,
|
||||||
StatusCmdType.uptime => l10n.uptime,
|
StatusCmdType.uptime => l10n.uptime,
|
||||||
StatusCmdType.battery => l10n.battery,
|
StatusCmdType.battery => l10n.battery,
|
||||||
|
StatusCmdType.sensors => l10n.sensors,
|
||||||
|
StatusCmdType.disk => l10n.disk,
|
||||||
final val => val.name,
|
final val => val.name,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
188
lib/data/model/server/amd.dart
Normal file
188
lib/data/model/server/amd.dart
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
/// AMD GPU monitoring data structures
|
||||||
|
/// Supports both amd-smi and rocm-smi tools
|
||||||
|
/// Example JSON output:
|
||||||
|
/// [
|
||||||
|
/// {
|
||||||
|
/// "name": "AMD Radeon RX 7900 XTX",
|
||||||
|
/// "device_id": "0",
|
||||||
|
/// "temp": 45,
|
||||||
|
/// "power": "120W / 355W",
|
||||||
|
/// "memory": {
|
||||||
|
/// "total": 24576,
|
||||||
|
/// "used": 1024,
|
||||||
|
/// "unit": "MB",
|
||||||
|
/// "processes": [
|
||||||
|
/// {
|
||||||
|
/// "pid": 2456,
|
||||||
|
/// "name": "firefox",
|
||||||
|
/// "memory": 512
|
||||||
|
/// }
|
||||||
|
/// ]
|
||||||
|
/// },
|
||||||
|
/// "utilization": 75,
|
||||||
|
/// "fan_speed": 1200,
|
||||||
|
/// "clock_speed": 2400
|
||||||
|
/// }
|
||||||
|
/// ]
|
||||||
|
|
||||||
|
class AmdSmi {
|
||||||
|
static List<AmdSmiItem> fromJson(String raw) {
|
||||||
|
try {
|
||||||
|
final jsonData = json.decode(raw);
|
||||||
|
if (jsonData is! List) return [];
|
||||||
|
|
||||||
|
return jsonData
|
||||||
|
.map((gpu) => _parseGpuItem(gpu))
|
||||||
|
.where((item) => item != null)
|
||||||
|
.cast<AmdSmiItem>()
|
||||||
|
.toList();
|
||||||
|
} catch (e) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static AmdSmiItem? _parseGpuItem(Map<String, dynamic> gpu) {
|
||||||
|
try {
|
||||||
|
final name = gpu['name'] ?? gpu['card_model'] ?? gpu['device_name'] ?? 'Unknown AMD GPU';
|
||||||
|
final deviceId = gpu['device_id']?.toString() ?? gpu['gpu_id']?.toString() ?? '0';
|
||||||
|
|
||||||
|
// Temperature parsing
|
||||||
|
final tempRaw = gpu['temperature'] ?? gpu['temp'] ?? gpu['gpu_temp'];
|
||||||
|
final temp = _parseIntValue(tempRaw);
|
||||||
|
|
||||||
|
// Power parsing
|
||||||
|
final powerDraw = gpu['power_draw'] ?? gpu['current_power'];
|
||||||
|
final powerCap = gpu['power_cap'] ?? gpu['power_limit'] ?? gpu['max_power'];
|
||||||
|
final power = _formatPower(powerDraw, powerCap);
|
||||||
|
|
||||||
|
// Memory parsing
|
||||||
|
final memory = _parseMemory(gpu['memory'] ?? gpu['vram'] ?? {});
|
||||||
|
|
||||||
|
// Utilization parsing
|
||||||
|
final utilization = _parseIntValue(gpu['utilization'] ?? gpu['gpu_util'] ?? gpu['activity']);
|
||||||
|
|
||||||
|
// Fan speed parsing
|
||||||
|
final fanSpeed = _parseIntValue(gpu['fan_speed'] ?? gpu['fan_rpm']);
|
||||||
|
|
||||||
|
// Clock speed parsing
|
||||||
|
final clockSpeed = _parseIntValue(gpu['clock_speed'] ?? gpu['gpu_clock'] ?? gpu['sclk']);
|
||||||
|
|
||||||
|
return AmdSmiItem(
|
||||||
|
deviceId: deviceId,
|
||||||
|
name: name,
|
||||||
|
temp: temp,
|
||||||
|
power: power,
|
||||||
|
memory: memory,
|
||||||
|
utilization: utilization,
|
||||||
|
fanSpeed: fanSpeed,
|
||||||
|
clockSpeed: clockSpeed,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static int _parseIntValue(dynamic value) {
|
||||||
|
if (value == null) return 0;
|
||||||
|
if (value is int) return value;
|
||||||
|
if (value is String) {
|
||||||
|
// Remove units and parse (e.g., "45°C" -> 45, "1200 RPM" -> 1200)
|
||||||
|
final cleanValue = value.replaceAll(RegExp(r'[^\d]'), '');
|
||||||
|
return int.tryParse(cleanValue) ?? 0;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String _formatPower(dynamic draw, dynamic cap) {
|
||||||
|
final drawValue = _parseIntValue(draw);
|
||||||
|
final capValue = _parseIntValue(cap);
|
||||||
|
|
||||||
|
if (drawValue == 0 && capValue == 0) return 'N/A';
|
||||||
|
if (capValue == 0) return '${drawValue}W';
|
||||||
|
return '${drawValue}W / ${capValue}W';
|
||||||
|
}
|
||||||
|
|
||||||
|
static AmdSmiMem _parseMemory(Map<String, dynamic> memData) {
|
||||||
|
final total = _parseIntValue(memData['total'] ?? memData['total_memory']);
|
||||||
|
final used = _parseIntValue(memData['used'] ?? memData['used_memory']);
|
||||||
|
final unit = memData['unit']?.toString() ?? 'MB';
|
||||||
|
|
||||||
|
final processes = <AmdSmiMemProcess>[];
|
||||||
|
final processesData = memData['processes'];
|
||||||
|
if (processesData is List) {
|
||||||
|
for (final proc in processesData) {
|
||||||
|
if (proc is Map<String, dynamic>) {
|
||||||
|
final process = _parseProcess(proc);
|
||||||
|
if (process != null) processes.add(process);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return AmdSmiMem(total, used, unit, processes);
|
||||||
|
}
|
||||||
|
|
||||||
|
static AmdSmiMemProcess? _parseProcess(Map<String, dynamic> procData) {
|
||||||
|
final pid = _parseIntValue(procData['pid']);
|
||||||
|
final name = procData['name']?.toString() ?? procData['process_name']?.toString() ?? 'Unknown';
|
||||||
|
final memory = _parseIntValue(procData['memory'] ?? procData['used_memory']);
|
||||||
|
|
||||||
|
if (pid == 0) return null;
|
||||||
|
return AmdSmiMemProcess(pid, name, memory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AmdSmiItem {
|
||||||
|
final String deviceId;
|
||||||
|
final String name;
|
||||||
|
final int temp;
|
||||||
|
final String power;
|
||||||
|
final AmdSmiMem memory;
|
||||||
|
final int utilization;
|
||||||
|
final int fanSpeed;
|
||||||
|
final int clockSpeed;
|
||||||
|
|
||||||
|
const AmdSmiItem({
|
||||||
|
required this.deviceId,
|
||||||
|
required this.name,
|
||||||
|
required this.temp,
|
||||||
|
required this.power,
|
||||||
|
required this.memory,
|
||||||
|
required this.utilization,
|
||||||
|
required this.fanSpeed,
|
||||||
|
required this.clockSpeed,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'AmdSmiItem{name: $name, temp: $temp, power: $power, utilization: $utilization%, memory: $memory}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AmdSmiMem {
|
||||||
|
final int total;
|
||||||
|
final int used;
|
||||||
|
final String unit;
|
||||||
|
final List<AmdSmiMemProcess> processes;
|
||||||
|
|
||||||
|
const AmdSmiMem(this.total, this.used, this.unit, this.processes);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'AmdSmiMem{total: $total, used: $used, unit: $unit, processes: ${processes.length}}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AmdSmiMemProcess {
|
||||||
|
final int pid;
|
||||||
|
final String name;
|
||||||
|
final int memory;
|
||||||
|
|
||||||
|
const AmdSmiMemProcess(this.pid, this.name, this.memory);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'AmdSmiMemProcess{pid: $pid, name: $name, memory: $memory}';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:dartssh2/dartssh2.dart';
|
import 'package:dartssh2/dartssh2.dart';
|
||||||
import 'package:fl_lib/fl_lib.dart';
|
import 'package:fl_lib/fl_lib.dart';
|
||||||
import 'package:server_box/data/model/app/shell_func.dart';
|
import 'package:server_box/data/model/app/shell_func.dart';
|
||||||
|
import 'package:server_box/data/model/server/amd.dart';
|
||||||
import 'package:server_box/data/model/server/battery.dart';
|
import 'package:server_box/data/model/server/battery.dart';
|
||||||
import 'package:server_box/data/model/server/conn.dart';
|
import 'package:server_box/data/model/server/conn.dart';
|
||||||
import 'package:server_box/data/model/server/cpu.dart';
|
import 'package:server_box/data/model/server/cpu.dart';
|
||||||
@@ -42,6 +43,7 @@ class ServerStatus {
|
|||||||
DiskIO diskIO;
|
DiskIO diskIO;
|
||||||
List<DiskSmart> diskSmart;
|
List<DiskSmart> diskSmart;
|
||||||
List<NvidiaSmiItem>? nvidia;
|
List<NvidiaSmiItem>? nvidia;
|
||||||
|
List<AmdSmiItem>? amd;
|
||||||
final List<Battery> batteries = [];
|
final List<Battery> batteries = [];
|
||||||
final Map<StatusCmdType, String> more = {};
|
final Map<StatusCmdType, String> more = {};
|
||||||
final List<SensorItem> sensors = [];
|
final List<SensorItem> sensors = [];
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:fl_lib/fl_lib.dart';
|
import 'package:fl_lib/fl_lib.dart';
|
||||||
import 'package:server_box/data/model/app/shell_func.dart';
|
import 'package:server_box/data/model/app/shell_func.dart';
|
||||||
|
import 'package:server_box/data/model/server/amd.dart';
|
||||||
import 'package:server_box/data/model/server/battery.dart';
|
import 'package:server_box/data/model/server/battery.dart';
|
||||||
import 'package:server_box/data/model/server/conn.dart';
|
import 'package:server_box/data/model/server/conn.dart';
|
||||||
import 'package:server_box/data/model/server/cpu.dart';
|
import 'package:server_box/data/model/server/cpu.dart';
|
||||||
@@ -143,6 +144,12 @@ Future<ServerStatus> _getLinuxStatus(ServerStatusUpdateReq req) async {
|
|||||||
Loggers.app.warning(e, s);
|
Loggers.app.warning(e, s);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
req.ss.amd = AmdSmi.fromJson(StatusCmdType.amd.find(segments));
|
||||||
|
} catch (e, s) {
|
||||||
|
Loggers.app.warning(e, s);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final battery = StatusCmdType.battery.find(segments);
|
final battery = StatusCmdType.battery.find(segments);
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
part of 'view.dart';
|
part of 'view.dart';
|
||||||
|
|
||||||
extension on _ServerDetailPageState {
|
extension on _ServerDetailPageState {
|
||||||
void _onTapGpuItem(NvidiaSmiItem item) {
|
void _onTapNvidiaGpuItem(NvidiaSmiItem item) {
|
||||||
final processes = item.memory.processes;
|
final processes = item.memory.processes;
|
||||||
final displayCount = processes.length > 5 ? 5 : processes.length;
|
final displayCount = processes.length > 5 ? 5 : processes.length;
|
||||||
final height = displayCount * 47.0;
|
final height = displayCount * 47.0;
|
||||||
@@ -19,6 +19,24 @@ extension on _ServerDetailPageState {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _onTapAmdGpuItem(AmdSmiItem item) {
|
||||||
|
final processes = item.memory.processes;
|
||||||
|
final displayCount = processes.length > 5 ? 5 : processes.length;
|
||||||
|
final height = displayCount * 47.0;
|
||||||
|
context.showRoundDialog(
|
||||||
|
title: item.name,
|
||||||
|
child: SizedBox(
|
||||||
|
width: double.maxFinite,
|
||||||
|
height: height,
|
||||||
|
child: ListView.builder(
|
||||||
|
itemCount: processes.length,
|
||||||
|
itemBuilder: (_, idx) => _buildAmdGpuProcessItem(processes[idx]),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: Btnx.oks,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
void _onTapGpuProcessItem(NvidiaSmiMemProcess process) {
|
void _onTapGpuProcessItem(NvidiaSmiMemProcess process) {
|
||||||
context.showRoundDialog(
|
context.showRoundDialog(
|
||||||
title: '${process.pid}',
|
title: '${process.pid}',
|
||||||
@@ -37,6 +55,24 @@ extension on _ServerDetailPageState {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _onTapAmdGpuProcessItem(AmdSmiMemProcess process) {
|
||||||
|
context.showRoundDialog(
|
||||||
|
title: '${process.pid}',
|
||||||
|
titleMaxLines: 1,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
UIs.height13,
|
||||||
|
Text('Memory: ${process.memory} ${process.memory > 1024 ? 'MB' : 'KB'}'),
|
||||||
|
UIs.height13,
|
||||||
|
Text('Process: ${process.name}'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [TextButton(onPressed: () => context.pop(), child: Text(libL10n.close))],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
void _onTapCustomItem(MapEntry<String, String> cmd) {
|
void _onTapCustomItem(MapEntry<String, String> cmd) {
|
||||||
context.showRoundDialog(
|
context.showRoundDialog(
|
||||||
title: cmd.key,
|
title: cmd.key,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ 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/server_detail_card.dart';
|
import 'package:server_box/data/model/app/server_detail_card.dart';
|
||||||
import 'package:server_box/data/model/app/shell_func.dart';
|
import 'package:server_box/data/model/app/shell_func.dart';
|
||||||
|
import 'package:server_box/data/model/server/amd.dart';
|
||||||
import 'package:server_box/data/model/server/battery.dart';
|
import 'package:server_box/data/model/server/battery.dart';
|
||||||
import 'package:server_box/data/model/server/cpu.dart';
|
import 'package:server_box/data/model/server/cpu.dart';
|
||||||
import 'package:server_box/data/model/server/disk.dart';
|
import 'package:server_box/data/model/server/disk.dart';
|
||||||
@@ -410,9 +411,23 @@ class _ServerDetailPageState extends State<ServerDetailPage> with SingleTickerPr
|
|||||||
|
|
||||||
Widget? _buildGpuView(Server si) {
|
Widget? _buildGpuView(Server si) {
|
||||||
final ss = si.status;
|
final ss = si.status;
|
||||||
if (ss.nvidia == null || ss.nvidia?.isEmpty == true) return null;
|
final hasNvidia = ss.nvidia != null && ss.nvidia!.isNotEmpty;
|
||||||
|
final hasAmd = ss.amd != null && ss.amd!.isNotEmpty;
|
||||||
|
|
||||||
|
if (!hasNvidia && !hasAmd) return null;
|
||||||
|
|
||||||
|
final children = <Widget>[];
|
||||||
|
|
||||||
|
// Add NVIDIA GPUs
|
||||||
|
if (hasNvidia) {
|
||||||
|
children.addAll(ss.nvidia!.map((e) => _buildNvidiaGpuItem(e)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add AMD GPUs
|
||||||
|
if (hasAmd) {
|
||||||
|
children.addAll(ss.amd!.map((e) => _buildAmdGpuItem(e)));
|
||||||
|
}
|
||||||
|
|
||||||
final children = ss.nvidia?.map((e) => _buildGpuItem(e)).toList() ?? [];
|
|
||||||
return ExpandTile(
|
return ExpandTile(
|
||||||
title: const Text('GPU'),
|
title: const Text('GPU'),
|
||||||
leading: const Icon(Icons.memory, size: 17),
|
leading: const Icon(Icons.memory, size: 17),
|
||||||
@@ -421,7 +436,7 @@ class _ServerDetailPageState extends State<ServerDetailPage> with SingleTickerPr
|
|||||||
).cardx;
|
).cardx;
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildGpuItem(NvidiaSmiItem item) {
|
Widget _buildNvidiaGpuItem(NvidiaSmiItem item) {
|
||||||
final mem = item.memory;
|
final mem = item.memory;
|
||||||
return ListTile(
|
return ListTile(
|
||||||
title: Text(item.name, style: UIs.text13),
|
title: Text(item.name, style: UIs.text13),
|
||||||
@@ -441,7 +456,36 @@ class _ServerDetailPageState extends State<ServerDetailPage> with SingleTickerPr
|
|||||||
mainAxisAlignment: MainAxisAlignment.end,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
IconButton(onPressed: () => _onTapGpuItem(item), icon: const Icon(Icons.info_outline, size: 17)),
|
IconButton(
|
||||||
|
onPressed: () => _onTapNvidiaGpuItem(item),
|
||||||
|
icon: const Icon(Icons.info_outline, size: 17),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildAmdGpuItem(AmdSmiItem item) {
|
||||||
|
final mem = item.memory;
|
||||||
|
return ListTile(
|
||||||
|
title: Text('${item.name} (AMD)', style: UIs.text13),
|
||||||
|
leading: Text(
|
||||||
|
'${item.utilization}%\n${item.temp} °C',
|
||||||
|
style: UIs.text12Grey,
|
||||||
|
textScaler: _textFactor,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
'${item.power} - FAN ${item.fanSpeed} RPM\n${item.clockSpeed} MHz\n${mem.used} / ${mem.total} ${mem.unit}',
|
||||||
|
style: UIs.text12Grey,
|
||||||
|
textScaler: _textFactor,
|
||||||
|
),
|
||||||
|
contentPadding: const EdgeInsets.only(left: 17, right: 17),
|
||||||
|
trailing: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
IconButton(onPressed: () => _onTapAmdGpuItem(item), icon: const Icon(Icons.info_outline, size: 17)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -468,6 +512,27 @@ class _ServerDetailPageState extends State<ServerDetailPage> with SingleTickerPr
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildAmdGpuProcessItem(AmdSmiMemProcess process) {
|
||||||
|
return ListTile(
|
||||||
|
title: Text(
|
||||||
|
process.name,
|
||||||
|
style: UIs.text12,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
textScaler: _textFactor,
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
'PID: ${process.pid} - ${process.memory} MiB',
|
||||||
|
style: UIs.text12Grey,
|
||||||
|
textScaler: _textFactor,
|
||||||
|
),
|
||||||
|
trailing: InkWell(
|
||||||
|
onTap: () => _onTapAmdGpuProcessItem(process),
|
||||||
|
child: const Icon(Icons.info_outline, size: 17),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget? _buildDiskView(Server si) {
|
Widget? _buildDiskView(Server si) {
|
||||||
final ss = si.status;
|
final ss = si.status;
|
||||||
final children = <Widget>[];
|
final children = <Widget>[];
|
||||||
@@ -646,7 +711,9 @@ class _ServerDetailPageState extends State<ServerDetailPage> with SingleTickerPr
|
|||||||
|
|
||||||
if (smart.model != null) details.add('Model: ${smart.model}');
|
if (smart.model != null) details.add('Model: ${smart.model}');
|
||||||
if (smart.serial != null) details.add('Serial: ${smart.serial}');
|
if (smart.serial != null) details.add('Serial: ${smart.serial}');
|
||||||
if (smart.temperature != null) details.add('Temperature: ${smart.temperature!.toStringAsFixed(1)}°C');
|
if (smart.temperature != null) {
|
||||||
|
details.add('Temperature: ${smart.temperature!.toStringAsFixed(1)}°C');
|
||||||
|
}
|
||||||
|
|
||||||
if (smart.powerOnHours != null) {
|
if (smart.powerOnHours != null) {
|
||||||
details.add('Power On: ${smart.powerOnHours} ${libL10n.hour}');
|
details.add('Power On: ${smart.powerOnHours} ${libL10n.hour}');
|
||||||
|
|||||||
412
test/amd_smi_test.dart
Normal file
412
test/amd_smi_test.dart
Normal file
@@ -0,0 +1,412 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:server_box/data/model/server/amd.dart';
|
||||||
|
|
||||||
|
const _amdSmiRaw = '''
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "AMD Radeon RX 7900 XTX",
|
||||||
|
"device_id": "0",
|
||||||
|
"temp": 45,
|
||||||
|
"power_draw": 120,
|
||||||
|
"power_cap": 355,
|
||||||
|
"memory": {
|
||||||
|
"total": 24576,
|
||||||
|
"used": 1024,
|
||||||
|
"unit": "MB",
|
||||||
|
"processes": [
|
||||||
|
{
|
||||||
|
"pid": 2456,
|
||||||
|
"name": "firefox",
|
||||||
|
"memory": 512
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pid": 3784,
|
||||||
|
"name": "blender",
|
||||||
|
"memory": 256
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"utilization": 75,
|
||||||
|
"fan_speed": 1200,
|
||||||
|
"clock_speed": 2400
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "AMD Radeon RX 6800 XT",
|
||||||
|
"device_id": "1",
|
||||||
|
"temp": 38,
|
||||||
|
"power_draw": 85,
|
||||||
|
"power_cap": 300,
|
||||||
|
"memory": {
|
||||||
|
"total": 16384,
|
||||||
|
"used": 512,
|
||||||
|
"unit": "MB",
|
||||||
|
"processes": []
|
||||||
|
},
|
||||||
|
"utilization": 25,
|
||||||
|
"fan_speed": 800,
|
||||||
|
"clock_speed": 2100
|
||||||
|
}
|
||||||
|
]
|
||||||
|
''';
|
||||||
|
|
||||||
|
const _amdSmiRocmRaw = '''
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"card_model": "AMD Radeon RX 6700 XT",
|
||||||
|
"gpu_id": "card0",
|
||||||
|
"temperature": "42°C",
|
||||||
|
"power_draw": "95",
|
||||||
|
"power_cap": "230",
|
||||||
|
"vram": {
|
||||||
|
"total_memory": 12288,
|
||||||
|
"used_memory": 768,
|
||||||
|
"unit": "MiB",
|
||||||
|
"processes": [
|
||||||
|
{
|
||||||
|
"pid": 1234,
|
||||||
|
"process_name": "game.exe",
|
||||||
|
"used_memory": 512
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"gpu_util": "60%",
|
||||||
|
"fan_rpm": "950 RPM",
|
||||||
|
"sclk": "1800MHz"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
''';
|
||||||
|
|
||||||
|
const _amdSmiAlternativeRaw = '''
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"device_name": "Radeon RX 580",
|
||||||
|
"gpu_temp": 55,
|
||||||
|
"current_power": 150,
|
||||||
|
"power_limit": 185,
|
||||||
|
"memory": {
|
||||||
|
"total": 8192,
|
||||||
|
"used": 2048,
|
||||||
|
"unit": "MB"
|
||||||
|
},
|
||||||
|
"activity": 90,
|
||||||
|
"fan_speed": 1500,
|
||||||
|
"gpu_clock": 1366
|
||||||
|
}
|
||||||
|
]
|
||||||
|
''';
|
||||||
|
|
||||||
|
const _amdSmiEdgeCasesRaw = '''
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "Unknown AMD GPU",
|
||||||
|
"device_id": "",
|
||||||
|
"temp": null,
|
||||||
|
"power": null,
|
||||||
|
"memory": {},
|
||||||
|
"utilization": null,
|
||||||
|
"fan_speed": null,
|
||||||
|
"clock_speed": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "AMD Test GPU",
|
||||||
|
"device_id": "test",
|
||||||
|
"temp": "50°C",
|
||||||
|
"power_draw": 100,
|
||||||
|
"memory": {
|
||||||
|
"total": "16384MB",
|
||||||
|
"used": "2048MB"
|
||||||
|
},
|
||||||
|
"utilization": "80%",
|
||||||
|
"fan_speed": "1100 RPM",
|
||||||
|
"clock_speed": "2000 MHz"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
''';
|
||||||
|
|
||||||
|
const _invalidJson = '''
|
||||||
|
{
|
||||||
|
"invalid": "not an array"
|
||||||
|
}
|
||||||
|
''';
|
||||||
|
|
||||||
|
const _emptyArray = '[]';
|
||||||
|
|
||||||
|
const _malformedJson = '''
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "Test GPU"
|
||||||
|
// missing closing brace
|
||||||
|
''';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('AmdSmi JSON parsing', () {
|
||||||
|
test('parse standard AMD SMI output', () {
|
||||||
|
final gpus = AmdSmi.fromJson(_amdSmiRaw);
|
||||||
|
expect(gpus.length, 2);
|
||||||
|
|
||||||
|
final gpu1 = gpus[0];
|
||||||
|
expect(gpu1.name, 'AMD Radeon RX 7900 XTX');
|
||||||
|
expect(gpu1.deviceId, '0');
|
||||||
|
expect(gpu1.temp, 45);
|
||||||
|
expect(gpu1.power, '120W / 355W');
|
||||||
|
expect(gpu1.memory.total, 24576);
|
||||||
|
expect(gpu1.memory.used, 1024);
|
||||||
|
expect(gpu1.memory.unit, 'MB');
|
||||||
|
expect(gpu1.memory.processes.length, 2);
|
||||||
|
expect(gpu1.memory.processes[0].pid, 2456);
|
||||||
|
expect(gpu1.memory.processes[0].name, 'firefox');
|
||||||
|
expect(gpu1.memory.processes[0].memory, 512);
|
||||||
|
expect(gpu1.memory.processes[1].pid, 3784);
|
||||||
|
expect(gpu1.memory.processes[1].name, 'blender');
|
||||||
|
expect(gpu1.memory.processes[1].memory, 256);
|
||||||
|
expect(gpu1.utilization, 75);
|
||||||
|
expect(gpu1.fanSpeed, 1200);
|
||||||
|
expect(gpu1.clockSpeed, 2400);
|
||||||
|
|
||||||
|
final gpu2 = gpus[1];
|
||||||
|
expect(gpu2.name, 'AMD Radeon RX 6800 XT');
|
||||||
|
expect(gpu2.deviceId, '1');
|
||||||
|
expect(gpu2.temp, 38);
|
||||||
|
expect(gpu2.power, '85W / 300W');
|
||||||
|
expect(gpu2.memory.total, 16384);
|
||||||
|
expect(gpu2.memory.used, 512);
|
||||||
|
expect(gpu2.memory.unit, 'MB');
|
||||||
|
expect(gpu2.memory.processes.length, 0);
|
||||||
|
expect(gpu2.utilization, 25);
|
||||||
|
expect(gpu2.fanSpeed, 800);
|
||||||
|
expect(gpu2.clockSpeed, 2100);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('parse ROCm SMI output with different field names', () {
|
||||||
|
final gpus = AmdSmi.fromJson(_amdSmiRocmRaw);
|
||||||
|
expect(gpus.length, 1);
|
||||||
|
|
||||||
|
final gpu = gpus[0];
|
||||||
|
expect(gpu.name, 'AMD Radeon RX 6700 XT');
|
||||||
|
expect(gpu.deviceId, 'card0');
|
||||||
|
expect(gpu.temp, 42);
|
||||||
|
expect(gpu.power, '95W / 230W');
|
||||||
|
expect(gpu.memory.total, 12288);
|
||||||
|
expect(gpu.memory.used, 768);
|
||||||
|
expect(gpu.memory.unit, 'MiB');
|
||||||
|
expect(gpu.memory.processes.length, 1);
|
||||||
|
expect(gpu.memory.processes[0].pid, 1234);
|
||||||
|
expect(gpu.memory.processes[0].name, 'game.exe');
|
||||||
|
expect(gpu.memory.processes[0].memory, 512);
|
||||||
|
expect(gpu.utilization, 60);
|
||||||
|
expect(gpu.fanSpeed, 950);
|
||||||
|
expect(gpu.clockSpeed, 1800);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('parse alternative field names', () {
|
||||||
|
final gpus = AmdSmi.fromJson(_amdSmiAlternativeRaw);
|
||||||
|
expect(gpus.length, 1);
|
||||||
|
|
||||||
|
final gpu = gpus[0];
|
||||||
|
expect(gpu.name, 'Radeon RX 580');
|
||||||
|
expect(gpu.deviceId, '0');
|
||||||
|
expect(gpu.temp, 55);
|
||||||
|
expect(gpu.power, '150W / 185W');
|
||||||
|
expect(gpu.memory.total, 8192);
|
||||||
|
expect(gpu.memory.used, 2048);
|
||||||
|
expect(gpu.memory.unit, 'MB');
|
||||||
|
expect(gpu.memory.processes.length, 0);
|
||||||
|
expect(gpu.utilization, 90);
|
||||||
|
expect(gpu.fanSpeed, 1500);
|
||||||
|
expect(gpu.clockSpeed, 1366);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handle edge cases and string parsing', () {
|
||||||
|
final gpus = AmdSmi.fromJson(_amdSmiEdgeCasesRaw);
|
||||||
|
expect(gpus.length, 2);
|
||||||
|
|
||||||
|
final gpu1 = gpus[0];
|
||||||
|
expect(gpu1.name, 'Unknown AMD GPU');
|
||||||
|
expect(gpu1.deviceId, '');
|
||||||
|
expect(gpu1.temp, 0);
|
||||||
|
expect(gpu1.power, 'N/A');
|
||||||
|
expect(gpu1.memory.total, 0);
|
||||||
|
expect(gpu1.memory.used, 0);
|
||||||
|
expect(gpu1.memory.unit, 'MB');
|
||||||
|
expect(gpu1.memory.processes.length, 0);
|
||||||
|
expect(gpu1.utilization, 0);
|
||||||
|
expect(gpu1.fanSpeed, 0);
|
||||||
|
expect(gpu1.clockSpeed, 0);
|
||||||
|
|
||||||
|
final gpu2 = gpus[1];
|
||||||
|
expect(gpu2.name, 'AMD Test GPU');
|
||||||
|
expect(gpu2.deviceId, 'test');
|
||||||
|
expect(gpu2.temp, 50);
|
||||||
|
expect(gpu2.power, '100W');
|
||||||
|
expect(gpu2.memory.total, 16384);
|
||||||
|
expect(gpu2.memory.used, 2048);
|
||||||
|
expect(gpu2.memory.unit, 'MB');
|
||||||
|
expect(gpu2.utilization, 80);
|
||||||
|
expect(gpu2.fanSpeed, 1100);
|
||||||
|
expect(gpu2.clockSpeed, 2000);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handle invalid JSON gracefully', () {
|
||||||
|
final gpus1 = AmdSmi.fromJson(_invalidJson);
|
||||||
|
expect(gpus1.length, 0);
|
||||||
|
|
||||||
|
final gpus2 = AmdSmi.fromJson(_malformedJson);
|
||||||
|
expect(gpus2.length, 0);
|
||||||
|
|
||||||
|
final gpus3 = AmdSmi.fromJson('invalid json');
|
||||||
|
expect(gpus3.length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handle empty array', () {
|
||||||
|
final gpus = AmdSmi.fromJson(_emptyArray);
|
||||||
|
expect(gpus.length, 0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('AmdSmi helper methods', () {
|
||||||
|
test('_parseIntValue handles various input types', () {
|
||||||
|
expect(AmdSmi.fromJson('[{"name":"test","temp":42}]')[0].temp, 42);
|
||||||
|
expect(AmdSmi.fromJson('[{"name":"test","temp":"45°C"}]')[0].temp, 45);
|
||||||
|
expect(AmdSmi.fromJson('[{"name":"test","temp":"1200 RPM"}]')[0].temp, 1200);
|
||||||
|
expect(AmdSmi.fromJson('[{"name":"test","temp":"N/A"}]')[0].temp, 0);
|
||||||
|
expect(AmdSmi.fromJson('[{"name":"test","temp":null}]')[0].temp, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('_formatPower handles different power scenarios', () {
|
||||||
|
final gpu1 = AmdSmi.fromJson('[{"name":"test","power_draw":100,"power_cap":200}]')[0];
|
||||||
|
expect(gpu1.power, '100W / 200W');
|
||||||
|
|
||||||
|
final gpu2 = AmdSmi.fromJson('[{"name":"test","power_draw":50}]')[0];
|
||||||
|
expect(gpu2.power, '50W');
|
||||||
|
|
||||||
|
final gpu3 = AmdSmi.fromJson('[{"name":"test"}]')[0];
|
||||||
|
expect(gpu3.power, 'N/A');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('_parseMemory handles missing memory data', () {
|
||||||
|
final gpu = AmdSmi.fromJson('[{"name":"test"}]')[0];
|
||||||
|
expect(gpu.memory.total, 0);
|
||||||
|
expect(gpu.memory.used, 0);
|
||||||
|
expect(gpu.memory.unit, 'MB');
|
||||||
|
expect(gpu.memory.processes.length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('_parseProcess filters invalid processes', () {
|
||||||
|
const jsonWithInvalidProcess = '''
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "Test GPU",
|
||||||
|
"memory": {
|
||||||
|
"processes": [
|
||||||
|
{
|
||||||
|
"pid": 0,
|
||||||
|
"name": "invalid",
|
||||||
|
"memory": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pid": 1234,
|
||||||
|
"name": "valid",
|
||||||
|
"memory": 200
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
''';
|
||||||
|
|
||||||
|
final gpu = AmdSmi.fromJson(jsonWithInvalidProcess)[0];
|
||||||
|
expect(gpu.memory.processes.length, 1);
|
||||||
|
expect(gpu.memory.processes[0].pid, 1234);
|
||||||
|
expect(gpu.memory.processes[0].name, 'valid');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('AmdSmi data classes', () {
|
||||||
|
test('AmdSmiItem toString', () {
|
||||||
|
final memory = AmdSmiMem(8192, 2048, 'MB', []);
|
||||||
|
final item = AmdSmiItem(
|
||||||
|
deviceId: '0',
|
||||||
|
name: 'Test GPU',
|
||||||
|
temp: 45,
|
||||||
|
power: '100W / 200W',
|
||||||
|
memory: memory,
|
||||||
|
utilization: 75,
|
||||||
|
fanSpeed: 1200,
|
||||||
|
clockSpeed: 2400,
|
||||||
|
);
|
||||||
|
|
||||||
|
final toString = item.toString();
|
||||||
|
expect(toString, contains('Test GPU'));
|
||||||
|
expect(toString, contains('45'));
|
||||||
|
expect(toString, contains('100W / 200W'));
|
||||||
|
expect(toString, contains('75%'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('AmdSmiMem toString', () {
|
||||||
|
final process = AmdSmiMemProcess(1234, 'test', 512);
|
||||||
|
final memory = AmdSmiMem(8192, 2048, 'MB', [process]);
|
||||||
|
|
||||||
|
final toString = memory.toString();
|
||||||
|
expect(toString, contains('8192'));
|
||||||
|
expect(toString, contains('2048'));
|
||||||
|
expect(toString, contains('MB'));
|
||||||
|
expect(toString, contains('1'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('AmdSmiMemProcess toString', () {
|
||||||
|
final process = AmdSmiMemProcess(1234, 'firefox', 512);
|
||||||
|
|
||||||
|
final toString = process.toString();
|
||||||
|
expect(toString, contains('1234'));
|
||||||
|
expect(toString, contains('firefox'));
|
||||||
|
expect(toString, contains('512'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('AmdSmi robustness', () {
|
||||||
|
test('handles malformed GPU objects gracefully', () {
|
||||||
|
const malformedGpuJson = '''
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "Valid GPU",
|
||||||
|
"temp": 45
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"malformed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Another Valid GPU",
|
||||||
|
"temp": 50
|
||||||
|
}
|
||||||
|
]
|
||||||
|
''';
|
||||||
|
|
||||||
|
final gpus = AmdSmi.fromJson(malformedGpuJson);
|
||||||
|
expect(gpus.length, 3);
|
||||||
|
expect(gpus[0].name, 'Valid GPU');
|
||||||
|
expect(gpus[0].temp, 45);
|
||||||
|
expect(gpus[1].name, 'Unknown AMD GPU');
|
||||||
|
expect(gpus[1].temp, 0);
|
||||||
|
expect(gpus[2].name, 'Another Valid GPU');
|
||||||
|
expect(gpus[2].temp, 50);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles missing required fields with defaults', () {
|
||||||
|
const minimalGpuJson = '''
|
||||||
|
[
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
''';
|
||||||
|
|
||||||
|
final gpus = AmdSmi.fromJson(minimalGpuJson);
|
||||||
|
expect(gpus.length, 1);
|
||||||
|
expect(gpus[0].name, 'Unknown AMD GPU');
|
||||||
|
expect(gpus[0].deviceId, '0');
|
||||||
|
expect(gpus[0].temp, 0);
|
||||||
|
expect(gpus[0].power, 'N/A');
|
||||||
|
expect(gpus[0].utilization, 0);
|
||||||
|
expect(gpus[0].fanSpeed, 0);
|
||||||
|
expect(gpus[0].clockSpeed, 0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user