feat: Windows compatibility (#836)

* feat: win compatibility

* fix

* fix: uptime parse

* opt.: linux uptime accuracy

* fix: windows temperature fetching

* opt.

* opt.: powershell exec

* refactor: address PR review feedback and improve code quality

### Major Improvements:
- **Refactored Windows status parsing**: Broke down large `_getWindowsStatus` method into 13 smaller, focused helper methods for better maintainability and readability
- **Extracted system detection logic**: Created dedicated `SystemDetector` helper class to separate OS detection concerns from ServerProvider
- **Improved concurrency handling**: Implemented proper synchronization for server updates using Future-based locks to prevent race conditions

### Bug Fixes:
- **Fixed CPU percentage parsing**: Removed incorrect '*100' multiplication in BSD CPU parsing (values were already percentages)
- **Enhanced memory parsing**: Added validation and error handling to BSD memory fallback parsing with proper logging
- **Improved uptime parsing**: Added support for multiple Windows date formats and robust error handling with validation
- **Fixed division by zero**: Added safety checks in Swap.usedPercent getter

### Code Quality Enhancements:
- **Added comprehensive documentation**: Documented Windows CPU counter limitations and approach
- **Strengthened error handling**: Added detailed logging and validation throughout parsing methods
- **Improved robustness**: Enhanced BSD CPU parsing with percentage validation and warnings
- **Better separation of concerns**: Each parsing method now has single responsibility

### Files Changed:
- `lib/data/helper/system_detector.dart` (new): System detection helper
- `lib/data/model/server/cpu.dart`: Fixed percentage parsing and added validation
- `lib/data/model/server/memory.dart`: Enhanced fallback parsing and division-by-zero protection
- `lib/data/model/server/server_status_update_req.dart`: Refactored into 13 focused parsing methods
- `lib/data/provider/server.dart`: Improved synchronization and extracted system detection

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor: parse & shell fn struct

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
lollipopkit🏳️‍⚧️
2025-08-08 16:56:36 +08:00
committed by GitHub
parent 46a12bc844
commit 3a615449e3
103 changed files with 9591 additions and 1906 deletions

View File

@@ -213,13 +213,10 @@ class Backup implements Mergeable {
_logger.info('Restore success');
}
factory Backup.fromJsonString(String raw) =>
Backup.fromJson(json.decode(_diyDecrypt(raw)));
factory Backup.fromJsonString(String raw) => Backup.fromJson(json.decode(_diyDecrypt(raw)));
}
String _diyEncrypt(String raw) => json.encode(
raw.codeUnits.map((e) => e * 2 + 1).toList(growable: false),
);
String _diyEncrypt(String raw) => json.encode(raw.codeUnits.map((e) => e * 2 + 1).toList(growable: false));
String _diyDecrypt(String raw) {
try {
@@ -234,4 +231,3 @@ String _diyDecrypt(String raw) {
rethrow;
}
}

View File

@@ -81,7 +81,7 @@ abstract class BackupV2 with _$BackupV2 implements Mergeable {
if (password != null && password.isNotEmpty) {
result = Cryptor.encrypt(result, password);
}
final path = Paths.doc.joinPath(name ?? Miscs.bakFileName);
await File(path).writeAsString(result);
return path;
@@ -94,7 +94,7 @@ abstract class BackupV2 with _$BackupV2 implements Mergeable {
}
jsonString = Cryptor.decrypt(jsonString, password);
}
final map = json.decode(jsonString) as Map<String, dynamic>;
return BackupV2.fromJson(map);
}

View File

@@ -7,13 +7,13 @@ import 'package:flutter/material.dart';
abstract class BackupSource {
/// Get content from this source for restore
Future<String?> getContent();
/// Save content to this source for backup
Future<void> saveContent(String filePath);
/// Display name for this source
String get displayName;
/// Icon for this source
IconData get icon;
}
@@ -59,4 +59,4 @@ class ClipboardBackupSource implements BackupSource {
@override
IconData get icon => Icons.content_paste;
}
}

