mirror of
https://github.com/lollipopkit/flutter_server_box.git
synced 2025-12-17 07:14:28 +01:00
fix: macOS ssh term unusable (#838)
This commit is contained in:
@@ -65,6 +65,8 @@ After you read the above, you can open an [issue](https://github.com/lollipopkit
|
|||||||
|
|
||||||
Any positive contribution is welcome.
|
Any positive contribution is welcome.
|
||||||
|
|
||||||
|
If I forgot to add your name to the contributors list, please add a comment in the issue or PR you opened to let me know, I will add it as soon as possible.
|
||||||
|
|
||||||
### Development
|
### Development
|
||||||
|
|
||||||
1. Setup [Flutter](https://flutter.dev/docs/get-started/install) environment.
|
1. Setup [Flutter](https://flutter.dev/docs/get-started/install) environment.
|
||||||
|
|||||||
@@ -67,6 +67,8 @@ Linux / Windows | [GitHub](https://github.com/lollipopkit/flutter_server_box/rel
|
|||||||
|
|
||||||
任何正面的贡献都欢迎。
|
任何正面的贡献都欢迎。
|
||||||
|
|
||||||
|
如果我忘记在贡献者列表中添加你的名字,请在你打开的 issue 或 PR 中添加评论让我知道,我会尽快添加。
|
||||||
|
|
||||||
### 开发
|
### 开发
|
||||||
|
|
||||||
1. 安装 [Flutter](https://flutter.dev/docs/get-started/install)
|
1. 安装 [Flutter](https://flutter.dev/docs/get-started/install)
|
||||||
|
|||||||
249
lib/data/model/app/scripts/cmd_types.dart
Normal file
249
lib/data/model/app/scripts/cmd_types.dart
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
import 'package:server_box/core/extension/context/locale.dart';
|
||||||
|
import 'package:server_box/data/model/server/system.dart';
|
||||||
|
|
||||||
|
/// Base class for all command type enums
|
||||||
|
abstract class CommandType {
|
||||||
|
String get cmd;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Linux/Unix status commands
|
||||||
|
enum StatusCmdType implements CommandType {
|
||||||
|
echo('echo ${SystemType.linuxSign}'),
|
||||||
|
time('date +%s'),
|
||||||
|
net('cat /proc/net/dev'),
|
||||||
|
sys('cat /etc/*-release | grep ^PRETTY_NAME'),
|
||||||
|
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',
|
||||||
|
),
|
||||||
|
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'),
|
||||||
|
|
||||||
|
/// Get battery information from Linux power supply subsystem
|
||||||
|
///
|
||||||
|
/// Reads battery data from sysfs power supply interface:
|
||||||
|
/// - Iterates through all power supply devices in /sys/class/power_supply/
|
||||||
|
/// - Each device has a uevent file with key-value pairs of power supply properties
|
||||||
|
/// - Includes battery level, status, technology type, and other attributes
|
||||||
|
/// - Works with laptops, UPS devices, and other power supplies
|
||||||
|
/// - Adds echo after each file to separate multiple power supplies
|
||||||
|
/// - Returns empty if no power supplies are detected (e.g., desktop systems)
|
||||||
|
battery('for f in /sys/class/power_supply/*/uevent; do cat "\$f"; echo; done'),
|
||||||
|
|
||||||
|
/// Get NVIDIA GPU information using nvidia-smi in XML format
|
||||||
|
/// Requires NVIDIA drivers and nvidia-smi utility to be installed
|
||||||
|
nvidia('nvidia-smi -q -x'),
|
||||||
|
|
||||||
|
/// Get AMD GPU information using multiple fallback methods
|
||||||
|
///
|
||||||
|
/// This command tries three different AMD monitoring tools in order of preference:
|
||||||
|
/// 1. amd-smi: Modern AMD System Management Interface (ROCm 5.0+)
|
||||||
|
/// - Uses 'amd-smi list --json' to get GPU list
|
||||||
|
/// - Uses 'amd-smi metric --json' to get performance metrics
|
||||||
|
/// 2. rocm-smi: ROCm System Management Interface (older versions)
|
||||||
|
/// - First tries '--json' output format if supported
|
||||||
|
/// - Falls back to human-readable format with comprehensive metrics
|
||||||
|
/// 3. radeontop: Real-time GPU usage monitor for older AMD cards
|
||||||
|
/// - Uses 2-second timeout to avoid hanging
|
||||||
|
/// - Skips header line with 'tail -n +2'
|
||||||
|
/// - Outputs single line of usage data
|
||||||
|
///
|
||||||
|
/// If none of these tools are available, outputs error message
|
||||||
|
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'),
|
||||||
|
|
||||||
|
/// Get SMART disk health information for all storage devices
|
||||||
|
///
|
||||||
|
/// Uses a combination of lsblk and smartctl to collect disk health data:
|
||||||
|
/// - lsblk -dn -o KNAME lists all block devices (kernel names only, no dependencies)
|
||||||
|
/// - For each device, runs smartctl with -a (all info) and -j (JSON output)
|
||||||
|
/// - Targets raw device nodes in /dev/ (e.g., /dev/sda, /dev/nvme0n1)
|
||||||
|
/// - Adds echo after each device to separate output blocks
|
||||||
|
/// - May require elevated privileges for some drives
|
||||||
|
/// - smartctl must be installed (part of smartmontools package)
|
||||||
|
diskSmart('for d in \$(lsblk -dn -o KNAME); do smartctl -a -j /dev/\$d; echo; done'),
|
||||||
|
cpuBrand('cat /proc/cpuinfo | grep "model name"');
|
||||||
|
|
||||||
|
@override
|
||||||
|
final String cmd;
|
||||||
|
|
||||||
|
const StatusCmdType(this.cmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// BSD/macOS status commands
|
||||||
|
enum BSDStatusCmdType implements CommandType {
|
||||||
|
echo('echo ${SystemType.bsdSign}'),
|
||||||
|
time('date +%s'),
|
||||||
|
net('netstat -ibn'),
|
||||||
|
sys('uname -or'),
|
||||||
|
cpu('top -l 1 | grep "CPU usage"'),
|
||||||
|
uptime('uptime'),
|
||||||
|
disk('df -k'), // Keep df -k for BSD systems as lsblk is not available on macOS/BSD
|
||||||
|
mem('top -l 1 | grep PhysMem'),
|
||||||
|
host('hostname'),
|
||||||
|
cpuBrand('sysctl -n machdep.cpu.brand_string');
|
||||||
|
|
||||||
|
@override
|
||||||
|
final String cmd;
|
||||||
|
|
||||||
|
const BSDStatusCmdType(this.cmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Windows PowerShell status commands
|
||||||
|
enum WindowsStatusCmdType implements CommandType {
|
||||||
|
echo('echo ${SystemType.windowsSign}'),
|
||||||
|
time('[DateTimeOffset]::UtcNow.ToUnixTimeSeconds()'),
|
||||||
|
|
||||||
|
/// Get network interface statistics using Windows Performance Counters
|
||||||
|
///
|
||||||
|
/// Uses Get-Counter to collect network I/O metrics from all network interfaces:
|
||||||
|
/// - Collects bytes received and sent per second for all network interfaces
|
||||||
|
/// - Takes 2 samples with 1 second interval to calculate rates
|
||||||
|
/// - Outputs results in JSON format for easy parsing
|
||||||
|
/// - Counter paths use double backslashes to escape PowerShell string literals
|
||||||
|
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',
|
||||||
|
),
|
||||||
|
|
||||||
|
/// Get system temperature using Windows Management Instrumentation (WMI)
|
||||||
|
///
|
||||||
|
/// Queries the MSAcpi_ThermalZoneTemperature class from the WMI root/wmi namespace:
|
||||||
|
/// - Uses Get-CimInstance to access ACPI thermal zone data
|
||||||
|
/// - ErrorAction SilentlyContinue prevents errors on systems without thermal sensors
|
||||||
|
/// - Converts temperature from 10ths of Kelvin to Celsius: (temp - 2732) / 10
|
||||||
|
/// - Uses calculated property to perform the temperature conversion
|
||||||
|
/// - Returns JSON with InstanceName and converted Temperature values
|
||||||
|
/// - May return empty result on systems without ACPI thermal sensor support
|
||||||
|
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'),
|
||||||
|
|
||||||
|
/// Get disk I/O statistics using Windows Performance Counters
|
||||||
|
///
|
||||||
|
/// Uses Get-Counter to collect disk I/O metrics from all physical disks:
|
||||||
|
/// - Monitors read and write bytes per second for all physical disks
|
||||||
|
/// - Takes 2 samples with 1 second interval to calculate I/O rates
|
||||||
|
/// - Physical disk counters provide hardware-level I/O statistics
|
||||||
|
/// - Outputs results in JSON format for parsing
|
||||||
|
/// - Counter names use wildcard (*) to capture all disk instances
|
||||||
|
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',
|
||||||
|
),
|
||||||
|
|
||||||
|
/// Get NVIDIA GPU information on Windows
|
||||||
|
///
|
||||||
|
/// Checks if nvidia-smi is available before attempting to use it:
|
||||||
|
/// - Uses Get-Command to test if nvidia-smi.exe exists in PATH
|
||||||
|
/// - ErrorAction SilentlyContinue prevents PowerShell errors if not found
|
||||||
|
/// - If available, runs nvidia-smi with -q (query) and -x (XML output) flags
|
||||||
|
/// - If not available, outputs standard error message for consistent handling
|
||||||
|
nvidia(
|
||||||
|
'if (Get-Command nvidia-smi -ErrorAction SilentlyContinue) { '
|
||||||
|
'nvidia-smi -q -x } else { echo "NVIDIA driver not found" }',
|
||||||
|
),
|
||||||
|
|
||||||
|
/// Get AMD GPU information on Windows
|
||||||
|
///
|
||||||
|
/// Checks for AMD monitoring tools using similar pattern to Linux version:
|
||||||
|
/// - Uses Get-Command to test if amd-smi.exe exists in PATH
|
||||||
|
/// - ErrorAction SilentlyContinue prevents PowerShell errors if not found
|
||||||
|
/// - If available, runs amd-smi list command with JSON output
|
||||||
|
/// - If not available, outputs standard error message for consistent handling
|
||||||
|
/// - Windows version is simpler than Linux due to fewer AMD tool variations
|
||||||
|
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',
|
||||||
|
),
|
||||||
|
|
||||||
|
/// Get SMART disk health information on Windows using Storage cmdlets
|
||||||
|
///
|
||||||
|
/// Uses Windows PowerShell storage management cmdlets:
|
||||||
|
/// - Get-PhysicalDisk retrieves all physical storage devices
|
||||||
|
/// - Get-StorageReliabilityCounter gets SMART health data via pipeline
|
||||||
|
/// - Selects key health metrics: DeviceId, Temperature, TemperatureMax, Wear, PowerOnHours
|
||||||
|
/// - Outputs results in JSON format for consistent parsing
|
||||||
|
/// - Works with NVMe, SATA, and other storage interfaces supported by Windows
|
||||||
|
/// - May require elevated privileges on some systems
|
||||||
|
diskSmart(
|
||||||
|
'Get-PhysicalDisk | Get-StorageReliabilityCounter | '
|
||||||
|
'Select-Object DeviceId, Temperature, TemperatureMax, Wear, PowerOnHours | '
|
||||||
|
'ConvertTo-Json',
|
||||||
|
),
|
||||||
|
cpuBrand('(Get-WmiObject -Class Win32_Processor).Name');
|
||||||
|
|
||||||
|
@override
|
||||||
|
final String cmd;
|
||||||
|
|
||||||
|
const WindowsStatusCmdType(this.cmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extensions for StatusCmdType
|
||||||
|
extension StatusCmdTypeX on StatusCmdType {
|
||||||
|
String get i18n => switch (this) {
|
||||||
|
StatusCmdType.sys => l10n.system,
|
||||||
|
StatusCmdType.host => l10n.host,
|
||||||
|
StatusCmdType.uptime => l10n.uptime,
|
||||||
|
StatusCmdType.battery => l10n.battery,
|
||||||
|
StatusCmdType.sensors => l10n.sensors,
|
||||||
|
StatusCmdType.disk => l10n.disk,
|
||||||
|
final val => val.name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generic extension for Enum types
|
||||||
|
extension EnumX on Enum {
|
||||||
|
/// Find out the required segment from [segments]
|
||||||
|
String find(List<String> segments) {
|
||||||
|
return segments[index];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:server_box/data/model/app/shell_func.dart';
|
import 'package:server_box/data/model/app/scripts/cmd_types.dart';
|
||||||
import 'package:server_box/data/res/build_data.dart';
|
import 'package:server_box/data/model/app/scripts/script_consts.dart';
|
||||||
|
import 'package:server_box/data/model/app/scripts/shell_func.dart';
|
||||||
|
|
||||||
/// Abstract base class for platform-specific script builders
|
/// Abstract base class for platform-specific script builders
|
||||||
abstract class ScriptBuilder {
|
abstract class ScriptBuilder {
|
||||||
@@ -18,10 +19,13 @@ abstract class ScriptBuilder {
|
|||||||
String getExecCommand(String scriptPath, ShellFunc func);
|
String getExecCommand(String scriptPath, ShellFunc func);
|
||||||
|
|
||||||
/// Get custom commands string for this platform
|
/// Get custom commands string for this platform
|
||||||
String getCustomCmdsString(
|
String getCustomCmdsString(ShellFunc func, Map<String, String>? customCmds);
|
||||||
ShellFunc func,
|
|
||||||
Map<String, String>? customCmds,
|
/// Get the script header for this platform
|
||||||
);
|
String get scriptHeader;
|
||||||
|
|
||||||
|
/// Get the command divider for this platform
|
||||||
|
String get cmdDivider => ScriptConstants.cmdDivider;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Windows PowerShell script builder
|
/// Windows PowerShell script builder
|
||||||
@@ -29,7 +33,10 @@ class WindowsScriptBuilder extends ScriptBuilder {
|
|||||||
const WindowsScriptBuilder();
|
const WindowsScriptBuilder();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get scriptFileName => 'srvboxm_v${BuildData.script}.ps1';
|
String get scriptFileName => ScriptConstants.scriptFileWindows;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scriptHeader => ScriptConstants.windowsScriptHeader;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String getInstallCommand(String scriptDir, String scriptPath) {
|
String getInstallCommand(String scriptDir, String scriptPath) {
|
||||||
@@ -44,10 +51,7 @@ class WindowsScriptBuilder extends ScriptBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String getCustomCmdsString(
|
String getCustomCmdsString(ShellFunc func, Map<String, String>? customCmds) {
|
||||||
ShellFunc func,
|
|
||||||
Map<String, String>? customCmds,
|
|
||||||
) {
|
|
||||||
if (func == ShellFunc.status && customCmds != null && customCmds.isNotEmpty) {
|
if (func == ShellFunc.status && customCmds != null && customCmds.isNotEmpty) {
|
||||||
return '\n${customCmds.values.map((cmd) => '\t$cmd').join('\n')}';
|
return '\n${customCmds.values.map((cmd) => '\t$cmd').join('\n')}';
|
||||||
}
|
}
|
||||||
@@ -57,13 +61,7 @@ class WindowsScriptBuilder extends ScriptBuilder {
|
|||||||
@override
|
@override
|
||||||
String buildScript(Map<String, String>? customCmds) {
|
String buildScript(Map<String, String>? customCmds) {
|
||||||
final sb = StringBuffer();
|
final sb = StringBuffer();
|
||||||
sb.write('''
|
sb.write(scriptHeader);
|
||||||
# PowerShell script for ServerBox app v1.0.${BuildData.build}
|
|
||||||
# DO NOT delete this file while app is running
|
|
||||||
|
|
||||||
\$ErrorActionPreference = "SilentlyContinue"
|
|
||||||
|
|
||||||
''');
|
|
||||||
|
|
||||||
// Write each function
|
// Write each function
|
||||||
for (final func in ShellFunc.values) {
|
for (final func in ShellFunc.values) {
|
||||||
@@ -93,8 +91,9 @@ switch (\$args[0]) {
|
|||||||
return sb.toString();
|
return sb.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get Windows-specific command for a shell function
|
||||||
String _getWindowsCommand(ShellFunc func) => switch (func) {
|
String _getWindowsCommand(ShellFunc func) => switch (func) {
|
||||||
ShellFunc.status => WindowsStatusCmdType.values.map((e) => e.cmd).join(ShellFunc.cmdDivider),
|
ShellFunc.status => WindowsStatusCmdType.values.map((e) => e.cmd).join(cmdDivider),
|
||||||
ShellFunc.process => 'Get-Process | Select-Object ProcessName, Id, CPU, WorkingSet | ConvertTo-Json',
|
ShellFunc.process => 'Get-Process | Select-Object ProcessName, Id, CPU, WorkingSet | ConvertTo-Json',
|
||||||
ShellFunc.shutdown => 'Stop-Computer -Force',
|
ShellFunc.shutdown => 'Stop-Computer -Force',
|
||||||
ShellFunc.reboot => 'Restart-Computer -Force',
|
ShellFunc.reboot => 'Restart-Computer -Force',
|
||||||
@@ -108,7 +107,10 @@ class UnixScriptBuilder extends ScriptBuilder {
|
|||||||
const UnixScriptBuilder();
|
const UnixScriptBuilder();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get scriptFileName => 'srvboxm_v${BuildData.script}.sh';
|
String get scriptFileName => ScriptConstants.scriptFile;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scriptHeader => ScriptConstants.unixScriptHeader;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String getInstallCommand(String scriptDir, String scriptPath) {
|
String getInstallCommand(String scriptDir, String scriptPath) {
|
||||||
@@ -125,12 +127,9 @@ chmod 755 $scriptPath
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String getCustomCmdsString(
|
String getCustomCmdsString(ShellFunc func, Map<String, String>? customCmds) {
|
||||||
ShellFunc func,
|
|
||||||
Map<String, String>? customCmds,
|
|
||||||
) {
|
|
||||||
if (func == ShellFunc.status && customCmds != null && customCmds.isNotEmpty) {
|
if (func == ShellFunc.status && customCmds != null && customCmds.isNotEmpty) {
|
||||||
return '${ShellFunc.cmdDivider}\n\t${customCmds.values.join(ShellFunc.cmdDivider)}';
|
return '$cmdDivider\n\t${customCmds.values.join(cmdDivider)}';
|
||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
@@ -138,25 +137,7 @@ chmod 755 $scriptPath
|
|||||||
@override
|
@override
|
||||||
String buildScript(Map<String, String>? customCmds) {
|
String buildScript(Map<String, String>? customCmds) {
|
||||||
final sb = StringBuffer();
|
final sb = StringBuffer();
|
||||||
sb.write('''
|
sb.write(scriptHeader);
|
||||||
#!/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
|
// Write each function
|
||||||
for (final func in ShellFunc.values) {
|
for (final func in ShellFunc.values) {
|
||||||
final customCmdsStr = getCustomCmdsString(func, customCmds);
|
final customCmdsStr = getCustomCmdsString(func, customCmds);
|
||||||
@@ -186,16 +167,39 @@ esac''');
|
|||||||
return sb.toString();
|
return sb.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get Unix-specific command for a shell function
|
||||||
String _getUnixCommand(ShellFunc func) {
|
String _getUnixCommand(ShellFunc func) {
|
||||||
switch (func) {
|
switch (func) {
|
||||||
case ShellFunc.status:
|
case ShellFunc.status:
|
||||||
|
return _getUnixStatusCommand();
|
||||||
|
case ShellFunc.process:
|
||||||
|
return _getUnixProcessCommand();
|
||||||
|
case ShellFunc.shutdown:
|
||||||
|
return _getUnixShutdownCommand();
|
||||||
|
case ShellFunc.reboot:
|
||||||
|
return _getUnixRebootCommand();
|
||||||
|
case ShellFunc.suspend:
|
||||||
|
return _getUnixSuspendCommand();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get Unix status command with OS detection
|
||||||
|
String _getUnixStatusCommand() {
|
||||||
|
// Generate command lists for better readability
|
||||||
|
final linuxCommands = StatusCmdType.values.map((e) => e.cmd).join(cmdDivider);
|
||||||
|
|
||||||
|
final bsdCommands = BSDStatusCmdType.values.map((e) => e.cmd).join(cmdDivider);
|
||||||
|
|
||||||
return '''
|
return '''
|
||||||
if [ "\$macSign" = "" ] && [ "\$bsdSign" = "" ]; then
|
if [ "\$macSign" = "" ] && [ "\$bsdSign" = "" ]; then
|
||||||
\t${StatusCmdType.values.map((e) => e.cmd).join(ShellFunc.cmdDivider)}
|
\t$linuxCommands
|
||||||
else
|
else
|
||||||
\t${BSDStatusCmdType.values.map((e) => e.cmd).join(ShellFunc.cmdDivider)}
|
\t$bsdCommands
|
||||||
fi''';
|
fi''';
|
||||||
case ShellFunc.process:
|
}
|
||||||
|
|
||||||
|
/// Get Unix process command with busybox detection
|
||||||
|
String _getUnixProcessCommand() {
|
||||||
return '''
|
return '''
|
||||||
if [ "\$macSign" = "" ] && [ "\$bsdSign" = "" ]; then
|
if [ "\$macSign" = "" ] && [ "\$bsdSign" = "" ]; then
|
||||||
\tif [ "\$isBusybox" != "" ]; then
|
\tif [ "\$isBusybox" != "" ]; then
|
||||||
@@ -205,23 +209,31 @@ if [ "\$macSign" = "" ] && [ "\$bsdSign" = "" ]; then
|
|||||||
\tfi
|
\tfi
|
||||||
else
|
else
|
||||||
\tps -ax
|
\tps -ax
|
||||||
fi
|
fi''';
|
||||||
''';
|
}
|
||||||
case ShellFunc.shutdown:
|
|
||||||
|
/// Get Unix shutdown command with privilege detection
|
||||||
|
String _getUnixShutdownCommand() {
|
||||||
return '''
|
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:
|
}
|
||||||
|
|
||||||
|
/// Get Unix reboot command with privilege detection
|
||||||
|
String _getUnixRebootCommand() {
|
||||||
return '''
|
return '''
|
||||||
if [ "\$userId" = "0" ]; then
|
if [ "\$userId" = "0" ]; then
|
||||||
\treboot
|
\treboot
|
||||||
else
|
else
|
||||||
\tsudo -S reboot
|
\tsudo -S reboot
|
||||||
fi''';
|
fi''';
|
||||||
case ShellFunc.suspend:
|
}
|
||||||
|
|
||||||
|
/// Get Unix suspend command with privilege detection
|
||||||
|
String _getUnixSuspendCommand() {
|
||||||
return '''
|
return '''
|
||||||
if [ "\$userId" = "0" ]; then
|
if [ "\$userId" = "0" ]; then
|
||||||
\tsystemctl suspend
|
\tsystemctl suspend
|
||||||
@@ -229,14 +241,19 @@ else
|
|||||||
\tsudo -S systemctl suspend
|
\tsudo -S systemctl suspend
|
||||||
fi''';
|
fi''';
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Factory class to get appropriate script builder for platform
|
/// Factory class to get appropriate script builder for platform
|
||||||
class ScriptBuilderFactory {
|
class ScriptBuilderFactory {
|
||||||
const ScriptBuilderFactory._();
|
const ScriptBuilderFactory._();
|
||||||
|
|
||||||
|
/// Get the appropriate script builder based on platform
|
||||||
static ScriptBuilder getBuilder(bool isWindows) {
|
static ScriptBuilder getBuilder(bool isWindows) {
|
||||||
return isWindows ? const WindowsScriptBuilder() : const UnixScriptBuilder();
|
return isWindows ? const WindowsScriptBuilder() : const UnixScriptBuilder();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get all available builders (useful for testing)
|
||||||
|
static List<ScriptBuilder> getAllBuilders() {
|
||||||
|
return const [WindowsScriptBuilder(), UnixScriptBuilder()];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
100
lib/data/model/app/scripts/script_consts.dart
Normal file
100
lib/data/model/app/scripts/script_consts.dart
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import 'package:server_box/data/res/build_data.dart';
|
||||||
|
|
||||||
|
/// Constants used throughout the script system
|
||||||
|
class ScriptConstants {
|
||||||
|
const ScriptConstants._();
|
||||||
|
|
||||||
|
// Script file names
|
||||||
|
static const String scriptFile = 'srvboxm_v${BuildData.script}.sh';
|
||||||
|
static const String scriptFileWindows = 'srvboxm_v${BuildData.script}.ps1';
|
||||||
|
|
||||||
|
// Script directories
|
||||||
|
static const String scriptDirHome = '~/.config/server_box';
|
||||||
|
static const String scriptDirTmp = '/tmp/server_box';
|
||||||
|
static const String scriptDirHomeWindows = '%USERPROFILE%/.config/server_box';
|
||||||
|
static const String scriptDirTmpWindows = '%TEMP%/server_box';
|
||||||
|
|
||||||
|
// Command separators and dividers
|
||||||
|
static const String separator = 'SrvBoxSep';
|
||||||
|
|
||||||
|
/// The suffix `\t` is for formatting
|
||||||
|
static const String cmdDivider = '\necho $separator\n\t';
|
||||||
|
|
||||||
|
// Path separators
|
||||||
|
static const String unixPathSeparator = '/';
|
||||||
|
static const String windowsPathSeparator = '\\';
|
||||||
|
|
||||||
|
// Script headers
|
||||||
|
static const String unixScriptHeader =
|
||||||
|
'''
|
||||||
|
#!/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
|
||||||
|
|
||||||
|
''';
|
||||||
|
|
||||||
|
static const String windowsScriptHeader =
|
||||||
|
'''
|
||||||
|
# PowerShell script for ServerBox app v1.0.${BuildData.build}
|
||||||
|
# DO NOT delete this file while app is running
|
||||||
|
|
||||||
|
\$ErrorActionPreference = "SilentlyContinue"
|
||||||
|
|
||||||
|
''';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Script path configuration and management
|
||||||
|
class ScriptPaths {
|
||||||
|
ScriptPaths._();
|
||||||
|
|
||||||
|
static final Map<String, String> _scriptDirMap = <String, String>{};
|
||||||
|
|
||||||
|
/// Get the script directory for the given [id].
|
||||||
|
///
|
||||||
|
/// Default is [ScriptConstants.scriptDirTmp]/[ScriptConstants.scriptFile],
|
||||||
|
/// if this path is not accessible, it will be changed to
|
||||||
|
/// [ScriptConstants.scriptDirHome]/[ScriptConstants.scriptFile].
|
||||||
|
static String getScriptDir(String id, {bool isWindows = false}) {
|
||||||
|
final defaultTmpDir = isWindows ? ScriptConstants.scriptDirTmpWindows : ScriptConstants.scriptDirTmp;
|
||||||
|
_scriptDirMap[id] ??= defaultTmpDir;
|
||||||
|
return _scriptDirMap[id]!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Switch between tmp and home directories for script storage
|
||||||
|
static String switchScriptDir(String id, {bool isWindows = false}) {
|
||||||
|
return switch (_scriptDirMap[id]) {
|
||||||
|
ScriptConstants.scriptDirTmp => _scriptDirMap[id] = ScriptConstants.scriptDirHome,
|
||||||
|
ScriptConstants.scriptDirTmpWindows => _scriptDirMap[id] = ScriptConstants.scriptDirHomeWindows,
|
||||||
|
ScriptConstants.scriptDirHome => _scriptDirMap[id] = ScriptConstants.scriptDirTmp,
|
||||||
|
ScriptConstants.scriptDirHomeWindows => _scriptDirMap[id] = ScriptConstants.scriptDirTmpWindows,
|
||||||
|
_ =>
|
||||||
|
_scriptDirMap[id] = isWindows ? ScriptConstants.scriptDirHomeWindows : ScriptConstants.scriptDirHome,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the full script path for the given [id]
|
||||||
|
static String getScriptPath(String id, {bool isWindows = false}) {
|
||||||
|
final dir = getScriptDir(id, isWindows: isWindows);
|
||||||
|
final fileName = isWindows ? ScriptConstants.scriptFileWindows : ScriptConstants.scriptFile;
|
||||||
|
final separator = isWindows ? ScriptConstants.windowsPathSeparator : ScriptConstants.unixPathSeparator;
|
||||||
|
return '$dir$separator$fileName';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear cached script directories (useful for testing)
|
||||||
|
static void clearCache() {
|
||||||
|
_scriptDirMap.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
102
lib/data/model/app/scripts/shell_func.dart
Normal file
102
lib/data/model/app/scripts/shell_func.dart
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import 'package:server_box/data/model/app/scripts/script_builders.dart';
|
||||||
|
import 'package:server_box/data/model/app/scripts/script_consts.dart';
|
||||||
|
import 'package:server_box/data/model/server/system.dart';
|
||||||
|
import 'package:server_box/data/provider/server.dart';
|
||||||
|
|
||||||
|
/// Shell functions available in the ServerBox application
|
||||||
|
enum ShellFunc {
|
||||||
|
status('SbStatus'),
|
||||||
|
process('SbProcess'),
|
||||||
|
shutdown('SbShutdown'),
|
||||||
|
reboot('SbReboot'),
|
||||||
|
suspend('SbSuspend');
|
||||||
|
|
||||||
|
/// The function name used in scripts
|
||||||
|
final String name;
|
||||||
|
|
||||||
|
const ShellFunc(this.name);
|
||||||
|
|
||||||
|
/// Get the command line flag for this function
|
||||||
|
String get flag => switch (this) {
|
||||||
|
ShellFunc.process => 'p',
|
||||||
|
ShellFunc.shutdown => 'sd',
|
||||||
|
ShellFunc.reboot => 'r',
|
||||||
|
ShellFunc.suspend => 'sp',
|
||||||
|
ShellFunc.status => 's',
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Execute this shell function on the specified server
|
||||||
|
String exec(String id, {SystemType? systemType}) {
|
||||||
|
final scriptPath = ShellFuncManager.getScriptPath(id, systemType: systemType);
|
||||||
|
final isWindows = systemType == SystemType.windows;
|
||||||
|
final builder = ScriptBuilderFactory.getBuilder(isWindows);
|
||||||
|
|
||||||
|
return builder.getExecCommand(scriptPath, this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Manager class for shell function operations
|
||||||
|
class ShellFuncManager {
|
||||||
|
const ShellFuncManager._();
|
||||||
|
|
||||||
|
/// Normalize a directory path to ensure it doesn't end with trailing separators
|
||||||
|
static String _normalizeDir(String dir, bool isWindows) {
|
||||||
|
final separator = isWindows ? ScriptConstants.windowsPathSeparator : ScriptConstants.unixPathSeparator;
|
||||||
|
|
||||||
|
// Remove all trailing separators
|
||||||
|
final pattern = RegExp('${RegExp.escape(separator)}+\$');
|
||||||
|
return dir.replaceAll(pattern, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the script directory for the given [id].
|
||||||
|
///
|
||||||
|
/// Checks for custom script directory first, then falls back to default.
|
||||||
|
static String getScriptDir(String id, {SystemType? systemType}) {
|
||||||
|
final customScriptDir = ServerProvider.pick(id: id)?.value.spi.custom?.scriptDir;
|
||||||
|
final isWindows = systemType == SystemType.windows;
|
||||||
|
|
||||||
|
if (customScriptDir != null) return _normalizeDir(customScriptDir, isWindows);
|
||||||
|
return ScriptPaths.getScriptDir(id, isWindows: isWindows);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Switch between tmp and home directories for script storage
|
||||||
|
static void switchScriptDir(String id, {SystemType? systemType}) {
|
||||||
|
final isWindows = systemType == SystemType.windows;
|
||||||
|
ScriptPaths.switchScriptDir(id, isWindows: isWindows);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the full script path for the given [id]
|
||||||
|
static String getScriptPath(String id, {SystemType? systemType}) {
|
||||||
|
final customScriptDir = ServerProvider.pick(id: id)?.value.spi.custom?.scriptDir;
|
||||||
|
if (customScriptDir != null) {
|
||||||
|
final isWindows = systemType == SystemType.windows;
|
||||||
|
final normalizedDir = _normalizeDir(customScriptDir, isWindows);
|
||||||
|
final fileName = isWindows ? ScriptConstants.scriptFileWindows : ScriptConstants.scriptFile;
|
||||||
|
final separator = isWindows ? ScriptConstants.windowsPathSeparator : ScriptConstants.unixPathSeparator;
|
||||||
|
return '$normalizedDir$separator$fileName';
|
||||||
|
}
|
||||||
|
|
||||||
|
final isWindows = systemType == SystemType.windows;
|
||||||
|
return ScriptPaths.getScriptPath(id, isWindows: isWindows);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the installation shell command for the script
|
||||||
|
static String getInstallShellCmd(String id, {SystemType? systemType}) {
|
||||||
|
final scriptDir = getScriptDir(id, systemType: systemType);
|
||||||
|
final isWindows = systemType == SystemType.windows;
|
||||||
|
final normalizedDir = _normalizeDir(scriptDir, isWindows);
|
||||||
|
final builder = ScriptBuilderFactory.getBuilder(isWindows);
|
||||||
|
final separator = isWindows ? ScriptConstants.windowsPathSeparator : ScriptConstants.unixPathSeparator;
|
||||||
|
final scriptPath = '$normalizedDir$separator${builder.scriptFileName}';
|
||||||
|
|
||||||
|
return builder.getInstallCommand(normalizedDir, scriptPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate complete 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,242 +0,0 @@
|
|||||||
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('SbStatus'),
|
|
||||||
//docker,
|
|
||||||
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';
|
|
||||||
|
|
||||||
/// 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>{};
|
|
||||||
|
|
||||||
/// Get the script directory for the given [id].
|
|
||||||
///
|
|
||||||
/// Default is [scriptDirTmp]/[scriptFile], if this path is not accessible,
|
|
||||||
/// it will be changed to [scriptDirHome]/[scriptFile].
|
|
||||||
static String getScriptDir(String id, {SystemType? systemType}) {
|
|
||||||
final customScriptDir = ServerProvider.pick(id: id)?.value.spi.custom?.scriptDir;
|
|
||||||
if (customScriptDir != null) return customScriptDir;
|
|
||||||
|
|
||||||
final defaultTmpDir = systemType == SystemType.windows ? scriptDirTmpWindows : scriptDirTmp;
|
|
||||||
_scriptDirMap[id] ??= defaultTmpDir;
|
|
||||||
return _scriptDirMap[id]!;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void switchScriptDir(String id, {SystemType? systemType}) => switch (_scriptDirMap[id]) {
|
|
||||||
scriptDirTmp => _scriptDirMap[id] = scriptDirHome,
|
|
||||||
scriptDirTmpWindows => _scriptDirMap[id] = scriptDirHomeWindows,
|
|
||||||
scriptDirHome => _scriptDirMap[id] = scriptDirTmp,
|
|
||||||
scriptDirHomeWindows => _scriptDirMap[id] = scriptDirTmpWindows,
|
|
||||||
_ => _scriptDirMap[id] = systemType == SystemType.windows ? scriptDirHomeWindows : scriptDirHome,
|
|
||||||
};
|
|
||||||
|
|
||||||
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, {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) {
|
|
||||||
ShellFunc.process => 'p',
|
|
||||||
ShellFunc.shutdown => 'sd',
|
|
||||||
ShellFunc.reboot => 'r',
|
|
||||||
ShellFunc.suspend => 'sp',
|
|
||||||
ShellFunc.status => 's',
|
|
||||||
// ShellFunc.docker=> 'd',
|
|
||||||
};
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/// 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 {
|
|
||||||
echo._('echo ${SystemType.linuxSign}'),
|
|
||||||
time._('date +%s'),
|
|
||||||
net._('cat /proc/net/dev'),
|
|
||||||
sys._('cat /etc/*-release | grep ^PRETTY_NAME'),
|
|
||||||
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',
|
|
||||||
),
|
|
||||||
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'),
|
|
||||||
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"');
|
|
||||||
|
|
||||||
final String cmd;
|
|
||||||
|
|
||||||
const StatusCmdType._(this.cmd);
|
|
||||||
}
|
|
||||||
|
|
||||||
enum BSDStatusCmdType {
|
|
||||||
echo._('echo ${SystemType.bsdSign}'),
|
|
||||||
time._('date +%s'),
|
|
||||||
net._('netstat -ibn'),
|
|
||||||
sys._('uname -or'),
|
|
||||||
cpu._('top -l 1 | grep "CPU usage"'),
|
|
||||||
uptime._('uptime'),
|
|
||||||
// Keep df -k for BSD systems as lsblk is not available on macOS/BSD
|
|
||||||
disk._('df -k'),
|
|
||||||
mem._('top -l 1 | grep PhysMem'),
|
|
||||||
//temp,
|
|
||||||
host._('hostname'),
|
|
||||||
cpuBrand._('sysctl -n machdep.cpu.brand_string');
|
|
||||||
|
|
||||||
final String cmd;
|
|
||||||
|
|
||||||
const BSDStatusCmdType._(this.cmd);
|
|
||||||
}
|
|
||||||
|
|
||||||
extension StatusCmdTypeX on StatusCmdType {
|
|
||||||
String get i18n => switch (this) {
|
|
||||||
StatusCmdType.sys => l10n.system,
|
|
||||||
StatusCmdType.host => l10n.host,
|
|
||||||
StatusCmdType.uptime => l10n.uptime,
|
|
||||||
StatusCmdType.battery => l10n.battery,
|
|
||||||
StatusCmdType.sensors => l10n.sensors,
|
|
||||||
StatusCmdType.disk => l10n.disk,
|
|
||||||
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];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
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/scripts/cmd_types.dart';
|
||||||
import 'package:server_box/data/model/server/amd.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';
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
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/scripts/cmd_types.dart';
|
||||||
|
import 'package:server_box/data/model/app/scripts/script_consts.dart';
|
||||||
import 'package:server_box/data/model/server/amd.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';
|
||||||
@@ -295,7 +296,7 @@ String? _parseSysVer(String raw) {
|
|||||||
|
|
||||||
String? _parseHostName(String raw) {
|
String? _parseHostName(String raw) {
|
||||||
if (raw.isEmpty) return null;
|
if (raw.isEmpty) return null;
|
||||||
if (raw.contains(ShellFunc.scriptFile)) return null;
|
if (raw.contains(ScriptConstants.scriptFile)) return null;
|
||||||
return raw;
|
return raw;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
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/scripts/cmd_types.dart';
|
||||||
|
|
||||||
enum SystemType {
|
enum SystemType {
|
||||||
linux(linuxSign),
|
linux(linuxSign),
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import 'package:fl_lib/fl_lib.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:server_box/core/extension/ssh_client.dart';
|
import 'package:server_box/core/extension/ssh_client.dart';
|
||||||
import 'package:server_box/data/model/app/error.dart';
|
import 'package:server_box/data/model/app/error.dart';
|
||||||
import 'package:server_box/data/model/app/shell_func.dart';
|
import 'package:server_box/data/model/app/scripts/script_consts.dart';
|
||||||
import 'package:server_box/data/model/container/image.dart';
|
import 'package:server_box/data/model/container/image.dart';
|
||||||
import 'package:server_box/data/model/container/ps.dart';
|
import 'package:server_box/data/model/container/ps.dart';
|
||||||
import 'package:server_box/data/model/container/type.dart';
|
import 'package:server_box/data/model/container/type.dart';
|
||||||
@@ -109,7 +109,7 @@ class ContainerProvider extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check result segments count
|
// Check result segments count
|
||||||
final segments = raw.split(ShellFunc.seperator);
|
final segments = raw.split(ScriptConstants.separator);
|
||||||
if (segments.length != ContainerCmdType.values.length) {
|
if (segments.length != ContainerCmdType.values.length) {
|
||||||
error = ContainerErr(
|
error = ContainerErr(
|
||||||
type: ContainerErrType.segmentsNotMatch,
|
type: ContainerErrType.segmentsNotMatch,
|
||||||
@@ -270,7 +270,7 @@ enum ContainerCmdType {
|
|||||||
stats,
|
stats,
|
||||||
images
|
images
|
||||||
// No specific commands needed for prune actions as they are simple
|
// No specific commands needed for prune actions as they are simple
|
||||||
// and don't require splitting output with ShellFunc.seperator
|
// and don't require splitting output with ScriptConstants.separator
|
||||||
;
|
;
|
||||||
|
|
||||||
String exec(ContainerType type, {bool sudo = false, bool includeStats = false}) {
|
String exec(ContainerType type, {bool sudo = false, bool includeStats = false}) {
|
||||||
@@ -296,6 +296,11 @@ enum ContainerCmdType {
|
|||||||
static String execAll(ContainerType type, {bool sudo = false, bool includeStats = false}) {
|
static String execAll(ContainerType type, {bool sudo = false, bool includeStats = false}) {
|
||||||
return ContainerCmdType.values
|
return ContainerCmdType.values
|
||||||
.map((e) => e.exec(type, sudo: sudo, includeStats: includeStats))
|
.map((e) => e.exec(type, sudo: sudo, includeStats: includeStats))
|
||||||
.join('\necho ${ShellFunc.seperator}\n');
|
.join('\necho ${ScriptConstants.separator}\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find out the required segment from [segments]
|
||||||
|
String find(List<String> segments) {
|
||||||
|
return segments[index];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ import 'package:server_box/core/utils/server.dart';
|
|||||||
import 'package:server_box/core/utils/ssh_auth.dart';
|
import 'package:server_box/core/utils/ssh_auth.dart';
|
||||||
import 'package:server_box/data/helper/system_detector.dart';
|
import 'package:server_box/data/helper/system_detector.dart';
|
||||||
import 'package:server_box/data/model/app/error.dart';
|
import 'package:server_box/data/model/app/error.dart';
|
||||||
import 'package:server_box/data/model/app/shell_func.dart';
|
import 'package:server_box/data/model/app/scripts/script_consts.dart';
|
||||||
|
import 'package:server_box/data/model/app/scripts/shell_func.dart';
|
||||||
import 'package:server_box/data/model/server/server.dart';
|
import 'package:server_box/data/model/server/server.dart';
|
||||||
import 'package:server_box/data/model/server/server_private_info.dart';
|
import 'package:server_box/data/model/server/server_private_info.dart';
|
||||||
import 'package:server_box/data/model/server/server_status_update_req.dart';
|
import 'package:server_box/data/model/server/server_status_update_req.dart';
|
||||||
@@ -337,12 +338,12 @@ class ServerProvider extends Provider {
|
|||||||
sv.status.system = detectedSystemType;
|
sv.status.system = detectedSystemType;
|
||||||
|
|
||||||
final (_, writeScriptResult) = await sv.client!.exec((session) async {
|
final (_, writeScriptResult) = await sv.client!.exec((session) async {
|
||||||
final scriptRaw = ShellFunc.allScript(spi.custom?.cmds, systemType: detectedSystemType).uint8List;
|
final scriptRaw = ShellFuncManager.allScript(spi.custom?.cmds, systemType: detectedSystemType).uint8List;
|
||||||
session.stdin.add(scriptRaw);
|
session.stdin.add(scriptRaw);
|
||||||
session.stdin.close();
|
session.stdin.close();
|
||||||
}, entry: ShellFunc.getInstallShellCmd(spi.id, systemType: detectedSystemType));
|
}, entry: ShellFuncManager.getInstallShellCmd(spi.id, systemType: detectedSystemType));
|
||||||
if (writeScriptResult.isNotEmpty && detectedSystemType != SystemType.windows) {
|
if (writeScriptResult.isNotEmpty && detectedSystemType != SystemType.windows) {
|
||||||
ShellFunc.switchScriptDir(spi.id, systemType: detectedSystemType);
|
ShellFuncManager.switchScriptDir(spi.id, systemType: detectedSystemType);
|
||||||
throw writeScriptResult;
|
throw writeScriptResult;
|
||||||
}
|
}
|
||||||
} on SSHAuthAbortError catch (e) {
|
} on SSHAuthAbortError catch (e) {
|
||||||
@@ -384,7 +385,7 @@ class ServerProvider extends Provider {
|
|||||||
try {
|
try {
|
||||||
raw = await sv.client?.run(ShellFunc.status.exec(spi.id, systemType: sv.status.system)).string;
|
raw = await sv.client?.run(ShellFunc.status.exec(spi.id, systemType: sv.status.system)).string;
|
||||||
dprint('Get status from ${spi.name}:\n$raw');
|
dprint('Get status from ${spi.name}:\n$raw');
|
||||||
segments = raw?.split(ShellFunc.seperator).map((e) => e.trim()).toList();
|
segments = raw?.split(ScriptConstants.separator).map((e) => e.trim()).toList();
|
||||||
if (raw == null || raw.isEmpty || segments == null || segments.isEmpty) {
|
if (raw == null || raw.isEmpty || segments == null || segments.isEmpty) {
|
||||||
if (Stores.setting.keepStatusWhenErr.fetch()) {
|
if (Stores.setting.keepStatusWhenErr.fetch()) {
|
||||||
// Keep previous server status when err occurs
|
// Keep previous server status when err occurs
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:fl_lib/fl_lib.dart';
|
import 'package:fl_lib/fl_lib.dart';
|
||||||
import 'package:server_box/core/extension/ssh_client.dart';
|
import 'package:server_box/core/extension/ssh_client.dart';
|
||||||
import 'package:server_box/data/model/app/shell_func.dart';
|
import 'package:server_box/data/model/app/scripts/script_consts.dart';
|
||||||
import 'package:server_box/data/model/server/server.dart';
|
import 'package:server_box/data/model/server/server.dart';
|
||||||
import 'package:server_box/data/model/server/server_private_info.dart';
|
import 'package:server_box/data/model/server/server_private_info.dart';
|
||||||
import 'package:server_box/data/model/server/systemd.dart';
|
import 'package:server_box/data/model/server/systemd.dart';
|
||||||
@@ -60,12 +60,12 @@ final class SystemdProvider {
|
|||||||
'''
|
'''
|
||||||
for unit in ${unitNames_.join(' ')}; do
|
for unit in ${unitNames_.join(' ')}; do
|
||||||
state=\$(systemctl show --no-pager \$unit)
|
state=\$(systemctl show --no-pager \$unit)
|
||||||
echo -n "${ShellFunc.seperator}\n\$state"
|
echo -n "${ScriptConstants.separator}\n\$state"
|
||||||
done
|
done
|
||||||
''';
|
''';
|
||||||
final client = _si.value.client!;
|
final client = _si.value.client!;
|
||||||
final result = await client.execForOutput(script);
|
final result = await client.execForOutput(script);
|
||||||
final units = result.split(ShellFunc.seperator);
|
final units = result.split(ScriptConstants.separator);
|
||||||
|
|
||||||
final parsedUnits = <SystemdUnit>[];
|
final parsedUnits = <SystemdUnit>[];
|
||||||
for (final unit in units) {
|
for (final unit in units) {
|
||||||
|
|||||||
@@ -119,6 +119,11 @@ abstract final class GithubIds {
|
|||||||
'AstroEngineeer',
|
'AstroEngineeer',
|
||||||
'mochasweet',
|
'mochasweet',
|
||||||
'back-lacking',
|
'back-lacking',
|
||||||
|
'cainiaojr',
|
||||||
|
'MisterMunkerz',
|
||||||
|
'CreeperKong',
|
||||||
|
'zxf945',
|
||||||
|
'cnen2018',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import 'package:fl_lib/fl_lib.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:server_box/core/extension/context/locale.dart';
|
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/shell_func.dart';
|
import 'package:server_box/data/model/app/scripts/shell_func.dart';
|
||||||
import 'package:server_box/data/model/server/proc.dart';
|
import 'package:server_box/data/model/server/proc.dart';
|
||||||
import 'package:server_box/data/model/server/server_private_info.dart';
|
import 'package:server_box/data/model/server/server_private_info.dart';
|
||||||
import 'package:server_box/data/res/store.dart';
|
import 'package:server_box/data/res/store.dart';
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import 'package:flutter_markdown/flutter_markdown.dart';
|
|||||||
import 'package:icons_plus/icons_plus.dart';
|
import 'package:icons_plus/icons_plus.dart';
|
||||||
import 'package:server_box/core/extension/context/locale.dart';
|
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/scripts/cmd_types.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/server/amd.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';
|
||||||
@@ -23,7 +23,6 @@ import 'package:server_box/data/res/store.dart';
|
|||||||
import 'package:server_box/view/page/pve.dart';
|
import 'package:server_box/view/page/pve.dart';
|
||||||
import 'package:server_box/view/page/server/edit.dart';
|
import 'package:server_box/view/page/server/edit.dart';
|
||||||
import 'package:server_box/view/page/server/logo.dart';
|
import 'package:server_box/view/page/server/logo.dart';
|
||||||
|
|
||||||
import 'package:server_box/view/widget/server_func_btns.dart';
|
import 'package:server_box/view/widget/server_func_btns.dart';
|
||||||
|
|
||||||
part 'misc.dart';
|
part 'misc.dart';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:fl_lib/fl_lib.dart';
|
import 'package:fl_lib/fl_lib.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:server_box/data/model/app/shell_func.dart';
|
import 'package:server_box/data/model/app/scripts/cmd_types.dart';
|
||||||
import 'package:server_box/data/model/server/dist.dart';
|
import 'package:server_box/data/model/server/dist.dart';
|
||||||
import 'package:server_box/data/model/server/server.dart';
|
import 'package:server_box/data/model/server/server.dart';
|
||||||
import 'package:server_box/data/res/store.dart';
|
import 'package:server_box/data/res/store.dart';
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ import 'package:server_box/core/extension/context/locale.dart';
|
|||||||
import 'package:server_box/core/extension/ssh_client.dart';
|
import 'package:server_box/core/extension/ssh_client.dart';
|
||||||
import 'package:server_box/core/route.dart';
|
import 'package:server_box/core/route.dart';
|
||||||
import 'package:server_box/data/model/app/net_view.dart';
|
import 'package:server_box/data/model/app/net_view.dart';
|
||||||
import 'package:server_box/data/model/app/shell_func.dart';
|
import 'package:server_box/data/model/app/scripts/cmd_types.dart';
|
||||||
|
import 'package:server_box/data/model/app/scripts/shell_func.dart';
|
||||||
import 'package:server_box/data/model/server/server.dart';
|
import 'package:server_box/data/model/server/server.dart';
|
||||||
import 'package:server_box/data/model/server/server_private_info.dart';
|
import 'package:server_box/data/model/server/server_private_info.dart';
|
||||||
import 'package:server_box/data/model/server/try_limiter.dart';
|
import 'package:server_box/data/model/server/try_limiter.dart';
|
||||||
|
|||||||
@@ -1821,8 +1821,8 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "."
|
path: "."
|
||||||
ref: "v1.0.588"
|
ref: "v4.0.3"
|
||||||
resolved-ref: d28207b988b5bed38c799618b9c412486592c689
|
resolved-ref: c64183346b924173eb7251800001a64771911185
|
||||||
url: "https://github.com/lollipopkit/xterm.dart"
|
url: "https://github.com/lollipopkit/xterm.dart"
|
||||||
source: git
|
source: git
|
||||||
version: "4.0.0"
|
version: "4.0.0"
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ dependencies:
|
|||||||
xterm:
|
xterm:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/lollipopkit/xterm.dart
|
url: https://github.com/lollipopkit/xterm.dart
|
||||||
ref: v1.0.588
|
ref: v4.0.3
|
||||||
computer:
|
computer:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/lollipopkit/dart_computer
|
url: https://github.com/lollipopkit/dart_computer
|
||||||
|
|||||||
186
test/script_builder_test.dart
Normal file
186
test/script_builder_test.dart
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:server_box/data/model/app/scripts/script_builders.dart';
|
||||||
|
import 'package:server_box/data/model/app/scripts/script_consts.dart';
|
||||||
|
import 'package:server_box/data/model/app/scripts/shell_func.dart';
|
||||||
|
import 'package:server_box/data/model/server/system.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('Script Builder Integration Tests', () {
|
||||||
|
test('script generation produces valid output for all platforms', () {
|
||||||
|
for (final builder in ScriptBuilderFactory.getAllBuilders()) {
|
||||||
|
final script = builder.buildScript(null);
|
||||||
|
|
||||||
|
// Basic validation
|
||||||
|
expect(script, isNotEmpty, reason: 'Script should not be empty for ${builder.runtimeType}');
|
||||||
|
|
||||||
|
// Should contain all required functions
|
||||||
|
for (final func in ShellFunc.values) {
|
||||||
|
expect(script, contains(func.name), reason: 'Script should contain function ${func.name}');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should contain proper headers
|
||||||
|
expect(script, contains(builder.scriptHeader), reason: 'Script should contain proper header');
|
||||||
|
|
||||||
|
// Should be well-formed
|
||||||
|
if (builder is WindowsScriptBuilder) {
|
||||||
|
expect(
|
||||||
|
script,
|
||||||
|
contains('switch (\$args[0])'),
|
||||||
|
reason: 'Windows script should contain switch statement',
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
script,
|
||||||
|
contains('PowerShell script for ServerBox'),
|
||||||
|
reason: 'Windows script should have PowerShell header',
|
||||||
|
);
|
||||||
|
} else if (builder is UnixScriptBuilder) {
|
||||||
|
expect(script, contains('#!/bin/sh'), reason: 'Unix script should have shebang');
|
||||||
|
expect(script, contains('case \$1 in'), reason: 'Unix script should contain case statement');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('script generation with custom commands works correctly', () {
|
||||||
|
final customCmds = {'custom_test': 'echo "Custom test command"', 'another_cmd': 'whoami'};
|
||||||
|
|
||||||
|
for (final builder in ScriptBuilderFactory.getAllBuilders()) {
|
||||||
|
final script = builder.buildScript(customCmds);
|
||||||
|
|
||||||
|
expect(script, isNotEmpty);
|
||||||
|
|
||||||
|
// Custom commands should only be included in status function for both platforms
|
||||||
|
if (builder is UnixScriptBuilder) {
|
||||||
|
expect(script, contains('echo "Custom test command"'));
|
||||||
|
expect(script, contains('whoami'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Windows builder should include custom commands in SbStatus function
|
||||||
|
if (builder is WindowsScriptBuilder) {
|
||||||
|
expect(script, contains('echo "Custom test command"'));
|
||||||
|
expect(script, contains('whoami'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('script file names are correct for each platform', () {
|
||||||
|
final windowsBuilder = ScriptBuilderFactory.getBuilder(true);
|
||||||
|
final unixBuilder = ScriptBuilderFactory.getBuilder(false);
|
||||||
|
|
||||||
|
expect(windowsBuilder.scriptFileName, equals(ScriptConstants.scriptFileWindows));
|
||||||
|
expect(windowsBuilder.scriptFileName, endsWith('.ps1'));
|
||||||
|
|
||||||
|
expect(unixBuilder.scriptFileName, equals(ScriptConstants.scriptFile));
|
||||||
|
expect(unixBuilder.scriptFileName, endsWith('.sh'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('install commands are generated correctly', () {
|
||||||
|
const testDir = '/tmp/test';
|
||||||
|
const testPath = '/tmp/test/script.sh';
|
||||||
|
|
||||||
|
final unixBuilder = ScriptBuilderFactory.getBuilder(false);
|
||||||
|
final installCmd = unixBuilder.getInstallCommand(testDir, testPath);
|
||||||
|
|
||||||
|
expect(installCmd, contains('mkdir'));
|
||||||
|
expect(installCmd, contains('chmod 755'));
|
||||||
|
expect(installCmd, contains(testPath));
|
||||||
|
|
||||||
|
const testDirWindows = 'C:\\temp\\test';
|
||||||
|
const testPathWindows = 'C:\\temp\\test\\script.ps1';
|
||||||
|
|
||||||
|
final windowsBuilder = ScriptBuilderFactory.getBuilder(true);
|
||||||
|
final installCmdWindows = windowsBuilder.getInstallCommand(testDirWindows, testPathWindows);
|
||||||
|
|
||||||
|
expect(installCmdWindows, contains('New-Item'));
|
||||||
|
expect(installCmdWindows, contains('Set-Content'));
|
||||||
|
expect(installCmdWindows, contains(testPathWindows));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('exec commands are generated correctly for all platforms', () {
|
||||||
|
const testPath = '/tmp/test/script.sh';
|
||||||
|
const testPathWindows = 'C:\\temp\\test\\script.ps1';
|
||||||
|
|
||||||
|
final unixBuilder = ScriptBuilderFactory.getBuilder(false);
|
||||||
|
final windowsBuilder = ScriptBuilderFactory.getBuilder(true);
|
||||||
|
|
||||||
|
for (final func in ShellFunc.values) {
|
||||||
|
final unixExec = unixBuilder.getExecCommand(testPath, func);
|
||||||
|
expect(unixExec, contains(testPath));
|
||||||
|
expect(unixExec, contains(func.flag));
|
||||||
|
|
||||||
|
final windowsExec = windowsBuilder.getExecCommand(testPathWindows, func);
|
||||||
|
expect(windowsExec, contains('powershell'));
|
||||||
|
expect(windowsExec, contains('-ExecutionPolicy Bypass'));
|
||||||
|
expect(windowsExec, contains(func.flag));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('script headers contain proper metadata', () {
|
||||||
|
final windowsBuilder = ScriptBuilderFactory.getBuilder(true);
|
||||||
|
final unixBuilder = ScriptBuilderFactory.getBuilder(false);
|
||||||
|
|
||||||
|
expect(windowsBuilder.scriptHeader, contains('PowerShell script for ServerBox'));
|
||||||
|
expect(windowsBuilder.scriptHeader, contains('DO NOT delete this file'));
|
||||||
|
expect(windowsBuilder.scriptHeader, contains('\$ErrorActionPreference = "SilentlyContinue"'));
|
||||||
|
|
||||||
|
expect(unixBuilder.scriptHeader, contains('#!/bin/sh'));
|
||||||
|
expect(unixBuilder.scriptHeader, contains('Script for ServerBox'));
|
||||||
|
expect(unixBuilder.scriptHeader, contains('DO NOT delete this file'));
|
||||||
|
expect(unixBuilder.scriptHeader, contains('export LANG=en_US.UTF-8'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('command dividers are consistent', () {
|
||||||
|
final windowsBuilder = ScriptBuilderFactory.getBuilder(true);
|
||||||
|
final unixBuilder = ScriptBuilderFactory.getBuilder(false);
|
||||||
|
|
||||||
|
expect(windowsBuilder.cmdDivider, equals(ScriptConstants.cmdDivider));
|
||||||
|
expect(unixBuilder.cmdDivider, equals(ScriptConstants.cmdDivider));
|
||||||
|
expect(ScriptConstants.cmdDivider, contains(ScriptConstants.separator));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('scripts handle all system types properly', () {
|
||||||
|
// Test that system type detection is properly handled
|
||||||
|
final unixScript = ShellFuncManager.allScript(null, systemType: SystemType.linux);
|
||||||
|
final bsdScript = ShellFuncManager.allScript(null, systemType: SystemType.bsd);
|
||||||
|
final windowsScript = ShellFuncManager.allScript(null, systemType: SystemType.windows);
|
||||||
|
|
||||||
|
expect(unixScript, contains('#!/bin/sh'));
|
||||||
|
expect(bsdScript, contains('#!/bin/sh')); // BSD uses same script as Linux
|
||||||
|
expect(windowsScript, contains('PowerShell script'));
|
||||||
|
|
||||||
|
// Verify OS detection logic in Unix script
|
||||||
|
expect(unixScript, contains('macSign='));
|
||||||
|
expect(unixScript, contains('bsdSign='));
|
||||||
|
expect(unixScript, contains('isBusybox='));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('error handling in script generation', () {
|
||||||
|
// Test that script generation doesn't throw with edge cases
|
||||||
|
expect(() => ShellFuncManager.allScript({}, systemType: SystemType.linux), returnsNormally);
|
||||||
|
expect(() => ShellFuncManager.allScript(null, systemType: SystemType.windows), returnsNormally);
|
||||||
|
|
||||||
|
// Test with empty custom commands
|
||||||
|
expect(() => ShellFuncManager.allScript({}, systemType: SystemType.bsd), returnsNormally);
|
||||||
|
|
||||||
|
// Test with null system type (should default to something)
|
||||||
|
expect(() => ShellFuncManager.allScript(null), returnsNormally);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('ScriptBuilderFactory Tests', () {
|
||||||
|
test('factory returns correct builder types', () {
|
||||||
|
final windowsBuilder = ScriptBuilderFactory.getBuilder(true);
|
||||||
|
final unixBuilder = ScriptBuilderFactory.getBuilder(false);
|
||||||
|
|
||||||
|
expect(windowsBuilder, isA<WindowsScriptBuilder>());
|
||||||
|
expect(unixBuilder, isA<UnixScriptBuilder>());
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getAllBuilders returns all available builders', () {
|
||||||
|
final builders = ScriptBuilderFactory.getAllBuilders();
|
||||||
|
|
||||||
|
expect(builders, hasLength(2));
|
||||||
|
expect(builders.any((b) => b is WindowsScriptBuilder), isTrue);
|
||||||
|
expect(builders.any((b) => b is UnixScriptBuilder), isTrue);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:server_box/data/model/app/shell_func.dart';
|
import 'package:server_box/data/model/app/scripts/cmd_types.dart';
|
||||||
|
import 'package:server_box/data/model/app/scripts/shell_func.dart';
|
||||||
import 'package:server_box/data/model/server/server_status_update_req.dart';
|
import 'package:server_box/data/model/server/server_status_update_req.dart';
|
||||||
import 'package:server_box/data/model/server/system.dart';
|
import 'package:server_box/data/model/server/system.dart';
|
||||||
import 'package:server_box/data/res/status.dart';
|
import 'package:server_box/data/res/status.dart';
|
||||||
@@ -14,7 +15,7 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should generate Windows PowerShell script correctly', () {
|
test('should generate Windows PowerShell script correctly', () {
|
||||||
final script = ShellFunc.allScript({'custom_cmd': 'echo "test"'}, systemType: SystemType.windows);
|
final script = ShellFuncManager.allScript({'custom_cmd': 'echo "test"'}, systemType: SystemType.windows);
|
||||||
|
|
||||||
expect(script, contains('PowerShell script for ServerBox'));
|
expect(script, contains('PowerShell script for ServerBox'));
|
||||||
expect(script, contains('function SbStatus'));
|
expect(script, contains('function SbStatus'));
|
||||||
@@ -225,11 +226,11 @@ void main() {
|
|||||||
test('should handle Windows script path generation', () {
|
test('should handle Windows script path generation', () {
|
||||||
const serverId = 'test-server';
|
const serverId = 'test-server';
|
||||||
|
|
||||||
final scriptPath = ShellFunc.getScriptPath(serverId, systemType: SystemType.windows);
|
final scriptPath = ShellFuncManager.getScriptPath(serverId, systemType: SystemType.windows);
|
||||||
expect(scriptPath, contains('.ps1'));
|
expect(scriptPath, contains('.ps1'));
|
||||||
expect(scriptPath, contains('\\'));
|
expect(scriptPath, contains('\\'));
|
||||||
|
|
||||||
final installCmd = ShellFunc.getInstallShellCmd(serverId, systemType: SystemType.windows);
|
final installCmd = ShellFuncManager.getInstallShellCmd(serverId, systemType: SystemType.windows);
|
||||||
expect(installCmd, contains('New-Item'));
|
expect(installCmd, contains('New-Item'));
|
||||||
expect(installCmd, contains('Set-Content'));
|
expect(installCmd, contains('Set-Content'));
|
||||||
// No longer contains 'powershell' prefix as commands now run in PowerShell session
|
// No longer contains 'powershell' prefix as commands now run in PowerShell session
|
||||||
|
|||||||
Reference in New Issue
Block a user