mirror of
https://github.com/MikuLeaks/MikuSB.git
synced 2026-06-04 04:03:58 +00:00
MikuSB.Updater
This commit is contained in:
56
.github/workflows/release.yml
vendored
Normal file
56
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
name: release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- version.txt
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-release:
|
||||||
|
runs-on: windows-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-dotnet@v4
|
||||||
|
with:
|
||||||
|
dotnet-version: 9.0.x
|
||||||
|
|
||||||
|
- name: Read version
|
||||||
|
id: version
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
$line = (Get-Content version.txt | Select-Object -First 1).Trim()
|
||||||
|
$version = $line -replace '^v=', ''
|
||||||
|
if ([string]::IsNullOrWhiteSpace($version)) { throw 'version.txt is empty.' }
|
||||||
|
"version=$version" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8
|
||||||
|
"tag=v$version" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8
|
||||||
|
|
||||||
|
- name: Publish server
|
||||||
|
run: dotnet publish .\MikuSB\MikuSB.csproj -c Release -p:PublishProfile=MikuSB-Win64-MultiFile -o .\artifacts\publish\MikuSB
|
||||||
|
|
||||||
|
- name: Assemble release package
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
$packageDir = ".\artifacts\package\MikuSB-win-x64"
|
||||||
|
New-Item -ItemType Directory -Force -Path $packageDir | Out-Null
|
||||||
|
Copy-Item .\artifacts\publish\MikuSB\* $packageDir -Recurse -Force
|
||||||
|
if (-not (Test-Path "$packageDir\MikuSB.Updater.exe")) { throw 'MikuSB.Updater.exe was not bundled into the publish output.' }
|
||||||
|
Copy-Item .\version.txt $packageDir -Force
|
||||||
|
Compress-Archive -Path "$packageDir\*" -DestinationPath .\artifacts\MikuSB-win-x64.zip -Force
|
||||||
|
$hash = (Get-FileHash .\artifacts\MikuSB-win-x64.zip -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||||
|
"$hash MikuSB-win-x64.zip" | Set-Content .\artifacts\MikuSB-win-x64.zip.sha256
|
||||||
|
|
||||||
|
- name: Create release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
tag_name: ${{ steps.version.outputs.tag }}
|
||||||
|
name: ${{ steps.version.outputs.tag }}
|
||||||
|
generate_release_notes: true
|
||||||
|
files: |
|
||||||
|
artifacts/MikuSB-win-x64.zip
|
||||||
|
artifacts/MikuSB-win-x64.zip.sha256
|
||||||
49
Common/Util/BuildVersion.cs
Normal file
49
Common/Util/BuildVersion.cs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace MikuSB.Util;
|
||||||
|
|
||||||
|
public static class BuildVersion
|
||||||
|
{
|
||||||
|
public static string Current
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var assembly = Assembly.GetEntryAssembly() ?? typeof(BuildVersion).Assembly;
|
||||||
|
var value = assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
|
||||||
|
return string.IsNullOrWhiteSpace(value) ? "0.0.0" : Normalize(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string Normalize(string? value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
return "0.0.0";
|
||||||
|
|
||||||
|
var trimmed = value.Trim();
|
||||||
|
if (trimmed.StartsWith("v", StringComparison.OrdinalIgnoreCase))
|
||||||
|
trimmed = trimmed[1..];
|
||||||
|
|
||||||
|
var separatorIndex = trimmed.IndexOfAny(['-', '+']);
|
||||||
|
return separatorIndex >= 0 ? trimmed[..separatorIndex] : trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsNewer(string candidate, string current)
|
||||||
|
{
|
||||||
|
return ToComparableVersion(candidate) > ToComparableVersion(current);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Version ToComparableVersion(string? value)
|
||||||
|
{
|
||||||
|
var normalized = Normalize(value);
|
||||||
|
var parts = normalized.Split('.', StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
var padded = new int[4];
|
||||||
|
|
||||||
|
for (var i = 0; i < padded.Length; i++)
|
||||||
|
{
|
||||||
|
if (i < parts.Length && int.TryParse(parts[i], out var parsed))
|
||||||
|
padded[i] = parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Version(padded[0], padded[1], padded[2], padded[3]);
|
||||||
|
}
|
||||||
|
}
|
||||||
5
Directory.Build.props
Normal file
5
Directory.Build.props
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<Project>
|
||||||
|
<PropertyGroup>
|
||||||
|
<GenerateAssemblyInformationalVersionAttribute>false</GenerateAssemblyInformationalVersionAttribute>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
||||||
21
Directory.Build.targets
Normal file
21
Directory.Build.targets
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<Project>
|
||||||
|
<Target Name="GenerateBuildVersionInfo" BeforeTargets="BeforeCompile">
|
||||||
|
<ReadLinesFromFile File="$(MSBuildThisFileDirectory)version.txt">
|
||||||
|
<Output TaskParameter="Lines" ItemName="_BuildVersionLines" />
|
||||||
|
</ReadLinesFromFile>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<_BuildVersionRaw>@(_BuildVersionLines->'%(Identity)', '')</_BuildVersionRaw>
|
||||||
|
<_BuildVersion>$([System.String]::Copy('$(_BuildVersionRaw)').Replace('v=', '').Trim())</_BuildVersion>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<WriteLinesToFile
|
||||||
|
File="$(IntermediateOutputPath)BuildVersion.g.cs"
|
||||||
|
Overwrite="true"
|
||||||
|
Lines="// <auto-generated/>
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("$(_BuildVersion)")]" />
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="$(IntermediateOutputPath)BuildVersion.g.cs" Visible="false" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Target>
|
||||||
|
</Project>
|
||||||
12
MikuSB.Updater/MikuSB.Updater.csproj
Normal file
12
MikuSB.Updater/MikuSB.Updater.csproj
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<AssemblyName>MikuSB.Updater</AssemblyName>
|
||||||
|
<RootNamespace>MikuSB.Updater</RootNamespace>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
90
MikuSB.Updater/Program.cs
Normal file
90
MikuSB.Updater/Program.cs
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO.Compression;
|
||||||
|
|
||||||
|
var argsMap = ParseArgs(args);
|
||||||
|
if (!argsMap.TryGetValue("--package", out var packagePath)
|
||||||
|
|| !argsMap.TryGetValue("--target", out var targetDirectory)
|
||||||
|
|| !argsMap.TryGetValue("--restart", out var restartExecutable))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine("Missing required arguments.");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
argsMap.TryGetValue("--pid", out var pidValue);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (int.TryParse(pidValue, out var pid))
|
||||||
|
WaitForExit(pid);
|
||||||
|
|
||||||
|
var stagingDirectory = Path.Combine(Path.GetTempPath(), "MikuSB", "staging", Guid.NewGuid().ToString("N"));
|
||||||
|
Directory.CreateDirectory(stagingDirectory);
|
||||||
|
|
||||||
|
ZipFile.ExtractToDirectory(packagePath, stagingDirectory, overwriteFiles: true);
|
||||||
|
CopyDirectory(stagingDirectory, targetDirectory);
|
||||||
|
|
||||||
|
Process.Start(new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = restartExecutable,
|
||||||
|
UseShellExecute = false,
|
||||||
|
WorkingDirectory = targetDirectory
|
||||||
|
});
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(ex);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Dictionary<string, string> ParseArgs(string[] args)
|
||||||
|
{
|
||||||
|
var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
for (var i = 0; i < args.Length - 1; i += 2)
|
||||||
|
{
|
||||||
|
result[args[i]] = args[i + 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void WaitForExit(int pid)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var process = Process.GetProcessById(pid);
|
||||||
|
process.WaitForExit(30000);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void CopyDirectory(string sourceDirectory, string targetDirectory)
|
||||||
|
{
|
||||||
|
foreach (var directory in Directory.GetDirectories(sourceDirectory, "*", SearchOption.AllDirectories))
|
||||||
|
{
|
||||||
|
var relativePath = Path.GetRelativePath(sourceDirectory, directory);
|
||||||
|
if (ShouldSkip(relativePath))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
Directory.CreateDirectory(Path.Combine(targetDirectory, relativePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var file in Directory.GetFiles(sourceDirectory, "*", SearchOption.AllDirectories))
|
||||||
|
{
|
||||||
|
var relativePath = Path.GetRelativePath(sourceDirectory, file);
|
||||||
|
if (ShouldSkip(relativePath))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var destinationPath = Path.Combine(targetDirectory, relativePath);
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)!);
|
||||||
|
File.Copy(file, destinationPath, overwrite: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool ShouldSkip(string relativePath)
|
||||||
|
{
|
||||||
|
return relativePath.StartsWith("Config", StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
@@ -22,6 +22,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TcpSharp", "TcpSharp\TcpSha
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Proxy", "Proxy\Proxy.csproj", "{B3C4D5E6-F7A8-9012-BCDE-F12345678901}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Proxy", "Proxy\Proxy.csproj", "{B3C4D5E6-F7A8-9012-BCDE-F12345678901}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MikuSB.Updater", "MikuSB.Updater\MikuSB.Updater.csproj", "{CE0F3A4B-8C55-4A31-A1B5-A0CB1C7F0A11}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@@ -56,6 +58,10 @@ Global
|
|||||||
{B3C4D5E6-F7A8-9012-BCDE-F12345678901}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{B3C4D5E6-F7A8-9012-BCDE-F12345678901}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{B3C4D5E6-F7A8-9012-BCDE-F12345678901}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{B3C4D5E6-F7A8-9012-BCDE-F12345678901}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{B3C4D5E6-F7A8-9012-BCDE-F12345678901}.Release|Any CPU.Build.0 = Release|Any CPU
|
{B3C4D5E6-F7A8-9012-BCDE-F12345678901}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{CE0F3A4B-8C55-4A31-A1B5-A0CB1C7F0A11}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{CE0F3A4B-8C55-4A31-A1B5-A0CB1C7F0A11}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{CE0F3A4B-8C55-4A31-A1B5-A0CB1C7F0A11}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{CE0F3A4B-8C55-4A31-A1B5-A0CB1C7F0A11}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|||||||
@@ -18,4 +18,45 @@
|
|||||||
<ProjectReference Include="..\SdkServer\SdkServer.csproj" />
|
<ProjectReference Include="..\SdkServer\SdkServer.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<Target Name="BuildBundledUpdater" BeforeTargets="Build">
|
||||||
|
<MSBuild
|
||||||
|
Projects="..\MikuSB.Updater\MikuSB.Updater.csproj"
|
||||||
|
Targets="Build"
|
||||||
|
Properties="Configuration=$(Configuration);TargetFramework=$(TargetFramework)" />
|
||||||
|
</Target>
|
||||||
|
|
||||||
|
<Target Name="CopyUpdaterAfterBuild" AfterTargets="Build">
|
||||||
|
<ItemGroup>
|
||||||
|
<_UpdaterBuildArtifacts Include="..\MikuSB.Updater\bin\$(Configuration)\$(TargetFramework)\MikuSB.Updater.exe" />
|
||||||
|
<_UpdaterBuildArtifacts Include="..\MikuSB.Updater\bin\$(Configuration)\$(TargetFramework)\MikuSB.Updater.dll" />
|
||||||
|
<_UpdaterBuildArtifacts Include="..\MikuSB.Updater\bin\$(Configuration)\$(TargetFramework)\MikuSB.Updater.deps.json" />
|
||||||
|
<_UpdaterBuildArtifacts Include="..\MikuSB.Updater\bin\$(Configuration)\$(TargetFramework)\MikuSB.Updater.runtimeconfig.json" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<Copy
|
||||||
|
SourceFiles="@(_UpdaterBuildArtifacts)"
|
||||||
|
DestinationFolder="$(OutDir)"
|
||||||
|
SkipUnchangedFiles="true"
|
||||||
|
Condition="Exists('%(_UpdaterBuildArtifacts.Identity)')" />
|
||||||
|
</Target>
|
||||||
|
|
||||||
|
<Target Name="PublishBundledUpdater" AfterTargets="Publish">
|
||||||
|
<PropertyGroup>
|
||||||
|
<_UpdaterPublishDir>$(PublishDir)updater-publish\</_UpdaterPublishDir>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<MSBuild
|
||||||
|
Projects="..\MikuSB.Updater\MikuSB.Updater.csproj"
|
||||||
|
Targets="Restore;Publish"
|
||||||
|
RemoveProperties="PublishProfile"
|
||||||
|
Properties="Configuration=$(Configuration);RuntimeIdentifier=$(RuntimeIdentifier);SelfContained=true;PublishSingleFile=true;PublishDir=$(_UpdaterPublishDir)" />
|
||||||
|
|
||||||
|
<Copy
|
||||||
|
SourceFiles="$(_UpdaterPublishDir)MikuSB.Updater.exe"
|
||||||
|
DestinationFiles="$(PublishDir)MikuSB.Updater.exe"
|
||||||
|
Condition="Exists('$(_UpdaterPublishDir)MikuSB.Updater.exe')" />
|
||||||
|
|
||||||
|
<RemoveDir Directories="$(_UpdaterPublishDir)" Condition="Exists('$(_UpdaterPublishDir)')" />
|
||||||
|
</Target>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ public class LoaderManager : MikuSB
|
|||||||
|
|
||||||
// Starting the server
|
// Starting the server
|
||||||
Logger.Info(I18NManager.Translate("Server.ServerInfo.StartingServer"));
|
Logger.Info(I18NManager.Translate("Server.ServerInfo.StartingServer"));
|
||||||
|
Logger.Info($"Build version: {BuildVersion.Current}");
|
||||||
|
|
||||||
// Load the config
|
// Load the config
|
||||||
Logger.Info(I18NManager.Translate("Server.ServerInfo.LoadingItem", I18NManager.Translate("Word.Config")));
|
Logger.Info(I18NManager.Translate("Server.ServerInfo.LoadingItem", I18NManager.Translate("Word.Config")));
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using MikuSB.MikuSB.Tool;
|
|||||||
using MikuSB.GameServer.Command;
|
using MikuSB.GameServer.Command;
|
||||||
using MikuSB.GameServer.Server;
|
using MikuSB.GameServer.Server;
|
||||||
using MikuSB.Internationalization;
|
using MikuSB.Internationalization;
|
||||||
|
using MikuSB.MikuSB.Update;
|
||||||
using MikuSB.TcpSharp;
|
using MikuSB.TcpSharp;
|
||||||
using MikuSB.Util;
|
using MikuSB.Util;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
@@ -20,9 +21,12 @@ public class MikuSB
|
|||||||
public static async Task Main()
|
public static async Task Main()
|
||||||
{
|
{
|
||||||
var time = DateTime.Now;
|
var time = DateTime.Now;
|
||||||
RegisterExitEvent();
|
|
||||||
IConsole.InitConsole();
|
IConsole.InitConsole();
|
||||||
LoaderManager.InitConfig();
|
LoaderManager.InitConfig();
|
||||||
|
if (await UpdateService.TryStartSelfUpdateAsync())
|
||||||
|
return;
|
||||||
|
|
||||||
|
RegisterExitEvent();
|
||||||
await LoaderManager.InitSdkServer();
|
await LoaderManager.InitSdkServer();
|
||||||
LoaderManager.InitPacket();
|
LoaderManager.InitPacket();
|
||||||
|
|
||||||
|
|||||||
229
MikuSB/Update/UpdateService.cs
Normal file
229
MikuSB/Update/UpdateService.cs
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using MikuSB.Util;
|
||||||
|
|
||||||
|
namespace MikuSB.MikuSB.Update;
|
||||||
|
|
||||||
|
public static class UpdateService
|
||||||
|
{
|
||||||
|
private static readonly Logger Logger = new("Updater");
|
||||||
|
private static readonly bool UpdateEnabled = true;
|
||||||
|
private static readonly bool AskBeforeUpdate = true;
|
||||||
|
private static readonly string RepositoryOwner = "DevilProMT";
|
||||||
|
private static readonly string RepositoryName = "MikuSB";
|
||||||
|
private static readonly string AssetName = "MikuSB-win-x64.zip";
|
||||||
|
private static readonly int TimeoutSeconds = 5;
|
||||||
|
|
||||||
|
public static async Task<bool> TryStartSelfUpdateAsync()
|
||||||
|
{
|
||||||
|
if (!UpdateEnabled)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(RepositoryOwner)
|
||||||
|
|| string.IsNullOrWhiteSpace(RepositoryName)
|
||||||
|
|| string.IsNullOrWhiteSpace(AssetName))
|
||||||
|
{
|
||||||
|
Logger.Debug("Auto update skipped because the GitHub release source is not configured.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var updaterPath = Path.Combine(AppContext.BaseDirectory, "MikuSB.Updater.exe");
|
||||||
|
if (!File.Exists(updaterPath))
|
||||||
|
{
|
||||||
|
Logger.Debug("Auto update skipped because MikuSB.Updater.exe was not found.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Logger.Info($"Current build version: {BuildVersion.Current}");
|
||||||
|
|
||||||
|
using var client = CreateHttpClient();
|
||||||
|
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(Math.Max(1, TimeoutSeconds)));
|
||||||
|
var release = await GetLatestReleaseAsync(client, cts.Token);
|
||||||
|
if (release == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var latestVersion = BuildVersion.Normalize(release.TagName);
|
||||||
|
if (!BuildVersion.IsNewer(latestVersion, BuildVersion.Current))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var asset = release.Assets.FirstOrDefault(x =>
|
||||||
|
string.Equals(x.Name, AssetName, StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (asset == null)
|
||||||
|
{
|
||||||
|
Logger.Warn($"Latest release {release.TagName} does not contain asset {AssetName}.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (AskBeforeUpdate && !ConfirmUpdate(latestVersion))
|
||||||
|
{
|
||||||
|
Logger.Info($"Skipped update {latestVersion} by user choice.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var tempRoot = Path.Combine(Path.GetTempPath(), "MikuSB", "updates", Guid.NewGuid().ToString("N"));
|
||||||
|
Directory.CreateDirectory(tempRoot);
|
||||||
|
|
||||||
|
var packagePath = Path.Combine(tempRoot, asset.Name);
|
||||||
|
Logger.Info($"Downloading update {release.TagName}.");
|
||||||
|
await DownloadFileAsync(client, asset.DownloadUrl, packagePath, cts.Token);
|
||||||
|
|
||||||
|
var checksumAsset = release.Assets.FirstOrDefault(x =>
|
||||||
|
string.Equals(x.Name, AssetName + ".sha256", StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (checksumAsset != null)
|
||||||
|
{
|
||||||
|
var checksumPath = Path.Combine(tempRoot, checksumAsset.Name);
|
||||||
|
await DownloadFileAsync(client, checksumAsset.DownloadUrl, checksumPath, cts.Token);
|
||||||
|
VerifySha256(packagePath, checksumPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
var stagedUpdaterPath = StageUpdaterExecutable();
|
||||||
|
var process = Process.Start(new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = stagedUpdaterPath,
|
||||||
|
UseShellExecute = false,
|
||||||
|
WorkingDirectory = Path.GetDirectoryName(stagedUpdaterPath)!,
|
||||||
|
ArgumentList =
|
||||||
|
{
|
||||||
|
"--package", packagePath,
|
||||||
|
"--target", AppContext.BaseDirectory,
|
||||||
|
"--restart", Path.Combine(AppContext.BaseDirectory, "MikuSB.exe"),
|
||||||
|
"--pid", Environment.ProcessId.ToString()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (process == null)
|
||||||
|
{
|
||||||
|
Logger.Warn("Failed to start MikuSB.Updater.exe.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.Warn($"Update {latestVersion} found. Handing over to updater and shutting down.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.Warn("Auto update check failed. Continuing normal startup.", ex);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string StageUpdaterExecutable()
|
||||||
|
{
|
||||||
|
var sourceDirectory = AppContext.BaseDirectory;
|
||||||
|
var stagingDirectory = Path.Combine(Path.GetTempPath(), "MikuSB", "updater", Guid.NewGuid().ToString("N"));
|
||||||
|
Directory.CreateDirectory(stagingDirectory);
|
||||||
|
|
||||||
|
foreach (var sourcePath in Directory.EnumerateFiles(sourceDirectory, "MikuSB.Updater*", SearchOption.TopDirectoryOnly))
|
||||||
|
{
|
||||||
|
var destinationPath = Path.Combine(stagingDirectory, Path.GetFileName(sourcePath));
|
||||||
|
File.Copy(sourcePath, destinationPath, overwrite: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
var stagedUpdaterPath = Path.Combine(stagingDirectory, "MikuSB.Updater.exe");
|
||||||
|
if (!File.Exists(stagedUpdaterPath))
|
||||||
|
throw new FileNotFoundException("Failed to stage MikuSB.Updater.exe.", stagedUpdaterPath);
|
||||||
|
|
||||||
|
return stagedUpdaterPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ConfirmUpdate(string latestVersion)
|
||||||
|
{
|
||||||
|
Console.Write($"New version found: {BuildVersion.Current} -> {latestVersion}. Update now? [Y/n]: ");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var key = Console.ReadKey(intercept: true);
|
||||||
|
Console.WriteLine();
|
||||||
|
return key.Key is ConsoleKey.Enter or ConsoleKey.Y;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
Console.WriteLine();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HttpClient CreateHttpClient()
|
||||||
|
{
|
||||||
|
var client = new HttpClient
|
||||||
|
{
|
||||||
|
Timeout = TimeSpan.FromSeconds(Math.Max(1, TimeoutSeconds))
|
||||||
|
};
|
||||||
|
|
||||||
|
client.DefaultRequestHeaders.UserAgent.Add(
|
||||||
|
new ProductInfoHeaderValue("MikuSB-Updater", BuildVersion.Current));
|
||||||
|
client.DefaultRequestHeaders.Accept.Add(
|
||||||
|
new MediaTypeWithQualityHeaderValue("application/vnd.github+json"));
|
||||||
|
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<GitHubReleaseResponse?> GetLatestReleaseAsync(
|
||||||
|
HttpClient client,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var requestUri =
|
||||||
|
$"https://api.github.com/repos/{RepositoryOwner}/{RepositoryName}/releases/latest";
|
||||||
|
using var response = await client.GetAsync(requestUri, cancellationToken);
|
||||||
|
|
||||||
|
if (response.StatusCode is HttpStatusCode.NotFound or HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden)
|
||||||
|
{
|
||||||
|
Logger.Warn("Latest GitHub release is not accessible. This is expected while the repository remains private.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
|
||||||
|
return await JsonSerializer.DeserializeAsync<GitHubReleaseResponse>(stream, new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
PropertyNameCaseInsensitive = true
|
||||||
|
}, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task DownloadFileAsync(
|
||||||
|
HttpClient client,
|
||||||
|
string downloadUrl,
|
||||||
|
string destinationPath,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
using var response = await client.GetAsync(downloadUrl, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
await using var source = await response.Content.ReadAsStreamAsync(cancellationToken);
|
||||||
|
await using var destination = File.Create(destinationPath);
|
||||||
|
await source.CopyToAsync(destination, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void VerifySha256(string packagePath, string checksumPath)
|
||||||
|
{
|
||||||
|
var expected = File.ReadAllText(checksumPath).Split(' ', StringSplitOptions.RemoveEmptyEntries)[0].Trim();
|
||||||
|
var actual = Convert.ToHexString(System.Security.Cryptography.SHA256.HashData(File.ReadAllBytes(packagePath)))
|
||||||
|
.ToLowerInvariant();
|
||||||
|
|
||||||
|
if (!string.Equals(expected, actual, StringComparison.OrdinalIgnoreCase))
|
||||||
|
throw new InvalidDataException("Downloaded update package checksum does not match the release checksum.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class GitHubReleaseResponse
|
||||||
|
{
|
||||||
|
[JsonPropertyName("tag_name")]
|
||||||
|
public string TagName { get; set; } = "";
|
||||||
|
|
||||||
|
[JsonPropertyName("assets")]
|
||||||
|
public List<GitHubReleaseAssetResponse> Assets { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class GitHubReleaseAssetResponse
|
||||||
|
{
|
||||||
|
[JsonPropertyName("name")]
|
||||||
|
public string Name { get; set; } = "";
|
||||||
|
|
||||||
|
[JsonPropertyName("browser_download_url")]
|
||||||
|
public string DownloadUrl { get; set; } = "";
|
||||||
|
}
|
||||||
1
version.txt
Normal file
1
version.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
v=0.1
|
||||||
Reference in New Issue
Block a user