From 224c4e52ea8850415070afe446921e3f2833608a Mon Sep 17 00:00:00 2001 From: Lightczx <1686188646@qq.com> Date: Mon, 29 Jan 2024 15:51:09 +0800 Subject: [PATCH] Activation using NamedPipe --- src/Snap.Hutao/Snap.Hutao/App.xaml.cs | 31 +++--- .../Core/ExceptionService/HutaoException.cs | 10 +- .../Core/ExceptionService/ThrowHelper.cs | 17 ++++ .../Core/IO/Http/Proxy/DynamicHttpProxy.cs | 1 - .../Snap.Hutao/Core/LifeCycle/Activation.cs | 95 +++++++------------ .../AppActivationArgumentsExtensions.cs | 2 +- .../LifeCycle/HutaoActivationArguments.cs | 52 ++++++++++ .../Core/LifeCycle/HutaoActivationKind.cs | 11 +++ .../Snap.Hutao/Core/LifeCycle/IActivation.cs | 23 +---- .../NamedPipeClientStreamExtension.cs | 22 +++++ .../InterProcess/PipePacketCommand.cs | 11 +++ .../InterProcess/PipePacketContentType.cs | 10 ++ .../InterProcess/PipePacketHeader.cs | 17 ++++ .../LifeCycle/InterProcess/PipePacketType.cs | 12 +++ .../InterProcess/PrivateNamedPipeClient.cs | 55 +++++++++++ .../PrivateNamedPipeMessageDispatcher.cs | 21 ++++ .../InterProcess/PrivateNamedPipeServer.cs | 81 ++++++++++++++++ .../Service/Update/IUpdateService.cs | 2 +- .../Service/Update/UpdateService.cs | 23 +++-- .../ViewModel/Feedback/FeedbackViewModel.cs | 14 +-- 20 files changed, 388 insertions(+), 122 deletions(-) create mode 100644 src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/HutaoActivationArguments.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/HutaoActivationKind.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/InterProcess/NamedPipeClientStreamExtension.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/InterProcess/PipePacketCommand.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/InterProcess/PipePacketContentType.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/InterProcess/PipePacketHeader.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/InterProcess/PipePacketType.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/InterProcess/PrivateNamedPipeClient.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/InterProcess/PrivateNamedPipeMessageDispatcher.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/InterProcess/PrivateNamedPipeServer.cs diff --git a/src/Snap.Hutao/Snap.Hutao/App.xaml.cs b/src/Snap.Hutao/Snap.Hutao/App.xaml.cs index bfa9a750..5d174557 100644 --- a/src/Snap.Hutao/Snap.Hutao/App.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/App.xaml.cs @@ -6,6 +6,7 @@ using Microsoft.Windows.AppLifecycle; using Snap.Hutao.Core; using Snap.Hutao.Core.ExceptionService; using Snap.Hutao.Core.LifeCycle; +using Snap.Hutao.Core.LifeCycle.InterProcess; using Snap.Hutao.Core.Shell; using System.Diagnostics; @@ -36,8 +37,6 @@ public sealed partial class App : Application ---------------------------------------------------------------- """; - private const string AppInstanceKey = "main"; - private readonly IServiceProvider serviceProvider; private readonly IActivation activation; private readonly ILogger logger; @@ -63,25 +62,21 @@ public sealed partial class App : Application try { AppActivationArguments activatedEventArgs = AppInstance.GetCurrent().GetActivatedEventArgs(); - AppInstance firstInstance = AppInstance.FindOrRegisterForKey(AppInstanceKey); - if (firstInstance.IsCurrent) + if (serviceProvider.GetRequiredService().TryRedirectActivationTo(activatedEventArgs)) { - logger.LogInformation(ConsoleBanner); - LogDiagnosticInformation(); - - // manually invoke - activation.NonRedirectToActivate(firstInstance, activatedEventArgs); - activation.InitializeWith(firstInstance); - - serviceProvider.GetRequiredService().ConfigureAsync().SafeForget(); - } - else - { - // Redirect the activation (and args) to the "main" instance, and exit. - firstInstance.RedirectActivationTo(activatedEventArgs); - Process.GetCurrentProcess().Kill(); + Exit(); + return; } + + logger.LogInformation(ConsoleBanner); + LogDiagnosticInformation(); + + // manually invoke + activation.Activate(HutaoActivationArguments.FromAppActivationArguments(activatedEventArgs)); + activation.Initialize(); + + serviceProvider.GetRequiredService().ConfigureAsync().SafeForget(); } catch { diff --git a/src/Snap.Hutao/Snap.Hutao/Core/ExceptionService/HutaoException.cs b/src/Snap.Hutao/Snap.Hutao/Core/ExceptionService/HutaoException.cs index 543b0098..5aa457ba 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/ExceptionService/HutaoException.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/ExceptionService/HutaoException.cs @@ -11,10 +11,18 @@ internal sealed class HutaoException : Exception Kind = kind; } - public HutaoException(string message, Exception? innerException) + private HutaoException(string message, Exception? innerException) : base($"{message}\n{innerException?.Message}", innerException) { } public HutaoExceptionKind Kind { get; private set; } + + public static void ThrowIf(bool condition, HutaoExceptionKind kind, string message, Exception? innerException = default) + { + if (condition) + { + throw new HutaoException(kind, message, innerException); + } + } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/ExceptionService/ThrowHelper.cs b/src/Snap.Hutao/Snap.Hutao/Core/ExceptionService/ThrowHelper.cs index 2fcc3a8d..5689cc8b 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/ExceptionService/ThrowHelper.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/ExceptionService/ThrowHelper.cs @@ -3,6 +3,7 @@ using Snap.Hutao.Service.Game; using Snap.Hutao.Service.Game.Package; +using System.IO; using System.Runtime.CompilerServices; namespace Snap.Hutao.Core.ExceptionService; @@ -35,6 +36,22 @@ internal static class ThrowHelper throw new GameFileOperationException(message, inner); } + [DoesNotReturn] + [MethodImpl(MethodImplOptions.NoInlining)] + public static InvalidDataException InvalidData(string message, Exception? inner = default) + { + throw new InvalidDataException(message, inner); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static void InvalidDataIf([DoesNotReturnIf(true)] bool condition, string message, Exception? inner = default) + { + if (condition) + { + throw new InvalidDataException(message, inner); + } + } + [DoesNotReturn] [MethodImpl(MethodImplOptions.NoInlining)] public static InvalidOperationException InvalidOperation(string message, Exception? inner = default) diff --git a/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/Proxy/DynamicHttpProxy.cs b/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/Proxy/DynamicHttpProxy.cs index bd7dc3f2..f40b8a4f 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/Proxy/DynamicHttpProxy.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/Proxy/DynamicHttpProxy.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. using CommunityToolkit.Mvvm.ComponentModel; -using Snap.Hutao.Web; using Snap.Hutao.Win32.Registry; using System.Net; using System.Reflection; diff --git a/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/Activation.cs b/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/Activation.cs index 4e4bbfb4..04fbba83 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/Activation.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/Activation.cs @@ -4,6 +4,7 @@ using CommunityToolkit.WinUI.Notifications; using Microsoft.Extensions.Caching.Memory; using Microsoft.Windows.AppLifecycle; +using Snap.Hutao.Core.LifeCycle.InterProcess; using Snap.Hutao.Core.Setting; using Snap.Hutao.Service.DailyNote; using Snap.Hutao.Service.Discord; @@ -24,24 +25,9 @@ namespace Snap.Hutao.Core.LifeCycle; [SuppressMessage("", "CA1001")] internal sealed partial class Activation : IActivation { - /// - /// 操作 - /// public const string Action = nameof(Action); - - /// - /// Uid - /// public const string Uid = nameof(Uid); - - /// - /// 启动游戏启动参数 - /// public const string LaunchGame = nameof(LaunchGame); - - /// - /// 从剪贴板导入成就 - /// public const string ImportUIAFFromClipboard = nameof(ImportUIAFFromClipboard); private const string CategoryAchievement = "ACHIEVEMENT"; @@ -55,29 +41,20 @@ internal sealed partial class Activation : IActivation private readonly SemaphoreSlim activateSemaphore = new(1); /// - public void Activate(object? sender, AppActivationArguments args) + public void Activate(HutaoActivationArguments args) { - _ = sender; - if (!ToastNotificationManagerCompat.WasCurrentProcessToastActivated()) + if (ToastNotificationManagerCompat.WasCurrentProcessToastActivated()) { - HandleActivationAsync(args, true).SafeForget(); + return; } + + HandleActivationAsync(args).SafeForget(); } /// - public void NonRedirectToActivate(object? sender, AppActivationArguments args) + public void Initialize() { - _ = sender; - if (!ToastNotificationManagerCompat.WasCurrentProcessToastActivated()) - { - HandleActivationAsync(args, false).SafeForget(); - } - } - - /// - public void InitializeWith(AppInstance appInstance) - { - appInstance.Activated += Activate; + serviceProvider.GetRequiredService().RunAsync().SafeForget(); ToastNotificationManagerCompat.OnActivated += NotificationActivate; } @@ -95,44 +72,40 @@ internal sealed partial class Activation : IActivation } } - private async ValueTask HandleActivationAsync(AppActivationArguments args, bool isRedirected) + private async ValueTask HandleActivationAsync(HutaoActivationArguments args) { if (activateSemaphore.CurrentCount > 0) { using (await activateSemaphore.EnterAsync().ConfigureAwait(false)) { - await HandleActivationCoreAsync(args, isRedirected).ConfigureAwait(false); + await HandleActivationCoreAsync(args).ConfigureAwait(false); } } } - private async ValueTask HandleActivationCoreAsync(AppActivationArguments args, bool isRedirected) + private async ValueTask HandleActivationCoreAsync(HutaoActivationArguments args) { - if (args.Kind == ExtendedActivationKind.Protocol) + if (args.Kind is HutaoActivationKind.Protocol) { - if (args.TryGetProtocolActivatedUri(out Uri? uri)) - { - await HandleUrlActivationAsync(uri, isRedirected).ConfigureAwait(false); - } + ArgumentNullException.ThrowIfNull(args.ProtocolActivatedUri); + await HandleUrlActivationAsync(args.ProtocolActivatedUri, args.IsRedirectTo).ConfigureAwait(false); } - else if (args.Kind == ExtendedActivationKind.Launch) + else if (args.Kind is HutaoActivationKind.Launch) { - if (args.TryGetLaunchActivatedArgument(out string? arguments)) + ArgumentNullException.ThrowIfNull(args.LaunchActivatedArguments); + switch (args.LaunchActivatedArguments) { - switch (arguments) - { - case LaunchGame: - { - await HandleLaunchGameActionAsync().ConfigureAwait(false); - break; - } + case LaunchGame: + { + await HandleLaunchGameActionAsync().ConfigureAwait(false); + break; + } - default: - { - await HandleNormalLaunchActionAsync().ConfigureAwait(false); - break; - } - } + default: + { + await HandleNormalLaunchActionAsync().ConfigureAwait(false); + break; + } } } } @@ -194,7 +167,7 @@ internal sealed partial class Activation : IActivation .SafeForget(); } - private async ValueTask HandleUrlActivationAsync(Uri uri, bool isRedirected) + private async ValueTask HandleUrlActivationAsync(Uri uri, bool isRedirectTo) { UriBuilder builder = new(uri); @@ -207,13 +180,13 @@ internal sealed partial class Activation : IActivation case CategoryAchievement: { await WaitMainWindowAsync().ConfigureAwait(false); - await HandleAchievementActionAsync(action, parameter, isRedirected).ConfigureAwait(false); + await HandleAchievementActionAsync(action, parameter, isRedirectTo).ConfigureAwait(false); break; } case CategoryDailyNote: { - await HandleDailyNoteActionAsync(action, parameter, isRedirected).ConfigureAwait(false); + await HandleDailyNoteActionAsync(action, parameter, isRedirectTo).ConfigureAwait(false); break; } @@ -225,10 +198,10 @@ internal sealed partial class Activation : IActivation } } - private async ValueTask HandleAchievementActionAsync(string action, string parameter, bool isRedirected) + private async ValueTask HandleAchievementActionAsync(string action, string parameter, bool isRedirectTo) { _ = parameter; - _ = isRedirected; + _ = isRedirectTo; switch (action) { case UrlActionImport: @@ -245,7 +218,7 @@ internal sealed partial class Activation : IActivation } } - private async ValueTask HandleDailyNoteActionAsync(string action, string parameter, bool isRedirected) + private async ValueTask HandleDailyNoteActionAsync(string action, string parameter, bool isRedirectTo) { _ = parameter; switch (action) @@ -264,7 +237,7 @@ internal sealed partial class Activation : IActivation } // Check if it's redirected. - if (!isRedirected) + if (!isRedirectTo) { // It's a direct open process, should exit immediately. Process.GetCurrentProcess().Kill(); diff --git a/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/AppActivationArgumentsExtensions.cs b/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/AppActivationArgumentsExtensions.cs index eb216a8e..c76b1b4d 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/AppActivationArgumentsExtensions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/AppActivationArgumentsExtensions.cs @@ -36,7 +36,7 @@ internal static class AppActivationArgumentsExtensions /// 应用程序激活参数 /// 参数 /// 是否存在参数 - public static bool TryGetLaunchActivatedArgument(this AppActivationArguments activatedEventArgs, [NotNullWhen(true)] out string? arguments) + public static bool TryGetLaunchActivatedArguments(this AppActivationArguments activatedEventArgs, [NotNullWhen(true)] out string? arguments) { arguments = null; if (activatedEventArgs.Data is not ILaunchActivatedEventArgs launchArgs) diff --git a/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/HutaoActivationArguments.cs b/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/HutaoActivationArguments.cs new file mode 100644 index 00000000..5c3ce973 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/HutaoActivationArguments.cs @@ -0,0 +1,52 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.Windows.AppLifecycle; + +namespace Snap.Hutao.Core.LifeCycle; + +internal sealed class HutaoActivationArguments +{ + public bool IsRedirectTo { get; set; } + + public HutaoActivationKind Kind { get; set; } + + public Uri? ProtocolActivatedUri { get; set; } + + public string? LaunchActivatedArguments { get; set; } + + public static HutaoActivationArguments FromAppActivationArguments(AppActivationArguments args, bool isRedirected = false) + { + HutaoActivationArguments result = new() + { + IsRedirectTo = isRedirected, + }; + + switch (args.Kind) + { + case ExtendedActivationKind.Launch: + { + result.Kind = HutaoActivationKind.Launch; + if (args.TryGetLaunchActivatedArguments(out string? arguments)) + { + result.LaunchActivatedArguments = arguments; + } + + break; + } + + case ExtendedActivationKind.Protocol: + { + result.Kind = HutaoActivationKind.Protocol; + if (args.TryGetProtocolActivatedUri(out Uri? uri)) + { + result.ProtocolActivatedUri = uri; + } + + break; + } + } + + return result; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/HutaoActivationKind.cs b/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/HutaoActivationKind.cs new file mode 100644 index 00000000..031fec97 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/HutaoActivationKind.cs @@ -0,0 +1,11 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Core.LifeCycle; + +internal enum HutaoActivationKind +{ + None, + Launch, + Protocol, +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/IActivation.cs b/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/IActivation.cs index 4859192d..ca73609f 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/IActivation.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/IActivation.cs @@ -1,8 +1,6 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -using Microsoft.Windows.AppLifecycle; - namespace Snap.Hutao.Core.LifeCycle; /// @@ -10,24 +8,7 @@ namespace Snap.Hutao.Core.LifeCycle; /// internal interface IActivation { - /// - /// 响应激活事件 - /// 激活事件一般不会在UI线程上触发 - /// - /// 发送方 - /// 激活参数 - void Activate(object? sender, AppActivationArguments args); + void Activate(HutaoActivationArguments args); - /// - /// 使用当前 App 实例初始化激活 - /// - /// App 实例 - void InitializeWith(AppInstance appInstance); - - /// - /// 无转发触发激活事件 - /// - /// 发送方 - /// 激活参数 - void NonRedirectToActivate(object? sender, AppActivationArguments args); + void Initialize(); } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/InterProcess/NamedPipeClientStreamExtension.cs b/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/InterProcess/NamedPipeClientStreamExtension.cs new file mode 100644 index 00000000..223266b3 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/InterProcess/NamedPipeClientStreamExtension.cs @@ -0,0 +1,22 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using System.IO.Pipes; + +namespace Snap.Hutao.Core.LifeCycle.InterProcess; + +internal static class NamedPipeClientStreamExtension +{ + public static bool TryConnectOnce(this NamedPipeClientStream clientStream) + { + try + { + clientStream.Connect(TimeSpan.Zero); + return true; + } + catch (TimeoutException) + { + return false; + } + } +} diff --git a/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/InterProcess/PipePacketCommand.cs b/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/InterProcess/PipePacketCommand.cs new file mode 100644 index 00000000..58f25a1e --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/InterProcess/PipePacketCommand.cs @@ -0,0 +1,11 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Core.LifeCycle.InterProcess; + +internal enum PipePacketCommand : byte +{ + None = 0, + + RedirectActivation = 10, +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/InterProcess/PipePacketContentType.cs b/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/InterProcess/PipePacketContentType.cs new file mode 100644 index 00000000..7fb519b3 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/InterProcess/PipePacketContentType.cs @@ -0,0 +1,10 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Core.LifeCycle.InterProcess; + +internal enum PipePacketContentType : byte +{ + None = 0, + Json = 1, +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/InterProcess/PipePacketHeader.cs b/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/InterProcess/PipePacketHeader.cs new file mode 100644 index 00000000..1c9b3bf5 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/InterProcess/PipePacketHeader.cs @@ -0,0 +1,17 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using System.Runtime.InteropServices; + +namespace Snap.Hutao.Core.LifeCycle.InterProcess; + +[StructLayout(LayoutKind.Sequential, Pack = 1)] +internal struct PipePacketHeader +{ + public byte Version; + public PipePacketType Type; + public PipePacketCommand Command; + public PipePacketContentType ContentType; + public int ContentLength; + public ulong Checksum; +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/InterProcess/PipePacketType.cs b/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/InterProcess/PipePacketType.cs new file mode 100644 index 00000000..a2cadeca --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/InterProcess/PipePacketType.cs @@ -0,0 +1,12 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Core.LifeCycle.InterProcess; + +internal enum PipePacketType : byte +{ + None = 0, + Request = 1, + Response = 2, + Termination = 3, +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/InterProcess/PrivateNamedPipeClient.cs b/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/InterProcess/PrivateNamedPipeClient.cs new file mode 100644 index 00000000..fdecdfef --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/InterProcess/PrivateNamedPipeClient.cs @@ -0,0 +1,55 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.Windows.AppLifecycle; +using System.IO.Hashing; +using System.IO.Pipes; + +namespace Snap.Hutao.Core.LifeCycle.InterProcess; + +[Injection(InjectAs.Singleton)] +internal sealed class PrivateNamedPipeClient : IDisposable +{ + private readonly NamedPipeClientStream clientStream = new(".", "Snap.Hutao.PrivateNamedPipe", PipeDirection.InOut, PipeOptions.Asynchronous | PipeOptions.WriteThrough); + + public unsafe bool TryRedirectActivationTo(AppActivationArguments args) + { + if (clientStream.TryConnectOnce()) + { + { + PipePacketHeader redirectActivationPacket = default; + redirectActivationPacket.Version = 1; + redirectActivationPacket.Type = PipePacketType.Request; + redirectActivationPacket.Command = PipePacketCommand.RedirectActivation; + redirectActivationPacket.ContentType = PipePacketContentType.Json; + + HutaoActivationArguments hutaoArgs = HutaoActivationArguments.FromAppActivationArguments(args, isRedirected: true); + byte[] jsonBytes = JsonSerializer.SerializeToUtf8Bytes(hutaoArgs); + + redirectActivationPacket.ContentLength = jsonBytes.Length; + redirectActivationPacket.Checksum = XxHash64.HashToUInt64(jsonBytes); + + clientStream.Write(new(&redirectActivationPacket, sizeof(PipePacketHeader))); + clientStream.Write(jsonBytes); + } + + { + PipePacketHeader terminationPacket = default; + terminationPacket.Version = 1; + terminationPacket.Type = PipePacketType.Termination; + + clientStream.Write(new(&terminationPacket, sizeof(PipePacketHeader))); + } + + clientStream.Flush(); + return true; + } + + return false; + } + + public void Dispose() + { + clientStream.Dispose(); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/InterProcess/PrivateNamedPipeMessageDispatcher.cs b/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/InterProcess/PrivateNamedPipeMessageDispatcher.cs new file mode 100644 index 00000000..611c91ca --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/InterProcess/PrivateNamedPipeMessageDispatcher.cs @@ -0,0 +1,21 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Core.LifeCycle.InterProcess; + +[Injection(InjectAs.Singleton)] +[ConstructorGenerated] +internal sealed partial class PrivateNamedPipeMessageDispatcher +{ + private readonly IServiceProvider serviceProvider; + + public void RedirectActivation(HutaoActivationArguments? args) + { + if (args is null) + { + return; + } + + serviceProvider.GetRequiredService().Activate(args); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/InterProcess/PrivateNamedPipeServer.cs b/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/InterProcess/PrivateNamedPipeServer.cs new file mode 100644 index 00000000..aecb8767 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/InterProcess/PrivateNamedPipeServer.cs @@ -0,0 +1,81 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Core.ExceptionService; +using System.IO.Hashing; +using System.IO.Pipes; + +namespace Snap.Hutao.Core.LifeCycle.InterProcess; + +[Injection(InjectAs.Singleton)] +[ConstructorGenerated] +internal sealed partial class PrivateNamedPipeServer : IDisposable +{ + private readonly PrivateNamedPipeMessageDispatcher messageDispatcher; + + private readonly NamedPipeServerStream serverStream = new("Snap.Hutao.PrivateNamedPipe", PipeDirection.InOut, NamedPipeServerStream.MaxAllowedServerInstances, PipeTransmissionMode.Byte, PipeOptions.Asynchronous | PipeOptions.WriteThrough); + private readonly CancellationTokenSource serverTokenSource = new(); + private readonly SemaphoreSlim serverSemaphore = new(1); + + public void Dispose() + { + serverTokenSource.Cancel(); + serverSemaphore.Wait(); + serverSemaphore.Dispose(); + serverTokenSource.Dispose(); + + serverStream.Dispose(); + } + + public async ValueTask RunAsync() + { + using (await serverSemaphore.EnterAsync(serverTokenSource.Token).ConfigureAwait(false)) + { + while (!serverTokenSource.IsCancellationRequested) + { + try + { + await serverStream.WaitForConnectionAsync(serverTokenSource.Token).ConfigureAwait(false); + RunPacketSession(serverStream, serverTokenSource.Token); + } + catch (OperationCanceledException) + { + } + } + } + } + + private static unsafe byte[] GetValidatedContent(NamedPipeServerStream serverStream, PipePacketHeader* header) + { + byte[] content = new byte[header->ContentLength]; + serverStream.ReadAtLeast(content, header->ContentLength, false); + ThrowHelper.InvalidDataIf(XxHash64.HashToUInt64(content) != header->Checksum, "PipePacket Content Hash incorrect"); + return content; + } + + private unsafe void RunPacketSession(NamedPipeServerStream serverStream, CancellationToken token) + { + Span headerSpan = stackalloc byte[sizeof(PipePacketHeader)]; + bool sessionTerminated = false; + while (serverStream.IsConnected && !sessionTerminated && !token.IsCancellationRequested) + { + serverStream.ReadExactly(headerSpan); + fixed (byte* pHeader = headerSpan) + { + PipePacketHeader* header = (PipePacketHeader*)pHeader; + + switch ((header->Type, header->Command, header->ContentType)) + { + case (PipePacketType.Request, PipePacketCommand.RedirectActivation, PipePacketContentType.Json): + ReadOnlySpan content = GetValidatedContent(serverStream, header); + messageDispatcher.RedirectActivation(JsonSerializer.Deserialize(content)); + break; + case (PipePacketType.Termination, _, _): + serverStream.Disconnect(); + sessionTerminated = true; + return; + } + } + } + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Update/IUpdateService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Update/IUpdateService.cs index a480f97b..51213be1 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Update/IUpdateService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Update/IUpdateService.cs @@ -9,5 +9,5 @@ internal interface IUpdateService { ValueTask CheckForUpdateAndDownloadAsync(IProgress progress, CancellationToken token = default); - ValueTask LaunchUpdaterAsync(); + ValueTask LaunchUpdaterAsync(); } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Update/UpdateService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Update/UpdateService.cs index 326b1ec1..093dace0 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Update/UpdateService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Update/UpdateService.cs @@ -6,6 +6,7 @@ using Snap.Hutao.Core.IO.Hashing; using Snap.Hutao.Core.IO.Http.Sharding; using Snap.Hutao.Core.Setting; using Snap.Hutao.Service.Abstraction; +using Snap.Hutao.Service.Notification; using Snap.Hutao.Web.Hutao; using Snap.Hutao.Web.Hutao.Response; using System.Diagnostics; @@ -70,7 +71,7 @@ internal sealed partial class UpdateService : IUpdateService } } - public async ValueTask LaunchUpdaterAsync() + public async ValueTask LaunchUpdaterAsync() { RuntimeOptions runtimeOptions = serviceProvider.GetRequiredService(); string updaterTargetPath = runtimeOptions.GetDataFolderUpdateCacheFolderFile(UpdaterFilename); @@ -85,12 +86,22 @@ internal sealed partial class UpdateService : IUpdateService .Append("--update-behavior", true) .ToString(); - Process.Start(new ProcessStartInfo() + try { - Arguments = commandLine, - FileName = updaterTargetPath, - UseShellExecute = true, - }); + Process.Start(new ProcessStartInfo() + { + Arguments = commandLine, + FileName = updaterTargetPath, + UseShellExecute = true, + }); + + return true; + } + catch (Exception ex) + { + serviceProvider.GetRequiredService().Error(ex); + return false; + } } private static async ValueTask CheckUpdateCacheSHA256Async(string filePath, string remoteHash, CancellationToken token = default) diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Feedback/FeedbackViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Feedback/FeedbackViewModel.cs index 863dd82b..190fa770 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Feedback/FeedbackViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Feedback/FeedbackViewModel.cs @@ -4,8 +4,8 @@ using Microsoft.UI.Xaml.Controls; using Snap.Hutao.Core; using Snap.Hutao.Core.IO.DataTransfer; -using Snap.Hutao.Core.IO.Http.Proxy; using Snap.Hutao.Core.IO.Http.Loopback; +using Snap.Hutao.Core.IO.Http.Proxy; using Snap.Hutao.Factory.ContentDialog; using Snap.Hutao.Service; using Snap.Hutao.Service.Notification; @@ -51,17 +51,7 @@ internal sealed partial class FeedbackViewModel : Abstraction.ViewModel protected override async ValueTask InitializeUIAsync() { Response resp = await hutaoInfrastructureClient.GetIPInformationAsync().ConfigureAwait(false); - IPInformation info; - - if (resp.IsOk()) - { - info = resp.Data; - } - else - { - info = IPInformation.Default; - } - + IPInformation info = resp.IsOk() ? resp.Data : IPInformation.Default; await taskContext.SwitchToMainThreadAsync(); IPInformation = info;