The Application Launcher

The application launcher developed in this chapter allows an application to update itself with automatic file downloads. This model is used, in a slightly different fashion, in the popular .NET Terrarium learning game developed by Microsoft architects to demonstrate .NET. (You can download the Terrarium application, but not the source code, from http://www.gotdotnet.com/terrarium.)

The key principle in this model is that the user never loads the application directly. Instead, the user launches a stub program called the application launcher. This program queries an XML Web service to determine whether a new version of the target program is available. If it is, the launcher notifies the user or installs it automatically. When this process is complete, the most recent version of the application is started.

The example presented in this chapter uses a polite application launcher that interacts with the user before taking any action. It provides a single window with a display that indicates the current local and server version of the requested application and a brief message indicating whether an upgrade is recommended. The application launcher then enables the user to decide whether the upgrade will be made before continuing. There's no reason to adopt this behavior, however. You can use the same technique to automatically install a critical update you just omit the steps that asks the user for confirmation. Figure 15-1 diagrams the end-to-end process.

It's up to you to determine how you want to keep track of different versions of the upgradeable application. In the example considered here, a separate directory is created for every new version under the application directory. (For example, MyApp\1.00.22 would hold version 1.00.22 of the application MyApp.) This approach makes it easy to distinguish between different versions and keep a backup in the case an update fails. Optionally, you can configure the application launcher to automatically remove old versions to conserve disk space.

Figure 15-1. The update process with an application launcher

graphics/f15dp01.jpg

Before embarking on the application launcher project, you need to import several namespaces. The application launcher uses the typical namespaces (such as System.Windows.Forms) that are imported at a project-level for every Windows application project in Microsoft Visual Studio .NET, along with the following:

 Imports System.Configuration Imports System.IO Imports System.Environment Imports System.Reflection 

The application launcher also defines several form-level variables, as shown in Listing 15-1. These variables track values used in more than one event handler in the application launcher.

Listing 15-1 The application launcher startup form
 Public Class LaunchForm     Inherits System.Windows.Forms.Form     ' The name of the file to launch (for example, "MyApp.exe").     Private LocalAppFile As String     ' The local directory for the most recent version.     Private CurrentDir As String     ' The version provided on the server (for example "1.1.0.0").     Private ServerVersion As String     ' The proxy object needed for communication with the XML Web service.     Private UpgradeProxy As New localhost.UpgradeService()     ' (Other code omitted.) End Class 

Note that the application launcher holds a reference to an UpgradeService XML Web service. We'll show this XML Web service code later, but here's the basic structure:

 Public Class UpgradeService     Inherits System.Web.Services.WebService     <WebMethod()> _     Public Function GetMostRecentVersion(ByVal appName As String) _       As String         ' (Code omitted.)     End Function     <WebMethod()> _     Public Function DownloadMostRecentAssembly( _       ByVal appName As String) As Byte()         ' (Code omitted.)     End Function End Class 

When the application launcher first loads, it checks the most recent local version and the available server version. The call to the XML Web service is executed in a Try/Catch block, ensuring that it can recover and continue if the client is currently disconnected from the Internet (or the Web server is offline). If a more recent version of the application is discovered on the server, the Upgrade button is enabled. Listing 15-2 compares the local and server versions on startup.

Listing 15-2 Comparing the local and server version on startup
 Private Sub Launcher_Load(ByVal sender As System.Object, _   ByVal e As System.EventArgs) Handles MyBase.Load     Dim LocalApp As [Assembly]     ' Retrieve configured file using .config file.     LocalAppFile = ConfigurationSettings.AppSettings("FileToLaunch")     lblDisplay.Text = "About to launch: " & LocalAppFile     lblDisplay.Text &= NewLine     ' Retrieve a list of all the directories in the current path.     Dim SourceDir As New DirectoryInfo(Application.StartupPath)     ' Find highest version by sorting the directories alphabetically.     ' and taking the first item in the list.     Dim ContainedDirectories() As DirectoryInfo     ContainedDirectories = SourceDir.GetDirectories()     Array.Sort(ContainedDirectories, New DirectoryInfoComparer())     CurrentDir = ContainedDirectories(0).Name     lblDisplay.Text &= "About to use version in directory: " & _                         CurrentDir     lblDisplay.Text &= NewLine & NewLine     ' Check what version the XML Web service is providing.     Try 
         ServerVersion = UpgradeProxy.GetMostRecentVersion(LocalAppFile)         lblDisplay.Text &= "Server hosts version: " & ServerVersion         lblDisplay.Text &= NewLine          ' Determine if the upgrade option should be provided.         If ServerVersion > CurrentDir Then             cmdUpgrade.Enabled = True             lblDisplay.Text &= "An upgrade is recommended."             lblDisplay.Text &= NewLine & NewLine         End If     Catch         lblDisplay.Text &= "Could not determine server version."         lblDisplay.Text &= NewLine         lblDisplay.Text &= "You may not be connected to the Internet."         lblDisplay.Text &= NewLine & NewLine     End Try End Sub 

This functionality could be incorporated into the updateable application itself, but using a separate application launcher makes life easier and enables you to reuse this design with other upgradeable programs. In fact, because the application launcher uses a configuration file to determine the application to launch, you can use it to launch any .NET application recognized by the remote XML Web service, without changing a single line of code! All you need to do is modify the configuration file.

Figure 15-2 shows the application launcher display when it first loads up.

Figure 15-2. A new file detected by the application launcher

graphics/f15dp02.jpg

Sorting DirectoryInfo Objects

To determine which directory contains the most recent version of the application, the launcher sorts the array of DirectoryInfo objects, which represent all the contained directories. The directory with the highest version number will then be placed first in the list.

Unfortunately, DirectoryInfo objects have no intrinsic ability to sort themselves. The problem is that there are far too many possible criteria for a sort operation. In the launcher application, it's clear that we need to sort based on the directory name. To actually implement this logic, however, we need to add one additional class: a custom DirectoryInfo comparer, as shown here:

[View full width]

Public Class DirectoryInfoComparer     Implements IComparer     Public Function Compare(ByVal x As Object, graphics/ccc.gif ByVal y As Object) _       As Integer Implements System.Collections.IComparer.Compare         Dim DirX, DirY As DirectoryInfo         DirX = CType(x, DirectoryInfo)         DirY = CType(y, DirectoryInfo)         ' Compare the DirectoryInfo objects alphabetically         ' using the name of the directory.         Return DirX.Name > DirY.Name     End Function End Class

On the server side, the code needed for the GetMostRecentVersion Web method is fairly straightforward (as shown in Listing 15-3). Using an Assembly object, the XML Web service loads the requested assembly and returns its version number as a string. Optionally, you can enhance this application so that instead of returning a single string it returns an entire structure that might include additional information (such as a Boolean flag indicating whether the upgrade is recommended or required, or a file size that can be used to estimate the required download time).

Listing 15-3 Returning the server-hosted file version
 <WebMethod()> _ Public Function GetMostRecentVersion(ByVal appName As String) As String     ' Check the version of this executable.     Dim App As [Assembly]     App = [Assembly].LoadFrom(Server.MapPath("CodeBase\" & appName))     Return App.GetName().Version.ToString() End Function 

Currently, the XML Web service is hard-coded to inspect a directory named CodeBase for the latest assembly versions. You can make this setting dependent on a configuration file, or you can move the assembly information to a database. You can even insert the assembly itself into a database record as a block of binary data.

Note

The Assembly type (from the System.Reflection namespace) must be referenced with a fully qualified name or using square brackets because Assembly is a reserved keyword in the Visual Basic language. By default, the Visual Basic compiler assumes you're trying to use the keyword, not the class name.


The next step is the programmatic upgrade of the application. If the user clicks the Upgrade button, the application launcher creates a new directory with the new version number and copies the new version of the assembly into this directory. Listing 15-4 shows the code needed for this process.

Listing 15-4 Upgrading the application
 Private Sub cmdUpgrade_Click(ByVal sender As System.Object, _   ByVal e As System.EventArgs) Handles cmdUpgrade.Click     ' Show the hourglass while the download is in progress.     Me.Cursor = Cursors.WaitCursor     ' Create a directory for the new version.     CurrentDir = Application.StartupPath & "\" & ServerVersion     Directory.CreateDirectory(CurrentDir)     ' Download the new version.     Dim Download() As Byte 
     Download = UpgradeProxy.DownloadMostRecentAssembly(LocalAppFile)     Dim fs As New FileStream(CurrentDir & "\" & LocalAppFile, _                              FileMode.CreateNew)     fs.Write(Download, 0, Download.Length)     fs.Close()     lblDisplay.Text &= "New version downloaded to " & CurrentDir     ' (You could add optional code here to delete directories     ' that correspond to old versions.)     cmdUpgrade.Enabled = False     Me.Cursor = Cursors.Default End Sub 

This code uses the DownloadMostRecentAssembly XML Web service method shown in Listing 15-5. Once again, this method works with any assembly, provided it's in the correct directory. The server-side code just opens a read-only file stream and returns the assembly as an array of bytes.

Listing 15-5 Returning the latest file as a byte array
 <WebMethod()>  Public Function DownloadMostRecentAssembly(ByVal appName As String) _   As Byte()     ' The best approach is to create a FileInfo object, then a     ' FileStream. This way you can specify a read-only stream.     ' Otherwise, the code may fail because the GetMostRecentVersion()     ' method creates an assembly reference to the file, preventing it     ' from being modified or opened in a writeable mode.     Dim f As New FileInfo(Server.MapPath("CodeBase\" & appName))     Dim fs As FileStream = f.OpenRead()     ' Create the byte array.     Dim Bytes As Byte()     ReDim Bytes(fs.Length)     ' Read the file into the byte array.     fs.Read(Bytes, 0, fs.Length)     fs.Close()     Return Bytes End Function 

Note that you can't create the FileStream directly by using its constructor and passing in the file path. If you do so, .NET attempts to create a writable stream. The problem is that the GetMostRecentVersion method has already created a reference to the assembly file in order to retrieve its version information. This means that the assembly is currently loaded in the XML Web service application domain. If you attempt to open a writable stream to a file that is in use, a runtime exception will occur (even if you don't write any data to the stream). In .NET, there is no way to unload an assembly reference without unloading the entire AppDomain. The solution is to explicitly open a read-only stream. (If you're feeling particularly sharp as you read this, an alternative solution might have already occurred to you. You can modify the GetMostRecentVersion method so that it creates a new AppDomain and loads the requested assembly into the new AppDomain. Then, the method can unload the entire domain by using the AppDomain.Unload method. This is an equivalent solution, although it requires a little more code.)

Finally, the application launcher starts the most recent version of the application when the Continue button is clicked, as shown in Listing 15-6.

Listing 15-6 Launching the application startup form
 Private Sub cmdContinue_Click(ByVal sender As System.Object, _   ByVal e As System.EventArgs) Handles cmdContinue.Click     ' Load the assembly for the most recent application version.     Dim LocalApp As [Assembly]     LocalApp = [Assembly].LoadFrom(CurrentDir & "\" & LocalAppFile)     ' Retrieve the name of the startup class for the application.     Dim ClassName As String     ClassName = ConfigurationSettings.AppSettings("StartupClass")     ' Create an instance of the startup form.     Dim AppForm As Form     AppForm = LocalApp.CreateInstance("UpgradeableApp.StartupForm")     ' Show the form.     Me.Hide()     AppForm.ShowDialog()     Me.Close() End Sub 

In this case, the code turns to the configuration file again to determine which class it should use to start the application. This setting must be a fully qualified class name (in this case, UpgradeableApp.StartupForm, the name of a Windows Form in the assembly). The full configuration file is shown here:

 <?xml version="1.0" encoding="utf-8" ?> <configuration>   <appSettings>     <add key="FileToLaunch"          value="UpgradeableApp.exe" />     <add key="StartupClass"          value="UpgradeableApp.StartupForm" />   </appSettings> </configuration> 

After the first window has been created for the form, the user can seamlessly branch out to any other part of the application. For example, if the UpgradeableApp.StartupForm attempts to launch another form from the assembly (such as Form2), the CLR automatically searches for the requested type in the same assembly. There is only one potential stumbling block. When an application is launched in this way, you can't use the Application class (in the System.Windows.Forms namespace) to correctly retrieve information such as the application's version number. If you do, this information will correspond to the hosting assembly, which in this case is the application launcher. If you want to retrieve version information dynamically, you must use the reflection types instead to retrieve a reference to the application assembly. (You can use a method such as Assembly.GetExecutingAssembly.)

The application launcher is a simple, reusable utility for automating deployment. You can use similar techniques to create a transparent application launcher that performs required upgrades in the background, without requiring any user interaction. You can even dedicate a separate thread to the process. This way, the version will be downloaded while the user works. The next time the user exits and restarts the application, the application launcher will start the newly downloaded version. Alternatively, you can make the process entirely optional. You might want to advertise on the application status bar when a new version is available and enable the user to ignore the message, for example, or initiate a download by choosing an Upgrade Now menu option.

Security Holes in the Upgrade XML Web Service

As it stands, the code has a significant but subtle security vulnerability. In fact, it's a good example of what can go wrong with an XML Web service. Clearly, the DownloadMostRecentAssembly Web method enables any user to retrieve a file from the CodeBase directory. If you needed to protect these files, you could create some sort of authentication system, as discussed in Chapter 13. The user would have to call a Web method to become authenticated before calling the XML Web service and then present some sort of user credential (such as a ticket) to the other XML Web service methods to be allowed to download a file.

This is straightforward enough, but it doesn't plug the real security hole. The problem is that the DownloadMostRecentAssembly XML Web service method provides an unchecked window onto the server's file system. Instead of submitting a valid filename, a client could submit a qualified path name such as "..\myfile". If this sequence were inserted into the path name, the Web method would actually download the file named myfile from the parent of the CodeBase directory. ("..\" means to move one directory up the tree.) Because the XML Web service is limited only by the restrictions of the ASP.NET worker process, this could enable a user to download a sensitive server-side file. This type of problem, which depends on being able to specify a path outside of the intended bounds, is called a canonicalization error. It's one of the most common but least high-tech security errors that an application can suffer from.

It's easy enough to create a more resilient Web method. One technique is to ensure that you strip out only the filename from the user's submitted string before processing it. For example, the following line of code uses the Path class (from the System.IO namespace) to remove only the final filename portion of the string. By inserting this line at the beginning of the DownloadMostRecentAssembly Web method, you ensure that the user is constrained to the correct directory.

 appName = Path.GetFileName(appName) 

If the user has behaved responsibly and submitted only a filename with no path characters, the appName string will be unchanged. Otherwise, the returned appName will include only the final filename portion.

Other Enhancements to the Application Launcher

The application launcher is a simple utility that works admirably well. If you want to incorporate this design into your own projects, you might consider a few enhancements:

  • Instead of returning just the version of server-side assemblies, you can return other information in a custom class. This might allow a client to choose whether a download is necessary, select among multiple versions (perhaps depending on regional settings), or even download optional features.

  • The upgrade XML Web service should log every download and use the properties of the Context.Request object to store additional information about the user. This helps both to identify suspicious behavior that might indicate downloads by the wrong users and, more commonly, errors in the application launcher or upgrade XML Web service that result in unnecessary downloads. If the Web server becomes a file server dedicated to constantly transmitting large binary files, performance will suffer.

  • To force the user to use the application launcher, you should modify the target application so that it can't be started independently. There are several possible ways to do this, but the easiest is just to compile the application as a DLL assembly. In Visual Studio .NET, this just involves tweaking the project properties. The next example uses this approach.



Microsoft. NET Distributed Applications(c) Integrating XML Web Services and. NET Remoting
MicrosoftВ® .NET Distributed Applications: Integrating XML Web Services and .NET Remoting (Pro-Developer)
ISBN: 0735619336
EAN: 2147483647
Year: 2005
Pages: 174

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net