Code for this article:

http://www.west-wind.com/presentations/wwCodeUpdate/wwCodeUpdate.zip

 

 

 

 

 

Applications change - imagine that! Whenever we as developers think an application is complete, along come the customers (or sometimes ourselves) and think up a few more features to add to that 'perfect' application. Other times we find out that the application wasn't so perfect after all and a few bug fixes are required. In this article Rick discusses an efficient way to update your Visual FoxPro based desktop applications via the Internet by presenting a class that handles version checking against an Internet based resource and updating of a running Exe file based application. Along the way he'll also discuss some related issues of dealing with application updates that are just as important as the physical process of updating the code.

 

Updating an installed application is not something that most of us as developers look forward to. Updating a desktop application, especially one that is distributed in many locations is not a trivial task. If you've ever distributed a vertical application to thousands of users only to find a small but critical bug in the application you probably are familiar with that sinking feeling in your stomach of trying to find and notify all of your customers of the problem and then providing a fix to them.

 

There are a number of issues involved in updating an application. There's the physical issue of how to get an update of the application to the user. But in addition there's how to check for new versions and how to actually install the update on the client machine. Rarely does an update require only a single Exe file to be updated, but often support files including data files and more importantly data file structures must also be updated.

 

This article focuses on the physical process of checking for new versions and updating an application over the Internet and I'll touch briefly on some of these related issues that I've run into in relatively small scale desktop applications that have gone out to large numbers of users. In the process I'll introduce a class that handles all of the logistics for checking for updates online, then downloading the update from the Web using the HTTP protocol and updating any number of files of the application.

The process of updating an application

Before I go into the details of how the wwCodeUpdate class works, let's take a look at the process involved in updating an application over the Internet. For my own uses inside of desktop applications I need to perform the following tasks as part of the update process:

 

 

At first blush this process seems pretty simple, but there is a fair amount of activity and an external application involved in making this all happen. The typical scenario for an application works something like this: The application starts up and as part of the start up process checks for a version number which is retrieved from the Web via an XML file that contains the version number as well as download information for the update file (URL, size of the download, a message to the user). It matches this retrieved version number with the internal version number of the application and compares the two. If the version available online is newer the user is prompted to download the update. The application then downloads the update file from the Web using the URL retrieved from the XML version resource on the Web into a specified local directory.

 

At this point the running application must shut down and launch another application that can update the running application and restart it. This is required because the running Exe file is locked and can't be overwritten while running. So, the current application quits and the update application starts. The update application handles updating the Exe file by unzipping the update file into the specified directory and replacing the original application Exe and any other files that need updating. It then restarts the original application, which when re-started is up to date and passes by the version comparison from the Web resource XML file.

 

If there are additional updates to be performed once the application files have been updated such as structure changes, updating or inserting data or moving files around, those can be handled the first time the application restarts. This requires that the application maintains an updateable version number in a configuration file to compare the applications compiled version and the data version and comparing the two on startup. If they don't match the specified code is fired to perform the update or custom actions followed by updating the data version number.

How it works

The wwCodeUpdate class handles most of this update process with a few property settings and simple method calls. The property settings determine where the update is to be performed and what files to access from the Web and where to install them to. In addition it uses an XML file on the server as the version and link resource used to download the update file which makes it possible to have multiple update versions online at the same time (for example for multi-step updates from ancient versions).

 

Before I go into detail of how the class operates let's look at a snippet of example code in Listing 1 that uses the wwCodeUpdate class inside of a client application to check for an update and install it.

 

Listing 1 � Using the wwCodeUpdateClass in a client application

* SampleAppForm :: CheckForNewVersion()

LOCAL loUpdate, loAPI, lcURL, lnVersion

 

*** Retreive the URL to use for version checks

*** Example: http://www.west-wind.com/files/updates/wwReader_update.xml

loAPI = CREATEOBJECT("wwAPI")

lcUrl = loAPI.GetProfileString(FULLPATH("wwReader.ini"),"Main","VersionUpdateUrl")

 

loUpdate=CREATEOBJECT("wwCodeUpdate")

 

*** Url where to check for new versions (XML file)

loUpdate.cVersionUrl = lcUrl

loUpdate.cVersionType = "N"�� && Numeric Type

 

*** Name of the application's Exe file to hotswap

loUpdate.cExeFile = "wwReader70.exe"

 

*** Returns the current available version from the XML file

*** Sets properties of the loUpdate object with the XML info

lnVersion = loUpdate.GetVersionInfo()

*** If no version we couldn't get the info from

*** the Web Server

IF lnVersion = 0

�� *** Couldn't retrieve version info � just exit

�� *** You can also display error info: loUpdate.cErrorMsg

�� RETURN

ENDIF

 

*** Compare the version number against

