Init Commit

This commit is contained in:
Lightczx
2023-12-26 14:26:14 +08:00
parent f661bba838
commit 5df406c785
12 changed files with 577 additions and 0 deletions

6
src/Snap.Hutao.Deployment/.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
.vs/
bin/
obj/
*.pubxml
*.user

View File

@@ -0,0 +1,24 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Net.Http;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
namespace Snap.Hutao.Deployment;
internal static class HttpClientExtension
{
public static Task<HttpResponseMessage> HeadAsync(this HttpClient httpClient, [StringSyntax(StringSyntaxAttribute.Uri)] string? requestUri, HttpCompletionOption completionOption)
{
return httpClient.SendAsync(PrivateCreateRequestMessage(httpClient, HttpMethod.Get, CreateUri(default!, requestUri)), completionOption, CancellationToken.None);
}
// private HttpRequestMessage CreateRequestMessage(HttpMethod method, Uri? uri)
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "CreateRequestMessage")]
private static extern HttpRequestMessage PrivateCreateRequestMessage(HttpClient httpClient, HttpMethod method, Uri? uri);
// private static Uri? CreateUri(string? uri)
[UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "CreateUri")]
private static extern Uri? CreateUri(HttpClient discard, string? uri);
}

View File

@@ -0,0 +1,168 @@
using Microsoft.Win32.SafeHandles;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
namespace Snap.Hutao.Deployment;
internal sealed class HttpShardCopyWorker<TStatus> : IDisposable
{
private const int ShardSize = 4 * 1024 * 1024;
private readonly HttpClient httpClient;
private readonly string sourceUrl;
private readonly Func<long, long, TStatus> statusFactory;
private readonly long contentLength;
private readonly int bufferSize;
private readonly SafeFileHandle destFileHandle;
private readonly List<Shard> shards;
private HttpShardCopyWorker(HttpShardCopyWorkerOptions<TStatus> options)
{
httpClient = options.HttpClient;
sourceUrl = options.SourceUrl;
statusFactory = options.StatusFactory;
contentLength = options.ContentLength;
bufferSize = options.BufferSize;
destFileHandle = options.GetFileHandle();
shards = CalculateShards(contentLength);
static List<Shard> CalculateShards(long contentLength)
{
List<Shard> shards = [];
long currentOffset = 0;
while (currentOffset < contentLength)
{
long end = Math.Min(currentOffset + ShardSize, contentLength) - 1;
shards.Add(new Shard(currentOffset, end));
currentOffset = end + 1;
}
return shards;
}
}
public static async ValueTask<HttpShardCopyWorker<TStatus>> CreateAsync(HttpShardCopyWorkerOptions<TStatus> options)
{
await options.DetectContentLengthAsync().ConfigureAwait(false);
return new(options);
}
public Task CopyAsync(IProgress<TStatus> progress, CancellationToken token = default)
{
ShardProgress shardProgress = new(progress, statusFactory, contentLength);
return Parallel.ForEachAsync(shards, token, (shard, token) => CopyShardAsync(shard, shardProgress, token));
async ValueTask CopyShardAsync(Shard shard, IProgress<ShardStatus> progress, CancellationToken token)
{
ValueStopwatch stopwatch = ValueStopwatch.StartNew();
HttpRequestMessage request = new(HttpMethod.Get, sourceUrl)
{
Headers = { Range = new(shard.StartOffset, shard.EndOffset), },
};
using (request)
{
using (HttpResponseMessage response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token).ConfigureAwait(false))
{
response.EnsureSuccessStatusCode();
Memory<byte> buffer = new byte[bufferSize];
using (Stream stream = await response.Content.ReadAsStreamAsync(token).ConfigureAwait(false))
{
int totalBytesRead = 0;
int bytesReadAfterPreviousReport = 0;
do
{
int bytesRead = await stream.ReadAsync(buffer, token).ConfigureAwait(false);
if (bytesRead <= 0)
{
progress.Report(new(bytesReadAfterPreviousReport));
bytesReadAfterPreviousReport = 0;
break;
}
await RandomAccess.WriteAsync(destFileHandle, buffer[..bytesRead], shard.StartOffset + totalBytesRead, token).ConfigureAwait(false);
totalBytesRead += bytesRead;
bytesReadAfterPreviousReport += bytesRead;
if (stopwatch.GetElapsedTime().TotalMilliseconds > 500)
{
progress.Report(new(bytesReadAfterPreviousReport));
bytesReadAfterPreviousReport = 0;
stopwatch = ValueStopwatch.StartNew();
}
}
while (true);
}
}
}
}
}
public void Dispose()
{
destFileHandle.Dispose();
}
private sealed class Shard
{
public Shard(long startOffset, long endOffset)
{
StartOffset = startOffset;
EndOffset = endOffset;
}
public long StartOffset { get; }
public long EndOffset { get; }
}
private sealed class ShardStatus
{
public ShardStatus(int bytesRead)
{
BytesRead = bytesRead;
}
public int BytesRead { get; }
}
private sealed class ShardProgress : IProgress<ShardStatus>
{
private readonly IProgress<TStatus> workerProgress;
private readonly Func<long, long, TStatus> statusFactory;
private readonly long contentLength;
private readonly object syncRoot = new();
private ValueStopwatch stopwatch = ValueStopwatch.StartNew();
private long totalBytesRead;
public ShardProgress(IProgress<TStatus> workerProgress, Func<long, long, TStatus> statusFactory, long contentLength)
{
this.workerProgress = workerProgress;
this.statusFactory = statusFactory;
this.contentLength = contentLength;
}
public void Report(ShardStatus value)
{
Interlocked.Add(ref totalBytesRead, value.BytesRead);
if (stopwatch.GetElapsedTime().TotalMilliseconds >= 500 || totalBytesRead == contentLength)
{
lock (syncRoot)
{
if (stopwatch.GetElapsedTime().TotalMilliseconds >= 500 || totalBytesRead == contentLength)
{
workerProgress.Report(statusFactory(totalBytesRead, contentLength));
stopwatch = ValueStopwatch.StartNew();
}
}
}
}
}
}

