33
|
1 # -*- coding: utf-8 -*-
|
|
2
|
|
3 # pidlockfile.py
|
|
4 #
|
|
5 # Copyright © 2008–2009 Ben Finney <ben+python@benfinney.id.au>
|
|
6 #
|
|
7 # This is free software: you may copy, modify, and/or distribute this work
|
|
8 # under the terms of the Python Software Foundation License, version 2 or
|
|
9 # later as published by the Python Software Foundation.
|
|
10 # No warranty expressed or implied. See the file LICENSE.PSF-2 for details.
|
|
11
|
|
12 """ Lockfile behaviour implemented via Unix PID files.
|
|
13 """
|
|
14
|
|
15 from __future__ import absolute_import
|
|
16
|
|
17 import os
|
|
18 import sys
|
|
19 import errno
|
|
20 import time
|
|
21
|
|
22 from . import (LockBase, AlreadyLocked, LockFailed, NotLocked, NotMyLock,
|
|
23 LockTimeout)
|
|
24
|
|
25
|
|
26 class PIDLockFile(LockBase):
|
|
27 """ Lockfile implemented as a Unix PID file.
|
|
28
|
|
29 The lock file is a normal file named by the attribute `path`.
|
|
30 A lock's PID file contains a single line of text, containing
|
|
31 the process ID (PID) of the process that acquired the lock.
|
|
32
|
|
33 >>> lock = PIDLockFile('somefile')
|
|
34 >>> lock = PIDLockFile('somefile')
|
|
35 """
|
|
36
|
|
37 def __init__(self, path, threaded=False, timeout=None):
|
|
38 # pid lockfiles don't support threaded operation, so always force
|
|
39 # False as the threaded arg.
|
|
40 LockBase.__init__(self, path, False, timeout)
|
|
41 dirname = os.path.dirname(self.lock_file)
|
|
42 basename = os.path.split(self.path)[-1]
|
|
43 self.unique_name = self.path
|
|
44
|
|
45 def read_pid(self):
|
|
46 """ Get the PID from the lock file.
|
|
47 """
|
|
48 return read_pid_from_pidfile(self.path)
|
|
49
|
|
50 def is_locked(self):
|
|
51 """ Test if the lock is currently held.
|
|
52
|
|
53 The lock is held if the PID file for this lock exists.
|
|
54
|
|
55 """
|
|
56 return os.path.exists(self.path)
|
|
57
|
|
58 def i_am_locking(self):
|
|
59 """ Test if the lock is held by the current process.
|
|
60
|
|
61 Returns ``True`` if the current process ID matches the
|
|
62 number stored in the PID file.
|
|
63 """
|
|
64 return self.is_locked() and os.getpid() == self.read_pid()
|
|
65
|
|
66 def acquire(self, timeout=None):
|
|
67 """ Acquire the lock.
|
|
68
|
|
69 Creates the PID file for this lock, or raises an error if
|
|
70 the lock could not be acquired.
|
|
71 """
|
|
72
|
|
73 timeout = timeout is not None and timeout or self.timeout
|
|
74 end_time = time.time()
|
|
75 if timeout is not None and timeout > 0:
|
|
76 end_time += timeout
|
|
77
|
|
78 while True:
|
|
79 try:
|
|
80 write_pid_to_pidfile(self.path)
|
|
81 except OSError as exc:
|
|
82 if exc.errno == errno.EEXIST:
|
|
83 # The lock creation failed. Maybe sleep a bit.
|
|
84 if timeout is not None and time.time() > end_time:
|
|
85 if timeout > 0:
|
|
86 raise LockTimeout("Timeout waiting to acquire"
|
|
87 " lock for %s" %
|
|
88 self.path)
|
|
89 else:
|
|
90 raise AlreadyLocked("%s is already locked" %
|
|
91 self.path)
|
|
92 time.sleep(timeout is not None and timeout/10 or 0.1)
|
|
93 else:
|
|
94 raise LockFailed("failed to create %s" % self.path)
|
|
95 else:
|
|
96 return
|
|
97
|
|
98 def release(self):
|
|
99 """ Release the lock.
|
|
100
|
|
101 Removes the PID file to release the lock, or raises an
|
|
102 error if the current process does not hold the lock.
|
|
103
|
|
104 """
|
|
105 if not self.is_locked():
|
|
106 raise NotLocked("%s is not locked" % self.path)
|
|
107 if not self.i_am_locking():
|
|
108 raise NotMyLock("%s is locked, but not by me" % self.path)
|
|
109 remove_existing_pidfile(self.path)
|
|
110
|
|
111 def break_lock(self):
|
|
112 """ Break an existing lock.
|
|
113
|
|
114 Removes the PID file if it already exists, otherwise does
|
|
115 nothing.
|
|
116
|
|
117 """
|
|
118 remove_existing_pidfile(self.path)
|
|
119
|
|
120 def read_pid_from_pidfile(pidfile_path):
|
|
121 """ Read the PID recorded in the named PID file.
|
|
122
|
|
123 Read and return the numeric PID recorded as text in the named
|
|
124 PID file. If the PID file cannot be read, or if the content is
|
|
125 not a valid PID, return ``None``.
|
|
126
|
|
127 """
|
|
128 pid = None
|
|
129 try:
|
|
130 pidfile = open(pidfile_path, 'r')
|
|
131 except IOError:
|
|
132 pass
|
|
133 else:
|
|
134 # According to the FHS 2.3 section on PID files in /var/run:
|
|
135 #
|
|
136 # The file must consist of the process identifier in
|
|
137 # ASCII-encoded decimal, followed by a newline character.
|
|
138 #
|
|
139 # Programs that read PID files should be somewhat flexible
|
|
140 # in what they accept; i.e., they should ignore extra
|
|
141 # whitespace, leading zeroes, absence of the trailing
|
|
142 # newline, or additional lines in the PID file.
|
|
143
|
|
144 line = pidfile.readline().strip()
|
|
145 try:
|
|
146 pid = int(line)
|
|
147 except ValueError:
|
|
148 pass
|
|
149 pidfile.close()
|
|
150
|
|
151 return pid
|
|
152
|
|
153
|
|
154 def write_pid_to_pidfile(pidfile_path):
|
|
155 """ Write the PID in the named PID file.
|
|
156
|
|
157 Get the numeric process ID (“PID”) of the current process
|
|
158 and write it to the named file as a line of text.
|
|
159
|
|
160 """
|
|
161 open_flags = (os.O_CREAT | os.O_EXCL | os.O_WRONLY)
|
|
162 open_mode = 0o644
|
|
163 pidfile_fd = os.open(pidfile_path, open_flags, open_mode)
|
|
164 pidfile = os.fdopen(pidfile_fd, 'w')
|
|
165
|
|
166 # According to the FHS 2.3 section on PID files in /var/run:
|
|
167 #
|
|
168 # The file must consist of the process identifier in
|
|
169 # ASCII-encoded decimal, followed by a newline character. For
|
|
170 # example, if crond was process number 25, /var/run/crond.pid
|
|
171 # would contain three characters: two, five, and newline.
|
|
172
|
|
173 pid = os.getpid()
|
|
174 line = "%(pid)d\n" % vars()
|
|
175 pidfile.write(line)
|
|
176 pidfile.close()
|
|
177
|
|
178
|
|
179 def remove_existing_pidfile(pidfile_path):
|
|
180 """ Remove the named PID file if it exists.
|
|
181
|
|
182 Removing a PID file that doesn't already exist puts us in the
|
|
183 desired state, so we ignore the condition if the file does not
|
|
184 exist.
|
|
185
|
|
186 """
|
|
187 try:
|
|
188 os.remove(pidfile_path)
|
|
189 except OSError as exc:
|
|
190 if exc.errno == errno.ENOENT:
|
|
191 pass
|
|
192 else:
|
|
193 raise
|