Asynchronous C # Program Update

    Good day, friends!

    In my previous articles ( once and twice ) I wrote about the implementation of the automatic update function of the program and having many shortcomings, it was decided to improve it, as well as to make the code more “friendly”, or something. By shortening the lines and optimizing the format, I was able to achieve better asynchronous file downloads, practically eliminating the possibility of spoofing the update file (checking the checksum), and also added several new developments. There I am making another attempt to rehabilitate myself.

    image

    In my work, my program uses the following files located in the same folder as the executable file:
    • Ionic.Zip.dll - implementation of archiving debug files;
    • LanguagePack.dll - its own library containing a translation of the name of the form elements in the desired language;
    • Newtonsoft.Json.dll - JSON library;
    • ProcessesLibrary.dll - its own library containing a list of processes;
    • restart.exe - utility to restart the main application;
    • updater.exe - utility for updating the main application
    • settings.xml - settings file.

    In previous versions of the code, each file was downloaded separately, which caused a lot of inconvenience, starting from the waiting time for the download. Also, there was no function for checking the checksum, which did not very well affect the safety of their use.
    So what has changed in the code that I decided to write a third article about the same story?

    First , the version.xml file located on the server underwent changes :

    1.0.7.881.0.0.71.0.1.91.0.1.101.0.0.46.0.1.170011.9.1.8


    Changes


    As you managed to notice, in comparison with its previous variant, the checksumm attribute was added , containing just the MD5 sum of a specific file.

    When using the code, as unnecessary, the components of the backgroundWorker class were removed in favor of Task , and the following lines were added to the class definition:

    debug debug = new debug();
    private string url = @"http://mysite/";
    private ProgressBar downloadPercent = null;
    

    The debug class writes errors to a file for better logging. But in this article we will not talk about him.
    The url string parameter sets the path to the folder on the site that contains all our files. Prior to this, the given path for each file was registered - but in vain.
    The downloadPercent component from the ProgressBar class is used to display the percentage of download updates for the main program file.

    Next, the function to start the Check () update process was reduced to the following:

    public void Check(bool launcher = false, ProgressBar report = null)
    {
    	try
    	{
    		XmlDocument doc = new XmlDocument();
    		doc.Load(url + "version.xml");
    		if (!File.Exists("settings.xml"))
    		{
    			using (var client = new WebClient())
    			Task.Factory.StartNew(() => client.DownloadFile(new Uri(url + "settings.xml"), "settings.xml")).Wait();
    		}
    		// Если файлы имеют нулевой размер, то удаляем их
    		if (File.Exists("settings.xml") && new FileInfo("settings.xml").Length == 0) { File.Delete("settings.xml"); }
    		if (File.Exists("Ionic.Zip.dll") && new FileInfo("Ionic.Zip.dll").Length == 0) { File.Delete("Ionic.Zip.dll"); }
    		if (File.Exists("restart.exe") && new FileInfo("restart.exe").Length == 0) { File.Delete("restart.exe"); }
    		if (File.Exists("updater.exe") && new FileInfo("updater.exe").Length == 0) { File.Delete("updater.exe"); }
    		if (File.Exists("Newtonsoft.Json.dll") && new FileInfo("Newtonsoft.Json.dll").Length == 0) { File.Delete("Newtonsoft.Json.dll"); }
    		if (File.Exists("ProcessesLibrary.dll") && new FileInfo("ProcessesLibrary.dll").Length == 0) { File.Delete("ProcessesLibrary.dll"); }
    		if (File.Exists("LanguagePack.dll") && new FileInfo("LanguagePack.dll").Length == 0) { File.Delete("LanguagePack.dll"); }
    		if (File.Exists("launcher.update") && new FileInfo("launcher.update").Length == 0) { File.Delete("launcher.update"); }
    		if (!launcher)
    		{
    			var task1 = Task.Factory.StartNew(() => DownloadFile("Ionic.Zip.dll", doc.GetElementsByTagName("Ionic.Zip")[0].InnerText, doc.GetElementsByTagName("Ionic.Zip")[0].Attributes["checksumm"].InnerText));
    			var task2 = Task.Factory.StartNew(() => DownloadFile("restart.exe", doc.GetElementsByTagName("restart")[0].InnerText, doc.GetElementsByTagName("restart")[0].Attributes["checksumm"].InnerText));
    			var task6 = Task.Factory.StartNew(() => DownloadFile("LanguagePack.dll", doc.GetElementsByTagName("languagePack")[0].InnerText, doc.GetElementsByTagName("languagePack")[0].Attributes["checksumm"].InnerText));
    			Task.WaitAll(task1, task2, task6);
    		}
    

    Now about everything in more detail.
    At the very beginning, we check if the program settings file ( settings.xml ) exists and if it does not exist, download it
    Next (sometimes it happened), if the files are of zero length, we also delete them. Why do we need broken files. Right, right?
    After that, there is a check whether the launcher parameter was set when the function was initialized. It is needed to determine the sequence of code execution, as well as to optimize the solution, since only 3 files from the above list are required when initializing the form of the main window. If launcher is false, then we download the main files (Ionic.Zip.dll, LanguagePack.dll, restart.exe), and then initialize the main program code.

    To check for updates of the main program and auxiliary files, in the code of the main form in the public form1 () handler after calling the InitializeComponent () function ; add a call to our update class. Yes, class, since all of its code is placed separately (for convenience).

    update_launcher update = new update_launcher();
    update.Check(true, pbDownload);
    

    In the call update.Check (true, progressBar1); we, as the first parameter, we indicate that now updates of auxiliary files and updates of the main application file will be checked. As the second, we point to progressBar, which is responsible for displaying the percent load of the main file.

    Since we specified the parameter launcher = true , the program will execute the following code from the Check () function (continuation of the code indicated above):

    		else
    		{
    			try
    			{
    				var task3 = Task.Factory.StartNew(() => DownloadFile("updater.exe", doc.GetElementsByTagName("updater")[0].InnerText, doc.GetElementsByTagName("updater")[0].Attributes["checksumm"].InnerText));
    				var task4 = Task.Factory.StartNew(() => DownloadFile("Newtonsoft.Json.dll", doc.GetElementsByTagName("Newtonsoft.Json")[0].InnerText, doc.GetElementsByTagName("Newtonsoft.Json")[0].Attributes["checksumm"].InnerText));
    				var task5 = Task.Factory.StartNew(() => DownloadFile("ProcessesLibrary.dll", doc.GetElementsByTagName("processesLibrary")[0].InnerText, doc.GetElementsByTagName("processesLibrary")[0].Attributes["checksumm"].InnerText));
    				Task.WaitAll(task3, task4, task5);
    				if (File.Exists("launcher.update") && new Version(FileVersionInfo.GetVersionInfo("launcher.update").FileVersion) > new Version(Application.ProductVersion))
    				{
    					Process.Start("updater.exe", "launcher.update \"" + Application.ProductName + ".exe\"");
    					Process.GetCurrentProcess().CloseMainWindow();
    				}
    				else if (new Version(Application.ProductVersion) < new Version(doc.GetElementsByTagName("version")[0].InnerText))
    				{
    					if (report != null)
    					{
    						downloadPercent = report;
    						downloadPercent.Value = 0;
    					}
    					Task.Factory.StartNew(() => DownloadFile("launcher.exe", doc.GetElementsByTagName("version")[0].InnerText, doc.GetElementsByTagName("version")[0].Attributes["checksumm"].InnerText, "launcher.update", true)).Wait();
    				}
    				else if (File.Exists("launcher.update")) { File.Delete("launcher.update"); }
    			}
    			catch (Exception ex1)
    			{
    				debug.Save("public void Check(bool launcher = false)", "launcher.update", ex1.Message);
    			}
    		}
    	}
    	catch (Exception ex)
    	{
    		debug.Save("public void Check(bool launcher = false)", "", ex.Message);
    	}
    }
    

    What do we have here. Not forgetting to add using System.Threading.Tasks; , we initialize the Task object by assigning them variable names ( task3 , task4 , task5 ).
    For those who are not in the know, the Task class is a wrapper over threads for performing asynchronous operations, giving the developer the opportunity to forget how to create a thread, start it and destroy it at the end.
    In general, in our case, we set the DownloadFile () function as a parameter ; passing it the necessary parameters, namely:

    private void DownloadFile(string filename, string xmlVersion, string xmlChecksumm, string localFile = null, bool showStatus = false)
    

    Where:

    • filename - the name of the file located on the server;
    • xmlVersion - the version of the file on the server (read from version.xml );
    • xmlChecksumm - a string containing the checksum of the file (read from version.xml );
    • localFile - an optional parameter, needed if the file stored locally differs in name from that located on the site (in our case it is used only when downloading updates to the main application file);
    • showStatus is an optional parameter used to determine whether the file upload status will be displayed in progressBar. Used only in conjunction with the localFile parameter .

    Function Task.Factory.StartNew (); allows you to run any processes asynchronously. In order to determine when all the files were downloaded, the Task.WaitAll function (task3, task4, task5) was used; , awaiting completion of code execution in all specified elements.
    So, after downloading additional files, you can go to check for updates of the main one, and since updates can already be downloaded, we first check the existence and version of the local update file, if it exists.

    if (File.Exists("launcher.update") && new Version(FileVersionInfo.GetVersionInfo("launcher.update").FileVersion) > new Version(Application.ProductVersion))
    

    Why there is no function for checking the checksum, I will tell you a little later, but for now let's get back to this one.
    If the update file (“launcher.update”) exists and its version is more recent, then we run the additional updater.exe utility by passing the file names in the parameter (see code above).

    Further, if the update file is missing or its version is not fresh, then we use the following correspondence check, where we check the software version with the version on the site:

    if (new Version(Application.ProductVersion) < new Version(doc.GetElementsByTagName("version")[0].InnerText))
    

    If a more recent version is found, then proceed to download it. And again about this later.
    The third condition is valid when the first two are not fulfilled, namely, if the file is present and has an older version, then we simply delete it.

    Function call debug.Save (); saves information about the error handler to a file so that you can read it later. For software updates, this code does not matter much, but is placed so that people do not ask "why do you have catch (Exception) {} , it's not comme il faut." Here it is.
    Move on.

    Download


    The private function DownloadFile () is responsible for downloading files ; having a set of parameters described above, and her cat code you can see below:

    private void DownloadFile(string filename, string xmlVersion, string xmlChecksumm, string localFile = null, bool showStatus = false)
    {
    	localFile = localFile != null ? localFile : filename;
    	if (File.Exists(localFile) && new FileInfo(localFile).Length == 0) { File.Delete(localFile); }
    	try
    	{
    		if ((File.Exists(localFile) && new Version(FileVersionInfo.GetVersionInfo(localFile).FileVersion) < new Version(xmlVersion)) || !File.Exists(localFile))
    		{
    			using (var client = new WebClient())
    			{
    				try
    				{
    					if (showStatus && downloadPercent != null) { client.DownloadProgressChanged += new DownloadProgressChangedEventHandler(ProgressChanged); }
    					client.DownloadFileAsync(new Uri(url + filename), localFile);
    					if (!Checksumm(localFile, xmlChecksumm) && File.Exists(localFile)) { File.Delete(localFile); }
    				}
    				catch (Exception ex)
    				{
    					debug.Save("private void DownloadFile(string filename, string xmlVersion, string xmlChecksumm)", "Filename: " + filename + Environment.NewLine + "Localname: " + (localFile != null ? localFile : "null") + Environment.NewLine + "URL: " + url, ex.Message);
    				}
    			}
    		}
    	}
    	catch (Exception ex1)
    	{
    		debug.Save("private void DownloadFile(string filename, string xmlVersion, string xmlChecksumm)", "Filename: " + filename + Environment.NewLine + "Localname: " + (localFile != null ? localFile : "null") + Environment.NewLine + "URL: " + url, ex1.Message);
    	}
    }
    

    At the very beginning, the passed value is checked in the localFile parameter and if the parameter is null , then we assign the value of the filename parameter to it . After that there is a check on the file length of the size and if it is zero, then remove it.
    Next, the key part of the function begins - we check the existence of the file and the relevance of its version and if the file is missing or a new version is found, then we proceed to download, otherwise, we skip accordingly.
    Immediately before downloading, we check the showStatus parameter , which we need to enable / disable the display of the download status. I will consider an example when the status is needed. So, if the showStatus parameteris not null And the downloadPercent parameter is set , then we connect the ProgressChanged () function to the client object of the WebClient () class ; to track download status.
    Next is the process of asynchronously downloading the DownloadFileAsync () file . File downloaded, what next?
    And then we check the checksum of the downloaded file with the value on the site in the version.xml file through the Checksumm () function in the parameters of which the name of the local file and the string containing the md5 cache from the site are transmitted.

    private bool Checksumm(string filename, string summ)
    {
    	try
    	{
    		if (File.Exists(filename) && summ != null && new FileInfo(filename).Length > 0)
    			using (FileStream fs = File.OpenRead(filename))
    			{
    				System.Security.Cryptography.MD5 md5 = new System.Security.Cryptography.MD5CryptoServiceProvider();
    				byte[] fileData = new byte[fs.Length];
    				fs.Read(fileData, 0, (int)fs.Length);
    				byte[] checkSumm = md5.ComputeHash(fileData);
    				return BitConverter.ToString(checkSumm) == summ.ToUpper() ? true : false;
    			}
    			else
    				return false;
    	}
    	catch (Exception ex)
    	{
    		debug.Save("private bool Checksumm(string filename, string summ)", "Filename: " + filename, ex.Message);
    		return false;
    	}
    }
    

    If the checksum of the local file matches the value on the site, the function returns true , otherwise by the action of the logic.
    But what about DownloadFile () ? If the checksum is correct, then we end the function, if not, delete the file.

    What has not yet been indicated? Um ... Oh yes! Download status processing function:

    Private void ProgressChanged(object sender, DownloadProgressChangedEventArgs e)
    {
    	downloadPercent.Value = e.ProgressPercentage;
    }
    

    And, I almost forgot, in the specified code, the update of the main program is downloaded to the launcher.update file . The application of the update will be carried out automatically the next time the program is launched, that is, the user will not receive any notifications, which, in my opinion, increases the program’s “friendliness”.

    Conclusion


    Here, in fact, the entire update code, and given that it is placed in a separate class, it can easily be used in any kind of project, indicating its sources of updates and the number and names of files.
    PS: in the debug class, when saving, 3 parameters are set - it is easier to search for the right place in the code by them.

    If anyone needs it, since November 2017 the repository has been posted HERE .

    Regards, Andrew Helldar!

    Also popular now: