From 0e09cdf20af3aeded612d1670d02407c91b22079 Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Wed, 30 Oct 2019 17:03:09 -0400 Subject: [PATCH] automated framework update enhancement --- .../Modules/Admin/Upgrade/Index.razor | 3 +- .../Controllers/InstallationController.cs | 2 +- .../Infrastructure/IInstallationManager.cs | 1 + .../Infrastructure/InstallationManager.cs | 93 +++++++++++++ Oqtane.Server/app_offline.bak | 17 +++ Oqtane.Upgrade/Oqtane.Upgrade.csproj | 15 ++ Oqtane.Upgrade/Program.cs | 129 ++++++++++++++++++ Oqtane.sln | 8 ++ 8 files changed, 266 insertions(+), 2 deletions(-) create mode 100644 Oqtane.Server/app_offline.bak create mode 100644 Oqtane.Upgrade/Oqtane.Upgrade.csproj create mode 100644 Oqtane.Upgrade/Program.cs diff --git a/Oqtane.Client/Modules/Admin/Upgrade/Index.razor b/Oqtane.Client/Modules/Admin/Upgrade/Index.razor index b2180d43..2ae44820 100644 --- a/Oqtane.Client/Modules/Admin/Upgrade/Index.razor +++ b/Oqtane.Client/Modules/Admin/Upgrade/Index.razor @@ -94,8 +94,9 @@ else } } - private void Upgrade() + private async Task Upgrade() { + await InstallationService.Upgrade(); NavigationManager.NavigateTo(NavigateUrl(Reload.Application)); } diff --git a/Oqtane.Server/Controllers/InstallationController.cs b/Oqtane.Server/Controllers/InstallationController.cs index 84c9c2d7..46966d70 100644 --- a/Oqtane.Server/Controllers/InstallationController.cs +++ b/Oqtane.Server/Controllers/InstallationController.cs @@ -279,7 +279,7 @@ namespace Oqtane.Controllers public GenericResponse Upgrade() { var response = new GenericResponse { Success = true, Message = "" }; - InstallationManager.InstallPackages("Framework", true); + InstallationManager.UpgradeFramework(); return response; } } diff --git a/Oqtane.Server/Infrastructure/IInstallationManager.cs b/Oqtane.Server/Infrastructure/IInstallationManager.cs index 8b4fdf33..52afc3f8 100644 --- a/Oqtane.Server/Infrastructure/IInstallationManager.cs +++ b/Oqtane.Server/Infrastructure/IInstallationManager.cs @@ -3,6 +3,7 @@ public interface IInstallationManager { void InstallPackages(string Folders, bool Restart); + void UpgradeFramework(); void RestartApplication(); } } diff --git a/Oqtane.Server/Infrastructure/InstallationManager.cs b/Oqtane.Server/Infrastructure/InstallationManager.cs index 7d6bd394..d6b99661 100644 --- a/Oqtane.Server/Infrastructure/InstallationManager.cs +++ b/Oqtane.Server/Infrastructure/InstallationManager.cs @@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Hosting; using System.Xml; using Oqtane.Shared; using System; +using System.Diagnostics; namespace Oqtane.Infrastructure { @@ -110,6 +111,98 @@ namespace Oqtane.Infrastructure } } + public void UpgradeFramework() + { + string folder = Path.Combine(environment.WebRootPath, "Framework"); + if (Directory.Exists(folder)) + { + string upgradepackage = ""; + + // iterate through packages + foreach (string packagename in Directory.GetFiles(folder, "Oqtane.Framework.*.nupkg")) + { + using (ZipArchive archive = ZipFile.OpenRead(packagename)) + { + string frameworkversion = ""; + // locate nuspec + foreach (ZipArchiveEntry entry in archive.Entries) + { + if (entry.FullName.ToLower().EndsWith(".nuspec")) + { + // open nuspec + XmlTextReader reader = new XmlTextReader(entry.Open()); + reader.Namespaces = false; // remove namespace + XmlDocument doc = new XmlDocument(); + doc.Load(reader); + // get framework version + XmlNode node = doc.SelectSingleNode("/package/metadata/version"); + if (node != null) + { + frameworkversion = node.InnerText; + } + reader.Close(); + } + } + + // ensure package version is higher than current framework + if (frameworkversion != "" && Version.Parse(Constants.Version).CompareTo(Version.Parse(frameworkversion)) < 0) + { + upgradepackage = packagename; + } + else + { + File.Delete(packagename); + } + } + } + + if (upgradepackage != "") + { + FinishUpgrade(upgradepackage); + } + } + } + + private void FinishUpgrade(string packagename) + { + string folder = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location); + + // check if upgrade application exists + if (File.Exists(Path.Combine(folder, "Oqtane.Upgrade.exe"))) + { + // unzip package + using (ZipArchive archive = ZipFile.OpenRead(packagename)) + { + foreach (ZipArchiveEntry entry in archive.Entries) + { + string filename = Path.GetFileName(entry.FullName); + if (Path.GetExtension(filename) == ".dll") + { + entry.ExtractToFile(Path.Combine(Path.Combine(environment.WebRootPath, "Framework"), filename), true); + } + } + } + + // delete package + File.Delete(packagename); + + // run upgrade application + var process = new Process(); + process.StartInfo.FileName = Path.Combine(folder, "Oqtane.Upgrade.exe"); + process.StartInfo.Arguments = ""; + process.StartInfo.ErrorDialog = false; + process.StartInfo.UseShellExecute = false; + process.StartInfo.CreateNoWindow = true; + process.StartInfo.RedirectStandardOutput = false; + process.StartInfo.RedirectStandardError = false; + process.Start(); + process.Dispose(); + + // stop application so upgrade application can proceed + RestartApplication(); + } + } + public void RestartApplication() { HostApplicationLifetime.StopApplication(); diff --git a/Oqtane.Server/app_offline.bak b/Oqtane.Server/app_offline.bak new file mode 100644 index 00000000..94be8d84 --- /dev/null +++ b/Oqtane.Server/app_offline.bak @@ -0,0 +1,17 @@ + + + + + + Oqtane + + + + +
+

Please Wait... Upgrade In Progress....

+ +
+ + + diff --git a/Oqtane.Upgrade/Oqtane.Upgrade.csproj b/Oqtane.Upgrade/Oqtane.Upgrade.csproj new file mode 100644 index 00000000..0ddddf67 --- /dev/null +++ b/Oqtane.Upgrade/Oqtane.Upgrade.csproj @@ -0,0 +1,15 @@ + + + + Exe + netcoreapp3.0 + + + + ..\Oqtane.Server\bin\Release\ + + + ..\Oqtane.Server\bin\Debug\ + + + diff --git a/Oqtane.Upgrade/Program.cs b/Oqtane.Upgrade/Program.cs new file mode 100644 index 00000000..f9ec133c --- /dev/null +++ b/Oqtane.Upgrade/Program.cs @@ -0,0 +1,129 @@ +using System.IO; +using System.Reflection; +using System.Threading; + +namespace Oqtane.Upgrade +{ + class Program + { + static void Main(string[] args) + { + string binfolder = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location); + // assumes that the application executable must be deployed to the /bin of the Oqtane.Server project + if (binfolder.Contains("Oqtane.Server\\bin")) + { + // ie. Oqtane.Server\bin\Debug\netcoreapp3.0\Oqtane.Upgrade.exe + string rootfolder = Directory.GetParent(binfolder).Parent.Parent.FullName; + string deployfolder = Path.Combine(rootfolder, "wwwroot\\Framework"); + + // take the app offline + if (File.Exists(Path.Combine(rootfolder, "app_offline.bak"))) + { + File.Move(Path.Combine(rootfolder, "app_offline.bak"), Path.Combine(rootfolder, "app_offline.htm")); + } + + if (Directory.Exists(deployfolder)) + { + string filename; + string[] files = Directory.GetFiles(deployfolder); + if (CanAccessFiles(files, binfolder)) + { + // backup the files + foreach (string file in files) + { + filename = Path.Combine(binfolder, Path.GetFileName(file)); + if (File.Exists(filename)) + { + File.Move(filename, filename.Replace(".dll", ".bak")); + } + } + + // copy the new files + bool success = true; + try + { + foreach (string file in files) + { + filename = Path.Combine(binfolder, Path.GetFileName(file)); + // delete the file from the /bin if it exists + if (File.Exists(filename)) + { + File.Delete(filename); + } + // copy the new file to the /bin + File.Move(Path.Combine(deployfolder, Path.GetFileName(file)), filename); + } + } + catch + { + // an error occurred deleting or moving a file + success = false; + } + + // restore on failure + if (!success) + { + foreach (string file in files) + { + filename = Path.Combine(binfolder, Path.GetFileName(file)); + if (File.Exists(filename)) + { + File.Move(filename, filename.Replace(".bak", ".dll")); + } + } + } + } + } + + // bring the app back online + if (File.Exists(Path.Combine(rootfolder, "app_offline.htm"))) + { + File.Move(Path.Combine(rootfolder, "app_offline.htm"), Path.Combine(rootfolder, "app_offline.bak")); + } + } + + return; + } + + private static bool CanAccessFiles(string[] files, string folder) + { + // ensure files are not locked by another process - the shutdownTimeLimit defines the duration for app shutdown + bool canaccess = true; + FileStream stream = null; + int i = 0; + while (i < (files.Length - 1) && canaccess) + { + string filepath = Path.Combine(folder, Path.GetFileName(files[i])); + int attempts = 0; + bool locked = true; + // try up to 30 times + while (attempts < 30 && locked == true) + { + try + { + stream = System.IO.File.Open(filepath, FileMode.Open, FileAccess.Read, FileShare.None); + locked = false; + } + catch // file is locked by another process + { + Thread.Sleep(1000); // wait 1 second + } + finally + { + if (stream != null) + { + stream.Close(); + } + } + attempts += 1; + } + if (locked && canaccess) + { + canaccess = false; + } + i += 1; + } + return canaccess; + } + } +} diff --git a/Oqtane.sln b/Oqtane.sln index dd568f30..e3a35ec0 100644 --- a/Oqtane.sln +++ b/Oqtane.sln @@ -9,6 +9,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Oqtane.Client", "Oqtane.Cli EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Oqtane.Shared", "Oqtane.Shared\Oqtane.Shared.csproj", "{19D67A9D-3F2E-41BD-80E6-0B50CA83C3AE}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Oqtane.Upgrade", "Oqtane.Upgrade\Oqtane.Upgrade.csproj", "{2E8C6889-37CF-4C8D-88B1-505547F25098}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -34,6 +36,12 @@ Global {19D67A9D-3F2E-41BD-80E6-0B50CA83C3AE}.Release|Any CPU.Build.0 = Release|Any CPU {19D67A9D-3F2E-41BD-80E6-0B50CA83C3AE}.Wasm|Any CPU.ActiveCfg = Wasm|Any CPU {19D67A9D-3F2E-41BD-80E6-0B50CA83C3AE}.Wasm|Any CPU.Build.0 = Wasm|Any CPU + {2E8C6889-37CF-4C8D-88B1-505547F25098}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2E8C6889-37CF-4C8D-88B1-505547F25098}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2E8C6889-37CF-4C8D-88B1-505547F25098}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2E8C6889-37CF-4C8D-88B1-505547F25098}.Release|Any CPU.Build.0 = Release|Any CPU + {2E8C6889-37CF-4C8D-88B1-505547F25098}.Wasm|Any CPU.ActiveCfg = Debug|Any CPU + {2E8C6889-37CF-4C8D-88B1-505547F25098}.Wasm|Any CPU.Build.0 = Debug|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE