• 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        os.chmod(self._file_path, 0o666)
110        os.chmod(parent_dir, 0o755)
111        try:
112            successful = self._Flock(timeout_secs)
113        finally:
114            if not successful:
115                os.close(self._file_desc)
116                self._file_desc = None
117        return successful
118
119    def _CheckFileDescriptor(self):
120        """Raise an error if the file is not opened or locked."""
121        if self._file_desc is None:
122            raise RuntimeError("%s has not been locked." % self._file_path)
123
124    def SetInUse(self, in_use):
125        """Write the instance state to the file.
126
127        Args:
128            in_use: A boolean, whether to set the instance to be in use.
129
130        Raises:
131            OSError: if any file operation fails.
132        """
133        self._CheckFileDescriptor()
134        os.lseek(self._file_desc, 0, os.SEEK_SET)
135        state = _IN_USE_STATE if in_use else _NOT_IN_USE_STATE
136        if os.write(self._file_desc, state) != _LOCK_FILE_SIZE:
137            raise OSError("Cannot write " + self._file_path)
138
139    def Unlock(self):
140        """Unlock the file.
141
142        Raises:
143            OSError: if any file operation fails.
144        """
145        self._CheckFileDescriptor()
146        fcntl.flock(self._file_desc, fcntl.LOCK_UN)
147        os.close(self._file_desc)
148        self._file_desc = None
149
150    def LockIfNotInUse(self, timeout_secs=_DEFAULT_TIMEOUT_SECS):
151        """Lock the file if the instance is not in use.
152
153        Returns:
154            True if the file is locked successfully.
155            False if timeout or the instance is in use.
156
157        Raises:
158            OSError: if any file operation fails.
159        """
160        if not self.Lock(timeout_secs):
161            return False
162        in_use = True
163        try:
164            in_use = os.read(self._file_desc, _LOCK_FILE_SIZE) == _IN_USE_STATE
165        finally:
166            if in_use:
167                self.Unlock()
168        return not in_use
169