Archive for November, 2008

How to Write an Auto-Updater System in Java

Occasionally I do things where I spend a large portion of time looking up tidbits of code from various sources to string together something really cool. Usually I wish someone had documented how to do them so I wouldn’t have to jump around from site to site looking for things. This is one such time. I wanted to write an auto-updater for a java program, distributed in a jar file. Although this may sound somewhat complicated, it’s actually fairly simple, especially when your entire program is just one jar file. More programs should implement something like this, but I digress.

Here are the steps necessary for a complete autoupdating system. I will elaborate sometime in the near future, with code examples.

Step 0: Automate your build process

This is optional, but makes things easier. Basically, you can set up a build script using Ant that you can just double click to compile, assign a revision number, jar, and then upload your program to a webserver. Ant is an XML based build system, and since I’m not really familiar with build scripts I was surprised at how powerful and easy to use it is. I won’t go into depth about Ant, but this is a good link for getting started. Basically, you use XML to describe a set of tasks you want it to do. These groups of tasks are called targets. Of course, Eclipse, my editor of choice, has excellent Ant support, so editing is really easy, properties are autocompleted, and running scripts is just a double click on the desired target. There are a couple things that are of specific use for an auto-updater, such as finding the current revision of your code (in my case, from SVN, although you could use anything really).

<target name="find_revision" description="Sets property 'svn.revision' to the head svn revision"> <taskdef resource="org/tigris/subversion/svnant/svnantlib.xml" /> <svn> <status path="src" revisionProperty="svn.revision" /> </svn> <echo>Revision: ${svn.revision}</echo> <echo file="revision.txt" append="false">${svn.revision}</echo> </target>

This code grabs the revision number from SVN, using the SVNAnt plugin, and then stores it in a file called revision.txt, as well as the variable svn.revision.

 

<target name="create_run_jar" depends="build,find_revision"> <jar destfile="foo.jar" filesetmanifest="mergewithoutmain"> <manifest> <attribute name="Built-By" value="${user.name}" /> <attribute name="Main-Class" value="package.MainClass" /> <attribute name="Class-Path" value="." /> <attribute name="Implementation-Vendor" value="The Team" /> <attribute name="Implementation-Title" value="The Program" /> <attribute name="Implementation-Version" value="${version.num}r${svn.revision}" /> </manifest> <fileset dir="classes" /> <fileset file="revision.txt" /> </jar> </target>

This bit packages the code into an executable jar file. It also grabs revision.txt, so the software can figure out what revision it is. I had a similar jar for my updater, since if your program is running you can’t overwrite it, at least on Windows, so you need to use a separate executable to actually download the update.

<target name="copy_to_server" depends="find_revision,create_run_jar,create_updater_jar"> <input message="Please enter username:" addproperty="scpuser" /> <input message="Please enter password:" addproperty="scppw" /> <scp file="foo.jar" trust="yes" remoteToFile="${scpuser}:${scppw}@my.server.com:~/public_html/stuff/foor${svn.revision}.jar"> </scp> <scp file="updater.jar" trust="yes" remoteToFile="${scpuser}:${scppw}@my.server.com:~/public_html/stuff/updater.jar"> </scp> <sshexec host="my.server.com" username="${scpuser}" password="${scppw}" trust="yes" command="chmod 644 ~/public_html/stuff/foor${svn.revision}.jar" /> <sshexec host="my.server.com" username="${scpuser}" password="${scppw}" trust="yes" command="ln -s -f ~/public_html/stuff/foor${svn.revision}.jar ~/public_html/stuff/foo.jar" /> <scp file="revision.txt" trust="yes" todir="${scpuser}:${scppw}@my.server.com:~/public_html/stuff"> </scp> </target>

This part copies the files to the server, using scp. SCP is the secure file copy client, part of SSH. There’s probably a plugin to use FTP too, if that’s your thing. The thing about SCP is that it gives files default permissions of 0600, meaning they aren’t world readable, no matter what your umask is, so you need to follow it up with an ssh command to chmod the file to the proper permissions, probably 644. Just for the sake of keeping old revisions on the server, instead of uploading something to foo.jar I upload to foor${svn.revision} and then create a symlink from the latest file to foo.jar.

Step 1: Store your local version somewhere

In my case, I stored it inside the jar file in a text file called revision.txt using the Ant script. Later, you can get at it by using this little bit of code.

public int currentRevision(){ BufferedReader is; try { is = new BufferedReader( new InputStreamReader(ClassLoader.getSystemResource("revision.txt").openStream())); int rev = Integer.valueOf(is.readLine()); return rev; } catch(NullPointerException e){ }catch (Exception e) { e.printStackTrace(); } return 1<<31-1; }

This checks inside the jar file for a revision.txt, and then parses it to get the current revision. If no file is found, it returns the largest possible value for an int, to avoid problems with trying to update needlessly.

Step 2: Host updates on a server, along with a latest version number

I used a remote revision.txt to check against the local one. You can check the latest revision using this snippet of code:

