add: store exes & presets

This commit is contained in:
lollipopkit🏳️‍⚧️
2025-11-01 23:47:37 +08:00
parent 12c8543352
commit 84921de7a7
4 changed files with 147 additions and 12 deletions

View File

@@ -1,9 +1,11 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:fl_lib/fl_lib.dart'; import 'package:fl_lib/fl_lib.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:server_box/data/store/setting.dart';
/// Exception thrown when executable management fails /// Exception thrown when executable management fails
class ExecutableException implements Exception { class ExecutableException implements Exception {
@@ -28,6 +30,8 @@ class ExecutableInfo {
abstract final class ExecutableManager { abstract final class ExecutableManager {
static const String _executablesDirName = 'executables'; static const String _executablesDirName = 'executables';
static late final Directory _executablesDir; static late final Directory _executablesDir;
static final Map<String, ExecutableInfo> _customExecutables = {};
static bool _customExecutablesLoaded = false;
static Future<void> initialize() async { static Future<void> initialize() async {
final appDir = await getApplicationSupportDirectory(); final appDir = await getApplicationSupportDirectory();
@@ -35,6 +39,8 @@ abstract final class ExecutableManager {
if (!await _executablesDir.exists()) { if (!await _executablesDir.exists()) {
await _executablesDir.create(recursive: true); await _executablesDir.create(recursive: true);
} }
_ensureCustomExecutablesLoaded();
} }
/// Predefined executables /// Predefined executables
@@ -45,6 +51,52 @@ abstract final class ExecutableManager {
'socat': ExecutableInfo(name: 'socat'), '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 /// Check if an executable exists in PATH or local directory
static Future<bool> isExecutableAvailable(String name) async { static Future<bool> isExecutableAvailable(String name) async {
// First check if it's in PATH // First check if it's in PATH
@@ -224,18 +276,27 @@ abstract final class ExecutableManager {
/// Get predefined executable info /// Get predefined executable info
static ExecutableInfo? getExecutableInfo(String name) { static ExecutableInfo? getExecutableInfo(String name) {
_ensureCustomExecutablesLoaded();
return _predefinedExecutables[name]; return _predefinedExecutables[name];
} }
/// Add a custom executable definition /// Add a custom executable definition
static void addCustomExecutable(String name, ExecutableInfo info) { static void addCustomExecutable(String name, ExecutableInfo info) {
// TODO: Implement persistent storage for custom executables _ensureCustomExecutablesLoaded();
_customExecutables[name] = info;
_predefinedExecutables[name] = info;
_persistCustomExecutables();
Loggers.app.info('Adding custom executable: $name'); Loggers.app.info('Adding custom executable: $name');
} }
/// Remove a custom executable definition /// Remove a custom executable definition
static void removeCustomExecutable(String name) { static void removeCustomExecutable(String name) {
// TODO: Implement persistent storage for custom executables _ensureCustomExecutablesLoaded();
final removed = _customExecutables.remove(name);
if (removed != null) {
_predefinedExecutables.remove(name);
_persistCustomExecutables();
Loggers.app.info('Removing custom executable: $name'); Loggers.app.info('Removing custom executable: $name');
} }
}
} }

View File

@@ -1,4 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:dartssh2/dartssh2.dart'; import 'package:dartssh2/dartssh2.dart';
@@ -6,6 +7,7 @@ import 'package:fl_lib/fl_lib.dart';
import 'package:server_box/core/utils/executable_manager.dart'; import 'package:server_box/core/utils/executable_manager.dart';
import 'package:server_box/core/utils/proxy_socket.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/model/server/proxy_command_config.dart';
import 'package:server_box/data/store/setting.dart';
/// Exception thrown when proxy command execution fails /// Exception thrown when proxy command execution fails
class ProxyCommandException implements Exception { class ProxyCommandException implements Exception {
@@ -26,6 +28,58 @@ class ProxyCommandException implements Exception {
/// Generic proxy command executor that handles SSH ProxyCommand functionality /// Generic proxy command executor that handles SSH ProxyCommand functionality
abstract final class ProxyCommandExecutor { 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 /// Execute a proxy command and return a socket connected through the proxy
static Future<SSHSocket> executeProxyCommand( static Future<SSHSocket> executeProxyCommand(
ProxyCommandConfig config, { ProxyCommandConfig config, {
@@ -124,10 +178,18 @@ abstract final class ProxyCommandExecutor {
return 'Proxy command must contain %h (hostname) placeholder'; return 'Proxy command must contain %h (hostname) placeholder';
} }
// If executable is required, check if it exists String executablePath;
// If executable is required, check if it exists and reuse resolved path
if (config.requiresExecutable && config.executableName != null) { if (config.requiresExecutable && config.executableName != null) {
try { try {
await ExecutableManager.ensureExecutable(config.executableName!); executablePath = await ExecutableManager.ensureExecutable(config.executableName!);
} catch (e) {
return e.toString();
}
} else {
try {
executablePath = await ExecutableManager.getExecutablePath(tokens.first);
} catch (e) { } catch (e) {
return e.toString(); return e.toString();
} }
@@ -135,7 +197,7 @@ abstract final class ProxyCommandExecutor {
// Try to validate command syntax (dry run) // Try to validate command syntax (dry run)
try { try {
await Process.run(tokens.first, ['--help']); await Process.run(executablePath, ['--help']);
} catch (e) { } catch (e) {
return 'Command validation failed: $e'; return 'Command validation failed: $e';
} }
@@ -145,21 +207,30 @@ abstract final class ProxyCommandExecutor {
/// Get available proxy command presets /// Get available proxy command presets
static Map<String, ProxyCommandConfig> getPresets() { static Map<String, ProxyCommandConfig> getPresets() {
return proxyCommandPresets; _ensureCustomPresetsLoaded();
return {
...proxyCommandPresets,
..._customPresets,
};
} }
/// Add a custom preset /// Add a custom preset
static Future<void> addCustomPreset(String name, ProxyCommandConfig config) async { static Future<void> addCustomPreset(String name, ProxyCommandConfig config) async {
// TODO: Implement custom preset storage _ensureCustomPresetsLoaded();
// This would involve storing custom presets in a persistent storage _customPresets[name] = config;
_persistCustomPresets();
Loggers.app.info('Adding custom proxy preset: $name'); Loggers.app.info('Adding custom proxy preset: $name');
} }
/// Remove a custom preset /// Remove a custom preset
static Future<void> removeCustomPreset(String name) async { static Future<void> removeCustomPreset(String name) async {
// TODO: Implement custom preset removal _ensureCustomPresetsLoaded();
final removed = _customPresets.remove(name);
if (removed != null) {
_persistCustomPresets();
Loggers.app.info('Removing custom proxy preset: $name'); Loggers.app.info('Removing custom proxy preset: $name');
} }
}
static List<String> tokenizeCommand(String command) => _tokenizeCommand(command); static List<String> tokenizeCommand(String command) => _tokenizeCommand(command);

View File

@@ -1,5 +1,4 @@
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hive_ce_flutter/adapters.dart';
part 'proxy_command_config.freezed.dart'; part 'proxy_command_config.freezed.dart';
part 'proxy_command_config.g.dart'; part 'proxy_command_config.g.dart';

View File

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