From 92a151441bcbf401914df539d761ab3d39b281b7 Mon Sep 17 00:00:00 2001 From: DismissedLight <1686188646@qq.com> Date: Sat, 18 May 2024 21:37:27 +0800 Subject: [PATCH] dailynote refresh refactor --- .../BaseClassLibrary/TypeReflectionTest.cs | 3 +- .../BaseClassLibrary/UnsafeAccessorTest.cs | 28 +++++++ .../DependencyInjectionTest.cs | 22 +++++ src/Snap.Hutao/Snap.Hutao/App.xaml.cs | 6 +- .../DependencyInjection.cs | 8 ++ .../Core/LifeCycle/AppActivation.cs | 5 ++ .../Core/Shell/IScheduleTaskInterop.cs | 11 --- .../Core/Shell/ScheduleTaskInterop.cs | 75 ----------------- .../Core/Windowing/HotKey/HotKeyOptions.cs | 8 +- .../Model/Entity/SettingEntry.Constant.cs | 1 + .../Service/DailyNote/DailyNoteOptions.cs | 67 +++++---------- .../Service/Job/DailyNoteRefreshJob.cs | 23 +++++ .../Job/DailyNoteRefreshJobScheduler.cs | 46 ++++++++++ .../Snap.Hutao/Service/Job/IJobScheduler.cs | 11 +++ .../Snap.Hutao/Service/Job/IQuartzService.cs | 15 ++++ .../Snap.Hutao/Service/Job/JobIdentity.cs | 14 ++++ .../Snap.Hutao/Service/Job/QuartzService.cs | 84 +++++++++++++++++++ src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj | 7 +- 18 files changed, 286 insertions(+), 148 deletions(-) create mode 100644 src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/UnsafeAccessorTest.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Job/DailyNoteRefreshJob.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Job/DailyNoteRefreshJobScheduler.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Job/IJobScheduler.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Job/IQuartzService.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Job/JobIdentity.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Job/QuartzService.cs diff --git a/src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/TypeReflectionTest.cs b/src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/TypeReflectionTest.cs index 668fc70a..42020e6d 100644 --- a/src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/TypeReflectionTest.cs +++ b/src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/TypeReflectionTest.cs @@ -1,4 +1,5 @@ -using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; namespace Snap.Hutao.Test.BaseClassLibrary; diff --git a/src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/UnsafeAccessorTest.cs b/src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/UnsafeAccessorTest.cs new file mode 100644 index 00000000..28c7c74f --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/UnsafeAccessorTest.cs @@ -0,0 +1,28 @@ +using System.Runtime.CompilerServices; + +namespace Snap.Hutao.Test.BaseClassLibrary; + +[TestClass] +public class UnsafeAccessorTest +{ + [TestMethod] + public void UnsafeAccessorCanGetInterfaceProperty() + { + TestClass test = new(); + int value = InternalGetInterfaceProperty(test); + Assert.AreEqual(3, value); + } + + [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "get_TestProperty")] + private static extern int InternalGetInterfaceProperty(ITestInterface instance); + + interface ITestInterface + { + internal int TestProperty { get; } + } + + internal sealed class TestClass : ITestInterface + { + public int TestProperty { get; } = 3; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.Test/PlatformExtensions/DependencyInjectionTest.cs b/src/Snap.Hutao/Snap.Hutao.Test/PlatformExtensions/DependencyInjectionTest.cs index 0075610c..eab5760c 100644 --- a/src/Snap.Hutao/Snap.Hutao.Test/PlatformExtensions/DependencyInjectionTest.cs +++ b/src/Snap.Hutao/Snap.Hutao.Test/PlatformExtensions/DependencyInjectionTest.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using System; +using System.Linq; namespace Snap.Hutao.Test.PlatformExtensions; @@ -11,6 +12,8 @@ public sealed class DependencyInjectionTest .AddSingleton() .AddSingleton() .AddScoped() + .AddKeyedTransient("A") + .AddKeyedTransient("B") .AddTransient(typeof(IGenericService<>), typeof(GenericService<>)) .AddLogging(builder => builder.AddConsole()) .BuildServiceProvider(); @@ -50,6 +53,15 @@ public sealed class DependencyInjectionTest Assert.IsNotNull(services.GetRequiredService().CreateLogger(nameof(IScopedService))); } + [TestMethod] + public void KeyedServicesCanBeResolvedAsEnumerable() + { + Assert.IsNotNull(services.GetRequiredKeyedService("A")); + Assert.IsNotNull(services.GetRequiredKeyedService("B")); + + Assert.AreEqual(0, services.GetServices().Count()); + } + private interface IService { Guid Id { get; } @@ -95,4 +107,14 @@ public sealed class DependencyInjectionTest { } } + + private interface IKeyedService; + + private sealed class KeyedServiceA : IKeyedService + { + } + + private sealed class KeyedServiceB : IKeyedService + { + } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/App.xaml.cs b/src/Snap.Hutao/Snap.Hutao/App.xaml.cs index d6a575a0..55495216 100644 --- a/src/Snap.Hutao/Snap.Hutao/App.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/App.xaml.cs @@ -57,8 +57,6 @@ public sealed partial class App : Application this.serviceProvider = serviceProvider; } - public bool IsExiting { get; private set; } - public new void Exit() { XamlWindowLifetime.IsApplicationExiting = true; @@ -85,9 +83,9 @@ public sealed partial class App : Application activation.Activate(HutaoActivationArguments.FromAppActivationArguments(activatedEventArgs)); activation.PostInitialization(); } - catch + catch (Exception ex) { - // AppInstance.GetCurrent() calls failed + System.Diagnostics.Debug.WriteLine(ex); Process.GetCurrentProcess().Kill(); } } diff --git a/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/DependencyInjection.cs b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/DependencyInjection.cs index d58b9257..81cac1dc 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/DependencyInjection.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/DependencyInjection.cs @@ -2,8 +2,13 @@ // Licensed under the MIT license. using CommunityToolkit.Mvvm.Messaging; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Quartz; using Snap.Hutao.Core.Logging; using Snap.Hutao.Service; +using Snap.Hutao.Service.DailyNote; +using Snap.Hutao.Service.Job; +using System.Collections.Specialized; using System.Globalization; using System.Runtime.CompilerServices; using Windows.Globalization; @@ -33,6 +38,9 @@ internal static class DependencyInjection }) .AddMemoryCache() + // Quartz + .AddQuartz() + // Hutao extensions .AddJsonOptions() .AddDatabase() diff --git a/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/AppActivation.cs b/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/AppActivation.cs index dbb2612f..7a2cb500 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/AppActivation.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/AppActivation.cs @@ -6,11 +6,13 @@ using Microsoft.Extensions.Caching.Memory; using Microsoft.UI.Xaml; using Snap.Hutao.Core.LifeCycle.InterProcess; using Snap.Hutao.Core.Setting; +using Snap.Hutao.Core.Shell; using Snap.Hutao.Core.Windowing.HotKey; using Snap.Hutao.Core.Windowing.NotifyIcon; using Snap.Hutao.Service.DailyNote; using Snap.Hutao.Service.Discord; using Snap.Hutao.Service.Hutao; +using Snap.Hutao.Service.Job; using Snap.Hutao.Service.Metadata; using Snap.Hutao.Service.Navigation; using Snap.Hutao.ViewModel.Guide; @@ -67,6 +69,9 @@ internal sealed partial class AppActivation : IAppActivation, IAppActivationActi serviceProvider.GetRequiredService().DispatcherShutdownMode = DispatcherShutdownMode.OnExplicitShutdown; _ = serviceProvider.GetRequiredService(); } + + serviceProvider.GetRequiredService().UnregisterAllTasks(); + serviceProvider.GetRequiredService().StartAsync(default).SafeForget(); } public void Dispose() diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Shell/IScheduleTaskInterop.cs b/src/Snap.Hutao/Snap.Hutao/Core/Shell/IScheduleTaskInterop.cs index 3bf419d4..a4bc6ae5 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Shell/IScheduleTaskInterop.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Shell/IScheduleTaskInterop.cs @@ -8,20 +8,9 @@ namespace Snap.Hutao.Core.Shell; /// internal interface IScheduleTaskInterop { - bool IsDailyNoteRefreshEnabled(); - - /// - /// 注册实时便笺刷新任务 - /// - /// 间隔(秒) - /// 是否注册或修改成功 - bool RegisterForDailyNoteRefresh(int interval); - /// /// 卸载全部注册的任务 /// /// 是否卸载成功 bool UnregisterAllTasks(); - - bool UnregisterForDailyNoteRefresh(); } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Shell/ScheduleTaskInterop.cs b/src/Snap.Hutao/Snap.Hutao/Core/Shell/ScheduleTaskInterop.cs index 738bcadd..40e11183 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Shell/ScheduleTaskInterop.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Shell/ScheduleTaskInterop.cs @@ -16,60 +16,6 @@ namespace Snap.Hutao.Core.Shell; internal sealed class ScheduleTaskInterop : IScheduleTaskInterop { private const string DailyNoteRefreshTaskName = "SnapHutaoDailyNoteRefreshTask"; - private const string DailyNoteRefreshScriptName = "DailyNoteRefresh"; - - /// - /// 注册实时便笺刷新任务 - /// - /// 间隔(秒) - /// 是否注册或修改成功 - public bool RegisterForDailyNoteRefresh(int interval) - { - try - { - TaskDefinition task = TaskService.Instance.NewTask(); - task.RegistrationInfo.Description = SH.CoreScheduleTaskHelperDailyNoteRefreshTaskDescription; - task.Triggers.Add(new TimeTrigger() { Repetition = new(TimeSpan.FromSeconds(interval), TimeSpan.Zero), }); - - string scriptPath = EnsureWScriptCreated(DailyNoteRefreshScriptName, "hutao://DailyNote/Refresh"); - task.Actions.Add("wscript", $@"/b ""{scriptPath}"""); - - TaskService.Instance.RootFolder.RegisterTaskDefinition(DailyNoteRefreshTaskName, task); - return true; - } - catch (Exception) - { - if (WScriptExists(DailyNoteRefreshScriptName, out string fullPath)) - { - File.Delete(fullPath); - } - - return false; - } - } - - public bool UnregisterForDailyNoteRefresh() - { - try - { - TaskService.Instance.RootFolder.DeleteTask(DailyNoteRefreshTaskName, false); - if (WScriptExists(DailyNoteRefreshScriptName, out string fullPath)) - { - File.Delete(fullPath); - } - - return true; - } - catch (Exception) - { - return false; - } - } - - public bool IsDailyNoteRefreshEnabled() - { - return TaskService.Instance.RootFolder.Tasks.Any(task => task.Name is DailyNoteRefreshTaskName); - } /// /// 卸载全部注册的任务 @@ -91,25 +37,4 @@ internal sealed class ScheduleTaskInterop : IScheduleTaskInterop return false; } } - - private static string EnsureWScriptCreated(string name, string url, bool forceCreate = false) - { - if (WScriptExists(name, out string fullName) && !forceCreate) - { - return fullName; - } - - string script = $"""CreateObject("WScript.Shell").Run "cmd /c start {url}", 0, False"""; - File.WriteAllText(fullName, script); - - return fullName; - } - - private static bool WScriptExists(string name, out string fullName) - { - string tempFolder = ApplicationData.Current.TemporaryFolder.Path; - fullName = Path.Combine(tempFolder, "Script", $"{name}.vbs"); - Directory.CreateDirectory(Path.Combine(tempFolder, "Script")); - return File.Exists(fullName); - } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Windowing/HotKey/HotKeyOptions.cs b/src/Snap.Hutao/Snap.Hutao/Core/Windowing/HotKey/HotKeyOptions.cs index b3f676dd..5ecec692 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Windowing/HotKey/HotKeyOptions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Windowing/HotKey/HotKeyOptions.cs @@ -58,7 +58,8 @@ internal sealed partial class HotKeyOptions : ObservableObject, IDisposable isDisposed = true; - UnregisterAll(); + MouseClickRepeatForeverKeyCombination.Unregister(); + hotKeyMessageWindow.Dispose(); cancellationTokenSource?.Dispose(); @@ -106,11 +107,6 @@ internal sealed partial class HotKeyOptions : ObservableObject, IDisposable } } - private void UnregisterAll() - { - MouseClickRepeatForeverKeyCombination.Unregister(); - } - [SuppressMessage("", "SH002")] private void OnHotKeyPressed(HotKeyParameter parameter) { diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Entity/SettingEntry.Constant.cs b/src/Snap.Hutao/Snap.Hutao/Model/Entity/SettingEntry.Constant.cs index 18bbdbad..8e625f56 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Entity/SettingEntry.Constant.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Entity/SettingEntry.Constant.cs @@ -23,6 +23,7 @@ internal sealed partial class SettingEntry public const string GeetestCustomCompositeUrl = "GeetestCustomCompositeUrl"; + public const string DailyNoteIsAutoRefreshEnabled = "DailyNote.IsAutoRefreshEnabled"; public const string DailyNoteRefreshSeconds = "DailyNote.RefreshSeconds"; public const string DailyNoteReminderNotify = "DailyNote.ReminderNotify"; public const string DailyNoteSilentWhenPlayingGame = "DailyNote.SilentWhenPlayingGame"; diff --git a/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteOptions.cs b/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteOptions.cs index 4a33e282..ffe57f1c 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteOptions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteOptions.cs @@ -1,12 +1,11 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -using Snap.Hutao.Core; -using Snap.Hutao.Core.Shell; +using Quartz; using Snap.Hutao.Model; using Snap.Hutao.Model.Entity; using Snap.Hutao.Service.Abstraction; -using Snap.Hutao.Service.Notification; +using Snap.Hutao.Service.Job; using System.Globalization; namespace Snap.Hutao.Service.DailyNote; @@ -26,10 +25,9 @@ internal sealed partial class DailyNoteOptions : DbStoreOptions new(SH.ViewModelDailyNoteRefreshTime60, OneMinute * 60), ]; - private readonly RuntimeOptions runtimeOptions; - private readonly IServiceProvider serviceProvider; - private readonly IScheduleTaskInterop scheduleTaskInterop; + private readonly IQuartzService quartzService; + private bool? isAutoRefreshEnabled; private NameValue? selectedRefreshTime; private bool? isReminderNotification; private bool? isSilentWhenPlayingGame; @@ -39,68 +37,41 @@ internal sealed partial class DailyNoteOptions : DbStoreOptions public bool IsAutoRefreshEnabled { - get => scheduleTaskInterop.IsDailyNoteRefreshEnabled(); + get => GetOption(ref isAutoRefreshEnabled, SettingEntry.DailyNoteIsAutoRefreshEnabled, true); set { - if (runtimeOptions.IsElevated) + if (SetOption(ref isAutoRefreshEnabled, SettingEntry.DailyNoteIsAutoRefreshEnabled, value)) { - // leave below untouched if we are running in elevated privilege - return; - } - - if (value) - { - if (SelectedRefreshTime is not null) + if (value) { - if (!scheduleTaskInterop.RegisterForDailyNoteRefresh(SelectedRefreshTime.Value)) + if (SelectedRefreshTime is not null) { - serviceProvider.GetRequiredService().Warning(SH.ViewModelDailyNoteModifyTaskFail); + quartzService.UpdateJobAsync(JobIdentity.DailyNoteGroupName, JobIdentity.DailyNoteRefreshTriggerName, builder => + { + return builder.WithSimpleSchedule(sb => sb.WithIntervalInMinutes(SelectedRefreshTime.Value).RepeatForever()); + }).SafeForget(); } } - } - else - { - if (!scheduleTaskInterop.UnregisterForDailyNoteRefresh()) + else { - serviceProvider.GetRequiredService().Warning(SH.ViewModelDailyNoteModifyTaskFail); + quartzService.StopJobAsync(JobIdentity.DailyNoteGroupName, JobIdentity.DailyNoteRefreshTriggerName).SafeForget(); } } - - OnPropertyChanged(); } } public NameValue? SelectedRefreshTime { - get - { - if (runtimeOptions.IsElevated) - { - // leave untouched when we are running in elevated privilege - return null; - } - - return GetOption(ref selectedRefreshTime, SettingEntry.DailyNoteRefreshSeconds, time => RefreshTimes.Single(t => t.Value == int.Parse(time, CultureInfo.InvariantCulture)), RefreshTimes[1]); - } - + get => GetOption(ref selectedRefreshTime, SettingEntry.DailyNoteRefreshSeconds, time => RefreshTimes.Single(t => t.Value == int.Parse(time, CultureInfo.InvariantCulture)), RefreshTimes[1]); set { - if (runtimeOptions.IsElevated) - { - // leave untouched when we are running in elevated privilege - return; - } - if (value is not null) { - if (scheduleTaskInterop.RegisterForDailyNoteRefresh(value.Value)) + SetOption(ref selectedRefreshTime, SettingEntry.DailyNoteRefreshSeconds, value, value => $"{value.Value}"); + quartzService.UpdateJobAsync(JobIdentity.DailyNoteGroupName, JobIdentity.DailyNoteRefreshTriggerName, builder => { - SetOption(ref selectedRefreshTime, SettingEntry.DailyNoteRefreshSeconds, value, value => $"{value.Value}"); - } - else - { - serviceProvider.GetRequiredService().Warning(SH.ViewModelDailyNoteModifyTaskFail); - } + return builder.WithSimpleSchedule(sb => sb.WithIntervalInSeconds(value.Value).RepeatForever()); + }).SafeForget(); } } } diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Job/DailyNoteRefreshJob.cs b/src/Snap.Hutao/Snap.Hutao/Service/Job/DailyNoteRefreshJob.cs new file mode 100644 index 00000000..5ec1d43c --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Job/DailyNoteRefreshJob.cs @@ -0,0 +1,23 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Quartz; +using Snap.Hutao.Service.DailyNote; + +namespace Snap.Hutao.Service.Job; + +internal sealed partial class DailyNoteRefreshJob : IJob +{ + private readonly IDailyNoteService dailyNoteService; + + public DailyNoteRefreshJob(IDailyNoteService dailyNoteService) + { + this.dailyNoteService = dailyNoteService; + } + + [SuppressMessage("", "SH003")] + public async Task Execute(IJobExecutionContext context) + { + await dailyNoteService.RefreshDailyNotesAsync(context.CancellationToken).ConfigureAwait(false); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Job/DailyNoteRefreshJobScheduler.cs b/src/Snap.Hutao/Snap.Hutao/Service/Job/DailyNoteRefreshJobScheduler.cs new file mode 100644 index 00000000..15ed1579 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Job/DailyNoteRefreshJobScheduler.cs @@ -0,0 +1,46 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Quartz; +using Snap.Hutao.Service.DailyNote; + +namespace Snap.Hutao.Service.Job; + +[ConstructorGenerated] +[Injection(InjectAs.Transient, typeof(IJobScheduler))] +internal sealed partial class DailyNoteRefreshJobScheduler : IJobScheduler +{ + private readonly DailyNoteOptions dailyNoteOptions; + + public async ValueTask ScheduleAsync(IScheduler scheduler) + { + if (!TryGetRefreshInterval(out int interval)) + { + return; + } + + IJobDetail dailyNoteJob = JobBuilder.Create() + .WithIdentity(JobIdentity.DailyNoteRefreshJobName, JobIdentity.DailyNoteGroupName) + .Build(); + + ITrigger dailyNoteTrigger = TriggerBuilder.Create() + .WithIdentity(JobIdentity.DailyNoteRefreshTriggerName, JobIdentity.DailyNoteGroupName) + .StartNow() + .WithSimpleSchedule(builder => builder.WithIntervalInMinutes(interval).RepeatForever()) + .Build(); + + await scheduler.ScheduleJob(dailyNoteJob, dailyNoteTrigger).ConfigureAwait(false); + } + + private bool TryGetRefreshInterval(out int interval) + { + if (dailyNoteOptions.IsAutoRefreshEnabled && dailyNoteOptions.SelectedRefreshTime is not null) + { + interval = dailyNoteOptions.SelectedRefreshTime.Value; + return true; + } + + interval = 0; + return false; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Job/IJobScheduler.cs b/src/Snap.Hutao/Snap.Hutao/Service/Job/IJobScheduler.cs new file mode 100644 index 00000000..11bfd760 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Job/IJobScheduler.cs @@ -0,0 +1,11 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Quartz; + +namespace Snap.Hutao.Service.Job; + +internal interface IJobScheduler +{ + ValueTask ScheduleAsync(IScheduler scheduler); +} diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Job/IQuartzService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Job/IQuartzService.cs new file mode 100644 index 00000000..85b8d8f3 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Job/IQuartzService.cs @@ -0,0 +1,15 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Quartz; + +namespace Snap.Hutao.Service.Job; + +internal interface IQuartzService +{ + ValueTask StartAsync(CancellationToken token = default); + + ValueTask StopJobAsync(string group, string triggerName, CancellationToken token = default); + + ValueTask UpdateJobAsync(string group, string triggerName, Func configure, CancellationToken token = default); +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Job/JobIdentity.cs b/src/Snap.Hutao/Snap.Hutao/Service/Job/JobIdentity.cs new file mode 100644 index 00000000..868f7bbd --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Job/JobIdentity.cs @@ -0,0 +1,14 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Quartz; +using Snap.Hutao.Service.DailyNote; + +namespace Snap.Hutao.Service.Job; + +internal static class JobIdentity +{ + public const string DailyNoteGroupName = "DailyNote"; + public const string DailyNoteRefreshJobName = "RefreshJob"; + public const string DailyNoteRefreshTriggerName = "RefreshTrigger"; +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Job/QuartzService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Job/QuartzService.cs new file mode 100644 index 00000000..41c5552b --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Job/QuartzService.cs @@ -0,0 +1,84 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Quartz; + +namespace Snap.Hutao.Service.Job; + +[Injection(InjectAs.Singleton, typeof(IQuartzService))] +[ConstructorGenerated] +internal sealed partial class QuartzService : IQuartzService, IDisposable +{ + private readonly TaskCompletionSource startupCompleted = new(); + + private readonly ISchedulerFactory schedulerFactory; + private readonly IServiceProvider serviceProvider; + + private IScheduler? scheduler; + + public async ValueTask StartAsync(CancellationToken token = default) + { + scheduler = await schedulerFactory.GetScheduler(token).ConfigureAwait(false); + await scheduler.Start(token).ConfigureAwait(false); + + foreach (IJobScheduler jobScheduler in serviceProvider.GetServices()) + { + await jobScheduler.ScheduleAsync(scheduler).ConfigureAwait(false); + } + + startupCompleted.SetResult(); + } + + public async ValueTask UpdateJobAsync(string group, string triggerName, Func configure, CancellationToken token = default) + { + if (scheduler is null) + { + return; + } + + await startupCompleted.Task.ConfigureAwait(false); + + TriggerKey key = new(triggerName, group); + if (await scheduler.GetTrigger(key, token).ConfigureAwait(false) is { } old) + { + ITrigger newTrigger = configure(old.GetTriggerBuilder()).Build(); + await scheduler.RescheduleJob(key, newTrigger, token).ConfigureAwait(false); + } + } + + public async ValueTask StopJobAsync(string group, string triggerName, CancellationToken token = default) + { + if (scheduler is null) + { + return; + } + + await startupCompleted.Task.ConfigureAwait(false); + + await scheduler.UnscheduleJob(new(triggerName, group), token).ConfigureAwait(false); + } + + public void Dispose() + { + DisposeAsync().GetAwaiter().GetResult(); + + async ValueTask DisposeAsync() + { + if (scheduler is null) + { + return; + } + + try + { + // Wait until any ongoing startup logic has finished or the graceful shutdown period is over + await startupCompleted.Task.ConfigureAwait(false); + } + finally + { + await scheduler.Shutdown(false).ConfigureAwait(false); + scheduler = default; + } + } + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj b/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj index 0f929257..37da3352 100644 --- a/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj +++ b/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj @@ -308,8 +308,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -326,8 +326,9 @@ + - + all runtime; build; native; contentfiles; analyzers; buildtransitive