public int latestRevision(){ URL url; try { url = new URL("http://my.server.com/~me/stuff/revision.txt"); HttpURLConnection hConnection = (HttpURLConnection) url .openConnection(); HttpURLConnection.setFollowRedirects(true); if (HttpURLConnection.HTTP_OK == hConnection.getResponseCode()) { BufferedReader is = new BufferedReader(new InputStreamReader(hConnection.getInputStream())); int rev = Integer.valueOf(is.readLine()); return rev; } }catch(IOException e){ e.printStackTrace(); } return -1; }

This simply downloads a file from the server and returns the int value of the file. All uploading tasks are taken care of by the Ant script, which you would run every time you want to publish a new revision. You would want to compare the latest revision with your revision at some time that makes sense for your program, probably at startup. If you find that there’s a new version, notify the user that there is an update available, and then start the updater and exit yourself (System.exit(0);).

Step 3: Write a downloader stub that downloads the new version

Since you can’t write to an open file (at least on Windows) you need to download the new version in a separate process and close the current one. I used a specialized class just for downloading the file. I also included a general purpose method for downloading files in case other parts need to download anything. This is used in another class to download the updater.jar, in case it doesn’t exist. You might find that you need to update the updater, in which case you can just skip the check for an existing updater, or go through the trouble of versioning the updater too. It’s not that difficult, but probably not really worth it when the updater.jar is around 3kb.

package updater; import java.awt.Dimension; import java.io.BufferedOutputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.HttpURLConnection; import java.net.URL; import javax.swing.JFrame; import javax.swing.JProgressBar; public class Updater extends JFrame{ String updateurl; JProgressBar progress; public static void main(String[] args){ Updater up = new Updater("http://my.server.com/~me/stuff/foo.jar"); up.downloadLatestVersion(); try { Process foo = Runtime.getRuntime().exec("java -jar foo.jar"); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.exit(0); } public Updater(String url){ updateurl = url; this.setPreferredSize(new Dimension(300, 80)); this.setSize(new Dimension(300, 80)); this.setTitle("Updater"); progress = new JProgressBar(0,100); progress.setValue(0); progress.setStringPainted(true); this.add(progress); this.setLocationRelativeTo(null); this.setVisible(true); this.requestFocus(true); } void downloadLatestVersion(){ URL url; try { url = new URL(updateurl); HttpURLConnection hConnection = (HttpURLConnection) url .openConnection(); HttpURLConnection.setFollowRedirects(true); if (HttpURLConnection.HTTP_OK == hConnection.getResponseCode()) { InputStream in = hConnection.getInputStream(); BufferedOutputStream out = new BufferedOutputStream( new FileOutputStream("foo.jar")); int filesize = hConnection.getContentLength(); progress.setMaximum(filesize); byte[] buffer = new byte[4096]; int numRead; long numWritten = 0; while ((numRead = in.read(buffer)) != -1) { out.write(buffer, 0, numRead); numWritten += numRead; System.out.println((double)numWritten/(double)filesize); progress.setValue((int) numWritten); } if(filesize!=numWritten) System.out.println("Wrote "+numWritten+" bytes, should have been "+filesize); else System.out.println("Downloaded successfully."); out.close(); in.close(); } }catch(IOException e){ e.printStackTrace(); } } public static void downloadFile(String sourceurl, String dest){ URL url; try { url = new URL(sourceurl); HttpURLConnection hConnection = (HttpURLConnection) url .openConnection(); HttpURLConnection.setFollowRedirects(true); if (HttpURLConnection.HTTP_OK == hConnection.getResponseCode()) { InputStream in = hConnection.getInputStream(); BufferedOutputStream out = new BufferedOutputStream( new FileOutputStream(dest)); int filesize = hConnection.getContentLength(); byte[] buffer = new byte[4096]; int numRead; long numWritten = 0; while ((numRead = in.read(buffer)) != -1) { out.write(buffer, 0, numRead); numWritten += numRead; System.out.println((double)numWritten/(double)filesize); } if(filesize!=numWritten) System.out.println("Wrote "+numWritten+" bytes, should have been "+filesize); else System.out.println("Downloaded successfully."); out.close(); in.close(); } }catch(IOException e){ e.printStackTrace(); } } }

This code is pretty easy to follow, and half of it is essentially the same method duplicated for reuse in other places. In hindsight, I probably could have statically downloaded the update file, but then I wouldn’t be able to use the nice progress bar.

Step 4: Restart with the new version

Once you’ve downloaded the new version, the updater will simply start the new version and quit. And then, you’ve got an automatic updater system that is pretty simple and easy to use. Thanks to jar files, you won’t even have to worry about grabbing a large number of files, although if you really wanted to save bandwidth you could do something where you split up your code and resources between jar files. In my case it wouldn’t really matter since my entire program, including resources, is under 500kb, and if it was just the code by itself it would download before the progress bar even showed up. As for my project, maybe I’ll reveal it in another post once I clear up some issues.

Advertisements