From e6388d296bac712a5d8117f65cf66631626f894e Mon Sep 17 00:00:00 2001 From: illuzen Date: Sat, 31 Jan 2026 10:11:34 +0800 Subject: [PATCH 01/10] dart format ./miner-app --- .../lib/features/miner/miner_app_bar.dart | 107 ++++++--- .../features/miner/miner_balance_card.dart | 41 +++- .../lib/features/miner/miner_controls.dart | 69 ++++-- .../miner/miner_dashboard_screen.dart | 37 ++- .../lib/features/miner/miner_stats_card.dart | 57 ++++- .../lib/features/miner/miner_status.dart | 43 +++- .../features/settings/settings_app_bar.dart | 26 ++- .../features/settings/settings_screen.dart | 29 ++- .../setup/node_identity_setup_screen.dart | 13 +- .../lib/features/setup/node_setup_screen.dart | 55 +++-- .../setup/rewards_address_setup_screen.dart | 70 ++++-- miner-app/lib/main.dart | 49 +++- .../lib/src/services/binary_manager.dart | 167 ++++++++++---- .../lib/src/services/chain_rpc_client.dart | 36 ++- .../services/external_miner_api_client.dart | 17 +- .../src/services/gpu_detection_service.dart | 4 +- .../lib/src/services/log_filter_service.dart | 15 +- miner-app/lib/src/services/miner_process.dart | 215 +++++++++++++----- .../src/services/miner_settings_service.dart | 7 +- .../lib/src/services/prometheus_service.dart | 23 +- .../extensions/snackbar_extensions.dart | 10 +- miner-app/lib/src/ui/logs_widget.dart | 75 ++++-- miner-app/lib/src/ui/snackbar_helper.dart | 18 +- .../lib/src/ui/top_snackbar_content.dart | 17 +- miner-app/lib/src/ui/update_banner.dart | 50 +++- 25 files changed, 960 insertions(+), 290 deletions(-) diff --git a/miner-app/lib/features/miner/miner_app_bar.dart b/miner-app/lib/features/miner/miner_app_bar.dart index 0d817bfc..7b8f3ca0 100644 --- a/miner-app/lib/features/miner/miner_app_bar.dart +++ b/miner-app/lib/features/miner/miner_app_bar.dart @@ -53,7 +53,9 @@ class _MinerAppBarState extends State { } void _goToSettingScreen() { - Navigator.of(context).push(MaterialPageRoute(builder: (context) => const SettingsScreen())); + Navigator.of( + context, + ).push(MaterialPageRoute(builder: (context) => const SettingsScreen())); } @override @@ -64,17 +66,31 @@ class _MinerAppBarState extends State { floating: true, pinned: false, flexibleSpace: ClipRRect( - borderRadius: const BorderRadius.only(bottomLeft: Radius.circular(24), bottomRight: Radius.circular(24)), + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(24), + bottomRight: Radius.circular(24), + ), child: BackdropFilter( - filter: ColorFilter.mode(Colors.black.useOpacity(0.1), BlendMode.srcOver), + filter: ColorFilter.mode( + Colors.black.useOpacity(0.1), + BlendMode.srcOver, + ), child: Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, - colors: [Colors.white.useOpacity(0.1), Colors.white.useOpacity(0.05)], + colors: [ + Colors.white.useOpacity(0.1), + Colors.white.useOpacity(0.05), + ], + ), + border: Border( + bottom: BorderSide( + color: Colors.white.useOpacity(0.1), + width: 1, + ), ), - border: Border(bottom: BorderSide(color: Colors.white.useOpacity(0.1), width: 1)), ), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), @@ -100,11 +116,16 @@ class _MinerAppBarState extends State { decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), color: Colors.white.useOpacity(0.1), - border: Border.all(color: Colors.white.useOpacity(0.2), width: 1), + border: Border.all( + color: Colors.white.useOpacity(0.2), + width: 1, + ), ), child: PopupMenuButton<_MenuValues>( color: const Color(0xFF1A1A1A), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), onSelected: (_MenuValues item) async { switch (item) { case _MenuValues.logout: @@ -115,35 +136,57 @@ class _MinerAppBarState extends State { break; } }, - itemBuilder: (BuildContext context) => >[ - PopupMenuItem<_MenuValues>( - value: _MenuValues.logout, - child: Row( - children: [ - Icon(Icons.logout, color: Colors.red.useOpacity(0.8), size: 20), - const SizedBox(width: 12), - Text( - 'Logout (Full Reset)', - style: TextStyle(color: Colors.white.useOpacity(0.9), fontSize: 14), + itemBuilder: (BuildContext context) => + >[ + PopupMenuItem<_MenuValues>( + value: _MenuValues.logout, + child: Row( + children: [ + Icon( + Icons.logout, + color: Colors.red.useOpacity(0.8), + size: 20, + ), + const SizedBox(width: 12), + Text( + 'Logout (Full Reset)', + style: TextStyle( + color: Colors.white.useOpacity(0.9), + fontSize: 14, + ), + ), + ], ), - ], - ), - ), - PopupMenuItem<_MenuValues>( - value: _MenuValues.setting, - child: Row( - children: [ - Icon(Icons.settings, color: Colors.grey.useOpacity(0.8), size: 20), - const SizedBox(width: 12), - Text('Settings', style: TextStyle(color: Colors.white.useOpacity(0.9), fontSize: 14)), - ], - ), - ), - ], + ), + PopupMenuItem<_MenuValues>( + value: _MenuValues.setting, + child: Row( + children: [ + Icon( + Icons.settings, + color: Colors.grey.useOpacity(0.8), + size: 20, + ), + const SizedBox(width: 12), + Text( + 'Settings', + style: TextStyle( + color: Colors.white.useOpacity(0.9), + fontSize: 14, + ), + ), + ], + ), + ), + ], child: Center( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: Icon(Icons.menu, color: Colors.white.useOpacity(0.7), size: 20), + child: Icon( + Icons.menu, + color: Colors.white.useOpacity(0.7), + size: 20, + ), ), ), ), diff --git a/miner-app/lib/features/miner/miner_balance_card.dart b/miner-app/lib/features/miner/miner_balance_card.dart index 36d8203e..580ebfce 100644 --- a/miner-app/lib/features/miner/miner_balance_card.dart +++ b/miner-app/lib/features/miner/miner_balance_card.dart @@ -56,7 +56,10 @@ class _MinerBalanceCardState extends State { setState(() { // Assuming NumberFormattingService and AppConstants are available via quantus_sdk export - _walletBalance = NumberFormattingService().formatBalance(balance, addSymbol: true); + _walletBalance = NumberFormattingService().formatBalance( + balance, + addSymbol: true, + ); _walletAddress = address; }); } else { @@ -99,7 +102,12 @@ class _MinerBalanceCardState extends State { borderRadius: BorderRadius.circular(24), border: Border.all(color: Colors.white.useOpacity(0.1), width: 1), boxShadow: [ - BoxShadow(color: Colors.black.useOpacity(0.2), blurRadius: 20, spreadRadius: 1, offset: const Offset(0, 8)), + BoxShadow( + color: Colors.black.useOpacity(0.2), + blurRadius: 20, + spreadRadius: 1, + offset: const Offset(0, 8), + ), ], ), child: Padding( @@ -120,12 +128,20 @@ class _MinerBalanceCardState extends State { ), borderRadius: BorderRadius.circular(12), ), - child: const Icon(Icons.account_balance_wallet, color: Colors.white, size: 20), + child: const Icon( + Icons.account_balance_wallet, + color: Colors.white, + size: 20, + ), ), const SizedBox(width: 12), Text( 'Wallet Balance', - style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Colors.white.useOpacity(0.9)), + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Colors.white.useOpacity(0.9), + ), ), ], ), @@ -146,11 +162,18 @@ class _MinerBalanceCardState extends State { decoration: BoxDecoration( color: Colors.white.useOpacity(0.05), borderRadius: BorderRadius.circular(12), - border: Border.all(color: Colors.white.useOpacity(0.1), width: 1), + border: Border.all( + color: Colors.white.useOpacity(0.1), + width: 1, + ), ), child: Row( children: [ - Icon(Icons.link, color: Colors.white.useOpacity(0.5), size: 16), + Icon( + Icons.link, + color: Colors.white.useOpacity(0.5), + size: 16, + ), const SizedBox(width: 8), Expanded( child: Text( @@ -164,7 +187,11 @@ class _MinerBalanceCardState extends State { ), ), IconButton( - icon: Icon(Icons.copy, color: Colors.white.useOpacity(0.5), size: 16), + icon: Icon( + Icons.copy, + color: Colors.white.useOpacity(0.5), + size: 16, + ), onPressed: () { if (_walletAddress != null) { context.copyTextWithSnackbar(_walletAddress!); diff --git a/miner-app/lib/features/miner/miner_controls.dart b/miner-app/lib/features/miner/miner_controls.dart index bf8a3e9e..9320ebbd 100644 --- a/miner-app/lib/features/miner/miner_controls.dart +++ b/miner-app/lib/features/miner/miner_controls.dart @@ -49,7 +49,9 @@ class _MinerControlsState extends State { if (mounted) { setState(() { - _cpuWorkers = savedCpuWorkers ?? (Platform.numberOfProcessors > 0 ? Platform.numberOfProcessors : 8); + _cpuWorkers = + savedCpuWorkers ?? + (Platform.numberOfProcessors > 0 ? Platform.numberOfProcessors : 8); _gpuDevices = savedGpuDevices ?? 0; }); } @@ -72,8 +74,12 @@ class _MinerControlsState extends State { print('Starting mining'); // Check for all required files and binaries - final id = File('${await BinaryManager.getQuantusHomeDirectoryPath()}/node_key.p2p'); - final rew = File('${await BinaryManager.getQuantusHomeDirectoryPath()}/rewards-address.txt'); + final id = File( + '${await BinaryManager.getQuantusHomeDirectoryPath()}/node_key.p2p', + ); + final rew = File( + '${await BinaryManager.getQuantusHomeDirectoryPath()}/rewards-address.txt', + ); final binPath = await BinaryManager.getNodeBinaryFilePath(); final bin = File(binPath); final minerBinPath = await BinaryManager.getExternalMinerBinaryFilePath(); @@ -83,7 +89,10 @@ class _MinerControlsState extends State { if (!await bin.exists()) { print('Node binary not found. Cannot start mining.'); if (mounted) { - context.showWarningSnackbar(title: 'Node binary not found!', message: 'Please run setup.'); + context.showWarningSnackbar( + title: 'Node binary not found!', + message: 'Please run setup.', + ); } setState(() => _isAttemptingToggle = false); return; @@ -93,7 +102,10 @@ class _MinerControlsState extends State { if (!await minerBin.exists()) { print('External miner binary not found. Cannot start mining.'); if (mounted) { - context.showWarningSnackbar(title: 'External miner binary not found!', message: 'Please run setup.'); + context.showWarningSnackbar( + title: 'External miner binary not found!', + message: 'Please run setup.', + ); } setState(() => _isAttemptingToggle = false); return; @@ -112,13 +124,19 @@ class _MinerControlsState extends State { widget.onMinerProcessChanged.call(newProc); try { - final newMiningStats = widget.miningStats.copyWith(isSyncing: true, status: MiningStatus.syncing); + final newMiningStats = widget.miningStats.copyWith( + isSyncing: true, + status: MiningStatus.syncing, + ); widget.onMetricsUpdate(newMiningStats); await newProc.start(); } catch (e) { print('Error starting miner process: $e'); if (mounted) { - context.showErrorSnackbar(title: 'Error starting miner!', message: e.toString()); + context.showErrorSnackbar( + title: 'Error starting miner!', + message: e.toString(), + ); } // Notify parent that miner process is null @@ -158,7 +176,9 @@ class _MinerControlsState extends State { try { widget.minerProcess!.forceStop(); } catch (e) { - print('MinerControls: Error force stopping miner process in dispose: $e'); + print( + 'MinerControls: Error force stopping miner process in dispose: $e', + ); } // Use GlobalMinerManager for comprehensive cleanup @@ -183,15 +203,24 @@ class _MinerControlsState extends State { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const Text('CPU Workers', style: TextStyle(fontWeight: FontWeight.bold)), + const Text( + 'CPU Workers', + style: TextStyle(fontWeight: FontWeight.bold), + ), Text('$_cpuWorkers'), ], ), Slider( value: _cpuWorkers.toDouble(), min: 0, - max: (Platform.numberOfProcessors > 0 ? Platform.numberOfProcessors : 16).toDouble(), - divisions: (Platform.numberOfProcessors > 0 ? Platform.numberOfProcessors : 16), + max: + (Platform.numberOfProcessors > 0 + ? Platform.numberOfProcessors + : 16) + .toDouble(), + divisions: (Platform.numberOfProcessors > 0 + ? Platform.numberOfProcessors + : 16), label: _cpuWorkers.toString(), onChanged: widget.minerProcess == null ? (value) { @@ -214,7 +243,10 @@ class _MinerControlsState extends State { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const Text('GPU Devices', style: TextStyle(fontWeight: FontWeight.bold)), + const Text( + 'GPU Devices', + style: TextStyle(fontWeight: FontWeight.bold), + ), Text('$_gpuDevices / $_detectedGpuCount'), ], ), @@ -238,13 +270,20 @@ class _MinerControlsState extends State { const SizedBox(height: 24), ElevatedButton( style: ElevatedButton.styleFrom( - backgroundColor: widget.minerProcess == null ? Colors.green : Colors.blue, + backgroundColor: widget.minerProcess == null + ? Colors.green + : Colors.blue, padding: const EdgeInsets.symmetric(vertical: 15), - textStyle: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + textStyle: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), minimumSize: const Size(200, 50), ), onPressed: _isAttemptingToggle ? null : _toggle, - child: Text(widget.minerProcess == null ? 'Start Mining' : 'Stop Mining'), + child: Text( + widget.minerProcess == null ? 'Start Mining' : 'Stop Mining', + ), ), ], ); diff --git a/miner-app/lib/features/miner/miner_dashboard_screen.dart b/miner-app/lib/features/miner/miner_dashboard_screen.dart index aac149c2..032b4067 100644 --- a/miner-app/lib/features/miner/miner_dashboard_screen.dart +++ b/miner-app/lib/features/miner/miner_dashboard_screen.dart @@ -126,7 +126,8 @@ class _MinerDashboardScreenState extends State { onProgress: (progress) { setState(() { if (progress.totalBytes > 0) { - _minerUpdateProgress = progress.downloadedBytes / progress.totalBytes; + _minerUpdateProgress = + progress.downloadedBytes / progress.totalBytes; } else { _minerUpdateProgress = progress.downloadedBytes > 0 ? 1.0 : 0.0; } @@ -188,7 +189,8 @@ class _MinerDashboardScreenState extends State { onProgress: (progress) { setState(() { if (progress.totalBytes > 0) { - _nodeUpdateProgress = progress.downloadedBytes / progress.totalBytes; + _nodeUpdateProgress = + progress.downloadedBytes / progress.totalBytes; } else { _nodeUpdateProgress = progress.downloadedBytes > 0 ? 1.0 : 0.0; } @@ -280,13 +282,20 @@ class _MinerDashboardScreenState extends State { // Logs section SliverToBoxAdapter( child: Padding( - padding: const EdgeInsets.only(left: 20, right: 20, bottom: 20), + padding: const EdgeInsets.only( + left: 20, + right: 20, + bottom: 20, + ), child: Container( height: 430, decoration: BoxDecoration( color: Colors.white.useOpacity(0.05), borderRadius: BorderRadius.circular(20), - border: Border.all(color: Colors.white.useOpacity(0.1), width: 1), + border: Border.all( + color: Colors.white.useOpacity(0.1), + width: 1, + ), ), child: Column( children: [ @@ -294,11 +303,20 @@ class _MinerDashboardScreenState extends State { Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - border: Border(bottom: BorderSide(color: Colors.white.useOpacity(0.1), width: 1)), + border: Border( + bottom: BorderSide( + color: Colors.white.useOpacity(0.1), + width: 1, + ), + ), ), child: Row( children: [ - Icon(Icons.terminal, color: Colors.white.useOpacity(0.7), size: 20), + Icon( + Icons.terminal, + color: Colors.white.useOpacity(0.7), + size: 20, + ), const SizedBox(width: 12), Text( 'Live Logs', @@ -312,7 +330,12 @@ class _MinerDashboardScreenState extends State { ), ), // Logs content - Expanded(child: LogsWidget(minerProcess: _currentMinerProcess, maxLines: 200)), + Expanded( + child: LogsWidget( + minerProcess: _currentMinerProcess, + maxLines: 200, + ), + ), ], ), ), diff --git a/miner-app/lib/features/miner/miner_stats_card.dart b/miner-app/lib/features/miner/miner_stats_card.dart index d9c9c3c3..b74d3a82 100644 --- a/miner-app/lib/features/miner/miner_stats_card.dart +++ b/miner-app/lib/features/miner/miner_stats_card.dart @@ -29,7 +29,10 @@ class _MinerStatsCardState extends State { return Container( padding: const EdgeInsets.all(40), margin: const EdgeInsets.only(bottom: 20), - decoration: BoxDecoration(color: Colors.white.useOpacity(0.05), borderRadius: BorderRadius.circular(20)), + decoration: BoxDecoration( + color: Colors.white.useOpacity(0.05), + borderRadius: BorderRadius.circular(20), + ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -38,11 +41,16 @@ class _MinerStatsCardState extends State { height: 20, child: CircularProgressIndicator( strokeWidth: 2, - valueColor: AlwaysStoppedAnimation(Colors.white.useOpacity(0.6)), + valueColor: AlwaysStoppedAnimation( + Colors.white.useOpacity(0.6), + ), ), ), const SizedBox(width: 16), - Text('Loading mining stats...', style: TextStyle(color: Colors.white.useOpacity(0.6), fontSize: 16)), + Text( + 'Loading mining stats...', + style: TextStyle(color: Colors.white.useOpacity(0.6), fontSize: 16), + ), ], ), ); @@ -61,7 +69,12 @@ class _MinerStatsCardState extends State { borderRadius: BorderRadius.circular(24), border: Border.all(color: Colors.white.useOpacity(0.1), width: 1), boxShadow: [ - BoxShadow(color: Colors.black.useOpacity(0.2), blurRadius: 20, spreadRadius: 1, offset: const Offset(0, 8)), + BoxShadow( + color: Colors.black.useOpacity(0.2), + blurRadius: 20, + spreadRadius: 1, + offset: const Offset(0, 8), + ), ], ), child: Padding( @@ -83,12 +96,20 @@ class _MinerStatsCardState extends State { ), borderRadius: BorderRadius.circular(14), ), - child: const Icon(Icons.analytics, color: Colors.white, size: 24), + child: const Icon( + Icons.analytics, + color: Colors.white, + size: 24, + ), ), const SizedBox(width: 16), Text( 'Mining Performance - ${_miningStats!.chainName}', - style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Colors.white.useOpacity(0.9)), + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Colors.white.useOpacity(0.9), + ), ), ], ), @@ -100,12 +121,17 @@ class _MinerStatsCardState extends State { Expanded( child: Column( children: [ - _buildCompactStat(icon: Icons.people, label: 'Peers', value: '${_miningStats!.peerCount}'), + _buildCompactStat( + icon: Icons.people, + label: 'Peers', + value: '${_miningStats!.peerCount}', + ), const SizedBox(height: 16), _buildDualStat( icon: Icons.memory, label1: 'CPU', - value1: '${_miningStats!.workers} / ${_miningStats!.cpuCapacity}', + value1: + '${_miningStats!.workers} / ${_miningStats!.cpuCapacity}', label2: 'GPU', value2: '${_miningStats!.gpuDevices} / ${_miningStats!.gpuCapacity > 0 ? _miningStats!.gpuCapacity : (_miningStats!.gpuDevices > 0 ? _miningStats!.gpuDevices : "-")}', @@ -127,7 +153,8 @@ class _MinerStatsCardState extends State { _buildCompactStat( icon: Icons.block, label: 'Block', - value: '${_miningStats!.currentBlock} / ${_miningStats!.targetBlock}', + value: + '${_miningStats!.currentBlock} / ${_miningStats!.targetBlock}', ), ], ), @@ -197,7 +224,11 @@ class _MinerStatsCardState extends State { ], ), const SizedBox(width: 8), - Container(width: 1, height: 28, color: Colors.white.useOpacity(0.3)), + Container( + width: 1, + height: 28, + color: Colors.white.useOpacity(0.3), + ), const SizedBox(width: 8), Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -232,7 +263,11 @@ class _MinerStatsCardState extends State { ); } - Widget _buildCompactStat({required IconData icon, required String label, required String value}) { + Widget _buildCompactStat({ + required IconData icon, + required String label, + required String value, + }) { return Row( children: [ Container( diff --git a/miner-app/lib/features/miner/miner_status.dart b/miner-app/lib/features/miner/miner_status.dart index 5afabe5f..48d63b27 100644 --- a/miner-app/lib/features/miner/miner_status.dart +++ b/miner-app/lib/features/miner/miner_status.dart @@ -16,7 +16,10 @@ class MinerStatus extends StatelessWidget { case MiningStatus.idle: return _StatusConfig( icon: Icons.pause_circle_outline, - colors: [const Color(0xFF64748B), const Color(0xFF475569)], // Slate gray + colors: [ + const Color(0xFF64748B), + const Color(0xFF475569), + ], // Slate gray glowColor: const Color(0xFF64748B), label: 'IDLE', ); @@ -80,7 +83,8 @@ class _StatusBadge extends StatefulWidget { State<_StatusBadge> createState() => _StatusBadgeState(); } -class _StatusBadgeState extends State<_StatusBadge> with TickerProviderStateMixin { +class _StatusBadgeState extends State<_StatusBadge> + with TickerProviderStateMixin { late AnimationController _rotationController; late AnimationController _pulseController; late Animation _pulseAnimation; @@ -90,16 +94,21 @@ class _StatusBadgeState extends State<_StatusBadge> with TickerProviderStateMixi super.initState(); // Rotation animation for syncing - _rotationController = AnimationController(duration: const Duration(seconds: 2), vsync: this); + _rotationController = AnimationController( + duration: const Duration(seconds: 2), + vsync: this, + ); // Pickaxe animation for mining (arcing back and forth) - _pulseController = AnimationController(duration: const Duration(milliseconds: 800), vsync: this); + _pulseController = AnimationController( + duration: const Duration(milliseconds: 800), + vsync: this, + ); // Arc rotation: -30 degrees to +30 degrees (in radians) - _pulseAnimation = Tween( - begin: -0.5, - end: 0.5, - ).animate(CurvedAnimation(parent: _pulseController, curve: Curves.easeInOut)); + _pulseAnimation = Tween(begin: -0.5, end: 0.5).animate( + CurvedAnimation(parent: _pulseController, curve: Curves.easeInOut), + ); _updateAnimations(); } @@ -152,7 +161,13 @@ class _StatusBadgeState extends State<_StatusBadge> with TickerProviderStateMixi end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(24), - boxShadow: [BoxShadow(color: widget.config.glowColor.useOpacity(0.4), blurRadius: 12, spreadRadius: 2)], + boxShadow: [ + BoxShadow( + color: widget.config.glowColor.useOpacity(0.4), + blurRadius: 12, + spreadRadius: 2, + ), + ], ), child: Row( mainAxisSize: MainAxisSize.min, @@ -164,8 +179,14 @@ class _StatusBadgeState extends State<_StatusBadge> with TickerProviderStateMixi ? (Matrix4.identity()..rotateZ(_pulseAnimation.value)) : Matrix4.identity(), child: RotationTransition( - turns: widget.config.isAnimated ? _rotationController : AlwaysStoppedAnimation(0), - child: Icon(widget.config.icon, color: Colors.white, size: 18), + turns: widget.config.isAnimated + ? _rotationController + : AlwaysStoppedAnimation(0), + child: Icon( + widget.config.icon, + color: Colors.white, + size: 18, + ), ), ), const SizedBox(width: 10), diff --git a/miner-app/lib/features/settings/settings_app_bar.dart b/miner-app/lib/features/settings/settings_app_bar.dart index 2402c167..323e0994 100644 --- a/miner-app/lib/features/settings/settings_app_bar.dart +++ b/miner-app/lib/features/settings/settings_app_bar.dart @@ -18,21 +18,37 @@ class _SettingsAppBarState extends State { floating: true, pinned: false, flexibleSpace: ClipRRect( - borderRadius: const BorderRadius.only(bottomLeft: Radius.circular(24), bottomRight: Radius.circular(24)), + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(24), + bottomRight: Radius.circular(24), + ), child: BackdropFilter( - filter: ColorFilter.mode(Colors.black.useOpacity(0.1), BlendMode.srcOver), + filter: ColorFilter.mode( + Colors.black.useOpacity(0.1), + BlendMode.srcOver, + ), child: Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, - colors: [Colors.white.useOpacity(0.1), Colors.white.useOpacity(0.05)], + colors: [ + Colors.white.useOpacity(0.1), + Colors.white.useOpacity(0.05), + ], + ), + border: Border( + bottom: BorderSide( + color: Colors.white.useOpacity(0.1), + width: 1, + ), ), - border: Border(bottom: BorderSide(color: Colors.white.useOpacity(0.1), width: 1)), ), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), - child: Center(child: Text('Settings', style: context.textTheme.titleMedium)), + child: Center( + child: Text('Settings', style: context.textTheme.titleMedium), + ), ), ), ), diff --git a/miner-app/lib/features/settings/settings_screen.dart b/miner-app/lib/features/settings/settings_screen.dart index 3d40bf27..17e541ce 100644 --- a/miner-app/lib/features/settings/settings_screen.dart +++ b/miner-app/lib/features/settings/settings_screen.dart @@ -70,7 +70,10 @@ class _SettingsScreenState extends State { SliverToBoxAdapter( child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 16.0), + padding: const EdgeInsets.symmetric( + horizontal: 20.0, + vertical: 16.0, + ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -135,14 +138,23 @@ class _SettingsScreenState extends State { color: const Color(0xFF1C1C1C), // Slightly lighter than background borderRadius: BorderRadius.circular(16), border: Border.all(color: Colors.white.useOpacity(0.05), width: 1), - boxShadow: [BoxShadow(color: Colors.black.useOpacity(0.2), blurRadius: 10, offset: const Offset(0, 4))], + boxShadow: [ + BoxShadow( + color: Colors.black.useOpacity(0.2), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], ), child: Row( children: [ // Icon Container Container( padding: const EdgeInsets.all(10), - decoration: BoxDecoration(color: accentColor.useOpacity(0.1), borderRadius: BorderRadius.circular(12)), + decoration: BoxDecoration( + color: accentColor.useOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), child: Icon(icon, color: accentColor, size: 20), ), const SizedBox(width: 16), @@ -151,7 +163,11 @@ class _SettingsScreenState extends State { Expanded( child: Text( title, - style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.w500), + style: const TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w500, + ), ), ), @@ -160,7 +176,10 @@ class _SettingsScreenState extends State { SizedBox( width: 16, height: 16, - child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white.useOpacity(0.3)), + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white.useOpacity(0.3), + ), ) else Container( diff --git a/miner-app/lib/features/setup/node_identity_setup_screen.dart b/miner-app/lib/features/setup/node_identity_setup_screen.dart index c58f6604..55bc4d29 100644 --- a/miner-app/lib/features/setup/node_identity_setup_screen.dart +++ b/miner-app/lib/features/setup/node_identity_setup_screen.dart @@ -8,7 +8,8 @@ class NodeIdentitySetupScreen extends StatefulWidget { const NodeIdentitySetupScreen({super.key}); @override - State createState() => _NodeIdentitySetupScreenState(); + State createState() => + _NodeIdentitySetupScreenState(); } class _NodeIdentitySetupScreenState extends State { @@ -88,7 +89,10 @@ class _NodeIdentitySetupScreenState extends State { children: [ const Icon(Icons.check_circle, color: Colors.green, size: 80), const SizedBox(height: 16), - const Text('Node Identity Set!', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)), + const Text( + 'Node Identity Set!', + style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), + ), const SizedBox(height: 24), ElevatedButton( onPressed: () { @@ -106,7 +110,10 @@ class _NodeIdentitySetupScreenState extends State { children: [ SvgPicture.asset('assets/logo/logo.svg', width: 80, height: 80), const SizedBox(height: 16), - const Text('Node Identity not set.', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)), + const Text( + 'Node Identity not set.', + style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), + ), const SizedBox(height: 8), const Text( 'You need to set a node identity to continue.', diff --git a/miner-app/lib/features/setup/node_setup_screen.dart b/miner-app/lib/features/setup/node_setup_screen.dart index 6e86da7c..a5665651 100644 --- a/miner-app/lib/features/setup/node_setup_screen.dart +++ b/miner-app/lib/features/setup/node_setup_screen.dart @@ -36,7 +36,8 @@ class _NodeSetupScreenState extends State { final String nodeBinaryPath = await BinaryManager.getNodeBinaryFilePath(); final bool nodeInstalled = await File(nodeBinaryPath).exists(); - final String minerBinaryPath = await BinaryManager.getExternalMinerBinaryFilePath(); + final String minerBinaryPath = + await BinaryManager.getExternalMinerBinaryFilePath(); final bool minerInstalled = await File(minerBinaryPath).exists(); setState(() { @@ -78,12 +79,15 @@ class _NodeSetupScreenState extends State { if (mounted) { setState(() { if (progress.totalBytes > 0) { - _downloadProgress = progress.downloadedBytes / progress.totalBytes; + _downloadProgress = + progress.downloadedBytes / progress.totalBytes; _downloadProgressText = "Node: ${(progress.downloadedBytes / (1024 * 1024)).toStringAsFixed(2)} MB / ${(progress.totalBytes / (1024 * 1024)).toStringAsFixed(2)} MB"; } else { _downloadProgress = progress.downloadedBytes > 0 ? 1.0 : 0.0; - _downloadProgressText = progress.downloadedBytes > 0 ? "Node Downloaded" : "Downloading Node..."; + _downloadProgressText = progress.downloadedBytes > 0 + ? "Node Downloaded" + : "Downloading Node..."; } }); } @@ -110,12 +114,15 @@ class _NodeSetupScreenState extends State { if (mounted) { setState(() { if (progress.totalBytes > 0) { - _downloadProgress = progress.downloadedBytes / progress.totalBytes; + _downloadProgress = + progress.downloadedBytes / progress.totalBytes; _downloadProgressText = "Miner: ${(progress.downloadedBytes / (1024 * 1024)).toStringAsFixed(2)} MB / ${(progress.totalBytes / (1024 * 1024)).toStringAsFixed(2)} MB"; } else { _downloadProgress = progress.downloadedBytes > 0 ? 1.0 : 0.0; - _downloadProgressText = progress.downloadedBytes > 0 ? "Miner Downloaded" : "Downloading Miner..."; + _downloadProgressText = progress.downloadedBytes > 0 + ? "Miner Downloaded" + : "Downloading Miner..."; } }); } @@ -147,14 +154,15 @@ class _NodeSetupScreenState extends State { }); } if (mounted) { - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text('Error installing binaries: ${e.toString()}'))); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Error installing binaries: ${e.toString()}')), + ); } } } - bool get _allBinariesInstalled => _isNodeInstalled && _isExternalMinerInstalled; + bool get _allBinariesInstalled => + _isNodeInstalled && _isExternalMinerInstalled; @override Widget build(BuildContext context) { @@ -164,13 +172,22 @@ class _NodeSetupScreenState extends State { bodyContent = Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Text('Installing Mining Software...', style: Theme.of(context).textTheme.headlineSmall), + Text( + 'Installing Mining Software...', + style: Theme.of(context).textTheme.headlineSmall, + ), const SizedBox(height: 8), - Text(_currentDownloadingBinary, style: Theme.of(context).textTheme.titleMedium), + Text( + _currentDownloadingBinary, + style: Theme.of(context).textTheme.titleMedium, + ), const SizedBox(height: 20), Padding( padding: const EdgeInsets.symmetric(horizontal: 40.0), - child: LinearProgressIndicator(value: _downloadProgress, minHeight: 10), + child: LinearProgressIndicator( + value: _downloadProgress, + minHeight: 10, + ), ), const SizedBox(height: 10), Text(_downloadProgressText), @@ -196,7 +213,10 @@ class _NodeSetupScreenState extends State { children: [ const Icon(Icons.check_circle, color: Colors.green, size: 80), const SizedBox(height: 16), - const Text('Mining Software Installed!', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)), + const Text( + 'Mining Software Installed!', + style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), + ), const SizedBox(height: 8), Column( children: [ @@ -237,7 +257,10 @@ class _NodeSetupScreenState extends State { children: [ SvgPicture.asset('assets/logo/logo.svg', width: 80, height: 80), const SizedBox(height: 16), - const Text('Mining software not found.', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)), + const Text( + 'Mining software not found.', + style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), + ), const SizedBox(height: 8), const Text( 'You need to install the node and external miner to continue.', @@ -279,7 +302,9 @@ class _NodeSetupScreenState extends State { ElevatedButton.icon( onPressed: _installBinaries, icon: const Icon(Icons.download), - label: Text(_allBinariesInstalled ? 'All Installed' : 'Install Mining Software'), + label: Text( + _allBinariesInstalled ? 'All Installed' : 'Install Mining Software', + ), style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), textStyle: const TextStyle(fontSize: 18), diff --git a/miner-app/lib/features/setup/rewards_address_setup_screen.dart b/miner-app/lib/features/setup/rewards_address_setup_screen.dart index 1bbcc6ad..bc5ab8bc 100644 --- a/miner-app/lib/features/setup/rewards_address_setup_screen.dart +++ b/miner-app/lib/features/setup/rewards_address_setup_screen.dart @@ -12,7 +12,8 @@ class RewardsAddressSetupScreen extends StatefulWidget { const RewardsAddressSetupScreen({super.key}); @override - State createState() => _RewardsAddressSetupScreenState(); + State createState() => + _RewardsAddressSetupScreenState(); } class _RewardsAddressSetupScreenState extends State { @@ -66,7 +67,10 @@ class _RewardsAddressSetupScreenState extends State { Future _saveRewardsAddress() async { final address = _addressController.text.trim(); if (address.isEmpty) { - context.showErrorSnackbar(title: 'Error', message: 'Please enter a valid address'); + context.showErrorSnackbar( + title: 'Error', + message: 'Please enter a valid address', + ); return; } @@ -82,16 +86,19 @@ class _RewardsAddressSetupScreenState extends State { print('Rewards address saved: $address'); if (mounted) { - ScaffoldMessenger.of( - context, - ).showSnackBar(const SnackBar(content: Text('Rewards address saved successfully!'))); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Rewards address saved successfully!')), + ); // Navigate to the main mining screen context.go('/miner_dashboard'); } } catch (e) { print('Error saving rewards address: $e'); if (mounted) { - context.showErrorSnackbar(title: 'Error', message: 'Error saving address: $e'); + context.showErrorSnackbar( + title: 'Error', + message: 'Error saving address: $e', + ); } } finally { if (mounted) { @@ -120,7 +127,11 @@ class _RewardsAddressSetupScreenState extends State { color: Colors.black87, borderRadius: BorderRadius.circular(16), boxShadow: [ - BoxShadow(color: Colors.black.useOpacity(0.5), blurRadius: 20, offset: const Offset(0, 10)), + BoxShadow( + color: Colors.black.useOpacity(0.5), + blurRadius: 20, + offset: const Offset(0, 10), + ), ], ), child: Column( @@ -144,7 +155,11 @@ class _RewardsAddressSetupScreenState extends State { top: 0, child: GestureDetector( onTap: () => Navigator.of(context).pop(), - child: const Icon(Icons.close, color: Colors.white, size: 24), + child: const Icon( + Icons.close, + color: Colors.white, + size: 24, + ), ), ), ], @@ -167,7 +182,11 @@ class _RewardsAddressSetupScreenState extends State { const Text( 'Scan with your mobile phone\nto set up your wallet', textAlign: TextAlign.center, - style: TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.w500), + style: TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w500, + ), ), const SizedBox(height: 24), OutlinedButton( @@ -175,7 +194,10 @@ class _RewardsAddressSetupScreenState extends State { style: OutlinedButton.styleFrom( foregroundColor: Colors.white, side: const BorderSide(color: Colors.white), - padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16), + padding: const EdgeInsets.symmetric( + horizontal: 32, + vertical: 16, + ), ), child: const Text('Close'), ), @@ -203,11 +225,18 @@ class _RewardsAddressSetupScreenState extends State { mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - SvgPicture.asset('assets/logo/logo.svg', width: 80, height: 80), + SvgPicture.asset( + 'assets/logo/logo.svg', + width: 80, + height: 80, + ), const SizedBox(height: 24), const Text( 'Add Rewards Account', - style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + ), textAlign: TextAlign.center, ), const SizedBox(height: 8), @@ -224,7 +253,9 @@ class _RewardsAddressSetupScreenState extends State { enableInteractiveSelection: true, onSubmitted: (_) => _saveRewardsAddress(), contextMenuBuilder: (context, editableTextState) { - return AdaptiveTextSelectionToolbar.editableText(editableTextState: editableTextState); + return AdaptiveTextSelectionToolbar.editableText( + editableTextState: editableTextState, + ); }, decoration: InputDecoration( labelText: 'Rewards Wallet Address', @@ -245,7 +276,9 @@ class _RewardsAddressSetupScreenState extends State { IconButton( icon: const Icon(Icons.paste), onPressed: () async { - final data = await Clipboard.getData(Clipboard.kTextPlain); + final data = await Clipboard.getData( + Clipboard.kTextPlain, + ); if (data?.text != null) { _addressController.text = data!.text!; } @@ -273,7 +306,10 @@ class _RewardsAddressSetupScreenState extends State { const Text( "Don't have an account?", textAlign: TextAlign.center, - style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), ), const SizedBox(height: 8), const Text( @@ -286,7 +322,9 @@ class _RewardsAddressSetupScreenState extends State { onPressed: _showQrOverlay, icon: const Icon(Icons.qr_code), label: const Text('Scan QR code to set up wallet'), - style: OutlinedButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 12)), + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 12), + ), ), ], ), diff --git a/miner-app/lib/main.dart b/miner-app/lib/main.dart index ae01d2be..cfca99bd 100644 --- a/miner-app/lib/main.dart +++ b/miner-app/lib/main.dart @@ -56,7 +56,10 @@ class GlobalMinerManager { } } -Future initialRedirect(BuildContext context, GoRouterState state) async { +Future initialRedirect( + BuildContext context, + GoRouterState state, +) async { final currentRoute = state.uri.toString(); print('initialRedirect'); @@ -79,7 +82,8 @@ Future initialRedirect(BuildContext context, GoRouterState state) async // Check 2: Node Identity Set bool isIdentitySet = false; try { - final identityPath = '${await BinaryManager.getQuantusHomeDirectoryPath()}/node_key.p2p'; + final identityPath = + '${await BinaryManager.getQuantusHomeDirectoryPath()}/node_key.p2p'; isIdentitySet = await File(identityPath).exists(); } catch (e) { print('Error checking node identity status: $e'); @@ -87,7 +91,9 @@ Future initialRedirect(BuildContext context, GoRouterState state) async } if (!isIdentitySet) { - return (currentRoute == '/node_identity_setup') ? null : '/node_identity_setup'; + return (currentRoute == '/node_identity_setup') + ? null + : '/node_identity_setup'; } // Check 3: Rewards Address Set @@ -102,7 +108,9 @@ Future initialRedirect(BuildContext context, GoRouterState state) async } if (!isRewardsAddressSet) { - return (currentRoute == '/rewards_address_setup') ? null : '/rewards_address_setup'; + return (currentRoute == '/rewards_address_setup') + ? null + : '/rewards_address_setup'; } // If all setup steps are complete, go to the miner dashboard @@ -117,12 +125,25 @@ final _router = GoRouter( path: '/', // Builder is not strictly necessary if initialLocation and redirect handle it, // but can be a fallback or initial loading screen. - builder: (context, state) => const Scaffold(body: Center(child: CircularProgressIndicator())), + builder: (context, state) => + const Scaffold(body: Center(child: CircularProgressIndicator())), + ), + GoRoute( + path: '/node_setup', + builder: (context, state) => const NodeSetupScreen(), + ), + GoRoute( + path: '/node_identity_setup', + builder: (context, state) => const NodeIdentitySetupScreen(), + ), + GoRoute( + path: '/rewards_address_setup', + builder: (context, state) => const RewardsAddressSetupScreen(), + ), + GoRoute( + path: '/miner_dashboard', + builder: (context, state) => const MinerDashboardScreen(), ), - GoRoute(path: '/node_setup', builder: (context, state) => const NodeSetupScreen()), - GoRoute(path: '/node_identity_setup', builder: (context, state) => const NodeIdentitySetupScreen()), - GoRoute(path: '/rewards_address_setup', builder: (context, state) => const RewardsAddressSetupScreen()), - GoRoute(path: '/miner_dashboard', builder: (context, state) => const MinerDashboardScreen()), ], ); @@ -191,13 +212,17 @@ class _MinerAppState extends State { void _onStateChanged(AppLifecycleState state) { print('App lifecycle state changed to: $state'); - if (state == AppLifecycleState.paused || state == AppLifecycleState.detached) { + if (state == AppLifecycleState.paused || + state == AppLifecycleState.detached) { print('App lifecycle: App backgrounded/detached, cleaning up...'); GlobalMinerManager.cleanup(); } } @override - Widget build(BuildContext context) => - MaterialApp.router(title: 'Quantus Miner', theme: ThemeData.dark(useMaterial3: true), routerConfig: _router); + Widget build(BuildContext context) => MaterialApp.router( + title: 'Quantus Miner', + theme: ThemeData.dark(useMaterial3: true), + routerConfig: _router, + ); } diff --git a/miner-app/lib/src/services/binary_manager.dart b/miner-app/lib/src/services/binary_manager.dart index 010b62c5..c88664cf 100644 --- a/miner-app/lib/src/services/binary_manager.dart +++ b/miner-app/lib/src/services/binary_manager.dart @@ -18,10 +18,15 @@ class BinaryVersion { BinaryVersion(this.version, this.checkedAt); - Map toJson() => {'version': version, 'checkedAt': checkedAt.toIso8601String()}; - - factory BinaryVersion.fromJson(Map json) => - BinaryVersion(json['version'] as String, DateTime.parse(json['checkedAt'] as String)); + Map toJson() => { + 'version': version, + 'checkedAt': checkedAt.toIso8601String(), + }; + + factory BinaryVersion.fromJson(Map json) => BinaryVersion( + json['version'] as String, + DateTime.parse(json['checkedAt'] as String), + ); } class BinaryUpdateInfo { @@ -30,7 +35,12 @@ class BinaryUpdateInfo { final String? latestVersion; final String? downloadUrl; - BinaryUpdateInfo({required this.updateAvailable, this.currentVersion, this.latestVersion, this.downloadUrl}); + BinaryUpdateInfo({ + required this.updateAvailable, + this.currentVersion, + this.latestVersion, + this.downloadUrl, + }); } class BinaryManager { @@ -127,7 +137,11 @@ class BinaryManager { } static Future getLatestNodeVersion() async { - final rel = await http.get(Uri.parse('https://api.github.com/repos/$_repoOwner/$_repoName/releases/latest')); + final rel = await http.get( + Uri.parse( + 'https://api.github.com/repos/$_repoOwner/$_repoName/releases/latest', + ), + ); if (rel.statusCode != 200) { throw Exception('Failed to fetch latest node version: ${rel.statusCode}'); @@ -137,10 +151,16 @@ class BinaryManager { } static Future getLatestMinerVersion() async { - final rel = await http.get(Uri.parse('https://api.github.com/repos/$_repoOwner/$_minerRepoName/releases/latest')); + final rel = await http.get( + Uri.parse( + 'https://api.github.com/repos/$_repoOwner/$_minerRepoName/releases/latest', + ), + ); if (rel.statusCode != 200) { - throw Exception('Failed to fetch latest miner version: ${rel.statusCode}'); + throw Exception( + 'Failed to fetch latest miner version: ${rel.statusCode}', + ); } return jsonDecode(rel.body)['tag_name'] as String; @@ -159,13 +179,18 @@ class BinaryManager { ); } - final updateAvailable = _isNewerVersion(currentVersion.version, latestVersion); + final updateAvailable = _isNewerVersion( + currentVersion.version, + latestVersion, + ); return BinaryUpdateInfo( updateAvailable: updateAvailable, currentVersion: currentVersion.version, latestVersion: latestVersion, - downloadUrl: updateAvailable ? _buildNodeDownloadUrl(latestVersion) : null, + downloadUrl: updateAvailable + ? _buildNodeDownloadUrl(latestVersion) + : null, ); } catch (e) { print('Error checking node update: $e'); @@ -186,13 +211,18 @@ class BinaryManager { ); } - final updateAvailable = _isNewerVersion(currentVersion.version, latestVersion); + final updateAvailable = _isNewerVersion( + currentVersion.version, + latestVersion, + ); return BinaryUpdateInfo( updateAvailable: updateAvailable, currentVersion: currentVersion.version, latestVersion: latestVersion, - downloadUrl: updateAvailable ? _buildMinerDownloadUrl(latestVersion) : null, + downloadUrl: updateAvailable + ? _buildMinerDownloadUrl(latestVersion) + : null, ); } catch (e) { print('Error checking miner update: $e'); @@ -225,7 +255,8 @@ class BinaryManager { // Force x86_64 on Windows to support x64 emulation on ARM devices // unless we specifically start releasing native ARM64 Windows binaries arch = 'x86_64'; - } else if (Platform.version.contains('arm64') || Platform.version.contains('aarch64')) { + } else if (Platform.version.contains('arm64') || + Platform.version.contains('aarch64')) { arch = 'aarch64'; } else { arch = 'x86_64'; @@ -240,7 +271,9 @@ class BinaryManager { static bool _isNewerVersion(String current, String latest) { // Remove 'v' prefix if present - final currentClean = current.startsWith('v') ? current.substring(1) : current; + final currentClean = current.startsWith('v') + ? current.substring(1) + : current; final latestClean = latest.startsWith('v') ? latest.substring(1) : latest; final currentParts = currentClean.split('.').map(int.tryParse).toList(); @@ -274,7 +307,9 @@ class BinaryManager { return await _downloadNodeBinary(onProgress: onProgress); } - static Future updateNodeBinary({void Function(DownloadProgress progress)? onProgress}) async { + static Future updateNodeBinary({ + void Function(DownloadProgress progress)? onProgress, + }) async { print('Updating node binary to latest version...'); final binPath = await getNodeBinaryFilePath(); @@ -291,7 +326,10 @@ class BinaryManager { try { // Download to temporary location first - final newBinary = await _downloadNodeBinary(onProgress: onProgress, isUpdate: true); + final newBinary = await _downloadNodeBinary( + onProgress: onProgress, + isUpdate: true, + ); // If download successful, replace the old binary if (await backupFile.exists()) { @@ -319,7 +357,11 @@ class BinaryManager { bool isUpdate = false, }) async { // Find latest tag on GitHub - final rel = await http.get(Uri.parse('https://api.github.com/repos/$_repoOwner/$_repoName/releases/latest')); + final rel = await http.get( + Uri.parse( + 'https://api.github.com/repos/$_repoOwner/$_repoName/releases/latest', + ), + ); final tag = jsonDecode(rel.body)['tag_name'] as String; print('found latest tag: $tag'); @@ -328,14 +370,17 @@ class BinaryManager { final target = _targetTriple(); final extension = Platform.isWindows ? "zip" : "tar.gz"; final asset = '$_binary-$tag-$target.$extension'; - final url = 'https://github.com/$_repoOwner/$_repoName/releases/download/$tag/$asset'; + final url = + 'https://github.com/$_repoOwner/$_repoName/releases/download/$tag/$asset'; // Download final cacheDir = await _getCacheDir(); final tgz = File(p.join(cacheDir.path, asset)); // Use temporary path for extraction during updates - final tempExtractDir = isUpdate ? Directory(p.join(cacheDir.path, 'temp_update')) : cacheDir; + final tempExtractDir = isUpdate + ? Directory(p.join(cacheDir.path, 'temp_update')) + : cacheDir; if (isUpdate && await tempExtractDir.exists()) { await tempExtractDir.delete(recursive: true); @@ -350,7 +395,9 @@ class BinaryManager { final response = await client.send(request); if (response.statusCode != 200) { - throw Exception('Failed to download binary: ${response.statusCode} ${response.reasonPhrase}'); + throw Exception( + 'Failed to download binary: ${response.statusCode} ${response.reasonPhrase}', + ); } final totalBytes = response.contentLength ?? -1; @@ -380,7 +427,10 @@ class BinaryManager { // Extract to temporary directory if updating await Process.run('tar', ['-xzf', tgz.path, '-C', tempExtractDir.path]); - final tempBinPath = p.join(tempExtractDir.path, _normalizeFilename(_binary)); + final tempBinPath = p.join( + tempExtractDir.path, + _normalizeFilename(_binary), + ); final finalBinPath = await getNodeBinaryFilePath(); if (!Platform.isWindows) await Process.run('chmod', ['+x', tempBinPath]); @@ -420,7 +470,9 @@ class BinaryManager { return await _downloadMinerBinary(onProgress: onProgress); } - static Future updateMinerBinary({void Function(DownloadProgress progress)? onProgress}) async { + static Future updateMinerBinary({ + void Function(DownloadProgress progress)? onProgress, + }) async { print('Updating miner binary to latest version...'); final binPath = await getExternalMinerBinaryFilePath(); @@ -437,7 +489,10 @@ class BinaryManager { try { // Download to temporary location first - final newBinary = await _downloadMinerBinary(onProgress: onProgress, isUpdate: true); + final newBinary = await _downloadMinerBinary( + onProgress: onProgress, + isUpdate: true, + ); // If download successful, replace the old binary if (await backupFile.exists()) { @@ -467,7 +522,8 @@ class BinaryManager { print('DEBUG: External miner binary download process starting...'); // Find latest tag on GitHub - final releaseUrl = 'https://api.github.com/repos/$_repoOwner/$_minerRepoName/releases/latest'; + final releaseUrl = + 'https://api.github.com/repos/$_repoOwner/$_minerRepoName/releases/latest'; print('DEBUG: Fetching latest release from: $releaseUrl'); final rel = await http.get(Uri.parse(releaseUrl)); @@ -495,7 +551,8 @@ class BinaryManager { // Force x86_64 on Windows to support x64 emulation on ARM devices // unless we specifically start releasing native ARM64 Windows binaries arch = 'x86_64'; - } else if (Platform.version.contains('arm64') || Platform.version.contains('aarch64')) { + } else if (Platform.version.contains('arm64') || + Platform.version.contains('aarch64')) { arch = 'aarch64'; } else { arch = 'x86_64'; @@ -507,7 +564,8 @@ class BinaryManager { print('DEBUG: Looking for asset: $asset'); - final url = 'https://github.com/$_repoOwner/$_minerRepoName/releases/download/$tag/$asset'; + final url = + 'https://github.com/$_repoOwner/$_minerRepoName/releases/download/$tag/$asset'; // Check if the asset exists in the release final assets = releaseData['assets'] as List; @@ -539,7 +597,9 @@ class BinaryManager { print('DEBUG: Download response status: ${response.statusCode}'); if (response.statusCode != 200) { - throw Exception('Failed to download external miner binary: ${response.statusCode} ${response.reasonPhrase}'); + throw Exception( + 'Failed to download external miner binary: ${response.statusCode} ${response.reasonPhrase}', + ); } final totalBytes = response.contentLength ?? -1; @@ -557,7 +617,9 @@ class BinaryManager { } } await tempBinaryFile.writeAsBytes(allBytes); - print('DEBUG: Downloaded ${allBytes.length} bytes to ${tempBinaryFile.path}'); + print( + 'DEBUG: Downloaded ${allBytes.length} bytes to ${tempBinaryFile.path}', + ); if (totalBytes > 0 && downloadedBytes < totalBytes) { onProgress?.call(DownloadProgress(totalBytes, totalBytes)); @@ -571,7 +633,10 @@ class BinaryManager { // Set executable permissions on temp file if (!Platform.isWindows) { print('DEBUG: Setting executable permissions on ${tempBinaryFile.path}'); - final chmodResult = await Process.run('chmod', ['+x', tempBinaryFile.path]); + final chmodResult = await Process.run('chmod', [ + '+x', + tempBinaryFile.path, + ]); print('DEBUG: chmod exit code: ${chmodResult.exitCode}'); if (chmodResult.exitCode != 0) { print('DEBUG: chmod stderr: ${chmodResult.stderr}'); @@ -600,8 +665,12 @@ class BinaryManager { // Save version info await _saveMinerVersion(tag); } else { - print('DEBUG: ERROR - External miner binary still not found at $binPath after download!'); - throw Exception('External miner binary not found after download at $binPath'); + print( + 'DEBUG: ERROR - External miner binary still not found at $binPath after download!', + ); + throw Exception( + 'External miner binary not found after download at $binPath', + ); } return binFile; @@ -619,7 +688,9 @@ class BinaryManager { if (await nodeKeyFile.exists()) { final stat = await nodeKeyFile.stat(); if (stat.size > 0) { - print('Node key file already exists and has content (size: ${stat.size} bytes)'); + print( + 'Node key file already exists and has content (size: ${stat.size} bytes)', + ); return nodeKeyFile; } } @@ -633,13 +704,20 @@ class BinaryManager { } try { - final processResult = await Process.run(nodeBinaryPath, ['key', 'generate-node-key', '--file', nodeKeyFile.path]); + final processResult = await Process.run(nodeBinaryPath, [ + 'key', + 'generate-node-key', + '--file', + nodeKeyFile.path, + ]); if (processResult.exitCode == 0) { if (await nodeKeyFile.exists()) { final stat = await nodeKeyFile.stat(); if (stat.size > 0) { - print('Successfully generated node key file: ${nodeKeyFile.path} (size: ${stat.size} bytes)'); + print( + 'Successfully generated node key file: ${nodeKeyFile.path} (size: ${stat.size} bytes)', + ); return nodeKeyFile; } else { throw Exception('Node key file was created but is empty'); @@ -658,15 +736,20 @@ class BinaryManager { } } - static String _normalizeFilename(String file) => Platform.isWindows ? "$file.exe" : file; + static String _normalizeFilename(String file) => + Platform.isWindows ? "$file.exe" : file; - static Future _getCacheDir() async => - Directory(p.join(await getQuantusHomeDirectoryPath(), 'bin')).create(recursive: true); + static Future _getCacheDir() async => Directory( + p.join(await getQuantusHomeDirectoryPath(), 'bin'), + ).create(recursive: true); - static String _home() => Platform.environment['HOME'] ?? Platform.environment['USERPROFILE']!; + static String _home() => + Platform.environment['HOME'] ?? Platform.environment['USERPROFILE']!; static String _targetTriple() { - final os = Platform.isMacOS ? 'apple-darwin' : (Platform.isWindows ? 'pc-windows-msvc' : 'unknown-linux-gnu'); + final os = Platform.isMacOS + ? 'apple-darwin' + : (Platform.isWindows ? 'pc-windows-msvc' : 'unknown-linux-gnu'); // Force x86_64 on Windows to ensure we download the x64 binary even on ARM devices // (since they can emulate x64, and we don't likely have a native ARM build for Windows yet) @@ -674,7 +757,11 @@ class BinaryManager { return 'x86_64-$os'; } - final arch = Platform.version.contains('arm64') || Platform.version.contains('aarch64') ? 'aarch64' : 'x86_64'; + final arch = + Platform.version.contains('arm64') || + Platform.version.contains('aarch64') + ? 'aarch64' + : 'x86_64'; return '$arch-$os'; } } diff --git a/miner-app/lib/src/services/chain_rpc_client.dart b/miner-app/lib/src/services/chain_rpc_client.dart index d5ee415d..5dcdf142 100644 --- a/miner-app/lib/src/services/chain_rpc_client.dart +++ b/miner-app/lib/src/services/chain_rpc_client.dart @@ -31,8 +31,10 @@ class ChainRpcClient { final http.Client _httpClient; int _requestId = 1; - ChainRpcClient({this.rpcUrl = 'http://127.0.0.1:9933', this.timeout = const Duration(seconds: 10)}) - : _httpClient = http.Client(); + ChainRpcClient({ + this.rpcUrl = 'http://127.0.0.1:9933', + this.timeout = const Duration(seconds: 10), + }) : _httpClient = http.Client(); /// Get comprehensive chain information Future getChainInfo() async { @@ -90,7 +92,8 @@ class ChainRpcClient { bool isSyncing = false; int? targetBlock; if (syncStateResult != null) { - if (syncStateResult['currentBlock'] != null && syncStateResult['highestBlock'] != null) { + if (syncStateResult['currentBlock'] != null && + syncStateResult['highestBlock'] != null) { final current = syncStateResult['currentBlock'] as int; final highest = syncStateResult['highestBlock'] as int; @@ -172,7 +175,9 @@ class ChainRpcClient { Future isSyncing() async { try { final syncState = await _rpcCall('system_syncState'); - if (syncState != null && syncState['currentBlock'] != null && syncState['highestBlock'] != null) { + if (syncState != null && + syncState['currentBlock'] != null && + syncState['highestBlock'] != null) { final current = syncState['currentBlock'] as int; final highest = syncState['highestBlock'] as int; return (highest - current) > 5; @@ -196,13 +201,22 @@ class ChainRpcClient { /// Execute a JSON-RPC call Future _rpcCall(String method, [List? params]) async { - final request = {'jsonrpc': '2.0', 'id': _requestId++, 'method': method, if (params != null) 'params': params}; + final request = { + 'jsonrpc': '2.0', + 'id': _requestId++, + 'method': method, + if (params != null) 'params': params, + }; // Only print RPC calls when debugging connection issues // print('DEBUG: Making RPC call: $method with request: ${json.encode(request)}'); final response = await _httpClient - .post(Uri.parse(rpcUrl), headers: {'Content-Type': 'application/json'}, body: json.encode(request)) + .post( + Uri.parse(rpcUrl), + headers: {'Content-Type': 'application/json'}, + body: json.encode(request), + ) .timeout(timeout); if (response.statusCode == 200) { @@ -216,7 +230,9 @@ class ChainRpcClient { } else { // Don't log connection errors during startup - they're expected if (response.statusCode != 0) { - print('DEBUG: RPC HTTP error for $method: ${response.statusCode} ${response.reasonPhrase}'); + print( + 'DEBUG: RPC HTTP error for $method: ${response.statusCode} ${response.reasonPhrase}', + ); } throw Exception('HTTP ${response.statusCode}: ${response.reasonPhrase}'); } @@ -246,7 +262,11 @@ class PollingChainRpcClient extends ChainRpcClient { void Function(ChainInfo info)? onChainInfoUpdate; void Function(String error)? onError; - PollingChainRpcClient({super.rpcUrl, super.timeout, this.pollInterval = const Duration(seconds: 3)}); + PollingChainRpcClient({ + super.rpcUrl, + super.timeout, + this.pollInterval = const Duration(seconds: 3), + }); /// Start polling for chain information void startPolling() { diff --git a/miner-app/lib/src/services/external_miner_api_client.dart b/miner-app/lib/src/services/external_miner_api_client.dart index b9d5b411..f717a1c7 100644 --- a/miner-app/lib/src/services/external_miner_api_client.dart +++ b/miner-app/lib/src/services/external_miner_api_client.dart @@ -49,7 +49,10 @@ class ExternalMinerApiClient { /// Start polling for metrics every second void startPolling() { _pollTimer?.cancel(); - _pollTimer = Timer.periodic(const Duration(seconds: 1), (_) => _pollMetrics()); + _pollTimer = Timer.periodic( + const Duration(seconds: 1), + (_) => _pollMetrics(), + ); } /// Stop polling for metrics @@ -64,7 +67,9 @@ class ExternalMinerApiClient { /// Get metrics from external miner Prometheus endpoint Future getMetrics() async { try { - final response = await _httpClient.get(Uri.parse(metricsUrl)).timeout(timeout); + final response = await _httpClient + .get(Uri.parse(metricsUrl)) + .timeout(timeout); if (response.statusCode == 200) { return _parsePrometheusMetrics(response.body); @@ -170,7 +175,9 @@ class ExternalMinerApiClient { /// Test if the external miner is reachable Future isReachable() async { try { - final response = await _httpClient.get(Uri.parse(baseUrl)).timeout(const Duration(seconds: 3)); + final response = await _httpClient + .get(Uri.parse(baseUrl)) + .timeout(const Duration(seconds: 3)); // Any response (even 404) means the server is running return response.statusCode >= 200 && response.statusCode < 500; @@ -182,7 +189,9 @@ class ExternalMinerApiClient { /// Test if the metrics endpoint is available Future isMetricsAvailable() async { try { - final response = await _httpClient.get(Uri.parse(metricsUrl)).timeout(const Duration(seconds: 3)); + final response = await _httpClient + .get(Uri.parse(metricsUrl)) + .timeout(const Duration(seconds: 3)); return response.statusCode == 200; } catch (e) { diff --git a/miner-app/lib/src/services/gpu_detection_service.dart b/miner-app/lib/src/services/gpu_detection_service.dart index 420d71b2..59f08344 100644 --- a/miner-app/lib/src/services/gpu_detection_service.dart +++ b/miner-app/lib/src/services/gpu_detection_service.dart @@ -38,7 +38,9 @@ class GpuDetectionService { // Failed. Check if we can extract the actual count from the error message to shortcut. // Message format: "❌ ERROR: Requested X GPU devices but only Y device(s) are available." final output = result.stdout.toString() + result.stderr.toString(); - final match = RegExp(r'only (\d+) device\(s\) are available').firstMatch(output); + final match = RegExp( + r'only (\d+) device\(s\) are available', + ).firstMatch(output); if (match != null) { final available = int.parse(match.group(1)!); return available; diff --git a/miner-app/lib/src/services/log_filter_service.dart b/miner-app/lib/src/services/log_filter_service.dart index a0ab0f04..1ace7136 100644 --- a/miner-app/lib/src/services/log_filter_service.dart +++ b/miner-app/lib/src/services/log_filter_service.dart @@ -5,7 +5,8 @@ class LogFilterService { final List criticalKeywordsDuringSync; LogFilterService({ - this.initialLinesToPrint = 50, // Increased initial lines to show more startup info + this.initialLinesToPrint = + 50, // Increased initial lines to show more startup info this.keywordsToWatch = const [ // Info level logs that users want to see by default 'info', @@ -67,16 +68,22 @@ class LogFilterService { final lowerLine = line.toLowerCase(); // Always print critical messages, regardless of sync state (after initial burst) - if (criticalKeywordsDuringSync.any((keyword) => lowerLine.contains(keyword.toLowerCase()))) { + if (criticalKeywordsDuringSync.any( + (keyword) => lowerLine.contains(keyword.toLowerCase()), + )) { return true; } if (isNodeSyncing) { // During sync, show info level logs and keywords (not just critical messages) - return keywordsToWatch.any((keyword) => lowerLine.contains(keyword.toLowerCase())); + return keywordsToWatch.any( + (keyword) => lowerLine.contains(keyword.toLowerCase()), + ); } else { // When synced (and after initial burst, and not critical), print if it matches normal keywords. - return keywordsToWatch.any((keyword) => lowerLine.contains(keyword.toLowerCase())); + return keywordsToWatch.any( + (keyword) => lowerLine.contains(keyword.toLowerCase()), + ); } } } diff --git a/miner-app/lib/src/services/miner_process.dart b/miner-app/lib/src/services/miner_process.dart index b8c65240..37cd0c0f 100644 --- a/miner-app/lib/src/services/miner_process.dart +++ b/miner-app/lib/src/services/miner_process.dart @@ -17,7 +17,11 @@ class LogEntry { final DateTime timestamp; final String source; // 'node', 'quantus-miner', 'error' - LogEntry({required this.message, required this.timestamp, required this.source}); + LogEntry({ + required this.message, + required this.timestamp, + required this.source, + }); @override String toString() { @@ -134,13 +138,16 @@ class MinerProcess { // Check if ports are available and cleanup if needed await _ensurePortsAvailable(); - final externalMinerBinPath = await BinaryManager.getExternalMinerBinaryFilePath(); + final externalMinerBinPath = + await BinaryManager.getExternalMinerBinaryFilePath(); await BinaryManager.ensureExternalMinerBinary(); final externalMinerBin = File(externalMinerBinPath); if (!await externalMinerBin.exists()) { - throw Exception('External miner binary not found at $externalMinerBinPath'); + throw Exception( + 'External miner binary not found at $externalMinerBinPath', + ); } // Start the external miner first with metrics enabled @@ -158,27 +165,40 @@ class MinerProcess { ]; try { - _externalMinerProcess = await Process.start(externalMinerBin.path, minerArgs); + _externalMinerProcess = await Process.start( + externalMinerBin.path, + minerArgs, + ); } catch (e) { throw Exception('Failed to start external miner: $e'); } // Set up external miner log handling - _externalMinerProcess!.stdout.transform(utf8.decoder).transform(const LineSplitter()).listen((line) { - final logEntry = LogEntry(message: line, timestamp: DateTime.now(), source: 'quantus-miner'); - _logsController.add(logEntry); - print('[ext-miner] $line'); - }); + _externalMinerProcess!.stdout + .transform(utf8.decoder) + .transform(const LineSplitter()) + .listen((line) { + final logEntry = LogEntry( + message: line, + timestamp: DateTime.now(), + source: 'quantus-miner', + ); + _logsController.add(logEntry); + print('[ext-miner] $line'); + }); - _externalMinerProcess!.stderr.transform(utf8.decoder).transform(const LineSplitter()).listen((line) { - final logEntry = LogEntry( - message: line, - timestamp: DateTime.now(), - source: line.isMinerError ? 'quantus-miner-error' : 'quantus-miner', - ); - _logsController.add(logEntry); - print('[ext-miner-err] $line'); - }); + _externalMinerProcess!.stderr + .transform(utf8.decoder) + .transform(const LineSplitter()) + .listen((line) { + final logEntry = LogEntry( + message: line, + timestamp: DateTime.now(), + source: line.isMinerError ? 'quantus-miner-error' : 'quantus-miner', + ); + _logsController.add(logEntry); + print('[ext-miner-err] $line'); + }); // Monitor external miner process exit _externalMinerProcess!.exitCode.then((exitCode) { @@ -208,7 +228,9 @@ class MinerProcess { try { final testClient = HttpClient(); testClient.connectionTimeout = const Duration(seconds: 5); - final request = await testClient.getUrl(Uri.parse('http://127.0.0.1:$externalMinerPort')); + final request = await testClient.getUrl( + Uri.parse('http://127.0.0.1:$externalMinerPort'), + ); final response = await request.close(); await response.drain(); // Consume the response testClient.close(); @@ -224,9 +246,13 @@ class MinerProcess { final nodeKeyFileFromFileSystem = await BinaryManager.getNodeKeyFile(); if (await nodeKeyFileFromFileSystem.exists()) { final stat = await nodeKeyFileFromFileSystem.stat(); - print('DEBUG: nodeKeyFileFromFileSystem (${nodeKeyFileFromFileSystem.path}) exists (size: ${stat.size} bytes)'); + print( + 'DEBUG: nodeKeyFileFromFileSystem (${nodeKeyFileFromFileSystem.path}) exists (size: ${stat.size} bytes)', + ); } else { - print('DEBUG: nodeKeyFileFromFileSystem (${nodeKeyFileFromFileSystem.path}) does not exist.'); + print( + 'DEBUG: nodeKeyFileFromFileSystem (${nodeKeyFileFromFileSystem.path}) does not exist.', + ); } if (!await identityPath.exists()) { @@ -243,7 +269,9 @@ class MinerProcess { rewardsAddress = rewardsAddress.trim(); // Remove any whitespace/newlines print('DEBUG: Read rewards address from file: $rewardsAddress'); } catch (e) { - throw Exception('Failed to read rewards address from file ${rewardsPath.path}: $e'); + throw Exception( + 'Failed to read rewards address from file ${rewardsPath.path}: $e', + ); } final List args = [ @@ -298,7 +326,10 @@ class MinerProcess { // Start Prometheus polling for target block (every 3 seconds) _syncStatusTimer?.cancel(); - _syncStatusTimer = Timer.periodic(const Duration(seconds: 3), (timer) => syncBlockTargetWithPrometheusMetrics()); + _syncStatusTimer = Timer.periodic( + const Duration(seconds: 3), + (timer) => syncBlockTargetWithPrometheusMetrics(), + ); // Start external miner API polling (every second) _externalMinerApiClient.startPolling(); @@ -310,9 +341,15 @@ class MinerProcess { void processLogLine(String line, String streamType) { bool shouldPrint; if (streamType == 'stdout') { - shouldPrint = _stdoutFilter.shouldPrintLine(line, isNodeSyncing: _statsService.currentStats.isSyncing); + shouldPrint = _stdoutFilter.shouldPrintLine( + line, + isNodeSyncing: _statsService.currentStats.isSyncing, + ); } else { - shouldPrint = _stderrFilter.shouldPrintLine(line, isNodeSyncing: _statsService.currentStats.isSyncing); + shouldPrint = _stderrFilter.shouldPrintLine( + line, + isNodeSyncing: _statsService.currentStats.isSyncing, + ); } if (shouldPrint) { @@ -325,19 +362,29 @@ class MinerProcess { source = 'node'; } - final logEntry = LogEntry(message: line, timestamp: DateTime.now(), source: source); + final logEntry = LogEntry( + message: line, + timestamp: DateTime.now(), + source: source, + ); _logsController.add(logEntry); print(source == 'node' ? '[node] $line' : '[node-error] $line'); } } - _nodeProcess.stdout.transform(utf8.decoder).transform(const LineSplitter()).listen((line) { - processLogLine(line, 'stdout'); - }); + _nodeProcess.stdout + .transform(utf8.decoder) + .transform(const LineSplitter()) + .listen((line) { + processLogLine(line, 'stdout'); + }); - _nodeProcess.stderr.transform(utf8.decoder).transform(const LineSplitter()).listen((line) { - processLogLine(line, 'stderr'); - }); + _nodeProcess.stderr + .transform(utf8.decoder) + .transform(const LineSplitter()) + .listen((line) { + processLogLine(line, 'stderr'); + }); } void stop() { @@ -349,7 +396,9 @@ class MinerProcess { // Kill external miner process first if (_externalMinerProcess != null) { try { - print('MinerProcess: Attempting to kill external miner process (PID: ${_externalMinerProcess!.pid})'); + print( + 'MinerProcess: Attempting to kill external miner process (PID: ${_externalMinerProcess!.pid})', + ); // Try graceful termination first _externalMinerProcess!.kill(ProcessSignal.sigterm); @@ -359,7 +408,9 @@ class MinerProcess { // Check if process is still running and force kill if necessary try { if (await _isProcessRunning(_externalMinerProcess!.pid)) { - print('MinerProcess: External miner still running, force killing...'); + print( + 'MinerProcess: External miner still running, force killing...', + ); _externalMinerProcess!.kill(ProcessSignal.sigkill); } } catch (e) { @@ -373,14 +424,18 @@ class MinerProcess { try { _externalMinerProcess!.kill(ProcessSignal.sigkill); } catch (e2) { - print('MinerProcess: Error force killing external miner process: $e2'); + print( + 'MinerProcess: Error force killing external miner process: $e2', + ); } } } // Kill node process try { - print('MinerProcess: Attempting to kill node process (PID: ${_nodeProcess.pid})'); + print( + 'MinerProcess: Attempting to kill node process (PID: ${_nodeProcess.pid})', + ); // Try graceful termination first _nodeProcess.kill(ProcessSignal.sigterm); @@ -478,33 +533,54 @@ class MinerProcess { print('MinerProcess: Force killing $processName process (PID: $pid)'); if (Platform.isWindows) { - final killResult = await Process.run('taskkill', ['/F', '/PID', pid.toString()]); + final killResult = await Process.run('taskkill', [ + '/F', + '/PID', + pid.toString(), + ]); if (killResult.exitCode == 0) { - print('MinerProcess: Successfully force killed $processName (PID: $pid)'); + print( + 'MinerProcess: Successfully force killed $processName (PID: $pid)', + ); } else { - print('MinerProcess: taskkill failed for $processName (PID: $pid), exit code: ${killResult.exitCode}'); + print( + 'MinerProcess: taskkill failed for $processName (PID: $pid), exit code: ${killResult.exitCode}', + ); } await Future.delayed(const Duration(milliseconds: 500)); // Verify - final checkResult = await Process.run('tasklist', ['/FI', 'PID eq $pid']); + final checkResult = await Process.run('tasklist', [ + '/FI', + 'PID eq $pid', + ]); if (checkResult.stdout.toString().contains(' $pid ')) { - print('MinerProcess: WARNING - $processName (PID: $pid) may still be running'); + print( + 'MinerProcess: WARNING - $processName (PID: $pid) may still be running', + ); // Try by name as last resort - final binaryName = processName.contains('miner') ? 'quantus-miner.exe' : 'quantus-node.exe'; + final binaryName = processName.contains('miner') + ? 'quantus-miner.exe' + : 'quantus-node.exe'; await Process.run('taskkill', ['/F', '/IM', binaryName]); } else { - print('MinerProcess: Verified $processName (PID: $pid) is terminated'); + print( + 'MinerProcess: Verified $processName (PID: $pid) is terminated', + ); } } else { // First try SIGKILL via kill command for better reliability final killResult = await Process.run('kill', ['-9', pid.toString()]); if (killResult.exitCode == 0) { - print('MinerProcess: Successfully force killed $processName (PID: $pid)'); + print( + 'MinerProcess: Successfully force killed $processName (PID: $pid)', + ); } else { - print('MinerProcess: kill command failed for $processName (PID: $pid), exit code: ${killResult.exitCode}'); + print( + 'MinerProcess: kill command failed for $processName (PID: $pid), exit code: ${killResult.exitCode}', + ); } // Wait a moment then verify the process is dead @@ -512,11 +588,19 @@ class MinerProcess { final checkResult = await Process.run('kill', ['-0', pid.toString()]); if (checkResult.exitCode != 0) { - print('MinerProcess: Verified $processName (PID: $pid) is terminated'); + print( + 'MinerProcess: Verified $processName (PID: $pid) is terminated', + ); } else { - print('MinerProcess: WARNING - $processName (PID: $pid) may still be running'); + print( + 'MinerProcess: WARNING - $processName (PID: $pid) may still be running', + ); // Try pkill as last resort - await Process.run('pkill', ['-9', '-f', processName.contains('miner') ? 'quantus-miner' : 'quantus-node']); + await Process.run('pkill', [ + '-9', + '-f', + processName.contains('miner') ? 'quantus-miner' : 'quantus-node', + ]); } } } catch (e) { @@ -594,7 +678,9 @@ class MinerProcess { await Future.delayed(const Duration(seconds: 1)); if (await _isPortInUse(externalMinerPort)) { - throw Exception('Port $externalMinerPort is still in use after cleanup attempt'); + throw Exception( + 'Port $externalMinerPort is still in use after cleanup attempt', + ); } } @@ -633,7 +719,8 @@ class MinerProcess { try { if (Platform.isWindows) { final result = await Process.run('netstat', ['-ano']); - return result.exitCode == 0 && result.stdout.toString().contains(':$port'); + return result.exitCode == 0 && + result.stdout.toString().contains(':$port'); } else { final result = await Process.run('lsof', ['-i', ':$port']); return result.exitCode == 0 && result.stdout.toString().isNotEmpty; @@ -718,7 +805,10 @@ class MinerProcess { await Future.delayed(const Duration(seconds: 1)); // Check if still running, force kill if needed - final checkResult = await Process.run('kill', ['-0', pid.trim()]); + final checkResult = await Process.run('kill', [ + '-0', + pid.trim(), + ]); if (checkResult.exitCode == 0) { await Process.run('kill', ['-9', pid.trim()]); } @@ -760,7 +850,10 @@ class MinerProcess { await Future.delayed(const Duration(seconds: 2)); // Check if still running, force kill if needed - final checkResult = await Process.run('kill', ['-0', pid.trim()]); + final checkResult = await Process.run('kill', [ + '-0', + pid.trim(), + ]); if (checkResult.exitCode == 0) { await Process.run('kill', ['-9', pid.trim()]); } @@ -864,7 +957,9 @@ class MinerProcess { } attempts++; - print('DEBUG: Node RPC not ready yet (attempt $attempts/$maxAttempts), waiting ${delay.inSeconds}s...'); + print( + 'DEBUG: Node RPC not ready yet (attempt $attempts/$maxAttempts), waiting ${delay.inSeconds}s...', + ); await Future.delayed(delay); @@ -874,13 +969,17 @@ class MinerProcess { } } - print('DEBUG: Failed to connect to node RPC after $maxAttempts attempts. Will retry with polling...'); + print( + 'DEBUG: Failed to connect to node RPC after $maxAttempts attempts. Will retry with polling...', + ); // Start polling anyway - the error handling in RPC client will manage failures _chainRpcClient.startPolling(); } void _handleChainInfoUpdate(ChainInfo info) { - print('DEBUG: Successfully received chain info - Peers: ${info.peerCount}, Block: ${info.currentBlock}'); + print( + 'DEBUG: Successfully received chain info - Peers: ${info.peerCount}, Block: ${info.currentBlock}', + ); // Update peer count from RPC (most accurate) if (info.peerCount >= 0) { @@ -892,7 +991,11 @@ class MinerProcess { _statsService.updateChainName(info.chainName); // Always update current block and target block from RPC (most authoritative) - _statsService.setSyncingState(info.isSyncing, info.currentBlock, info.targetBlock ?? info.currentBlock); + _statsService.setSyncingState( + info.isSyncing, + info.currentBlock, + info.targetBlock ?? info.currentBlock, + ); print( 'DEBUG: Updated blocks - current: ${info.currentBlock}, target: ${info.targetBlock ?? info.currentBlock}, syncing: ${info.isSyncing}, chain: ${info.chainName}', ); diff --git a/miner-app/lib/src/services/miner_settings_service.dart b/miner-app/lib/src/services/miner_settings_service.dart index a6f873cf..5fa5fb9b 100644 --- a/miner-app/lib/src/services/miner_settings_service.dart +++ b/miner-app/lib/src/services/miner_settings_service.dart @@ -74,7 +74,8 @@ class MinerSettingsService { // 4. Delete external miner binary try { - final minerBinaryPath = await BinaryManager.getExternalMinerBinaryFilePath(); + final minerBinaryPath = + await BinaryManager.getExternalMinerBinaryFilePath(); final minerFile = File(minerBinaryPath); if (await minerFile.exists()) { await minerFile.delete(); @@ -106,7 +107,9 @@ class MinerSettingsService { final binDir = Directory('$quantusHome/bin'); if (await binDir.exists()) { // Remove any leftover tar.gz files - final tarFiles = binDir.listSync().where((file) => file.path.endsWith('.tar.gz')); + final tarFiles = binDir.listSync().where( + (file) => file.path.endsWith('.tar.gz'), + ); for (var file in tarFiles) { await file.delete(); print('✅ Cleaned up archive: ${file.path}'); diff --git a/miner-app/lib/src/services/prometheus_service.dart b/miner-app/lib/src/services/prometheus_service.dart index ab5629c2..9ade835f 100644 --- a/miner-app/lib/src/services/prometheus_service.dart +++ b/miner-app/lib/src/services/prometheus_service.dart @@ -8,7 +8,12 @@ class PrometheusMetrics { final int? targetBlock; final int? peerCount; - PrometheusMetrics({required this.isMajorSyncing, this.bestBlock, this.targetBlock, this.peerCount}); + PrometheusMetrics({ + required this.isMajorSyncing, + this.bestBlock, + this.targetBlock, + this.peerCount, + }); @override String toString() { @@ -23,7 +28,9 @@ class PrometheusService { Future fetchMetrics() async { try { - final response = await http.get(Uri.parse(metricsUrl)).timeout(const Duration(seconds: 3)); + final response = await http + .get(Uri.parse(metricsUrl)) + .timeout(const Duration(seconds: 3)); if (response.statusCode == 200) { final lines = response.body.split('\n'); @@ -44,13 +51,17 @@ class PrometheusService { if (parts.length == 2) { bestBlock = int.tryParse(parts[1]); } - } else if (line.startsWith('substrate_block_height{status="sync_target"')) { + } else if (line.startsWith( + 'substrate_block_height{status="sync_target"', + )) { final parts = line.split(' '); if (parts.length == 2) { targetBlock = int.tryParse(parts[1]); } } else if (line.startsWith('substrate_sub_libp2p_peers_count ') || - line.startsWith('substrate_sub_libp2p_kademlia_query_duration_count ') || + line.startsWith( + 'substrate_sub_libp2p_kademlia_query_duration_count ', + ) || line.contains('substrate_sub_libp2p_connections_opened_total') || line.contains('substrate_peerset_num_discovered_peers')) { // Try various peer-related metrics @@ -69,7 +80,9 @@ class PrometheusService { if (bestBlock != null && targetBlock != null && (targetBlock - bestBlock) > 5 && - !lines.any((l) => l.startsWith('substrate_sub_libp2p_is_major_syncing'))) { + !lines.any( + (l) => l.startsWith('substrate_sub_libp2p_is_major_syncing'), + )) { // If the specific major sync metric isn't there, but there's a clear block difference, // infer syncing state. isSyncing = true; diff --git a/miner-app/lib/src/shared/extensions/snackbar_extensions.dart b/miner-app/lib/src/shared/extensions/snackbar_extensions.dart index 28667de2..faaf24b3 100644 --- a/miner-app/lib/src/shared/extensions/snackbar_extensions.dart +++ b/miner-app/lib/src/shared/extensions/snackbar_extensions.dart @@ -13,11 +13,17 @@ extension SnackbarExtensions on BuildContext { await sh.showCopySnackbar(this, title: title, message: message); } - Future showWarningSnackbar({required String title, required String message}) async { + Future showWarningSnackbar({ + required String title, + required String message, + }) async { await sh.showWarningSnackbar(this, title: title, message: message); } - Future showErrorSnackbar({required String title, required String message}) async { + Future showErrorSnackbar({ + required String title, + required String message, + }) async { await sh.showErrorSnackbar(this, title: title, message: message); } } diff --git a/miner-app/lib/src/ui/logs_widget.dart b/miner-app/lib/src/ui/logs_widget.dart index f86ed0d3..6a55efc1 100644 --- a/miner-app/lib/src/ui/logs_widget.dart +++ b/miner-app/lib/src/ui/logs_widget.dart @@ -108,18 +108,35 @@ class _LogsWidgetState extends State { padding: const EdgeInsets.all(8.0), decoration: BoxDecoration( color: Theme.of(context).primaryColor.useOpacity(0.1), - borderRadius: const BorderRadius.only(topLeft: Radius.circular(12), topRight: Radius.circular(12)), + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(12), + topRight: Radius.circular(12), + ), ), child: Row( children: [ - const Text('Live Logs', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), + const Text( + 'Live Logs', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), const Spacer(), IconButton( - icon: Icon(_autoScroll ? Icons.vertical_align_bottom : Icons.vertical_align_top, size: 20), + icon: Icon( + _autoScroll + ? Icons.vertical_align_bottom + : Icons.vertical_align_top, + size: 20, + ), onPressed: _toggleAutoScroll, - tooltip: _autoScroll ? 'Disable auto-scroll' : 'Enable auto-scroll', + tooltip: _autoScroll + ? 'Disable auto-scroll' + : 'Enable auto-scroll', + ), + IconButton( + icon: const Icon(Icons.clear, size: 20), + onPressed: _clearLogs, + tooltip: 'Clear logs', ), - IconButton(icon: const Icon(Icons.clear, size: 20), onPressed: _clearLogs, tooltip: 'Clear logs'), ], ), ), @@ -133,7 +150,10 @@ class _LogsWidgetState extends State { child: Text( 'No logs available\nStart mining to see live logs', textAlign: TextAlign.center, - style: TextStyle(color: Colors.grey, fontStyle: FontStyle.italic), + style: TextStyle( + color: Colors.grey, + fontStyle: FontStyle.italic, + ), ), ) : ListView.builder( @@ -150,8 +170,15 @@ class _LogsWidgetState extends State { SizedBox( width: 80, child: Text( - log.timestamp.toIso8601String().substring(11, 19), - style: TextStyle(fontSize: 12, color: Colors.grey[600], fontFamily: 'monospace'), + log.timestamp.toIso8601String().substring( + 11, + 19, + ), + style: TextStyle( + fontSize: 12, + color: Colors.grey[600], + fontFamily: 'monospace', + ), ), ), @@ -160,7 +187,10 @@ class _LogsWidgetState extends State { width: 12, height: 12, margin: const EdgeInsets.only(right: 8, top: 2), - decoration: BoxDecoration(color: _getLogColor(log.source), shape: BoxShape.circle), + decoration: BoxDecoration( + color: _getLogColor(log.source), + shape: BoxShape.circle, + ), ), // Source label @@ -180,7 +210,11 @@ class _LogsWidgetState extends State { Expanded( child: SelectableText( log.message, - style: const TextStyle(fontSize: 12, fontFamily: 'monospace', height: 1.2), + style: const TextStyle( + fontSize: 12, + fontFamily: 'monospace', + height: 1.2, + ), ), ), ], @@ -196,19 +230,32 @@ class _LogsWidgetState extends State { padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), decoration: BoxDecoration( color: Theme.of(context).primaryColor.useOpacity(0.05), - borderRadius: const BorderRadius.only(bottomLeft: Radius.circular(12), bottomRight: Radius.circular(12)), + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(12), + bottomRight: Radius.circular(12), + ), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text('Total logs: ${_logs.length}', style: TextStyle(fontSize: 12, color: Colors.grey[600])), + Text( + 'Total logs: ${_logs.length}', + style: TextStyle(fontSize: 12, color: Colors.grey[600]), + ), if (widget.minerProcess != null) Text( 'Live', - style: TextStyle(fontSize: 12, color: Colors.green, fontWeight: FontWeight.w500), + style: TextStyle( + fontSize: 12, + color: Colors.green, + fontWeight: FontWeight.w500, + ), ) else - Text('Not connected', style: TextStyle(fontSize: 12, color: Colors.grey)), + Text( + 'Not connected', + style: TextStyle(fontSize: 12, color: Colors.grey), + ), ], ), ), diff --git a/miner-app/lib/src/ui/snackbar_helper.dart b/miner-app/lib/src/ui/snackbar_helper.dart index fa971978..942355b1 100644 --- a/miner-app/lib/src/ui/snackbar_helper.dart +++ b/miner-app/lib/src/ui/snackbar_helper.dart @@ -41,11 +41,19 @@ Future showTopSnackBar( ); } -Future showCopySnackbar(BuildContext context, {required String title, required String message}) async { +Future showCopySnackbar( + BuildContext context, { + required String title, + required String message, +}) async { await showTopSnackBar(context, title: title, message: message); } -Future showWarningSnackbar(BuildContext context, {required String title, required String message}) async { +Future showWarningSnackbar( + BuildContext context, { + required String title, + required String message, +}) async { await showTopSnackBar( context, title: title, @@ -54,7 +62,11 @@ Future showWarningSnackbar(BuildContext context, {required String title, r ); } -Future showErrorSnackbar(BuildContext context, {required String title, required String message}) async { +Future showErrorSnackbar( + BuildContext context, { + required String title, + required String message, +}) async { await showTopSnackBar( context, title: title, diff --git a/miner-app/lib/src/ui/top_snackbar_content.dart b/miner-app/lib/src/ui/top_snackbar_content.dart index 98510740..fa9a4e8e 100644 --- a/miner-app/lib/src/ui/top_snackbar_content.dart +++ b/miner-app/lib/src/ui/top_snackbar_content.dart @@ -7,7 +7,12 @@ class TopSnackBarContent extends StatelessWidget { final String message; final Icon? icon; - const TopSnackBarContent({super.key, required this.title, required this.message, this.icon}); + const TopSnackBarContent({ + super.key, + required this.title, + required this.message, + this.icon, + }); @override Widget build(BuildContext context) { @@ -20,7 +25,11 @@ class TopSnackBarContent extends StatelessWidget { shape: OvalBorder(), // Use OvalBorder for circle ), alignment: Alignment.center, - child: Icon(icon?.icon ?? Icons.check, color: icon?.color ?? Colors.white, size: 24), // Default check icon + child: Icon( + icon?.icon ?? Icons.check, + color: icon?.color ?? Colors.white, + size: 24, + ), // Default check icon ); return Container( @@ -33,7 +42,9 @@ class TopSnackBarContent extends StatelessWidget { side: BorderSide(color: Colors.white.useOpacity(0.1), width: 1), ), // Optional shadow for better visibility - shadows: const [BoxShadow(color: Colors.black26, blurRadius: 4, offset: Offset(0, 2))], + shadows: const [ + BoxShadow(color: Colors.black26, blurRadius: 4, offset: Offset(0, 2)), + ], ), child: Row( mainAxisAlignment: MainAxisAlignment.start, diff --git a/miner-app/lib/src/ui/update_banner.dart b/miner-app/lib/src/ui/update_banner.dart index 3db69c67..837ac867 100644 --- a/miner-app/lib/src/ui/update_banner.dart +++ b/miner-app/lib/src/ui/update_banner.dart @@ -29,7 +29,13 @@ class UpdateBanner extends StatelessWidget { width: double.infinity, decoration: BoxDecoration( color: backgroundColor ?? Colors.blue.shade500, - boxShadow: [BoxShadow(color: Colors.black.useOpacity(0.1), blurRadius: 4, offset: const Offset(0, 2))], + boxShadow: [ + BoxShadow( + color: Colors.black.useOpacity(0.1), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], ), child: SafeArea( bottom: false, @@ -37,7 +43,11 @@ class UpdateBanner extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Row( children: [ - Icon(icon ?? Icons.download, color: textColor ?? Colors.white, size: 24), + Icon( + icon ?? Icons.download, + color: textColor ?? Colors.white, + size: 24, + ), const SizedBox(width: 12), Expanded( child: Column( @@ -46,35 +56,57 @@ class UpdateBanner extends StatelessWidget { children: [ Text( message, - style: TextStyle(color: textColor ?? Colors.white, fontSize: 14, fontWeight: FontWeight.w600), + style: TextStyle( + color: textColor ?? Colors.white, + fontSize: 14, + fontWeight: FontWeight.w600, + ), ), const SizedBox(height: 2), Text( 'Version $version', - style: TextStyle(color: (textColor ?? Colors.white).useOpacity(0.9), fontSize: 12), + style: TextStyle( + color: (textColor ?? Colors.white).useOpacity(0.9), + fontSize: 12, + ), ), ], ), ), const SizedBox(width: 8), if (updateProgress != null) - SizedBox(width: 100, child: LinearProgressIndicator(value: updateProgress)) + SizedBox( + width: 100, + child: LinearProgressIndicator(value: updateProgress), + ) else ElevatedButton( onPressed: onUpdate, style: ElevatedButton.styleFrom( backgroundColor: Colors.white, foregroundColor: Colors.black, - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + ), + child: const Text( + 'Update', + style: TextStyle(fontWeight: FontWeight.bold), ), - child: const Text('Update', style: TextStyle(fontWeight: FontWeight.bold)), ), if (onDismiss != null && updateProgress == null) ...[ const SizedBox(width: 8), IconButton( onPressed: onDismiss, - icon: Icon(Icons.close, color: textColor ?? Colors.white, size: 20), + icon: Icon( + Icons.close, + color: textColor ?? Colors.white, + size: 20, + ), padding: EdgeInsets.zero, constraints: const BoxConstraints(), ), From 0f46e15a43aa8f3fd27e2d0091d8e9e1304142e1 Mon Sep 17 00:00:00 2001 From: illuzen Date: Sat, 31 Jan 2026 10:32:27 +0800 Subject: [PATCH 02/10] quic --- .../services/external_miner_api_client.dart | 16 - miner-app/lib/src/services/miner_process.dart | 283 ++++++++---------- 2 files changed, 131 insertions(+), 168 deletions(-) diff --git a/miner-app/lib/src/services/external_miner_api_client.dart b/miner-app/lib/src/services/external_miner_api_client.dart index f717a1c7..2e8fd1a8 100644 --- a/miner-app/lib/src/services/external_miner_api_client.dart +++ b/miner-app/lib/src/services/external_miner_api_client.dart @@ -28,7 +28,6 @@ class ExternalMinerMetrics { } class ExternalMinerApiClient { - final String baseUrl; final String metricsUrl; final Duration timeout; final http.Client _httpClient; @@ -40,7 +39,6 @@ class ExternalMinerApiClient { void Function(String error)? onError; ExternalMinerApiClient({ - this.baseUrl = 'http://127.0.0.1:9833', String? metricsUrl, this.timeout = const Duration(seconds: 5), }) : metricsUrl = metricsUrl ?? 'http://127.0.0.1:9900/metrics', @@ -172,20 +170,6 @@ class ExternalMinerApiClient { } } - /// Test if the external miner is reachable - Future isReachable() async { - try { - final response = await _httpClient - .get(Uri.parse(baseUrl)) - .timeout(const Duration(seconds: 3)); - - // Any response (even 404) means the server is running - return response.statusCode >= 200 && response.statusCode < 500; - } catch (e) { - return false; - } - } - /// Test if the metrics endpoint is available Future isMetricsAvailable() async { try { diff --git a/miner-app/lib/src/services/miner_process.dart b/miner-app/lib/src/services/miner_process.dart index 37cd0c0f..cb07336a 100644 --- a/miner-app/lib/src/services/miner_process.dart +++ b/miner-app/lib/src/services/miner_process.dart @@ -49,7 +49,7 @@ class MinerProcess { final int cpuWorkers; final int gpuDevices; - final int externalMinerPort; + final int minerListenPort; final int detectedGpuCount; // Track metrics state to prevent premature hashrate reset @@ -88,7 +88,7 @@ class MinerProcess { this.cpuWorkers = 8, this.gpuDevices = 0, this.detectedGpuCount = 0, - this.externalMinerPort = 9833, + this.minerListenPort = 9833, }) { // Initialize services _statsService = MiningStatsService(); @@ -98,7 +98,6 @@ class MinerProcess { // Initialize external miner API client with metrics endpoint _externalMinerApiClient = ExternalMinerApiClient( - baseUrl: 'http://127.0.0.1:$externalMinerPort', metricsUrl: 'http://127.0.0.1:9900/metrics', // Standard metrics port ); @@ -124,6 +123,7 @@ class MinerProcess { Future start() async { // First, ensure both binaries are available await BinaryManager.ensureNodeBinary(); + await BinaryManager.ensureExternalMinerBinary(); // Cleanup any existing processes first await _cleanupExistingNodeProcesses(); @@ -138,107 +138,7 @@ class MinerProcess { // Check if ports are available and cleanup if needed await _ensurePortsAvailable(); - final externalMinerBinPath = - await BinaryManager.getExternalMinerBinaryFilePath(); - - await BinaryManager.ensureExternalMinerBinary(); - final externalMinerBin = File(externalMinerBinPath); - - if (!await externalMinerBin.exists()) { - throw Exception( - 'External miner binary not found at $externalMinerBinPath', - ); - } - - // Start the external miner first with metrics enabled - - final minerArgs = [ - 'serve', - '--port', - externalMinerPort.toString(), - '--cpu-workers', - cpuWorkers.toString(), - '--gpu-devices', - gpuDevices.toString(), - '--metrics-port', - await _getMetricsPort().then((port) => port.toString()), - ]; - - try { - _externalMinerProcess = await Process.start( - externalMinerBin.path, - minerArgs, - ); - } catch (e) { - throw Exception('Failed to start external miner: $e'); - } - - // Set up external miner log handling - _externalMinerProcess!.stdout - .transform(utf8.decoder) - .transform(const LineSplitter()) - .listen((line) { - final logEntry = LogEntry( - message: line, - timestamp: DateTime.now(), - source: 'quantus-miner', - ); - _logsController.add(logEntry); - print('[ext-miner] $line'); - }); - - _externalMinerProcess!.stderr - .transform(utf8.decoder) - .transform(const LineSplitter()) - .listen((line) { - final logEntry = LogEntry( - message: line, - timestamp: DateTime.now(), - source: line.isMinerError ? 'quantus-miner-error' : 'quantus-miner', - ); - _logsController.add(logEntry); - print('[ext-miner-err] $line'); - }); - - // Monitor external miner process exit - _externalMinerProcess!.exitCode.then((exitCode) { - if (exitCode != 0) { - print('External miner process exited with code: $exitCode'); - } - }); - - // Give the external miner a moment to start up - await Future.delayed(const Duration(seconds: 3)); - - // Check if external miner process is still alive - bool minerStillRunning = true; - try { - // Check if the process has exited by looking at its PID - final pid = _externalMinerProcess!.pid; - minerStillRunning = await _isProcessRunning(pid); - } catch (e) { - minerStillRunning = false; - } - - if (!minerStillRunning) { - throw Exception('External miner process died during startup'); - } - - // Test if external miner is responding on the port - try { - final testClient = HttpClient(); - testClient.connectionTimeout = const Duration(seconds: 5); - final request = await testClient.getUrl( - Uri.parse('http://127.0.0.1:$externalMinerPort'), - ); - final response = await request.close(); - await response.drain(); // Consume the response - testClient.close(); - } catch (e) { - // External miner might still be starting up - } - - // Now start the node process + // === START NODE FIRST (QUIC server) === final quantusHome = await BinaryManager.getQuantusHomeDirectoryPath(); final basePath = p.join(quantusHome, 'node_data'); await Directory(basePath).create(recursive: true); @@ -292,8 +192,8 @@ class MinerProcess { 'listen-addr=127.0.0.1:9933,methods=unsafe,cors=all', '--name', 'QuantusMinerGUI', - '--external-miner-url', - 'http://127.0.0.1:$externalMinerPort', + '--miner-listen-port', + minerListenPort.toString(), '--enable-peer-sharing', ]; @@ -303,40 +203,10 @@ class MinerProcess { _nodeProcess = await Process.start(bin.path, args); _stdoutFilter = LogFilterService(); _stderrFilter = LogFilterService(); - // Services are now initialized in constructor _stdoutFilter.reset(); _stderrFilter.reset(); - Future syncBlockTargetWithPrometheusMetrics() async { - try { - final metrics = await _prometheusService.fetchMetrics(); - if (metrics == null || metrics.targetBlock == null) return; - if (_statsService.currentStats.targetBlock >= metrics.targetBlock!) { - return; - } - - _statsService.updateTargetBlock(metrics.targetBlock!); - - onStatsUpdate?.call(_statsService.currentStats); - } catch (e) { - print('Failed to fetch target block height: $e'); - } - } - - // Start Prometheus polling for target block (every 3 seconds) - _syncStatusTimer?.cancel(); - _syncStatusTimer = Timer.periodic( - const Duration(seconds: 3), - (timer) => syncBlockTargetWithPrometheusMetrics(), - ); - - // Start external miner API polling (every second) - _externalMinerApiClient.startPolling(); - - // Wait for node to be ready before starting RPC polling - _waitForNodeReadyThenStartRpc(); - // Process each log line void processLogLine(String line, String streamType) { bool shouldPrint; @@ -385,6 +255,119 @@ class MinerProcess { .listen((line) { processLogLine(line, 'stderr'); }); + + Future syncBlockTargetWithPrometheusMetrics() async { + try { + final metrics = await _prometheusService.fetchMetrics(); + if (metrics == null || metrics.targetBlock == null) return; + if (_statsService.currentStats.targetBlock >= metrics.targetBlock!) { + return; + } + + _statsService.updateTargetBlock(metrics.targetBlock!); + + onStatsUpdate?.call(_statsService.currentStats); + } catch (e) { + print('Failed to fetch target block height: $e'); + } + } + + // Start Prometheus polling for target block (every 3 seconds) + _syncStatusTimer?.cancel(); + _syncStatusTimer = Timer.periodic( + const Duration(seconds: 3), + (timer) => syncBlockTargetWithPrometheusMetrics(), + ); + + // Wait for node RPC to be ready before starting miner + await _waitForNodeRpcReady(); + + // === START MINER (QUIC client connects to node) === + final externalMinerBinPath = + await BinaryManager.getExternalMinerBinaryFilePath(); + final externalMinerBin = File(externalMinerBinPath); + + if (!await externalMinerBin.exists()) { + throw Exception( + 'External miner binary not found at $externalMinerBinPath', + ); + } + + final minerArgs = [ + '--node-addr', + '127.0.0.1:$minerListenPort', + '--cpu-workers', + cpuWorkers.toString(), + '--gpu-devices', + gpuDevices.toString(), + '--metrics-port', + await _getMetricsPort().then((port) => port.toString()), + ]; + + try { + _externalMinerProcess = await Process.start( + externalMinerBin.path, + minerArgs, + ); + } catch (e) { + throw Exception('Failed to start external miner: $e'); + } + + // Set up external miner log handling + _externalMinerProcess!.stdout + .transform(utf8.decoder) + .transform(const LineSplitter()) + .listen((line) { + final logEntry = LogEntry( + message: line, + timestamp: DateTime.now(), + source: 'quantus-miner', + ); + _logsController.add(logEntry); + print('[ext-miner] $line'); + }); + + _externalMinerProcess!.stderr + .transform(utf8.decoder) + .transform(const LineSplitter()) + .listen((line) { + final logEntry = LogEntry( + message: line, + timestamp: DateTime.now(), + source: line.isMinerError ? 'quantus-miner-error' : 'quantus-miner', + ); + _logsController.add(logEntry); + print('[ext-miner-err] $line'); + }); + + // Monitor external miner process exit + _externalMinerProcess!.exitCode.then((exitCode) { + if (exitCode != 0) { + print('External miner process exited with code: $exitCode'); + } + }); + + // Give the external miner a moment to start up and connect + await Future.delayed(const Duration(seconds: 2)); + + // Check if external miner process is still alive + bool minerStillRunning = true; + try { + final pid = _externalMinerProcess!.pid; + minerStillRunning = await _isProcessRunning(pid); + } catch (e) { + minerStillRunning = false; + } + + if (!minerStillRunning) { + throw Exception('External miner process died during startup'); + } + + // Start external miner API polling (every second) + _externalMinerApiClient.startPolling(); + + // Start RPC polling now that everything is ready + _chainRpcClient.startPolling(); } void stop() { @@ -672,14 +655,14 @@ class MinerProcess { /// Check if required ports are available and cleanup if needed Future _ensurePortsAvailable() async { - // Check if external miner port (9833) is in use - if (await _isPortInUse(externalMinerPort)) { - await _killProcessOnPort(externalMinerPort); + // Check if node QUIC port (9833) is in use + if (await _isPortInUse(minerListenPort)) { + await _killProcessOnPort(minerListenPort); await Future.delayed(const Duration(seconds: 1)); - if (await _isPortInUse(externalMinerPort)) { + if (await _isPortInUse(minerListenPort)) { throw Exception( - 'Port $externalMinerPort is still in use after cleanup attempt', + 'Port $minerListenPort is still in use after cleanup attempt', ); } } @@ -694,7 +677,6 @@ class MinerProcess { if (altMetricsPort != 9900) { // Update the metrics URL for the API client _externalMinerApiClient = ExternalMinerApiClient( - baseUrl: 'http://127.0.0.1:$externalMinerPort', metricsUrl: 'http://127.0.0.1:$altMetricsPort/metrics', ); _externalMinerApiClient.onMetricsUpdate = _handleExternalMinerMetrics; @@ -934,22 +916,21 @@ class MinerProcess { } } - /// Handle chain RPC information updates - /// Wait for the node RPC to be ready, then start polling - Future _waitForNodeReadyThenStartRpc() async { - print('DEBUG: Waiting for node RPC to be ready...'); + /// Wait for the node RPC to be ready (blocking) + /// Used to ensure node is ready before starting miner + Future _waitForNodeRpcReady() async { + print('DEBUG: Waiting for node RPC to be ready before starting miner...'); // Try to connect to RPC endpoint with exponential backoff int attempts = 0; - const maxAttempts = 20; // Up to ~2 minutes of retries + const maxAttempts = 30; // Up to ~3 minutes of retries Duration delay = const Duration(seconds: 2); while (attempts < maxAttempts) { try { final isReady = await _chainRpcClient.isReachable(); if (isReady) { - print('DEBUG: Node RPC is ready! Starting chain RPC polling...'); - _chainRpcClient.startPolling(); + print('DEBUG: Node RPC is ready! Proceeding to start miner...'); return; } } catch (e) { @@ -970,10 +951,8 @@ class MinerProcess { } print( - 'DEBUG: Failed to connect to node RPC after $maxAttempts attempts. Will retry with polling...', + 'WARNING: Node RPC not ready after $maxAttempts attempts, proceeding to start miner anyway...', ); - // Start polling anyway - the error handling in RPC client will manage failures - _chainRpcClient.startPolling(); } void _handleChainInfoUpdate(ChainInfo info) { From 4421f0503faf190e15541f18cfaec39eb4b8118f Mon Sep 17 00:00:00 2001 From: illuzen Date: Sat, 31 Jan 2026 12:52:13 +0800 Subject: [PATCH 03/10] indirect chainId --- miner-app/lib/src/services/miner_process.dart | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/miner-app/lib/src/services/miner_process.dart b/miner-app/lib/src/services/miner_process.dart index cb07336a..75f1321e 100644 --- a/miner-app/lib/src/services/miner_process.dart +++ b/miner-app/lib/src/services/miner_process.dart @@ -51,6 +51,7 @@ class MinerProcess { final int minerListenPort; final int detectedGpuCount; + final String chainId; // Track metrics state to prevent premature hashrate reset double _lastValidHashrate = 0.0; @@ -89,6 +90,7 @@ class MinerProcess { this.gpuDevices = 0, this.detectedGpuCount = 0, this.minerListenPort = 9833, + this.chainId = 'dev', }) { // Initialize services _statsService = MiningStatsService(); @@ -182,8 +184,8 @@ class MinerProcess { '--rewards-address', rewardsAddress, '--validator', - '--chain', - 'dirac', + // Use --dev for local development, --chain for testnet/mainnet + if (chainId == 'dev') '--dev' else ...['--chain', chainId], '--port', '30333', '--prometheus-port', @@ -859,7 +861,8 @@ class MinerProcess { try { // Get the quantus home directory path final quantusHome = await BinaryManager.getQuantusHomeDirectoryPath(); - final lockFilePath = '$quantusHome/node_data/chains/dirac/db/full/LOCK'; + final lockFilePath = + '$quantusHome/node_data/chains/$chainId/db/full/LOCK'; final lockFile = File(lockFilePath); if (await lockFile.exists()) { @@ -869,7 +872,7 @@ class MinerProcess { } // Also check for other potential lock files - final dbDir = Directory('$quantusHome/node_data/chains/dirac/db/full'); + final dbDir = Directory('$quantusHome/node_data/chains/$chainId/db/full'); if (await dbDir.exists()) { await for (final entity in dbDir.list()) { if (entity is File && entity.path.contains('LOCK')) { @@ -890,7 +893,7 @@ class MinerProcess { Future _ensureDatabaseDirectoryAccess() async { try { final quantusHome = await BinaryManager.getQuantusHomeDirectoryPath(); - final dbPath = '$quantusHome/node_data/chains/dirac/db'; + final dbPath = '$quantusHome/node_data/chains/$chainId/db'; final dbDir = Directory(dbPath); // Create the directory if it doesn't exist From 2e039fa34226cc6a4395ef9380e0e95710423924 Mon Sep 17 00:00:00 2001 From: illuzen Date: Sat, 31 Jan 2026 14:11:26 +0800 Subject: [PATCH 04/10] split into sub-services --- miner-app/lib/main.dart | 19 +- miner-app/lib/src/config/miner_config.dart | 174 +++++++ miner-app/lib/src/services/miner_process.dart | 399 ++------------- .../src/services/miner_settings_service.dart | 29 ++ .../src/services/process_cleanup_service.dart | 465 ++++++++++++++++++ 5 files changed, 711 insertions(+), 375 deletions(-) create mode 100644 miner-app/lib/src/config/miner_config.dart create mode 100644 miner-app/lib/src/services/process_cleanup_service.dart diff --git a/miner-app/lib/main.dart b/miner-app/lib/main.dart index cfca99bd..98c3d266 100644 --- a/miner-app/lib/main.dart +++ b/miner-app/lib/main.dart @@ -9,6 +9,7 @@ import 'features/setup/rewards_address_setup_screen.dart'; import 'features/miner/miner_dashboard_screen.dart'; import 'src/services/binary_manager.dart'; import 'src/services/miner_process.dart'; +import 'src/services/process_cleanup_service.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; @@ -37,22 +38,8 @@ class GlobalMinerManager { } } - // Kill any remaining quantus processes - await _killQuantusProcesses(); - } - - static Future _killQuantusProcesses() async { - try { - print('GlobalMinerManager: Killing quantus processes by name...'); - - // Kill all quantus processes - await Process.run('pkill', ['-9', '-f', 'quantus-node']); - await Process.run('pkill', ['-9', '-f', 'quantus-miner']); - - print('GlobalMinerManager: Cleanup commands executed'); - } catch (e) { - print('GlobalMinerManager: Error killing processes by name: $e'); - } + // Kill any remaining quantus processes using the cleanup service + await ProcessCleanupService.killAllQuantusProcesses(); } } diff --git a/miner-app/lib/src/config/miner_config.dart b/miner-app/lib/src/config/miner_config.dart new file mode 100644 index 00000000..1643bc9b --- /dev/null +++ b/miner-app/lib/src/config/miner_config.dart @@ -0,0 +1,174 @@ +/// Centralized configuration for the miner application. +/// +/// All ports, timeouts, URLs, and other constants should be defined here +/// rather than scattered throughout the codebase. +class MinerConfig { + MinerConfig._(); + + // ============================================================ + // Network Ports + // ============================================================ + + /// QUIC port for miner-to-node communication + static const int defaultQuicPort = 9833; + + /// Prometheus metrics port for the external miner + static const int defaultMinerMetricsPort = 9900; + + /// JSON-RPC port for the node + static const int defaultNodeRpcPort = 9933; + + /// Prometheus metrics port for the node + static const int defaultNodePrometheusPort = 9616; + + /// P2P port for node networking + static const int defaultNodeP2pPort = 30333; + + // ============================================================ + // Timeouts & Retry Configuration + // ============================================================ + + /// Time to wait for graceful process shutdown before force killing + static const Duration gracefulShutdownTimeout = Duration(seconds: 2); + + /// Initial delay between RPC connection retries + static const Duration rpcInitialRetryDelay = Duration(seconds: 2); + + /// Maximum delay between RPC connection retries (with exponential backoff) + static const Duration rpcMaxRetryDelay = Duration(seconds: 10); + + /// Maximum number of RPC connection attempts before giving up + static const int maxRpcRetries = 30; + + /// Number of consecutive metrics failures before resetting hashrate to zero + static const int maxConsecutiveMetricsFailures = 5; + + /// Delay after killing a process before checking if port is free + static const Duration portCleanupDelay = Duration(seconds: 1); + + /// Delay after process cleanup before continuing + static const Duration processCleanupDelay = Duration(seconds: 2); + + /// Timeout for force kill operations + static const Duration forceKillTimeout = Duration(seconds: 5); + + /// Delay for process verification after kill + static const Duration processVerificationDelay = Duration(milliseconds: 500); + + // ============================================================ + // Polling Intervals + // ============================================================ + + /// How often to poll external miner metrics endpoint + static const Duration metricsPollingInterval = Duration(seconds: 1); + + /// How often to poll node Prometheus metrics (for target block) + static const Duration prometheusPollingInterval = Duration(seconds: 3); + + /// How often to check for binary updates + static const Duration binaryUpdatePollingInterval = Duration(minutes: 30); + + /// How often to poll chain RPC for peer count and block info + static const Duration chainRpcPollingInterval = Duration(seconds: 1); + + // ============================================================ + // URLs & Endpoints + // ============================================================ + + /// Returns the miner metrics URL for a given port + static String minerMetricsUrl(int port) => 'http://127.0.0.1:$port/metrics'; + + /// Returns the node RPC URL for a given port + static String nodeRpcUrl(int port) => 'http://127.0.0.1:$port'; + + /// Returns the node Prometheus metrics URL for a given port + static String nodePrometheusUrl(int port) => 'http://127.0.0.1:$port/metrics'; + + /// Default localhost address for connections + static const String localhost = '127.0.0.1'; + + // ============================================================ + // Chain Configuration + // ============================================================ + + /// Available chain IDs + static const List availableChains = [ + ChainConfig( + id: 'dev', + displayName: 'Development', + description: 'Local development chain', + isDefault: true, + ), + ChainConfig( + id: 'dirac', + displayName: 'Dirac', + description: 'Dirac testnet', + isDefault: false, + ), + ]; + + /// Get chain config by ID, returns dev chain if not found + static ChainConfig getChainById(String id) { + return availableChains.firstWhere( + (chain) => chain.id == id, + orElse: () => availableChains.first, + ); + } + + /// The default chain ID + static String get defaultChainId => + availableChains.firstWhere((c) => c.isDefault).id; + + // ============================================================ + // Process Names (for cleanup) + // ============================================================ + + /// Node binary name (without extension) + static const String nodeBinaryName = 'quantus-node'; + + /// Miner binary name (without extension) + static const String minerBinaryName = 'quantus-miner'; + + /// Node binary name with Windows extension + static String get nodeBinaryNameWindows => '$nodeBinaryName.exe'; + + /// Miner binary name with Windows extension + static String get minerBinaryNameWindows => '$minerBinaryName.exe'; + + // ============================================================ + // Logging + // ============================================================ + + /// Maximum number of log lines to keep in memory + static const int maxLogLines = 200; + + /// Initial lines to print before filtering kicks in + static const int initialLinesToPrint = 50; + + // ============================================================ + // Port Range for Finding Alternatives + // ============================================================ + + /// Number of ports to try when finding an alternative + static const int portSearchRange = 10; +} + +/// Configuration for a blockchain network. +/// +/// Named ChainConfig to avoid conflict with ChainInfo in chain_rpc_client.dart +class ChainConfig { + final String id; + final String displayName; + final String description; + final bool isDefault; + + const ChainConfig({ + required this.id, + required this.displayName, + required this.description, + required this.isDefault, + }); + + @override + String toString() => 'ChainConfig(id: $id, displayName: $displayName)'; +} diff --git a/miner-app/lib/src/services/miner_process.dart b/miner-app/lib/src/services/miner_process.dart index 75f1321e..026d4a96 100644 --- a/miner-app/lib/src/services/miner_process.dart +++ b/miner-app/lib/src/services/miner_process.dart @@ -3,6 +3,8 @@ import 'dart:convert'; import 'dart:io'; import 'package:path/path.dart' as p; +import 'package:quantus_miner/src/config/miner_config.dart'; +import 'package:quantus_miner/src/services/process_cleanup_service.dart'; import 'package:quantus_miner/src/services/prometheus_service.dart'; import 'package:quantus_miner/src/shared/extensions/log_string_extension.dart'; @@ -56,7 +58,6 @@ class MinerProcess { // Track metrics state to prevent premature hashrate reset double _lastValidHashrate = 0.0; int _consecutiveMetricsFailures = 0; - static const int _maxConsecutiveFailures = 5; // Public getters for process PIDs (for cleanup tracking) int? get nodeProcessPid { @@ -100,7 +101,9 @@ class MinerProcess { // Initialize external miner API client with metrics endpoint _externalMinerApiClient = ExternalMinerApiClient( - metricsUrl: 'http://127.0.0.1:9900/metrics', // Standard metrics port + metricsUrl: MinerConfig.minerMetricsUrl( + MinerConfig.defaultMinerMetricsPort, + ), ); // Set up external miner API callbacks @@ -108,7 +111,9 @@ class MinerProcess { _externalMinerApiClient.onError = _handleExternalMinerError; // Initialize chain RPC client - _chainRpcClient = PollingChainRpcClient(rpcUrl: 'http://127.0.0.1:9933'); + _chainRpcClient = PollingChainRpcClient( + rpcUrl: MinerConfig.nodeRpcUrl(MinerConfig.defaultNodeRpcPort), + ); _chainRpcClient.onChainInfoUpdate = _handleChainInfoUpdate; _chainRpcClient.onError = _handleChainRpcError; @@ -127,15 +132,8 @@ class MinerProcess { await BinaryManager.ensureNodeBinary(); await BinaryManager.ensureExternalMinerBinary(); - // Cleanup any existing processes first - await _cleanupExistingNodeProcesses(); - await _cleanupExistingMinerProcesses(); - - // Cleanup database lock files if needed - await _cleanupDatabaseLocks(); - - // Ensure database directory has proper access - await _ensureDatabaseDirectoryAccess(); + // Perform pre-start cleanup using the cleanup service + await ProcessCleanupService.performPreStartCleanup(chainId); // Check if ports are available and cleanup if needed await _ensurePortsAvailable(); @@ -303,7 +301,7 @@ class MinerProcess { '--gpu-devices', gpuDevices.toString(), '--metrics-port', - await _getMetricsPort().then((port) => port.toString()), + _getMetricsPort().toString(), ]; try { @@ -497,100 +495,16 @@ class MinerProcess { } } - /// Check if a process with the given PID is running + /// Check if a process with the given PID is running. + /// Delegates to ProcessCleanupService. Future _isProcessRunning(int pid) async { - try { - if (Platform.isWindows) { - final result = await Process.run('tasklist', ['/FI', 'PID eq $pid']); - return result.stdout.toString().contains(' $pid '); - } else { - final result = await Process.run('kill', ['-0', pid.toString()]); - return result.exitCode == 0; - } - } catch (e) { - return false; - } + return ProcessCleanupService.isProcessRunning(pid); } - /// Helper method to force kill a process by PID with verification + /// Helper method to force kill a process by PID with verification. + /// Delegates to ProcessCleanupService. Future _forceKillProcess(int pid, String processName) async { - try { - print('MinerProcess: Force killing $processName process (PID: $pid)'); - - if (Platform.isWindows) { - final killResult = await Process.run('taskkill', [ - '/F', - '/PID', - pid.toString(), - ]); - if (killResult.exitCode == 0) { - print( - 'MinerProcess: Successfully force killed $processName (PID: $pid)', - ); - } else { - print( - 'MinerProcess: taskkill failed for $processName (PID: $pid), exit code: ${killResult.exitCode}', - ); - } - - await Future.delayed(const Duration(milliseconds: 500)); - - // Verify - final checkResult = await Process.run('tasklist', [ - '/FI', - 'PID eq $pid', - ]); - if (checkResult.stdout.toString().contains(' $pid ')) { - print( - 'MinerProcess: WARNING - $processName (PID: $pid) may still be running', - ); - // Try by name as last resort - final binaryName = processName.contains('miner') - ? 'quantus-miner.exe' - : 'quantus-node.exe'; - await Process.run('taskkill', ['/F', '/IM', binaryName]); - } else { - print( - 'MinerProcess: Verified $processName (PID: $pid) is terminated', - ); - } - } else { - // First try SIGKILL via kill command for better reliability - final killResult = await Process.run('kill', ['-9', pid.toString()]); - - if (killResult.exitCode == 0) { - print( - 'MinerProcess: Successfully force killed $processName (PID: $pid)', - ); - } else { - print( - 'MinerProcess: kill command failed for $processName (PID: $pid), exit code: ${killResult.exitCode}', - ); - } - - // Wait a moment then verify the process is dead - await Future.delayed(const Duration(milliseconds: 500)); - - final checkResult = await Process.run('kill', ['-0', pid.toString()]); - if (checkResult.exitCode != 0) { - print( - 'MinerProcess: Verified $processName (PID: $pid) is terminated', - ); - } else { - print( - 'MinerProcess: WARNING - $processName (PID: $pid) may still be running', - ); - // Try pkill as last resort - await Process.run('pkill', [ - '-9', - '-f', - processName.contains('miner') ? 'quantus-miner' : 'quantus-node', - ]); - } - } - } catch (e) { - print('MinerProcess: Error in _forceKillProcess for $processName: $e'); - } + await ProcessCleanupService.forceKillProcess(pid, processName); } /// Handle external miner metrics updates @@ -627,7 +541,8 @@ class MinerProcess { _consecutiveMetricsFailures++; // Only reset to zero after multiple consecutive failures - if (_consecutiveMetricsFailures >= _maxConsecutiveFailures) { + if (_consecutiveMetricsFailures >= + MinerConfig.maxConsecutiveMetricsFailures) { _statsService.updateHashrate(0.0); _lastValidHashrate = 0.0; onStatsUpdate?.call(_statsService.currentStats); @@ -646,7 +561,8 @@ class MinerProcess { _consecutiveMetricsFailures++; // Only reset hashrate after multiple consecutive errors - if (_consecutiveMetricsFailures >= _maxConsecutiveFailures) { + if (_consecutiveMetricsFailures >= + MinerConfig.maxConsecutiveMetricsFailures) { if (_statsService.currentStats.hashrate != 0.0) { _statsService.updateHashrate(0.0); _lastValidHashrate = 0.0; @@ -657,266 +573,31 @@ class MinerProcess { /// Check if required ports are available and cleanup if needed Future _ensurePortsAvailable() async { - // Check if node QUIC port (9833) is in use - if (await _isPortInUse(minerListenPort)) { - await _killProcessOnPort(minerListenPort); - await Future.delayed(const Duration(seconds: 1)); - - if (await _isPortInUse(minerListenPort)) { - throw Exception( - 'Port $minerListenPort is still in use after cleanup attempt', - ); - } - } - - // Check if metrics port (9900) is in use - if (await _isPortInUse(9900)) { - await _killProcessOnPort(9900); - await Future.delayed(const Duration(seconds: 1)); - - if (await _isPortInUse(9900)) { - final altMetricsPort = await _findAvailablePort(9900); - if (altMetricsPort != 9900) { - // Update the metrics URL for the API client - _externalMinerApiClient = ExternalMinerApiClient( - metricsUrl: 'http://127.0.0.1:$altMetricsPort/metrics', - ); - _externalMinerApiClient.onMetricsUpdate = _handleExternalMinerMetrics; - _externalMinerApiClient.onError = _handleExternalMinerError; - } - } - } - } - - /// Find an available port starting from the given port - Future _findAvailablePort(int startPort) async { - for (int port = startPort; port <= startPort + 10; port++) { - if (!(await _isPortInUse(port))) { - return port; - } - } - return startPort; // Return original if no alternative found - } - - /// Check if a port is currently in use - Future _isPortInUse(int port) async { - try { - if (Platform.isWindows) { - final result = await Process.run('netstat', ['-ano']); - return result.exitCode == 0 && - result.stdout.toString().contains(':$port'); - } else { - final result = await Process.run('lsof', ['-i', ':$port']); - return result.exitCode == 0 && result.stdout.toString().isNotEmpty; - } - } catch (e) { - // lsof might not be available, try netstat as fallback - try { - final result = await Process.run('netstat', ['-an']); - return result.stdout.toString().contains(':$port'); - } catch (e2) { - print('DEBUG: Could not check port $port availability: $e2'); - return false; - } - } - } - - /// Kill process using a specific port - Future _killProcessOnPort(int port) async { - try { - if (Platform.isWindows) { - final result = await Process.run('netstat', ['-ano']); - if (result.exitCode == 0) { - final lines = result.stdout.toString().split('\n'); - for (final line in lines) { - if (line.contains(':$port')) { - final parts = line.trim().split(RegExp(r'\s+')); - if (parts.isNotEmpty) { - final pid = parts.last; - // Verify it's a valid PID number - if (int.tryParse(pid) != null) { - await Process.run('taskkill', ['/F', '/PID', pid]); - } - } - } - } - } - } else { - // Find process using the port - final result = await Process.run('lsof', ['-ti', ':$port']); - if (result.exitCode == 0) { - final pids = result.stdout.toString().trim().split('\n'); - for (final pid in pids) { - if (pid.isNotEmpty) { - await Process.run('kill', ['-9', pid.trim()]); - } - } - } - } - } catch (e) { - // Ignore cleanup errors - } - } - - /// Get the metrics port to use (either 9900 or alternative) - Future _getMetricsPort() async { - if (await _isPortInUse(9900)) { - return await _findAvailablePort(9901); - } - return 9900; - } - - /// Cleanup any existing quantus-miner processes - Future _cleanupExistingMinerProcesses() async { - try { - if (Platform.isWindows) { - try { - await Process.run('taskkill', ['/F', '/IM', 'quantus-miner.exe']); - await Future.delayed(const Duration(seconds: 2)); - } catch (_) { - // Process might not exist - } - } else { - // Find all quantus-miner processes - final result = await Process.run('pgrep', ['-f', 'quantus-miner']); - if (result.exitCode == 0) { - final pids = result.stdout.toString().trim().split('\n'); - for (final pid in pids) { - if (pid.isNotEmpty) { - try { - // Try graceful termination first - await Process.run('kill', ['-15', pid.trim()]); - await Future.delayed(const Duration(seconds: 1)); - - // Check if still running, force kill if needed - final checkResult = await Process.run('kill', [ - '-0', - pid.trim(), - ]); - if (checkResult.exitCode == 0) { - await Process.run('kill', ['-9', pid.trim()]); - } - } catch (e) { - // Ignore cleanup errors - } - } - } - - // Wait a moment for processes to fully terminate - await Future.delayed(const Duration(seconds: 2)); - } - } - } catch (e) { - // Ignore cleanup errors - } - } - - /// Cleanup any existing quantus-node processes - Future _cleanupExistingNodeProcesses() async { - try { - if (Platform.isWindows) { - try { - await Process.run('taskkill', ['/F', '/IM', 'quantus-node.exe']); - await Future.delayed(const Duration(seconds: 2)); - } catch (_) { - // Process might not exist - } - } else { - // Find all quantus-node processes - final result = await Process.run('pgrep', ['-f', 'quantus-node']); - if (result.exitCode == 0) { - final pids = result.stdout.toString().trim().split('\n'); - for (final pid in pids) { - if (pid.isNotEmpty) { - try { - // Try graceful termination first - await Process.run('kill', ['-15', pid.trim()]); - await Future.delayed(const Duration(seconds: 2)); - - // Check if still running, force kill if needed - final checkResult = await Process.run('kill', [ - '-0', - pid.trim(), - ]); - if (checkResult.exitCode == 0) { - await Process.run('kill', ['-9', pid.trim()]); - } - } catch (e) { - // Ignore cleanup errors - } - } - } + final ports = await ProcessCleanupService.ensurePortsAvailable( + quicPort: minerListenPort, + metricsPort: MinerConfig.defaultMinerMetricsPort, + ); - // Wait a moment for processes to fully terminate - await Future.delayed(const Duration(seconds: 3)); - } - } - } catch (e) { - // Ignore cleanup errors + // If metrics port changed, update the API client + final actualMetricsPort = ports['metrics']!; + if (actualMetricsPort != MinerConfig.defaultMinerMetricsPort) { + _externalMinerApiClient = ExternalMinerApiClient( + metricsUrl: MinerConfig.minerMetricsUrl(actualMetricsPort), + ); + _externalMinerApiClient.onMetricsUpdate = _handleExternalMinerMetrics; + _externalMinerApiClient.onError = _handleExternalMinerError; } - } - /// Cleanup database lock files that may prevent node startup - Future _cleanupDatabaseLocks() async { - try { - // Get the quantus home directory path - final quantusHome = await BinaryManager.getQuantusHomeDirectoryPath(); - final lockFilePath = - '$quantusHome/node_data/chains/$chainId/db/full/LOCK'; - final lockFile = File(lockFilePath); - - if (await lockFile.exists()) { - // At this point node processes should already be cleaned up - // Safe to remove the stale lock file - await lockFile.delete(); - } - - // Also check for other potential lock files - final dbDir = Directory('$quantusHome/node_data/chains/$chainId/db/full'); - if (await dbDir.exists()) { - await for (final entity in dbDir.list()) { - if (entity is File && entity.path.contains('LOCK')) { - try { - await entity.delete(); - } catch (e) { - // Ignore cleanup errors - } - } - } - } - } catch (e) { - // Ignore cleanup errors - } + // Store the metrics port for later use + _actualMetricsPort = actualMetricsPort; } - /// Check and fix database directory permissions - Future _ensureDatabaseDirectoryAccess() async { - try { - final quantusHome = await BinaryManager.getQuantusHomeDirectoryPath(); - final dbPath = '$quantusHome/node_data/chains/$chainId/db'; - final dbDir = Directory(dbPath); - - // Create the directory if it doesn't exist - if (!await dbDir.exists()) { - await dbDir.create(recursive: true); - } + // Track the actual metrics port being used (may differ from default) + int _actualMetricsPort = MinerConfig.defaultMinerMetricsPort; - // Check if directory is writable - final testFile = File('$dbPath/test_write_access'); - try { - await testFile.writeAsString('test'); - await testFile.delete(); - } catch (e) { - // Try to fix permissions - try { - await Process.run('chmod', ['-R', '755', dbPath]); - } catch (permError) { - // Ignore permission fix errors - } - } - } catch (e) { - // Ignore directory access errors - } + /// Get the metrics port to use (determined during _ensurePortsAvailable) + int _getMetricsPort() { + return _actualMetricsPort; } /// Wait for the node RPC to be ready (blocking) diff --git a/miner-app/lib/src/services/miner_settings_service.dart b/miner-app/lib/src/services/miner_settings_service.dart index 5fa5fb9b..313fd526 100644 --- a/miner-app/lib/src/services/miner_settings_service.dart +++ b/miner-app/lib/src/services/miner_settings_service.dart @@ -1,11 +1,13 @@ import 'dart:io'; +import 'package:quantus_miner/src/config/miner_config.dart'; import 'package:quantus_miner/src/services/binary_manager.dart'; import 'package:shared_preferences/shared_preferences.dart'; class MinerSettingsService { static const String _keyCpuWorkers = 'cpu_workers'; static const String _keyGpuDevices = 'gpu_devices'; + static const String _keyChainId = 'chain_id'; Future saveCpuWorkers(int cpuWorkers) async { final prefs = await SharedPreferences.getInstance(); @@ -27,6 +29,33 @@ class MinerSettingsService { return prefs.getInt(_keyGpuDevices); } + /// Save the selected chain ID. + Future saveChainId(String chainId) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString(_keyChainId, chainId); + } + + /// Get the saved chain ID, returns default if not set. + Future getChainId() async { + final prefs = await SharedPreferences.getInstance(); + final savedChainId = prefs.getString(_keyChainId); + if (savedChainId == null) { + return MinerConfig.defaultChainId; + } + // Validate that the chain ID is still valid + final validIds = MinerConfig.availableChains.map((c) => c.id).toList(); + if (!validIds.contains(savedChainId)) { + return MinerConfig.defaultChainId; + } + return savedChainId; + } + + /// Get the ChainConfig for the saved chain ID. + Future getChainConfig() async { + final chainId = await getChainId(); + return MinerConfig.getChainById(chainId); + } + Future logout() async { print('Starting app logout/reset...'); diff --git a/miner-app/lib/src/services/process_cleanup_service.dart b/miner-app/lib/src/services/process_cleanup_service.dart new file mode 100644 index 00000000..49a6b1cd --- /dev/null +++ b/miner-app/lib/src/services/process_cleanup_service.dart @@ -0,0 +1,465 @@ +import 'dart:io'; + +import 'package:quantus_miner/src/config/miner_config.dart'; +import 'package:quantus_miner/src/services/binary_manager.dart'; + +/// Service responsible for platform-specific process management operations. +/// +/// This includes: +/// - Checking if processes are running +/// - Killing processes by PID or name +/// - Port availability checking and cleanup +/// - Database lock file cleanup +/// - Directory access verification +class ProcessCleanupService { + ProcessCleanupService._(); + + // ============================================================ + // Process Running Checks + // ============================================================ + + /// Check if a process with the given PID is currently running. + static Future isProcessRunning(int pid) async { + try { + if (Platform.isWindows) { + final result = await Process.run('tasklist', ['/FI', 'PID eq $pid']); + return result.stdout.toString().contains(' $pid '); + } else { + // On Unix, kill -0 checks if process exists without killing it + final result = await Process.run('kill', ['-0', pid.toString()]); + return result.exitCode == 0; + } + } catch (e) { + return false; + } + } + + // ============================================================ + // Process Killing + // ============================================================ + + /// Force kill a process by PID with verification. + /// + /// Returns true if the process was successfully killed or was already dead. + static Future forceKillProcess(int pid, String processName) async { + try { + print('ProcessCleanupService: Force killing $processName (PID: $pid)'); + + if (Platform.isWindows) { + return await _forceKillWindowsProcess(pid, processName); + } else { + return await _forceKillUnixProcess(pid, processName); + } + } catch (e) { + print( + 'ProcessCleanupService: Error in forceKillProcess for $processName: $e', + ); + return false; + } + } + + static Future _forceKillWindowsProcess( + int pid, + String processName, + ) async { + final killResult = await Process.run('taskkill', [ + '/F', + '/PID', + pid.toString(), + ]); + + if (killResult.exitCode == 0) { + print( + 'ProcessCleanupService: Successfully force killed $processName (PID: $pid)', + ); + } else { + print( + 'ProcessCleanupService: taskkill failed for $processName (PID: $pid), exit code: ${killResult.exitCode}', + ); + } + + await Future.delayed(MinerConfig.processVerificationDelay); + + // Verify process is dead + final checkResult = await Process.run('tasklist', ['/FI', 'PID eq $pid']); + if (checkResult.stdout.toString().contains(' $pid ')) { + print( + 'ProcessCleanupService: WARNING - $processName (PID: $pid) may still be running', + ); + + // Try by name as last resort + final binaryName = processName.contains('miner') + ? MinerConfig.minerBinaryNameWindows + : MinerConfig.nodeBinaryNameWindows; + await Process.run('taskkill', ['/F', '/IM', binaryName]); + return false; + } + + print( + 'ProcessCleanupService: Verified $processName (PID: $pid) is terminated', + ); + return true; + } + + static Future _forceKillUnixProcess(int pid, String processName) async { + // First try SIGKILL via kill command + final killResult = await Process.run('kill', ['-9', pid.toString()]); + + if (killResult.exitCode == 0) { + print( + 'ProcessCleanupService: Successfully force killed $processName (PID: $pid)', + ); + } else { + print( + 'ProcessCleanupService: kill command failed for $processName (PID: $pid), exit code: ${killResult.exitCode}', + ); + } + + await Future.delayed(MinerConfig.processVerificationDelay); + + // Verify process is dead + final checkResult = await Process.run('kill', ['-0', pid.toString()]); + if (checkResult.exitCode == 0) { + print( + 'ProcessCleanupService: WARNING - $processName (PID: $pid) may still be running', + ); + + // Try pkill as last resort + final binaryName = processName.contains('miner') + ? MinerConfig.minerBinaryName + : MinerConfig.nodeBinaryName; + await Process.run('pkill', ['-9', '-f', binaryName]); + return false; + } + + print( + 'ProcessCleanupService: Verified $processName (PID: $pid) is terminated', + ); + return true; + } + + /// Kill all processes matching the given binary name. + static Future killProcessesByName(String binaryName) async { + try { + if (Platform.isWindows) { + await Process.run('taskkill', ['/F', '/IM', '$binaryName.exe']); + } else { + await Process.run('pkill', ['-9', '-f', binaryName]); + } + } catch (e) { + // Ignore errors - processes might not exist + } + } + + // ============================================================ + // Port Management + // ============================================================ + + /// Check if a port is currently in use. + static Future isPortInUse(int port) async { + try { + if (Platform.isWindows) { + final result = await Process.run('netstat', ['-ano']); + return result.exitCode == 0 && + result.stdout.toString().contains(':$port'); + } else { + final result = await Process.run('lsof', ['-i', ':$port']); + return result.exitCode == 0 && result.stdout.toString().isNotEmpty; + } + } catch (e) { + // lsof might not be available, try netstat as fallback + try { + final result = await Process.run('netstat', ['-an']); + return result.stdout.toString().contains(':$port'); + } catch (e2) { + print( + 'ProcessCleanupService: Could not check port $port availability: $e2', + ); + return false; + } + } + } + + /// Kill any process using the specified port. + static Future killProcessOnPort(int port) async { + try { + if (Platform.isWindows) { + await _killProcessOnPortWindows(port); + } else { + await _killProcessOnPortUnix(port); + } + } catch (e) { + // Ignore cleanup errors + } + } + + static Future _killProcessOnPortWindows(int port) async { + final result = await Process.run('netstat', ['-ano']); + if (result.exitCode != 0) return; + + final lines = result.stdout.toString().split('\n'); + for (final line in lines) { + if (line.contains(':$port')) { + final parts = line.trim().split(RegExp(r'\s+')); + if (parts.isNotEmpty) { + final pid = parts.last; + // Verify it's a valid PID number + if (int.tryParse(pid) != null) { + await Process.run('taskkill', ['/F', '/PID', pid]); + } + } + } + } + } + + static Future _killProcessOnPortUnix(int port) async { + final result = await Process.run('lsof', ['-ti', ':$port']); + if (result.exitCode != 0) return; + + final pids = result.stdout.toString().trim().split('\n'); + for (final pid in pids) { + if (pid.isNotEmpty) { + await Process.run('kill', ['-9', pid.trim()]); + } + } + } + + /// Find an available port starting from the given port. + /// + /// Tries ports in range [startPort, startPort + MinerConfig.portSearchRange]. + /// Returns the original port if no alternative is found. + static Future findAvailablePort(int startPort) async { + for ( + int port = startPort; + port <= startPort + MinerConfig.portSearchRange; + port++ + ) { + if (!(await isPortInUse(port))) { + return port; + } + } + return startPort; // Return original if no alternative found + } + + /// Ensure required ports are available, cleaning up if necessary. + /// + /// Returns a map of port names to their actual values (may differ from defaults + /// if an alternative port was needed). + static Future> ensurePortsAvailable({ + required int quicPort, + required int metricsPort, + }) async { + final result = {'quic': quicPort, 'metrics': metricsPort}; + + // Check QUIC port + if (await isPortInUse(quicPort)) { + await killProcessOnPort(quicPort); + await Future.delayed(MinerConfig.portCleanupDelay); + + if (await isPortInUse(quicPort)) { + throw Exception('Port $quicPort is still in use after cleanup attempt'); + } + } + + // Check metrics port + if (await isPortInUse(metricsPort)) { + await killProcessOnPort(metricsPort); + await Future.delayed(MinerConfig.portCleanupDelay); + + if (await isPortInUse(metricsPort)) { + // Try to find an alternative port + final altPort = await findAvailablePort(metricsPort + 1); + if (altPort != metricsPort + 1) { + print( + 'ProcessCleanupService: Using alternative metrics port: $altPort', + ); + } + result['metrics'] = altPort; + } + } + + return result; + } + + // ============================================================ + // Existing Process Cleanup + // ============================================================ + + /// Cleanup any existing quantus-node processes. + static Future cleanupExistingNodeProcesses() async { + try { + if (Platform.isWindows) { + await Process.run('taskkill', [ + '/F', + '/IM', + MinerConfig.nodeBinaryNameWindows, + ]); + await Future.delayed(MinerConfig.processCleanupDelay); + } else { + await _cleanupUnixProcesses(MinerConfig.nodeBinaryName); + } + } catch (e) { + // Ignore cleanup errors + } + } + + /// Cleanup any existing quantus-miner processes. + static Future cleanupExistingMinerProcesses() async { + try { + if (Platform.isWindows) { + await Process.run('taskkill', [ + '/F', + '/IM', + MinerConfig.minerBinaryNameWindows, + ]); + await Future.delayed(MinerConfig.processCleanupDelay); + } else { + await _cleanupUnixProcesses(MinerConfig.minerBinaryName); + } + } catch (e) { + // Ignore cleanup errors + } + } + + static Future _cleanupUnixProcesses(String processName) async { + final result = await Process.run('pgrep', ['-f', processName]); + if (result.exitCode != 0) return; + + final pids = result.stdout.toString().trim().split('\n'); + for (final pid in pids) { + if (pid.isEmpty) continue; + + try { + // Try graceful termination first (SIGTERM) + await Process.run('kill', ['-15', pid.trim()]); + await Future.delayed(const Duration(seconds: 1)); + + // Check if still running, force kill if needed + final checkResult = await Process.run('kill', ['-0', pid.trim()]); + if (checkResult.exitCode == 0) { + await Process.run('kill', ['-9', pid.trim()]); + } + } catch (e) { + // Ignore cleanup errors + } + } + + // Wait for processes to fully terminate + await Future.delayed(MinerConfig.processCleanupDelay); + } + + // ============================================================ + // Database & Directory Cleanup + // ============================================================ + + /// Cleanup database lock files that may prevent node startup. + static Future cleanupDatabaseLocks(String chainId) async { + try { + final quantusHome = await BinaryManager.getQuantusHomeDirectoryPath(); + final lockFilePath = + '$quantusHome/node_data/chains/$chainId/db/full/LOCK'; + final lockFile = File(lockFilePath); + + if (await lockFile.exists()) { + await lockFile.delete(); + print('ProcessCleanupService: Deleted lock file: $lockFilePath'); + } + + // Also check for other potential lock files + final dbDir = Directory('$quantusHome/node_data/chains/$chainId/db/full'); + if (await dbDir.exists()) { + await for (final entity in dbDir.list()) { + if (entity is File && entity.path.contains('LOCK')) { + try { + await entity.delete(); + print('ProcessCleanupService: Deleted lock file: ${entity.path}'); + } catch (e) { + // Ignore cleanup errors + } + } + } + } + } catch (e) { + // Ignore cleanup errors + } + } + + /// Check and fix database directory permissions. + static Future ensureDatabaseDirectoryAccess(String chainId) async { + try { + final quantusHome = await BinaryManager.getQuantusHomeDirectoryPath(); + final dbPath = '$quantusHome/node_data/chains/$chainId/db'; + final dbDir = Directory(dbPath); + + // Create the directory if it doesn't exist + if (!await dbDir.exists()) { + await dbDir.create(recursive: true); + } + + // Check if directory is writable + final testFile = File('$dbPath/test_write_access'); + try { + await testFile.writeAsString('test'); + await testFile.delete(); + } catch (e) { + // Try to fix permissions (Unix only) + if (!Platform.isWindows) { + try { + await Process.run('chmod', ['-R', '755', dbPath]); + } catch (permError) { + // Ignore permission fix errors + } + } + } + } catch (e) { + // Ignore directory access errors + } + } + + // ============================================================ + // Combined Cleanup Operations + // ============================================================ + + /// Perform full cleanup before starting mining. + /// + /// This cleans up: + /// - Existing node processes + /// - Existing miner processes + /// - Database locks + /// - Ensures directory access + static Future performPreStartCleanup(String chainId) async { + await cleanupExistingNodeProcesses(); + await cleanupExistingMinerProcesses(); + await cleanupDatabaseLocks(chainId); + await ensureDatabaseDirectoryAccess(chainId); + } + + /// Kill all quantus processes by name. + /// + /// This is a more aggressive cleanup used during app exit. + static Future killAllQuantusProcesses() async { + try { + print('ProcessCleanupService: Killing all quantus processes...'); + + if (Platform.isWindows) { + await Process.run('taskkill', [ + '/F', + '/IM', + MinerConfig.nodeBinaryNameWindows, + ]); + await Process.run('taskkill', [ + '/F', + '/IM', + MinerConfig.minerBinaryNameWindows, + ]); + } else { + await Process.run('pkill', ['-9', '-f', MinerConfig.nodeBinaryName]); + await Process.run('pkill', ['-9', '-f', MinerConfig.minerBinaryName]); + } + + print('ProcessCleanupService: Cleanup commands executed'); + } catch (e) { + print('ProcessCleanupService: Error killing processes: $e'); + } + } +} From 18f265b6be5be88134069edbb334499855711389 Mon Sep 17 00:00:00 2001 From: illuzen Date: Sat, 31 Jan 2026 15:07:20 +0800 Subject: [PATCH 05/10] better logging --- miner-app/lib/main.dart | 43 ++-- .../lib/src/services/binary_manager.dart | 7 +- .../lib/src/services/chain_rpc_client.dart | 8 +- .../services/external_miner_api_client.dart | 5 +- miner-app/lib/src/services/miner_process.dart | 112 +++++----- .../src/services/process_cleanup_service.dart | 61 ++---- .../lib/src/services/prometheus_service.dart | 6 +- miner-app/lib/src/utils/app_logger.dart | 192 ++++++++++++++++++ miner-app/pubspec.yaml | 6 +- 9 files changed, 305 insertions(+), 135 deletions(-) create mode 100644 miner-app/lib/src/utils/app_logger.dart diff --git a/miner-app/lib/main.dart b/miner-app/lib/main.dart index 98c3d266..8936254b 100644 --- a/miner-app/lib/main.dart +++ b/miner-app/lib/main.dart @@ -10,16 +10,19 @@ import 'features/miner/miner_dashboard_screen.dart'; import 'src/services/binary_manager.dart'; import 'src/services/miner_process.dart'; import 'src/services/process_cleanup_service.dart'; +import 'src/utils/app_logger.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; +final _log = log.withTag('App'); + /// Global class to manage miner process lifecycle class GlobalMinerManager { static MinerProcess? _globalMinerProcess; static void setMinerProcess(MinerProcess? process) { _globalMinerProcess = process; - print('GlobalMinerManager: Set miner process: ${process != null}'); + _log.d('Miner process registered: ${process != null}'); } static MinerProcess? getMinerProcess() { @@ -27,14 +30,13 @@ class GlobalMinerManager { } static Future cleanup() async { - print('GlobalMinerManager: Starting cleanup...'); + _log.i('Starting global cleanup...'); if (_globalMinerProcess != null) { try { - print('GlobalMinerManager: Force stopping global miner process'); _globalMinerProcess!.forceStop(); _globalMinerProcess = null; } catch (e) { - print('GlobalMinerManager: Error stopping miner process: $e'); + _log.e('Error stopping miner process', error: e); } } @@ -49,20 +51,17 @@ Future initialRedirect( ) async { final currentRoute = state.uri.toString(); - print('initialRedirect'); - // Check 1: Node Installed bool isNodeInstalled = false; try { isNodeInstalled = await BinaryManager.hasBinary(); - print('isNodeInstalled: $isNodeInstalled'); } catch (e) { - print('Error checking node installation status: $e'); + _log.e('Error checking node installation', error: e); isNodeInstalled = false; } if (!isNodeInstalled) { - print('node not installed, going to node setup'); + _log.d('Node not installed, redirecting to setup'); return (currentRoute == '/node_setup') ? null : '/node_setup'; } @@ -73,7 +72,7 @@ Future initialRedirect( '${await BinaryManager.getQuantusHomeDirectoryPath()}/node_key.p2p'; isIdentitySet = await File(identityPath).exists(); } catch (e) { - print('Error checking node identity status: $e'); + _log.e('Error checking node identity', error: e); isIdentitySet = false; } @@ -90,7 +89,7 @@ Future initialRedirect( final rewardsFile = File('$quantusHome/rewards-address.txt'); isRewardsAddressSet = await rewardsFile.exists(); } catch (e) { - print('Error checking rewards address status: $e'); + _log.e('Error checking rewards address', error: e); isRewardsAddressSet = false; } @@ -141,10 +140,9 @@ Future main() async { try { await QuantusSdk.init(); - print('SubstrateService and QuantusSdk initialized successfully.'); + _log.i('SDK initialized'); } catch (e) { - print('Error initializing SDK: $e'); - // Depending on the app, you might want to show an error UI or prevent app startup + _log.e('Error initializing SDK', error: e); } runApp(const MinerApp()); } @@ -178,32 +176,27 @@ class _MinerAppState extends State { } void _onAppDetach() { - print('App lifecycle: App detached, forcing cleanup...'); + _log.i('App detached, cleaning up...'); GlobalMinerManager.cleanup(); } Future _onExitRequested() async { - print('App lifecycle: Exit requested, cleaning up processes...'); + _log.i('Exit requested, cleaning up...'); try { await GlobalMinerManager.cleanup(); - print('App lifecycle: Cleanup completed, allowing exit'); return AppExitResponse.exit; } catch (e) { - print('App lifecycle: Error during cleanup: $e'); + _log.e('Error during exit cleanup', error: e); // Still allow exit even if cleanup fails return AppExitResponse.exit; } } void _onStateChanged(AppLifecycleState state) { - print('App lifecycle state changed to: $state'); - - if (state == AppLifecycleState.paused || - state == AppLifecycleState.detached) { - print('App lifecycle: App backgrounded/detached, cleaning up...'); - GlobalMinerManager.cleanup(); - } + _log.d('Lifecycle state: $state'); + // Note: We intentionally do NOT cleanup on pause/background + // Mining should continue when the app is backgrounded } @override diff --git a/miner-app/lib/src/services/binary_manager.dart b/miner-app/lib/src/services/binary_manager.dart index c88664cf..64fa1ea4 100644 --- a/miner-app/lib/src/services/binary_manager.dart +++ b/miner-app/lib/src/services/binary_manager.dart @@ -4,6 +4,9 @@ import 'dart:io'; import 'package:http/http.dart' as http; import 'package:path/path.dart' as p; +import 'package:quantus_miner/src/utils/app_logger.dart'; + +final _log = log.withTag('BinaryManager'); class DownloadProgress { final int downloadedBytes; @@ -99,7 +102,7 @@ class BinaryManager { final json = jsonDecode(content) as Map; return BinaryVersion.fromJson(json); } catch (e) { - print('Error reading node version file: $e'); + _log.d('Error reading node version file: $e'); return null; } } @@ -117,7 +120,7 @@ class BinaryManager { final json = jsonDecode(content) as Map; return BinaryVersion.fromJson(json); } catch (e) { - print('Error reading miner version file: $e'); + _log.d('Error reading miner version file: $e'); return null; } } diff --git a/miner-app/lib/src/services/chain_rpc_client.dart b/miner-app/lib/src/services/chain_rpc_client.dart index 5dcdf142..4f3c49c0 100644 --- a/miner-app/lib/src/services/chain_rpc_client.dart +++ b/miner-app/lib/src/services/chain_rpc_client.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:convert'; import 'package:http/http.dart' as http; +import 'package:quantus_miner/src/config/miner_config.dart'; class ChainInfo { final int peerCount; @@ -31,10 +32,9 @@ class ChainRpcClient { final http.Client _httpClient; int _requestId = 1; - ChainRpcClient({ - this.rpcUrl = 'http://127.0.0.1:9933', - this.timeout = const Duration(seconds: 10), - }) : _httpClient = http.Client(); + ChainRpcClient({String? rpcUrl, this.timeout = const Duration(seconds: 10)}) + : rpcUrl = rpcUrl ?? MinerConfig.nodeRpcUrl(MinerConfig.defaultNodeRpcPort), + _httpClient = http.Client(); /// Get comprehensive chain information Future getChainInfo() async { diff --git a/miner-app/lib/src/services/external_miner_api_client.dart b/miner-app/lib/src/services/external_miner_api_client.dart index 2e8fd1a8..0f9468b6 100644 --- a/miner-app/lib/src/services/external_miner_api_client.dart +++ b/miner-app/lib/src/services/external_miner_api_client.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:http/http.dart' as http; +import 'package:quantus_miner/src/config/miner_config.dart'; class ExternalMinerMetrics { final double hashRate; @@ -41,7 +42,9 @@ class ExternalMinerApiClient { ExternalMinerApiClient({ String? metricsUrl, this.timeout = const Duration(seconds: 5), - }) : metricsUrl = metricsUrl ?? 'http://127.0.0.1:9900/metrics', + }) : metricsUrl = + metricsUrl ?? + MinerConfig.minerMetricsUrl(MinerConfig.defaultMinerMetricsPort), _httpClient = http.Client(); /// Start polling for metrics every second diff --git a/miner-app/lib/src/services/miner_process.dart b/miner-app/lib/src/services/miner_process.dart index 026d4a96..11747445 100644 --- a/miner-app/lib/src/services/miner_process.dart +++ b/miner-app/lib/src/services/miner_process.dart @@ -7,6 +7,7 @@ import 'package:quantus_miner/src/config/miner_config.dart'; import 'package:quantus_miner/src/services/process_cleanup_service.dart'; import 'package:quantus_miner/src/services/prometheus_service.dart'; import 'package:quantus_miner/src/shared/extensions/log_string_extension.dart'; +import 'package:quantus_miner/src/utils/app_logger.dart'; import './binary_manager.dart'; import './chain_rpc_client.dart'; @@ -14,6 +15,8 @@ import './external_miner_api_client.dart'; import './log_filter_service.dart'; import './mining_stats_service.dart'; +final _log = log.withTag('MinerProcess'); + class LogEntry { final String message; final DateTime timestamp; @@ -146,13 +149,11 @@ class MinerProcess { final nodeKeyFileFromFileSystem = await BinaryManager.getNodeKeyFile(); if (await nodeKeyFileFromFileSystem.exists()) { final stat = await nodeKeyFileFromFileSystem.stat(); - print( - 'DEBUG: nodeKeyFileFromFileSystem (${nodeKeyFileFromFileSystem.path}) exists (size: ${stat.size} bytes)', + _log.d( + 'Node key file exists (${nodeKeyFileFromFileSystem.path}), size: ${stat.size} bytes', ); } else { - print( - 'DEBUG: nodeKeyFileFromFileSystem (${nodeKeyFileFromFileSystem.path}) does not exist.', - ); + _log.d('Node key file does not exist: ${nodeKeyFileFromFileSystem.path}'); } if (!await identityPath.exists()) { @@ -167,7 +168,7 @@ class MinerProcess { } rewardsAddress = await rewardsPath.readAsString(); rewardsAddress = rewardsAddress.trim(); // Remove any whitespace/newlines - print('DEBUG: Read rewards address from file: $rewardsAddress'); + _log.d('Read rewards address: $rewardsAddress'); } catch (e) { throw Exception( 'Failed to read rewards address from file ${rewardsPath.path}: $e', @@ -197,8 +198,7 @@ class MinerProcess { '--enable-peer-sharing', ]; - print('DEBUG: Executing command:\n ${bin.path} ${args.join(' ')}'); - print('DEBUG: Args: ${args.join('\n')}'); + _log.d('Executing: ${bin.path} ${args.join(' ')}'); _nodeProcess = await Process.start(bin.path, args); _stdoutFilter = LogFilterService(); @@ -238,7 +238,11 @@ class MinerProcess { source: source, ); _logsController.add(logEntry); - print(source == 'node' ? '[node] $line' : '[node-error] $line'); + if (source == 'node-error') { + _log.w('[node] $line'); + } else { + _log.d('[node] $line'); + } } } @@ -268,7 +272,7 @@ class MinerProcess { onStatsUpdate?.call(_statsService.currentStats); } catch (e) { - print('Failed to fetch target block height: $e'); + _log.w('Failed to fetch target block height', error: e); } } @@ -324,7 +328,7 @@ class MinerProcess { source: 'quantus-miner', ); _logsController.add(logEntry); - print('[ext-miner] $line'); + _log.d('[miner] $line'); }); _externalMinerProcess!.stderr @@ -337,13 +341,17 @@ class MinerProcess { source: line.isMinerError ? 'quantus-miner-error' : 'quantus-miner', ); _logsController.add(logEntry); - print('[ext-miner-err] $line'); + if (line.isMinerError) { + _log.w('[miner] $line'); + } else { + _log.d('[miner] $line'); + } }); // Monitor external miner process exit _externalMinerProcess!.exitCode.then((exitCode) { if (exitCode != 0) { - print('External miner process exited with code: $exitCode'); + _log.w('External miner exited with code: $exitCode'); } }); @@ -371,7 +379,7 @@ class MinerProcess { } void stop() { - print('MinerProcess: stop() called. Killing processes.'); + _log.i('Stopping mining processes...'); _syncStatusTimer?.cancel(); _externalMinerApiClient.stopPolling(); _chainRpcClient.stopPolling(); @@ -379,70 +387,62 @@ class MinerProcess { // Kill external miner process first if (_externalMinerProcess != null) { try { - print( - 'MinerProcess: Attempting to kill external miner process (PID: ${_externalMinerProcess!.pid})', - ); + _log.d('Killing external miner (PID: ${_externalMinerProcess!.pid})'); // Try graceful termination first _externalMinerProcess!.kill(ProcessSignal.sigterm); // Wait briefly for graceful shutdown - Future.delayed(const Duration(seconds: 2)).then((_) async { + Future.delayed(MinerConfig.gracefulShutdownTimeout).then((_) async { // Check if process is still running and force kill if necessary try { if (await _isProcessRunning(_externalMinerProcess!.pid)) { - print( - 'MinerProcess: External miner still running, force killing...', - ); + _log.d('External miner still running, force killing...'); _externalMinerProcess!.kill(ProcessSignal.sigkill); } } catch (e) { // Process is already dead, which is what we want - print('MinerProcess: External miner process already terminated'); + _log.d('External miner already terminated'); } }); } catch (e) { - print('MinerProcess: Error killing external miner process: $e'); + _log.e('Error killing external miner', error: e); // Try force kill as backup try { _externalMinerProcess!.kill(ProcessSignal.sigkill); } catch (e2) { - print( - 'MinerProcess: Error force killing external miner process: $e2', - ); + _log.e('Error force killing external miner', error: e2); } } } // Kill node process try { - print( - 'MinerProcess: Attempting to kill node process (PID: ${_nodeProcess.pid})', - ); + _log.d('Killing node process (PID: ${_nodeProcess.pid})'); // Try graceful termination first _nodeProcess.kill(ProcessSignal.sigterm); // Wait briefly for graceful shutdown - Future.delayed(const Duration(seconds: 2)).then((_) async { + Future.delayed(MinerConfig.gracefulShutdownTimeout).then((_) async { // Check if process is still running and force kill if necessary try { if (await _isProcessRunning(_nodeProcess.pid)) { - print('MinerProcess: Node process still running, force killing...'); + _log.d('Node still running, force killing...'); _nodeProcess.kill(ProcessSignal.sigkill); } } catch (e) { // Process is already dead, which is what we want - print('MinerProcess: Node process already terminated'); + _log.d('Node already terminated'); } }); } catch (e) { - print('MinerProcess: Error killing node process: $e'); + _log.e('Error killing node process', error: e); // Try force kill as backup try { _nodeProcess.kill(ProcessSignal.sigkill); } catch (e2) { - print('MinerProcess: Error force killing node process: $e2'); + _log.e('Error force killing node process', error: e2); } } @@ -454,7 +454,7 @@ class MinerProcess { /// Force stop both processes immediately with SIGKILL void forceStop() { - print('MinerProcess: forceStop() called. Force killing processes.'); + _log.i('Force stopping all processes...'); _syncStatusTimer?.cancel(); final List> killFutures = []; @@ -466,7 +466,7 @@ class MinerProcess { try { _externalMinerProcess!.kill(ProcessSignal.sigkill); } catch (e) { - print('MinerProcess: Error force killing external miner process: $e'); + _log.e('Error force killing external miner', error: e); } _externalMinerProcess = null; } @@ -477,14 +477,14 @@ class MinerProcess { killFutures.add(_forceKillProcess(nodePid, 'node')); _nodeProcess.kill(ProcessSignal.sigkill); } catch (e) { - print('MinerProcess: Error force killing node process: $e'); + _log.e('Error force killing node', error: e); } // Wait for all kills to complete (with timeout) Future.wait(killFutures).timeout( - const Duration(seconds: 5), + MinerConfig.forceKillTimeout, onTimeout: () { - print('MinerProcess: Force kill operations timed out'); + _log.w('Force kill operations timed out'); return []; }, ); @@ -603,18 +603,17 @@ class MinerProcess { /// Wait for the node RPC to be ready (blocking) /// Used to ensure node is ready before starting miner Future _waitForNodeRpcReady() async { - print('DEBUG: Waiting for node RPC to be ready before starting miner...'); + _log.d('Waiting for node RPC to be ready...'); // Try to connect to RPC endpoint with exponential backoff int attempts = 0; - const maxAttempts = 30; // Up to ~3 minutes of retries - Duration delay = const Duration(seconds: 2); + Duration delay = MinerConfig.rpcInitialRetryDelay; - while (attempts < maxAttempts) { + while (attempts < MinerConfig.maxRpcRetries) { try { final isReady = await _chainRpcClient.isReachable(); if (isReady) { - print('DEBUG: Node RPC is ready! Proceeding to start miner...'); + _log.i('Node RPC is ready'); return; } } catch (e) { @@ -622,32 +621,32 @@ class MinerProcess { } attempts++; - print( - 'DEBUG: Node RPC not ready yet (attempt $attempts/$maxAttempts), waiting ${delay.inSeconds}s...', + _log.d( + 'Node RPC not ready (attempt $attempts/${MinerConfig.maxRpcRetries}), waiting ${delay.inSeconds}s...', ); await Future.delayed(delay); - // Exponential backoff, but cap at 10 seconds - if (delay.inSeconds < 10) { + // Exponential backoff, but cap at max retry delay + if (delay < MinerConfig.rpcMaxRetryDelay) { delay = Duration(seconds: (delay.inSeconds * 1.5).round()); + if (delay > MinerConfig.rpcMaxRetryDelay) { + delay = MinerConfig.rpcMaxRetryDelay; + } } } - print( - 'WARNING: Node RPC not ready after $maxAttempts attempts, proceeding to start miner anyway...', + _log.w( + 'Node RPC not ready after ${MinerConfig.maxRpcRetries} attempts, proceeding anyway...', ); } void _handleChainInfoUpdate(ChainInfo info) { - print( - 'DEBUG: Successfully received chain info - Peers: ${info.peerCount}, Block: ${info.currentBlock}', - ); + _log.d('Chain info: peers=${info.peerCount}, block=${info.currentBlock}'); // Update peer count from RPC (most accurate) if (info.peerCount >= 0) { _statsService.updatePeerCount(info.peerCount); - print('DEBUG: Updated peer count to: ${info.peerCount}'); } // Update chain name from RPC @@ -659,9 +658,6 @@ class MinerProcess { info.currentBlock, info.targetBlock ?? info.currentBlock, ); - print( - 'DEBUG: Updated blocks - current: ${info.currentBlock}, target: ${info.targetBlock ?? info.currentBlock}, syncing: ${info.isSyncing}, chain: ${info.chainName}', - ); onStatsUpdate?.call(_statsService.currentStats); } @@ -670,7 +666,7 @@ class MinerProcess { void _handleChainRpcError(String error) { // Only log significant RPC errors, not connection issues during startup if (!error.contains('Connection refused') && !error.contains('timeout')) { - print('Chain RPC error: $error'); + _log.w('Chain RPC error: $error'); } } diff --git a/miner-app/lib/src/services/process_cleanup_service.dart b/miner-app/lib/src/services/process_cleanup_service.dart index 49a6b1cd..18a8b1a1 100644 --- a/miner-app/lib/src/services/process_cleanup_service.dart +++ b/miner-app/lib/src/services/process_cleanup_service.dart @@ -2,6 +2,9 @@ import 'dart:io'; import 'package:quantus_miner/src/config/miner_config.dart'; import 'package:quantus_miner/src/services/binary_manager.dart'; +import 'package:quantus_miner/src/utils/app_logger.dart'; + +final _log = log.withTag('ProcessCleanup'); /// Service responsible for platform-specific process management operations. /// @@ -43,7 +46,7 @@ class ProcessCleanupService { /// Returns true if the process was successfully killed or was already dead. static Future forceKillProcess(int pid, String processName) async { try { - print('ProcessCleanupService: Force killing $processName (PID: $pid)'); + _log.d(' Force killing $processName (PID: $pid)'); if (Platform.isWindows) { return await _forceKillWindowsProcess(pid, processName); @@ -51,9 +54,7 @@ class ProcessCleanupService { return await _forceKillUnixProcess(pid, processName); } } catch (e) { - print( - 'ProcessCleanupService: Error in forceKillProcess for $processName: $e', - ); + _log.e('Error killing $processName', error: e); return false; } } @@ -69,12 +70,10 @@ class ProcessCleanupService { ]); if (killResult.exitCode == 0) { - print( - 'ProcessCleanupService: Successfully force killed $processName (PID: $pid)', - ); + _log.d('Killed $processName (PID: $pid)'); } else { - print( - 'ProcessCleanupService: taskkill failed for $processName (PID: $pid), exit code: ${killResult.exitCode}', + _log.w( + 'taskkill failed for $processName (PID: $pid), exit: ${killResult.exitCode}', ); } @@ -83,9 +82,7 @@ class ProcessCleanupService { // Verify process is dead final checkResult = await Process.run('tasklist', ['/FI', 'PID eq $pid']); if (checkResult.stdout.toString().contains(' $pid ')) { - print( - 'ProcessCleanupService: WARNING - $processName (PID: $pid) may still be running', - ); + _log.w('$processName (PID: $pid) may still be running'); // Try by name as last resort final binaryName = processName.contains('miner') @@ -95,9 +92,7 @@ class ProcessCleanupService { return false; } - print( - 'ProcessCleanupService: Verified $processName (PID: $pid) is terminated', - ); + _log.d('Verified $processName (PID: $pid) terminated'); return true; } @@ -106,12 +101,10 @@ class ProcessCleanupService { final killResult = await Process.run('kill', ['-9', pid.toString()]); if (killResult.exitCode == 0) { - print( - 'ProcessCleanupService: Successfully force killed $processName (PID: $pid)', - ); + _log.d('Killed $processName (PID: $pid)'); } else { - print( - 'ProcessCleanupService: kill command failed for $processName (PID: $pid), exit code: ${killResult.exitCode}', + _log.w( + 'kill failed for $processName (PID: $pid), exit: ${killResult.exitCode}', ); } @@ -120,9 +113,7 @@ class ProcessCleanupService { // Verify process is dead final checkResult = await Process.run('kill', ['-0', pid.toString()]); if (checkResult.exitCode == 0) { - print( - 'ProcessCleanupService: WARNING - $processName (PID: $pid) may still be running', - ); + _log.w('$processName (PID: $pid) may still be running'); // Try pkill as last resort final binaryName = processName.contains('miner') @@ -132,9 +123,7 @@ class ProcessCleanupService { return false; } - print( - 'ProcessCleanupService: Verified $processName (PID: $pid) is terminated', - ); + _log.d('Verified $processName (PID: $pid) terminated'); return true; } @@ -172,9 +161,7 @@ class ProcessCleanupService { final result = await Process.run('netstat', ['-an']); return result.stdout.toString().contains(':$port'); } catch (e2) { - print( - 'ProcessCleanupService: Could not check port $port availability: $e2', - ); + _log.d('Could not check port $port availability'); return false; } } @@ -269,11 +256,7 @@ class ProcessCleanupService { if (await isPortInUse(metricsPort)) { // Try to find an alternative port final altPort = await findAvailablePort(metricsPort + 1); - if (altPort != metricsPort + 1) { - print( - 'ProcessCleanupService: Using alternative metrics port: $altPort', - ); - } + _log.i('Using alternative metrics port: $altPort'); result['metrics'] = altPort; } } @@ -362,7 +345,7 @@ class ProcessCleanupService { if (await lockFile.exists()) { await lockFile.delete(); - print('ProcessCleanupService: Deleted lock file: $lockFilePath'); + _log.d(' Deleted lock file: $lockFilePath'); } // Also check for other potential lock files @@ -372,7 +355,7 @@ class ProcessCleanupService { if (entity is File && entity.path.contains('LOCK')) { try { await entity.delete(); - print('ProcessCleanupService: Deleted lock file: ${entity.path}'); + _log.d(' Deleted lock file: ${entity.path}'); } catch (e) { // Ignore cleanup errors } @@ -439,7 +422,7 @@ class ProcessCleanupService { /// This is a more aggressive cleanup used during app exit. static Future killAllQuantusProcesses() async { try { - print('ProcessCleanupService: Killing all quantus processes...'); + _log.d(' Killing all quantus processes...'); if (Platform.isWindows) { await Process.run('taskkill', [ @@ -457,9 +440,9 @@ class ProcessCleanupService { await Process.run('pkill', ['-9', '-f', MinerConfig.minerBinaryName]); } - print('ProcessCleanupService: Cleanup commands executed'); + _log.d(' Cleanup commands executed'); } catch (e) { - print('ProcessCleanupService: Error killing processes: $e'); + _log.d(' Error killing processes: $e'); } } } diff --git a/miner-app/lib/src/services/prometheus_service.dart b/miner-app/lib/src/services/prometheus_service.dart index 9ade835f..46540403 100644 --- a/miner-app/lib/src/services/prometheus_service.dart +++ b/miner-app/lib/src/services/prometheus_service.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'package:http/http.dart' as http; +import 'package:quantus_miner/src/config/miner_config.dart'; // Data class to hold the parsed metrics class PrometheusMetrics { @@ -24,7 +25,10 @@ class PrometheusMetrics { class PrometheusService { final String metricsUrl; - PrometheusService({this.metricsUrl = 'http://127.0.0.1:9616/metrics'}); + PrometheusService({String? metricsUrl}) + : metricsUrl = + metricsUrl ?? + MinerConfig.nodePrometheusUrl(MinerConfig.defaultNodePrometheusPort); Future fetchMetrics() async { try { diff --git a/miner-app/lib/src/utils/app_logger.dart b/miner-app/lib/src/utils/app_logger.dart new file mode 100644 index 00000000..54c1bc3b --- /dev/null +++ b/miner-app/lib/src/utils/app_logger.dart @@ -0,0 +1,192 @@ +import 'package:flutter/foundation.dart'; +import 'package:logger/logger.dart'; + +/// Application-wide logger instance. +/// +/// Usage: +/// ```dart +/// import 'package:quantus_miner/src/utils/app_logger.dart'; +/// +/// log.d('Debug message'); +/// log.i('Info message'); +/// log.w('Warning message'); +/// log.e('Error message', error: e, stackTrace: st); +/// ``` +/// +/// Log levels: +/// - `d` (debug): Detailed debugging information +/// - `i` (info): General information about app operation +/// - `w` (warning): Potential issues that don't prevent operation +/// - `e` (error): Errors that affect functionality +final Logger log = Logger( + // In release mode, only show warnings and errors + // In debug mode, show all logs + level: kReleaseMode ? Level.warning : Level.all, + printer: _AppLogPrinter(), + // No file output for now + output: null, +); + +/// Custom log printer for cleaner console output. +/// +/// Format: `[LEVEL] [SOURCE] message` +/// Example: `[D] [MinerProcess] Starting node...` +class _AppLogPrinter extends LogPrinter { + static final _levelPrefixes = { + Level.trace: 'T', + Level.debug: 'D', + Level.info: 'I', + Level.warning: 'W', + Level.error: 'E', + Level.fatal: 'F', + }; + + static final _levelColors = { + Level.trace: AnsiColor.fg(AnsiColor.grey(0.5)), + Level.debug: AnsiColor.none(), + Level.info: AnsiColor.fg(12), // Light blue + Level.warning: AnsiColor.fg(208), // Orange + Level.error: AnsiColor.fg(196), // Red + Level.fatal: AnsiColor.fg(199), // Magenta + }; + + @override + List log(LogEvent event) { + final prefix = _levelPrefixes[event.level] ?? '?'; + final color = _levelColors[event.level] ?? AnsiColor.none(); + final time = _formatTime(event.time); + + final messageStr = event.message.toString(); + final lines = []; + + // Main log line + lines.add(color('[$time] [$prefix] $messageStr')); + + // Add error if present + if (event.error != null) { + lines.add(color('[$time] [$prefix] Error: ${event.error}')); + } + + // Add stack trace if present (only for errors) + if (event.stackTrace != null && event.level.index >= Level.error.index) { + final stackLines = event.stackTrace.toString().split('\n').take(5); + for (final line in stackLines) { + if (line.trim().isNotEmpty) { + lines.add(color('[$time] [$prefix] $line')); + } + } + } + + return lines; + } + + String _formatTime(DateTime time) { + return '${time.hour.toString().padLeft(2, '0')}:' + '${time.minute.toString().padLeft(2, '0')}:' + '${time.second.toString().padLeft(2, '0')}'; + } +} + +/// Extension to make logging from specific sources cleaner. +/// +/// Usage: +/// ```dart +/// final _log = log.withTag('MinerProcess'); +/// _log.d('Starting...'); // Output: [D] [MinerProcess] Starting... +/// ``` +extension TaggedLogger on Logger { + /// Create a logger that prefixes all messages with a tag. + TaggedLoggerWrapper withTag(String tag) => TaggedLoggerWrapper(this, tag); +} + +/// Wrapper that adds a tag prefix to all log messages. +class TaggedLoggerWrapper { + final Logger _logger; + final String _tag; + + TaggedLoggerWrapper(this._logger, this._tag); + + void t( + dynamic message, { + DateTime? time, + Object? error, + StackTrace? stackTrace, + }) { + _logger.t( + '[$_tag] $message', + time: time, + error: error, + stackTrace: stackTrace, + ); + } + + void d( + dynamic message, { + DateTime? time, + Object? error, + StackTrace? stackTrace, + }) { + _logger.d( + '[$_tag] $message', + time: time, + error: error, + stackTrace: stackTrace, + ); + } + + void i( + dynamic message, { + DateTime? time, + Object? error, + StackTrace? stackTrace, + }) { + _logger.i( + '[$_tag] $message', + time: time, + error: error, + stackTrace: stackTrace, + ); + } + + void w( + dynamic message, { + DateTime? time, + Object? error, + StackTrace? stackTrace, + }) { + _logger.w( + '[$_tag] $message', + time: time, + error: error, + stackTrace: stackTrace, + ); + } + + void e( + dynamic message, { + DateTime? time, + Object? error, + StackTrace? stackTrace, + }) { + _logger.e( + '[$_tag] $message', + time: time, + error: error, + stackTrace: stackTrace, + ); + } + + void f( + dynamic message, { + DateTime? time, + Object? error, + StackTrace? stackTrace, + }) { + _logger.f( + '[$_tag] $message', + time: time, + error: error, + stackTrace: stackTrace, + ); + } +} diff --git a/miner-app/pubspec.yaml b/miner-app/pubspec.yaml index c3d1e6fb..94b17a12 100644 --- a/miner-app/pubspec.yaml +++ b/miner-app/pubspec.yaml @@ -19,14 +19,10 @@ dependencies: http: # Version managed by melos.yaml shared_preferences: # Version managed by melos.yaml - # State management and architecture - provider: # Version managed by melos.yaml - hooks_riverpod: # Version managed by melos.yaml - flutter_hooks: # Version managed by melos.yaml + # Routing go_router: # Version managed by melos.yaml # UI and utilities - another_flushbar: # Version managed by melos.yaml flutter_svg: # Version managed by melos.yaml # System and file operations From b27fae63ee576acf1812b55a08c205e023b2cc08 Mon Sep 17 00:00:00 2001 From: illuzen Date: Mon, 2 Feb 2026 08:51:49 +0800 Subject: [PATCH 06/10] split up MinerProcess into smaller files --- .../lib/features/miner/miner_controls.dart | 207 +++--- .../miner/miner_dashboard_screen.dart | 94 ++- miner-app/lib/main.dart | 31 +- miner-app/lib/src/models/miner_error.dart | 92 +++ .../src/services/log_stream_processor.dart | 164 +++++ miner-app/lib/src/services/miner_process.dart | 679 ------------------ .../src/services/miner_process_manager.dart | 241 +++++++ .../lib/src/services/mining_orchestrator.dart | 534 ++++++++++++++ .../src/services/node_process_manager.dart | 260 +++++++ miner-app/lib/src/ui/logs_widget.dart | 20 +- 10 files changed, 1488 insertions(+), 834 deletions(-) create mode 100644 miner-app/lib/src/models/miner_error.dart create mode 100644 miner-app/lib/src/services/log_stream_processor.dart delete mode 100644 miner-app/lib/src/services/miner_process.dart create mode 100644 miner-app/lib/src/services/miner_process_manager.dart create mode 100644 miner-app/lib/src/services/mining_orchestrator.dart create mode 100644 miner-app/lib/src/services/node_process_manager.dart diff --git a/miner-app/lib/features/miner/miner_controls.dart b/miner-app/lib/features/miner/miner_controls.dart index 9320ebbd..335bfdeb 100644 --- a/miner-app/lib/features/miner/miner_controls.dart +++ b/miner-app/lib/features/miner/miner_controls.dart @@ -2,27 +2,25 @@ import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:quantus_miner/src/services/mining_orchestrator.dart'; import 'package:quantus_miner/src/services/mining_stats_service.dart'; import 'package:quantus_miner/src/shared/extensions/snackbar_extensions.dart'; import '../../main.dart'; import '../../src/services/binary_manager.dart'; import '../../src/services/gpu_detection_service.dart'; -import '../../src/services/miner_process.dart'; import '../../src/services/miner_settings_service.dart'; class MinerControls extends StatefulWidget { - final MinerProcess? minerProcess; + final MiningOrchestrator? orchestrator; final MiningStats miningStats; - final Function(MiningStats) onMetricsUpdate; - final Function(MinerProcess?) onMinerProcessChanged; + final Function(MiningOrchestrator?) onOrchestratorChanged; const MinerControls({ super.key, - required this.minerProcess, + required this.orchestrator, required this.miningStats, - required this.onMetricsUpdate, - required this.onMinerProcessChanged, + required this.onOrchestratorChanged, }); @override @@ -34,6 +32,7 @@ class _MinerControlsState extends State { int _cpuWorkers = 8; int _gpuDevices = 0; int _detectedGpuCount = 0; + String _chainId = 'dev'; final _settingsService = MinerSettingsService(); @override @@ -46,6 +45,7 @@ class _MinerControlsState extends State { Future _loadSettings() async { final savedCpuWorkers = await _settingsService.getCpuWorkers(); final savedGpuDevices = await _settingsService.getGpuDevices(); + final savedChainId = await _settingsService.getChainId(); if (mounted) { setState(() { @@ -53,6 +53,7 @@ class _MinerControlsState extends State { savedCpuWorkers ?? (Platform.numberOfProcessors > 0 ? Platform.numberOfProcessors : 8); _gpuDevices = savedGpuDevices ?? 0; + _chainId = savedChainId; }); } } @@ -70,125 +71,111 @@ class _MinerControlsState extends State { if (_isAttemptingToggle) return; setState(() => _isAttemptingToggle = true); - if (widget.minerProcess == null) { - print('Starting mining'); - - // Check for all required files and binaries - final id = File( - '${await BinaryManager.getQuantusHomeDirectoryPath()}/node_key.p2p', - ); - final rew = File( - '${await BinaryManager.getQuantusHomeDirectoryPath()}/rewards-address.txt', - ); - final binPath = await BinaryManager.getNodeBinaryFilePath(); - final bin = File(binPath); - final minerBinPath = await BinaryManager.getExternalMinerBinaryFilePath(); - final minerBin = File(minerBinPath); + if (widget.orchestrator == null || !widget.orchestrator!.isMining) { + // Start mining + await _startMining(); + } else { + // Stop mining + await _stopMining(); + } - // Check node binary - if (!await bin.exists()) { - print('Node binary not found. Cannot start mining.'); - if (mounted) { - context.showWarningSnackbar( - title: 'Node binary not found!', - message: 'Please run setup.', - ); - } - setState(() => _isAttemptingToggle = false); - return; - } + if (mounted) { + setState(() => _isAttemptingToggle = false); + } + } - // Check external miner binary - if (!await minerBin.exists()) { - print('External miner binary not found. Cannot start mining.'); - if (mounted) { - context.showWarningSnackbar( - title: 'External miner binary not found!', - message: 'Please run setup.', - ); - } - setState(() => _isAttemptingToggle = false); - return; - } + Future _startMining() async { + print('Starting mining'); - final newProc = MinerProcess( - bin, - id, - rew, - onStatsUpdate: widget.onMetricsUpdate, - cpuWorkers: _cpuWorkers, - gpuDevices: _gpuDevices, - detectedGpuCount: _detectedGpuCount, - ); - // Notify parent about the new miner process - widget.onMinerProcessChanged.call(newProc); + // Check for all required files and binaries + final quantusHome = await BinaryManager.getQuantusHomeDirectoryPath(); + final identityFile = File('$quantusHome/node_key.p2p'); + final rewardsFile = File('$quantusHome/rewards-address.txt'); + final nodeBinPath = await BinaryManager.getNodeBinaryFilePath(); + final nodeBin = File(nodeBinPath); + final minerBinPath = await BinaryManager.getExternalMinerBinaryFilePath(); + final minerBin = File(minerBinPath); - try { - final newMiningStats = widget.miningStats.copyWith( - isSyncing: true, - status: MiningStatus.syncing, + // Check node binary + if (!await nodeBin.exists()) { + print('Node binary not found. Cannot start mining.'); + if (mounted) { + context.showWarningSnackbar( + title: 'Node binary not found!', + message: 'Please run setup.', ); - widget.onMetricsUpdate(newMiningStats); - await newProc.start(); - } catch (e) { - print('Error starting miner process: $e'); - if (mounted) { - context.showErrorSnackbar( - title: 'Error starting miner!', - message: e.toString(), - ); - } - - // Notify parent that miner process is null - widget.onMinerProcessChanged.call(null); - final newMiningStats = MiningStats.empty(); - widget.onMetricsUpdate(newMiningStats); } - } else { - print('Stopping mining'); + return; + } - try { - widget.minerProcess!.stop(); - // Wait a moment for graceful shutdown - await Future.delayed(const Duration(seconds: 1)); - } catch (e) { - print('Error during graceful stop: $e'); + // Check external miner binary + if (!await minerBin.exists()) { + print('External miner binary not found. Cannot start mining.'); + if (mounted) { + context.showWarningSnackbar( + title: 'External miner binary not found!', + message: 'Please run setup.', + ); } + return; + } - await GlobalMinerManager.cleanup(); + // Create new orchestrator + final orchestrator = MiningOrchestrator(); + widget.onOrchestratorChanged(orchestrator); - // Notify parent that miner process is stopped - widget.onMinerProcessChanged.call(null); - final newMiningStats = MiningStats.empty(); - widget.onMetricsUpdate(newMiningStats); - } - if (mounted) { - setState(() => _isAttemptingToggle = false); + try { + await orchestrator.start( + MiningSessionConfig( + nodeBinary: nodeBin, + minerBinary: minerBin, + identityFile: identityFile, + rewardsFile: rewardsFile, + chainId: _chainId, + cpuWorkers: _cpuWorkers, + gpuDevices: _gpuDevices, + detectedGpuCount: _detectedGpuCount, + ), + ); + } catch (e) { + print('Error starting miner: $e'); + if (mounted) { + context.showErrorSnackbar( + title: 'Error starting miner!', + message: e.toString(), + ); + } + + // Clean up on failure + orchestrator.dispose(); + widget.onOrchestratorChanged(null); } } - @override - void dispose() { - // _poll?.cancel(); // _poll removed - if (widget.minerProcess != null) { - print('MinerControls: disposing, force stopping miner process'); + Future _stopMining() async { + print('Stopping mining'); + if (widget.orchestrator != null) { try { - widget.minerProcess!.forceStop(); + await widget.orchestrator!.stop(); } catch (e) { - print( - 'MinerControls: Error force stopping miner process in dispose: $e', - ); + print('Error during stop: $e'); } - // Use GlobalMinerManager for comprehensive cleanup - GlobalMinerManager.cleanup(); - - widget.onMinerProcessChanged.call(null); + widget.orchestrator!.dispose(); } + + await GlobalMinerManager.cleanup(); + widget.onOrchestratorChanged(null); + } + + @override + void dispose() { super.dispose(); } + bool get _isMining => widget.orchestrator?.isMining ?? false; + @override Widget build(BuildContext context) { return Column( @@ -222,7 +209,7 @@ class _MinerControlsState extends State { ? Platform.numberOfProcessors : 16), label: _cpuWorkers.toString(), - onChanged: widget.minerProcess == null + onChanged: !_isMining ? (value) { final rounded = value.round(); setState(() => _cpuWorkers = rounded); @@ -256,7 +243,7 @@ class _MinerControlsState extends State { max: _detectedGpuCount > 0 ? _detectedGpuCount.toDouble() : 1, divisions: _detectedGpuCount > 0 ? _detectedGpuCount : 1, label: _gpuDevices.toString(), - onChanged: widget.minerProcess == null + onChanged: !_isMining ? (value) { final rounded = value.round(); setState(() => _gpuDevices = rounded); @@ -270,9 +257,7 @@ class _MinerControlsState extends State { const SizedBox(height: 24), ElevatedButton( style: ElevatedButton.styleFrom( - backgroundColor: widget.minerProcess == null - ? Colors.green - : Colors.blue, + backgroundColor: !_isMining ? Colors.green : Colors.blue, padding: const EdgeInsets.symmetric(vertical: 15), textStyle: const TextStyle( fontSize: 18, @@ -281,9 +266,7 @@ class _MinerControlsState extends State { minimumSize: const Size(200, 50), ), onPressed: _isAttemptingToggle ? null : _toggle, - child: Text( - widget.minerProcess == null ? 'Start Mining' : 'Stop Mining', - ), + child: Text(!_isMining ? 'Start Mining' : 'Stop Mining'), ), ], ); diff --git a/miner-app/lib/features/miner/miner_dashboard_screen.dart b/miner-app/lib/features/miner/miner_dashboard_screen.dart index 032b4067..9a360c72 100644 --- a/miner-app/lib/features/miner/miner_dashboard_screen.dart +++ b/miner-app/lib/features/miner/miner_dashboard_screen.dart @@ -5,8 +5,9 @@ import 'package:quantus_miner/features/miner/miner_balance_card.dart'; import 'package:quantus_miner/features/miner/miner_app_bar.dart'; import 'package:quantus_miner/features/miner/miner_stats_card.dart'; import 'package:quantus_miner/features/miner/miner_status.dart'; +import 'package:quantus_miner/src/models/miner_error.dart'; import 'package:quantus_miner/src/services/binary_manager.dart'; -import 'package:quantus_miner/src/services/miner_process.dart'; +import 'package:quantus_miner/src/services/mining_orchestrator.dart'; import 'package:quantus_miner/src/services/mining_stats_service.dart'; import 'package:quantus_miner/src/shared/extensions/snackbar_extensions.dart'; import 'package:quantus_miner/src/ui/logs_widget.dart'; @@ -34,25 +35,30 @@ class _MinerDashboardScreenState extends State { Timer? _nodePollingTimer; MiningStats _miningStats = MiningStats.empty(); - MinerProcess? _currentMinerProcess; + + // The orchestrator manages all mining operations + MiningOrchestrator? _orchestrator; + + // Subscriptions + StreamSubscription? _statsSubscription; + StreamSubscription? _errorSubscription; @override void initState() { super.initState(); - _initializeNodeUpdatePolling(); _initializeMinerUpdatePolling(); } @override void dispose() { - // Clean up global miner process - if (_currentMinerProcess != null) { - try { - _currentMinerProcess!.forceStop(); - } catch (e) { - print('MinerDashboard: Error stopping miner process on dispose: $e'); - } + // Clean up subscriptions + _statsSubscription?.cancel(); + _errorSubscription?.cancel(); + + // Clean up orchestrator + if (_orchestrator != null) { + _orchestrator!.forceStop(); } GlobalMinerManager.cleanup(); @@ -62,21 +68,62 @@ class _MinerDashboardScreenState extends State { super.dispose(); } - void _onMetricsUpdate(MiningStats miningStats) { - setState(() { - _miningStats = miningStats; - }); + void _onStatsUpdate(MiningStats stats) { + if (mounted) { + setState(() { + _miningStats = stats; + }); + } } - void _onMinerProcessChanged(MinerProcess? minerProcess) { + void _onOrchestratorChanged(MiningOrchestrator? orchestrator) { + // Cancel old subscriptions + _statsSubscription?.cancel(); + _errorSubscription?.cancel(); + if (mounted) { setState(() { - _currentMinerProcess = minerProcess; + _orchestrator = orchestrator; }); } - // Register with global app lifecycle for cleanup - GlobalMinerManager.setMinerProcess(minerProcess); + // Set up new subscriptions + if (orchestrator != null) { + _statsSubscription = orchestrator.statsStream.listen(_onStatsUpdate); + _errorSubscription = orchestrator.errorStream.listen(_onError); + } + + // Register with global manager for cleanup + GlobalMinerManager.setOrchestrator(orchestrator); + } + + void _onError(MinerError error) { + if (!mounted) return; + + // Show error to user + context.showErrorSnackbar( + title: _getErrorTitle(error), + message: error.message, + ); + } + + String _getErrorTitle(MinerError error) { + switch (error.type) { + case MinerErrorType.minerCrashed: + return 'Miner Crashed'; + case MinerErrorType.nodeCrashed: + return 'Node Crashed'; + case MinerErrorType.minerStartupFailed: + return 'Miner Startup Failed'; + case MinerErrorType.nodeStartupFailed: + return 'Node Startup Failed'; + case MinerErrorType.metricsConnectionLost: + return 'Metrics Connection Lost'; + case MinerErrorType.rpcConnectionLost: + return 'RPC Connection Lost'; + case MinerErrorType.unknown: + return 'Error'; + } } void _initializeMinerUpdatePolling() { @@ -114,7 +161,7 @@ class _MinerDashboardScreenState extends State { } void _handleUpdateMiner() async { - if (_currentMinerProcess != null) { + if (_orchestrator?.isMining == true) { context.showErrorSnackbar( title: 'Miner is running!', message: 'To update the binary please stop the miner first.', @@ -177,7 +224,7 @@ class _MinerDashboardScreenState extends State { } void _handleUpdateNode() async { - if (_currentMinerProcess != null) { + if (_orchestrator?.isMining == true) { context.showErrorSnackbar( title: 'Miner is running!', message: 'To update the binary please stop the miner first.', @@ -263,10 +310,9 @@ class _MinerDashboardScreenState extends State { child: SizedBox( width: double.infinity, child: MinerControls( - minerProcess: _currentMinerProcess, + orchestrator: _orchestrator, miningStats: _miningStats, - onMetricsUpdate: _onMetricsUpdate, - onMinerProcessChanged: _onMinerProcessChanged, + onOrchestratorChanged: _onOrchestratorChanged, ), ), ), @@ -332,7 +378,7 @@ class _MinerDashboardScreenState extends State { // Logs content Expanded( child: LogsWidget( - minerProcess: _currentMinerProcess, + orchestrator: _orchestrator, maxLines: 200, ), ), diff --git a/miner-app/lib/main.dart b/miner-app/lib/main.dart index 8936254b..329c59d4 100644 --- a/miner-app/lib/main.dart +++ b/miner-app/lib/main.dart @@ -8,7 +8,7 @@ import 'features/setup/node_identity_setup_screen.dart'; import 'features/setup/rewards_address_setup_screen.dart'; import 'features/miner/miner_dashboard_screen.dart'; import 'src/services/binary_manager.dart'; -import 'src/services/miner_process.dart'; +import 'src/services/mining_orchestrator.dart'; import 'src/services/process_cleanup_service.dart'; import 'src/utils/app_logger.dart'; @@ -16,27 +16,34 @@ import 'package:quantus_sdk/quantus_sdk.dart'; final _log = log.withTag('App'); -/// Global class to manage miner process lifecycle +/// Global class to manage mining orchestrator lifecycle. +/// +/// This is used for cleanup during app exit/detach events. class GlobalMinerManager { - static MinerProcess? _globalMinerProcess; + static MiningOrchestrator? _orchestrator; - static void setMinerProcess(MinerProcess? process) { - _globalMinerProcess = process; - _log.d('Miner process registered: ${process != null}'); + /// Register the active orchestrator for lifecycle management. + static void setOrchestrator(MiningOrchestrator? orchestrator) { + _orchestrator = orchestrator; + _log.d('Orchestrator registered: ${orchestrator != null}'); } - static MinerProcess? getMinerProcess() { - return _globalMinerProcess; + /// Get the current orchestrator, if any. + static MiningOrchestrator? getOrchestrator() { + return _orchestrator; } + /// Cleanup all mining processes. + /// + /// Called during app exit or detach. static Future cleanup() async { _log.i('Starting global cleanup...'); - if (_globalMinerProcess != null) { + if (_orchestrator != null) { try { - _globalMinerProcess!.forceStop(); - _globalMinerProcess = null; + _orchestrator!.forceStop(); + _orchestrator = null; } catch (e) { - _log.e('Error stopping miner process', error: e); + _log.e('Error stopping orchestrator', error: e); } } diff --git a/miner-app/lib/src/models/miner_error.dart b/miner-app/lib/src/models/miner_error.dart new file mode 100644 index 00000000..ba902485 --- /dev/null +++ b/miner-app/lib/src/models/miner_error.dart @@ -0,0 +1,92 @@ +/// Types of errors that can occur during mining. +enum MinerErrorType { + /// The miner process crashed unexpectedly. + minerCrashed, + + /// The node process crashed unexpectedly. + nodeCrashed, + + /// Failed to start the miner process. + minerStartupFailed, + + /// Failed to start the node process. + nodeStartupFailed, + + /// Lost connection to the miner metrics endpoint. + metricsConnectionLost, + + /// Lost connection to the node RPC endpoint. + rpcConnectionLost, + + /// Generic/unknown error. + unknown, +} + +/// Represents an error that occurred during mining operations. +class MinerError { + /// The type of error. + final MinerErrorType type; + + /// Human-readable error message. + final String message; + + /// Process exit code, if applicable. + final int? exitCode; + + /// The underlying exception, if any. + final Object? exception; + + /// Stack trace, if available. + final StackTrace? stackTrace; + + /// When the error occurred. + final DateTime timestamp; + + MinerError({ + required this.type, + required this.message, + this.exitCode, + this.exception, + this.stackTrace, + DateTime? timestamp, + }) : timestamp = timestamp ?? DateTime.now(); + + /// Create a miner crash error. + factory MinerError.minerCrashed(int exitCode) => MinerError( + type: MinerErrorType.minerCrashed, + message: 'Miner process crashed unexpectedly (exit code: $exitCode)', + exitCode: exitCode, + ); + + /// Create a node crash error. + factory MinerError.nodeCrashed(int exitCode) => MinerError( + type: MinerErrorType.nodeCrashed, + message: 'Node process crashed unexpectedly (exit code: $exitCode)', + exitCode: exitCode, + ); + + /// Create a miner startup failure error. + factory MinerError.minerStartupFailed( + Object error, [ + StackTrace? stackTrace, + ]) => MinerError( + type: MinerErrorType.minerStartupFailed, + message: 'Failed to start miner: $error', + exception: error, + stackTrace: stackTrace, + ); + + /// Create a node startup failure error. + factory MinerError.nodeStartupFailed( + Object error, [ + StackTrace? stackTrace, + ]) => MinerError( + type: MinerErrorType.nodeStartupFailed, + message: 'Failed to start node: $error', + exception: error, + stackTrace: stackTrace, + ); + + @override + String toString() => 'MinerError($type): $message'; +} diff --git a/miner-app/lib/src/services/log_stream_processor.dart b/miner-app/lib/src/services/log_stream_processor.dart new file mode 100644 index 00000000..5baef43f --- /dev/null +++ b/miner-app/lib/src/services/log_stream_processor.dart @@ -0,0 +1,164 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:quantus_miner/src/services/log_filter_service.dart'; +import 'package:quantus_miner/src/shared/extensions/log_string_extension.dart'; +import 'package:quantus_miner/src/utils/app_logger.dart'; + +final _log = log.withTag('LogProcessor'); + +/// Represents a single log entry from a process. +class LogEntry { + /// The log message content. + final String message; + + /// When the log was received. + final DateTime timestamp; + + /// Source identifier (e.g., 'node', 'miner', 'node-error'). + final String source; + + /// Whether this is an error-level log. + final bool isError; + + LogEntry({ + required this.message, + required this.timestamp, + required this.source, + this.isError = false, + }); + + @override + String toString() { + final timeStr = timestamp.toIso8601String().substring(11, 19); // HH:MM:SS + return '[$timeStr] [$source] $message'; + } +} + +/// Callback type for checking if node is currently syncing. +typedef SyncStateProvider = bool Function(); + +/// Processes stdout/stderr streams from a process and emits filtered LogEntries. +/// +/// Handles: +/// - Stream decoding (UTF8) +/// - Line splitting +/// - Log filtering based on keywords and sync state +/// - Error detection +class LogStreamProcessor { + final String sourceName; + final LogFilterService _filter; + final SyncStateProvider? _getSyncState; + + StreamSubscription? _stdoutSubscription; + StreamSubscription? _stderrSubscription; + + final _logController = StreamController.broadcast(); + + /// Stream of processed log entries. + Stream get logs => _logController.stream; + + /// Whether the processor is currently active. + bool get isActive => + _stdoutSubscription != null || _stderrSubscription != null; + + LogStreamProcessor({ + required this.sourceName, + SyncStateProvider? getSyncState, + }) : _filter = LogFilterService(), + _getSyncState = getSyncState; + + /// Start processing logs from a process. + /// + /// Call this after starting the process. + void attach(Process process) { + _filter.reset(); + + _stdoutSubscription = process.stdout + .transform(utf8.decoder) + .transform(const LineSplitter()) + .listen(_processStdoutLine); + + _stderrSubscription = process.stderr + .transform(utf8.decoder) + .transform(const LineSplitter()) + .listen(_processStderrLine); + + _log.d('Attached to process (PID: ${process.pid})'); + } + + /// Stop processing and release resources. + void detach() { + _stdoutSubscription?.cancel(); + _stderrSubscription?.cancel(); + _stdoutSubscription = null; + _stderrSubscription = null; + _log.d('Detached from process'); + } + + /// Close the log stream permanently. + void dispose() { + detach(); + if (!_logController.isClosed) { + _logController.close(); + } + } + + void _processStdoutLine(String line) { + final shouldPrint = _filter.shouldPrintLine( + line, + isNodeSyncing: _getSyncState?.call() ?? false, + ); + + if (shouldPrint) { + final isError = _isErrorLine(line); + final entry = LogEntry( + message: line, + timestamp: DateTime.now(), + source: isError ? '$sourceName-error' : sourceName, + isError: isError, + ); + _logController.add(entry); + + if (isError) { + _log.w('[$sourceName] $line'); + } else { + _log.d('[$sourceName] $line'); + } + } + } + + void _processStderrLine(String line) { + // stderr is always potentially important + final isError = _isErrorLine(line); + final entry = LogEntry( + message: line, + timestamp: DateTime.now(), + source: isError ? '$sourceName-error' : sourceName, + isError: isError, + ); + _logController.add(entry); + + if (isError) { + _log.w('[$sourceName] $line'); + } else { + _log.d('[$sourceName] $line'); + } + } + + bool _isErrorLine(String line) { + // Use the extension method if available for source-specific checks + if (sourceName == 'node') { + return line.isNodeError; + } else if (sourceName == 'miner') { + return line.isMinerError; + } + // Fallback generic error detection + final lower = line.toLowerCase(); + return lower.contains('error') || + lower.contains('panic') || + lower.contains('fatal') || + lower.contains('failed'); + } +} diff --git a/miner-app/lib/src/services/miner_process.dart b/miner-app/lib/src/services/miner_process.dart deleted file mode 100644 index 11747445..00000000 --- a/miner-app/lib/src/services/miner_process.dart +++ /dev/null @@ -1,679 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; - -import 'package:path/path.dart' as p; -import 'package:quantus_miner/src/config/miner_config.dart'; -import 'package:quantus_miner/src/services/process_cleanup_service.dart'; -import 'package:quantus_miner/src/services/prometheus_service.dart'; -import 'package:quantus_miner/src/shared/extensions/log_string_extension.dart'; -import 'package:quantus_miner/src/utils/app_logger.dart'; - -import './binary_manager.dart'; -import './chain_rpc_client.dart'; -import './external_miner_api_client.dart'; -import './log_filter_service.dart'; -import './mining_stats_service.dart'; - -final _log = log.withTag('MinerProcess'); - -class LogEntry { - final String message; - final DateTime timestamp; - final String source; // 'node', 'quantus-miner', 'error' - - LogEntry({ - required this.message, - required this.timestamp, - required this.source, - }); - - @override - String toString() { - final timeStr = timestamp.toIso8601String().substring(11, 19); // HH:MM:SS - return '[$timeStr] [$source] $message'; - } -} - -/// quantus_sdk/lib/src/services/miner_process.dart -class MinerProcess { - final File bin; - final File identityPath; - final File rewardsPath; - late Process _nodeProcess; - Process? _externalMinerProcess; - late LogFilterService _stdoutFilter; - late LogFilterService _stderrFilter; - - late MiningStatsService _statsService; - late PrometheusService _prometheusService; - late ExternalMinerApiClient _externalMinerApiClient; - late PollingChainRpcClient _chainRpcClient; - - Timer? _syncStatusTimer; - final int cpuWorkers; - final int gpuDevices; - - final int minerListenPort; - final int detectedGpuCount; - final String chainId; - - // Track metrics state to prevent premature hashrate reset - double _lastValidHashrate = 0.0; - int _consecutiveMetricsFailures = 0; - - // Public getters for process PIDs (for cleanup tracking) - int? get nodeProcessPid { - try { - return _nodeProcess.pid; - } catch (e) { - return null; - } - } - - int? get externalMinerProcessPid { - try { - return _externalMinerProcess?.pid; - } catch (e) { - return null; - } - } - - // Stream for logs - final _logsController = StreamController.broadcast(); - Stream get logsStream => _logsController.stream; - - final Function(MiningStats stats)? onStatsUpdate; - - MinerProcess( - this.bin, - this.identityPath, - this.rewardsPath, { - this.onStatsUpdate, - this.cpuWorkers = 8, - this.gpuDevices = 0, - this.detectedGpuCount = 0, - this.minerListenPort = 9833, - this.chainId = 'dev', - }) { - // Initialize services - _statsService = MiningStatsService(); - _prometheusService = PrometheusService(); - _stdoutFilter = LogFilterService(); - _stderrFilter = LogFilterService(); - - // Initialize external miner API client with metrics endpoint - _externalMinerApiClient = ExternalMinerApiClient( - metricsUrl: MinerConfig.minerMetricsUrl( - MinerConfig.defaultMinerMetricsPort, - ), - ); - - // Set up external miner API callbacks - _externalMinerApiClient.onMetricsUpdate = _handleExternalMinerMetrics; - _externalMinerApiClient.onError = _handleExternalMinerError; - - // Initialize chain RPC client - _chainRpcClient = PollingChainRpcClient( - rpcUrl: MinerConfig.nodeRpcUrl(MinerConfig.defaultNodeRpcPort), - ); - _chainRpcClient.onChainInfoUpdate = _handleChainInfoUpdate; - _chainRpcClient.onError = _handleChainRpcError; - - // Initialize stats with the configured worker count - _statsService.updateWorkers(cpuWorkers); - // Initialize stats with total CPU capacity from platform - _statsService.updateCpuCapacity(Platform.numberOfProcessors); - // Initialize stats with the configured GPU devices - _statsService.updateGpuDevices(gpuDevices); - // Initialize stats with total GPU capacity from detection - _statsService.updateGpuCapacity(detectedGpuCount); - } - - Future start() async { - // First, ensure both binaries are available - await BinaryManager.ensureNodeBinary(); - await BinaryManager.ensureExternalMinerBinary(); - - // Perform pre-start cleanup using the cleanup service - await ProcessCleanupService.performPreStartCleanup(chainId); - - // Check if ports are available and cleanup if needed - await _ensurePortsAvailable(); - - // === START NODE FIRST (QUIC server) === - final quantusHome = await BinaryManager.getQuantusHomeDirectoryPath(); - final basePath = p.join(quantusHome, 'node_data'); - await Directory(basePath).create(recursive: true); - - final nodeKeyFileFromFileSystem = await BinaryManager.getNodeKeyFile(); - if (await nodeKeyFileFromFileSystem.exists()) { - final stat = await nodeKeyFileFromFileSystem.stat(); - _log.d( - 'Node key file exists (${nodeKeyFileFromFileSystem.path}), size: ${stat.size} bytes', - ); - } else { - _log.d('Node key file does not exist: ${nodeKeyFileFromFileSystem.path}'); - } - - if (!await identityPath.exists()) { - throw Exception('Identity file not found: ${identityPath.path}'); - } - - // Read the rewards address from the file - String rewardsAddress; - try { - if (!await rewardsPath.exists()) { - throw Exception('Rewards address file not found: ${rewardsPath.path}'); - } - rewardsAddress = await rewardsPath.readAsString(); - rewardsAddress = rewardsAddress.trim(); // Remove any whitespace/newlines - _log.d('Read rewards address: $rewardsAddress'); - } catch (e) { - throw Exception( - 'Failed to read rewards address from file ${rewardsPath.path}: $e', - ); - } - - final List args = [ - '--base-path', - basePath, - '--node-key-file', - identityPath.path, - '--rewards-address', - rewardsAddress, - '--validator', - // Use --dev for local development, --chain for testnet/mainnet - if (chainId == 'dev') '--dev' else ...['--chain', chainId], - '--port', - '30333', - '--prometheus-port', - '9616', - '--experimental-rpc-endpoint', - 'listen-addr=127.0.0.1:9933,methods=unsafe,cors=all', - '--name', - 'QuantusMinerGUI', - '--miner-listen-port', - minerListenPort.toString(), - '--enable-peer-sharing', - ]; - - _log.d('Executing: ${bin.path} ${args.join(' ')}'); - - _nodeProcess = await Process.start(bin.path, args); - _stdoutFilter = LogFilterService(); - _stderrFilter = LogFilterService(); - - _stdoutFilter.reset(); - _stderrFilter.reset(); - - // Process each log line - void processLogLine(String line, String streamType) { - bool shouldPrint; - if (streamType == 'stdout') { - shouldPrint = _stdoutFilter.shouldPrintLine( - line, - isNodeSyncing: _statsService.currentStats.isSyncing, - ); - } else { - shouldPrint = _stderrFilter.shouldPrintLine( - line, - isNodeSyncing: _statsService.currentStats.isSyncing, - ); - } - - if (shouldPrint) { - String source; - if (line.isNodeError) { - source = 'node-error'; - } else if (streamType == 'stdout') { - source = 'node'; - } else { - source = 'node'; - } - - final logEntry = LogEntry( - message: line, - timestamp: DateTime.now(), - source: source, - ); - _logsController.add(logEntry); - if (source == 'node-error') { - _log.w('[node] $line'); - } else { - _log.d('[node] $line'); - } - } - } - - _nodeProcess.stdout - .transform(utf8.decoder) - .transform(const LineSplitter()) - .listen((line) { - processLogLine(line, 'stdout'); - }); - - _nodeProcess.stderr - .transform(utf8.decoder) - .transform(const LineSplitter()) - .listen((line) { - processLogLine(line, 'stderr'); - }); - - Future syncBlockTargetWithPrometheusMetrics() async { - try { - final metrics = await _prometheusService.fetchMetrics(); - if (metrics == null || metrics.targetBlock == null) return; - if (_statsService.currentStats.targetBlock >= metrics.targetBlock!) { - return; - } - - _statsService.updateTargetBlock(metrics.targetBlock!); - - onStatsUpdate?.call(_statsService.currentStats); - } catch (e) { - _log.w('Failed to fetch target block height', error: e); - } - } - - // Start Prometheus polling for target block (every 3 seconds) - _syncStatusTimer?.cancel(); - _syncStatusTimer = Timer.periodic( - const Duration(seconds: 3), - (timer) => syncBlockTargetWithPrometheusMetrics(), - ); - - // Wait for node RPC to be ready before starting miner - await _waitForNodeRpcReady(); - - // === START MINER (QUIC client connects to node) === - final externalMinerBinPath = - await BinaryManager.getExternalMinerBinaryFilePath(); - final externalMinerBin = File(externalMinerBinPath); - - if (!await externalMinerBin.exists()) { - throw Exception( - 'External miner binary not found at $externalMinerBinPath', - ); - } - - final minerArgs = [ - '--node-addr', - '127.0.0.1:$minerListenPort', - '--cpu-workers', - cpuWorkers.toString(), - '--gpu-devices', - gpuDevices.toString(), - '--metrics-port', - _getMetricsPort().toString(), - ]; - - try { - _externalMinerProcess = await Process.start( - externalMinerBin.path, - minerArgs, - ); - } catch (e) { - throw Exception('Failed to start external miner: $e'); - } - - // Set up external miner log handling - _externalMinerProcess!.stdout - .transform(utf8.decoder) - .transform(const LineSplitter()) - .listen((line) { - final logEntry = LogEntry( - message: line, - timestamp: DateTime.now(), - source: 'quantus-miner', - ); - _logsController.add(logEntry); - _log.d('[miner] $line'); - }); - - _externalMinerProcess!.stderr - .transform(utf8.decoder) - .transform(const LineSplitter()) - .listen((line) { - final logEntry = LogEntry( - message: line, - timestamp: DateTime.now(), - source: line.isMinerError ? 'quantus-miner-error' : 'quantus-miner', - ); - _logsController.add(logEntry); - if (line.isMinerError) { - _log.w('[miner] $line'); - } else { - _log.d('[miner] $line'); - } - }); - - // Monitor external miner process exit - _externalMinerProcess!.exitCode.then((exitCode) { - if (exitCode != 0) { - _log.w('External miner exited with code: $exitCode'); - } - }); - - // Give the external miner a moment to start up and connect - await Future.delayed(const Duration(seconds: 2)); - - // Check if external miner process is still alive - bool minerStillRunning = true; - try { - final pid = _externalMinerProcess!.pid; - minerStillRunning = await _isProcessRunning(pid); - } catch (e) { - minerStillRunning = false; - } - - if (!minerStillRunning) { - throw Exception('External miner process died during startup'); - } - - // Start external miner API polling (every second) - _externalMinerApiClient.startPolling(); - - // Start RPC polling now that everything is ready - _chainRpcClient.startPolling(); - } - - void stop() { - _log.i('Stopping mining processes...'); - _syncStatusTimer?.cancel(); - _externalMinerApiClient.stopPolling(); - _chainRpcClient.stopPolling(); - - // Kill external miner process first - if (_externalMinerProcess != null) { - try { - _log.d('Killing external miner (PID: ${_externalMinerProcess!.pid})'); - - // Try graceful termination first - _externalMinerProcess!.kill(ProcessSignal.sigterm); - - // Wait briefly for graceful shutdown - Future.delayed(MinerConfig.gracefulShutdownTimeout).then((_) async { - // Check if process is still running and force kill if necessary - try { - if (await _isProcessRunning(_externalMinerProcess!.pid)) { - _log.d('External miner still running, force killing...'); - _externalMinerProcess!.kill(ProcessSignal.sigkill); - } - } catch (e) { - // Process is already dead, which is what we want - _log.d('External miner already terminated'); - } - }); - } catch (e) { - _log.e('Error killing external miner', error: e); - // Try force kill as backup - try { - _externalMinerProcess!.kill(ProcessSignal.sigkill); - } catch (e2) { - _log.e('Error force killing external miner', error: e2); - } - } - } - - // Kill node process - try { - _log.d('Killing node process (PID: ${_nodeProcess.pid})'); - - // Try graceful termination first - _nodeProcess.kill(ProcessSignal.sigterm); - - // Wait briefly for graceful shutdown - Future.delayed(MinerConfig.gracefulShutdownTimeout).then((_) async { - // Check if process is still running and force kill if necessary - try { - if (await _isProcessRunning(_nodeProcess.pid)) { - _log.d('Node still running, force killing...'); - _nodeProcess.kill(ProcessSignal.sigkill); - } - } catch (e) { - // Process is already dead, which is what we want - _log.d('Node already terminated'); - } - }); - } catch (e) { - _log.e('Error killing node process', error: e); - // Try force kill as backup - try { - _nodeProcess.kill(ProcessSignal.sigkill); - } catch (e2) { - _log.e('Error force killing node process', error: e2); - } - } - - // Close the logs stream - if (!_logsController.isClosed) { - _logsController.close(); - } - } - - /// Force stop both processes immediately with SIGKILL - void forceStop() { - _log.i('Force stopping all processes...'); - _syncStatusTimer?.cancel(); - - final List> killFutures = []; - - // Force kill external miner - if (_externalMinerProcess != null) { - final minerPid = _externalMinerProcess!.pid; - killFutures.add(_forceKillProcess(minerPid, 'external miner')); - try { - _externalMinerProcess!.kill(ProcessSignal.sigkill); - } catch (e) { - _log.e('Error force killing external miner', error: e); - } - _externalMinerProcess = null; - } - - // Force kill node process - try { - final nodePid = _nodeProcess.pid; - killFutures.add(_forceKillProcess(nodePid, 'node')); - _nodeProcess.kill(ProcessSignal.sigkill); - } catch (e) { - _log.e('Error force killing node', error: e); - } - - // Wait for all kills to complete (with timeout) - Future.wait(killFutures).timeout( - MinerConfig.forceKillTimeout, - onTimeout: () { - _log.w('Force kill operations timed out'); - return []; - }, - ); - - // Close the logs stream - if (!_logsController.isClosed) { - _logsController.close(); - } - } - - /// Check if a process with the given PID is running. - /// Delegates to ProcessCleanupService. - Future _isProcessRunning(int pid) async { - return ProcessCleanupService.isProcessRunning(pid); - } - - /// Helper method to force kill a process by PID with verification. - /// Delegates to ProcessCleanupService. - Future _forceKillProcess(int pid, String processName) async { - await ProcessCleanupService.forceKillProcess(pid, processName); - } - - /// Handle external miner metrics updates - void _handleExternalMinerMetrics(ExternalMinerMetrics metrics) { - if (metrics.isHealthy && metrics.hashRate > 0) { - // Valid metrics received - _lastValidHashrate = metrics.hashRate; - _consecutiveMetricsFailures = 0; - - _statsService.updateHashrate(metrics.hashRate); - - // Update workers count from external miner if available - if (metrics.workers > 0) { - _statsService.updateWorkers(metrics.workers); - } - - // Update CPU capacity from external miner if available - if (metrics.cpuCapacity > 0) { - _statsService.updateCpuCapacity(metrics.cpuCapacity); - } - - // Update GPU devices count from external miner if available - if (metrics.gpuDevices > 0) { - _statsService.updateGpuDevices(metrics.gpuDevices); - } - - onStatsUpdate?.call(_statsService.currentStats); - } else if (metrics.hashRate == 0.0 && _lastValidHashrate > 0) { - // Received 0.0 but we have a valid hashrate - ignore it and keep the last valid one - _statsService.updateHashrate(_lastValidHashrate); - onStatsUpdate?.call(_statsService.currentStats); - } else { - // Invalid or zero metrics - _consecutiveMetricsFailures++; - - // Only reset to zero after multiple consecutive failures - if (_consecutiveMetricsFailures >= - MinerConfig.maxConsecutiveMetricsFailures) { - _statsService.updateHashrate(0.0); - _lastValidHashrate = 0.0; - onStatsUpdate?.call(_statsService.currentStats); - } else { - // Keep the last valid hashrate during temporary issues - if (_lastValidHashrate > 0) { - _statsService.updateHashrate(_lastValidHashrate); - onStatsUpdate?.call(_statsService.currentStats); - } - } - } - } - - /// Handle external miner API errors - void _handleExternalMinerError(String error) { - _consecutiveMetricsFailures++; - - // Only reset hashrate after multiple consecutive errors - if (_consecutiveMetricsFailures >= - MinerConfig.maxConsecutiveMetricsFailures) { - if (_statsService.currentStats.hashrate != 0.0) { - _statsService.updateHashrate(0.0); - _lastValidHashrate = 0.0; - onStatsUpdate?.call(_statsService.currentStats); - } - } - } - - /// Check if required ports are available and cleanup if needed - Future _ensurePortsAvailable() async { - final ports = await ProcessCleanupService.ensurePortsAvailable( - quicPort: minerListenPort, - metricsPort: MinerConfig.defaultMinerMetricsPort, - ); - - // If metrics port changed, update the API client - final actualMetricsPort = ports['metrics']!; - if (actualMetricsPort != MinerConfig.defaultMinerMetricsPort) { - _externalMinerApiClient = ExternalMinerApiClient( - metricsUrl: MinerConfig.minerMetricsUrl(actualMetricsPort), - ); - _externalMinerApiClient.onMetricsUpdate = _handleExternalMinerMetrics; - _externalMinerApiClient.onError = _handleExternalMinerError; - } - - // Store the metrics port for later use - _actualMetricsPort = actualMetricsPort; - } - - // Track the actual metrics port being used (may differ from default) - int _actualMetricsPort = MinerConfig.defaultMinerMetricsPort; - - /// Get the metrics port to use (determined during _ensurePortsAvailable) - int _getMetricsPort() { - return _actualMetricsPort; - } - - /// Wait for the node RPC to be ready (blocking) - /// Used to ensure node is ready before starting miner - Future _waitForNodeRpcReady() async { - _log.d('Waiting for node RPC to be ready...'); - - // Try to connect to RPC endpoint with exponential backoff - int attempts = 0; - Duration delay = MinerConfig.rpcInitialRetryDelay; - - while (attempts < MinerConfig.maxRpcRetries) { - try { - final isReady = await _chainRpcClient.isReachable(); - if (isReady) { - _log.i('Node RPC is ready'); - return; - } - } catch (e) { - // Expected during startup - } - - attempts++; - _log.d( - 'Node RPC not ready (attempt $attempts/${MinerConfig.maxRpcRetries}), waiting ${delay.inSeconds}s...', - ); - - await Future.delayed(delay); - - // Exponential backoff, but cap at max retry delay - if (delay < MinerConfig.rpcMaxRetryDelay) { - delay = Duration(seconds: (delay.inSeconds * 1.5).round()); - if (delay > MinerConfig.rpcMaxRetryDelay) { - delay = MinerConfig.rpcMaxRetryDelay; - } - } - } - - _log.w( - 'Node RPC not ready after ${MinerConfig.maxRpcRetries} attempts, proceeding anyway...', - ); - } - - void _handleChainInfoUpdate(ChainInfo info) { - _log.d('Chain info: peers=${info.peerCount}, block=${info.currentBlock}'); - - // Update peer count from RPC (most accurate) - if (info.peerCount >= 0) { - _statsService.updatePeerCount(info.peerCount); - } - - // Update chain name from RPC - _statsService.updateChainName(info.chainName); - - // Always update current block and target block from RPC (most authoritative) - _statsService.setSyncingState( - info.isSyncing, - info.currentBlock, - info.targetBlock ?? info.currentBlock, - ); - - onStatsUpdate?.call(_statsService.currentStats); - } - - /// Handle chain RPC errors - void _handleChainRpcError(String error) { - // Only log significant RPC errors, not connection issues during startup - if (!error.contains('Connection refused') && !error.contains('timeout')) { - _log.w('Chain RPC error: $error'); - } - } - - /// Dispose of resources - void dispose() { - _syncStatusTimer?.cancel(); - _externalMinerApiClient.dispose(); - _chainRpcClient.dispose(); - } -} diff --git a/miner-app/lib/src/services/miner_process_manager.dart b/miner-app/lib/src/services/miner_process_manager.dart new file mode 100644 index 00000000..f7a72e5a --- /dev/null +++ b/miner-app/lib/src/services/miner_process_manager.dart @@ -0,0 +1,241 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:quantus_miner/src/config/miner_config.dart'; +import 'package:quantus_miner/src/models/miner_error.dart'; +import 'package:quantus_miner/src/services/log_stream_processor.dart'; +import 'package:quantus_miner/src/services/process_cleanup_service.dart'; +import 'package:quantus_miner/src/utils/app_logger.dart'; + +final _log = log.withTag('ExternalMiner'); + +/// Configuration for starting the external miner process. +class ExternalMinerConfig { + /// Path to the miner binary. + final File binary; + + /// Address and port of the node's QUIC endpoint (e.g., "127.0.0.1:9833"). + final String nodeAddress; + + /// Number of CPU worker threads. + final int cpuWorkers; + + /// Number of GPU devices to use. + final int gpuDevices; + + /// Port for the miner's Prometheus metrics endpoint. + final int metricsPort; + + ExternalMinerConfig({ + required this.binary, + required this.nodeAddress, + this.cpuWorkers = 8, + this.gpuDevices = 0, + this.metricsPort = 9900, + }); +} + +/// Manages the quantus-miner (external miner) process lifecycle. +/// +/// Responsibilities: +/// - Starting the miner process with proper arguments +/// - Monitoring process health and exit +/// - Stopping the process gracefully or forcefully +/// - Emitting log entries and error events +class MinerProcessManager { + Process? _process; + late LogStreamProcessor _logProcessor; + final _errorController = StreamController.broadcast(); + + bool _intentionalStop = false; + + /// Stream of log entries from the miner. + Stream get logs => _logProcessor.logs; + + /// Stream of errors (crashes, startup failures). + Stream get errors => _errorController.stream; + + /// The process ID, or null if not running. + int? get pid => _process?.pid; + + /// Whether the miner process is currently running. + bool get isRunning => _process != null; + + MinerProcessManager() { + _logProcessor = LogStreamProcessor(sourceName: 'miner'); + } + + /// Start the miner process. + /// + /// Throws an exception if startup fails. + Future start(ExternalMinerConfig config) async { + if (_process != null) { + _log.w('Miner already running (PID: ${_process!.pid})'); + return; + } + + _intentionalStop = false; + + // Validate binary exists + if (!await config.binary.exists()) { + final error = MinerError.minerStartupFailed( + 'Miner binary not found: ${config.binary.path}', + ); + _errorController.add(error); + throw Exception(error.message); + } + + // Build command arguments + final args = _buildArgs(config); + + _log.i('Starting miner...'); + _log.d('Command: ${config.binary.path} ${args.join(' ')}'); + + try { + _process = await Process.start(config.binary.path, args); + _logProcessor.attach(_process!); + + // Monitor for unexpected exit + _process!.exitCode.then(_handleExit); + + // Verify it started successfully by waiting briefly + await Future.delayed(const Duration(seconds: 2)); + + if (_process != null) { + final stillRunning = await ProcessCleanupService.isProcessRunning( + _process!.pid, + ); + if (!stillRunning) { + final error = MinerError.minerStartupFailed( + 'Miner died during startup', + ); + _errorController.add(error); + _process = null; + throw Exception(error.message); + } + } + + _log.i('Miner started (PID: ${_process!.pid})'); + } catch (e, st) { + if (e.toString().contains('Miner died during startup')) { + rethrow; + } + final error = MinerError.minerStartupFailed(e, st); + _errorController.add(error); + _process = null; + rethrow; + } + } + + /// Stop the miner process gracefully. + /// + /// Returns a Future that completes when the process has stopped. + Future stop() async { + if (_process == null) { + return; + } + + _intentionalStop = true; + final processPid = _process!.pid; + _log.i('Stopping miner (PID: $processPid)...'); + + // Try graceful termination first + _process!.kill(ProcessSignal.sigterm); + + // Wait for graceful shutdown + final exited = await _waitForExit(MinerConfig.gracefulShutdownTimeout); + + if (!exited) { + // Force kill if still running + _log.d('Miner still running, force killing...'); + await _forceKill(); + } + + _cleanup(); + _log.i('Miner stopped'); + } + + /// Force stop the miner process immediately. + void forceStop() { + if (_process == null) { + return; + } + + _intentionalStop = true; + final processPid = _process!.pid; + _log.i('Force stopping miner (PID: $processPid)...'); + + try { + _process!.kill(ProcessSignal.sigkill); + } catch (e) { + _log.e('Error force killing miner', error: e); + } + + // Also use system cleanup as backup + ProcessCleanupService.forceKillProcess(processPid, 'miner'); + + _cleanup(); + } + + /// Dispose of all resources. + void dispose() { + forceStop(); + _logProcessor.dispose(); + if (!_errorController.isClosed) { + _errorController.close(); + } + } + + List _buildArgs(ExternalMinerConfig config) { + return [ + '--node-addr', + config.nodeAddress, + '--cpu-workers', + config.cpuWorkers.toString(), + '--gpu-devices', + config.gpuDevices.toString(), + '--metrics-port', + config.metricsPort.toString(), + ]; + } + + void _handleExit(int exitCode) { + if (_intentionalStop) { + _log.d('Miner exited (code: $exitCode) - intentional stop'); + } else { + _log.w('Miner crashed (exit code: $exitCode)'); + _errorController.add(MinerError.minerCrashed(exitCode)); + } + _cleanup(); + } + + Future _waitForExit(Duration timeout) async { + if (_process == null) return true; + + try { + await _process!.exitCode.timeout(timeout); + return true; + } on TimeoutException { + return false; + } + } + + Future _forceKill() async { + if (_process == null) return; + + try { + _process!.kill(ProcessSignal.sigkill); + await _process!.exitCode.timeout( + MinerConfig.processVerificationDelay, + onTimeout: () => -1, + ); + } catch (e) { + _log.e('Error during force kill', error: e); + } + } + + void _cleanup() { + _logProcessor.detach(); + _process = null; + } +} diff --git a/miner-app/lib/src/services/mining_orchestrator.dart b/miner-app/lib/src/services/mining_orchestrator.dart new file mode 100644 index 00000000..305153de --- /dev/null +++ b/miner-app/lib/src/services/mining_orchestrator.dart @@ -0,0 +1,534 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:quantus_miner/src/config/miner_config.dart'; +import 'package:quantus_miner/src/models/miner_error.dart'; +import 'package:quantus_miner/src/services/chain_rpc_client.dart'; +import 'package:quantus_miner/src/services/external_miner_api_client.dart'; +import 'package:quantus_miner/src/services/log_stream_processor.dart'; +import 'package:quantus_miner/src/services/miner_process_manager.dart'; +import 'package:quantus_miner/src/services/mining_stats_service.dart'; +import 'package:quantus_miner/src/services/node_process_manager.dart'; +import 'package:quantus_miner/src/services/process_cleanup_service.dart'; +import 'package:quantus_miner/src/services/prometheus_service.dart'; +import 'package:quantus_miner/src/utils/app_logger.dart'; + +final _log = log.withTag('Orchestrator'); + +/// Current state of the mining orchestrator. +enum MiningState { + /// Not started, ready to begin. + idle, + + /// Node is starting up. + startingNode, + + /// Node is running, waiting for RPC to be ready. + waitingForRpc, + + /// Miner is starting up. + startingMiner, + + /// Both node and miner are running, mining is active. + mining, + + /// Currently stopping. + stopping, + + /// An error occurred. + error, +} + +/// Configuration for starting a mining session. +class MiningSessionConfig { + /// Path to the node binary. + final File nodeBinary; + + /// Path to the miner binary. + final File minerBinary; + + /// Path to the node identity key file. + final File identityFile; + + /// Path to the rewards address file. + final File rewardsFile; + + /// Chain ID to connect to. + final String chainId; + + /// Number of CPU worker threads. + final int cpuWorkers; + + /// Number of GPU devices to use. + final int gpuDevices; + + /// Detected GPU count for stats. + final int detectedGpuCount; + + /// Port for QUIC miner connection. + final int minerListenPort; + + MiningSessionConfig({ + required this.nodeBinary, + required this.minerBinary, + required this.identityFile, + required this.rewardsFile, + this.chainId = 'dev', + this.cpuWorkers = 8, + this.gpuDevices = 0, + this.detectedGpuCount = 0, + this.minerListenPort = 9833, + }); +} + +/// Orchestrates the complete mining workflow. +/// +/// Coordinates: +/// - Node process lifecycle +/// - Miner process lifecycle +/// - Stats collection from multiple sources +/// - Error handling and crash detection +/// +/// This is the main entry point for mining operations and replaces +/// the old monolithic MinerProcess class. +class MiningOrchestrator { + // Process managers + final NodeProcessManager _nodeManager = NodeProcessManager(); + final MinerProcessManager _minerManager = MinerProcessManager(); + + // API clients for stats + late ExternalMinerApiClient _minerApiClient; + late PollingChainRpcClient _chainRpcClient; + late PrometheusService _prometheusService; + + // Stats + final MiningStatsService _statsService = MiningStatsService(); + + // State + MiningState _state = MiningState.idle; + Timer? _prometheusTimer; + int _actualMetricsPort = MinerConfig.defaultMinerMetricsPort; + + // Hashrate tracking for resilience + double _lastValidHashrate = 0.0; + int _consecutiveMetricsFailures = 0; + + // Stream controllers + final _logsController = StreamController.broadcast(); + final _statsController = StreamController.broadcast(); + final _errorController = StreamController.broadcast(); + final _stateController = StreamController.broadcast(); + + // Subscriptions + StreamSubscription? _nodeLogsSubscription; + StreamSubscription? _minerLogsSubscription; + StreamSubscription? _nodeErrorSubscription; + StreamSubscription? _minerErrorSubscription; + + // ============================================================ + // Public API + // ============================================================ + + /// Current mining state. + MiningState get state => _state; + + /// Stream of log entries from both node and miner. + Stream get logsStream => _logsController.stream; + + /// Stream of mining statistics updates. + Stream get statsStream => _statsController.stream; + + /// Stream of errors (crashes, startup failures, etc.). + Stream get errorStream => _errorController.stream; + + /// Stream of state changes. + Stream get stateStream => _stateController.stream; + + /// Current mining statistics. + MiningStats get currentStats => _statsService.currentStats; + + /// Whether mining is currently active. + bool get isMining => _state == MiningState.mining; + + /// Whether the orchestrator is in any running state. + bool get isRunning => + _state != MiningState.idle && _state != MiningState.error; + + /// Node process PID, if running. + int? get nodeProcessPid => _nodeManager.pid; + + /// Miner process PID, if running. + int? get minerProcessPid => _minerManager.pid; + + MiningOrchestrator() { + _initializeApiClients(); + _setupNodeSyncCallback(); + _subscribeToProcessEvents(); + } + + /// Start mining with the given configuration. + /// + /// This will: + /// 1. Cleanup any existing processes + /// 2. Ensure ports are available + /// 3. Start the node and wait for RPC + /// 4. Start the miner + /// 5. Begin polling for stats + Future start(MiningSessionConfig config) async { + if (_state != MiningState.idle && _state != MiningState.error) { + _log.w('Cannot start: already in state $_state'); + return; + } + + try { + // Initialize stats with worker counts + _statsService.updateWorkers(config.cpuWorkers); + _statsService.updateCpuCapacity(Platform.numberOfProcessors); + _statsService.updateGpuDevices(config.gpuDevices); + _statsService.updateGpuCapacity(config.detectedGpuCount); + _emitStats(); + + // Perform pre-start cleanup + _setState(MiningState.startingNode); + await ProcessCleanupService.performPreStartCleanup(config.chainId); + + // Ensure ports are available + final ports = await ProcessCleanupService.ensurePortsAvailable( + quicPort: config.minerListenPort, + metricsPort: MinerConfig.defaultMinerMetricsPort, + ); + _actualMetricsPort = ports['metrics']!; + _updateMetricsClient(); + + // Read rewards address + final rewardsAddress = await _readRewardsAddress(config.rewardsFile); + + // Start node + await _nodeManager.start( + NodeConfig( + binary: config.nodeBinary, + identityFile: config.identityFile, + rewardsAddress: rewardsAddress, + chainId: config.chainId, + minerListenPort: config.minerListenPort, + ), + ); + + // Wait for node RPC to be ready + _setState(MiningState.waitingForRpc); + await _waitForNodeRpc(); + + // Start miner + _setState(MiningState.startingMiner); + await _minerManager.start( + ExternalMinerConfig( + binary: config.minerBinary, + nodeAddress: '${MinerConfig.localhost}:${config.minerListenPort}', + cpuWorkers: config.cpuWorkers, + gpuDevices: config.gpuDevices, + metricsPort: _actualMetricsPort, + ), + ); + + // Start polling + _startPolling(); + + _setState(MiningState.mining); + _log.i('Mining started successfully'); + } catch (e, st) { + _log.e('Failed to start mining', error: e, stackTrace: st); + _setState(MiningState.error); + await _stopInternal(); + rethrow; + } + } + + /// Stop mining gracefully. + Future stop() async { + if (_state == MiningState.idle) { + return; + } + + _log.i('Stopping mining...'); + _setState(MiningState.stopping); + await _stopInternal(); + _setState(MiningState.idle); + _resetStats(); + _log.i('Mining stopped'); + } + + /// Force stop mining immediately. + void forceStop() { + _log.i('Force stopping mining...'); + _setState(MiningState.stopping); + + _stopPolling(); + _minerManager.forceStop(); + _nodeManager.forceStop(); + + _setState(MiningState.idle); + _resetStats(); + _log.i('Mining force stopped'); + } + + /// Dispose of all resources. + void dispose() { + forceStop(); + + _nodeLogsSubscription?.cancel(); + _minerLogsSubscription?.cancel(); + _nodeErrorSubscription?.cancel(); + _minerErrorSubscription?.cancel(); + + _nodeManager.dispose(); + _minerManager.dispose(); + _minerApiClient.dispose(); + _chainRpcClient.dispose(); + + _logsController.close(); + _statsController.close(); + _errorController.close(); + _stateController.close(); + } + + // ============================================================ + // Internal Implementation + // ============================================================ + + void _initializeApiClients() { + _minerApiClient = ExternalMinerApiClient( + metricsUrl: MinerConfig.minerMetricsUrl( + MinerConfig.defaultMinerMetricsPort, + ), + ); + _minerApiClient.onMetricsUpdate = _handleMinerMetrics; + _minerApiClient.onError = _handleMinerMetricsError; + + _chainRpcClient = PollingChainRpcClient(); + _chainRpcClient.onChainInfoUpdate = _handleChainInfo; + _chainRpcClient.onError = _handleChainRpcError; + + _prometheusService = PrometheusService(); + } + + void _setupNodeSyncCallback() { + _nodeManager.getSyncState = () => _statsService.currentStats.isSyncing; + } + + void _subscribeToProcessEvents() { + // Forward node logs + _nodeLogsSubscription = _nodeManager.logs.listen((entry) { + _logsController.add(entry); + }); + + // Forward miner logs + _minerLogsSubscription = _minerManager.logs.listen((entry) { + _logsController.add(entry); + }); + + // Forward node errors + _nodeErrorSubscription = _nodeManager.errors.listen((error) { + _errorController.add(error); + if (error.type == MinerErrorType.nodeCrashed && + _state == MiningState.mining) { + _log.w('Node crashed while mining, stopping...'); + _handleCrash(); + } + }); + + // Forward miner errors + _minerErrorSubscription = _minerManager.errors.listen((error) { + _errorController.add(error); + if (error.type == MinerErrorType.minerCrashed && + _state == MiningState.mining) { + _log.w('Miner crashed while mining'); + // Don't stop everything - just emit the error for UI to show + } + }); + } + + void _updateMetricsClient() { + if (_actualMetricsPort != MinerConfig.defaultMinerMetricsPort) { + _minerApiClient = ExternalMinerApiClient( + metricsUrl: MinerConfig.minerMetricsUrl(_actualMetricsPort), + ); + _minerApiClient.onMetricsUpdate = _handleMinerMetrics; + _minerApiClient.onError = _handleMinerMetricsError; + } + } + + Future _readRewardsAddress(File rewardsFile) async { + if (!await rewardsFile.exists()) { + throw Exception('Rewards address file not found: ${rewardsFile.path}'); + } + final address = await rewardsFile.readAsString(); + return address.trim(); + } + + Future _waitForNodeRpc() async { + _log.d('Waiting for node RPC...'); + int attempts = 0; + Duration delay = MinerConfig.rpcInitialRetryDelay; + + while (attempts < MinerConfig.maxRpcRetries) { + try { + final isReady = await _chainRpcClient.isReachable(); + if (isReady) { + _log.i('Node RPC is ready'); + return; + } + } catch (e) { + // Expected during startup + } + + attempts++; + _log.d('RPC not ready (attempt $attempts/${MinerConfig.maxRpcRetries})'); + await Future.delayed(delay); + + if (delay < MinerConfig.rpcMaxRetryDelay) { + delay = Duration(seconds: (delay.inSeconds * 1.5).round()); + if (delay > MinerConfig.rpcMaxRetryDelay) { + delay = MinerConfig.rpcMaxRetryDelay; + } + } + } + + _log.w('Node RPC not ready after max attempts, proceeding anyway'); + } + + void _startPolling() { + _minerApiClient.startPolling(); + _chainRpcClient.startPolling(); + + // Prometheus polling for target block + _prometheusTimer?.cancel(); + _prometheusTimer = Timer.periodic( + MinerConfig.prometheusPollingInterval, + (_) => _fetchPrometheusMetrics(), + ); + } + + void _stopPolling() { + _minerApiClient.stopPolling(); + _chainRpcClient.stopPolling(); + _prometheusTimer?.cancel(); + _prometheusTimer = null; + } + + Future _stopInternal() async { + _stopPolling(); + + // Stop miner first (depends on node) + await _minerManager.stop(); + + // Then stop node + await _nodeManager.stop(); + } + + void _handleCrash() { + _setState(MiningState.error); + _stopPolling(); + } + + void _setState(MiningState newState) { + if (_state != newState) { + _state = newState; + _stateController.add(newState); + _log.d('State changed to: $newState'); + } + } + + void _emitStats() { + _statsController.add(_statsService.currentStats); + } + + void _resetStats() { + _statsService.updateHashrate(0); + _lastValidHashrate = 0; + _consecutiveMetricsFailures = 0; + _emitStats(); + } + + // ============================================================ + // Metrics Handlers + // ============================================================ + + void _handleMinerMetrics(ExternalMinerMetrics metrics) { + if (metrics.isHealthy && metrics.hashRate > 0) { + _lastValidHashrate = metrics.hashRate; + _consecutiveMetricsFailures = 0; + + _statsService.updateHashrate(metrics.hashRate); + if (metrics.workers > 0) { + _statsService.updateWorkers(metrics.workers); + } + if (metrics.cpuCapacity > 0) { + _statsService.updateCpuCapacity(metrics.cpuCapacity); + } + if (metrics.gpuDevices > 0) { + _statsService.updateGpuDevices(metrics.gpuDevices); + } + _emitStats(); + } else if (metrics.hashRate == 0.0 && _lastValidHashrate > 0) { + // Keep last valid hashrate during temporary zeroes + _statsService.updateHashrate(_lastValidHashrate); + _emitStats(); + } else { + _consecutiveMetricsFailures++; + if (_consecutiveMetricsFailures >= + MinerConfig.maxConsecutiveMetricsFailures) { + _statsService.updateHashrate(0); + _lastValidHashrate = 0; + _emitStats(); + } else if (_lastValidHashrate > 0) { + _statsService.updateHashrate(_lastValidHashrate); + _emitStats(); + } + } + } + + void _handleMinerMetricsError(String error) { + _consecutiveMetricsFailures++; + if (_consecutiveMetricsFailures >= + MinerConfig.maxConsecutiveMetricsFailures) { + if (_statsService.currentStats.hashrate != 0) { + _statsService.updateHashrate(0); + _lastValidHashrate = 0; + _emitStats(); + } + } + } + + void _handleChainInfo(ChainInfo info) { + if (info.peerCount >= 0) { + _statsService.updatePeerCount(info.peerCount); + } + _statsService.updateChainName(info.chainName); + _statsService.setSyncingState( + info.isSyncing, + info.currentBlock, + info.targetBlock ?? info.currentBlock, + ); + _emitStats(); + } + + void _handleChainRpcError(String error) { + if (!error.contains('Connection refused') && !error.contains('timeout')) { + _log.w('Chain RPC error: $error'); + } + } + + Future _fetchPrometheusMetrics() async { + try { + final metrics = await _prometheusService.fetchMetrics(); + if (metrics?.targetBlock != null) { + if (_statsService.currentStats.targetBlock < metrics!.targetBlock!) { + _statsService.updateTargetBlock(metrics.targetBlock!); + _emitStats(); + } + } + } catch (e) { + _log.w('Failed to fetch Prometheus metrics', error: e); + } + } +} diff --git a/miner-app/lib/src/services/node_process_manager.dart b/miner-app/lib/src/services/node_process_manager.dart new file mode 100644 index 00000000..8f829264 --- /dev/null +++ b/miner-app/lib/src/services/node_process_manager.dart @@ -0,0 +1,260 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:path/path.dart' as p; +import 'package:quantus_miner/src/config/miner_config.dart'; +import 'package:quantus_miner/src/models/miner_error.dart'; +import 'package:quantus_miner/src/services/binary_manager.dart'; +import 'package:quantus_miner/src/services/log_stream_processor.dart'; +import 'package:quantus_miner/src/services/process_cleanup_service.dart'; +import 'package:quantus_miner/src/utils/app_logger.dart'; + +final _log = log.withTag('NodeProcess'); + +/// Configuration for starting the node process. +class NodeConfig { + /// Path to the node binary. + final File binary; + + /// Path to the node identity key file. + final File identityFile; + + /// The rewards address for mining. + final String rewardsAddress; + + /// Chain ID to connect to ('dev' or 'dirac'). + final String chainId; + + /// Port for the QUIC miner connection. + final int minerListenPort; + + /// Port for JSON-RPC endpoint. + final int rpcPort; + + /// Port for Prometheus metrics. + final int prometheusPort; + + /// Port for P2P networking. + final int p2pPort; + + NodeConfig({ + required this.binary, + required this.identityFile, + required this.rewardsAddress, + this.chainId = 'dev', + this.minerListenPort = 9833, + this.rpcPort = 9933, + this.prometheusPort = 9616, + this.p2pPort = 30333, + }); +} + +/// Manages the quantus-node process lifecycle. +/// +/// Responsibilities: +/// - Starting the node process with proper arguments +/// - Monitoring process health and exit +/// - Stopping the process gracefully or forcefully +/// - Emitting log entries and error events +class NodeProcessManager { + Process? _process; + late LogStreamProcessor _logProcessor; + final _errorController = StreamController.broadcast(); + + bool _intentionalStop = false; + + /// Stream of log entries from the node. + Stream get logs => _logProcessor.logs; + + /// Stream of errors (crashes, startup failures). + Stream get errors => _errorController.stream; + + /// The process ID, or null if not running. + int? get pid => _process?.pid; + + /// Whether the node process is currently running. + bool get isRunning => _process != null; + + /// Callback to get current sync state for log filtering. + SyncStateProvider? getSyncState; + + NodeProcessManager() { + _logProcessor = LogStreamProcessor( + sourceName: 'node', + getSyncState: () => getSyncState?.call() ?? false, + ); + } + + /// Start the node process. + /// + /// Throws an exception if startup fails. + Future start(NodeConfig config) async { + if (_process != null) { + _log.w('Node already running (PID: ${_process!.pid})'); + return; + } + + _intentionalStop = false; + + // Validate binary exists + if (!await config.binary.exists()) { + final error = MinerError.nodeStartupFailed( + 'Node binary not found: ${config.binary.path}', + ); + _errorController.add(error); + throw Exception(error.message); + } + + // Validate identity file exists + if (!await config.identityFile.exists()) { + final error = MinerError.nodeStartupFailed( + 'Identity file not found: ${config.identityFile.path}', + ); + _errorController.add(error); + throw Exception(error.message); + } + + // Prepare data directory + final quantusHome = await BinaryManager.getQuantusHomeDirectoryPath(); + final basePath = p.join(quantusHome, 'node_data'); + await Directory(basePath).create(recursive: true); + + // Build command arguments + final args = _buildArgs(config, basePath); + + _log.i('Starting node...'); + _log.d('Command: ${config.binary.path} ${args.join(' ')}'); + + try { + _process = await Process.start(config.binary.path, args); + _logProcessor.attach(_process!); + + // Monitor for unexpected exit + _process!.exitCode.then(_handleExit); + + _log.i('Node started (PID: ${_process!.pid})'); + } catch (e, st) { + final error = MinerError.nodeStartupFailed(e, st); + _errorController.add(error); + _process = null; + rethrow; + } + } + + /// Stop the node process gracefully. + /// + /// Returns a Future that completes when the process has stopped. + Future stop() async { + if (_process == null) { + return; + } + + _intentionalStop = true; + final processPid = _process!.pid; + _log.i('Stopping node (PID: $processPid)...'); + + // Try graceful termination first + _process!.kill(ProcessSignal.sigterm); + + // Wait for graceful shutdown + final exited = await _waitForExit(MinerConfig.gracefulShutdownTimeout); + + if (!exited) { + // Force kill if still running + _log.d('Node still running, force killing...'); + await _forceKill(); + } + + _cleanup(); + _log.i('Node stopped'); + } + + /// Force stop the node process immediately. + void forceStop() { + if (_process == null) { + return; + } + + _intentionalStop = true; + final processPid = _process!.pid; + _log.i('Force stopping node (PID: $processPid)...'); + + try { + _process!.kill(ProcessSignal.sigkill); + } catch (e) { + _log.e('Error force killing node', error: e); + } + + // Also use system cleanup as backup + ProcessCleanupService.forceKillProcess(processPid, 'node'); + + _cleanup(); + } + + /// Dispose of all resources. + void dispose() { + forceStop(); + _logProcessor.dispose(); + if (!_errorController.isClosed) { + _errorController.close(); + } + } + + List _buildArgs(NodeConfig config, String basePath) { + return [ + '--base-path', basePath, + '--node-key-file', config.identityFile.path, + '--rewards-address', config.rewardsAddress, + '--validator', + // Chain selection + if (config.chainId == 'dev') '--dev' else ...['--chain', config.chainId], + '--port', config.p2pPort.toString(), + '--prometheus-port', config.prometheusPort.toString(), + '--experimental-rpc-endpoint', + 'listen-addr=${MinerConfig.localhost}:${config.rpcPort},methods=unsafe,cors=all', + '--name', 'QuantusMinerGUI', + '--miner-listen-port', config.minerListenPort.toString(), + '--enable-peer-sharing', + ]; + } + + void _handleExit(int exitCode) { + if (_intentionalStop) { + _log.d('Node exited (code: $exitCode) - intentional stop'); + } else { + _log.w('Node crashed (exit code: $exitCode)'); + _errorController.add(MinerError.nodeCrashed(exitCode)); + } + _cleanup(); + } + + Future _waitForExit(Duration timeout) async { + if (_process == null) return true; + + try { + await _process!.exitCode.timeout(timeout); + return true; + } on TimeoutException { + return false; + } + } + + Future _forceKill() async { + if (_process == null) return; + + try { + _process!.kill(ProcessSignal.sigkill); + await _process!.exitCode.timeout( + MinerConfig.processVerificationDelay, + onTimeout: () => -1, + ); + } catch (e) { + _log.e('Error during force kill', error: e); + } + } + + void _cleanup() { + _logProcessor.detach(); + _process = null; + } +} diff --git a/miner-app/lib/src/ui/logs_widget.dart b/miner-app/lib/src/ui/logs_widget.dart index 6a55efc1..1eba86a6 100644 --- a/miner-app/lib/src/ui/logs_widget.dart +++ b/miner-app/lib/src/ui/logs_widget.dart @@ -3,13 +3,14 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; -import '../services/miner_process.dart'; +import '../services/log_stream_processor.dart'; +import '../services/mining_orchestrator.dart'; class LogsWidget extends StatefulWidget { - final MinerProcess? minerProcess; + final MiningOrchestrator? orchestrator; final int maxLines; - const LogsWidget({super.key, this.minerProcess, this.maxLines = 20000}); + const LogsWidget({super.key, this.orchestrator, this.maxLines = 20000}); @override State createState() => _LogsWidgetState(); @@ -30,7 +31,7 @@ class _LogsWidgetState extends State { @override void didUpdateWidget(LogsWidget oldWidget) { super.didUpdateWidget(oldWidget); - if (oldWidget.minerProcess != widget.minerProcess) { + if (oldWidget.orchestrator != widget.orchestrator) { _setupLogsListener(); } } @@ -39,8 +40,8 @@ class _LogsWidgetState extends State { _logsSubscription?.cancel(); _logs.clear(); - if (widget.minerProcess != null) { - _logsSubscription = widget.minerProcess!.logsStream.listen((logEntry) { + if (widget.orchestrator != null) { + _logsSubscription = widget.orchestrator!.logsStream.listen((logEntry) { if (mounted) { setState(() { _logs.add(logEntry); @@ -89,6 +90,11 @@ class _LogsWidgetState extends State { return Colors.blue; case 'node-error': return Colors.red; + case 'miner': + return Colors.green; + case 'miner-error': + return Colors.orange; + // Legacy source names for compatibility case 'quantus-miner': return Colors.green; case 'quantus-miner-error': @@ -242,7 +248,7 @@ class _LogsWidgetState extends State { 'Total logs: ${_logs.length}', style: TextStyle(fontSize: 12, color: Colors.grey[600]), ), - if (widget.minerProcess != null) + if (widget.orchestrator?.isMining ?? false) Text( 'Live', style: TextStyle( From 3ccc267a75c9b171093d6a5ecea1fa1e5d15328b Mon Sep 17 00:00:00 2001 From: illuzen Date: Mon, 2 Feb 2026 10:12:18 +0800 Subject: [PATCH 07/10] separate buttons for node and miner --- .../features/miner/miner_balance_card.dart | 101 ++++++-- .../lib/features/miner/miner_controls.dart | 241 ++++++++++++++---- .../features/settings/settings_screen.dart | 207 ++++++++++++++- miner-app/lib/src/config/miner_config.dart | 11 +- .../src/services/miner_process_manager.dart | 1 + .../lib/src/services/mining_orchestrator.dart | 144 +++++++++-- miner-app/pubspec.yaml | 1 + 7 files changed, 592 insertions(+), 114 deletions(-) diff --git a/miner-app/lib/features/miner/miner_balance_card.dart b/miner-app/lib/features/miner/miner_balance_card.dart index 580ebfce..f2a7ef51 100644 --- a/miner-app/lib/features/miner/miner_balance_card.dart +++ b/miner-app/lib/features/miner/miner_balance_card.dart @@ -2,10 +2,14 @@ import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:polkadart/polkadart.dart'; +import 'package:quantus_miner/src/config/miner_config.dart'; import 'package:quantus_miner/src/services/binary_manager.dart'; +import 'package:quantus_miner/src/services/miner_settings_service.dart'; import 'package:quantus_miner/src/shared/extensions/snackbar_extensions.dart'; import 'package:quantus_miner/src/shared/miner_app_constants.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; +import 'package:quantus_sdk/generated/schrodinger/schrodinger.dart'; class MinerBalanceCard extends StatefulWidget { const MinerBalanceCard({super.key}); @@ -17,16 +21,18 @@ class MinerBalanceCard extends StatefulWidget { class _MinerBalanceCardState extends State { String _walletBalance = 'Loading...'; String? _walletAddress; + String _chainId = MinerConfig.defaultChainId; Timer? _balanceTimer; + final _settingsService = MinerSettingsService(); @override void initState() { super.initState(); - _fetchWalletBalance(); + _loadChainAndFetchBalance(); // Start automatic polling every 30 seconds _balanceTimer = Timer.periodic(const Duration(seconds: 30), (_) { - _fetchWalletBalance(); + _loadChainAndFetchBalance(); }); } @@ -36,9 +42,16 @@ class _MinerBalanceCardState extends State { super.dispose(); } + Future _loadChainAndFetchBalance() async { + final chainId = await _settingsService.getChainId(); + if (mounted) { + setState(() => _chainId = chainId); + } + await _fetchWalletBalance(); + } + Future _fetchWalletBalance() async { - // Implement actual wallet balance fetching using quantus_sdk - print('fetching wallet balance'); + print('fetching wallet balance for chain: $_chainId'); try { final quantusHome = await BinaryManager.getQuantusHomeDirectoryPath(); final rewardsFile = File('$quantusHome/rewards-address.txt'); @@ -49,43 +62,81 @@ class _MinerBalanceCardState extends State { if (address.isNotEmpty) { print('address: $address'); - // Fetch balance using SubstrateService (exported by quantus_sdk) - final balance = await SubstrateService().queryBalance(address); + final chainConfig = MinerConfig.getChainById(_chainId); + BigInt balance; + + if (chainConfig.isLocalNode) { + // Use local node RPC for dev chain + balance = await _queryBalanceFromLocalNode( + address, + chainConfig.rpcUrl, + ); + } else { + // Use SDK's SubstrateService for remote chains (dirac) + balance = await SubstrateService().queryBalance(address); + } print('balance: $balance'); - setState(() { - // Assuming NumberFormattingService and AppConstants are available via quantus_sdk export - _walletBalance = NumberFormattingService().formatBalance( - balance, - addSymbol: true, - ); - _walletAddress = address; - }); + if (mounted) { + setState(() { + _walletBalance = NumberFormattingService().formatBalance( + balance, + addSymbol: true, + ); + _walletAddress = address; + }); + } } else { - // Address file exists but is empty _handleAddressNotSet(); } } else { - // Address file does not exist _handleAddressNotSet(); } } catch (e) { - setState(() { - _walletBalance = 'Error fetching balance'; - }); + if (mounted) { + setState(() { + // Show helpful message for dev chain when node not running + if (_chainId == 'dev') { + _walletBalance = 'Start node to view'; + } else { + _walletBalance = 'Error'; + } + }); + } print('Error fetching wallet balance: $e'); } } + /// Query balance directly from local node using Polkadart + Future _queryBalanceFromLocalNode( + String address, + String rpcUrl, + ) async { + try { + final provider = Provider.fromUri(Uri.parse(rpcUrl)); + final quantusApi = Schrodinger(provider); + + // Convert SS58 address to account ID using the SDK's crypto + final accountId = ss58ToAccountId(s: address); + + final accountInfo = await quantusApi.query.system.account(accountId); + return accountInfo.data.free; + } catch (e) { + print('Error querying local node balance: $e'); + // Return zero if node is not running or address has no balance + return BigInt.zero; + } + } + void _handleAddressNotSet() { - setState(() { - _walletBalance = 'Address not set'; - _walletAddress = null; - }); + if (mounted) { + setState(() { + _walletBalance = 'Address not set'; + _walletAddress = null; + }); + } print('Rewards address file not found or empty.'); - // Example Navigation (requires go_router setup) - // context.go('/rewards_address_setup'); } @override diff --git a/miner-app/lib/features/miner/miner_controls.dart b/miner-app/lib/features/miner/miner_controls.dart index 335bfdeb..f1d9b0f7 100644 --- a/miner-app/lib/features/miner/miner_controls.dart +++ b/miner-app/lib/features/miner/miner_controls.dart @@ -28,7 +28,8 @@ class MinerControls extends StatefulWidget { } class _MinerControlsState extends State { - bool _isAttemptingToggle = false; + bool _isNodeToggling = false; + bool _isMinerToggling = false; int _cpuWorkers = 8; int _gpuDevices = 0; int _detectedGpuCount = 0; @@ -67,27 +68,35 @@ class _MinerControlsState extends State { } } - Future _toggle() async { - if (_isAttemptingToggle) return; - setState(() => _isAttemptingToggle = true); + // ============================================================ + // Node Control + // ============================================================ - if (widget.orchestrator == null || !widget.orchestrator!.isMining) { - // Start mining - await _startMining(); + Future _toggleNode() async { + if (_isNodeToggling) return; + setState(() => _isNodeToggling = true); + + if (!_isNodeRunning) { + await _startNode(); } else { - // Stop mining - await _stopMining(); + await _stopNode(); } if (mounted) { - setState(() => _isAttemptingToggle = false); + setState(() => _isNodeToggling = false); } } - Future _startMining() async { - print('Starting mining'); + Future _startNode() async { + print('Starting node'); + + // Reload chain ID in case it was changed in settings + final chainId = await _settingsService.getChainId(); + if (mounted) { + setState(() => _chainId = chainId); + } - // Check for all required files and binaries + // Check for required files final quantusHome = await BinaryManager.getQuantusHomeDirectoryPath(); final identityFile = File('$quantusHome/node_key.p2p'); final rewardsFile = File('$quantusHome/rewards-address.txt'); @@ -96,9 +105,8 @@ class _MinerControlsState extends State { final minerBinPath = await BinaryManager.getExternalMinerBinaryFilePath(); final minerBin = File(minerBinPath); - // Check node binary if (!await nodeBin.exists()) { - print('Node binary not found. Cannot start mining.'); + print('Node binary not found.'); if (mounted) { context.showWarningSnackbar( title: 'Node binary not found!', @@ -108,24 +116,12 @@ class _MinerControlsState extends State { return; } - // Check external miner binary - if (!await minerBin.exists()) { - print('External miner binary not found. Cannot start mining.'); - if (mounted) { - context.showWarningSnackbar( - title: 'External miner binary not found!', - message: 'Please run setup.', - ); - } - return; - } - // Create new orchestrator final orchestrator = MiningOrchestrator(); widget.onOrchestratorChanged(orchestrator); try { - await orchestrator.start( + await orchestrator.startNode( MiningSessionConfig( nodeBinary: nodeBin, minerBinary: minerBin, @@ -138,30 +134,27 @@ class _MinerControlsState extends State { ), ); } catch (e) { - print('Error starting miner: $e'); + print('Error starting node: $e'); if (mounted) { context.showErrorSnackbar( - title: 'Error starting miner!', + title: 'Error starting node!', message: e.toString(), ); } - - // Clean up on failure orchestrator.dispose(); widget.onOrchestratorChanged(null); } } - Future _stopMining() async { - print('Stopping mining'); + Future _stopNode() async { + print('Stopping node'); if (widget.orchestrator != null) { try { - await widget.orchestrator!.stop(); + await widget.orchestrator!.stopNode(); } catch (e) { - print('Error during stop: $e'); + print('Error stopping node: $e'); } - widget.orchestrator!.dispose(); } @@ -169,15 +162,115 @@ class _MinerControlsState extends State { widget.onOrchestratorChanged(null); } - @override - void dispose() { - super.dispose(); + // ============================================================ + // Miner Control + // ============================================================ + + Future _toggleMiner() async { + if (_isMinerToggling) return; + setState(() => _isMinerToggling = true); + + if (!_isMining) { + await _startMiner(); + } else { + await _stopMiner(); + } + + if (mounted) { + setState(() => _isMinerToggling = false); + } + } + + Future _startMiner() async { + print('Starting miner'); + + if (widget.orchestrator == null) { + if (mounted) { + context.showWarningSnackbar( + title: 'Node not running!', + message: 'Start the node first.', + ); + } + return; + } + + // Check miner binary exists + final minerBinPath = await BinaryManager.getExternalMinerBinaryFilePath(); + final minerBin = File(minerBinPath); + + if (!await minerBin.exists()) { + print('Miner binary not found.'); + if (mounted) { + context.showWarningSnackbar( + title: 'Miner binary not found!', + message: 'Please run setup.', + ); + } + return; + } + + try { + await widget.orchestrator!.startMiner(); + } catch (e) { + print('Error starting miner: $e'); + if (mounted) { + context.showErrorSnackbar( + title: 'Error starting miner!', + message: e.toString(), + ); + } + } + } + + Future _stopMiner() async { + print('Stopping miner'); + + if (widget.orchestrator != null) { + try { + await widget.orchestrator!.stopMiner(); + } catch (e) { + print('Error stopping miner: $e'); + } + } } + // ============================================================ + // State Helpers + // ============================================================ + + bool get _isNodeRunning => widget.orchestrator?.isNodeRunning ?? false; bool get _isMining => widget.orchestrator?.isMining ?? false; + String get _nodeButtonText { + final state = widget.orchestrator?.state; + if (state == MiningState.startingNode) return 'Starting...'; + if (state == MiningState.waitingForRpc) return 'Connecting...'; + if (_isNodeRunning) return 'Stop Node'; + return 'Start Node'; + } + + String get _minerButtonText { + final state = widget.orchestrator?.state; + if (state == MiningState.startingMiner) return 'Starting...'; + if (state == MiningState.stoppingMiner) return 'Stopping...'; + if (_isMining) return 'Stop Mining'; + return 'Start Mining'; + } + + Color get _nodeButtonColor { + if (_isNodeRunning) return Colors.orange; + return Colors.blue; + } + + Color get _minerButtonColor { + if (_isMining) return Colors.red; + return Colors.green; + } + @override Widget build(BuildContext context) { + final canEditSettings = !_isNodeRunning; + return Column( mainAxisSize: MainAxisSize.min, children: [ @@ -209,7 +302,7 @@ class _MinerControlsState extends State { ? Platform.numberOfProcessors : 16), label: _cpuWorkers.toString(), - onChanged: !_isMining + onChanged: canEditSettings ? (value) { final rounded = value.round(); setState(() => _cpuWorkers = rounded); @@ -221,6 +314,7 @@ class _MinerControlsState extends State { ), ), const SizedBox(height: 16), + // GPU Devices Control Padding( padding: const EdgeInsets.symmetric(horizontal: 24.0), @@ -243,7 +337,7 @@ class _MinerControlsState extends State { max: _detectedGpuCount > 0 ? _detectedGpuCount.toDouble() : 1, divisions: _detectedGpuCount > 0 ? _detectedGpuCount : 1, label: _gpuDevices.toString(), - onChanged: !_isMining + onChanged: canEditSettings ? (value) { final rounded = value.round(); setState(() => _gpuDevices = rounded); @@ -255,19 +349,60 @@ class _MinerControlsState extends State { ), ), const SizedBox(height: 24), - ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: !_isMining ? Colors.green : Colors.blue, - padding: const EdgeInsets.symmetric(vertical: 15), - textStyle: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, + + // Control Buttons + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Node Button + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: _nodeButtonColor, + padding: const EdgeInsets.symmetric( + vertical: 15, + horizontal: 20, + ), + textStyle: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + minimumSize: const Size(140, 50), + ), + onPressed: _isNodeToggling ? null : _toggleNode, + child: Text(_nodeButtonText), ), - minimumSize: const Size(200, 50), - ), - onPressed: _isAttemptingToggle ? null : _toggle, - child: Text(!_isMining ? 'Start Mining' : 'Stop Mining'), + const SizedBox(width: 16), + + // Miner Button + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: _minerButtonColor, + padding: const EdgeInsets.symmetric( + vertical: 15, + horizontal: 20, + ), + textStyle: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + minimumSize: const Size(140, 50), + ), + onPressed: (_isMinerToggling || !_isNodeRunning) + ? null + : _toggleMiner, + child: Text(_minerButtonText), + ), + ], ), + + // Status indicator + if (_isNodeRunning && !_isMining) ...[ + const SizedBox(height: 12), + Text( + 'Node running - ready to mine', + style: TextStyle(color: Colors.green.shade300, fontSize: 12), + ), + ], ], ); } diff --git a/miner-app/lib/features/settings/settings_screen.dart b/miner-app/lib/features/settings/settings_screen.dart index 17e541ce..f77da330 100644 --- a/miner-app/lib/features/settings/settings_screen.dart +++ b/miner-app/lib/features/settings/settings_screen.dart @@ -1,6 +1,9 @@ import 'package:flutter/material.dart'; import 'package:quantus_miner/features/settings/settings_app_bar.dart'; +import 'package:quantus_miner/main.dart'; +import 'package:quantus_miner/src/config/miner_config.dart'; import 'package:quantus_miner/src/services/binary_manager.dart'; +import 'package:quantus_miner/src/services/miner_settings_service.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; class SettingsScreen extends StatefulWidget { @@ -15,32 +18,107 @@ class _SettingsScreenState extends State { BinaryVersion? _nodeUpdateInfo; bool _isLoading = true; + // Chain selection + final MinerSettingsService _settingsService = MinerSettingsService(); + String _selectedChainId = MinerConfig.defaultChainId; + @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { - _getBinaryInfo(); + _loadSettings(); }); } - Future _getBinaryInfo() async { - // Simulate a tiny delay for smooth UI transition if cached - // await Future.delayed(const Duration(milliseconds: 300)); - + Future _loadSettings() async { final [nodeUpdateInfo, minerUpdateInfo] = await Future.wait([ BinaryManager.getNodeBinaryVersion(), BinaryManager.getMinerBinaryVersion(), ]); + final chainId = await _settingsService.getChainId(); + if (mounted) { setState(() { _minerUpdateInfo = minerUpdateInfo; _nodeUpdateInfo = nodeUpdateInfo; + _selectedChainId = chainId; _isLoading = false; }); } } + Future _onChainChanged(String? newChainId) async { + if (newChainId == null || newChainId == _selectedChainId) return; + + // Check if mining is currently running + final orchestrator = GlobalMinerManager.getOrchestrator(); + final isMining = orchestrator?.isRunning ?? false; + + if (isMining) { + // Show warning dialog + final shouldChange = await showDialog( + context: context, + builder: (context) => AlertDialog( + backgroundColor: const Color(0xFF1C1C1C), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + title: const Text( + 'Stop Mining?', + style: TextStyle(color: Colors.white), + ), + content: const Text( + 'Changing the chain requires stopping mining first. ' + 'Do you want to stop mining and switch chains?', + style: TextStyle(color: Colors.white70), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: Text( + 'Cancel', + style: TextStyle(color: Colors.white.useOpacity(0.7)), + ), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(true), + style: TextButton.styleFrom( + foregroundColor: const Color(0xFF00E676), + ), + child: const Text('Stop & Switch'), + ), + ], + ), + ); + + if (shouldChange != true) return; + + // Stop mining + await orchestrator?.stop(); + } + + // Save the new chain ID + await _settingsService.saveChainId(newChainId); + + if (mounted) { + setState(() { + _selectedChainId = newChainId; + }); + + // Show confirmation + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'Switched to ${MinerConfig.getChainById(newChainId).displayName}', + ), + backgroundColor: const Color(0xFF00E676), + behavior: SnackBarBehavior.floating, + ), + ); + } + } + @override Widget build(BuildContext context) { // Define a theme-consistent accent color (e.g., a tech green or teal) @@ -111,8 +189,22 @@ class _SettingsScreenState extends State { const SizedBox(height: 32), - // Example: You could add another section here later - // Text('ACCOUNT', style: ...), + // Network Section Header + Text( + 'NETWORK', + style: TextStyle( + color: Colors.white.useOpacity(0.5), + fontSize: 12, + letterSpacing: 1.5, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 16), + + // Chain Selector + _buildChainSelector(accentColor), + + const SizedBox(height: 32), ], ), ), @@ -203,4 +295,105 @@ class _SettingsScreenState extends State { ), ); } + + Widget _buildChainSelector(Color accentColor) { + final selectedChain = MinerConfig.getChainById(_selectedChainId); + + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0xFF1C1C1C), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: Colors.white.useOpacity(0.05), width: 1), + boxShadow: [ + BoxShadow( + color: Colors.black.useOpacity(0.2), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Row( + children: [ + // Icon Container + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: accentColor.useOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Icon(Icons.link_rounded, color: accentColor, size: 20), + ), + const SizedBox(width: 16), + + // Title and description + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Chain', + style: TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 2), + Text( + selectedChain.description, + style: TextStyle( + color: Colors.white.useOpacity(0.5), + fontSize: 12, + ), + ), + ], + ), + ), + + // Dropdown + if (_isLoading) + SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white.useOpacity(0.3), + ), + ) + else + Container( + padding: const EdgeInsets.symmetric(horizontal: 8), + decoration: BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.white.useOpacity(0.1)), + ), + child: DropdownButton( + value: _selectedChainId, + dropdownColor: const Color(0xFF1C1C1C), + underline: const SizedBox(), + icon: Icon( + Icons.arrow_drop_down, + color: Colors.white.useOpacity(0.7), + ), + style: TextStyle( + color: Colors.white.useOpacity(0.9), + fontFamily: 'Courier', + fontWeight: FontWeight.bold, + fontSize: 13, + ), + items: MinerConfig.availableChains.map((chain) { + return DropdownMenuItem( + value: chain.id, + child: Text(chain.displayName), + ); + }).toList(), + onChanged: _onChainChanged, + ), + ), + ], + ), + ); + } } diff --git a/miner-app/lib/src/config/miner_config.dart b/miner-app/lib/src/config/miner_config.dart index 1643bc9b..643f68af 100644 --- a/miner-app/lib/src/config/miner_config.dart +++ b/miner-app/lib/src/config/miner_config.dart @@ -97,12 +97,14 @@ class MinerConfig { id: 'dev', displayName: 'Development', description: 'Local development chain', + rpcUrl: 'http://127.0.0.1:9933', isDefault: true, ), ChainConfig( id: 'dirac', displayName: 'Dirac', description: 'Dirac testnet', + rpcUrl: 'https://a1-dirac.quantus.cat', isDefault: false, ), ]; @@ -160,15 +162,22 @@ class ChainConfig { final String id; final String displayName; final String description; + final String rpcUrl; final bool isDefault; const ChainConfig({ required this.id, required this.displayName, required this.description, + required this.rpcUrl, required this.isDefault, }); + /// Whether this chain uses the local node RPC + bool get isLocalNode => + rpcUrl.contains('127.0.0.1') || rpcUrl.contains('localhost'); + @override - String toString() => 'ChainConfig(id: $id, displayName: $displayName)'; + String toString() => + 'ChainConfig(id: $id, displayName: $displayName, rpcUrl: $rpcUrl)'; } diff --git a/miner-app/lib/src/services/miner_process_manager.dart b/miner-app/lib/src/services/miner_process_manager.dart index f7a72e5a..9a42ec1d 100644 --- a/miner-app/lib/src/services/miner_process_manager.dart +++ b/miner-app/lib/src/services/miner_process_manager.dart @@ -188,6 +188,7 @@ class MinerProcessManager { List _buildArgs(ExternalMinerConfig config) { return [ + 'serve', // Subcommand required by new miner CLI '--node-addr', config.nodeAddress, '--cpu-workers', diff --git a/miner-app/lib/src/services/mining_orchestrator.dart b/miner-app/lib/src/services/mining_orchestrator.dart index 305153de..a12dcbb1 100644 --- a/miner-app/lib/src/services/mining_orchestrator.dart +++ b/miner-app/lib/src/services/mining_orchestrator.dart @@ -26,13 +26,19 @@ enum MiningState { /// Node is running, waiting for RPC to be ready. waitingForRpc, + /// Node is running (RPC ready), miner not started. + nodeRunning, + /// Miner is starting up. startingMiner, /// Both node and miner are running, mining is active. mining, - /// Currently stopping. + /// Stopping miner only. + stoppingMiner, + + /// Currently stopping everything. stopping, /// An error occurred. @@ -150,6 +156,13 @@ class MiningOrchestrator { /// Whether mining is currently active. bool get isMining => _state == MiningState.mining; + /// Whether the node is running (with or without miner). + bool get isNodeRunning => + _state == MiningState.nodeRunning || + _state == MiningState.startingMiner || + _state == MiningState.mining || + _state == MiningState.stoppingMiner; + /// Whether the orchestrator is in any running state. bool get isRunning => _state != MiningState.idle && _state != MiningState.error; @@ -160,13 +173,16 @@ class MiningOrchestrator { /// Miner process PID, if running. int? get minerProcessPid => _minerManager.pid; + // Store config for later use when starting miner separately + MiningSessionConfig? _currentConfig; + MiningOrchestrator() { _initializeApiClients(); _setupNodeSyncCallback(); _subscribeToProcessEvents(); } - /// Start mining with the given configuration. + /// Start mining with the given configuration (starts both node and miner). /// /// This will: /// 1. Cleanup any existing processes @@ -175,11 +191,23 @@ class MiningOrchestrator { /// 4. Start the miner /// 5. Begin polling for stats Future start(MiningSessionConfig config) async { + await startNode(config); + if (_state == MiningState.nodeRunning) { + await startMiner(); + } + } + + /// Start only the node (without the miner). + /// + /// Use this to enable balance queries and chain sync without mining. + Future startNode(MiningSessionConfig config) async { if (_state != MiningState.idle && _state != MiningState.error) { - _log.w('Cannot start: already in state $_state'); + _log.w('Cannot start node: already in state $_state'); return; } + _currentConfig = config; + try { // Initialize stats with worker counts _statsService.updateWorkers(config.cpuWorkers); @@ -218,8 +246,43 @@ class MiningOrchestrator { _setState(MiningState.waitingForRpc); await _waitForNodeRpc(); - // Start miner + // Start chain RPC polling (for balance, sync status, etc.) + _chainRpcClient.startPolling(); + + // Start Prometheus polling for target block + _prometheusTimer?.cancel(); + _prometheusTimer = Timer.periodic( + MinerConfig.prometheusPollingInterval, + (_) => _fetchPrometheusMetrics(), + ); + + _setState(MiningState.nodeRunning); + _log.i('Node started successfully'); + } catch (e, st) { + _log.e('Failed to start node', error: e, stackTrace: st); + _setState(MiningState.error); + await _stopInternal(); + rethrow; + } + } + + /// Start the miner (node must already be running). + Future startMiner() async { + if (_state != MiningState.nodeRunning) { + _log.w('Cannot start miner: node not running (state: $_state)'); + return; + } + + if (_currentConfig == null) { + _log.e('Cannot start miner: no config available'); + return; + } + + final config = _currentConfig!; + + try { _setState(MiningState.startingMiner); + await _minerManager.start( ExternalMinerConfig( binary: config.minerBinary, @@ -230,36 +293,72 @@ class MiningOrchestrator { ), ); - // Start polling - _startPolling(); + // Start miner metrics polling + _minerApiClient.startPolling(); _setState(MiningState.mining); - _log.i('Mining started successfully'); + _log.i('Miner started successfully'); } catch (e, st) { - _log.e('Failed to start mining', error: e, stackTrace: st); - _setState(MiningState.error); - await _stopInternal(); + _log.e('Failed to start miner', error: e, stackTrace: st); + _setState(MiningState.nodeRunning); // Revert to node-only state rethrow; } } - /// Stop mining gracefully. + /// Stop only the miner (keep node running). + Future stopMiner() async { + if (_state != MiningState.mining) { + _log.w('Cannot stop miner: not mining (state: $_state)'); + return; + } + + _log.i('Stopping miner...'); + _setState(MiningState.stoppingMiner); + + _minerApiClient.stopPolling(); + await _minerManager.stop(); + + _resetStats(); + _setState(MiningState.nodeRunning); + _log.i('Miner stopped, node still running'); + } + + /// Stop everything (node and miner) gracefully. Future stop() async { if (_state == MiningState.idle) { return; } - _log.i('Stopping mining...'); + _log.i('Stopping everything...'); _setState(MiningState.stopping); await _stopInternal(); _setState(MiningState.idle); _resetStats(); - _log.i('Mining stopped'); + _currentConfig = null; + _log.i('All processes stopped'); } - /// Force stop mining immediately. + /// Stop only the node (and miner if running). + Future stopNode() async { + if (!isNodeRunning && + _state != MiningState.startingNode && + _state != MiningState.waitingForRpc) { + _log.w('Cannot stop node: not running (state: $_state)'); + return; + } + + _log.i('Stopping node...'); + _setState(MiningState.stopping); + await _stopInternal(); + _setState(MiningState.idle); + _resetStats(); + _currentConfig = null; + _log.i('Node stopped'); + } + + /// Force stop everything immediately. void forceStop() { - _log.i('Force stopping mining...'); + _log.i('Force stopping everything...'); _setState(MiningState.stopping); _stopPolling(); @@ -268,7 +367,8 @@ class MiningOrchestrator { _setState(MiningState.idle); _resetStats(); - _log.i('Mining force stopped'); + _currentConfig = null; + _log.i('Force stopped'); } /// Dispose of all resources. @@ -396,18 +496,6 @@ class MiningOrchestrator { _log.w('Node RPC not ready after max attempts, proceeding anyway'); } - void _startPolling() { - _minerApiClient.startPolling(); - _chainRpcClient.startPolling(); - - // Prometheus polling for target block - _prometheusTimer?.cancel(); - _prometheusTimer = Timer.periodic( - MinerConfig.prometheusPollingInterval, - (_) => _fetchPrometheusMetrics(), - ); - } - void _stopPolling() { _minerApiClient.stopPolling(); _chainRpcClient.stopPolling(); diff --git a/miner-app/pubspec.yaml b/miner-app/pubspec.yaml index 94b17a12..f23926b3 100644 --- a/miner-app/pubspec.yaml +++ b/miner-app/pubspec.yaml @@ -18,6 +18,7 @@ dependencies: # Networking and storage http: # Version managed by melos.yaml shared_preferences: # Version managed by melos.yaml + polkadart: # For local node RPC queries # Routing go_router: # Version managed by melos.yaml From 6de191162137b035e05fd16a26ef7c71654a79e9 Mon Sep 17 00:00:00 2001 From: illuzen Date: Mon, 2 Feb 2026 15:44:33 +0800 Subject: [PATCH 08/10] various bug fixes --- .../features/miner/miner_balance_card.dart | 5 +++ .../lib/features/miner/miner_controls.dart | 18 ++++++++- .../miner/miner_dashboard_screen.dart | 12 ++++++ .../lib/src/services/mining_orchestrator.dart | 38 +++++++++++++++++++ .../src/services/mining_stats_service.dart | 31 +++++++++++++-- .../src/services/node_process_manager.dart | 3 +- 6 files changed, 102 insertions(+), 5 deletions(-) diff --git a/miner-app/lib/features/miner/miner_balance_card.dart b/miner-app/lib/features/miner/miner_balance_card.dart index f2a7ef51..21206d86 100644 --- a/miner-app/lib/features/miner/miner_balance_card.dart +++ b/miner-app/lib/features/miner/miner_balance_card.dart @@ -63,16 +63,21 @@ class _MinerBalanceCardState extends State { print('address: $address'); final chainConfig = MinerConfig.getChainById(_chainId); + print( + 'Chain config: id=${chainConfig.id}, rpcUrl=${chainConfig.rpcUrl}, isLocalNode=${chainConfig.isLocalNode}', + ); BigInt balance; if (chainConfig.isLocalNode) { // Use local node RPC for dev chain + print('Querying balance from LOCAL node: ${chainConfig.rpcUrl}'); balance = await _queryBalanceFromLocalNode( address, chainConfig.rpcUrl, ); } else { // Use SDK's SubstrateService for remote chains (dirac) + print('Querying balance from REMOTE (SDK SubstrateService)'); balance = await SubstrateService().queryBalance(address); } diff --git a/miner-app/lib/features/miner/miner_controls.dart b/miner-app/lib/features/miner/miner_controls.dart index f1d9b0f7..784a2387 100644 --- a/miner-app/lib/features/miner/miner_controls.dart +++ b/miner-app/lib/features/miner/miner_controls.dart @@ -210,6 +210,12 @@ class _MinerControlsState extends State { } try { + // Update settings in case they changed while miner was stopped + widget.orchestrator!.updateMinerSettings( + cpuWorkers: _cpuWorkers, + gpuDevices: _gpuDevices, + ); + await widget.orchestrator!.startMiner(); } catch (e) { print('Error starting miner: $e'); @@ -241,6 +247,14 @@ class _MinerControlsState extends State { bool get _isNodeRunning => widget.orchestrator?.isNodeRunning ?? false; bool get _isMining => widget.orchestrator?.isMining ?? false; + /// Whether miner is starting or running (for disabling settings) + bool get _isMinerActive { + final state = widget.orchestrator?.state; + return state == MiningState.startingMiner || + state == MiningState.mining || + state == MiningState.stoppingMiner; + } + String get _nodeButtonText { final state = widget.orchestrator?.state; if (state == MiningState.startingNode) return 'Starting...'; @@ -269,7 +283,9 @@ class _MinerControlsState extends State { @override Widget build(BuildContext context) { - final canEditSettings = !_isNodeRunning; + // Allow editing settings when miner is stopped (even if node is running) + // Disable during startingMiner, mining, and stoppingMiner states + final canEditSettings = !_isMinerActive; return Column( mainAxisSize: MainAxisSize.min, diff --git a/miner-app/lib/features/miner/miner_dashboard_screen.dart b/miner-app/lib/features/miner/miner_dashboard_screen.dart index 9a360c72..f5544155 100644 --- a/miner-app/lib/features/miner/miner_dashboard_screen.dart +++ b/miner-app/lib/features/miner/miner_dashboard_screen.dart @@ -42,6 +42,7 @@ class _MinerDashboardScreenState extends State { // Subscriptions StreamSubscription? _statsSubscription; StreamSubscription? _errorSubscription; + StreamSubscription? _stateSubscription; @override void initState() { @@ -55,6 +56,7 @@ class _MinerDashboardScreenState extends State { // Clean up subscriptions _statsSubscription?.cancel(); _errorSubscription?.cancel(); + _stateSubscription?.cancel(); // Clean up orchestrator if (_orchestrator != null) { @@ -80,6 +82,7 @@ class _MinerDashboardScreenState extends State { // Cancel old subscriptions _statsSubscription?.cancel(); _errorSubscription?.cancel(); + _stateSubscription?.cancel(); if (mounted) { setState(() { @@ -91,12 +94,21 @@ class _MinerDashboardScreenState extends State { if (orchestrator != null) { _statsSubscription = orchestrator.statsStream.listen(_onStatsUpdate); _errorSubscription = orchestrator.errorStream.listen(_onError); + _stateSubscription = orchestrator.stateStream.listen(_onStateChange); } // Register with global manager for cleanup GlobalMinerManager.setOrchestrator(orchestrator); } + void _onStateChange(MiningState state) { + // Trigger rebuild when orchestrator state changes + // This ensures button labels and UI state update properly + if (mounted) { + setState(() {}); + } + } + void _onError(MinerError error) { if (!mounted) return; diff --git a/miner-app/lib/src/services/mining_orchestrator.dart b/miner-app/lib/src/services/mining_orchestrator.dart index a12dcbb1..95eef1b2 100644 --- a/miner-app/lib/src/services/mining_orchestrator.dart +++ b/miner-app/lib/src/services/mining_orchestrator.dart @@ -266,6 +266,36 @@ class MiningOrchestrator { } } + /// Update miner settings (CPU workers, GPU devices). + /// Call this before startMiner() if settings have changed. + void updateMinerSettings({int? cpuWorkers, int? gpuDevices}) { + if (_currentConfig == null) { + _log.w('Cannot update settings: no config available'); + return; + } + + _currentConfig = MiningSessionConfig( + nodeBinary: _currentConfig!.nodeBinary, + minerBinary: _currentConfig!.minerBinary, + identityFile: _currentConfig!.identityFile, + rewardsFile: _currentConfig!.rewardsFile, + chainId: _currentConfig!.chainId, + cpuWorkers: cpuWorkers ?? _currentConfig!.cpuWorkers, + gpuDevices: gpuDevices ?? _currentConfig!.gpuDevices, + detectedGpuCount: _currentConfig!.detectedGpuCount, + minerListenPort: _currentConfig!.minerListenPort, + ); + + // Update stats to reflect new settings + _statsService.updateWorkers(_currentConfig!.cpuWorkers); + _statsService.updateGpuDevices(_currentConfig!.gpuDevices); + _emitStats(); + + _log.i( + 'Miner settings updated: cpuWorkers=${_currentConfig!.cpuWorkers}, gpuDevices=${_currentConfig!.gpuDevices}', + ); + } + /// Start the miner (node must already be running). Future startMiner() async { if (_state != MiningState.nodeRunning) { @@ -296,10 +326,15 @@ class MiningOrchestrator { // Start miner metrics polling _minerApiClient.startPolling(); + // Update stats to reflect miner is running + _statsService.setMinerRunning(true); + _emitStats(); + _setState(MiningState.mining); _log.i('Miner started successfully'); } catch (e, st) { _log.e('Failed to start miner', error: e, stackTrace: st); + _statsService.setMinerRunning(false); _setState(MiningState.nodeRunning); // Revert to node-only state rethrow; } @@ -318,6 +353,8 @@ class MiningOrchestrator { _minerApiClient.stopPolling(); await _minerManager.stop(); + // Update stats to reflect miner is stopped + _statsService.setMinerRunning(false); _resetStats(); _setState(MiningState.nodeRunning); _log.i('Miner stopped, node still running'); @@ -532,6 +569,7 @@ class MiningOrchestrator { void _resetStats() { _statsService.updateHashrate(0); + _statsService.setMinerRunning(false); _lastValidHashrate = 0; _consecutiveMetricsFailures = 0; _emitStats(); diff --git a/miner-app/lib/src/services/mining_stats_service.dart b/miner-app/lib/src/services/mining_stats_service.dart index e07f86cc..1786e93b 100644 --- a/miner-app/lib/src/services/mining_stats_service.dart +++ b/miner-app/lib/src/services/mining_stats_service.dart @@ -10,6 +10,7 @@ class MiningStats { final int gpuDevices; final int gpuCapacity; final bool isSyncing; + final bool isMinerRunning; final MiningStatus status; final String chainName; @@ -23,6 +24,7 @@ class MiningStats { this.gpuDevices = 0, this.gpuCapacity = 0, required this.isSyncing, + this.isMinerRunning = false, required this.status, required this.chainName, }); @@ -37,6 +39,7 @@ class MiningStats { gpuDevices = 0, gpuCapacity = 0, isSyncing = false, + isMinerRunning = false, status = MiningStatus.idle, chainName = ''; @@ -50,6 +53,7 @@ class MiningStats { int? gpuDevices, int? gpuCapacity, bool? isSyncing, + bool? isMinerRunning, MiningStatus? status, String? chainName, }) { @@ -63,6 +67,7 @@ class MiningStats { gpuDevices: gpuDevices ?? this.gpuDevices, gpuCapacity: gpuCapacity ?? this.gpuCapacity, isSyncing: isSyncing ?? this.isSyncing, + isMinerRunning: isMinerRunning ?? this.isMinerRunning, status: status ?? this.status, chainName: chainName ?? this.chainName, ); @@ -134,14 +139,34 @@ class MiningStatsService { /// Manually set syncing state (used by RPC client) void setSyncingState(bool isSyncing, int? currentBlock, int? targetBlock) { - final status = isSyncing ? MiningStatus.syncing : MiningStatus.mining; - _currentStats = _currentStats.copyWith( isSyncing: isSyncing, - status: status, currentBlock: currentBlock ?? _currentStats.currentBlock, targetBlock: targetBlock ?? _currentStats.targetBlock, ); + _updateStatus(); + } + + /// Set whether the miner process is running + void setMinerRunning(bool isRunning) { + _currentStats = _currentStats.copyWith(isMinerRunning: isRunning); + _updateStatus(); + } + + /// Update the status based on current state + void _updateStatus() { + MiningStatus status; + if (_currentStats.isSyncing) { + status = MiningStatus.syncing; + } else if (_currentStats.isMinerRunning) { + status = MiningStatus.mining; + } else { + status = MiningStatus.idle; + } + + if (_currentStats.status != status) { + _currentStats = _currentStats.copyWith(status: status); + } } /// Update chain name from RPC data diff --git a/miner-app/lib/src/services/node_process_manager.dart b/miner-app/lib/src/services/node_process_manager.dart index 8f829264..8cae007f 100644 --- a/miner-app/lib/src/services/node_process_manager.dart +++ b/miner-app/lib/src/services/node_process_manager.dart @@ -202,7 +202,8 @@ class NodeProcessManager { List _buildArgs(NodeConfig config, String basePath) { return [ - '--base-path', basePath, + // Only use --base-path for non-dev chains (dev uses temp storage for fresh state) + if (config.chainId != 'dev') ...['--base-path', basePath], '--node-key-file', config.identityFile.path, '--rewards-address', config.rewardsAddress, '--validator', From 87d6b45881dd1ae16c884fc21cd95dcf72424f43 Mon Sep 17 00:00:00 2001 From: illuzen Date: Mon, 2 Feb 2026 15:51:36 +0800 Subject: [PATCH 09/10] better state handling --- .../lib/features/miner/miner_balance_card.dart | 18 ++++++++++++++++-- .../features/miner/miner_dashboard_screen.dart | 8 ++++++-- .../lib/src/services/mining_orchestrator.dart | 12 ++++++------ 3 files changed, 28 insertions(+), 10 deletions(-) diff --git a/miner-app/lib/features/miner/miner_balance_card.dart b/miner-app/lib/features/miner/miner_balance_card.dart index 21206d86..41c4d65b 100644 --- a/miner-app/lib/features/miner/miner_balance_card.dart +++ b/miner-app/lib/features/miner/miner_balance_card.dart @@ -12,7 +12,10 @@ import 'package:quantus_sdk/quantus_sdk.dart'; import 'package:quantus_sdk/generated/schrodinger/schrodinger.dart'; class MinerBalanceCard extends StatefulWidget { - const MinerBalanceCard({super.key}); + /// Current block number - when this changes, balance is refreshed + final int currentBlock; + + const MinerBalanceCard({super.key, this.currentBlock = 0}); @override State createState() => _MinerBalanceCardState(); @@ -24,18 +27,29 @@ class _MinerBalanceCardState extends State { String _chainId = MinerConfig.defaultChainId; Timer? _balanceTimer; final _settingsService = MinerSettingsService(); + int _lastRefreshedBlock = 0; @override void initState() { super.initState(); _loadChainAndFetchBalance(); - // Start automatic polling every 30 seconds + // Start automatic polling every 30 seconds as backup _balanceTimer = Timer.periodic(const Duration(seconds: 30), (_) { _loadChainAndFetchBalance(); }); } + @override + void didUpdateWidget(MinerBalanceCard oldWidget) { + super.didUpdateWidget(oldWidget); + // Refresh balance when block number increases (new block found) + if (widget.currentBlock > _lastRefreshedBlock && widget.currentBlock > 0) { + _lastRefreshedBlock = widget.currentBlock; + _loadChainAndFetchBalance(); + } + } + @override void dispose() { _balanceTimer?.cancel(); diff --git a/miner-app/lib/features/miner/miner_dashboard_screen.dart b/miner-app/lib/features/miner/miner_dashboard_screen.dart index f5544155..7247e283 100644 --- a/miner-app/lib/features/miner/miner_dashboard_screen.dart +++ b/miner-app/lib/features/miner/miner_dashboard_screen.dart @@ -413,7 +413,11 @@ class _MinerDashboardScreenState extends State { if (constraints.maxWidth > 800) { return Row( children: [ - Expanded(child: MinerBalanceCard()), + Expanded( + child: MinerBalanceCard( + currentBlock: _miningStats.currentBlock, + ), + ), const SizedBox(width: 16), Expanded(child: MinerStatsCard(miningStats: _miningStats)), ], @@ -421,7 +425,7 @@ class _MinerDashboardScreenState extends State { } else { return Column( children: [ - MinerBalanceCard(), + MinerBalanceCard(currentBlock: _miningStats.currentBlock), MinerStatsCard(miningStats: _miningStats), ], ); diff --git a/miner-app/lib/src/services/mining_orchestrator.dart b/miner-app/lib/src/services/mining_orchestrator.dart index 95eef1b2..e89474d2 100644 --- a/miner-app/lib/src/services/mining_orchestrator.dart +++ b/miner-app/lib/src/services/mining_orchestrator.dart @@ -585,15 +585,15 @@ class MiningOrchestrator { _consecutiveMetricsFailures = 0; _statsService.updateHashrate(metrics.hashRate); - if (metrics.workers > 0) { - _statsService.updateWorkers(metrics.workers); - } + // NOTE: Don't update workers from metrics - miner_workers includes GPU workers + // which would incorrectly inflate the CPU count. We use the configured cpuWorkers instead. if (metrics.cpuCapacity > 0) { _statsService.updateCpuCapacity(metrics.cpuCapacity); } - if (metrics.gpuDevices > 0) { - _statsService.updateGpuDevices(metrics.gpuDevices); - } + // NOTE: Don't update gpuDevices from metrics - use the configured value instead + // if (metrics.gpuDevices > 0) { + // _statsService.updateGpuDevices(metrics.gpuDevices); + // } _emitStats(); } else if (metrics.hashRate == 0.0 && _lastValidHashrate > 0) { // Keep last valid hashrate during temporary zeroes From 57c5e7e676d287855d2cf37210292e8ba6258f34 Mon Sep 17 00:00:00 2001 From: illuzen Date: Mon, 2 Feb 2026 16:16:36 +0800 Subject: [PATCH 10/10] improve logs, refactor shared process manager code --- .../features/miner/miner_balance_card.dart | 27 +-- .../lib/features/miner/miner_controls.dart | 26 +-- miner-app/lib/main.dart | 24 ++- miner-app/lib/src/config/miner_config.dart | 10 + .../src/services/base_process_manager.dart | 171 ++++++++++++++++++ .../lib/src/services/binary_manager.dart | 90 +++++---- .../lib/src/services/chain_rpc_client.dart | 19 +- .../services/external_miner_api_client.dart | 4 +- .../src/services/gpu_detection_service.dart | 15 +- .../src/services/miner_process_manager.dart | 168 ++++------------- .../src/services/miner_settings_service.dart | 57 +++--- .../src/services/node_process_manager.dart | 164 +++-------------- miner-app/lib/src/ui/logs_widget.dart | 2 +- 13 files changed, 392 insertions(+), 385 deletions(-) create mode 100644 miner-app/lib/src/services/base_process_manager.dart diff --git a/miner-app/lib/features/miner/miner_balance_card.dart b/miner-app/lib/features/miner/miner_balance_card.dart index 41c4d65b..011a2881 100644 --- a/miner-app/lib/features/miner/miner_balance_card.dart +++ b/miner-app/lib/features/miner/miner_balance_card.dart @@ -8,9 +8,12 @@ import 'package:quantus_miner/src/services/binary_manager.dart'; import 'package:quantus_miner/src/services/miner_settings_service.dart'; import 'package:quantus_miner/src/shared/extensions/snackbar_extensions.dart'; import 'package:quantus_miner/src/shared/miner_app_constants.dart'; +import 'package:quantus_miner/src/utils/app_logger.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; import 'package:quantus_sdk/generated/schrodinger/schrodinger.dart'; +final _log = log.withTag('BalanceCard'); + class MinerBalanceCard extends StatefulWidget { /// Current block number - when this changes, balance is refreshed final int currentBlock; @@ -34,8 +37,8 @@ class _MinerBalanceCardState extends State { super.initState(); _loadChainAndFetchBalance(); - // Start automatic polling every 30 seconds as backup - _balanceTimer = Timer.periodic(const Duration(seconds: 30), (_) { + // Start automatic polling as backup + _balanceTimer = Timer.periodic(MinerConfig.balancePollingInterval, (_) { _loadChainAndFetchBalance(); }); } @@ -65,7 +68,7 @@ class _MinerBalanceCardState extends State { } Future _fetchWalletBalance() async { - print('fetching wallet balance for chain: $_chainId'); + _log.d('Fetching wallet balance for chain: $_chainId'); try { final quantusHome = await BinaryManager.getQuantusHomeDirectoryPath(); final rewardsFile = File('$quantusHome/rewards-address.txt'); @@ -74,28 +77,26 @@ class _MinerBalanceCardState extends State { final address = (await rewardsFile.readAsString()).trim(); if (address.isNotEmpty) { - print('address: $address'); - final chainConfig = MinerConfig.getChainById(_chainId); - print( - 'Chain config: id=${chainConfig.id}, rpcUrl=${chainConfig.rpcUrl}, isLocalNode=${chainConfig.isLocalNode}', + _log.d( + 'Chain: ${chainConfig.id}, rpcUrl: ${chainConfig.rpcUrl}, isLocal: ${chainConfig.isLocalNode}', ); BigInt balance; if (chainConfig.isLocalNode) { // Use local node RPC for dev chain - print('Querying balance from LOCAL node: ${chainConfig.rpcUrl}'); + _log.d('Querying balance from local node: ${chainConfig.rpcUrl}'); balance = await _queryBalanceFromLocalNode( address, chainConfig.rpcUrl, ); } else { // Use SDK's SubstrateService for remote chains (dirac) - print('Querying balance from REMOTE (SDK SubstrateService)'); + _log.d('Querying balance from remote (SDK SubstrateService)'); balance = await SubstrateService().queryBalance(address); } - print('balance: $balance'); + _log.d('Balance: $balance'); if (mounted) { setState(() { @@ -123,7 +124,7 @@ class _MinerBalanceCardState extends State { } }); } - print('Error fetching wallet balance: $e'); + _log.w('Error fetching wallet balance', error: e); } } @@ -142,7 +143,7 @@ class _MinerBalanceCardState extends State { final accountInfo = await quantusApi.query.system.account(accountId); return accountInfo.data.free; } catch (e) { - print('Error querying local node balance: $e'); + _log.d('Error querying local node balance: $e'); // Return zero if node is not running or address has no balance return BigInt.zero; } @@ -155,7 +156,7 @@ class _MinerBalanceCardState extends State { _walletAddress = null; }); } - print('Rewards address file not found or empty.'); + _log.w('Rewards address file not found or empty'); } @override diff --git a/miner-app/lib/features/miner/miner_controls.dart b/miner-app/lib/features/miner/miner_controls.dart index 784a2387..1e7fba8f 100644 --- a/miner-app/lib/features/miner/miner_controls.dart +++ b/miner-app/lib/features/miner/miner_controls.dart @@ -2,15 +2,19 @@ import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:quantus_miner/src/config/miner_config.dart'; import 'package:quantus_miner/src/services/mining_orchestrator.dart'; import 'package:quantus_miner/src/services/mining_stats_service.dart'; import 'package:quantus_miner/src/shared/extensions/snackbar_extensions.dart'; +import 'package:quantus_miner/src/utils/app_logger.dart'; import '../../main.dart'; import '../../src/services/binary_manager.dart'; import '../../src/services/gpu_detection_service.dart'; import '../../src/services/miner_settings_service.dart'; +final _log = log.withTag('MinerControls'); + class MinerControls extends StatefulWidget { final MiningOrchestrator? orchestrator; final MiningStats miningStats; @@ -33,7 +37,7 @@ class _MinerControlsState extends State { int _cpuWorkers = 8; int _gpuDevices = 0; int _detectedGpuCount = 0; - String _chainId = 'dev'; + String _chainId = MinerConfig.defaultChainId; final _settingsService = MinerSettingsService(); @override @@ -88,7 +92,7 @@ class _MinerControlsState extends State { } Future _startNode() async { - print('Starting node'); + _log.i('Starting node'); // Reload chain ID in case it was changed in settings final chainId = await _settingsService.getChainId(); @@ -106,7 +110,7 @@ class _MinerControlsState extends State { final minerBin = File(minerBinPath); if (!await nodeBin.exists()) { - print('Node binary not found.'); + _log.w('Node binary not found'); if (mounted) { context.showWarningSnackbar( title: 'Node binary not found!', @@ -134,7 +138,7 @@ class _MinerControlsState extends State { ), ); } catch (e) { - print('Error starting node: $e'); + _log.e('Error starting node', error: e); if (mounted) { context.showErrorSnackbar( title: 'Error starting node!', @@ -147,13 +151,13 @@ class _MinerControlsState extends State { } Future _stopNode() async { - print('Stopping node'); + _log.i('Stopping node'); if (widget.orchestrator != null) { try { await widget.orchestrator!.stopNode(); } catch (e) { - print('Error stopping node: $e'); + _log.e('Error stopping node', error: e); } widget.orchestrator!.dispose(); } @@ -182,7 +186,7 @@ class _MinerControlsState extends State { } Future _startMiner() async { - print('Starting miner'); + _log.i('Starting miner'); if (widget.orchestrator == null) { if (mounted) { @@ -199,7 +203,7 @@ class _MinerControlsState extends State { final minerBin = File(minerBinPath); if (!await minerBin.exists()) { - print('Miner binary not found.'); + _log.w('Miner binary not found'); if (mounted) { context.showWarningSnackbar( title: 'Miner binary not found!', @@ -218,7 +222,7 @@ class _MinerControlsState extends State { await widget.orchestrator!.startMiner(); } catch (e) { - print('Error starting miner: $e'); + _log.e('Error starting miner', error: e); if (mounted) { context.showErrorSnackbar( title: 'Error starting miner!', @@ -229,13 +233,13 @@ class _MinerControlsState extends State { } Future _stopMiner() async { - print('Stopping miner'); + _log.i('Stopping miner'); if (widget.orchestrator != null) { try { await widget.orchestrator!.stopMiner(); } catch (e) { - print('Error stopping miner: $e'); + _log.e('Error stopping miner', error: e); } } } diff --git a/miner-app/lib/main.dart b/miner-app/lib/main.dart index 329c59d4..7f9b5fdc 100644 --- a/miner-app/lib/main.dart +++ b/miner-app/lib/main.dart @@ -33,9 +33,28 @@ class GlobalMinerManager { return _orchestrator; } + /// Synchronous force stop for app detach scenarios. + /// + /// This is called from _onAppDetach which cannot be async. + /// It fires off process kills without waiting for completion. + static void forceStopAll() { + _log.i('Force stopping all processes (sync)...'); + if (_orchestrator != null) { + try { + _orchestrator!.forceStop(); + _orchestrator = null; + } catch (e) { + _log.e('Error force stopping orchestrator', error: e); + } + } + + // Fire and forget - kill any remaining quantus processes + ProcessCleanupService.killAllQuantusProcesses(); + } + /// Cleanup all mining processes. /// - /// Called during app exit or detach. + /// Called during app exit (async context). static Future cleanup() async { _log.i('Starting global cleanup...'); if (_orchestrator != null) { @@ -184,7 +203,8 @@ class _MinerAppState extends State { void _onAppDetach() { _log.i('App detached, cleaning up...'); - GlobalMinerManager.cleanup(); + // Use synchronous force stop since _onAppDetach cannot be async + GlobalMinerManager.forceStopAll(); } Future _onExitRequested() async { diff --git a/miner-app/lib/src/config/miner_config.dart b/miner-app/lib/src/config/miner_config.dart index 643f68af..f10d5529 100644 --- a/miner-app/lib/src/config/miner_config.dart +++ b/miner-app/lib/src/config/miner_config.dart @@ -71,6 +71,16 @@ class MinerConfig { /// How often to poll chain RPC for peer count and block info static const Duration chainRpcPollingInterval = Duration(seconds: 1); + /// How often to poll wallet balance (backup timer) + static const Duration balancePollingInterval = Duration(seconds: 30); + + // ============================================================ + // Hardware Detection + // ============================================================ + + /// Maximum number of GPU devices to probe for during detection + static const int maxGpuProbeCount = 8; + // ============================================================ // URLs & Endpoints // ============================================================ diff --git a/miner-app/lib/src/services/base_process_manager.dart b/miner-app/lib/src/services/base_process_manager.dart new file mode 100644 index 00000000..6142e3ec --- /dev/null +++ b/miner-app/lib/src/services/base_process_manager.dart @@ -0,0 +1,171 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter/foundation.dart' show protected; +import 'package:quantus_miner/src/config/miner_config.dart'; +import 'package:quantus_miner/src/models/miner_error.dart'; +import 'package:quantus_miner/src/services/log_stream_processor.dart'; +import 'package:quantus_miner/src/services/process_cleanup_service.dart'; +import 'package:quantus_miner/src/utils/app_logger.dart'; + +/// Abstract base class for process managers. +/// +/// Provides common functionality for managing external processes like +/// the node and miner processes. +abstract class BaseProcessManager { + Process? _process; + late LogStreamProcessor _logProcessor; + final _errorController = StreamController.broadcast(); + + bool _intentionalStop = false; + + /// Tag for logging - subclasses should override + TaggedLoggerWrapper get log; + + /// Name of this process type (for logging) + String get processName; + + /// Stream of log entries from the process. + Stream get logs => _logProcessor.logs; + + /// Stream of errors (crashes, startup failures). + Stream get errors => _errorController.stream; + + /// The process ID, or null if not running. + int? get pid => _process?.pid; + + /// Whether the process is currently running. + bool get isRunning => _process != null; + + /// Access to the error controller for subclasses + @protected + StreamController get errorController => _errorController; + + /// Set the intentional stop flag (for subclasses) + @protected + set intentionalStop(bool value) => _intentionalStop = value; + + /// Clear the process reference (for subclasses) + @protected + void clearProcess() => _process = null; + + /// Initialize the log processor for a source + void initLogProcessor(String sourceName, {SyncStateProvider? getSyncState}) { + _logProcessor = LogStreamProcessor( + sourceName: sourceName, + getSyncState: getSyncState, + ); + } + + /// Attach process streams to log processor + void attachProcess(Process process) { + _process = process; + _logProcessor.attach(process); + } + + /// Create an error for startup failure - subclasses should override + MinerError createStartupError(dynamic error, [StackTrace? stackTrace]); + + /// Create an error for crash - subclasses should override + MinerError createCrashError(int exitCode); + + /// Stop the process gracefully. + /// + /// Returns a Future that completes when the process has stopped. + Future stop() async { + if (_process == null) { + return; + } + + _intentionalStop = true; + final processPid = _process!.pid; + log.i('Stopping $processName (PID: $processPid)...'); + + // Try graceful termination first + _process!.kill(ProcessSignal.sigterm); + + // Wait for graceful shutdown + final exited = await _waitForExit(MinerConfig.gracefulShutdownTimeout); + + if (!exited) { + // Force kill if still running + log.d('$processName still running, force killing...'); + await _forceKill(); + } + + _cleanup(); + log.i('$processName stopped'); + } + + /// Force stop the process immediately. + void forceStop() { + if (_process == null) { + return; + } + + _intentionalStop = true; + final processPid = _process!.pid; + log.i('Force stopping $processName (PID: $processPid)...'); + + try { + _process!.kill(ProcessSignal.sigkill); + } catch (e) { + log.e('Error force killing $processName', error: e); + } + + // Also use system cleanup as backup + ProcessCleanupService.forceKillProcess(processPid, processName); + + _cleanup(); + } + + /// Handle process exit. + void handleExit(int exitCode) { + if (_intentionalStop) { + log.d('$processName exited (code: $exitCode) - intentional stop'); + } else { + log.w('$processName crashed (exit code: $exitCode)'); + _errorController.add(createCrashError(exitCode)); + } + _cleanup(); + } + + /// Dispose of all resources. + void dispose() { + forceStop(); + _logProcessor.dispose(); + if (!_errorController.isClosed) { + _errorController.close(); + } + } + + Future _waitForExit(Duration timeout) async { + if (_process == null) return true; + + try { + await _process!.exitCode.timeout(timeout); + return true; + } on TimeoutException { + return false; + } + } + + Future _forceKill() async { + if (_process == null) return; + + try { + _process!.kill(ProcessSignal.sigkill); + await _process!.exitCode.timeout( + MinerConfig.processVerificationDelay, + onTimeout: () => -1, + ); + } catch (e) { + log.e('Error during force kill', error: e); + } + } + + void _cleanup() { + _logProcessor.detach(); + _process = null; + } +} diff --git a/miner-app/lib/src/services/binary_manager.dart b/miner-app/lib/src/services/binary_manager.dart index 64fa1ea4..474ca915 100644 --- a/miner-app/lib/src/services/binary_manager.dart +++ b/miner-app/lib/src/services/binary_manager.dart @@ -196,7 +196,7 @@ class BinaryManager { : null, ); } catch (e) { - print('Error checking node update: $e'); + _log.w('Error checking node update', error: e); return BinaryUpdateInfo(updateAvailable: false); } } @@ -228,7 +228,7 @@ class BinaryManager { : null, ); } catch (e) { - print('Error checking miner update: $e'); + _log.w('Error checking miner update', error: e); return BinaryUpdateInfo(updateAvailable: false); } } @@ -313,7 +313,7 @@ class BinaryManager { static Future updateNodeBinary({ void Function(DownloadProgress progress)? onProgress, }) async { - print('Updating node binary to latest version...'); + _log.i('Updating node binary to latest version...'); final binPath = await getNodeBinaryFilePath(); final binFile = File(binPath); @@ -322,9 +322,9 @@ class BinaryManager { // Create backup of existing binary if it exists if (await binFile.exists()) { - print('Creating backup of existing binary...'); + _log.d('Creating backup of existing binary...'); await binFile.copy(backupPath); - print('Backup created at: $backupPath'); + _log.d('Backup created at: $backupPath'); } try { @@ -337,19 +337,19 @@ class BinaryManager { // If download successful, replace the old binary if (await backupFile.exists()) { await backupFile.delete(); - print('Backup removed after successful update'); + _log.d('Backup removed after successful update'); } - print('Node binary updated successfully!'); + _log.i('Node binary updated successfully!'); return newBinary; } catch (e) { // If download failed, restore from backup - print('Download failed: $e'); + _log.e('Download failed', error: e); if (await backupFile.exists()) { - print('Restoring from backup...'); + _log.i('Restoring from backup...'); await backupFile.copy(binPath); await backupFile.delete(); - print('Binary restored from backup'); + _log.i('Binary restored from backup'); } rethrow; } @@ -367,7 +367,7 @@ class BinaryManager { ); final tag = jsonDecode(rel.body)['tag_name'] as String; - print('found latest tag: $tag'); + _log.d('Found latest tag: $tag'); // Pick asset name final target = _targetTriple(); @@ -462,10 +462,10 @@ class BinaryManager { final binPath = await getExternalMinerBinaryFilePath(); final binFile = File(binPath); - print('DEBUG: Checking for external miner at path: $binPath'); + _log.d('Checking for external miner at path: $binPath'); if (await binFile.exists() && !forceDownload) { - print('DEBUG: External miner binary already exists at $binPath'); + _log.d('External miner binary already exists at $binPath'); onProgress?.call(DownloadProgress(1, 1)); return binFile; } @@ -476,7 +476,7 @@ class BinaryManager { static Future updateMinerBinary({ void Function(DownloadProgress progress)? onProgress, }) async { - print('Updating miner binary to latest version...'); + _log.i('Updating miner binary to latest version...'); final binPath = await getExternalMinerBinaryFilePath(); final binFile = File(binPath); @@ -485,9 +485,9 @@ class BinaryManager { // Create backup of existing binary if it exists if (await binFile.exists()) { - print('Creating backup of existing miner binary...'); + _log.d('Creating backup of existing miner binary...'); await binFile.copy(backupPath); - print('Backup created at: $backupPath'); + _log.d('Backup created at: $backupPath'); } try { @@ -500,19 +500,19 @@ class BinaryManager { // If download successful, replace the old binary if (await backupFile.exists()) { await backupFile.delete(); - print('Backup removed after successful update'); + _log.d('Backup removed after successful update'); } - print('Miner binary updated successfully!'); + _log.i('Miner binary updated successfully!'); return newBinary; } catch (e) { // If download failed, restore from backup - print('Download failed: $e'); + _log.e('Download failed', error: e); if (await backupFile.exists()) { - print('Restoring from backup...'); + _log.i('Restoring from backup...'); await backupFile.copy(binPath); await backupFile.delete(); - print('Binary restored from backup'); + _log.i('Binary restored from backup'); } rethrow; } @@ -522,19 +522,19 @@ class BinaryManager { void Function(DownloadProgress progress)? onProgress, bool isUpdate = false, }) async { - print('DEBUG: External miner binary download process starting...'); + _log.d('External miner binary download process starting...'); // Find latest tag on GitHub final releaseUrl = 'https://api.github.com/repos/$_repoOwner/$_minerRepoName/releases/latest'; - print('DEBUG: Fetching latest release from: $releaseUrl'); + _log.d('Fetching latest release from: $releaseUrl'); final rel = await http.get(Uri.parse(releaseUrl)); final releaseData = jsonDecode(rel.body); final tag = releaseData['tag_name'] as String; - print('DEBUG: Found latest external miner tag: $tag'); + _log.d('Found latest external miner tag: $tag'); // Pick asset name String platform; @@ -565,17 +565,17 @@ class BinaryManager { ? '$_minerReleaseBinary-$platform-$arch.exe' : '$_minerReleaseBinary-$platform-$arch'; - print('DEBUG: Looking for asset: $asset'); + _log.d('Looking for asset: $asset'); final url = 'https://github.com/$_repoOwner/$_minerRepoName/releases/download/$tag/$asset'; // Check if the asset exists in the release final assets = releaseData['assets'] as List; - print('DEBUG: Available assets in release:'); + _log.d('Available assets in release:'); bool assetFound = false; for (var assetInfo in assets) { - print(' - ${assetInfo['name']} (${assetInfo['browser_download_url']})'); + _log.d(' - ${assetInfo['name']} (${assetInfo['browser_download_url']})'); if (assetInfo['name'] == asset) { assetFound = true; } @@ -591,14 +591,14 @@ class BinaryManager { final cacheDir = await _getCacheDir(); final tempFileName = isUpdate ? '$asset.tmp' : asset; final tempBinaryFile = File(p.join(cacheDir.path, tempFileName)); - print('DEBUG: Will download to: ${tempBinaryFile.path}'); + _log.d('Will download to: ${tempBinaryFile.path}'); final client = http.Client(); try { final request = http.Request('GET', Uri.parse(url)); final response = await client.send(request); - print('DEBUG: Download response status: ${response.statusCode}'); + _log.d('Download response status: ${response.statusCode}'); if (response.statusCode != 200) { throw Exception( 'Failed to download external miner binary: ${response.statusCode} ${response.reasonPhrase}', @@ -606,7 +606,7 @@ class BinaryManager { } final totalBytes = response.contentLength ?? -1; - print('DEBUG: Expected download size: $totalBytes bytes'); + _log.d('Expected download size: $totalBytes bytes'); int downloadedBytes = 0; List allBytes = []; @@ -620,9 +620,7 @@ class BinaryManager { } } await tempBinaryFile.writeAsBytes(allBytes); - print( - 'DEBUG: Downloaded ${allBytes.length} bytes to ${tempBinaryFile.path}', - ); + _log.d('Downloaded ${allBytes.length} bytes to ${tempBinaryFile.path}'); if (totalBytes > 0 && downloadedBytes < totalBytes) { onProgress?.call(DownloadProgress(totalBytes, totalBytes)); @@ -635,41 +633,41 @@ class BinaryManager { // Set executable permissions on temp file if (!Platform.isWindows) { - print('DEBUG: Setting executable permissions on ${tempBinaryFile.path}'); + _log.d('Setting executable permissions on ${tempBinaryFile.path}'); final chmodResult = await Process.run('chmod', [ '+x', tempBinaryFile.path, ]); - print('DEBUG: chmod exit code: ${chmodResult.exitCode}'); + _log.d('chmod exit code: ${chmodResult.exitCode}'); if (chmodResult.exitCode != 0) { - print('DEBUG: chmod stderr: ${chmodResult.stderr}'); + _log.e('chmod stderr: ${chmodResult.stderr}'); throw Exception('Failed to set executable permissions'); } } // Move to final location (atomic operation) final binPath = await getExternalMinerBinaryFilePath(); - print('DEBUG: Moving binary from ${tempBinaryFile.path} to $binPath'); + _log.d('Moving binary from ${tempBinaryFile.path} to $binPath'); // Copy instead of rename for cross-device compatibility await tempBinaryFile.copy(binPath); await tempBinaryFile.delete(); - print('DEBUG: Contents of cache directory after download:'); + _log.d('Contents of cache directory after download:'); final cacheDirContents = await cacheDir.list().toList(); for (var item in cacheDirContents) { - print(' - ${item.path}'); + _log.d(' - ${item.path}'); } // Final check final binFile = File(binPath); if (await binFile.exists()) { - print('DEBUG: External miner binary successfully created at $binPath'); + _log.i('External miner binary successfully created at $binPath'); // Save version info await _saveMinerVersion(tag); } else { - print( - 'DEBUG: ERROR - External miner binary still not found at $binPath after download!', + _log.e( + 'External miner binary still not found at $binPath after download!', ); throw Exception( 'External miner binary not found after download at $binPath', @@ -691,14 +689,14 @@ class BinaryManager { if (await nodeKeyFile.exists()) { final stat = await nodeKeyFile.stat(); if (stat.size > 0) { - print( + _log.d( 'Node key file already exists and has content (size: ${stat.size} bytes)', ); return nodeKeyFile; } } - print('Node key file not found or empty. Generating new key...'); + _log.i('Node key file not found or empty. Generating new key...'); final nodeBinaryPath = await getNodeBinaryFilePath(); if (!await File(nodeBinaryPath).exists()) { throw Exception( @@ -718,7 +716,7 @@ class BinaryManager { if (await nodeKeyFile.exists()) { final stat = await nodeKeyFile.stat(); if (stat.size > 0) { - print( + _log.i( 'Successfully generated node key file: ${nodeKeyFile.path} (size: ${stat.size} bytes)', ); return nodeKeyFile; @@ -734,7 +732,7 @@ class BinaryManager { ); } } catch (e) { - print('Error generating node key: $e'); + _log.e('Error generating node key', error: e); rethrow; } } diff --git a/miner-app/lib/src/services/chain_rpc_client.dart b/miner-app/lib/src/services/chain_rpc_client.dart index 4f3c49c0..4ebee2a4 100644 --- a/miner-app/lib/src/services/chain_rpc_client.dart +++ b/miner-app/lib/src/services/chain_rpc_client.dart @@ -2,6 +2,9 @@ import 'dart:async'; import 'dart:convert'; import 'package:http/http.dart' as http; import 'package:quantus_miner/src/config/miner_config.dart'; +import 'package:quantus_miner/src/utils/app_logger.dart'; + +final _log = log.withTag('ChainRpc'); class ChainInfo { final int peerCount; @@ -121,15 +124,14 @@ class ChainRpcClient { nodeVersion: nodeVersion, ); - // Only log successful chain info - print('DEBUG: Chain connected - Peers: $peerCount, Block: $currentBlock'); + _log.d('Chain connected - Peers: $peerCount, Block: $currentBlock'); return info; } catch (e) { // Only log unexpected errors, not connection issues during startup if (!e.toString().contains('Connection refused') && !e.toString().contains('Connection reset') && !e.toString().contains('timeout')) { - print('DEBUG: getChainInfo error: $e'); + _log.w('getChainInfo error', error: e); } return null; } @@ -230,8 +232,8 @@ class ChainRpcClient { } else { // Don't log connection errors during startup - they're expected if (response.statusCode != 0) { - print( - 'DEBUG: RPC HTTP error for $method: ${response.statusCode} ${response.reasonPhrase}', + _log.w( + 'RPC HTTP error for $method: ${response.statusCode} ${response.reasonPhrase}', ); } throw Exception('HTTP ${response.statusCode}: ${response.reasonPhrase}'); @@ -262,11 +264,8 @@ class PollingChainRpcClient extends ChainRpcClient { void Function(ChainInfo info)? onChainInfoUpdate; void Function(String error)? onError; - PollingChainRpcClient({ - super.rpcUrl, - super.timeout, - this.pollInterval = const Duration(seconds: 3), - }); + PollingChainRpcClient({super.rpcUrl, super.timeout, Duration? pollInterval}) + : pollInterval = pollInterval ?? MinerConfig.prometheusPollingInterval; /// Start polling for chain information void startPolling() { diff --git a/miner-app/lib/src/services/external_miner_api_client.dart b/miner-app/lib/src/services/external_miner_api_client.dart index 0f9468b6..4a4f8cc1 100644 --- a/miner-app/lib/src/services/external_miner_api_client.dart +++ b/miner-app/lib/src/services/external_miner_api_client.dart @@ -47,11 +47,11 @@ class ExternalMinerApiClient { MinerConfig.minerMetricsUrl(MinerConfig.defaultMinerMetricsPort), _httpClient = http.Client(); - /// Start polling for metrics every second + /// Start polling for metrics void startPolling() { _pollTimer?.cancel(); _pollTimer = Timer.periodic( - const Duration(seconds: 1), + MinerConfig.metricsPollingInterval, (_) => _pollMetrics(), ); } diff --git a/miner-app/lib/src/services/gpu_detection_service.dart b/miner-app/lib/src/services/gpu_detection_service.dart index 59f08344..b3d97181 100644 --- a/miner-app/lib/src/services/gpu_detection_service.dart +++ b/miner-app/lib/src/services/gpu_detection_service.dart @@ -1,7 +1,12 @@ import 'dart:io'; +import 'package:quantus_miner/src/config/miner_config.dart'; +import 'package:quantus_miner/src/utils/app_logger.dart'; + import 'binary_manager.dart'; +final _log = log.withTag('GpuDetection'); + class GpuDetectionService { /// Detects the number of GPU devices on the system by probing the miner process static Future detectGpuCount() async { @@ -10,12 +15,12 @@ class GpuDetectionService { final bin = File(binPath); if (!await bin.exists()) { - print('External miner binary not found at $binPath'); + _log.w('External miner binary not found at $binPath'); return 0; } - // Start probing from 8 down to 1 - for (int i = 8; i >= 1; i--) { + // Start probing from maxGpuProbeCount down to 1 + for (int i = MinerConfig.maxGpuProbeCount; i >= 1; i--) { try { // Use a very short duration to fail fast or succeed quickly // If it succeeds, it will take 1 second. @@ -47,11 +52,11 @@ class GpuDetectionService { } } } catch (e) { - print('Error probing for $i GPUs: $e'); + _log.d('Error probing for $i GPUs', error: e); } } } catch (e) { - print('Error in GPU detection service: $e'); + _log.e('Error in GPU detection service', error: e); } return 0; } diff --git a/miner-app/lib/src/services/miner_process_manager.dart b/miner-app/lib/src/services/miner_process_manager.dart index 9a42ec1d..2e258f85 100644 --- a/miner-app/lib/src/services/miner_process_manager.dart +++ b/miner-app/lib/src/services/miner_process_manager.dart @@ -1,9 +1,7 @@ -import 'dart:async'; import 'dart:io'; -import 'package:quantus_miner/src/config/miner_config.dart'; import 'package:quantus_miner/src/models/miner_error.dart'; -import 'package:quantus_miner/src/services/log_stream_processor.dart'; +import 'package:quantus_miner/src/services/base_process_manager.dart'; import 'package:quantus_miner/src/services/process_cleanup_service.dart'; import 'package:quantus_miner/src/utils/app_logger.dart'; @@ -42,150 +40,92 @@ class ExternalMinerConfig { /// - Monitoring process health and exit /// - Stopping the process gracefully or forcefully /// - Emitting log entries and error events -class MinerProcessManager { - Process? _process; - late LogStreamProcessor _logProcessor; - final _errorController = StreamController.broadcast(); +class MinerProcessManager extends BaseProcessManager { + @override + TaggedLoggerWrapper get log => _log; - bool _intentionalStop = false; + @override + String get processName => 'miner'; - /// Stream of log entries from the miner. - Stream get logs => _logProcessor.logs; - - /// Stream of errors (crashes, startup failures). - Stream get errors => _errorController.stream; - - /// The process ID, or null if not running. - int? get pid => _process?.pid; + @override + MinerError createStartupError(dynamic error, [StackTrace? stackTrace]) { + return MinerError.minerStartupFailed(error, stackTrace); + } - /// Whether the miner process is currently running. - bool get isRunning => _process != null; + @override + MinerError createCrashError(int exitCode) { + return MinerError.minerCrashed(exitCode); + } MinerProcessManager() { - _logProcessor = LogStreamProcessor(sourceName: 'miner'); + initLogProcessor('miner'); } /// Start the miner process. /// /// Throws an exception if startup fails. Future start(ExternalMinerConfig config) async { - if (_process != null) { - _log.w('Miner already running (PID: ${_process!.pid})'); + if (isRunning) { + log.w('Miner already running (PID: $pid)'); return; } - _intentionalStop = false; + intentionalStop = false; // Validate binary exists if (!await config.binary.exists()) { final error = MinerError.minerStartupFailed( 'Miner binary not found: ${config.binary.path}', ); - _errorController.add(error); + errorController.add(error); throw Exception(error.message); } // Build command arguments final args = _buildArgs(config); - _log.i('Starting miner...'); - _log.d('Command: ${config.binary.path} ${args.join(' ')}'); + log.i('Starting miner...'); + log.d('Command: ${config.binary.path} ${args.join(' ')}'); try { - _process = await Process.start(config.binary.path, args); - _logProcessor.attach(_process!); + final proc = await Process.start(config.binary.path, args); + attachProcess(proc); // Monitor for unexpected exit - _process!.exitCode.then(_handleExit); + proc.exitCode.then(handleExit); // Verify it started successfully by waiting briefly await Future.delayed(const Duration(seconds: 2)); - if (_process != null) { + // Check if process is still running + // We just attached, so pid should be available + final processPid = pid; + if (processPid != null) { final stillRunning = await ProcessCleanupService.isProcessRunning( - _process!.pid, + processPid, ); if (!stillRunning) { final error = MinerError.minerStartupFailed( 'Miner died during startup', ); - _errorController.add(error); - _process = null; + errorController.add(error); + clearProcess(); throw Exception(error.message); } } - _log.i('Miner started (PID: ${_process!.pid})'); + log.i('Miner started (PID: $pid)'); } catch (e, st) { if (e.toString().contains('Miner died during startup')) { rethrow; } final error = MinerError.minerStartupFailed(e, st); - _errorController.add(error); - _process = null; + errorController.add(error); + clearProcess(); rethrow; } } - /// Stop the miner process gracefully. - /// - /// Returns a Future that completes when the process has stopped. - Future stop() async { - if (_process == null) { - return; - } - - _intentionalStop = true; - final processPid = _process!.pid; - _log.i('Stopping miner (PID: $processPid)...'); - - // Try graceful termination first - _process!.kill(ProcessSignal.sigterm); - - // Wait for graceful shutdown - final exited = await _waitForExit(MinerConfig.gracefulShutdownTimeout); - - if (!exited) { - // Force kill if still running - _log.d('Miner still running, force killing...'); - await _forceKill(); - } - - _cleanup(); - _log.i('Miner stopped'); - } - - /// Force stop the miner process immediately. - void forceStop() { - if (_process == null) { - return; - } - - _intentionalStop = true; - final processPid = _process!.pid; - _log.i('Force stopping miner (PID: $processPid)...'); - - try { - _process!.kill(ProcessSignal.sigkill); - } catch (e) { - _log.e('Error force killing miner', error: e); - } - - // Also use system cleanup as backup - ProcessCleanupService.forceKillProcess(processPid, 'miner'); - - _cleanup(); - } - - /// Dispose of all resources. - void dispose() { - forceStop(); - _logProcessor.dispose(); - if (!_errorController.isClosed) { - _errorController.close(); - } - } - List _buildArgs(ExternalMinerConfig config) { return [ 'serve', // Subcommand required by new miner CLI @@ -199,44 +139,4 @@ class MinerProcessManager { config.metricsPort.toString(), ]; } - - void _handleExit(int exitCode) { - if (_intentionalStop) { - _log.d('Miner exited (code: $exitCode) - intentional stop'); - } else { - _log.w('Miner crashed (exit code: $exitCode)'); - _errorController.add(MinerError.minerCrashed(exitCode)); - } - _cleanup(); - } - - Future _waitForExit(Duration timeout) async { - if (_process == null) return true; - - try { - await _process!.exitCode.timeout(timeout); - return true; - } on TimeoutException { - return false; - } - } - - Future _forceKill() async { - if (_process == null) return; - - try { - _process!.kill(ProcessSignal.sigkill); - await _process!.exitCode.timeout( - MinerConfig.processVerificationDelay, - onTimeout: () => -1, - ); - } catch (e) { - _log.e('Error during force kill', error: e); - } - } - - void _cleanup() { - _logProcessor.detach(); - _process = null; - } } diff --git a/miner-app/lib/src/services/miner_settings_service.dart b/miner-app/lib/src/services/miner_settings_service.dart index 313fd526..82663dd7 100644 --- a/miner-app/lib/src/services/miner_settings_service.dart +++ b/miner-app/lib/src/services/miner_settings_service.dart @@ -2,8 +2,11 @@ import 'dart:io'; import 'package:quantus_miner/src/config/miner_config.dart'; import 'package:quantus_miner/src/services/binary_manager.dart'; +import 'package:quantus_miner/src/utils/app_logger.dart'; import 'package:shared_preferences/shared_preferences.dart'; +final _log = log.withTag('Settings'); + class MinerSettingsService { static const String _keyCpuWorkers = 'cpu_workers'; static const String _keyGpuDevices = 'gpu_devices'; @@ -57,7 +60,7 @@ class MinerSettingsService { } Future logout() async { - print('Starting app logout/reset...'); + _log.i('Starting app logout/reset...'); // 1. Delete node identity file (node_key.p2p) try { @@ -65,12 +68,12 @@ class MinerSettingsService { final identityFile = File('$quantusHome/node_key.p2p'); if (await identityFile.exists()) { await identityFile.delete(); - print('✅ Node identity file deleted: ${identityFile.path}'); + _log.i('✅ Node identity file deleted: ${identityFile.path}'); } else { - print('ℹ️ Node identity file not found, skipping deletion.'); + _log.d('ℹ️ Node identity file not found, skipping deletion.'); } } catch (e) { - print('❌ Error deleting node identity file: $e'); + _log.e('❌ Error deleting node identity file', error: e); } // 2. Delete rewards address file @@ -79,12 +82,12 @@ class MinerSettingsService { final rewardsFile = File('$quantusHome/rewards-address.txt'); if (await rewardsFile.exists()) { await rewardsFile.delete(); - print('✅ Rewards address file deleted: ${rewardsFile.path}'); + _log.i('✅ Rewards address file deleted: ${rewardsFile.path}'); } else { - print('ℹ️ Rewards address file not found, skipping deletion.'); + _log.d('ℹ️ Rewards address file not found, skipping deletion.'); } } catch (e) { - print('❌ Error deleting rewards address file: $e'); + _log.e('❌ Error deleting rewards address file', error: e); } // 3. Delete node binary @@ -93,12 +96,12 @@ class MinerSettingsService { final binaryFile = File(nodeBinaryPath); if (await binaryFile.exists()) { await binaryFile.delete(); - print('✅ Node binary file deleted: ${binaryFile.path}'); + _log.i('✅ Node binary file deleted: ${binaryFile.path}'); } else { - print('ℹ️ Node binary file not found, skipping deletion.'); + _log.d('ℹ️ Node binary file not found, skipping deletion.'); } } catch (e) { - print('❌ Error deleting node binary file: $e'); + _log.e('❌ Error deleting node binary file', error: e); } // 4. Delete external miner binary @@ -108,12 +111,12 @@ class MinerSettingsService { final minerFile = File(minerBinaryPath); if (await minerFile.exists()) { await minerFile.delete(); - print('✅ External miner binary deleted: ${minerFile.path}'); + _log.i('✅ External miner binary deleted: ${minerFile.path}'); } else { - print('ℹ️ External miner binary not found, skipping deletion.'); + _log.d('ℹ️ External miner binary not found, skipping deletion.'); } } catch (e) { - print('❌ Error deleting external miner binary: $e'); + _log.e('❌ Error deleting external miner binary', error: e); } // 5. Delete node data directory (blockchain data) @@ -122,12 +125,12 @@ class MinerSettingsService { final nodeDataDir = Directory('$quantusHome/node_data'); if (await nodeDataDir.exists()) { await nodeDataDir.delete(recursive: true); - print('✅ Node data directory deleted: ${nodeDataDir.path}'); + _log.i('✅ Node data directory deleted: ${nodeDataDir.path}'); } else { - print('ℹ️ Node data directory not found, skipping deletion.'); + _log.d('ℹ️ Node data directory not found, skipping deletion.'); } } catch (e) { - print('❌ Error deleting node data directory: $e'); + _log.e('❌ Error deleting node data directory', error: e); } // 6. Clean up bin directory and leftover files @@ -141,22 +144,22 @@ class MinerSettingsService { ); for (var file in tarFiles) { await file.delete(); - print('✅ Cleaned up archive: ${file.path}'); + _log.i('✅ Cleaned up archive: ${file.path}'); } // Try to remove bin directory if it's empty try { await binDir.delete(); - print('✅ Empty bin directory removed: ${binDir.path}'); + _log.i('✅ Empty bin directory removed: ${binDir.path}'); } catch (e) { // Directory not empty, that's fine - print('ℹ️ Bin directory not empty, keeping it.'); + _log.d('ℹ️ Bin directory not empty, keeping it.'); } } else { - print('ℹ️ Bin directory not found, skipping cleanup.'); + _log.d('ℹ️ Bin directory not found, skipping cleanup.'); } } catch (e) { - print('❌ Error cleaning up bin directory: $e'); + _log.e('❌ Error cleaning up bin directory', error: e); } // 7. Try to remove the entire .quantus directory if it's empty @@ -166,25 +169,25 @@ class MinerSettingsService { if (await quantusDir.exists()) { try { await quantusDir.delete(); - print('✅ Removed empty .quantus directory: $quantusHome'); + _log.i('✅ Removed empty .quantus directory: $quantusHome'); } catch (e) { // Directory not empty, that's fine - print('ℹ️ .quantus directory not empty, keeping it.'); + _log.d('ℹ️ .quantus directory not empty, keeping it.'); } } } catch (e) { - print('❌ Error removing .quantus directory: $e'); + _log.e('❌ Error removing .quantus directory', error: e); } // 8. Clear SharedPreferences try { final prefs = await SharedPreferences.getInstance(); await prefs.clear(); - print('✅ SharedPreferences cleared'); + _log.i('✅ SharedPreferences cleared'); } catch (e) { - print('❌ Error clearing SharedPreferences: $e'); + _log.e('❌ Error clearing SharedPreferences', error: e); } - print('🎉 App logout/reset complete! You can now go through setup again.'); + _log.i('🎉 App logout/reset complete!'); } } diff --git a/miner-app/lib/src/services/node_process_manager.dart b/miner-app/lib/src/services/node_process_manager.dart index 8cae007f..b8f35f2d 100644 --- a/miner-app/lib/src/services/node_process_manager.dart +++ b/miner-app/lib/src/services/node_process_manager.dart @@ -4,9 +4,9 @@ import 'dart:io'; import 'package:path/path.dart' as p; import 'package:quantus_miner/src/config/miner_config.dart'; import 'package:quantus_miner/src/models/miner_error.dart'; +import 'package:quantus_miner/src/services/base_process_manager.dart'; import 'package:quantus_miner/src/services/binary_manager.dart'; import 'package:quantus_miner/src/services/log_stream_processor.dart'; -import 'package:quantus_miner/src/services/process_cleanup_service.dart'; import 'package:quantus_miner/src/utils/app_logger.dart'; final _log = log.withTag('NodeProcess'); @@ -56,52 +56,47 @@ class NodeConfig { /// - Monitoring process health and exit /// - Stopping the process gracefully or forcefully /// - Emitting log entries and error events -class NodeProcessManager { - Process? _process; - late LogStreamProcessor _logProcessor; - final _errorController = StreamController.broadcast(); - - bool _intentionalStop = false; - - /// Stream of log entries from the node. - Stream get logs => _logProcessor.logs; +class NodeProcessManager extends BaseProcessManager { + /// Callback to get current sync state for log filtering. + SyncStateProvider? getSyncState; - /// Stream of errors (crashes, startup failures). - Stream get errors => _errorController.stream; + @override + TaggedLoggerWrapper get log => _log; - /// The process ID, or null if not running. - int? get pid => _process?.pid; + @override + String get processName => 'node'; - /// Whether the node process is currently running. - bool get isRunning => _process != null; + @override + MinerError createStartupError(dynamic error, [StackTrace? stackTrace]) { + return MinerError.nodeStartupFailed(error, stackTrace); + } - /// Callback to get current sync state for log filtering. - SyncStateProvider? getSyncState; + @override + MinerError createCrashError(int exitCode) { + return MinerError.nodeCrashed(exitCode); + } NodeProcessManager() { - _logProcessor = LogStreamProcessor( - sourceName: 'node', - getSyncState: () => getSyncState?.call() ?? false, - ); + initLogProcessor('node', getSyncState: () => getSyncState?.call() ?? false); } /// Start the node process. /// /// Throws an exception if startup fails. Future start(NodeConfig config) async { - if (_process != null) { - _log.w('Node already running (PID: ${_process!.pid})'); + if (isRunning) { + log.w('Node already running (PID: $pid)'); return; } - _intentionalStop = false; + intentionalStop = false; // Validate binary exists if (!await config.binary.exists()) { final error = MinerError.nodeStartupFailed( 'Node binary not found: ${config.binary.path}', ); - _errorController.add(error); + errorController.add(error); throw Exception(error.message); } @@ -110,7 +105,7 @@ class NodeProcessManager { final error = MinerError.nodeStartupFailed( 'Identity file not found: ${config.identityFile.path}', ); - _errorController.add(error); + errorController.add(error); throw Exception(error.message); } @@ -122,84 +117,25 @@ class NodeProcessManager { // Build command arguments final args = _buildArgs(config, basePath); - _log.i('Starting node...'); - _log.d('Command: ${config.binary.path} ${args.join(' ')}'); + log.i('Starting node...'); + log.d('Command: ${config.binary.path} ${args.join(' ')}'); try { - _process = await Process.start(config.binary.path, args); - _logProcessor.attach(_process!); + final proc = await Process.start(config.binary.path, args); + attachProcess(proc); // Monitor for unexpected exit - _process!.exitCode.then(_handleExit); + proc.exitCode.then(handleExit); - _log.i('Node started (PID: ${_process!.pid})'); + log.i('Node started (PID: $pid)'); } catch (e, st) { final error = MinerError.nodeStartupFailed(e, st); - _errorController.add(error); - _process = null; + errorController.add(error); + clearProcess(); rethrow; } } - /// Stop the node process gracefully. - /// - /// Returns a Future that completes when the process has stopped. - Future stop() async { - if (_process == null) { - return; - } - - _intentionalStop = true; - final processPid = _process!.pid; - _log.i('Stopping node (PID: $processPid)...'); - - // Try graceful termination first - _process!.kill(ProcessSignal.sigterm); - - // Wait for graceful shutdown - final exited = await _waitForExit(MinerConfig.gracefulShutdownTimeout); - - if (!exited) { - // Force kill if still running - _log.d('Node still running, force killing...'); - await _forceKill(); - } - - _cleanup(); - _log.i('Node stopped'); - } - - /// Force stop the node process immediately. - void forceStop() { - if (_process == null) { - return; - } - - _intentionalStop = true; - final processPid = _process!.pid; - _log.i('Force stopping node (PID: $processPid)...'); - - try { - _process!.kill(ProcessSignal.sigkill); - } catch (e) { - _log.e('Error force killing node', error: e); - } - - // Also use system cleanup as backup - ProcessCleanupService.forceKillProcess(processPid, 'node'); - - _cleanup(); - } - - /// Dispose of all resources. - void dispose() { - forceStop(); - _logProcessor.dispose(); - if (!_errorController.isClosed) { - _errorController.close(); - } - } - List _buildArgs(NodeConfig config, String basePath) { return [ // Only use --base-path for non-dev chains (dev uses temp storage for fresh state) @@ -218,44 +154,4 @@ class NodeProcessManager { '--enable-peer-sharing', ]; } - - void _handleExit(int exitCode) { - if (_intentionalStop) { - _log.d('Node exited (code: $exitCode) - intentional stop'); - } else { - _log.w('Node crashed (exit code: $exitCode)'); - _errorController.add(MinerError.nodeCrashed(exitCode)); - } - _cleanup(); - } - - Future _waitForExit(Duration timeout) async { - if (_process == null) return true; - - try { - await _process!.exitCode.timeout(timeout); - return true; - } on TimeoutException { - return false; - } - } - - Future _forceKill() async { - if (_process == null) return; - - try { - _process!.kill(ProcessSignal.sigkill); - await _process!.exitCode.timeout( - MinerConfig.processVerificationDelay, - onTimeout: () => -1, - ); - } catch (e) { - _log.e('Error during force kill', error: e); - } - } - - void _cleanup() { - _logProcessor.detach(); - _process = null; - } } diff --git a/miner-app/lib/src/ui/logs_widget.dart b/miner-app/lib/src/ui/logs_widget.dart index 1eba86a6..49322d5d 100644 --- a/miner-app/lib/src/ui/logs_widget.dart +++ b/miner-app/lib/src/ui/logs_widget.dart @@ -154,7 +154,7 @@ class _LogsWidgetState extends State { child: _logs.isEmpty ? const Center( child: Text( - 'No logs available\nStart mining to see live logs', + 'No logs available\nStart the node to see live logs', textAlign: TextAlign.center, style: TextStyle( color: Colors.grey,