/* * $Id$ * Copyright 2008-2009 The Eraser Project * Original Author: Joel Low * Modified By: * * This file is part of Eraser. * * Eraser 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. * * Eraser 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. * * A copy of the GNU General Public License can be found at * . */ using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Text; using System.Windows.Forms; using System.Net; using System.Reflection; using System.IO; using System.Xml; using Eraser.Util; using System.Net.Cache; using System.Net.Mime; using System.Globalization; namespace Eraser { public partial class UpdateForm : Form { /// /// Constructor. /// public UpdateForm() { InitializeComponent(); UXThemeApi.UpdateControlTheme(this); updateListDownloader.RunWorkerAsync(); } /// /// Called when the form is about to be closed. /// /// The object triggering this event/ /// Event argument. private void UpdateForm_FormClosing(object sender, FormClosingEventArgs e) { //Cancel all running background tasks if (updateListDownloader.IsBusy || downloader.IsBusy || installer.IsBusy) { updateListDownloader.CancelAsync(); downloader.CancelAsync(); installer.CancelAsync(); e.Cancel = true; } } /// /// Called when any of the Cancel buttons are clicked. /// /// The object triggering this event/ /// Event argument. private void cancelBtn_Click(object sender, EventArgs e) { Close(); } #region Update List retrieval /// /// Downloads and parses the list of updates available for this client. /// /// The object triggering this event/ /// Event argument. private void updateListDownloader_DoWork(object sender, DoWorkEventArgs e) { try { updates.OnProgressEvent += updateListDownloader_ProgressChanged; updates.DownloadUpdateList(); } finally { updates.OnProgressEvent -= updateListDownloader_ProgressChanged; } } /// /// Called when progress has been made in the update list download. /// /// The object triggering this event/ /// Event argument. private void updateListDownloader_ProgressChanged(object sender, ProgressEventArgs e) { if (InvokeRequired) { if (updateListDownloader.CancellationPending) throw new OperationCanceledException(); Invoke((EventHandler)updateListDownloader_ProgressChanged, sender, e); return; } progressPb.Style = ProgressBarStyle.Continuous; progressPb.Value = (int)(e.OverallProgressPercentage * 100); progressProgressLbl.Text = e.Message; if (progressPb.Value == 100) progressProgressLbl.Text = S._("Processing update list..."); } /// /// Displays the parsed updates on the updates list view, filtering and displaying /// only those relevant to the current system's architecture. /// /// The object triggering this event/ /// Event argument. private void updateListDownloader_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) { //The Error property will normally be null unless there are errors during the download. if (e.Error != null) { if (!(e.Error is OperationCanceledException)) MessageBox.Show(this, e.Error.Message, S._("Eraser"), MessageBoxButtons.OK, MessageBoxIcon.Error, MessageBoxDefaultButton.Button1, S.IsRightToLeft(this) ? MessageBoxOptions.RtlReading : 0); Close(); return; } progressPanel.Visible = false; updatesPanel.Visible = true; //First list all available mirrors Dictionary.Enumerator iter = updates.Mirrors.GetEnumerator(); while (iter.MoveNext()) updatesMirrorCmb.Items.Add(iter.Current.Value); updatesMirrorCmb.SelectedIndex = 0; //Get a list of translatable categories (this will change as more categories //are added) Dictionary updateCategories = new Dictionary(); updateCategories.Add("update", S._("Updates")); updateCategories.Add("plugin", S._("Plugins")); //Only include those whose architecture is compatible with ours. List compatibleArchs = new List(); { //any is always compatible. compatibleArchs.Add("any"); switch (KernelApi.ProcessorArchitecture) { case ProcessorArchitecture.Amd64: compatibleArchs.Add("x64"); break; case ProcessorArchitecture.IA64: compatibleArchs.Add("ia64"); break; case ProcessorArchitecture.X86: compatibleArchs.Add("x86"); break; } } foreach (string key in updates.Categories) { ListViewGroup group = new ListViewGroup(updateCategories.ContainsKey(key) ? updateCategories[key] : key); updatesLv.Groups.Add(group); foreach (UpdateInfo update in updates.Updates[key]) { //Skip if this update won't work on our current architecture. if (compatibleArchs.IndexOf(update.Architecture) == -1) continue; ListViewItem item = new ListViewItem(update.Name); item.SubItems.Add(update.Version.ToString()); item.SubItems.Add(update.Publisher); item.SubItems.Add(Util.File.GetHumanReadableFilesize(update.FileSize)); item.Tag = update; item.Group = group; item.Checked = true; updatesLv.Items.Add(item); uiUpdates.Add(update, new UpdateData(update, item)); } } updatesBtn.Enabled = updatesLv.Items.Count > 0; //Check if there are any updates at all. if (updatesLv.Items.Count == 0) { MessageBox.Show(this, S._("There are no new updates or plugins available for " + "Eraser."), S._("Eraser"), MessageBoxButtons.OK, MessageBoxIcon.Information, MessageBoxDefaultButton.Button1, S.IsRightToLeft(this) ? MessageBoxOptions.RtlReading : 0); Close(); } } #endregion #region Update downloader /// /// Handles the update checked event. /// /// The object triggering this event/ /// Event argument. private void updatesLv_ItemChecked(object sender, ItemCheckedEventArgs e) { if (selectedUpdates == -1 || updatesCount != updatesLv.Items.Count) { updatesCount = updatesLv.Items.Count; selectedUpdates = 0; foreach (ListViewItem item in updatesLv.Items) if (item.Checked) ++selectedUpdates; } else selectedUpdates += e.Item.Checked ? 1 : -1; updatesBtn.Text = selectedUpdates == 0 ? S._("Close") : S._("Install"); } /// /// Handles the Install button click; fetches and installs the updates selected. /// /// The object triggering this event/ /// Event argument. private void updatesBtn_Click(object sender, EventArgs e) { updatesPanel.Visible = false; downloadingPnl.Visible = true; List updatesToInstall = new List(); //Set the mirror updates.SelectedMirror = (Mirror)updatesMirrorCmb.SelectedItem; //Collect the items that need to be installed foreach (ListViewItem item in updatesLv.Items) if (item.Checked) { item.Remove(); item.SubItems.RemoveAt(1); item.SubItems.RemoveAt(1); downloadingLv.Items.Add(item); updatesToInstall.Add((UpdateInfo)item.Tag); } else uiUpdates.Remove((UpdateInfo)item.Tag); //Then run the thread if there are updates. if (updatesToInstall.Count > 0) downloader.RunWorkerAsync(updatesToInstall); else Close(); } /// /// Background thread to do the downloading and installing of updates. /// /// The object triggering this event/ /// Event argument. private void downloader_DoWork(object sender, DoWorkEventArgs e) { try { updates.OnProgressEvent += downloader_ProgressChanged; object downloadedUpdates = updates.DownloadUpdates((List)e.Argument); e.Result = downloadedUpdates; } finally { updates.OnProgressEvent -= downloader_ProgressChanged; } } /// /// Handles the download progress changed event. /// /// The object triggering this event/ /// Event argument. private void downloader_ProgressChanged(object sender, ProgressEventArgs e) { if (InvokeRequired) { if (updateListDownloader.CancellationPending) throw new OperationCanceledException(); Invoke((EventHandler)downloader_ProgressChanged, sender, e); return; } UpdateData update = uiUpdates[(UpdateInfo)e.UserState]; if (e is ProgressErrorEventArgs) { update.Error = ((ProgressErrorEventArgs)e).Exception; update.LVItem.ImageIndex = 3; update.LVItem.SubItems[1].Text = S._("Error"); update.LVItem.ToolTipText = update.Error.Message; } else { if (e.ProgressPercentage >= 1.0f) { update.LVItem.ImageIndex = -1; update.LVItem.SubItems[1].Text = S._("Downloaded"); } else { update.amountDownloaded = (long)(e.ProgressPercentage * update.Update.FileSize); update.LVItem.ImageIndex = 0; update.LVItem.SubItems[1].Text = Util.File.GetHumanReadableFilesize( update.Update.FileSize - update.amountDownloaded); } } downloadingItemLbl.Text = e.Message; downloadingItemPb.Value = (int)(e.ProgressPercentage * 100); downloadingOverallPb.Value = (int)(e.OverallProgressPercentage * 100); long amountToDownload = 0; foreach (UpdateData upd in uiUpdates.Values) amountToDownload += upd.Update.FileSize - upd.amountDownloaded; downloadingOverallLbl.Text = S._("Overall progress: {0} left", Util.File.GetHumanReadableFilesize(amountToDownload)); } /// /// Handles the completion of updating event /// /// The object triggering this event/ /// Event argument. private void downloader_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) { if (e.Error != null) { if (!(e.Error is OperationCanceledException)) MessageBox.Show(this, e.Error.Message, S._("Eraser"), MessageBoxButtons.OK, MessageBoxIcon.Error, MessageBoxDefaultButton.Button1, S.IsRightToLeft(this) ? MessageBoxOptions.RtlReading : 0); Close(); return; } downloadingPnl.Visible = false; installingPnl.Visible = true; foreach (ListViewItem item in downloadingLv.Items) { item.Remove(); installingLv.Items.Add(item); UpdateData update = uiUpdates[(UpdateInfo)item.Tag]; if (update.Error == null) item.SubItems[1].Text = string.Empty; else item.SubItems[1].Text = S._("Error: {0}", update.Error.Message); } installer.RunWorkerAsync(e.Result); } #endregion #region Update installer /// /// Background thread to install downloaded updates /// /// The object triggering this event/ /// Event argument. private void installer_DoWork(object sender, DoWorkEventArgs e) { try { updates.OnProgressEvent += installer_ProgressChanged; updates.InstallUpdates(e.Argument); } finally { updates.OnProgressEvent -= installer_ProgressChanged; } } /// /// Handles the progress events generated during update installation. /// /// The object triggering this event/ /// Event argument. private void installer_ProgressChanged(object sender, ProgressChangedEventArgs e) { if (InvokeRequired) { if (updateListDownloader.CancellationPending) throw new OperationCanceledException(); Invoke((EventHandler)installer_ProgressChanged, sender, e); return; } UpdateData update = uiUpdates[(UpdateInfo)e.UserState]; if (e is ProgressErrorEventArgs) { update.Error = ((ProgressErrorEventArgs)e).Exception; update.LVItem.ImageIndex = 3; update.LVItem.SubItems[1].Text = S._("Error: {0}", update.Error.Message); } else switch (update.LVItem.ImageIndex) { case -1: update.LVItem.ImageIndex = 1; break; case 1: update.LVItem.ImageIndex = 2; break; } } /// /// Re-enables the close dialog button. /// /// The object triggering this event/ /// Event argument. private void installer_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) { if (e.Error is OperationCanceledException) Close(); installingPnl.UseWaitCursor = false; } #endregion /// /// The Update manager instance used by this form. /// UpdateManager updates = new UpdateManager(); /// /// Maps listview items to the UpdateManager.Update object. /// Dictionary uiUpdates = new Dictionary(); /// /// Manages information associated with the update. /// private class UpdateData { /// /// Constructor. /// /// The UpdateManager.Update object containing the /// internal representation of the update. /// The ListViewItem used for the display of the /// update. public UpdateData(UpdateInfo update, ListViewItem item) { Update = update; LVItem = item; } /// /// The UpdateManager.Update object containing the internal representation /// of the update. /// public UpdateInfo Update; /// /// The ListViewItem used for the display of the update. /// public ListViewItem LVItem; /// /// The amount of the download already completed. /// public long amountDownloaded; /// /// The error raised when downloading/installing the update, if any. Null /// otherwise. /// public Exception Error; } /// /// The number of updates selected for download. /// private int selectedUpdates = -1; /// /// The number of updates present in the previous count, so the Selected /// Updates number can be deemed invalid. /// private int updatesCount = -1; } public class UpdateManager { /// /// Constructor. /// public UpdateManager() { Updates = new UpdateCategoriesDictionary(); } /// /// Retrieves the update list from the server. /// public void DownloadUpdateList() { WebRequest.DefaultCachePolicy = new HttpRequestCachePolicy( HttpRequestCacheLevel.Refresh); HttpWebRequest req = (HttpWebRequest) WebRequest.Create(new Uri("http://eraser.heidi.ie/scripts/updates?" + "action=listupdates&version=" + Assembly.GetExecutingAssembly().GetName().Version.ToString())); using (WebResponse response = req.GetResponse()) using (Stream responseStream = response.GetResponseStream()) using (MemoryStream memoryStream = new MemoryStream()) { Manager.ProgressManager progress = new Manager.ProgressManager(); progress.Total = response.ContentLength; //Download the response int lastRead = 0; byte[] buffer = new byte[16384]; while ((lastRead = responseStream.Read(buffer, 0, buffer.Length)) != 0) { memoryStream.Write(buffer, 0, lastRead); progress.Completed = memoryStream.Position; OnProgress(new ProgressEventArgs(progress.Progress, progress.Progress, null, S._("{0} of {1} downloaded", Util.File.GetHumanReadableFilesize(progress.Completed), Util.File.GetHumanReadableFilesize(progress.Total)))); } //Parse it. memoryStream.Position = 0; ParseUpdateList(memoryStream); } } /// /// Parses the list of updates provided by the server /// /// The stream containing the XML data. private void ParseUpdateList(Stream strm) { //Move the XmlReader to the root node Updates.Clear(); mirrors.Clear(); XmlReader rdr = XmlReader.Create(strm); rdr.ReadToFollowing("updateList"); //Read the descendants of the updateList node (which are categories, //except for the element) XmlReader categories = rdr.ReadSubtree(); bool cont = categories.ReadToDescendant("mirrors"); while (cont) { if (categories.NodeType == XmlNodeType.Element) { if (categories.Name == "mirrors") { Dictionary mirrorsList = ParseMirror(categories.ReadSubtree()); Dictionary.Enumerator e = mirrorsList.GetEnumerator(); while (e.MoveNext()) this.mirrors.Add(e.Current.Key, new Mirror(e.Current.Value, e.Current.Key)); } else Updates.Add(categories.Name, ParseUpdateCategory(categories.ReadSubtree())); } cont = categories.Read(); } } /// /// Parses a list of mirrors. /// /// The XML reader object representing the <mirrors> node /// The list of mirrors defined by the element. private static Dictionary ParseMirror(XmlReader rdr) { Dictionary result = new Dictionary(); if (!rdr.ReadToDescendant("mirror")) return result; //Load every element. do { if (rdr.NodeType != XmlNodeType.Element || rdr.Name != "mirror") continue; string location = rdr.GetAttribute("location"); result.Add(rdr.ReadElementContentAsString(), location); } while (rdr.ReadToNextSibling("mirror")); return result; } /// /// Parses a specific category and its assocaited updates. /// /// The XML reader object representing the element and its children. /// A list of updates in the category. private static UpdateCollection ParseUpdateCategory(XmlReader rdr) { UpdateCollection result = new UpdateCollection(); if (!rdr.ReadToDescendant("item")) return result; //Load every element. do { if (rdr.Name != "item") continue; UpdateInfo update = new UpdateInfo(); update.Name = rdr.GetAttribute("name"); update.Version = new Version(rdr.GetAttribute("version")); update.Publisher = rdr.GetAttribute("publisher"); update.Architecture = rdr.GetAttribute("architecture"); update.FileSize = Convert.ToInt64(rdr.GetAttribute("filesize"), CultureInfo.InvariantCulture); update.Link = rdr.ReadElementContentAsString(); result.Add(update); } while (rdr.ReadToNextSibling("item")); return result; } /// /// Downloads the list of updates. /// /// The updates to retrieve and install. /// An opaque object for use with InstallUpdates. public object DownloadUpdates(ICollection downloadQueue) { //Create a folder to hold all our updates. DirectoryInfo tempDir = new DirectoryInfo(Path.GetTempPath()); tempDir = tempDir.CreateSubdirectory("eraser" + Environment.TickCount.ToString( CultureInfo.InvariantCulture)); int currUpdate = 0; Dictionary tempFilesMap = new Dictionary(); Manager.SteppedProgressManager progress = new Manager.SteppedProgressManager(); foreach (UpdateInfo update in downloadQueue) { try { //Add the update to the overall progress. Manager.ProgressManager step = new Eraser.Manager.ProgressManager(); progress.Steps.Add(new Manager.SteppedProgressManager.Step( step, 1.0f / downloadQueue.Count)); //Decide on the URL to connect to. The Link of the update may //be a relative path (relative to the selected mirror) or an //absolute path (which we have no choice) Uri reqUri = null; if (Uri.IsWellFormedUriString(update.Link, UriKind.Absolute)) reqUri = new Uri(update.Link); else reqUri = new Uri(new Uri(SelectedMirror.Link), new Uri(update.Link)); //Then grab the download. HttpWebRequest req = (HttpWebRequest)WebRequest.Create(reqUri); using (WebResponse resp = req.GetResponse()) { //Check for a suggested filename. ContentDisposition contentDisposition = null; foreach (string header in resp.Headers.AllKeys) if (header.ToLowerInvariant() == "content-disposition") contentDisposition = new ContentDisposition(resp.Headers[header]); string tempFilePath = Path.Combine( tempDir.FullName, string.Format(CultureInfo.InvariantCulture, "{0}-{1}", ++currUpdate, contentDisposition == null ? Path.GetFileName(reqUri.GetComponents(UriComponents.Path, UriFormat.Unescaped)) : contentDisposition.FileName)); using (Stream responseStream = resp.GetResponseStream()) using (FileStream fileStream = new FileStream(tempFilePath, FileMode.CreateNew)) { //Update the progress of this step step.Total = resp.ContentLength; //Copy the information into the file stream int lastRead = 0; byte[] buffer = new byte[16384]; while ((lastRead = responseStream.Read(buffer, 0, buffer.Length)) != 0) { fileStream.Write(buffer, 0, lastRead); //Compute progress step.Completed = fileStream.Position; OnProgress(new ProgressEventArgs(step.Progress, progress.Progress, update, S._("Downloading: {0}", update.Name))); } } //Store the filename-to-update mapping tempFilesMap.Add(tempFilePath, update); //Let the event handler know the download is complete. step.Completed = step.Total; OnProgress(new ProgressEventArgs(step.Progress, progress.Progress, update, S._("Downloaded: {0}", update.Name))); } } catch (Exception e) { OnProgress(new ProgressErrorEventArgs(new ProgressEventArgs(1.0f, (float)currUpdate / downloadQueue.Count, update, S._("Error downloading {0}: {1}", update.Name, e.Message)), e)); } } return tempFilesMap; } /// /// Installs all updates downloaded. /// /// The value returned from a call to /// . public void InstallUpdates(object value) { Manager.ProgressManager progress = new Manager.ProgressManager(); Dictionary tempFiles = (Dictionary)value; Dictionary.KeyCollection files = tempFiles.Keys; try { progress.Total = files.Count; foreach (string path in files) { UpdateInfo item = tempFiles[path]; ++progress.Completed; OnProgress(new ProgressEventArgs(0.0f, progress.Progress, item, S._("Installing {0}", item.Name))); System.Diagnostics.ProcessStartInfo info = new System.Diagnostics.ProcessStartInfo(); info.FileName = path; info.UseShellExecute = true; System.Diagnostics.Process process = System.Diagnostics.Process.Start(info); process.WaitForExit(Int32.MaxValue); if (process.ExitCode == 0) OnProgress(new ProgressEventArgs(1.0f, progress.Progress, item, S._("Installed {0}", item.Name))); else OnProgress(new ProgressErrorEventArgs(new ProgressEventArgs(1.0f, progress.Progress, item, S._("Error installing {0}", item.Name)), new ApplicationException(S._("The installer exited with an error code {0}", process.ExitCode)))); } } finally { //Clean up after ourselves foreach (string file in files) { DirectoryInfo tempDir = null; { FileInfo info = new FileInfo(file); tempDir = info.Directory; } tempDir.Delete(true); break; } } } /// /// Called when the progress of the operation changes. /// public EventHandler OnProgressEvent { get; set; } /// /// Helper function: invokes the OnProgressEvent delegate. /// /// The ProgressEventArgs object holding information /// about the progress of the current operation. private void OnProgress(ProgressEventArgs arg) { if (OnProgressEvent != null) OnProgressEvent(this, arg); } /// /// Retrieves the list of mirrors which the server has indicated to exist. /// public Dictionary Mirrors { get { return mirrors; } } /// /// Gets or sets the active mirror to use to download mirrored updates. /// public Mirror SelectedMirror { get { if (selectedMirror.Link.Length == 0) { Dictionary.Enumerator iter = mirrors.GetEnumerator(); if (iter.MoveNext()) return iter.Current.Value; } return selectedMirror; } set { foreach (Mirror mirror in Mirrors.Values) if (mirror.Equals(value)) { selectedMirror = value; return; } throw new ArgumentException(S._("Unknown mirror selected.")); } } /// /// Retrieves the categories available. /// public ICollection Categories { get { return Updates.Keys; } } /// /// Retrieves all updates available. /// public UpdateCategoriesDictionary Updates { get; private set; } /// /// The list of mirrors to download updates from. /// private Dictionary mirrors = new Dictionary(); /// /// The currently selected mirror. /// private Mirror selectedMirror; } /// /// Manages a list of categories, mapping categories to a list of updates. /// public class UpdateCategoriesDictionary : IDictionary, ICollection>, IEnumerable> { #region IDictionary Members public void Add(string key, UpdateCollection value) { dictionary.Add(key, value); } public bool ContainsKey(string key) { return dictionary.ContainsKey(key); } public ICollection Keys { get { return dictionary.Keys; } } public bool Remove(string key) { return dictionary.Remove(key); } public bool TryGetValue(string key, out UpdateCollection value) { return dictionary.TryGetValue(key, out value); } public ICollection Values { get { return dictionary.Values; } } public UpdateCollection this[string key] { get { return dictionary[key]; } set { dictionary[key] = value; } } #endregion #region ICollection> Members public void Add(KeyValuePair item) { dictionary.Add(item.Key, item.Value); } public void Clear() { dictionary.Clear(); } public bool Contains(KeyValuePair item) { return dictionary.ContainsKey(item.Key) && dictionary[item.Key] == item.Value; } public void CopyTo(KeyValuePair[] array, int arrayIndex) { throw new NotImplementedException(); } public int Count { get { return dictionary.Count; } } public bool IsReadOnly { get { return true; } } public bool Remove(KeyValuePair item) { return dictionary.Remove(item.Key); } #endregion #region IEnumerable> Members public IEnumerator> GetEnumerator() { return dictionary.GetEnumerator(); } #endregion #region IEnumerable Members System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return GetEnumerator(); } #endregion /// /// The store for the current object. /// private Dictionary dictionary = new Dictionary(); } /// /// Manages a category, containing a list of updates. /// public class UpdateCollection : IList, ICollection, IEnumerable { #region IList Members public int IndexOf(UpdateInfo item) { return list.IndexOf(item); } public void Insert(int index, UpdateInfo item) { list.Insert(index, item); } public void RemoveAt(int index) { list.RemoveAt(index); } public UpdateInfo this[int index] { get { return list[index]; } set { list[index] = value; } } #endregion #region ICollection Members public void Add(UpdateInfo item) { list.Add(item); } public void Clear() { list.Clear(); } public bool Contains(UpdateInfo item) { return list.Contains(item); } public void CopyTo(UpdateInfo[] array, int arrayIndex) { list.CopyTo(array, arrayIndex); } public int Count { get { return list.Count; } } public bool IsReadOnly { get { return true; } } public bool Remove(UpdateInfo item) { return list.Remove(item); } #endregion #region IEnumerable Members public IEnumerator GetEnumerator() { return list.GetEnumerator(); } #endregion #region IEnumerable Members System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return list.GetEnumerator(); } #endregion /// /// The store for this object. /// private List list = new List(); } /// /// Represents a download mirror. /// public struct Mirror { public Mirror(string location, string link) : this() { Location = location; Link = link; } /// /// The location where the mirror is at. /// public string Location { get; set; } /// /// The URL prefix to utilise the mirror. /// public string Link { get; set; } public override string ToString() { return Location; } public override bool Equals(object obj) { if (!(obj is Mirror)) return false; return Equals((Mirror)obj); } public bool Equals(Mirror other) { return Link == other.Link; } public static bool operator ==(Mirror mirror1, Mirror mirror2) { return mirror1.Equals(mirror2); } public static bool operator !=(Mirror mirror1, Mirror mirror2) { return !mirror1.Equals(mirror2); } public override int GetHashCode() { return Link.GetHashCode(); } } /// /// Represents an update available on the server. /// public struct UpdateInfo { public string Name { get; set; } public Version Version { get; set; } public string Publisher { get; set; } public string Architecture { get; set; } public long FileSize { get; set; } public string Link { get; set; } public override bool Equals(object obj) { if (!(obj is UpdateInfo)) return false; return Equals((UpdateInfo)obj); } public bool Equals(UpdateInfo other) { return Link == other.Link; } public static bool operator ==(UpdateInfo update1, UpdateInfo update2) { return update1.Equals(update2); } public static bool operator !=(UpdateInfo update1, UpdateInfo update2) { return !update1.Equals(update2); } public override int GetHashCode() { return Link.GetHashCode(); } } /// /// Specialised progress event argument, containing message describing /// current action, and overall progress percentage. /// public class ProgressEventArgs : ProgressChangedEventArgs { public ProgressEventArgs(float progressPercentage, float overallPercentage, object userState, string message) : base((int)(progressPercentage * 100), userState) { ProgressPercentage = progressPercentage; OverallProgressPercentage = overallPercentage; Message = message; } /// /// Gets the asynchronous task progress percentage. /// public new float ProgressPercentage { get; private set; } /// /// Gets the asynchronous task overall progress percentage. /// public float OverallProgressPercentage { get; private set; } /// /// Gets the message associated with the current task. /// public string Message { get; private set; } } /// /// Extends the ProgressEventArgs further by allowing for the inclusion of /// an exception. /// public class ProgressErrorEventArgs : ProgressEventArgs { /// /// Constructor. /// /// The base ProgressEventArgs object. /// The exception public ProgressErrorEventArgs(ProgressEventArgs e, Exception ex) : base(e.ProgressPercentage, e.OverallProgressPercentage, e.UserState, e.Message) { Exception = ex; } /// /// The exception associated with the progress event. /// public Exception Exception { get; private set; } } }