1
0
mirror of https://github.com/chylex/TweetDuck.git synced 2025-05-13 08:34:08 +02:00

Replace ScriptLoader & reimplement resource hot swap

This commit is contained in:
chylex 2021-12-24 06:37:27 +01:00
parent 57b03baad9
commit 5ebfc67e48
Signed by: chylex
GPG Key ID: 4DE42C8F19A80548
13 changed files with 73 additions and 173 deletions

View File

@ -3,6 +3,7 @@
using CefSharp;
using TweetDuck.Utils;
using TweetLib.Core.Browser;
using TweetLib.Core.Utils;
namespace TweetDuck.Browser.Adapters {
sealed class CefScriptExecutor : IScriptExecutor {
@ -21,11 +22,6 @@ public void RunScript(string identifier, string script) {
RunScript(frame, script, identifier);
}
public bool RunFile(string file) {
using IFrame frame = browser.GetMainFrame();
return RunFile(frame, file);
}
public void RunBootstrap(string moduleNamespace) {
using IFrame frame = browser.GetMainFrame();
RunBootstrap(frame, moduleNamespace);
@ -39,12 +35,6 @@ public static void RunScript(IFrame frame, string script, string identifier) {
}
}
public static bool RunFile(IFrame frame, string file) {
string script = Program.Resources.Load(file);
RunScript(frame, script, "root:" + Path.GetFileNameWithoutExtension(file));
return script != null;
}
public static void RunBootstrap(IFrame frame, string moduleNamespace) {
string script = GetBootstrapScript(moduleNamespace, includeStylesheets: true);
@ -54,7 +44,7 @@ public static void RunBootstrap(IFrame frame, string moduleNamespace) {
}
public static string GetBootstrapScript(string moduleNamespace, bool includeStylesheets) {
string script = Program.Resources.Load("bootstrap.js");
string script = FileUtils.ReadFileOrNull(Path.Combine(Program.ResourcesPath, "bootstrap.js"));
if (script == null) {
return null;

View File

@ -16,6 +16,7 @@
using TweetDuck.Dialogs.Settings;
using TweetDuck.Management;
using TweetDuck.Plugins;
using TweetDuck.Resources;
using TweetDuck.Updates;
using TweetDuck.Utils;
using TweetLib.Core.Features.Plugins;
@ -51,6 +52,7 @@ public bool IsWaiting {
private readonly FormNotificationTweet notification;
#pragma warning restore IDE0069 // Disposable fields should be disposed
private readonly ResourceProvider resourceProvider;
private readonly PluginManager plugins;
private readonly UpdateHandler updates;
private readonly ContextMenu contextMenu;
@ -62,11 +64,13 @@ public bool IsWaiting {
private TweetScreenshotManager notificationScreenshotManager;
private VideoPlayer videoPlayer;
public FormBrowser(PluginSchemeFactory pluginScheme) {
public FormBrowser(ResourceProvider resourceProvider, PluginSchemeFactory pluginScheme) {
InitializeComponent();
Text = Program.BrandName;
this.resourceProvider = resourceProvider;
this.plugins = new PluginManager(Program.Config.Plugins, Program.PluginPath, Program.PluginDataPath);
this.plugins.Reloaded += plugins_Reloaded;
this.plugins.Executed += plugins_Executed;
@ -385,7 +389,15 @@ public void ReinjectCustomCSS(string css) {
}
public void ReloadToTweetDeck() {
Program.Resources.OnReloadTriggered();
#if DEBUG
ResourceHotSwap.Run();
resourceProvider.ClearCache();
#else
if (ModifierKeys.HasFlag(Keys.Shift)) {
resourceProvider.ClearCache();
}
#endif
ignoreUpdateCheckError = false;
browser.ReloadToTweetDeck();
}

View File

@ -9,29 +9,6 @@
namespace TweetDuck.Browser.Handling {
internal sealed class ResourceProvider : IResourceProvider<IResourceHandler> {
private static ResourceHandler CreateHandler(byte[] bytes) {
var handler = ResourceHandler.FromStream(new MemoryStream(bytes), autoDisposeStream: true);
handler.Headers.Set("Access-Control-Allow-Origin", "*");
return handler;
}
private static IResourceHandler CreateFileContentsHandler(byte[] bytes, string extension) {
if (bytes.Length == 0) {
return CreateStatusHandler(HttpStatusCode.NoContent, "File is empty."); // FromByteArray crashes CEF internals with no contents
}
else {
var handler = CreateHandler(bytes);
handler.MimeType = Cef.GetMimeType(extension);
return handler;
}
}
private static IResourceHandler CreateStatusHandler(HttpStatusCode code, string message) {
var handler = CreateHandler(Encoding.UTF8.GetBytes(message));
handler.StatusCode = (int) code;
return handler;
}
private readonly Dictionary<string, ICachedResource> cache = new Dictionary<string, ICachedResource>();
public IResourceHandler Status(HttpStatusCode code, string message) {
@ -62,6 +39,33 @@ private ICachedResource FileWithCaching(string path) {
}
}
public void ClearCache() {
cache.Clear();
}
private static ResourceHandler CreateHandler(byte[] bytes) {
var handler = ResourceHandler.FromStream(new MemoryStream(bytes), autoDisposeStream: true);
handler.Headers.Set("Access-Control-Allow-Origin", "*");
return handler;
}
private static IResourceHandler CreateFileContentsHandler(byte[] bytes, string extension) {
if (bytes.Length == 0) {
return CreateStatusHandler(HttpStatusCode.NoContent, "File is empty."); // FromByteArray crashes CEF internals with no contents
}
else {
var handler = CreateHandler(bytes);
handler.MimeType = Cef.GetMimeType(extension);
return handler;
}
}
private static IResourceHandler CreateStatusHandler(HttpStatusCode code, string message) {
var handler = CreateHandler(Encoding.UTF8.GetBytes(message));
handler.StatusCode = (int) code;
return handler;
}
private interface ICachedResource {
IResourceHandler GetResource();
}

View File

@ -1,9 +1,11 @@
using System;
using System.IO;
using System.Windows.Forms;
using CefSharp;
using TweetDuck.Controls;
using TweetLib.Core.Features.Notifications;
using TweetLib.Core.Features.Plugins;
using TweetLib.Core.Utils;
namespace TweetDuck.Browser.Notification.Example {
sealed class FormNotificationExample : FormNotificationMain {
@ -32,7 +34,7 @@ protected override FormBorderStyle NotificationBorderStyle {
public FormNotificationExample(FormBrowser owner, PluginManager pluginManager) : base(owner, pluginManager, false) {
browser.LoadingStateChanged += browser_LoadingStateChanged;
string exampleTweetHTML = Program.Resources.LoadSilent("notification/example/example.html")?.Replace("{avatar}", AppLogo.Url) ?? string.Empty;
string exampleTweetHTML = FileUtils.ReadFileOrNull(Path.Combine(Program.ResourcesPath, "notification/example/example.html"))?.Replace("{avatar}", AppLogo.Url) ?? string.Empty;
#if DEBUG
exampleTweetHTML = exampleTweetHTML.Replace("</p>", @"</p><div style='margin-top:256px'>Scrollbar test padding...</div>");

View File

@ -1,6 +1,7 @@
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Windows.Forms;
using CefSharp;
using TweetDuck.Browser.Adapters;
@ -9,6 +10,7 @@
using TweetDuck.Utils;
using TweetLib.Core.Features.Notifications;
using TweetLib.Core.Features.Plugins;
using TweetLib.Core.Utils;
namespace TweetDuck.Browser.Notification.Screenshot {
sealed class FormNotificationScreenshotable : FormNotificationBase {
@ -29,7 +31,7 @@ public FormNotificationScreenshotable(Action callback, FormBrowser owner, Plugin
return;
}
string script = Program.Resources.LoadSilent("notification/screenshot/screenshot.js");
string script = FileUtils.ReadFileOrNull(Path.Combine(Program.ResourcesPath, "notification/screenshot/screenshot.js"));
if (script == null) {
this.InvokeAsyncSafe(callback);

View File

@ -54,7 +54,6 @@ static class Program {
public static Reporter Reporter { get; }
public static ConfigManager Config { get; }
public static ScriptLoader Resources { get; }
static Program() {
Reporter = new Reporter(ErrorLogFilePath);
@ -62,16 +61,9 @@ static Program() {
Config = new ConfigManager();
#if DEBUG
Resources = new ScriptLoaderDebug();
#else
Resources = new ScriptLoader();
#endif
Lib.Initialize(new App.Builder {
ErrorHandler = Reporter,
SystemHandler = new SystemHandler(),
ResourceHandler = Resources
});
}
@ -147,8 +139,7 @@ private static void Main() {
Win.Application.ApplicationExit += (sender, args) => ExitCleanup();
FormBrowser mainForm = new FormBrowser(pluginScheme);
Resources.Initialize(mainForm);
FormBrowser mainForm = new FormBrowser(resourceProvider, pluginScheme);
Win.Application.Run(mainForm);
if (mainForm.UpdateInstaller != null) {

View File

@ -1,14 +1,9 @@
#if DEBUG
using System;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using TweetDuck.Browser;
using TweetDuck.Management;
using TweetLib.Core.Features.Plugins;
namespace TweetDuck.Resources {
sealed class ScriptLoaderDebug : ScriptLoader {
static class ResourceHotSwap {
private static readonly string HotSwapProjectRoot = FixPathSlash(Path.GetFullPath(Path.Combine(Program.ProgramPath, "../../../")));
private static readonly string HotSwapTargetDir = FixPathSlash(Path.Combine(HotSwapProjectRoot, "bin", "tmp"));
private static readonly string HotSwapRebuildScript = Path.Combine(HotSwapProjectRoot, "bld", "post_build.exe");
@ -17,35 +12,15 @@ private static string FixPathSlash(string path) {
return path.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + '\\';
}
public ScriptLoaderDebug() {
if (File.Exists(HotSwapRebuildScript)) {
Debug.WriteLine("Activating resource hot swap...");
ResetHotSwap();
System.Windows.Forms.Application.ApplicationExit += (sender, args) => ResetHotSwap();
}
}
public override void OnReloadTriggered() {
HotSwap();
}
protected override string LocateFile(string path) {
if (Directory.Exists(HotSwapTargetDir)) {
Debug.WriteLine($"Hot swap active, redirecting {path}");
return Path.Combine(HotSwapTargetDir, "resources", path);
}
return base.LocateFile(path);
}
private void HotSwap() {
public static void Run() {
if (!File.Exists(HotSwapRebuildScript)) {
Debug.WriteLine($"Failed resource hot swap, missing rebuild script: {HotSwapRebuildScript}");
return;
}
ResetHotSwap();
Debug.WriteLine("Performing resource hot swap...");
DeleteHotSwapFolder();
Directory.CreateDirectory(HotSwapTargetDir);
Stopwatch sw = Stopwatch.StartNew();
@ -69,32 +44,20 @@ private void HotSwap() {
sw.Stop();
Debug.WriteLine($"Finished rebuild script in {sw.ElapsedMilliseconds} ms");
ClearCache();
Directory.Delete(Program.ResourcesPath, true);
Directory.Delete(Program.PluginPath, true);
// Force update plugin manager setup scripts
Directory.Move(Path.Combine(HotSwapTargetDir, "resources"), Program.ResourcesPath);
Directory.Move(Path.Combine(HotSwapTargetDir, "plugins"), Program.PluginPath);
string newPluginRoot = Path.Combine(HotSwapTargetDir, "plugins");
const BindingFlags flagsInstance = BindingFlags.Instance | BindingFlags.NonPublic;
Type typePluginManager = typeof(PluginManager);
Type typeFormBrowser = typeof(FormBrowser);
// ReSharper disable PossibleNullReferenceException
object instPluginManager = typeFormBrowser.GetField("plugins", flagsInstance).GetValue(FormManager.TryFind<FormBrowser>());
typePluginManager.GetField("pluginFolder", flagsInstance).SetValue(instPluginManager, newPluginRoot);
Debug.WriteLine("Reloading hot swapped plugins...");
((PluginManager) instPluginManager).Reload();
// ReSharper restore PossibleNullReferenceException
DeleteHotSwapFolder();
}
private void ResetHotSwap() {
private static void DeleteHotSwapFolder() {
try {
Directory.Delete(HotSwapTargetDir, true);
} catch (DirectoryNotFoundException) {}
}
}
}
#endif

View File

@ -1,65 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Windows.Forms;
using TweetDuck.Controls;
using TweetDuck.Dialogs;
using TweetLib.Core.Application;
namespace TweetDuck.Resources {
class ScriptLoader : IAppResourceHandler {
private readonly Dictionary<string, string> cache = new Dictionary<string, string>(16);
private Control sync;
public void Initialize(Control sync) {
this.sync = sync;
}
protected void ClearCache() {
cache.Clear();
}
public virtual void OnReloadTriggered() {
if (Control.ModifierKeys.HasFlag(Keys.Shift)) {
ClearCache();
}
}
public string Load(string path) => LoadInternal(path, silent: false);
public string LoadSilent(string path) => LoadInternal(path, silent: true);
protected virtual string LocateFile(string path) {
return Path.Combine(Program.ResourcesPath, path);
}
private string LoadInternal(string path, bool silent) {
if (sync == null) {
throw new InvalidOperationException("Cannot use ScriptLoader before initialization.");
}
else if (sync.IsDisposed) {
return null; // better than crashing I guess...?
}
if (cache.TryGetValue(path, out string resourceData)) {
return resourceData;
}
string location = LocateFile(path);
string resource;
try {
resource = File.ReadAllText(location, Encoding.UTF8);
} catch (Exception ex) {
ShowLoadError(silent ? null : sync, $"Could not load {path}. The program will continue running with limited functionality.\n\n{ex.Message}");
resource = null;
}
return cache[path] = resource;
}
private static void ShowLoadError(Control sync, string message) {
sync?.InvokeSafe(() => FormMessage.Error("Resource Error", message, FormMessage.OK));
}
}
}

View File

@ -114,8 +114,7 @@
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Reporter.cs" />
<Compile Include="Resources\ResourcesSchemeFactory.cs" />
<Compile Include="Resources\ScriptLoader.cs" />
<Compile Include="Resources\ScriptLoaderDebug.cs" />
<Compile Include="Resources\ResourceHotSwap.cs" />
<Compile Include="Updates\UpdateCheckClient.cs" />
<Compile Include="Updates\UpdateInstaller.cs" />
<Compile Include="Utils\BrowserUtils.cs" />

View File

@ -7,7 +7,6 @@ public sealed class App {
#pragma warning disable CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable.
public static IAppErrorHandler ErrorHandler { get; private set; }
public static IAppSystemHandler SystemHandler { get; private set; }
public static IAppResourceHandler ResourceHandler { get; private set; }
#pragma warning restore CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable.
// Builder
@ -16,14 +15,12 @@ public sealed class App {
public sealed class Builder {
public IAppErrorHandler? ErrorHandler { get; set; }
public IAppSystemHandler? SystemHandler { get; set; }
public IAppResourceHandler? ResourceHandler { get; set; }
// Validation
internal void Initialize() {
App.ErrorHandler = Validate(ErrorHandler, nameof(ErrorHandler));
App.SystemHandler = Validate(SystemHandler, nameof(SystemHandler));
App.ResourceHandler = Validate(ResourceHandler, nameof(ResourceHandler));
}
private T Validate<T>(T? obj, string name) where T : class {

View File

@ -1,5 +0,0 @@
namespace TweetLib.Core.Application {
public interface IAppResourceHandler {
string? Load(string path);
}
}

View File

@ -2,7 +2,6 @@
public interface IScriptExecutor {
void RunFunction(string name, params object[] args);
void RunScript(string identifier, string script);
bool RunFile(string file);
void RunBootstrap(string moduleNamespace);
}
}

View File

@ -1,5 +1,6 @@
using System;
using System.IO;
using System.Text;
namespace TweetLib.Core.Utils {
public static class FileUtils {
@ -37,6 +38,16 @@ public static bool FileExistsAndNotEmpty(string path) {
}
}
public static string? ReadFileOrNull(string path) {
try {
return File.ReadAllText(path, Encoding.UTF8);
} catch (Exception e) {
App.ErrorHandler.Log("Error reading file: " + path);
App.ErrorHandler.Log(e.ToString());
return null;
}
}
public static string ResolveRelativePathSafely(string rootFolder, string relativePath) {
string fullPath = Path.Combine(rootFolder, relativePath);