Skip to content
This repository was archived by the owner on Jan 25, 2026. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 152 additions & 0 deletions App/Updates/CheckUpdateService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
using PCL.Core.App.Updates.Sources;
using PCL.Core.UI;
using System;
using System.IO;
using System.Threading.Tasks;

namespace PCL.Core.App.Updates;

[LifecycleService(LifecycleState.Running)]
[LifecycleScope("check-update", "检查更新")]
public sealed partial class CheckUpdateService
{
private static readonly SourceController _SourceController = new([
new UpdateMinioSource("https://s3.pysio.online/pcl2-ce/", "Pysio"),
new UpdateMinioSource("https://staticassets.naids.com/resources/pclce/", "Naids")
]);

public static VersionData? LatestVersion { get; private set; }

public static bool IsUpdateDownloaded { get; private set; }

[LifecycleStart]
private static async Task _Start()
{
if (Config.System.Update.UpdateMode == 3)
{
Context.Info("更新模式为禁用,跳过检查");
return;
}

Context.Info("检查更新中...");
if (!await TryCheckUpdate() || LatestVersion is null) return;

if (!LatestVersion.IsAvailable)
{
Context.Info("已经是最新版本,跳过更新");
return;
}

Context.Info($"发现新版本: {LatestVersion.Version.Code}, 准备更新");

if (Config.System.Update.UpdateMode == 2 && !_PromptUpdate()) return;

if (!await TryDownloadUpdate()) return;

if (Config.System.Update.UpdateMode == 1 && !_PromptInstall()) return;

Context.Info("准备重启并安装...");
UpdateHelper.Restart(true, true);
}

#region Public Methods

public static async Task<bool> TryCheckUpdate()
{
try
{
LatestVersion = await _SourceController.CheckUpdateAsync().ConfigureAwait(false);
return true;
}
catch (InvalidOperationException ex)
{
if (ex.Message.Contains("不可用"))
{
Context.Warn("所有更新源均不可用", ex);
HintWrapper.Show("所有更新源均不可用,可能是网络问题", HintTheme.Error);
}
else
{
Context.Warn("检查更新时发生未知异常", ex);
HintWrapper.Show("检查更新时发生未知异常,可能是网络问题", HintTheme.Error);
}
}
catch (Exception ex)
{
Context.Warn("检查更新时发生未知异常", ex);
HintWrapper.Show("检查更新时发生未知异常,可能是网络问题", HintTheme.Error);
}
return false;
}

public static async Task<bool> TryDownloadUpdate()
{
Context.Info("下载更新包中...");
try
{
var outputPath = Path.Combine(
Basics.ExecutableDirectory,
"PCL",
"Plain Craft Launcher Community Edition.exe");
if (LatestVersion == null) return false;
await _SourceController.DownloadAsync(outputPath).ConfigureAwait(false);
Context.Info("更新包下载完成");
IsUpdateDownloaded = true;
return true;
}
catch (InvalidOperationException ex)
{
if (ex.Message.Contains("不可用"))
{
Context.Warn("所有更新源均不可用", ex);
HintWrapper.Show("所有更新源均不可用,可能是网络问题", HintTheme.Error);
}
else
{
Context.Warn("下载更新包时发生未知异常", ex);
HintWrapper.Show("下载更新包时发生未知异常,可能是网络问题", HintTheme.Error);
}
}
catch (Exception ex)
{
Context.Warn("下载更新包时发生未知异常", ex);
HintWrapper.Show("下载更新包时发生未知异常,可能是网络问题", HintTheme.Error);
}
return false;
}

#endregion

#region Prompt Wrappers

private static bool _PromptUpdate()
{
if (LatestVersion == null) return false;

if (MsgBoxWrapper.Show(
$"启动器有新版本可用 ({Basics.VersionName} -> {LatestVersion.Version.Name})\r\n" +
$"是否立即下载并安装?\r\n" +
"你也可以稍后在 设置 -> 检查更新 界面中更新。",
"发现新版本", MsgBoxTheme.Info, true, "立刻更新", "以后再说") == 1) return true;

Context.Info("用户取消更新");
return false;
}

private static bool _PromptInstall()
{
if (LatestVersion == null) return false;
if (!IsUpdateDownloaded) return false;

if (MsgBoxWrapper.Show(
$"启动器有新版本可用 ({Basics.VersionName} -> {LatestVersion.Version.Name})\r\n" +
$"已自动下载,是否立即安装?\r\n" +
"你也可以稍后在 设置 -> 检查更新 界面中安装。",
"发现新版本", MsgBoxTheme.Info, true, "立刻更新", "以后再说") == 1) return true;

Context.Info("用户取消安装");
return false;
}

#endregion
}
39 changes: 39 additions & 0 deletions App/Updates/Sources/DataModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using System.Text.Json.Serialization;
using PCL.Core.Utils;

namespace PCL.Core.App.Updates.Sources;

public sealed record VersionInfoData(
[property: JsonPropertyName("name")] string Name,
[property: JsonPropertyName("code")] int Code
);

