OnTopReplica/OnTopReplica/Update/UpdateManager.cs
2013-12-03 21:28:46 +01:00

298 lines
11 KiB
C#

using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Cache;
using System.Text.RegularExpressions;
using System.Windows.Forms;
using System.Xml.Linq;
using WindowsFormsAero.TaskDialog;
namespace OnTopReplica.Update {
/// <summary>
/// Handles update checking and information display.
/// </summary>
class UpdateManager {
/// <summary>
/// Constructs a new update manager with an attached form.
/// </summary>
/// <param name="attachedForm">Form through which all GUI calls are made. Closing this form should terminate the application.</param>
public UpdateManager(Form attachedForm) {
if (attachedForm == null)
throw new ArgumentNullException();
AttachedForm = attachedForm;
}
/// <summary>
/// Gets or sets the attached form (through which all GUI calls are made).
/// </summary>
protected Form AttachedForm { get; private set; }
#region Checking
const string UpdateFeedUrl = "https://ontopreplica.codeplex.com/project/feeds/rss?ProjectRSSFeed=codeplex%3a%2f%2frelease%2fontopreplica";
/// <summary>
/// Gets the latest update information available.
/// </summary>
public UpdateInformation LastInformation { get; private set; }
HttpWebRequest _checkRequest;
/// <summary>
/// Checks for update asynchronously, updating update information.
/// When check is completed, raises update events.
/// </summary>
public void CheckForUpdate() {
if (_checkRequest != null) {
_checkRequest.Abort();
}
//Build web request
_checkRequest = (HttpWebRequest)HttpWebRequest.Create(UpdateFeedUrl);
_checkRequest.AllowAutoRedirect = true;
_checkRequest.AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip;
_checkRequest.CachePolicy = new RequestCachePolicy(RequestCacheLevel.BypassCache);
_checkRequest.BeginGetResponse(CheckForUpdateCallback, null);
}
/// <summary>
/// Asynchronous callback that handles the update check request.
/// </summary>
private void CheckForUpdateCallback(IAsyncResult result) {
if (_checkRequest == null)
return;
try {
var response = _checkRequest.EndGetResponse(result);
LastInformation = ParseUpdateCheckResponse(response.GetResponseStream());
OnUpdateCheckSuccess(LastInformation);
}
catch (Exception ex) {
OnUpdateCheckError(ex);
}
_checkRequest = null;
}
private Regex _versionExtractor = new Regex(@"^Released: Release (?<version>([0-9]\.){0,3}[0-9]?)", RegexOptions.Compiled | RegexOptions.Singleline);
private UpdateInformation ParseUpdateCheckResponse(Stream stream) {
var xdoc = XDocument.Load(stream);
var releases = from item in xdoc.Descendants("item")
let title = item.Element("title").Value
let match = _versionExtractor.Match(title)
where match.Success
let versionNumber = match.Groups["version"].Value
orderby versionNumber descending
select new { versionNumber, item.Element("link").Value };
return new UpdateInformation();
}
#endregion
#region Eventing
public event EventHandler<UpdateCheckCompletedEventArgs> UpdateCheckCompleted;
protected virtual void OnUpdateCheckError(Exception ex) {
var evt = UpdateCheckCompleted;
if (evt != null) {
evt(this, new UpdateCheckCompletedEventArgs {
Success = false,
Error = ex
});
}
}
protected virtual void OnUpdateCheckSuccess(UpdateInformation information) {
var evt = UpdateCheckCompleted;
if (evt != null) {
evt(this, new UpdateCheckCompletedEventArgs {
Success = true,
Information = information
});
}
}
#endregion
#region Updating
HttpWebRequest _downloadRequest;
TaskDialog _updateDialog;
bool _updateDownloaded = false;
/// <summary>
/// Asks confirmation for an update and installs the update.
/// </summary>
public void ConfirmAndInstall() {
if (LastInformation == null || !LastInformation.IsNewVersion)
return;
AttachedForm.SafeInvoke(new Action(ConfirmAndInstallCore));
}
/// <summary>
/// Core delegate that asks for update confirmation and installs. Must be called from GUI thread.
/// </summary>
private void ConfirmAndInstallCore() {
_updateDialog = new TaskDialog {
Title = Strings.UpdateTitle,
Instruction = string.Format(Strings.UpdateAvailableInstruction, LastInformation.LatestVersion),
Content = Strings.UpdateAvailableContent,
CustomButtons = new CustomButton[] {
new CustomButton(Result.OK, string.Format(Strings.UpdateAvailableCommandOk, LastInformation.LatestVersion)),
new CustomButton(Result.Cancel, Strings.UpdateAvailableCommandCancel)
},
UseCommandLinks = true,
CommonIcon = TaskDialogIcon.Information,
ExpandedInformation = string.Format(Strings.UpdateAvailableExpanded, LastInformation.CurrentVersion, LastInformation.LatestVersion),
};
_updateDialog.ButtonClick += delegate(object sender, ClickEventArgs args) {
if (args.ButtonID == (int)Result.OK) {
args.PreventClosing = true;
if (_updateDownloaded) {
//Terminate application
AttachedForm.Close();
//Launch updater
Process.Start(UpdateInstallerPath);
}
else {
var downDlg = new TaskDialog {
Title = Strings.UpdateTitle,
Instruction = Strings.UpdateDownloadingInstruction,
ShowProgressBar = true,
ProgressBarMinRange = 0,
ProgressBarMaxRange = 100,
ProgressBarPosition = 0,
CommonButtons = TaskDialogButton.Cancel
};
_updateDialog.Navigate(downDlg);
_downloadRequest = (HttpWebRequest)HttpWebRequest.Create(LastInformation.DownloadInstaller);
_downloadRequest.BeginGetResponse(DownloadAsyncCallback, null);
}
}
};
_updateDialog.Show(AttachedForm);
}
/// <summary>
/// Gets the target filename used when downloading the update from the Internet.
/// </summary>
private string UpdateInstallerPath {
get {
var downloadPath = Native.FilesystemMethods.DownloadsPath;
string versionName = (LastInformation != null) ?
LastInformation.LatestVersion.ToString() : string.Empty;
string filename = string.Format("OnTopReplica-Update-{0}.exe", versionName);
return Path.Combine(downloadPath, filename);
}
}
/// <summary>
/// Handles background downloading.
/// </summary>
private void DownloadAsyncCallback(IAsyncResult result) {
if (_downloadRequest == null || _updateDialog == null)
return;
try {
var response = _downloadRequest.EndGetResponse(result);
var responseStream = response.GetResponseStream();
long total = response.ContentLength;
byte[] buffer = new byte[1024];
using (var stream = new FileStream(UpdateInstallerPath, FileMode.Create)) {
int readTotal = 0;
while (true) {
int read = responseStream.Read(buffer, 0, buffer.Length);
readTotal += read;
if (read <= 0) //EOF
break;
stream.Write(buffer, 0, read);
_updateDialog.Content = string.Format(Strings.UpdateDownloadingContent, readTotal, total);
_updateDialog.ProgressBarPosition = (int)((readTotal * 100.0) / total);
}
}
}
catch (Exception ex) {
DownloadShowError(ex.Message);
return;
}
_updateDownloaded = true;
var okDlg = new TaskDialog {
Title = Strings.UpdateTitle,
Instruction = Strings.UpdateReadyInstruction,
Content = string.Format(Strings.UpdateReadyContent, LastInformation.LatestVersion),
UseCommandLinks = true,
CommonButtons = TaskDialogButton.Cancel,
CustomButtons = new CustomButton[] {
new CustomButton(Result.OK, Strings.UpdateReadyCommandOk)
}
};
_updateDialog.Navigate(okDlg);
}
private void DownloadShowError(string msg) {
if (_updateDialog == null)
return;
_updateDialog.ProgressBarState = WindowsFormsAero.ProgressBar.States.Error;
_updateDialog.Content = msg;
}
/// <summary>
/// Displays some information about the current installation and available updates.
/// </summary>
public void DisplayInfo() {
AttachedForm.SafeInvoke(new Action(DisplayInfoCore));
}
/// <summary>
/// Displays info. Called from GUI thread.
/// </summary>
private void DisplayInfoCore() {
//No updates, but need to inform the user
var dlg = new TaskDialog {
Title = Strings.UpdateTitle,
Instruction = Strings.UpdateInfoInstruction,
Content = Strings.UpdateInfoContent,
EnableHyperlinks = true,
CommonButtons = TaskDialogButton.Close,
AllowDialogCancellation = true,
CommonIcon = TaskDialogIcon.Information,
Footer = string.Format(Strings.UpdateInfoFooter, LastInformation.LatestVersionRelease.ToLongDateString())
};
dlg.HyperlinkClick += delegate(object sender, HyperlinkEventArgs args) {
Process.Start("http://ontopreplica.codeplex.com");
};
dlg.Show(AttachedForm);
}
#endregion
}
}