Убедитесь, что запущен только один экземпляр программы

120

Есть ли способ Python запустить только один экземпляр программы?

Единственное разумное решение, которое я придумал, - это попытаться запустить его как сервер на каком-то порте, а затем вторая программа, пытающаяся привязаться к тому же порту, терпит неудачу. Но это не очень хорошая идея, может быть, есть что-то более легкое, чем это?

(Учтите, что иногда ожидается сбой программы, например segfault - поэтому такие вещи, как "файл блокировки" не будут работать)

Слава В
источник
1
Возможно, ваша жизнь была бы проще, если бы вы отследили и исправили segfault. Не то чтобы это было легко сделать.
Дэвид Локк,
Его нет в моей библиотеке, он в привязках python libxml и очень застенчив - срабатывает только раз в пару дней.
Slava V
5
Стандартная библиотека Python поддерживает flock (), что является правильным решением для современных программ UNIX. При открытии порта используется место в гораздо более ограниченном пространстве имен, тогда как файлы pid более сложны, так как вам необходимо проверить запущенные процессы, чтобы безопасно сделать их недействительными; у flock нет ни одной проблемы.
Чарльз Даффи,
s / UNIX / linux / вот и все, FTFY.
kaleissin

Ответы:

101

Следующий код должен выполнить эту работу, он кроссплатформенный и работает на Python 2.4-3.2. Я тестировал его в Windows, OS X и Linux.

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

Доступна последняя версия кода singleton.py . Пожалуйста, сообщайте об ошибках здесь .

Вы можете установить тендер одним из следующих способов:

Сорин
источник
2
Я обновил ответ и включил ссылку на последнюю версию. Если вы обнаружите ошибку, отправьте ее на github, и я исправлю ее как можно скорее.
Sorin
2
@Johny_M Спасибо, я сделал патч и выпустил более новую версию на pypi.python.org/pypi/tendo
sorin
2
Этот синтаксис не работал у меня в Windows под Python 2.6. Что сработало для меня: 1: из импорта синглтона тендо 2: я = синглтон.SingleInstance ()
Брайан
25
Еще одна зависимость от такой тривиальной вещи? Звучит не очень привлекательно.
WhyNotHugo
2
Обрабатывает ли singleton процессы, которые получают sigterm (например, если процесс выполняется слишком долго), или мне нужно это обрабатывать?
JimJty
43

Простое, кросс-платформенное решение, нашел в другом вопрос по 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

Очень похоже на предложение С.Лотта, но с кодом.

Слава В
источник
Из любопытства: действительно ли это кроссплатформенный? На винде работает?
Joachim Sauer
1
В fcntlWindows нет модуля (хотя функциональность можно эмулировать).
jfs
10
СОВЕТ: если вы хотите заключить это в функцию, 'fp' должен быть глобальным, иначе файл будет закрыт после выхода из функции.
cmcginty
1
@Mirko Control + Z не закрывает приложение (на любой известной мне ОС), а приостанавливает его. Приложение можно вернуть на передний план с помощью fg. Итак, похоже, что оно у вас работает правильно (т.е. приложение все еще активно, но приостановлено, поэтому блокировка остается на месте).
Сэм Булл,
1
Этот код в моей ситуации (Python 3.8.3 в Linux) нуждался в модификации:lock_file_pointer = os.open(lock_path, os.O_WRONLY | os.O_CREAT)
baziorek
30

Этот код специфичен для Linux. Он использует «абстрактные» доменные сокеты UNIX, но он прост и не оставляет устаревших файлов блокировки. Я предпочитаю это решение, потому что оно не требует специально зарезервированного TCP-порта.

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) 

Уникальную строку postconnect_gateway_notify_lockможно изменить, чтобы разрешить использование нескольких программ, которым требуется один экземпляр.

Роберто Росарио
источник
1
Роберто, вы уверены, что после паники ядра или полной перезагрузки файл \ 0postconnect_gateway_notify_lock не будет присутствовать при загрузке? В моем случае файл сокета AF_UNIX все еще присутствует после этого, и это разрушает всю идею. Вышеупомянутое решение с установкой блокировки на конкретное имя файла в этом случае очень надежно.
Данило Гурьянов
2
Как отмечалось выше, это решение работает в Linux, но не в Mac OS X.
Билал и Ольга
2
Это решение не работает. Пробовал на Ubuntu 14.04. Запустите один и тот же сценарий одновременно из двух окон терминала. Они оба отлично бегают.
Димон
1
У меня это сработало в Ubuntu 16. И завершение процесса любым способом позволило запустить другой. Даймон, я думаю, ты сделал что-то не так в тесте. (Возможно, вы забыли перевести свой скрипт в спящий режим после выполнения приведенного выше кода, поэтому он немедленно вышел и освободил сокет.)
Люк,
1
Это не вопрос сна. Код работает, но только как встроенный код. Я вставлял это в функцию. Сокет исчезал, как только функция существовала.
Стив Коэн
25