View File

@@ -0,0 +1,42 @@
using Microsoft.Win32.SafeHandles;
using System;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
namespace Snap.Hutao.Deployment;
internal sealed class HttpShardCopyWorkerOptions<TStatus>
{
public HttpClient HttpClient { get; set; } = default!;
public string SourceUrl { get; set; } = default!;
public string DestinationFilePath { get; set; } = default!;
public long ContentLength { get; private set; }
public Func<long, long, TStatus> StatusFactory { get; set; } = default!;
public int BufferSize { get; set; } = 80 * 1024;
public SafeFileHandle GetFileHandle()
{
return File.OpenHandle(DestinationFilePath, FileMode.Create, FileAccess.Write, FileShare.None, FileOptions.RandomAccess | FileOptions.Asynchronous, ContentLength);
}
public async ValueTask DetectContentLengthAsync()
{
if (ContentLength > 0)
{
return;
}
HttpResponseMessage response = await HttpClient.HeadAsync(SourceUrl, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
long contentLength = response.Content.Headers.ContentLength ?? 0;
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(contentLength);
ContentLength = contentLength;
}
}

View File

@@ -0,0 +1,137 @@
using System;
using System.CommandLine.Invocation;
using System.Diagnostics;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
using Windows.Management.Deployment;
namespace Snap.Hutao.Deployment;
internal static class Invocation
{
public static async Task RunDeploymentAsync(InvocationContext context)
{
string? path = context.ParseResult.GetValueForOption(InvocationOptions.PackagePath);
string? name = context.ParseResult.GetValueForOption(InvocationOptions.FamilyName);
bool isUpdateMode = context.ParseResult.GetValueForOption(InvocationOptions.UpdateBehavior);
ArgumentException.ThrowIfNullOrEmpty(path);
Console.WriteLine($"""
PackagePath: {path}
FamilyName: {name}
------------------------------------------------------------
""");
if (!File.Exists(path))
{
Console.WriteLine($"Package file not found.");
if (isUpdateMode)
{
Console.WriteLine("Exit in 10 seconds...");
await Task.Delay(10000);
return;
}
else
{
Console.WriteLine("Start downloading package...");
await DownloadPackageAsync(path);
}
}
try
{
Console.WriteLine("Initializing PackageManager...");
PackageManager packageManager = new();
AddPackageOptions addPackageOptions = new()
{
ForceAppShutdown = true,
RetainFilesOnFailure = true,
StageInPlace = true,
};
Console.WriteLine("Start deploying...");
IProgress<DeploymentProgress> progress = new Progress<DeploymentProgress>(p =>
{
Console.WriteLine($"[Deploying]: State: {p.state} Progress: {p.percentage}%");
});
DeploymentResult result = await packageManager.AddPackageByUriAsync(new Uri(path), addPackageOptions).AsTask(progress);
if (result.IsRegistered)
{
Console.WriteLine("Package deployed.");
if (string.IsNullOrEmpty(name))
{
Console.WriteLine("FamilyName not provided, enumerating packages.");
foreach (Windows.ApplicationModel.Package package in packageManager.FindPackages())
{
if (package is { DisplayName: "Snap Hutao", PublisherDisplayName: "DGP Studio" })
{
name = package.Id.FamilyName;
Console.WriteLine($"Package found: {name}");
}
}
}
Console.WriteLine("Starting app...");
Process.Start(new ProcessStartInfo()
{
UseShellExecute = true,
FileName = $@"shell:AppsFolder\{name}!App",
});
}
else
{
Console.WriteLine($"""
ActivityId: {result.ActivityId}
ExtendedErrorCode: {result.ExtendedErrorCode}
ErrorText: {result.ErrorText}
Exit in 10 seconds...
""");
await Task.Delay(10000);
}
}
catch (Exception ex)
{
Console.WriteLine($"""
Exception occured:
{ex}
Exit in 10 seconds...
""");
await Task.Delay(10000);
}
}
private static async Task DownloadPackageAsync(string packagePath)
{
using (HttpClient httpClient = new())
{
HttpShardCopyWorkerOptions<PackageDownloadStatus> options = new()
{
HttpClient = httpClient,
SourceUrl = "https://api.snapgenshin.com/patch/hutao/download",
DestinationFilePath = packagePath,
StatusFactory = (bytesRead, totalBytes) => new PackageDownloadStatus(bytesRead, totalBytes),
};
using (HttpShardCopyWorker<PackageDownloadStatus> worker = await HttpShardCopyWorker<PackageDownloadStatus>.CreateAsync(options).ConfigureAwait(false))
{
Progress<PackageDownloadStatus> progress = new(ConsoleWriteProgress);
await worker.CopyAsync(progress).ConfigureAwait(false);
}
}
static void ConsoleWriteProgress(PackageDownloadStatus status)
{
Console.Write($"\r{status.ProgressDescription}");
}
}
}

View File

@@ -0,0 +1,22 @@
using System;
using System.CommandLine;
using System.IO;
namespace Snap.Hutao.Deployment;
internal static class InvocationOptions
{
public static readonly Option<string> PackagePath = new(
"--package-path",
() => Path.Combine(AppContext.BaseDirectory, "Snap.Hutao.msix"),
"The path of the package to be deployed.");
public static readonly Option<string> FamilyName = new(
"--family-name",
"The family name of the app to be updated.");
public static readonly Option<bool> UpdateBehavior = new(
"--update-behavior",
() => false,
"Change behavior of the tool into update mode");
}

View File

@@ -0,0 +1,45 @@
namespace Snap.Hutao.Deployment;
internal sealed class PackageDownloadStatus
{
public PackageDownloadStatus(long bytesRead, long totalBytes)
{
ProgressDescription = bytesRead != totalBytes
? $"Download Progress: {ToFileSizeString(bytesRead),8}/{ToFileSizeString(totalBytes),8} | {(double)bytesRead / totalBytes,8:P3}"
: "Download Completed\n";
}
public string ProgressDescription { get; }
private static string ToFileSizeString(long size)
{
if (size < 1024)
{
return size.ToString("F0") + " bytes";
}
else if ((size >> 10) < 1024)
{
return (size / 1024F).ToString("F1") + " KB";
}
else if ((size >> 20) < 1024)
{
return ((size >> 10) / 1024F).ToString("F1") + " MB";
}
else if ((size >> 30) < 1024)
{
return ((size >> 20) / 1024F).ToString("F1") + " GB";
}
else if ((size >> 40) < 1024)
{
return ((size >> 30) / 1024F).ToString("F1") + " TB";
}
else if ((size >> 50) < 1024)
{
return ((size >> 40) / 1024F).ToString("F1") + " PB";
}
else
{
return ((size >> 50) / 1024F).ToString("F1") + " EB";
}
}
}

View File

@@ -0,0 +1,23 @@
using System.CommandLine;
using System.Threading.Tasks;
namespace Snap.Hutao.Deployment;
internal static class Program
{
internal static async Task<int> Main(string[] args)
{
string description = $@"
Snap Hutao Updater
Copyright (c) DGP Studio. All rights reserved.
";
RootCommand root = new(description);
root.AddOption(InvocationOptions.PackagePath);
root.AddOption(InvocationOptions.FamilyName);
root.AddOption(InvocationOptions.UpdateBehavior);
root.SetHandler(Invocation.RunDeploymentAsync);
return await root.InvokeAsync(args);
}
}

View File

@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0-windows10.0.22621.0</TargetFramework>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
<Platforms>x64</Platforms>
<PublishSingleFile>true</PublishSingleFile>
<PublishTrimmed>true</PublishTrimmed>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<SelfContained>true</SelfContained>
<DebugType>embedded</DebugType>
<ApplicationManifest>app.manifest</ApplicationManifest>
</PropertyGroup>
<ItemGroup>
<None Remove=".gitignore" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,31 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.8.34330.188
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Snap.Hutao.Deployment", "Snap.Hutao.Deployment.csproj", "{B457BB36-A85E-4C2B-91C1-9453949445E3}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{B457BB36-A85E-4C2B-91C1-9453949445E3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B457BB36-A85E-4C2B-91C1-9453949445E3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B457BB36-A85E-4C2B-91C1-9453949445E3}.Debug|x64.ActiveCfg = Debug|x64
{B457BB36-A85E-4C2B-91C1-9453949445E3}.Debug|x64.Build.0 = Debug|x64
{B457BB36-A85E-4C2B-91C1-9453949445E3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B457BB36-A85E-4C2B-91C1-9453949445E3}.Release|Any CPU.Build.0 = Release|Any CPU
{B457BB36-A85E-4C2B-91C1-9453949445E3}.Release|x64.ActiveCfg = Release|x64
{B457BB36-A85E-4C2B-91C1-9453949445E3}.Release|x64.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {A7AE25C3-D7A1-4F58-809B-786CC4CA6951}
EndGlobalSection
EndGlobal

View File

@@ -0,0 +1,29 @@
using System;
using System.Diagnostics;
namespace Snap.Hutao.Deployment;
internal readonly struct ValueStopwatch
{
private readonly long startTimestamp;
private ValueStopwatch(long startTimestamp)
{
this.startTimestamp = startTimestamp;
}
public bool IsActive
{
get => startTimestamp != 0;
}
public static ValueStopwatch StartNew()
{
return new(Stopwatch.GetTimestamp());
}
public TimeSpan GetElapsedTime()
{
return Stopwatch.GetElapsedTime(startTimestamp);
}
}

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="MyApplication.app"/>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security>
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
<!--https://learn.microsoft.com/en-us/previous-versions/bb756929(v=msdn.10)-->
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
</requestedPrivileges>
</security>
</trustInfo>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<!--https://docs.microsoft.com/windows/win32/fileio/maximum-file-path-limitation-->
<longPathAware xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true</longPathAware>
</windowsSettings>
</application>
</assembly>