public sealed record VersionData (
[property: JsonPropertyName("version")] VersionInfoData Version,
[property: JsonPropertyName("sha256")] string Sha256,
[property: JsonPropertyName("changelog")] string ChangeLog,
[property: JsonPropertyName("patches")] string[] Patches,
[property: JsonPropertyName("downloads")] string[] Downloads
) {
public bool IsAvailable => Version.Code > Basics.VersionCode &&
SemVer.Parse(Version.Name) > SemVer.Parse(Basics.VersionName);
}

public record AnnouncementsList(
[property: JsonPropertyName("content")] AnnouncementContent[] Contents
);

public record AnnouncementContent(
[property: JsonPropertyName("title")] string Title,
[property: JsonPropertyName("detail")] string Detail,
[property: JsonPropertyName("id")] string Id,
[property: JsonPropertyName("date")] string Date,
[property: JsonPropertyName("btn1")] AnnouncementBtnInfo? Btn1,
[property: JsonPropertyName("btn2")] AnnouncementBtnInfo? Btn2
);

public record AnnouncementBtnInfo (
[property: JsonPropertyName("text")] string Text,
[property: JsonPropertyName("command")] string Command,
[property: JsonPropertyName("command_paramter")] string CommandParameter
);
34 changes: 34 additions & 0 deletions App/Updates/Sources/IUpdateSource.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using System.Threading.Tasks;

namespace PCL.Core.App.Updates.Sources;

public interface IUpdateSource
{
/// <summary>
/// 检查更新
/// </summary>
/// <returns>检查更新结果</returns>
public Task<VersionData> CheckUpdateAsync();

/// <summary>
/// 获取版本公告列表
/// </summary>
/// <returns>版本公告列表</returns>
public Task<AnnouncementsList> GetAnnouncementAsync();

/// <summary>
/// 下载更新文件
/// </summary>
/// <param name="outputPath">输出路径</param>
public Task DownloadAsync(string outputPath);

/// <summary>
/// 更新源名称
/// </summary>
public string SourceName { get; }

/// <summary>
/// 更新源是否可用
/// </summary>
public bool IsAvailable { get; }
}
118 changes: 118 additions & 0 deletions App/Updates/Sources/SourceController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
using PCL.Core.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace PCL.Core.App.Updates.Sources;

/// <summary>
/// 管理多个更新源,尝试找到可用源并调用。
/// </summary>
public sealed class SourceController
{
private readonly List<IUpdateSource> _availableSources;

private readonly SemaphoreSlim _semaphore = new(1, 1);

/// <summary>
/// 初始化并过滤出可用的更新源。
/// </summary>
public SourceController(IEnumerable<IUpdateSource> sources)
{
_availableSources = sources
.Where(s => s.IsAvailable)
.ToList();
}

/// <summary>
/// 尝试使用当前源处理操作,若失败则遍历其他可用源直至成功或无可用源。
/// </summary>
/// <param name="action">指定操作</param>
/// <typeparam name="T">返回类型</typeparam>
/// <returns>操作返回值</returns>
/// <exception cref="InvalidOperationException">所有更新源均不可用时抛出</exception>
private async Task<T> _TryFindSourceAsync<T>(Func<IUpdateSource, Task<T>> action)
{
await _semaphore.WaitAsync().ConfigureAwait(false);
try
{
foreach (var source in _availableSources)
{
try
{
var res = await action(source).ConfigureAwait(false);
_LogInfo($"源 {source.SourceName} 处理成功");
return res;
}
catch (Exception ex)
{
_LogWarning($"源 {source.SourceName} 不可用,使用下一个源", ex);
}
}

throw new InvalidOperationException("所有源均不可用");
}
finally
{
_semaphore.Release();
}
}

/// <summary>
/// 尝试使用当前源处理操作,若失败则遍历其他可用源直至成功或无可用源。
/// </summary>
/// <param name="action">指定操作</param>
/// <exception cref="InvalidOperationException">所有更新源均不可用时抛出</exception>
private async Task _TryFindSourceAsync(Func<IUpdateSource, Task> action)
{
await _TryFindSourceAsync<object?>(async s =>
{
await action(s).ConfigureAwait(false);
return null;
}).ConfigureAwait(false);
}

/// <summary>
/// 检查是否有新版本并返回结果。
/// </summary>
public Task<VersionData> CheckUpdateAsync() =>
_TryFindSourceAsync(s => s.CheckUpdateAsync());

/// <summary>
/// 获取公告列表。
/// </summary>
public Task<AnnouncementsList> GetAnnouncementListAsync() =>
_TryFindSourceAsync(s => s.GetAnnouncementAsync());

/// <summary>
/// 使用可用源下载到指定路径。
/// </summary>
public Task DownloadAsync(string outputPath) =>
_TryFindSourceAsync(s => s.DownloadAsync(outputPath));

#region Logger Wrapper

private void _LogInfo(string msg)
{
LogWrapper.Info("Update", msg);
}

private void _LogWarning(string msg, Exception? ex = null)
{
LogWrapper.Warn(ex, "Update", msg);
}

private void _LogError(string msg, Exception? ex = null)
{
LogWrapper.Error(ex, "Update", msg);
}

private void _LogTrace(string msg)
{
LogWrapper.Trace("Update", msg);
}

#endregion
}
Loading
Loading