Converting an Application to use the Application Updater Block from PAG

Setting up the directory structure

Ok, so the first thing I did.. I created a little directory tree for my app on my machine, emulating the final configuration on a user's machine. I just made this tree on my desktop;
Desktop\
--- \AutoUpdatingPagePlanner\
---------\1.6.0.0\

I copied the AppStart.exe program (provided with all of the samples, and the code is available if you wanted to customize it) into the Page Planner directory, along with its config file, and then I copied the 1.6.0.0 .exe and related DLLs for my app into the 1.6.0.0 directory.

I modified the .config file for the AppStart.exe program as follows.

  
 <appStart>
  <ClientApplicationInfo>
  
   <appFolderName>C:\PagePlanner\1.6.1.0</appFolderName>
   
   <appExeName>PagePlan.exe</appExeName>
   
   <installedVersion>1.6.1.0</installedVersion>
   
   <lastUpdated>2003-07-16T10:50:36.7831101-07:00</lastUpdated>
   
  </ClientApplicationInfo>
 </appStart>
 

Essentially I had to supply the appfoldername, (which seems to require the full path, which bothers me... Either I am wrong, and relative paths will work, or I can always go and fix the code to make them work... I'll get to one of those two solutions soon... ), the .exe filename, and the currently installed version #. Note that there are two places with the version # there... and they are both the same, I skipped the version # in the FolderName the first time around because I assumed it would concat the version # to the folder to get the full path ....

Modifying my application

Next, it was time to modifiy my application's code and .config file... because it handles the actual update work.

I added a reference to the Application Block DLL and then added this section of code to the top of my app;

 
#Region "App Updater Declares"
    Private WithEvents _updater As _
         ApplicationUpdateManager = Nothing
    Private _updaterThread As Thread = Nothing
    Private Const UPDATERTHREAD_JOIN_TIMEOUT _
         As Integer = 3 * 1000
    Private WithEvents myDomain As _
         AppDomain = AppDomain.CurrentDomain
#End Region

Not the best code around, and it doesn't match up with the naming conventions I use... but it does match up with the sample apps shipping with the application block, and that seemed safest as a starting point. Next, I need to add code to start up the updater on a background thread when the application starts;

 
Private Sub InitializeAutoUpdate()
    _updater = New ApplicationUpdateManager
    '  start the updater on a separate thread _
    'so that our UI remains responsive
    _updaterThread = New Thread( _
         New ThreadStart(AddressOf _updater.StartUpdater))
    _updaterThread.Start()
End Sub

(I call InitializeAutoUpdate() as the first line of the constructor of my main form)

Then all that was left was handling the events, which involved a bit of thread marshalling as I can't interact with the UI on the updater's thread, I needed to use Invoke to jump over to the Form's thread.

 
Private Sub _updater_FilesValidated( _
          ByVal sender As Object, _
          ByVal e As UpdaterActionEventArgs) _
          Handles _updater.FilesValidated
    Me.BeginInvoke(New MarshalEventDelegate( _
          AddressOf Me.OnUpdaterFilesValidatedHandler), _
          New Object() {sender, e})
End Sub
Private Sub OnUpdaterFilesValidatedHandler( _
          ByVal sender As Object, _
          ByVal e As UpdaterActionEventArgs)
    Dim dialog As DialogResult = _
      MessageBox.Show( _
"Would you like to stop this application and open the new version?", _
"Open New Version?", MessageBoxButtons.YesNo)
    If DialogResult.Yes = dialog Then
        StartNewVersion(e.ServerInformation)
    End If
End Sub
Private Sub _updater_UpdateAvailable( _
         ByVal sender As Object, _
         ByVal e As UpdaterActionEventArgs) _
         Handles _updater.UpdateAvailable
    Me.Invoke(New MarshalEventDelegate( _
        AddressOf Me.OnUpdateAvailableHandler), _
        New Object() {sender, e})
End Sub

Private Sub OnUpdateAvailableHandler( _
         ByVal sender As Object, _
         ByVal e As UpdaterActionEventArgs)
    Debug.WriteLine(("Thread: " + _
       Thread.CurrentThread.GetHashCode().ToString()))
    Dim message As String = _
        String.Format("Update available:  
The new version on the server is {0} and current version is {1} 
would you like to upgrade?", _
         e.ServerInformation.AvailableVersion, _
         ConfigurationSettings.AppSettings("version"))
    Dim dialog As DialogResult = _
         MessageBox.Show(message, _
         "Update Available", MessageBoxButtons.YesNo)
    '  for update available we actually WANT to block
    'the downloading thread so we can refuse an update
    '  and reset until next polling cycle;
    '  NOTE that we don't block the thread _in the UI_, 
    'we have it blocked at the marshalling dispatcher 
    '"OnUpdaterUpdateAvailable"
    If DialogResult.No = dialog Then
        '  if no, stop the updater for this app
        _updater.StopUpdater(e.ApplicationName)
    End If
End Sub
Private Sub myDomain_ProcessExit(ByVal sender As Object, _
       ByVal e As System.EventArgs) _
       Handles myDomain.ProcessExit
    StopUpdater()
End Sub
Private Sub frmMain_Closed( _
      ByVal sender As Object, _
      ByVal e As System.EventArgs) Handles MyBase.Closed
    StopUpdater()
End Sub
Delegate Sub MarshalEventDelegate( _
      ByVal sender As Object, _
      ByVal e As UpdaterActionEventArgs)
 
Private Sub StartNewVersion( _
      ByVal server As ServerApplicationInfo)
    '  NOTE:  this pathing trick will ONLY work when 
    '  this app is run from the expected "1.0.0.0" 
    '  sub-dir of the demo; 
    '  it expects to have AppStart.exe in dir directly 
    '  above it, as would be case in demo.
    Dim doc As System.Xml.XmlDocument = New System.Xml.XmlDocument
    Dim basePath As String
    doc.Load(AppDomain.CurrentDomain.SetupInformation.ConfigurationFile)
    basePath = doc.SelectSingleNode( _
"configuration/appUpdater/UpdaterConfiguration/
application/client/baseDir").InnerText
    Dim newDir As String = Path.Combine(basePath, "AppStart.exe")
    Dim newProcess As New ProcessStartInfo(newDir)
    newProcess.WorkingDirectory = newDir + server.AvailableVersion
    '  launch new version (actually, launch AppStart.exe which 
    '  HAS pointer to new version )
    Process.Start(newProcess)
    '  tell updater to stop
    myDomain_ProcessExit(Nothing, Nothing)
    '  leave this app
    Environment.Exit(0)
End Sub

Private Sub StopUpdater()
    '  tell updater to stop
    _updater.StopUpdater()
    If Not (_updaterThread Is Nothing) Then
        '  join the updater thread with a suitable timeout
        Dim isThreadJoined As Boolean = _
           _updaterThread.Join(UPDATERTHREAD_JOIN_TIMEOUT)
        '  check if we joined, if we didn't interrupt the thread
        If Not isThreadJoined Then
            _updaterThread.Interrupt()
        End If
        _updaterThread = Nothing
    End If
End Sub

That was it for code, I didn't change any other line anywhere in the app... so, as you can hopefully see, nothing I have done so far has been specific to my application at all. In fact, it could probably apply to your app without any major changes!

Modifying the app.config

I must admit, I mostly just copied the sample one here... and made a few modifications... I'll save some space and only point out the mods I made;

First, there is yet another place where I have to enter the same version #, and keep it in sync...

  
 <appSettings>
  <add key="version" value="1.6.1.0" ü>
 FONT color=#800000>appSettings>
 

next, the log listener needs to point at a good local path... relative paths don't seem to work here either so I decided upon a final destination of c:\PagePlanner\ for my application on the user's machines (cringing at the idea of forcing and hardcoding an install location, but doing it anyway)... and modified all of my paths accordingly;

 
<logListener logPath="C:\PagePlanner\UpdaterLog.txt" />

    <application 
      name="PagePlanner" 
      useValidation="true">
    <client>
     <baseDir>C:\PagePlanner\FONT color=#800000>baseDir>
     <xmlFile>C:\PagePlanner\AppStart.exe.configFONT color=#800000>xmlFile>
     <tempDir>C:\PagePlanner\newFilesFONT color=#800000>tempDir>
    FONT color=#800000>client>
    <server>
     <xmlFile>darkmajesty/PagePlan/Manifest.xmlFONT color=#800000>xmlFile>
     <xmlFileDest>C:\PagePlanner\Manifest.xmlFONT color=#800000>xmlFileDest>
     <maxWaitXmlFile>60000FONT color=#800000>maxWaitXmlFile>
    FONT color=#800000>server>
   FONT color=#800000>application>

There is also an RSA key that needed to be replaced, but more on that in a bit...

Creating the Initial Install

Now that this was no longer an href-exe, I needed to install it onto the client machine at least once. I dragged my directory structure into a new setup file and hardcoded an install location of c:\pageplanner and then built the thing into an .MSI (after removing the depencies it had found, that just ends up installing them twice...

Not a complicated procedure at all.

Creating the Manifest.xml file and setting up the web server

Ok, Ok... so this is a bit more complicated than it sounded at the beginning of the post, but it truly takes only a couple of hours and then it is done..

Creating the manifest file by hand would really suck, but luckily there is a "Manifest Utility" included with the Application Block that handles most of the work for you. You enter a directory path containing your app's .exe and all required files (other assemblies, config files, etc.), a private key (which the tool can also create for you), the version number, again and the target web server that will be the source of these updated files.

This tool will output the .xml file you need, which you can then copy up to the location specifying in the application config file (darkmajesty/PagePlan/Manifest.xml in my case).... copy all the app's files up to your web server as well and you truly are done.

That was a bit of work, but when it was done I install the app onto a clean machine, clicked on the shortcut to appstart.exe that I had added to the Start Menu (as part of the install) and it auto-updated itself beautifully...

Process to build and prop a new version

Steps to update Page Planner using the new "auto-update" system 1. Change stuff in the app

2. Change the assemblyinfo.vb version and the version key in the app.config file in the PagePlan project to the same (higher) version #.

  
 <appSettings>
  <add key="version" value="1.6.2.0">
 FONT color=#800000>appSettings>

3. Build and test the application locally.

4. Build in Release Mode

5. Copy the files from the \bin directory to \\darkmajesty\c$\inetpub\wwwroot\pageplan\  (my web directory)

5. Create a new manifest file by pointing the manifest utility at the \bin directory, entering darkmajesty/pageplan as the update location, updating the version # to match the values from step 2, and loading PrivateKey.xml from the pageplan project folder.

6. Save the manifest to \\darkmajesty\c$\inetpub\wwwroot\pageplan\manifest.xml

7. Apps should start updating within 120 seconds if they are currently running.

No need to rebuild the .msi, they will update on first run... but you could if you wished to ensure they started with the most recent version.