diff --git a/lib/services/windows_local_node_service.dart b/lib/services/windows_local_node_service.dart new file mode 100644 index 0000000..05bd53d --- /dev/null +++ b/lib/services/windows_local_node_service.dart @@ -0,0 +1,206 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:logger/logger.dart'; +import 'package:path/path.dart' as p; + +import 'app_paths.dart'; +import 'local_node_service.dart'; + +class WindowsLocalNodeService implements LocalNodeService { + WindowsLocalNodeService({required Logger logger}) : _logger = logger; + + final Logger _logger; + Future? _startFuture; + LocalNodeConfig? _lastConfig; + + @override + Future isRunning({LocalNodeConfig? config}) async { + final resolved = _resolveConfig(config); + return _isRpcAvailable(resolved); + } + + @override + Future ensureRunning({LocalNodeConfig? config}) async { + final effective = _resolveConfig(config); + if (await _isRpcAvailable(effective)) { + return true; + } + return _startNode(effective); + } + + @override + Future start({LocalNodeConfig? config}) async { + final effective = _resolveConfig(config); + return _startNode(effective); + } + + @override + Future stop() async { + final effective = _resolveConfig(null); + final pidFile = File(await _pidFilePath()); + if (!await pidFile.exists()) { + _logger.w('Local node pidfile not found at ${pidFile.path}'); + return false; + } + final rawPid = await pidFile.readAsString(); + final pid = int.tryParse(rawPid.trim()); + if (pid == null) { + _logger.w('Local node pidfile invalid: $rawPid'); + return false; + } + final killed = Process.killPid(pid); + if (!killed) { + _logger.w('Failed to terminate local node process ($pid)'); + return false; + } + return _waitForStop(effective); + } + + @override + Future restart({LocalNodeConfig? config}) async { + final effective = _resolveConfig(config); + await stop(); + return _startNode(effective); + } + + Future _startNode(LocalNodeConfig config) async { + if (_startFuture != null) { + return _startFuture!; + } + _startFuture = _startNodeInternal(config); + try { + return await _startFuture!; + } finally { + _startFuture = null; + } + } + + Future _startNodeInternal(LocalNodeConfig config) async { + _lastConfig = config; + final binary = await _locateBinary(); + if (binary == null) { + _logger.w( + 'Local node binary not found. Expected external\\daemon\\peyad.exe', + ); + return false; + } + final pidFile = await _pidFilePath(); + final args = [ + '--non-interactive', + '--rpc-bind-ip', + config.rpcHost, + '--rpc-bind-port', + config.rpcPort.toString(), + '--pidfile', + pidFile, + ...config.extraArgs, + ]; + try { + await Process.start( + binary, + args, + workingDirectory: Directory.current.path, + mode: ProcessStartMode.detached, + ); + } catch (error) { + _logger.w('Failed to start local node: $error'); + return false; + } + return _waitForRpc(config); + } + + Future _waitForRpc(LocalNodeConfig config) async { + const attempts = 20; + for (var i = 0; i < attempts; i++) { + if (await _isRpcAvailable(config)) { + return true; + } + await Future.delayed(const Duration(seconds: 1)); + } + _logger.w('Local node did not become available after ${attempts}s.'); + return false; + } + + Future _waitForStop(LocalNodeConfig config) async { + const attempts = 15; + for (var i = 0; i < attempts; i++) { + if (!await _isRpcAvailable(config)) { + return true; + } + await Future.delayed(const Duration(seconds: 1)); + } + return false; + } + + Future _isRpcAvailable(LocalNodeConfig config) async { + try { + final socket = await Socket.connect( + config.rpcHost, + config.rpcPort, + timeout: const Duration(seconds: 1), + ); + socket.destroy(); + return true; + } catch (_) { + return false; + } + } + + LocalNodeConfig _resolveConfig(LocalNodeConfig? config) { + final base = config ?? _lastConfig ?? const LocalNodeConfig(); + return _applyOverrides(base); + } + + LocalNodeConfig _applyOverrides(LocalNodeConfig config) { + var host = config.rpcHost; + var port = config.rpcPort; + final args = config.extraArgs; + for (var i = 0; i < args.length; i++) { + final arg = args[i]; + if (arg.startsWith('--rpc-bind-ip=')) { + host = arg.split('=').last.trim(); + continue; + } + if (arg == '--rpc-bind-ip' && i + 1 < args.length) { + host = args[i + 1].trim(); + continue; + } + if (arg.startsWith('--rpc-bind-port=')) { + final parsed = int.tryParse(arg.split('=').last.trim()); + if (parsed != null) { + port = parsed; + } + continue; + } + if (arg == '--rpc-bind-port' && i + 1 < args.length) { + final parsed = int.tryParse(args[i + 1].trim()); + if (parsed != null) { + port = parsed; + } + } + } + return config.copyWith(rpcHost: host, rpcPort: port); + } + + Future _locateBinary() async { + final cwd = Directory.current.path; + final executableDir = p.dirname(Platform.resolvedExecutable); + final candidates = [ + p.join(executableDir, 'external', 'daemon', 'peyad.exe'), + p.join(cwd, 'external', 'daemon', 'peyad.exe'), + p.join(cwd, '..', 'external', 'daemon', 'peyad.exe'), + ]; + for (final candidate in candidates) { + final file = File(candidate); + if (await file.exists()) { + return file.path; + } + } + return null; + } + + Future _pidFilePath() async { + return (await AppPaths.localNodePidFile()).path; + } +} diff --git a/lib/state/providers.dart b/lib/state/providers.dart index d65bbab..bac8d16 100644 --- a/lib/state/providers.dart +++ b/lib/state/providers.dart @@ -14,6 +14,7 @@ import '../services/noop_local_node_service.dart'; import '../services/noop_tray_service.dart'; import '../services/sync_scheduler.dart'; import '../services/tray_service.dart'; +import '../services/windows_local_node_service.dart'; import '../services/window_lifecycle_service.dart'; import 'app_config_controller.dart'; import 'wallet_controller.dart'; @@ -68,6 +69,9 @@ final localNodeServiceProvider = Provider((ref) { if (Platform.isLinux) { return LinuxLocalNodeService(logger: logger); } + if (Platform.isWindows) { + return WindowsLocalNodeService(logger: logger); + } return NoopLocalNodeService(); });