/* * Copyright (c) 2026 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.EventMessaging.Contracts; using ProtonVPN.Client.Logic.Auth.Contracts.Messages; using ProtonVPN.Client.Logic.Servers.Cache; using ProtonVPN.Client.Logic.Servers.Contracts; using ProtonVPN.Client.Settings.Contracts; using ProtonVPN.Common.Core.Extensions; using ProtonVPN.Logging.Contracts; using ProtonVPN.Logging.Contracts.Events.AppLogs; namespace ProtonVPN.Client.Logic.Servers; public class ServersUpdater : IServersUpdater, IEventMessageReceiver, IEventMessageReceiver { private readonly ILogger _logger; private readonly IServersCache _serversCache; private readonly IServerCountCache _serverCountCache; private readonly ISettings _settings; private readonly SemaphoreSlim _semaphore = new(1, 1); private bool _isLoggedIn = false; public ServersUpdater( ILogger logger, IServersCache serversCache, IServerCountCache serverCountCache, ISettings settings) { _logger = logger; _serversCache = serversCache; _serverCountCache = serverCountCache; _settings = settings; } public async Task UpdateAsync(CancellationToken cancellationToken) { await _semaphore.WaitAsync(cancellationToken); try { _logger.Info("Server update requested"); _serversCache.LoadFromFileIfEmpty(); if (_serversCache.IsEmpty() || _serversCache.IsStale()) { _logger.Info("Server cache is invalid, forcing full update"); await ForceUpdateServersAsync(cancellationToken); } else if (_serversCache.IsOutdated()) { _logger.Info("Server cache is outdated, updating"); await UpdateServersAsync(cancellationToken); } else if (_serversCache.IsLoadOutdated()) { _logger.Info("Load cache is outdated, updating"); await UpdateLoadsAsync(cancellationToken); } else { _logger.Info("No server update needed, using cached servers"); } } finally { _semaphore.Release(); } } public async Task ForceUpdateAsync(CancellationToken cancellationToken) { await _semaphore.WaitAsync(cancellationToken); try { _logger.Info("Force server update requested"); _serversCache.LoadFromFileIfEmpty(); await ForceUpdateServersAsync(cancellationToken); } finally { _semaphore.Release(); } } public async Task ClearCacheAsync(CancellationToken cancellationToken) { await _semaphore.WaitAsync(cancellationToken); try { _logger.Info("Clear server cache requested"); _serversCache.Clear(); } finally { _semaphore.Release(); } } public void Receive(LoggedInMessage message) { _isLoggedIn = true; } public void Receive(LoggedOutMessage message) { _isLoggedIn = false; } private Task ForceUpdateServersAsync(CancellationToken cancellationToken) { _settings.LogicalsLastModifiedDate = DefaultSettings.LogicalsLastModifiedDate; return UpdateServersAsync(cancellationToken); } private Task UpdateServersAsync(CancellationToken cancellationToken) { // No need to await here, the server count is not critical for the servers cache update _serverCountCache.UpdateAsync().FireAndForget(); return _serversCache.UpdateAsync(cancellationToken); } private Task UpdateLoadsAsync(CancellationToken cancellationToken) { return _serversCache.UpdateLoadsAsync(cancellationToken); } }