222 lines
5.9 KiB
Dart
222 lines
5.9 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 'local_node_service.dart';
|
|
|
|
class LinuxLocalNodeService implements LocalNodeService {
|
|
LinuxLocalNodeService({required Logger logger}) : _logger = logger;
|
|
|
|
final Logger _logger;
|
|
Future<bool>? _startFuture;
|
|
LocalNodeConfig? _lastConfig;
|
|
|
|
@override
|
|
Future<bool> isRunning({LocalNodeConfig? config}) async {
|
|
final resolved = _resolveConfig(config);
|
|
return _isRpcAvailable(resolved);
|
|
}
|
|
|
|
@override
|
|
Future<bool> ensureRunning({LocalNodeConfig? config}) async {
|
|
final effective = _resolveConfig(config);
|
|
if (await _isRpcAvailable(effective)) {
|
|
return true;
|
|
}
|
|
return _startNode(effective);
|
|
}
|
|
|
|
@override
|
|
Future<bool> start({LocalNodeConfig? config}) async {
|
|
final effective = _resolveConfig(config);
|
|
return _startNode(effective);
|
|
}
|
|
|
|
@override
|
|
Future<bool> 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<bool> restart({LocalNodeConfig? config}) async {
|
|
final effective = _resolveConfig(config);
|
|
await stop();
|
|
return _startNode(effective);
|
|
}
|
|
|
|
Future<bool> _startNode(LocalNodeConfig config) async {
|
|
if (_startFuture != null) {
|
|
return _startFuture!;
|
|
}
|
|
_startFuture = _startNodeInternal(config);
|
|
try {
|
|
return await _startFuture!;
|
|
} finally {
|
|
_startFuture = null;
|
|
}
|
|
}
|
|
|
|
Future<bool> _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;
|
|
}
|
|
if (!await _ensureExecutable(binary)) {
|
|
_logger.w('Local node binary is not executable: $binary');
|
|
return false;
|
|
}
|
|
final pidFile = await _pidFilePath();
|
|
final args = <String>[
|
|
'--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<bool> _ensureExecutable(String binaryPath) async {
|
|
final result = await Process.run('chmod', ['755', binaryPath]);
|
|
if (result.exitCode == 0) {
|
|
return true;
|
|
}
|
|
_logger.w(
|
|
'Failed to chmod local node binary ($binaryPath): '
|
|
'${result.stderr.toString().trim()}',
|
|
);
|
|
return false;
|
|
}
|
|
|
|
Future<bool> _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<bool> _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<bool> _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<String?> _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<String> _pidFilePath() async {
|
|
return (await AppPaths.localNodePidFile()).path;
|
|
}
|
|
}
|