Asegúrese de que solo se esté ejecutando una instancia de un programa

120

¿Existe una forma Pythonic de tener solo una instancia de un programa en ejecución?

La única solución razonable que se me ocurrió es intentar ejecutarlo como un servidor en algún puerto, luego el segundo programa que intenta vincularse al mismo puerto falla. Pero realmente no es una gran idea, ¿tal vez hay algo más liviano que esto?

(Tenga en cuenta que se espera que el programa falle a veces, es decir, segfault, por lo que cosas como "bloquear archivo" no funcionarán)

Slava V
fuente
1
Quizás su vida sería más fácil si rastreara y arreglara la falla de segmento. No es que sea fácil de hacer.
David Locke
No está en mi biblioteca, está en los enlaces libxml de Python y es extremadamente tímido: se activa solo una vez cada dos días.
Slava V
5
La biblioteca estándar de Python admite flock (), que es lo correcto para los programas UNIX modernos. La apertura de un puerto usa un lugar en un espacio de nombres mucho más restringido, mientras que los archivos pid son más complejos, ya que necesita verificar los procesos en ejecución para invalidarlos de manera segura; el rebaño no tiene ningún problema.
Charles Duffy
s / UNIX / linux / listo, FTFY.
kaleissin

Respuestas:

100

El siguiente código debería hacer el trabajo, es multiplataforma y se ejecuta en Python 2.4-3.2. Lo probé en Windows, OS X y Linux.

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

La última versión del código está disponible singleton.py . Por favor, presente los errores aquí .

Puede instalar tender utilizando uno de los siguientes métodos:

sorin
fuente
2
Actualicé la respuesta e incluí un enlace a la última versión. Si encuentra un error, envíelo a github y lo resolveré lo antes posible.
sorin
2
@Johny_M Gracias, he hecho un parche y liberado una versión más reciente de pypi.python.org/pypi/tendo
Sorin
2
Esta sintaxis no me funcionó en Windows bajo Python 2.6. Lo que funcionó para mí fue: 1: de tendo import singleton 2: me = singleton.SingleInstance ()
Brian
25
¿Otra dependencia para algo tan trivial como esto? No suena muy atractivo.
WhyNotHugo
2
¿El singleton maneja procesos que obtienen un sigterm (por ejemplo, si un proceso se está ejecutando durante demasiado tiempo), o tengo que manejar eso?
JimJty
43

Solución simple y multiplataforma , encontrada en otra pregunta 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

Muy parecido a la sugerencia de S.Lott, pero con el código.

Slava V
fuente
Por curiosidad: ¿es esto realmente multiplataforma? ¿Funciona en Windows?
Joachim Sauer
1
No hay ningún fcntlmódulo en Windows (aunque la funcionalidad podría emularse).
jfs
10
SUGERENCIA: si desea ajustar esto en una función, 'fp' debe ser global o el archivo se cerrará después de que la función salga.
cmcginty
1
@Mirko Control + Z no sale de una aplicación (en cualquier sistema operativo que conozca), la suspende. La aplicación se puede volver al primer plano con fg. Entonces, parece que está funcionando correctamente para usted (es decir, la aplicación aún está activa, pero suspendida, por lo que el bloqueo permanece en su lugar).
Sam Bull
1
Este código en mi situación (Python 3.8.3 en Linux) necesitaba modificación:lock_file_pointer = os.open(lock_path, os.O_WRONLY | os.O_CREAT)
baziorek
30

Este código es específico de Linux. Utiliza sockets de dominio UNIX 'abstractos', pero es simple y no dejará archivos de bloqueo obsoletos. Lo prefiero a la solución anterior porque no requiere un puerto TCP especialmente reservado.

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 cadena única postconnect_gateway_notify_lockse puede cambiar para permitir múltiples programas que necesitan que se aplique una sola instancia.

Roberto Rosario
fuente
1
Roberto, ¿estás seguro de que después de un reinicio completo o de emergencia del kernel, el archivo \ 0postconnect_gateway_notify_lock no estará presente en el arranque? En mi caso, el archivo de socket AF_UNIX todavía está presente después de esto y esto destruye toda la idea. La solución anterior con la adquisición de un bloqueo en un nombre de archivo específico es mucho más confiable en este caso.
Danylo Gurianov
2
Como se señaló anteriormente, esta solución funciona en Linux pero no en Mac OS X.
Bilal y Olga
2
Esta solución no funciona. Lo probé en Ubuntu 14.04. Ejecute el mismo script desde 2 ventanas de terminal simultáneamente. Ambos funcionan bien.
Dimon
1
Esto funcionó para mí en Ubuntu 16. Y matar el proceso por cualquier medio permitió que se iniciara otro. Dimon, creo que hiciste algo mal en tu prueba. (Quizás olvidó hacer que su script se durmiera después de que se ejecutó el código anterior, por lo que inmediatamente salió y liberó el socket).
Luke
1
No es cuestión de dormir. El código funciona pero solo como código en línea. Lo estaba poniendo en una función. El enchufe desapareció tan pronto como existió la función.
Steve Cohen
25