View File

@@ -1,29 +1,19 @@
import 'package:fl_lib/fl_lib.dart';
import 'package:server_box/core/extension/context/locale.dart';
enum SSHErrType {
unknown,
connect,
auth,
noPrivateKey,
chdir,
segements,
writeScript,
getStatus,
;
}
enum SSHErrType { unknown, connect, auth, noPrivateKey, chdir, segements, writeScript, getStatus }
class SSHErr extends Err<SSHErrType> {
SSHErr({required super.type, super.message});
@override
String? get solution => switch (type) {
SSHErrType.chdir => l10n.needHomeDir,
SSHErrType.auth => l10n.authFailTip,
SSHErrType.writeScript => l10n.writeScriptFailTip,
SSHErrType.noPrivateKey => l10n.noPrivateKeyTip,
_ => null,
};
SSHErrType.chdir => l10n.needHomeDir,
SSHErrType.auth => l10n.authFailTip,
SSHErrType.writeScript => l10n.writeScriptFailTip,
SSHErrType.noPrivateKey => l10n.noPrivateKeyTip,
_ => null,
};
}
enum ContainerErrType {
@@ -45,11 +35,7 @@ class ContainerErr extends Err<ContainerErrType> {
String? get solution => null;
}
enum ICloudErrType {
generic,
notFound,
multipleFiles,
}
enum ICloudErrType { generic, notFound, multipleFiles }
class ICloudErr extends Err<ICloudErrType> {
ICloudErr({required super.type, super.message});
@@ -58,11 +44,7 @@ class ICloudErr extends Err<ICloudErrType> {
String? get solution => null;
}
enum WebdavErrType {
generic,
notFound,
;
}
enum WebdavErrType { generic, notFound }
class WebdavErr extends Err<WebdavErrType> {
WebdavErr({required super.type, super.message});
@@ -71,12 +53,7 @@ class WebdavErr extends Err<WebdavErrType> {
String? get solution => null;
}
enum PveErrType {
unknown,
net,
loginFailed,
;
}
enum PveErrType { unknown, net, loginFailed }
class PveErr extends Err<PveErrType> {
PveErr({required super.type, super.message});

View File

@@ -8,7 +8,7 @@ enum ContainerMenu {
restart,
rm,
logs,
terminal,
terminal
//stats,
;
@@ -27,22 +27,22 @@ enum ContainerMenu {
}
IconData get icon => switch (this) {
ContainerMenu.start => Icons.play_arrow,
ContainerMenu.stop => Icons.stop,
ContainerMenu.restart => Icons.restart_alt,
ContainerMenu.rm => Icons.delete,
ContainerMenu.logs => Icons.logo_dev,
ContainerMenu.terminal => Icons.terminal,
// DockerMenuType.stats => Icons.bar_chart,
};
ContainerMenu.start => Icons.play_arrow,
ContainerMenu.stop => Icons.stop,
ContainerMenu.restart => Icons.restart_alt,
ContainerMenu.rm => Icons.delete,
ContainerMenu.logs => Icons.logo_dev,
ContainerMenu.terminal => Icons.terminal,
// DockerMenuType.stats => Icons.bar_chart,
};
String get toStr => switch (this) {
ContainerMenu.start => l10n.start,
ContainerMenu.stop => l10n.stop,
ContainerMenu.restart => l10n.restart,
ContainerMenu.rm => libL10n.delete,
ContainerMenu.logs => libL10n.log,
ContainerMenu.terminal => l10n.terminal,
// DockerMenuType.stats => s.stats,
};
ContainerMenu.start => l10n.start,
ContainerMenu.stop => l10n.stop,
ContainerMenu.restart => l10n.restart,
ContainerMenu.rm => libL10n.delete,
ContainerMenu.logs => libL10n.log,
ContainerMenu.terminal => l10n.terminal,
// DockerMenuType.stats => s.stats,
};
}

View File

@@ -12,8 +12,7 @@ enum ServerFuncBtn {
snippet(),
iperf(),
// pve(),
systemd(1058),
;
systemd(1058);
final int? addedVersion;
@@ -41,24 +40,24 @@ enum ServerFuncBtn {
].map((e) => e.index).toList();
IconData get icon => switch (this) {
sftp => Icons.insert_drive_file,
snippet => Icons.code,
//pkg => Icons.system_security_update,
container => FontAwesome.docker_brand,
process => Icons.list_alt_outlined,
terminal => Icons.terminal,
iperf => Icons.speed,
systemd => MingCute.plugin_2_fill,
};
sftp => Icons.insert_drive_file,
snippet => Icons.code,
//pkg => Icons.system_security_update,
container => FontAwesome.docker_brand,
process => Icons.list_alt_outlined,
terminal => Icons.terminal,
iperf => Icons.speed,
systemd => MingCute.plugin_2_fill,
};
String get toStr => switch (this) {
sftp => 'SFTP',
snippet => l10n.snippet,
//pkg => l10n.pkg,
container => l10n.container,
process => l10n.process,
terminal => l10n.terminal,
iperf => 'iperf',
systemd => 'Systemd',
};
sftp => 'SFTP',
snippet => l10n.snippet,
//pkg => l10n.pkg,
container => l10n.container,
process => l10n.process,
terminal => l10n.terminal,
iperf => 'iperf',
systemd => 'Systemd',
};
}

