Skip to content

自更新 ​

自更新的流程是:拉清单 → 比版本 → 选形态 → 下载校验 → 退到后台脚本 → 重启。中间任何一步失败都不会让启动器变砖。

更新清单 ​

服务端放一个 latest.json,默认地址写在代码里:

csharp
public const string DefaultManifestUrl = "https://starcloudfur.cn/downloads/latest.json";

设置项 update_manifest_url 可以覆盖它(测试时指向本地服务器)。清单格式:

json
{
  "version": "0.3.9",
  "notes": "更新内容(中文)",
  "notes_en": "Release notes (English)",
  "published_at": "2026-10-22T15:30:12Z",
  "setup":    { "file": "Starcloud Minecraft Launcher_0.3.9_x64-setup-20261022-153012.exe", "sha256": "…", "size": 62345678 },
  "portable": { "file": "Starcloud Minecraft Launcher_0.3.9_x64-portable-20261022-153012.zip", "sha256": "…", "size": 87234567 }
}

file 只是文件名,实际 URL 是「清单 URL 所在目录 + 文件名」:

csharp
private Uri ResolveArtifactUrl(string fileName)
{
    if (!Uri.TryCreate(ManifestUrl, UriKind.Absolute, out var baseUri) || baseUri.Scheme is not ("http" or "https"))
        throw new InvalidOperationException($"清单地址无效:{ManifestUrl}");
    return new Uri(baseUri, fileName);
}

所以发布只要把三个文件(两个产物 + 清单)拷到服务器同一个目录,不用改配置。

版本比较规则 ​

候选版本比当前版本新才提示更新。任一方解析失败,或候选不比当前新,一律返回 false——安全侧不误升级。TryParseVersion 会先切掉 +commit 后缀(0.3.9+abc123 → 0.3.9)。

坑一:System.Version 把缺省段当 -1 ​

System.Version 里没写的段不是 0,是 -1:

csharp
new Version("0.2.1").Build        // -1
new Version("0.2.1").CompareTo(new Version("0.2.1.0"))  // -1,被判为"更旧"

"0.2.1" 和 "0.2.1.0" 直接比是不相等的。所以比较前必须归一化:

csharp
private static Version NormalizeVersion(Version v) => new(
    v.Major, v.Minor, Math.Max(v.Build, 0), Math.Max(v.Revision, 0));

不归一化的后果很具体:清单写 0.2.1、程序集版本是 0.2.1.0,用户永远看不到更新提示。

坑二:latest.json 必须 UTF-8 无 BOM ​

这是整个自更新功能最容易整体失效的一点。

System.Text.Json 的字符串重载遇到开头的 U+FEFF 会直接判定为非法 JSON。而 Windows PowerShell 5.1 的 Set-Content -Encoding UTF8 默认写 BOM。

打包脚本因此改用:

powershell
[IO.File]::WriteAllText($manifestPath, $json, (New-Object System.Text.UTF8Encoding($false)))

运行时还有一道兜底:(await _http.GetStringAsync(ManifestUrl, ct)).TrimStart('\uFEFF')——手工放的清单不至于把整条更新路堵死。

反过来:生成的脚本需要 BOM

清单要无 BOM,但更新脚本要带 BOM——它是给 Windows PowerShell 5.1 执行的,脚本里有中文路径:

csharp
File.WriteAllText(scriptPath, script, new UTF8Encoding(encoderShouldEmitUTF8Identifier: true));

一个无 BOM,一个有 BOM,方向相反,理由不同。

两种安装形态 ​

DetectInstallForm() 判当前是哪种:注册表 InstallLocation 指向当前目录,或目录里有 uninstall.exe → NSIS 安装版;其余情况 → 便携版。

NSIS 安装版:下载 setup.exe,退出后跑 setup /S 静默原地升级(NSIS 静默安装自动沿用注册表里的原安装目录),再重启,最后自删脚本。

便携版:下载 zip,解压到 <数据根>/update/staging/,退出后用 robocopy /MIR 覆盖回 exe 所在目录,再重启:

powershell
& robocopy '@STAGING@' '@APPDIR@' /MIR /R:2 /W:1 /NFL /NDL /NJH /NJS /NP | Out-Null
if ($LASTEXITCODE -ge 8) { exit 1 }

用 /MIR 而不是覆盖复制,是为了跟 NSIS 的 RMDir /r 语义对齐——杜绝新旧 dll 混装。混装的典型症状是「更新完启动崩,报某个方法的 MissingMethodException」。

两种脚本都先等两轮退出:等发起更新的那个实例(最多 60 秒),再等所有 Starcloud.App 实例(覆盖多开)。

清理残留会把缓存一起删 ​

CleanupLeftovers() 在启动时清空整个自更新工作目录:

csharp
public static void CleanupLeftovers()
{
    try
    {
        if (Directory.Exists(LauncherPaths.UpdateDir))
            Directory.Delete(LauncherPaths.UpdateDir, recursive: true);
    }
    catch { /* 被占用等场景下忽略,下次再清 */ }
    try { Directory.CreateDirectory(LauncherPaths.UpdateDir); } catch { }
}

UpdateDir 下面有 downloads/(已下载的 setup / zip)和 staging/(解压好的新版)。整个目录被删,所以已下载校验通过的更新包缓存也没了——下次检查要重新下一遍。

这本身不是 bug(清理残留是必须的),但要知道:刚下载完更新包还没重启就退出启动器,那个包会被删掉。

校验

下载先写 .part,校验通过才 Move 到正式名。校验看两样:文件大小(清单里 size > 0 时)和 SHA256(清单里给了才校验)。已存在的文件会先比对一遍,匹配就跳过下载。

相关链接:打包与安装器 · 代码约定 · 常见坑速查

基于 MIT 许可证(有修改)开源