mirror of
https://github.com/MikuLeaks/MikuSB.git
synced 2026-06-04 13:23:58 +00:00
MikuSB.Updater
This commit is contained in:
@@ -18,4 +18,45 @@
|
||||
<ProjectReference Include="..\SdkServer\SdkServer.csproj" />
|
||||
</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>
|
||||
|
||||
@@ -46,6 +46,7 @@ public class LoaderManager : MikuSB
|
||||
|
||||
// Starting the server
|
||||
Logger.Info(I18NManager.Translate("Server.ServerInfo.StartingServer"));
|
||||
Logger.Info($"Build version: {BuildVersion.Current}");
|
||||
|
||||
// Load the 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.Server;
|
||||
using MikuSB.Internationalization;
|
||||
using MikuSB.MikuSB.Update;
|
||||
using MikuSB.TcpSharp;
|
||||
using MikuSB.Util;
|
||||
using System.Globalization;
|
||||
@@ -20,9 +21,12 @@ public class MikuSB
|
||||
public static async Task Main()
|
||||
{
|
||||
var time = DateTime.Now;
|
||||
RegisterExitEvent();
|
||||
IConsole.InitConsole();
|
||||
LoaderManager.InitConfig();
|
||||
if (await UpdateService.TryStartSelfUpdateAsync())
|
||||
return;
|
||||
|
||||
RegisterExitEvent();
|
||||
await LoaderManager.InitSdkServer();
|
||||
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; } = "";
|
||||
}
|
||||
Reference in New Issue
Block a user