From ab9fcbe9adcdc9ccce1dd7e6c0edf7efcdb7ddc3 Mon Sep 17 00:00:00 2001 From: Chlna6666 <79357769+Chlna6666@users.noreply.github.com> Date: Fri, 15 May 2026 00:03:37 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix(updater):=20=E9=87=8D=E6=9E=84=E8=B7=A8?= =?UTF-8?q?=E5=B9=B3=E5=8F=B0=E8=87=AA=E6=9B=B4=E6=96=B0=E6=B5=81=E7=A8=8B?= =?UTF-8?q?=E4=BB=A5=E9=99=8D=E4=BD=8E=E6=8A=A5=E6=AF=92=E9=A3=8E=E9=99=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/BedrockBoot/AppUpdater.cs | 435 +++++++++++++----- src/BedrockBoot/Program.cs | 4 +- .../TaskDownloadUpdateFileItem.axaml.cs | 149 +++--- 3 files changed, 409 insertions(+), 179 deletions(-) diff --git a/src/BedrockBoot/AppUpdater.cs b/src/BedrockBoot/AppUpdater.cs index 42b7424..bc9242e 100644 --- a/src/BedrockBoot/AppUpdater.cs +++ b/src/BedrockBoot/AppUpdater.cs @@ -1,21 +1,27 @@ -using System; +using System; using System.Diagnostics; using System.IO; using System.Runtime.InteropServices; using System.Threading; +using BedrockBoot.Models.Global; namespace BedrockBoot; /// -/// 应用程序更新器 +/// 应用程序更新器。 +/// 更新流程刻意采用“当前已安装程序复制出 updater runner,再由 runner 替换下载好的新文件”的模式, +/// 以避免让刚下载完成的 payload 直接参与自我覆盖,这样更接近常规安装器/更新器的行为。 /// public static class AppUpdater { + private const int ReplacementRetryCount = 8; + private const int RetryDelayMilliseconds = 500; + private static bool IsLinux => RuntimeInformation.IsOSPlatform(OSPlatform.Linux); private static bool IsWindows => RuntimeInformation.IsOSPlatform(OSPlatform.Windows); /// - /// 主入口:根据参数解析决定是执行更新流程还是正常启动 + /// 在程序启动初期处理更新专用参数。 /// public static void ProcessStartupArgs(string[] args) { @@ -23,21 +29,19 @@ public static void ProcessStartupArgs(string[] args) switch (args[i]) { case "--update-launcher": - // 模式1:作为更新引导程序启动 - if (i + 1 < args.Length) + if (TryGetArgument(args, i + 1, out var targetPath) && + TryGetArgument(args, i + 2, out var sourcePath)) { - var targetPath = args[i + 1]; - LaunchUpdateReplacement(targetPath); + LaunchUpdateReplacement(targetPath, sourcePath); Environment.Exit(0); } break; case "--update-replace": - // 模式2:作为替换程序启动 - if (i + 1 < args.Length) + if (TryGetArgument(args, i + 1, out var replacementTargetPath) && + TryGetArgument(args, i + 2, out var replacementSourcePath)) { - var oldPath = args[i + 1]; - PerformFileReplacement(oldPath); + PerformFileReplacement(replacementTargetPath, replacementSourcePath); Environment.Exit(0); } @@ -46,178 +50,379 @@ public static void ProcessStartupArgs(string[] args) } /// - /// 从旧版本主程序启动更新 + /// 兼容旧的更新入口。 + /// 当前进程是“下载好的新版本文件”,参数是“已安装旧版本路径”。 /// - public static void StartUpdateFromOldVersion(string oldVersionFullPath) + public static void StartUpdateFromOldVersion(string installedExecutablePath) { try { - Console.WriteLine($@"开始更新流程,目标文件: {oldVersionFullPath}"); - - if (!File.Exists(oldVersionFullPath)) + var downloadedPayloadPath = GetCurrentExecutablePath(); + if (string.IsNullOrWhiteSpace(downloadedPayloadPath)) { - Console.WriteLine(@"旧版本文件不存在,无法更新"); + Console.WriteLine(@"无法获取下载后的更新文件路径"); return; } - var currentExePath = Process.GetCurrentProcess().MainModule?.FileName; - if (string.IsNullOrEmpty(currentExePath) || !File.Exists(currentExePath)) - { - Console.WriteLine(@"无法获取当前程序路径"); - return; - } + StartUpdateLauncher(installedExecutablePath, downloadedPayloadPath, downloadedPayloadPath); + } + catch (Exception ex) + { + Console.WriteLine($@"启动兼容更新流程失败: {ex}"); + } + } - // 检查是否试图自我更新 - if (Path.GetFullPath(currentExePath).Equals( - Path.GetFullPath(oldVersionFullPath), StringComparison.OrdinalIgnoreCase)) + /// + /// 首选更新入口。 + /// 当前进程是“已安装程序”,参数是“已下载完成的新版本文件路径”。 + /// + public static void StartUpdateFromDownloadedFile(string downloadedPayloadPath) + { + try + { + var installedExecutablePath = GetCurrentExecutablePath(); + if (string.IsNullOrWhiteSpace(installedExecutablePath)) { - Console.WriteLine(@"新旧文件路径相同,跳过更新"); + Console.WriteLine(@"无法获取当前安装程序路径"); return; } - // 关键步骤1:启动更新引导程序(当前程序的新实例) - var launcherInfo = new ProcessStartInfo - { - FileName = currentExePath, - Arguments = $"--update-launcher \"{oldVersionFullPath}\"", - UseShellExecute = !IsLinux, // Linux 上设置为 false - WindowStyle = ProcessWindowStyle.Normal - }; - - // Linux 特殊处理 - if (IsLinux) - { - launcherInfo.UseShellExecute = false; - launcherInfo.CreateNoWindow = true; - } - - Console.WriteLine($@"启动更新引导程序: {currentExePath}"); - Process.Start(launcherInfo); - - Console.WriteLine(@"更新引导程序已启动,当前进程即将退出"); - Environment.Exit(0); + StartUpdateLauncher(installedExecutablePath, downloadedPayloadPath, installedExecutablePath); } catch (Exception ex) { - Console.WriteLine($"启动更新流程失败: {ex.Message}", ex); + Console.WriteLine($@"启动更新流程失败: {ex}"); } } /// - /// 作为更新引导程序启动:复制自身并启动替换程序 + /// 启动一个轻量 helper 实例,再由 helper 复制自己为 runner。 + /// 这样原始 UI 进程可以尽快退出,runner 只负责文件替换与重启。 /// - private static void LaunchUpdateReplacement(string targetPath) + private static void StartUpdateLauncher( + string targetPath, + string sourcePath, + string launcherExecutablePath) { - try + var resolvedTargetPath = NormalizePath(targetPath); + var resolvedSourcePath = NormalizePath(sourcePath); + var resolvedLauncherExecutablePath = NormalizePath(launcherExecutablePath); + + Console.WriteLine($@"开始更新流程,目标文件: {resolvedTargetPath}"); + Console.WriteLine($@"更新源文件: {resolvedSourcePath}"); + + if (!File.Exists(resolvedTargetPath)) { - var currentPath = Process.GetCurrentProcess().MainModule?.FileName; - var tempDir = Path.GetTempPath(); - var tempFileName = IsLinux - ? $"BedrockBoot_Update_{Guid.NewGuid():N}" - : $"BedrockBoot_Update_{Guid.NewGuid():N}.exe"; - var tempPath = Path.Combine(tempDir, tempFileName); + Console.WriteLine(@"目标程序不存在,无法更新"); + return; + } + + if (!File.Exists(resolvedSourcePath)) + { + Console.WriteLine(@"更新源文件不存在,无法更新"); + return; + } + + if (!File.Exists(resolvedLauncherExecutablePath)) + { + Console.WriteLine(@"更新引导程序不存在,无法更新"); + return; + } + + if (resolvedTargetPath.Equals(resolvedSourcePath, StringComparison.OrdinalIgnoreCase)) + { + Console.WriteLine(@"更新源文件与目标文件路径相同,跳过更新"); + return; + } - Console.WriteLine($@"引导程序:复制到临时位置 {tempPath}"); + var launcherInfo = CreateLauncherStartInfo( + resolvedLauncherExecutablePath, + resolvedTargetPath, + resolvedSourcePath); - // 复制当前程序到临时位置 - File.Copy(currentPath, tempPath, true); + Console.WriteLine($@"启动更新引导程序: {resolvedLauncherExecutablePath}"); + Process.Start(launcherInfo); - // Linux: 设置可执行权限 - if (IsLinux) + Console.WriteLine(@"更新引导程序已启动,当前进程即将退出"); + Environment.Exit(0); + } + + /// + /// 由 helper 实例复制出 runner,再由 runner 执行真正的替换。 + /// runner 固定落在应用更新目录,而不是系统临时目录。 + /// + private static void LaunchUpdateReplacement(string targetPath, string sourcePath) + { + try + { + var currentPath = GetCurrentExecutablePath(); + if (string.IsNullOrWhiteSpace(currentPath) || !File.Exists(currentPath)) { - var chmodProcess = Process.Start("chmod", $"+x \"{tempPath}\""); - chmodProcess?.WaitForExit(); + Console.WriteLine(@"无法获取当前更新程序路径"); + return; } - // 关键步骤2:启动临时副本作为替换程序 - var replaceInfo = new ProcessStartInfo + var resolvedTargetPath = NormalizePath(targetPath); + var resolvedSourcePath = NormalizePath(sourcePath); + if (!File.Exists(resolvedTargetPath) || !File.Exists(resolvedSourcePath)) { - FileName = tempPath, - Arguments = $"--update-replace \"{targetPath}\"", - UseShellExecute = false, - CreateNoWindow = true - }; + Console.WriteLine(@"目标文件或更新源文件不存在,无法继续更新"); + return; + } + + var updateWorkspace = PrepareUpdateWorkspace(); + CleanupUpdaterWorkspace(updateWorkspace); + + var runnerPath = Path.Combine( + updateWorkspace, + $"updater_runner_{Environment.ProcessId}_{Guid.NewGuid():N}{GetExecutableSuffix(currentPath)}"); + + Console.WriteLine($@"引导程序:复制 runner 到 {runnerPath}"); + File.Copy(currentPath, runnerPath, true); + EnsureExecutableBit(runnerPath); - Console.WriteLine($@"启动替换程序来更新 {targetPath}"); + var replaceInfo = CreateReplacementStartInfo(runnerPath, resolvedTargetPath, resolvedSourcePath); + + Console.WriteLine($@"启动替换程序来更新 {resolvedTargetPath}"); Process.Start(replaceInfo); Console.WriteLine(@"更新引导程序退出"); } catch (Exception ex) { - Console.WriteLine($"引导更新失败: {ex.Message}", ex); + Console.WriteLine($@"引导更新失败: {ex}"); } } /// - /// 作为替换程序执行文件替换操作 + /// runner 会先把新版本复制到目标目录的 stage 文件,再执行备份替换。 + /// 这样即便重试失败,也能保留回滚点。 /// - private static void PerformFileReplacement(string oldPath) + private static void PerformFileReplacement(string targetPath, string sourcePath) { try { - var currentPath = Process.GetCurrentProcess().MainModule?.FileName; + var resolvedTargetPath = NormalizePath(targetPath); + var resolvedSourcePath = NormalizePath(sourcePath); - Console.WriteLine($@"替换程序:准备替换 {oldPath}"); + if (!File.Exists(resolvedSourcePath)) + { + Console.WriteLine(@"替换程序无法定位更新源文件"); + return; + } + + if (resolvedTargetPath.Equals(resolvedSourcePath, StringComparison.OrdinalIgnoreCase)) + { + Console.WriteLine(@"更新源文件与目标文件路径相同,跳过替换"); + return; + } + + var targetDirectory = Path.GetDirectoryName(resolvedTargetPath); + if (string.IsNullOrWhiteSpace(targetDirectory) || !Directory.Exists(targetDirectory)) + { + Console.WriteLine(@"目标目录不存在,无法执行替换"); + return; + } + + var stagePath = Path.Combine( + targetDirectory, + $"{Path.GetFileName(resolvedTargetPath)}.update-stage"); + var backupPath = Path.Combine( + targetDirectory, + $"{Path.GetFileName(resolvedTargetPath)}.bak"); + + Console.WriteLine($@"替换程序:准备使用 {resolvedSourcePath} 更新 {resolvedTargetPath}"); + TryDeleteFile(stagePath); + File.Copy(resolvedSourcePath, stagePath, true); + EnsureExecutableBit(stagePath); - // 确保原进程已退出,重试多次 var replaced = false; - for (var i = 0; i < 5; i++) + Exception? lastException = null; + + for (var i = 0; i < ReplacementRetryCount; i++) try { - // 关键步骤3:执行文件替换 - File.Delete(oldPath); - File.Move(currentPath, oldPath); + if (File.Exists(backupPath)) + File.Delete(backupPath); + + if (File.Exists(resolvedTargetPath)) + File.Move(resolvedTargetPath, backupPath); + + File.Move(stagePath, resolvedTargetPath); + EnsureExecutableBit(resolvedTargetPath); replaced = true; Console.WriteLine($@"文件替换成功 (第{i + 1}次尝试)"); break; } - catch (IOException ioEx) when (i < 4) + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) { - Console.WriteLine($@"文件被占用,等待后重试... (错误: {ioEx.Message})"); - Thread.Sleep(500 * (i + 1)); - } + lastException = ex; + RestoreBackupIfNeeded(resolvedTargetPath, backupPath); - if (replaced) - { - // Linux: 确保新文件有执行权限 - if (IsLinux) - { - var chmodProcess = Process.Start("chmod", $"+x \"{oldPath}\""); - chmodProcess?.WaitForExit(); - } - - // 关键步骤4:启动更新后的程序 - var finalStartInfo = new ProcessStartInfo - { - FileName = oldPath, - UseShellExecute = !IsLinux, - WindowStyle = ProcessWindowStyle.Normal - }; + if (i == ReplacementRetryCount - 1) + break; - // Linux 特殊处理 - if (IsLinux) - { - finalStartInfo.UseShellExecute = false; - finalStartInfo.CreateNoWindow = true; + Console.WriteLine($@"文件暂时不可替换,等待后重试... (错误: {ex.Message})"); + Thread.Sleep(RetryDelayMilliseconds * (i + 1)); } - Console.WriteLine($@"启动更新后的程序: {oldPath}"); - Process.Start(finalStartInfo); - Console.WriteLine(@"更新流程完成"); - } - else + if (!replaced) { + lastException ??= new IOException("替换重试次数已耗尽"); + RestoreBackupIfNeeded(resolvedTargetPath, backupPath); + TryDeleteFile(stagePath); Console.WriteLine(@"文件替换失败,可能被其他进程锁定"); + Console.WriteLine($@"最后一次错误: {lastException.Message}"); + return; } + + var finalStartInfo = CreateFinalStartInfo(resolvedTargetPath); + + Console.WriteLine($@"启动更新后的程序: {resolvedTargetPath}"); + Process.Start(finalStartInfo); + + TryDeleteFile(backupPath); + TryDeleteFile(resolvedSourcePath); + Console.WriteLine(@"更新流程完成"); } catch (Exception ex) { - Console.WriteLine($"替换过程失败: {ex.Message}", ex); + Console.WriteLine($@"替换过程失败: {ex}"); } finally { Environment.Exit(0); } } -} \ No newline at end of file + + private static ProcessStartInfo CreateLauncherStartInfo( + string launcherExecutablePath, + string targetPath, + string sourcePath) + { + return new ProcessStartInfo + { + FileName = launcherExecutablePath, + Arguments = $"--update-launcher \"{targetPath}\" \"{sourcePath}\"", + UseShellExecute = false, + WorkingDirectory = Path.GetDirectoryName(launcherExecutablePath) ?? Environment.CurrentDirectory + }; + } + + private static ProcessStartInfo CreateReplacementStartInfo( + string runnerPath, + string targetPath, + string sourcePath) + { + return new ProcessStartInfo + { + FileName = runnerPath, + Arguments = $"--update-replace \"{targetPath}\" \"{sourcePath}\"", + UseShellExecute = false, + WorkingDirectory = Path.GetDirectoryName(runnerPath) ?? Environment.CurrentDirectory + }; + } + + private static ProcessStartInfo CreateFinalStartInfo(string executablePath) + { + return new ProcessStartInfo + { + FileName = executablePath, + UseShellExecute = false, + WorkingDirectory = Path.GetDirectoryName(executablePath) ?? Environment.CurrentDirectory + }; + } + + private static string PrepareUpdateWorkspace() + { + Directory.CreateDirectory(PathsList.UpdatePath); + return PathsList.UpdatePath; + } + + private static void CleanupUpdaterWorkspace(string workspacePath) + { + foreach (var pattern in new[] { "updater_runner_*", "*.update-stage" }) + foreach (var file in Directory.GetFiles(workspacePath, pattern)) + TryDeleteFile(file); + } + + private static string GetCurrentExecutablePath() + { + if (IsLinux) + { + var appImagePath = Environment.GetEnvironmentVariable("APPIMAGE"); + if (!string.IsNullOrWhiteSpace(appImagePath) && File.Exists(appImagePath)) + return NormalizePath(appImagePath); + } + + var processPath = Environment.ProcessPath; + if (!string.IsNullOrWhiteSpace(processPath) && File.Exists(processPath)) + return NormalizePath(processPath); + + var mainModulePath = Process.GetCurrentProcess().MainModule?.FileName; + return string.IsNullOrWhiteSpace(mainModulePath) ? string.Empty : NormalizePath(mainModulePath); + } + + private static string NormalizePath(string path) + { + return Path.GetFullPath(path); + } + + private static string GetExecutableSuffix(string executablePath) + { + var extension = Path.GetExtension(executablePath); + if (!string.IsNullOrWhiteSpace(extension)) + return extension; + + return IsWindows ? ".exe" : string.Empty; + } + + private static void EnsureExecutableBit(string filePath) + { + if (!IsLinux || string.IsNullOrWhiteSpace(filePath)) + return; + + var chmodProcess = Process.Start(new ProcessStartInfo + { + FileName = "chmod", + Arguments = $"+x \"{filePath}\"", + UseShellExecute = false + }); + chmodProcess?.WaitForExit(); + } + + private static void RestoreBackupIfNeeded(string targetPath, string backupPath) + { + if (File.Exists(targetPath) || !File.Exists(backupPath)) + return; + + File.Move(backupPath, targetPath); + } + + private static void TryDeleteFile(string filePath) + { + if (!File.Exists(filePath)) + return; + + try + { + File.Delete(filePath); + } + catch (Exception ex) + { + Console.WriteLine($@"清理文件失败 {filePath}: {ex.Message}"); + } + } + + private static bool TryGetArgument(string[] args, int index, out string value) + { + if (index < args.Length && !string.IsNullOrWhiteSpace(args[index])) + { + value = args[index]; + return true; + } + + value = string.Empty; + return false; + } +} diff --git a/src/BedrockBoot/Program.cs b/src/BedrockBoot/Program.cs index f996fd8..cc4264d 100644 --- a/src/BedrockBoot/Program.cs +++ b/src/BedrockBoot/Program.cs @@ -121,7 +121,7 @@ private static bool ArgsAnalytical(List args) { case "-update": Console.WriteLine(@"触发更新,本次启动将不会拉起窗体。"); - AppUpdater.StartUpdateFromOldVersion(args[args.FindIndex(x => x == "-update") + 1]); + AppUpdater.StartUpdateFromDownloadedFile(args[args.FindIndex(x => x == "-update") + 1]); return true; case "-shell": @@ -214,4 +214,4 @@ public static AppBuilder BuildAvaloniaApp() .WithInterFont() .LogToTrace(); } -} \ No newline at end of file +} diff --git a/src/BedrockBoot/Views/TaskItem/TaskDownloadUpdateFileItem.axaml.cs b/src/BedrockBoot/Views/TaskItem/TaskDownloadUpdateFileItem.axaml.cs index a4e034f..09a5ff1 100644 --- a/src/BedrockBoot/Views/TaskItem/TaskDownloadUpdateFileItem.axaml.cs +++ b/src/BedrockBoot/Views/TaskItem/TaskDownloadUpdateFileItem.axaml.cs @@ -1,5 +1,6 @@ -using System; +using System; using System.Diagnostics; +using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -11,7 +12,6 @@ using BedrockBoot.Models.Global; using Octokit; using OnePointUI.Avalonia.Base.Entry; -using OnePointUI.Avalonia.Styling.Controls.OnePointControls.Dialog; using GlobalModel = BedrockBoot.Core.Global.GlobalModel; using Path = System.IO.Path; @@ -19,7 +19,10 @@ namespace BedrockBoot.Views.TaskItem; public partial class TaskDownloadUpdateFileItem : UserControl { - private Action _cancelCallBack; + private readonly string _currentExecutablePath = GetCurrentLauncherPath(); + + private Action _cancelCallBack = () => { }; + private CancellationTokenSource? _cts; public TaskDownloadUpdateFileItem() { @@ -31,66 +34,75 @@ public TaskDownloadUpdateFileItem(Release release) : this() Release = release; } - public Release Release { get; set; } - private CancellationTokenSource _cts; + public Release Release { get; set; } = null!; public void Update(Action cancelCallBack) { _cancelCallBack = cancelCallBack; - // 标题国际化 CardTitle.Text = string.Format(I18nManager.Instance["Task.Update.Title.Format"], Release.TagName); - // 查找名���包含 "win" 的 asset - var winAsset = Release.Assets.FirstOrDefault(asset => - asset.Name.Contains("win", StringComparison.OrdinalIgnoreCase)); - - if (winAsset == null) + var asset = SelectPreferredAsset(); + if (asset == null) { - // 如果没有找到包含 "win" 的 asset,可以记录错误���回退到第一个 asset - Console.WriteLine(@"未找到包含 'win' 标志的 asset"); + Console.WriteLine(@"未找到适用于当前平台的更新资源"); return; } - var url = winAsset.BrowserDownloadUrl; - var path = Path.Combine(PathsList.UpdatePath, $"{Release.TagName}.exe"); + Directory.CreateDirectory(PathsList.UpdatePath); + + var downloadUrl = asset.BrowserDownloadUrl; + var downloadPath = Path.Combine(PathsList.UpdatePath, asset.Name); _cts = new CancellationTokenSource(); var token = _cts.Token; Task.Run(async () => { - var download = new GithubFilesDownloader(GlobalModel.Config.Data.DownloadChunkCount, + var download = new GithubFilesDownloader( + GlobalModel.Config.Data.DownloadChunkCount, 1024); - await download.DownloadAsync(url, path, new Progress(xprogress => - { - Dispatcher.UIThread.Invoke(() => + await download.DownloadAsync( + downloadUrl, + downloadPath, + new Progress(progress => { - if (ProgressBar.IsIndeterminate) + Dispatcher.UIThread.Invoke(() => { - ProgressBar.IsIndeterminate = false; - ProgressBar.Value = 100; - } + if (ProgressBar.IsIndeterminate) + { + ProgressBar.IsIndeterminate = false; + ProgressBar.Value = 100; + } + + ProgressBar.Value = (int)progress.ProgressPercentage; + ProgressText.Text = $"{progress.ProgressPercentage:F2} %"; + }); + }), + token); - ProgressBar.Value = (int)xprogress.ProgressPercentage; - ProgressText.Text = $"{xprogress.ProgressPercentage:F2} %"; - }); - }), token); - - // 给予 UI 刷新的缓冲时间 await Task.Delay(100, token); - // 启动更新程序 - Process.Start(path, new[] { "-update", Process.GetCurrentProcess().MainModule?.FileName }); + if (string.IsNullOrWhiteSpace(_currentExecutablePath) || !File.Exists(_currentExecutablePath)) + throw new FileNotFoundException("无法定位当前程序,不能启动更新引导流程", _currentExecutablePath); + + var startInfo = new ProcessStartInfo + { + FileName = _currentExecutablePath, + UseShellExecute = false + }; + startInfo.ArgumentList.Add("-update"); + startInfo.ArgumentList.Add(downloadPath); + + Process.Start(startInfo); await Task.Delay(100, token); Environment.Exit(0); - }); + }, token); } public static void Update(Release release) { -#if WINDOWS Models.Global.GlobalModel.MainWindow.Notice.AddNotice(new NoticeInfo { Title = I18nManager.Instance["Task.Update.Notice.Title"], @@ -99,35 +111,9 @@ public static void Update(Release release) }); var body = new TaskDownloadUpdateFileItem(release); - var tuid = Models.Global.GlobalModel.TaskManager.AddTask(body); - - body.Update(() => Models.Global.GlobalModel.TaskManager.RemoveTask(tuid)); -#endif + var taskId = Models.Global.GlobalModel.TaskManager.AddTask(body); -#if LINUX - OnePointUI.Avalonia.Styling.Controls.OnePointControls.Dialog.DialogHost.Show(new DialogInfo() - { - Title = "您的系统尚不支持自动更新", - Content = new StackPanel() - { - Spacing = 4, - Children = - { - new TextBlock() - { - Text = "您的系统为 Linux 发行版,尚不支持使用内置更新工具进行自动更新。\n" + - "请前往 Github Release 或 官网 下载新的程序包替换以完成更新" - }, - new HyperlinkButton() - { - Content = $"Github Release {release.Name}", - NavigateUri = new Uri(release.HtmlUrl) - } - } - }, - CloseButtonText = "确定" - }); -#endif + body.Update(() => Models.Global.GlobalModel.TaskManager.RemoveTask(taskId)); } private void CancelButton_OnClick(object? sender, RoutedEventArgs e) @@ -135,4 +121,43 @@ private void CancelButton_OnClick(object? sender, RoutedEventArgs e) _cts?.Cancel(); _cancelCallBack.Invoke(); } -} \ No newline at end of file + + /// + /// 根据当前平台选择可直接替换的发行资源。 + /// Windows 选择单文件 .exe,Linux 选择可直接启动的 .AppImage。 + /// + private ReleaseAsset? SelectPreferredAsset() + { + var assets = Release.Assets.ToList(); + if (assets.Count == 0) + return null; + + if (OperatingSystem.IsWindows()) + return assets.FirstOrDefault(asset => + asset.Name.Contains("win", StringComparison.OrdinalIgnoreCase) && + asset.Name.EndsWith(".exe", StringComparison.OrdinalIgnoreCase)) + ?? assets.FirstOrDefault(asset => + asset.Name.EndsWith(".exe", StringComparison.OrdinalIgnoreCase)); + + if (OperatingSystem.IsLinux()) + return assets.FirstOrDefault(asset => + asset.Name.Contains("linux", StringComparison.OrdinalIgnoreCase) && + asset.Name.EndsWith(".AppImage", StringComparison.OrdinalIgnoreCase)) + ?? assets.FirstOrDefault(asset => + asset.Name.EndsWith(".AppImage", StringComparison.OrdinalIgnoreCase)); + + return null; + } + + private static string GetCurrentLauncherPath() + { + if (OperatingSystem.IsLinux()) + { + var appImagePath = Environment.GetEnvironmentVariable("APPIMAGE"); + if (!string.IsNullOrWhiteSpace(appImagePath)) + return appImagePath; + } + + return Environment.ProcessPath ?? Process.GetCurrentProcess().MainModule?.FileName ?? string.Empty; + } +} From a5064b39decaa4764d76babe730ee05f76c0ca8d Mon Sep 17 00:00:00 2001 From: Chlna6666 <79357769+Chlna6666@users.noreply.github.com> Date: Fri, 15 May 2026 09:42:07 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix(updater):=20=E4=BF=AE=E5=A4=8D=20Linux?= =?UTF-8?q?=20AppImage=20=E6=9B=BF=E6=8D=A2=E5=90=8E=E6=9D=83=E9=99=90?= =?UTF-8?q?=E4=B8=A2=E5=A4=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/BedrockBoot/AppUpdater.cs | 29 +++++++++++++++---- .../TaskDownloadUpdateFileItem.axaml.cs | 2 ++ 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/src/BedrockBoot/AppUpdater.cs b/src/BedrockBoot/AppUpdater.cs index bc9242e..91ee8de 100644 --- a/src/BedrockBoot/AppUpdater.cs +++ b/src/BedrockBoot/AppUpdater.cs @@ -382,13 +382,30 @@ private static void EnsureExecutableBit(string filePath) if (!IsLinux || string.IsNullOrWhiteSpace(filePath)) return; - var chmodProcess = Process.Start(new ProcessStartInfo + try { - FileName = "chmod", - Arguments = $"+x \"{filePath}\"", - UseShellExecute = false - }); - chmodProcess?.WaitForExit(); + File.SetUnixFileMode( + filePath, + UnixFileMode.UserRead + | UnixFileMode.UserWrite + | UnixFileMode.UserExecute + | UnixFileMode.GroupRead + | UnixFileMode.GroupExecute + | UnixFileMode.OtherRead + | UnixFileMode.OtherExecute); + } + catch (Exception ex) + { + Console.WriteLine($@"设置可执行权限失败 {filePath}: {ex.Message}"); + } + } + + /// + /// 给当前平台上的更新产物补齐执行权限。 + /// + public static void EnsureExecutableForCurrentPlatform(string filePath) + { + EnsureExecutableBit(filePath); } private static void RestoreBackupIfNeeded(string targetPath, string backupPath) diff --git a/src/BedrockBoot/Views/TaskItem/TaskDownloadUpdateFileItem.axaml.cs b/src/BedrockBoot/Views/TaskItem/TaskDownloadUpdateFileItem.axaml.cs index 09a5ff1..d232232 100644 --- a/src/BedrockBoot/Views/TaskItem/TaskDownloadUpdateFileItem.axaml.cs +++ b/src/BedrockBoot/Views/TaskItem/TaskDownloadUpdateFileItem.axaml.cs @@ -86,6 +86,8 @@ await download.DownloadAsync( if (string.IsNullOrWhiteSpace(_currentExecutablePath) || !File.Exists(_currentExecutablePath)) throw new FileNotFoundException("无法定位当前程序,不能启动更新引导流程", _currentExecutablePath); + AppUpdater.EnsureExecutableForCurrentPlatform(downloadPath); + var startInfo = new ProcessStartInfo { FileName = _currentExecutablePath,