Files
peyawallet/lib/services/windows_local_node_service.dart
Codex Bot 21e8d088df
build / Build Linux wallet (push) Successful in 2m15s
build / Build Windows wallet (push) Has been cancelled
Reduce false local node start failures
2026-04-19 19:27:37 +02:00

295 lines
8.2 KiB
Dart

import 'dart:async';
import 'dart:convert';
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);
if (!await _isRpcAvailable(effective)) {
return true;
}
final rpcAccepted = await _stopViaRpc(effective);
final stopped = await _waitForStop(effective);
if (!stopped) {
_logger.w('Failed to stop local node via RPC.');
return false;
}
if (!rpcAccepted) {
_logger.i('Local node stopped despite RPC response/read failure.');
}
return true;
}
@override
Future<bool> restart({LocalNodeConfig? config}) async {
final effective = _resolveConfig(config);
final stopped = await stop();
if (!stopped) {
return false;
}
return _startNode(effective);
}
Future<bool> _startNode(LocalNodeConfig config) async {
if (_startFuture != null) {
return _startFuture!;
}
_startFuture = _startNodeInternal(config);
try {
final started = await _startFuture!;
if (started) {
return true;
}
final eventuallyAvailable = await _waitForRpc(
config,
attempts: 8,
logOnFailure: false,
);
if (eventuallyAvailable) {
_logger.i('Local node RPC became available after delayed startup.');
return true;
}
return false;
} 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 daemonLogFile = await AppPaths.localNodeLogFile();
final launcherLogFile = await AppPaths.localNodeLauncherLogFile();
await launcherLogFile.parent.create(recursive: true);
final workingDirectory = p.dirname(binary);
final args = <String>[
'--non-interactive',
'--rpc-bind-ip',
config.rpcHost,
'--rpc-bind-port',
config.rpcPort.toString(),
'--log-file',
daemonLogFile.path,
...config.extraArgs,
];
try {
final process = await Process.start(
binary,
args,
workingDirectory: workingDirectory,
mode: ProcessStartMode.detachedWithStdio,
);
_logger.i(
'Starting local node: "$binary" ${args.join(' ')} '
'(cwd=$workingDirectory, log=${daemonLogFile.path})',
);
unawaited(_pipeLauncherLogs(process, launcherLogFile));
final earlyExitCode = await _waitForEarlyExit(process);
if (earlyExitCode != null) {
_logger.w(
'Local node exited immediately with code $earlyExitCode. '
'See ${launcherLogFile.path} and ${daemonLogFile.path}',
);
return false;
}
} catch (error) {
_logger.w('Failed to start local node: $error');
return false;
}
return _waitForRpc(config);
}
Future<void> _pipeLauncherLogs(Process process, File logFile) async {
final sink = logFile.openWrite(mode: FileMode.writeOnlyAppend);
sink.writeln('=== ${DateTime.now().toIso8601String()} local node launch ===');
unawaited(
process.stdout
.transform(utf8.decoder)
.transform(const LineSplitter())
.forEach((line) => sink.writeln('[stdout] $line')),
);
unawaited(
process.stderr
.transform(utf8.decoder)
.transform(const LineSplitter())
.forEach((line) => sink.writeln('[stderr] $line')),
);
unawaited(
process.exitCode.then((code) async {
sink.writeln('[exit] $code');
await sink.flush();
await sink.close();
}),
);
}
Future<int?> _waitForEarlyExit(Process process) async {
const startupGrace = Duration(seconds: 2);
final result = await Future.any<Object?>([
process.exitCode,
Future<Object?>.delayed(startupGrace, () => null),
]);
return result is int ? result : null;
}
Future<bool> _waitForRpc(
LocalNodeConfig config, {
int attempts = 20,
bool logOnFailure = true,
}) async {
for (var i = 0; i < attempts; i++) {
if (await _isRpcAvailable(config)) {
return true;
}
await Future.delayed(const Duration(seconds: 1));
}
if (logOnFailure) {
_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;
}
}
Future<bool> _stopViaRpc(LocalNodeConfig config) async {
HttpClient? client;
try {
client = HttpClient();
final uri = Uri.parse('http://${config.rpcHost}:${config.rpcPort}/stop_daemon');
final request = await client.postUrl(uri);
request.headers.contentType = ContentType.json;
request.write('{}');
final response = await request.close();
final body = await utf8.decoder.bind(response).join();
if (response.statusCode != HttpStatus.ok) {
_logger.w(
'Local node stop_daemon returned HTTP ${response.statusCode}: $body',
);
return false;
}
return true;
} catch (error) {
_logger.w('Failed to call stop_daemon on local node: $error');
return false;
} finally {
client?.close(force: true);
}
}
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;
}
}