Я не знаю, достаточно ли он питонический, но в мире Java прослушивание определенного порта - довольно широко используемое решение, поскольку оно работает на всех основных платформах и не имеет проблем с аварийным завершением программ.

Еще одно преимущество прослушивания порта состоит в том, что вы можете отправить команду запущенному экземпляру. Например, когда пользователи запускают программу во второй раз, вы можете отправить запущенному экземпляру команду, чтобы он открыл другое окно (например, это то, что делает Firefox. Я не знаю, используют ли они порты TCP или именованные каналы или что-то вроде этого '').

Иоахим Зауэр
источник
+1 к этому, особенно потому, что он позволяет мне уведомлять запущенный экземпляр, поэтому он создает другое окно, всплывает и т. Д.
WhyNotHugo
1
Используйте, например import socket; s = socket.socket(socket.AF_INET, socket.SOCK_STREAM); s.bind(('localhost', DEFINED_PORT)). OSErrorБудет повышена , если другой процесс связан с тем же портом.
crishoj
13

Никогда раньше не писал python, но это то, что я только что реализовал в mycheckpoint, чтобы предотвратить его запуск дважды или более 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()

Нашел предложение Slava-N после публикации в другом выпуске (http://stackoverflow.com/questions/2959474). Он вызывается как функция, блокирует исполняемый файл сценария (не файл pid) и поддерживает блокировку до завершения сценария (нормального или ошибочного).

MD Klapwijk
источник
1
Очень элегантно. Я изменил его, чтобы он получал путь из аргументов скрипта. Также рекомендует встраивать это в
какое-
10

Используйте файл pid. У вас есть известное местоположение, «/ path / to / pidfile», и при запуске вы делаете что-то вроде этого (частично псевдокод, потому что я до кофе и не хочу так много работать):

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

Другими словами, вы проверяете, существует ли pid-файл; если нет, запишите свой pid в этот файл. Если pid-файл действительно существует, проверьте, является ли pid pid-идентификатором запущенного процесса; Если да, то у вас запущен еще один живой процесс, поэтому просто выключите его. Если нет, то предыдущий процесс потерпел крах, поэтому зарегистрируйте его, а затем запишите свой собственный pid в файл вместо старого. Тогда продолжай.

Чарли Мартин
источник
4
Это состояние гонки. Последовательность «тест-запись» может вызвать исключение, когда две программы запускаются почти одновременно, не находят файла и пытаются одновременно открыть для записи. Он должен вызвать исключение для одного, позволяя продолжить выполнение другого.
S.Lott
6

Вы уже нашли ответ на аналогичный вопрос в другом потоке, поэтому для полноты картины посмотрим, как добиться того же в объединении Windows с именем mutex.

http://code.activestate.com/recipes/474070/

Zgoda
источник
5

Это может сработать.

  1. Попытайтесь создать файл PID в известном месте. Если вы потерпели неудачу, кто-то заблокировал файл, все готово.

  2. Когда вы закончите в обычном режиме, закройте и удалите файл PID, чтобы кто-то другой мог его перезаписать.

Вы можете заключить свою программу в сценарий оболочки, который удаляет файл PID, даже если ваша программа выйдет из строя.

Вы также можете использовать файл PID, чтобы убить программу, если она зависнет.

С. Лотт
источник
3

Использование файла блокировки - довольно распространенный подход в unix. Если произойдет сбой, вам придется очистить его вручную. Вы можете сохранить PID в файле и при запуске проверить, есть ли процесс с этим PID, переопределив файл блокировки, если нет. (Однако вам также нужна блокировка вокруг файла read-file-check-pid-rewrite-file). Вы найдете то, что вам нужно для получения и проверки pid, в пакете os . Обычный способ проверить, существует ли процесс с данным pid, - отправить ему нефатальный сигнал.

Другими альтернативами могут быть комбинирование этого с семафорами flock или posix.

Открытие сетевого сокета, как предлагает saua, вероятно, будет самым простым и портативным.

Рольф Рандер
источник
3

Для всех, кто использует wxPython для своих приложений, вы можете использовать wx.SingleInstanceChecker описанную здесь функцию .

Я лично использую подкласс , wx.Appкоторый делает использование wx.SingleInstanceCheckerи возврат Falseиз OnInit()если есть существующий экземпляр приложения уже выполняется следующим образом:

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

Это простая замена wx.App, запрещающая несколько экземпляров. Для того, чтобы использовать его просто заменить wx.Appс SingleAppв коде следующим образом:

app = SingleApp(redirect=False)
frame = wx.Frame(None, wx.ID_ANY, "Hello World")
frame.Show(True)
app.MainLoop()
Мэтт Коубро
источник
После кодирования потока со списком сокетов для синглтона я нашел это, который отлично работает, и я уже установил пару программ, однако мне бы хотелось, чтобы я мог дополнительно «пробудить» синглтон, чтобы я мог перенести его в спереди и по центру большой кучи перекрывающихся окон. Также: ссылка «задокументировано здесь» указывает на довольно бесполезную автоматически сгенерированную документацию, это лучшая ссылка
RufusVS
@RufusVS Вы правы - это гораздо лучшая ссылка на документацию, обновили ответ.
Мэтт Коубро
3

Вот мое возможное решение только для Windows. Поместите следующее в модуль, возможно, с именем onlyone.py или как-нибудь еще. Включите этот модуль непосредственно в ваш __ main __ файл скрипта python.

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)

