/* * Copyright (c) 2024 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 ProtonVPN.Client.Common.Observers; using ProtonVPN.Client.Contracts.Services.Lifecycle; using ProtonVPN.Client.EventMessaging.Contracts; using ProtonVPN.Client.Logic.Connection.Contracts; using ProtonVPN.Client.Logic.Connection.Contracts.Enums; using ProtonVPN.Client.Logic.Connection.Contracts.Messages; using ProtonVPN.Client.Logic.Connection.Contracts.Models; using ProtonVPN.Client.Logic.Servers.Contracts.Enums; using ProtonVPN.Client.Logic.Services.Contracts; using ProtonVPN.Client.Logic.Updates.Contracts; using ProtonVPN.Client.Settings.Contracts; using ProtonVPN.Client.Settings.Contracts.Extensions; using ProtonVPN.Client.Settings.Contracts.Messages; using ProtonVPN.Common.Legacy.OS.Processes; using ProtonVPN.Configurations.Contracts; using ProtonVPN.EntityMapping.Contracts; using ProtonVPN.IssueReporting.Contracts; using ProtonVPN.Logging.Contracts; using ProtonVPN.Logging.Contracts.Events.AppUpdateLogs; using ProtonVPN.ProcessCommunication.Contracts.Entities.Update; using ProtonVPN.ProcessCommunication.Contracts.Entities.Vpn; using ProtonVPN.Update.Contracts; namespace ProtonVPN.Client.Logic.Updates; public class UpdatesManager : PollingObserverBase, IUpdatesManager, IEventMessageReceiver, IEventMessageReceiver, IEventMessageReceiver { private readonly IConnectionManager _connectionManager; private readonly IConfiguration _config; private readonly IEntityMapper _entityMapper; private readonly ISettings _settings; private readonly IUpdateServiceCaller _updateServiceCaller; private readonly IEventMessageSender _eventMessageSender; private readonly IVpnServiceSettingsUpdater _vpnServiceSettingsUpdater; private readonly IOsProcesses _osProcesses; private readonly IAppExitInvoker _appExitInvoker; private bool _requestedManualCheck; private DateTime _lastCheckTime; private FeedType _feedType; private AppUpdateStateContract? _lastUpdateState; private bool IsToCheckForUpdate => DateTime.UtcNow - _lastCheckTime >= _config.UpdateCheckInterval; protected override TimeSpan PollingInterval => _config.UpdateCheckInterval; public bool IsAutoUpdated { get; private set; } public bool IsAutoUpdateInProgress { get; private set; } public bool IsUpdateAvailable => _lastUpdateState?.IsReady == true && (!_settings.AreAutomaticUpdatesEnabled || IsAutoUpdated); public UpdatesManager( ILogger logger, IIssueReporter issueReporter, IConnectionManager connectionManager, IConfiguration config, IEntityMapper entityMapper, ISettings settings, IUpdateServiceCaller updateServiceCaller, IEventMessageSender eventMessageSender, IVpnServiceSettingsUpdater vpnServiceSettingsUpdater, IOsProcesses osProcesses, IAppExitInvoker appExitInvoker) : base(logger, issueReporter) { _connectionManager = connectionManager; _config = config; _entityMapper = entityMapper; _settings = settings; _updateServiceCaller = updateServiceCaller; _eventMessageSender = eventMessageSender; _vpnServiceSettingsUpdater = vpnServiceSettingsUpdater; _osProcesses = osProcesses; _appExitInvoker = appExitInvoker; } protected override Task OnTriggerAsync() { CheckForUpdate(false); return Task.CompletedTask; } public void CheckForUpdate(bool isManualCheck) { _requestedManualCheck |= isManualCheck; if (isManualCheck || IsToCheckForUpdate) { _updateServiceCaller.CheckForUpdateAsync(new UpdateSettingsIpcEntity { FeedType = (FeedTypeIpcEntity)_feedType, IsEarlyAccess = _settings.IsBetaAccessEnabled, }); _lastCheckTime = DateTime.UtcNow; } } public void Receive(SettingChangedMessage message) { if (message.PropertyName == nameof(ISettings.IsBetaAccessEnabled)) { SendClientUpdateStateChangeMessage(new ClientUpdateStateChangedMessage()); CheckForUpdate(true); } } private void SendClientUpdateStateChangeMessage(ClientUpdateStateChangedMessage message) { _eventMessageSender.Send(message); } public void Initialize() { TriggerAndStartTimer(); } public void Receive(UpdateStateIpcEntity message) { AppUpdateStateContract state = _entityMapper.Map(message); if (state.IsReady && _settings.AreAutomaticUpdatesEnabled && state.Status == AppUpdateStatus.Ready) { IsAutoUpdateInProgress = true; SendClientUpdateStateChangeMessage(new ClientUpdateStateChangedMessage()); _updateServiceCaller.StartAutoUpdateAsync(); } else { if (state.Status == AppUpdateStatus.AutoUpdated) { IsAutoUpdated = true; IsAutoUpdateInProgress = false; } if (IsAutoUpdated && state.IsReady) { state.Status = AppUpdateStatus.AutoUpdated; } OnUpdateStateChanged(state); } } private void OnUpdateStateChanged(AppUpdateStateContract state) { if (state.Status != _lastUpdateState?.Status || state.IsReady != _lastUpdateState?.IsReady || _requestedManualCheck) { if (state.Status == AppUpdateStatus.Checking) { _requestedManualCheck = false; } SendClientUpdateStateChangeMessage(new ClientUpdateStateChangedMessage { State = state }); _lastUpdateState = state; } } public void Receive(ConnectionStatusChangedMessage message) { ConnectionDetails? connectionDetails = _connectionManager.CurrentConnectionDetails; FeedType feedType = message.ConnectionStatus == ConnectionStatus.Connected && connectionDetails?.ServerTier == ServerTiers.Internal ? FeedType.Internal : FeedType.Public; if (_feedType != feedType) { _feedType = feedType; CheckForUpdate(true); } } public async Task UpdateAsync(bool isToOpenOnDesktop) { if (_lastUpdateState == null) { return; } if (_lastUpdateState.Status == AppUpdateStatus.AutoUpdated) { Logger.Info("Restarting app after auto update due to manual request."); await _appExitInvoker.RestartAsync(isToOpenOnDesktop); } else if (_lastUpdateState.IsReady) { await UpdateManuallyAsync(isToOpenOnDesktop); } } private async Task UpdateManuallyAsync(bool isToOpenOnDesktop) { if (_lastUpdateState == null) { return; } LogUpdateStartingMessage(); if (_settings.IsAdvancedKillSwitchActive()) { await _vpnServiceSettingsUpdater.SendAsync(KillSwitchModeIpcEntity.Off); } try { string openOnDesktopArg = isToOpenOnDesktop ? " /OPENONDESKTOP" : ""; string fileArguments = $"{_lastUpdateState.FileArguments}{openOnDesktopArg}"; _osProcesses.ElevatedProcess(_lastUpdateState.FilePath, fileArguments).Start(); await _appExitInvoker.ForceExitAsync(); } catch (System.ComponentModel.Win32Exception) { // Privileges were not granted if (_settings.IsAdvancedKillSwitchActive()) { await _vpnServiceSettingsUpdater.SendAsync(KillSwitchModeIpcEntity.Hard); } } } private void LogUpdateStartingMessage() { string fileName = GetUpdateFileName(); string message = $"Closing the app and starting installer '{fileName}'. " + $"Current app version: {_config.ClientVersion}, OS: {Environment.OSVersion.VersionString}"; Logger.Info(message); } private string GetUpdateFileName() { string fileName; string filePath = _lastUpdateState?.FilePath ?? string.Empty; try { fileName = Path.GetFileNameWithoutExtension(filePath); } catch (Exception e) { Logger.Error($"Failed to parse file name of path '{filePath}'.", e); fileName = filePath; } return fileName; } }