Add Windows local node service
build / Build Linux wallet (push) Successful in 2m19s
build / Build Windows wallet (push) Successful in 21m3s

This commit is contained in:
Codex Bot
2026-04-18 17:03:37 +02:00
parent e45f8a3f20
commit 9b7597f782
2 changed files with 210 additions and 0 deletions
@@ -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;
}
}
+4
View File
@@ -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();
});