This is a tutorial on Script version checking and updating
tutorial by -dt- (Matt Labrum)
So you’ve made a script, and you want to make sure your users have the most current version of your script.
Well you’ve come to the right place. This tutorial is about exactly that!
Now I’m going to assume a few things
1) you’re not a complete noob
2) you’re not segosa
3) you have a script (If you don’t just create a new one)
4) the script must have a ScriptInfo.xml file
5) somewhere online where you can upload files
Now that thats out of the way, we can get to the fun part
A few reference documents for later:
XMLHttpRequest – http://msdn2.microsoft.com/en-us/library/ms535874.aspx
XMLDomDocument – http://msdn2.microsoft.com/en-us/library/ms757878.aspx
Now open up your ScriptInfo.xml file it should look something like the one shown below
updateExample This script is an example script to test update checking http://blog.thedt.net 1.00
You should notice the Version tag, this is one of the core tags needed in our update checker. You should also notice that there aren’t any tags to specify an update location.
This is a problem since our update checker needs to find the new version information somewhere, so lets add a section in our ScriptInfo.xml and add some updater tags
updateExample This script is an example script to test update checking http://blog.thedt.net 1.00 http://yoursite.com/updateExample/ScriptInfo.xml http://yoursite.com/updateExample/updateExample.plsc - First release 10/11/2007
Ok so now we have alot more information for our script to use, but I bet you’re looking at me like “dt what the hell are all those tags” well
I’ll explain each one
ScriptInfoUrl – the location of the updated scriptInfo.xml (our checker will download this file and compare it to the current one)
PlscLocation – Plsc to download if our update checker detects that we need updating
ReleaseNotes – optional, the changelog to display to the user
DateReleased – optional, the date the new version was released
Now i bet you’re wondering how the update checker will work. Well, inside your script’s OnEvent_Initialize you’ll trigger UpdateChecker.Check(), which will then download the update xml (gets this value from Update/ScriptInfoUrl).
Once the updateXML is downloaded it will trigger UpdateChecker.parseUpdate() which then compares the local ScriptInfo.xml tag with the downloaded ScriptInfo.xml tag.
If it’s a newer version then the script will display a dialog to the user asking them if they want to update, if they say yes then your script will download the plsc file and run it.
Diagram of how all this fits together
(click for a bigger image)
Now to make the javascript for the above process
Create a new file called UpdateChecker.js
Inside that file we’ll create an Object called UpdateChecker, with the functions we need
var UpdateChecker = function(){ this.Check(); }; UpdateChecker.prototype = { "Check" : function(){}, //This function will parse the local XML file and trigger the download the update ScriptInfo.xml "ParseScriptInfo" : function(xml){}, //returns an object with the parsed ScriptInfo information "AskUser" : function(){}, "GetNewPlsc" : function(){} }
Now we need to start filling the functions and making it do something
We will start with the checking code
"Check" : function(){ //This function will parse the local XML file and trigger the download the update ScriptInfo.xml var scriptInfo = this.ParseScriptInfo(); //will return an object in the form of {"Version" : 1.0, "UpdateUrl" : "http://.../scriptInfo.xml", "ReleaseNotes" : "", "DateReleased" : ""} this.details = scriptInfo; //add the details to the object, incase the script wants to use this for something Interop.Call("Wininet.dll", "DeleteUrlCacheEntryW", scriptInfo.UpdateUrl); //make sure theres no cached version of the update var xmlhttp = new ActiveXObject("Microsoft.XMLHTTP"); // Create a new XMLHttp request object to download our update ScriptInfo.xml // see http://msdn2.microsoft.com/en-us/library/ms536648.aspx for details on the open method xmlhttp.open("GET", scriptInfo.UpdateUrl, true) //open a new xmlhttp request to the updateUrl and set it to asynchronous operation var thisd = this; //create a reference to the current scope to use in the callback //see http://msdn2.microsoft.com/en-us/library/ms534308.aspx for more information on the onreadystatechange callback xmlhttp.onreadystatechange = function(){ if(xmlhttp.readyState == 4){ var updateInfo = thisd.ParseScriptInfo(xmlhttp.responseXML); //parses the update scriptInfo and returns an object if(updateInfo.Version > scriptInfo.Version){ //if version is newer Debug.Trace("Update is newer"); }else{ Debug.Trace("Update Is older"); } } } xmlhttp.send(); }
Now this can’t do anything on its own because it requires the object from ParseScriptInfo, so we will implement that and then test the script
"ParseScriptInfo" : function(xml){ //returns an object with the parsed ScriptInfo information if(typeof(xml) == "undefined"){ //if no xml is passed, we will load the local script info var xml = new ActiveXObject("Microsoft.XMLDOM"); xml.async = false; xml.load(MsgPlus.ScriptFilesPath + "\\" + "ScriptInfo.xml"); } //object to return var ob = { "Version" : 0, "UpdateUrl" : "", "PlscLocation" : "", "ReleaseNotes" : "", "DateReleased" : "" }; //get the Update node values ob.UpdateUrl = xml.selectSingleNode("//Update/ScriptInfoUrl").text; ob.PlscLocation = xml.selectSingleNode("//Update/PlscLocation").text; ob.ReleaseNotes = xml.selectSingleNode("//Update/ReleaseNotes").text; ob.DateReleased = xml.selectSingleNode("//Update/DateReleased").text; var version = xml.selectSingleNode("//Version").text; //convert the version to a real number, parseInt chops off the decimal points so we cheat and * it by 1 ob.Version = version * 1; return ob; //return our object }
The full code so far
var UpdateChecker = function(){ this.Check(); }; UpdateChecker.prototype = { "Check" : function(){ //This function will parse the local XML file and trigger the download the update ScriptInfo.xml var scriptInfo = this.ParseScriptInfo(); //will return an object in the form of {"Version" : 1.0, "UpdateUrl" : "http://.../scriptInfo.xml", "ReleaseNotes" : "", "DateReleased" : ""} this.details = scriptInfo; //add the details to the object, incase the script wants to use this for something Interop.Call("Wininet.dll", "DeleteUrlCacheEntryW", scriptInfo.UpdateUrl); //make sure theres no cached version of the update var xmlhttp = new ActiveXObject("Microsoft.XMLHTTP"); // Create a new XMLHttp request object to download our update ScriptInfo.xml // see http://msdn2.microsoft.com/en-us/library/ms536648.aspx for details on the open method xmlhttp.open("GET", scriptInfo.UpdateUrl, true) //open a new xmlhttp request to the updateUrl and set it to asynchronous operation var thisd = this; //create a reference to the current scope to use in the callback //see http://msdn2.microsoft.com/en-us/library/ms534308.aspx for more information on the onreadystatechange callback xmlhttp.onreadystatechange = function(){ //because this refers the function we have to use thisd when refering to our class if(xmlhttp.readyState == 4){ var updateInfo = thisd.ParseScriptInfo(xmlhttp.responseXML); //parses the update scriptInfo and returns an object if(updateInfo.Version > scriptInfo.Version){ //if version is newer Debug.Trace("Update is newer"); }else{ Debug.Trace("Update Is older"); } } } xmlhttp.send(); }, "ParseScriptInfo" : function(xml){ //returns an object with the parsed ScriptInfo information if(typeof(xml) == "undefined"){ //if no xml is passed, we will load the local script info var xml = new ActiveXObject("Microsoft.XMLDOM"); xml.async = false; xml.load(MsgPlus.ScriptFilesPath + "\\" + "ScriptInfo.xml"); } //object to return var ob = { "Version" : 0, "UpdateUrl" : "", "PlscLocation" : "", "ReleaseNotes" : "", "DateReleased" : "", "Name" : "" }; //get the Update node values ob.UpdateUrl = xml.selectSingleNode("//Update/ScriptInfoUrl").text; ob.PlscLocation = xml.selectSingleNode("//Update/PlscLocation").text; ob.ReleaseNotes = xml.selectSingleNode("//Update/ReleaseNotes").text; ob.DateReleased = xml.selectSingleNode("//Update/DateReleased").text; ob.Name = xml.selectSingleNode("//Name").text; var version = xml.selectSingleNode("//Version").text; //convert the version to a real number, parseInt chops off the decimal points so we cheat and * it by 1 ob.Version = version * 1; return ob; //return our object }, "AskUser" : function(){}, "GetNewPlsc" : function(){} }
Now if you want to test the above code, add the following to your OnEvent_Initialize
function OnEvent_Initialize(){ var uc = new UpdateChecker(); }
Make sure you have a copy of your ScriptInfo.xml uploaded at the location specified at ScriptInfoUrl, otherwise the code will fail.
Now run the script, if your uploaded ScriptInfo.xml file has a greater version than the local ScriptInfo.xml it should display “Update is newer”
Ok, so now we need to do something if the update is newer, we will call the UpdateChecker.AskUser() method to ask the user if they want to update.
We will just display a messagebox with yes or no buttons, in your own script I recommend that you create a fancy GUI
"AskUser" : function(updateInfo){ //we will just display a simple yes/no messagebox, in your own version you could take this a step futher and display a pretty plus GUI for it, but thats outside the scope of this tutorial var message = "A new Script Update has been detected for " + updateInfo.Name + " Do you want to update?"; var result = Interop.Call("User32", "MessageBoxW", 0, message, "Script Update", 4 /* MB_YESNO */); if(result == 6){ //a result of 6 means that the user pressed "yes" this.GetNewPlsc(updateInfo.PlscLocation); //call the GetNewPlsc function to download the file } }
And finally we need to create the GetNewPlsc function to open the Plsc file.
Now in your own scripts you should use MsgPlus.DownloadFile to download the plsc and then use
new ActiveXObject("WScript.Shell").Run('"downloadlocation"');
to run it
but in this example we will simply execute the url
"GetNewPlsc" : function(url){ //function to download the plsc //execute the url new ActiveXObject("WScript.Shell").Run('"' + url + '"'); }
Ok so lets look at the code so far.
It should download the new ScriptInfo.xml and compare the versions, if theres an update then it will open the PlscLocation
var UpdateChecker = function(){ this.Check(); }; UpdateChecker.prototype = { "Check" : function(){ //This function will parse the local XML file and trigger the download the update ScriptInfo.xml var scriptInfo = this.ParseScriptInfo(); //will return an object in the form of {"Version" : 1.0, "UpdateUrl" : "http://.../scriptInfo.xml", "ReleaseNotes" : "", "DateReleased" : ""} this.details = scriptInfo; //add the details to the object, incase the script wants to use this for something Interop.Call("Wininet.dll", "DeleteUrlCacheEntryW", scriptInfo.UpdateUrl); //make sure theres no cached version of the update var xmlhttp = new ActiveXObject("Microsoft.XMLHTTP"); // Create a new XMLHttp request object to download our update ScriptInfo.xml // see http://msdn2.microsoft.com/en-us/library/ms536648.aspx for details on the open method xmlhttp.open("GET", scriptInfo.UpdateUrl, true) //open a new xmlhttp request to the updateUrl and set it to asynchronous operation var thisd = this; //create a reference to the current scope to use in the callback //see http://msdn2.microsoft.com/en-us/library/ms534308.aspx for more information on the onreadystatechange callback xmlhttp.onreadystatechange = function(){ //because this refers the function we have to use thisd when refering to our class if(xmlhttp.readyState == 4){ var updateInfo = thisd.ParseScriptInfo(xmlhttp.responseXML); //parses the update scriptInfo and returns an object if(updateInfo.Version > scriptInfo.Version){ //if version is newer Debug.Trace("Update is newer"); thisd.AskUser(updateInfo); }else{ Debug.Trace("Update Is older"); } } } xmlhttp.send(); }, "ParseScriptInfo" : function(xml){ //returns an object with the parsed ScriptInfo information if(typeof(xml) == "undefined"){ //if no xml is passed, we will load the local script info var xml = new ActiveXObject("Microsoft.XMLDOM"); xml.async = false; xml.load(MsgPlus.ScriptFilesPath + "\\" + "ScriptInfo.xml"); } //object to return var ob = { "Version" : 0, "UpdateUrl" : "", "PlscLocation" : "", "ReleaseNotes" : "", "DateReleased" : "", "Name" : "" }; //get the Update node values ob.UpdateUrl = xml.selectSingleNode("//Update/ScriptInfoUrl").text; ob.PlscLocation = xml.selectSingleNode("//Update/PlscLocation").text; ob.ReleaseNotes = xml.selectSingleNode("//Update/ReleaseNotes").text; ob.DateReleased = xml.selectSingleNode("//Update/DateReleased").text; ob.Name = xml.selectSingleNode("//Name").text; var version = xml.selectSingleNode("//Version").text; //convert the version to a real number, parseInt chops off the decimal points so we cheat and * it by 1 ob.Version = version * 1; return ob; //return our object }, "AskUser" : function(updateInfo){ //we will just display a simple yes/no messagebox, in your own version you could take this a step futher and display a pretty plus GUI for it, but thats outside the scope of this tutorial var message = "A new Script Update has been detected for " + updateInfo.Name + " Do you want to update?"; var result = Interop.Call("User32", "MessageBoxW", 0, message, "Script Update", 4 /* MB_YESNO */); if(result == 6){ //a result of 6 means that the user pressed "yes" this.GetNewPlsc(updateInfo.PlscLocation); } }, "GetNewPlsc" : function(url){ //function to download the plsc //now in your own scripts you should use MsgPlus.DownloadFile to download the plsc and new ActiveXObject("WScript.Shell").Run('"downloadlocation"'); to run it //but in this example we will simply execute the url new ActiveXObject("WScript.Shell").Run('"' + url + '"'); } }
and your OnEvent_Initialize
function OnEvent_Initialize(){ var uc = new UpdateChecker(); }
This is a basic updater, which works but it could be improved quite a bit. One such modification could be to only check for a new version every 24 hours. Let’s modify the above code to add such a feature.
On first update we store the time into the registry, on the next update if(time() > stored_time + timeToWait) we will check for an update and rewrite the time else we do nothing.
That sounds simple enough right?
Below are the links to the functions that we will be using to read and write to the registry
RegRead – http://msdn2.microsoft.com/en-us/library/x05fawxd.aspx
RegWrite – http://msdn2.microsoft.com/en-us/library/yfdfhz1b.aspx
First we need to know how to read and write registry keys before we can continue.
Checking if a key exists, reading it and writing to the registry.
try{ //RegRead will throw an error if it can't read the path var lastUpdate = shell.RegRead(path) * 1; //convert it to a number //if curtime is less than lastUpdate + timeToWait then we return and dont check if(curTime < lastUpdate){ Debug.Trace("no update"); return; } else{ shell.RegWrite(path, curTime + timeToWait); // if it is, we write the new time and let it check for an update } }catch(e){ //key doesn't exist so we will write it for the first time shell.RegWrite(path, curTime + timeToWait); //write time() + timeToWait to the registry } Debug.Trace("Update");
Now we can implement this into the Check function of our UpdateChecker
"Check" : function(){ //This function will parse the local XML file and trigger the download the update ScriptInfo.xml var scriptInfo = this.ParseScriptInfo(); //will return an object in the form of {"Version" : 1.0, "UpdateUrl" : "http://.../scriptInfo.xml", "ReleaseNotes" : "", "DateReleased" : ""} this.details = scriptInfo; //add the details to the object, incase the script wants to use this for something // we check if we need to update here after the local script info is stored inside the object var shell = new ActiveXObject("WScript.Shell"); var path = MsgPlus.ScriptRegPath + "lastUpdateTime"; var curTime = new Date().getTime(); var timeToWait = 24 * 60 * 60 * 1000; //time to wait in milliseconds try{ //RegRead will throw an error if it can't read the path var lastUpdate = shell.RegRead(path) * 1; //convert it to a number //if curtime is less than lastUpdate + timeToWait then we return and dont check if(curTime < lastUpdate){ Debug.Trace("no update"); return; } else{ shell.RegWrite(path, curTime + timeToWait); // if it is, we write the new time and let it check for an update } }catch(e){ //key doesn't exist so we will write it for the first time shell.RegWrite(path, curTime + timeToWait); //write time() + timeToWait to the registry } Interop.Call("Wininet.dll", "DeleteUrlCacheEntryW", scriptInfo.UpdateUrl); //make sure theres no cached version of the update var xmlhttp = new ActiveXObject("Microsoft.XMLHTTP"); // Create a new XMLHttp request object to download our update ScriptInfo.xml // see http://msdn2.microsoft.com/en-us/library/ms536648.aspx for details on the open method xmlhttp.open("GET", scriptInfo.UpdateUrl, true) //open a new xmlhttp request to the updateUrl and set it to asynchronous operation var thisd = this; //create a reference to the current scope to use in the callback //see http://msdn2.microsoft.com/en-us/library/ms534308.aspx for more information on the onreadystatechange callback xmlhttp.onreadystatechange = function(){ //because this refers the function we have to use thisd when refering to our class if(xmlhttp.readyState == 4){ var updateInfo = thisd.ParseScriptInfo(xmlhttp.responseXML); //parses the update scriptInfo and returns an object if(updateInfo.Version > scriptInfo.Version){ //if version is newer Debug.Trace("Update is newer"); thisd.AskUser(updateInfo); }else{ Debug.Trace("Update Is older"); } } } xmlhttp.send(); },
And the complete code
var UpdateChecker = function(){ this.Check(); }; UpdateChecker.prototype = { "Check" : function(){ //This function will parse the local XML file and trigger the download the update ScriptInfo.xml var scriptInfo = this.ParseScriptInfo(); //will return an object in the form of {"Version" : 1.0, "UpdateUrl" : "http://.../scriptInfo.xml", "ReleaseNotes" : "", "DateReleased" : ""} this.details = scriptInfo; //add the details to the object, incase the script wants to use this for something // we check if we need to update here after the local script info is stored inside the object var shell = new ActiveXObject("WScript.Shell"); var path = MsgPlus.ScriptRegPath + "lastUpdateTime"; var curTime = new Date().getTime(); var timeToWait = 24 * 60 * 60 * 1000; //time to wait in milliseconds try{ //RegRead will throw an error if it can't read the path var lastUpdate = shell.RegRead(path) * 1; //convert it to a number //if curtime is less than lastUpdate + timeToWait then we return and dont check if(curTime < lastUpdate){ Debug.Trace("no update"); return; } else{ shell.RegWrite(path, curTime + timeToWait); // if it is, we write the new time and let it check for an update } }catch(e){ //key doesn't exist so we will write it for the first time shell.RegWrite(path, curTime + timeToWait); //write time() + timeToWait to the registry } Interop.Call("Wininet.dll", "DeleteUrlCacheEntryW", scriptInfo.UpdateUrl); //make sure theres no cached version of the update var xmlhttp = new ActiveXObject("Microsoft.XMLHTTP"); // Create a new XMLHttp request object to download our update ScriptInfo.xml // see http://msdn2.microsoft.com/en-us/library/ms536648.aspx for details on the open method xmlhttp.open("GET", scriptInfo.UpdateUrl, true) //open a new xmlhttp request to the updateUrl and set it to asynchronous operation var thisd = this; //create a reference to the current scope to use in the callback //see http://msdn2.microsoft.com/en-us/library/ms534308.aspx for more information on the onreadystatechange callback xmlhttp.onreadystatechange = function(){ //because this refers the function we have to use thisd when refering to our class if(xmlhttp.readyState == 4){ var updateInfo = thisd.ParseScriptInfo(xmlhttp.responseXML); //parses the update scriptInfo and returns an object if(updateInfo.Version > scriptInfo.Version){ //if version is newer Debug.Trace("Update is newer"); thisd.AskUser(updateInfo); }else{ Debug.Trace("Update Is older"); } } } xmlhttp.send(); }, "ParseScriptInfo" : function(xml){ //returns an object with the parsed ScriptInfo information if(typeof(xml) == "undefined"){ //if no xml is passed, we will load the local script info var xml = new ActiveXObject("Microsoft.XMLDOM"); xml.async = false; xml.load(MsgPlus.ScriptFilesPath + "\\" + "ScriptInfo.xml"); } //object to return var ob = { "Version" : 0, "UpdateUrl" : "", "PlscLocation" : "", "ReleaseNotes" : "", "DateReleased" : "", "Name" : "" }; //get the Update node values ob.UpdateUrl = xml.selectSingleNode("//Update/ScriptInfoUrl").text; ob.PlscLocation = xml.selectSingleNode("//Update/PlscLocation").text; ob.ReleaseNotes = xml.selectSingleNode("//Update/ReleaseNotes").text; ob.DateReleased = xml.selectSingleNode("//Update/DateReleased").text; ob.Name = xml.selectSingleNode("//Name").text; var version = xml.selectSingleNode("//Version").text; //convert the version to a real number, parseInt chops <pre lang="LANGUAGE"> off the decimal points so we cheat and * it by 1 ob.Version = version * 1; return ob; //return our object }, "AskUser" : function(updateInfo){ //we will just display a simple yes/no messagebox, in your own version you could take this a step futher and display a pretty plus GUI for it, but thats outside the scope of this tutorial var message = "A new Script Update has been detected for " + updateInfo.Name + " Do you want to update?"; var result = Interop.Call("User32", "MessageBoxW", 0, message, "Script Update", 4 /* MB_YESNO */); if(result == 6){ //a result of 6 means that the user pressed "yes" this.GetNewPlsc(updateInfo.PlscLocation); } }, "GetNewPlsc" : function(url){ //function to download the plsc //now in your own scripts you should use MsgPlus.DownloadFile to download the plsc and new ActiveXObject("WScript.Shell").Run('"downloadlocation"'); to run it //but in this example we will simply execute the url new ActiveXObject("WScript.Shell").Run('"' + url + '"'); } }
and your OnEvent_Initialize
function OnEvent_Initialize(){ var uc = new UpdateChecker(); }
There you have it kids
, a complete update checker that only checks for a new version every 24 hours.
You can download the complete plsc here
http://random.thedt.net/tutorial/scriptUpdating/updateExample.plsc
I recommend that you expand on this and replace the MessageBox in UpdateChecker.AskUser with a nice Plus GUI and in GetNewPlsc replace that with MsgPlus.DownloadFile
I might create another tutorial later to explain how to implement those
This is my first time writing a tutorial like this, I would love feedback on it
did you like/hate it?
If you want to repost this somewhere please contact me (either by leaving a comment or some other crazy way) so we can work out the details
A special thanks to Sunshine for helping me with the grammar and spelling issues
Leave a comment