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:
parent
57b03baad9
commit
5ebfc67e48
Browser
Program.csResources
TweetDuck.csprojlib/TweetLib.Core
@ -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;
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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>");
|
||||
|
@ -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);
|
||||
|
11
Program.cs
11
Program.cs
@ -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) {
|
||||
|
@ -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
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
@ -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" />
|
||||
|
@ -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 {
|
||||
|
@ -1,5 +0,0 @@
|
||||
namespace TweetLib.Core.Application {
|
||||
public interface IAppResourceHandler {
|
||||
string? Load(string path);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user