optimize the System Update process

This commit is contained in:
sbwalker 2025-04-07 13:18:52 -04:00
parent 1f8c54ce74
commit 5dcc7c14f3
8 changed files with 150 additions and 81 deletions

View File

@ -13,9 +13,21 @@
<TabPanel Name="Download" ResourceKey="Download"> <TabPanel Name="Download" ResourceKey="Download">
@if (_package != null && _upgradeavailable) @if (_package != null && _upgradeavailable)
{ {
<ModuleMessage Type="MessageType.Info" Message="Select The Download Button To Download The Framework Upgrade Package And Then Select Upgrade"></ModuleMessage> <div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" HelpText="Specify if you want to backup files during the upgrade process. Disabling this option will result in a better experience in some environments." ResourceKey="Backup">Backup Files? </Label>
<div class="col-sm-9">
<select id="backup" class="form-select" @bind="@_backup">
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
</div>
<button type="button" class="btn btn-primary" @onclick=@(async () => await Download(Constants.PackageId, @_package.Version))>@SharedLocalizer["Download"] @_package.Version</button> <button type="button" class="btn btn-primary" @onclick=@(async () => await Download(Constants.PackageId, @_package.Version))>@SharedLocalizer["Download"] @_package.Version</button>
<button type="button" class="btn btn-success" @onclick="Upgrade">@SharedLocalizer["Upgrade"]</button> <button type="button" class="btn btn-success" @onclick="Upgrade">@SharedLocalizer["Upgrade"]</button>
<br /><br />
<ModuleMessage Type="MessageType.Info" Message="Select The Download Button To Download The Framework Upgrade Package And Then Select Upgrade"></ModuleMessage>
} }
else else
{ {
@ -23,7 +35,6 @@
} }
</TabPanel> </TabPanel>
<TabPanel Name="Upload" ResourceKey="Upload"> <TabPanel Name="Upload" ResourceKey="Upload">
<ModuleMessage Type="MessageType.Info" Message=@Localizer["MessageUpgrade.Text"]></ModuleMessage>
<div class="container"> <div class="container">
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" HelpText="Upload A Framework Package And Then Select Upgrade" ResourceKey="Framework">Framework: </Label> <Label Class="col-sm-3" HelpText="Upload A Framework Package And Then Select Upgrade" ResourceKey="Framework">Framework: </Label>
@ -31,8 +42,19 @@
<FileManager Folder="@Constants.PackagesFolder" /> <FileManager Folder="@Constants.PackagesFolder" />
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" HelpText="Specify if you want to backup files during the upgrade process. Disabling this option will result in a better experience in some environments." ResourceKey="Backup">Backup Files? </Label>
<div class="col-sm-9">
<select id="backup" class="form-select" @bind="@_backup">
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
</div> </div>
<button type="button" class="btn btn-success" @onclick="Upgrade">@SharedLocalizer["Upgrade"]</button> <button type="button" class="btn btn-success" @onclick="Upgrade">@SharedLocalizer["Upgrade"]</button>
<br /><br />
<ModuleMessage Type="MessageType.Info" Message=@Localizer["MessageUpgrade.Text"]></ModuleMessage>
</TabPanel> </TabPanel>
</TabStrip> </TabStrip>
} }
@ -41,6 +63,7 @@
private bool _initialized = false; private bool _initialized = false;
private Package _package; private Package _package;
private bool _upgradeavailable = false; private bool _upgradeavailable = false;
private string _backup = "True";
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
@ -86,7 +109,7 @@
ShowProgressIndicator(); ShowProgressIndicator();
var interop = new Interop(JSRuntime); var interop = new Interop(JSRuntime);
await interop.RedirectBrowser(NavigateUrl(), 10); await interop.RedirectBrowser(NavigateUrl(), 10);
await InstallationService.Upgrade(); await InstallationService.Upgrade(bool.Parse(_backup));
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@ -151,6 +151,12 @@
<value>You Cannot Perform A System Update In A Development Environment</value> <value>You Cannot Perform A System Update In A Development Environment</value>
</data> </data>
<data name="Disclaimer.Text" xml:space="preserve"> <data name="Disclaimer.Text" xml:space="preserve">
<value>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.</value> <value>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.</value>
</data>
<data name="Backup.Text" xml:space="preserve">
<value>Backup Files?</value>
</data>
<data name="Backup.HelpText" xml:space="preserve">
<value>Specify if you want to backup files during the upgrade process. Disabling this option will result in a better experience in some environments.</value>
</data> </data>
</root> </root>

View File

