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