• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python
2#
3# Copyright 2020 - The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9#     http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16"""LocalInstanceLock class."""
17
18import errno
19import fcntl
20import logging
21import os
22
23from acloud import errors
24from acloud.internal.lib import utils
25
26
27logger = logging.getLogger(__name__)
28
29_LOCK_FILE_SIZE = 1
30# An empty file is equivalent to NOT_IN_USE.
31_IN_USE_STATE = b"I"
32_NOT_IN_USE_STATE = b"N"
33
34_DEFAULT_TIMEOUT_SECS = 5
35
36
37class LocalInstanceLock:
38    """The class that controls a lock file for a local instance.
39
40    Acloud acquires the lock file of a local instance before it creates,
41    deletes, or queries it. The lock prevents multiple acloud processes from
42    accessing an instance simultaneously.
43
44    The lock file records whether the instance is in use. Acloud checks the
45    state when it needs an unused id to create a new instance.
46
47    Attributes:
48        _file_path: The path to the lock file.
49        _file_desc: The file descriptor of the file. It is set to None when
50                    this object does not hold the lock.
51    """
52
53    def __init__(self, file_path):
54        self._file_path = file_path
55        self._file_desc = None
56
57    def _Flock(self, timeout_secs):
58        """Call fcntl.flock with timeout.
59
60        Args:
61            timeout_secs: An integer or a float, the timeout for acquiring the
62                          lock file. 0 indicates non-block.
63
64        Returns:
65            True if the file is locked successfully. False if timeout.
66
67        Raises:
68            OSError: if any file operation fails.
69        """
70        try:
71            if timeout_secs > 0:
72                wrapper = utils.TimeoutException(timeout_secs)
73                wrapper(fcntl.flock)(self._file_desc, fcntl.LOCK_EX)
74            else:
75                fcntl.flock(self._file_desc, fcntl.LOCK_EX | fcntl.LOCK_NB)
76        except errors.FunctionTimeoutError as e:
77            logger.debug("Cannot lock %s within %s seconds",
78                         self._file_path, timeout_secs)
79            return False
80        except (OSError, IOError) as e:
81            # flock raises IOError in python2; OSError in python3.
82            if e.errno in (errno.EACCES, errno.EAGAIN):
83                logger.debug("Cannot lock %s", self._file_path)
84                return False
85            raise
86        return True
87
88    def Lock(self, timeout_secs=_DEFAULT_TIMEOUT_SECS):
89        """Acquire the lock file.
90
91        Args:
92            timeout_secs: An integer or a float, the timeout for acquiring the
93                          lock file. 0 indicates non-block.
94
95        Returns:
96            True if the file is locked successfully. False if timeout.
97
98        Raises:
99            OSError: if any file operation fails.
100        """
101        if self._file_desc is not None:
102            raise OSError("%s has been locked." % self._file_path)
103        parent_dir = os.path.dirname(self._file_path)
104        if not os.path.exists(parent_dir):
105            os.makedirs(parent_dir)
106        successful = False
107        self._file_desc = os.open(self._file_path, os.O_CREAT | os.O_RDWR,
108                                  0o666)
109        try:
110            successful = self._Flock(timeout_secs)
111        finally:
112            if not successful:
113                os.close(self._file_desc)
114                self._file_desc = None
115        return successful
116
117    def _CheckFileDescriptor(self):
118        """Raise an error if the file is not opened or locked."""
119        if self._file_desc is None:
120            raise RuntimeError("%s has not been locked." % self._file_path)
121
122    def SetInUse(self, in_use):
123        """Write the instance state to the file.
124
125        Args:
126            in_use: A boolean, whether to set the instance to be in use.
127
128        Raises:
129            OSError: if any file operation fails.
130        """
131        self._CheckFileDescriptor()
132        os.lseek(self._file_desc, 0, os.SEEK_SET)
133        state = _IN_USE_STATE if in_use else _NOT_IN_USE_STATE
134        if os.write(self._file_desc, state) != _LOCK_FILE_SIZE:
135            raise OSError("Cannot write " + self._file_path)
136
137    def Unlock(self):
138        """Unlock the file.
139
140        Raises:
141            OSError: if any file operation fails.
142        """
143        self._CheckFileDescriptor()
144        fcntl.flock(self._file_desc, fcntl.LOCK_UN)
145        os.close(self._file_desc)
146        self._file_desc = None
147
148    def LockIfNotInUse(self, timeout_secs=_DEFAULT_TIMEOUT_SECS):
149        """Lock the file if the instance is not in use.
150
151        Returns:
152            True if the file is locked successfully.
153            False if timeout or the instance is in use.
154
155        Raises:
156            OSError: if any file operation fails.
157        """
158        if not self.Lock(timeout_secs):
159            return False
160        in_use = True
161        try:
162            in_use = os.read(self._file_desc, _LOCK_FILE_SIZE) == _IN_USE_STATE
163        finally:
164            if in_use:
165                self.Unlock()
166        return not in_use
167