Die grundlegende Logik wurde bereits erläutert, weshalb ich an dieser Stelle nicht noch einmal alles wiederholen werde. Stattdessen sollten Sie anhand der vorhergehenden Abschnitte und der Kommentare im Code die Funktionen der Klasse ohne Probleme nachvollziehen können.
package de.jbb.sic;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
/**
* SingleInstanceController ist eine Klasse, mit der ueberprueft werden kann, ob
* eine andere, mit dem SingleInstanceController registrierte, Klasse bereits auf
* dem System laeuft (Port- und Klassenabhaengig). Sollte eine solche Klasse
* bereits ausgefuehrt worden sein, besteht die Moeglichkeit mit dieser zu
* kommunizieren.
*/
public class SingleInstanceController {
private boolean result = false;
private File file = null;
private ObjectOutputStream oos = null;
private ObjectInputStream ois = null;
private ServerSocket server = null;
private Socket client = null;
private ArrayList<ApplicationStartedListener> listener = null;
private String appname = null;
/**
* Konstruktor für ein neues SingleInstanceController Objekt. Uebergeben
* wird ein File-Objekt, dass auf den Ort verweist, wo der aktuelle Port
* gespeichert werden soll. Zusaetzlich wird eine eindeutige
* Programmbezeichnung uebergeben (z. B. package + classenname), anhand der
* identische Applikationen identifiziert werden koennen.
*
* @param file Speicherort
* @param masterclass eindeutige Programmbezeichnung
* @see SingleInstanceController(String)
*/
public SingleInstanceController(File file, String appname) {
this.file = file;
this.appname = appname;
this.listener = new ArrayList<ApplicationStartedListener>();
}
/**
* Konstruktor für ein neues SingleInstanceController Objekt. Es wird ein
* default Pfad für den Port erstellt. Es wird eine eindeutige
* Programmbezeichnung uebergeben (z. B. package + classenname), anhand der
* identische Applikationen identifiziert werden koennen. Dieser Konstruktor
* ist nicht zu empfehlen da auch andere Applikationen, die den
* SingleInstanceController verwenden, auf dieses File zugreifen koennten.
*
* @param masterclass aufrufende Klasse
* @see SingleInstanceController(File, String)
*/
public SingleInstanceController(String appname) {
this(new File(System.getProperty("java.io.tmpdir") + "/923jhakE53Kk9235b43.6m7"), appname);
}
/**
* Fuegt einen ApplicationStartedListener hinzu
*
* @param asl
*/
public void addApplicationStartedListener(ApplicationStartedListener asl) {
this.listener.add(asl);
}
/**
* Entfernt einen ApplicationStartedListener
*
* @param asl
*/
public void removeApplicationStartedListener(ApplicationStartedListener asl) {
this.listener.remove(asl);
}
/**
* Ueberprueft, ob die Applikation schon registriert wurde und somit bereits
* ausgefuert wird
*
* @return true falls sichergestellt werden kann, dass die Applikation
* bereits laeuft
*/
public boolean isOtherInstanceRunning() {
if (!this.file.exists()) {
return false;
}
return sendMessageToRunningApplication(new ClassCheck(this.appname));
}
/**
* Schickt der registrierten Applikation eine Nachricht als Object.
*
* @param obj das zu sendende Objekt
* @return true falls die Nachricht erfolgreich uebermittelt wurde
*/
public boolean sendMessageToRunningApplication(final Object obj) {
this.result = false;
try {
this.client = new Socket("localhost", getPortNumber());
// In einem neuen Thread kommunizieren, um einen Deadlock zu verhindern
new Thread(new Runnable() {
public void run() {
try {
SingleInstanceController.this.oos = new ObjectOutputStream(SingleInstanceController.this.client.getOutputStream());
SingleInstanceController.this.ois = new ObjectInputStream(SingleInstanceController.this.client.getInputStream());
SingleInstanceController.this.oos.writeObject(obj);
SingleInstanceController.this.oos.flush();
SingleInstanceController.this.result = SingleInstanceController.this.ois.readBoolean();
}
catch (IOException e) {
SingleInstanceController.this.result = false;
}
}
}).start();
// Falls nach 1 Sekunde keine Antwort = Server nicht erreichbar
for (int i = 0; i < 10; i++) {
if (this.result == true) {
break;
}
try {
Thread.sleep(100);
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
this.client.close();
return this.result;
}
catch (IOException e) {
return false;
}
}
/**
* Registriert diese Applikation als gestartet. Sollte die Applikation
* nochmal gestartet werden, so kann diese registrierte Applikation darueber
* informiert werden
*
* @return true wenn die Applikation erfolgreich registriert wurde
*/
public boolean registerApplication() {
try {
if (!this.file.exists()) {
if (!this.file.getParentFile().mkdirs() && !this.file.getParentFile().exists()) {
return false;
}
if (!this.file.createNewFile()) {
return false;
}
}
BufferedWriter wuffy = new BufferedWriter(new FileWriter(this.file));
int port = getFreeServerSocket();
if (port != -1) {
startServer();
}
wuffy.write(String.valueOf(port));
wuffy.close();
return true;
}
catch (IOException e) {
return false;
}
}
/**
* Listener informieren, dass Nachricht angekommen ist
*
* @param obj Objekt fuer die Listener
*/
protected void messageArrived(Object obj) {
for (ApplicationStartedListener asl : this.listener) {
asl.messageArrived(obj);
}
}
/**
* Listener informieren, dass selbe Applikation gestartet wurde und eine
* Verbindung aufgebaut hat
*/
protected void applicationStartet() {
for (ApplicationStartedListener asl : this.listener) {
asl.applicationStarted();
}
}
/**
* Listener informieren, dass eine andere Applikation versucht hat sich
* anzumelden
*/
protected void foreignApplicationStarted(String name) {
for (ApplicationStartedListener asl : this.listener) {
asl.foreignApplicationStarted(name);
}
}
/**
* Ermittelt die zuletzt eingetragene Port-Nummer
*
* @return die zuletzt eingetragene Port-Nummer
*/
private int getPortNumber() {
try {
BufferedReader buffy = new BufferedReader(new FileReader(this.file));
int port = Integer.parseInt(buffy.readLine().trim());
buffy.close();
return port;
}
catch (Exception e) {
return -1;
}
}
/**
* Startet den Server, um mit spaeter gestarteten Applikationen zu
* kommunizieren
*/
private void startServer() {
new Thread(new Runnable() {
public void run() {
while (true) {
try {
SingleInstanceController.this.client = SingleInstanceController.this.server.accept();
if (SingleInstanceController.this.client.getInetAddress().isLoopbackAddress()) {
new Thread(new Runnable() {
public void run() {
try {
SingleInstanceController.this.oos = new ObjectOutputStream(SingleInstanceController.this.client.getOutputStream());
SingleInstanceController.this.ois = new ObjectInputStream(SingleInstanceController.this.client.getInputStream());
Object obj = SingleInstanceController.this.ois.readObject();
if (obj instanceof ClassCheck) {
if (obj.toString().equals(SingleInstanceController.this.appname)) {
SingleInstanceController.this.oos.writeBoolean(true);
applicationStartet();
}
else {
SingleInstanceController.this.oos.writeBoolean(false);
foreignApplicationStarted(obj.toString());
}
}
else {
messageArrived(obj);
SingleInstanceController.this.oos.writeBoolean(true);
}
SingleInstanceController.this.oos.flush();
SingleInstanceController.this.client.close();
}
catch (IOException e) {
e.printStackTrace();
}
catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}).start();
}
}
catch (IOException e) {
e.printStackTrace();
}
}
}
}).start();
}
/**
* Ermittelt einen freien Port zwischen 2000 und 10000
*
* @return den 1. freien Port oder falls kein freier Port gefunden wurde -1
*/
private int getFreeServerSocket() {
for (int i = 2000; i < 10000; i++) {
try {
this.server = new ServerSocket(i);
return i;
}
catch (IOException ignore) {}
}
return -1;
}
}
Ein Beispiel zur Implementierung des SingleInstanceControllers finden Sie auf der nächsten Seite.


{ 21 } Comments
Eine kleine Anmerkung zur FileLock-Methode: Seit Java NIO gibt es die Möglichkeit, “echte” File-Locks zu realisieren. Hierzu wird die Methode FileChannel#tryLock verwendet.
Dadurch fallen ein Großteil der Nachteile weg. Eine Kommunikation ist allerdings immer noch nicht möglich. Solch ein FileLock wird aber eben automatisch aufgehoben sobald das Programm nicht mehr läuft. Das wichtige Kriterium wäre nicht mehr, ob die Datei existiert, sondern ob tryLock ein FileLock oder null zurückgibt. Ob die Datei noch existiert, kann dafür als Kriterium verwendet werden, ob das Programm sauber beendet wurde.
Hallo Illuvatar,
vielen Dank für deinen Beitrag. Ich habe die
tryLockMethode der KlasseFileChannelnicht angesprochen, da das Verhalten vonFileLocksehr systemspezifisch ist, und eine Kommunikation zwischen den beiden Applikationen nicht mehr möglich wird.Bei Gelegenheit werde ich dieses Verfahren als kleinen Zusatz aber ergänzen.
Gruß
Stefan
Super Tutorial!
Sowas hätte ich denke ich kaum zu Stande gebracht, zumindest nicht so genau
Gruß,
pcworld
SingleInstanceController.this.client.getInetAddress().getCanonicalHostName()
liefert bei mir “127.0.0.1″ statt “localhost”. Damit akzeptiert der Server den Client nicht und die Sache geht schief.
Scheint eine Eigenheit von MS-Vista zu sein? Hab’ verschiedene JDKs probiert.
Hi Stefan,
ein solches Problem ist mir nicht bekannt. Ich habe den Quellcode aber einmal ausgebessert. Anstatt von
getCanonicalHostName().equals("localhost")wird jetzt einfachisLoopbackAddress()geschrieben.Über eine Rückmeldung, ob es jetzt funktioniert oder nicht, würde ich mich freuen.
Gruß
Stefan
Hi Stefan,
ja. Das klappt jetzt auch unter Vista.
Danke und schöne Grüße,
Stefan
Ich muss in main() explizit System.exit() aufrufen sonst hängt das Programm bis ich Ctl-C drücke. Da wartet die JVM bis sich der Thread beendet.
Könnte helfen: http://java.sun.com/j2se/1.5.0/docs/api/java/lang/Thread.html#setDaemon(boolean)
Hallo Stefan,
ich kann leider gerade nicht nachvollziehen, wovon Sie reden. Das
System.exit(0);in derExampleKlasse kann auch ohne Probleme weggelassen werden.Grüße
Stefan
Nicht in der Klasse sondern im main() muss ich
exit-en sonst hängt das Programm.
public static void main(String[] args) {
new Example();
System.exit();
}
Passiert auf Ubuntu Java Version 1.6.0_14
Ich denke der exit bricht den Thread ab. Sonst wartet die JVM bis sich der Thread selbst beendet (was er nicht tut).
Kann ich leider nicht nachvollziehen, hab auch leider keine installierte Ubuntu-Version. Nur damit es zu keinen Missverständnissen kommt: Das zuerst gestartete Programm soll bis zu einem manuellen Abbruch durchlaufen. Alle weiteren Instanzen beenden sich sofort nach dem Start und dem Austausch der Nachrichten selbst.
Grüße
Stefan
Hallo,
ich benutze auch die Logik mittels ServerSocket, wie zu Beginn unter “Einen Port sperren” beschrieben!
Seltsamerweise wird die Exception (bei mir ist es übrigens keine IOException, sondern eine BindException – ich fange aber alle ab) nicht auf allen Rechner in der Firma geworfen (auf ca. 10% nicht).
Hat jemand eine Idee, woher das kommen kann ???
Danke und Gruß
Klaus
Hallo Herr Brokmann,
vermutlich läuft unter dem gewählten Port auf bestimmten Rechnern in der Firma bereits eine andere Anwendung. Entweder müssen Sie diese Anwendung(en) identifizieren und beenden, oder – was die vermutlich leichtere Lösung ist – einen anderen Port wählen.
Gruß
Stefan
Guten Morgen Herr Kiesel,
Danke für die schnelle Antwort!
Ich bemerke gerade, dass ich mich vlt. etwas unglücklich ausgedrückt habe …
Auch ich möchte mittels des Code prüfen, ob bereits eine Instanz der betroffenen Applikation läuft. Dazu lege ich genau so wie oben beschrieben, zu Beginn eine neue Socketvariable an und fange dann ggf. die Exception ab, um dabei dann ggf. die Applikation zu beenden!
Dies klappt wie gesagt auf den meisten PCs sehr gut, nur auf einigen wird die Exception leider nicht geworfen, und dann wird die Anwendung ein zweites Mal gestartet!
Hier mal der relevante Code, den ich eigentlich für fehlerfrei halte :
Freundliche Grüße
Klaus Brokmann
Ihr Code ist soweit fehlerfrei. Sind Sie sicher, dass beide Applikationen zu der Zeile “listenerSocket = new ServerSocket(nMutexServerport);” gelangen und nicht vorher abgebrochen werden oder einen anderen Weg (if-Abfrage) wählen können? Können Sie sicherstellen, dass nMutexServerport für beide Anwendungen exakt identisch ist? Können Sie sicherstellen, dass beide Anwendungen noch laufen?
Ansonsten versuchen Sie es doch mal primitiv mit einer einfachen Main-Klasse, die Sie zweimal hintereinander ausführen:
Wenn ich mich nicht vertippt habe (gerade alles aus dem Kopf und ohne Compiler und IDE) sollte dann beim zweiten Ausführen eine Fehlermeldung erscheinen. Passiert das bei Ihnen (wovon ich einmal stark ausgehe), dann ist der Fehler nicht in dem von Ihnen geposteten Codeausschnitt zu suchen.
Grüße
Stefan
Guten Morgen,
ja, alle Aufrufe erreichen zwangsläufig die Stelle (ist zentral zu Beginn der main-Funktion); das habe ich auch im Debugger überprüft.
Der Port ist ebenfalls zentral so deklariert:
// Mutexserverport zur Instanzenkontrolle
public static final int nMutexServerport = 62987;
Der Aufruf mit “.accept()” klappt hier nicht, da dieser genannte Port vermutlich nicht in der Firewall resp. im Proxy freigegeben ist (die Anwendung wartet dann ‘ewig’ auf . Ich möchte ja auch nicht damit arbeiten, sondern ihn nur zu Instanzenkontrolle nutzen
Mir fiel eben noch auf, dass mit dem von Ihnen oben beschriebene Aufruf
new ServerSocket(nMutexServerport );
KEINE Exception bei mir auslöst !
Nur für den Fall
listenerSocket = new ServerSocket( nMutexServerport );
tritt (beim zweiten Aufruf) die Bind-Exception auf und das “listenerSocket”-Objekt ist dann null.
Ist schon alles sehr seltsam …
Freundliche Grüße
Klaus
Hallo Herr Brokmann,
das accept() ist schon so beabsichtigt. So wartet Ihr Programm, bis sich jemand zu diesem ServerSocket verbindet. Da dies nicht geschehen wird, wartet das Programm ewig (bzw. bis Sie es abbrechen). Hierdurch wird wiederum gewährleistet, dass das erste Programm (die erste Ausführung) noch läuft und auch noch den ServerSocket gebunden hat, wenn Sie das Programm ein zweites Mal ausführen, so dass es zu einem Fehler kommt.
Bitte versuchen Sie es noch einmal auf die von mir beschriebene Art und Weise.
Grüße
Stefan Kiesel
Hallo Herr Kiesel,
damit wartet dann aber auch die erste Anwendung ewig – und ich komme dort nicht mal bis zum Anmeldedialog, geschweige denn, dass ich arbeiten kann !!
Ich möchte ja nur beim zweiten Aufruf die Exception bekommen, um den zweiten Aufruf zu canceln !
Freundliche Grüße
Klaus Brokmann
Natürlich wartet so die erste Anwendung ewig. Sie sollen das ja auch nicht in Ihre Anwendung einbauen, sondern nur so wie es ist (ohne auch nur eine Zeile Code zu verändern) ausführen und testen
. Dieses kleine Programm soll Ihnen lediglich beweisen, dass der Fehler nicht an dem von Ihnen geposteten Codeschnipsel liegt. Und diesen Beweis bringt es, wenn Sie das Programm genau in dieser Form kompilieren, zweimal ausführen und beim zweiten Mal ausführen eine Exception geworfen wird.
Um auf mögliche Fehlerursachen Ihres eigentlichen Problems zurück zu kommen:
Genaues kann man natürlich nur sagen, wenn man einen aussagekräftigeren Teil Ihres Codes zu Gesicht bekommt, da der Fehler (wie gesagt) in Ihrem uns zur Verfügung gestellten Teil garantiert nicht liegt. Natürlich versteh ich auch, wenn Sie nicht einfach Code Ihrer Firma hier veröffentlichen können. Deshalb ein paar allgemeine Tipps:
1.) Könnte evtl. irgendwo eine unabgefangene Exception geworfen werden, die das schließen des Sockets bewirkt?
2.) Sie wissen wie der GarbageCollector in Java arbeitet? Wenn Ihr Objekt nirgends mehr referenziert wird und der GarbageCollector das feststellt, wird Ihr Objekt irgendwann aufgeräumt.
3.) Aus Ihrem letzten Kommentar schließe ich, dass Sie eine GUI verwenden. Eine Swing-GUI läuft in Java in einem separierten Thread. Nach Abarbeitung Ihrer Main-Methode wird der Thread, in welchem die Main-Methode gestartet wurde, beendet. Es läuft folglich nur noch der GUI-Thread. Könnte evtl. hier im Bezug auf die Erzeugung des ServerSockets das Problem liegen?
Nach Ihrer Aussage
// Zitat
new ServerSocket(nMutexServerport );
KEINE Exception bei mir auslöst !
Nur für den Fall
listenerSocket = new ServerSocket( nMutexServerport );
tritt (beim zweiten Aufruf) die Bind-Exception auf und das “listenerSocket”-Objekt ist dann null.
// Zitat Ende
sollten Sie insbesondere die Punkte 2 und 3 in Betracht ziehen.
Grüße
Stefan
Hallo Stefan,
ah, die Punkte 2 und 3 habe ich so noch nicht ernsthafter in Betracht bezogen …
Ok, ich werde mal versuchen, die Anwendung dahingehend zu testen! Referenziert wird die Variable sicherlich nirgendwo mehr – das könnte in der Tat vlt. ein Knackpunkt sein …
Ich melde mich, sobald ich weitere Ergebnisse habe!
Zuerst nochmals vielen Dank für die Unterstützung!
Freundliche Grüße
Klaus
Hallo Herr Kiesel,
gibt es einen speziellen Grund dafür, dass sie das FileLock per ShutDown-Hook-Thread löschen? Ich hätte spontan einfach die deleteOnExit()-Methode des Files verwendet. Gibt es hierbei einen Unterschied, den ich noch nicht kenne?
Viele Grüße
Maik Jäkel
Hallo Herr Jäkel,
ja, den gibt es. Ich zitiere die API-Doc für deleteOnExit:
“Deletion will be attempted only for normal termination of the virtual machine, as defined by the Java Language Specification.” und “Note: this method should not be used for file-locking, as the resulting protocol cannot be made to work reliably”
Ein Shutdown-Hook hingegen wird auch dann ausgeführt, wenn die VM auf abnormalem Wege terminiert.
Grüße
Stefan
Kommentar verfassen