云算力平台windows桌面应用
This commit is contained in:
67
lib/services/config_service.dart
Normal file
67
lib/services/config_service.dart
Normal file
@@ -0,0 +1,67 @@
|
||||
import 'dart:io';
|
||||
// import 'package:ini/ini.dart';
|
||||
import '../core/mining_manager.dart';
|
||||
import '../utils/ini_utils.dart';
|
||||
import '../utils/path_utils.dart';
|
||||
|
||||
class ConfigService {
|
||||
String get _configFile => PathUtils.binFile('mining.windows.conf');
|
||||
|
||||
/// 读取配置文件内容
|
||||
Future<String> readConfig() async {
|
||||
try {
|
||||
final file = File(_configFile);
|
||||
if (await file.exists()) {
|
||||
return await file.readAsString();
|
||||
}
|
||||
} catch (e) {
|
||||
// 静默失败,返回空字符串
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/// 保存配置文件
|
||||
Future<bool> saveConfig(String content) async {
|
||||
try {
|
||||
final file = File(_configFile);
|
||||
await file.writeAsString(content);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 解析配置文件并返回 MiningConfig
|
||||
Future<MiningConfig?> parseMiningConfig() async {
|
||||
try {
|
||||
final content = await readConfig();
|
||||
if (content.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final config = parseIni(content);
|
||||
|
||||
// ini 2.x: 通过 get(section, option) 访问键值
|
||||
final lolMinerPath = (config.get('lolminer', 'path') ?? '').trim();
|
||||
final rigelPath = (config.get('rigel', 'path') ?? '').trim();
|
||||
final bzMinerPath = (config.get('bzminer', 'path') ?? '').trim();
|
||||
final proxyEnabled = (config.get('proxy', 'proxy') ?? '').toLowerCase() == 'true';
|
||||
|
||||
// 读取服务器和更新URL
|
||||
final serverUrl = (config.get('client', 'server_url') ?? '').trim();
|
||||
final updateUrl = (config.get('client', 'update_url') ?? '').trim();
|
||||
|
||||
return MiningConfig(
|
||||
lolMinerPath: lolMinerPath.isEmpty ? null : lolMinerPath,
|
||||
rigelPath: rigelPath.isEmpty ? null : rigelPath,
|
||||
bzMinerPath: bzMinerPath.isEmpty ? null : bzMinerPath,
|
||||
proxyEnabled: proxyEnabled,
|
||||
serverUrl: serverUrl.isEmpty ? null : serverUrl,
|
||||
updateUrl: updateUrl.isEmpty ? null : updateUrl,
|
||||
);
|
||||
} catch (e) {
|
||||
// 静默失败,返回 null
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
91
lib/services/log_service.dart
Normal file
91
lib/services/log_service.dart
Normal file
@@ -0,0 +1,91 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:logging/logging.dart';
|
||||
import '../utils/path_utils.dart';
|
||||
|
||||
/// 日志服务
|
||||
class LogService {
|
||||
static final LogService _instance = LogService._internal();
|
||||
factory LogService() => _instance;
|
||||
LogService._internal();
|
||||
|
||||
final Logger _rootLogger = Logger.root;
|
||||
final StreamController<String> _logController = StreamController<String>.broadcast();
|
||||
IOSink? _logFile;
|
||||
|
||||
Stream<String> get logStream => _logController.stream;
|
||||
|
||||
/// 初始化日志系统
|
||||
Future<void> initialize() async {
|
||||
// 配置日志输出
|
||||
_rootLogger.level = Level.ALL;
|
||||
_rootLogger.onRecord.listen((record) {
|
||||
final message = '[${record.time}] [${record.level.name}] ${record.message}';
|
||||
_logController.add(message);
|
||||
_logToFile(message);
|
||||
});
|
||||
|
||||
// 打开日志文件
|
||||
try {
|
||||
final logDir = Directory(PathUtils.binFile('logs'));
|
||||
if (!await logDir.exists()) {
|
||||
await logDir.create(recursive: true);
|
||||
}
|
||||
|
||||
final logFile = File(PathUtils.binFile('logs/client.log'));
|
||||
_logFile = logFile.openWrite(mode: FileMode.append);
|
||||
} catch (e) {
|
||||
// 静默失败
|
||||
}
|
||||
}
|
||||
|
||||
/// 写入日志文件
|
||||
void _logToFile(String message) {
|
||||
try {
|
||||
if (_logFile != null) {
|
||||
_logFile!.writeln(message);
|
||||
_logFile!.flush();
|
||||
}
|
||||
} catch (e) {
|
||||
// 忽略写入错误,避免崩溃
|
||||
// print('写入日志文件失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取最新日志(限制行数)
|
||||
Future<String> getRecentLogs({int maxLines = 1000}) async {
|
||||
try {
|
||||
final file = File(PathUtils.binFile('logs/client.log'));
|
||||
if (await file.exists()) {
|
||||
final lines = await file.readAsLines();
|
||||
if (lines.length > maxLines) {
|
||||
return lines.sublist(lines.length - maxLines).join('\n');
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
} catch (e) {
|
||||
// 静默失败
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/// 清理日志
|
||||
Future<void> clearLogs() async {
|
||||
try {
|
||||
final file = File(PathUtils.binFile('logs/client.log'));
|
||||
if (await file.exists()) {
|
||||
await file.writeAsString('');
|
||||
_logController.add('日志已清理');
|
||||
}
|
||||
} catch (e) {
|
||||
// 静默失败
|
||||
}
|
||||
}
|
||||
|
||||
/// 关闭日志服务
|
||||
Future<void> close() async {
|
||||
await _logFile?.flush();
|
||||
await _logFile?.close();
|
||||
await _logController.close();
|
||||
}
|
||||
}
|
||||
154
lib/services/update_service.dart
Normal file
154
lib/services/update_service.dart
Normal file
@@ -0,0 +1,154 @@
|
||||
import 'dart:io';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
/// 更新服务 - 处理版本检查和客户端更新
|
||||
class UpdateService {
|
||||
static final UpdateService _instance = UpdateService._internal();
|
||||
factory UpdateService() => _instance;
|
||||
UpdateService._internal();
|
||||
|
||||
final Logger _logger = Logger('UpdateService');
|
||||
|
||||
/// 检查远程版本信息
|
||||
Future<RemoteVersionInfo?> checkVersion(String updateUrl) async {
|
||||
try {
|
||||
_logger.info('检查远程版本: $updateUrl');
|
||||
|
||||
// 构建版本检查URL: update_url/user/getClientVersion
|
||||
final versionUrl = updateUrl.endsWith('/')
|
||||
? '${updateUrl}user/getClientVersion'
|
||||
: '$updateUrl/user/getClientVersion';
|
||||
|
||||
final response = await http.get(Uri.parse(versionUrl)).timeout(
|
||||
const Duration(seconds: 10),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
// 返回的是纯文本版本号,不是JSON
|
||||
final remoteVersion = response.body.trim();
|
||||
|
||||
if (remoteVersion.isEmpty) {
|
||||
_logger.warning('远程版本号为空');
|
||||
return null;
|
||||
}
|
||||
|
||||
_logger.info('获取到远程版本号: $remoteVersion');
|
||||
|
||||
// 注意:这里只返回版本号,下载URL需要从其他地方获取
|
||||
// 可以根据需要扩展 RemoteVersionInfo 或使用其他方式获取下载URL
|
||||
return RemoteVersionInfo(
|
||||
version: remoteVersion,
|
||||
downloadUrl: '', // 需要从其他接口获取或使用默认URL
|
||||
releaseNotes: null,
|
||||
);
|
||||
} else {
|
||||
_logger.warning('获取版本信息失败: HTTP ${response.statusCode}');
|
||||
}
|
||||
} catch (e) {
|
||||
_logger.warning('检查版本失败: $e');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// 比较版本号(简单字符串比较,可以改进为语义化版本比较)
|
||||
bool isNewerVersion(String remoteVersion, String localVersion) {
|
||||
// 简单的字符串比较,如果不同则认为远程版本更新
|
||||
// 可以改进为语义化版本比较(如 1.2.3 vs 1.2.4)
|
||||
return remoteVersion != localVersion && remoteVersion.isNotEmpty;
|
||||
}
|
||||
|
||||
/// 下载并更新客户端
|
||||
Future<bool> downloadAndUpdate(String downloadUrl) async {
|
||||
try {
|
||||
_logger.info('开始下载新版本: $downloadUrl');
|
||||
|
||||
// 获取当前可执行文件路径
|
||||
final currentExe = Platform.resolvedExecutable;
|
||||
final exeDir = p.dirname(currentExe);
|
||||
final exeName = p.basename(currentExe);
|
||||
|
||||
// 下载文件保存为临时文件
|
||||
final tempFile = p.join(exeDir, '${exeName}.new');
|
||||
final backupFile = p.join(exeDir, '${exeName}.backup');
|
||||
|
||||
// 下载文件
|
||||
final response = await http.get(Uri.parse(downloadUrl)).timeout(
|
||||
const Duration(minutes: 10),
|
||||
);
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
_logger.severe('下载失败: HTTP ${response.statusCode}');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 保存到临时文件
|
||||
final file = File(tempFile);
|
||||
await file.writeAsBytes(response.bodyBytes);
|
||||
_logger.info('文件下载完成: $tempFile');
|
||||
|
||||
// 备份当前文件
|
||||
final currentFile = File(currentExe);
|
||||
if (await currentFile.exists()) {
|
||||
await currentFile.copy(backupFile);
|
||||
_logger.info('已备份当前文件: $backupFile');
|
||||
}
|
||||
|
||||
// 替换文件(需要关闭当前进程)
|
||||
// 注意:在 Windows 上,不能直接覆盖正在运行的可执行文件
|
||||
// 需要使用批处理脚本在下次启动时替换
|
||||
await _createUpdateScript(exeDir, exeName, tempFile, backupFile);
|
||||
|
||||
_logger.info('更新脚本已创建,将在下次启动时应用更新');
|
||||
return true;
|
||||
} catch (e) {
|
||||
_logger.severe('下载更新失败: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建更新脚本(Windows批处理文件)
|
||||
Future<void> _createUpdateScript(
|
||||
String exeDir,
|
||||
String exeName,
|
||||
String tempFile,
|
||||
String backupFile,
|
||||
) async {
|
||||
final scriptPath = p.join(exeDir, 'update.bat');
|
||||
final script = '''
|
||||
@echo off
|
||||
timeout /t 2 /nobreak >nul
|
||||
copy /Y "$tempFile" "$exeDir\\$exeName"
|
||||
if %ERRORLEVEL% EQU 0 (
|
||||
del "$tempFile"
|
||||
echo 更新成功
|
||||
) else (
|
||||
copy /Y "$backupFile" "$exeDir\\$exeName"
|
||||
echo 更新失败,已恢复备份
|
||||
del "$tempFile"
|
||||
)
|
||||
del "$backupFile"
|
||||
del "%~f0"
|
||||
''';
|
||||
|
||||
final scriptFile = File(scriptPath);
|
||||
await scriptFile.writeAsString(script);
|
||||
|
||||
// 执行脚本(延迟执行,以便当前进程退出)
|
||||
Process.start('cmd', ['/c', 'start', '/min', scriptPath], runInShell: true);
|
||||
}
|
||||
}
|
||||
|
||||
/// 远程版本信息
|
||||
class RemoteVersionInfo {
|
||||
final String version;
|
||||
final String downloadUrl;
|
||||
final String? releaseNotes;
|
||||
|
||||
RemoteVersionInfo({
|
||||
required this.version,
|
||||
required this.downloadUrl,
|
||||
this.releaseNotes,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user