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