CP-39382: Lock msi file while verifying and launching install process (#2973)

* CP-39382 adds lock around msi file while being verified and launched. Improves naming of variables in line with conventions.

Signed-off-by: Chris Lancaster <Christopher.Lancaste1@citrix.com>

* CP-39382 removes unnessessary usings, fixes background tasks running check. Tidies up structure.

Signed-off-by: Chris Lancaster <Christopher.Lancaste1@citrix.com>

* Further modifications.

Signed-off-by: Konstantina Chremmou <konstantina.chremmou@citrix.com>

* CP-39382 adds back in messages lost in merge conflict resolution

Signed-off-by: Christophe25 <christopher.lancaste1@citrix.com>

Co-authored-by: Konstantina Chremmou <konstantina.chremmou@citrix.com>
This commit is contained in:
CitrixChris 2022-04-11 10:11:33 +01:00 committed by GitHub
parent 4656114963
commit 5b0b27eca1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 129 additions and 83 deletions

View File

@ -36,6 +36,7 @@ using System.IO;
using System.Windows.Forms;
using XenAdmin.Actions;
using XenAdmin.Actions.GUIActions;
using XenAdmin.Actions.Updates;
using XenAdmin.Core;
using XenAdmin.Dialogs;
@ -135,25 +136,24 @@ namespace XenAdmin.Alerts
if (currentTasks)
{
if (new Dialogs.WarningDialogs.CloseXenCenterWarningDialog(true).ShowDialog() != DialogResult.OK)
if (new Dialogs.WarningDialogs.CloseXenCenterWarningDialog(true).ShowDialog(parent) != DialogResult.OK)
return;
}
// Install the msi
try
{
// Start the install process, it will handle closing of application.
Process.Start(outputPathAndFileName);
log.DebugFormat("Update {0} found and install started", updateAlert.Name);
downloadAndInstallClientAction.ReleaseInstaller();
Application.Exit();
}
catch (Exception e)
{
if (File.Exists(outputPathAndFileName))
File.Delete(outputPathAndFileName);
log.Error("Exception occurred when starting the installation process.", e);
throw;
downloadAndInstallClientAction.ReleaseInstaller(true);
using (var dlg = new ErrorDialog(Messages.UPDATE_CLIENT_FAILED_INSTALLER_LAUNCH))
dlg.ShowDialog(parent);
}
}
}

View File

