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 LinuxLocalNodeService implements LocalNodeService { LinuxLocalNodeService({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'); return false; } final pidFile = await _pidFilePath(); final args = [ '--detach', '--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'), p.join(cwd, 'external', 'daemon', 'peyad'), p.join(cwd, '..', 'external', 'daemon', 'peyad'), ]; 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; } }