diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..b29b472 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "cmake.sourceDirectory": "C:/Users/86133/Desktop/windows/windows" +} \ No newline at end of file diff --git a/UPDATE.md b/UPDATE.md new file mode 100644 index 0000000..e0fbf28 --- /dev/null +++ b/UPDATE.md @@ -0,0 +1,43 @@ +## 更新记录 + +> 说明:本文件用于记录每次客户端代码层面的更新内容,按时间倒序排列。 + +--- + +### 2026-01-23 + +- **网络与构建相关** + - 配置 Flutter 使用国内镜像源(`PUB_HOSTED_URL` / `FLUTTER_STORAGE_BASE_URL`),解决 `pub.dev` 访问失败问题。 + - 清理并修复 Windows 构建缓存(CMake 路径不一致导致的构建失败)。 + - 排查 `sqlite3` 原生资产构建失败的原因(无法访问 GitHub 下载预编译库),为后续在有外网环境下构建做准备。 + +- **数据库与路径** + - 移除未使用的 `package:path/path.dart` 导入,database 层统一通过 `PathUtils.binFile` 处理路径。 + +- **重连与认证逻辑** + - `ClientCore`: + - 为 TCP 连接新增**指数退避重连策略**:10s、20s、40s、80s、160s,最多重试 5 次,并在连接成功后重置重试次数。 + - 在连接错误、连接关闭和心跳超时(超过 60 分钟未收到 ping)时触发重连。 + - **重连成功后自动发送 `auth.machineCode` 认证消息**(在已成功获取机器码和身份信息的前提下)。 + +- **持续挖矿(SustainMiner)** + - 从 `mining.windows.conf` 的 `[sustain]` 段读取配置,校验必填字段与矿工程序路径。 + - 当没有租约任务时自动启动持续挖矿,有租约任务到来时暂停,租约结束后自动恢复持续挖矿。 + - 新增持续挖矿进程监控: + - 监听矿工进程退出事件,如果在持续挖矿运行且未暂停的情况下进程意外退出,5 秒后自动重启持续挖矿。 + +- **状态与 UI 展示** + - `ClientStatus`: + - 新增 `sustainingMining` 状态,用于表示“持续挖矿中”,在界面上显示为**蓝色圆点 + 文案「持续挖矿中」**。 + - `ClientProvider`: + - 根据是否有租约任务 / 持续挖矿任务,区分显示: + - `mining`:租约挖矿中。 + - `sustainingMining`:持续挖矿中。 + - 统一通过 `_getMiningInfo()` 构造当前挖矿信息(支持租约挖矿和持续挖矿),供界面展示。 + - `MainScreen`: + - “挖矿信息”按钮在以下状态下可用:`ClientStatus.mining` 或 `ClientStatus.sustainingMining`,并且存在 `miningInfo`。 + - 点击后进入同一套挖矿信息界面。 + - `MiningInfoScreen`: + - 复用同一界面展示日志和当前任务信息。 + - 当为**持续挖矿**时,不再显示“结束时间”和“剩余时间”区域,仅展示基本任务信息和日志。 + diff --git a/bin/mining.windows.conf b/bin/mining.windows.conf index 1300f03..388a34f 100644 --- a/bin/mining.windows.conf +++ b/bin/mining.windows.conf @@ -1,20 +1,29 @@ -[client] -server_url=10.168.2.220:2345 -update_url=http://10.168.2.220:8888/lease #请确认您的主机上安装了下列挖矿软件,确认后可以打开注释,并修改其路径,如果没有安装,请勿打开注释 #请使用双\\,否则可能无法解析出准确的路径 -[bzminer] -# path=C:\\path\\bzminer + +[client] +server_url=18.183.240.108:2345 +update_url=https://test.m2pool.com/api/lease + +#请确认您的主机上安装了下列挖矿软件,确认后可以打开注释,并修改其路径,如果没有安装,请勿打开注释 +#请使用双\\,否则可能无法解析出准确的路径 + [lolminer] # path=C:\\path\\lolminer + [rigel] # path=C:\\path\\rigel -#如果您的网络无法直接连通各个矿池,需要使用各大矿池专用网咯,请打开proxy的注释 +[bzminer] +# path=C:\\path\\bzminer + + +#如果您的网络无法直接连通各个矿池,需要使用各大矿池专用网络,请打开proxy的注释 #打开此注释后会使用各大矿池的专用网络,每笔订单额外增加1%的网络费用 [proxy] # proxy=true + #持续挖矿开关,即在矿机没有租约期间是否自行挖矿 #开启此选项启动客户端后,客户端会自动根据下面配置开启挖矿任务,直到云算力平台有人租赁本台GPU主机 #当该租约结束后,本台GPU主机会自动切回下方配置的挖矿任务 @@ -27,4 +36,5 @@ update_url=http://10.168.2.220:8888/lease #wallet="挖矿钱包" #worker_id="矿工号" #pool_user="挖矿账号名,f2pool/m2pool等不支持钱包挖矿的矿池需配置,其余支持钱包挖矿的矿池无需配置" -#wallet_mining=true #pool_user打开时同时打开本配置 \ No newline at end of file +#wallet_mining=true #pool_user打开时同时打开本配置 + diff --git a/lib/core/client_core.dart b/lib/core/client_core.dart index 26dcd50..3252ddf 100644 --- a/lib/core/client_core.dart +++ b/lib/core/client_core.dart @@ -20,8 +20,13 @@ class ClientCore { bool _isConnected = false; DateTime? _lastPingTime; Timer? _heartbeatTimer; + Timer? _reconnectTimer; StreamController? _logController; + // 重连相关 + int _reconnectAttempts = 0; + static const int _maxReconnectAttempts = 5; + // 需要GPU和挖矿软件信息用于认证 Map? _gpusInfo; List? _miningSofts; @@ -86,11 +91,10 @@ class ClientCore { _socket = await Socket.connect(host, port, timeout: const Duration(seconds: 10)); _isConnected = true; + // 连接成功,重置重连计数器 + _reconnectAttempts = 0; _log('连接到服务器成功: $_serverUrl'); - // 注意:不在这里发送身份认证,等待机器码获取完成后再发送 - // 身份认证消息将在机器码获取完成后通过 sendMachineCode() 发送 - // 开始接收消息 _socket!.listen( _onDataReceived, @@ -100,6 +104,12 @@ class ClientCore { ); onStatusChanged?.call(ui.ClientStatus.online); + + // 如果已经有机器码和认证信息,立即发送认证消息(重连场景) + if (_auth != null && _machineCode != null && _machineCode!.isNotEmpty && _machineCode != '正在获取...' && _machineCode != '获取失败') { + _log('重连成功,自动发送身份认证消息'); + _sendMachineCode(); + } } catch (e) { _isConnected = false; _log('连接失败: $e'); @@ -280,20 +290,54 @@ class ClientCore { _reconnect(); } - /// 重连 + /// 重连(指数退避策略) void _reconnect() { - Future.delayed(const Duration(seconds: 5), () { - // 检查是否正在停止或已停止 + // 取消之前的重连定时器 + _reconnectTimer?.cancel(); + + // 检查是否正在停止或已停止 + if (_socket == null || _logController == null) { + return; // 已经停止,不执行重连 + } + + // 检查是否已达到最大重试次数 + if (_reconnectAttempts >= _maxReconnectAttempts) { + _log('已达到最大重连次数($_maxReconnectAttempts次),停止重连'); + return; + } + + // 计算延迟时间:10秒 * 2^(重试次数) + // 第1次:10秒,第2次:20秒,第3次:40秒,第4次:80秒,第5次:160秒 + final delaySeconds = 10 * (1 << _reconnectAttempts); + _reconnectAttempts++; + + _log('将在 ${delaySeconds}秒 后进行第 $_reconnectAttempts 次重连尝试(最多$_maxReconnectAttempts次)'); + + _reconnectTimer = Timer(Duration(seconds: delaySeconds), () { + // 再次检查是否正在停止或已停止 if (_socket == null || _logController == null) { return; // 已经停止,不执行重连 } - if (!_isConnected) { - _log('尝试重新连接...'); - _connect().catchError((e) { - _log('重连失败: $e'); - }); + // 检查是否已经连接成功(可能在其他地方已经连接) + if (_isConnected) { + _reconnectAttempts = 0; // 重置计数器 + return; } + + _log('尝试第 $_reconnectAttempts 次重新连接...'); + _connect().then((_) { + // 连接成功,计数器已在 _connect() 中重置 + _log('重连成功'); + }).catchError((e) { + _log('第 $_reconnectAttempts 次重连失败: $e'); + // 如果未达到最大重试次数,继续重连 + if (_reconnectAttempts < _maxReconnectAttempts) { + _reconnect(); + } else { + _log('已达到最大重连次数,停止重连'); + } + }); }); } @@ -307,6 +351,8 @@ class ClientCore { _log('超过60分钟未收到心跳,连接可能已断开'); _isConnected = false; onStatusChanged?.call(ui.ClientStatus.offline); + // 心跳超时视为新的断开事件,重置重连计数器 + _reconnectAttempts = 0; _reconnect(); } } @@ -331,6 +377,10 @@ class ClientCore { _heartbeatTimer?.cancel(); _heartbeatTimer = null; + _reconnectTimer?.cancel(); + _reconnectTimer = null; + _reconnectAttempts = 0; // 重置重连计数器 + // 先取消 socket 监听,避免 onDone 回调在关闭 controller 后执行 _socket?.destroy(); _socket = null; diff --git a/lib/core/mining_manager.dart b/lib/core/mining_manager.dart index 889ec1b..00e57be 100644 --- a/lib/core/mining_manager.dart +++ b/lib/core/mining_manager.dart @@ -15,6 +15,10 @@ class MiningManager { MiningTaskInfo? _currentTask; Timer? _taskMonitor; final StreamController _minerLogController = StreamController.broadcast(); + final StreamController _processExitController = StreamController.broadcast(); + + /// 进程退出事件流 + Stream get processExitStream => _processExitController.stream; /// 启动挖矿 Future startMining(MiningTaskInfo task, MiningConfig config) async { @@ -71,6 +75,7 @@ class MiningManager { _currentTask = task; _startTaskMonitor(task); _startLogCapture(); + _monitorProcessExit(); _logger.info('挖矿已启动 (PID: ${_currentProcess!.pid})'); return true; @@ -183,6 +188,28 @@ class MiningManager { }); } + /// 监控进程退出 + void _monitorProcessExit() { + if (_currentProcess == null) return; + + _currentProcess!.exitCode.then((exitCode) { + _logger.warning('挖矿进程已退出,退出码: $exitCode'); + // 清理状态 + _currentProcess = null; + final wasMining = _currentTask != null; + _currentTask = null; + _taskMonitor?.cancel(); + _taskMonitor = null; + + // 发送进程退出事件 + if (wasMining) { + _processExitController.add(null); + } + }).catchError((e) { + _logger.severe('监控进程退出失败: $e'); + }); + } + /// 获取挖矿日志流 Stream get minerLogStream => _minerLogController.stream; @@ -192,6 +219,7 @@ class MiningManager { /// 清理资源 void dispose() { _minerLogController.close(); + _processExitController.close(); } } diff --git a/lib/core/sustain_miner.dart b/lib/core/sustain_miner.dart index ceb5326..aea1d99 100644 --- a/lib/core/sustain_miner.dart +++ b/lib/core/sustain_miner.dart @@ -19,6 +19,7 @@ class SustainMiner { SustainMiningConfig? _config; MiningConfig? _miningConfig; Timer? _monitorTimer; + StreamSubscription? _processExitSubscription; bool _isRunning = false; bool _isPaused = false; @@ -104,6 +105,7 @@ class SustainMiner { _logger.info('启动持续挖矿...'); await _startMining(); + _startProcessMonitor(); } /// 停止持续挖矿 @@ -111,6 +113,8 @@ class SustainMiner { _isRunning = false; _monitorTimer?.cancel(); _monitorTimer = null; + _processExitSubscription?.cancel(); + _processExitSubscription = null; if (_miningManager.isMining && _miningManager.currentTask == null) { // 只有持续挖矿任务在运行时才停止 @@ -148,6 +152,10 @@ class SustainMiner { _logger.info('恢复持续挖矿...'); _isPaused = false; await _startMining(); + // 确保进程监控已启动 + if (_processExitSubscription == null) { + _startProcessMonitor(); + } } /// 启动挖矿 @@ -176,6 +184,26 @@ class SustainMiner { await _miningManager.startMining(task, _miningConfig!); } + /// 启动进程监控 + void _startProcessMonitor() { + _processExitSubscription?.cancel(); + + // 监听挖矿进程退出事件 + _processExitSubscription = _miningManager.processExitStream.listen((_) { + // 检查是否是持续挖矿进程退出 + if (_isRunning && !_isPaused && !_miningManager.isMining && _miningManager.currentTask == null) { + _logger.warning('持续挖矿进程意外退出,将在5秒后自动重启...'); + // 延迟5秒后重启,避免频繁重启 + Future.delayed(const Duration(seconds: 5), () { + if (_isRunning && !_isPaused && !_miningManager.isMining) { + _logger.info('自动重启持续挖矿...'); + _startMining(); + } + }); + } + }); + } + String _trimQuotes(String value) { var v = value.trim(); if (v.length >= 2) { diff --git a/lib/models/client_status.dart b/lib/models/client_status.dart index e7a2aa7..624d280 100644 --- a/lib/models/client_status.dart +++ b/lib/models/client_status.dart @@ -1,9 +1,10 @@ import 'package:flutter/material.dart'; enum ClientStatus { - offline, // 离线(心跳异常,红色) - online, // 在线(心跳正常,绿色) - mining, // 挖矿中(挖矿程序启动时开启,挖矿程序结束后关闭,黄色) + offline, // 离线(心跳异常,红色) + online, // 在线(心跳正常,绿色) + mining, // 挖矿中(租约挖矿,黄色) + sustainingMining, // 持续挖矿中(持续挖矿,蓝色) } extension ClientStatusExtension on ClientStatus { @@ -15,6 +16,8 @@ extension ClientStatusExtension on ClientStatus { return '在线'; case ClientStatus.mining: return '挖矿中'; + case ClientStatus.sustainingMining: + return '持续挖矿中'; } } @@ -26,6 +29,8 @@ extension ClientStatusExtension on ClientStatus { return Colors.green; case ClientStatus.mining: return Colors.orange; + case ClientStatus.sustainingMining: + return Colors.blue; } } } diff --git a/lib/providers/client_provider.dart b/lib/providers/client_provider.dart index 2bd221e..0a93d67 100644 --- a/lib/providers/client_provider.dart +++ b/lib/providers/client_provider.dart @@ -165,20 +165,8 @@ class ClientProvider with ChangeNotifier { memory: gpu.memory, )).toList(), machineCode: machineCode, - status: ClientStatus.online, - miningInfo: _currentMiningTask != null - ? MiningInfo( - coin: _currentMiningTask!.coin, - algo: _currentMiningTask!.algo, - pool: _currentMiningTask!.pool, - poolUrl: _currentMiningTask!.poolUrl, - walletAddress: _currentMiningTask!.walletAddress, - workerId: _currentMiningTask!.workerId, - pid: null, // Dart 进程管理可能需要额外处理 - miner: _currentMiningTask!.miner, - endTimestamp: _currentMiningTask!.endTimestamp, - ) - : null, + status: _getInitialStatus(), + miningInfo: _getMiningInfo(), ); _isInitialized = true; @@ -218,9 +206,16 @@ class ClientProvider with ChangeNotifier { /// 状态变化回调 void _onStatusChanged(ClientStatus status) { if (_clientInfo != null) { - final newStatus = status == ClientStatus.mining || _miningManager.isMining - ? ClientStatus.mining - : status; + // 判断当前状态:优先显示挖矿状态 + ClientStatus newStatus; + if (_miningManager.isMining) { + // 如果有租约任务,显示挖矿中;否则显示持续挖矿中 + newStatus = _currentMiningTask != null + ? ClientStatus.mining + : (_sustainMiner.isRunning ? ClientStatus.sustainingMining : ClientStatus.mining); + } else { + newStatus = status; + } _clientInfo = ClientInfo( version: _clientInfo!.version, @@ -228,19 +223,7 @@ class ClientProvider with ChangeNotifier { gpus: _clientInfo!.gpus, machineCode: _clientInfo!.machineCode, status: newStatus, - miningInfo: _currentMiningTask != null - ? MiningInfo( - coin: _currentMiningTask!.coin, - algo: _currentMiningTask!.algo, - pool: _currentMiningTask!.pool, - poolUrl: _currentMiningTask!.poolUrl, - walletAddress: _currentMiningTask!.walletAddress, - workerId: _currentMiningTask!.workerId, - pid: null, - miner: _currentMiningTask!.miner, - endTimestamp: _currentMiningTask!.endTimestamp, - ) - : null, + miningInfo: _getMiningInfo(), ); notifyListeners(); } @@ -268,6 +251,7 @@ class ClientProvider with ChangeNotifier { await _sustainMiner.resume(); } + // 更新状态 _onStatusChanged(_clientCore.isConnected ? ClientStatus.online : ClientStatus.offline); } @@ -285,9 +269,15 @@ class ClientProvider with ChangeNotifier { try { // 只更新状态,不重新获取GPU信息(GPU信息只在启动时获取一次) - final status = _miningManager.isMining - ? ClientStatus.mining - : (_clientCore.isConnected ? ClientStatus.online : ClientStatus.offline); + ClientStatus status; + if (_miningManager.isMining) { + // 如果有租约任务,显示挖矿中;否则显示持续挖矿中 + status = _currentMiningTask != null + ? ClientStatus.mining + : (_sustainMiner.isRunning ? ClientStatus.sustainingMining : ClientStatus.mining); + } else { + status = _clientCore.isConnected ? ClientStatus.online : ClientStatus.offline; + } _clientInfo = ClientInfo( version: _clientInfo!.version, @@ -295,19 +285,7 @@ class ClientProvider with ChangeNotifier { gpus: _clientInfo!.gpus, // 使用已有的GPU信息,不重新获取 machineCode: _clientInfo!.machineCode, status: status, - miningInfo: _currentMiningTask != null - ? MiningInfo( - coin: _currentMiningTask!.coin, - algo: _currentMiningTask!.algo, - pool: _currentMiningTask!.pool, - poolUrl: _currentMiningTask!.poolUrl, - walletAddress: _currentMiningTask!.walletAddress, - workerId: _currentMiningTask!.workerId, - pid: null, - miner: _currentMiningTask!.miner, - endTimestamp: _currentMiningTask!.endTimestamp, - ) - : null, + miningInfo: _getMiningInfo(), ); notifyListeners(); } catch (e) { @@ -315,6 +293,51 @@ class ClientProvider with ChangeNotifier { } } + /// 获取初始状态 + ClientStatus _getInitialStatus() { + if (_miningManager.isMining) { + return _currentMiningTask != null + ? ClientStatus.mining + : (_sustainMiner.isRunning ? ClientStatus.sustainingMining : ClientStatus.mining); + } + return _clientCore.isConnected ? ClientStatus.online : ClientStatus.offline; + } + + /// 获取挖矿信息(支持租约挖矿和持续挖矿) + MiningInfo? _getMiningInfo() { + if (_currentMiningTask != null) { + // 租约挖矿任务 + return MiningInfo( + coin: _currentMiningTask!.coin, + algo: _currentMiningTask!.algo, + pool: _currentMiningTask!.pool, + poolUrl: _currentMiningTask!.poolUrl, + walletAddress: _currentMiningTask!.walletAddress, + workerId: _currentMiningTask!.workerId, + pid: null, + miner: _currentMiningTask!.miner, + endTimestamp: _currentMiningTask!.endTimestamp, + ); + } else if (_miningManager.isMining && _sustainMiner.isRunning) { + // 持续挖矿任务 + final sustainTask = _miningManager.currentTask; + if (sustainTask != null) { + return MiningInfo( + coin: sustainTask.coin, + algo: sustainTask.algo, + pool: sustainTask.pool, + poolUrl: sustainTask.poolUrl, + walletAddress: sustainTask.walletAddress, + workerId: sustainTask.workerId, + pid: null, + miner: sustainTask.miner, + endTimestamp: sustainTask.endTimestamp, + ); + } + } + return null; + } + /// 重启客户端 Future restart() async { _clientCore.stop(); diff --git a/lib/screens/main_screen.dart b/lib/screens/main_screen.dart index 369ebf0..1c62a6b 100644 --- a/lib/screens/main_screen.dart +++ b/lib/screens/main_screen.dart @@ -295,12 +295,15 @@ class MainScreen extends StatelessWidget { label: const Text('查看/修改配置'), ), ElevatedButton.icon( - onPressed: clientInfo.status == ClientStatus.mining && clientInfo.miningInfo != null + onPressed: (clientInfo.status == ClientStatus.mining || clientInfo.status == ClientStatus.sustainingMining) && clientInfo.miningInfo != null ? () { Navigator.push( context, MaterialPageRoute( - builder: (_) => MiningInfoScreen(miningInfo: clientInfo.miningInfo!), + builder: (_) => MiningInfoScreen( + miningInfo: clientInfo.miningInfo!, + isSustainMining: clientInfo.status == ClientStatus.sustainingMining, + ), ), ); } @@ -308,7 +311,7 @@ class MainScreen extends StatelessWidget { icon: const Icon(Icons.diamond), label: const Text('挖矿信息'), style: ElevatedButton.styleFrom( - backgroundColor: clientInfo.status == ClientStatus.mining && clientInfo.miningInfo != null + backgroundColor: ((clientInfo.status == ClientStatus.mining || clientInfo.status == ClientStatus.sustainingMining) && clientInfo.miningInfo != null) ? null : Colors.grey, ), diff --git a/lib/screens/mining_info_screen.dart b/lib/screens/mining_info_screen.dart index bcc1ede..e4d7a01 100644 --- a/lib/screens/mining_info_screen.dart +++ b/lib/screens/mining_info_screen.dart @@ -6,10 +6,12 @@ import '../core/mining_manager.dart'; class MiningInfoScreen extends StatefulWidget { final MiningInfo miningInfo; + final bool isSustainMining; // 是否为持续挖矿 const MiningInfoScreen({ super.key, required this.miningInfo, + this.isSustainMining = false, }); @override @@ -63,8 +65,10 @@ class _MiningInfoScreenState extends State { @override Widget build(BuildContext context) { - final endTime = DateTime.fromMillisecondsSinceEpoch(widget.miningInfo.endTimestamp * 1000); final formatter = DateFormat('yyyy-MM-dd HH:mm:ss'); + final endTime = widget.isSustainMining + ? null + : DateTime.fromMillisecondsSinceEpoch(widget.miningInfo.endTimestamp * 1000); return Scaffold( appBar: AppBar( @@ -100,12 +104,14 @@ class _MiningInfoScreenState extends State { _buildInfoRow('挖矿软件', widget.miningInfo.miner!), if (widget.miningInfo.pid != null) _buildInfoRow('进程ID', widget.miningInfo.pid.toString()), - _buildInfoRow( - '结束时间', - formatter.format(endTime), - ), - const SizedBox(height: 16), - _buildTimeRemaining(endTime), + if (!widget.isSustainMining && endTime != null) ...[ + _buildInfoRow( + '结束时间', + formatter.format(endTime), + ), + const SizedBox(height: 16), + _buildTimeRemaining(endTime), + ], ], ), ),