xref: /aosp_15_r20/external/autotest/autotest_lib/server/cros/lockfile.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
1*9c5db199SXin Li# Lint as: python2, python3
2*9c5db199SXin Li
3*9c5db199SXin Li"""
4*9c5db199SXin Lilockfile.py - Platform-independent advisory file locks.
5*9c5db199SXin Li
6*9c5db199SXin LiForked from python2.7/dist-packages/lockfile version 0.8.
7*9c5db199SXin Li
8*9c5db199SXin LiUsage:
9*9c5db199SXin Li
10*9c5db199SXin Li>>> lock = FileLock('somefile')
11*9c5db199SXin Li>>> try:
12*9c5db199SXin Li...     lock.acquire()
13*9c5db199SXin Li... except AlreadyLocked:
14*9c5db199SXin Li...     print 'somefile', 'is locked already.'
15*9c5db199SXin Li... except LockFailed:
16*9c5db199SXin Li...     print 'somefile', 'can\\'t be locked.'
17*9c5db199SXin Li... else:
18*9c5db199SXin Li...     print 'got lock'
19*9c5db199SXin Ligot lock
20*9c5db199SXin Li>>> print lock.is_locked()
21*9c5db199SXin LiTrue
22*9c5db199SXin Li>>> lock.release()
23*9c5db199SXin Li
24*9c5db199SXin Li>>> lock = FileLock('somefile')
25*9c5db199SXin Li>>> print lock.is_locked()
26*9c5db199SXin LiFalse
27*9c5db199SXin Li>>> with lock:
28*9c5db199SXin Li...    print lock.is_locked()
29*9c5db199SXin LiTrue
30*9c5db199SXin Li>>> print lock.is_locked()
31*9c5db199SXin LiFalse
32*9c5db199SXin Li>>> # It is okay to lock twice from the same thread...
33*9c5db199SXin Li>>> with lock:
34*9c5db199SXin Li...     lock.acquire()
35*9c5db199SXin Li...
36*9c5db199SXin Li>>> # Though no counter is kept, so you can't unlock multiple times...
37*9c5db199SXin Li>>> print lock.is_locked()
38*9c5db199SXin LiFalse
39*9c5db199SXin Li
40*9c5db199SXin LiExceptions:
41*9c5db199SXin Li
42*9c5db199SXin Li    Error - base class for other exceptions
43*9c5db199SXin Li        LockError - base class for all locking exceptions
44*9c5db199SXin Li            AlreadyLocked - Another thread or process already holds the lock
45*9c5db199SXin Li            LockFailed - Lock failed for some other reason
46*9c5db199SXin Li        UnlockError - base class for all unlocking exceptions
47*9c5db199SXin Li            AlreadyUnlocked - File was not locked.
48*9c5db199SXin Li            NotMyLock - File was locked but not by the current thread/process
49*9c5db199SXin Li"""
50*9c5db199SXin Li
51*9c5db199SXin Lifrom __future__ import absolute_import
52*9c5db199SXin Lifrom __future__ import division
53*9c5db199SXin Lifrom __future__ import print_function
54*9c5db199SXin Li
55*9c5db199SXin Liimport logging
56*9c5db199SXin Liimport socket
57*9c5db199SXin Liimport os
58*9c5db199SXin Liimport threading
59*9c5db199SXin Liimport time
60*9c5db199SXin Liimport six
61*9c5db199SXin Lifrom six.moves import urllib
62*9c5db199SXin Li
63*9c5db199SXin Li# Work with PEP8 and non-PEP8 versions of threading module.
64*9c5db199SXin Liif not hasattr(threading, "current_thread"):
65*9c5db199SXin Li    threading.current_thread = threading.currentThread
66*9c5db199SXin Liif not hasattr(threading.Thread, "get_name"):
67*9c5db199SXin Li    threading.Thread.get_name = threading.Thread.getName
68*9c5db199SXin Li
69*9c5db199SXin Li__all__ = ['Error', 'LockError', 'LockTimeout', 'AlreadyLocked',
70*9c5db199SXin Li           'LockFailed', 'UnlockError', 'LinkFileLock']
71*9c5db199SXin Li
72*9c5db199SXin Liclass Error(Exception):
73*9c5db199SXin Li    """
74*9c5db199SXin Li    Base class for other exceptions.
75*9c5db199SXin Li
76*9c5db199SXin Li    >>> try:
77*9c5db199SXin Li    ...   raise Error
78*9c5db199SXin Li    ... except Exception:
79*9c5db199SXin Li    ...   pass
80*9c5db199SXin Li    """
81*9c5db199SXin Li    pass
82*9c5db199SXin Li
83*9c5db199SXin Liclass LockError(Error):
84*9c5db199SXin Li    """
85*9c5db199SXin Li    Base class for error arising from attempts to acquire the lock.
86*9c5db199SXin Li
87*9c5db199SXin Li    >>> try:
88*9c5db199SXin Li    ...   raise LockError
89*9c5db199SXin Li    ... except Error:
90*9c5db199SXin Li    ...   pass
91*9c5db199SXin Li    """
92*9c5db199SXin Li    pass
93*9c5db199SXin Li
94*9c5db199SXin Liclass LockTimeout(LockError):
95*9c5db199SXin Li    """Raised when lock creation fails within a user-defined period of time.
96*9c5db199SXin Li
97*9c5db199SXin Li    >>> try:
98*9c5db199SXin Li    ...   raise LockTimeout
99*9c5db199SXin Li    ... except LockError:
100*9c5db199SXin Li    ...   pass
101*9c5db199SXin Li    """
102*9c5db199SXin Li    pass
103*9c5db199SXin Li
104*9c5db199SXin Liclass AlreadyLocked(LockError):
105*9c5db199SXin Li    """Some other thread/process is locking the file.
106*9c5db199SXin Li
107*9c5db199SXin Li    >>> try:
108*9c5db199SXin Li    ...   raise AlreadyLocked
109*9c5db199SXin Li    ... except LockError:
110*9c5db199SXin Li    ...   pass
111*9c5db199SXin Li    """
112*9c5db199SXin Li    pass
113*9c5db199SXin Li
114*9c5db199SXin Liclass LockFailed(LockError):
115*9c5db199SXin Li    """Lock file creation failed for some other reason.
116*9c5db199SXin Li
117*9c5db199SXin Li    >>> try:
118*9c5db199SXin Li    ...   raise LockFailed
119*9c5db199SXin Li    ... except LockError:
120*9c5db199SXin Li    ...   pass
121*9c5db199SXin Li    """
122*9c5db199SXin Li    pass
123*9c5db199SXin Li
124*9c5db199SXin Liclass UnlockError(Error):
125*9c5db199SXin Li    """
126*9c5db199SXin Li    Base class for errors arising from attempts to release the lock.
127*9c5db199SXin Li
128*9c5db199SXin Li    >>> try:
129*9c5db199SXin Li    ...   raise UnlockError
130*9c5db199SXin Li    ... except Error:
131*9c5db199SXin Li    ...   pass
132*9c5db199SXin Li    """
133*9c5db199SXin Li    pass
134*9c5db199SXin Li
135*9c5db199SXin Liclass LockBase(object):
136*9c5db199SXin Li    """Base class for platform-specific lock classes."""
137*9c5db199SXin Li    def __init__(self, path):
138*9c5db199SXin Li        """
139*9c5db199SXin Li        Unlike the original implementation we always assume the threaded case.
140*9c5db199SXin Li        """
141*9c5db199SXin Li        self.path = path
142*9c5db199SXin Li        self.lock_file = os.path.abspath(path) + ".lock"
143*9c5db199SXin Li        self.hostname = socket.gethostname()
144*9c5db199SXin Li        self.pid = os.getpid()
145*9c5db199SXin Li        name = threading.current_thread().get_name()
146*9c5db199SXin Li        tname = "%s-" % urllib.parse.quote(name, safe="")
147*9c5db199SXin Li        dirname = os.path.dirname(self.lock_file)
148*9c5db199SXin Li        self.unique_name = os.path.join(dirname, "%s.%s%s" % (self.hostname,
149*9c5db199SXin Li                                                              tname, self.pid))
150*9c5db199SXin Li
151*9c5db199SXin Li    def __del__(self):
152*9c5db199SXin Li        """Paranoia: We are trying hard to not leave any file behind. This
153*9c5db199SXin Li        might possibly happen in very unusual acquire exception cases."""
154*9c5db199SXin Li        if os.path.exists(self.unique_name):
155*9c5db199SXin Li            logging.warning("Removing unexpected file %s", self.unique_name)
156*9c5db199SXin Li            os.unlink(self.unique_name)
157*9c5db199SXin Li
158*9c5db199SXin Li    def acquire(self, timeout=None):
159*9c5db199SXin Li        """
160*9c5db199SXin Li        Acquire the lock.
161*9c5db199SXin Li
162*9c5db199SXin Li        * If timeout is omitted (or None), wait forever trying to lock the
163*9c5db199SXin Li          file.
164*9c5db199SXin Li
165*9c5db199SXin Li        * If timeout > 0, try to acquire the lock for that many seconds.  If
166*9c5db199SXin Li          the lock period expires and the file is still locked, raise
167*9c5db199SXin Li          LockTimeout.
168*9c5db199SXin Li
169*9c5db199SXin Li        * If timeout <= 0, raise AlreadyLocked immediately if the file is
170*9c5db199SXin Li          already locked.
171*9c5db199SXin Li        """
172*9c5db199SXin Li        raise NotImplementedError("implement in subclass")
173*9c5db199SXin Li
174*9c5db199SXin Li    def release(self):
175*9c5db199SXin Li        """
176*9c5db199SXin Li        Release the lock.
177*9c5db199SXin Li
178*9c5db199SXin Li        If the file is not locked, raise NotLocked.
179*9c5db199SXin Li        """
180*9c5db199SXin Li        raise NotImplementedError("implement in subclass")
181*9c5db199SXin Li
182*9c5db199SXin Li    def is_locked(self):
183*9c5db199SXin Li        """
184*9c5db199SXin Li        Tell whether or not the file is locked.
185*9c5db199SXin Li        """
186*9c5db199SXin Li        raise NotImplementedError("implement in subclass")
187*9c5db199SXin Li
188*9c5db199SXin Li    def i_am_locking(self):
189*9c5db199SXin Li        """
190*9c5db199SXin Li        Return True if this object is locking the file.
191*9c5db199SXin Li        """
192*9c5db199SXin Li        raise NotImplementedError("implement in subclass")
193*9c5db199SXin Li
194*9c5db199SXin Li    def break_lock(self):
195*9c5db199SXin Li        """
196*9c5db199SXin Li        Remove a lock.  Useful if a locking thread failed to unlock.
197*9c5db199SXin Li        """
198*9c5db199SXin Li        raise NotImplementedError("implement in subclass")
199*9c5db199SXin Li
200*9c5db199SXin Li    def age_of_lock(self):
201*9c5db199SXin Li        """
202*9c5db199SXin Li        Return the time since creation of lock in seconds.
203*9c5db199SXin Li        """
204*9c5db199SXin Li        raise NotImplementedError("implement in subclass")
205*9c5db199SXin Li
206*9c5db199SXin Li    def __enter__(self):
207*9c5db199SXin Li        """
208*9c5db199SXin Li        Context manager support.
209*9c5db199SXin Li        """
210*9c5db199SXin Li        self.acquire()
211*9c5db199SXin Li        return self
212*9c5db199SXin Li
213*9c5db199SXin Li    def __exit__(self, *_exc):
214*9c5db199SXin Li        """
215*9c5db199SXin Li        Context manager support.
216*9c5db199SXin Li        """
217*9c5db199SXin Li        self.release()
218*9c5db199SXin Li
219*9c5db199SXin Li
220*9c5db199SXin Liclass LinkFileLock(LockBase):
221*9c5db199SXin Li    """Lock access to a file using atomic property of link(2)."""
222*9c5db199SXin Li
223*9c5db199SXin Li    def acquire(self, timeout=None):
224*9c5db199SXin Li        try:
225*9c5db199SXin Li            open(self.unique_name, "wb").close()
226*9c5db199SXin Li        except IOError:
227*9c5db199SXin Li            raise LockFailed("failed to create %s" % self.unique_name)
228*9c5db199SXin Li
229*9c5db199SXin Li        end_time = time.time()
230*9c5db199SXin Li        if timeout is not None and timeout > 0:
231*9c5db199SXin Li            end_time += timeout
232*9c5db199SXin Li
233*9c5db199SXin Li        while True:
234*9c5db199SXin Li            # Try and create a hard link to it.
235*9c5db199SXin Li            try:
236*9c5db199SXin Li                os.link(self.unique_name, self.lock_file)
237*9c5db199SXin Li            except OSError:
238*9c5db199SXin Li                # Link creation failed.  Maybe we've double-locked?
239*9c5db199SXin Li                nlinks = os.stat(self.unique_name).st_nlink
240*9c5db199SXin Li                if nlinks == 2:
241*9c5db199SXin Li                    # The original link plus the one I created == 2.  We're
242*9c5db199SXin Li                    # good to go.
243*9c5db199SXin Li                    return
244*9c5db199SXin Li                else:
245*9c5db199SXin Li                    # Otherwise the lock creation failed.
246*9c5db199SXin Li                    if timeout is not None and time.time() > end_time:
247*9c5db199SXin Li                        os.unlink(self.unique_name)
248*9c5db199SXin Li                        if timeout > 0:
249*9c5db199SXin Li                            raise LockTimeout
250*9c5db199SXin Li                        else:
251*9c5db199SXin Li                            raise AlreadyLocked
252*9c5db199SXin Li                    # IHF: The original code used integer division/10.
253*9c5db199SXin Li                    time.sleep(timeout is not None and timeout / 10.0 or 0.1)
254*9c5db199SXin Li            else:
255*9c5db199SXin Li                # Link creation succeeded.  We're good to go.
256*9c5db199SXin Li                return
257*9c5db199SXin Li
258*9c5db199SXin Li    def release(self):
259*9c5db199SXin Li        # IHF: I think original cleanup was not correct when somebody else broke
260*9c5db199SXin Li        # our lock and took it. Then we released the new process' lock causing
261*9c5db199SXin Li        # a cascade of wrong lock releases. Notice the SQLiteFileLock::release()
262*9c5db199SXin Li        # doesn't seem to run into this problem as it uses i_am_locking().
263*9c5db199SXin Li        if self.i_am_locking():
264*9c5db199SXin Li            # We own the lock and clean up both files.
265*9c5db199SXin Li            os.unlink(self.unique_name)
266*9c5db199SXin Li            os.unlink(self.lock_file)
267*9c5db199SXin Li            return
268*9c5db199SXin Li        if os.path.exists(self.unique_name):
269*9c5db199SXin Li            # We don't own lock_file but clean up after ourselves.
270*9c5db199SXin Li            os.unlink(self.unique_name)
271*9c5db199SXin Li        raise UnlockError
272*9c5db199SXin Li
273*9c5db199SXin Li    def is_locked(self):
274*9c5db199SXin Li        """Check if anybody is holding the lock."""
275*9c5db199SXin Li        return os.path.exists(self.lock_file)
276*9c5db199SXin Li
277*9c5db199SXin Li    def i_am_locking(self):
278*9c5db199SXin Li        """Check if we are holding the lock."""
279*9c5db199SXin Li        return (self.is_locked() and
280*9c5db199SXin Li                os.path.exists(self.unique_name) and
281*9c5db199SXin Li                os.stat(self.unique_name).st_nlink == 2)
282*9c5db199SXin Li
283*9c5db199SXin Li    def break_lock(self):
284*9c5db199SXin Li        """Break (another processes) lock."""
285*9c5db199SXin Li        if os.path.exists(self.lock_file):
286*9c5db199SXin Li            os.unlink(self.lock_file)
287*9c5db199SXin Li
288*9c5db199SXin Li    def age_of_lock(self):
289*9c5db199SXin Li        """Returns the time since creation of lock in seconds."""
290*9c5db199SXin Li        try:
291*9c5db199SXin Li            # Creating the hard link for the lock updates the change time.
292*9c5db199SXin Li            age = time.time() - os.stat(self.lock_file).st_ctime
293*9c5db199SXin Li        except OSError:
294*9c5db199SXin Li            age = -1.0
295*9c5db199SXin Li        return age
296*9c5db199SXin Li
297*9c5db199SXin Li
298*9c5db199SXin LiFileLock = LinkFileLock
299