From 5dcc7c14f3d157ef9082254631d4c0cff8de157b Mon Sep 17 00:00:00 2001 From: sbwalker Date: Mon, 7 Apr 2025 13:18:52 -0400 Subject: [PATCH] optimize the System Update process --- .../Modules/Admin/Upgrade/Index.razor | 29 ++- .../Modules/Admin/Upgrade/Index.resx | 8 +- Oqtane.Client/Services/InstallationService.cs | 4 +- .../Interfaces/IInstallationService.cs | 3 +- .../Controllers/InstallationController.cs | 4 +- .../Infrastructure/InstallationManager.cs | 8 +- .../Interfaces/IInstallationManager.cs | 2 +- Oqtane.Updater/Program.cs | 173 +++++++++++------- 8 files changed, 150 insertions(+), 81 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/Upgrade/Index.razor b/Oqtane.Client/Modules/Admin/Upgrade/Index.razor index c43ae566..31fee382 100644 --- a/Oqtane.Client/Modules/Admin/Upgrade/Index.razor +++ b/Oqtane.Client/Modules/Admin/Upgrade/Index.razor @@ -13,9 +13,21 @@ @if (_package != null && _upgradeavailable) { - +
+
+ +
+ +
+
+
+

+ } else { @@ -23,7 +35,6 @@ }
-
@@ -31,8 +42,19 @@
+
+ +
+ +
+
+

