Click here to Skip to main content
15,885,757 members
Articles / Programming Languages / C#

Auto Updatable Application

Rate me:
Please Sign up or sign in to vote.
4.88/5 (15 votes)
29 Jul 2011CPOL5 min read 53.3K   3.9K   95   8
Explains how a .NET application can update its own binaries and dependencies

Introduction

Some applications may need to update themselves periodically. Typically the applications which are designed to be highly extensible usually may update few modules and thus can get new capabilities and functionality dynamically, just like plugins.

.NET applications can take advantage of loading updatable modules in separate AppDomains and unload them when update is available and reload them with updated assemblies. One problem that you may encounter is that the assembly files which are loaded get locked and this prevents replacing them. If you are using separate AppDomains to load assemblies, then the solution is easier. You can enable shadow copy feature for AppDomain which actually makes a copy of the assembly before loading, thus original assemblies remain unlocked and you can replace them while the application is running.

If you are not using a separate AppDomain and you want to update the assemblies including your application EXE, then you need to do some tricky things as explained in the rest of the article.

Background

I wanted to basically run the application and let the application update its own EXE and other assemblies from some remote location. This requires starting the application in such a way that it do not lock its assemblies as normally happens when an application starts. If the application can update itself, distribution of application can be done from a centralized location. I am not including any logic to download the assemblies in this article.

Using the Code

I found two solutions for the problem as below.

Solution One

Here I have used an additional application (i.e., Starter.exe) along with the main application. The Starter.exe should be present in the application's start up directory. If you are downloading code, then make sure that you copy the Startup.exe after building.

The code for Solution one is as below:

