mirror of
https://github.com/lollipopkit/flutter_server_box.git
synced 2026-01-30 21:04:46 +01:00
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:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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});
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
242
lib/data/model/app/script_builders.dart
Normal file
242
lib/data/model/app/script_builders.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user