GadgetChecker: a proposal for updating gadgets automatically

Published Wed, Mar 14 2007 11:24

I've been playing with SideBar gadgets for some time now. Besides some quirks (ok, they're really bugs :) ), I've been please with how easy it is to build a gadget and deploy it. One of the things I've been missing though, is an auto-update feature that automatically checks a web site and downloads a new version (when it's available). Since gadgets run at full trust, you can do several interesting things, like accessing the installation folder and making XML HTTP requests that (in theory, this looks like a security hole). After having played for some hours, I've built a small class that tries to do just that: checks a server and downloads a new version if it's available. I've called it GadgetChecker.

Before going on, you should note that I didn't run exaustive tests on it and I'm not giving any support. So, if you decide to use it and find bugs, I don't give you any garantees of fixes. You can, however,  tweek its code and use it as you like. Now that we've got this straight, lets proceed...

The idea is to have a web site (or virtual dir) which has the manifest file with the latest version available and a zip file with the contents of the gadget. GadgetChecker will always compare the version number of the gadget.xml file that exists on the installation folder with the one that you've got online (currently, the online file must be called gadget.xml - to change that nameyou'll have to change the code). If the online version is newer (ie, has a bigger number), it'll download the zip file and copy its contents to the installation folder of the gadget. After doing that, it'll call a callback method (if is is set).

The first thing we need is to get the current version number of the gadget, the url of the online gadget and the url of the possible zip file with the new version:

function loadGadgetXmlFile(){
  var path = System.Gadget.path + "\\gadget.xml"
  var gadgetDoc = null;
  try{
       gadgetDoc = new ActiveXObject( "Msxml2.DOMDocument.3.0" );
       gadgetDoc.load( path );
       var versionNode = gadgetDoc.selectSingleNode( "/gadget/version" );
       _version = versionNode.text;
       var urlNode = gadgetDoc.selectSingleNode( "/gadget/hosts/host[0]/site/@url");
       _url = urlNode.text;
       var fileNode = gadgetDoc.selectSingleNode( "/gadget/hosts/host[0]/site/@file" );
      _urlFile = fileNode.text;
  }
  catch(ex){
     System.Debug.outputString( ex.message );
  }
  finally{
     gadgetDoc = null;
  }
}

As you can see, I'm just using the MS XML DOMDocument to load the xml contained in the manifest. Btw, note that I've added a custom element (called site) to the 1st host element of the gadget file. This element is supposed to have 2 attributes (url and file) which point to the vdir where the manifest and zip file are (the url of the zip file is obtained by concatenating the url and fle attributes).

After getting these values, we must get the online gadget and compare its version with the one we've just got from the installation file manifest:

function _checkForUpdates(){
  var request = new XMLHttpRequest(); 
  request.onreadystatechange = function(){
    if( request && request.readyState == 4 ){   
       if( request.status != 200 ) {
          return;
       }
       var remoteGadgetManifest = request.responseXML;
       if( remoteGadgetManifest ){
          var newVersionNode = remoteGadgetManifest.selectSingleNode( "/gadget/version" );
          var newVersion = newVersionNode.text;
          var existsNewerVersion = _compareVersions( _version, newVersion );
          if( existsNewerVersion ){
              //download newer version
              var newFilePath = _url;
              if( newFilePath.lastIndexOf( "/" ) !== newFilePath.length - 1){
                 newFilePath += "/"
              }
              var urlFile = newFilePath + _urlFile;
              _donwloadFile( urlFile );
          }
       }
    remoteGadgetManifest = null;
   } 
  }//end of callback function
  var remoteGadgetPath = _url;
  if( remoteGadgetPath.lastIndexOf( "/" ) !== remoteGadgetPath.length - 1){
        remoteGadgetPath += "/"
   }
   remoteGadgetPath += "gadget.xml"
   request.open( "GET", remoteGadgetPath, true );
   request.setRequestHeader( "If-Modified-Since", "Sat, 1 Jan 2000 00:00:00 GMT" );
   request.send();
}

