Compare commits

..

4 Commits

Author SHA1 Message Date
lollipopkit🏳️‍⚧️
038f0d4d77 chore: bump: v1276 2025-12-06 12:03:10 +08:00
lxdklp
141519d952 fix: SFTP err caused by known host key (#970)
Fixes #965
2025-11-25 10:35:14 +08:00
lxdklp
75d1a59e77 fix: Unable to obtain Windows server information (#963)
* fix: FormatException: Unexpected extension byte (at offset 8) error

* fix: PowerShell script error repair, Windows data parsing repair

* fix: Unable to obtain network card information

* fix: Unable to obtain system startup time

* fix conversation as resolved.
2025-11-22 19:17:40 +08:00
lollipopkit🏳️‍⚧️
ca4e65d7a5 chore: flutter 3.38 2025-11-13 15:24:22 +08:00
44 changed files with 674 additions and 2279 deletions

View File

@@ -25,7 +25,7 @@ jobs:
uses: subosito/flutter-action@v2
with:
channel: "stable"
flutter-version: "3.35.3"
flutter-version: "3.38.0"
- uses: actions/setup-java@v4
with:
distribution: "zulu"

View File

@@ -748,7 +748,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = 1270;
CURRENT_PROJECT_VERSION = 1276;
DEVELOPMENT_TEAM = BA88US33G6;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist";
@@ -758,7 +758,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.1270;
MARKETING_VERSION = 1.0.1276;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@@ -884,7 +884,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = 1270;
CURRENT_PROJECT_VERSION = 1276;
DEVELOPMENT_TEAM = BA88US33G6;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist";
@@ -894,7 +894,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.1270;
MARKETING_VERSION = 1.0.1276;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@@ -912,7 +912,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = 1270;
CURRENT_PROJECT_VERSION = 1276;
DEVELOPMENT_TEAM = BA88US33G6;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist";
@@ -922,7 +922,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.1270;
MARKETING_VERSION = 1.0.1276;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@@ -943,7 +943,7 @@
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1270;
CURRENT_PROJECT_VERSION = 1276;
DEVELOPMENT_TEAM = BA88US33G6;
GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES;
@@ -956,7 +956,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.0.1270;
MARKETING_VERSION = 1.0.1276;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget;
@@ -982,7 +982,7 @@
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1270;
CURRENT_PROJECT_VERSION = 1276;
DEVELOPMENT_TEAM = BA88US33G6;
GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES;
@@ -995,7 +995,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.0.1270;
MARKETING_VERSION = 1.0.1276;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -1018,7 +1018,7 @@
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1270;
CURRENT_PROJECT_VERSION = 1276;
DEVELOPMENT_TEAM = BA88US33G6;
GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES;
@@ -1031,7 +1031,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.0.1270;
MARKETING_VERSION = 1.0.1276;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -1054,7 +1054,7 @@
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1270;
CURRENT_PROJECT_VERSION = 1276;
DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = BA88US33G6;
ENABLE_PREVIEWS = YES;
@@ -1066,7 +1066,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.1270;
MARKETING_VERSION = 1.0.1276;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd;
@@ -1095,7 +1095,7 @@
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1270;
CURRENT_PROJECT_VERSION = 1276;
DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = BA88US33G6;
ENABLE_PREVIEWS = YES;
@@ -1107,7 +1107,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.1270;
MARKETING_VERSION = 1.0.1276;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd;
PRODUCT_NAME = ServerBox;
@@ -1133,7 +1133,7 @@
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1270;
CURRENT_PROJECT_VERSION = 1276;
DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = BA88US33G6;
ENABLE_PREVIEWS = YES;
@@ -1145,7 +1145,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.1270;
MARKETING_VERSION = 1.0.1276;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd;
PRODUCT_NAME = ServerBox;

View File

@@ -4,6 +4,7 @@ import 'dart:typed_data';
import 'package:dartssh2/dartssh2.dart';
import 'package:fl_lib/fl_lib.dart';
import 'package:flutter/widgets.dart';
import 'package:server_box/data/helper/ssh_decoder.dart';
import 'package:server_box/data/model/server/system.dart';
import 'package:server_box/data/res/misc.dart';
@@ -170,4 +171,98 @@ extension SSHClientX on SSHClient {
);
return ret.$2;
}
/// Runs a command and decodes output safely with encoding fallback
///
/// [systemType] - The system type (affects encoding choice)
/// Runs a command and safely decodes the result
Future<String> runSafe(
String command, {
SystemType? systemType,
String? context,
}) async {
// Let SSH errors propagate with their original type (e.g., SSHError subclasses)
final result = await run(command);
// Only catch decoding failures and add context
try {
return SSHDecoder.decode(
result,
isWindows: systemType == SystemType.windows,
context: context,
);
} on FormatException catch (e) {
throw Exception(
'Failed to decode command output${context != null ? " [$context]" : ""}: $e',
);
}
}
/// Executes a command with stdin and safely decodes stdout/stderr
Future<(String stdout, String stderr)> execSafe(
void Function(SSHSession session) callback, {
required String entry,
SystemType? systemType,
String? context,
}) async {
final stdoutBuilder = BytesBuilder(copy: false);
final stderrBuilder = BytesBuilder(copy: false);
final stdoutDone = Completer<void>();
final stderrDone = Completer<void>();
final session = await execute(entry);
session.stdout.listen(
(e) {
stdoutBuilder.add(e);
},
onDone: stdoutDone.complete,
onError: stdoutDone.completeError,
);
session.stderr.listen(
(e) {
stderrBuilder.add(e);
},
onDone: stderrDone.complete,
onError: stderrDone.completeError,
);
callback(session);
await stdoutDone.future;
await stderrDone.future;
final stdoutBytes = stdoutBuilder.takeBytes();
final stderrBytes = stderrBuilder.takeBytes();
// Only catch decoding failures, let other errors propagate
String stdout;
try {
stdout = SSHDecoder.decode(
stdoutBytes,
isWindows: systemType == SystemType.windows,
context: context != null ? '$context (stdout)' : 'stdout',
);
} on FormatException catch (e) {
throw Exception(
'Failed to decode stdout${context != null ? " [$context]" : ""}: $e',
);
}
String stderr;
try {
stderr = SSHDecoder.decode(
stderrBytes,
isWindows: systemType == SystemType.windows,
context: context != null ? '$context (stderr)' : 'stderr',
);
} on FormatException catch (e) {
throw Exception(
'Failed to decode stderr${context != null ? " [$context]" : ""}: $e',
);
}
return (stdout, stderr);
}
}

View File

@@ -1,303 +0,0 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:fl_lib/fl_lib.dart';
import 'package:path/path.dart' as path;
import 'package:path_provider/path_provider.dart';
import 'package:server_box/data/store/setting.dart';
/// Exception thrown when executable management fails
class ExecutableException implements Exception {
final String message;
ExecutableException(this.message);
@override
String toString() => 'ExecutableException: $message';
}
/// Information about an executable
class ExecutableInfo {
final String name;
final String? spokenName;
final String? version;
ExecutableInfo({required this.name, this.version, this.spokenName});
}
/// Generic executable manager for downloading and managing external tools
abstract final class ExecutableManager {
static const String _executablesDirName = 'executables';
static const int _posixExecuteBitsMask = 0x49; // Equivalent to POSIX octal 0o111
static late final Directory _executablesDir;
static final Map<String, ExecutableInfo> _customExecutables = {};
static bool _customExecutablesLoaded = false;
static Future<void> initialize() async {
final appDir = await getApplicationSupportDirectory();
_executablesDir = Directory(path.join(appDir.path, _executablesDirName));
if (!await _executablesDir.exists()) {
await _executablesDir.create(recursive: true);
}
_ensureCustomExecutablesLoaded();
}
/// Predefined executables
static final Map<String, ExecutableInfo> _predefinedExecutables = {
'cloudflared': ExecutableInfo(name: 'cloudflared'),
'ssh': ExecutableInfo(name: 'ssh'),
'nc': ExecutableInfo(name: 'nc'),
'socat': ExecutableInfo(name: 'socat'),
};
static void _ensureCustomExecutablesLoaded() {
if (_customExecutablesLoaded) return;
final List<dynamic> stored = SettingStore.instance.proxyCmdCustomExecs.get();
for (final raw in stored) {
final info = _parseExecutableInfo(raw);
if (info == null) continue;
_customExecutables[info.name] = info;
_predefinedExecutables[info.name] = info;
}
_customExecutablesLoaded = true;
}
static void _persistCustomExecutables() {
final values = _customExecutables.values
.map((info) => {
'name': info.name,
if (info.spokenName != null) 'spokenName': info.spokenName,
if (info.version != null) 'version': info.version,
})
.toList();
SettingStore.instance.proxyCmdCustomExecs.set(values);
}
static ExecutableInfo? _parseExecutableInfo(dynamic raw) {
if (raw is String) {
try {
return _parseExecutableInfo(jsonDecode(raw));
} catch (e) {
Loggers.app.warning('Failed to decode custom executable entry: $e');
return null;
}
}
if (raw is! Map) return null;
final name = raw['name']?.toString();
if (name == null || name.isEmpty) return null;
return ExecutableInfo(
name: name,
spokenName: raw['spokenName']?.toString(),
version: raw['version']?.toString(),
);
}
/// Check if an executable exists in PATH or local directory
static Future<bool> isExecutableAvailable(String name) async {
// First check if it's in PATH
final pathExecutable = await _lookupExecutableInSystemPath(name);
if (pathExecutable != null) {
return true;
}
// Check local executables directory
final localExecutable = _getLocalExecutablePath(name);
if (await localExecutable.exists()) {
return true;
}
return false;
}
/// Get the path to an executable (either in PATH or local)
static Future<String> getExecutablePath(String name) async {
// First check if it's in PATH
final pathExecutable = await _lookupExecutableInSystemPath(name);
if (pathExecutable != null) {
return pathExecutable;
}
// Check local executables directory
final localExecutable = _getLocalExecutablePath(name);
if (await localExecutable.exists()) {
return localExecutable.path;
}
throw ExecutableException('Executable $name not found in PATH or local directory');
}
/// Download an executable if it's not available
static Future<String> ensureExecutable(String name) async {
if (await isExecutableAvailable(name)) {
return await getExecutablePath(name);
}
throw ExecutableException('Executable "$name" not found and automatic installation is not implemented');
}
/// Remove a local executable
static Future<void> removeExecutable(String name) async {
final localExecutable = _getLocalExecutablePath(name);
if (await localExecutable.exists()) {
await localExecutable.delete();
Loggers.app.info('Removed local executable: $name');
}
}
/// List all locally downloaded executables
static Future<List<String>> listLocalExecutables() async {
if (!await _executablesDir.exists()) {
return [];
}
final executables = <String>[];
await for (final entity in _executablesDir.list()) {
if (entity is File && _isExecutable(entity)) {
executables.add(path.basenameWithoutExtension(entity.path));
}
}
return executables;
}
/// Get the size of a local executable
static Future<int> getExecutableSize(String name) async {
final localExecutable = _getLocalExecutablePath(name);
if (await localExecutable.exists()) {
return await localExecutable.length();
}
return 0;
}
/// Get the version of an executable
static Future<String?> getExecutableVersion(String name) async {
try {
final executablePath = await getExecutablePath(name);
// Try common version flags
final versionFlags = ['--version', '-v', '-V', 'version'];
for (final flag in versionFlags) {
try {
final result = await Process.run(executablePath, [flag]);
if (result.exitCode == 0) {
final output = result.stdout.toString().trim();
if (output.isNotEmpty) {
return output.split('\n').first; // Return first line only
}
}
} catch (e) {
// Try next flag
}
}
} catch (e) {
Loggers.app.warning('Error getting version for $name: $e');
}
return null;
}
/// Validate an executable by trying to run it with a help flag
static Future<bool> validateExecutable(String name) async {
try {
final executablePath = await getExecutablePath(name);
// Try to run the executable with a help flag
final helpFlags = ['--help', '-h', '-help'];
for (final flag in helpFlags) {
try {
final result = await Process.run(executablePath, [flag]);
if (result.exitCode == 0 || result.exitCode == 1) {
// Help often returns 1
return true;
}
} catch (e) {
// Try next flag
}
}
} catch (e) {
Loggers.app.warning('Error validating $name: $e');
return false;
}
return false;
}
static Future<String?> _lookupExecutableInSystemPath(String name) async {
final command = Platform.isWindows ? 'where' : 'which';
try {
final result = await Process.run(command, [name]);
if (result.exitCode != 0) {
return null;
}
final stdoutString = result.stdout.toString().trim();
if (stdoutString.isEmpty) {
return null;
}
final candidate = stdoutString
.split('\n')
.map((line) => line.trim())
.firstWhere((line) => line.isNotEmpty, orElse: () => '');
if (candidate.isEmpty) {
return null;
}
return candidate;
} catch (e) {
Loggers.app.warning('Error checking PATH for $name: $e');
return null;
}
}
/// Get the local path for an executable
static File _getLocalExecutablePath(String name) {
final extension = Platform.isWindows ? '.exe' : '';
return File(path.join(_executablesDir.path, '$name$extension'));
}
/// Check if a file is executable
static bool _isExecutable(File file) {
if (Platform.isWindows) {
return file.path.endsWith('.exe');
} else {
// Check file permissions
final stat = file.statSync();
return (stat.mode & _posixExecuteBitsMask) != 0; // Check execute bits
}
}
/// Get predefined executable info
static ExecutableInfo? getExecutableInfo(String name) {
_ensureCustomExecutablesLoaded();
return _predefinedExecutables[name];
}
/// Add a custom executable definition
static void addCustomExecutable(String name, ExecutableInfo info) {
_ensureCustomExecutablesLoaded();
_customExecutables[name] = info;
_predefinedExecutables[name] = info;
_persistCustomExecutables();
Loggers.app.info('Adding custom executable: $name');
}
/// Remove a custom executable definition
static void removeCustomExecutable(String name) {
_ensureCustomExecutablesLoaded();
final removed = _customExecutables.remove(name);
if (removed != null) {
_predefinedExecutables.remove(name);
_persistCustomExecutables();
Loggers.app.info('Removing custom executable: $name');
}
}
}

View File

@@ -1,300 +0,0 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:dartssh2/dartssh2.dart';
import 'package:fl_lib/fl_lib.dart';
import 'package:server_box/core/utils/executable_manager.dart';
import 'package:server_box/core/utils/proxy_socket.dart';
import 'package:server_box/data/model/server/proxy_command_config.dart';
import 'package:server_box/data/store/setting.dart';
/// Exception thrown when proxy command execution fails
class ProxyCommandException implements Exception {
final String message;
final int? exitCode;
final String? stdout;
final String? stderr;
ProxyCommandException({required this.message, this.exitCode, this.stdout, this.stderr});
@override
String toString() {
return 'ProxyCommandException: $message'
'${exitCode != null ? ' (exit code: $exitCode)' : ''}'
'${stderr != null ? '\nStderr: $stderr' : ''}';
}
}
/// Generic proxy command executor that handles SSH ProxyCommand functionality
abstract final class ProxyCommandExecutor {
static final Map<String, ProxyCommandConfig> _customPresets = {};
static bool _customPresetsLoaded = false;
static void _ensureCustomPresetsLoaded() {
if (_customPresetsLoaded) return;
final List<dynamic> stored = SettingStore.instance.proxyCmdCustomPresets.get();
for (final raw in stored) {
final preset = _parsePreset(raw);
if (preset == null) continue;
_customPresets[preset.key] = preset.value;
}
_customPresetsLoaded = true;
}
static void _persistCustomPresets() {
final list = _customPresets.entries
.map((entry) => {
'name': entry.key,
'config': entry.value.toJson(),
})
.toList();
SettingStore.instance.proxyCmdCustomPresets.set(list);
}
static MapEntry<String, ProxyCommandConfig>? _parsePreset(dynamic raw) {
dynamic payload = raw;
if (payload is String) {
try {
payload = jsonDecode(payload);
} catch (e) {
Loggers.app.warning('Failed to decode custom proxy preset entry: $e');
return null;
}
}
if (payload is! Map) return null;
final name = payload['name']?.toString();
final configRaw = payload['config'];
if (name == null || name.isEmpty || configRaw is! Map) return null;
try {
final config = ProxyCommandConfig.fromJson(Map<String, dynamic>.from(configRaw));
return MapEntry(name, config);
} catch (e) {
Loggers.app.warning('Failed to parse custom proxy preset "$name": $e');
return null;
}
}
/// Execute a proxy command and return a socket connected through the proxy
static Future<SSHSocket> executeProxyCommand(
ProxyCommandConfig config, {
required String hostname,
required int port,
required String user,
}) async {
if (Platform.isIOS) {
throw ProxyCommandException(message: 'ProxyCommand is not supported on iOS');
}
final finalCommand = config.getFinalCommand(hostname: hostname, port: port, user: user);
final tokens = _tokenizeCommand(finalCommand);
if (tokens.isEmpty) {
throw ProxyCommandException(message: 'ProxyCommand resolved to an empty command');
}
final executableToken = tokens.first;
Loggers.app.info('Executing proxy command: $finalCommand');
// Ensure executable is available if required
String executablePath;
if (config.requiresExecutable && config.executableName != null) {
executablePath = await ExecutableManager.ensureExecutable(config.executableName!);
} else {
executablePath = await ExecutableManager.getExecutablePath(executableToken);
}
// Parse command and arguments
final args = tokens.skip(1).toList();
// Set up environment
final environment = {...Platform.environment, ...?config.environment};
// Start the process
Process process;
try {
process = await Process.start(
executablePath,
args,
workingDirectory: config.workingDirectory,
environment: environment,
);
} catch (e) {
throw ProxyCommandException(message: 'Failed to start proxy command: $e', exitCode: -1);
}
// Set up timeout handling
var timedOut = false;
final timeoutTimer = Timer(config.timeout, () {
timedOut = true;
process.kill();
});
try {
// For ProxyCommand, we create a ProxySocket that wraps the process
final socket = ProxySocket(process);
// Monitor the process for immediate failures
unawaited(
process.exitCode.then((code) {
if (code != 0 && !socket.closed && !timedOut) {
socket.close();
}
}),
);
return socket;
} catch (e) {
process.kill();
rethrow;
} finally {
timeoutTimer.cancel();
}
}
/// Validate proxy command configuration
static Future<String?> validateConfig(ProxyCommandConfig config) async {
if (Platform.isIOS) {
return 'ProxyCommand is not supported on iOS';
}
final testCommand = config.getFinalCommand(hostname: 'test.example.com', port: 22, user: 'testuser');
late final List<String> tokens;
try {
tokens = _tokenizeCommand(testCommand);
} on ProxyCommandException catch (e) {
return e.message;
}
if (tokens.isEmpty) {
return 'Proxy command must not be empty';
}
// Check if required placeholders are present
if (!config.command.contains('%h')) {
return 'Proxy command must contain %h (hostname) placeholder';
}
String executablePath;
// If executable is required, check if it exists and reuse resolved path
if (config.requiresExecutable && config.executableName != null) {
try {
executablePath = await ExecutableManager.ensureExecutable(config.executableName!);
} catch (e) {
return e.toString();
}
} else {
try {
executablePath = await ExecutableManager.getExecutablePath(tokens.first);
} catch (e) {
return e.toString();
}
}
// Try to validate command syntax (dry run)
try {
await Process.run(executablePath, ['--help']);
} catch (e) {
return 'Command validation failed: $e';
}
return null; // No error
}
/// Get available proxy command presets
static Map<String, ProxyCommandConfig> getPresets() {
_ensureCustomPresetsLoaded();
return {
...proxyCommandPresets,
..._customPresets,
};
}
/// Add a custom preset
static Future<void> addCustomPreset(String name, ProxyCommandConfig config) async {
_ensureCustomPresetsLoaded();
_customPresets[name] = config;
_persistCustomPresets();
Loggers.app.info('Adding custom proxy preset: $name');
}
/// Remove a custom preset
static Future<void> removeCustomPreset(String name) async {
_ensureCustomPresetsLoaded();
final removed = _customPresets.remove(name);
if (removed != null) {
_persistCustomPresets();
Loggers.app.info('Removing custom proxy preset: $name');
}
}
static List<String> tokenizeCommand(String command) => _tokenizeCommand(command);
static List<String> _tokenizeCommand(String command) {
final tokens = <String>[];
final buffer = StringBuffer();
String? quote;
var escaped = false;
void flush() {
if (buffer.isEmpty) return;
tokens.add(buffer.toString());
buffer.clear();
}
for (final rune in command.runes) {
final char = String.fromCharCode(rune);
if (escaped) {
buffer.write(char);
escaped = false;
continue;
}
if (quote != null) {
if (char == '\\' && quote == '"') {
escaped = true;
continue;
}
if (char == quote) {
quote = null;
continue;
}
buffer.write(char);
continue;
}
if (char == '\\') {
escaped = true;
continue;
}
if (char == '"' || char == "'") {
quote = char;
continue;
}
if (char.trim().isEmpty) {
flush();
continue;
}
buffer.write(char);
}
if (quote != null) {
throw ProxyCommandException(message: 'ProxyCommand has unmatched quote');
}
if (escaped) {
throw ProxyCommandException(message: 'ProxyCommand ends with an incomplete escape sequence');
}
flush();
return tokens;
}
}