No sé si es lo suficientemente pitónico, pero en el mundo de Java escuchar en un puerto definido es una solución bastante utilizada, ya que funciona en todas las plataformas principales y no tiene ningún problema con los programas que se bloquean.

Otra ventaja de escuchar un puerto es que puede enviar un comando a la instancia en ejecución. Por ejemplo, cuando los usuarios inician el programa por segunda vez, puede enviar a la instancia en ejecución un comando para indicarle que abra otra ventana (eso es lo que hace Firefox, por ejemplo. No sé si usan puertos TCP o canalizaciones con nombre o algo así ', aunque).

Joachim Sauer
fuente
+1 a esto, especialmente porque me permite notificar la instancia en ejecución, por lo que crea otra ventana, aparece, etc.
WhyNotHugo
1
Utilice por ejemplo import socket; s = socket.socket(socket.AF_INET, socket.SOCK_STREAM); s.bind(('localhost', DEFINED_PORT)). Se OSErrorgenerará un si otro proceso está vinculado al mismo puerto.
crishoj
13

Nunca antes escribí python, pero esto es lo que acabo de implementar en mycheckpoint, para evitar que crond lo inicie dos veces o más:

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

Encontré la sugerencia de Slava-N después de publicar esto en otro número (http://stackoverflow.com/questions/2959474). Este se llama como una función, bloquea el archivo de scripts en ejecución (no un archivo pid) y mantiene el bloqueo hasta que el script finaliza (normal o error).

MD Klapwijk
fuente
1
Muy elegante. Lo cambié para que obtenga la ruta de los argumentos del script. También recomienda incrustar esto en algún lugar común - Ejemplo
Jossef Harush
10

Utilice un archivo pid. Tienes una ubicación conocida, "/ ruta / a / pidfile" y al iniciar haces algo como esto (parcialmente pseudocódigo porque estoy pre-café y no quiero trabajar tan duro):

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

Entonces, en otras palabras, está comprobando si existe un archivo pid; si no, escriba su pid en ese archivo. Si el archivo pid existe, compruebe si es el pid de un proceso en ejecución; si es así, entonces tiene otro proceso en vivo ejecutándose, así que cierre. De lo contrario, el proceso anterior se bloqueó, así que regístrelo y luego escriba su propio pid en el archivo en lugar del anterior. Entonces continúe.

Charlie martin
fuente
4
Esto tiene una condición de carrera. La secuencia de prueba-luego-escritura puede generar una excepción de dos programas que se inician casi simultáneamente, no encuentran ningún archivo e intentan abrirse para escritura al mismo tiempo. Se debe lanzar una excepción en uno, permitiendo que el otro para continuar.
S.Lott
5

Esto puede funcionar.

  1. Intente crear un archivo PID en una ubicación conocida. Si falla, alguien tiene el archivo bloqueado, ya está.

  2. Cuando termine normalmente, cierre y elimine el archivo PID para que otra persona pueda sobrescribirlo.

Puede envolver su programa en un script de shell que elimina el archivo PID incluso si su programa falla.

También puede usar el archivo PID para matar el programa si se cuelga.

S. Lot
fuente
3

El uso de un archivo de bloqueo es un enfoque bastante común en Unix. Si falla, debe limpiar manualmente. Puede almacenar el PID en el archivo y, en el inicio, verificar si hay un proceso con este PID, anulando el archivo de bloqueo si no es así. (Sin embargo, también necesita un bloqueo alrededor de read-file-check-pid-rewrite-file). Encontrará lo que necesita para obtener y verificar pid en el paquete del sistema operativo. La forma común de comprobar si existe un proceso con un pid determinado es enviarle una señal no fatal.

Otras alternativas podrían combinar esto con semáforos flock o posix.

Abrir un conector de red, como propuso saua, probablemente sería lo más fácil y portátil.

Rolf Rander
fuente
3

Para cualquiera que use wxPython para su aplicación, puede usar la función wx.SingleInstanceChecker documentada aquí .

Yo personalmente uso una subclase de wx.Appla que hace uso de wx.SingleInstanceCheckery vuelve Falsea OnInit()si hay una instancia existente de la aplicación ya la ejecución de este modo:

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

Este es un simple reemplazo directo wx.Appque prohíbe múltiples instancias. Para usarlo, simplemente reemplazar wx.Appcon SingleAppsu código de este modo:

app = SingleApp(redirect=False)
frame = wx.Frame(None, wx.ID_ANY, "Hello World")
frame.Show(True)
app.MainLoop()
Matt Coubrough
fuente
Después de codificar un hilo de lista de sockets para un singleton, encontré esto, que funciona muy bien y ya lo he instalado en un par de programas, sin embargo, me gustaría el "despertador" adicional que puedo dar al singleton para poder llevarlo al al frente y al centro de una gran pila de ventanas superpuestas. Además: el enlace "documentado aquí" apunta a documentación autogenerada bastante inútil, este es un enlace mejor
RufusVS
@RufusVS Tienes razón, ese es un enlace de documentación mucho mejor, hemos actualizado la respuesta.
Matt Coubrough
3

Aquí está mi solución eventual solo para Windows. Ponga lo siguiente en un módulo, quizás llamado 'onlyone.py', o lo que sea. Incluya ese módulo directamente en su archivo de secuencia de comandos de Python __ principal __.

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)

Explicación

El código intenta crear un mutex con un nombre derivado de la ruta completa al script. Usamos barras diagonales para evitar posibles confusiones con el sistema de archivos real.

Ventajas

  • No se necesitan configuraciones o identificadores "mágicos", utilícelo en tantos scripts diferentes como necesite.
  • No quedan archivos obsoletos, el mutex muere contigo.
  • Imprime un mensaje útil cuando espera
Keeely
fuente
3

La mejor solución para esto en Windows es usar mutex como lo sugiere @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")

Algunas respuestas usan fctnl(incluido también en el paquete @sorin tendo) que no está disponible en Windows y si intenta congelar su aplicación de Python usando un paquete comopyinstaller que hace importaciones estáticas, arroja un error.

Además, el uso del método de archivo de bloqueo crea un read-onlyproblema con los archivos de la base de datos (experimentado esto con sqlite3).

Chuck G
fuente
2

Estoy publicando esto como respuesta porque soy un usuario nuevo y Stack Overflow todavía no me deja votar.

La solución de Sorin Sbarnea me funciona en OS X, Linux y Windows, y estoy agradecido por ello.

Sin embargo, tempfile.gettempdir () se comporta de una manera en OS X y Windows y de otra en algunos / muchos / todos (?) * Nixes (¡ignorando el hecho de que OS X también es Unix!). La diferencia es importante para este código.

OS X y Windows tienen directorios temporales específicos del usuario, por lo que un archivo temporal creado por un usuario no es visible para otro usuario. Por el contrario, en muchas versiones de * nix (probé Ubuntu 9, RHEL 5, OpenSolaris 2008 y FreeBSD 8), el directorio temporal es / tmp para todos los usuarios.

Eso significa que cuando el archivo de bloqueo se crea en una máquina multiusuario, se crea en / tmp y solo el usuario que crea el archivo de bloqueo por primera vez podrá ejecutar la aplicación.

Una posible solución es incrustar el nombre de usuario actual en el nombre del archivo de bloqueo.

Vale la pena señalar que la solución del OP de tomar un puerto también se comportará mal en una máquina multiusuario.

Philip Semanchuk
fuente
Para algunos lectores (por ejemplo, yo), el comportamiento deseado es que solo se pueda ejecutar una copia, independientemente de cuántos usuarios estén involucrados. Entonces, los directorios tmp por usuario están rotos, mientras que el / tmp compartido o el bloqueo de puerto exhiben el comportamiento deseado.
Jonathan Hartley
2

Lo uso single_processen mi gentoo;

pip install single_process

ejemplo :

from single_process import single_process

@single_process
def main():
    print 1

if __name__ == "__main__":
    main()   

consulte: https://pypi.python.org/pypi/single_process/1.0

gkiwi
fuente
Falla en Py3. El paquete parece estar mal construido.
Ekevoo
En Windows obtengo: ImportError: Ningún módulo llamado fcntl
Andrew W. Phillips
1

Sigo sospechando que debería haber una buena solución POSIXy usando grupos de procesos, sin tener que presionar el sistema de archivos, pero no puedo concretarlo. Algo como:

En el inicio, su proceso envía un 'kill -0' a todos los procesos en un grupo en particular. Si existe alguno de estos procesos, sale. Luego se une al grupo. Ningún otro proceso usa ese grupo.

Sin embargo, esto tiene una condición de carrera: múltiples procesos podrían hacer esto exactamente al mismo tiempo y todos terminan uniéndose al grupo y ejecutándose simultáneamente. Cuando haya agregado algún tipo de mutex para que sea hermético, ya no necesitará los grupos de procesos.

Esto podría ser aceptable si su proceso solo se inicia con cron, una vez cada minuto o cada hora, pero me pone un poco nervioso que salga mal precisamente el día en que no lo desea.

Supongo que esta no es una muy buena solución después de todo, a menos que alguien pueda mejorarla.

Jonathan Hartley
fuente
1

Me encontré con este problema exacto la semana pasada, y aunque encontré algunas buenas soluciones, decidí hacer un paquete de Python muy simple y limpio y lo cargué en PyPI. Se diferencia de tendo en que puede bloquear cualquier nombre de recurso de cadena. Aunque ciertamente podrías bloquear__file__ para lograr el mismo efecto.

Instalar con: pip install quicklock

Usarlo es extremadamente 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'!

Eche un vistazo: https://pypi.python.org/pypi/quicklock

Nate Ferrero
fuente
1
Acabo de instalarlo a través de "pip install quicklock", pero cuando intento usarlo a través de "from quicklock import singleton" obtengo una excepción: "ImportError: no se puede importar el nombre 'singleton'". Esto es en una Mac.
grayaii
Resulta que quicklock no funciona con python 3. Esa es la razón por la que me estaba fallando.
grayaii
Sí, lo siento, no estaba preparado para el futuro. ¡Daré la bienvenida a una contribución para que funcione!
Nate Ferrero
1

Basándome en la respuesta de Roberto Rosario, se me ocurre la siguiente función:

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

Necesitamos definir global SOCKET variable ya que solo se recolectará basura cuando se cierre todo el proceso. Si declaramos una variable local en la función, saldrá del alcance después de que la función salga, por lo que se eliminará el socket.

Todo el mérito debe ser para Roberto Rosario, ya que solo aclaro y amplío su código. Y este código solo funcionará en Linux, como explica el siguiente texto citado de https://troydhanson.github.io/network/Unix_domain_sockets.html :

Linux tiene una característica especial: si el nombre de la ruta de un socket de dominio UNIX comienza con un byte nulo \ 0, su nombre no se asigna al sistema de archivos. Por lo tanto, no chocará con otros nombres en el sistema de archivos. Además, cuando un servidor cierra su socket de escucha de dominio UNIX en el espacio de nombres abstracto, su archivo se elimina; con sockets de dominio UNIX normales, el archivo persiste después de que el servidor lo cierra.

makiko_fly
fuente
0

ejemplo de linux

Este método se basa en la creación de un archivo temporal que se elimina automáticamente después de cerrar la aplicación. al iniciar el programa verificamos la existencia del archivo; si el archivo existe (hay una ejecución pendiente), el programa se cierra; de lo contrario, crea el archivo y continúa la ejecución del programa.

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
kerwal
fuente
1
¡Bienvenido a Stack Overflow! Si bien esta respuesta puede ser correcta, agregue alguna explicación. Impartir la lógica subyacente es más importante que solo dar el código, porque ayuda al OP y a otros lectores a solucionar este y otros problemas similares.
CodeMouse92
¿Es seguro este hilo? Parece que la verificación y la creación del archivo temporal no son atómicos ...
coppit
0

En un sistema Linux, también se puede solicitar pgrep -ael número de instancias, el script se encuentra en la lista de procesos (la opción -a revela la cadena de línea de comandos completa). P.ej

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

Elimínelo -u $UIDsi la restricción debe aplicarse a todos los usuarios. Descargo de responsabilidad: a) se asume que el nombre del script (base) es único, b) puede haber condiciones de carrera.

user71769
fuente
-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()  
Ertan Özer
fuente
2
¡Bienvenido a Stack Overflow! Si bien este bloque de código puede responder la pregunta, sería mejor si pudiera proporcionar una pequeña explicación de por qué lo hace. Por favor, editar su respuesta para incluir tal descripción.
Artjom B.