update
This commit is contained in:
@@ -20,8 +20,13 @@ class ClientCore {
|
||||
bool _isConnected = false;
|
||||
DateTime? _lastPingTime;
|
||||
Timer? _heartbeatTimer;
|
||||
Timer? _reconnectTimer;
|
||||
StreamController<String>? _logController;
|
||||
|
||||
// 重连相关
|
||||
int _reconnectAttempts = 0;
|
||||
static const int _maxReconnectAttempts = 5;
|
||||
|
||||
// 需要GPU和挖矿软件信息用于认证
|
||||
Map<String, dynamic>? _gpusInfo;
|
||||
List<String>? _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;
|
||||
|
||||
@@ -15,6 +15,10 @@ class MiningManager {
|
||||
MiningTaskInfo? _currentTask;
|
||||
Timer? _taskMonitor;
|
||||
final StreamController<String> _minerLogController = StreamController<String>.broadcast();
|
||||
final StreamController<void> _processExitController = StreamController<void>.broadcast();
|
||||
|
||||
/// 进程退出事件流
|
||||
Stream<void> get processExitStream => _processExitController.stream;
|
||||
|
||||
/// 启动挖矿
|
||||
Future<bool> 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<String> get minerLogStream => _minerLogController.stream;
|
||||
|
||||
@@ -192,6 +219,7 @@ class MiningManager {
|
||||
/// 清理资源
|
||||
void dispose() {
|
||||
_minerLogController.close();
|
||||
_processExitController.close();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ class SustainMiner {
|
||||
SustainMiningConfig? _config;
|
||||
MiningConfig? _miningConfig;
|
||||
Timer? _monitorTimer;
|
||||
StreamSubscription<void>? _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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<void> restart() async {
|
||||
_clientCore.stop();
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -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<MiningInfoScreen> {
|
||||
|
||||
@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<MiningInfoScreen> {
|
||||
_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),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user