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>http://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 (http://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 http://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.

Comments (11)

  1. Chris Wolf says:

    This works without a hitch. Sorry to contact you directly but apparently I don’t have access to the message board from which I pulled this webpage, and you appear to be ‘the man’ on the board…. A problem I’m experiencing is this: I’m versioning an app that is updating 9 objects which total up to 866 kb in size. When I use my local machine’s webserver (self-updating app) I have to increase the polling period from 120 to about 600 in order for all the files to be downloaded, as it takes close to 5 minutes. When I use a remote box’s webserver I increase the polling period to something very high and the download takes a variant amount of time (which is a moot point because I receive an error before the polling period). The error is this: all 9 files are downloaded locally as far as the BIT[etc].tmp stage, with 0kb sizes and slowly the sizes pop in (just like when I’m running on localhost) but after 4, sometimes 5, sometimes 6 files, I receive the following error message in my logfile:

    [BITSDownloader] :

    The BITS service returned an error for the job with the ID ’03e8db99-0ffe-4128-9589-dc335174a424′;

    the job’s name and description are ‘BITS_Files_Download_Job’ and ‘BITS_Files_Download_Job’.

    The BITS service error message for this job is

    ‘The server understood the request, but is refusing to fulfill it.

    Do you have any ideas as to why this happens and any advice as to how to fix it/speed up the downloads? Thanks!!!

    chris wolf/everettwolf@hotmail.com

  2. Jeff Deville says:

    I must apologize for direct contact as well, but I also get an error when trying to create a new thread.

    My problem is that running the manifest utility (either the CS or the VB version) results in an error that reads:

    "A first chance exception of type ‘System.FormatException’ occurred in microsoft.applicationblocks.applicationupdater.dll

    Additional information: Invalid character in a Base-64 string."

    Error occurs in KeyValidator.cs on line 220, w/stacktrace:

    "StackTrace: " at System.Convert.FromBase64String(String s)rn at Microsoft.ApplicationBlocks.ApplicationUpdater.Validators.KeyValidator.Microsoft.ApplicationBlocks.ApplicationUpdater.Interfaces.IValidator.Sign(String filePath, String key) in C:\Program Files\Microsoft Application Blocks for .NET\Updater\Code\CS\Microsoft.ApplicationBlocks.Updater\Microsoft.ApplicationBlocks.ApplicationUpdater\KeyValidator.cs:line 220"

    I am using the manifest utility to create the XML manifest, so I don’t know what could be causing the issue.

    Sorry again to ask you this in your blog.

    -Jeff

    (I can be reached at jeffrey.w.deville@intel.com)

  3. Snailbug says:

    I got the same problem as Chris Wolf. I get a lot of .tmp files in the updated folder. I can’t find what’s problem, can you help?

    Sorry to contact you directly.

  4. Jeff Deville says:

    As much as I enjoy hearing that other people have made the same mistake I have, I’ll go ahead and just tell you:

    If you received the error I did, you forgot to set the validator to RSA.

    It’s the stupid mistakes that are so time-consuming eh? 🙂

  5. TheCoyote says:

    Even if I specify a RSAKey (by using the Manifest Utility) to generate the manifest.xml, I have the same behavior and error message than Chris.

    I tried with asymetric Keys or RSA or with "useValidation=’false’" on the "application" node of the App.Config xml file. Lot of time already lost…

    Any idea?

  6. Tim Jones (TJ) says:

    When my app tries to open my app.config file (in Init() of UpdaterConfiguration.vb – line is: _configuration = CType(ConfigurationSettings.GetConfig("appUpdater"), UpdaterConfiguration))

    I get the following error: ConfigurationSettings.GetConfig("appUpdater")Run-time exception thrown:

    System.Configuration.ConfigurationException – Microsoft.ApplicationBlocks.ApplicationUpdater.UpdaterSectionHandler,Microsoft.ApplicationBlocks.ApplicationUpdater does not implement IConfigSectionHandler…..I copied the app.config from the MS SelfUpdating example and just changed the paths to match my testing app so I can’t understand why this error is occuring.

    Any info is appreciated…thanks,

    TJ

  7. Dan Barnes says:

    Do you have any How-To information for adding a post processor? I am unable to get my post processor to fire after downloading the updated files to the client…the post processor is downloaded and I can run it manually (it is an exe) and the code performs the desired actions, but I can’t figure out why it doesn’t run after all files are downloaded. The manifest appears to have all the correct information about the post processor…

    Any ideas?

    Dan

  8. TJ,

    I had the same trouble you are having. If you add a Break Point on the line and step into the code until an exception is caught, you can drill down into the ‘ex’ object into the Inner Exception and you’ll probably find that you have a problem in your app.config "appUpdater" sections under <application> and either <client> or <server>. For example, the <baseDir> must exist and the <xmlFile> must be available when you run the application.

    <application name="AppName" useValidation="true">

    <client>

    <baseDir>C:Program FilesAppDir</baseDir>

    <xmlFile>C:Program FilesAppDirAppStart.exe.config</xmlFile>

    <tempDir>C:Program FilesAppDirnewFiles</tempDir>

    </client>

    <server>

    <xmlFile>http://www.domain.com/updates/Manifest.xml</xmlFile&gt;

    <xmlFileDest>C:Program FilesAppDirManifest.xml</xmlFileDest>

    <maxWaitXmlFile>60000</maxWaitXmlFile>

    </server>

    </application>

  9. Sur42 says:

    I am having a problem in blocking the updater thread. What exactly am trying to do is when the user says no to the download I want to wait for the next polling interval, but some how am get thread interrupted exception. can anyone please help me out

  10. Jay Virk says:

    I am using Application Updater for my App and KeyValidator for validation. All goes fine until after the download, the validation starts to happen, all files are validated but for an .exe file. I dont understand , what has got against an .exe file.

    Thanks

    Jay