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