1# Copyright 2017 Google Inc. 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15import time 16 17from mobly.controllers.android_device_lib import errors 18from mobly.controllers.android_device_lib import snippet_event 19 20# The max timeout cannot be larger than the max time the socket waits for a 21# response message. Otherwise, the socket would timeout before the Rpc call 22# does, leaving both server and client in unknown states. 23MAX_TIMEOUT = 60 * 10 24DEFAULT_TIMEOUT = 120 # two minutes 25 26 27class Error(errors.DeviceError): 28 pass 29 30 31class TimeoutError(Error): 32 pass 33 34 35class CallbackHandler: 36 """The class used to handle a specific group of callback events. 37 38 All the events handled by a CallbackHandler are originally triggered by one 39 async Rpc call. All the events are tagged with a callback_id specific to a 40 call to an AsyncRpc method defined on the server side. 41 42 The raw message representing an event looks like: 43 44 .. code-block:: python 45 46 { 47 'callbackId': <string, callbackId>, 48 'name': <string, name of the event>, 49 'time': <long, epoch time of when the event was created on the 50 server side>, 51 'data': <dict, extra data from the callback on the server side> 52 } 53 54 Each message is then used to create a SnippetEvent object on the client 55 side. 56 57 Attributes: 58 ret_value: The direct return value of the async Rpc call. 59 """ 60 61 def __init__(self, callback_id, event_client, ret_value, method_name, ad): 62 self._id = callback_id 63 self._event_client = event_client 64 self.ret_value = ret_value 65 self._method_name = method_name 66 self._ad = ad 67 68 @property 69 def callback_id(self): 70 return self._id 71 72 def _callEventWaitAndGet(self, callback_id, event_name, timeout): 73 """Calls snippet lib's eventWaitAndGet. 74 75 Override this method to use this class with various snippet lib 76 implementations. 77 78 Args: 79 callback_id: The callback identifier. 80 event_name: The callback name. 81 timeout: The number of seconds to wait for the event. 82 83 Returns: 84 The event dictionary. 85 """ 86 # Convert to milliseconds for Java side. 87 timeout_ms = int(timeout * 1000) 88 return self._event_client.eventWaitAndGet(callback_id, event_name, 89 timeout_ms) 90 91 def _callEventGetAll(self, callback_id, event_name): 92 """Calls snippet lib's eventGetAll. 93 94 Override this method to use this class with various snippet lib 95 implementations. 96 97 Args: 98 callback_id: The callback identifier. 99 event_name: The callback name. 100 101 Returns: 102 A list of event dictionaries. 103 """ 104 return self._event_client.eventGetAll(callback_id, event_name) 105 106 def waitAndGet(self, event_name, timeout=DEFAULT_TIMEOUT): 107 """Blocks until an event of the specified name has been received and 108 return the event, or timeout. 109 110 Args: 111 event_name: string, name of the event to get. 112 timeout: float, the number of seconds to wait before giving up. 113 114 Returns: 115 SnippetEvent, the oldest entry of the specified event. 116 117 Raises: 118 Error: If the specified timeout is longer than the max timeout 119 supported. 120 TimeoutError: The expected event does not occur within time limit. 121 """ 122 if timeout: 123 if timeout > MAX_TIMEOUT: 124 raise Error( 125 self._ad, 'Specified timeout %s is longer than max timeout %s.' % 126 (timeout, MAX_TIMEOUT)) 127 try: 128 raw_event = self._callEventWaitAndGet(self._id, event_name, timeout) 129 except Exception as e: 130 if 'EventSnippetException: timeout.' in str(e): 131 raise TimeoutError( 132 self._ad, 'Timed out after waiting %ss for event "%s" triggered by' 133 ' %s (%s).' % (timeout, event_name, self._method_name, self._id)) 134 raise 135 return snippet_event.from_dict(raw_event) 136 137 def waitForEvent(self, event_name, predicate, timeout=DEFAULT_TIMEOUT): 138 """Wait for an event of a specific name that satisfies the predicate. 139 140 This call will block until the expected event has been received or time 141 out. 142 143 The predicate function defines the condition the event is expected to 144 satisfy. It takes an event and returns True if the condition is 145 satisfied, False otherwise. 146 147 Note all events of the same name that are received but don't satisfy 148 the predicate will be discarded and not be available for further 149 consumption. 150 151 Args: 152 event_name: string, the name of the event to wait for. 153 predicate: function, a function that takes an event (dictionary) and 154 returns a bool. 155 timeout: float, default is 120s. 156 157 Returns: 158 dictionary, the event that satisfies the predicate if received. 159 160 Raises: 161 TimeoutError: raised if no event that satisfies the predicate is 162 received after timeout seconds. 163 """ 164 deadline = time.perf_counter() + timeout 165 while time.perf_counter() <= deadline: 166 # Calculate the max timeout for the next event rpc call. 167 rpc_timeout = deadline - time.perf_counter() 168 if rpc_timeout < 0: 169 break 170 # A single RPC call cannot exceed MAX_TIMEOUT. 171 rpc_timeout = min(rpc_timeout, MAX_TIMEOUT) 172 try: 173 event = self.waitAndGet(event_name, rpc_timeout) 174 except TimeoutError: 175 # Ignoring TimeoutError since we need to throw one with a more 176 # specific message. 177 break 178 if predicate(event): 179 return event 180 raise TimeoutError( 181 self._ad, 182 'Timed out after %ss waiting for an "%s" event that satisfies the ' 183 'predicate "%s".' % (timeout, event_name, predicate.__name__)) 184 185 def getAll(self, event_name): 186 """Gets all the events of a certain name that have been received so 187 far. This is a non-blocking call. 188 189 Args: 190 callback_id: The id of the callback. 191 event_name: string, the name of the event to get. 192 193 Returns: 194 A list of SnippetEvent, each representing an event from the Java 195 side. 196 """ 197 raw_events = self._callEventGetAll(self._id, event_name) 198 return [snippet_event.from_dict(msg) for msg in raw_events] 199