pidlock.py 11.2 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#!/usr/bin/env python
## @file pidlock.py
# @brief Module which implements a pid handler which makes use of a lockfile.
# 
# -----------------------------------------------------------------------------
# 
# @author Daniel Armbruster
# \date 04/01/2012
# 
# Purpose: Module which implements a pid handler which makes use of a lockfile.
#
# ----
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# 
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
# ----
# 
# Copyright (c) 2012 by Daniel Armbruster
# 
# REVISIONS and CHANGES 
# 04/01/2012  V1.0  Daniel Armbruster
32
33
34
# 23/02/2013  V1.1  Implement wait()-mechanism and complete rework of code.
#                   Simplification of Lock class interface and implementation of
#                   test cases.
35
36
# 
# =============================================================================
Daniel Armbruster's avatar
Daniel Armbruster committed
37
38
39
"""
Module to handle process IDs (pid) by using a lockfile mechanism. The module
comes along with a PidHandler class. Additionally there is a Exception class
40
LockError which might be caught while announcing a pid.
Daniel Armbruster's avatar
Daniel Armbruster committed
41

42
The name of the lockfile could be configured using the global variable
43
LOCKFILENAME.
Daniel Armbruster's avatar
Daniel Armbruster committed
44
"""
45
46
47
48

import os
import sys
import subprocess
49
50
import datetime
import csbackErrorCodes as eCodes
51

52
53
__version__ = "V1.1"
__license__ = "GPLv2+"
54
55
56
57
58
__author__ = "Daniel Armbruster"
__copyright__ = "Copyright (c) 2012 by Daniel Armbruster"

# -----------------------------------------------------------------------------
# variables
59
60
LOCKFILENAME = ".lock"
DEBUG = False
61
62

# -----------------------------------------------------------------------------
63
class LockError(Exception):
64
65
66
  """
  Exception class of pidlock module.
  """
67
  def __init__(self, msg, errorCode=1):
68
    self.msg = msg
69
70
71
72
    self.errorCode = errorCode

  def __str__(self):
    return "Lock (ERROR): {0} (CODE: {1}).\n".format(self.msg, self.errorCode)
73
74

# -----------------------------------------------------------------------------
75
class Lock:
76
  """
77
78
79
  Implements a handler for the lock mechanism.
  Internally, this handler uses a pidlockfile mechanism to register the
  announced pids.
80
81
82
  Note that this handler does its work only on *nix platforms properly. Use
  inheritance to overload the corresponding functions.
  """
83
84
  # register active process ids announced
  activeLockPids = []
85

86
  def __init__(self, dir, timeout=600):
87
    """
88
89
90
91
92
93
    Constructor for a directory lock.
    
    Keyword arguments:
    dir -- Directory to announce the lock.
    timeout -- Timeout in seconds until announcement is expired (default 600).

94
    """
95
96
97
98
99
100
101
102
103
104
    self.path = os.path.join(dir, LOCKFILENAME) 
    self.dir = dir
    self.time = datetime.datetime.now()
    self.timeout = datetime.timedelta(seconds=timeout)
    self.expire = self.time+self.timeout
    self.pid = os.getpid() 
    # check if lock with same PID is active
    if self.pid in Lock.activeLockPids:
      raise LockError("Lock with same PID already announced", \
          eCodes.LOCK_SamePidActive)
105
    else:
106
107
108
109
110
111
      Lock.activeLockPids.append(self.pid)
    # remove directory lock if pid not valid anymore
    if os.access(self.path, os.F_OK) and not self.__lockValid():
      if DEBUG:
        sys.stdout.write( \
            "DEBUG: Lock with invalid PID found. Removing lock ...\n")