C#
internal static class Program
    {
        private static readonly string DomainName = "MarvinDomain";
        private static readonly string CatchFolder = "AssemblyCatch";

    
        [STAThread]
        private static void Main()
        {
if (AppDomain.CurrentDomain.FriendlyName != DomainName)
{
    string exeFilePath = Assembly.GetExecutingAssembly().Location;
    string applicationFolder = Path.GetDirectoryName(exeFilePath);

    string starterExePath = applicationFolder + "\\Starter.exe";
    //Debugger.Launch(); // You can uncomment to automatically launch VS for debugging
    //Solution one - Using Separate EXE to start application.
    if (File.Exists(starterExePath)) // Check if starter EXE is available
    {
        XmlSerializer serialier = new XmlSerializer(typeof(AppDomainSetup));
        string xmlFilePath = applicationFolder + "\\XmlData.xml";
        using (var stream = File.Create(xmlFilePath))
        {
	serialier.Serialize(stream, AppDomain.CurrentDomain.SetupInformation);
        }

        exeFilePath = exeFilePath.Replace(" ", "*");
        xmlFilePath = xmlFilePath.Replace(" ", "*");
        string args = exeFilePath + " " + xmlFilePath;

        Process starter = Process.Start(starterExePath, args);
    }

In Main method, the application checks for the FriendlyName of current AppDomain if it’s matching to a specific name (i.e. MarvinDomain) .This can be anything that you want.

If the name is not matching, then the application checks if Starter.exe is available in its startup path. If it gets this EXE, it starts the new process and that will launch the Starter.exe. While launching, it passes two string arguments to the new process:

  1. Its own EXE path
  2. XML File path.

The application serializes AppDomainSetup for current AppDomain to XML file and passes it as a second string argument.

Note that I have replaced the spaces in the file path with star because if space is there, it generates additional arguments.

Now when Starter.exe runs, it receives the EXE path of our main application and path of XML file which contains serialized AppDomainSetup from the st<code>ring arguments in its Main method. It then creates the new AppDomain and enables shadow copy feature to this newly created domain. To do so, it makes use deserialized AppDomainSetup and changes the relevant properties. If deserialized AppDomainSetup is not available, it creates a new one. It then runs the application, i.e., our main application in this new domain.

Code for Starter Application

C#
private static readonly string DomainName = "MarvinDomain";
        private static AppDomainSetup setup;

        private static void Main(string[] args)
        {
string executablePath = string.Empty; 
           //  Debugger.Launch();       	// You can uncomment to automatically 
					// launch VS for debugging
if (args.Length > 0)         
{
    executablePath = args[0];
    executablePath = executablePath.Replace("*", " ");
    if (args.Length > 1)
    {
        string xmlFilePath = args[1];
        xmlFilePath = xmlFilePath.Replace("*", " ");
        if (File.Exists(xmlFilePath))
        {
XmlSerializer serializer = new XmlSerializer(typeof(AppDomainSetup));
using (var stream = File.Open(xmlFilePath, FileMode.Open, FileAccess.Read))
{
    setup = serializer.Deserialize(stream) as AppDomainSetup;
}
File.Delete(xmlFilePath);
        }
    }
}
if (File.Exists(executablePath))
{
    AppDomainSetup appDomainShodowCopySetup = AppDomain.CurrentDomain.SetupInformation;

    if (setup != null)
    {
        appDomainShodowCopySetup = setup;
    }

    appDomainShodowCopySetup.ShadowCopyFiles = true.ToString();

    appDomainShodowCopySetup.CachePath = Path.GetDirectoryName
					(executablePath) + "\\AssemblyCatch";

    AppDomain marvinDomain = AppDomain.CreateDomain
				(DomainName, null, appDomainShodowCopySetup);

    marvinDomain.ExecuteAssembly(executablePath);

    AppDomain.Unload(marvinDomain);
}
}
}

When application runs in new domain which has the expected name, this time our main Application will do its normal job, like running the MainForm instead of looking for Starter.exe.

To check the demo, download the binaries and click UpgradableApplication.exe which is a WinForm application.You will note that an additional catch folder for assemblies will be created inside startup folder and all assemblies will be copied to this folder. To make sure that our UpgradableApplication.exe is not locked, you can delete the file while the application is running.

Solution Two

Though solution one works, it requires additional Startup.exe and if you look into Windows Task Manager you will see the name of Starter.exe instead of our main EXE.

The demo application uses solution two if it does not find Starter.exe. So to check the demo for this solution, you can just rename Starter.exe to something else and the second approach will be used. The second approach is not radically different. The code is as below:

C#
if (!applicationFolder.EndsWith(CatchFolder))
        {
string copyDirectoryPath = applicationFolder + "\\" + CatchFolder;
if (!Directory.Exists(copyDirectoryPath))
{
    Directory.CreateDirectory(copyDirectoryPath);
}
DateTime now = DateTime.Now;
string dateTimeStr = now.Date.Day.ToString() + now.Month.ToString() 
         + now.Second.ToString() + now.Millisecond.ToString();
string copyExePath = copyDirectoryPath + "\\CopyOf" +
          Path.GetFileNameWithoutExtension(exeFilePath)
         + dateTimeStr + ".exe";
           
File.Copy(exeFilePath, copyExePath,false );

Process.Start(copyExePath);
return;
        }

        Thread mainThread = new Thread(() =>
         {
 // Debugger.Launch();    // You can uncomment to automatically launch VS for debugging
 AppDomainSetup appDomainShodowCopySetup = AppDomain.CurrentDomain.SetupInformation;

 appDomainShodowCopySetup.ShadowCopyFiles = true.ToString();
 appDomainShodowCopySetup.ApplicationBase = applicationFolder.Replace(CatchFolder, "");
 appDomainShodowCopySetup.CachePath = 
	Path.GetDirectoryName(exeFilePath) + "\\" + CatchFolder;
 //Configure shadow copy directories 
 //appDomainShodowCopySetup.ShadowCopyDirectories = "C:\\DllsToBeShadowCopyied";
 AppDomain marvinDomain = AppDomain.CreateDomain
			(DomainName, null, appDomainShodowCopySetup);

 marvinDomain.ExecuteAssembly(exeFilePath);

 AppDomain.Unload(marvinDomain);
         });

        mainThread.Start();
    }
    return;
}

This approach also makes use of FriendlyName of the current AppDomain along with startup path of application. Application checks its startup path if it is not from a catch path, then it copies its EXE to AssemblyCatch folder and launches the new process that uses EXE from new path. I have used the system date time to give a unique name to the copied EXE file. When new process is launched this time, it's from catch path, now this time the application will create a new domain with shadow copy feature enabled and will launch the application in this new domain. When application runs in new domain, it has valid Friendly name for domain as well as running form catch path so this time it will do its normal job of running a MainForm.

I am using a new thread for creating a new AppDomain since I encountered a problem to unload AppDomain on default Main thread.

