// Greenshot - a free and open source screenshot tool // Copyright (C) 2007-2021 Thomas Braun, Jens Klingen, Robin Krom // // For more information see: http://getgreenshot.org/ // The Greenshot project is hosted on GitHub https://github.com/greenshot/greenshot // // This program 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 1 of the License, or // (at your option) any later version. // // This program 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 this program. If not, see . using System; using System.Diagnostics; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using Dapplo.HttpExtensions; using Dapplo.HttpExtensions.JsonNet; using Greenshot.Configuration; using Greenshot.Helpers.Entities; using GreenshotPlugin.Core; using GreenshotPlugin.IniFile; using GreenshotPlugin.Interfaces; using log4net; namespace Greenshot.Helpers { /// /// This processes the information, if there are updates available. /// public class UpdateService { private static readonly ILog Log = LogManager.GetLogger(typeof(UpdateService)); private static readonly CoreConfiguration CoreConfig = IniConfig.GetIniSection(); private static readonly Uri UpdateFeed = new Uri("https://getgreenshot.org/update-feed.json"); private static readonly Uri Downloads = new Uri("https://getgreenshot.org/downloads"); private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); /// /// Provides the current version /// public Version CurrentVersion { get; } /// /// Provides the latest known version /// public Version LatestReleaseVersion { get; private set; } /// /// The latest beta version /// public Version LatestBetaVersion { get; private set; } /// /// Checks if there is an release update available /// public bool IsUpdateAvailable => LatestReleaseVersion > CurrentVersion; /// /// Checks if there is an beta update available /// public bool IsBetaUpdateAvailable => LatestBetaVersion > CurrentVersion; /// /// Keep track of when the update was shown, so it won't be every few minutes /// public DateTimeOffset LastUpdateShown = DateTimeOffset.MinValue; /// /// Constructor with dependencies /// public UpdateService() { JsonNetJsonSerializer.RegisterGlobally(); var version = FileVersionInfo.GetVersionInfo(GetType().Assembly.Location); LatestReleaseVersion = CurrentVersion = new Version(version.FileMajorPart, version.FileMinorPart, version.FileBuildPart); CoreConfig.LastSaveWithVersion = CurrentVersion.ToString(); } /// /// Start the background task which checks for updates /// public void Startup() { _ = BackgroundTask(() => TimeSpan.FromDays(CoreConfig.UpdateCheckInterval), UpdateCheck, _cancellationTokenSource.Token); } /// /// This runs a periodic task in the background /// /// Func which returns a TimeSpan /// Func which returns a task /// CancellationToken /// Task private async Task BackgroundTask(Func intervalFactory, Func reoccurringTask, CancellationToken cancellationToken = default) { // Initial delay, to make sure this doesn't happen at the startup await Task.Delay(20000, cancellationToken); Log.Info("Starting background task to check for updates"); await Task.Run(async () => { while (!cancellationToken.IsCancellationRequested) { var interval = intervalFactory(); var task = reoccurringTask; // If the check is disabled, handle that here if (TimeSpan.Zero == interval) { interval = TimeSpan.FromMinutes(10); task = c => Task.FromResult(true); } try { await task(cancellationToken).ConfigureAwait(false); } catch (Exception ex) { Log.Error("Error occured when trying to check for updates.", ex); } try { await Task.Delay(interval, cancellationToken).ConfigureAwait(false); } catch (TaskCanceledException) { // Ignore, this always happens } catch (Exception ex) { Log.Error("Error occured await for the next update check.", ex); } } }, cancellationToken).ConfigureAwait(false); } /// /// Do the actual update check /// /// CancellationToken /// Task private async Task UpdateCheck(CancellationToken cancellationToken = default) { Log.InfoFormat("Checking for updates from {0}", UpdateFeed); var updateFeed = await UpdateFeed.GetAsAsync(cancellationToken); if (updateFeed == null) { return; } CoreConfig.LastUpdateCheck = DateTime.Now; ProcessFeed(updateFeed); // Only show if the update was shown >24 hours ago. if (DateTimeOffset.Now.AddDays(-1) > LastUpdateShown) { if (IsBetaUpdateAvailable) { LastUpdateShown = DateTimeOffset.Now; ShowUpdate(LatestBetaVersion); } else if (IsUpdateAvailable) { LastUpdateShown = DateTimeOffset.Now; ShowUpdate(LatestReleaseVersion); } } } /// /// This takes care of creating the toast view model, publishing it, and disposing afterwards /// /// Version private void ShowUpdate(Version newVersion) { var notificationService = SimpleServiceProvider.Current.GetInstance(); var message = Language.GetFormattedString(LangKey.update_found, newVersion.ToString()); notificationService.ShowInfoMessage(message, TimeSpan.FromHours(1), () => Process.Start(Downloads.AbsoluteUri)); } /// /// Process the update feed to get the latest version /// /// private void ProcessFeed(UpdateFeed updateFeed) { var latestReleaseString = Regex.Replace(updateFeed.CurrentReleaseVersion, "[a-zA-Z\\-]*", ""); if (Version.TryParse(latestReleaseString, out var latestReleaseVersion)) { LatestReleaseVersion = latestReleaseVersion; } var latestBetaString = Regex.Replace(updateFeed.CurrentBetaVersion, "[a-zA-Z\\-]*", ""); if (Version.TryParse(latestBetaString, out var latestBetaVersion)) { LatestBetaVersion = latestBetaVersion; } } } }