Introduction
I was playing the role of a consultant in a small software house for almost a year. The software house developed large-scale health care software for a corporate health care establishment. The software was architected as a true three-tier model. The business tier (middle tier we can say) was written in J2EE (using couple of fancy frameworks like Spring, Hibernate etc) and was hosted in a Web Logic servlet container. And the client tier was written in C# using Windows Form API. The communication protocol between these two tiers is SOAP Web Service.
The deployment process was a big deal because they needed to distribute the client application into over 350 machines (the client terminals) – although it was an XCopy deployment. Well, this is a known and old pain of desktop application deployment. But the software is in a maintenance mode, and naturally, the bugs and new enhancements requested are getting noticed on a regular basis and some developers are working on those.
Essentially another old pain introduced is redistributing the updates among the client terminal machines. We already know that many applications update themselves automatically (for example, messenger applications, i.e., yahoo, MSN, skype, etc). Therefore, the company was simply looking for such a solution.
Eventually, when they notified me about this, I searched on the internet to see if somebody already did something on it or not. But I did not find any general solution on this (rather the proprietary solutions). Finally, I wrote a quick solution (which I am going to discuss in this article) to make the workaround. I am not saying that my approach is a standard/best (or any other adjective) but it just worked for us.
Idea
When I started documenting the workflow of this auto update use-case, I found the following execution points:
A. | The application (assume the application is running in a process named process A) will have a button with text "Check for updates" and after clicking on it, it will request the server if there are any updates available or not. If there are no updates available, then the application will continue from the point G (below mentioned). |
B. | If any updates are ready, then it will notify user that updates are ready to be downloaded, and request a response for starting the download task. If the response is negative, then the application will continue its work from point G (below mentioned). |
C. | If user agrees to update, then the application will launch another process (let's name it as process B). |
D. | Process B will kill process A first. And then, it will download all the files from the server (using remoting and binary formatters- it is worth using these because we are in an intranet environment). |
E. | Process B will re-launch process A (which is the main application essentially). |
F. | Process B will terminate. |
G. | Process A will continue its task as usual. |
Simple enough I believe.
Now, let's start thinking about the implementations.
Analyze Complexity and Select Technology
I broke down the tasks into several small parts to analyze where the complexity could arise. And I found only the communication portion was challenging compared to the others. I decided to use .NET Remoting because of its promising architecture and performances over intranet scenarios. I need to thank .NET framework that it has the cool XCopy deployment feature- this provides a clean and simple deployment process and leverages from old native installation hassles. Our target environment is a corporate intranet therefore I am going to use a binary formatter along with TCP/IP channel-because, I believe, this is the best solution over an intranet environment, as there is no firewall present. Essentially, it can provide a huge performance to an application.
So far so good, now we can start implementing the solution.
Implementing the Update Distribution Server
First of all, we will implement a remote server which will have an updated copy of our application, and all the terminals will ask/communicate to this server for updates, and this server will respond accordingly. If any updates are available, then it will also provide the updated assemblies to the client terminals via binary format. In real scenario, an application is supposed to build as one or more assemblies (essentially DLLs) so that updates can be applied only by replacing some or all assemblies (DLLs) of that application – rather than replacing the entire application. But, in this article, I am going to update *all* the executables (EXEs) and the assemblies (DLLs) just for demonstration purposes. Updating only the necessary assemblies will provide more performance improvement.
First of all, I am writing a new project – essentially, it is a Windows form application that will host the remote server in it. I am going to write a remote interface first which will play the contact role between the client and server.
public interface IUpdateService
{
string[] GetFiles();
string GetCurrentVersion(string fileName);
byte[] GetFile(string fileName);
}
As you might already have guessed, this is the interface that our remote server is going to implement. Now we will implement this interface as follows:
public class UpdateService : MarshalByRefObject, IUpdateService
{
#region IUpdateService Members
public string[] GetFiles()
{
Logger.LogMessage("Inside UpdateService::GetFiles()");
ArrayList collection = new ArrayList();
foreach(FileObject fileObject in ConfigInfo.Instance.FileObjects)
{
collection.Add(fileObject.FileInfo.Name);
}
return collection.ToArray(typeof(string)) as string[];
}
GetFiles
essentially reads the available update files from a predefined configured directory and exposes the name of each file to its consumer.
public string GetCurrentVersion(string fileName)
{
Logger.LogMessage("Inside UpdateService::GetCurrentVersion()");
foreach(FileObject fileObject in ConfigInfo.Instance.FileObjects)
{
if( fileObject.FileInfo.Name.Equals(fileName)
|| fileName.EndsWith(fileObject.FileInfo.Name))
{
return fileObject.Version;
}
}
throw
new ArgumentException("Given file is not found into the server.");
}
GetCurrentVersion
replies with the latest version number of a file that is available on the server. We are going to use the .NET assembly version to keep track of version number. For non-assembly files (for example XML, config, etc.), it will reply with a string
like "NA
".
public byte[] GetFile(string fileName)
{
Logger.LogMessage("Inside UpdateService::GetFile()");
foreach(FileObject fileObject
in ConfigInfo.Instance.FileObjects)
{
if( fileObject.FileInfo.Name.Equals(fileName)
|| fileName.EndsWith(fileObject.FileInfo.Name))
{
return GetBinaryContents(fileObject);
}
}
throw
new ArgumentException("Given file is not found into the server.");
}
Now, as the method name says, GetFile
simply returns the entire file content in binary format. And the method reads the file content using the following method:
private byte[] GetBinaryContents(FileObject fileObject)
{
byte[] block;
using(FileStream fileStream =
File.OpenRead(fileObject.FileInfo.FullName))
{
using(BinaryReader reader = new BinaryReader(fileStream))
{
block = reader.ReadBytes((int)fileStream.Length);
}
}
return block;
}
}
It is time to expose the service to the outside world. So I am going to host the service into a Windows Form. Here is the code snippet for exposing the service:
private void StartServer()
{
Logger.LogMessage("Opening channel..");
TcpServerChannel serverChannel = new TcpServerChannel(7444);
Logger.LogMessage("Opening channel..completed.");
Logger.LogMessage("Registering channel..");
ChannelServices.RegisterChannel(serverChannel);
Logger.LogMessage("Registering channel..completed.");
Logger.LogMessage("Registering WKO Objects..");
RemotingConfiguration.RegisterWellKnownServiceType(
typeof(UpdateService),"UpdateService",
WellKnownObjectMode.SingleCall);
Logger.LogMessage("Registering WKO Objects..completed.");
}
The AppUpdateServer
project is ready now.
Now it is time to modify our main application which will eventually contact the remote server and ask for updates. To demonstrate this, I am going to write a small application named SampleApplication
. It is a GUI application and let's say it has a menu called "Check for Updates". Whenever the user clicks on it, the application will start the update process.
One thing we need to keep in mind is that this application needs the remote URL of the remote server in order to communicate with the remote server. So we need to put that into the configuration file of the SampleApplication
. Now, I am writing a class here that will contain the update related stuff (just to keep this code separate from the other business code of the application), which will have a method named "Update
" that will do all the work.
So here is the class implementation:
public class UpdateUtil
{
private string remoteObjectUri = string.Empty;
private IUpdateService remoteService;
private IWin32Window owner;
private string applicationName;
public UpdateUtil(IWin32Window owner,string remoteObjectUri)
{
remoteService = null;
this.owner = owner;
this.remoteObjectUri = remoteObjectUri;
}
As you can see, the class needs the remote object URI
and a win32
owner (the sample application in this case) as a constructor argument.
private bool ConnectRemoteServer()
{
try
{
remoteService =
Activator.GetObject( typeof(IUpdateService),
remoteObjectUri )
as IUpdateService;
}
catch(Exception remoteException )
{
System.Diagnostics.Trace.WriteLine(remoteException.Message);
}
return remoteService != null;
}
ConnectRemoteServer
simply establishes the connection to the server using the remote URI.
private bool UpdateAvailable()
{
try
{
string assemblylocation
= Assembly.GetExecutingAssembly().CodeBase;
assemblylocation
= assemblylocation.Substring(
assemblylocation.LastIndexOf("/")+1);
applicationName = assemblylocation;
AssemblyName assemblyName
= Assembly.GetExecutingAssembly().GetName();
string localVersion
= assemblyName.Version.ToString();
string remoteVersion
= remoteService.GetCurrentVersion(applicationName);
return IsUpdateNecessary(localVersion,remoteVersion);
}
catch(Exception ex)
{
System.Diagnostics.Trace.WriteLine(ex.Message);
}
return false;
}
Now, this method performs an important task, it asks the server what version is available at the remote side for the SampleApplication.exe; if any update is available then it returns true
to its caller. The UpdateAvailable
method using the following utility method IsUpdateNecessary
to determine if the available version number is an update over the current one or not.
private bool IsUpdateNecessary(string localVersion,string remoteVersion)
{
try
{
long lcVersion = Convert.ToInt64( localVersion.Replace(".",""));
long rmVersion = Convert.ToInt64( remoteVersion.Replace(".",""));
return lcVersion < rmVersion ;
}
catch(Exception ex)
{
System.Diagnostics.Trace.WriteLine(ex.Message);
}
return false;
}
Now this is the only public
method of the class that actually invokes the above defined method to do the Update
kick. This method does some noticeable stuff inside it. I will explain these now. First of all, the method simply checks if any updates are available, if yes, then it *does not do* the Update
process itself. It launches another process AppUpdate.exe to do this job. The theory behind this is simple; the Update
process will replace the current EXE. But the system will not allow us to replace an EXE file when it is in execution. So we have to launch another process.
public void Update()
{
if( !ConnectRemoteServer())
return ;
if( UpdateAvailable())
{
if( DialogResult.Yes ==
MessageBox.Show(owner,
"An update is available. \nWould you like to update now?",
"Sample Application Update",
MessageBoxButtons.YesNo,
MessageBoxIcon.Question))
{
string updateAppPath
= Path.Combine(AppDomain.CurrentDomain.BaseDirectory,
"AppUpdate.exe");
Process updateProcess = new Process();
updateProcess.StartInfo =
new ProcessStartInfo(updateAppPath,
Process.GetCurrentProcess().Id.ToString()
+ " "+remoteObjectUri);
updateProcess.Start();
}
}
}
}
Now, AppUpdate.exe is the only thing that we need to implement to complete the task. AppUpdate
is essentially another tiny Windows Forms application (I am using Windows Forms because my intent is to display a progress bar Window during the update process) which will perform the actual update task. This process takes two command line arguments. One is the Process ID of the Sample Application. The other is the remote server URL. The purpose of the latter one is simple to us – I believe. I am just explaining the purpose of the former one. The AppUpdate.exe will replace the SampleApplication EXE, so it will first kill the process (inside which SampleApplication
is running) and then it will update the application EXEs and DLLs, finally it will re-launch the SampleApplication
.
Let's focus on to the AppUpdate.exe now.
AppUpdate
project contains a single Windows Form class where I have written the Update
stuff. Here are the code snippets:
public class AppUpdate : System.Windows.Forms.Form
{
private string applicationProcessID;
private string remoteObjectUri ;
public AppUpdate(string applicationProcessID,string remoteUrl)
{
InitializeComponent();
try
{
this.applicationProcessID = applicationProcessID;
this.remoteObjectUri = remoteUrl;
}
catch(Exception){}
}
The constructor of the class takes two arguments that I just talked about, one is the application process id of SampleApplication
and another is the remote server URI.
public void RunUpdateProcess()
{
Process applicationProcess =
Process.GetProcessById(GetProcessID());
if( applicationProcess == null ) return;
targetApplicationFullpath =
applicationProcess.MainModule.FileName;
applicationProcess.Kill();
applicationProcess.WaitForExit();
if( !ConnectRemoteServer()) return ;
UpdateFiles();
}
RunUpdateProcess
is the cue method that will do all the tasks. We will invoke this method when the Form
gets loaded.
I mean, when the Progress Window will be activated, we will invoke this method. So we can write an event handler for Form_Load
inside this class and can invoke this method from inside Form_Load()
. This method kills the SampleApplication
process and then invokes another private
method UpdateFiles
, the implementation of which is given below:
private void UpdateFiles()
{
string targetDir
= targetApplicationFullpath.Substring(0,
targetApplicationFullpath.LastIndexOf("\\"));
try
{
foreach( string file
in remoteService.GetFiles())
{
byte[] array =
remoteService.GetFile(file);
string fileName = Path.Combine(targetDir,file);
try
{
FileInfo finfo = new FileInfo(fileName);
if( finfo.Exists ) finfo.Delete();
using(FileStream outputFile
= new FileStream(fileName,
FileMode.CreateNew ,
FileAccess.Write ))
{
using(BinaryWriter writer =
new BinaryWriter(outputFile))
{
writer.Write(array,0,array.Length);
}
}
}
catch(Exception)
{
}
}
}
catch(Exception )
{
}
Process launch = new Process();
launch.StartInfo =
new ProcessStartInfo(targetApplicationFullpath);
launch.Start();
Application.Exit();
}
Therefore, we can see that this method copies each updated file from the remote server and replaces the existing ole version with those. Now it is time to run this application's Window – which we are going to do inside the Main
method.
[STAThread()]
public static void Main(string [] args )
{
if( args.Length > 0 )
{
Application.Run(
new AppUpdate(args[0],args[1]));
}
}
}
The Main
method receives the process id and remote URI through the command line arguments and provides these into the AppUpdate
class while instantiating it.
That's all there is to our implementation.
Review and Test
To test out this solution, first of all, we will have to launch the server application. In the server machine, there should be a directory that will contain all the updated assemblies along with all XML files and the config file (as needed). And the server application will read all the file version numbers and expose the remote services. Then we will launch the client application (SampleApplication
in this case) and click on the menu "Check for Updates". And the Update
process will work.
Conclusion
Again, this is a quick and easy solution that I have written. So there no reason for me to claim it a standard or "something classic". But I believe this can help the desktop application written in .NET and running in an intranet scenario. In recent days, more classic Frameworks like ClickOnce and XBAP are being used in organizations for this kind of solution.