It's up to the application developer how to get updates and when. Application may keep checking periodically for updates in background and if it finds, it will download. Application can inform user to restart application or if possible it can restart itself and get new changes.You can specify the folders from which the assemblies will be copied by using property ShadowCopyDirectories of AppDomainSetup. To prevent restarting the entire application, you can also design an application that will update only few modules which can be in separate domain and can be unloaded and reloaded once update is available.

Points of Interest

Here I have managed to make enable shadow copy to default application domain. As a result, the original application assemblies remains unlocked and those can be replaced by application itself. With shadow copying for few modules, you can make sure that your application can continue to run forever without restart and is still updated.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer Honeywell Automation India Ltd.
India India
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionDoes it work with mixed dlls of C# and C++ Pin
Chandra Mohan BS27-Oct-15 22:35
Chandra Mohan BS27-Oct-15 22:35 
GeneralMy vote of 5 Pin
johannesnestler11-Jul-14 4:04
johannesnestler11-Jul-14 4:04 
GeneralA simpler (?) solution Pin
ShdNx1-Aug-11 9:28
ShdNx1-Aug-11 9:28 
GeneralRe: A simpler (?) solution Pin
Prafulla Hunde1-Aug-11 19:35
Prafulla Hunde1-Aug-11 19:35 
GeneralRe: A simpler (?) solution Pin
kiquenet.com4-Aug-11 0:45
professionalkiquenet.com4-Aug-11 0:45 
GeneralRe: A simpler (?) solution Pin
Prafulla Hunde4-Aug-11 2:58
Prafulla Hunde4-Aug-11 2:58 
AnswerRe: A simpler (?) solution Pin
ShdNx11-Aug-11 0:22
ShdNx11-Aug-11 0:22 
Hey,

It should be pretty simple, something like this: (the actual code is under NDA)

C#
private static readonly string _relocationFolder = Environment.GetFolderPath(Environment.SpecialFolder.Temp);
private static readonly string _relocateFiles = new string[]
{
     Application.ExecutablePath,
     // any other files to be relocated with the application, e.g. dlls
     // NOTE that this code doesn't support files in subdirectories, you have to modify the code for that!
};

static void Main(string[] args)
{
     if(Application.StartupPath == _relocationFolder)
     {
         // lets wait for the parent process to exit - you may not have to do this in your application, but in mine, it was imperative that only one instance was running at a time
         if(args.Length > 0 && args[0] == "-reloc")
         {
             int parentProcId = Int32.Parse(args[1]);

             var parentProc = Process.GetProcessById(parentProcId);
             if(parentProc != null)
             {
                 parentProc.WaitForExit(); // you may want to add some timeout here, in case the parent process doesn't exit for some reason
             }
         }

         // great, relocation is completed, the parent process exited, so you're free to go
     }
     else
     {
         // lets do the relocation
         foreach(string sourceFile in _relocateFiles)
         {
              string destFile = Path.Combine(_relocationFolder, Path.GetFileName(sourceFile));

              if(File.Exists(destFile))
              {
                   // I have encountered a weird bug one day, on one of the client's computer
                   // I was using File.Copy(sourceFile, destFile, true) to allow the files to be overwritten
                   // on his machine, the above call did NOT overwrite the files, and just failed silently (i.e. no exception) - and the files were never overwritten, unless I did an explicit File.Delete beforehand
                   File.Delete(destFile);
              }
              File.Copy(sourceFile, destFile);
         }

         // start the child process
         var childProc = new Process();
         childProc.StartInfo.FileName = Path.Combine(_relocationFolder, Path.GetFileName(Application.ExecutablePath));
         childProc.StartInfo.Arguments = "-reloc " + Process.GetCurrentProcess().Id;
         childProc.StartInfo.WorkingDirectory = Application.StartupPath;
         childProc.Start();

         // all done, exit
         return;
     }

     // your normal code can go here: the execution will only get this far if the application is running from the relocated location
     // the application working directory is the same as it would have been on the original location, so paths are relative to that
     // you can get/set your current working directory with Environment.CurrentDirectory
}


Note that the above code works on WinForms, but not with console or WPF applications - you'll have to modify it to suit those, but it should be trivial.
Also you can see that there's no exception handling, which you might want to add.
I wrote it without IntelliSense, so there may be some member names I didn't remember correctly - you should have no trouble finding out which one is correct, though. Similarly, the code is not tested, I wrote it from memory.
GeneralMy Own Solution Pin
Matt U.29-Jul-11 17:31
Matt U.29-Jul-11 17:31 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.