diff --git a/Daybreak/Configuration/Options/LauncherOptions.cs b/Daybreak/Configuration/Options/LauncherOptions.cs index 098f8e40..6900df96 100644 --- a/Daybreak/Configuration/Options/LauncherOptions.cs +++ b/Daybreak/Configuration/Options/LauncherOptions.cs @@ -56,4 +56,8 @@ public sealed class LauncherOptions [JsonProperty(nameof(BetaUpdate))] [OptionName(Name = "Beta Update", Description = "If true, the launcher will use the new update procedure")] public bool BetaUpdate { get; set; } = true; + + [JsonProperty(nameof(AutoBackupSettings))] + [OptionName(Name = "Auto Backup Settings", Description = "If true, the launcher will attempt to backup settings periodically")] + public bool AutoBackupSettings { get; set; } = false; } diff --git a/Daybreak/Configuration/ProjectConfiguration.cs b/Daybreak/Configuration/ProjectConfiguration.cs index 95988f63..0a1ec130 100644 --- a/Daybreak/Configuration/ProjectConfiguration.cs +++ b/Daybreak/Configuration/ProjectConfiguration.cs @@ -82,6 +82,8 @@ using Daybreak.Views.Onboarding.DirectSong; using Daybreak.Services.SevenZip; using Daybreak.Services.ReShade.Notifications; +using Daybreak.Services.BrowserExtensions; +using Daybreak.Services.UBlockOrigin; namespace Daybreak.Configuration; @@ -155,6 +157,10 @@ public override void RegisterResolvers(IServiceManager serviceManager) .RegisterHttpClient() .WithMessageHandler(this.SetupLoggingAndMetrics) .WithDefaultRequestHeadersSetup(this.SetupDaybreakUserAgent) + .Build() + .RegisterHttpClient() + .WithMessageHandler(this.SetupLoggingAndMetrics) + .WithDefaultRequestHeadersSetup(this.SetupDaybreakUserAgent) .Build(); } @@ -190,19 +196,19 @@ public override void RegisterServices(IServiceCollection services) services.AddSingleton(sp => sp.GetRequiredService()); services.AddSingleton(sp => sp.GetRequiredService()); services.AddSingleton(); - services.AddSingleton(sp => sp.GetRequiredService().As()!); + services.AddSingleton(sp => sp.GetRequiredService().Cast()); services.AddSingleton(sp => new LiteDatabase("Daybreak.db")); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(sp => sp.GetRequiredService().As()!); - services.AddSingleton(sp => sp.GetRequiredService().As()!); - services.AddSingleton(sp => sp.GetRequiredService().As()!); + services.AddSingleton(sp => sp.GetRequiredService().Cast()); + services.AddSingleton(sp => sp.GetRequiredService().Cast()); + services.AddSingleton(sp => sp.GetRequiredService().Cast()); services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(sp => sp.GetRequiredService().As()!); - services.AddSingleton(sp => sp.GetRequiredService().As()!); + services.AddSingleton(sp => sp.GetRequiredService().Cast()); + services.AddSingleton(sp => sp.GetRequiredService().Cast()); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); @@ -220,6 +226,8 @@ public override void RegisterServices(IServiceCollection services) services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddScoped(); + services.AddScoped(sp => sp.GetRequiredService().Cast()); services.AddScoped(); services.AddScoped(); services.AddScoped(); @@ -446,6 +454,12 @@ public override void RegisterMods(IModsManager modsManager) modsManager.RegisterMod(singleton: true); } + public override void RegisterBrowserExtensions(IBrowserExtensionsProducer browserExtensionsProducer) + { + browserExtensionsProducer.ThrowIfNull(); + browserExtensionsProducer.RegisterExtension(); + } + private void RegisterLiteCollections(IServiceCollection services) { this.RegisterLiteCollection(services); diff --git a/Daybreak/Controls/ChromiumBrowserWrapper.xaml.cs b/Daybreak/Controls/ChromiumBrowserWrapper.xaml.cs index ac1c9eea..432640e8 100644 --- a/Daybreak/Controls/ChromiumBrowserWrapper.xaml.cs +++ b/Daybreak/Controls/ChromiumBrowserWrapper.xaml.cs @@ -3,6 +3,7 @@ using Daybreak.Models; using Daybreak.Models.Browser; using Daybreak.Models.Guildwars; +using Daybreak.Services.BrowserExtensions; using Daybreak.Services.BuildTemplates; using Daybreak.Utils; using Microsoft.Extensions.DependencyInjection; @@ -40,9 +41,9 @@ public partial class ChromiumBrowserWrapper : UserControl private const string BrowserDownloadLink = "https://developer.microsoft.com/en-us/microsoft-edge/webview2/"; private static readonly Regex WebAddressRegex = new("^((http|ftp|https)://)?([\\w_-]+(?:(?:\\.[\\w_-]+)+))([\\w.,@?^=%&:/~+#-]*[\\w@?^=%&/~+#-])?", RegexOptions.Compiled); - private static readonly object Lock = new(); + private static readonly SemaphoreSlim SemaphoreSlim = new(1, 1); - private static CoreWebView2Environment? CoreWebView2Environment; + private static CoreWebView2Environment? CoreWebView2Environment; public event EventHandler? FavoriteUriChanged; public event EventHandler? MaximizeClicked; @@ -55,7 +56,8 @@ public partial class ChromiumBrowserWrapper : UserControl private readonly ILiveOptions liveOptions; private readonly ILogger logger; private readonly IBuildTemplateManager buildTemplateManager; - + private readonly IBrowserExtensionsManager browserExtensionsManager; + [GenerateDependencyProperty(InitialValue = true)] private bool canDownloadBuild; [GenerateDependencyProperty(InitialValue = true)] @@ -96,7 +98,9 @@ public string Address } public ChromiumBrowserWrapper() - : this(Launch.Launcher.Instance.ApplicationServiceProvider.GetRequiredService>(), + : this( + Launch.Launcher.Instance.ApplicationServiceProvider.GetRequiredService(), + Launch.Launcher.Instance.ApplicationServiceProvider.GetRequiredService>(), Launch.Launcher.Instance.ApplicationServiceProvider.GetRequiredService>(), Launch.Launcher.Instance.ApplicationServiceProvider.GetRequiredService(), Launch.Launcher.Instance.ApplicationServiceProvider.GetRequiredService>()) @@ -104,16 +108,17 @@ public ChromiumBrowserWrapper() } public ChromiumBrowserWrapper( + IBrowserExtensionsManager browserExtensionsManager, IHttpClient httpClient, ILiveOptions liveOptions, IBuildTemplateManager buildTemplateManager, ILogger logger) { + this.browserExtensionsManager = browserExtensionsManager.ThrowIfNull(); this.httpClient = httpClient.ThrowIfNull(); this.liveOptions = liveOptions.ThrowIfNull(); this.buildTemplateManager = buildTemplateManager.ThrowIfNull(); this.logger = logger.ThrowIfNull(); - this.InitializeEnvironment(); this.InitializeComponent(); this.initializationTask = Task.Run(this.InitializeBrowserSafe); @@ -149,24 +154,22 @@ private void InitializeEnvironment() { try { - lock (Lock) + if (CoreWebView2Environment is not null) { - if (CoreWebView2Environment is not null) - { - this.BrowserSupported = true; - return; - } - - CoreWebView2Environment ??= System.Extensions.TaskExtensions.RunSync(() => CoreWebView2Environment.CreateAsync(null, "BrowserData", new CoreWebView2EnvironmentOptions - { - EnableTrackingPrevention = true, - AllowSingleSignOnUsingOSPrimaryAccount = true - })); - this.BrowserSupported = true; + return; } + + CoreWebView2Environment ??= System.Extensions.TaskExtensions.RunSync(() => CoreWebView2Environment.CreateAsync(null, "BrowserData", new CoreWebView2EnvironmentOptions + { + EnableTrackingPrevention = true, + AllowSingleSignOnUsingOSPrimaryAccount = true, + AreBrowserExtensionsEnabled = true, + })); + + this.BrowserSupported = true; } - catch(Exception e) + catch (Exception e) { this.logger!.LogWarning($"Browser initialization failed. Details: {e}"); this.BrowserSupported = false; @@ -177,10 +180,7 @@ private async Task InitializeBrowserSafe() { await this.Dispatcher.InvokeAsync(async () => { - while (!Monitor.TryEnter(Lock)) - { - await Task.Delay(100); - } + await SemaphoreSlim.WaitAsync(); try { @@ -189,7 +189,7 @@ await this.Dispatcher.InvokeAsync(async () => } finally { - Monitor.Exit(Lock); + SemaphoreSlim.Release(); } }); } @@ -201,12 +201,18 @@ private async Task InitializeBrowser() if (!this.BrowserSupported || !this.BrowserEnabled) { - + + return; + } + + if (this.WebBrowser.CoreWebView2 is not null) + { return; } this.ShowBrowserDisabledMessage = false; - await this.WebBrowser.EnsureCoreWebView2Async(CoreWebView2Environment); + await this.WebBrowser.EnsureCoreWebView2Async(CoreWebView2Environment).ConfigureAwait(true); + await this.browserExtensionsManager.InitializeBrowserEnvironment(this.WebBrowser.CoreWebView2!.Profile, CoreWebView2Environment!.BrowserVersionString).ConfigureAwait(true); if (this.Address is not null && Uri.TryCreate(this.Address, UriKind.RelativeOrAbsolute, out var uri)) { @@ -376,7 +382,7 @@ private void CoreWebView2_WebMessageReceived(object sender, CoreWebView2WebMessa { payload = args.WebMessageAsJson.Deserialize(); } - catch(Exception e) + catch (Exception e) { this.logger!.LogError(e, $"Exception encountered when deserializing {nameof(BrowserPayload)}"); } @@ -411,7 +417,7 @@ private void CoreWebView2_WebMessageReceived(object sender, CoreWebView2WebMessa this.ContextMenu.IsOpen = true; }); } - catch(Exception e) + catch (Exception e) { this.logger!.LogWarning($"Exception when decoding template {maybeTemplate}. Details {e}"); } diff --git a/Daybreak/Daybreak.csproj b/Daybreak/Daybreak.csproj index 48e60758..c35af372 100644 --- a/Daybreak/Daybreak.csproj +++ b/Daybreak/Daybreak.csproj @@ -11,7 +11,7 @@ preview Daybreak.ico true - 0.9.9.6 + 0.9.9.7 true cfb2a489-db80-448d-a969-80270f314c46 True diff --git a/Daybreak/Launch/Launcher.cs b/Daybreak/Launch/Launcher.cs index ec8e2c20..c21f0cda 100644 --- a/Daybreak/Launch/Launcher.cs +++ b/Daybreak/Launch/Launcher.cs @@ -1,5 +1,6 @@ using Daybreak.Configuration; using Daybreak.Models.Progress; +using Daybreak.Services.BrowserExtensions; using Daybreak.Services.Drawing; using Daybreak.Services.ExceptionHandling; using Daybreak.Services.Mods; @@ -95,6 +96,7 @@ protected override void ApplicationStarting() var drawingModuleProducer = this.ServiceProvider.GetRequiredService(); var notificationHandlerProducer = this.ServiceProvider.GetRequiredService(); var modsManager = this.ServiceProvider.GetRequiredService(); + var browserExtensionsProducer = this.ServiceProvider.GetRequiredService(); startupStatus.CurrentStep = StartupStatus.Custom("Loading views"); this.projectConfiguration.RegisterViews(viewProducer); @@ -108,6 +110,8 @@ protected override void ApplicationStarting() this.projectConfiguration.RegisterNotificationHandlers(notificationHandlerProducer); startupStatus.CurrentStep = StartupStatus.Custom("Loading mods"); this.projectConfiguration.RegisterMods(modsManager); + startupStatus.CurrentStep = StartupStatus.Custom("Loading browser extensions"); + this.projectConfiguration.RegisterBrowserExtensions(browserExtensionsProducer); this.logger = this.ServiceProvider.GetRequiredService>(); this.exceptionHandler = this.ServiceProvider.GetRequiredService(); @@ -123,7 +127,8 @@ protected override void ApplicationStarting() startupActionProducer, drawingModuleProducer, notificationHandlerProducer, - modsManager); + modsManager, + browserExtensionsProducer); } catch(Exception e) { diff --git a/Daybreak/Launch/MainWindow.xaml.cs b/Daybreak/Launch/MainWindow.xaml.cs index 27cb9050..4e9a26ab 100644 --- a/Daybreak/Launch/MainWindow.xaml.cs +++ b/Daybreak/Launch/MainWindow.xaml.cs @@ -112,9 +112,9 @@ protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e) private void Window_Loaded(object sender, RoutedEventArgs e) { - this.splashScreenService.HideSplashScreen(); this.SetupImageCycle(); this.viewManager.ShowView(); + this.splashScreenService.HideSplashScreen(); } private void SynchronizeButton_Click(object sender, EventArgs e) diff --git a/Daybreak/Models/Plugins/PluginConfigurationBase.cs b/Daybreak/Models/Plugins/PluginConfigurationBase.cs index 7669d842..44713730 100644 --- a/Daybreak/Models/Plugins/PluginConfigurationBase.cs +++ b/Daybreak/Models/Plugins/PluginConfigurationBase.cs @@ -1,4 +1,5 @@ using Daybreak.Configuration.Options; +using Daybreak.Services.BrowserExtensions; using Daybreak.Services.Drawing; using Daybreak.Services.Metrics; using Daybreak.Services.Mods; @@ -34,6 +35,7 @@ public virtual void RegisterDrawingModules(IDrawingModuleProducer drawingModuleP public virtual void RegisterOptions(IOptionsProducer optionsProducer) { } public virtual void RegisterNotificationHandlers(INotificationHandlerProducer notificationHandlerProducer) { } public virtual void RegisterMods(IModsManager modsManager) { } + public virtual void RegisterBrowserExtensions(IBrowserExtensionsProducer browserExtensionsProducer) { } public PluginConfigurationBase() { diff --git a/Daybreak/Services/BrowserExtensions/BrowserExtensionsManager.cs b/Daybreak/Services/BrowserExtensions/BrowserExtensionsManager.cs new file mode 100644 index 00000000..198d92ef --- /dev/null +++ b/Daybreak/Services/BrowserExtensions/BrowserExtensionsManager.cs @@ -0,0 +1,49 @@ +using Microsoft.Web.WebView2.Core; +using Slim; +using System.Core.Extensions; +using System.Extensions; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Daybreak.Services.BrowserExtensions; + +public sealed class BrowserExtensionsManager : IBrowserExtensionsManager, IBrowserExtensionsProducer +{ + private readonly IServiceManager serviceManager; + + public BrowserExtensionsManager( + IServiceManager serviceManager) + { + this.serviceManager = serviceManager.ThrowIfNull(); + } + + public void RegisterExtension() + where T : class, IBrowserExtension + { + this.serviceManager.RegisterScoped(); + } + + public async Task InitializeBrowserEnvironment(CoreWebView2Profile coreWebView2Profile, string browserVersion) + { + var existingExtensions = await coreWebView2Profile.GetBrowserExtensionsAsync(); + foreach (var extension in this.serviceManager.GetServicesOfType() + .Where(e => existingExtensions.None(ee => ee.Id == e.ExtensionId))) + { + await extension.CheckAndUpdate(browserVersion); + var extensionPath = await extension.GetExtensionPath(); + if (!Directory.Exists(extensionPath)) + { + continue; + } + + if (!File.Exists(Path.Combine(extensionPath, "manifest.json"))) + { + continue; + } + + await coreWebView2Profile.AddBrowserExtensionAsync(extensionPath); + } + } +} diff --git a/Daybreak/Services/BrowserExtensions/IBrowserExtension.cs b/Daybreak/Services/BrowserExtensions/IBrowserExtension.cs new file mode 100644 index 00000000..03ef6d15 --- /dev/null +++ b/Daybreak/Services/BrowserExtensions/IBrowserExtension.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; + +namespace Daybreak.Services.BrowserExtensions; + +public interface IBrowserExtension +{ + string ExtensionId { get; } + Task CheckAndUpdate(string browserVersion); + Task GetExtensionPath(); +} diff --git a/Daybreak/Services/BrowserExtensions/IBrowserExtensionsManager.cs b/Daybreak/Services/BrowserExtensions/IBrowserExtensionsManager.cs new file mode 100644 index 00000000..bd235371 --- /dev/null +++ b/Daybreak/Services/BrowserExtensions/IBrowserExtensionsManager.cs @@ -0,0 +1,9 @@ +using Microsoft.Web.WebView2.Core; +using System.Threading.Tasks; + +namespace Daybreak.Services.BrowserExtensions; + +public interface IBrowserExtensionsManager +{ + Task InitializeBrowserEnvironment(CoreWebView2Profile coreWebView2Profile, string browserVersion); +} diff --git a/Daybreak/Services/BrowserExtensions/IBrowserExtensionsProducer.cs b/Daybreak/Services/BrowserExtensions/IBrowserExtensionsProducer.cs new file mode 100644 index 00000000..3eba4df9 --- /dev/null +++ b/Daybreak/Services/BrowserExtensions/IBrowserExtensionsProducer.cs @@ -0,0 +1,6 @@ +namespace Daybreak.Services.BrowserExtensions; +public interface IBrowserExtensionsProducer +{ + void RegisterExtension() + where T : class, IBrowserExtension; +} diff --git a/Daybreak/Services/Downloads/DownloadService.cs b/Daybreak/Services/Downloads/DownloadService.cs index 8cd91462..6a3cbbb7 100644 --- a/Daybreak/Services/Downloads/DownloadService.cs +++ b/Daybreak/Services/Downloads/DownloadService.cs @@ -48,7 +48,7 @@ public async Task DownloadFile(string downloadUri, string destinationPath, var fileInfo = new FileInfo(destinationPath); fileInfo.Directory?.Create(); var fileStream = File.Open(destinationPath, FileMode.Create, FileAccess.Write); - var downloadSize = (double)response.Content!.Headers!.ContentLength!; + var downloadSize = response.Content?.Headers?.ContentLength ?? double.MaxValue; var buffer = new byte[1024]; var length = 0; var downloaded = 0d; diff --git a/Daybreak/Services/Options/OptionsSynchronizationService.cs b/Daybreak/Services/Options/OptionsSynchronizationService.cs index eb24c479..4580e7de 100644 --- a/Daybreak/Services/Options/OptionsSynchronizationService.cs +++ b/Daybreak/Services/Options/OptionsSynchronizationService.cs @@ -1,32 +1,38 @@ using Daybreak.Attributes; +using Daybreak.Configuration.Options; using Daybreak.Services.Graph; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; +using System.Configuration; using System.Core.Extensions; using System.Extensions; using System.Linq; using System.Reflection; using System.Threading; using System.Threading.Tasks; +using System.Windows.Extensions.Services; namespace Daybreak.Services.Options; -public sealed class OptionsSynchronizationService : IOptionsSynchronizationService +public sealed class OptionsSynchronizationService : IOptionsSynchronizationService, IApplicationLifetimeService { private readonly IOptionsProvider optionsProvider; private readonly IGraphClient graphClient; + private readonly ILiveOptions liveOptions; private readonly ILogger logger; public OptionsSynchronizationService( IOptionsProvider optionsProvider, IGraphClient graphClient, + ILiveOptions liveOptions, ILogger logger) { this.optionsProvider = optionsProvider.ThrowIfNull(); this.graphClient = graphClient.ThrowIfNull(); + this.liveOptions = liveOptions.ThrowIfNull(); this.logger = logger.ThrowIfNull(); } @@ -134,4 +140,32 @@ private static string GetOptionsName(Type type) return optionsNameAttribute.Name!; } + + public async void OnStartup() + { + while (true) + { + await Task.Delay(15000); + + var remoteOptions = await this.GetRemoteOptionsInternal(CancellationToken.None); + var remoteOptionsSerialized = JsonConvert.SerializeObject(remoteOptions); + var currentOptions = JsonConvert.SerializeObject(this.GetCurrentOptionsInternal()); + if (remoteOptions is not null && + currentOptions != remoteOptionsSerialized && + this.liveOptions.Value.AutoBackupSettings) + { + try + { + await this.BackupOptions(CancellationToken.None); + } + catch + { + } + } + } + } + + public void OnClosing() + { + } } diff --git a/Daybreak/Services/Plugins/IPluginsService.cs b/Daybreak/Services/Plugins/IPluginsService.cs index 8eb12738..5b4b246b 100644 --- a/Daybreak/Services/Plugins/IPluginsService.cs +++ b/Daybreak/Services/Plugins/IPluginsService.cs @@ -1,4 +1,5 @@ using Daybreak.Models.Plugins; +using Daybreak.Services.BrowserExtensions; using Daybreak.Services.Drawing; using Daybreak.Services.Mods; using Daybreak.Services.Navigation; @@ -26,7 +27,8 @@ void LoadPlugins( IStartupActionProducer startupActionProducer, IDrawingModuleProducer drawingModuleProducer, INotificationHandlerProducer notificationHandlerProducer, - IModsManager modsManager); + IModsManager modsManager, + IBrowserExtensionsProducer browserExtensionsProducer); IEnumerable GetAvailablePlugins(); diff --git a/Daybreak/Services/Plugins/PluginsService.cs b/Daybreak/Services/Plugins/PluginsService.cs index dd960756..e4299217 100644 --- a/Daybreak/Services/Plugins/PluginsService.cs +++ b/Daybreak/Services/Plugins/PluginsService.cs @@ -1,5 +1,6 @@ using Daybreak.Configuration.Options; using Daybreak.Models.Plugins; +using Daybreak.Services.BrowserExtensions; using Daybreak.Services.Drawing; using Daybreak.Services.Mods; using Daybreak.Services.Navigation; @@ -92,7 +93,8 @@ public void LoadPlugins( IStartupActionProducer startupActionProducer, IDrawingModuleProducer drawingModuleProducer, INotificationHandlerProducer notificationHandlerProducer, - IModsManager modsManager) + IModsManager modsManager, + IBrowserExtensionsProducer browserExtensionsProducer) { serviceManager.ThrowIfNull(); optionsProducer.ThrowIfNull(); @@ -102,6 +104,7 @@ public void LoadPlugins( drawingModuleProducer.ThrowIfNull(); notificationHandlerProducer.ThrowIfNull(); modsManager.ThrowIfNull(); + browserExtensionsProducer.ThrowIfNull(); while (!Monitor.TryEnter(Lock)) { } @@ -135,48 +138,57 @@ public void LoadPlugins( foreach (var result in results) { var pluginScopedLogger = this.logger.CreateScopedLogger(nameof(this.LoadPlugins), result.PluginEntry?.Name ?? string.Empty); - var assembly = ExtractAssembly(result); - LogLoadOperation(result, pluginScopedLogger); - - if (assembly is null) + try { - continue; + var assembly = ExtractAssembly(result); + LogLoadOperation(result, pluginScopedLogger); + + if (assembly is null) + { + continue; + } + + var entryPoint = assembly.GetTypes().FirstOrDefault(t => t.IsSubclassOf(typeof(PluginConfigurationBase))); + if (entryPoint is null) + { + pluginScopedLogger.LogError($"Assembly loaded but unable to find entry point. The plugin will not start"); + continue; + } + + var pluginConfig = Activator.CreateInstance(entryPoint)?.As(); + if (pluginConfig is null) + { + pluginScopedLogger.LogError($"Assembly loaded but unable to create entry point. The plugin will not start"); + continue; + } + + RegisterResolvers(pluginConfig, serviceManager); + pluginScopedLogger.LogInformation("Registered resolvers"); + RegisterServices(pluginConfig, serviceManager); + pluginScopedLogger.LogInformation("Registered services"); + RegisterOptions(pluginConfig, optionsProducer); + pluginScopedLogger.LogInformation("Registered options"); + RegisterViews(pluginConfig, viewManager); + pluginScopedLogger.LogInformation("Registered views"); + RegisterPostUpdateActions(pluginConfig, postUpdateActionProducer); + pluginScopedLogger.LogInformation("Registered post-update actions"); + RegisterStartupActions(pluginConfig, startupActionProducer); + pluginScopedLogger.LogInformation("Registered startup actions"); + RegisterDrawingModules(pluginConfig, drawingModuleProducer); + pluginScopedLogger.LogInformation("Registered drawing modules"); + RegisterNotificationHandlers(pluginConfig, notificationHandlerProducer); + pluginScopedLogger.LogInformation("Registered notification handlers"); + RegisterMods(pluginConfig, modsManager); + pluginScopedLogger.LogInformation("Registered mods"); + RegisterBrowserExtensions(pluginConfig, browserExtensionsProducer); + pluginScopedLogger.LogInformation("Registered browser extensions"); + this.loadedPlugins.Add(new AvailablePlugin { Name = result.PluginEntry?.Name ?? string.Empty, Path = result.PluginEntry?.Path ?? string.Empty, Enabled = true }); + pluginScopedLogger.LogInformation("Loaded plugin"); } - - var entryPoint = assembly.GetTypes().FirstOrDefault(t => t.IsSubclassOf(typeof(PluginConfigurationBase))); - if (entryPoint is null) + catch(Exception e) { - pluginScopedLogger.LogError($"Assembly loaded but unable to find entry point. The plugin will not start"); - continue; + pluginScopedLogger.LogError(e, $"Encountered exception while loading plugin"); } - - var pluginConfig = Activator.CreateInstance(entryPoint)?.As(); - if (pluginConfig is null) - { - pluginScopedLogger.LogError($"Assembly loaded but unable to create entry point. The plugin will not start"); - continue; - } - - RegisterResolvers(pluginConfig, serviceManager); - pluginScopedLogger.LogInformation("Registered resolvers"); - RegisterServices(pluginConfig, serviceManager); - pluginScopedLogger.LogInformation("Registered services"); - RegisterOptions(pluginConfig, optionsProducer); - pluginScopedLogger.LogInformation("Registered options"); - RegisterViews(pluginConfig, viewManager); - pluginScopedLogger.LogInformation("Registered views"); - RegisterPostUpdateActions(pluginConfig, postUpdateActionProducer); - pluginScopedLogger.LogInformation("Registered post-update actions"); - RegisterStartupActions(pluginConfig, startupActionProducer); - pluginScopedLogger.LogInformation("Registered startup actions"); - RegisterDrawingModules(pluginConfig, drawingModuleProducer); - pluginScopedLogger.LogInformation("Registered drawing modules"); - RegisterNotificationHandlers(pluginConfig, notificationHandlerProducer); - pluginScopedLogger.LogInformation("Registered notification handlers"); - RegisterMods(pluginConfig, modsManager); - pluginScopedLogger.LogInformation("Registered mods"); - this.loadedPlugins.Add(new AvailablePlugin { Name = result.PluginEntry?.Name ?? string.Empty, Path = result.PluginEntry?.Path ?? string.Empty, Enabled = true }); - pluginScopedLogger.LogInformation("Loaded plugin"); } Monitor.Exit(Lock); @@ -309,4 +321,6 @@ private static void RegisterServices(PluginConfigurationBase pluginConfig, IServ private static void RegisterNotificationHandlers(PluginConfigurationBase pluginConfig, INotificationHandlerProducer notificationHandlerProducer) => pluginConfig.RegisterNotificationHandlers(notificationHandlerProducer); private static void RegisterMods(PluginConfigurationBase pluginConfig, IModsManager modsManager) => pluginConfig.RegisterMods(modsManager); + + private static void RegisterBrowserExtensions(PluginConfigurationBase pluginConfig, IBrowserExtensionsProducer browserExtensionsProducer) => pluginConfig.RegisterBrowserExtensions(browserExtensionsProducer); } diff --git a/Daybreak/Services/UBlockOrigin/UBlockOriginService.cs b/Daybreak/Services/UBlockOrigin/UBlockOriginService.cs new file mode 100644 index 00000000..3fdbaf52 --- /dev/null +++ b/Daybreak/Services/UBlockOrigin/UBlockOriginService.cs @@ -0,0 +1,185 @@ +using Daybreak.Models.Github; +using Daybreak.Models.Progress; +using Daybreak.Services.BrowserExtensions; +using System.Collections.Generic; +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using System.Core.Extensions; +using System.Net.Http; +using Microsoft.Extensions.Logging; +using System.Extensions; +using System.Linq; +using Daybreak.Services.Downloads; +using Ionic.Zip; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Daybreak.Services.Notifications; + +namespace Daybreak.Services.UBlockOrigin; +public sealed class UBlockOriginService : IBrowserExtension +{ + private const string TagPlaceholder = "[TAG_PLACEHOLDER]"; + private const string ReleaseUrl = "https://github.com/gorhill/uBlock/releases/download/[TAG_PLACEHOLDER]/uBlock0_[TAG_PLACEHOLDER].chromium.zip"; + private const string ReleasesUrl = "https://api.github.com/repos/gorhill/uBlock/git/refs/tags"; + private const string InstallationPath = "BrowserExtensions"; + private const string ZipName = "ublock.chromium.zip"; + private const string InstallationFolderName = "uBlock0.chromium"; + + private static readonly SemaphoreSlim SemaphoreSlim = new(1); + + private static volatile bool VersionUpToDate; + + private readonly INotificationService notificationService; + private readonly IDownloadService downloadService; + private readonly IHttpClient httpClient; + private readonly ILogger logger; + + public UBlockOriginService( + INotificationService notificationService, + IDownloadService downloadService, + IHttpClient httpClient, + ILogger logger) + { + this.notificationService = notificationService.ThrowIfNull(); + this.downloadService = downloadService.ThrowIfNull(); + this.httpClient = httpClient.ThrowIfNull(); + this.logger = logger.ThrowIfNull(); + } + + public string ExtensionId { get; } = "uBlock-Origin"; + + public async Task CheckAndUpdate(string browserVersion) + { + var scopedLogger = this.logger.CreateScopedLogger(nameof(this.CheckAndUpdate), string.Empty); + await SemaphoreSlim.WaitAsync(); + try + { + await this.CheckAndUpdateInternal(browserVersion); + } + catch (Exception ex) + { + scopedLogger.LogError(ex, "Encountered exception"); + } + + SemaphoreSlim.Release(); + } + + public Task GetExtensionPath() + { + return Task.FromResult(Path.GetFullPath(Path.Combine(InstallationPath, InstallationFolderName))); + } + + private async Task CheckAndUpdateInternal(string browserVersion) + { + var scopedLogger = this.logger.CreateScopedLogger(nameof(this.CheckAndUpdateInternal), string.Empty); + if (VersionUpToDate) + { + return; + } + + using var cancellationTokenSource = new CancellationTokenSource(5000); + var currentVersionString = await this.GetCurrentVersion(cancellationTokenSource.Token); + var latestVersionString = await this.GetLatestVersion(cancellationTokenSource.Token); + if (latestVersionString is null) + { + scopedLogger.LogError("Failed to retrieve latest uBlock-Origin version"); + return; + } + + if (currentVersionString is not null && + Models.Versioning.Version.TryParse(currentVersionString, out var currentVersion) && + Models.Versioning.Version.TryParse(latestVersionString, out var latestVersion) && + currentVersion.CompareTo(latestVersion) >= 0) + { + scopedLogger.LogInformation("uBlock-Origin is up to date"); + return; + } + + this.notificationService.NotifyInformation( + title:"Updating uBlock-Origin", + description: "Updating uBlock-Origin browser extension"); + var zipFilePath = await this.DownloadVersion(latestVersionString, CancellationToken.None); + if (zipFilePath is null) + { + scopedLogger.LogError("Failed to retrieve latest uBlock-Origin"); + this.notificationService.NotifyInformation( + title: "Failed to update uBlock-Origin", + description: "Failed to update uBlock-Origin"); + return; + } + + using var zipFile = ZipFile.Read(zipFilePath); + zipFile.ExtractAll(Path.GetFullPath(InstallationPath), ExtractExistingFileAction.OverwriteSilently); + zipFile.Dispose(); + File.Delete(zipFilePath); + VersionUpToDate = true; + this.notificationService.NotifyInformation( + title: "Updated uBlock-Origin", + description: "Updated uBlock-Origin browser extension"); + } + + private async Task DownloadVersion(string version, CancellationToken cancellationToken) + { + var scopedLogger = this.logger.CreateScopedLogger(nameof(this.GetLatestVersion), version); + scopedLogger.LogInformation($"Retrieving version {version}"); + var downloadUrl = ReleaseUrl.Replace(TagPlaceholder, version); + var destinationFolder = Path.GetFullPath(InstallationPath); + var destinationPath = Path.Combine(destinationFolder, ZipName); + var success = await this.downloadService.DownloadFile(downloadUrl, destinationPath, new UpdateStatus(), cancellationToken); + if (!success) + { + throw new InvalidOperationException($"Failed to download uMod version {version}"); + } + + return destinationPath; + } + + private async Task GetCurrentVersion(CancellationToken cancellationToken) + { + var manifestFilePath = Path.GetFullPath(Path.Combine(InstallationPath, Path.Combine(InstallationFolderName, "manifest.json"))); + var fileInfo = new FileInfo(manifestFilePath); + if (!fileInfo.Exists) + { + return default; + } + + var manifest = JsonConvert.DeserializeObject(await File.ReadAllTextAsync(fileInfo.FullName, cancellationToken)); + if (manifest?.TryGetValue("version", out var token) is not true || + token is not JValue tokenValue || + tokenValue.Value is not string value) + { + return default; + } + + return value; + } + + private async Task GetLatestVersion(CancellationToken cancellationToken) + { + var scopedLogger = this.logger.CreateScopedLogger(nameof(this.GetLatestVersion), string.Empty); + scopedLogger.LogInformation("Retrieving version list"); + var getListResponse = await this.httpClient.GetAsync(ReleasesUrl, cancellationToken); + if (!getListResponse.IsSuccessStatusCode) + { + scopedLogger.LogError($"Received non success status code [{getListResponse.StatusCode}]"); + return default; + } + + var responseString = await getListResponse.Content.ReadAsStringAsync(); + var releasesList = responseString.Deserialize>(); + var latestRelease = releasesList? + .Select(t => t.Ref?.Replace("refs/tags/", "")) + .OfType() + .Where(v => !v.Contains("b") && !v.Contains("rc")) + .LastOrDefault(); + if (latestRelease is not string tag) + { + scopedLogger.LogError("Could not parse version list. No latest version found"); + return default; + } + + return latestRelease; + } +}