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:
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";
if (File.Exists(starterExePath)) {
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:
- Its own EXE path
- 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
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
private static readonly string DomainName = "MarvinDomain";
private static AppDomainSetup setup;
private static void Main(string[] args)
{
string executablePath = string.Empty;
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:
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.