263 lines
6.8 KiB
Dart
263 lines
6.8 KiB
Dart
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<bool>? _startFuture;
|
|
P2poolConfig? _lastConfig;
|
|
|
|
@override
|
|
Future<bool> isRunning({P2poolConfig? config}) async {
|
|
final resolved = _resolveConfig(config);
|
|
final pid = await _readPid();
|
|
if (pid != null && _isProcessAlive(pid)) {
|
|
return true;
|
|
}
|
|
return _isStratumAvailable(resolved);
|
|
}
|
|
|
|
@override
|
|
Future<bool> start({required P2poolConfig config}) async {
|
|
return _startP2pool(config);
|
|
}
|
|
|
|
@override
|
|
Future<bool> 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<bool> restart({required P2poolConfig config}) async {
|
|
await stop();
|
|
return _startP2pool(config);
|
|
}
|
|
|
|
Future<bool> _startP2pool(P2poolConfig config) async {
|
|
if (_startFuture != null) {
|
|
return _startFuture!;
|
|
}
|
|
_startFuture = _startP2poolInternal(config);
|
|
try {
|
|
return await _startFuture!;
|
|
} finally {
|
|
_startFuture = null;
|
|
}
|
|
}
|
|
|
|
Future<bool> _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 = <String>[
|
|
'--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<bool> _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<bool> _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<bool> _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<bool> _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<int?> _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<String?> _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<String> _dataDirPath() async {
|
|
return (await AppPaths.p2poolDir()).path;
|
|
}
|
|
|
|
Future<String> _pidFilePath() async {
|
|
return (await AppPaths.p2poolPidFile()).path;
|
|
}
|
|
}
|