123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190 |
- # -*- coding: utf-8 -*-
- # pidlockfile.py
- #
- # Copyright © 2008–2009 Ben Finney <ben+python@benfinney.id.au>
- #
- # This is free software: you may copy, modify, and/or distribute this work
- # under the terms of the Python Software Foundation License, version 2 or
- # later as published by the Python Software Foundation.
- # No warranty expressed or implied. See the file LICENSE.PSF-2 for details.
- """ Lockfile behaviour implemented via Unix PID files.
- """
- from __future__ import absolute_import
- import errno
- import os
- import time
- from . import (LockBase, AlreadyLocked, LockFailed, NotLocked, NotMyLock,
- LockTimeout)
- class PIDLockFile(LockBase):
- """ Lockfile implemented as a Unix PID file.
- The lock file is a normal file named by the attribute `path`.
- A lock's PID file contains a single line of text, containing
- the process ID (PID) of the process that acquired the lock.
- >>> lock = PIDLockFile('somefile')
- >>> lock = PIDLockFile('somefile')
- """
- def __init__(self, path, threaded=False, timeout=None):
- # pid lockfiles don't support threaded operation, so always force
- # False as the threaded arg.
- LockBase.__init__(self, path, False, timeout)
- self.unique_name = self.path
- def read_pid(self):
- """ Get the PID from the lock file.
- """
- return read_pid_from_pidfile(self.path)
- def is_locked(self):
- """ Test if the lock is currently held.
- The lock is held if the PID file for this lock exists.
- """
- return os.path.exists(self.path)
- def i_am_locking(self):
- """ Test if the lock is held by the current process.
- Returns ``True`` if the current process ID matches the
- number stored in the PID file.
- """
- return self.is_locked() and os.getpid() == self.read_pid()
- def acquire(self, timeout=None):
- """ Acquire the lock.
- Creates the PID file for this lock, or raises an error if
- the lock could not be acquired.
- """
- timeout = timeout if timeout is not None else self.timeout
- end_time = time.time()
- if timeout is not None and timeout > 0:
- end_time += timeout
- while True:
- try:
- write_pid_to_pidfile(self.path)
- except OSError as exc:
- if exc.errno == errno.EEXIST:
- # The lock creation failed. Maybe sleep a bit.
- if time.time() > end_time:
- if timeout is not None and timeout > 0:
- raise LockTimeout("Timeout waiting to acquire"
- " lock for %s" %
- self.path)
- else:
- raise AlreadyLocked("%s is already locked" %
- self.path)
- time.sleep(timeout is not None and timeout / 10 or 0.1)
- else:
- raise LockFailed("failed to create %s" % self.path)
- else:
- return
- def release(self):
- """ Release the lock.
- Removes the PID file to release the lock, or raises an
- error if the current process does not hold the lock.
- """
- if not self.is_locked():
- raise NotLocked("%s is not locked" % self.path)
- if not self.i_am_locking():
- raise NotMyLock("%s is locked, but not by me" % self.path)
- remove_existing_pidfile(self.path)
- def break_lock(self):
- """ Break an existing lock.
- Removes the PID file if it already exists, otherwise does
- nothing.
- """
- remove_existing_pidfile(self.path)
- def read_pid_from_pidfile(pidfile_path):
- """ Read the PID recorded in the named PID file.
- Read and return the numeric PID recorded as text in the named
- PID file. If the PID file cannot be read, or if the content is
- not a valid PID, return ``None``.
- """
- pid = None
- try:
- pidfile = open(pidfile_path, 'r')
- except IOError:
- pass
- else:
- # According to the FHS 2.3 section on PID files in /var/run:
- #
- # The file must consist of the process identifier in
- # ASCII-encoded decimal, followed by a newline character.
- #
- # Programs that read PID files should be somewhat flexible
- # in what they accept; i.e., they should ignore extra
- # whitespace, leading zeroes, absence of the trailing
- # newline, or additional lines in the PID file.
- line = pidfile.readline().strip()
- try:
- pid = int(line)
- except ValueError:
- pass
- pidfile.close()
- return pid
- def write_pid_to_pidfile(pidfile_path):
- """ Write the PID in the named PID file.
- Get the numeric process ID (“PID”) of the current process
- and write it to the named file as a line of text.
- """
- open_flags = (os.O_CREAT | os.O_EXCL | os.O_WRONLY)
- open_mode = 0o644
- pidfile_fd = os.open(pidfile_path, open_flags, open_mode)
- pidfile = os.fdopen(pidfile_fd, 'w')
- # According to the FHS 2.3 section on PID files in /var/run:
- #
- # The file must consist of the process identifier in
- # ASCII-encoded decimal, followed by a newline character. For
- # example, if crond was process number 25, /var/run/crond.pid
- # would contain three characters: two, five, and newline.
- pid = os.getpid()
- pidfile.write("%s\n" % pid)
- pidfile.close()
- def remove_existing_pidfile(pidfile_path):
- """ Remove the named PID file if it exists.
- Removing a PID file that doesn't already exist puts us in the
- desired state, so we ignore the condition if the file does not
- exist.
- """
- try:
- os.remove(pidfile_path)
- except OSError as exc:
- if exc.errno == errno.ENOENT:
- pass
- else:
- raise
|