View File

@@ -1,125 +0,0 @@
import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'package:dartssh2/dartssh2.dart';
import 'package:fl_lib/fl_lib.dart';
/// Socket implementation that communicates through a Process stdin/stdout
/// This is used for ProxyCommand functionality where the SSH connection
/// is proxied through an external command
class ProxySocket implements SSHSocket {
final Process _process;
final StreamController<Uint8List> _incomingController =
StreamController<Uint8List>();
final StreamController<List<int>> _outgoingController =
StreamController<List<int>>();
final Completer<void> _doneCompleter = Completer<void>();
bool _closed = false;
late StreamSubscription<Uint8List> _stdoutSubscription;
late StreamSubscription<Uint8List> _stderrSubscription;
ProxySocket(this._process) {
// Set up stdout reading
_stdoutSubscription = _process.stdout
.transform(Uint8ListStreamTransformer())
.listen(_onIncomingData,
onError: _onError,
onDone: _onProcessDone,
cancelOnError: true);
// Set up stderr reading (for logging)
_stderrSubscription = _process.stderr
.transform(Uint8ListStreamTransformer())
.listen((data) {
Loggers.app.warning('Proxy stderr: ${String.fromCharCodes(data)}');
});
// Set up outgoing data
_outgoingController.stream.listen(_onOutgoingData);
// Handle process exit
_process.exitCode.then((code) {
if (!_closed && code != 0) {
_onError('Proxy process exited with code: $code');
}
});
}
@override
Stream<Uint8List> get stream => _incomingController.stream;
@override
StreamSink<List<int>> get sink => _outgoingController.sink;
@override
Future<void> get done => _doneCompleter.future;
/// Check if the socket is closed
bool get closed => _closed;
@override
Future<void> close() async {
if (_closed) return;
_closed = true;
await _stdoutSubscription.cancel();
await _stderrSubscription.cancel();
await _outgoingController.close();
await _incomingController.close();
if (!_doneCompleter.isCompleted) {
_doneCompleter.complete();
}
// Kill the process if it's still running
try {
_process.kill();
} catch (e) {
Loggers.app.warning('Error killing proxy process: $e');
}
}
@override
void destroy() {
close();
}
void _onIncomingData(Uint8List data) {
if (!_closed) {
_incomingController.add(data);
}
}
void _onOutgoingData(List<int> data) {
if (!_closed) {
_process.stdin.add(data);
}
}
void _onError(dynamic error, [StackTrace? stackTrace]) {
if (!_closed) {
_incomingController.addError(error, stackTrace);
close();
}
}
void _onProcessDone() {
if (!_closed) {
_incomingController.close();
close();
}
}
}
/// Transformer to convert `Stream<List<int>>` to `Stream<Uint8List>`
class Uint8ListStreamTransformer
extends StreamTransformerBase<List<int>, Uint8List> {
const Uint8ListStreamTransformer();
@override
Stream<Uint8List> bind(Stream<List<int>> stream) {
return stream.map((data) => Uint8List.fromList(data));
}
}

View File