+
} @@ -41,6 +63,7 @@ private bool _initialized = false; private Package _package; private bool _upgradeavailable = false; + private string _backup = "True"; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host; @@ -86,7 +109,7 @@ ShowProgressIndicator(); var interop = new Interop(JSRuntime); await interop.RedirectBrowser(NavigateUrl(), 10); - await InstallationService.Upgrade(); + await InstallationService.Upgrade(bool.Parse(_backup)); } catch (Exception ex) { diff --git a/Oqtane.Client/Resources/Modules/Admin/Upgrade/Index.resx b/Oqtane.Client/Resources/Modules/Admin/Upgrade/Index.resx index a833fa20..e609f42a 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Upgrade/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Upgrade/Index.resx @@ -151,6 +151,12 @@ You Cannot Perform A System Update In A Development Environment - Please Note That The System Update Capability Is A Simplified Upgrade Process Intended For Small To Medium Sized Installations. For Larger Enterprise Installations You Will Want To Use A Manual Upgrade Process. Also Note That The System Update Capability Is Not Recommended When Using Microsoft Azure Due To Environmental Limitations. + Please Note That The System Update Capability Is A Simplified Upgrade Process Intended For Small To Medium Sized Installations. For Larger Enterprise Installations You Will Want To Use A Manual Upgrade Process. + + + Backup Files? + + + Specify if you want to backup files during the upgrade process. Disabling this option will result in a better experience in some environments. \ No newline at end of file diff --git a/Oqtane.Client/Services/InstallationService.cs b/Oqtane.Client/Services/InstallationService.cs index f1fbd057..e0c388c8 100644 --- a/Oqtane.Client/Services/InstallationService.cs +++ b/Oqtane.Client/Services/InstallationService.cs @@ -47,9 +47,9 @@ namespace Oqtane.Services return await PostJsonAsync(ApiUrl, config); } - public async Task Upgrade() + public async Task Upgrade(bool backup) { - return await GetJsonAsync($"{ApiUrl}/upgrade"); + return await GetJsonAsync($"{ApiUrl}/upgrade/?backup={backup}"); } public async Task RestartAsync() diff --git a/Oqtane.Client/Services/Interfaces/IInstallationService.cs b/Oqtane.Client/Services/Interfaces/IInstallationService.cs index e8a433c7..1e2311ce 100644 --- a/Oqtane.Client/Services/Interfaces/IInstallationService.cs +++ b/Oqtane.Client/Services/Interfaces/IInstallationService.cs @@ -26,8 +26,9 @@ namespace Oqtane.Services /// /// Starts the upgrade process /// + /// indicates if files should be backed up during upgrade /// internal status/message object - Task Upgrade(); + Task Upgrade(bool backup); /// /// Restarts the installation diff --git a/Oqtane.Server/Controllers/InstallationController.cs b/Oqtane.Server/Controllers/InstallationController.cs index 0ad80eb8..534e05c3 100644 --- a/Oqtane.Server/Controllers/InstallationController.cs +++ b/Oqtane.Server/Controllers/InstallationController.cs @@ -88,10 +88,10 @@ namespace Oqtane.Controllers [HttpGet("upgrade")] [Authorize(Roles = RoleNames.Host)] - public Installation Upgrade() + public Installation Upgrade(string backup) { var installation = new Installation { Success = true, Message = "" }; - _installationManager.UpgradeFramework(); + _installationManager.UpgradeFramework(bool.Parse(backup)); return installation; } diff --git a/Oqtane.Server/Infrastructure/InstallationManager.cs b/Oqtane.Server/Infrastructure/InstallationManager.cs index fdfca028..1591c150 100644 --- a/Oqtane.Server/Infrastructure/InstallationManager.cs +++ b/Oqtane.Server/Infrastructure/InstallationManager.cs @@ -380,7 +380,7 @@ namespace Oqtane.Infrastructure File.WriteAllText(assemblyLogPath, JsonSerializer.Serialize(assemblies, new JsonSerializerOptions { WriteIndented = true })); } - public async Task UpgradeFramework() + public async Task UpgradeFramework(bool backup) { string folder = Path.Combine(_environment.ContentRootPath, Constants.PackagesFolder); if (Directory.Exists(folder)) @@ -448,14 +448,14 @@ namespace Oqtane.Infrastructure // install Oqtane.Upgrade zip package if (File.Exists(upgradepackage)) { - FinishUpgrade(); + FinishUpgrade(backup); } } } } } - private void FinishUpgrade() + private void FinishUpgrade(bool backup) { // check if updater application exists string Updater = Constants.UpdaterPackageId + ".dll"; @@ -469,7 +469,7 @@ namespace Oqtane.Infrastructure { WorkingDirectory = folder, FileName = "dotnet", - Arguments = Path.Combine(folder, Updater) + " \"" + _environment.ContentRootPath + "\" \"" + _environment.WebRootPath + "\"", + Arguments = Path.Combine(folder, Updater) + " \"" + _environment.ContentRootPath + "\" \"" + _environment.WebRootPath + "\" \"" + backup.ToString() + "\"", UseShellExecute = false, ErrorDialog = false, CreateNoWindow = true, diff --git a/Oqtane.Server/Infrastructure/Interfaces/IInstallationManager.cs b/Oqtane.Server/Infrastructure/Interfaces/IInstallationManager.cs index 0e5bcc6d..1d1648c7 100644 --- a/Oqtane.Server/Infrastructure/Interfaces/IInstallationManager.cs +++ b/Oqtane.Server/Infrastructure/Interfaces/IInstallationManager.cs @@ -7,7 +7,7 @@ namespace Oqtane.Infrastructure void InstallPackages(); bool UninstallPackage(string PackageName); int RegisterAssemblies(); - Task UpgradeFramework(); + Task UpgradeFramework(bool backup); void RestartApplication(); } } diff --git a/Oqtane.Updater/Program.cs b/Oqtane.Updater/Program.cs index cd8f8359..5cac25f6 100644 --- a/Oqtane.Updater/Program.cs +++ b/Oqtane.Updater/Program.cs @@ -15,17 +15,19 @@ namespace Oqtane.Updater /// static void Main(string[] args) { - // requires 2 arguments - the ContentRootPath and the WebRootPath of the site + // requires 3 arguments - the ContentRootPath, the WebRootPath of the site, and a backup flag // for testing purposes you can uncomment and modify the logic below - //Array.Resize(ref args, 2); + //Array.Resize(ref args, 3); //args[0] = @"C:\yourpath\oqtane.framework\Oqtane.Server"; //args[1] = @"C:\yourpath\oqtane.framework\Oqtane.Server\wwwroot"; + //args[2] = @"true"; - if (args.Length == 2) + if (args.Length == 3) { string contentrootfolder = args[0]; string webrootfolder = args[1]; + bool backup = bool.Parse(args[2]); string deployfolder = Path.Combine(contentrootfolder, "Packages"); string backupfolder = Path.Combine(contentrootfolder, "Backup"); @@ -49,6 +51,7 @@ namespace Oqtane.Updater WriteLog(logFilePath, "Upgrade Process Started: " + DateTime.UtcNow.ToString() + Environment.NewLine); WriteLog(logFilePath, "ContentRootPath: " + contentrootfolder + Environment.NewLine); WriteLog(logFilePath, "WebRootPath: " + webrootfolder + Environment.NewLine); + if (packagename != "" && File.Exists(Path.Combine(webrootfolder, "app_offline.bak"))) { WriteLog(logFilePath, "Located Upgrade Package: " + packagename + Environment.NewLine); @@ -74,12 +77,12 @@ namespace Oqtane.Updater } bool success = true; - // ensure files are not locked - if (CanAccessFiles(files)) + + if (backup) { UpdateOfflineContent(offlineFilePath, offlineTemplate, 10, "Preparing Backup Folder"); WriteLog(logFilePath, "Preparing Backup Folder: " + backupfolder + Environment.NewLine); - + try { // clear out backup folder @@ -112,8 +115,28 @@ namespace Oqtane.Updater { Directory.CreateDirectory(Path.GetDirectoryName(filename)); } - File.Copy(file, filename); - WriteLog(logFilePath, "Copy File: " + filename + Environment.NewLine); + + try + { + // try optimistically to backup the file + File.Copy(file, filename); + WriteLog(logFilePath, "Copy File: " + filename + Environment.NewLine); + } + catch + { + // if the file is locked, wait until it is unlocked + if (CanAccessFile(file)) + { + File.Copy(file, filename); + WriteLog(logFilePath, "Copy File: " + filename + Environment.NewLine); + } + else + { + // file could not be backed up, upgrade unsuccessful + success = false; + WriteLog(logFilePath, "Error Backing Up Files" + Environment.NewLine); + } + } } } catch (Exception ex) @@ -125,17 +148,20 @@ namespace Oqtane.Updater } } } + } - // extract files - if (success) + // extract files + if (success) + { + UpdateOfflineContent(offlineFilePath, offlineTemplate, 50, "Extracting Files From Upgrade Package"); + WriteLog(logFilePath, "Extracting Files From Upgrade Package..." + Environment.NewLine); + try { - UpdateOfflineContent(offlineFilePath, offlineTemplate, 50, "Extracting Files From Upgrade Package"); - WriteLog(logFilePath, "Extracting Files From Upgrade Package..." + Environment.NewLine); - try + using (ZipArchive archive = ZipFile.OpenRead(packagename)) { - using (ZipArchive archive = ZipFile.OpenRead(packagename)) + foreach (ZipArchiveEntry entry in archive.Entries) { - foreach (ZipArchiveEntry entry in archive.Entries) + if (success) { if (!string.IsNullOrEmpty(entry.Name)) { @@ -144,20 +170,42 @@ namespace Oqtane.Updater { Directory.CreateDirectory(Path.GetDirectoryName(filename)); } - entry.ExtractToFile(filename, true); - WriteLog(logFilePath, "Exact File: " + filename + Environment.NewLine); + + try + { + // try optimistically to extract the file + entry.ExtractToFile(filename, true); + WriteLog(logFilePath, "Exact File: " + filename + Environment.NewLine); + } + catch + { + // if the file is locked, wait until it is unlocked + if (CanAccessFile(filename)) + { + entry.ExtractToFile(filename, true); + WriteLog(logFilePath, "Exact File: " + filename + Environment.NewLine); + } + else + { + // file could not be extracted, upgrade unsuccessful + success = false; + WriteLog(logFilePath, "Error Extracting Files From Upgrade Package" + Environment.NewLine); + } + } } } } } - catch (Exception ex) - { - success = false; - UpdateOfflineContent(offlineFilePath, offlineTemplate, 95, "Error Extracting Files From Upgrade Package", "bg-danger"); - WriteLog(logFilePath, "Error Extracting Files From Upgrade Package: " + ex.Message + Environment.NewLine); - } + } + catch (Exception ex) + { + success = false; + WriteLog(logFilePath, "Error Extracting Files From Upgrade Package: " + ex.Message + Environment.NewLine); + } - if (success) + if (success) + { + if (backup) { UpdateOfflineContent(offlineFilePath, offlineTemplate, 90, "Removing Backup Folder"); WriteLog(logFilePath, "Removing Backup Folder..." + Environment.NewLine); @@ -174,7 +222,12 @@ namespace Oqtane.Updater WriteLog(logFilePath, "Error Removing Backup Folder: " + ex.Message + Environment.NewLine); } } - else + } + else + { + UpdateOfflineContent(offlineFilePath, offlineTemplate, 95, "Error Extracting Files From Upgrade Package", "bg-danger"); + + if (backup) { UpdateOfflineContent(offlineFilePath, offlineTemplate, 50, "Upgrade Failed, Restoring Files From Backup Folder", "bg-warning"); WriteLog(logFilePath, "Restoring Files From Backup Folder..." + Environment.NewLine); @@ -201,18 +254,14 @@ namespace Oqtane.Updater } } } - else - { - UpdateOfflineContent(offlineFilePath, offlineTemplate, 95, "Upgrade Failed: Could Not Backup Files", "bg-danger"); - WriteLog(logFilePath, "Upgrade Failed: Could Not Backup Files" + Environment.NewLine); - } } else { - UpdateOfflineContent(offlineFilePath, offlineTemplate, 95, "Upgrade Failed: Some Files Are Locked By The Hosting Environment", "bg-danger"); - WriteLog(logFilePath, "Upgrade Failed: Some Files Are Locked By The Hosting Environment" + Environment.NewLine); + UpdateOfflineContent(offlineFilePath, offlineTemplate, 95, "Upgrade Failed: Could Not Backup Files", "bg-danger"); + WriteLog(logFilePath, "Upgrade Failed: Could Not Backup Files" + Environment.NewLine); } + UpdateOfflineContent(offlineFilePath, offlineTemplate, 100, "Upgrade Process Finished, Reloading", success ? "" : "bg-danger"); Thread.Sleep(3000); //wait for 3 seconds to complete the upgrade process. // bring the app back online @@ -240,55 +289,45 @@ namespace Oqtane.Updater } } - private static bool CanAccessFiles(List files) + private static bool CanAccessFile(string filepath) { - // ensure files are not locked by another process - // the IIS ShutdownTimeLimit defines the duration for app shutdown (default is 90 seconds) - // websockets can delay application shutdown (ie. Blazor Server) + // ensure file is not locked by another process int retries = 60; int sleep = 2; - - bool canAccess = true; + int attempts = 0; FileStream stream = null; - int i = 0; - while (i < (files.Count - 1) && canAccess) - { - string filepath = files[i]; - int attempts = 0; - bool locked = true; - while (attempts < retries && locked) + bool locked = true; + while (attempts < retries && locked) + { + try { - try + if (File.Exists(filepath)) { - if (File.Exists(filepath)) - { - stream = File.Open(filepath, FileMode.Open, FileAccess.Read, FileShare.None); - locked = false; - } - else - { - locked = false; - } + stream = File.Open(filepath, FileMode.Open, FileAccess.Read, FileShare.None); + locked = false; } - catch // file is locked by another process + else { - Thread.Sleep(sleep * 1000); // wait + locked = false; } - finally - { - stream?.Close(); - } - attempts += 1; } - if (locked) + catch // file is locked by another process { - canAccess = false; - Console.WriteLine("File Locked: " + filepath); + Thread.Sleep(sleep * 1000); // wait } - i += 1; + finally + { + stream?.Close(); + } + attempts += 1; } - return canAccess; + if (locked) + { + Console.WriteLine("File Locked: " + filepath); + } + + return !locked; } private static void UpdateOfflineContent(string filePath, string contentTemplate, int progress, string status, string progressClass = "")