@ -47,9 +47,9 @@ namespace Oqtane.Services
return await PostJsonAsync<InstallConfig,Installation>(ApiUrl, config); return await PostJsonAsync<InstallConfig,Installation>(ApiUrl, config);
} }
public async Task<Installation> Upgrade() public async Task<Installation> Upgrade(bool backup)
{ {
return await GetJsonAsync<Installation>($"{ApiUrl}/upgrade"); return await GetJsonAsync<Installation>($"{ApiUrl}/upgrade/?backup={backup}");
} }
public async Task RestartAsync() public async Task RestartAsync()

View File

@ -26,8 +26,9 @@ namespace Oqtane.Services
/// <summary> /// <summary>
/// Starts the upgrade process /// Starts the upgrade process
/// </summary> /// </summary>
/// <param name="backup">indicates if files should be backed up during upgrade</param>
/// <returns>internal status/message object</returns> /// <returns>internal status/message object</returns>
Task<Installation> Upgrade(); Task<Installation> Upgrade(bool backup);
/// <summary> /// <summary>
/// Restarts the installation /// Restarts the installation

View File

@ -88,10 +88,10 @@ namespace Oqtane.Controllers
[HttpGet("upgrade")] [HttpGet("upgrade")]
[Authorize(Roles = RoleNames.Host)] [Authorize(Roles = RoleNames.Host)]
public Installation Upgrade() public Installation Upgrade(string backup)
{ {
var installation = new Installation { Success = true, Message = "" }; var installation = new Installation { Success = true, Message = "" };
_installationManager.UpgradeFramework(); _installationManager.UpgradeFramework(bool.Parse(backup));
return installation; return installation;
} }

View File

@ -380,7 +380,7 @@ namespace Oqtane.Infrastructure
File.WriteAllText(assemblyLogPath, JsonSerializer.Serialize(assemblies, new JsonSerializerOptions { WriteIndented = true })); 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); string folder = Path.Combine(_environment.ContentRootPath, Constants.PackagesFolder);
if (Directory.Exists(folder)) if (Directory.Exists(folder))
@ -448,14 +448,14 @@ namespace Oqtane.Infrastructure
// install Oqtane.Upgrade zip package // install Oqtane.Upgrade zip package
if (File.Exists(upgradepackage)) if (File.Exists(upgradepackage))
{ {
FinishUpgrade(); FinishUpgrade(backup);
} }
} }
} }
} }
} }
private void FinishUpgrade() private void FinishUpgrade(bool backup)
{ {
// check if updater application exists // check if updater application exists
string Updater = Constants.UpdaterPackageId + ".dll"; string Updater = Constants.UpdaterPackageId + ".dll";
@ -469,7 +469,7 @@ namespace Oqtane.Infrastructure
{ {
WorkingDirectory = folder, WorkingDirectory = folder,
FileName = "dotnet", FileName = "dotnet",
Arguments = Path.Combine(folder, Updater) + " \"" + _environment.ContentRootPath + "\" \"" + _environment.WebRootPath + "\"", Arguments = Path.Combine(folder, Updater) + " \"" + _environment.ContentRootPath + "\" \"" + _environment.WebRootPath + "\" \"" + backup.ToString() + "\"",
UseShellExecute = false, UseShellExecute = false,
ErrorDialog = false, ErrorDialog = false,
CreateNoWindow = true, CreateNoWindow = true,

View File

@ -7,7 +7,7 @@ namespace Oqtane.Infrastructure
void InstallPackages(); void InstallPackages();
bool UninstallPackage(string PackageName); bool UninstallPackage(string PackageName);
int RegisterAssemblies(); int RegisterAssemblies();
Task UpgradeFramework(); Task UpgradeFramework(bool backup);
void RestartApplication(); void RestartApplication();
} }
} }

View File

