feat: #179 使用镜像站API实现内置更新

This commit is contained in:
wxl0430
2025-06-22 11:36:53 +08:00
committed by GitHub
parent 3b415ead7e
commit 33d208a7d6
2 changed files with 378 additions and 0 deletions

View File

@@ -42,6 +42,8 @@ namespace CRSim
services.AddTransient<WebsiteSimulator.Simulator>();
services.AddScreenSimulatorServices();
services.AddTransient<UpdateChecker>();
}).Build();
[STAThread]
@@ -57,6 +59,9 @@ namespace CRSim
_host.Services.GetRequiredService<ITimeService>().Start();
_host.Services.GetRequiredService<IDialogService>().SetOwner(app.MainWindow);
var updateChecker = new UpdateChecker();
updateChecker.CheckForUpdates().Wait();
app.MainWindow.Visibility = Visibility.Visible;
app.Run();
}

373
CRSim/UpdateChecker.cs Normal file
View File

@@ -0,0 +1,373 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Security.Cryptography;
using System.Net.Http;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Reflection;
namespace CRSim
{
public class UpdateChecker
{
private const string ApiUrl = "https://crsim.com.cn/api/version";
private readonly HttpClient _httpClient = new HttpClient();
private ProgressWindow _progressWindow;
private CancellationTokenSource _cancellationTokenSource;
public async Task CheckForUpdates()
{
try
{
await ExecuteOnUIThread(async () =>
{
var currentVersion = Assembly.GetExecutingAssembly().GetName().Version.ToString();
var latestVersionInfo = await GetLatestVersionInfo();
if (latestVersionInfo != null && latestVersionInfo.ver != currentVersion)
{
var result = ShowQuestion($"有新版本可用,当前版本:{currentVersion}\n最新版本: {latestVersionInfo.ver}\n\n更新日志:\n{latestVersionInfo.log}\n\n是否更新?(点击更新后请等待,程序将自动重启,时间与下载速度有关)");
if (result)
{
_cancellationTokenSource = new CancellationTokenSource();
ShowProgressWindow("准备下载更新...");
try
{
await DownloadAndInitiateUpdate(latestVersionInfo, _cancellationTokenSource.Token);
}
catch (OperationCanceledException)
{
ShowMessage("更新已取消");
}
finally
{
CloseProgressWindow();
_cancellationTokenSource = null;
}
}
}
});
}
catch (Exception ex)
{
CloseProgressWindow();
ShowError($"更新检查失败:{ex.Message}");
}
}
private async Task<VersionInfo> GetLatestVersionInfo()
{
try
{
var response = await _httpClient.GetAsync(ApiUrl);
if (response.IsSuccessStatusCode)
{
var json = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<VersionInfo>(json);
}
}
catch (Exception ex)
{
ShowError($"调用API失败 {ex.Message}");
}
return null;
}
private async Task DownloadAndInitiateUpdate(VersionInfo versionInfo, CancellationToken cancellationToken)
{
try
{
var tempFilePath = await DownloadFileWithProgress(versionInfo.download_file, cancellationToken);
var isValid = await Task.Run(() => ValidateFile(tempFilePath, versionInfo.file_md5, versionInfo.file_sha256));
if (!isValid)
{
var forceUpdate = ShowQuestion("md5和sha256验证失败可能遭到网络劫持是否强制更新");
if (!forceUpdate)
{
File.Delete(tempFilePath);
return;
}
}
var programDirectory = AppDomain.CurrentDomain.BaseDirectory;
var appExePath = Process.GetCurrentProcess().MainModule.FileName;
var appName = Path.GetFileName(appExePath);
string batchScript = $@"
@echo off
taskkill /F /IM ""{appName}"" >nul 2>&1
timeout /t 2 >nul
cd /d ""{programDirectory}""
powershell -Command ""Expand-Archive -Path '{tempFilePath}' -DestinationPath '{programDirectory}' -Force""
del /f /q ""{tempFilePath}""
start """" ""{appExePath}""
exit /b 0
";
string batchFilePath = Path.Combine(Path.GetTempPath(), "CRSimUpdate.bat");
File.WriteAllText(batchFilePath, batchScript);
try
{
var processInfo = new ProcessStartInfo
{
FileName = "cmd.exe",
Arguments = $"/c \"{batchFilePath}\"",
WindowStyle = ProcessWindowStyle.Hidden,
UseShellExecute = false,
CreateNoWindow = true
};
Process.Start(processInfo);
Application.Current.Shutdown();
}
catch (Exception ex)
{
ShowError($"启动更新脚本进程失败: {ex.Message}");
}
}
catch (Exception ex)
{
ShowError($"更新失败: {ex.Message}");
throw;
}
}
private async Task<string> DownloadFileWithProgress(string downloadUrl, CancellationToken cancellationToken)
{
var tempFile = Path.GetTempFileName() + ".zip";
try
{
using var response = await _httpClient.GetAsync(downloadUrl, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
response.EnsureSuccessStatusCode();
var contentLength = response.Content.Headers.ContentLength;
UpdateProgress("开始下载更新文件...", 0);
using var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
using var fileStream = new FileStream(tempFile, FileMode.Create);
var buffer = new byte[8192];
var totalBytesRead = 0L;
var lastUpdateBytes = 0L;
var updateIntervalBytes = contentLength.HasValue ? Math.Max(1024 * 1024, contentLength.Value / 100) : 1024 * 1024;
int bytesRead;
UpdateProgress($"准备下载: {GetSizeString(contentLength ?? 0)}", 0);
while ((bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken)) > 0)
{
await fileStream.WriteAsync(buffer, 0, bytesRead, cancellationToken);
totalBytesRead += bytesRead;
if (totalBytesRead - lastUpdateBytes >= updateIntervalBytes || totalBytesRead == contentLength)
{
if (contentLength.HasValue)
{
var progressPercentage = (int)((totalBytesRead * 100) / contentLength.Value);
UpdateProgress($"下载中: {progressPercentage}%", progressPercentage);
}
else
{
UpdateProgress($"下载中: {GetSizeString(totalBytesRead)}", 0);
}
lastUpdateBytes = totalBytesRead;
}
}
UpdateProgress("下载完成,准备更新...", 100);
return tempFile;
}
catch (OperationCanceledException)
{
if (File.Exists(tempFile))
{
File.Delete(tempFile);
}
throw;
}
catch (Exception ex)
{
if (File.Exists(tempFile))
{
File.Delete(tempFile);
}
ShowError($"下载文件失败: {ex.Message}");
throw;
}
}
private string GetSizeString(long bytes)
{
if (bytes < 1024) return $"{bytes} B";
if (bytes < 1024 * 1024) return $"{bytes / 1024.0:F1} KB";
return $"{bytes / (1024.0 * 1024):F1} MB";
}
private bool ValidateFile(string filePath, string expectedMd5, string expectedSha256)
{
try
{
using var md5 = MD5.Create();
using var sha256 = SHA256.Create();
using var stream = File.OpenRead(filePath);
var actualMd5 = BitConverter.ToString(md5.ComputeHash(stream)).Replace("-", "").ToLowerInvariant();
stream.Position = 0;
var actualSha256 = BitConverter.ToString(sha256.ComputeHash(stream)).Replace("-", "").ToLowerInvariant();
return actualMd5 == expectedMd5 && actualSha256 == expectedSha256;
}
catch
{
return false;
}
}
private void ShowProgressWindow(string message)
{
ExecuteOnUIThread(() =>
{
_progressWindow = new ProgressWindow { Owner = Application.Current.MainWindow };
_progressWindow.UpdateStatus(message);
_progressWindow.Show();
});
}
private void UpdateProgress(string message, int progress)
{
ExecuteOnUIThread(() =>
{
if (_progressWindow != null && _progressWindow.IsVisible)
{
_progressWindow.UpdateStatus(message);
_progressWindow.UpdateProgress(progress);
}
});
}
private void CloseProgressWindow()
{
ExecuteOnUIThread(() =>
{
_progressWindow?.Close();
_progressWindow = null;
});
}
private Task ExecuteOnUIThread(Action action)
{
if (Application.Current == null)
{
action();
return Task.CompletedTask;
}
if (Application.Current.Dispatcher.CheckAccess())
{
action();
return Task.CompletedTask;
}
else
{
return Application.Current.Dispatcher.InvokeAsync(action).Task;
}
}
private void ShowMessage(string message)
{
ExecuteOnUIThread(() =>
{
MessageBox.Show(message, "CRSim", MessageBoxButton.OK, MessageBoxImage.Information);
});
}
private void ShowError(string message)
{
ExecuteOnUIThread(() =>
{
MessageBox.Show(message, "CRSim - Error", MessageBoxButton.OK, MessageBoxImage.Error);
});
}
private bool ShowQuestion(string message)
{
var result = false;
ExecuteOnUIThread(() =>
{
result = MessageBox.Show(message, "CRSim", MessageBoxButton.YesNo, MessageBoxImage.Question) == MessageBoxResult.Yes;
}).Wait();
return result;
}
}
public class ProgressWindow : Window
{
private readonly TextBlock _statusText;
private readonly ProgressBar _progressBar;
public ProgressWindow()
{
Title = "更新进度";
Width = 400;
Height = 150;
WindowStartupLocation = WindowStartupLocation.CenterOwner;
ResizeMode = ResizeMode.NoResize;
ShowInTaskbar = false;
var stackPanel = new StackPanel { Margin = new Thickness(10) };
_statusText = new TextBlock
{
Text = "准备中...",
TextWrapping = TextWrapping.Wrap,
Margin = new Thickness(0, 10, 0, 20)
};
_progressBar = new ProgressBar
{
Minimum = 0,
Maximum = 100,
Value = 0,
Height = 20
};
stackPanel.Children.Add(_statusText);
stackPanel.Children.Add(_progressBar);
Content = stackPanel;
}
public void UpdateStatus(string message)
{
_statusText.Text = message;
}
public void UpdateProgress(int value)
{
_progressBar.Value = value;
}
}
public class VersionInfo
{
public string status_code { get; set; }
public string message { get; set; }
public string ver { get; set; }
public string download_file { get; set; }
public string file_md5 { get; set; }
public string file_sha256 { get; set; }
public string log { get; set; }
}
}