import 'dart:async'; import 'dart:io'; import 'package:logger/logger.dart'; import 'package:path/path.dart' as p; import 'app_paths.dart'; import 'p2pool_service.dart'; class LinuxP2poolService implements P2poolService { LinuxP2poolService({required Logger logger}) : _logger = logger; final Logger _logger; Future? _startFuture; P2poolConfig? _lastConfig; @override Future isRunning({P2poolConfig? config}) async { final resolved = _resolveConfig(config); final pid = await _readPid(); if (pid != null && _isProcessAlive(pid)) { return true; } return _isStratumAvailable(resolved); } @override Future start({required P2poolConfig config}) async { return _startP2pool(config); } @override Future stop() async { final pidFile = File(await _pidFilePath()); if (!await pidFile.exists()) { _logger.w('P2Pool pidfile not found at ${pidFile.path}'); return false; } final rawPid = await pidFile.readAsString(); final pid = int.tryParse(rawPid.trim()); if (pid == null) { _logger.w('P2Pool pidfile invalid: $rawPid'); return false; } if (!_isProcessAlive(pid)) { try { await pidFile.delete(); } catch (_) {} return true; } var stopped = Process.killPid(pid, ProcessSignal.sigterm); if (!stopped) { _logger.w('Failed to terminate P2Pool process ($pid)'); return false; } if (!await _waitForExit(pid)) { Process.killPid(pid, ProcessSignal.sigkill); } stopped = await _waitForExit(pid); if (stopped) { try { await pidFile.delete(); } catch (_) {} } return stopped; } @override Future restart({required P2poolConfig config}) async { await stop(); return _startP2pool(config); } Future _startP2pool(P2poolConfig config) async { if (_startFuture != null) { return _startFuture!; } _startFuture = _startP2poolInternal(config); try { return await _startFuture!; } finally { _startFuture = null; } } Future _startP2poolInternal(P2poolConfig config) async { if (config.walletAddress.trim().isEmpty) { _logger.w('P2Pool wallet address missing.'); return false; } if (await _isStratumAvailable(config)) { return true; } _lastConfig = config; final binary = await _locateBinary(); if (binary == null) { _logger.w( 'P2Pool binary not found. Expected external/p2pool/p2pool-salvium'); return false; } final pidFile = await _pidFilePath(); final dataDir = await _dataDirPath(); final args = [ '--wallet', config.walletAddress, '--host', config.host, '--rpc-port', config.rpcPort.toString(), '--zmq-port', config.zmqPort.toString(), '--data-dir', dataDir, '--data-api', dataDir, '--local-api', ]; if (config.stratum.trim().isNotEmpty) { args.addAll(['--stratum', config.stratum.trim()]); } if (config.p2p.trim().isNotEmpty) { args.addAll(['--p2p', config.p2p.trim()]); } if (config.startMining) { final threads = config.miningThreads.clamp(1, 64).toInt(); args.addAll(['--start-mining', threads.toString()]); } try { final process = await Process.start( binary, args, workingDirectory: Directory.current.path, mode: ProcessStartMode.detached, ); await File(pidFile).writeAsString(process.pid.toString()); return _waitForStratum(config, process.pid); } catch (error) { _logger.w('Failed to start P2Pool: $error'); return false; } } Future _waitForStratum(P2poolConfig config, int pid) async { const attempts = 20; for (var i = 0; i < attempts; i++) { if (!_isProcessAlive(pid)) { _logger.w('P2Pool process exited before it became ready.'); return false; } if (await _isStratumAvailable(config)) { return true; } await Future.delayed(const Duration(seconds: 1)); } _logger.w('P2Pool did not become available after ${attempts}s.'); return _isProcessAlive(pid); } Future _waitForStop(P2poolConfig config) async { const attempts = 10; for (var i = 0; i < attempts; i++) { if (!await _isStratumAvailable(config)) { return true; } await Future.delayed(const Duration(seconds: 1)); } return false; } Future _waitForExit(int pid) async { const attempts = 10; for (var i = 0; i < attempts; i++) { if (!_isProcessAlive(pid)) { return true; } await Future.delayed(const Duration(milliseconds: 500)); } return !_isProcessAlive(pid); } Future _isStratumAvailable(P2poolConfig config) async { final target = _resolveStratumTarget(config.stratum); if (target == null) { return false; } try { final socket = await Socket.connect( target.host, target.port, timeout: const Duration(seconds: 1), ); socket.destroy(); return true; } catch (_) { return false; } } P2poolConfig _resolveConfig(P2poolConfig? config) { return config ?? _lastConfig ?? const P2poolConfig(walletAddress: ''); } Future _readPid() async { final pidFile = File(await _pidFilePath()); if (!await pidFile.exists()) { return null; } final rawPid = await pidFile.readAsString(); return int.tryParse(rawPid.trim()); } bool _isProcessAlive(int pid) { final proc = Directory('/proc/$pid'); return proc.existsSync(); } ({String host, int port})? _resolveStratumTarget(String raw) { final trimmed = raw.trim(); if (trimmed.isEmpty) { return null; } final first = trimmed.split(',').first.trim(); final match = RegExp(r'^(.*):(\d+)$').firstMatch(first); if (match == null) { return null; } var host = match.group(1) ?? ''; final port = int.tryParse(match.group(2) ?? '') ?? 0; if (port <= 0) { return null; } if (host == '0.0.0.0' || host == '*') { host = '127.0.0.1'; } else if (host == '::' || host == '[::]') { host = '::1'; } return (host: host, port: port); } Future _locateBinary() async { final cwd = Directory.current.path; final candidates = [ p.join(cwd, 'external', 'p2pool', 'p2pool-salvium'), p.join(cwd, '..', 'external', 'p2pool', 'p2pool-salvium'), ]; for (final candidate in candidates) { final file = File(candidate); if (await file.exists()) { return file.path; } } return null; } Future _dataDirPath() async { return (await AppPaths.p2poolDir()).path; } Future _pidFilePath() async { return (await AppPaths.p2poolPidFile()).path; } }