Daniel Armbruster's avatar
Daniel Armbruster committed
112
      try:
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
        os.remove(self.path)
      except:
        raise LockError("While removing zombie lock", \
            eCodes.LOCK_UnableToRemove)
    # wait until active process has terminated
    while(self.__lockValid()):
      if datetime.datetime.now() > self.expire:
        raise LockError( \
            "Timeout expired while waiting for active process",
            eCodes.LOCK_TimeOutExpired)
      if DEBUG:
        sys.stdout.write( \
            "DEBUG: Waiting until valid lock has been released.\n")
    # create new lock
    try:
      if DEBUG:
        sys.stdout.write( \
            "DEBUG: Creating lock of directory '{0}' ...\n".format(self.dir))
131
132
      with open(self.path, 'w') as pidfile:
        pidfile.write("{0}".format(self.pid))
133
134
135
    except IOError as err:
      raise LockError("While creating lockfile. "+err.strerror+" [Errno "+ \
          str(err.errno)+"]: "+err.filename, eCodes.LOCK_UnableToCreate)
136
137


138
  def release(self, pid):
139
    """
140
141
142
143
144
145
    Release a lock. The lock only can be released if the PID does match with the
    PID in the lockfile of the lock.

    Keyword arguments:
      pid -- process ID

146
    """
147
148
149
150
151
    if DEBUG:
      sys.stdout.write( \
          "DEBUG: Releasing lock of directory '{0}'.\n".format(self.dir))

    try:
152
153
154
155
156
157
158
159
160
161
162
163
164
      with open(self.path, 'r') as pidfile:
        pidfile.seek(0)
        if pidfile.readline() == str(pid):
          try:
            pidfile.close()
            os.remove(self.path)
            # remove PID from registration list
            if pid in Lock.activeLockPids:
              Lock.activeLockPids.remove(pid) 
          except:
            raise LockError("Unable to remove lock", eCodes.LOCK_UnableToRemove)
        else:
          raise LockError("Invalid PID passed", eCodes.LOCK_InvalidPID)
165
166
167
    except:
      raise LockError("Missing lock file in directory '{0}'".format( \
          self.dir), eCodes.LOCK_LockFileMissing)
168

169
170

  def __lockValid(self):
171
    """
172
    Check if the process with the pid in the pid lockfile is still active.
173
    """
174
    try:
175
176
177
      with open(self.path, 'r') as pidfile:
        pidfile.seek(0)
        pid = pidfile.readline().strip()
178
179
180
181
182
183
184
185
186
187
188
189
190
      if 0 == len(pid):
        return False
      if DEBUG:
        sys.stdout.write( \
            "DEBUG: Directory '{0}' locked by process with PID: '{1}'\n". \
            format(self.dir, pid))
      return self.__pidActive(pid)
    except:
      if DEBUG:
        sys.stdout.write("DEBUG: Directory '{0}' is not locked.\n".format( \
            self.dir))
      return False

191
192
193

  def __pidActive(self, pid):
    """
194
195
196
197
198
    Check if process with PID is still active and return True or False.

    Keyword arguments:
      pid -- process ID

199
200
201
    Note that this function only works on *nix platforms. Overload it for other
    platforms.
    """
202
203
204
205
206
207
208
209
210
    if DEBUG:
      sys.stdout.write("DEBUG: Checking if process is still active ...\n")
      if os.path.exists("/proc/{0}".format(pid)):
        sys.stdout.write( \
            "DEBUG: Process with PID '{0}' is still active.\n".format(pid))
      else: 
        sys.stdout.write( \
            "DEBUG: Process with PID '{0}' is is not active.\n".format(pid))

211
    return os.path.exists("/proc/{0}".format(pid))
212

213
214
215
216
 