Several things happen in this method:

  • The gadget is obtained through an async XMLHttpRequest method call;
  • The If-Modified-Since header is used to avoid caching (I didn't try it, but I'm assuming it works);
  • The _compareVersions auxiliary method is responsible for comparing the gadgets' versions;
  • When the online gadget has a newer version that the one we've got from the installation dir, it's time to download the new zip file and copy its contents to the gadget installation folder. That's what the _donwloadFile method does.

Let's take a quick peek at the _compareVersions method. It will only perform a comparison if both versions have the same "version parts" and it'll only return true if the second version is newer:

function _compareVersions( version1, version2 ){
  var parts1 = version1.split( "." );
  var parts2 = version2.split( "." );
  if( parts1.length != parts2.length ){
         return false;
  }
  for( var i = 0; i < parts1.length; i++ ){
      var v1 = parseInt( parts1[ i ] );
      var v2 = parseInt( parts2[ i ] );
     if( v1 < v2 ){
         return true;
     }
  }
  return false;
}

The _downloadFile isn't really difficult, as you can see:

function _donwloadFile( fileName ){
    var requestZip = new XMLHttpRequest();
    requestZip.onreadystatechange = function(){
         if( requestZip && requestZip.readyState == 4 ){
             if( requestZip.status != 200 ){
               return;
            }
            var fileName = System.Gadget.path;
            if( fileName.lastIndexOf( "\\" ) < fileName.length ){
                 fileName += "\\"
            }
            fileName += _urlFile;
           _saveFile( fileName, requestZip.responseBody );
      }
    }
    requestZip.open( "GET", fileName, true );
    requestZip.setRequestHeader( "If-Modified-Since", "Sat, 1 Jan 2000 00:00:00 GMT" );
    requestZip.send();
}

The responseBody property is great: it lets you get an array of bytes with the response returned from the server. Saving it to the disk is the tricky part. As the great Eric Lippert said, Binary files and the File System Objects do not mix! Yep, that means that you CANNOT use FSO to save the bytes to disk. Looking at the gadget API, it really looks like you can only read and get items, but can't really create new files. The solution I've used (which, btw, is also presented in the comments of the Eric's post) is to use the ADODB.Stream object:

function _saveFile( fileName, bytes ){
   var fso = null;
   try{
     fso = new ActiveXObject( "ADODB.Stream" ); //garantee that binary data is saved correctly
     fso.Type = 1; //binary
     fso.Open();
     fso.Write( bytes );
     fso.SaveToFile( fileName, 2 );
     //unpack
     _unpackFiles( fileName );
     _deleteFile( fileName );
     if( _cb ){
       _cb();
     }
   }
   catch( ex ){
       System.Debug.outputString( ex.message );
   }
   finally{
      fso = null;
   }
}

Hurray! now, the only thing left is getting the contents of the zip file. As I've said before, the files must be zipped (and not rared, for instance). Why? well, because i can treat zip files like folders, making it get its content a child's play. As you can see from the previous code excerpt, after unpacking the zip contents, the downloaded file is deleted and a callback method (which can be  passed as an argument to the constructor) is called.

The _unpackMethod is simple and it uses the Gadgets API to copy the items to the current gadget installation folder. It'll overwrite all the items (including folders). You do need to garantee that none of the files is "locked" by another process (don't worry about the html file used as the gadget entry point because the sidebar doesn't lock it -at least it didn't in my pc). Here's that method:

function _unpackFiles(fileName){
  var currentFolder = System.Shell.itemFromPath( System.Gadget.path );
  var pathToZip = fileName;
  var folder = System.Shell.itemFromPath( pathToZip );
  for( var i = 0; i < folder.SHFolder.Items.count; i++ ){
     var item = folder.SHFolder.Items.item(i);
     currentFolder.SHFolder.copyHere( item, 16 + 256 + 512 );
  }
}

