云算力平台windows桌面应用

This commit is contained in:
lzx
2026-01-22 15:14:27 +08:00
commit 1fe0e54138
52 changed files with 5447 additions and 0 deletions

349
lib/core/client_core.dart Normal file
View 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
View 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;
}
}

View 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,
});
}

View 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
View 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
View 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;
}
}
}