# -----------------------------------------------------------------------------
# Tests
if __name__ == '__main__':
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
  """
  Run different test cases. Depending on the commandline argument different
  tests will be run. Tests will only work properly on *nix platforms.

  Usage:
  ./pidlock TESTCASE DIR

  Keywords description:

    Valid TESTCASE arguments:
      0: Announce a lock in DIR - and release the lock afterwards.
      1: Trying to lock a directory which already contains a valid lock.
      2: Remove lock while process is still active.
      3: Same process trys to lock the same directory two times.

    DIR: Directory within tests will be performed.

  """
  DEBUG = True

  # fetch commandline arguments
  testCase=int(sys.argv[1])
  testDir=sys.argv[2]

  # commandline argument checks
  if not os.access(testDir, os.F_OK):
    sys.stderr.write("ERROR: Unable to access directory '{0}'.\n".format( \
        testDir))
    sys.exit(1);

  lockFilePath=os.path.join(testDir, LOCKFILENAME)  
  if os.access(lockFilePath, os.F_OK):
    sys.stderr.write( \
        "ERROR: Directory '{0}' already contains a file '{1}'.\n". \
        format(testDir, LOCKFILENAME))
    sys.exit(1)
  
  sys.stdout.write("Tests: PID of test process: {0}.\n".format(os.getpid()))

256
  try:
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
    # Test case 0:
    if 0 == testCase: 
      testCaseDescription="""
      Test case 0:
      ------------
      Lock a given directory and release the lock afterwards. The directory had
      not been locked before.
      """
      sys.stdout.write(testCaseDescription+"\n")

      lock = Lock(testDir, timeout=10)
      # read lock file
      sys.stdout.write("Tests ["+str(testCase)+ \
          "]: Reading content of lock file ...\n")
      pidfile = open(lockFilePath, 'r')
      pidfile.seek(0)
      pid = pidfile.readline().strip()
      sys.stdout.write("Tests ["+str(testCase)+ \
          "]: Content of lock file: '{0}'\n".format(pid))

      lock.release(os.getpid())

    # Test case 1:
    elif 1 == testCase:
      testCaseDescription="""
      Test case 1:
      ------------
      Trying to lock a directory which already contains a valid lock.
      """
      sys.stdout.write(testCaseDescription+"\n")

      # create pseudo lock (with PID of init process)
      sys.stdout.write("Tests ["+str(testCase)+ \
          "]: Creating pseudo lock ...\n")
      try:
        pseudoLock = open(lockFilePath, 'w')
        pseudoLock.write("1")
        pseudoLock.close()
      except:
        sys.stderr.write("ERROR: While creating pseudo lock.\n")
        sys.exit(1)
      
      try:
        # create lock
        timeout=3
        sys.stdout.write("Tests ["+str(testCase)+ \
            "]: Trying to create lock (timeout: {0}s) ...\n")
        lock = Lock(testDir, timeout=timeout)
        lock.release(os.getpid())
      except LockError as err:
        sys.stderr.write(str(err))
        if err.errorCode == eCodes.LOCK_TimeOutExpired:
          sys.stdout.write( \
              "Tests: Clean directory of generated test lock files.\n")
          try:
            os.remove(lockFilePath)
          except:
            sys.stderr.write("ERROR: While cleaning directory ...\n") 
            sys.exit(1)

    # Test case 2:
    elif 2 == testCase:
      testCaseDescription="""
      Test case 2:
      ------------
      Remove lock while process is still active.
      """
      sys.stdout.write(testCaseDescription+"\n")

      # create lock
      lock = Lock(testDir)

      # remove lock before releasing lock
      sys.stdout.write("Tests ["+str(testCase)+"]: Removing lock ...\n")
      try:
        os.remove(lockFilePath)
      except:
        sys.stderr.write("ERROR: Unable to remove lock file.\n")
        sys.exit(1)

      # trying to release lock
      lock.release(os.getpid())

    # Test case 3:
    elif 3 == testCase:
      testCaseDescription="""
      Test case 3:
      ------------
      Same process trys to lock the same directory two times.
      """
      sys.stdout.write(testCaseDescription+"\n")

      try:
        # create first lock
        lock1 = Lock(testDir)
        # create second lock
        lock2 = Lock(testDir, 0.5)
        # release lock
      except LockError as err:
        sys.stderr.write(str(err))
        lock1.release(os.getpid())
    else:
      sys.stderr.write("ERROR: Invalid test case.\n")
      sys.exit(1)
  except LockError as err:
    sys.stderr.write(str(err))
  else:
    sys.exit(0)

366
367

# ----- END OF pidlock.py -----