Mk0.Software.OnlineUpdater/Mk0.Software.OnlineUpdater/OnlineUpdater.cs

914 lines
33 KiB
C#

using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Drawing;
using System.Globalization;
using System.IO;
using System.Net;
using System.Net.Cache;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Windows.Forms;
using System.Xml;
using Mk0.Software.OnlineUpdater.Properties;
using Microsoft.Win32;
namespace Mk0.Software.OnlineUpdater
{
/// <summary>
/// Enum representing the remind later time span.
/// </summary>
public enum RemindLaterFormat
{
/// <summary>
/// Represents the time span in minutes.
/// </summary>
Minutes,
/// <summary>
/// Represents the time span in hours.
/// </summary>
Hours,
/// <summary>
/// Represents the time span in days.
/// </summary>
Days
}
/// <summary>
/// Enum representing the effect of Mandatory flag.
/// </summary>
public enum Mode
{
/// <summary>
/// In this mode, it ignores Remind Later and Skip values set previously and hide both buttons.
/// </summary>
Normal,
/// <summary>
/// In this mode, it won't show close button in addition to Normal mode behaviour.
/// </summary>
Forced,
/// <summary>
/// In this mode, it will start downloading and applying update without showing standarad update dialog in addition to Forced mode behaviour.
/// </summary>
ForcedDownload
}
/// <summary>
/// Main class that lets you auto update applications by setting some static fields and executing its Start method.
/// </summary>
public static class AutoUpdater
{
private static System.Timers.Timer _remindLaterTimer;
internal static String ChangelogURL;
internal static String DownloadURL;
internal static String InstallerArgs;
internal static String RegistryLocation;
internal static String Checksum;
internal static String HashingAlgorithm;
internal static Version CurrentVersion;
internal static Version InstalledVersion;
internal static bool IsWinFormsApplication;
internal static bool Running;
/// <summary>
/// Set it to folder path where you want to download the update file. If not provided then it defaults to Temp folder.
/// </summary>
public static String DownloadPath;
/// <summary>
/// Set the Application Title shown in Update dialog. Although AutoUpdater.NET will get it automatically, you can set this property if you like to give custom Title.
/// </summary>
public static String AppTitle;
/// <summary>
/// URL of the xml file that contains information about latest version of the application.
/// </summary>
public static String AppCastURL;
/// <summary>
/// Login/password/domain for FTP-request
/// </summary>
public static NetworkCredential FtpCredentials;
/// <summary>
/// Opens the download URL in default browser if true. Very usefull if you have portable application.
/// </summary>
public static bool OpenDownloadPage;
/// <summary>
/// Set Basic Authentication credentials required to download the file.
/// </summary>
public static IAuthentication BasicAuthDownload;
/// <summary>
/// Set Basic Authentication credentials required to download the XML file.
/// </summary>
public static IAuthentication BasicAuthXML;
/// <summary>
/// Set Basic Authentication credentials to navigate to the change log URL.
/// </summary>
public static IAuthentication BasicAuthChangeLog;
/// <summary>
/// Set the User-Agent string to be used for HTTP web requests.
/// </summary>
public static string HttpUserAgent;
/// <summary>
/// If this is true users can see the skip button.
/// </summary>
public static Boolean ShowSkipButton = true;
/// <summary>
/// If this is true users can see the Remind Later button.
/// </summary>
public static Boolean ShowRemindLaterButton = true;
/// <summary>
/// If this is true users see dialog where they can set remind later interval otherwise it will take the interval from
/// RemindLaterAt and RemindLaterTimeSpan fields.
/// </summary>
public static Boolean LetUserSelectRemindLater = true;
/// <summary>
/// Remind Later interval after user should be reminded of update.
/// </summary>
public static int RemindLaterAt = 2;
///<summary>
/// AutoUpdater.NET will report errors if this is true.
/// </summary>
public static bool ReportErrors = false;
/// <summary>
/// Set this to false if your application doesn't need administrator privileges to replace the old version.
/// </summary>
public static bool RunUpdateAsAdmin = true;
///<summary>
/// Set this to true if you want to ignore previously assigned Remind Later and Skip settings. It will also hide Remind Later and Skip buttons.
/// </summary>
public static bool Mandatory;
/// <summary>
/// Set this to any of the available modes to change behaviour of the Mandatory flag.
/// </summary>
public static Mode UpdateMode;
/// <summary>
/// Set Proxy server to use for all the web requests in AutoUpdater.NET.
/// </summary>
public static IWebProxy Proxy;
/// <summary>
/// Set if RemindLaterAt interval should be in Minutes, Hours or Days.
/// </summary>
public static RemindLaterFormat RemindLaterTimeSpan = RemindLaterFormat.Days;
/// <summary>
/// A delegate type to handle how to exit the application after update is downloaded.
/// </summary>
public delegate void ApplicationExitEventHandler();
/// <summary>
/// An event that developers can use to exit the application gracefully.
/// </summary>
public static event ApplicationExitEventHandler ApplicationExitEvent;
/// <summary>
/// A delegate type for hooking up update notifications.
/// </summary>
/// <param name="args">An object containing all the parameters recieved from AppCast XML file. If there will be an error while looking for the XML file then this object will be null.</param>
public delegate void CheckForUpdateEventHandler(UpdateInfoEventArgs args);
/// <summary>
/// An event that clients can use to be notified whenever the update is checked.
/// </summary>
public static event CheckForUpdateEventHandler CheckForUpdateEvent;
/// <summary>
/// A delegate type for hooking up parsing logic.
/// </summary>
/// <param name="args">An object containing the AppCast file received from server.</param>
public delegate void ParseUpdateInfoHandler(ParseUpdateInfoEventArgs args);
/// <summary>
/// An event that clients can use to be notified whenever the AppCast file needs parsing.
/// </summary>
public static event ParseUpdateInfoHandler ParseUpdateInfoEvent;
/// <summary>
/// Set if you want the default update form to have a different size.
/// </summary>
public static Size? UpdateFormSize = null;
/// <summary>
/// Start checking for new version of application and display dialog to the user if update is available.
/// </summary>
/// <param name="myAssembly">Assembly to use for version checking.</param>
public static void Start(Assembly myAssembly = null)
{
Start(AppCastURL, myAssembly);
}
/// <summary>
/// Start checking for new version of application via FTP and display dialog to the user if update is available.
/// </summary>
/// <param name="appCast">FTP URL of the xml file that contains information about latest version of the application.</param>
/// <param name="ftpCredentials">Credentials required to connect to FTP server.</param>
/// <param name="myAssembly">Assembly to use for version checking.</param>
public static void Start(String appCast, NetworkCredential ftpCredentials, Assembly myAssembly = null)
{
FtpCredentials = ftpCredentials;
Start(appCast, myAssembly);
}
/// <summary>
/// Start checking for new version of application and display dialog to the user if update is available.
/// </summary>
/// <param name="appCast">URL of the xml file that contains information about latest version of the application.</param>
/// <param name="myAssembly">Assembly to use for version checking.</param>
public static void Start(String appCast, Assembly myAssembly = null)
{
try
{
ServicePointManager.SecurityProtocol |= (SecurityProtocolType)192 |
(SecurityProtocolType)768 | (SecurityProtocolType)3072;
}
catch (NotSupportedException) { }
if (Mandatory && _remindLaterTimer != null)
{
_remindLaterTimer.Stop();
_remindLaterTimer.Close();
_remindLaterTimer = null;
}
if (!Running && _remindLaterTimer == null)
{
Running = true;
AppCastURL = appCast;
IsWinFormsApplication = Application.MessageLoop;
var backgroundWorker = new BackgroundWorker();
backgroundWorker.DoWork += BackgroundWorkerDoWork;
backgroundWorker.RunWorkerCompleted += BackgroundWorkerOnRunWorkerCompleted;
backgroundWorker.RunWorkerAsync(myAssembly ?? Assembly.GetEntryAssembly());
}
}
private static void BackgroundWorkerOnRunWorkerCompleted(object sender,
RunWorkerCompletedEventArgs runWorkerCompletedEventArgs)
{
if (!runWorkerCompletedEventArgs.Cancelled)
{
if (runWorkerCompletedEventArgs.Result is DateTime)
{
SetTimer((DateTime)runWorkerCompletedEventArgs.Result);
}
else
{
var args = runWorkerCompletedEventArgs.Result as UpdateInfoEventArgs;
if (CheckForUpdateEvent != null)
{
CheckForUpdateEvent(args);
}
else
{
if (args != null)
{
if (args.IsUpdateAvailable)
{
if (!IsWinFormsApplication)
{
Application.EnableVisualStyles();
}
if (Mandatory && UpdateMode == Mode.ForcedDownload)
{
DownloadUpdate();
Exit();
}
else
{
if (Thread.CurrentThread.GetApartmentState().Equals(ApartmentState.STA))
{
ShowUpdateForm();
}
else
{
Thread thread = new Thread(ShowUpdateForm);
thread.CurrentCulture = thread.CurrentUICulture = CultureInfo.CurrentCulture;
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
thread.Join();
}
}
return;
}
else
{
if (ReportErrors)
{
MessageBox.Show(Resources.UpdateUnavailableMessage,
Resources.UpdateUnavailableCaption,
MessageBoxButtons.OK, MessageBoxIcon.Information);
}
}
}
else
{
if (ReportErrors)
{
MessageBox.Show(
Resources.UpdateCheckFailedMessage,
Resources.UpdateCheckFailedCaption, MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
}
}
}
Running = false;
}
/// <summary>
/// Shows standard update dialog.
/// </summary>
public static void ShowUpdateForm()
{
var updateForm = new UpdateForm();
if (UpdateFormSize.HasValue)
{
updateForm.Size = UpdateFormSize.Value;
}
if (updateForm.ShowDialog().Equals(DialogResult.OK))
{
Exit();
}
}
private static void BackgroundWorkerDoWork(object sender, DoWorkEventArgs e)
{
e.Cancel = true;
Assembly mainAssembly = e.Argument as Assembly;
var companyAttribute =
(AssemblyCompanyAttribute)GetAttribute(mainAssembly, typeof(AssemblyCompanyAttribute));
if (string.IsNullOrEmpty(AppTitle))
{
var titleAttribute =
(AssemblyTitleAttribute)GetAttribute(mainAssembly, typeof(AssemblyTitleAttribute));
AppTitle = titleAttribute != null ? titleAttribute.Title : mainAssembly.GetName().Name;
}
string appCompany = companyAttribute != null ? companyAttribute.Company : "";
RegistryLocation = !string.IsNullOrEmpty(appCompany)
? $@"Software\{appCompany}\{AppTitle}\AutoUpdater"
: $@"Software\{AppTitle}\AutoUpdater";
InstalledVersion = mainAssembly.GetName().Version;
WebRequest webRequest = WebRequest.Create(AppCastURL);
webRequest.CachePolicy = new RequestCachePolicy(RequestCacheLevel.NoCacheNoStore);
if (Proxy != null)
{
webRequest.Proxy = Proxy;
}
var uri = new Uri(AppCastURL);
WebResponse webResponse;
try
{
if (uri.Scheme.Equals(Uri.UriSchemeFtp))
{
var ftpWebRequest = (FtpWebRequest)webRequest;
ftpWebRequest.Credentials = FtpCredentials;
ftpWebRequest.UseBinary = true;
ftpWebRequest.UsePassive = true;
ftpWebRequest.KeepAlive = true;
ftpWebRequest.Method = WebRequestMethods.Ftp.DownloadFile;
webResponse = ftpWebRequest.GetResponse();
}
else if (uri.Scheme.Equals(Uri.UriSchemeHttp) || uri.Scheme.Equals(Uri.UriSchemeHttps))
{
HttpWebRequest httpWebRequest = (HttpWebRequest)webRequest;
httpWebRequest.UserAgent = GetUserAgent();
if (BasicAuthXML != null)
{
httpWebRequest.Headers[HttpRequestHeader.Authorization] = BasicAuthXML.ToString();
}
webResponse = httpWebRequest.GetResponse();
}
else
{
webResponse = webRequest.GetResponse();
}
}
catch (Exception exception)
{
Debug.WriteLine(exception);
e.Cancel = false;
return;
}
UpdateInfoEventArgs args;
using (Stream appCastStream = webResponse.GetResponseStream())
{
if (appCastStream != null)
{
if (ParseUpdateInfoEvent != null)
{
using (StreamReader streamReader = new StreamReader(appCastStream))
{
string data = streamReader.ReadToEnd();
ParseUpdateInfoEventArgs parseArgs = new ParseUpdateInfoEventArgs(data);
ParseUpdateInfoEvent(parseArgs);
args = parseArgs.UpdateInfo;
}
}
else
{
XmlDocument receivedAppCastDocument = new XmlDocument { XmlResolver = null };
try
{
receivedAppCastDocument.Load(appCastStream);
XmlNodeList appCastItems = receivedAppCastDocument.SelectNodes("item");
args = new UpdateInfoEventArgs();
if (appCastItems != null)
{
foreach (XmlNode item in appCastItems)
{
XmlNode appCastVersion = item.SelectSingleNode("version");
try
{
CurrentVersion = new Version(appCastVersion?.InnerText);
}
catch (Exception)
{
CurrentVersion = null;
}
args.CurrentVersion = CurrentVersion;
XmlNode appCastChangeLog = item.SelectSingleNode("changelog");
args.ChangelogURL = appCastChangeLog?.InnerText;
XmlNode appCastUrl = item.SelectSingleNode("url");
args.DownloadURL = appCastUrl?.InnerText;
if (Mandatory.Equals(false))
{
XmlNode mandatory = item.SelectSingleNode("mandatory");
Boolean.TryParse(mandatory?.InnerText, out Mandatory);
string mode = mandatory?.Attributes["mode"]?.InnerText;
if (!string.IsNullOrEmpty(mode))
{
UpdateMode = (Mode)Enum.Parse(typeof(Mode), mode);
if (ReportErrors && !Enum.IsDefined(typeof(Mode), UpdateMode))
{
throw new InvalidDataException(
$"{UpdateMode} is not an underlying value of the Mode enumeration.");
}
}
}
args.Mandatory = Mandatory;
args.UpdateMode = UpdateMode;
XmlNode appArgs = item.SelectSingleNode("args");
args.InstallerArgs = appArgs?.InnerText;
XmlNode checksum = item.SelectSingleNode("checksum");
args.HashingAlgorithm = checksum?.Attributes["algorithm"]?.InnerText;
args.Checksum = checksum?.InnerText;
}
}
}
catch (Exception)
{
e.Cancel = false;
webResponse.Close();
return;
}
}
}
else
{
e.Cancel = false;
webResponse.Close();
return;
}
}
if (args.CurrentVersion == null || string.IsNullOrEmpty(args.DownloadURL))
{
webResponse.Close();
if (ReportErrors)
{
throw new InvalidDataException();
}
return;
}
CurrentVersion = args.CurrentVersion;
ChangelogURL = args.ChangelogURL = GetURL(webResponse.ResponseUri, args.ChangelogURL);
DownloadURL = args.DownloadURL = GetURL(webResponse.ResponseUri, args.DownloadURL);
InstallerArgs = args.InstallerArgs ?? String.Empty;
HashingAlgorithm = args.HashingAlgorithm ?? "MD5";
Checksum = args.Checksum ?? String.Empty;
webResponse.Close();
if (Mandatory)
{
ShowRemindLaterButton = false;
ShowSkipButton = false;
}
else
{
using (RegistryKey updateKey = Registry.CurrentUser.OpenSubKey(RegistryLocation))
{
if (updateKey != null)
{
object skip = updateKey.GetValue("skip");
object applicationVersion = updateKey.GetValue("version");
if (skip != null && applicationVersion != null)
{
string skipValue = skip.ToString();
var skipVersion = new Version(applicationVersion.ToString());
if (skipValue.Equals("1") && CurrentVersion <= skipVersion)
return;
if (CurrentVersion > skipVersion)
{
using (RegistryKey updateKeyWrite = Registry.CurrentUser.CreateSubKey(RegistryLocation))
{
if (updateKeyWrite != null)
{
updateKeyWrite.SetValue("version", CurrentVersion.ToString());
updateKeyWrite.SetValue("skip", 0);
}
}
}
}
object remindLaterTime = updateKey.GetValue("remindlater");
if (remindLaterTime != null)
{
DateTime remindLater = Convert.ToDateTime(remindLaterTime.ToString(),
CultureInfo.CreateSpecificCulture("en-US").DateTimeFormat);
int compareResult = DateTime.Compare(DateTime.Now, remindLater);
if (compareResult < 0)
{
e.Cancel = false;
e.Result = remindLater;
return;
}
}
}
}
}
args.IsUpdateAvailable = CurrentVersion > InstalledVersion;
args.InstalledVersion = InstalledVersion;
e.Cancel = false;
e.Result = args;
}
private static string GetURL(Uri baseUri, String url)
{
if (!string.IsNullOrEmpty(url) && Uri.IsWellFormedUriString(url, UriKind.Relative))
{
Uri uri = new Uri(baseUri, url);
if (uri.IsAbsoluteUri)
{
url = uri.AbsoluteUri;
}
}
return url;
}
/// <summary>
/// Detects and exits all instances of running assembly, including current.
/// </summary>
private static void Exit()
{
if (ApplicationExitEvent != null)
{
ApplicationExitEvent();
}
else
{
var currentProcess = Process.GetCurrentProcess();
foreach (var process in Process.GetProcessesByName(currentProcess.ProcessName))
{
string processPath;
try
{
processPath = process.MainModule.FileName;
}
catch (Win32Exception)
{
// Current process should be same as processes created by other instances of the application so it should be able to access modules of other instances.
// This means this is not the process we are looking for so we can safely skip this.
continue;
}
if (process.Id != currentProcess.Id &&
currentProcess.MainModule.FileName == processPath
) //get all instances of assembly except current
{
if (process.CloseMainWindow())
{
process.WaitForExit((int)TimeSpan.FromSeconds(10)
.TotalMilliseconds); //give some time to process message
}
if (!process.HasExited)
{
process.Kill(); //TODO show UI message asking user to close program himself instead of silently killing it
}
}
}
if (IsWinFormsApplication)
{
MethodInvoker methodInvoker = Application.Exit;
methodInvoker.Invoke();
}
#if NETWPF
else if (System.Windows.Application.Current != null)
{
System.Windows.Application.Current.Dispatcher.BeginInvoke(new Action(() =>
System.Windows.Application.Current.Shutdown()));
}
#endif
else
{
Environment.Exit(0);
}
}
}
private static Attribute GetAttribute(Assembly assembly, Type attributeType)
{
object[] attributes = assembly.GetCustomAttributes(attributeType, false);
if (attributes.Length == 0)
{
return null;
}
return (Attribute)attributes[0];
}
internal static string GetUserAgent()
{
return string.IsNullOrEmpty(HttpUserAgent) ? $"AutoUpdater.NET" : HttpUserAgent;
}
internal static void SetTimer(DateTime remindLater)
{
TimeSpan timeSpan = remindLater - DateTime.Now;
var context = SynchronizationContext.Current;
_remindLaterTimer = new System.Timers.Timer
{
Interval = (int)timeSpan.TotalMilliseconds,
AutoReset = false
};
_remindLaterTimer.Elapsed += delegate
{
_remindLaterTimer = null;
if (context != null)
{
try
{
context.Send(state => Start(), null);
}
catch (InvalidAsynchronousStateException)
{
Start();
}
}
else
{
Start();
}
};
_remindLaterTimer.Start();
}
/// <summary>
/// Opens the Download window that download the update and execute the installer when download completes.
/// </summary>
public static bool DownloadUpdate()
{
var downloadDialog = new DownloadUpdateDialog(DownloadURL);
try
{
return downloadDialog.ShowDialog().Equals(DialogResult.OK);
}
catch (TargetInvocationException)
{
}
return false;
}
}
/// <summary>
/// Object of this class gives you all the details about the update useful in handling the update logic yourself.
/// </summary>
public class UpdateInfoEventArgs : EventArgs
{
/// <summary>
/// If new update is available then returns true otherwise false.
/// </summary>
public bool IsUpdateAvailable { get; set; }
/// <summary>
/// Download URL of the update file.
/// </summary>
public string DownloadURL { get; set; }
/// <summary>
/// URL of the webpage specifying changes in the new update.
/// </summary>
public string ChangelogURL { get; set; }
/// <summary>
/// Returns newest version of the application available to download.
/// </summary>
public Version CurrentVersion { get; set; }
/// <summary>
/// Returns version of the application currently installed on the user's PC.
/// </summary>
public Version InstalledVersion { get; set; }
/// <summary>
/// Shows if the update is required or optional.
/// </summary>
public bool Mandatory { get; set; }
/// <summary>
/// Defines how the Mandatory flag should work.
/// </summary>
public Mode UpdateMode { get; set; }
/// <summary>
/// Command line arguments used by Installer.
/// </summary>
public string InstallerArgs { get; set; }
/// <summary>
/// Checksum of the update file.
/// </summary>
public string Checksum { get; set; }
/// <summary>
/// Hash algorithm that generated the checksum provided in the XML file.
/// </summary>
public string HashingAlgorithm { get; set; }
}
/// <summary>
/// An object of this class contains the AppCast file received from server.
/// </summary>
public class ParseUpdateInfoEventArgs : EventArgs
{
/// <summary>
/// Remote data received from the AppCast file.
/// </summary>
public string RemoteData { get; }
/// <summary>
/// Set this object with values received from the AppCast file.
/// </summary>
public UpdateInfoEventArgs UpdateInfo { get; set; }
/// <summary>
/// An object containing the AppCast file received from server.
/// </summary>
/// <param name="remoteData">A string containing remote data received from the AppCast file.</param>
public ParseUpdateInfoEventArgs(string remoteData)
{
RemoteData = remoteData;
}
}
/// <summary>
/// Interface for authentication
/// </summary>
public interface IAuthentication
{
}
/// <summary>
/// Provides Basic Authentication header for web request.
/// </summary>
public class BasicAuthentication : IAuthentication
{
private string Username { get; }
private string Password { get; }
/// <summary>
/// Initializes credentials for Basic Authentication.
/// </summary>
/// <param name="username">Username to use for Basic Authentication</param>
/// <param name="password">Password to use for Basic Authentication</param>
public BasicAuthentication(string username, string password)
{
Username = username;
Password = password;
}
/// <inheritdoc />
public override string ToString()
{
var token = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{Username}:{Password}"));
return $"Basic {token}";
}
}
/// <summary>
/// Provides Custom Authentication header for web request.
/// </summary>
public class CustomAuthentication : IAuthentication
{
private string HttpRequestHeaderAuthorizationValue { get; }
/// <summary>
/// Initializes authorization header value for Custom Authentication
/// </summary>
/// <param name="httpRequestHeaderAuthorizationValue">Value to use as http request header authorization value</param>
public CustomAuthentication(string httpRequestHeaderAuthorizationValue)
{
HttpRequestHeaderAuthorizationValue = httpRequestHeaderAuthorizationValue;
}
/// <inheritdoc />
public override string ToString()
{
return HttpRequestHeaderAuthorizationValue;
}
}
}