View File

@@ -8,16 +8,16 @@ enum NetViewType {
traffic;
NetViewType get next => switch (this) {
conn => speed,
speed => traffic,
traffic => conn,
};
conn => speed,
speed => traffic,
traffic => conn,
};
String get toStr => switch (this) {
NetViewType.conn => l10n.conn,
NetViewType.traffic => l10n.traffic,
NetViewType.speed => l10n.speed,
};
NetViewType.conn => l10n.conn,
NetViewType.traffic => l10n.traffic,
NetViewType.speed => l10n.speed,
};
/// If no device is specified, return the cached value (only real devices,
/// such as ethX, wlanX...).
@@ -26,32 +26,17 @@ enum NetViewType {
try {
switch (this) {
case NetViewType.conn:
return (
'${l10n.conn}:\n${ss.tcp.maxConn}',
'${libL10n.fail}:\n${ss.tcp.fail}',
);
return ('${l10n.conn}:\n${ss.tcp.maxConn}', '${libL10n.fail}:\n${ss.tcp.fail}');
case NetViewType.speed:
if (notSepcifyDev) {
return (
'↓:\n${ss.netSpeed.cachedVals.speedIn}',
'↑:\n${ss.netSpeed.cachedVals.speedOut}',
);
return ('↓:\n${ss.netSpeed.cachedVals.speedIn}', '↑:\n${ss.netSpeed.cachedVals.speedOut}');
}
return (
'↓:\n${ss.netSpeed.speedIn(device: dev)}',
'↑:\n${ss.netSpeed.speedOut(device: dev)}',
);
return ('↓:\n${ss.netSpeed.speedIn(device: dev)}', '↑:\n${ss.netSpeed.speedOut(device: dev)}');
case NetViewType.traffic:
if (notSepcifyDev) {
return (
'↓:\n${ss.netSpeed.cachedVals.sizeIn}',
'↑:\n${ss.netSpeed.cachedVals.sizeOut}',
);
return ('↓:\n${ss.netSpeed.cachedVals.sizeIn}', '↑:\n${ss.netSpeed.cachedVals.sizeOut}');
}
return (
'↓:\n${ss.netSpeed.sizeIn(device: dev)}',
'↑:\n${ss.netSpeed.sizeOut(device: dev)}',
);
return ('↓:\n${ss.netSpeed.sizeIn(device: dev)}', '↑:\n${ss.netSpeed.sizeOut(device: dev)}');
}
} catch (e, s) {
Loggers.app.warning('NetViewType.build', e, s);
@@ -60,14 +45,14 @@ enum NetViewType {
}
int toJson() => switch (this) {
NetViewType.conn => 0,
NetViewType.speed => 1,
NetViewType.traffic => 2,
};
NetViewType.conn => 0,
NetViewType.speed => 1,
NetViewType.traffic => 2,
};
static NetViewType fromJson(int json) => switch (json) {
0 => NetViewType.conn,
1 => NetViewType.speed,
_ => NetViewType.traffic,
};
0 => NetViewType.conn,
1 => NetViewType.speed,
_ => NetViewType.traffic,
};
}

