Programm Lock

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()

Programm Lock (last edited 2010-01-14 15:25:15 by JensDiemer)