From 33d208a7d6fb7655b9fbd603ea145fc455d6ba0b Mon Sep 17 00:00:00 2001 From: wxl0430 <141288415+wxl0430@users.noreply.github.com> Date: Sun, 22 Jun 2025 11:36:53 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20#179=20=E4=BD=BF=E7=94=A8=E9=95=9C?= =?UTF-8?q?=E5=83=8F=E7=AB=99API=E5=AE=9E=E7=8E=B0=E5=86=85=E7=BD=AE?= =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CRSim/App.xaml.cs | 5 + CRSim/UpdateChecker.cs | 373 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 378 insertions(+) create mode 100644 CRSim/UpdateChecker.cs diff --git a/CRSim/App.xaml.cs b/CRSim/App.xaml.cs index 8aa4317..609ed71 100644 --- a/CRSim/App.xaml.cs +++ b/CRSim/App.xaml.cs @@ -42,6 +42,8 @@ namespace CRSim services.AddTransient(); services.AddScreenSimulatorServices(); + services.AddTransient(); + }).Build(); [STAThread] @@ -57,6 +59,9 @@ namespace CRSim _host.Services.GetRequiredService().Start(); _host.Services.GetRequiredService().SetOwner(app.MainWindow); + var updateChecker = new UpdateChecker(); + updateChecker.CheckForUpdates().Wait(); + app.MainWindow.Visibility = Visibility.Visible; app.Run(); } diff --git a/CRSim/UpdateChecker.cs b/CRSim/UpdateChecker.cs new file mode 100644 index 0000000..060c370 --- /dev/null +++ b/CRSim/UpdateChecker.cs @@ -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 GetLatestVersionInfo() + { + try + { + var response = await _httpClient.GetAsync(ApiUrl); + if (response.IsSuccessStatusCode) + { + var json = await response.Content.ReadAsStringAsync(); + return JsonSerializer.Deserialize(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 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; } + } +} \ No newline at end of file