View File

@@ -0,0 +1,242 @@
import 'package:server_box/data/model/app/shell_func.dart';
import 'package:server_box/data/res/build_data.dart';
/// Abstract base class for platform-specific script builders
abstract class ScriptBuilder {
const ScriptBuilder();
/// Generate a complete script for all shell functions
String buildScript(Map<String, String>? customCmds);
/// Get the script file name for this platform
String get scriptFileName;
/// Get the command to install the script
String getInstallCommand(String scriptDir, String scriptPath);
/// Get the execution command for a specific function
String getExecCommand(String scriptPath, ShellFunc func);
/// Get custom commands string for this platform
String getCustomCmdsString(
ShellFunc func,
Map<String, String>? customCmds,
);
}
/// Windows PowerShell script builder
class WindowsScriptBuilder extends ScriptBuilder {
const WindowsScriptBuilder();
@override
String get scriptFileName => 'srvboxm_v${BuildData.script}.ps1';
@override
String getInstallCommand(String scriptDir, String scriptPath) {
return 'New-Item -ItemType Directory -Force -Path \'$scriptDir\' | Out-Null; '
'\$content = [System.Console]::In.ReadToEnd(); '
'Set-Content -Path \'$scriptPath\' -Value \$content -Encoding UTF8';
}
@override
String getExecCommand(String scriptPath, ShellFunc func) {
return 'powershell -ExecutionPolicy Bypass -File "$scriptPath" -${func.flag}';
}
@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')}';
}
return '';
}
@override
String buildScript(Map<String, String>? customCmds) {
final sb = StringBuffer();
sb.write('''
# PowerShell script for ServerBox app v1.0.${BuildData.build}
# DO NOT delete this file while app is running
\$ErrorActionPreference = "SilentlyContinue"
''');
// Write each function
for (final func in ShellFunc.values) {
final customCmdsStr = getCustomCmdsString(func, customCmds);
sb.write('''
function ${func.name} {
${_getWindowsCommand(func).split('\n').map((e) => e.isEmpty ? '' : ' $e').join('\n')}$customCmdsStr
}
''');
}
// Write switch case
sb.write('''
switch (\$args[0]) {
''');
for (final func in ShellFunc.values) {
sb.write('''
"-${func.flag}" { ${func.name} }
''');
}
sb.write('''
default { Write-Host "Invalid argument \$(\$args[0])" }
}
''');
return sb.toString();
}
String _getWindowsCommand(ShellFunc func) => switch (func) {
ShellFunc.status => WindowsStatusCmdType.values.map((e) => e.cmd).join(ShellFunc.cmdDivider),
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)',
};
}
/// Unix shell script builder
class UnixScriptBuilder extends ScriptBuilder {
const UnixScriptBuilder();
@override
String get scriptFileName => 'srvboxm_v${BuildData.script}.sh';
@override
String getInstallCommand(String scriptDir, String scriptPath) {
return '''
mkdir -p $scriptDir
cat > $scriptPath
chmod 755 $scriptPath
''';
}
@override
String getExecCommand(String scriptPath, ShellFunc func) {
return 'sh $scriptPath -${func.flag}';
}
@override
String getCustomCmdsString(
ShellFunc func,
Map<String, String>? customCmds,
) {
if (func == ShellFunc.status && customCmds != null && customCmds.isNotEmpty) {
return '${ShellFunc.cmdDivider}\n\t${customCmds.values.join(ShellFunc.cmdDivider)}';
}
return '';
}
@override
String buildScript(Map<String, String>? customCmds) {
final sb = StringBuffer();
sb.write('''
#!/bin/sh
# Script for ServerBox app v1.0.${BuildData.build}
# DO NOT delete this file while app is running
export LANG=en_US.UTF-8
# If macSign & bsdSign are both empty, then it's linux
macSign=\$(uname -a 2>&1 | grep "Darwin")
bsdSign=\$(uname -a 2>&1 | grep "BSD")
# Link /bin/sh to busybox?
isBusybox=\$(ls -l /bin/sh | grep "busybox")
userId=\$(id -u)
exec 2>/dev/null
''');
// Write each function
for (final func in ShellFunc.values) {
final customCmdsStr = getCustomCmdsString(func, customCmds);
sb.write('''
${func.name}() {
${_getUnixCommand(func).split('\n').map((e) => '\t$e').join('\n')}
$customCmdsStr
}
''');
}
// Write switch case
sb.write('case \$1 in\n');
for (final func in ShellFunc.values) {
sb.write('''
'-${func.flag}')
${func.name}
;;
''');
}
sb.write('''
*)
echo "Invalid argument \$1"
;;
esac''');
return sb.toString();
}
String _getUnixCommand(ShellFunc func) {
switch (func) {
case ShellFunc.status:
return '''
if [ "\$macSign" = "" ] && [ "\$bsdSign" = "" ]; then
\t${StatusCmdType.values.map((e) => e.cmd).join(ShellFunc.cmdDivider)}
else
\t${BSDStatusCmdType.values.map((e) => e.cmd).join(ShellFunc.cmdDivider)}
fi''';
case ShellFunc.process:
return '''
if [ "\$macSign" = "" ] && [ "\$bsdSign" = "" ]; then
\tif [ "\$isBusybox" != "" ]; then
\t\tps w
\telse
\t\tps -aux
\tfi
else
\tps -ax
fi
''';
case ShellFunc.shutdown:
return '''
if [ "\$userId" = "0" ]; then
\tshutdown -h now
else
\tsudo -S shutdown -h now
fi''';
case ShellFunc.reboot:
return '''
if [ "\$userId" = "0" ]; then
\treboot
else
\tsudo -S reboot
fi''';
case ShellFunc.suspend:
return '''
if [ "\$userId" = "0" ]; then
\tsystemctl suspend
else
\tsudo -S systemctl suspend
fi''';
}
}
}
/// Factory class to get appropriate script builder for platform
class ScriptBuilderFactory {
const ScriptBuilderFactory._();
static ScriptBuilder getBuilder(bool isWindows) {
return isWindows ? const WindowsScriptBuilder() : const UnixScriptBuilder();
}
}