@@ -1,6 +1,5 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:dartssh2/dartssh2.dart';
import 'package:fl_lib/fl_lib.dart';
@@ -8,7 +7,6 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:server_box/core/app_navigator.dart';
import 'package:server_box/core/extension/context/locale.dart';
import 'package:server_box/core/utils/proxy_command_executor.dart';
import 'package:server_box/data/model/app/error.dart';
import 'package:server_box/data/model/server/server_private_info.dart';
import 'package:server_box/data/res/store.dart';
@@ -70,7 +68,8 @@ Future<SSHClient> genClient(
String? alterUser;
// Check for Jump Server first - this needs special handling
final socket = await () async {
// Proxy
final jumpSpi_ = () {
// Multi-thread or key login
if (jumpSpi != null) return jumpSpi;
@@ -78,7 +77,6 @@ Future<SSHClient> genClient(
if (spi.jumpId != null) return Stores.server.box.get(spi.jumpId);
}();
if (jumpSpi_ != null) {
// For jump server, we establish connection through the jump client
final jumpClient = await genClient(
jumpSpi_,
privateKey: jumpPrivateKey,
@@ -88,85 +86,25 @@ Future<SSHClient> genClient(
onHostKeyPrompt: onHostKeyPrompt,
);
final forwardChannel = await jumpClient.forwardLocal(spi.ip, spi.port);
final hostKeyVerifier = _HostKeyVerifier(
spi: spi,
cache: hostKeyCache,
persistCallback: hostKeyPersist,
prompt: hostKeyPrompt,
);
final keyId = spi.keyId;
if (keyId == null) {
onStatus?.call(GenSSHClientStatus.pwd);
return SSHClient(
forwardChannel,
username: spi.user,
onPasswordRequest: () => spi.pwd,
onUserInfoRequest: onKeyboardInteractive,
onVerifyHostKey: hostKeyVerifier.call,
);
}
privateKey ??= getPrivateKey(keyId);
onStatus?.call(GenSSHClientStatus.key);
return SSHClient(
forwardChannel,
username: spi.user,
identities: await compute(loadIndentity, privateKey),
onUserInfoRequest: onKeyboardInteractive,
onVerifyHostKey: hostKeyVerifier.call,
);
return await jumpClient.forwardLocal(spi.ip, spi.port);
}
// For ProxyCommand and direct connections, get SSHSocket
SSHSocket? socket;
// Direct
try {
final proxyCommand = spi.proxyCommand;
// ProxyCommand support - Check for ProxyCommand configuration first
if (proxyCommand != null && !Platform.isIOS) {
try {
Loggers.app.info('Connecting via ProxyCommand: ${proxyCommand.command}');
socket = await ProxyCommandExecutor.executeProxyCommand(
proxyCommand,
hostname: spi.ip,
port: spi.port,
user: spi.user,
);
} catch (e) {
Loggers.app.warning('ProxyCommand failed', e);
if (!proxyCommand.retryOnFailure) {
rethrow;
}
// If retry is enabled, fall through to direct connection
Loggers.app.info('ProxyCommand failed, falling back to direct connection');
}
} else if (proxyCommand != null && Platform.isIOS) {
Loggers.app.info('ProxyCommand configuration is ignored on iOS');
}
// Direct connection (or fallback)
socket ??= await () async {
try {
return await SSHSocket.connect(spi.ip, spi.port, timeout: timeout);
} catch (e) {
Loggers.app.warning('genClient', e);
if (spi.alterUrl == null) rethrow;
try {
final res = spi.parseAlterUrl();
alterUser = res.$2;
return await SSHSocket.connect(res.$1, res.$3, timeout: timeout);
} catch (e) {
Loggers.app.warning('genClient alterUrl', e);
rethrow;
}
}
}();
return await SSHSocket.connect(spi.ip, spi.port, timeout: timeout);
} catch (e) {
Loggers.app.warning('Failed to establish connection', e);
rethrow;
Loggers.app.warning('genClient', e);
if (spi.alterUrl == null) rethrow;
try {
final res = spi.parseAlterUrl();
alterUser = res.$2;
return await SSHSocket.connect(res.$1, res.$3, timeout: timeout);
} catch (e) {
Loggers.app.warning('genClient alterUrl', e);
rethrow;
}
}
}();
final hostKeyVerifier = _HostKeyVerifier(
spi: spi,
@@ -184,6 +122,8 @@ Future<SSHClient> genClient(
onPasswordRequest: () => spi.pwd,
onUserInfoRequest: onKeyboardInteractive,
onVerifyHostKey: hostKeyVerifier.call,
// printDebug: debugPrint,
// printTrace: debugPrint,
);
}
privateKey ??= getPrivateKey(keyId);
@@ -192,9 +132,12 @@ Future<SSHClient> genClient(
return SSHClient(
socket,
username: spi.user,
// Must use [compute] here, instead of [Computer.shared.start]
identities: await compute(loadIndentity, privateKey),
onUserInfoRequest: onKeyboardInteractive,
onVerifyHostKey: hostKeyVerifier.call,
// printDebug: debugPrint,
// printTrace: debugPrint,
);
}

View File

@@ -149,28 +149,11 @@ abstract final class SSHConfig {
/// Extract jump host from ProxyJump or ProxyCommand
static String? _extractJumpHost(String value) {
// Normalize whitespace
final parts = value.trim().split(RegExp(r'\s+'));
// Try to find a token that looks like a user@host[:port]
// This covers common patterns like:
// - ProxyJump user@host
// - ProxyCommand ssh -W %h:%p user@host
for (final token in parts) {
if (token.contains('@')) {
// Strip any surrounding quotes just in case
var cleaned = token;
if ((cleaned.startsWith("'") && cleaned.endsWith("'")) ||
(cleaned.startsWith('"') && cleaned.endsWith('"'))) {
cleaned = cleaned.substring(1, cleaned.length - 1);
}
return cleaned;
}
// For ProxyJump, the format is usually: user@host:port
// For ProxyCommand, it's more complex and might need custom parsing
if (value.contains('@')) {
return value.split(' ').first;
}
// ProxyJump may also be provided as just a hostname (no user@)
// In that case we don't have enough information to build an oldId-style reference,
// so we ignore it here and let the user configure a jump server manually.
return null;
}

View File

@@ -0,0 +1,66 @@
import 'dart:convert';
import 'package:fl_lib/fl_lib.dart';
import 'package:flutter_gbk2utf8/flutter_gbk2utf8.dart';
/// Utility class for decoding SSH command output with encoding fallback
class SSHDecoder {
/// Decodes bytes to string with multiple encoding fallback strategies
///
/// Tries in order:
/// 1. UTF-8 (with allowMalformed for lenient parsing)
/// - Windows PowerShell scripts now set UTF-8 output encoding by default
/// 2. GBK (for Windows Chinese systems)
/// - In some cases, Windows will still revert to GBK.
/// - Only attempted if UTF-8 produces replacement characters (<28>)
static String decode(
List<int> bytes, {
bool isWindows = false,
String? context,
}) {
if (bytes.isEmpty) return '';
// Try UTF-8 first with allowMalformed
try {
final result = utf8.decode(bytes, allowMalformed: true);
// Check if there are replacement characters indicating decode failure
// For non-Windows systems, always use UTF-8 result
if (!result.contains('<EFBFBD>') || !isWindows) {
return result;
}
// For Windows with replacement chars, log and try GBK fallback
if (isWindows && result.contains('<EFBFBD>')) {
final contextInfo = context != null ? ' [$context]' : '';
Loggers.app.info('UTF-8 decode has replacement chars$contextInfo, trying GBK fallback');
}
} catch (e) {
final contextInfo = context != null ? ' [$context]' : '';
Loggers.app.warning('UTF-8 decode failed$contextInfo: $e');
}
// For Windows or when UTF-8 has replacement chars, try GBK
try {
return gbk.decode(bytes);
} catch (e) {
final contextInfo = context != null ? ' [$context]' : '';
Loggers.app.warning('GBK decode failed$contextInfo: $e');
// Return empty string if all decoding attempts fail
return '';
}
}
/// Encodes string to bytes for SSH command input
///
/// Uses GBK for Windows, UTF-8 for others
static List<int> encode(String text, {bool isWindows = false}) {
if (isWindows) {
try {
return gbk.encode(text);
} catch (e) {
Loggers.app.warning('GBK encode failed: $e, falling back to UTF-8');
return utf8.encode(text);
}
}
return utf8.encode(text);
}
}

View File

@@ -1,5 +1,6 @@
import 'package:dartssh2/dartssh2.dart';
import 'package:fl_lib/fl_lib.dart';
import 'package:server_box/core/extension/ssh_client.dart';
import 'package:server_box/data/model/server/server_private_info.dart';
import 'package:server_box/data/model/server/system.dart';
@@ -23,7 +24,10 @@ class SystemDetector {
try {
// Try to detect Unix/Linux/BSD systems first (more reliable and doesn't create files)
final unixResult = await client.run('uname -a 2>/dev/null').string;
final unixResult = await client.runSafe(
'uname -a 2>/dev/null',
context: 'uname detection for ${spi.oldId}',
);
if (unixResult.contains('Linux')) {
detectedSystemType = SystemType.linux;
dprint('Detected Linux system type for ${spi.oldId}');
@@ -35,15 +39,19 @@ class SystemDetector {
}
// If uname fails, try to detect Windows systems
final powershellResult = await client.run('ver 2>nul').string;
final powershellResult = await client.runSafe(
'ver 2>nul',
systemType: SystemType.windows,
context: 'ver detection for ${spi.oldId}',
);
if (powershellResult.isNotEmpty &&
(powershellResult.contains('Windows') || powershellResult.contains('NT'))) {
detectedSystemType = SystemType.windows;
dprint('Detected Windows system type for ${spi.oldId}');
return detectedSystemType;
}
} catch (e) {
Loggers.app.warning('System detection failed for ${spi.oldId}: $e');
} catch (e, stackTrace) {
Loggers.app.warning('System detection failed for ${spi.oldId}: $e\n$stackTrace');
}
// Default fallback

View File

@@ -166,25 +166,34 @@ enum WindowsStatusCmdType implements ShellCmdType {
echo('echo ${SystemType.windowsSign}'),
time('[DateTimeOffset]::UtcNow.ToUnixTimeSeconds()'),
/// Get network interface statistics using Windows Performance Counters
/// Get network interface statistics using WMI
///
/// Uses Get-Counter to collect network I/O metrics from all network interfaces:
/// - Collects bytes received and sent per second for all network interfaces
/// Uses WMI Win32_PerfRawData_Tcpip_NetworkInterface for cross-language compatibility:
/// - 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',
r'$s1 = @(Get-WmiObject Win32_PerfRawData_Tcpip_NetworkInterface | '
r'Select-Object Name, BytesReceivedPersec, BytesSentPersec, Timestamp_Sys100NS); '
r'Start-Sleep -Seconds 1; '
r'$s2 = @(Get-WmiObject Win32_PerfRawData_Tcpip_NetworkInterface | '
r'Select-Object Name, BytesReceivedPersec, BytesSentPersec, Timestamp_Sys100NS); '
r'@($s1, $s2) | ConvertTo-Json -Depth 5',
),
sys('(Get-ComputerInfo).OsName'),
cpu(
'Get-WmiObject -Class Win32_Processor | '
'Select-Object Name, LoadPercentage | ConvertTo-Json',
'Select-Object Name, LoadPercentage, NumberOfCores, NumberOfLogicalProcessors | ConvertTo-Json',
),
/// Get system uptime by calculating time since last boot
///
/// Calculates uptime directly in PowerShell to avoid date format parsing issues:
/// - Gets LastBootUpTime from Win32_OperatingSystem
/// - Calculates difference from current time
/// - Returns pre-formatted string: "X days, H:MM" or "H:MM" (if less than 1 day)
/// - Uses ToString('00') for zero-padding to avoid quote escaping issues
uptime(
r"""$up = (Get-Date) - (Get-CimInstance Win32_OperatingSystem).LastBootUpTime; if ($up.Days -gt 0) { "$($up.Days) days, $($up.Hours):$($up.Minutes.ToString('00'))" } else { "$($up.Hours):$($up.Minutes.ToString('00'))" }""",
),
uptime('(Get-CimInstance -ClassName Win32_OperatingSystem).LastBootUpTime'),
conn('(netstat -an | findstr ESTABLISHED | Measure-Object -Line).Count'),
disk(
'Get-WmiObject -Class Win32_LogicalDisk | '
@@ -213,19 +222,19 @@ enum WindowsStatusCmdType implements ShellCmdType {
),
host(r'Write-Output $env:COMPUTERNAME'),
/// Get disk I/O statistics using Windows Performance Counters
/// Get disk I/O statistics using WMI
///
/// Uses Get-Counter to collect disk I/O metrics from all physical disks:
/// Uses WMI Win32_PerfRawData_PerfDisk_PhysicalDisk:
/// - 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
/// - Takes 2 samples with 1 second interval to calculate rates
/// - DiskReadBytesPersec and DiskWriteBytesPersec are cumulative counters
diskio(
r'Get-Counter -Counter '
r'"\\PhysicalDisk(*)\\Disk Read Bytes/sec", '
r'"\\PhysicalDisk(*)\\Disk Write Bytes/sec" '
r'-SampleInterval 1 -MaxSamples 2 | ConvertTo-Json',
r'$s1 = @(Get-WmiObject Win32_PerfRawData_PerfDisk_PhysicalDisk | '
r'Select-Object Name, DiskReadBytesPersec, DiskWriteBytesPersec, Timestamp_Sys100NS); '
r'Start-Sleep -Seconds 1; '
r'$s2 = @(Get-WmiObject Win32_PerfRawData_PerfDisk_PhysicalDisk | '
r'Select-Object Name, DiskReadBytesPersec, DiskWriteBytesPersec, Timestamp_Sys100NS); '
r'@($s1, $s2) | ConvertTo-Json -Depth 5',
),
battery(
'Get-WmiObject -Class Win32_Battery | '
@@ -287,7 +296,7 @@ enum WindowsStatusCmdType implements ShellCmdType {
String get separator => ScriptConstants.getCmdSeparator(name);
@override
String get divider => ScriptConstants.getCmdDivider(name);
String get divider => ScriptConstants.getWindowsCmdDivider(name);
@override
CmdTypeSys get sysType => CmdTypeSys.windows;

View File

@@ -29,6 +29,9 @@ class ScriptConstants {
/// Generate command-specific divider
static String getCmdDivider(String cmdName) => '\necho ${getCmdSeparator(cmdName)}\n\t';
/// Generate command-specific divider for Windows PowerShell
static String getWindowsCmdDivider(String cmdName) => '\n Write-Host "${getCmdSeparator(cmdName)}"\n ';
/// Parse script output into command-specific map
static Map<String, String> parseScriptOutput(String raw) {
final result = <String, String>{};
@@ -102,6 +105,7 @@ exec 2>/dev/null
# DO NOT delete this file while app is running
\$ErrorActionPreference = "SilentlyContinue"
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
''';
}

View File

@@ -6,7 +6,7 @@ import 'package:server_box/data/res/status.dart';
/// Capacity of the FIFO queue
const _kCap = 30;
class Cpus extends TimeSeq<List<SingleCpuCore>> {
class Cpus extends TimeSeq<SingleCpuCore> {
Cpus(super.init1, super.init2);
final Map<String, int> brand = {};
@@ -14,13 +14,20 @@ class Cpus extends TimeSeq<List<SingleCpuCore>> {
@override
void onUpdate() {
_coresCount = now.length;
if (pre.isEmpty || now.isEmpty || pre.length != now.length) {
_totalDelta = 0;
_user = 0;
_sys = 0;
_iowait = 0;
_idle = 0;
return;
}
_totalDelta = now[0].total - pre[0].total;
_user = _getUser();
_sys = _getSys();
_iowait = _getIowait();
_idle = _getIdle();
_updateSpots();
//_updateRange();
}
double usedPercent({int coreIdx = 0}) {

View File

@@ -280,7 +280,7 @@ class Disk with EquatableMixin {
];
}
class DiskIO extends TimeSeq<List<DiskIOPiece>> {
class DiskIO extends TimeSeq<DiskIOPiece> {
DiskIO(super.init1, super.init2);
@override

View File

@@ -18,7 +18,7 @@ class NetSpeedPart extends TimeSeqIface<NetSpeedPart> {
typedef CachedNetVals = ({String sizeIn, String sizeOut, String speedIn, String speedOut});
class NetSpeed extends TimeSeq<List<NetSpeedPart>> {
class NetSpeed extends TimeSeq<NetSpeedPart> {
NetSpeed(super.init1, super.init2);
@override

View File

@@ -1,81 +0,0 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:server_box/core/utils/proxy_command_executor.dart' show ProxyCommandException;
part 'proxy_command_config.freezed.dart';
part 'proxy_command_config.g.dart';
/// ProxyCommand configuration for SSH connections
@freezed
abstract class ProxyCommandConfig with _$ProxyCommandConfig {
const factory ProxyCommandConfig({
/// Command template with placeholders
/// Available placeholders: %h (hostname), %p (port), %r (user)
required String command,
/// Command arguments (optional, can be included in command)
List<String>? args,
/// Working directory for the command
String? workingDirectory,
/// Environment variables for the command
Map<String, String>? environment,
/// Timeout for command execution
@Default(Duration(seconds: 30)) Duration timeout,
/// Whether to retry on connection failure
@Default(false) bool retryOnFailure,
/// Maximum retry attempts
@Default(3) int maxRetries,
/// Whether the proxy command requires executable download
@Default(false) bool requiresExecutable,
/// Executable name for download management
String? executableName,
/// Executable download URL
String? executableDownloadUrl,
}) = _ProxyCommandConfig;
factory ProxyCommandConfig.fromJson(Map<String, dynamic> json) => _$ProxyCommandConfigFromJson(json);
}
/// Common proxy command presets
const Map<String, ProxyCommandConfig> proxyCommandPresets = {
'cloudflare_access': ProxyCommandConfig(
command: 'cloudflared access ssh --hostname %h',
requiresExecutable: true,
executableName: 'cloudflared',
timeout: Duration(seconds: 15),
),
'ssh_via_bastion': ProxyCommandConfig(
command: 'ssh -W %h:%p bastion.example.com',
timeout: Duration(seconds: 10),
),
'nc_netcat': ProxyCommandConfig(command: 'nc %h %p', timeout: Duration(seconds: 10)),
'socat': ProxyCommandConfig(
command: 'socat - PROXY:%h:%p,proxyport=8080',
timeout: Duration(seconds: 10),
),
};
/// Extension for ProxyCommandConfig to add utility methods
extension ProxyCommandConfigExtension on ProxyCommandConfig {
/// Get the final command with placeholders replaced
String getFinalCommand({required String hostname, required int port, required String user}) {
if (!command.contains('%h') && !command.contains('%p') && !command.contains('%r')) {
throw ProxyCommandException(
message: 'Proxy command "$command" must include at least one placeholder (%h, %p, %r)',
);
}
var finalCommand = command;
finalCommand = finalCommand.replaceAll('%h', hostname);
finalCommand = finalCommand.replaceAll('%p', port.toString());
finalCommand = finalCommand.replaceAll('%r', user);
return finalCommand;
}
}

View File

@@ -1,344 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// coverage:ignore-file
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'proxy_command_config.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$ProxyCommandConfig {
/// Command template with placeholders
/// Available placeholders: %h (hostname), %p (port), %r (user)
String get command;/// Command arguments (optional, can be included in command)
List<String>? get args;/// Working directory for the command
String? get workingDirectory;/// Environment variables for the command
Map<String, String>? get environment;/// Timeout for command execution
Duration get timeout;/// Whether to retry on connection failure
bool get retryOnFailure;/// Maximum retry attempts
int get maxRetries;/// Whether the proxy command requires executable download
bool get requiresExecutable;/// Executable name for download management
String? get executableName;/// Executable download URL
String? get executableDownloadUrl;
/// Create a copy of ProxyCommandConfig
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$ProxyCommandConfigCopyWith<ProxyCommandConfig> get copyWith => _$ProxyCommandConfigCopyWithImpl<ProxyCommandConfig>(this as ProxyCommandConfig, _$identity);
/// Serializes this ProxyCommandConfig to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is ProxyCommandConfig&&(identical(other.command, command) || other.command == command)&&const DeepCollectionEquality().equals(other.args, args)&&(identical(other.workingDirectory, workingDirectory) || other.workingDirectory == workingDirectory)&&const DeepCollectionEquality().equals(other.environment, environment)&&(identical(other.timeout, timeout) || other.timeout == timeout)&&(identical(other.retryOnFailure, retryOnFailure) || other.retryOnFailure == retryOnFailure)&&(identical(other.maxRetries, maxRetries) || other.maxRetries == maxRetries)&&(identical(other.requiresExecutable, requiresExecutable) || other.requiresExecutable == requiresExecutable)&&(identical(other.executableName, executableName) || other.executableName == executableName)&&(identical(other.executableDownloadUrl, executableDownloadUrl) || other.executableDownloadUrl == executableDownloadUrl));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,command,const DeepCollectionEquality().hash(args),workingDirectory,const DeepCollectionEquality().hash(environment),timeout,retryOnFailure,maxRetries,requiresExecutable,executableName,executableDownloadUrl);
@override
String toString() {
return 'ProxyCommandConfig(command: $command, args: $args, workingDirectory: $workingDirectory, environment: $environment, timeout: $timeout, retryOnFailure: $retryOnFailure, maxRetries: $maxRetries, requiresExecutable: $requiresExecutable, executableName: $executableName, executableDownloadUrl: $executableDownloadUrl)';
}
}
/// @nodoc
abstract mixin class $ProxyCommandConfigCopyWith<$Res> {
factory $ProxyCommandConfigCopyWith(ProxyCommandConfig value, $Res Function(ProxyCommandConfig) _then) = _$ProxyCommandConfigCopyWithImpl;
@useResult
$Res call({
String command, List<String>? args, String? workingDirectory, Map<String, String>? environment, Duration timeout, bool retryOnFailure, int maxRetries, bool requiresExecutable, String? executableName, String? executableDownloadUrl
});
}
/// @nodoc
class _$ProxyCommandConfigCopyWithImpl<$Res>
implements $ProxyCommandConfigCopyWith<$Res> {
_$ProxyCommandConfigCopyWithImpl(this._self, this._then);
final ProxyCommandConfig _self;
final $Res Function(ProxyCommandConfig) _then;
/// Create a copy of ProxyCommandConfig
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? command = null,Object? args = freezed,Object? workingDirectory = freezed,Object? environment = freezed,Object? timeout = null,Object? retryOnFailure = null,Object? maxRetries = null,Object? requiresExecutable = null,Object? executableName = freezed,Object? executableDownloadUrl = freezed,}) {
return _then(_self.copyWith(
command: null == command ? _self.command : command // ignore: cast_nullable_to_non_nullable
as String,args: freezed == args ? _self.args : args // ignore: cast_nullable_to_non_nullable
as List<String>?,workingDirectory: freezed == workingDirectory ? _self.workingDirectory : workingDirectory // ignore: cast_nullable_to_non_nullable
as String?,environment: freezed == environment ? _self.environment : environment // ignore: cast_nullable_to_non_nullable
as Map<String, String>?,timeout: null == timeout ? _self.timeout : timeout // ignore: cast_nullable_to_non_nullable
as Duration,retryOnFailure: null == retryOnFailure ? _self.retryOnFailure : retryOnFailure // ignore: cast_nullable_to_non_nullable
as bool,maxRetries: null == maxRetries ? _self.maxRetries : maxRetries // ignore: cast_nullable_to_non_nullable
as int,requiresExecutable: null == requiresExecutable ? _self.requiresExecutable : requiresExecutable // ignore: cast_nullable_to_non_nullable
as bool,executableName: freezed == executableName ? _self.executableName : executableName // ignore: cast_nullable_to_non_nullable
as String?,executableDownloadUrl: freezed == executableDownloadUrl ? _self.executableDownloadUrl : executableDownloadUrl // ignore: cast_nullable_to_non_nullable
as String?,
));
}
}
/// Adds pattern-matching-related methods to [ProxyCommandConfig].
extension ProxyCommandConfigPatterns on ProxyCommandConfig {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _ProxyCommandConfig value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _ProxyCommandConfig() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _ProxyCommandConfig value) $default,){
final _that = this;
switch (_that) {
case _ProxyCommandConfig():
return $default(_that);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _ProxyCommandConfig value)? $default,){
final _that = this;
switch (_that) {
case _ProxyCommandConfig() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String command, List<String>? args, String? workingDirectory, Map<String, String>? environment, Duration timeout, bool retryOnFailure, int maxRetries, bool requiresExecutable, String? executableName, String? executableDownloadUrl)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _ProxyCommandConfig() when $default != null:
return $default(_that.command,_that.args,_that.workingDirectory,_that.environment,_that.timeout,_that.retryOnFailure,_that.maxRetries,_that.requiresExecutable,_that.executableName,_that.executableDownloadUrl);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String command, List<String>? args, String? workingDirectory, Map<String, String>? environment, Duration timeout, bool retryOnFailure, int maxRetries, bool requiresExecutable, String? executableName, String? executableDownloadUrl) $default,) {final _that = this;
switch (_that) {
case _ProxyCommandConfig():
return $default(_that.command,_that.args,_that.workingDirectory,_that.environment,_that.timeout,_that.retryOnFailure,_that.maxRetries,_that.requiresExecutable,_that.executableName,_that.executableDownloadUrl);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String command, List<String>? args, String? workingDirectory, Map<String, String>? environment, Duration timeout, bool retryOnFailure, int maxRetries, bool requiresExecutable, String? executableName, String? executableDownloadUrl)? $default,) {final _that = this;
switch (_that) {
case _ProxyCommandConfig() when $default != null:
return $default(_that.command,_that.args,_that.workingDirectory,_that.environment,_that.timeout,_that.retryOnFailure,_that.maxRetries,_that.requiresExecutable,_that.executableName,_that.executableDownloadUrl);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable()
class _ProxyCommandConfig implements ProxyCommandConfig {
const _ProxyCommandConfig({required this.command, final List<String>? args, this.workingDirectory, final Map<String, String>? environment, this.timeout = const Duration(seconds: 30), this.retryOnFailure = false, this.maxRetries = 3, this.requiresExecutable = false, this.executableName, this.executableDownloadUrl}): _args = args,_environment = environment;
factory _ProxyCommandConfig.fromJson(Map<String, dynamic> json) => _$ProxyCommandConfigFromJson(json);
/// Command template with placeholders
/// Available placeholders: %h (hostname), %p (port), %r (user)
@override final String command;
/// Command arguments (optional, can be included in command)
final List<String>? _args;
/// Command arguments (optional, can be included in command)
@override List<String>? get args {
final value = _args;
if (value == null) return null;
if (_args is EqualUnmodifiableListView) return _args;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(value);
}
/// Working directory for the command
@override final String? workingDirectory;
/// Environment variables for the command
final Map<String, String>? _environment;
/// Environment variables for the command
@override Map<String, String>? get environment {
final value = _environment;
if (value == null) return null;
if (_environment is EqualUnmodifiableMapView) return _environment;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(value);
}
/// Timeout for command execution
@override@JsonKey() final Duration timeout;
/// Whether to retry on connection failure
@override@JsonKey() final bool retryOnFailure;
/// Maximum retry attempts
@override@JsonKey() final int maxRetries;
/// Whether the proxy command requires executable download
@override@JsonKey() final bool requiresExecutable;
/// Executable name for download management
@override final String? executableName;
/// Executable download URL
@override final String? executableDownloadUrl;
/// Create a copy of ProxyCommandConfig
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$ProxyCommandConfigCopyWith<_ProxyCommandConfig> get copyWith => __$ProxyCommandConfigCopyWithImpl<_ProxyCommandConfig>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$ProxyCommandConfigToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _ProxyCommandConfig&&(identical(other.command, command) || other.command == command)&&const DeepCollectionEquality().equals(other._args, _args)&&(identical(other.workingDirectory, workingDirectory) || other.workingDirectory == workingDirectory)&&const DeepCollectionEquality().equals(other._environment, _environment)&&(identical(other.timeout, timeout) || other.timeout == timeout)&&(identical(other.retryOnFailure, retryOnFailure) || other.retryOnFailure == retryOnFailure)&&(identical(other.maxRetries, maxRetries) || other.maxRetries == maxRetries)&&(identical(other.requiresExecutable, requiresExecutable) || other.requiresExecutable == requiresExecutable)&&(identical(other.executableName, executableName) || other.executableName == executableName)&&(identical(other.executableDownloadUrl, executableDownloadUrl) || other.executableDownloadUrl == executableDownloadUrl));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,command,const DeepCollectionEquality().hash(_args),workingDirectory,const DeepCollectionEquality().hash(_environment),timeout,retryOnFailure,maxRetries,requiresExecutable,executableName,executableDownloadUrl);
@override
String toString() {
return 'ProxyCommandConfig(command: $command, args: $args, workingDirectory: $workingDirectory, environment: $environment, timeout: $timeout, retryOnFailure: $retryOnFailure, maxRetries: $maxRetries, requiresExecutable: $requiresExecutable, executableName: $executableName, executableDownloadUrl: $executableDownloadUrl)';
}
}
/// @nodoc
abstract mixin class _$ProxyCommandConfigCopyWith<$Res> implements $ProxyCommandConfigCopyWith<$Res> {
factory _$ProxyCommandConfigCopyWith(_ProxyCommandConfig value, $Res Function(_ProxyCommandConfig) _then) = __$ProxyCommandConfigCopyWithImpl;
@override @useResult
$Res call({
String command, List<String>? args, String? workingDirectory, Map<String, String>? environment, Duration timeout, bool retryOnFailure, int maxRetries, bool requiresExecutable, String? executableName, String? executableDownloadUrl
});
}
/// @nodoc
class __$ProxyCommandConfigCopyWithImpl<$Res>
implements _$ProxyCommandConfigCopyWith<$Res> {
__$ProxyCommandConfigCopyWithImpl(this._self, this._then);
final _ProxyCommandConfig _self;
final $Res Function(_ProxyCommandConfig) _then;
/// Create a copy of ProxyCommandConfig
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? command = null,Object? args = freezed,Object? workingDirectory = freezed,Object? environment = freezed,Object? timeout = null,Object? retryOnFailure = null,Object? maxRetries = null,Object? requiresExecutable = null,Object? executableName = freezed,Object? executableDownloadUrl = freezed,}) {
return _then(_ProxyCommandConfig(
command: null == command ? _self.command : command // ignore: cast_nullable_to_non_nullable
as String,args: freezed == args ? _self._args : args // ignore: cast_nullable_to_non_nullable
as List<String>?,workingDirectory: freezed == workingDirectory ? _self.workingDirectory : workingDirectory // ignore: cast_nullable_to_non_nullable
as String?,environment: freezed == environment ? _self._environment : environment // ignore: cast_nullable_to_non_nullable
as Map<String, String>?,timeout: null == timeout ? _self.timeout : timeout // ignore: cast_nullable_to_non_nullable
as Duration,retryOnFailure: null == retryOnFailure ? _self.retryOnFailure : retryOnFailure // ignore: cast_nullable_to_non_nullable
as bool,maxRetries: null == maxRetries ? _self.maxRetries : maxRetries // ignore: cast_nullable_to_non_nullable
as int,requiresExecutable: null == requiresExecutable ? _self.requiresExecutable : requiresExecutable // ignore: cast_nullable_to_non_nullable
as bool,executableName: freezed == executableName ? _self.executableName : executableName // ignore: cast_nullable_to_non_nullable
as String?,executableDownloadUrl: freezed == executableDownloadUrl ? _self.executableDownloadUrl : executableDownloadUrl // ignore: cast_nullable_to_non_nullable
as String?,
));
}
}
// dart format on

View File

@@ -1,39 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'proxy_command_config.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_ProxyCommandConfig _$ProxyCommandConfigFromJson(Map<String, dynamic> json) =>
_ProxyCommandConfig(
command: json['command'] as String,
args: (json['args'] as List<dynamic>?)?.map((e) => e as String).toList(),
workingDirectory: json['workingDirectory'] as String?,
environment: (json['environment'] as Map<String, dynamic>?)?.map(
(k, e) => MapEntry(k, e as String),
),
timeout: json['timeout'] == null
? const Duration(seconds: 30)
: Duration(microseconds: (json['timeout'] as num).toInt()),
retryOnFailure: json['retryOnFailure'] as bool? ?? false,
maxRetries: (json['maxRetries'] as num?)?.toInt() ?? 3,
requiresExecutable: json['requiresExecutable'] as bool? ?? false,
executableName: json['executableName'] as String?,
executableDownloadUrl: json['executableDownloadUrl'] as String?,
);
Map<String, dynamic> _$ProxyCommandConfigToJson(_ProxyCommandConfig instance) =>
<String, dynamic>{
'command': instance.command,
'args': instance.args,
'workingDirectory': instance.workingDirectory,
'environment': instance.environment,
'timeout': instance.timeout.inMicroseconds,
'retryOnFailure': instance.retryOnFailure,
'maxRetries': instance.maxRetries,
'requiresExecutable': instance.requiresExecutable,
'executableName': instance.executableName,
'executableDownloadUrl': instance.executableDownloadUrl,
};

View File

@@ -4,7 +4,6 @@ import 'package:fl_lib/fl_lib.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:server_box/data/model/app/error.dart';
import 'package:server_box/data/model/server/custom.dart';
import 'package:server_box/data/model/server/proxy_command_config.dart';
import 'package:server_box/data/model/server/system.dart';
import 'package:server_box/data/model/server/wol_cfg.dart';
import 'package:server_box/data/store/server.dart';
@@ -22,7 +21,7 @@ part 'server_private_info.g.dart';
abstract class Spi with _$Spi {
const Spi._();
@JsonSerializable(includeIfNull: false, explicitToJson: true)
@JsonSerializable(includeIfNull: false)
const factory Spi({
required String name,
required String ip,
@@ -46,18 +45,14 @@ abstract class Spi with _$Spi {
@Default('') @JsonKey(fromJson: Spi.parseId) String id,
/// Custom system type (unix or windows). If set, skip auto-detection.
SystemType? customSystemType,
@JsonKey(includeIfNull: false) SystemType? customSystemType,
/// Disabled command types for this server
List<String>? disabledCmdTypes,
/// ProxyCommand configuration for SSH connections
ProxyCommandConfig? proxyCommand,
@JsonKey(includeIfNull: false) List<String>? disabledCmdTypes,
}) = _Spi;
factory Spi.fromJson(Map<String, dynamic> json) => _$SpiFromJson(json);
@override
String toString() => 'Spi<$oldId>';

View File

@@ -19,9 +19,8 @@ mixin _$Spi {
@JsonKey(name: 'pubKeyId') String? get keyId; List<String>? get tags; String? get alterUrl; bool get autoConnect;/// [id] of the jump server
String? get jumpId; ServerCustom? get custom; WakeOnLanCfg? get wolCfg;/// It only applies to SSH terminal.
Map<String, String>? get envs;@JsonKey(fromJson: Spi.parseId) String get id;/// Custom system type (unix or windows). If set, skip auto-detection.
SystemType? get customSystemType;/// Disabled command types for this server
List<String>? get disabledCmdTypes;/// ProxyCommand configuration for SSH connections
ProxyCommandConfig? get proxyCommand;
@JsonKey(includeIfNull: false) SystemType? get customSystemType;/// Disabled command types for this server
@JsonKey(includeIfNull: false) List<String>? get disabledCmdTypes;
/// Create a copy of Spi
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -34,12 +33,12 @@ $SpiCopyWith<Spi> get copyWith => _$SpiCopyWithImpl<Spi>(this as Spi, _$identity
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is Spi&&(identical(other.name, name) || other.name == name)&&(identical(other.ip, ip) || other.ip == ip)&&(identical(other.port, port) || other.port == port)&&(identical(other.user, user) || other.user == user)&&(identical(other.pwd, pwd) || other.pwd == pwd)&&(identical(other.keyId, keyId) || other.keyId == keyId)&&const DeepCollectionEquality().equals(other.tags, tags)&&(identical(other.alterUrl, alterUrl) || other.alterUrl == alterUrl)&&(identical(other.autoConnect, autoConnect) || other.autoConnect == autoConnect)&&(identical(other.jumpId, jumpId) || other.jumpId == jumpId)&&(identical(other.custom, custom) || other.custom == custom)&&(identical(other.wolCfg, wolCfg) || other.wolCfg == wolCfg)&&const DeepCollectionEquality().equals(other.envs, envs)&&(identical(other.id, id) || other.id == id)&&(identical(other.customSystemType, customSystemType) || other.customSystemType == customSystemType)&&const DeepCollectionEquality().equals(other.disabledCmdTypes, disabledCmdTypes)&&(identical(other.proxyCommand, proxyCommand) || other.proxyCommand == proxyCommand));
return identical(this, other) || (other.runtimeType == runtimeType&&other is Spi&&(identical(other.name, name) || other.name == name)&&(identical(other.ip, ip) || other.ip == ip)&&(identical(other.port, port) || other.port == port)&&(identical(other.user, user) || other.user == user)&&(identical(other.pwd, pwd) || other.pwd == pwd)&&(identical(other.keyId, keyId) || other.keyId == keyId)&&const DeepCollectionEquality().equals(other.tags, tags)&&(identical(other.alterUrl, alterUrl) || other.alterUrl == alterUrl)&&(identical(other.autoConnect, autoConnect) || other.autoConnect == autoConnect)&&(identical(other.jumpId, jumpId) || other.jumpId == jumpId)&&(identical(other.custom, custom) || other.custom == custom)&&(identical(other.wolCfg, wolCfg) || other.wolCfg == wolCfg)&&const DeepCollectionEquality().equals(other.envs, envs)&&(identical(other.id, id) || other.id == id)&&(identical(other.customSystemType, customSystemType) || other.customSystemType == customSystemType)&&const DeepCollectionEquality().equals(other.disabledCmdTypes, disabledCmdTypes));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,name,ip,port,user,pwd,keyId,const DeepCollectionEquality().hash(tags),alterUrl,autoConnect,jumpId,custom,wolCfg,const DeepCollectionEquality().hash(envs),id,customSystemType,const DeepCollectionEquality().hash(disabledCmdTypes),proxyCommand);
int get hashCode => Object.hash(runtimeType,name,ip,port,user,pwd,keyId,const DeepCollectionEquality().hash(tags),alterUrl,autoConnect,jumpId,custom,wolCfg,const DeepCollectionEquality().hash(envs),id,customSystemType,const DeepCollectionEquality().hash(disabledCmdTypes));
@@ -50,11 +49,11 @@ abstract mixin class $SpiCopyWith<$Res> {
factory $SpiCopyWith(Spi value, $Res Function(Spi) _then) = _$SpiCopyWithImpl;
@useResult
$Res call({
String name, String ip, int port, String user, String? pwd,@JsonKey(name: 'pubKeyId') String? keyId, List<String>? tags, String? alterUrl, bool autoConnect, String? jumpId, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map<String, String>? envs,@JsonKey(fromJson: Spi.parseId) String id, SystemType? customSystemType, List<String>? disabledCmdTypes, ProxyCommandConfig? proxyCommand
String name, String ip, int port, String user, String? pwd,@JsonKey(name: 'pubKeyId') String? keyId, List<String>? tags, String? alterUrl, bool autoConnect, String? jumpId, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map<String, String>? envs,@JsonKey(fromJson: Spi.parseId) String id,@JsonKey(includeIfNull: false) SystemType? customSystemType,@JsonKey(includeIfNull: false) List<String>? disabledCmdTypes
});
$ProxyCommandConfigCopyWith<$Res>? get proxyCommand;
}
/// @nodoc
@@ -67,7 +66,7 @@ class _$SpiCopyWithImpl<$Res>
/// Create a copy of Spi
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? name = null,Object? ip = null,Object? port = null,Object? user = null,Object? pwd = freezed,Object? keyId = freezed,Object? tags = freezed,Object? alterUrl = freezed,Object? autoConnect = null,Object? jumpId = freezed,Object? custom = freezed,Object? wolCfg = freezed,Object? envs = freezed,Object? id = null,Object? customSystemType = freezed,Object? disabledCmdTypes = freezed,Object? proxyCommand = freezed,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? name = null,Object? ip = null,Object? port = null,Object? user = null,Object? pwd = freezed,Object? keyId = freezed,Object? tags = freezed,Object? alterUrl = freezed,Object? autoConnect = null,Object? jumpId = freezed,Object? custom = freezed,Object? wolCfg = freezed,Object? envs = freezed,Object? id = null,Object? customSystemType = freezed,Object? disabledCmdTypes = freezed,}) {
return _then(_self.copyWith(
name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
as String,ip: null == ip ? _self.ip : ip // ignore: cast_nullable_to_non_nullable
@@ -85,23 +84,10 @@ as WakeOnLanCfg?,envs: freezed == envs ? _self.envs : envs // ignore: cast_nulla
as Map<String, String>?,id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,customSystemType: freezed == customSystemType ? _self.customSystemType : customSystemType // ignore: cast_nullable_to_non_nullable
as SystemType?,disabledCmdTypes: freezed == disabledCmdTypes ? _self.disabledCmdTypes : disabledCmdTypes // ignore: cast_nullable_to_non_nullable
as List<String>?,proxyCommand: freezed == proxyCommand ? _self.proxyCommand : proxyCommand // ignore: cast_nullable_to_non_nullable
as ProxyCommandConfig?,
as List<String>?,
));
}
/// Create a copy of Spi
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$ProxyCommandConfigCopyWith<$Res>? get proxyCommand {
if (_self.proxyCommand == null) {
return null;
}
return $ProxyCommandConfigCopyWith<$Res>(_self.proxyCommand!, (value) {
return _then(_self.copyWith(proxyCommand: value));
});
}
}
@@ -183,10 +169,10 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String name, String ip, int port, String user, String? pwd, @JsonKey(name: 'pubKeyId') String? keyId, List<String>? tags, String? alterUrl, bool autoConnect, String? jumpId, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map<String, String>? envs, @JsonKey(fromJson: Spi.parseId) String id, SystemType? customSystemType, List<String>? disabledCmdTypes, ProxyCommandConfig? proxyCommand)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String name, String ip, int port, String user, String? pwd, @JsonKey(name: 'pubKeyId') String? keyId, List<String>? tags, String? alterUrl, bool autoConnect, String? jumpId, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map<String, String>? envs, @JsonKey(fromJson: Spi.parseId) String id, @JsonKey(includeIfNull: false) SystemType? customSystemType, @JsonKey(includeIfNull: false) List<String>? disabledCmdTypes)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _Spi() when $default != null:
return $default(_that.name,_that.ip,_that.port,_that.user,_that.pwd,_that.keyId,_that.tags,_that.alterUrl,_that.autoConnect,_that.jumpId,_that.custom,_that.wolCfg,_that.envs,_that.id,_that.customSystemType,_that.disabledCmdTypes,_that.proxyCommand);case _:
return $default(_that.name,_that.ip,_that.port,_that.user,_that.pwd,_that.keyId,_that.tags,_that.alterUrl,_that.autoConnect,_that.jumpId,_that.custom,_that.wolCfg,_that.envs,_that.id,_that.customSystemType,_that.disabledCmdTypes);case _:
return orElse();
}
@@ -204,10 +190,10 @@ return $default(_that.name,_that.ip,_that.port,_that.user,_that.pwd,_that.keyId,
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String name, String ip, int port, String user, String? pwd, @JsonKey(name: 'pubKeyId') String? keyId, List<String>? tags, String? alterUrl, bool autoConnect, String? jumpId, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map<String, String>? envs, @JsonKey(fromJson: Spi.parseId) String id, SystemType? customSystemType, List<String>? disabledCmdTypes, ProxyCommandConfig? proxyCommand) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String name, String ip, int port, String user, String? pwd, @JsonKey(name: 'pubKeyId') String? keyId, List<String>? tags, String? alterUrl, bool autoConnect, String? jumpId, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map<String, String>? envs, @JsonKey(fromJson: Spi.parseId) String id, @JsonKey(includeIfNull: false) SystemType? customSystemType, @JsonKey(includeIfNull: false) List<String>? disabledCmdTypes) $default,) {final _that = this;
switch (_that) {
case _Spi():
return $default(_that.name,_that.ip,_that.port,_that.user,_that.pwd,_that.keyId,_that.tags,_that.alterUrl,_that.autoConnect,_that.jumpId,_that.custom,_that.wolCfg,_that.envs,_that.id,_that.customSystemType,_that.disabledCmdTypes,_that.proxyCommand);case _:
return $default(_that.name,_that.ip,_that.port,_that.user,_that.pwd,_that.keyId,_that.tags,_that.alterUrl,_that.autoConnect,_that.jumpId,_that.custom,_that.wolCfg,_that.envs,_that.id,_that.customSystemType,_that.disabledCmdTypes);case _:
throw StateError('Unexpected subclass');
}
@@ -224,10 +210,10 @@ return $default(_that.name,_that.ip,_that.port,_that.user,_that.pwd,_that.keyId,
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String name, String ip, int port, String user, String? pwd, @JsonKey(name: 'pubKeyId') String? keyId, List<String>? tags, String? alterUrl, bool autoConnect, String? jumpId, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map<String, String>? envs, @JsonKey(fromJson: Spi.parseId) String id, SystemType? customSystemType, List<String>? disabledCmdTypes, ProxyCommandConfig? proxyCommand)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String name, String ip, int port, String user, String? pwd, @JsonKey(name: 'pubKeyId') String? keyId, List<String>? tags, String? alterUrl, bool autoConnect, String? jumpId, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map<String, String>? envs, @JsonKey(fromJson: Spi.parseId) String id, @JsonKey(includeIfNull: false) SystemType? customSystemType, @JsonKey(includeIfNull: false) List<String>? disabledCmdTypes)? $default,) {final _that = this;
switch (_that) {
case _Spi() when $default != null:
return $default(_that.name,_that.ip,_that.port,_that.user,_that.pwd,_that.keyId,_that.tags,_that.alterUrl,_that.autoConnect,_that.jumpId,_that.custom,_that.wolCfg,_that.envs,_that.id,_that.customSystemType,_that.disabledCmdTypes,_that.proxyCommand);case _:
return $default(_that.name,_that.ip,_that.port,_that.user,_that.pwd,_that.keyId,_that.tags,_that.alterUrl,_that.autoConnect,_that.jumpId,_that.custom,_that.wolCfg,_that.envs,_that.id,_that.customSystemType,_that.disabledCmdTypes);case _:
return null;
}
@@ -237,9 +223,9 @@ return $default(_that.name,_that.ip,_that.port,_that.user,_that.pwd,_that.keyId,
/// @nodoc
@JsonSerializable(includeIfNull: false, explicitToJson: true)
@JsonSerializable(includeIfNull: false)
class _Spi extends Spi {
const _Spi({required this.name, required this.ip, required this.port, required this.user, this.pwd, @JsonKey(name: 'pubKeyId') this.keyId, final List<String>? tags, this.alterUrl, this.autoConnect = true, this.jumpId, this.custom, this.wolCfg, final Map<String, String>? envs, @JsonKey(fromJson: Spi.parseId) this.id = '', this.customSystemType, final List<String>? disabledCmdTypes, this.proxyCommand}): _tags = tags,_envs = envs,_disabledCmdTypes = disabledCmdTypes,super._();
const _Spi({required this.name, required this.ip, required this.port, required this.user, this.pwd, @JsonKey(name: 'pubKeyId') this.keyId, final List<String>? tags, this.alterUrl, this.autoConnect = true, this.jumpId, this.custom, this.wolCfg, final Map<String, String>? envs, @JsonKey(fromJson: Spi.parseId) this.id = '', @JsonKey(includeIfNull: false) this.customSystemType, @JsonKey(includeIfNull: false) final List<String>? disabledCmdTypes}): _tags = tags,_envs = envs,_disabledCmdTypes = disabledCmdTypes,super._();
factory _Spi.fromJson(Map<String, dynamic> json) => _$SpiFromJson(json);
@override final String name;
@@ -277,11 +263,11 @@ class _Spi extends Spi {
@override@JsonKey(fromJson: Spi.parseId) final String id;
/// Custom system type (unix or windows). If set, skip auto-detection.
@override final SystemType? customSystemType;
@override@JsonKey(includeIfNull: false) final SystemType? customSystemType;
/// Disabled command types for this server
final List<String>? _disabledCmdTypes;
/// Disabled command types for this server
@override List<String>? get disabledCmdTypes {
@override@JsonKey(includeIfNull: false) List<String>? get disabledCmdTypes {
final value = _disabledCmdTypes;
if (value == null) return null;
if (_disabledCmdTypes is EqualUnmodifiableListView) return _disabledCmdTypes;
@@ -289,8 +275,6 @@ class _Spi extends Spi {
return EqualUnmodifiableListView(value);
}
/// ProxyCommand configuration for SSH connections
@override final ProxyCommandConfig? proxyCommand;
/// Create a copy of Spi
/// with the given fields replaced by the non-null parameter values.
@@ -305,12 +289,12 @@ Map<String, dynamic> toJson() {
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _Spi&&(identical(other.name, name) || other.name == name)&&(identical(other.ip, ip) || other.ip == ip)&&(identical(other.port, port) || other.port == port)&&(identical(other.user, user) || other.user == user)&&(identical(other.pwd, pwd) || other.pwd == pwd)&&(identical(other.keyId, keyId) || other.keyId == keyId)&&const DeepCollectionEquality().equals(other._tags, _tags)&&(identical(other.alterUrl, alterUrl) || other.alterUrl == alterUrl)&&(identical(other.autoConnect, autoConnect) || other.autoConnect == autoConnect)&&(identical(other.jumpId, jumpId) || other.jumpId == jumpId)&&(identical(other.custom, custom) || other.custom == custom)&&(identical(other.wolCfg, wolCfg) || other.wolCfg == wolCfg)&&const DeepCollectionEquality().equals(other._envs, _envs)&&(identical(other.id, id) || other.id == id)&&(identical(other.customSystemType, customSystemType) || other.customSystemType == customSystemType)&&const DeepCollectionEquality().equals(other._disabledCmdTypes, _disabledCmdTypes)&&(identical(other.proxyCommand, proxyCommand) || other.proxyCommand == proxyCommand));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _Spi&&(identical(other.name, name) || other.name == name)&&(identical(other.ip, ip) || other.ip == ip)&&(identical(other.port, port) || other.port == port)&&(identical(other.user, user) || other.user == user)&&(identical(other.pwd, pwd) || other.pwd == pwd)&&(identical(other.keyId, keyId) || other.keyId == keyId)&&const DeepCollectionEquality().equals(other._tags, _tags)&&(identical(other.alterUrl, alterUrl) || other.alterUrl == alterUrl)&&(identical(other.autoConnect, autoConnect) || other.autoConnect == autoConnect)&&(identical(other.jumpId, jumpId) || other.jumpId == jumpId)&&(identical(other.custom, custom) || other.custom == custom)&&(identical(other.wolCfg, wolCfg) || other.wolCfg == wolCfg)&&const DeepCollectionEquality().equals(other._envs, _envs)&&(identical(other.id, id) || other.id == id)&&(identical(other.customSystemType, customSystemType) || other.customSystemType == customSystemType)&&const DeepCollectionEquality().equals(other._disabledCmdTypes, _disabledCmdTypes));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,name,ip,port,user,pwd,keyId,const DeepCollectionEquality().hash(_tags),alterUrl,autoConnect,jumpId,custom,wolCfg,const DeepCollectionEquality().hash(_envs),id,customSystemType,const DeepCollectionEquality().hash(_disabledCmdTypes),proxyCommand);
int get hashCode => Object.hash(runtimeType,name,ip,port,user,pwd,keyId,const DeepCollectionEquality().hash(_tags),alterUrl,autoConnect,jumpId,custom,wolCfg,const DeepCollectionEquality().hash(_envs),id,customSystemType,const DeepCollectionEquality().hash(_disabledCmdTypes));
@@ -321,11 +305,11 @@ abstract mixin class _$SpiCopyWith<$Res> implements $SpiCopyWith<$Res> {
factory _$SpiCopyWith(_Spi value, $Res Function(_Spi) _then) = __$SpiCopyWithImpl;
@override @useResult
$Res call({
String name, String ip, int port, String user, String? pwd,@JsonKey(name: 'pubKeyId') String? keyId, List<String>? tags, String? alterUrl, bool autoConnect, String? jumpId, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map<String, String>? envs,@JsonKey(fromJson: Spi.parseId) String id, SystemType? customSystemType, List<String>? disabledCmdTypes, ProxyCommandConfig? proxyCommand
String name, String ip, int port, String user, String? pwd,@JsonKey(name: 'pubKeyId') String? keyId, List<String>? tags, String? alterUrl, bool autoConnect, String? jumpId, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map<String, String>? envs,@JsonKey(fromJson: Spi.parseId) String id,@JsonKey(includeIfNull: false) SystemType? customSystemType,@JsonKey(includeIfNull: false) List<String>? disabledCmdTypes
});
@override $ProxyCommandConfigCopyWith<$Res>? get proxyCommand;
}
/// @nodoc
@@ -338,7 +322,7 @@ class __$SpiCopyWithImpl<$Res>
/// Create a copy of Spi
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? name = null,Object? ip = null,Object? port = null,Object? user = null,Object? pwd = freezed,Object? keyId = freezed,Object? tags = freezed,Object? alterUrl = freezed,Object? autoConnect = null,Object? jumpId = freezed,Object? custom = freezed,Object? wolCfg = freezed,Object? envs = freezed,Object? id = null,Object? customSystemType = freezed,Object? disabledCmdTypes = freezed,Object? proxyCommand = freezed,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? name = null,Object? ip = null,Object? port = null,Object? user = null,Object? pwd = freezed,Object? keyId = freezed,Object? tags = freezed,Object? alterUrl = freezed,Object? autoConnect = null,Object? jumpId = freezed,Object? custom = freezed,Object? wolCfg = freezed,Object? envs = freezed,Object? id = null,Object? customSystemType = freezed,Object? disabledCmdTypes = freezed,}) {
return _then(_Spi(
name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
as String,ip: null == ip ? _self.ip : ip // ignore: cast_nullable_to_non_nullable
@@ -356,24 +340,11 @@ as WakeOnLanCfg?,envs: freezed == envs ? _self._envs : envs // ignore: cast_null
as Map<String, String>?,id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,customSystemType: freezed == customSystemType ? _self.customSystemType : customSystemType // ignore: cast_nullable_to_non_nullable
as SystemType?,disabledCmdTypes: freezed == disabledCmdTypes ? _self._disabledCmdTypes : disabledCmdTypes // ignore: cast_nullable_to_non_nullable
as List<String>?,proxyCommand: freezed == proxyCommand ? _self.proxyCommand : proxyCommand // ignore: cast_nullable_to_non_nullable
as ProxyCommandConfig?,
as List<String>?,
));
}
/// Create a copy of Spi
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$ProxyCommandConfigCopyWith<$Res>? get proxyCommand {
if (_self.proxyCommand == null) {
return null;
}
return $ProxyCommandConfigCopyWith<$Res>(_self.proxyCommand!, (value) {
return _then(_self.copyWith(proxyCommand: value));
});
}
}
// dart format on

View File

@@ -34,11 +34,6 @@ _Spi _$SpiFromJson(Map<String, dynamic> json) => _Spi(
disabledCmdTypes: (json['disabledCmdTypes'] as List<dynamic>?)
?.map((e) => e as String)
.toList(),
proxyCommand: json['proxyCommand'] == null
? null
: ProxyCommandConfig.fromJson(
json['proxyCommand'] as Map<String, dynamic>,
),
);
Map<String, dynamic> _$SpiToJson(_Spi instance) => <String, dynamic>{
@@ -52,13 +47,12 @@ Map<String, dynamic> _$SpiToJson(_Spi instance) => <String, dynamic>{
'alterUrl': ?instance.alterUrl,
'autoConnect': instance.autoConnect,
'jumpId': ?instance.jumpId,
'custom': ?instance.custom?.toJson(),
'wolCfg': ?instance.wolCfg?.toJson(),
'custom': ?instance.custom,
'wolCfg': ?instance.wolCfg,
'envs': ?instance.envs,
'id': instance.id,
'customSystemType': ?_$SystemTypeEnumMap[instance.customSystemType],
'disabledCmdTypes': ?instance.disabledCmdTypes,
'proxyCommand': ?instance.proxyCommand?.toJson(),
};
const _$SystemTypeEnumMap = {

View File

@@ -378,18 +378,27 @@ void _parseWindowsCpuData(ServerStatusUpdateReq req, Map<String, String> parsedO
// Windows CPU parsing - JSON format from PowerShell
final cpuRaw = WindowsStatusCmdType.cpu.findInMap(parsedOutput);
if (cpuRaw.isNotEmpty && cpuRaw != 'null' && !cpuRaw.contains('error') && !cpuRaw.contains('Exception')) {
final cpus = WindowsParser.parseCpu(cpuRaw, req.ss);
if (cpus.isNotEmpty) {
req.ss.cpu.update(cpus);
final cpuResult = WindowsParser.parseCpu(cpuRaw, req.ss);
if (cpuResult.cores.isNotEmpty) {
req.ss.cpu.update(cpuResult.cores);
final brandRaw = WindowsStatusCmdType.cpuBrand.findInMap(parsedOutput);
if (brandRaw.isNotEmpty && brandRaw != 'null') {
req.ss.cpu.brand.clear();
final brandLines = brandRaw.trim().split('\n');
final uniqueBrands = <String>{};
for (final line in brandLines) {
final trimmedLine = line.trim();
if (trimmedLine.isNotEmpty) {
uniqueBrands.add(trimmedLine);
}
}
if (uniqueBrands.isNotEmpty) {
final brandName = uniqueBrands.first;
req.ss.cpu.brand[brandName] = cpuResult.coreCount;
}
}
}
}
// Windows CPU brand parsing
final brandRaw = WindowsStatusCmdType.cpuBrand.findInMap(parsedOutput);
if (brandRaw.isNotEmpty && brandRaw != 'null') {
req.ss.cpu.brand.clear();
req.ss.cpu.brand[brandRaw.trim()] = 1;
}
} catch (e, s) {
Loggers.app.warning('Windows CPU parsing failed: $e', s);
}
@@ -427,8 +436,11 @@ void _parseWindowsDiskData(ServerStatusUpdateReq req, Map<String, String> parsed
/// Parse Windows uptime data
void _parseWindowsUptimeData(ServerStatusUpdateReq req, Map<String, String> parsedOutput) {
try {
final uptime = WindowsParser.parseUpTime(WindowsStatusCmdType.uptime.findInMap(parsedOutput));
if (uptime != null) {
final uptimeRaw = WindowsStatusCmdType.uptime.findInMap(parsedOutput);
if (uptimeRaw.isNotEmpty && uptimeRaw != 'null') {
// PowerShell now returns pre-formatted uptime string (e.g., "28 days, 5:00" or "5:00")
// No parsing needed - use it directly
final uptime = uptimeRaw.trim();
req.ss.more[StatusCmdType.uptime] = uptime;
}
} catch (e, s) {
@@ -541,38 +553,36 @@ List<NetSpeedPart> _parseWindowsNetwork(String raw, int currentTime) {
final dynamic jsonData = json.decode(raw);
final List<NetSpeedPart> netParts = [];
// PowerShell Get-Counter returns a structure with CounterSamples
if (jsonData is Map && jsonData.containsKey('CounterSamples')) {
final samples = jsonData['CounterSamples'] as List?;
if (samples != null && samples.length >= 2) {
// We need 2 samples to calculate speed (interval between them)
final Map<String, double> interfaceRx = {};
final Map<String, double> interfaceTx = {};
for (final sample in samples) {
final path = sample['Path']?.toString() ?? '';
final cookedValue = sample['CookedValue'] as num? ?? 0;
if (path.contains('Bytes Received/sec')) {
final interfaceName = _extractInterfaceName(path);
if (interfaceName.isNotEmpty) {
interfaceRx[interfaceName] = cookedValue.toDouble();
}
} else if (path.contains('Bytes Sent/sec')) {
final interfaceName = _extractInterfaceName(path);
if (interfaceName.isNotEmpty) {
interfaceTx[interfaceName] = cookedValue.toDouble();
}
}
}
// Create NetSpeedPart for each interface
for (final interfaceName in interfaceRx.keys) {
final rx = interfaceRx[interfaceName] ?? 0;
final tx = interfaceTx[interfaceName] ?? 0;
if (jsonData is List && jsonData.length >= 2) {
var sample1 = jsonData[jsonData.length - 2];
var sample2 = jsonData[jsonData.length - 1];
if (sample1 is Map && sample1.containsKey('value')) {
sample1 = sample1['value'];
}
if (sample2 is Map && sample2.containsKey('value')) {
sample2 = sample2['value'];
}
if (sample1 is List && sample2 is List && sample1.length == sample2.length) {
for (int i = 0; i < sample1.length; i++) {
final s1 = sample1[i];
final s2 = sample2[i];
final name = s1['Name']?.toString() ?? '';
if (name.isEmpty || name == '_Total') continue;
final rx1 = (s1['BytesReceivedPersec'] as num?)?.toDouble() ?? 0;
final rx2 = (s2['BytesReceivedPersec'] as num?)?.toDouble() ?? 0;
final tx1 = (s1['BytesSentPersec'] as num?)?.toDouble() ?? 0;
final tx2 = (s2['BytesSentPersec'] as num?)?.toDouble() ?? 0;
final time1 = (s1['Timestamp_Sys100NS'] as num?)?.toDouble() ?? 0;
final time2 = (s2['Timestamp_Sys100NS'] as num?)?.toDouble() ?? 0;
final timeDelta = (time2 - time1) / 10000000;
if (timeDelta <= 0) continue;
final rxDelta = rx2 - rx1;
final txDelta = tx2 - tx1;
if (rxDelta < 0 || txDelta < 0) continue;
final rxSpeed = rxDelta / timeDelta;
final txSpeed = txDelta / timeDelta;
netParts.add(
NetSpeedPart(interfaceName, BigInt.from(rx.toInt()), BigInt.from(tx.toInt()), currentTime),
NetSpeedPart(name, BigInt.from(rxSpeed.toInt()), BigInt.from(txSpeed.toInt()), currentTime),
);
}
}
@@ -584,53 +594,45 @@ List<NetSpeedPart> _parseWindowsNetwork(String raw, int currentTime) {
}
}
String _extractInterfaceName(String path) {
// Extract interface name from path like
// "\\Computer\\NetworkInterface(Interface Name)\\..."
final match = RegExp(r'\\NetworkInterface\(([^)]+)\)\\').firstMatch(path);
return match?.group(1) ?? '';
}
List<DiskIOPiece> _parseWindowsDiskIO(String raw, int currentTime) {
try {
final dynamic jsonData = json.decode(raw);
final List<DiskIOPiece> diskParts = [];
// PowerShell Get-Counter returns a structure with CounterSamples
if (jsonData is Map && jsonData.containsKey('CounterSamples')) {
final samples = jsonData['CounterSamples'] as List?;
if (samples != null) {
final Map<String, double> diskReads = {};
final Map<String, double> diskWrites = {};
for (final sample in samples) {
final path = sample['Path']?.toString() ?? '';
final cookedValue = sample['CookedValue'] as num? ?? 0;
if (path.contains('Disk Read Bytes/sec')) {
final diskName = _extractDiskName(path);
if (diskName.isNotEmpty) {
diskReads[diskName] = cookedValue.toDouble();
}
} else if (path.contains('Disk Write Bytes/sec')) {
final diskName = _extractDiskName(path);
if (diskName.isNotEmpty) {
diskWrites[diskName] = cookedValue.toDouble();
}
}
}
// Create DiskIOPiece for each disk - convert bytes to sectors
// (assuming 512 bytes per sector)
for (final diskName in diskReads.keys) {
final readBytes = diskReads[diskName] ?? 0;
final writeBytes = diskWrites[diskName] ?? 0;
final sectorsRead = (readBytes / 512).round();
final sectorsWrite = (writeBytes / 512).round();
if (jsonData is List && jsonData.length >= 2) {
var sample1 = jsonData[jsonData.length - 2];
var sample2 = jsonData[jsonData.length - 1];
if (sample1 is Map && sample1.containsKey('value')) {
sample1 = sample1['value'];
}
if (sample2 is Map && sample2.containsKey('value')) {
sample2 = sample2['value'];
}
if (sample1 is List && sample2 is List && sample1.length == sample2.length) {
for (int i = 0; i < sample1.length; i++) {
final s1 = sample1[i];
final s2 = sample2[i];
final name = s1['Name']?.toString() ?? '';
if (name.isEmpty || name == '_Total') continue;
final read1 = (s1['DiskReadBytesPersec'] as num?)?.toDouble() ?? 0;
final read2 = (s2['DiskReadBytesPersec'] as num?)?.toDouble() ?? 0;
final write1 = (s1['DiskWriteBytesPersec'] as num?)?.toDouble() ?? 0;
final write2 = (s2['DiskWriteBytesPersec'] as num?)?.toDouble() ?? 0;
final time1 = (s1['Timestamp_Sys100NS'] as num?)?.toDouble() ?? 0;
final time2 = (s2['Timestamp_Sys100NS'] as num?)?.toDouble() ?? 0;
final timeDelta = (time2 - time1) / 10000000;
if (timeDelta <= 0) continue;
final readDelta = read2 - read1;
final writeDelta = write2 - write1;
if (readDelta < 0 || writeDelta < 0) continue;
final readSpeed = readDelta / timeDelta;
final writeSpeed = writeDelta / timeDelta;
final sectorsRead = (readSpeed / 512).round();
final sectorsWrite = (writeSpeed / 512).round();
diskParts.add(
DiskIOPiece(
dev: diskName,
dev: name,
sectorsRead: sectorsRead,
sectorsWrite: sectorsWrite,
time: currentTime,
@@ -646,13 +648,6 @@ List<DiskIOPiece> _parseWindowsDiskIO(String raw, int currentTime) {
}
}
String _extractDiskName(String path) {
// Extract disk name from path like
// "\\Computer\\PhysicalDisk(Disk Name)\\..."
final match = RegExp(r'\\PhysicalDisk\(([^)]+)\)\\').firstMatch(path);
return match?.group(1) ?? '';
}
void _parseWindowsTemperatures(Temperatures temps, String raw) {
try {
// Handle error output

View File

@@ -37,27 +37,39 @@ class Fifo<T> extends ListBase<T> {
}
}
abstract class TimeSeq<T extends List<TimeSeqIface>> extends Fifo<T> {
abstract class TimeSeq<T extends TimeSeqIface<T>> extends Fifo<List<T>> {
/// Due to the design, at least two elements are required, otherwise [pre] /
/// [now] will throw.
TimeSeq(T init1, T init2, {super.capacity}) : super(list: [init1, init2]);
TimeSeq(List<T> init1, List<T> init2, {super.capacity}) : super(list: [init1, init2]);
T get pre {
List<T> get pre {
return _list[length - 2];
}
T get now {
List<T> get now {
return _list[length - 1];
}
void onUpdate();
void update(T new_) {
void update(List<T> new_) {
add(new_);
if (pre.length != now.length) {
pre.removeWhere((e) => now.any((el) => e.same(el)));
pre.addAll(now.where((e) => pre.every((el) => !e.same(el))));
final previous = pre.toList(growable: false);
final remaining = previous.toList(growable: true);
final aligned = <T>[];
for (final current in now) {
final matchIndex = remaining.indexWhere((item) => item.same(current));
if (matchIndex >= 0) {
aligned.add(remaining.removeAt(matchIndex));
} else {
aligned.add(current);
}
}
_list[length - 2] = aligned;
}
onUpdate();

View File

@@ -7,6 +7,13 @@ import 'package:server_box/data/model/server/disk.dart';
import 'package:server_box/data/model/server/memory.dart';
import 'package:server_box/data/model/server/server.dart';
/// Windows CPU parse result
class WindowsCpuResult {
final List<SingleCpuCore> cores;
final int coreCount;
const WindowsCpuResult(this.cores, this.coreCount);
}
/// Windows-specific status parsing utilities
///
/// This module handles parsing of Windows PowerShell command outputs
@@ -94,30 +101,75 @@ class WindowsParser {
}
/// Parse Windows CPU information from PowerShell output
static List<SingleCpuCore> parseCpu(String raw, ServerStatus serverStatus) {
/// Returns WindowsCpuResult containing CPU cores and total core count
static WindowsCpuResult parseCpu(String raw, ServerStatus serverStatus) {
try {
final dynamic jsonData = json.decode(raw);
final List<SingleCpuCore> cpus = [];
int totalCoreCount = 1;
if (jsonData is List) {
for (int i = 0; i < jsonData.length; i++) {
final cpu = jsonData[i];
final loadPercentage = cpu['LoadPercentage'] ?? 0;
final usage = loadPercentage as int;
// Multiple physical processors
totalCoreCount = 0; // Reset to sum up
var logicalProcessorOffset = 0;
final prevCpus = serverStatus.cpu.now;
for (int procIdx = 0; procIdx < jsonData.length; procIdx++) {
final processor = jsonData[procIdx];
final loadPercentage = (processor['LoadPercentage'] as num?) ?? 0;
final numberOfCores = (processor['NumberOfCores'] as int?) ?? 1;
final numberOfLogicalProcessors = (processor['NumberOfLogicalProcessors'] as int?) ?? numberOfCores;
totalCoreCount += numberOfCores;
final usage = loadPercentage.toInt();
final idle = 100 - usage;
// Get previous CPU data to calculate cumulative values
final prevCpus = serverStatus.cpu.now;
final prevCpu = i < prevCpus.length ? prevCpus[i] : null;
// Create a SingleCpuCore entry for each logical processor
// Windows only reports overall CPU load, so we distribute it evenly
for (int i = 0; i < numberOfLogicalProcessors; i++) {
final coreId = logicalProcessorOffset + i;
// Skip summary entry at index 0 when looking up previous samples
final prevIndex = coreId + 1;
final prevCpu = prevIndex < prevCpus.length ? prevCpus[prevIndex] : null;
// LIMITATION: Windows CPU counters approach
// PowerShell provides LoadPercentage as instantaneous percentage, not cumulative time.
// We simulate cumulative counters by adding current percentages to previous totals.
// This approach has limitations:
// 1. Not as accurate as true cumulative time counters (Linux /proc/stat)
// 2. May drift over time with variable polling intervals
// 3. Results depend on consistent polling frequency
// However, this allows compatibility with existing delta-based CPU calculation logic.
// LIMITATION: Windows CPU counters approach
// PowerShell provides LoadPercentage as instantaneous percentage, not cumulative time.
// We simulate cumulative counters by adding current percentages to previous totals.
// Additionally, Windows only provides overall CPU load, not per-core load.
// We distribute the load evenly across all logical processors.
final newUser = (prevCpu?.user ?? 0) + usage;
final newIdle = (prevCpu?.idle ?? 0) + idle;
cpus.add(
SingleCpuCore(
'cpu$coreId',
newUser, // cumulative user time
0, // sys (not available)
0, // nice (not available)
newIdle, // cumulative idle time
0, // iowait (not available)
0, // irq (not available)
0, // softirq (not available)
),
);
}
logicalProcessorOffset += numberOfLogicalProcessors;
}
} else if (jsonData is Map) {
// Single physical processor
final loadPercentage = (jsonData['LoadPercentage'] as num?) ?? 0;
final numberOfCores = (jsonData['NumberOfCores'] as int?) ?? 1;
final numberOfLogicalProcessors = (jsonData['NumberOfLogicalProcessors'] as int?) ?? numberOfCores;
totalCoreCount = numberOfCores;
final usage = loadPercentage.toInt();
final idle = 100 - usage;
// Create a SingleCpuCore entry for each logical processor
final prevCpus = serverStatus.cpu.now;
for (int i = 0; i < numberOfLogicalProcessors; i++) {
// Skip summary entry at index 0 when looking up previous samples
final prevIndex = i + 1;
final prevCpu = prevIndex < prevCpus.length ? prevCpus[prevIndex] : null;
// LIMITATION: See comment above for Windows CPU counter limitations
final newUser = (prevCpu?.user ?? 0) + usage;
final newIdle = (prevCpu?.idle ?? 0) + idle;
@@ -125,46 +177,43 @@ class WindowsParser {
SingleCpuCore(
'cpu$i',
newUser, // cumulative user time
0, // sys (not available)
0, // nice (not available)
0, // sys
0, // nice
newIdle, // cumulative idle time
0, // iowait (not available)
0, // irq (not available)
0, // softirq (not available)
0, // iowait
0, // irq
0, // softirq
),
);
}
} else if (jsonData is Map) {
// Single CPU core
final loadPercentage = jsonData['LoadPercentage'] ?? 0;
final usage = loadPercentage as int;
final idle = 100 - usage;
// Get previous CPU data to calculate cumulative values
final prevCpus = serverStatus.cpu.now;
final prevCpu = prevCpus.isNotEmpty ? prevCpus[0] : null;
// LIMITATION: See comment above for Windows CPU counter limitations
final newUser = (prevCpu?.user ?? 0) + usage;
final newIdle = (prevCpu?.idle ?? 0) + idle;
cpus.add(
SingleCpuCore(
'cpu0',
newUser, // cumulative user time
0, // sys
0, // nice
newIdle, // cumulative idle time
0, // iowait
0, // irq
0, // softirq
),
);
}
return cpus;
} catch (e) {
return [];
// Add a summary entry at the beginning (like Linux 'cpu' line)
// This is the aggregate of all logical processors
if (cpus.isNotEmpty) {
int totalUser = 0;
int totalIdle = 0;
for (final core in cpus) {
totalUser += core.user;
totalIdle += core.idle;
}
// Insert at the beginning with ID 'cpu' (matching Linux format)
cpus.insert(0, SingleCpuCore(
'cpu', // Summary entry, like Linux
totalUser,
0,
0,
totalIdle,
0,
0,
0,
));
}
return WindowsCpuResult(cpus, totalCoreCount);
} catch (e, s) {
Loggers.app.warning('Windows CPU parsing failed: $e', s);
return WindowsCpuResult([], 1);
}
}

View File

@@ -8,6 +8,7 @@ class SftpReq {
String? privateKey;
Spi? jumpSpi;
String? jumpPrivateKey;
Map<String, String>? knownHostFingerprints;
SftpReq(this.spi, this.remotePath, this.localPath, this.type) {
final keyId = spi.keyId;
@@ -18,6 +19,11 @@ class SftpReq {
jumpSpi = Stores.server.box.get(spi.jumpId);
jumpPrivateKey = Stores.key.fetchOne(jumpSpi?.keyId)?.key;
}
try {
knownHostFingerprints = Map<String, String>.from(Stores.setting.sshKnownHostFingerprints.get());
} catch (_) {
knownHostFingerprints = null;
}
}
}

View File

@@ -65,6 +65,7 @@ Future<void> _download(SftpReq req, SendPort mainSendPort, SendErrorFunction sen
privateKey: req.privateKey,
jumpSpi: req.jumpSpi,
jumpPrivateKey: req.jumpPrivateKey,
knownHostFingerprints: req.knownHostFingerprints,
);
mainSendPort.send(SftpWorkerStatus.sshConnectted);
@@ -121,6 +122,7 @@ Future<void> _upload(SftpReq req, SendPort mainSendPort, SendErrorFunction sendE
privateKey: req.privateKey,
jumpSpi: req.jumpSpi,
jumpPrivateKey: req.jumpPrivateKey,
knownHostFingerprints: req.knownHostFingerprints,
);
mainSendPort.send(SftpWorkerStatus.sshConnectted);

View File

@@ -58,7 +58,7 @@ final class ContainerNotifierProvider
}
}
String _$containerNotifierHash() => r'e6ced8a914631253daabe0de452e0338078cd1d9';
String _$containerNotifierHash() => r'fea65e66499234b0a59bffff8d69c4ab8c93b2fd';
final class ContainerNotifierFamily extends $Family
with

View File

@@ -1,15 +1,14 @@
import 'dart:async';
import 'dart:convert';
import 'package:computer/computer.dart';
import 'package:dartssh2/dartssh2.dart';
import 'package:fl_lib/fl_lib.dart';
import 'package:flutter_gbk2utf8/flutter_gbk2utf8.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:server_box/core/extension/ssh_client.dart';
import 'package:server_box/core/utils/server.dart';
import 'package:server_box/core/utils/ssh_auth.dart';
import 'package:server_box/data/helper/ssh_decoder.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/scripts/script_consts.dart';
@@ -213,7 +212,9 @@ class ServerNotifier extends _$ServerNotifier {
final newStatus = state.status..system = detectedSystemType;
updateStatus(newStatus);
final (_, writeScriptResult) = await state.client!.exec(
Loggers.app.info('Writing script for ${spi.name} (${detectedSystemType.name})');
final (stdoutResult, writeScriptResult) = await state.client!.execSafe(
(session) async {
final scriptRaw = ShellFuncManager.allScript(
spi.custom?.cmds,
@@ -228,10 +229,22 @@ class ServerNotifier extends _$ServerNotifier {
systemType: detectedSystemType,
customDir: spi.custom?.scriptDir,
),
systemType: detectedSystemType,
context: 'WriteScript<${spi.name}>',
);
if (writeScriptResult.isNotEmpty && detectedSystemType != SystemType.windows) {
ShellFuncManager.switchScriptDir(spi.id, systemType: detectedSystemType);
throw writeScriptResult;
if (stdoutResult.isNotEmpty) {
Loggers.app.info('Script write stdout for ${spi.name}: $stdoutResult');
}
if (writeScriptResult.isNotEmpty) {
Loggers.app.warning('Script write stderr for ${spi.name}: $writeScriptResult');
if (detectedSystemType != SystemType.windows) {
ShellFuncManager.switchScriptDir(spi.id, systemType: detectedSystemType);
throw writeScriptResult;
}
} else {
Loggers.app.info('Script written successfully for ${spi.name}');
}
} on SSHAuthAbortError catch (e) {
TryLimiter.inc(sid);
@@ -278,43 +291,25 @@ class ServerNotifier extends _$ServerNotifier {
String? raw;
try {
final execResult = await state.client?.run(
ShellFunc.status.exec(spi.id, systemType: state.status.system, customDir: spi.custom?.scriptDir),
);
final statusCmd = ShellFunc.status.exec(spi.id, systemType: state.status.system, customDir: spi.custom?.scriptDir);
Loggers.app.info('Running status command for ${spi.name} (${state.status.system.name}): $statusCmd');
final execResult = await state.client?.run(statusCmd);
if (execResult != null) {
String? rawStr;
bool needGbk = false;
try {
rawStr = utf8.decode(execResult, allowMalformed: true);
// If there are unparseable characters, try fallback to GBK decoding
if (rawStr.contains('<EFBFBD>')) {
Loggers.app.warning('UTF8 decoding failed, use GBK decoding');
needGbk = true;
}
} catch (e) {
Loggers.app.warning('UTF8 decoding failed, use GBK decoding', e);
needGbk = true;
}
if (needGbk) {
try {
rawStr = gbk.decode(execResult);
} catch (e2) {
Loggers.app.warning('GBK decoding failed', e2);
rawStr = null;
}
}
if (rawStr == null) {
Loggers.app.warning('Decoding failed, execResult: $execResult');
}
raw = rawStr;
raw = SSHDecoder.decode(
execResult,
isWindows: state.status.system == SystemType.windows,
context: 'GetStatus<${spi.name}>',
);
Loggers.app.info('Status response length for ${spi.name}: ${raw.length} bytes');
} else {
raw = execResult.toString();
raw = '';
Loggers.app.warning('No status result from ${spi.name}');
}
if (raw == null || raw.isEmpty) {
if (raw.isEmpty) {
TryLimiter.inc(sid);
final newStatus = state.status
..err = SSHErr(type: SSHErrType.segements, message: 'decode or split failed, raw:\n$raw');
..err = SSHErr(type: SSHErrType.segements, message: 'Empty response from server');
updateStatus(newStatus);
updateConnection(ServerConn.failed);
@@ -324,7 +319,7 @@ class ServerNotifier extends _$ServerNotifier {
}
segments = raw.split(ScriptConstants.separator).map((e) => e.trim()).toList();
if (raw.isEmpty || segments.isEmpty) {
if (segments.isEmpty) {
if (Stores.setting.keepStatusWhenErr.fetch()) {
// Keep previous server status when error occurs
if (state.conn != ServerConn.failed && state.status.more.isNotEmpty) {
@@ -333,7 +328,7 @@ class ServerNotifier extends _$ServerNotifier {
}
TryLimiter.inc(sid);
final newStatus = state.status
..err = SSHErr(type: SSHErrType.segements, message: 'Seperate segments failed, raw:\n$raw');
..err = SSHErr(type: SSHErrType.segements, message: 'Separate segments failed, raw:\n$raw');
updateStatus(newStatus);
updateConnection(ServerConn.failed);

View File

@@ -3,6 +3,6 @@
abstract class BuildData {
static const String name = "ServerBox";
static const int build = 1270;
static const int script = 69;
static const int build = 1276;
static const int script = 70;
}

View File

@@ -228,10 +228,6 @@ class SettingStore extends HiveStore {
late final betaTest = propertyDefault('betaTest', false);
late final proxyCmdCustomExecs = listProperty('proxyCmdCustomExecs');
late final proxyCmdCustomPresets = listProperty('proxyCmdCustomPresets');
/// For desktop only.
/// Record the position and size of the window.
late final windowState = property<WindowState>(

View File

@@ -3,7 +3,6 @@ import 'package:server_box/data/model/app/menu/server_func.dart';
import 'package:server_box/data/model/app/net_view.dart';
import 'package:server_box/data/model/server/custom.dart';
import 'package:server_box/data/model/server/private_key_info.dart';
import 'package:server_box/data/model/server/proxy_command_config.dart';
import 'package:server_box/data/model/server/server_private_info.dart';
import 'package:server_box/data/model/server/snippet.dart';
import 'package:server_box/data/model/server/system.dart';
@@ -20,6 +19,5 @@ import 'package:server_box/data/model/ssh/virtual_key.dart';
AdapterSpec<ServerCustom>(),
AdapterSpec<WakeOnLanCfg>(),
AdapterSpec<SystemType>(),
AdapterSpec<ProxyCommandConfig>(),
])
part 'hive_adapters.g.dart';

View File

@@ -113,14 +113,13 @@ class SpiAdapter extends TypeAdapter<Spi> {
id: fields[13] == null ? '' : fields[13] as String,
customSystemType: fields[14] as SystemType?,
disabledCmdTypes: (fields[15] as List?)?.cast<String>(),
proxyCommand: fields[16] as ProxyCommandConfig?,
);
}
@override
void write(BinaryWriter writer, Spi obj) {
writer
..writeByte(17)
..writeByte(16)
..writeByte(0)
..write(obj.name)
..writeByte(1)
@@ -152,9 +151,7 @@ class SpiAdapter extends TypeAdapter<Spi> {
..writeByte(14)
..write(obj.customSystemType)
..writeByte(15)
..write(obj.disabledCmdTypes)
..writeByte(16)
..write(obj.proxyCommand);
..write(obj.disabledCmdTypes);
}
@override
@@ -607,66 +604,3 @@ class SystemTypeAdapter extends TypeAdapter<SystemType> {
runtimeType == other.runtimeType &&
typeId == other.typeId;
}
class ProxyCommandConfigAdapter extends TypeAdapter<ProxyCommandConfig> {
@override
final typeId = 10;
@override
ProxyCommandConfig read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return ProxyCommandConfig(
command: fields[0] as String,
args: (fields[1] as List?)?.cast<String>(),
workingDirectory: fields[2] as String?,
environment: (fields[3] as Map?)?.cast<String, String>(),
timeout: fields[4] == null
? const Duration(seconds: 30)
: fields[4] as Duration,
retryOnFailure: fields[5] == null ? false : fields[5] as bool,
maxRetries: fields[6] == null ? 3 : (fields[6] as num).toInt(),
requiresExecutable: fields[7] == null ? false : fields[7] as bool,
executableName: fields[8] as String?,
executableDownloadUrl: fields[9] as String?,
);
}
@override
void write(BinaryWriter writer, ProxyCommandConfig obj) {
writer
..writeByte(10)
..writeByte(0)
..write(obj.command)
..writeByte(1)
..write(obj.args)
..writeByte(2)
..write(obj.workingDirectory)
..writeByte(3)
..write(obj.environment)
..writeByte(4)
..write(obj.timeout)
..writeByte(5)
..write(obj.retryOnFailure)
..writeByte(6)
..write(obj.maxRetries)
..writeByte(7)
..write(obj.requiresExecutable)
..writeByte(8)
..write(obj.executableName)
..writeByte(9)
..write(obj.executableDownloadUrl);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is ProxyCommandConfigAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@@ -1,7 +1,7 @@
# Generated by Hive CE
# Manual modifications may be necessary for certain migrations
# Check in to version control
nextTypeId: 11
nextTypeId: 10
types:
PrivateKeyInfo:
typeId: 1
@@ -27,7 +27,7 @@ types:
index: 4
Spi:
typeId: 3
nextIndex: 17
nextIndex: 16
fields:
name:
index: 0
@@ -61,8 +61,6 @@ types:
index: 14
disabledCmdTypes:
index: 15
proxyCommand:
index: 16
VirtKey:
typeId: 4
nextIndex: 45
@@ -223,27 +221,3 @@ types:
index: 1
windows:
index: 2
ProxyCommandConfig:
typeId: 10
nextIndex: 10
fields:
command:
index: 0
args:
index: 1
workingDirectory:
index: 2
environment:
index: 3
timeout:
index: 4
retryOnFailure:
index: 5
maxRetries:
index: 6
requiresExecutable:
index: 7
executableName:
index: 8
executableDownloadUrl:
index: 9

View File

@@ -14,7 +14,6 @@ extension HiveRegistrar on HiveInterface {
registerAdapter(ConnectionStatAdapter());
registerAdapter(NetViewTypeAdapter());
registerAdapter(PrivateKeyInfoAdapter());
registerAdapter(ProxyCommandConfigAdapter());
registerAdapter(ServerConnectionStatsAdapter());
registerAdapter(ServerCustomAdapter());
registerAdapter(ServerFuncBtnAdapter());
@@ -33,7 +32,6 @@ extension IsolatedHiveRegistrar on IsolatedHiveInterface {
registerAdapter(ConnectionStatAdapter());
registerAdapter(NetViewTypeAdapter());
registerAdapter(PrivateKeyInfoAdapter());
registerAdapter(ProxyCommandConfigAdapter());
registerAdapter(ServerConnectionStatsAdapter());
registerAdapter(ServerCustomAdapter());
registerAdapter(ServerFuncBtnAdapter());

View File

@@ -10,7 +10,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hive_ce_flutter/hive_flutter.dart';
import 'package:logging/logging.dart';
import 'package:server_box/app.dart';
import 'package:server_box/core/utils/executable_manager.dart';
import 'package:server_box/data/model/app/menu/server_func.dart';
import 'package:server_box/data/model/app/server_detail_card.dart';
import 'package:server_box/data/res/build_data.dart';
@@ -58,9 +57,6 @@ Future<void> _initData() async {
await PrefStore.shared.init(); // Call this before accessing any store
await Stores.init();
// Initialize executable manager
await ExecutableManager.initialize();
// It may effect the following logic, so await it.
// DO DB migration before load any provider.
await _doDbMigrate();

View File

@@ -265,54 +265,6 @@ extension _Actions on _ServerEditPageState {
}
}
// ProxyCommand configuration
ProxyCommandConfig? proxyCommand;
if (!Platform.isIOS && _proxyCommandEnabled.value) {
final command = _proxyCommandController.text.trim();
if (command.isEmpty) {
context.showSnackBar('ProxyCommand is enabled but command is empty');
return;
}
// Check if command contains required placeholders
if (!command.contains('%h')) {
context.showSnackBar('ProxyCommand must contain %h (hostname) placeholder');
return;
}
List<String> tokens;
try {
tokens = ProxyCommandExecutor.tokenizeCommand(command);
} on ProxyCommandException catch (e) {
context.showSnackBar(e.message);
return;
}
if (tokens.isEmpty) {
context.showSnackBar('ProxyCommand must not be empty');
return;
}
// Determine if this requires an executable
final executableToken = tokens.first;
var normalized = p.basename(executableToken).toLowerCase();
if (normalized.endsWith('.exe')) {
normalized = normalized.substring(0, normalized.length - 4);
}
const builtinExecutables = {'ssh', 'nc', 'socat'};
final requiresExecutable = !builtinExecutables.contains(normalized);
proxyCommand = ProxyCommandConfig(
command: command,
timeout: Duration(seconds: _proxyCommandTimeout.value),
retryOnFailure: true,
requiresExecutable: requiresExecutable,
executableName: requiresExecutable ? executableToken : null,
);
} else if (Platform.isIOS && _proxyCommandEnabled.value) {
context.showSnackBar('ProxyCommand is not supported on iOS');
return;
}
final spi = Spi(
name: _nameController.text.isEmpty ? _ipController.text : _nameController.text,
ip: _ipController.text,
@@ -332,7 +284,6 @@ extension _Actions on _ServerEditPageState {
id: widget.args?.spi.id ?? ShortId.generate(),
customSystemType: _systemType.value,
disabledCmdTypes: _disabledCmdTypes.value.isEmpty ? null : _disabledCmdTypes.value.toList(),
proxyCommand: proxyCommand,
);
if (this.spi == null) {
@@ -499,34 +450,5 @@ extension _Utils on _ServerEditPageState {
final allAvailableCmdTypes = ShellCmdType.all.map((e) => e.displayName);
disabledCmdTypes.removeWhere((e) => !allAvailableCmdTypes.contains(e));
_disabledCmdTypes.value = disabledCmdTypes;
// Load ProxyCommand configuration
final proxyCommand = spi.proxyCommand;
if (proxyCommand != null && !Platform.isIOS) {
_proxyCommandEnabled.value = true;
_proxyCommandController.text = proxyCommand.command;
_proxyCommandTimeout.value = proxyCommand.timeout.inSeconds;
// Try to match with a preset
final presets = ProxyCommandExecutor.getPresets();
for (final entry in presets.entries) {
if (entry.value.command == proxyCommand.command) {
_proxyCommandPreset.value = entry.key;
break;
}
}
} else {
_proxyCommandEnabled.value = false;
_proxyCommandController.text = '';
_proxyCommandTimeout.value = 30;
_proxyCommandPreset.value = null;
}
if (Platform.isIOS) {
_proxyCommandEnabled.value = false;
_proxyCommandController.text = '';
_proxyCommandTimeout.value = 30;
_proxyCommandPreset.value = null;
}
}
}

View File

@@ -7,16 +7,13 @@ import 'package:fl_lib/fl_lib.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:icons_plus/icons_plus.dart';
import 'package:path/path.dart' as p;
import 'package:server_box/core/extension/context/locale.dart';
import 'package:server_box/core/route.dart';
import 'package:server_box/core/utils/proxy_command_executor.dart';
import 'package:server_box/core/utils/server_dedup.dart';
import 'package:server_box/core/utils/ssh_config.dart';
import 'package:server_box/data/model/app/scripts/cmd_types.dart';
import 'package:server_box/data/model/server/custom.dart';
import 'package:server_box/data/model/server/discovery_result.dart';
import 'package:server_box/data/model/server/proxy_command_config.dart';
import 'package:server_box/data/model/server/server_private_info.dart';
import 'package:server_box/data/model/server/system.dart';
import 'package:server_box/data/model/server/wol_cfg.dart';
@@ -77,12 +74,6 @@ class _ServerEditPageState extends ConsumerState<ServerEditPage> with AfterLayou
final _systemType = ValueNotifier<SystemType?>(null);
final _disabledCmdTypes = <String>{}.vn;
// ProxyCommand fields
final _proxyCommandEnabled = ValueNotifier(false);
final _proxyCommandController = TextEditingController();
final _proxyCommandPreset = nvn<String>();
final _proxyCommandTimeout = ValueNotifier(30);
@override
void dispose() {
super.dispose();
@@ -116,11 +107,6 @@ class _ServerEditPageState extends ConsumerState<ServerEditPage> with AfterLayou
_tags.dispose();
_systemType.dispose();
_disabledCmdTypes.dispose();
_proxyCommandEnabled.dispose();
_proxyCommandController.dispose();
_proxyCommandPreset.dispose();
_proxyCommandTimeout.dispose();
}
@override
@@ -214,7 +200,6 @@ class _ServerEditPageState extends ConsumerState<ServerEditPage> with AfterLayou
_buildAuth(),
_buildSystemType(),
_buildJumpServer(),
_buildProxyCommand(),
_buildMore(),
];
return AutoMultiList(children: children);

View File

@@ -449,123 +449,6 @@ extension _Widgets on _ServerEditPageState {
);
}
Widget _buildProxyCommand() {
if (Platform.isIOS) {
return ListTile(
title: const Text('ProxyCommand'),
subtitle: const Text('ProxyCommand is not available on iOS'),
trailing: const Icon(Icons.block, color: Colors.grey),
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ListTile(
title: Text('ProxyCommand'),
subtitle: Text('Use a proxy command for SSH connection'),
trailing: _proxyCommandEnabled.listenVal(
(enabled) => Switch(
value: enabled,
onChanged: (value) {
_proxyCommandEnabled.value = value;
if (value && _proxyCommandController.text.isEmpty) {
// Set default preset when enabled
_proxyCommandPreset.value = 'cloudflare_access';
_proxyCommandController.text = 'cloudflared access ssh --hostname %h';
}
},
),
),
),
_proxyCommandEnabled.listenVal((enabled) {
if (!enabled) return const SizedBox.shrink();
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Preset selection
Text('Presets:', style: Theme.of(context).textTheme.titleSmall),
const SizedBox(height: 8),
_proxyCommandPreset.listenVal((preset) {
final presets = ProxyCommandExecutor.getPresets();
return Wrap(
spacing: 8,
runSpacing: 8,
children: presets.entries.map((entry) {
final isSelected = preset == entry.key;
return FilterChip(
label: Text(_getPresetDisplayName(entry.key)),
selected: isSelected,
onSelected: (selected) {
if (selected) {
_proxyCommandPreset.value = entry.key;
_proxyCommandController.text = entry.value.command;
}
},
);
}).toList(),
);
}),
const SizedBox(height: 16),
// Custom command input
Input(
controller: _proxyCommandController,
type: TextInputType.text,
label: 'Proxy Command',
icon: Icons.settings_ethernet,
hint: 'e.g., cloudflared access ssh --hostname %h',
suggestion: false,
),
const SizedBox(height: 8),
// Help text
Text(
'Available placeholders:\n'
'• %h - hostname\n'
'• %p - port\n'
'• %r - username',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Colors.grey[600],
),
),
const SizedBox(height: 16),
// Timeout setting
_proxyCommandTimeout.listenVal((timeout) {
return ListTile(
title: Text('Connection Timeout'),
subtitle: Text('$timeout seconds'),
trailing: DropdownButton<int>(
value: timeout,
items: [10, 30, 60, 120].map((seconds) {
return DropdownMenuItem(
value: seconds,
child: Text('${seconds}s'),
);
}).toList(),
onChanged: (value) {
if (value != null) {
_proxyCommandTimeout.value = value;
}
},
),
);
}),
],
),
);
}),
],
);
}
Widget _buildDelBtn() {
return IconButton(
onPressed: () {
@@ -589,21 +472,4 @@ extension _Widgets on _ServerEditPageState {
icon: const Icon(Icons.delete),
);
}
String _getPresetDisplayName(String presetKey) {
switch (presetKey) {
case 'cloudflare_access':
return 'Cloudflare Access';
case 'ssh_via_bastion':
return 'SSH via Bastion';
case 'nc_netcat':
return 'Netcat';
case 'socat':
return 'Socat';
default:
return presetKey.split('_').map((word) =>
word[0].toUpperCase() + word.substring(1)
).join(' ');
}
}
}

View File

@@ -88,14 +88,14 @@ SPEC CHECKSUMS:
flutter_secure_storage_macos: 7f45e30f838cf2659862a4e4e3ee1c347c2b3b54
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
icloud_storage: eb5b0f20687cf5a4fabc0b541f3b079cd6df7dcb
local_auth_darwin: d2e8c53ef0c4f43c646462e3415432c4dab3ae19
local_auth_darwin: c3ee6cce0a8d56be34c8ccb66ba31f7f180aaebb
package_info_plus: f0052d280d17aa382b932f399edf32507174e870
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
screen_retriever_macos: 452e51764a9e1cdb74b3c541238795849f21557f
share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673
wakelock_plus: 21ddc249ac4b8d018838dbdabd65c5976c308497
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
url_launcher_macos: f87a979182d112f911de6820aefddaf56ee9fbfd
wakelock_plus: 917609be14d812ddd9e9528876538b2263aaa03b
window_manager: b729e31d38fb04905235df9ea896128991cad99e
PODFILE CHECKSUM: 8cdf29216ea1ab6b9743188287968d22b4579c1d

View File

@@ -471,7 +471,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1270;
CURRENT_PROJECT_VERSION = 1276;
DEVELOPMENT_TEAM = BA88US33G6;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Server Box";
@@ -481,7 +481,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.15;
MARKETING_VERSION = 1.0.1270;
MARKETING_VERSION = 1.0.1276;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
PRODUCT_NAME = "Server Box";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -608,7 +608,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1270;
CURRENT_PROJECT_VERSION = 1276;
DEVELOPMENT_TEAM = BA88US33G6;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Server Box";
@@ -618,7 +618,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.15;
MARKETING_VERSION = 1.0.1270;
MARKETING_VERSION = 1.0.1276;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
PRODUCT_NAME = "Server Box";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -638,7 +638,7 @@
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "3rd Party Mac Developer Application";
CODE_SIGN_STYLE = Manual;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1270;
CURRENT_PROJECT_VERSION = 1276;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=macosx*]" = BA88US33G6;
INFOPLIST_FILE = Runner/Info.plist;
@@ -649,7 +649,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.15;
MARKETING_VERSION = 1.0.1270;
MARKETING_VERSION = 1.0.1276;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
PRODUCT_NAME = "Server Box";
PROVISIONING_PROFILE_SPECIFIER = "";

View File

@@ -5,42 +5,42 @@ packages:
dependency: transitive
description:
name: _fe_analyzer_shared
sha256: da0d9209ca76bde579f2da330aeb9df62b6319c834fa7baae052021b0462401f
sha256: f0bb5d1648339c8308cc0b9838d8456b3cfe5c91f9dc1a735b4d003269e5da9a
url: "https://pub.dev"
source: hosted
version: "85.0.0"
version: "88.0.0"
analyzer:
dependency: "direct dev"
description:
name: analyzer
sha256: f4ad0fea5f102201015c9aae9d93bc02f75dd9491529a8c21f88d17a8523d44c
sha256: "0b7b9c329d2879f8f05d6c05b32ee9ec025f39b077864bdb5ac9a7b63418a98f"
url: "https://pub.dev"
source: hosted
version: "7.6.0"
version: "8.1.1"
analyzer_buffer:
dependency: transitive
description:
name: analyzer_buffer
sha256: f7833bee67c03c37241c67f8741b17cc501b69d9758df7a5a4a13ed6c947be43
sha256: aba2f75e63b3135fd1efaa8b6abefe1aa6e41b6bd9806221620fa48f98156033
url: "https://pub.dev"
source: hosted
version: "0.1.10"
version: "0.1.11"
analyzer_plugin:
dependency: transitive
description:
name: analyzer_plugin
sha256: a5ab7590c27b779f3d4de67f31c4109dbe13dd7339f86461a6f2a8ab2594d8ce
sha256: dd574a0ab77de88b7d9c12bc4b626109a5ca9078216a79041a5c24c3a1bd103c
url: "https://pub.dev"
source: hosted
version: "0.13.4"
version: "0.13.7"
animations:
dependency: transitive
description:
name: animations
sha256: d3d6dcfb218225bbe68e87ccf6378bbb2e32a94900722c5f81611dad089911cb
sha256: a8031b276f0a7986ac907195f10ca7cd04ecf2a8a566bd6dbe03018a9b02b427
url: "https://pub.dev"
source: hosted
version: "2.0.11"
version: "2.1.0"
ansicolor:
dependency: transitive
description:
@@ -125,50 +125,34 @@ packages:
dependency: transitive
description:
name: build
sha256: "7d95cbbb1526ab5ae977df9b4cc660963b9b27f6d1075c0b34653868911385e4"
sha256: dfb67ccc9a78c642193e0c2d94cb9e48c2c818b3178a86097d644acdcde6a8d9
url: "https://pub.dev"
source: hosted
version: "3.0.0"
version: "4.0.2"
build_config:
dependency: transitive
description:
name: build_config
sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33"
sha256: "4f64382b97504dc2fcdf487d5aae33418e08b4703fc21249e4db6d804a4d0187"
url: "https://pub.dev"
source: hosted
version: "1.1.2"
version: "1.2.0"
build_daemon:
dependency: transitive
description:
name: build_daemon
sha256: "8e928697a82be082206edb0b9c99c5a4ad6bc31c9e9b8b2f291ae65cd4a25daa"
sha256: bf05f6e12cfea92d3c09308d7bcdab1906cd8a179b023269eed00c071004b957
url: "https://pub.dev"
source: hosted
version: "4.0.4"
build_resolvers:
dependency: transitive
description:
name: build_resolvers
sha256: "38c9c339333a09b090a638849a4c56e70a404c6bdd3b511493addfbc113b60c2"
url: "https://pub.dev"
source: hosted
version: "3.0.0"
version: "4.1.1"
build_runner:
dependency: "direct dev"
description:
name: build_runner
sha256: b971d4a1c789eba7be3e6fe6ce5e5b50fd3719e3cb485b3fad6d04358304351d
sha256: "04f69b1502f66e22ae7990bbd01eb552b7f12793c4d3ea6e715d0ac5e98bcdac"
url: "https://pub.dev"
source: hosted
version: "2.6.0"
build_runner_core:
dependency: transitive
description:
name: build_runner_core
sha256: c04e612ca801cd0928ccdb891c263a2b1391cb27940a5ea5afcf9ba894de5d62
url: "https://pub.dev"
source: hosted
version: "9.2.0"
version: "2.10.2"
built_collection:
dependency: transitive
description:
@@ -181,42 +165,42 @@ packages:
dependency: transitive
description:
name: built_value
sha256: ba95c961bafcd8686d1cf63be864eb59447e795e124d98d6a27d91fcd13602fb
sha256: a30f0a0e38671e89a492c44d005b5545b830a961575bbd8336d42869ff71066d
url: "https://pub.dev"
source: hosted
version: "8.11.1"
version: "8.12.0"
camera:
dependency: transitive
description:
name: camera
sha256: d6ec2cbdbe2fa8f5e0d07d8c06368fe4effa985a4a5ddade9cc58a8cd849557d
sha256: eefad89f262a873f38d21e5eec853461737ea074d7c9ede39f3ceb135d201cab
url: "https://pub.dev"
source: hosted
version: "0.11.2"
version: "0.11.3"
camera_android_camerax:
dependency: transitive
description:
name: camera_android_camerax
sha256: "2d438248554f44766bf9ea34c117a5bb0074e241342ef7c22c768fb431335234"
sha256: d5256612833f9169c1698599a87370490622a188c5a7fb601169bb7b2f41f22b
url: "https://pub.dev"
source: hosted
version: "0.6.21"
version: "0.6.24+1"
camera_avfoundation:
dependency: transitive
description:
name: camera_avfoundation
sha256: "951ef122d01ebba68b7a54bfe294e8b25585635a90465c311b2f875ae72c412f"
sha256: "34bcd5db30e52414f1f0783c5e3f566909fab14141a21b3b576c78bd35382bf6"
url: "https://pub.dev"
source: hosted
version: "0.9.21+2"
version: "0.9.22+4"
camera_platform_interface:
dependency: transitive
description:
name: camera_platform_interface
sha256: "2f757024a48696ff4814a789b0bd90f5660c0fb25f393ab4564fb483327930e2"
sha256: "98cfc9357e04bad617671b4c1f78a597f25f08003089dd94050709ae54effc63"
url: "https://pub.dev"
source: hosted
version: "2.10.0"
version: "2.12.0"
camera_web:
dependency: transitive
description:
@@ -286,10 +270,10 @@ packages:
dependency: transitive
description:
name: code_builder
sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e"
sha256: "11654819532ba94c34de52ff5feb52bd81cba1de00ef2ed622fd50295f9d4243"
url: "https://pub.dev"
source: hosted
version: "4.10.1"
version: "4.11.0"
collection:
dependency: transitive
description:
@@ -327,18 +311,18 @@ packages:
dependency: transitive
description:
name: cross_file
sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670"
sha256: "942a4791cd385a68ccb3b32c71c427aba508a1bb949b86dff2adbe4049f16239"
url: "https://pub.dev"
source: hosted
version: "0.3.4+2"
version: "0.3.5"
crypto:
dependency: "direct main"
description:
name: crypto
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
url: "https://pub.dev"
source: hosted
version: "3.0.6"
version: "3.0.7"
csslib:
dependency: transitive
description:
@@ -351,26 +335,26 @@ packages:
dependency: transitive
description:
name: custom_lint_core
sha256: cc4684d22ca05bf0a4a51127e19a8aea576b42079ed2bc9e956f11aaebe35dd1
sha256: "85b339346154d5646952d44d682965dfe9e12cae5febd706f0db3aa5010d6423"
url: "https://pub.dev"
source: hosted
version: "0.8.0"
version: "0.8.1"
custom_lint_visitor:
dependency: transitive
description:
name: custom_lint_visitor
sha256: "4a86a0d8415a91fbb8298d6ef03e9034dc8e323a599ddc4120a0e36c433983a2"
sha256: "446d68322747ec1c36797090de776aa72228818d3d80685a91ff524d163fee6d"
url: "https://pub.dev"
source: hosted
version: "1.0.0+7.7.0"
version: "1.0.0+8.1.1"
dart_style:
dependency: transitive
description:
name: dart_style
sha256: "8a0e5fba27e8ee025d2ffb4ee820b4e6e2cf5e4246a6b1a477eb66866947e0bb"
sha256: c87dfe3d56f183ffe9106a18aebc6db431fc7c98c31a54b952a77f3d54a85697
url: "https://pub.dev"
source: hosted
version: "3.1.1"
version: "3.1.2"
dartssh2:
dependency: "direct main"
description:
@@ -472,10 +456,10 @@ packages:
dependency: "direct main"
description:
name: file_picker
sha256: e7e16c9d15c36330b94ca0e2ad8cb61f93cd5282d0158c09805aed13b5452f22
sha256: f8f4ea435f791ab1f817b4e338ed958cb3d04ba43d6736ffc39958d950754967
url: "https://pub.dev"
source: hosted
version: "10.3.2"
version: "10.3.6"
fixnum:
dependency: transitive
description:
@@ -497,10 +481,10 @@ packages:
dependency: "direct main"
description:
name: fl_chart
sha256: d3f82f4a38e33ba23d05a08ff304d7d8b22d2a59a5503f20bd802966e915db89
sha256: "7ca9a40f4eb85949190e54087be8b4d6ac09dc4c54238d782a34cf1f7c011de9"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
version: "1.1.1"
fl_lib:
dependency: "direct main"
description:
@@ -580,26 +564,26 @@ packages:
dependency: "direct dev"
description:
name: flutter_native_splash
sha256: "8321a6d11a8d13977fa780c89de8d257cce3d841eecfb7a4cadffcc4f12d82dc"
sha256: "4fb9f4113350d3a80841ce05ebf1976a36de622af7d19aca0ca9a9911c7ff002"
url: "https://pub.dev"
source: hosted
version: "2.4.6"
version: "2.4.7"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
sha256: b0694b7fb1689b0e6cc193b3f1fcac6423c4f93c74fb20b806c6b6f196db0c31
sha256: "306f0596590e077338312f38837f595c04f28d6cdeeac392d3d74df2f0003687"
url: "https://pub.dev"
source: hosted
version: "2.0.30"
version: "2.0.32"
flutter_riverpod:
dependency: "direct main"
description:
name: flutter_riverpod
sha256: "71a420767ae786f9402e4efb74e1119fa95b53e6a3781ab8ab21c121ac1349c6"
sha256: "9e2d6907f12cc7d23a846847615941bddee8709bf2bfd274acdf5e80bcf22fde"
url: "https://pub.dev"
source: hosted
version: "3.0.1"
version: "3.0.3"
flutter_secure_storage:
dependency: transitive
description:
@@ -660,10 +644,10 @@ packages:
dependency: transitive
description:
name: flutter_svg
sha256: cd57f7969b4679317c17af6fd16ee233c1e60a82ed209d8a475c54fd6fd6f845
sha256: "055de8921be7b8e8b98a233c7a5ef84b3a6fcc32f46f1ebf5b9bb3576d108355"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
version: "2.2.2"
flutter_test:
dependency: "direct dev"
description: flutter
@@ -702,10 +686,10 @@ packages:
dependency: "direct main"
description:
name: get_it
sha256: a4292e7cf67193f8e7c1258203104eb2a51ec8b3a04baa14695f4064c144297b
sha256: "84792561b731b6463d053e9761a5236da967c369da10b134b8585a5e18429956"
url: "https://pub.dev"
source: hosted
version: "8.2.0"
version: "9.0.5"
glob:
dependency: transitive
description:
@@ -743,26 +727,26 @@ packages:
dependency: transitive
description:
name: hive_ce
sha256: "708bb39050998707c5d422752159f91944d3c81ab42d80e1bd0ee37d8e130658"
sha256: "81d39a03c4c0ba5938260a8c3547d2e71af59defecea21793d57fc3551f0d230"
url: "https://pub.dev"
source: hosted
version: "2.11.3"
version: "2.15.1"
hive_ce_flutter:
dependency: "direct main"
description:
name: hive_ce_flutter
sha256: f5bd57fda84402bca7557fedb8c629c96c8ea10fab4a542968d7b60864ca02cc
sha256: "26d656c9e8974f0732f1d09020e2d7b08ba841b8961a02dbfb6caf01474b0e9a"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
version: "2.3.3"
hive_ce_generator:
dependency: "direct dev"
description:
name: hive_ce_generator
sha256: a169feeff2da9cc2c417ce5ae9bcebf7c8a95d7a700492b276909016ad70a786
sha256: b19ac263cb37529513508ba47352c41e6de72ba879952898d9c18c9c8a955921
url: "https://pub.dev"
source: hosted
version: "1.9.3"
version: "1.10.0"
html:
dependency: transitive
description:
@@ -775,10 +759,10 @@ packages:
dependency: transitive
description:
name: http
sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
url: "https://pub.dev"
source: hosted
version: "1.5.0"
version: "1.6.0"
http_client_helper:
dependency: transitive
description:
@@ -895,10 +879,10 @@ packages:
dependency: transitive
description:
name: leak_tracker
sha256: "8dcda04c3fc16c14f48a7bb586d4be1f0d1572731b6d81d51772ef47c02081e0"
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
url: "https://pub.dev"
source: hosted
version: "11.0.1"
version: "11.0.2"
leak_tracker_flutter_testing:
dependency: transitive
description:
@@ -935,26 +919,26 @@ packages:
dependency: transitive
description:
name: local_auth_android
sha256: "48924f4a8b3cc45994ad5993e2e232d3b00788a305c1bf1c7db32cef281ce9a3"
sha256: a0bdfcc0607050a26ef5b31d6b4b254581c3d3ce3c1816ab4d4f4a9173e84467
url: "https://pub.dev"
source: hosted
version: "1.0.52"
version: "1.0.56"
local_auth_darwin:
dependency: transitive
description:
name: local_auth_darwin
sha256: "0e9706a8543a4a2eee60346294d6a633dd7c3ee60fae6b752570457c4ff32055"
sha256: "699873970067a40ef2f2c09b4c72eb1cfef64224ef041b3df9fdc5c4c1f91f49"
url: "https://pub.dev"
source: hosted
version: "1.6.0"
version: "1.6.1"
local_auth_platform_interface:
dependency: transitive
description:
name: local_auth_platform_interface
sha256: "1b842ff177a7068442eae093b64abe3592f816afd2a533c0ebcdbe40f9d2075a"
sha256: f98b8e388588583d3f781f6806e4f4c9f9e189d898d27f0c249b93a1973dd122
url: "https://pub.dev"
source: hosted
version: "1.0.10"
version: "1.1.0"
local_auth_windows:
dependency: transitive
description:
@@ -1023,10 +1007,10 @@ packages:
dependency: transitive
description:
name: mockito
sha256: "2314cbe9165bcd16106513df9cf3c3224713087f09723b128928dc11a4379f99"
sha256: "4feb43bc4eb6c03e832f5fcd637d1abb44b98f9cfa245c58e27382f58859f8f6"
url: "https://pub.dev"
source: hosted
version: "5.5.0"
version: "5.5.1"
multi_split_view:
dependency: transitive
description:
@@ -1063,10 +1047,10 @@ packages:
dependency: transitive
description:
name: package_info_plus
sha256: "16eee997588c60225bda0488b6dcfac69280a6b7a3cf02c741895dd370a02968"
sha256: f69da0d3189a4b4ceaeb1a3defb0f329b3b352517f52bed4290f83d4f06bc08d
url: "https://pub.dev"
source: hosted
version: "8.3.1"
version: "9.0.0"
package_info_plus_platform_interface:
dependency: transitive
description:
@@ -1103,18 +1087,18 @@ packages:
dependency: transitive
description:
name: path_provider_android
sha256: "993381400e94d18469750e5b9dcb8206f15bc09f9da86b9e44a9b0092a0066db"
sha256: e122c5ea805bb6773bb12ce667611265980940145be920cd09a4b0ec0285cb16
url: "https://pub.dev"
source: hosted
version: "2.2.18"
version: "2.2.20"
path_provider_foundation:
dependency: transitive
description:
name: path_provider_foundation
sha256: "16eef174aacb07e09c351502740fa6254c165757638eba1e9116b0a781201bbd"
sha256: efaec349ddfc181528345c56f8eda9d6cccd71c177511b132c6a0ddaefaa2738
url: "https://pub.dev"
source: hosted
version: "2.4.2"
version: "2.4.3"
path_provider_linux:
dependency: transitive
description:
@@ -1192,10 +1176,10 @@ packages:
dependency: transitive
description:
name: pool
sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a"
sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d"
url: "https://pub.dev"
source: hosted
version: "1.5.1"
version: "1.5.2"
posix:
dependency: transitive
description:
@@ -1256,10 +1240,10 @@ packages:
dependency: transitive
description:
name: qr_code_dart_scan
sha256: "1b317b47f475f6995c19e0f41d790902a8cd158b23c435d936763d86ba44309c"
sha256: "81443d940f8f27baaa4b9aeaa8d3d2155ad2c0b9842a9bacb03dab85c111e2f6"
url: "https://pub.dev"
source: hosted
version: "0.11.3"
version: "0.11.5"
quiver:
dependency: transitive
description:
@@ -1296,10 +1280,10 @@ packages:
dependency: transitive
description:
name: riverpod
sha256: b21446f04474040479b19004c5b729605cde221bbd14cf16ad0db8804dce4810
sha256: c406de02bff19d920b832bddfb8283548bfa05ce41c59afba57ce643e116aa59
url: "https://pub.dev"
source: hosted
version: "3.0.1"
version: "3.0.3"
riverpod_analyzer_utils:
dependency: transitive
description:
@@ -1312,18 +1296,18 @@ packages:
dependency: "direct main"
description:
name: riverpod_annotation
sha256: d8154e04008b98015ee3e0514a5d929e3d15605790d28bcd74e63e2d415632a1
sha256: "7230014155777fc31ba3351bc2cb5a3b5717b11bfafe52b1553cb47d385f8897"
url: "https://pub.dev"
source: hosted
version: "3.0.1"
version: "3.0.3"
riverpod_generator:
dependency: "direct dev"
description:
name: riverpod_generator
sha256: af92fa4051568071f7e12bd44b8b4f3adc7470e3676bd5ac953582a9cce4a1c0
sha256: "49894543a42cf7a9954fc4e7366b6d3cb2e6ec0fa07775f660afcdd92d097702"
url: "https://pub.dev"
source: hosted
version: "3.0.1"
version: "3.0.3"
screen_retriever:
dependency: transitive
description:
@@ -1376,10 +1360,10 @@ packages:
dependency: transitive
description:
name: share_plus
sha256: "3424e9d5c22fd7f7590254ba09465febd6f8827c8b19a44350de4ac31d92d3a6"
sha256: "14c8860d4de93d3a7e53af51bff479598c4e999605290756bbbe45cf65b37840"
url: "https://pub.dev"
source: hosted
version: "12.0.0"
version: "12.0.1"
share_plus_platform_interface:
dependency: transitive
description:
@@ -1400,18 +1384,18 @@ packages:
dependency: transitive
description:
name: shared_preferences_android
sha256: a2608114b1ffdcbc9c120eb71a0e207c71da56202852d4aab8a5e30a82269e74
sha256: "34266009473bf71d748912da4bf62d439185226c03e01e2d9687bc65bbfcb713"
url: "https://pub.dev"
source: hosted
version: "2.4.12"
version: "2.4.15"
shared_preferences_foundation:
dependency: transitive
description:
name: shared_preferences_foundation
sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03"
sha256: "1c33a907142607c40a7542768ec9badfd16293bac51da3a4482623d15845f88b"
url: "https://pub.dev"
source: hosted
version: "2.5.4"
version: "2.5.5"
shared_preferences_linux:
dependency: transitive
description:
@@ -1485,18 +1469,18 @@ packages:
dependency: transitive
description:
name: source_gen
sha256: "7b19d6ba131c6eb98bfcbf8d56c1a7002eba438af2e7ae6f8398b2b0f4f381e3"
sha256: "9098ab86015c4f1d8af6486b547b11100e73b193e1899015033cb3e14ad20243"
url: "https://pub.dev"
source: hosted
version: "3.1.0"
version: "4.0.2"
source_helper:
dependency: transitive
description:
name: source_helper
sha256: a447acb083d3a5ef17f983dd36201aeea33fedadb3228fa831f2f0c92f0f3aca
sha256: "6a3c6cc82073a8797f8c4dc4572146114a39652851c157db37e964d9c7038723"
url: "https://pub.dev"
source: hosted
version: "1.3.7"
version: "1.3.8"
source_map_stack_trace:
dependency: transitive
description:
@@ -1521,14 +1505,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.10.1"
sprintf:
dependency: transitive
description:
name: sprintf
sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"
url: "https://pub.dev"
source: hosted
version: "7.0.0"
stack_trace:
dependency: transitive
description:
@@ -1601,14 +1577,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.6.12"
timing:
dependency: transitive
description:
name: timing
sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe"
url: "https://pub.dev"
source: hosted
version: "1.0.2"
tuple:
dependency: transitive
description:
@@ -1645,18 +1613,18 @@ packages:
dependency: transitive
description:
name: url_launcher_android
sha256: "69ee86740f2847b9a4ba6cffa74ed12ce500bbe2b07f3dc1e643439da60637b7"
sha256: "5c8b6c2d89a78f5a1cca70a73d9d5f86c701b36b42f9c9dac7bad592113c28e9"
url: "https://pub.dev"
source: hosted
version: "6.3.18"
version: "6.3.24"
url_launcher_ios:
dependency: transitive
description:
name: url_launcher_ios
sha256: d80b3f567a617cb923546034cc94bfe44eb15f989fe670b37f26abdb9d939cb7
sha256: "6b63f1441e4f653ae799166a72b50b1767321ecc263a57aadf825a7a2a5477d9"
url: "https://pub.dev"
source: hosted
version: "6.3.4"
version: "6.3.5"
url_launcher_linux:
dependency: transitive
description:
@@ -1669,10 +1637,10 @@ packages:
dependency: transitive
description:
name: url_launcher_macos
sha256: c043a77d6600ac9c38300567f33ef12b0ef4f4783a2c1f00231d2b1941fea13f
sha256: "8262208506252a3ed4ff5c0dc1e973d2c0e0ef337d0a074d35634da5d44397c9"
url: "https://pub.dev"
source: hosted
version: "3.2.3"
version: "3.2.4"
url_launcher_platform_interface:
dependency: transitive
description:
@@ -1701,10 +1669,10 @@ packages:
dependency: transitive
description:
name: uuid
sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff
sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8
url: "https://pub.dev"
source: hosted
version: "4.5.1"
version: "4.5.2"
vector_graphics:
dependency: transitive
description:
@@ -1757,18 +1725,18 @@ packages:
dependency: "direct main"
description:
name: wakelock_plus
sha256: a474e314c3e8fb5adef1f9ae2d247e57467ad557fa7483a2b895bc1b421c5678
sha256: "9296d40c9adbedaba95d1e704f4e0b434be446e2792948d0e4aa977048104228"
url: "https://pub.dev"
source: hosted
version: "1.3.2"
version: "1.4.0"
wakelock_plus_platform_interface:
dependency: transitive
description:
name: wakelock_plus_platform_interface
sha256: e10444072e50dbc4999d7316fd303f7ea53d31c824aa5eb05d7ccbdd98985207
sha256: "036deb14cd62f558ca3b73006d52ce049fabcdcb2eddfe0bf0fe4e8a943b5cf2"
url: "https://pub.dev"
source: hosted
version: "1.2.3"
version: "1.3.0"
watch_connectivity:
dependency: "direct main"
description:
@@ -1782,10 +1750,10 @@ packages:
dependency: transitive
description:
name: watcher
sha256: "5bf046f41320ac97a469d506261797f35254fa61c641741ef32dacda98b7d39c"
sha256: "592ab6e2892f67760543fb712ff0177f4ec76c031f02f5b4ff8d3fc5eb9fb61a"
url: "https://pub.dev"
source: hosted
version: "1.1.3"
version: "1.1.4"
web:
dependency: transitive
description:
@@ -1830,10 +1798,10 @@ packages:
dependency: transitive
description:
name: win32
sha256: "66814138c3562338d05613a6e368ed8cfb237ad6d64a9e9334be3f309acfca03"
sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e
url: "https://pub.dev"
source: hosted
version: "5.14.0"
version: "5.15.0"
window_manager:
dependency: transitive
description:
@@ -1901,4 +1869,4 @@ packages:
version: "1.1.4"
sdks:
dart: ">=3.9.0 <4.0.0"
flutter: ">=3.35.0"
flutter: ">=3.38.0"

View File

@@ -1,11 +1,11 @@
name: server_box
description: server status & toolbox app.
publish_to: "none"
version: 1.0.1270+1270
version: 1.0.1276+1276
environment:
sdk: ">=3.9.0"
flutter: ">=3.35.0"
flutter: ">=3.38.0"
dependencies:
flutter:
@@ -25,7 +25,7 @@ dependencies:
flutter_displaymode: ^0.7.0
fl_chart: ^1.0.0
freezed_annotation: ^3.0.0
get_it: ^8.2.0
get_it: ^9.0.5
highlight: ^0.7.0
hive_ce_flutter: ^2.3.1
intl: ^0.20.2
@@ -84,7 +84,7 @@ dependency_overrides:
ref: v0.0.36
dev_dependencies:
analyzer: ^7.3.0
analyzer: ^8.1.1
flutter_native_splash: ^2.1.6
hive_ce_generator: ^1.9.2
build_runner: ^2.4.15

View File

@@ -1,165 +0,0 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:server_box/core/utils/proxy_command_executor.dart';
import 'package:server_box/data/model/server/proxy_command_config.dart';
import 'package:server_box/data/model/server/server_private_info.dart';
void main() {
group('ProxyCommandConfig Tests', () {
test('should create ProxyCommandConfig with required fields', () {
const config = ProxyCommandConfig(
command: 'cloudflared access ssh --hostname %h',
timeout: Duration(seconds: 30),
requiresExecutable: true,
executableName: 'cloudflared',
);
expect(config.command, equals('cloudflared access ssh --hostname %h'));
expect(config.timeout.inSeconds, equals(30));
expect(config.requiresExecutable, isTrue);
expect(config.executableName, equals('cloudflared'));
});
test('should get final command with placeholders replaced', () {
const config = ProxyCommandConfig(
command: 'cloudflared access ssh --hostname %h --port %p',
timeout: Duration(seconds: 30),
);
final finalCommand = config.getFinalCommand(
hostname: 'example.com',
port: 22,
user: 'testuser',
);
expect(finalCommand, equals('cloudflared access ssh --hostname example.com --port 22'));
});
test('should handle all placeholders correctly', () {
const config = ProxyCommandConfig(
command: 'ssh -W %h:%p -l %r bastion.example.com',
timeout: Duration(seconds: 10),
);
final finalCommand = config.getFinalCommand(
hostname: 'target.example.com',
port: 2222,
user: 'admin',
);
expect(finalCommand, equals('ssh -W target.example.com:2222 -l admin bastion.example.com'));
});
test('should validate presets from map', () {
final presets = proxyCommandPresets;
final cloudflareConfig = presets['cloudflare_access'];
expect(cloudflareConfig, isNotNull);
expect(cloudflareConfig!.command, equals('cloudflared access ssh --hostname %h'));
expect(cloudflareConfig.requiresExecutable, isTrue);
expect(cloudflareConfig.executableName, equals('cloudflared'));
final sshBastionConfig = presets['ssh_via_bastion'];
expect(sshBastionConfig, isNotNull);
expect(sshBastionConfig!.command, equals('ssh -W %h:%p bastion.example.com'));
expect(sshBastionConfig.requiresExecutable, isFalse);
final ncConfig = presets['nc_netcat'];
expect(ncConfig, isNotNull);
expect(ncConfig!.command, equals('nc %h %p'));
expect(ncConfig.requiresExecutable, isFalse);
final socatConfig = presets['socat'];
expect(socatConfig, isNotNull);
expect(socatConfig!.command, equals('socat - PROXY:%h:%p,proxyport=8080'));
expect(socatConfig.requiresExecutable, isFalse);
});
});
group('Spi with ProxyCommand Tests', () {
test('should create Spi with ProxyCommand configuration', () {
final spi = Spi(
name: 'Test Server',
ip: 'example.com',
port: 22,
user: 'testuser',
pwd: 'testpass',
proxyCommand: const ProxyCommandConfig(
command: 'cloudflared access ssh --hostname %h',
timeout: Duration(seconds: 30),
requiresExecutable: true,
executableName: 'cloudflared',
),
);
expect(spi.name, equals('Test Server'));
expect(spi.proxyCommand, isNotNull);
expect(spi.proxyCommand!.command, equals('cloudflared access ssh --hostname %h'));
expect(spi.proxyCommand!.requiresExecutable, isTrue);
});
test('should handle Spi without ProxyCommand', () {
final spi = Spi(
name: 'Test Server',
ip: 'example.com',
port: 22,
user: 'testuser',
pwd: 'testpass',
);
expect(spi.proxyCommand, isNull);
});
test('should serialize and deserialize Spi with ProxyCommand', () {
final originalSpi = Spi(
name: 'Test Server',
ip: 'example.com',
port: 22,
user: 'testuser',
pwd: 'testpass',
proxyCommand: const ProxyCommandConfig(
command: 'cloudflared access ssh --hostname %h',
timeout: Duration(seconds: 30),
requiresExecutable: true,
executableName: 'cloudflared',
),
);
final json = originalSpi.toJson();
final deserializedSpi = Spi.fromJson(json);
expect(deserializedSpi.name, equals(originalSpi.name));
expect(deserializedSpi.ip, equals(originalSpi.ip));
expect(deserializedSpi.proxyCommand, isNotNull);
expect(deserializedSpi.proxyCommand!.command, equals(originalSpi.proxyCommand!.command));
expect(deserializedSpi.proxyCommand!.requiresExecutable, equals(originalSpi.proxyCommand!.requiresExecutable));
expect(deserializedSpi.proxyCommand!.executableName, equals(originalSpi.proxyCommand!.executableName));
});
});
group('ProxyCommandExecutor Tokenization', () {
test('tokenizeCommand handles quoted paths', () {
final tokens = ProxyCommandExecutor.tokenizeCommand(
'ssh -i "/Users/John Doe/.ssh/id_rsa" -W %h:%p bastion.example.com',
);
expect(
tokens,
equals([
'ssh',
'-i',
'/Users/John Doe/.ssh/id_rsa',
'-W',
'%h:%p',
'bastion.example.com',
]),
);
});
test('tokenizeCommand throws on unmatched quote', () {
expect(
() => ProxyCommandExecutor.tokenizeCommand('ssh -i "/Users/John Doe/.ssh/id_rsa'),
throwsA(isA<ProxyCommandException>()),
);
});
});
}

View File

@@ -286,25 +286,6 @@ Host jumpserver
// ProxyJump is ignored in current implementation
});
test('parseConfig handles ProxyCommand with ssh -W jump host', () async {
await configFile.writeAsString('''
Host internal
HostName 172.16.0.50
User admin
ProxyCommand ssh -W %h:%p user@bastion.example.com
''');
final servers = await SSHConfig.parseConfig(configFile.path);
expect(servers, hasLength(1));
final server = servers.first;
expect(server.name, 'internal');
expect(server.ip, '172.16.0.50');
expect(server.user, 'admin');
// Jump host extracted from ProxyCommand token containing user@host
expect(server.jumpId, 'user@bastion.example.com');
});
test('parseConfig returns empty list for non-existent file', () async {
final servers = await SSHConfig.parseConfig('/non/existent/path');
expect(servers, isEmpty);
@@ -371,4 +352,4 @@ Host internal-server
expect(dev.keyId, isNull);
});
});
}
}