/*
* Copyright (c) 2023 Proton AG
*
* This file is part of ProtonVPN.
*
* ProtonVPN is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* ProtonVPN is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with ProtonVPN. If not, see .
*/
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ProtonVPN.Logging.Contracts;
using ProtonVPN.Logging.Contracts.Events.AppUpdateLogs;
using ProtonVPN.Common.Legacy.Threading;
using ProtonVPN.Update.Contracts;
namespace ProtonVPN.Update.Updates
{
///
/// Performs series of asynchronous update checking, downloading and verifying operations
/// and notifies about the state change.
///
public class NotifyingAppUpdate : INotifyingAppUpdate
{
private readonly CoalescingAction _checkForUpdate;
private IAppUpdate _update;
private readonly ILogger _logger;
private AppUpdateStatus _status = AppUpdateStatus.None;
private bool _earlyAccess;
private volatile bool _requestedEarlyAccess;
public NotifyingAppUpdate(IAppUpdate update, ILogger logger)
{
_update = update;
_logger = logger;
_checkForUpdate = new CoalescingAction(SafeCheckForUpdate);
}
public event EventHandler StateChanged;
public void StartCheckingForUpdate(bool earlyAccess)
{
if (_checkForUpdate.Running)
{
if (_requestedEarlyAccess == earlyAccess)
{
return;
}
_checkForUpdate.Cancel();
}
_requestedEarlyAccess = earlyAccess;
_checkForUpdate.Run();
}
public async Task StartUpdating(bool auto)
{
await _update.Started(auto);
// The state change to Updating triggers the app to exit.
// State is changed to Updating only if update has been successfully started (not raised an exception).
OnStateChanged(AppUpdateStatus.Updating);
}
private async Task SafeCheckForUpdate(CancellationToken cancellationToken)
{
try
{
await UnsafeCheckForUpdate(cancellationToken);
}
catch (OperationCanceledException)
{
HandleCancellation();
}
catch (AppUpdateException)
{
HandleFailure();
}
catch (Exception e)
{
HandleFailure(e);
}
}
private async Task UnsafeCheckForUpdate(CancellationToken cancellationToken)
{
_logger.Info("Checking for updates.");
_earlyAccess = _requestedEarlyAccess;
_status = AppUpdateStatus.Checking;
HandleSuccess(_update.CachedLatest(_earlyAccess), cancellationToken);
HandleSuccess(await _update.Latest(_earlyAccess), cancellationToken);
if (_update.Available)
{
string fileName = Path.GetFileNameWithoutExtension(_update.FilePath);
_logger.Info($"An update is available (File name: {fileName}).");
HandleSuccess(await _update.Validated(), cancellationToken);
if (!_update.Ready)
{
_logger.Info($"The latest update is being downloaded (File name: {fileName}).");
_status = AppUpdateStatus.Downloading;
OnStateChanged();
HandleSuccess(await _update.Downloaded(), cancellationToken);
HandleSuccess(await _update.Validated(), cancellationToken);
if (_update.Ready)
{
_logger.Info("The latest update was successfully " +
$"downloaded and validated (File name: {fileName}).");
}
else
{
_logger.Error("The latest update failed to download " +
$"(File path: {_update.FilePath}).");
_status = AppUpdateStatus.DownloadFailed;
OnStateChanged();
return;
}
}
}
_status = _update.Ready ? AppUpdateStatus.Ready : AppUpdateStatus.None;
OnStateChanged();
}
private void HandleSuccess(IAppUpdate update, CancellationToken cancellationToken)
{
_update = update;
cancellationToken.ThrowIfCancellationRequested();
OnStateChanged();
}
private void HandleCancellation()
{
_status = AppUpdateStatus.None;
OnStateChanged();
}
private void HandleFailure(Exception e = null)
{
switch (_status)
{
case AppUpdateStatus.Checking:
_status = AppUpdateStatus.CheckFailed;
break;
case AppUpdateStatus.Downloading:
_status = AppUpdateStatus.DownloadFailed;
break;
default:
_status = AppUpdateStatus.None;
break;
}
_logger.Error($"An update failed with status '{_status}' (File path: {_update.FilePath}).", e);
OnStateChanged();
}
private void OnStateChanged()
{
OnStateChanged(_status);
}
private void OnStateChanged(AppUpdateStatus status)
{
AppUpdateStateContract eventArgs = new()
{
IsAvailable = _update.Available,
FileArguments = _update.FileArguments,
FilePath = _update.FilePath,
Version = _update.Version,
IsReady = _update.Ready,
Status = status,
ReleaseHistory = Map(_update.ReleaseHistory()),
};
StateChanged?.Invoke(this, eventArgs);
}
private IReadOnlyList Map(IReadOnlyList releases)
{
return releases.Select(release => new ReleaseContract
{
ChangeLog = release.ChangeLog,
IsEarlyAccess = release.IsEarlyAccess,
ReleaseDate = release.ReleaseDate,
IsNew = release.IsNew,
Version = release.Version
}).ToList();
}
}
}