View File

@@ -1,31 +1,33 @@
import 'package:server_box/core/extension/context/locale.dart';
import 'package:server_box/data/model/app/script_builders.dart';
import 'package:server_box/data/model/server/system.dart';
import 'package:server_box/data/provider/server.dart';
import 'package:server_box/data/res/build_data.dart';
enum ShellFunc {
status,
status('SbStatus'),
//docker,
process,
shutdown,
reboot,
suspend;
process('SbProcess'),
shutdown('SbShutdown'),
reboot('SbReboot'),
suspend('SbSuspend');
final String name;
const ShellFunc(this.name);
static const seperator = 'SrvBoxSep';
/// 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 scriptFileWindows = 'srvboxm_v${BuildData.script}.ps1';
static const scriptDirHome = '~/.config/server_box';
static const scriptDirTmp = '/tmp/server_box';
static const scriptDirHomeWindows = '%USERPROFILE%/.config/server_box';
static const scriptDirTmpWindows = '%TEMP%/server_box';
static final _scriptDirMap = <String, String>{};
@@ -33,31 +35,38 @@ enum ShellFunc {
///
/// Default is [scriptDirTmp]/[scriptFile], if this path is not accessible,
/// it will be changed to [scriptDirHome]/[scriptFile].
static String getScriptDir(String id) {
static String getScriptDir(String id, {SystemType? systemType}) {
final customScriptDir = ServerProvider.pick(id: id)?.value.spi.custom?.scriptDir;
if (customScriptDir != null) return customScriptDir;
_scriptDirMap[id] ??= scriptDirTmp;
final defaultTmpDir = systemType == SystemType.windows ? scriptDirTmpWindows : scriptDirTmp;
_scriptDirMap[id] ??= defaultTmpDir;
return _scriptDirMap[id]!;
}
static void switchScriptDir(String id) => switch (_scriptDirMap[id]) {
static void switchScriptDir(String id, {SystemType? systemType}) => switch (_scriptDirMap[id]) {
scriptDirTmp => _scriptDirMap[id] = scriptDirHome,
scriptDirTmpWindows => _scriptDirMap[id] = scriptDirHomeWindows,
scriptDirHome => _scriptDirMap[id] = scriptDirTmp,
_ => _scriptDirMap[id] = scriptDirHome,
scriptDirHomeWindows => _scriptDirMap[id] = scriptDirTmpWindows,
_ => _scriptDirMap[id] = systemType == SystemType.windows ? scriptDirHomeWindows : scriptDirHome,
};
static String getScriptPath(String id) {
return '${getScriptDir(id)}/$scriptFile';
static String getScriptPath(String id, {SystemType? systemType}) {
final dir = getScriptDir(id, systemType: systemType);
final fileName = systemType == SystemType.windows ? scriptFileWindows : scriptFile;
final separator = systemType == SystemType.windows ? '\\' : '/';
return '$dir$separator$fileName';
}
static String getInstallShellCmd(String id) {
final scriptDir = getScriptDir(id);
final scriptPath = '$scriptDir/$scriptFile';
return '''
mkdir -p $scriptDir
cat > $scriptPath
chmod 755 $scriptPath
''';
static String getInstallShellCmd(String id, {SystemType? systemType}) {
final scriptDir = getScriptDir(id, systemType: systemType);
final isWindows = systemType == SystemType.windows;
final builder = ScriptBuilderFactory.getBuilder(isWindows);
final separator = isWindows ? '\\' : '/';
final scriptPath = '$scriptDir$separator${builder.scriptFileName}';
return builder.getInstallCommand(scriptDir, scriptPath);
}
String get flag => switch (this) {
@@ -69,120 +78,24 @@ chmod 755 $scriptPath
// ShellFunc.docker=> 'd',
};
String exec(String id) => 'sh ${getScriptPath(id)} -$flag';
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) {
ShellFunc.status =>
'''
if [ "\$macSign" = "" ] && [ "\$bsdSign" = "" ]; then
\t$_linuxStatusCmds
else
\t$_bsdStatusCmds
fi''',
ShellFunc.process =>
'''
if [ "\$macSign" = "" ] && [ "\$bsdSign" = "" ]; then
\tif [ "\$isBusybox" != "" ]; then
\t\tps w
\telse
\t\tps -aux
\tfi
else
\tps -ax
fi
''',
ShellFunc.shutdown =>
'''
if [ "\$userId" = "0" ]; then
\tshutdown -h now
else
\tsudo -S shutdown -h now
fi''',
ShellFunc.reboot =>
'''
if [ "\$userId" = "0" ]; then
\treboot
else
\tsudo -S reboot
fi''',
ShellFunc.suspend =>
'''
if [ "\$userId" = "0" ]; then
\tsystemctl suspend
else
\tsudo -S systemctl suspend
fi''',
};
static String allScript(Map<String, String>? customCmds) {
final sb = StringBuffer();
sb.write('''
#!/bin/sh
# Script for ServerBox app v1.0.${BuildData.build}
# DO NOT delete this file while app is running
export LANG=en_US.UTF-8
# If macSign & bsdSign are both empty, then it's linux
macSign=\$(uname -a 2>&1 | grep "Darwin")
bsdSign=\$(uname -a 2>&1 | grep "BSD")
# Link /bin/sh to busybox?
isBusybox=\$(ls -l /bin/sh | grep "busybox")
userId=\$(id -u)
exec 2>/dev/null
''');
// Write each func
for (final func in values) {
final customCmdsStr = () {
if (func == ShellFunc.status && customCmds != null && customCmds.isNotEmpty) {
return '$cmdDivider\n\t${customCmds.values.join(cmdDivider)}';
}
return '';
}();
sb.write('''
${func.name}() {
${func._cmd.split('\n').map((e) => '\t$e').join('\n')}
$customCmdsStr
}
''');
}
// Write switch case
sb.write('case \$1 in\n');
for (final func in values) {
sb.write('''
'-${func.flag}')
${func.name}
;;
''');
}
sb.write('''
*)
echo "Invalid argument \$1"
;;
esac''');
return sb.toString();
String exec(String id, {SystemType? systemType}) {
final scriptPath = getScriptPath(id, systemType: systemType);
final isWindows = systemType == SystemType.windows;
final builder = ScriptBuilderFactory.getBuilder(isWindows);
return builder.getExecCommand(scriptPath, this);
}
}
extension EnumX on Enum {
/// Find out the required segment from [segments]
String find(List<String> segments) {
return segments[index];
/// Generate script based on system type
static String allScript(Map<String, String>? customCmds, {SystemType? systemType}) {
final isWindows = systemType == SystemType.windows;
final builder = ScriptBuilderFactory.getBuilder(isWindows);
return builder.buildScript(customCmds);
}
}
enum StatusCmdType {
@@ -193,7 +106,10 @@ 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'),
@@ -201,7 +117,16 @@ enum StatusCmdType {
diskio._('cat /proc/diskstats'),
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'),
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"');
@@ -241,3 +166,77 @@ extension StatusCmdTypeX on StatusCmdType {
final val => val.name,
};
}
enum WindowsStatusCmdType {
echo._('echo ${SystemType.windowsSign}'),
time._('[DateTimeOffset]::UtcNow.ToUnixTimeSeconds()'),
net._(
r'Get-Counter -Counter '
r'"\\NetworkInterface(*)\\Bytes Received/sec", '
r'"\\NetworkInterface(*)\\Bytes Sent/sec" '
r'-SampleInterval 1 -MaxSamples 2 | ConvertTo-Json',
),
sys._('(Get-ComputerInfo).OsName'),
cpu._(
'Get-WmiObject -Class Win32_Processor | '
'Select-Object Name, LoadPercentage | ConvertTo-Json',
),
uptime._('(Get-CimInstance -ClassName Win32_OperatingSystem).LastBootUpTime'),
conn._('(netstat -an | findstr ESTABLISHED | Measure-Object -Line).Count'),
disk._(
'Get-WmiObject -Class Win32_LogicalDisk | '
'Select-Object DeviceID, Size, FreeSpace, FileSystem | ConvertTo-Json',
),
mem._(
'Get-WmiObject -Class Win32_OperatingSystem | '
'Select-Object TotalVisibleMemorySize, FreePhysicalMemory | ConvertTo-Json',
),
temp._(
'Get-CimInstance -ClassName MSAcpi_ThermalZoneTemperature '
'-Namespace root/wmi -ErrorAction SilentlyContinue | '
'Select-Object InstanceName, @{Name=\'Temperature\';'
'Expression={[math]::Round((\$_.CurrentTemperature - 2732) / 10, 1)}} | '
'ConvertTo-Json',
),
host._(r'Write-Output $env:COMPUTERNAME'),
diskio._(
r'Get-Counter -Counter '
r'"\\PhysicalDisk(*)\\Disk Read Bytes/sec", '
r'"\\PhysicalDisk(*)\\Disk Write Bytes/sec" '
r'-SampleInterval 1 -MaxSamples 2 | ConvertTo-Json',
),
battery._(
'Get-WmiObject -Class Win32_Battery | '
'Select-Object EstimatedChargeRemaining, BatteryStatus | ConvertTo-Json',
),
nvidia._(
'if (Get-Command nvidia-smi -ErrorAction SilentlyContinue) { '
'nvidia-smi -q -x } else { echo "NVIDIA driver not found" }',
),
amd._(
'if (Get-Command amd-smi -ErrorAction SilentlyContinue) { '
'amd-smi list --json } else { echo "AMD driver not found" }',
),
sensors._(
'Get-CimInstance -ClassName Win32_TemperatureProbe '
'-ErrorAction SilentlyContinue | '
'Select-Object Name, CurrentReading | ConvertTo-Json',
),
diskSmart._(
'Get-PhysicalDisk | Get-StorageReliabilityCounter | '
'Select-Object DeviceId, Temperature, TemperatureMax, Wear, PowerOnHours | '
'ConvertTo-Json',
),
cpuBrand._('(Get-WmiObject -Class Win32_Processor).Name');
final String cmd;
const WindowsStatusCmdType._(this.cmd);
}
extension EnumX on Enum {
/// Find out the required segment from [segments]
String find(List<String> segments) {
return segments[index];
}
}

View File

@@ -12,7 +12,7 @@ enum AppTab {
server,
ssh,
file,
snippet,
snippet
//settings,
;
@@ -29,60 +29,60 @@ enum AppTab {
NavigationDestination get navDestination {
return switch (this) {
server => NavigationDestination(
icon: const Icon(BoxIcons.bx_server),
label: l10n.server,
selectedIcon: const Icon(BoxIcons.bxs_server),
),
icon: const Icon(BoxIcons.bx_server),
label: l10n.server,
selectedIcon: const Icon(BoxIcons.bxs_server),
),
// settings => NavigationDestination(
// icon: const Icon(Icons.settings),
// label: libL10n.setting,
// selectedIcon: const Icon(Icons.settings),
// ),
ssh => const NavigationDestination(
icon: Icon(Icons.terminal_outlined),
label: 'SSH',
selectedIcon: Icon(Icons.terminal),
),
icon: Icon(Icons.terminal_outlined),
label: 'SSH',
selectedIcon: Icon(Icons.terminal),
),
snippet => NavigationDestination(
icon: const Icon(Icons.code),
label: l10n.snippet,
selectedIcon: const Icon(Icons.code),
),
icon: const Icon(Icons.code),
label: l10n.snippet,
selectedIcon: const Icon(Icons.code),
),
file => NavigationDestination(
icon: const Icon(Icons.folder_open),
label: libL10n.file,
selectedIcon: const Icon(Icons.folder),
),
icon: const Icon(Icons.folder_open),
label: libL10n.file,
selectedIcon: const Icon(Icons.folder),
),
};
}
NavigationRailDestination get navRailDestination {
return switch (this) {
server => NavigationRailDestination(
icon: const Icon(BoxIcons.bx_server),
label: Text(l10n.server),
selectedIcon: const Icon(BoxIcons.bxs_server),
),
icon: const Icon(BoxIcons.bx_server),
label: Text(l10n.server),
selectedIcon: const Icon(BoxIcons.bxs_server),
),
// settings => NavigationRailDestination(
// icon: const Icon(Icons.settings),
// label: libL10n.setting,
// selectedIcon: const Icon(Icons.settings),
// ),
ssh => const NavigationRailDestination(
icon: Icon(Icons.terminal_outlined),
label: Text('SSH'),
selectedIcon: Icon(Icons.terminal),
),
icon: Icon(Icons.terminal_outlined),
label: Text('SSH'),
selectedIcon: Icon(Icons.terminal),
),
snippet => NavigationRailDestination(
icon: const Icon(Icons.code),
label: Text(l10n.snippet),
selectedIcon: const Icon(Icons.code),
),
icon: const Icon(Icons.code),
label: Text(l10n.snippet),
selectedIcon: const Icon(Icons.code),
),
file => NavigationRailDestination(
icon: const Icon(Icons.folder_open),
label: Text(libL10n.file),
selectedIcon: const Icon(Icons.folder),
),
icon: const Icon(Icons.folder_open),
label: Text(libL10n.file),
selectedIcon: const Icon(Icons.folder),
),
};
}