#!/usr/bin/env python # # Copyright 2020 - The Android Open Source Project # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """LocalInstanceLock class.""" import errno import fcntl import logging import os from acloud import errors from acloud.internal.lib import utils logger = logging.getLogger(__name__) _LOCK_FILE_SIZE = 1 # An empty file is equivalent to NOT_IN_USE. _IN_USE_STATE = b"I" _NOT_IN_USE_STATE = b"N" _DEFAULT_TIMEOUT_SECS = 5 class LocalInstanceLock: """The class that controls a lock file for a local instance. Acloud acquires the lock file of a local instance before it creates, deletes, or queries it. The lock prevents multiple acloud processes from accessing an instance simultaneously. The lock file records whether the instance is in use. Acloud checks the state when it needs an unused id to create a new instance. Attributes: _file_path: The path to the lock file. _file_desc: The file descriptor of the file. It is set to None when this object does not hold the lock. """ def __init__(self, file_path): self._file_path = file_path self._file_desc = None def _Flock(self, timeout_secs): """Call fcntl.flock with timeout. Args: timeout_secs: An integer or a float, the timeout for acquiring the lock file. 0 indicates non-block. Returns: True if the file is locked successfully. False if timeout. Raises: OSError: if any file operation fails. """ try: if timeout_secs > 0: wrapper = utils.TimeoutException(timeout_secs) wrapper(fcntl.flock)(self._file_desc, fcntl.LOCK_EX) else: fcntl.flock(self._file_desc, fcntl.LOCK_EX | fcntl.LOCK_NB) except errors.FunctionTimeoutError as e: logger.debug("Cannot lock %s within %s seconds", self._file_path, timeout_secs) return False except (OSError, IOError) as e: # flock raises IOError in python2; OSError in python3. if e.errno in (errno.EACCES, errno.EAGAIN): logger.debug("Cannot lock %s", self._file_path) return False raise return True def Lock(self, timeout_secs=_DEFAULT_TIMEOUT_SECS): """Acquire the lock file. Args: timeout_secs: An integer or a float, the timeout for acquiring the lock file. 0 indicates non-block. Returns: True if the file is locked successfully. False if timeout. Raises: OSError: if any file operation fails. """ if self._file_desc is not None: raise OSError("%s has been locked." % self._file_path) parent_dir = os.path.dirname(self._file_path) if not os.path.exists(parent_dir): os.makedirs(parent_dir) successful = False self._file_desc = os.open(self._file_path, os.O_CREAT | os.O_RDWR, 0o666) try: successful = self._Flock(timeout_secs) finally: if not successful: os.close(self._file_desc) self._file_desc = None return successful def _CheckFileDescriptor(self): """Raise an error if the file is not opened or locked.""" if self._file_desc is None: raise RuntimeError("%s has not been locked." % self._file_path) def SetInUse(self, in_use): """Write the instance state to the file. Args: in_use: A boolean, whether to set the instance to be in use. Raises: OSError: if any file operation fails. """ self._CheckFileDescriptor() os.lseek(self._file_desc, 0, os.SEEK_SET) state = _IN_USE_STATE if in_use else _NOT_IN_USE_STATE if os.write(self._file_desc, state) != _LOCK_FILE_SIZE: raise OSError("Cannot write " + self._file_path) def Unlock(self): """Unlock the file. Raises: OSError: if any file operation fails. """ self._CheckFileDescriptor() fcntl.flock(self._file_desc, fcntl.LOCK_UN) os.close(self._file_desc) self._file_desc = None def LockIfNotInUse(self, timeout_secs=_DEFAULT_TIMEOUT_SECS): """Lock the file if the instance is not in use. Returns: True if the file is locked successfully. False if timeout or the instance is in use. Raises: OSError: if any file operation fails. """ if not self.Lock(timeout_secs): return False in_use = True try: in_use = os.read(self._file_desc, _LOCK_FILE_SIZE) == _IN_USE_STATE finally: if in_use: self.Unlock() return not in_use