Переконайтеся, що працює лише один екземпляр програми


120

Чи існує пітонічний спосіб запуску лише одного екземпляра програми?

Єдине розумне рішення, яке я придумав - це спробувати запустити його як сервер на якомусь порту, тоді друга програма, яка намагається прив’язатись до того ж порту, - не вдасться. Але це насправді не чудова ідея, можливо, є щось легше, ніж це?

(Враховуйте, що очікується, що програма іноді виходить з ладу, тобто segfault - тому такі речі, як "файл блокування", не будуть працювати)


1
Можливо, ваше життя було б простішим, якби ви відстежили і виправили сегменти за замовчуванням. Не те, щоб це було легко зробити.
Девід Локк

Це не в моїй бібліотеці, воно знаходиться в прив'язках libxml python і надзвичайно сором’язливі - вистрілює лише раз на пару днів.
Слава V

5
Стандартна бібліотека Python підтримує flock (), що є правильною річчю для сучасних програм UNIX. Відкриваючи порт, використовується місце у значно більш обмеженому просторі імен, тоді як pidfiles є складнішими, оскільки вам потрібно перевірити запущені процеси, щоб безпечно їх визнати недійсними; зграя не має жодної проблеми.
Чарльз Даффі

s / UNIX / linux / туди, FTFY.
kaleissin

Відповіді:


100

Наступний код повинен зробити цю роботу, він є кросплатформенним і працює на 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: від tendo import singletonton 2: me = Singleton.SingleInstance ()
Брайан

25
Ще одна залежність від чогось такого тривіального, як це? Це не дуже привабливо.
WhyNotHugo

2
Чи одноразово обробляє процеси, які отримують ознаку (наприклад, якщо процес працює занадто довго), чи мені це доводиться обробляти?
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

Дуже схожа на пропозицію С.Лотта, але з кодом.


З цікавості: це справді кросплатформа? Це працює в Windows?
Йоахім Зауер

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, щоб запобігти його запуску двічі чи більше кроном:

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-файл) і підтримує блокування до завершення сценарію (нормальне або помилка).


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

Отже, іншими словами, ви перевіряєте, чи існує pidfile; якщо ні, напишіть свій pid у цей файл. Якщо pidfile існує, перевірте, чи є pid запущеним процесом; якщо так, то у вас запущений ще один процес роботи, тому просто вимкніть. Якщо ні, то попередній процес вийшов з ладу, тому запишіть його в журнал, а потім напишіть власний pid у файл замість старого. Потім продовжуйте.


4
Це умова перегонів. Послідовність "тест-потім-запис" може викликати виняток з двох програм, які запускаються майже одночасно, не знайдіть жодного файлу та спробуйте відкрити для запису одночасно. Він повинен створити виняток на одному, що дозволить іншому продовжуватись.
С.Лотт


5

Це може спрацювати.

  1. Спроба створити PID-файл у відомому місці. Якщо не вдалося, у когось файл заблокований, ви закінчили.

  2. Коли ви закінчите нормально, закрийте та видаліть файл PID, щоб хтось інший міг його перезаписати.

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

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


3

Використання файлу блокування є досить поширеним підходом до Unix. Якщо вона виходить з ладу, вам доведеться чистити вручну. Ви можете зберегти PID у файлі та під час запуску перевірити, чи є процес із цим PID, замінивши файл блокування, якщо ні. (Однак вам також потрібен замок навколо файла read-file-check-pid-rewrite-file). Ви знайдете те, що потрібно для отримання та перевірки pid в os -packge. Загальний спосіб перевірити, чи існує процес із заданим під, - це надіслати його не фатальним сигналом.

Інші альтернативи можуть поєднувати це із семафорами з отари та пози.

Як було запропоновано 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", або ще. Включіть цей модуль безпосередньо у свій основний __ файл сценарію __ 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)

Пояснення

Код намагається створити mutex з ім'ям, похідним від повного шляху до сценарію. Ми використовуємо косої риски, щоб уникнути потенційного плутанини з реальною файловою системою.

Переваги

  • Ніяких конфігураційних або "магічних" ідентифікаторів не потрібно, використовуйте їх у стільки різних сценаріїв, скільки потрібно.
  • Більше не залишилося застарілих файлів, файловий файл вмирає з вами.
  • Друкує корисне повідомлення під час очікування

3

Найкращим рішенням для Windows є використання файлів mutex, як це запропонував @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 tendo), який недоступний у Windows, і якщо ви спробуєте заморозити додаток python, використовуючи пакет, як-от pyinstallerстатичний імпорт, він видає помилку.

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


2

Я публікую це як відповідь, тому що я новий користувач, і переповнення стека ще не дозволяє мені проголосувати.

Рішення Соріна Сбарнеа працює для мене в ОС X, Linux та Windows, і я за це вдячний.

Однак tempfile.gettempdir () поводиться в одну сторону під ОС X та Windows, а інший під іншими деякими / багатьма / усіма (?) * Ніксами (ігноруючи той факт, що 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


Помилки в Py3. Пакет здається неправильним.
Екевоо

У Windows я отримую: ImportError: Немає модуля з іменем fcntl
Andrew W. Phillips

1

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

При запуску ваш процес надсилає 'kill -0' всім процесам певної групи. Якщо такі процеси існують, він закінчується. Потім він приєднується до групи. Ніякі інші процеси не використовують цю групу.

Однак у цьому є умова перегонів - кілька процесів можуть зробити це точно в один і той же час і всі в кінцевому підсумку приєднаються до групи та працюють одночасно. На той момент, коли ви додали якийсь мютекс, щоб зробити його водонепроникним, вам більше не потрібні групи процесів.

Це може бути прийнятним, якщо ваш процес розпочинається лише кроном, раз на хвилину чи кожну годину, але це мене трохи нервує, що піде не так саме в той день, коли ви цього не хочете.

Я думаю, це не дуже вдале рішення, якщо хтось не може вдосконалити це?


1

Я натрапив на цю точну проблему минулого тижня, і хоча я знайшов якісь хороші рішення, я вирішив зробити дуже простий і чистий пакет python і завантажив його в PyPI. Він відрізняється від tendo тим, що може блокувати будь-яке ім'я ресурсу рядка. Хоча ви, звичайно, могли заблокувати, __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

Нам потрібно визначити глобальну SOCKETзмінну, оскільки це сміття буде зібране лише тоді, коли весь процес завершиться. Якщо ми оголосимо локальну змінну у функції, вона вийде за межі після виходу функції, таким чином сокет буде видалений.

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

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


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

1
Ласкаво просимо до переповнення стека! Хоча ця відповідь може бути правильною, будь ласка, додайте пояснення. Розміщення основної логіки важливіше, ніж просто надання коду, оскільки це допомагає ОП та іншим читачам самостійно виправляти цю та подібні проблеми.
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якщо обмеження має стосуватися всіх користувачів. Відмова від відповідальності: а) передбачається, що (базове) ім'я скрипту є унікальним; б) можуть бути умови перегонів.


-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
Ласкаво просимо до переповнення стека! Хоча цей блок коду може відповісти на питання, було б краще, якщо ви могли б дати невелике пояснення, чому це робиться. Будь ласка, відредагуйте свою відповідь, щоб включити такий опис.
Artjom B.
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.