mirror of
https://github.com/lollipopkit/flutter_server_box.git
synced 2025-12-18 15:54:35 +01:00
feat: amd gpu (#831)
This commit is contained in:
@@ -16,6 +16,12 @@ enum ShellFunc {
|
||||
/// The suffix `\t` is for formatting
|
||||
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
|
||||
static const scriptFile = 'srvboxm_v${BuildData.script}.sh';
|
||||
static const scriptDirHome = '~/.config/server_box';
|
||||
@@ -28,13 +34,10 @@ enum ShellFunc {
|
||||
/// Default is [scriptDirTmp]/[scriptFile], if this path is not accessible,
|
||||
/// it will be changed to [scriptDirHome]/[scriptFile].
|
||||
static String getScriptDir(String id) {
|
||||
final customScriptDir = ServerProvider.pick(
|
||||
id: id,
|
||||
)?.value.spi.custom?.scriptDir;
|
||||
final customScriptDir = ServerProvider.pick(id: id)?.value.spi.custom?.scriptDir;
|
||||
if (customScriptDir != null) return customScriptDir;
|
||||
return _scriptDirMap.putIfAbsent(id, () {
|
||||
return scriptDirTmp;
|
||||
});
|
||||
_scriptDirMap[id] ??= scriptDirTmp;
|
||||
return _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 get name {
|
||||
switch (this) {
|
||||
case ShellFunc.status:
|
||||
return 'status';
|
||||
// case ShellFunc.docker:
|
||||
// // `dockeR` -> avoid conflict with `docker` command
|
||||
// return 'dockeR';
|
||||
case ShellFunc.process:
|
||||
return 'process';
|
||||
case ShellFunc.shutdown:
|
||||
return 'ShutDown';
|
||||
case ShellFunc.reboot:
|
||||
return 'Reboot';
|
||||
case ShellFunc.suspend:
|
||||
return 'Suspend';
|
||||
}
|
||||
}
|
||||
String get name => switch (this) {
|
||||
ShellFunc.status => 'status',
|
||||
ShellFunc.process => 'process',
|
||||
ShellFunc.shutdown => 'ShutDown',
|
||||
ShellFunc.reboot => 'Reboot',
|
||||
ShellFunc.suspend => 'Suspend',
|
||||
};
|
||||
|
||||
String get _cmd {
|
||||
switch (this) {
|
||||
case ShellFunc.status:
|
||||
return '''
|
||||
String get _cmd => switch (this) {
|
||||
ShellFunc.status =>
|
||||
'''
|
||||
if [ "\$macSign" = "" ] && [ "\$bsdSign" = "" ]; then
|
||||
\t${StatusCmdType.values.map((e) => e.cmd).join(cmdDivider)}
|
||||
\t$_linuxStatusCmds
|
||||
else
|
||||
\t${BSDStatusCmdType.values.map((e) => e.cmd).join(cmdDivider)}
|
||||
fi''';
|
||||
// case ShellFunc.docker:
|
||||
// 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 '''
|
||||
\t$_bsdStatusCmds
|
||||
fi''',
|
||||
ShellFunc.process =>
|
||||
'''
|
||||
if [ "\$macSign" = "" ] && [ "\$bsdSign" = "" ]; then
|
||||
\tif [ "\$isBusybox" != "" ]; then
|
||||
\t\tps w
|
||||
@@ -114,30 +98,29 @@ if [ "\$macSign" = "" ] && [ "\$bsdSign" = "" ]; then
|
||||
else
|
||||
\tps -ax
|
||||
fi
|
||||
''';
|
||||
case ShellFunc.shutdown:
|
||||
return '''
|
||||
''',
|
||||
ShellFunc.shutdown =>
|
||||
'''
|
||||
if [ "\$userId" = "0" ]; then
|
||||
\tshutdown -h now
|
||||
else
|
||||
\tsudo -S shutdown -h now
|
||||
fi''';
|
||||
case ShellFunc.reboot:
|
||||
return '''
|
||||
fi''',
|
||||
ShellFunc.reboot =>
|
||||
'''
|
||||
if [ "\$userId" = "0" ]; then
|
||||
\treboot
|
||||
else
|
||||
\tsudo -S reboot
|
||||
fi''';
|
||||
case ShellFunc.suspend:
|
||||
return '''
|
||||
fi''',
|
||||
ShellFunc.suspend =>
|
||||
'''
|
||||
if [ "\$userId" = "0" ]; then
|
||||
\tsystemctl suspend
|
||||
else
|
||||
\tsudo -S systemctl suspend
|
||||
fi''';
|
||||
}
|
||||
}
|
||||
fi''',
|
||||
};
|
||||
|
||||
static String allScript(Map<String, String>? customCmds) {
|
||||
final sb = StringBuffer();
|
||||
@@ -163,9 +146,7 @@ exec 2>/dev/null
|
||||
// Write each func
|
||||
for (final func in values) {
|
||||
final customCmdsStr = () {
|
||||
if (func == ShellFunc.status &&
|
||||
customCmds != null &&
|
||||
customCmds.isNotEmpty) {
|
||||
if (func == ShellFunc.status && customCmds != null && customCmds.isNotEmpty) {
|
||||
return '$cmdDivider\n\t${customCmds.values.join(cmdDivider)}';
|
||||
}
|
||||
return '';
|
||||
@@ -212,18 +193,15 @@ enum StatusCmdType {
|
||||
cpu._('cat /proc/stat | grep cpu'),
|
||||
uptime._('uptime'),
|
||||
conn._('cat /proc/net/snmp'),
|
||||
disk._(
|
||||
'lsblk --bytes --json --output FSTYPE,PATH,NAME,KNAME,MOUNTPOINT,FSSIZE,FSUSED,FSAVAIL,FSUSE%,UUID',
|
||||
),
|
||||
disk._('lsblk --bytes --json --output FSTYPE,PATH,NAME,KNAME,MOUNTPOINT,FSSIZE,FSUSED,FSAVAIL,FSUSE%,UUID'),
|
||||
mem._("cat /proc/meminfo | grep -E 'Mem|Swap'"),
|
||||
tempType._('cat /sys/class/thermal/thermal_zone*/type'),
|
||||
tempVal._('cat /sys/class/thermal/thermal_zone*/temp'),
|
||||
host._('cat /etc/hostname'),
|
||||
diskio._('cat /proc/diskstats'),
|
||||
battery._(
|
||||
'for f in /sys/class/power_supply/*/uevent; do cat "\$f"; echo; done',
|
||||
),
|
||||
battery._('for f in /sys/class/power_supply/*/uevent; do cat "\$f"; echo; done'),
|
||||
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'),
|
||||
diskSmart._('for d in \$(lsblk -dn -o KNAME); do smartctl -a -j /dev/\$d; echo; done'),
|
||||
cpuBrand._('cat /proc/cpuinfo | grep "model name"');
|
||||
@@ -258,6 +236,8 @@ extension StatusCmdTypeX on StatusCmdType {
|
||||
StatusCmdType.host => l10n.host,
|
||||
StatusCmdType.uptime => l10n.uptime,
|
||||
StatusCmdType.battery => l10n.battery,
|
||||
StatusCmdType.sensors => l10n.sensors,
|
||||
StatusCmdType.disk => l10n.disk,
|
||||
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:fl_lib/fl_lib.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/conn.dart';
|
||||
import 'package:server_box/data/model/server/cpu.dart';
|
||||
@@ -42,6 +43,7 @@ class ServerStatus {
|
||||
DiskIO diskIO;
|
||||
List<DiskSmart> diskSmart;
|
||||
List<NvidiaSmiItem>? nvidia;
|
||||
List<AmdSmiItem>? amd;
|
||||
final List<Battery> batteries = [];
|
||||
final Map<StatusCmdType, String> more = {};
|
||||
final List<SensorItem> sensors = [];
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:fl_lib/fl_lib.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/conn.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);
|
||||
}
|
||||
|
||||
try {
|
||||
req.ss.amd = AmdSmi.fromJson(StatusCmdType.amd.find(segments));
|
||||
} catch (e, s) {
|
||||
Loggers.app.warning(e, s);
|
||||
}
|
||||
|
||||
try {
|
||||
final battery = StatusCmdType.battery.find(segments);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user