Java Getriebe

Java und NetBeans

Selbstaktualisierende Anwendungen

Bei heutige Software wird vom Anwender schon fast vorausgesetzt, dass die Anwendung automatisch „im Internet“ nachschaut ob eine neuere Version seiner Selbst vorhanden ist und im positiven Fall dann auch automatisch eine Aktualisierung von sich selbst auf dem lokalen Rechner vornimmt. Allerdings stellt sich diese „Selbstverständlichkeit“ in der Praxis oft als erst zu nehmendes Problem heraus: Wie bitte schön, soll ich eine Datei (also die Jar-Datei) mit neuem Inhalt befüllen, wenn sie gerade geöffnet und für Änderungen gesperrt ist? Schließlich werden die .class-Datei aus der Datei gerade von der Java-VM verwendet. Vielleicht ist dies auch ein Grund, warum es keine Standardlösung von Java selbst existiert um mit dieser Problematik umzugehen.

Größere Programmsysteme wie die IDEs NetBeans oder Eclipse nutzen für solche Zwecke so genannte „Starter“, die nicht innerhalb der VM ausgeführt werden sonder selbige Starten und bei Beenden der VM auf „Ergebnisse“ reagieren können. In einem solchen Fall kann die Java-VM erst alle notwendigen Updatedateien aus dem Netz herunterladen und sich selbst beenden. Der „Starter“ erkennt daraufhin, dass Updatedateien vorliegen, überschreibt die Programmdateien mit den heruntergeladenen Dateien und starten danach einfach das Programm neu. Beim Überschreiben ist also keine VM mehr aktiv, die die Dateien sperren könnte und der ganze Prozess läuft ohne weitere Schwierigkeiten ab.

Hat man sein eigenes kleines Programm mit nur einer einzelnen Jar-Datei hat man oft keinen solchen „Starter“, der das Update durchführen könnte. Da mich dieser Umstand zum einen schon eine ganze Weile gestört hat und ich zum Anderen auch wissen wollte, ob so etwas überhaupt machbar ist, habe ich eine kleine Klasse geschrieben, die das Updateproblem löst. Ich werde hier nur den Grundlegenden Ablauf des Prozesses beschreiben. Den Quelltext selbst habe ich auf Github hochgeladen, so das jeder lustig Erweiterungen daran vornehmen kann. Die Klasse ist unter der LGPL freigegeben.

Der gesamte Prozess unterteilt sich grob in 2 Phasen:

  1. Die Vorbereitung des Updaters.
  2. Der Download der aktualisieren Daten und Neustart der Anwendung

Bei der Vorbereitung des Updaters kopiert sich die Klasse selbst in eine neue temporären Jar-Datei. Diese Jar-Datei wird danach einfach in einer neuen Java-VM mit verschiedenen Parametern gestartet die für den Download und Neustart der Anwendung benötigt werden. Diese zweite Phase läuft also nicht mehr in der alten VM ab, die den Updateprozess initiiert hatte. Die alte VM muss sich nach dem Aufruf der update()-Methode zwingend selbst beenden, ansonsten kann das Update nicht erfolgreich durchgeführt werden. Der Update-Aufruf sollte aus direkt(!) gefolgt werden von einem System.exit(). Alle Abfragen an den Anwender, ob zum Beispiel ungespeicherte Daten gesichert werden sollen, sollten vor dem Update durchgeführt worden sein.

Zu Beginn der zweiten Phase wird versucht die bisherige Jar-Datei zu sichern. Wenn dies nach mehreren Versuchen nicht erfolgreich war, wird der weitere Ablauf abgebrochen. Meistens bedeutet dies, dass die bisherige VM noch nicht korrekt beendet wurde. Der Download der neuen Daten erfolgt direkt an die Zielstelle. Dies ist erfolgt vor allem aus Faulheit. Eigentlich müssten an dieser Stelle noch verschiedene weitere Sicherungsvorkehrungen getroffen werden.

Schön kann man hier sehen, wie die neuen NIO2 Routinen zusammen mit der Path-Klasse aus Java7 die Codezeilen reduziert:

1
2
3
4
5
6
7
8
9
10
11
//java.nio.file.Path localDestination;
//java.net.URL remoteLocation;

Path parent = localDestination.getParent();
if(!Files.exists(parent))
  Files.createDirectories(parent);
try(InputStream remoteData = remoteLocation.openStream())
{
  Files.copy(remoteData, localDestination,
      StandardCopyOption.REPLACE_EXISTING);
}

Nach dem Download wird einfach die Jar-Datei in einer weiteren Java-VM mit den „durchgeschleiften“ Kommandozeilenparametern neu gestartet. Das Programm sollte von dem ganzen Update nichts mitbekommen. Für ihn sollte es sich „anfühlen“ als sei es ganz normal vom Benutzer gestartet worden.

Ein paar Nebeneffekte

Der Updater ist leider nur ein „schneller Schuss“. Er hat mit ein paar „Unzulänglichkeiten“ zu kämpfen, die ich im Moment allerdings ganz gut akzeptieren kann. Wer mag, kann ja die entsprechenden Korrekturen auf Github vorschlagen.

Zum einen bleibt im Temp-Ordner des Anwenders eine „SimpleUpdaterXXXX.jar“ zurück, die nicht automatisch gelöscht wird. Diese Datei wird bei jedem Updateversuch erstellt, da dies für Phase 1 essentiell ist und dort noch nicht zwingend bekannt ist ob zum Beispiel im Programmverzeichnis Schreibrechte vorhanden sind. Solche Dinge müsste der Entwickler, der diese Klasse verwendet vorher selber prüfen. In der Klasse sind auch unterschiedliche „isUpToDate()“ Methoden vorhanden, mit denen die Notwendigkeit des Updates geprüft werden kann.

Das andere ist, dass derzeit bei den Neustarts der verschiedenen VMs eine Argument an die VM selbst übergeben werden können. Dies können Angaben zum Speicherverbrauch sein oder zum Beispiel Systemproperties, die wichtig für die Ausführung des entsprechenden Programms sind. Aber auch hier bin ich für Vorschläge offen.

Sollte der Downloadbereich auf dem Webserver hinter einem Login versteckt sein, wird diese Klasse ebenfalls fehlschlagen. Es wird einfach nur „URL.openConnection()“ aufgerufen um die Datei herunterzuladen. Allerdings spricht nichts dagegen, dass die Anwendung vorher selbst geeignete Funktionen bereits stellt um den Download durchzuführen (vielleicht mit hübscher Fortschrittsanzeige) und dann eine „file://„-URL an die update()-Routine weiter zu geben. Man hätte dann allerdings eine weitere temporäre Datei, die mach nach dem Update aufräumen müsste.