объяснение

Код пытается создать мьютекс с именем, производным от полного пути к сценарию. Мы используем косую черту, чтобы избежать путаницы с реальной файловой системой.

преимущества

  • Никакой конфигурации или «волшебных» идентификаторов не требуется, используйте их в сколь угодно разных скриптах.
  • Не осталось устаревших файлов, мьютекс умрет вместе с вами.
  • Печатает полезное сообщение при ожидании
Keeely
источник
3

Лучшее решение для этого в Windows - использовать мьютексы, как предлагает @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")

В некоторых ответах используется fctnl(также входит в пакет @sorin teno), который недоступен в Windows, и если вы попытаетесь заморозить свое приложение python с помощью пакета, подобного тому, pyinstallerкоторый выполняет статический импорт, он выдает ошибку.

Кроме того, использование метода файла блокировки создает read-onlyпроблему с файлами базы данных (с этим сталкивался sqlite3).

Чак Джи
источник
2

Я отправляю это как ответ, потому что я новый пользователь и Stack Overflow пока не позволяет мне голосовать.

Решение Сорина Сбарнеа у меня работает под OS X, Linux и Windows, за что я ему благодарен.

Однако tempfile.gettempdir () ведет себя одним способом под OS X и Windows, а другим - под другими some / many / all (?) * Nixes (игнорируя тот факт, что OS X также является Unix!). Для этого кода важна разница.

В OS X и Windows есть временные каталоги для конкретного пользователя, поэтому временный файл, созданный одним пользователем, не виден другому пользователю. Напротив, во многих версиях * nix (я тестировал Ubuntu 9, RHEL 5, OpenSolaris 2008 и FreeBSD 8) временный каталог для всех пользователей - / tmp.

Это означает, что когда файл блокировки создается на многопользовательской машине, он создается в / tmp, и только пользователь, который создает файл блокировки в первый раз, сможет запустить приложение.

Возможное решение - встроить текущее имя пользователя в имя файла блокировки.

Стоит отметить, что решение OP по захвату порта также будет некорректно работать на многопользовательской машине.

Филипп Семанчук
источник
Для некоторых читателей (например, меня) желаемое поведение состоит в том, что только одна копия может работать за период, независимо от того, сколько пользователей задействовано. Таким образом, каталоги tmp для каждого пользователя не работают, в то время как общий / tmp или блокировка порта демонстрируют желаемое поведение.
Джонатан Хартли
2

Я использую single_processна своем Gentoo;

pip install single_process

пример :

from single_process import single_process

@single_process
def main():
    print 1

if __name__ == "__main__":
    main()   

см .: https://pypi.python.org/pypi/single_process/1.0

gkiwi
источник
Не работает в Py3. Упаковка выглядит неправильно.
Ekevoo
В Windows я получаю: ImportError: Нет модуля с именем fcntl
Эндрю В. Филлипс,
1

