Add Windows local node service
This commit is contained in:
@@ -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<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.exe',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
final pidFile = await _pidFilePath();
|
||||
final args = <String>[
|
||||
'--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> _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.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<String> _pidFilePath() async {
|
||||
return (await AppPaths.localNodePidFile()).path;
|
||||
}
|
||||
}
|
||||
@@ -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<LocalNodeService>((ref) {
|
||||
if (Platform.isLinux) {
|
||||
return LinuxLocalNodeService(logger: logger);
|
||||
}
|
||||
if (Platform.isWindows) {
|
||||
return WindowsLocalNodeService(logger: logger);
|
||||
}
|
||||
return NoopLocalNodeService();
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user