@ -15,17 +15,19 @@ namespace Oqtane.Updater
/// <param name="args"></param> /// <param name="args"></param>
static void Main(string[] args) 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 // 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[0] = @"C:\yourpath\oqtane.framework\Oqtane.Server";
//args[1] = @"C:\yourpath\oqtane.framework\Oqtane.Server\wwwroot"; //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 contentrootfolder = args[0];
string webrootfolder = args[1]; string webrootfolder = args[1];
bool backup = bool.Parse(args[2]);
string deployfolder = Path.Combine(contentrootfolder, "Packages"); string deployfolder = Path.Combine(contentrootfolder, "Packages");
string backupfolder = Path.Combine(contentrootfolder, "Backup"); 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, "Upgrade Process Started: " + DateTime.UtcNow.ToString() + Environment.NewLine);
WriteLog(logFilePath, "ContentRootPath: " + contentrootfolder + Environment.NewLine); WriteLog(logFilePath, "ContentRootPath: " + contentrootfolder + Environment.NewLine);
WriteLog(logFilePath, "WebRootPath: " + webrootfolder + Environment.NewLine); WriteLog(logFilePath, "WebRootPath: " + webrootfolder + Environment.NewLine);
if (packagename != "" && File.Exists(Path.Combine(webrootfolder, "app_offline.bak"))) if (packagename != "" && File.Exists(Path.Combine(webrootfolder, "app_offline.bak")))
{ {
WriteLog(logFilePath, "Located Upgrade Package: " + packagename + Environment.NewLine); WriteLog(logFilePath, "Located Upgrade Package: " + packagename + Environment.NewLine);
@ -74,12 +77,12 @@ namespace Oqtane.Updater
} }
bool success = true; bool success = true;
// ensure files are not locked
if (CanAccessFiles(files)) if (backup)
{ {
UpdateOfflineContent(offlineFilePath, offlineTemplate, 10, "Preparing Backup Folder"); UpdateOfflineContent(offlineFilePath, offlineTemplate, 10, "Preparing Backup Folder");
WriteLog(logFilePath, "Preparing Backup Folder: " + backupfolder + Environment.NewLine); WriteLog(logFilePath, "Preparing Backup Folder: " + backupfolder + Environment.NewLine);
try try
{ {
// clear out backup folder // clear out backup folder
@ -112,8 +115,28 @@ namespace Oqtane.Updater
{ {
Directory.CreateDirectory(Path.GetDirectoryName(filename)); 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) catch (Exception ex)
@ -125,17 +148,20 @@ namespace Oqtane.Updater
} }
} }
} }
}
// extract files // extract files
if (success) 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"); using (ZipArchive archive = ZipFile.OpenRead(packagename))
WriteLog(logFilePath, "Extracting Files From Upgrade Package..." + Environment.NewLine);
try
{ {
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)) if (!string.IsNullOrEmpty(entry.Name))
{ {
@ -144,20 +170,42 @@ namespace Oqtane.Updater
{ {
Directory.CreateDirectory(Path.GetDirectoryName(filename)); 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) }
{ catch (Exception ex)
success = false; {
UpdateOfflineContent(offlineFilePath, offlineTemplate, 95, "Error Extracting Files From Upgrade Package", "bg-danger"); success = false;
WriteLog(logFilePath, "Error Extracting Files From Upgrade Package: " + ex.Message + Environment.NewLine); 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"); UpdateOfflineContent(offlineFilePath, offlineTemplate, 90, "Removing Backup Folder");
WriteLog(logFilePath, "Removing Backup Folder..." + Environment.NewLine); WriteLog(logFilePath, "Removing Backup Folder..." + Environment.NewLine);
@ -174,7 +222,12 @@ namespace Oqtane.Updater
WriteLog(logFilePath, "Error Removing Backup Folder: " + ex.Message + Environment.NewLine); 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"); UpdateOfflineContent(offlineFilePath, offlineTemplate, 50, "Upgrade Failed, Restoring Files From Backup Folder", "bg-warning");
WriteLog(logFilePath, "Restoring Files From Backup Folder..." + Environment.NewLine); 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 else
{ {
UpdateOfflineContent(offlineFilePath, offlineTemplate, 95, "Upgrade Failed: Some Files Are Locked By The Hosting Environment", "bg-danger"); UpdateOfflineContent(offlineFilePath, offlineTemplate, 95, "Upgrade Failed: Could Not Backup Files", "bg-danger");
WriteLog(logFilePath, "Upgrade Failed: Some Files Are Locked By The Hosting Environment" + Environment.NewLine); WriteLog(logFilePath, "Upgrade Failed: Could Not Backup Files" + Environment.NewLine);
} }
UpdateOfflineContent(offlineFilePath, offlineTemplate, 100, "Upgrade Process Finished, Reloading", success ? "" : "bg-danger"); UpdateOfflineContent(offlineFilePath, offlineTemplate, 100, "Upgrade Process Finished, Reloading", success ? "" : "bg-danger");
Thread.Sleep(3000); //wait for 3 seconds to complete the upgrade process. Thread.Sleep(3000); //wait for 3 seconds to complete the upgrade process.
// bring the app back online // bring the app back online
@ -240,55 +289,45 @@ namespace Oqtane.Updater
} }
} }
private static bool CanAccessFiles(List<string> files) private static bool CanAccessFile(string filepath)
{ {
// ensure files are not locked by another process // ensure file is 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)
int retries = 60; int retries = 60;
int sleep = 2; int sleep = 2;
int attempts = 0;
bool canAccess = true;
FileStream stream = null; 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;
stream = File.Open(filepath, FileMode.Open, FileAccess.Read, FileShare.None);
locked = false;
}
else
{
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; Thread.Sleep(sleep * 1000); // wait
Console.WriteLine("File Locked: " + filepath);
} }
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 = "") private static void UpdateOfflineContent(string filePath, string contentTemplate, int progress, string status, string progressClass = "")