dailynote refresh refactor

This commit is contained in:
DismissedLight
2024-05-18 21:37:27 +08:00
parent faefc9c093
commit 92a151441b
18 changed files with 286 additions and 148 deletions

View File

@@ -1,4 +1,5 @@
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
namespace Snap.Hutao.Test.BaseClassLibrary;

View File

@@ -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;
}
}

View File

@@ -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<IService, ServiceA>()
.AddSingleton<IService, ServiceB>()
.AddScoped<IScopedService, ServiceA>()
.AddKeyedTransient<IKeyedService, KeyedServiceA>("A")
.AddKeyedTransient<IKeyedService, KeyedServiceB>("B")
.AddTransient(typeof(IGenericService<>), typeof(GenericService<>))
.AddLogging(builder => builder.AddConsole())
.BuildServiceProvider();
@@ -50,6 +53,15 @@ public sealed class DependencyInjectionTest
Assert.IsNotNull(services.GetRequiredService<ILoggerFactory>().CreateLogger(nameof(IScopedService)));
}
[TestMethod]
public void KeyedServicesCanBeResolvedAsEnumerable()
{
Assert.IsNotNull(services.GetRequiredKeyedService<IKeyedService>("A"));
Assert.IsNotNull(services.GetRequiredKeyedService<IKeyedService>("B"));
Assert.AreEqual(0, services.GetServices<IKeyedService>().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
{
}
}

View File

@@ -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();
}
}

View File

@@ -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()

View File

@@ -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<App>().DispatcherShutdownMode = DispatcherShutdownMode.OnExplicitShutdown;
_ = serviceProvider.GetRequiredService<NotifyIconController>();
}
serviceProvider.GetRequiredService<IScheduleTaskInterop>().UnregisterAllTasks();
serviceProvider.GetRequiredService<IQuartzService>().StartAsync(default).SafeForget();
}
public void Dispose()

View File

@@ -8,20 +8,9 @@ namespace Snap.Hutao.Core.Shell;
/// </summary>
internal interface IScheduleTaskInterop
{
bool IsDailyNoteRefreshEnabled();
/// <summary>
/// 注册实时便笺刷新任务
/// </summary>
/// <param name="interval">间隔(秒)</param>
/// <returns>是否注册或修改成功</returns>
bool RegisterForDailyNoteRefresh(int interval);
/// <summary>
/// 卸载全部注册的任务
/// </summary>
/// <returns>是否卸载成功</returns>
bool UnregisterAllTasks();
bool UnregisterForDailyNoteRefresh();
}

View File

@@ -16,60 +16,6 @@ namespace Snap.Hutao.Core.Shell;
internal sealed class ScheduleTaskInterop : IScheduleTaskInterop
{
private const string DailyNoteRefreshTaskName = "SnapHutaoDailyNoteRefreshTask";
private const string DailyNoteRefreshScriptName = "DailyNoteRefresh";
/// <summary>
/// 注册实时便笺刷新任务
/// </summary>
/// <param name="interval">间隔(秒)</param>
/// <returns>是否注册或修改成功</returns>
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);
}
/// <summary>
/// 卸载全部注册的任务
@@ -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);
}
}

View File

@@ -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)
{

View File

@@ -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";

View File

@@ -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<int>? 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<IInfoBarService>().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<IInfoBarService>().Warning(SH.ViewModelDailyNoteModifyTaskFail);
quartzService.StopJobAsync(JobIdentity.DailyNoteGroupName, JobIdentity.DailyNoteRefreshTriggerName).SafeForget();
}
}
OnPropertyChanged();
}
}
public NameValue<int>? 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<IInfoBarService>().Warning(SH.ViewModelDailyNoteModifyTaskFail);
}
return builder.WithSimpleSchedule(sb => sb.WithIntervalInSeconds(value.Value).RepeatForever());
}).SafeForget();
}
}
}

View File

@@ -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);
}
}

View File

@@ -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<DailyNoteRefreshJob>()
.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;
}
}

View File

@@ -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);
}

View File

@@ -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<TriggerBuilder, TriggerBuilder> configure, CancellationToken token = default);
}

View File

@@ -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";
}

View File

@@ -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<IJobScheduler>())
{
await jobScheduler.ScheduleAsync(scheduler).ConfigureAwait(false);
}
startupCompleted.SetResult();
}
public async ValueTask UpdateJobAsync(string group, string triggerName, Func<TriggerBuilder, TriggerBuilder> 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;
}
}
}
}

View File

@@ -308,8 +308,8 @@
<PackageReference Include="CommunityToolkit.WinUI.Controls.TokenizingTextBox" Version="8.0.240109" />
<PackageReference Include="CommunityToolkit.WinUI.Media" Version="8.0.240109" />
<PackageReference Include="CommunityToolkit.WinUI.Notifications" Version="7.1.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.4">
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
@@ -326,8 +326,9 @@
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.22621.3233" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.5.240428000" />
<PackageReference Include="QRCoder" Version="1.5.1" />
<PackageReference Include="Quartz.Extensions.DependencyInjection" Version="3.9.0" />
<PackageReference Include="Snap.Discord.GameSDK" Version="1.6.0" />
<PackageReference Include="Snap.Hutao.Deployment.Runtime" Version="1.16.0">
<PackageReference Include="Snap.Hutao.Deployment.Runtime" Version="1.16.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>