Skip to content

Instantly share code, notes, and snippets.

@anaisbetts
Created September 24, 2012 19:57
Show Gist options
  • Save anaisbetts/3777989 to your computer and use it in GitHub Desktop.
Save anaisbetts/3777989 to your computer and use it in GitHub Desktop.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.Composition;
using System.Deployment.Application;
using System.Globalization;
using System.Linq;
using System.Reactive.Linq;
using System.Timers;
using System.Windows;
using GitHub.Helpers;
using GitHub.Models;
using NLog;
using ReactiveUI;
namespace GitHub.ViewModels
{
[Export(typeof(SoftwareUpdateViewModel))]
public class SoftwareUpdateViewModel : AboutViewModel, IDisposable
{
static readonly Logger log = LogManager.GetCurrentClassLogger();
readonly IChangeLogFetcher changeLogFetcher;
readonly IAppInstanceCommunicator instanceCommunicator;
readonly ObservableAsPropertyHelper<List<ChangeLogEntry>> latestReleases;
readonly ObservableAsPropertyHelper<bool> retrievingChangeLogFailed;
ChangeLog changeLog;
bool updateAvailableAndDownloaded;
string updateVersion;
volatile bool isUpdateCheckInProgress;
string updateCheckStatus;
readonly Timer timer;
[ImportingConstructor]
public SoftwareUpdateViewModel(IServiceProvider serviceProvider) : base(serviceProvider)
{
Ensure.ArgumentNotNull(serviceProvider, "serviceProvider");
changeLogFetcher = serviceProvider.Get<IChangeLogFetcher>();
instanceCommunicator = serviceProvider.Get<IAppInstanceCommunicator>();
CurrentVersionString = string.Format(CultureInfo.InvariantCulture, "You are currently running: {0} ({1}).", Version, ShortSha);
if (ApplicationDeployment.IsNetworkDeployed)
ClickOnceAppIconHelper.SetAddRemoveProgramsIcon();
else
log.Info("ApplicationDeployment.IsNetworkDeployed = false. We are not going to try and update the Add/Remove Programs Icon.");
this.WhenAny(x => x.UpdateVersion, x => x.Value)
.Where(x => !string.IsNullOrWhiteSpace(x))
.Subscribe(_ => FetchLatestChangeLog());
retrievingChangeLogFailed = this.WhenAny(x => x.ChangeLog, x => x.Value == null)
.ToProperty(this, x => x.RetrievingChangeLogFailed);
latestReleases = this.WhenAny(x => x.ChangeLog, x => x.Value)
.Where(x => x != null)
.Select(x => FilterChangeLog(x.Releases))
.ToProperty(this, x => x.LatestReleases);
timer = new Timer(10*1000); // first check in 10 seconds
timer.Elapsed += CheckForUpdates;
if (!App.IsInDesignMode())
timer.Start();
}
/// <summary>
/// Filters a full change log to just the releases between the current version and
/// the version to be updated to.
///
/// Example:
/// Application is running 1.0.0.105
/// There have been two subsequent releases (1.1.0 and 1.2.0)
/// Change log should show changes for 1.1.0, 1.2.0
/// Change log should not show changes for 1.0.0
///
/// </summary>
/// <param name="fullChangeLog"></param>
/// <returns></returns>
List<ChangeLogEntry> FilterChangeLog(IEnumerable<ChangeLogEntry> fullChangeLog)
{
return fullChangeLog.Where(x =>
{
var entryVersion = new Version(x.Version);
var latestVersion = new Version(UpdateVersion);
var currentVersion = new Version(Version);
return entryVersion <= latestVersion && entryVersion > currentVersion;
})
.ToList();
}
public void CheckForUpdates(object sender, EventArgs args)
{
if (IsUpdateCheckInProgress) return;
timer.Stop();
if (!ApplicationDeployment.IsNetworkDeployed)
{
UpdateCheckStatus = "This isn't a networked deployed app.";
UpdateVersion = Version;
return;
}
if (!instanceCommunicator.IsMasterInstance)
{
UpdateCheckStatus = "There is another instance running that will check for updates.";
UpdateVersion = Version;
return;
}
timer.Interval = 2*60*60*1000; // from now on check every 2 hrs
// todo: checking for a clickone version causes memory to leak ea. time you check.
// We want to do this only once, so this should be replace with a call to central
// to see if a new version is available. Only at that point will we call the clickone
// update* methods.
UpdateCheckStatus = "Checking for update…";
IsUpdateCheckInProgress = true;
log.Info("Checking for new version");
var deployment = ApplicationDeployment.CurrentDeployment;
try
{
deployment.CheckForUpdateCompleted -= CheckForUpdateCompleted;
deployment.CheckForUpdateCompleted += CheckForUpdateCompleted;
deployment.CheckForUpdateAsync();
}
catch (Exception ex)
{
log.ErrorException("Failed to check for sw update", ex);
deployment.CheckForUpdateCompleted -= CheckForUpdateCompleted;
}
timer.Start();
}
public string CurrentVersionString { get; private set; }
/// <summary>
/// Forces an immediate check for updates instead of waiting for the timer.
/// </summary>
public void ForceCheckForUpdates()
{
CheckForUpdates(null, null);
}
/// <summary>
/// Tells if we are already curerntly checking for or downloading an update.
/// </summary>
public bool IsUpdateCheckInProgress
{
get { return isUpdateCheckInProgress; }
set { this.RaiseAndSetIfChanged(x => x.IsUpdateCheckInProgress, value); }
}
/// <summary>
/// User friendly status of current state of checking for an update.
/// </summary>
public string UpdateCheckStatus
{
get { return updateCheckStatus; }
set { this.RaiseAndSetIfChanged(x => x.UpdateCheckStatus, value); }
}
/// <summary>
/// The full changelog as served by windows.github.com
/// </summary>
public ChangeLog ChangeLog
{
get { return changeLog; }
set { this.RaiseAndSetIfChanged(x => x.ChangeLog, value); }
}
/// <summary>
/// The latest release is the release that matches the Version ClickOnce wants to download.
/// </summary>
public List<ChangeLogEntry> LatestReleases
{
get { return latestReleases.Value; }
}
/// <summary>
/// Tells if fetching the changelog from the server has failed.
/// </summary>
public bool RetrievingChangeLogFailed
{
get { return retrievingChangeLogFailed.Value; }
}
/// <summary>
/// Tells if an updated is available and has been downloaded.
/// </summary>
public bool UpdateAvailableAndDownloaded
{
get { return updateAvailableAndDownloaded; }
set { this.RaiseAndSetIfChanged(x => x.UpdateAvailableAndDownloaded, value); }
}
/// <summary>
/// The latest version of the application according to ClickOnce.
/// </summary>
public string UpdateVersion
{
get { return updateVersion; }
set { this.RaiseAndSetIfChanged(x => x.UpdateVersion, value); }
}
/// <summary>
/// Restart the application immediately.
/// </summary>
public void Restart()
{
if (UpdateAvailableAndDownloaded)
{
System.Windows.Forms.Application.Restart();
Application.Current.Shutdown();
}
Cancel();
}
void CheckForUpdateCompleted(object sender, CheckForUpdateCompletedEventArgs e)
{
if (e.Error != null)
{
IsUpdateCheckInProgress = false;
log.ErrorException("Checking for application update failed.", e.Error);
UpdateCheckStatus = "Checking for update failed.";
return;
}
if (e.Cancelled || !e.UpdateAvailable)
{
IsUpdateCheckInProgress = false;
UpdateCheckStatus = "GitHub for Windows is up to date.";
UpdateVersion = Version;
return;
}
log.Info("New version available: {0}", e.AvailableVersion);
UpdateCheckStatus = "Downloading update…";
UpdateVersion = e.AvailableVersion.ToString();
var deployment = ApplicationDeployment.CurrentDeployment;
try
{
deployment.UpdateCompleted -= UpdateCompleted;
deployment.UpdateCompleted += UpdateCompleted;
deployment.UpdateAsync();
}
catch (Exception ex)
{
log.ErrorException("Failed to update", ex);
deployment.UpdateCompleted -= UpdateCompleted;
}
}
/// <summary>
/// Grab the latest changelog from the server (windows.github.com)
/// </summary>
void FetchLatestChangeLog()
{
changeLogFetcher.FetchChangeLog()
.Catch<ChangeLog, Exception>(ex =>
{
log.WarnException("Couldn't fetch ChangeLog", ex);
return Observable.Return<ChangeLog>(null);
})
.Subscribe(x => ChangeLog = x);
}
void UpdateCompleted(object sender, AsyncCompletedEventArgs e)
{
IsUpdateCheckInProgress = false;
if (e.Error != null)
{
log.ErrorException("Downloading application update failed.", e.Error);
UpdateCheckStatus = "Downloading update failed.";
return;
}
if (e.Cancelled) return;
UpdateCheckStatus = "New updates available.";
UpdateAvailableAndDownloaded = true;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
timer.Dispose();
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment