Mit diesem Skript kann man verhindern das ein Programm mehrmals gestartet wird, siehe DocString.
Aus http://www.python-forum.de/topic-8282.html :
1 #!/usr/bin/env python
2 # -*- coding: iso-8859-1 -*-
3 """
4 Die Klasse ``FirstStart`` ist dazu da, um zu prüfen ob ein Programm bereits
5 gestartet wurde.
6 Dabei wird zwischen einem Pro-User-Modus und einem globalen Modus
7 unterschieden. Im Pro-User-Modus wird nur für den aktuellen Benutzer
8 geprüft, ob das Programm bereits gestartet wurde, im globalen Modus
9 geschieht dies benutzerübergreifend. (Siehe dazu auch die Beschreibung
10 der __init__ Methode.)
11 Der Mechanismus arbeitet mit einem Lockfile. Können die
12 PIDs der laufenden Programme nicht eruiert werden, dann wird auf eine Lösung
13 mit XMLRPC ausgewichen, allerdings nur im globbalen Modus.
14 Einen Beispielaufruf findet man in der ``main()``.
15
16 Ursprüngliche Version: 13.12.2006 Gerold Penz - gerold.penz(at)tirol.utanet.at
17 Verschiedene Änderungen: 05.04.2007 Oliver Kitzing
18 MacOSX Tests: 05.04.2007 Henrik "Kato" Lowack
19 Anforderungen: Python: http://www.python.org/
20 Für Windows wird "pywin32" oder alternativ
21 "ctypes" empfohlen, ab Windows XP geht es auch
22 ohne diese Erweiterungen.
23 (Python ab Version 2.5 hat ctypes bereits integriert.)
24 http://sourceforge.net/projects/pywin32/
25 http://sourceforge.net/projects/ctypes/
26 """
27
28 import os
29 import sys
30
31
32 def do_command(command):
33 """
34 Führt ein Shell-Kommando aus, je nach Python-Version in
35 der "richtigen" Art und Weise.
36 :return: Liste mit den Zeilen der Ausgabe des Shell-Kommandos.
37 """
38 try:
39 # Für Python ab 2.4
40 from subprocess import Popen, PIPE
41 p1 = Popen(command, stdout=PIPE, shell=True)
42 output = p1.communicate()[0]
43 lines = output.split("\n")
44 except ImportError:
45 # Ältere Python Versionen
46 p1 = os.popen(command, 'r')
47 lines = p1.readlines()
48
49 return lines
50
51
52 def set_access_rights(targetdir, dir, files):
53 """
54 Das ist eine Callback - Funktion, die von
55 "os.path.walk" aufgerufen wird. Wir brauchen diese, um im
56 Falle des globalen Verzeichnisses die Zugriffsrechte
57 für alle Verzeichnisse im Pfad zu setzen.
58 HINWEIS: Diese Funktion sollte nie von uns selbst aufgerufen
59 werden, es handelt sich um eine reine Callback-Funktion.
60 :param targetdir: Das Startverzeichnis (voller Pfad)
61 :param dir: Das aktuelle Verzeichnis
62 :param files: Die zu bearbeiteten Dateien
63 """
64 import os
65
66 if targetdir in dir:
67 os.chmod(dir, 0777)
68
69
70 def get_current_pids():
71 """
72 Liefert die aktuellen Prozess-IDs.
73 Funktioniert mit Windows 32-Bit Versionen (64-Bit nicht getestet,
74 sollte aber funktionieren), Linux, MacOSX.
75 Sollte auch mit diversen BSD-Varianten funktionieren, aber noch nicht
76 getestet.
77
78 :return: Liste mit den Prozess-IDs
79 """
80
81 if sys.platform.startswith("win"):
82 # Windows
83 try:
84 import win32process
85 # pywin32 ist installiert --> EnumProcesses
86 return list(win32process.EnumProcesses())
87 except ImportError:
88 try:
89 import ctypes as ct
90 # ctypes ist installiert --> Probiere mit psapi.dll
91 psapi = ct.windll.psapi
92 arr = ct.c_long * 1024
93 process_ids = arr()
94 cb = ct.sizeof(process_ids)
95 bytes_returned = ct.c_ulong()
96 psapi.EnumProcesses(ct.byref(process_ids), cb,
97 ct.byref(bytes_returned))
98 return sorted(list(set(process_ids)))
99 except ImportError:
100 # pywin32 und ctypes sind nicht installiert --> tasklist.exe
101 # Läuft mit Windows XP und höher.
102 import csv
103 csvlines = []
104 current_pids = []
105 for line in do_command("tasklist.exe /fo csv /nh"):
106 line = line.strip()
107 if line:
108 csvlines.append(line)
109 for line in csv.reader(csvlines):
110 current_pids.append(int(line[1]))
111 if not current_pids:
112 raise NotImplementedError("tasklist.exe not found (>WinXP)")
113 return current_pids
114 else:
115 # Linux, Cygwin, BSD-Varianten, MacOSX
116 # Wir fragen hier nicht etwa das "/proc" Verzeichnis
117 # ab, was unter Linux gehen würde, aber nicht unter den
118 # BSD - Systemen (wie MacOSX), sondern wir lesen einfach
119 # die Ausgabe des Befehls "ps a" aus.
120 current_pids = []
121 command = 'ps a'
122 lines = do_command(command)
123
124 # Überspringe erste Zeile, die die Header-Zeile ist
125 for line in lines[1:]:
126 line = line.strip()
127 if line != "":
128 pid = line.strip().split(" ")[0]
129 try:
130 current_pids.append(int(pid))
131 except ValueError:
132 pass
133
134 return current_pids
135
136
137 class FirstStart(object):
138
139 usexmlrpc = False
140
141 def __init__(self, appname, global_lock = False, debug_mode = False):
142 """
143 Initialisiert die Klasse und legt den Lockfilenamen fest.
144
145 :param appname: Eindeutiger Name der Anwendung. Anhand dieses
146 Parameters wird der Name des Lockfiles oder der Serverport für den
147 XMLRPC-Server generiert.
148 :param global_lock: Wenn True, dann wird diese Anwendung systemweit
149 nur einmal ausgeführt, auch bei Mehrbenutzer-Systemen.
150 :param debug_mode: Wenn True, dann Ausgabe von Debug-Informationen.
151 """
152 self.os = ""
153 self.appname = appname
154 self.xmlrpcport = None
155 self.global_lock = global_lock
156 self.debug_mode = debug_mode
157 self.appdir = None
158 self.lockfiledir = None
159 self.lockfilename = None
160 self.xmlrpcport = int("5" + \
str(hash(self.appname)).replace("0","").replace("-", "")[:4])
161 lockdir = None
162
163 self._debugprint("XMLRPCPORT: " + str(self.xmlrpcport))
164
165 # Lockfilename festlegen
166
167 if os.name in ['nt', 'dos']:
168 self._debugprint("Windows-System entdeckt")
169 self.os = "win"
170 appdatadir = os.environ.get("APPDATA", None)
171 allusersdir = os.environ.get("ALLUSERSPROFILE", None)
172 if appdatadir and allusersdir:
173 globaldir = allusersdir + appdatadir[appdatadir.rfind('\\'):]
174 else:
175 globaldir = None
176 else:
177 # POSIX - Code: Könnte eventuell noch problematisch
178 # sein.. "/tmp" wirklich bei allen Linux / BSD Systemen
179 # für normale Nutzer schreibbar? (Bei bisherigen Tests, ja).
180 # Der Filesystem Hierarchy Standard ist hier nicht so ganz klar,
181 # eventuell ist auch "/var/tmp" geeignet.
182 self.os = "posix"
183 self._debugprint("POSIX-System entdeckt")
184 globaldir = "/tmp"
185
186
187 if self.global_lock:
188 self._debugprint("Global-Modus aktiv")
189 if globaldir:
190 self.appdir = appdir = os.path.join(globaldir, appname)
191 lockdir = os.path.join(appdir, "Lockfiles")
192 else:
193 lockdir = None
194 else:
195 # Wir speichern bei POSIX-Systemen nicht mehr in
196 # "/var/lock", weil die Rechte nicht unbedingt bei jedem
197 # System dergestalt gesetzt sind, dass Prozesse mit einfachen
198 # Nutzerrechten auch schreiben können (z.B. gibt es bei diversen
199 # Linux-Varianten hier Unterschiede), stattdessen speichern
200 # wir im HOME - Verzeichnis.
201 self._debugprint("Pro-User Modus aktiv")
202 env_home = self._get_home_dir()
203
204 if env_home:
205 self.appdir = appdir = os.path.join(env_home, "." + appname)
206 lockdir = os.path.join(appdir, "Lockfiles")
207
208 # Prüfen, ob dieses Verzeichnis existiert, evtl. erstellen.
209 if lockdir:
210 if not os.path.exists(lockdir):
211 self._debugprint("Lockdir-Verzeichnis existiert noch nicht,"
212 " erstelle es")
213 os.makedirs(lockdir)
214 # Setze noch die Zugriffsrechte (nur bei globalen Modus)
215 if self.global_lock:
216 os.path.walk(globaldir, set_access_rights, self.appdir)
217
218
219 self.lockfiledir = lockdir
220 self.lockfilename = os.path.join(lockdir, appname + "_pylock.lock")
221 self._debugprint("Lockfilename: " + str(self.lockfilename))
222
223
224 def _get_lockfile_pid(self):
225 """
226 Gibt die PID zurueck, die im Lockfile steht.
227 :return: Die PID.
228 """
229
230 f = file(self.lockfilename, "r")
231 try:
232 pid = int(f.readline().strip())
233 except ValueError:
234 import warnings
235 warnings.warn("Lockfile without valid PID: '%s'" % self.lockfilename)
236 raise
237 f.close()
238 return pid
239
240
241 def _exists_lockfile(self):
242 """
243 Prüft ob das Lockfile existiert.
244 :return: True, wenn ja, ansonsten False.
245 """
246 if self.lockfilename and os.path.isfile(self.lockfilename):
247 return True
248
249
250 def __xmlrpc_server(self, port):
251 """
252 XMLRPC-Server. Dieser läuft, gestartet von ``self._start_xmlrpc_server``,
253 innerhalb eines eigenen Threads.
254 :param port: Der zu verwendende Port für den XMLRPC-Server.
255 """
256
257 from SimpleXMLRPCServer import SimpleXMLRPCServer
258 import socket # Wegen socket.error
259
260 def is_up():
261 return True
262
263 try:
264 server = SimpleXMLRPCServer(("localhost", port))
265 server.register_function(is_up)
266 server.serve_forever()
267 except socket.error:
268 pass
269
270
271 def _start_xmlrpc_server(self, port):
272 """
273 Startet den XMLRPC-Server
274 :param port: Der zu verwendende Port für den XMLRPC-Server.
275 """
276
277 from thread import start_new_thread
278
279 start_new_thread(self.__xmlrpc_server, (port,))
280
281
282 def create_lock(self):
283 """
284 Erstellt ein Lockfile mit der aktuellen PID oder startet den
285 XMLRPC-Server.
286 """
287 if self.lockfilename:
288 f = file(self.lockfilename, "w")
289 f.write(str(os.getpid()))
290 f.close()
291 else:
292 self._debugprint("Keine Lockfile-Erstellung moeglich,"
293 " versuche stattdessen XMLRPC-Loesung")
294 self._start_xmlrpc_server(self.xmlrpcport)
295
296 create_lockfile = create_lock # --> um abwärtskompatibel zu bleiben
297
298
299 def delete_lock(self):
300 """
301 Löscht das Lockfile der Anwendung. Der XMLRPC-Server wird NICHT beendet, da
302 dieser sowiso nach Programmende automatisch beendet wird.
303
304 HINWEIS:
305 Es wird nur der Lockfile selber gelöscht, nicht die angelegten Verzeichnisse.
306 Sollte das gewünscht sein, müsste diese Methode noch dementsprechend
307 umgeschrieben werden.
308
309 (Sollte es notwendig sein, dass der XMLRPC-Server mit dem Aufruf dieser
310 Methode beendet wird, dann müsste man den XMLRPC-Server so umschreiben,
311 dass nicht ``server.serve_forever()`` verwendet wird.)
312 """
313
314 if not self.usexmlrpc:
315 if self._exists_lockfile():
316 os.remove(self.lockfilename)
317 else:
318 pass
319
320 delete_lockfile = delete_lock # --> um abwärtskompatibel zu bleiben
321
322
323 def is_first_start(self):
324 """
325 Prüft ob die beim Initialisieren angegebene Anwendung bereits ausgeführt wird.
326
327 :return: True, wenn die Anwendung bereits läuft.
328 False, wenn die Anwendung noch nicht läuft.
329 None, wenn kein Lockfile erstellt werden konnte.
330 """
331
332 if self.usexmlrpc:
333 return self._check_via_xmlrpc()
334
335 return self._check_via_lockfile()
336
337
338 def _check_via_lockfile(self):
339 """
340 Prüft, ob eine Sperrung mittels Lockfile vorliegt.
341 :return: False, wenn ein Lockfile da ist (umgekehrte Logik, weil
342 False aussagt "Programm NICHT starten", True wenn KEIN Lockfile
343 da ist ("Programm starten ERLAUBT")
344 """
345 # LOCKFILE-Lösung und PID-Lösung (letzteres nur für globalen Modus)
346 self._debugprint("Versuche LOCKFILE-Methode")
347 lockfilename = self.lockfilename
348
349 # Wenn bei Initialisierung der FirstStart-Instanz erfolgreich das
350 # Lockfile-Verzeichnis gefunden bzw. angelegt werden konnte,
351 # dann prüfe ob darin das Lockfile existiert.
352 # Wenn Lockfile-Verzeichnis nicht korrekt, Rückfall auf die
353 # XMLRPC-Methode
354 if self.lockfiledir:
355 if not self._exists_lockfile():
356 self._debugprint("Es existiert KEIN Lockfile!")
357 return True
358
359 # Lockfile ist da, wir checken jetzt die PID, ob es sich wirklich
360 # um den gesuchten Prozess handelt, ansonsten könnte nämlich der
361 # Lockfile zwar von der gleichen Anwendung stammen, die aber vorher
362 # mal inkorrekt beendet worden ist.
363 try:
364 lockfilepid = self._get_lockfile_pid()
365 currentpids = get_current_pids()
366 self._debugprint("LOCKFILEPID: " + str(lockfilepid))
367 except (NotImplementedError, ValueError, IOError):
368 # Wenn die PID nicht ermittelt werden kann,
369 # können wir nicht wirklich weitermachen,
370 self._debugprint("Konnte PID nicht ermitteln, Abbruch!")
371 return None
372 else:
373 if self.global_lock:
374 self.usexmlrpc = True
375 return self.is_first_start()
376 else:
377 return None
378
379 if lockfilepid in currentpids:
380 # Das Programm läuft noch
381 self._debugprint("PID gefunden, Programmprozess laeuft noch!")
382 return False
383 else:
384 # Das Programm mit der ermittelten PID ist nicht gestartet.
385 self._debugprint("PID NICHT gefunden, starten erlaubt, "
386 "lösche evtl. verwaisten Lockfile!")
387 self.delete_lockfile()
388 return True
389
390
391 def _check_via_xmlrpc(self):
392 """
393 Prüft, ob eine Sperrung über dem XMLRPC-Server vorliegt
394 (nur für den globalen Modus von Belang).
395 :return: True, wenn Sperrung vorhanden.
396 False, wenn nicht.
397 """
398 # XMLRPC-Lösung
399 self._debugprint("Versuche XMLRPC-Methode")
400
401 import xmlrpclib
402 import socket
403
404 try:
405 server = xmlrpclib.ServerProxy("http://localhost:" +
406 str(self.xmlrpcport))
407 return not bool(server.is_up())
408 except socket.error:
409 return True
410
411
412 def _get_home_dir(self):
413 """
414 Eine einfache Methode um "möglichst" plattformübergreifend das
415 Home-Verzeichnis zu erhalten.
416 (Getestet auf Windows 2000/XP (32-Bit), Linux, MacOSX. Vista und
417 64-Bit-Windows-Varianten stehen noch aus.)
418
419 :return: Das Homeverzeichnis als String, wenn Fehler dann None.
420 """
421
422 appdir = self.appname
423 dirname = ""
424 home_dir = ""
425
426 try:
427 if os.name == "posix":
428 home_dir = os.environ.get("HOME", None)
429 elif os.name in ['nt', 'dos']:
430 # Windows 2000 scheint die HOMEPATH Umgebungsvariable
431 # nicht zu kennen, in dem Falle wird auf USERPROFILE
432 # zurückgegriffen.
433 if os.getenv("HOMEPATH") == "\\":
434 home_dir = os.environ.get("USERPROFILE", None)
435 else:
436 # In HOMEPATH ist das Laufwerk nicht angegeben,
437 # das muss deswegen extra abgefragt werden mit
438 # HOMEDRIVE.
439 home_dir = os.path.join(os.environ.get("HOMEDRIVE", None) ,\
os.environ.get("HOMEPATH", None))
440 else:
441 return None
442
443 except OSError:
444 raise Exception, "Exception: " + str(sys.exc_info()[0])
445 return None
446
447 if not (os.path.exists(home_dir) and os.path.isdir(home_dir)):
448 os.mkdir(home_dir)
449
450 return home_dir
451
452
453 def _debugprint(self, text):
454 """
455 Nur eine einfache Print-Methode, die nur aktiv
456 ist wenn Instanzvariable self.debug_mode auf True steht.
457 ACHTUNG: Variable self.debug_mode muss in dieser Klasse definiert sein.
458 :param text: Der zu druckende Text.
459 """
460 if self.debug_mode:
461 print text
462
463
464 def main():
465 """Testen"""
466 import time
467
468 globalmode = False
469 debugmode = False
470 valid_option_found = False
471
472 if len(sys.argv) > 1:
473 if "--global" in sys.argv:
474 globalmode = True
475 valid_option_found = True
476
477 if "--debug" in sys.argv:
478 debugmode = True
479 valid_option_found = True
480
481 # Diese Ausgabe eventuell mal den Gepflogenheiten bei
482 # den Manpages (Linux/BSD und Konsorten) anpassen.
483 # Ist aber ohnehin nur für diesen Beispielcode.
484 if not valid_option_found:
485 print ( "WARNUNG: Es war zumindest ein Parameter"
486 "vorhanden, der nicht gueltig war.")
487 print ("Gueltige Optionen:")
488 print ("--global: Pruefe systemweit, ob Prozess bereits laeuft.")
489 print ("--debug: Erweiterte Informationen bei der Ausgabe.")
490 sys.exit(1)
491
492
493 firststart = FirstStart("testapplikation", globalmode, debugmode)
494
495 # Wenn man noch den Fall unterscheiden wollte, ob "is_first_start()" wegen
496 # eines Fehlers abgebrochen hat, müsste man explizit den Rückgabewert auf
497 # "None" testen. Eventuell könnte man natürlich mal "is_first_start()" so
498 # umschreiben, dass eine entsprechende Exception geworfen wird.
499 if firststart.is_first_start():
500 firststart.create_lock()
501
502 # Hier wird gearbeitet
503 for i in range(15):
504 print i
505 time.sleep(1)
506 print
507
508 ## wxPython-App
509 #import wx
510 #app = wx.PySimpleApp(True)
511 #print "Ich bin ein Fenster"
512 #app.MainLoop()
513
514 firststart.delete_lock()
515 else:
516 print ("Das Programm wurde bereits gestartet oder keine "
517 "Lockfile-Erstellung moeglich..")
518
519
520 if __name__ == "__main__":
521 main()