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,