190 lines
5.9 KiB
Python
190 lines
5.9 KiB
Python
# -*- 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
|