云算力平台windows桌面应用
This commit is contained in:
349
lib/core/client_core.dart
Normal file
349
lib/core/client_core.dart
Normal file
@@ -0,0 +1,349 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'mining_task_info.dart';
|
||||
import '../models/client_status.dart' as ui;
|
||||
|
||||
/// 客户端核心 - 实现与服务器通信、心跳、挖矿管理等核心功能
|
||||
class ClientCore {
|
||||
static final ClientCore _instance = ClientCore._internal();
|
||||
factory ClientCore() => _instance;
|
||||
ClientCore._internal();
|
||||
|
||||
final Logger _logger = Logger('ClientCore');
|
||||
|
||||
Socket? _socket;
|
||||
String? _serverUrl;
|
||||
String? _auth;
|
||||
String? _machineCode;
|
||||
bool _isConnected = false;
|
||||
DateTime? _lastPingTime;
|
||||
Timer? _heartbeatTimer;
|
||||
StreamController<String>? _logController;
|
||||
|
||||
// 需要GPU和挖矿软件信息用于认证
|
||||
Map<String, dynamic>? _gpusInfo;
|
||||
List<String>? _miningSofts;
|
||||
|
||||
// 状态回调
|
||||
Function(ui.ClientStatus)? onStatusChanged;
|
||||
Function(MiningTaskInfo?)? onMiningTaskChanged;
|
||||
|
||||
Stream<String> get logStream => _logController?.stream ?? const Stream.empty();
|
||||
bool get isConnected => _isConnected;
|
||||
|
||||
/// 更新系统信息(用于后台异步加载后更新)
|
||||
void setSystemInfo(Map<String, dynamic> gpusInfo, List<String> miningSofts) {
|
||||
_gpusInfo = gpusInfo;
|
||||
_miningSofts = miningSofts;
|
||||
// 如果已连接,重新发送认证消息更新服务器
|
||||
if (_isConnected) {
|
||||
_sendMachineCode();
|
||||
}
|
||||
}
|
||||
|
||||
/// 初始化客户端
|
||||
Future<bool> initialize({
|
||||
required String serverUrl,
|
||||
required String auth,
|
||||
required String machineCode,
|
||||
Map<String, dynamic>? gpusInfo,
|
||||
List<String>? miningSofts,
|
||||
}) async {
|
||||
_serverUrl = serverUrl;
|
||||
_auth = auth;
|
||||
_machineCode = machineCode;
|
||||
_gpusInfo = gpusInfo;
|
||||
_miningSofts = miningSofts;
|
||||
_logController = StreamController<String>.broadcast();
|
||||
|
||||
try {
|
||||
await _connect();
|
||||
// 注意:不在这里发送身份认证,等待机器码获取完成后再发送
|
||||
_startHeartbeat();
|
||||
return true;
|
||||
} catch (e) {
|
||||
_logger.severe('初始化失败: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 连接到服务器
|
||||
Future<void> _connect() async {
|
||||
if (_serverUrl == null) {
|
||||
throw Exception('服务器地址未设置');
|
||||
}
|
||||
|
||||
try {
|
||||
final parts = _serverUrl!.split(':');
|
||||
if (parts.length != 2) {
|
||||
throw Exception('服务器地址格式错误');
|
||||
}
|
||||
|
||||
final host = parts[0];
|
||||
final port = int.parse(parts[1]);
|
||||
|
||||
_socket = await Socket.connect(host, port, timeout: const Duration(seconds: 10));
|
||||
_isConnected = true;
|
||||
_log('连接到服务器成功: $_serverUrl');
|
||||
|
||||
// 注意:不在这里发送身份认证,等待机器码获取完成后再发送
|
||||
// 身份认证消息将在机器码获取完成后通过 sendMachineCode() 发送
|
||||
|
||||
// 开始接收消息
|
||||
_socket!.listen(
|
||||
_onDataReceived,
|
||||
onError: _onError,
|
||||
onDone: _onDone,
|
||||
cancelOnError: false,
|
||||
);
|
||||
|
||||
onStatusChanged?.call(ui.ClientStatus.online);
|
||||
} catch (e) {
|
||||
_isConnected = false;
|
||||
_log('连接失败: $e');
|
||||
onStatusChanged?.call(ui.ClientStatus.offline);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// 更新机器码并发送身份认证(等待硬盘身份码获取完成后调用)
|
||||
void updateMachineCode(String machineCode, Map<String, dynamic> gpusInfo, List<String> miningSofts) {
|
||||
_machineCode = machineCode;
|
||||
_gpusInfo = gpusInfo;
|
||||
_miningSofts = miningSofts;
|
||||
}
|
||||
|
||||
/// 发送身份认证消息(公开方法,供外部调用)
|
||||
void sendMachineCode() {
|
||||
_sendMachineCode();
|
||||
}
|
||||
|
||||
/// 发送机器码认证消息(内部方法)
|
||||
void _sendMachineCode() {
|
||||
if (_auth == null || _machineCode == null) {
|
||||
_log('身份信息未设置');
|
||||
return;
|
||||
}
|
||||
|
||||
// 使用 身份信息::硬盘身份码 格式
|
||||
final msg = {
|
||||
'id': '$_auth::$_machineCode',
|
||||
'method': 'auth.machineCode',
|
||||
'params': {
|
||||
'gpus': _gpusInfo ?? {},
|
||||
'miningsofts': _miningSofts ?? [],
|
||||
},
|
||||
};
|
||||
|
||||
_sendMessage(msg);
|
||||
_log('发送身份认证消息: $_auth::$_machineCode');
|
||||
}
|
||||
|
||||
/// 发送消息到服务器
|
||||
void _sendMessage(Map<String, dynamic> message) {
|
||||
if (!_isConnected || _socket == null) {
|
||||
_log('连接未建立,无法发送消息');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final jsonStr = jsonEncode(message);
|
||||
_socket!.add(utf8.encode('$jsonStr\n'));
|
||||
} catch (e) {
|
||||
_log('发送消息失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 接收数据
|
||||
void _onDataReceived(List<int> data) {
|
||||
try {
|
||||
final message = utf8.decode(data);
|
||||
final lines = message.split('\n').where((line) => line.trim().isNotEmpty);
|
||||
|
||||
for (final line in lines) {
|
||||
_handleMessage(line);
|
||||
}
|
||||
} catch (e) {
|
||||
_log('处理接收数据失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 处理接收到的消息
|
||||
void _handleMessage(String messageJson) {
|
||||
try {
|
||||
final msg = jsonDecode(messageJson) as Map<String, dynamic>;
|
||||
final method = msg['method'] as String?;
|
||||
|
||||
_log('收到消息: $method');
|
||||
|
||||
if (method == 'ping') {
|
||||
_handlePing(msg);
|
||||
} else if (method == 'mining.req') {
|
||||
_handleMiningRequest(msg);
|
||||
} else if (method == 'mining.end') {
|
||||
_handleMiningEnd(msg);
|
||||
}
|
||||
} catch (e) {
|
||||
_log('处理消息失败: $e, 原始数据: $messageJson');
|
||||
}
|
||||
}
|
||||
|
||||
/// 处理 ping 消息
|
||||
void _handlePing(Map<String, dynamic> msg) {
|
||||
_lastPingTime = DateTime.now();
|
||||
|
||||
// 回复 pong
|
||||
final pongMsg = {
|
||||
'id': msg['id'],
|
||||
'method': 'pong',
|
||||
'params': null,
|
||||
};
|
||||
_sendMessage(pongMsg);
|
||||
}
|
||||
|
||||
/// 处理挖矿请求
|
||||
void _handleMiningRequest(Map<String, dynamic> msg) {
|
||||
try {
|
||||
final params = msg['params'] as Map<String, dynamic>?;
|
||||
if (params == null) {
|
||||
_sendMiningResponse(msg['id'] as String, false, '参数为空');
|
||||
return;
|
||||
}
|
||||
|
||||
// 注意:miner 需要从配置中获取,这里先使用默认值
|
||||
params['miner'] = params['miner'] ?? 'lolminer';
|
||||
|
||||
final task = MiningTaskInfo.fromJson(params);
|
||||
onMiningTaskChanged?.call(task);
|
||||
|
||||
// 启动挖矿软件由 ClientProvider 处理
|
||||
// 这里只负责响应成功
|
||||
final respData = {
|
||||
'coin': task.coin,
|
||||
'algo': task.algo,
|
||||
'pool': task.pool,
|
||||
'pool_url': task.poolUrl,
|
||||
'worker_id': task.workerId,
|
||||
'wallet_address': task.walletAddress,
|
||||
'watch_url': '',
|
||||
};
|
||||
|
||||
_sendMiningResponse(msg['id'] as String, true, respData);
|
||||
} catch (e) {
|
||||
_log('处理挖矿请求失败: $e');
|
||||
_sendMiningResponse(msg['id'] as String, false, e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/// 发送挖矿响应
|
||||
void _sendMiningResponse(String id, bool success, dynamic data) {
|
||||
final resp = {
|
||||
'id': id,
|
||||
'method': 'mining.resp',
|
||||
'params': success ? data : data.toString(),
|
||||
};
|
||||
_sendMessage(resp);
|
||||
}
|
||||
|
||||
/// 处理挖矿结束消息
|
||||
void _handleMiningEnd(Map<String, dynamic> msg) {
|
||||
_log('收到挖矿结束消息');
|
||||
// 通知 ClientProvider 停止挖矿(通过回调实现)
|
||||
onMiningTaskChanged?.call(null);
|
||||
}
|
||||
|
||||
/// 错误处理
|
||||
void _onError(dynamic error) {
|
||||
// 检查是否正在停止,避免在停止过程中执行重连
|
||||
if (_socket == null) {
|
||||
return; // 已经停止,不执行后续操作
|
||||
}
|
||||
|
||||
_log('连接错误: $error');
|
||||
_isConnected = false;
|
||||
onStatusChanged?.call(ui.ClientStatus.offline);
|
||||
_reconnect();
|
||||
}
|
||||
|
||||
/// 连接关闭
|
||||
void _onDone() {
|
||||
// 检查是否正在停止,避免在停止过程中执行重连
|
||||
if (_socket == null) {
|
||||
return; // 已经停止,不执行后续操作
|
||||
}
|
||||
|
||||
_log('连接已关闭');
|
||||
_isConnected = false;
|
||||
onStatusChanged?.call(ui.ClientStatus.offline);
|
||||
_reconnect();
|
||||
}
|
||||
|
||||
/// 重连
|
||||
void _reconnect() {
|
||||
Future.delayed(const Duration(seconds: 5), () {
|
||||
// 检查是否正在停止或已停止
|
||||
if (_socket == null || _logController == null) {
|
||||
return; // 已经停止,不执行重连
|
||||
}
|
||||
|
||||
if (!_isConnected) {
|
||||
_log('尝试重新连接...');
|
||||
_connect().catchError((e) {
|
||||
_log('重连失败: $e');
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// 启动心跳检查
|
||||
void _startHeartbeat() {
|
||||
_heartbeatTimer?.cancel();
|
||||
_heartbeatTimer = Timer.periodic(const Duration(seconds: 30), (timer) {
|
||||
if (_lastPingTime != null) {
|
||||
final duration = DateTime.now().difference(_lastPingTime!);
|
||||
if (duration.inMinutes > 60) {
|
||||
_log('超过60分钟未收到心跳,连接可能已断开');
|
||||
_isConnected = false;
|
||||
onStatusChanged?.call(ui.ClientStatus.offline);
|
||||
_reconnect();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _log(String message) {
|
||||
final logMsg = '[${DateTime.now().toString()}] $message';
|
||||
_logger.info(logMsg);
|
||||
// 检查 controller 是否已关闭,避免向已关闭的 controller 添加事件
|
||||
try {
|
||||
if (_logController != null && !_logController!.isClosed) {
|
||||
_logController!.add(logMsg);
|
||||
}
|
||||
} catch (e) {
|
||||
// 忽略已关闭的 controller 错误
|
||||
}
|
||||
}
|
||||
|
||||
/// 停止客户端
|
||||
void stop() {
|
||||
_heartbeatTimer?.cancel();
|
||||
_heartbeatTimer = null;
|
||||
|
||||
// 先取消 socket 监听,避免 onDone 回调在关闭 controller 后执行
|
||||
_socket?.destroy();
|
||||
_socket = null;
|
||||
|
||||
// 延迟关闭 logController,确保所有回调都已完成
|
||||
Future.microtask(() {
|
||||
if (_logController != null && !_logController!.isClosed) {
|
||||
_logController!.close();
|
||||
}
|
||||
_logController = null;
|
||||
});
|
||||
|
||||
_isConnected = false;
|
||||
onStatusChanged?.call(ui.ClientStatus.offline);
|
||||
}
|
||||
}
|
||||
185
lib/core/database.dart
Normal file
185
lib/core/database.dart
Normal file
@@ -0,0 +1,185 @@
|
||||
import 'dart:async';
|
||||
// import 'dart:io';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
|
||||
import 'mining_task_info.dart';
|
||||
import '../utils/path_utils.dart';
|
||||
|
||||
/// 数据库管理服务
|
||||
class DatabaseService {
|
||||
static final DatabaseService _instance = DatabaseService._internal();
|
||||
factory DatabaseService() => _instance;
|
||||
DatabaseService._internal();
|
||||
|
||||
final Logger _logger = Logger('DatabaseService');
|
||||
Database? _database;
|
||||
|
||||
/// 初始化数据库
|
||||
Future<void> initialize() async {
|
||||
try {
|
||||
// Windows/Desktop: 使用 sqflite_common_ffi
|
||||
sqfliteFfiInit();
|
||||
databaseFactory = databaseFactoryFfi;
|
||||
|
||||
// 与 Go 版一致,尽量落到 ./bin/mining_task.db
|
||||
final dbPath = PathUtils.binFile('mining_task.db');
|
||||
|
||||
_database = await databaseFactory.openDatabase(
|
||||
dbPath,
|
||||
options: OpenDatabaseOptions(
|
||||
version: 1,
|
||||
onCreate: (db, version) async {
|
||||
await db.execute('''
|
||||
CREATE TABLE mining_tasks (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
coin TEXT NOT NULL,
|
||||
algo TEXT NOT NULL,
|
||||
pool TEXT NOT NULL,
|
||||
pool_url TEXT NOT NULL,
|
||||
wallet_address TEXT NOT NULL,
|
||||
worker_id TEXT NOT NULL,
|
||||
pool_user TEXT,
|
||||
wallet_mining INTEGER NOT NULL,
|
||||
end_timestamp INTEGER NOT NULL,
|
||||
miner TEXT NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
)
|
||||
''');
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
_logger.info('数据库初始化成功');
|
||||
} catch (e) {
|
||||
_logger.severe('数据库初始化失败: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// 插入挖矿任务
|
||||
Future<int> insertMiningTask(MiningTaskInfo task) async {
|
||||
if (_database == null) {
|
||||
await initialize();
|
||||
}
|
||||
|
||||
try {
|
||||
final now = DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
||||
return await _database!.insert(
|
||||
'mining_tasks',
|
||||
{
|
||||
'coin': task.coin,
|
||||
'algo': task.algo,
|
||||
'pool': task.pool,
|
||||
'pool_url': task.poolUrl,
|
||||
'wallet_address': task.walletAddress,
|
||||
'worker_id': task.workerId,
|
||||
'pool_user': task.poolUser,
|
||||
'wallet_mining': task.walletMining ? 1 : 0,
|
||||
'end_timestamp': task.endTimestamp,
|
||||
'miner': task.miner,
|
||||
'status': 'running',
|
||||
'created_at': now,
|
||||
'updated_at': now,
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
_logger.severe('插入挖矿任务失败: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// 完成挖矿任务
|
||||
Future<void> finishMiningTask(int taskId) async {
|
||||
if (_database == null) {
|
||||
await initialize();
|
||||
}
|
||||
|
||||
try {
|
||||
final now = DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
||||
await _database!.update(
|
||||
'mining_tasks',
|
||||
{
|
||||
'status': 'finished',
|
||||
'updated_at': now,
|
||||
},
|
||||
where: 'id = ?',
|
||||
whereArgs: [taskId],
|
||||
);
|
||||
} catch (e) {
|
||||
_logger.severe('完成挖矿任务失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 加载未完成的挖矿任务
|
||||
Future<MiningTaskInfo?> loadUnfinishedTask() async {
|
||||
if (_database == null) {
|
||||
await initialize();
|
||||
}
|
||||
|
||||
try {
|
||||
final results = await _database!.query(
|
||||
'mining_tasks',
|
||||
where: 'status = ?',
|
||||
whereArgs: ['running'],
|
||||
orderBy: 'created_at DESC',
|
||||
limit: 1,
|
||||
);
|
||||
|
||||
if (results.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final row = results.first;
|
||||
final currentTime = DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
||||
final endTimestamp = row['end_timestamp'] as int;
|
||||
|
||||
if (currentTime >= endTimestamp) {
|
||||
// 任务已过期,标记为完成
|
||||
await finishMiningTask(row['id'] as int);
|
||||
return null;
|
||||
}
|
||||
|
||||
return MiningTaskInfo(
|
||||
coin: row['coin'] as String,
|
||||
algo: row['algo'] as String,
|
||||
pool: row['pool'] as String,
|
||||
poolUrl: row['pool_url'] as String,
|
||||
walletAddress: row['wallet_address'] as String,
|
||||
workerId: row['worker_id'] as String,
|
||||
poolUser: row['pool_user'] as String?,
|
||||
walletMining: (row['wallet_mining'] as int) == 1,
|
||||
endTimestamp: endTimestamp,
|
||||
miner: row['miner'] as String,
|
||||
);
|
||||
} catch (e) {
|
||||
_logger.severe('加载挖矿任务失败: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取任务历史
|
||||
Future<List<Map<String, dynamic>>> getTaskHistory({int limit = 100}) async {
|
||||
if (_database == null) {
|
||||
await initialize();
|
||||
}
|
||||
|
||||
try {
|
||||
return await _database!.query(
|
||||
'mining_tasks',
|
||||
orderBy: 'created_at DESC',
|
||||
limit: limit,
|
||||
);
|
||||
} catch (e) {
|
||||
_logger.severe('获取任务历史失败: $e');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// 关闭数据库
|
||||
Future<void> close() async {
|
||||
await _database?.close();
|
||||
_database = null;
|
||||
}
|
||||
}
|
||||
214
lib/core/mining_manager.dart
Normal file
214
lib/core/mining_manager.dart
Normal file
@@ -0,0 +1,214 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'mining_task_info.dart';
|
||||
|
||||
/// 挖矿管理器 - 管理挖矿软件的启动、停止
|
||||
class MiningManager {
|
||||
static final MiningManager _instance = MiningManager._internal();
|
||||
factory MiningManager() => _instance;
|
||||
MiningManager._internal();
|
||||
|
||||
final Logger _logger = Logger('MiningManager');
|
||||
Process? _currentProcess;
|
||||
MiningTaskInfo? _currentTask;
|
||||
Timer? _taskMonitor;
|
||||
final StreamController<String> _minerLogController = StreamController<String>.broadcast();
|
||||
|
||||
/// 启动挖矿
|
||||
Future<bool> startMining(MiningTaskInfo task, MiningConfig config) async {
|
||||
if (_currentProcess != null) {
|
||||
_logger.warning('已有挖矿任务在运行');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
final minerPath = _getMinerPath(task.miner, config);
|
||||
if (minerPath == null) {
|
||||
_logger.severe('挖矿软件路径未配置: ${task.miner}');
|
||||
return false;
|
||||
}
|
||||
|
||||
final args = _buildMinerArgs(task, config);
|
||||
String executable;
|
||||
|
||||
switch (task.miner.toLowerCase()) {
|
||||
case 'lolminer':
|
||||
executable = Platform.isWindows
|
||||
? '${minerPath}\\lolMiner.exe'
|
||||
: '$minerPath/lolMiner';
|
||||
break;
|
||||
case 'rigel':
|
||||
executable = Platform.isWindows
|
||||
? '${minerPath}\\rigel.exe'
|
||||
: '$minerPath/rigel';
|
||||
break;
|
||||
case 'bzminer':
|
||||
executable = Platform.isWindows
|
||||
? '${minerPath}\\bzminer.exe'
|
||||
: '$minerPath/bzminer';
|
||||
break;
|
||||
default:
|
||||
throw Exception('不支持的挖矿软件: ${task.miner}');
|
||||
}
|
||||
|
||||
// 确保可执行文件存在
|
||||
final executableFile = File(executable);
|
||||
if (!await executableFile.exists()) {
|
||||
throw Exception('挖矿软件不存在: $executable');
|
||||
}
|
||||
|
||||
_logger.info('启动挖矿: $executable ${args.join(' ')}');
|
||||
|
||||
_currentProcess = await Process.start(
|
||||
executable,
|
||||
args,
|
||||
workingDirectory: minerPath,
|
||||
mode: ProcessStartMode.normal, // 改为 normal 以捕获输出
|
||||
);
|
||||
|
||||
_currentTask = task;
|
||||
_startTaskMonitor(task);
|
||||
_startLogCapture();
|
||||
|
||||
_logger.info('挖矿已启动 (PID: ${_currentProcess!.pid})');
|
||||
return true;
|
||||
} catch (e) {
|
||||
_logger.severe('启动挖矿失败: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 停止挖矿
|
||||
Future<void> stopMining() async {
|
||||
_taskMonitor?.cancel();
|
||||
_taskMonitor = null;
|
||||
|
||||
if (_currentProcess != null) {
|
||||
try {
|
||||
_currentProcess!.kill();
|
||||
await _currentProcess!.exitCode;
|
||||
_logger.info('挖矿已停止');
|
||||
} catch (e) {
|
||||
_logger.severe('停止挖矿失败: $e');
|
||||
} finally {
|
||||
_currentProcess = null;
|
||||
_currentTask = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取挖矿软件路径
|
||||
String? _getMinerPath(String miner, MiningConfig config) {
|
||||
switch (miner.toLowerCase()) {
|
||||
case 'lolminer':
|
||||
return config.lolMinerPath;
|
||||
case 'rigel':
|
||||
return config.rigelPath;
|
||||
case 'bzminer':
|
||||
return config.bzMinerPath;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// 构建挖矿软件参数
|
||||
List<String> _buildMinerArgs(MiningTaskInfo task, MiningConfig config) {
|
||||
final address = task.walletMining ? task.walletAddress : (task.poolUser ?? task.walletAddress);
|
||||
|
||||
switch (task.miner.toLowerCase()) {
|
||||
case 'lolminer':
|
||||
return [
|
||||
'--algo', task.coin,
|
||||
'--pool', task.poolUrl,
|
||||
'--user', '$address.${task.workerId}',
|
||||
];
|
||||
case 'rigel':
|
||||
return [
|
||||
'--no-watchdog',
|
||||
'-a', task.algo.toLowerCase(),
|
||||
'-o', task.poolUrl,
|
||||
'-u', address,
|
||||
'-w', task.workerId,
|
||||
'--log-file', 'logs/miner.log',
|
||||
];
|
||||
case 'bzminer':
|
||||
return [
|
||||
'-a', task.coin,
|
||||
'-w', '$address.${task.workerId}',
|
||||
'-p', task.poolUrl,
|
||||
];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// 启动任务监控
|
||||
void _startTaskMonitor(MiningTaskInfo task) {
|
||||
_taskMonitor?.cancel();
|
||||
|
||||
final endTime = DateTime.fromMillisecondsSinceEpoch(task.endTimestamp * 1000);
|
||||
final now = DateTime.now();
|
||||
|
||||
if (endTime.isBefore(now)) {
|
||||
// 任务已过期,立即停止
|
||||
stopMining();
|
||||
return;
|
||||
}
|
||||
|
||||
final duration = endTime.difference(now);
|
||||
_taskMonitor = Timer(duration, () {
|
||||
_logger.info('挖矿任务已到期,自动停止');
|
||||
stopMining();
|
||||
});
|
||||
}
|
||||
|
||||
/// 开始捕获挖矿进程输出
|
||||
void _startLogCapture() {
|
||||
if (_currentProcess == null) return;
|
||||
|
||||
_currentProcess!.stdout
|
||||
.transform(const SystemEncoding().decoder)
|
||||
.transform(const LineSplitter())
|
||||
.listen((line) {
|
||||
_minerLogController.add('[${DateTime.now().toString().substring(11, 19)}] $line');
|
||||
});
|
||||
|
||||
_currentProcess!.stderr
|
||||
.transform(const SystemEncoding().decoder)
|
||||
.transform(const LineSplitter())
|
||||
.listen((line) {
|
||||
_minerLogController.add('[${DateTime.now().toString().substring(11, 19)}] [ERROR] $line');
|
||||
});
|
||||
}
|
||||
|
||||
/// 获取挖矿日志流
|
||||
Stream<String> get minerLogStream => _minerLogController.stream;
|
||||
|
||||
MiningTaskInfo? get currentTask => _currentTask;
|
||||
bool get isMining => _currentProcess != null;
|
||||
|
||||
/// 清理资源
|
||||
void dispose() {
|
||||
_minerLogController.close();
|
||||
}
|
||||
}
|
||||
|
||||
class MiningConfig {
|
||||
final String? lolMinerPath;
|
||||
final String? rigelPath;
|
||||
final String? bzMinerPath;
|
||||
final bool proxyEnabled;
|
||||
final String? serverUrl;
|
||||
final String? updateUrl;
|
||||
|
||||
MiningConfig({
|
||||
this.lolMinerPath,
|
||||
this.rigelPath,
|
||||
this.bzMinerPath,
|
||||
this.proxyEnabled = false,
|
||||
this.serverUrl,
|
||||
this.updateUrl,
|
||||
});
|
||||
}
|
||||
40
lib/core/mining_task_info.dart
Normal file
40
lib/core/mining_task_info.dart
Normal file
@@ -0,0 +1,40 @@
|
||||
class MiningTaskInfo {
|
||||
final String coin;
|
||||
final String algo;
|
||||
final String pool;
|
||||
final String poolUrl;
|
||||
final String walletAddress;
|
||||
final String workerId;
|
||||
final String? poolUser;
|
||||
final bool walletMining;
|
||||
final int endTimestamp;
|
||||
final String miner; // 从配置或请求中获取
|
||||
|
||||
MiningTaskInfo({
|
||||
required this.coin,
|
||||
required this.algo,
|
||||
required this.pool,
|
||||
required this.poolUrl,
|
||||
required this.walletAddress,
|
||||
required this.workerId,
|
||||
this.poolUser,
|
||||
required this.walletMining,
|
||||
required this.endTimestamp,
|
||||
required this.miner,
|
||||
});
|
||||
|
||||
factory MiningTaskInfo.fromJson(Map<String, dynamic> json) {
|
||||
return MiningTaskInfo(
|
||||
coin: json['coin'] as String,
|
||||
algo: json['algo'] as String,
|
||||
pool: json['pool'] as String,
|
||||
poolUrl: json['pool_url'] as String,
|
||||
walletAddress: json['wallet_address'] as String,
|
||||
workerId: json['worker_id'] as String,
|
||||
poolUser: json['pool_user'] as String?,
|
||||
walletMining: json['wallet_mining'] as bool? ?? true,
|
||||
endTimestamp: json['end_timestamp'] as int,
|
||||
miner: json['miner'] as String? ?? 'lolminer', // 默认值
|
||||
);
|
||||
}
|
||||
}
|
||||
232
lib/core/sustain_miner.dart
Normal file
232
lib/core/sustain_miner.dart
Normal file
@@ -0,0 +1,232 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:logging/logging.dart';
|
||||
// import 'package:ini/ini.dart';
|
||||
import 'mining_manager.dart';
|
||||
import 'mining_task_info.dart';
|
||||
import '../utils/ini_utils.dart';
|
||||
import '../utils/path_utils.dart';
|
||||
|
||||
/// 持续挖矿管理器
|
||||
class SustainMiner {
|
||||
static final SustainMiner _instance = SustainMiner._internal();
|
||||
factory SustainMiner() => _instance;
|
||||
SustainMiner._internal();
|
||||
|
||||
final Logger _logger = Logger('SustainMiner');
|
||||
final MiningManager _miningManager = MiningManager();
|
||||
|
||||
SustainMiningConfig? _config;
|
||||
MiningConfig? _miningConfig;
|
||||
Timer? _monitorTimer;
|
||||
bool _isRunning = false;
|
||||
bool _isPaused = false;
|
||||
|
||||
/// 加载配置
|
||||
Future<bool> loadConfig(MiningConfig miningConfig) async {
|
||||
try {
|
||||
_miningConfig = miningConfig;
|
||||
|
||||
final configFile = File(PathUtils.binFile('mining.windows.conf'));
|
||||
if (!await configFile.exists()) {
|
||||
_logger.warning('配置文件不存在');
|
||||
return false;
|
||||
}
|
||||
|
||||
final content = await configFile.readAsString();
|
||||
final config = parseIni(content);
|
||||
|
||||
// ini 2.x: 使用 get(section, option)
|
||||
final enabledStr = (config.get('sustain', 'enabled') ?? '').toLowerCase();
|
||||
final enabled = enabledStr == 'true';
|
||||
|
||||
if (!enabled) {
|
||||
_logger.info('持续挖矿未启用');
|
||||
return false;
|
||||
}
|
||||
|
||||
_config = SustainMiningConfig(
|
||||
enabled: enabled,
|
||||
algo: _trimQuotes(config.get('sustain', 'algo') ?? ''),
|
||||
coin: _trimQuotes(config.get('sustain', 'coin') ?? ''),
|
||||
miner: _trimQuotes(config.get('sustain', 'miner') ?? ''),
|
||||
poolUrl: _trimQuotes(config.get('sustain', 'pool_url') ?? ''),
|
||||
wallet: _trimQuotes(config.get('sustain', 'wallet') ?? ''),
|
||||
workerId: _trimQuotes(config.get('sustain', 'worker_id') ?? ''),
|
||||
poolUser: _trimQuotes(config.get('sustain', 'pool_user') ?? ''),
|
||||
walletMining: (config.get('sustain', 'wallet_mining') ?? '').toLowerCase() == 'true',
|
||||
);
|
||||
|
||||
// 验证配置
|
||||
if (_config!.algo.isEmpty ||
|
||||
_config!.coin.isEmpty ||
|
||||
_config!.miner.isEmpty ||
|
||||
_config!.poolUrl.isEmpty ||
|
||||
_config!.wallet.isEmpty ||
|
||||
_config!.workerId.isEmpty) {
|
||||
_logger.severe('持续挖矿配置不完整');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 验证挖矿软件路径
|
||||
final minerPath = _getMinerPath(_config!.miner);
|
||||
if (minerPath == null || minerPath.isEmpty) {
|
||||
_logger.severe('${_config!.miner} 路径未配置');
|
||||
return false;
|
||||
}
|
||||
|
||||
_logger.info('持续挖矿配置加载成功: 算法=${_config!.algo}, 币种=${_config!.coin}, 挖矿软件=${_config!.miner}');
|
||||
return true;
|
||||
} catch (e) {
|
||||
_logger.severe('加载持续挖矿配置失败: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 启动持续挖矿
|
||||
Future<void> start() async {
|
||||
if (_config == null || !_config!.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_isRunning) {
|
||||
_logger.info('持续挖矿已在运行中');
|
||||
return;
|
||||
}
|
||||
|
||||
if (_miningManager.isMining) {
|
||||
_logger.info('当前有挖矿任务,等待任务结束后启动持续挖矿');
|
||||
return;
|
||||
}
|
||||
|
||||
_isRunning = true;
|
||||
_isPaused = false;
|
||||
|
||||
_logger.info('启动持续挖矿...');
|
||||
await _startMining();
|
||||
}
|
||||
|
||||
/// 停止持续挖矿
|
||||
Future<void> stop() async {
|
||||
_isRunning = false;
|
||||
_monitorTimer?.cancel();
|
||||
_monitorTimer = null;
|
||||
|
||||
if (_miningManager.isMining && _miningManager.currentTask == null) {
|
||||
// 只有持续挖矿任务在运行时才停止
|
||||
await _miningManager.stopMining();
|
||||
}
|
||||
|
||||
_logger.info('持续挖矿已停止');
|
||||
}
|
||||
|
||||
/// 暂停持续挖矿
|
||||
Future<void> pause() async {
|
||||
if (!_isRunning || _isPaused) {
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.info('暂停持续挖矿(有新任务)...');
|
||||
_isPaused = true;
|
||||
|
||||
if (_miningManager.isMining && _miningManager.currentTask == null) {
|
||||
await _miningManager.stopMining();
|
||||
}
|
||||
}
|
||||
|
||||
/// 恢复持续挖矿
|
||||
Future<void> resume() async {
|
||||
if (!_isRunning || !_isPaused) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_miningManager.isMining) {
|
||||
_logger.info('当前有挖矿任务,等待任务结束后恢复持续挖矿');
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.info('恢复持续挖矿...');
|
||||
_isPaused = false;
|
||||
await _startMining();
|
||||
}
|
||||
|
||||
/// 启动挖矿
|
||||
Future<void> _startMining() async {
|
||||
if (_config == null || _isPaused || _miningConfig == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_miningManager.isMining) {
|
||||
return;
|
||||
}
|
||||
|
||||
final task = MiningTaskInfo(
|
||||
coin: _config!.coin,
|
||||
algo: _config!.algo,
|
||||
pool: '',
|
||||
poolUrl: _config!.poolUrl,
|
||||
walletAddress: _config!.wallet,
|
||||
workerId: _config!.workerId,
|
||||
poolUser: _config!.poolUser,
|
||||
walletMining: _config!.walletMining,
|
||||
endTimestamp: DateTime.now().add(const Duration(days: 365)).millisecondsSinceEpoch ~/ 1000, // 持续挖矿设置很长的结束时间
|
||||
miner: _config!.miner,
|
||||
);
|
||||
|
||||
await _miningManager.startMining(task, _miningConfig!);
|
||||
}
|
||||
|
||||
String _trimQuotes(String value) {
|
||||
var v = value.trim();
|
||||
if (v.length >= 2) {
|
||||
final first = v[0];
|
||||
final last = v[v.length - 1];
|
||||
if ((first == '"' && last == '"') || (first == "'" && last == "'")) {
|
||||
v = v.substring(1, v.length - 1);
|
||||
}
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
String? _getMinerPath(String miner) {
|
||||
if (_miningConfig == null) return null;
|
||||
|
||||
switch (miner.toLowerCase()) {
|
||||
case 'lolminer':
|
||||
return _miningConfig!.lolMinerPath;
|
||||
case 'rigel':
|
||||
return _miningConfig!.rigelPath;
|
||||
case 'bzminer':
|
||||
return _miningConfig!.bzMinerPath;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
bool get isRunning => _isRunning;
|
||||
bool get isPaused => _isPaused;
|
||||
}
|
||||
|
||||
class SustainMiningConfig {
|
||||
final bool enabled;
|
||||
final String algo;
|
||||
final String coin;
|
||||
final String miner;
|
||||
final String poolUrl;
|
||||
final String wallet;
|
||||
final String workerId;
|
||||
final String poolUser;
|
||||
final bool walletMining;
|
||||
|
||||
SustainMiningConfig({
|
||||
required this.enabled,
|
||||
required this.algo,
|
||||
required this.coin,
|
||||
required this.miner,
|
||||
required this.poolUrl,
|
||||
required this.wallet,
|
||||
required this.workerId,
|
||||
required this.poolUser,
|
||||
required this.walletMining,
|
||||
});
|
||||
}
|
||||
198
lib/core/system_info.dart
Normal file
198
lib/core/system_info.dart
Normal file
@@ -0,0 +1,198 @@
|
||||
import 'dart:io';
|
||||
import 'package:logging/logging.dart';
|
||||
import '../models/client_status.dart';
|
||||
import '../utils/path_utils.dart';
|
||||
|
||||
/// 系统信息获取服务
|
||||
class SystemInfoService {
|
||||
static final SystemInfoService _instance = SystemInfoService._internal();
|
||||
factory SystemInfoService() => _instance;
|
||||
SystemInfoService._internal();
|
||||
|
||||
final Logger _logger = Logger('SystemInfoService');
|
||||
|
||||
/// 获取机器码(使用硬盘序列号)
|
||||
Future<String> getMachineCode() async {
|
||||
try {
|
||||
if (Platform.isWindows) {
|
||||
// 延迟执行,避免启动时崩溃
|
||||
await Future.delayed(const Duration(seconds: 3));
|
||||
|
||||
// 使用 wmic 获取硬盘序列号
|
||||
try {
|
||||
final result = await Process.run(
|
||||
'wmic',
|
||||
['diskdrive', 'get', 'serialnumber'],
|
||||
runInShell: true,
|
||||
);
|
||||
|
||||
if (result.exitCode == 0) {
|
||||
final output = result.stdout.toString();
|
||||
final lines = output.split('\n');
|
||||
|
||||
// 查找序列号(跳过标题行)
|
||||
for (final line in lines) {
|
||||
final trimmed = line.trim();
|
||||
if (trimmed.isNotEmpty &&
|
||||
!trimmed.toLowerCase().contains('serialnumber') &&
|
||||
trimmed != '') {
|
||||
// 提取序列号(去除空格)
|
||||
final serial = trimmed.replaceAll(RegExp(r'\s+'), '');
|
||||
if (serial.isNotEmpty) {
|
||||
_logger.info('获取到硬盘序列号: $serial');
|
||||
return serial;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_logger.warning('wmic 命令执行成功但未找到有效序列号');
|
||||
} catch (e) {
|
||||
_logger.warning('使用 wmic 获取硬盘序列号失败: $e');
|
||||
}
|
||||
|
||||
// 备用方案:使用环境变量生成标识符
|
||||
final computerName = Platform.environment['COMPUTERNAME'] ?? 'UNKNOWN';
|
||||
final userName = Platform.environment['USERNAME'] ?? 'UNKNOWN';
|
||||
final userProfile = Platform.environment['USERPROFILE'] ?? '';
|
||||
|
||||
final identifier = '${computerName}_${userName}_${userProfile.hashCode.abs()}';
|
||||
_logger.info('使用备用标识符作为机器码: $identifier');
|
||||
return identifier;
|
||||
}
|
||||
|
||||
// 非 Windows 系统
|
||||
_logger.warning('无法获取机器码,使用临时标识');
|
||||
return 'UNKNOWN_${DateTime.now().millisecondsSinceEpoch}';
|
||||
} catch (e) {
|
||||
_logger.severe('获取机器码失败: $e');
|
||||
// 不抛出异常,返回临时标识
|
||||
return 'ERROR_${DateTime.now().millisecondsSinceEpoch}';
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取GPU信息(仅在启动时调用一次)
|
||||
Future<List<GPUInfo>> getGPUInfo() async {
|
||||
try {
|
||||
// 使用 nvidia-smi 获取GPU信息
|
||||
final result = await Process.run(
|
||||
'nvidia-smi',
|
||||
['--query-gpu=index,name,memory.total', '--format=csv,noheader,nounits'],
|
||||
runInShell: true,
|
||||
).timeout(
|
||||
const Duration(seconds: 10),
|
||||
onTimeout: () {
|
||||
_logger.warning('nvidia-smi 执行超时');
|
||||
return ProcessResult(0, -1, '', 'timeout');
|
||||
},
|
||||
);
|
||||
|
||||
if (result.exitCode != 0) {
|
||||
_logger.warning('nvidia-smi 执行失败: exitCode=${result.exitCode}, stderr=${result.stderr}');
|
||||
return [];
|
||||
}
|
||||
|
||||
final output = result.stdout.toString().trim();
|
||||
if (output.isEmpty) {
|
||||
_logger.info('nvidia-smi 返回空输出,未检测到GPU');
|
||||
return [];
|
||||
}
|
||||
|
||||
final lines = output.split('\n');
|
||||
final gpus = <GPUInfo>[];
|
||||
|
||||
for (final line in lines) {
|
||||
if (line.trim().isEmpty) continue;
|
||||
|
||||
final parts = line.split(', ');
|
||||
if (parts.length >= 3) {
|
||||
try {
|
||||
final index = int.parse(parts[0].trim());
|
||||
final model = parts[1].trim();
|
||||
final memoryStr = parts[2].trim();
|
||||
final memory = double.tryParse(memoryStr) ?? 0;
|
||||
|
||||
gpus.add(GPUInfo(
|
||||
index: index,
|
||||
brand: 'NVIDIA',
|
||||
model: model,
|
||||
memory: memory,
|
||||
));
|
||||
|
||||
_logger.info('检测到GPU: 索引=$index, 型号=$model, 显存=${memory}MB');
|
||||
} catch (e) {
|
||||
_logger.warning('解析GPU信息失败: $line, 错误: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (gpus.isEmpty) {
|
||||
_logger.info('未检测到GPU');
|
||||
} else {
|
||||
_logger.info('成功获取 ${gpus.length} 个GPU信息');
|
||||
}
|
||||
return gpus;
|
||||
} catch (e) {
|
||||
_logger.warning('获取GPU信息失败: $e');
|
||||
// 不抛出异常,返回空列表,UI会显示"未检测到GPU"
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// 读取版本号(从根目录 bin/version 文件)
|
||||
Future<String> getVersion() async {
|
||||
try {
|
||||
final versionPath = PathUtils.binFile('version');
|
||||
final file = File(versionPath);
|
||||
if (await file.exists()) {
|
||||
final content = await file.readAsString();
|
||||
return content.trim();
|
||||
} else {
|
||||
_logger.warning('版本文件不存在: $versionPath');
|
||||
}
|
||||
} catch (e) {
|
||||
_logger.warning('读取版本号失败: $e');
|
||||
}
|
||||
return '未知版本';
|
||||
}
|
||||
|
||||
/// 读取身份信息(从根目录 bin/auth 文件)
|
||||
Future<String> getAuth() async {
|
||||
try {
|
||||
final authPath = PathUtils.binFile('auth');
|
||||
final file = File(authPath);
|
||||
if (await file.exists()) {
|
||||
final content = await file.readAsString();
|
||||
return content.trim();
|
||||
} else {
|
||||
_logger.warning('身份文件不存在: $authPath');
|
||||
}
|
||||
} catch (e) {
|
||||
_logger.warning('读取身份信息失败: $e');
|
||||
}
|
||||
return '未配置';
|
||||
}
|
||||
|
||||
/// 检查管理员权限(Windows)
|
||||
Future<bool> checkAdminPermission() async {
|
||||
if (!Platform.isWindows) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
// 由于 Process.run 会导致崩溃,这里使用环境变量判断
|
||||
// 如果存在管理员相关的环境变量,可能具有管理员权限
|
||||
// 这是一个简化的判断,可能不准确,但不会崩溃
|
||||
final userProfile = Platform.environment['USERPROFILE'];
|
||||
if (userProfile != null && userProfile.contains('Administrator')) {
|
||||
return true;
|
||||
}
|
||||
// 默认返回 false,表示可能没有管理员权限
|
||||
_logger.info('无法准确检测管理员权限(避免 Process.run 崩溃),返回 false');
|
||||
return false;
|
||||
} catch (e) {
|
||||
_logger.warning('检查管理员权限失败: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user