*** the app's version number - here a property of the form

*** Usually you'd use a .H setting, database field or Config object

IF lnVersion > THISFORM.nVersion

�� *** Display message for the user and ask to download

�� IF MessageBox(loUpdate.cUserMessage + CHR(13) + CHR(13) +;

���������������� "Would you like to download and install it now? (" + ;

���������������� LTRIM(TRANSFORM(loUpdate.nFileSize,"999,999,999")) + ;

���������������� "kb)",32+4,"West Wind Technologies") = 6���������������

 

����� IF !loUpdate.DownloadUpdate()

�������� MessageBox(loUpdate.cErrorMsg,48,"Download Error")

����� ELSE

� �������*** Shutdown VFP

�������� CLEAR EVENTS

�������� RELEASE THISFORM

��������

�������� *** Run the generic CodeUpdate Exe and quit application

������� loUpdate.RunUpdateExe(;
������������������ [WSCRIPT CodeUpdate.vbs "wwReader70.exe" "] + ;

��������� ���������SYS(5) + CURDIR() +;

������������������ [codeupdate\wwReader_Update.exe /auto ] + ;
������������������ SYS(5) + CURDIR() + [" ] + ;

������������������ ["West Wind Message Reader"])������ ENDIF

�� ENDIF

ENDIF

 

RETURN

 

This code starts by creating the wwCodeUpdate class and retrieving the URL to check for updates from an INI file. This value can come from anywhere � a config table, the registry, an XML file etc. � but I like to have this value configurable in some easy way so that I can change the location of the file for different versions or while testing against a local server. This URL points at an XML file on the Web Server that contains the version info for the latest available version as shown in Listing 2.

 

Listing 2 - XML configuration file that contains update information

<?xml version="1.0"?>

<codeupdate>

��� <version>4.21</version>

��� <fileurl>http://www.west-wind.com/files/updates/wwreader_update.exe</fileurl>

��� <size>372</size>

�� <usermessage>Version 4.21 of the West Wind Message Reader

is now available for download.</usermessage>

</codeupdate>

 

The GetVersionInfo() method of the class then retrieves this XML file and assigns the values of the content to properties of the class. You should set the following properties of the class before calling this method:

 

Configurable Property

Function

cVersionUrl

The URL to the XML file that contains the version info

cVersionType

The FoxPro type of the version. Can be either "N" or "C" (default)

cExeFile

The filename of the Exe file. Should be just the file name (ex: SampleApp.exe)

cApplicationName

Used for prompts and dialogs when updating the application. Defaults to "the application"

cDownloadPath

The path where to download and unzip the update file. Typically use a relative path for this (ex: .\codeupdate\ (default)). The path is created if it doesn't exist

 

Only the first three are required settings. After the GetVersionInfo() method has been successfully called the following properties are set:

 

Downloaded Property

Function

vOnlineVersion

Character or String value that holds the version number retrieved. Depends on the setting of the cVersionType property.

cOnlineFileUrl

The URL to the update file to be downloaded. Typically this is an Exe file containing zipped content (WinZip Executable)

vOnlineMinVersion

Minimum version that this update can be applied to. This can be set and checked to avoid updating versions that are too old.

nFileSize

The size of the update file in kilobytes

cUserMessage

Message to display to the user before downloading the update. This message goes into a message box followed by a download request question.

 

The method returns the version number depending on the cVersionType property setting. If the version retrieval fails - due to a connection problem most likely � the call to GetVersionInfo returns 0 or "0" in which case you should just move on with your application. When a version number is retrieved the application can compare version numbers prompt for the download with a user defined message that was retrieved from the XML file, if the versions don't match.

 

Figure 1.1 � Displaying the download message from the Web Resource's <usermessage> entry.

 

If the user decides to download, a call to the DownloadUpdate() method handles downloading of the update file. The code in the class handles the HTTP connection, displaying a progress dialog (requires the Common Dialogs OCX (MSCOMCTL.OCX) and creating the download file and directory if necessary. Figure 1 shows what this dialog looks like. When DownloadUpdate() is complete you should check for errors by checking the return value of this method, and if .F. checking the cErrorMsg property.

Figure 1.2 � the progress dialog that displays while downloading the update file.

 

After the download has completed you will now have an update file sitting in the directory you specified in the cDownloadPath property. wwCodeUpdate assumes that this file self extracting Zip file that contains the Exe file plus any other files required to update the application. The Exe file should unzip into the application root directory as is and if you include subdirectories make sure they are included as relative directories.

 

Up to this point all code is called from your main application which makes method calls into the wwCodeUpdate class. Once the update file has been downloaded your application has to exit in order to swap the application files. To do so you can call the RunUpdateExe() method, which runs the specified Exe or Script and then quits the current application. As you can see in Listing 1 you should perform any application cleanup before calling this method, which after running the Exe file asynchronously quits your application. The parameter to the method is the OS command line to the update application, which in this case is Windows Scripting Host VBScript file � CodeUpdate.vbs, which is a generic script that runs the self extracting Zip file and then restarts the application. Codeupdate.vbs is included in download archive for this article.

 

The script itself takes three parameters � the name of the Exe file, the full command line for the self extractor which includes the unzip path, and the name of the application for display in the update dialogs. These parameters and the full command line for running the script is what you see in the call to RunUpdateExe() above.

Behind the scenes

The wwCodeUpdate class takes care of all the work required to perform the version checks and the HTTP access to download the XML version file and the actual update file from the Web site.

 

To get an idea of how the wwCodeUpdate class works let's look at a few key methods. The GetVersionInfo() method shown in Listing 3 is the key method used to retrieve the version info from the XML Web resource file and update the local properties.

 

Listing 3 � The GetVersionInfo method reads the XML file and updates properties

FUNCTION GetVersionInfo

LOCAL loIP, lcVersionType, lcXML, lnSize, lcVersion

 

THIS.SetError()

 

lcVersionType = THIS.cVersionType

 

IF ISNULL(THIS.oHTTP)

�� loIP = THIS.CreateHTTPClient()

ELSE

�� loIP = THIS.oHTTP��

ENDIF

 

*** Don't want a dialog on the version check

loIP.lShowDialog = .F.

 

lcXML = loIP.HTTPGet(This.cVersionUrl)

 

*** Check for errors

IF loIP.nError # 0

�� THIS.SetError(loIp.cErrorMsg)

�� RETURN IIF(lcVersionType = "C","",0)

ENDIF

 

IF lcXML <> "<?xml"

�� THIS.SetError("Missing or invalid XML returned from server")

� �RETURN IIF(lcVersionType = "C","",0)

ENDIF

 

lcVersion = Extract(lcXML,"<version>","</version>")

IF EMPTY(lcVersion)

�� THIS.SetError("No version number found in XML")

�� RETURN IIF(lcVersionType = "C","",0)

ENDIF

 

THIS.vOnlineVersion = IIF(lcVersionType="C",lcVersion,VAL(lcVersion))

 

lcVersion = Extract(lcXML,"<minversion>","</minversion>")

IF !EMPTY(lcVersion)

�� THIS.vOnlineMinVersion = IIF(lcVersionType="C",lcVersion,VAL(lcVersion))

ENDIF

 

THIS.cOnlineFileUrl = Extract(lcXML,"<fileurl>","</fileurl>")

THIS.nFileSize = VAL( Extract(lcXML,"<filesize>","</filesize>") )

THIS.cUserMessage = Extract(lcXML,"<usermessage>","</usermessage>")

 

*** Save the XML just in case

THIS.cXML = lcXML

 

IF lcVersionType = "C"

�� RETURN lcVersion

ENDIF

 

RETURN THIS.vOnlineVersion

 

This simple code retrieves the version info and deals with the XML content by using the wwHTTP object for HTTP retrieval with the HTTPGet() method. It then parses the XML content simply by using string extraction. Values are extracted and in some cases converted to the proper types. This code doesn't use the MSXML parser to read the XML because the values are known simple values (string and numbers) to parse without incurring the MSXML COM requirement.

 

The next method called is DownloadUpdate() which is used to retrieve the Update file from the Web. It too uses wwHTTP, this time with its support to directly download content to a file. Listing 4 shows the code to DownloadUpdate().

 

Listing 4 � Downloading the update file from the Web site

FUNCTION DownloadUpdate

LPARAMETER llCheckforExistingVersion

LOCAL loIP, lcData, lnSize, loUrl

 

IF ISNULL(THIS.oHTTP)

�� loIP = THIS.CreateHTTPClient()

ELSE

�� loIP = THIS.oHTTP

ENDIF��

 

loIP.lShowDialog = THIS.lShowDialog

 

*** Break down the URL into its components

loUrl = loIP.InternetCrackUrl(THIS.cOnlineFileUrl)

IF ISNULL(loUrl)

�� RETURN .F.

ENDIF

 

IF loIP.HTTPConnect(loUrl.cServer,"","",;

������������������� IIF(lower(loUrl.cProtocol)="https",.T.,.F.)) # 0

�� THIS.SetError(loIP.cErrorMsg)

�� RETURN .F.

ENDIF��

 

*** Create a temporary directory if it doesn't exist

IF !ISDIR(THIS.cDownloadPath)

�� MD (THIS.cDownloadPath)

ENDIF��

 

*** Get the file name to download to

lcTFile = THIS.cDownloadPath + JUSTFNAME(STRTRAN(THIS.cOnlineFileUrl,"/","\"))

lcData = ""

lnSize = 0

 

*** Actually download the data � fires progress events

IF loIp.HTTPGetEx( TRIM(loUrl.cPath),@lcData,@lnSize,,lcTFile) # 0

�� THIS.SetError(loIP.cErrorMsg)

�� RETURN .F.

ENDIF��

 

RETURN .T.

 

DownloadUpdate() uses the lower level wwHTTP members HttpConnect and HTTPGetEx to download the file. The last parameter of the HTTPGetEx call allows specification of a filename which allows wwHTTP to download directly to file instead of returning data into a string first. This is important for this application because the download file could potentially be very large and the file might not fit into memory � going to file is more efficient in this case. It also uses these methods in order to have events fire in order to show the progress dialog and the ability to cancel the download.

 

After the download is complete the call to UpdateExe() wraps the Exe command line for running the update Exe. This method runs whatever you pass it using the RUN command then simply QUITs the application.

 

Listing 5 � The RunUpdate Exe runs the external Exe and quits the app

FUNCTION RunUpdateExe

LPARAMETER lcUpdateExe

 

*** Run external program to copy in the files

lcParms = [RUN /n1 ] + lcUpdateExe

&lcParms

 

*** Required if READ EVENTS IS ACTIVE

*** otherwise Exe won't release

CLEAR EVENTS

 

ON ERROR *

RELEASE ALL

CLEAR ALL

 

QUIT

 

The Update Application

RunUpdate() simply runs an external operating system application or script that is responsible for unpacking the self-extracting Zip file which replaces the running application and then restarting the original application. This external application is required as you cannot update an Exe file that is currently running. The parameter passed into this method is the full command line that runs the swapping application.

 

This external application can take many different forms. You can use a VFP application and a small stub program is actually provided in the wwCodeUpdate.prg file. To create the swapping EXE simply add wwCodeUpdate.prg to a project and compile it into an Exe file. The mainline program handles swapping exe's by calling the SwapExes method. You can take a look at this code in the provided source code to see how it works � it's only a few lines of code.

 

Using a VFP application for the swapping application is useful as you can control the user interface and potentially run any other code to update or massage the data copied. But there is one problem with this scenario of using a VFP application for this update: You cannot swap your VFP application if the VFP runtime files also need to be updated as those are required to run the swapping application which itself uses the VFP runtimes. This may not be a problem right now, but in the future as your application revs you might want to have the runtime update available as an option also without having to physically ship or install an update.

 

To address that particular issue you can use an external application totally unrelated to VFP. For example, one could write a small C++ executable that handles the swapping. Or even simpler � use a Windows Script file or batch file to perform the task. The code shown in Listing 6 shows a VBScript Windows Scripting Host file that performs this task.

 

Listing 6 � Using a Windows Scripting Host script to swap Exes

* CodeUpdate.vbs

if WScript.Arguments.Length < 3 then

�� WScript.Echo("Invalid number of parameters passed." + vbcrlf + _

��������������� "Syntax: WSCRIPT CodeUpdate.vbs " + _

��������������� <ExeFile> <UpdateFile> <ApplicationName>")

�� Wscript.Quit(1)

end if

 

lcExe = WScript.Arguments(0)

lcUpdateExe = WScript.Arguments(1)

lcAppName = WScript.Arguments(2)

 

Set oShell = Wscript.CreateObject("Wscript.Shell")

 

'*** Wait for 5 seconds for app to shut down

lnResult = oShell.Popup("Waiting for application to shut down...",_

����������������������� 5,lcAppName + "Update",64)

 

'*** Run the Update Exe

oShell.Exec(lcUpdateExe)

 

lnResult = oShell.Popup("Updating Exe File... ",_
����������������������� 5,lcAppName + "Update",64)

 

'*** Check that the Exe exists

Set oFile = CreateObject("Scripting.FileSystemObject")

if not oFile.FileExists(lcExe) then

�� lnResult = oShell.Popup("Updating Exe File......",_
�������� ������������������5,lcAppName + "Update",64)

end if

 

oShell.Exec(lcExe)

 

To call this script you have to call it with a full commandline to RunUpdateExe():

 

loUpdate.RunUpdateExe([WSCRIPT CodeUpdate.vbs "wwReader70.exe" "] + ;

������������������ SYS(5) + CURDIR() +;

������������������ [codeupdate\wwReader_Update.exe /auto ] + ;
������������������ SYS(5) + CURDIR() + [" ] + ;

������������������ ["West Wind Message Reader"])

 

CodeUpdate.vbs is fairly generic and you pass it three parameters:

 

  1. lcExeFile
    The filename of the Exe file that runs. This is used to restart the Exe file when the update is complete. Example: SampleApp.exe (path is optional as long as it's visible from the current directory via PATH)
  2. lcUpdateCommandLine
    This is the fullcommand line that is used to run the self-extracting Zip file. You should use full paths for all paths provided, which includes the name of the Exe and the update directory. The /auto flag on the Zip file is used to cause the Zip file to unzip without any user interaction.
    Example: d:\sampleapp\codeupdate\SampleApp_Update.exe /auto d:\sampleapp
  3. lcApplicationName
    The name of the application which is used in display dialogs.

A typical OS commandline looks like this (all on one line separated by spaces):

 

CodeUpdate "SampleApp.exe"
�������������������� "d:\sampleapp\codeupdate\sampleapp_update.exe /auto d:\sampleapp"
�������������������� "Sample Application"

 

The script takes these parameters and runs the self extracting zip and then restarts the application. Notice that the script has a few wait states in it using the Popup method with a 5 second delay � it first waits to make sure the application has shut down, then waits after the Exec() method that runs the self-extractor for it to complete. You can tweak the script as needed for longer timeframes if the Zip file should be huge or the application takes much longer to shut down.

 

This script file (or the VFP Codeupdate.exe) is generic and can handle updates for most applications as is. You can customize either the script or the VFP code, but I think you'll want to leave the swapping application as generic and simple as possible and leave any real data updates up to the main application. This way you can easily reuse the update code.

Performing post-update application operations

Most application updates require additional work to be performed after the actual Exe file update. Typically this involves updating structures of data files or copying additional files or massaging existing data into new formats etc. The idea is that you want your Exe to run this update routine only the first time the Exe is started under the new version.

 

One way to do this is to store an update version number in some data source that is updateable in addition to a version number that is part of the application. In the sample application included with this article (SampleApp.scx) there is a version number hardcoded into the main application form (nVersion) and there's a current data version number stored in a configuration INI file (the same one that houses the version check URL). The INI file looks something like this:

 

[Main]

Version=1.1

VersionUpdateUrl=http://localhost/files/updates/sampleapp.xml

LastVersionCheck=07/01/2002

 

On startup the application can read the current data version value from this INI file, then compare that with the version of the application. For example, Listing 7 shows the UpdateData() method of the sample form that checks for a version change read from the INI file.

 

Listing 7 � Checking for a data version change after the Exe has been updated

*** Retreive the URL to use for version checks

*** This is an XML file

loAPI = CREATEOBJECT("wwAPI")

 

lnDataVersion = VAL(loAPI.GetProfileString(;

������������������� FULLPATH("SampleApp.ini"),;

������� ������������"Main",;

������������������� "Version") )

�������������������

IF lnDataVersion = THISFORM.nVersion

�� *** Nothing to do

�� RETURN

ENDIF

 

*** Code to perform data updates goes here

WAIT WINDOW "Data version is not up to date. Updating..." TIMEOUT 5

 

*** And write out the updated version number

*** but only after this process completes

loAPI.WriteProfileString(;

������������������� FULLPATH("SampleApp.ini"), ;

������������������� "Main", ;

������������������� "Version",;

������������������� LTRIM(TRANSFORM(THISFORM.nVersion,"99.99")))

�������������������

RETURN�������������������

 

You would put any data update code into the body of this method or call out to another method or object to perform specific update tasks.

 

One of the most common things my applications do are structure changes for data files. If you're using a database container you can use the ALTER TABLE command, but if you have updates coming from multiple different versions this gets to be messy in many cases. So rather than trying to keep all those ALTER TABLE statements properly versioned I often prefer to simply create a new table with the new structure and import the data into it, then swap the files. An example of an update for a specific data file is shown in Listing 8.

 

Listing 8 � A simple Table structure update routine

USE wwThreads in 0&& Earlier

SELE wwThreads

 

*** Save the old file settings

oldfile=SUBSTR(DBF(),1,LEN(DBF())-4)

 

*** close the file

USE

 

*** wwthreads.DBF file

CREATE TABLE (tfile) ;

(��� USERID����� C (8),;

�� THREADID��� C (9),;

�� MSGID������ C (9),;

�� ParentID��� C (9),;

�� SUBJECT���� M ,;

�� MESSAGE���� M ,;

�� ATTACHMT��� M ,;

�� ATTACHNM��� M ,;

�� FROMNAME��� C (35),;

�� FROMEMAIL�� M ,;

�� TO��������� M ,;

�� FORUM������ C (40),;

�� TIMESTAMP�� T ,;

�� NOFORMAT��� L ,;

�� READ������� L ,;

�� POST������� L ,;

�� SAVE������� L ,;

�� EMAIL������ L ,;

�� FOLLOWUP��� L ,;

�� EXPANDED��� L )

��������������������

*** Append the old file's records into new structure

APPEND FROM (oldfile)

USE

 

*** Now rename the files

ERASE (oldfile+".DBF")

ERASE (oldfile+".FPT")

RENAME (tfile+".DBF") TO (oldfile+".dbf")

RENAME (tfile+".fpt") TO (oldfile+".fpt")

ERASE (tfile+".DBF")

ERASE (tfile+".FPT")

 

USE ("wwthreads")

 

To help with this process the West Wind Client Tools include a utility to generate the CREATE TABLE commands for me automatically from a table so I don't have to manually keep track of it. The utility is called CRT_DBF and comes with both a programmatic and form interface which returns a string either to the clipboard or as a result value with the table structure command. This routine makes the structure update process easy as I don't have to remember which structure change was associated with which version and instead create a new fully updated structure of the file each time.

 

You might have to do additional checks based on the current data version such as:

 

*** Version 1.5 and earlier updates

IF lnDataVersion < 1.5

��� REPLACE To with "All" for empty(To)

ENDIF

 

Note that you should update the stored data version stamp only after you've completed all of your updates successfully. If for some reason the update process fails the writing to the INI file in the example above should not occur. This way the next time you run the update will occur again. This can be tricky if data updates occur that write data into files, and you might need additional checks to validate the data before updating. But in most cases you can simply re-run the routines.

Customizing the HTTP Process

In a perfect world HTTP downloads are very simple. Unfortunately in today's security environments you'll find that often times you need to customize your HTTP handler to account for proxy and firewalls settings and different HTTP connect modes.

 

The wwCodeUpdate class uses the West Wind HTTP class (wwHTTP)to handle the HTTP communication with the Web Server. This class provides full control over retrieving content from a Web server. It is also fully configurable for advanced HTTP operation such as username/passwords, SSL operation, timeout configuration, proxy configuration etc. Full documentation of the wwHTTP class is available at the West Wind Client Tools Web page (http://www.west-wind.com/wwClientTools.asp) which wwHTTP is part of. The choice of using wwHTTP instead of the MSXML's XMLHTTP is done primarily to allow custom configuration of the HTTP process, the need to download binary data and most importantly to provide progress information events as part of the HTTP download of the update file. wwHTTP supports all of these easily with pure VFP code without any COM requirements.

 

wwCodeUpdate uses the CreateHTTPClient() method to create a default instance of the HTTP client to centralize the configuration of the HTTP client in case you need custom configuration (Listing 9).

Listing 9 � Creating an instance of the HTTP client

FUNCTION CreateHTTPClient()

THIS.oHTTP = CREATEOBJECT("CU_wwHTTP")

THIS.oHTTP.nConnectTimeout = THIS.nConnectTimeout && 10 secs


RETURN THIS.oHTTP

 

This method does nothing more than instantiate a custom version of the wwHTTP class called CU_wwHTTP. It then sets the connection timeout which is important since this class is also used to check for the XML file on the Web � if there's no connection or the Web server is not responding in some way you want to timeout fairly quickly so that your application doesn't hang too long waiting to retrieve the version info. Note that if the machine is completely offline and disconnected from the network the connection will immediately fail and GetVersionInfo() will return 0. The worst case scenario is that the Web server is up but not responding in which case the timeout will determine when the request stops.

 

The point is that once CreateHTTPClient() has been called you can completely customize the oHTTP property reference. For example, if you need to set Proxy information on the client you can use code like this:

 

loUpdate.CreateHttpClient()&& create .oHTTP reference

loHttp = loUpdate.oHTTP&& Easier reference

 

loHttp.nHttpConnectType = 3&& Proxy Mode

loHttp.chttpproxyname = "proxy-server.hawaii.rr.com"

 

The nHttpConnectType property is probably the most important one to set and all of my applications that access Web content tend to provide a configuration setting for the connect mode. Figure 2 shows a configuration dialog for the West Wind Message Reader that provides this configuration option. By default this value is 0 (using Internet Explorer configuration) and can also be 1 (direct connection) or 3 (for Proxy configuration). Generally 0 will work as it tries to read the Internet Explorer network and proxy settings, which works 99% of the time, but in rare cases it won't work with some non-standard proxy servers. You can then use mode 3 or mode 3 with specific proxy server name, username and password provided.

 


Figure 2 � A typical application configuration dialog that contains Web Server and connection mode settings. In this application this value is stored in a configuration object that is accessed everytime HTTP access is performed (loUpdate.oHTTP.nHTTPConnectType = loConfig.nHttpConnectType).

 

If you like to allow access to updates only to specific users you can use a username and password to limit access to the update files and version resource:

loHttp.cUsername = "rickstrahl"

loHttp.cPassword = "password"

 

For those of you familiar with wwIPStuff or wwHTTP you probably know that in order to handle download events, the wwHTTP class should be subclassed and the OnHttpBufferUpdate method implemented with a handler that provides the progress information. For this reason the CreateHttpClient() method creates the custom CU_wwHTTP class which implements the OnHttpBufferUpdate methodalong with some basic property settings. For example, the default HTTP timeout is set to the value specified on the wwCodeUpdate class.

 

Listing 10 shows the implementation of the CU_wwHTTP subclass, which adds a few custom properties to the wwHTTP class that relate to the progress dialog display.

 

Listing 10 � wwHTTP subclass to allow displaying progress information events

*****************************************************

DEFINE CLASS CU_wwHTTP as wwHTTP

**************************************

 

*** Custom properties dealing with display

*** of download information

lShowDialog = .F.

oProgressForm = .NULL.

cCaption = "Downloading Update"

nContentSize = 0

 

************************************************************************

* CU_wwHTTP :: OnHTTPBufferUpdate

************************************

***Function: HTTP Progress Event Handler

***��� Assume: Relies on wwDialogs.vcx for Progress Form

************************************************************************

FUNCTION OnHTTPBufferUpdate

LPARAMETERS lnbytes,lnbufferreads,lccurrentchunk

 

DO CASE

*** If this is the 0 chunk it's HTTP Header

CASE lnBufferReads = 0

������ THIS.nContentSize = VAL( EXTRACT(lcCurrentChunk,CHR(13)+CHR(10) + "Content-length: ",CHR(13)) )

������ DO CASE

�������� CASE THIS.nContentSize > 90000

������������� THIS.nHTTPWorkBufferSize = 16484

�������� CASE THIS.nContentSize > 40000

������������� THIS.nHTTPWorkBufferSize = 8182

������ ENDCASE

����� �RETURN

CASE lnBufferReads = -1

������ *** Done or cancelled (if lHttpDownloadCancelled=.T.)

������ RETURN

OTHERWISE

������ DoEvents

ENDCASE

 

IF THIS.lShowDialog

�� IF lnBufferReads=1

����� THIS.oProgressForm = CREATEOBJECT("wwProgressForm")

����� THIS.oProgressForm.SetCaption(THIS.cCaption)

����� THIS.oProgressForm.Show()

�� ENDIF

 

�� IF THIS.oProgressForm.lCancelled

������ *** Cause HTTP Request to cancel on next buffer���������

������ THIS.lHTTPCancelDownload = .T.

�� ENDIF

 

�� THIS.oProgressForm.SetDescription("Received from " + THIS.cserver + ":" +CHR(13) +;

������������������������� LTRIM( TRANSFORM(lnBytes,"999,999,999") ) + " of " +;

����� ��������������������LTRIM(TRANSFORM(THIS.nContentSize,"999,999,999"))+ " bytes")

�� THIS.oProgressForm.SetProgress(lnBytes/THIS.nContentSize * 100)�������������������������

ENDIF

 

ENDFUNC

��

ENDDEFINE

 

OnHttpBufferUpdate() method is called whenever wwHTTP is downloading a chunk of data from the Web server. The value of lnBufferReads determines how many times the method was called. The first call or 0 returns only the HTTP header which is important to retrieve the length of the data to be downloaded. We need this in order to give the user a n bytes of x downloaded progress window. The code figures out the length by extracting the Content-length header line from the complete HTTP header and storing it in the custom nContentSize property of the class. The code then adjusts the nHTTPWorkBufferSize which determines how much data is read in a single chunk at a time. The bigger the chunk the faster the download proceeds, but the less status events fire.

 

You can capture the end of the request whether it was cancelled by the user or whether it completed successfully with the -1 value for lnBufferReads. In this case the code simply returns � there's nothing to do when the download completes or cancels here.

 

The otherwise clause handles each one of the buffer reads � data is returned from the Web server. In this case we basically want to update the status display. The wwCodeUpdate class uses the wwProgressForm (contained in wwDialogs.vcx) class which is a progress bar display that displays the bytes downloaded and gives an estimate for how long the download will take. Note that the status display is optional and can be turned off by setting the lShowDialog property to .F. on the oHttp member of wwCodeUpdate, but in most cases you'll want the dialog since the download is extensive.

What you need to use wwCodeUpdate

The wwCodeupdate class is fairly self contained, but there are a couple of dependencies that you need to consider.

 

An Internet Connection
Obviously you need an Internet connection to check for the Web version resource and download the update file. If no connection is available the checks will simply fail quickly and not affect your application other than a short delay. Timeouts will determine how long the app waits in the worst case scenario where there is a network connection, but no Internet connection. Use nConnectTimeout to set the length of time waited before timing out.

 

MS Common Dialogs Active X control
MSCOMCTL.OCX is required for the progress bar dialog and should be installed with your application. Most desktop applications use these controls anyway.

 

Windows Scripting Host
This software should be installed on all current versions of Windows starting with Windows 98SE and later and any machine that has IE 5.0 or later installed. Generally you won't have to worry about this, but to be safe you might want to require your application to have IE 5.0 installed. You can check from within your application with whether the Scripting Host is installed by using IsComObject("Wscript.Shell"). IsComObject() is contained in wwUtils which is included in the source code. You can avoid this dependency by creating an Exe file of your own � a VFP Exe or if you don't want any dependencies a C++ or Delphi Exe.

 

A Zip Self-Extractor or the like
The wwCodeUpdate class assumes that you have a self-extracting ZIP file that contains the update. Specifically it relies on WinZip's Self-Extractor, but you can change the command line to fit any other self-extractor in the SwapExe() method of the wwCodeUpdateClass. You can get WinZip from www.winzip.com. The advantage of using a ZIP file is that you can store multiple files and actual directory structures inside of the file. Figure 11 shows what a typical application update looks like.



Figure 3 � Files to be updated can be stored in a WinZip archive with hierarchical directories using the 'Include Subdirectories' options when zipping. Make sure you don't check 'Save full path info'.

 

When you create the Zip file you can include subdirectories as necessary. What I like to do is set up an update directory for my application and copy all the files to update into it, then run WinZip against that to create my Zip file. Check 'Include Subdirectories' and make sure not to check 'Save full path info'.

 

If your structures include files that live outside of the application root hierarchy, you should copy them into the update structure, and then use Post update code to move these files and directories as necessary using Fox code into these external directories as WinZip can only copy files into a specific directory and copy files into directories below it, not above it.

 

After you've created the Zip file you will need to create a self-extracting Zip file and you can use the WinZip Self-Extractor (http://www.winzip.com/winzipse.htm?wzt) for creating a self-extracting Exe. It's very important that you set up the Exe in such a way that no prompts occur and so you should use all the default settings (simply click Next on each step of the Wizard until you see Finish). Once created the Exe can be extracted by using the /auto option to run without user interface.

Quick Checklist

Phew, I've provided a ton of information on updating applications over the Web. With all of this info, realize that your actual work to implement Web updates is pretty small:

 

  1. Create code similar to the code in Listing 1 in your application
    to check for updates and download them.
  2. Copy CodeUpdate.vbs or compile and create CodeUpdate.exe into your application
    directory and ship it with your application.
  3. Update your application's 'hard' version number that you compare against.
  4. Create an update structure on your harddrive that makes up
    all the files that need updating.
  5. Create a Self-Extracting Zip Exe file of your update file(s).
  6. Copy the Self-Extracting file to the Web server in the proper
    Web download directory.
  7. Create or update the XML Web Resource file and copy it
    to the Web server server. See Listing 2 for content.
  8. Test the update process preferably separately from the live version.

 

The last step is crucially important! I highly suggest you set up a test scenario at a different update URL first and test the update process. For testing purposes I like to build the final update file and deploy it then recompile my local application with a lower version number (I use header file constants) by rolling back the version number. This is nice and easy as you can simply rebuild and then re-run the application and update again.

Summing up

A self-updating application adds a professional touch to your application. It also gives you the opportunity to remind your customers/users that you are still alive and have their welfare on your mind.

 

I've presented lots of information on this what would seem to be a simple topic. I hope that you've gotten some useful ideas in how you can easily integrate automatic code updates into your own applications either by using the class I presented here or using a similar mechanism that is hand-coded. Just about any desktop application can benefit from this process and can make the life of any administrator a lot easier by reducing the amount of manual copying and updating that needs to be performed. Have fun with the tools and stay up to date!

Resources 

Source Code

http://www.west-wind.com/presentations/wwCodeUpdate/wwCodeUpdate.zip

 

West Wind Client Tools
HTTP and general application tools for Visual FoxPro applications (shareware)

http://www.west-wind.com/wwClientTools.asp

 

Amazon Honor System Click Here to Pay Learn More

 

 

 

 

 

Rick Strahl is president of West Wind Technologies on Maui, Hawaii. The company specializes in Web and distributed application development and tools with focus on Visual FoxPro and the .Net Framework for Web and distributed technologies. Rick is author of West Wind Web Connection, a powerful and widely used Web application framework for Visual FoxPro and West Wind HTML Help Builder. He's also a Microsoft Most Valuable Professional, and a frequent contributor to magazines and books. He is co-publisher and co-editor of CoDe magazine, and his book, "Internet Applications with Visual FoxPro 6.0", is published by Hentzenwerke Publishing. For more information please visit: http://www.west-wind.com/.
  White Papers                  Home |  White Papers  |  Message Board |  Search |  Products |  Purchase | News |