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 { /// /// Enum representing the remind later time span. /// public enum RemindLaterFormat { /// /// Represents the time span in minutes. /// Minutes, /// /// Represents the time span in hours. /// Hours, /// /// Represents the time span in days. /// Days } /// /// Enum representing the effect of Mandatory flag. /// public enum Mode { /// /// In this mode, it ignores Remind Later and Skip values set previously and hide both buttons. /// Normal, /// /// In this mode, it won't show close button in addition to Normal mode behaviour. /// Forced, /// /// In this mode, it will start downloading and applying update without showing standarad update dialog in addition to Forced mode behaviour. /// ForcedDownload } /// /// Main class that lets you auto update applications by setting some static fields and executing its Start method. /// 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; /// /// Set it to folder path where you want to download the update file. If not provided then it defaults to Temp folder. /// public static String DownloadPath; /// /// 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. /// public static String AppTitle; /// /// URL of the xml file that contains information about latest version of the application. /// public static String AppCastURL; /// /// Login/password/domain for FTP-request /// public static NetworkCredential FtpCredentials; /// /// Opens the download URL in default browser if true. Very usefull if you have portable application. /// public static bool OpenDownloadPage; /// /// Set Basic Authentication credentials required to download the file. /// public static IAuthentication BasicAuthDownload; /// /// Set Basic Authentication credentials required to download the XML file. /// public static IAuthentication BasicAuthXML; /// /// Set Basic Authentication credentials to navigate to the change log URL. /// public static IAuthentication BasicAuthChangeLog; /// /// Set the User-Agent string to be used for HTTP web requests. /// public static string HttpUserAgent; /// /// If this is true users can see the skip button. /// public static Boolean ShowSkipButton = true; /// /// If this is true users can see the Remind Later button. /// public static Boolean ShowRemindLaterButton = true; /// /// 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. /// public static Boolean LetUserSelectRemindLater = true; /// /// Remind Later interval after user should be reminded of update. /// public static int RemindLaterAt = 2; /// /// AutoUpdater.NET will report errors if this is true. /// public static bool ReportErrors = false; /// /// Set this to false if your application doesn't need administrator privileges to replace the old version. /// public static bool RunUpdateAsAdmin = true; /// /// 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. /// public static bool Mandatory; /// /// Set this to any of the available modes to change behaviour of the Mandatory flag. /// public static Mode UpdateMode; /// /// Set Proxy server to use for all the web requests in AutoUpdater.NET. /// public static IWebProxy Proxy; /// /// Set if RemindLaterAt interval should be in Minutes, Hours or Days. /// public static RemindLaterFormat RemindLaterTimeSpan = RemindLaterFormat.Days; /// /// A delegate type to handle how to exit the application after update is downloaded. /// public delegate void ApplicationExitEventHandler(); /// /// An event that developers can use to exit the application gracefully. /// public static event ApplicationExitEventHandler ApplicationExitEvent; /// /// A delegate type for hooking up update notifications. /// /// 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. public delegate void CheckForUpdateEventHandler(UpdateInfoEventArgs args); /// /// An event that clients can use to be notified whenever the update is checked. /// public static event CheckForUpdateEventHandler CheckForUpdateEvent; /// /// A delegate type for hooking up parsing logic. /// /// An object containing the AppCast file received from server. public delegate void ParseUpdateInfoHandler(ParseUpdateInfoEventArgs args); /// /// An event that clients can use to be notified whenever the AppCast file needs parsing. /// public static event ParseUpdateInfoHandler ParseUpdateInfoEvent; /// /// Set if you want the default update form to have a different size. /// public static Size? UpdateFormSize = null; /// /// Start checking for new version of application and display dialog to the user if update is available. /// /// Assembly to use for version checking. public static void Start(Assembly myAssembly = null) { Start(AppCastURL, myAssembly); } /// /// Start checking for new version of application via FTP and display dialog to the user if update is available. /// /// FTP URL of the xml file that contains information about latest version of the application. /// Credentials required to connect to FTP server. /// Assembly to use for version checking. public static void Start(String appCast, NetworkCredential ftpCredentials, Assembly myAssembly = null) { FtpCredentials = ftpCredentials; Start(appCast, myAssembly); } /// /// Start checking for new version of application and display dialog to the user if update is available. /// /// URL of the xml file that contains information about latest version of the application. /// Assembly to use for version checking. 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; } /// /// Shows standard update dialog. /// 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; } /// /// Detects and exits all instances of running assembly, including current. /// 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(); } /// /// Opens the Download window that download the update and execute the installer when download completes. /// public static bool DownloadUpdate() { var downloadDialog = new DownloadUpdateDialog(DownloadURL); try { return downloadDialog.ShowDialog().Equals(DialogResult.OK); } catch (TargetInvocationException) { } return false; } } /// /// Object of this class gives you all the details about the update useful in handling the update logic yourself. /// public class UpdateInfoEventArgs : EventArgs { /// /// If new update is available then returns true otherwise false. /// public bool IsUpdateAvailable { get; set; } /// /// Download URL of the update file. /// public string DownloadURL { get; set; } /// /// URL of the webpage specifying changes in the new update. /// public string ChangelogURL { get; set; } /// /// Returns newest version of the application available to download. /// public Version CurrentVersion { get; set; } /// /// Returns version of the application currently installed on the user's PC. /// public Version InstalledVersion { get; set; } /// /// Shows if the update is required or optional. /// public bool Mandatory { get; set; } /// /// Defines how the Mandatory flag should work. /// public Mode UpdateMode { get; set; } /// /// Command line arguments used by Installer. /// public string InstallerArgs { get; set; } /// /// Checksum of the update file. /// public string Checksum { get; set; } /// /// Hash algorithm that generated the checksum provided in the XML file. /// public string HashingAlgorithm { get; set; } } /// /// An object of this class contains the AppCast file received from server. /// public class ParseUpdateInfoEventArgs : EventArgs { /// /// Remote data received from the AppCast file. /// public string RemoteData { get; } /// /// Set this object with values received from the AppCast file. /// public UpdateInfoEventArgs UpdateInfo { get; set; } /// /// An object containing the AppCast file received from server. /// /// A string containing remote data received from the AppCast file. public ParseUpdateInfoEventArgs(string remoteData) { RemoteData = remoteData; } } /// /// Interface for authentication /// public interface IAuthentication { } /// /// Provides Basic Authentication header for web request. /// public class BasicAuthentication : IAuthentication { private string Username { get; } private string Password { get; } /// /// Initializes credentials for Basic Authentication. /// /// Username to use for Basic Authentication /// Password to use for Basic Authentication public BasicAuthentication(string username, string password) { Username = username; Password = password; } /// public override string ToString() { var token = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{Username}:{Password}")); return $"Basic {token}"; } } /// /// Provides Custom Authentication header for web request. /// public class CustomAuthentication : IAuthentication { private string HttpRequestHeaderAuthorizationValue { get; } /// /// Initializes authorization header value for Custom Authentication /// /// Value to use as http request header authorization value public CustomAuthentication(string httpRequestHeaderAuthorizationValue) { HttpRequestHeaderAuthorizationValue = httpRequestHeaderAuthorizationValue; } /// public override string ToString() { return HttpRequestHeaderAuthorizationValue; } } }