I guess I could also move the items, instead of copying them. The only thing that you need to know is that the whole process I've described here starts when you create a new GadgetChecker object and call its initialize method.

The demo code has a very simple (almost too simple) gadget that you can use to test the code. To do that, you only need to create or use an existing vdir and, for example, copy the gadget manifest and the updatable.htm file (which is used as the entry point of the gadget) to that folder. Then, change the version number of that manifest file (for instance, set it 1.0.2) and change the message of the html file for something like version 2 (or whatever you want). Then compress both files into a zip file (which you can call test.zip). Note that you'll have to change the url attribute on the gadget.xml file so that it points to the folder url where the gadget file is (another gotcha: using localhost won't work - you need to use the name of the machine if you're hosting the site on the same machine where the gadget is running).

After doing that, install the original gadget (by copying it to the gadgets folrder or changing its extension and double clicking it) and add it to the sidebar. You should get a message saying that a newer version has been downloaded and that you need to restart the sidebar.

Before ending this long post, I must say that I've tried restarting the gadget, but I didn't manage to do it. Calling window.location.reload( true ) resulted in a system crash because the System object went dead while reloading the html page. That's why the simple gadget I've built uses a flyout to alert the user about the new version. Btw, note that you need to exit and restart the gadget in order to get the new version. Closing it and reopening isn't enought to get a refresh.

wow! I may never used, but I can assure you that the 3 hours I've need to build this small class were lots of fun :)

Filed under:

Comments

# andreas said on Monday, September 10, 2007 7:27 AM

trust me it will come to some use :)

great examples

# Peter said on Tuesday, July 27, 2010 5:15 AM

I would REALLY recommend using SSL if deploying this. Otherwise any local computer in the subnet could do a simple arp spoofing, send out a fake update and then execute code on your machine.

# Victor said on Friday, November 19, 2010 3:10 PM

function _compareVersions( version1, version2 ){

var parts1 = version1.split('.');

var parts2 = version2.split('.');

var length;

if( parts1.length>parts2.length ){ length=parts1.length; }

else{ length=parts2.length; }

for(var i=0;i<length;i++){

if(parts1[i]==undefined){ parts1[i]=0; }

if(parts2[i]==undefined){ parts2[i]=0; }

var v1=parseInt(parts1[i]);

var v2=parseInt(parts2[i]);

if(v1<v2){ return true; }

}

return false;

}

# Victor said on Thursday, January 13, 2011 12:17 AM

awesome post.

tou can restart the sidebar using this: I write this and works perfectly for me.

System.Shell.execute("cmd","/c taskkill -f -im sidebar.exe & start sidebar.exe");

the demo link is broken, please fix it

[code]

function restartSidebar(){

var command;

command ="@echo off"

command+="&title Restarting SideBar"

command+="&color F0"

command+="&echo. Updating Windows Desktop Gadgets..."

command+="&taskkill -f -im sidebar.exe"

command+="&start sidebar.exe"

System.Shell.execute("cmd","/c "+command);

}

restartSidebar();

[/code]

# Taher said on Thursday, January 13, 2011 12:26 AM

Can you please post the _deleteFile() code.. I am unable to download the demo code.

Anyways, nice example. Helped me a lot.. Thanks :)

# FidoBoy said on Monday, March 28, 2011 5:52 PM

Really helpful tutorial. But the demo code link is broken... can anyone re-upload it?

regards,

Leave a Comment

(required) 
(required) 
(optional)
(required) 
If you can't read this number refresh your screen
Enter the numbers above:  

Search

This Blog

Tags

Community

Archives

Syndication

Email Notifications

News




  • View Luis Abreu's profile on LinkedIn


    Follow me at Twitter

    My books

    Silverlight 4.0: Curso Completo

    ASP.NET 4.0: Curso Completo

    Portuguese LINQ book cover

    Portuguese ASP.NET 3.5 book cover

    Portuguese ASP.NET AJAX book cover

    Portuguese ASP.NET AJAX book cover