Я продолжаю подозревать, что должно быть хорошее решение POSIXy, использующее группы процессов, без необходимости затрагивать файловую систему, но я не могу его понять. Что-то вроде:

При запуске ваш процесс отправляет «kill -0» всем процессам в определенной группе. Если такие процессы существуют, он завершается. Затем он присоединяется к группе. Никакие другие процессы не используют эту группу.

Однако это имеет состояние гонки - все несколько процессов могут делать это в одно и то же время, и все в конечном итоге присоединяются к группе и работают одновременно. К тому времени, когда вы добавите какой-то мьютекс, чтобы сделать его водонепроницаемым, вам больше не нужны группы процессов.

Это может быть приемлемо, если ваш процесс запускается cron только раз в минуту или каждый час, но меня немного беспокоит, что он пойдет не так именно в тот день, когда вы этого не хотите.

Я думаю, это не очень хорошее решение, если только кто-то не может его улучшить?

Джонатан Хартли
источник
1

Я столкнулся с этой проблемой на прошлой неделе, и, хотя я нашел несколько хороших решений, я решил сделать очень простой и чистый пакет Python и загрузил его в PyPI. Он отличается от teno тем, что может заблокировать любое строковое имя ресурса. Хотя можно, конечно, заблокировать, __file__чтобы добиться такого же эффекта.

Установить с помощью: pip install quicklock

Использовать его предельно просто:

[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'!

Взгляните: https://pypi.python.org/pypi/quicklock

Нейт Ферреро
источник
1
Я только что установил его с помощью «pip install quicklock», но когда я пытаюсь использовать его с помощью «from quicklock import singleton», я получаю исключение: «ImportError: невозможно импортировать имя« singleton »». Это на Mac.
Grayaii
Оказалось, что quicklock не работает с python 3. Вот почему у меня он не работал.
Grayaii
Да, извините, это вообще не было рассчитано на будущее. Я буду приветствовать вклад, чтобы он заработал!
Нейт Ферреро
1

Основываясь на ответе Роберто Росарио, я придумал следующую функцию:

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

Нам нужно определить глобальный SOCKETvaiable, поскольку он будет собираться мусором только после завершения всего процесса. Если мы объявим локальную переменную в функции, она выйдет из области видимости после выхода из функции, и сокет будет удален.

Вся заслуга принадлежит Роберто Росарио, поскольку я только поясняю и уточняю его кодекс. И этот код будет работать только в Linux, как поясняет следующий цитируемый текст из https://troydhanson.github.io/network/Unix_domain_sockets.html :

В Linux есть особенность: если путь к сокету домена UNIX начинается с нулевого байта \ 0, его имя не отображается в файловой системе. Таким образом, он не будет конфликтовать с другими именами в файловой системе. Кроме того, когда сервер закрывает свой прослушивающий сокет домена UNIX в абстрактном пространстве имен, его файл удаляется; с обычными сокетами домена UNIX файл сохраняется после его закрытия сервером.

makiko_fly
источник
0

пример linux

Этот метод основан на создании временного файла, который автоматически удаляется после закрытия приложения. при запуске программы проверяем наличие файла; если файл существует (есть отложенное выполнение), программа закрывается; в противном случае он создает файл и продолжает выполнение программы.

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
источник
1
Добро пожаловать в Stack Overflow! Хотя этот ответ может быть правильным, пожалуйста, добавьте некоторые пояснения. Передача базовой логики более важна, чем просто предоставление кода, потому что это помогает OP и другим читателям исправить эту и подобные проблемы самостоятельно.
CodeMouse92,
Это потокобезопасно? Похоже, проверка и создание временного файла не атомарны ...
coppit
0

В системе Linux можно также запросить pgrep -aколичество экземпляров, сценарий находится в списке процессов (опция -a показывает полную строку командной строки). Например

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

Удалите, -u $UIDесли ограничение должно применяться ко всем пользователям. Отказ от ответственности: а) предполагается, что (базовое) имя скрипта уникально, б) могут быть условия гонки.

user71769
источник
-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
Добро пожаловать в Stack Overflow! Хотя этот блок кода может ответить на вопрос, было бы лучше, если бы вы могли дать небольшое объяснение, почему он это делает. Пожалуйста , измените свой ответ , чтобы включить такое описание.
Artjom B.