diff --git a/Dalamud/Dalamud.cs b/Dalamud/Dalamud.cs index 11cb5202f..92c574e4b 100644 --- a/Dalamud/Dalamud.cs +++ b/Dalamud/Dalamud.cs @@ -44,6 +44,7 @@ namespace Dalamud { public readonly DiscordBotManager BotManager; public PluginManager PluginManager { get; private set; } + public PluginRepository PluginRepository { get; private set; } public readonly ClientState ClientState; @@ -112,6 +113,8 @@ namespace Dalamud { try { this.PluginManager = new PluginManager(this, this.StartInfo.PluginDirectory, this.StartInfo.DefaultPluginDirectory); this.PluginManager.LoadPlugins(); + + PluginRepository = new PluginRepository(PluginManager, this.StartInfo.PluginDirectory, this.StartInfo.GameVersion); } catch (Exception ex) { this.Framework.Gui.Chat.PrintError( "[XIVLAUNCHER] There was an error loading additional plugins. Please check the log for more details."); @@ -220,7 +223,7 @@ namespace Dalamud { { if (ImGui.MenuItem("Open Plugin installer")) { - this.pluginWindow = new PluginInstallerWindow(this.PluginManager, this.StartInfo.PluginDirectory, this.StartInfo.GameVersion); + this.pluginWindow = new PluginInstallerWindow(this.PluginManager, this.PluginRepository, this.StartInfo.GameVersion); this.isImguiDrawPluginWindow = true; } if (ImGui.MenuItem("Print plugin info")) { @@ -582,7 +585,7 @@ namespace Dalamud { } private void OnOpenInstallerCommand(string command, string arguments) { - this.pluginWindow = new PluginInstallerWindow(this.PluginManager, this.StartInfo.PluginDirectory, this.StartInfo.GameVersion); + this.pluginWindow = new PluginInstallerWindow(this.PluginManager, PluginRepository, this.StartInfo.GameVersion); this.isImguiDrawPluginWindow = true; } diff --git a/Dalamud/Plugin/PluginInstallerWindow.cs b/Dalamud/Plugin/PluginInstallerWindow.cs index 2abcad69e..7931345ee 100644 --- a/Dalamud/Plugin/PluginInstallerWindow.cs +++ b/Dalamud/Plugin/PluginInstallerWindow.cs @@ -20,9 +20,9 @@ namespace Dalamud.Plugin private const string PluginRepoBaseUrl = "https://goaaats.github.io/DalamudPlugins/"; private PluginManager manager; - private string pluginDirectory; + private PluginRepository repository; private string gameVersion; - private ReadOnlyCollection pluginMaster; + private bool errorModalDrawing = true; private bool errorModalOnNextFrame = false; @@ -38,151 +38,16 @@ namespace Dalamud.Plugin private PluginInstallStatus installStatus = PluginInstallStatus.None; - private bool masterDownloadFailed = false; - - public PluginInstallerWindow(PluginManager manager, string pluginDirectory, string gameVersion) { + public PluginInstallerWindow(PluginManager manager, PluginRepository repository, string gameVersion) { this.manager = manager; - this.pluginDirectory = pluginDirectory; + this.repository = repository; this.gameVersion = gameVersion; - Task.Run(CachePluginMaster).ContinueWith(t => { - this.masterDownloadFailed = this.masterDownloadFailed || t.IsFaulted; - this.errorModalDrawing = this.masterDownloadFailed; - this.errorModalOnNextFrame = this.masterDownloadFailed; - }); - } - - private void CachePluginMaster() { - try { - using var client = new WebClient(); - - var data = client.DownloadString(PluginRepoBaseUrl + "pluginmaster.json"); - - this.pluginMaster = JsonConvert.DeserializeObject>(data); - } catch { - this.masterDownloadFailed = true; - } - } - - public void UpdatePlugins() { - Log.Information("Starting plugin update..."); - - var updatedCount = 0; - this.installStatus = PluginInstallStatus.Success; - - try { - var pluginsDirectory = new DirectoryInfo(this.pluginDirectory); - foreach (var installed in pluginsDirectory.GetDirectories()) { - var versions = installed.GetDirectories(); - - if (versions.Length == 0) { - Log.Information("Has no versions: {0}", installed.FullName); - continue; - } - - var sortedVersions = versions.OrderBy(x => int.Parse(x.Name.Replace(".",""))); - var latest = sortedVersions.Last(); - - var localInfoFile = new FileInfo(Path.Combine(latest.FullName, $"{installed.Name}.json")); - - if (!localInfoFile.Exists) - { - Log.Information("Has no definition: {0}", localInfoFile.FullName); - continue; - } - - var info = JsonConvert.DeserializeObject(File.ReadAllText(localInfoFile.FullName)); - - var remoteInfo = this.pluginMaster.FirstOrDefault(x => x.Name == info.Name); - - if (remoteInfo == null) - { - Log.Information("Is not in pluginmaster: {0}", info.Name); - continue; - } - - if (remoteInfo.AssemblyVersion != info.AssemblyVersion) - { - Log.Information("Eligible for update: {0}", remoteInfo.InternalName); - - // DisablePlugin() below immediately creates a .disabled file anyway, but will fail - // with an exception if we try to do it twice in row like this - // TODO: not sure if doing this for all versions is really necessary, since the - // others really needed to be disabled before anyway - //foreach (var sortedVersion in sortedVersions) { - // File.Create(Path.Combine(sortedVersion.FullName, ".disabled")); - //} - - // Try to disable plugin if it is loaded - try { - this.manager.DisablePlugin(info); - } catch (Exception ex) { - Log.Error(ex, "Plugin disable failed"); - } - - var installSuccess = InstallPlugin(remoteInfo); - - if (installSuccess) { - updatedCount++; - } else { - Log.Error("InstallPlugin failed."); - } - } else { - Log.Information("Up to date: {0}", remoteInfo.InternalName); - } - } - } - catch (Exception e) - { - Log.Error(e, "Plugin update failed hard."); - this.installStatus = PluginInstallStatus.Fail; - } - - this.updatePluginCount = updatedCount; - this.updateComplete = true; - - Log.Information("Plugin update OK."); - } - - private bool InstallPlugin(PluginDefinition definition) { - try { - var outputDir = new DirectoryInfo(Path.Combine(this.pluginDirectory, definition.InternalName, definition.AssemblyVersion)); - var dllFile = new FileInfo(Path.Combine(outputDir.FullName, $"{definition.InternalName}.dll")); - var disabledFile = new FileInfo(Path.Combine(outputDir.FullName, ".disabled")); - - if (dllFile.Exists) { - if (disabledFile.Exists) - disabledFile.Delete(); - - this.installStatus = this.manager.LoadPluginFromAssembly(dllFile, false) ? PluginInstallStatus.Success : PluginInstallStatus.Fail; - return this.installStatus == PluginInstallStatus.Success; - } - - if (outputDir.Exists) - outputDir.Delete(true); - outputDir.Create(); - - var path = Path.GetTempFileName(); - Log.Information("Downloading plugin to {0}", path); - using var client = new WebClient(); - client.DownloadFile(PluginRepoBaseUrl + $"/plugins/{definition.InternalName}/latest.zip", path); - - Log.Information("Extracting to {0}", outputDir); - - ZipFile.ExtractToDirectory(path, outputDir.FullName); - - this.installStatus = this.manager.LoadPluginFromAssembly(dllFile, false) ? PluginInstallStatus.Success : PluginInstallStatus.Fail; - } catch (Exception e) { - Log.Error(e, "Plugin download failed hard."); - this.installStatus = PluginInstallStatus.Fail; - } - - return this.installStatus == PluginInstallStatus.Success; } public bool Draw() { var windowOpen = true; - ImGui.SetNextWindowSize(new Vector2(750, 518)); + ImGui.SetNextWindowSize(new Vector2(750, 520)); ImGui.Begin("Plugin Installer", ref windowOpen, ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoScrollbar); @@ -195,14 +60,14 @@ namespace Dalamud.Plugin ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(1, 3)); - if (this.pluginMaster == null) { + if (this.repository.State == PluginRepository.InitializationState.InProgress) { ImGui.Text("Loading plugins..."); - } else if (this.masterDownloadFailed) { + } else if (this.repository.State == PluginRepository.InitializationState.Fail) { ImGui.Text("Download failed."); } else { - foreach (var pluginDefinition in this.pluginMaster) { + foreach (var pluginDefinition in this.repository.PluginMaster) { if (pluginDefinition.ApplicableVersion != this.gameVersion && pluginDefinition.ApplicableVersion != "any") continue; @@ -231,9 +96,12 @@ namespace Dalamud.Plugin if (ImGui.Button($"Install v{pluginDefinition.AssemblyVersion}")) { this.installStatus = PluginInstallStatus.InProgress; - Task.Run(() => InstallPlugin(pluginDefinition)).ContinueWith(t => { + Task.Run(() => this.repository.InstallPlugin(pluginDefinition)).ContinueWith(t => { + this.installStatus = + t.Result ? PluginInstallStatus.Success : PluginInstallStatus.Fail; this.installStatus = t.IsFaulted ? PluginInstallStatus.Fail : this.installStatus; + this.errorModalDrawing = this.installStatus == PluginInstallStatus.Fail; this.errorModalOnNextFrame = this.installStatus == PluginInstallStatus.Fail; }); @@ -288,13 +156,21 @@ namespace Dalamud.Plugin { this.installStatus = PluginInstallStatus.InProgress; - Task.Run(UpdatePlugins).ContinueWith(t => { + Task.Run(() => this.repository.UpdatePlugins()).ContinueWith(t => { + this.installStatus = + t.Result.Success ? PluginInstallStatus.Success : PluginInstallStatus.Fail; this.installStatus = t.IsFaulted ? PluginInstallStatus.Fail : this.installStatus; + + if (this.installStatus == PluginInstallStatus.Success) { + this.updateComplete = true; + this.updatePluginCount = t.Result.UpdatedCount; + } + this.errorModalDrawing = this.installStatus == PluginInstallStatus.Fail; this.errorModalOnNextFrame = this.installStatus == PluginInstallStatus.Fail; }); - } + } } } diff --git a/Dalamud/Plugin/PluginRepository.cs b/Dalamud/Plugin/PluginRepository.cs new file mode 100644 index 000000000..0f386b1d1 --- /dev/null +++ b/Dalamud/Plugin/PluginRepository.cs @@ -0,0 +1,197 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Serilog; + +namespace Dalamud.Plugin +{ + public class PluginRepository + { + private const string PluginRepoBaseUrl = "https://goaaats.github.io/DalamudPlugins/"; + + private PluginManager manager; + private string pluginDirectory; + public ReadOnlyCollection PluginMaster; + + public enum InitializationState { + Unknown, + InProgress, + Success, + Fail + } + + public InitializationState State { get; private set; } + + public PluginRepository(PluginManager manager, string pluginDirectory, string gameVersion) + { + this.manager = manager; + this.pluginDirectory = pluginDirectory; + + State = InitializationState.InProgress; + Task.Run(CachePluginMaster).ContinueWith(t => { + if (t.IsFaulted) + State = InitializationState.Fail; + }); + } + + private void CachePluginMaster() + { + try + { + using var client = new WebClient(); + + var data = client.DownloadString(PluginRepoBaseUrl + "pluginmaster.json"); + + this.PluginMaster = JsonConvert.DeserializeObject>(data); + + State = InitializationState.Success; + } + catch { + State = InitializationState.Fail; + } + } + + public bool InstallPlugin(PluginDefinition definition) { + try + { + var outputDir = new DirectoryInfo(Path.Combine(this.pluginDirectory, definition.InternalName, definition.AssemblyVersion)); + var dllFile = new FileInfo(Path.Combine(outputDir.FullName, $"{definition.InternalName}.dll")); + var disabledFile = new FileInfo(Path.Combine(outputDir.FullName, ".disabled")); + + if (dllFile.Exists) + { + if (disabledFile.Exists) + disabledFile.Delete(); + + return this.manager.LoadPluginFromAssembly(dllFile, false); + } + + if (outputDir.Exists) + outputDir.Delete(true); + outputDir.Create(); + + var path = Path.GetTempFileName(); + Log.Information("Downloading plugin to {0}", path); + using var client = new WebClient(); + client.DownloadFile(PluginRepoBaseUrl + $"/plugins/{definition.InternalName}/latest.zip", path); + + Log.Information("Extracting to {0}", outputDir); + + ZipFile.ExtractToDirectory(path, outputDir.FullName); + + return this.manager.LoadPluginFromAssembly(dllFile, false); + } + catch (Exception e) + { + Log.Error(e, "Plugin download failed hard."); + return false; + } + } + + public (bool Success, int UpdatedCount) UpdatePlugins(bool dryRun = false) + { + Log.Information("Starting plugin update... dry:{0}", dryRun); + + var updatedCount = 0; + var hasError = false; + + try + { + var pluginsDirectory = new DirectoryInfo(this.pluginDirectory); + foreach (var installed in pluginsDirectory.GetDirectories()) + { + var versions = installed.GetDirectories(); + + if (versions.Length == 0) + { + Log.Information("Has no versions: {0}", installed.FullName); + continue; + } + + var sortedVersions = versions.OrderBy(x => int.Parse(x.Name.Replace(".", ""))); + var latest = sortedVersions.Last(); + + var localInfoFile = new FileInfo(Path.Combine(latest.FullName, $"{installed.Name}.json")); + + if (!localInfoFile.Exists) + { + Log.Information("Has no definition: {0}", localInfoFile.FullName); + continue; + } + + var info = JsonConvert.DeserializeObject(File.ReadAllText(localInfoFile.FullName)); + + var remoteInfo = this.PluginMaster.FirstOrDefault(x => x.Name == info.Name); + + if (remoteInfo == null) + { + Log.Information("Is not in pluginmaster: {0}", info.Name); + continue; + } + + if (remoteInfo.AssemblyVersion != info.AssemblyVersion) + { + Log.Information("Eligible for update: {0}", remoteInfo.InternalName); + + // DisablePlugin() below immediately creates a .disabled file anyway, but will fail + // with an exception if we try to do it twice in row like this + // TODO: not sure if doing this for all versions is really necessary, since the + // others really needed to be disabled before anyway + //foreach (var sortedVersion in sortedVersions) { + // File.Create(Path.Combine(sortedVersion.FullName, ".disabled")); + //} + + if (!dryRun) + { + // Try to disable plugin if it is loaded + try + { + this.manager.DisablePlugin(info); + } + catch (Exception ex) + { + Log.Error(ex, "Plugin disable failed"); + hasError = true; + } + + var installSuccess = InstallPlugin(remoteInfo); + + if (installSuccess) + { + updatedCount++; + } + else + { + Log.Error("InstallPlugin failed."); + hasError = true; + } + } + else { + updatedCount++; + } + } + else + { + Log.Information("Up to date: {0}", remoteInfo.InternalName); + } + } + } + catch (Exception e) + { + Log.Error(e, "Plugin update failed hard."); + hasError = true; + } + + Log.Information("Plugin update OK."); + + return (!hasError, updatedCount); + } + } +}