Assurez-vous qu'une seule instance d'un programme est en cours d'exécution


120

Existe-t-il un moyen pythonique d'avoir une seule instance d'un programme en cours d'exécution?

La seule solution raisonnable que j'ai trouvée est d'essayer de l'exécuter en tant que serveur sur un port, puis le deuxième programme essayant de se lier au même port - échoue. Mais ce n'est pas vraiment une bonne idée, peut-être qu'il y a quelque chose de plus léger que ça?

(Tenez compte du fait que le programme devrait parfois échouer, c'est-à-dire segfault - donc des choses comme "verrouiller le fichier" ne fonctionneront pas)


1
Peut-être que votre vie serait plus facile si vous traquiez et corrigiez le segfault. Non pas que ce soit une chose facile à faire.
David Locke

Ce n'est pas dans ma bibliothèque, c'est dans les liaisons libxml de python et extrêmement timide - ne se déclenche qu'une fois tous les deux jours.
Slava V

5
La bibliothèque standard de Python prend en charge flock (), qui est la bonne chose pour les programmes UNIX modernes. L'ouverture d'un port utilise un emplacement dans un espace de noms beaucoup plus contraint, alors que les fichiers pid sont plus complexes car vous devez vérifier les processus en cours pour les invalider en toute sécurité; troupeau n'a aucun problème.
Charles Duffy

s / UNIX / linux / voilà, FTFY.
kaleissin

Réponses:


100

Le code suivant devrait faire le travail, il est multiplateforme et fonctionne sur Python 2.4-3.2. Je l'ai testé sous Windows, OS X et Linux.

from tendo import singleton
me = singleton.SingleInstance() # will sys.exit(-1) if other instance is running

La dernière version du code est disponible singleton.py . Veuillez signaler les bogues ici .

Vous pouvez installer tend en utilisant l'une des méthodes suivantes:


2
J'ai mis à jour la réponse et inclus un lien vers la dernière version. Si vous trouvez un bogue, veuillez le soumettre à github et je le résoudrai dès que possible.
sorin

2
@Johny_M Merci, j'ai fait un patch et publié une version plus récente sur pypi.python.org/pypi/tendo
sorin

2
Cette syntaxe ne fonctionnait pas pour moi sous Windows sous Python 2.6. Ce qui a fonctionné pour moi était: 1: from tendo import singleton 2: me = singleton.SingleInstance ()
Brian

25
Une autre dépendance pour quelque chose d'aussi trivial que cela? Cela ne semble pas très attrayant.
WhyNotHugo

2
Le singleton gère-t-il les processus qui obtiennent un sigterm (par exemple si un processus s'exécute trop longtemps), ou dois-je gérer cela?
JimJty

43

Facile, Solution multiplateforme , trouvée dans une autre question de zgoda :

import fcntl
import os
import sys

def instance_already_running(label="default"):
    """
    Detect if an an instance with the label is already running, globally
    at the operating system level.

    Using `os.open` ensures that the file pointer won't be closed
    by Python's garbage collector after the function's scope is exited.

    The lock will be released when the program exits, or could be
    released if the file pointer were closed.
    """

    lock_file_pointer = os.open(f"/tmp/instance_{label}.lock", os.O_WRONLY)

    try:
        fcntl.lockf(lock_file_pointer, fcntl.LOCK_EX | fcntl.LOCK_NB)
        already_running = False
    except IOError:
        already_running = True

    return already_running

Un peu comme la suggestion de S.Lott, mais avec le code.


Par curiosité: est-ce vraiment multiplateforme? Fonctionne-t-il sous Windows?
Joachim Sauer

1
Il n'y a pas de fcntlmodule sous Windows (bien que la fonctionnalité puisse être émulée).
jfs

10
ASTUCE: si vous voulez envelopper cela dans une fonction, «fp» doit être global ou le fichier sera fermé après la sortie de la fonction.
cmcginty

1
@Mirko Control + Z ne quitte pas une application (sur tous les OS dont je connais), il la suspend. L'application peut être remise au premier plan avec fg. Donc, il semble que cela fonctionne correctement pour vous (c'est-à-dire que l'application est toujours active, mais suspendue, de sorte que le verrou reste en place).
Sam Bull

1
Ce code dans ma situation (Python 3.8.3 sous Linux) nécessitait une modification:lock_file_pointer = os.open(lock_path, os.O_WRONLY | os.O_CREAT)
baziorek

30

Ce code est spécifique à Linux. Il utilise des sockets de domaine UNIX «abstraits», mais c'est simple et ne laissera pas de fichiers de verrouillage périmés. Je le préfère à la solution ci-dessus car il ne nécessite pas de port TCP spécialement réservé.

try:
    import socket
    s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
    ## Create an abstract socket, by prefixing it with null. 
    s.bind( '\0postconnect_gateway_notify_lock') 
except socket.error as e:
    error_code = e.args[0]
    error_string = e.args[1]
    print "Process already running (%d:%s ). Exiting" % ( error_code, error_string) 
    sys.exit (0) 

La chaîne unique postconnect_gateway_notify_lockpeut être modifiée pour autoriser plusieurs programmes qui nécessitent une seule instance appliquée.


1
Roberto, êtes-vous sûr qu'après une panique du noyau ou une réinitialisation matérielle, le fichier \ 0postconnect_gateway_notify_lock ne sera pas présent au démarrage? Dans mon cas, le fichier de socket AF_UNIX est toujours présent après cela et cela détruit toute l'idée. La solution ci-dessus avec l'acquisition d'un verrou sur un nom de fichier spécifique est très fiable dans ce cas.
Danylo Gurianov

2
Comme indiqué ci-dessus, cette solution fonctionne sur Linux mais pas sur Mac OS X.
Bilal et Olga

2
Cette solution ne fonctionne pas. Je l'ai essayé sur Ubuntu 14.04. Exécutez le même script à partir de 2 fenêtres de terminal simultanément. Ils fonctionnent tous les deux très bien.
Dimon

1
Cela a fonctionné pour moi dans Ubuntu 16. Et tuer le processus par tous les moyens a permis à un autre de démarrer. Dimon Je pense que tu as fait quelque chose de mal dans ton test. (Vous avez peut-être oublié de mettre votre script en veille après l'exécution du code ci-dessus, il s'est donc immédiatement arrêté et a libéré le socket.)
Luc

1
Ce n'est pas une question de sommeil. Le code fonctionne mais uniquement en tant que code en ligne. Je mettais ça en fonction. Le socket disparaissait dès que la fonction existait.
Steve Cohen

25

Je ne sais pas si c'est assez pythonique, mais dans le monde Java, écouter sur un port défini est une solution assez largement utilisée, car elle fonctionne sur toutes les principales plates-formes et n'a aucun problème avec les programmes qui plantent.

Un autre avantage d'écouter un port est que vous pouvez envoyer une commande à l'instance en cours d'exécution. Par exemple, lorsque les utilisateurs démarrent le programme une deuxième fois, vous pouvez envoyer à l'instance en cours d'exécution une commande pour lui dire d'ouvrir une autre fenêtre (c'est ce que fait Firefox, par exemple. Je ne sais pas s'ils utilisent des ports TCP ou des tubes nommés ou quelque chose comme ça, 'cependant).


+1 à cela, spécialement parce qu'il me permet de notifier l'instance en cours d'exécution, donc il crée une autre fenêtre, apparaît, etc.
WhyNotHugo

1
Utilisez par exemple import socket; s = socket.socket(socket.AF_INET, socket.SOCK_STREAM); s.bind(('localhost', DEFINED_PORT)). Un OSErrorsera déclenché si un autre processus est lié au même port.
crishoj

13

Jamais écrit python auparavant, mais c'est ce que je viens d'implémenter dans mycheckpoint, pour éviter qu'il ne soit démarré deux fois ou plus par crond:

import os
import sys
import fcntl
fh=0
def run_once():
    global fh
    fh=open(os.path.realpath(__file__),'r')
    try:
        fcntl.flock(fh,fcntl.LOCK_EX|fcntl.LOCK_NB)
    except:
        os._exit(0)

run_once()

J'ai trouvé la suggestion de Slava-N après l'avoir publiée dans un autre numéro (http://stackoverflow.com/questions/2959474). Celui-ci est appelé en tant que fonction, verrouille le fichier de scripts en cours d'exécution (pas un fichier pid) et maintient le verrou jusqu'à la fin du script (normal ou erreur).


1
Très élégant. Je l'ai changé pour qu'il récupère le chemin des arguments du script. Recommande également d'intégrer cela dans un endroit commun - Exemple
Jossef Harush

10

Utilisez un fichier pid. Vous avez un emplacement connu, "/ chemin / vers / pidfile" et au démarrage vous faites quelque chose comme ça (partiellement pseudocode parce que je suis en pré-café et que je ne veux pas travailler si dur):

import os, os.path
pidfilePath = """/path/to/pidfile"""
if os.path.exists(pidfilePath):
   pidfile = open(pidfilePath,"r")
   pidString = pidfile.read()
   if <pidString is equal to os.getpid()>:
      # something is real weird
      Sys.exit(BADCODE)
   else:
      <use ps or pidof to see if the process with pid pidString is still running>
      if  <process with pid == 'pidString' is still running>:
          Sys.exit(ALREADAYRUNNING)
      else:
          # the previous server must have crashed
          <log server had crashed>
          <reopen pidfilePath for writing>
          pidfile.write(os.getpid())
else:
    <open pidfilePath for writing>
    pidfile.write(os.getpid())

Donc, en d'autres termes, vous vérifiez si un pidfile existe; sinon, écrivez votre pid dans ce fichier. Si le pidfile existe, vérifiez si le pid est le pid d'un processus en cours d'exécution; si c'est le cas, alors vous avez un autre processus en direct en cours d'exécution, alors arrêtez simplement. Sinon, le processus précédent s'est écrasé, alors enregistrez-le, puis écrivez votre propre pid dans le fichier à la place de l'ancien. Alors continuez.


4
Cela a une condition de course. La séquence test-puis-écriture peut déclencher une exception de deux programmes démarrant presque simultanément, ne trouve aucun fichier et essaie de s'ouvrir en écriture simultanément. Il devrait soulever une exception sur l'un, permettant à l'autre de procéder.
S.Lott


5

Cela peut fonctionner.

  1. Essayez de créer un fichier PID vers un emplacement connu. Si vous échouez, quelqu'un a verrouillé le fichier, vous avez terminé.

  2. Lorsque vous avez terminé normalement, fermez et supprimez le fichier PID afin que quelqu'un d'autre puisse l'écraser.

Vous pouvez envelopper votre programme dans un script shell qui supprime le fichier PID même si votre programme plante.

Vous pouvez également utiliser le fichier PID pour tuer le programme s'il se bloque.


3

L'utilisation d'un fichier de verrouillage est une approche assez courante sous unix. En cas de panne, vous devez nettoyer manuellement. Vous pouvez stocker le PID dans le fichier et, au démarrage, vérifier s'il existe un processus avec ce PID, en remplaçant le fichier de verrouillage dans le cas contraire. (Cependant, vous avez également besoin d'un verrou autour du fichier read-file-check-pid-rewrite-file). Vous trouverez ce dont vous avez besoin pour obtenir et vérifier pid dans l' os paquet . La manière courante de vérifier s'il existe un processus avec un pid donné est de lui envoyer un signal non fatal.

D'autres alternatives pourraient être de combiner cela avec des sémaphores flock ou posix.

Ouvrir une prise réseau, comme le proposait saua, serait probablement la plus simple et la plus portable.


3

Pour toute personne utilisant wxPython pour son application, vous pouvez utiliser la fonction wx.SingleInstanceChecker documentée ici .

Personnellement , j'utiliser une sous - classe wx.Appqui utilise wx.SingleInstanceCheckeret revient Falsede OnInit()s'il y a une instance existante de l'application exécutant déjà comme ceci:

import wx

class SingleApp(wx.App):
    """
    class that extends wx.App and only permits a single running instance.
    """

    def OnInit(self):
        """
        wx.App init function that returns False if the app is already running.
        """
        self.name = "SingleApp-%s".format(wx.GetUserId())
        self.instance = wx.SingleInstanceChecker(self.name)
        if self.instance.IsAnotherRunning():
            wx.MessageBox(
                "An instance of the application is already running", 
                "Error", 
                 wx.OK | wx.ICON_WARNING
            )
            return False
        return True

Il s'agit d'un simple remplacement instantané wx.Appqui interdit plusieurs instances. Pour l'utiliser, remplacez-le simplement wx.Apppar SingleAppdans votre code comme ceci:

app = SingleApp(redirect=False)
frame = wx.Frame(None, wx.ID_ANY, "Hello World")
frame.Show(True)
app.MainLoop()

Après avoir codé un thread de liste de sockets pour un singleton, j'ai trouvé ceci, qui fonctionne très bien et j'ai déjà installé dans un programme de couple, cependant, je voudrais le "réveil" supplémentaire que je peux donner au singleton afin que je puisse l'amener au devant et au centre d'un gros tas de fenêtres qui se chevauchent. Aussi: le lien «documenté ici» pointe vers une documentation assez inutile générée automatiquement, c'est un meilleur lien
RufusVS

@RufusVS Vous avez raison - c'est un bien meilleur lien de documentation, nous avons mis à jour la réponse.
Matt Coubrough

3

Voici mon éventuelle solution Windows uniquement. Mettez ce qui suit dans un module, peut-être appelé «onlyone.py», ou autre. Incluez ce module directement dans votre __ fichier de script python __ main __.

import win32event, win32api, winerror, time, sys, os
main_path = os.path.abspath(sys.modules['__main__'].__file__).replace("\\", "/")

first = True
while True:
        mutex = win32event.CreateMutex(None, False, main_path + "_{<paste YOUR GUID HERE>}")
        if win32api.GetLastError() == 0:
            break
        win32api.CloseHandle(mutex)
        if first:
            print "Another instance of %s running, please wait for completion" % main_path
            first = False
        time.sleep(1)

Explication

Le code tente de créer un mutex avec un nom dérivé du chemin d'accès complet au script. Nous utilisons des barres obliques pour éviter toute confusion potentielle avec le système de fichiers réel.

Avantages

  • Aucune configuration ou identifiant «magique» nécessaire, utilisez-le dans autant de scripts différents que nécessaire.
  • Pas de fichiers périmés, le mutex meurt avec vous.
  • Imprime un message utile en attendant

3

La meilleure solution pour cela sur Windows est d'utiliser des mutex comme suggéré par @zgoda.

import win32event
import win32api
from winerror import ERROR_ALREADY_EXISTS

mutex = win32event.CreateMutex(None, False, 'name')
last_error = win32api.GetLastError()

if last_error == ERROR_ALREADY_EXISTS:
   print("App instance already running")

Certaines réponses utilisent fctnl(inclus également dans le package @sorin tendo) qui n'est pas disponible sur Windows et si vous essayez de geler votre application python en utilisant un package comme celui pyinstallerqui effectue des importations statiques, cela génère une erreur.

En outre, l'utilisation de la méthode de fichier de verrouillage crée un read-onlyproblème avec les fichiers de base de données (rencontré avec sqlite3).


2

Je publie ceci comme réponse car je suis un nouvel utilisateur et Stack Overflow ne me permettra pas encore de voter.

La solution de Sorin Sbarnea fonctionne pour moi sous OS X, Linux et Windows, et j'en suis reconnaissant.

Cependant, tempfile.gettempdir () se comporte d'une manière sous OS X et Windows et d'une autre sous certains / plusieurs / tous (?) * Nixes (en ignorant le fait qu'OS X est aussi Unix!). La différence est importante pour ce code.

OS X et Windows ont des répertoires temporaires spécifiques à l'utilisateur, donc un fichier temporaire créé par un utilisateur n'est pas visible pour un autre utilisateur. En revanche, sous de nombreuses versions de * nix (j'ai testé Ubuntu 9, RHEL 5, OpenSolaris 2008 et FreeBSD 8), le répertoire temporaire est / tmp pour tous les utilisateurs.

Cela signifie que lorsque le fichier de verrouillage est créé sur une machine multi-utilisateurs, il est créé dans / tmp et que seul l'utilisateur qui crée le fichier de verrouillage la première fois pourra exécuter l'application.

Une solution possible consiste à intégrer le nom d'utilisateur actuel dans le nom du fichier de verrouillage.

Il convient de noter que la solution de l'OP consistant à saisir un port se comportera également mal sur une machine multi-utilisateur.


Pour certains lecteurs (par exemple moi), le comportement souhaité est qu'une seule copie peut être exécutée, quel que soit le nombre d'utilisateurs impliqués. Ainsi, les répertoires tmp par utilisateur sont cassés, tandis que le verrou partagé / tmp ou port montre le comportement souhaité.
Jonathan Hartley


1

Je soupçonne toujours qu'il devrait y avoir une bonne solution POSIXy utilisant des groupes de processus, sans avoir à frapper le système de fichiers, mais je ne peux pas tout à fait clouer. Quelque chose comme:

Au démarrage, votre processus envoie un «kill -0» à tous les processus d'un groupe particulier. Si de tels processus existent, il s'arrête. Puis il rejoint le groupe. Aucun autre processus n'utilise ce groupe.

Cependant, cela a une condition de concurrence - plusieurs processus pourraient tous faire cela exactement en même temps et tous finiraient par rejoindre le groupe et fonctionner simultanément. Au moment où vous avez ajouté une sorte de mutex pour le rendre étanche, vous n'avez plus besoin des groupes de processus.

Cela pourrait être acceptable si votre processus ne démarre que par cron, une fois par minute ou toutes les heures, mais cela me rend un peu nerveux à l'idée que cela se passe mal précisément le jour où vous ne le souhaitez pas.

Je suppose que ce n'est pas une très bonne solution après tout, à moins que quelqu'un ne puisse l'améliorer?


1

J'ai rencontré ce problème exact la semaine dernière, et bien que j'aie trouvé de bonnes solutions, j'ai décidé de créer un package python très simple et propre et de le télécharger sur PyPI. Il diffère de tendo en ce qu'il peut verrouiller n'importe quel nom de ressource de chaîne. Bien que vous puissiez certainement verrouiller __file__pour obtenir le même effet.

Installer avec: pip install quicklock

Son utilisation est extrêmement simple:

[nate@Nates-MacBook-Pro-3 ~/live] python
Python 2.7.6 (default, Sep  9 2014, 15:04:36)
[GCC 4.2.1 Compatible Apple LLVM 6.0 (clang-600.0.39)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> from quicklock import singleton
>>> # Let's create a lock so that only one instance of a script will run
...
>>> singleton('hello world')
>>>
>>> # Let's try to do that again, this should fail
...
>>> singleton('hello world')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/nate/live/gallery/env/lib/python2.7/site-packages/quicklock/quicklock.py", line 47, in singleton
    raise RuntimeError('Resource <{}> is currently locked by <Process {}: "{}">'.format(resource, other_process.pid, other_process.name()))
RuntimeError: Resource <hello world> is currently locked by <Process 24801: "python">
>>>
>>> # But if we quit this process, we release the lock automatically
...
>>> ^D
[nate@Nates-MacBook-Pro-3 ~/live] python
Python 2.7.6 (default, Sep  9 2014, 15:04:36)
[GCC 4.2.1 Compatible Apple LLVM 6.0 (clang-600.0.39)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> from quicklock import singleton
>>> singleton('hello world')
>>>
>>> # No exception was thrown, we own 'hello world'!

Jetez un œil: https://pypi.python.org/pypi/quicklock


1
Je viens de l'installer via "pip install quicklock", mais quand j'essaye de l'utiliser via "from quicklock import singleton" j'obtiens une exception: "ImportError: impossible d'importer le nom" singleton "". Ceci est sur un Mac.
grayaii

Il s'avère que quicklock ne fonctionne pas avec python 3. C'est la raison pour laquelle il a échoué pour moi.
grayaii

Oui, désolé, ce n'était pas du tout à l'épreuve du temps. Je serai heureux de recevoir une contribution pour le faire fonctionner!
Nate Ferrero

1

En me basant sur la réponse de Roberto Rosario, je propose la fonction suivante:

SOCKET = None
def run_single_instance(uniq_name):
    try:
        import socket
        global SOCKET
        SOCKET = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
        ## Create an abstract socket, by prefixing it with null.
        # this relies on a feature only in linux, when current process quits, the
        # socket will be deleted.
        SOCKET.bind('\0' + uniq_name)
        return True
    except socket.error as e:
        return False

Nous devons définir la SOCKETvariable globale car elle ne sera récupérée que lorsque l'ensemble du processus se fermera. Si nous déclarons une variable locale dans la fonction, elle sortira de la portée après la sortie de la fonction, donc le socket sera supprimé.

Tout le mérite revient à Roberto Rosario, puisque je ne fais que clarifier et développer son code. Et ce code ne fonctionnera que sous Linux, comme l' explique le texte cité suivant de https://troydhanson.github.io/network/Unix_domain_sockets.html :

Linux a une particularité: si le chemin d'une socket de domaine UNIX commence par un octet nul \ 0, son nom n'est pas mappé dans le système de fichiers. Ainsi, il n'entrera pas en collision avec d'autres noms dans le système de fichiers. De plus, lorsqu'un serveur ferme son socket d'écoute de domaine UNIX dans l'espace de noms abstrait, son fichier est supprimé; avec les sockets de domaine UNIX standard, le fichier persiste après sa fermeture par le serveur.


0

exemple Linux

Cette méthode repose sur la création d'un fichier temporaire supprimé automatiquement après la fermeture de l'application. le lancement du programme nous vérifions l'existence du fichier; si le fichier existe (il y a une exécution en attente), le programme est fermé; sinon, il crée le fichier et poursuit l'exécution du programme.

from tempfile import *
import time
import os
import sys


f = NamedTemporaryFile( prefix='lock01_', delete=True) if not [f  for f in     os.listdir('/tmp') if f.find('lock01_')!=-1] else sys.exit()

YOUR CODE COMES HERE

1
Bienvenue dans Stack Overflow! Bien que cette réponse puisse être correcte, veuillez ajouter quelques explications. Il est plus important de transmettre la logique sous-jacente que de simplement donner le code, car cela aide l'OP et les autres lecteurs à résoudre eux-mêmes ce problème et des problèmes similaires.
CodeMouse92

Est-ce threadsafe? On dirait que la vérification et la création du fichier temporaire ne sont pas atomiques ...
coppit

0

Sur un système Linux, on peut également demander pgrep -ale nombre d'instances, le script se trouve dans la liste des processus (l'option -a révèle la chaîne complète de la ligne de commande). Par exemple

import os
import sys
import subprocess

procOut = subprocess.check_output( "/bin/pgrep -u $UID -a python", shell=True, 
                                   executable="/bin/bash", universal_newlines=True)

if procOut.count( os.path.basename(__file__)) > 1 :        
    sys.exit( ("found another instance of >{}<, quitting."
              ).format( os.path.basename(__file__)))

Supprimer -u $UIDsi la restriction doit s'appliquer à tous les utilisateurs. Avertissement: a) il est supposé que le nom (de base) du script est unique, b) il peut y avoir des conditions de concurrence.


-1
import sys,os

# start program
try:  # (1)
    os.unlink('lock')  # (2)
    fd=os.open("lock", os.O_CREAT|os.O_EXCL) # (3)  
except: 
    try: fd=os.open("lock", os.O_CREAT|os.O_EXCL) # (4) 
    except:  
        print "Another Program running !.."  # (5)
        sys.exit()  

# your program  ...
# ...

# exit program
try: os.close(fd)  # (6)
except: pass
try: os.unlink('lock')  
except: pass
sys.exit()  

2
Bienvenue dans Stack Overflow! Bien que ce bloc de code puisse répondre à la question, il serait préférable que vous fournissiez une petite explication pour expliquer pourquoi il le fait. Veuillez modifier votre réponse pour inclure une telle description.
Artjom B.
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.