@ -39,7 +39,7 @@ using System.Security.Cryptography.X509Certificates;
using System.Threading;
using XenCenterLib;
namespace XenAdmin.Actions
namespace XenAdmin.Actions.Updates
{
public class DownloadAndUpdateClientAction : AsyncAction, IByteProgressAction
{
@ -51,14 +51,15 @@ namespace XenAdmin.Actions
//If you consider increasing this for any reason (I think 5 is already more than enough), have a look at the usage of SLEEP_TIME_BEFORE_RETRY_MS in DownloadFile() as well.
private const int MAX_NUMBER_OF_TRIES = 5;
private readonly Uri address;
private readonly string outputPathAndFileName;
private readonly string updateName;
private readonly bool downloadUpdate;
private DownloadState updateDownloadState;
private Exception updateDownloadError;
private string checksum;
private WebClient client;
private readonly Uri _address;
private readonly string _outputPathAndFileName;
private readonly string _updateName;
private readonly bool _downloadUpdate;
private DownloadState _updateDownloadState;
private Exception _updateDownloadError;
private readonly string _checksum;
private WebClient _client;
private FileStream _msiStream;
public string ByteProgressDescription { get; set; }
@ -66,11 +67,11 @@ namespace XenAdmin.Actions
: base(null, string.Format(Messages.DOWNLOAD_CLIENT_INSTALLER_ACTION_TITLE, updateName),
string.Empty, true)
{
this.updateName = updateName;
address = uri;
downloadUpdate = address != null;
this.outputPathAndFileName = outputFileName;
this.checksum = checksum;
_updateName = updateName;
_address = uri;
_downloadUpdate = _address != null;
_outputPathAndFileName = outputFileName;
_checksum = checksum;
}
private void DownloadFile()
@ -78,9 +79,9 @@ namespace XenAdmin.Actions
int errorCount = 0;
bool needToRetry = false;
client = new WebClient();
client.DownloadProgressChanged += client_DownloadProgressChanged;
client.DownloadFileCompleted += client_DownloadFileCompleted;
_client = new WebClient();
_client.DownloadProgressChanged += client_DownloadProgressChanged;
_client.DownloadFileCompleted += client_DownloadFileCompleted;
// register event handler to detect changes in network connectivity
NetworkChange.NetworkAvailabilityChanged += NetworkAvailabilityChanged;
@ -94,31 +95,31 @@ namespace XenAdmin.Actions
needToRetry = false;
client.Proxy = XenAdminConfigManager.Provider.GetProxyFromSettings(null, false);
_client.Proxy = XenAdminConfigManager.Provider.GetProxyFromSettings(null, false);
//start the download
updateDownloadState = DownloadState.InProgress;
client.DownloadFileAsync(address, outputPathAndFileName);
_updateDownloadState = DownloadState.InProgress;
_client.DownloadFileAsync(_address, _outputPathAndFileName);
bool updateDownloadCancelling = false;
//wait for the file to be downloaded
while (updateDownloadState == DownloadState.InProgress)
while (_updateDownloadState == DownloadState.InProgress)
{
if (!updateDownloadCancelling && (Cancelling || Cancelled))
{
Description = Messages.DOWNLOAD_AND_EXTRACT_ACTION_DOWNLOAD_CANCELLED_DESC;
client.CancelAsync();
_client.CancelAsync();
updateDownloadCancelling = true;
}
Thread.Sleep(SLEEP_TIME_TO_CHECK_DOWNLOAD_STATUS_MS);
}
if (updateDownloadState == DownloadState.Cancelled)
if (_updateDownloadState == DownloadState.Cancelled)
throw new CancelledException();
if (updateDownloadState == DownloadState.Error)
if (_updateDownloadState == DownloadState.Error)
{
needToRetry = true;
@ -128,100 +129,121 @@ namespace XenAdmin.Actions
// logging only, it will retry again.
log.ErrorFormat(
"Error while downloading from '{0}'. Number of errors so far (including this): {1}. Trying maximum {2} times.",
address, errorCount, MAX_NUMBER_OF_TRIES);
_address, errorCount, MAX_NUMBER_OF_TRIES);
if (updateDownloadError == null)
if (_updateDownloadError == null)
log.Error("An unknown error occurred.");
else
log.Error(updateDownloadError);
log.Error(_updateDownloadError);
}
} while (errorCount < MAX_NUMBER_OF_TRIES && needToRetry);
}
finally
{
client.DownloadProgressChanged -= client_DownloadProgressChanged;
client.DownloadFileCompleted -= client_DownloadFileCompleted;
_client.DownloadProgressChanged -= client_DownloadProgressChanged;
_client.DownloadFileCompleted -= client_DownloadFileCompleted;
NetworkChange.NetworkAvailabilityChanged -= NetworkAvailabilityChanged;
client.Dispose();
_client.Dispose();
}
//if this is still the case after having retried MAX_NUMBER_OF_TRIES number of times.
if (updateDownloadState == DownloadState.Error)
if (_updateDownloadState == DownloadState.Error)
{
log.ErrorFormat("Giving up - Maximum number of retries ({0}) has been reached.", MAX_NUMBER_OF_TRIES);
throw updateDownloadError ?? new Exception(Messages.ERROR_UNKNOWN);
throw _updateDownloadError ?? new Exception(Messages.ERROR_UNKNOWN);
}
}
private void NetworkAvailabilityChanged(object sender, NetworkAvailabilityEventArgs e)
{
if (!e.IsAvailable && client != null && updateDownloadState == DownloadState.InProgress)
if (!e.IsAvailable && _client != null && _updateDownloadState == DownloadState.InProgress)
{
updateDownloadError = new WebException(Messages.NETWORK_CONNECTIVITY_ERROR);
updateDownloadState = DownloadState.Error;
client.CancelAsync();
_updateDownloadError = new WebException(Messages.NETWORK_CONNECTIVITY_ERROR);
_updateDownloadState = DownloadState.Error;
_client.CancelAsync();
}
}
protected override void Run()
{
if (downloadUpdate)
{
log.InfoFormat("Downloading '{0}' installer (from '{1}') to '{2}'", updateName, address, outputPathAndFileName);
Description = string.Format(Messages.DOWNLOAD_CLIENT_INSTALLER_ACTION_DESCRIPTION, updateName);
LogDescriptionChanges = false;
DownloadFile();
LogDescriptionChanges = true;
if (!_downloadUpdate)
return;
if (IsCompleted || Cancelled)
return;
log.InfoFormat("Downloading '{0}' installer (from '{1}') to '{2}'", _updateName, _address, _outputPathAndFileName);
Description = string.Format(Messages.DOWNLOAD_CLIENT_INSTALLER_ACTION_DESCRIPTION, _updateName);
LogDescriptionChanges = false;
DownloadFile();
LogDescriptionChanges = true;
if (Cancelling)
throw new CancelledException();
}
if (IsCompleted || Cancelled)
return;
if (Cancelling)
throw new CancelledException();
if (!File.Exists(_outputPathAndFileName))
throw new Exception(Messages.DOWNLOAD_CLIENT_INSTALLER_MSI_NOT_FOUND);
ValidateMsi();
if (!File.Exists(outputPathAndFileName))
throw new Exception(Messages.DOWNLOAD_CLIENT_INSTALLER_MSI_NOT_FOUND);
Description = Messages.COMPLETED;
}
protected override void CleanOnError()
{
ReleaseInstaller(true);
}
public void ReleaseInstaller(bool deleteMsi = false)
{
_msiStream?.Dispose();
if (!deleteMsi)
return;
try
{
if (File.Exists(_outputPathAndFileName))
File.Delete(_outputPathAndFileName);
}
catch
{
//ignore
}
}
private void ValidateMsi()
{
using (FileStream stream = new FileStream(outputPathAndFileName, FileMode.Open, FileAccess.Read))
{
var calculatedChecksum = string.Empty;
Description = Messages.UPDATE_CLIENT_VALIDATING_INSTALLER;
var hash = StreamUtilities.ComputeHash(stream, out _);
if (hash != null)
calculatedChecksum = string.Join("", hash.Select(b => $"{b:x2}"));
_msiStream = new FileStream(_outputPathAndFileName, FileMode.Open, FileAccess.Read);
// Check if calculatedChecksum matches what is in chcupdates.xml
if (!checksum.Equals(calculatedChecksum, StringComparison.InvariantCultureIgnoreCase))
throw new Exception(Messages.UPDATE_CLIENT_INVALID_CHECKSUM );
}
var calculatedChecksum = string.Empty;
bool valid = false;
var hash = StreamUtilities.ComputeHash(_msiStream, out _);
if (hash != null)
calculatedChecksum = string.Join(string.Empty, hash.Select(b => $"{b:x2}"));
// Check if calculatedChecksum matches what is in chcupdates.xml
if (!_checksum.Equals(calculatedChecksum, StringComparison.InvariantCultureIgnoreCase))
throw new Exception(Messages.UPDATE_CLIENT_INVALID_CHECKSUM );
bool valid;
try
{
// Check digital signature of .msi
using (var basicSigner = X509Certificate.CreateFromSignedFile(outputPathAndFileName))
using (var basicSigner = X509Certificate.CreateFromSignedFile(_outputPathAndFileName))
{
using (var cert = new X509Certificate2(basicSigner))
{
valid = cert.Verify();
}
}
}
catch (Exception e)
{
throw new Exception(Messages.UPDATE_CLIENT_FAILED_CERTIFICATE_CHECK, e);
}
if (!valid)
throw new Exception(Messages.UPDATE_CLIENT_INVALID_DIGITAL_CERTIFICATE);
@ -230,7 +252,7 @@ namespace XenAdmin.Actions
private void client_DownloadProgressChanged(object sender, DownloadProgressChangedEventArgs e)
{
int pc = (int)(95.0 * e.BytesReceived / e.TotalBytesToReceive);
var descr = string.Format(Messages.DOWNLOAD_CLIENT_INSTALLER_ACTION_PROGRESS_DESCRIPTION, updateName,
var descr = string.Format(Messages.DOWNLOAD_CLIENT_INSTALLER_ACTION_PROGRESS_DESCRIPTION, _updateName,
Util.DiskSizeString(e.BytesReceived, "F1"),
Util.DiskSizeString(e.TotalBytesToReceive));
ByteProgressDescription = descr;
@ -239,31 +261,31 @@ namespace XenAdmin.Actions
private void client_DownloadFileCompleted(object sender, AsyncCompletedEventArgs e)
{
if (e.Cancelled && updateDownloadState == DownloadState.Error) // cancelled due to network connectivity issue (see NetworkAvailabilityChanged)
if (e.Cancelled && _updateDownloadState == DownloadState.Error) // cancelled due to network connectivity issue (see NetworkAvailabilityChanged)
return;
if (e.Cancelled)
{
updateDownloadState = DownloadState.Cancelled;
log.DebugFormat("Client update '{0}' download cancelled by the user", updateName);
_updateDownloadState = DownloadState.Cancelled;
log.DebugFormat("Client update '{0}' download cancelled by the user", _updateName);
return;
}
if (e.Error != null)
{
updateDownloadError = e.Error;
log.DebugFormat("Client update '{0}' download failed", updateName);
updateDownloadState = DownloadState.Error;
_updateDownloadError = e.Error;
log.DebugFormat("Client update '{0}' download failed", _updateName);
_updateDownloadState = DownloadState.Error;
return;
}
updateDownloadState = DownloadState.Completed;
log.DebugFormat("Client update '{0}' download completed successfully", updateName);
_updateDownloadState = DownloadState.Completed;
log.DebugFormat("Client update '{0}' download completed successfully", _updateName);
}
public override void RecomputeCanCancel()
{
CanCancel = !Cancelling && !IsCompleted && (updateDownloadState == DownloadState.InProgress);
CanCancel = !Cancelling && !IsCompleted && (_updateDownloadState == DownloadState.InProgress);
}
}
}

View File

@ -37240,6 +37240,15 @@ namespace XenAdmin {
}
}
/// <summary>
/// Looks up a localized string similar to An error occurred when launching the downloaded installer. Please see the logs for more information..
/// </summary>
public static string UPDATE_CLIENT_FAILED_INSTALLER_LAUNCH {
get {
return ResourceManager.GetString("UPDATE_CLIENT_FAILED_INSTALLER_LAUNCH", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The checksum of the downloaded installer does not match the expected value..
/// </summary>
@ -37258,6 +37267,15 @@ namespace XenAdmin {
}
}
/// <summary>
/// Looks up a localized string similar to Validating the downloaded installer....
/// </summary>
public static string UPDATE_CLIENT_VALIDATING_INSTALLER {
get {
return ResourceManager.GetString("UPDATE_CLIENT_VALIDATING_INSTALLER", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to You have applied filters to the list of update notifications. Do you want to dismiss all update notifications for every connected server, or only the update notifications you have chosen to view?
///

View File

@ -12874,12 +12874,18 @@ Verify that the file is a valid {2} export.</value>
<data name="UPDATE_CLIENT_FAILED_CERTIFICATE_CHECK" xml:space="preserve">
<value>Could not validate the certificate associated with the downloaded installer.</value>
</data>
<data name="UPDATE_CLIENT_FAILED_INSTALLER_LAUNCH" xml:space="preserve">
<value>An error occurred when launching the downloaded installer. Please see the logs for more information.</value>
</data>
<data name="UPDATE_CLIENT_INVALID_CHECKSUM" xml:space="preserve">
<value>The checksum of the downloaded installer does not match the expected value.</value>
</data>
<data name="UPDATE_CLIENT_INVALID_DIGITAL_CERTIFICATE" xml:space="preserve">
<value>Invalid digital signature on msi.</value>
</data>
<data name="UPDATE_CLIENT_VALIDATING_INSTALLER" xml:space="preserve">
<value>Validating the downloaded installer...</value>
</data>
<data name="UPDATE_DISMISS_ALL_CONTINUE" xml:space="preserve">
<value>You have applied filters to the list of update notifications. Do you want to dismiss all update notifications for every connected server, or only the update notifications you have chosen to view?