1# Copyright 2018 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"""Module for the manager of services.""" 15# TODO(xpconanfan: move the device errors to a more generic location so 16# other device controllers like iOS can share it. 17import collections 18import inspect 19 20from mobly import expects 21from mobly.controllers.android_device_lib import errors 22from mobly.controllers.android_device_lib.services import base_service 23 24 25class Error(errors.DeviceError): 26 """Root error type for this module.""" 27 28 29class ServiceManager: 30 """Manager for services of AndroidDevice. 31 32 A service is a long running process that involves an Android device, like 33 adb logcat or Snippet. 34 """ 35 36 def __init__(self, device): 37 self._service_objects = collections.OrderedDict() 38 self._device = device 39 40 def has_service_by_name(self, name): 41 """Checks if the manager has a service registered with a specific name. 42 43 Args: 44 name: string, the name to look for. 45 46 Returns: 47 True if a service is registered with the specified name, False 48 otherwise. 49 """ 50 return name in self._service_objects 51 52 @property 53 def is_any_alive(self): 54 """True if any service is alive; False otherwise.""" 55 for service in self._service_objects.values(): 56 if service.is_alive: 57 return True 58 return False 59 60 def register(self, alias, service_class, configs=None, start_service=True): 61 """Registers a service. 62 63 This will create a service instance, starts the service, and adds the 64 instance to the mananger. 65 66 Args: 67 alias: string, the alias for this instance. 68 service_class: class, the service class to instantiate. 69 configs: (optional) config object to pass to the service class's 70 constructor. 71 start_service: bool, whether to start the service instance or not. 72 Default is True. 73 """ 74 if not inspect.isclass(service_class): 75 raise Error(self._device, '"%s" is not a class!' % service_class) 76 if not issubclass(service_class, base_service.BaseService): 77 raise Error( 78 self._device, 79 'Class %s is not a subclass of BaseService!' % service_class, 80 ) 81 if alias in self._service_objects: 82 raise Error( 83 self._device, 84 'A service is already registered with alias "%s".' % alias, 85 ) 86 service_obj = service_class(self._device, configs) 87 service_obj.alias = alias 88 if start_service: 89 service_obj.start() 90 self._service_objects[alias] = service_obj 91 92 def unregister(self, alias): 93 """Unregisters a service instance. 94 95 Stops a service and removes it from the manager. 96 97 Args: 98 alias: string, the alias of the service instance to unregister. 99 """ 100 if alias not in self._service_objects: 101 raise Error( 102 self._device, 'No service is registered with alias "%s".' % alias 103 ) 104 service_obj = self._service_objects.pop(alias) 105 if service_obj.is_alive: 106 with expects.expect_no_raises( 107 'Failed to stop service instance "%s".' % alias 108 ): 109 service_obj.stop() 110 111 def for_each(self, func): 112 """Executes a function with all registered services. 113 114 Args: 115 func: function, the function to execute. This function should take 116 a service object as args. 117 """ 118 aliases = list(self._service_objects.keys()) 119 for alias in aliases: 120 with expects.expect_no_raises( 121 'Failed to execute "%s" for service "%s".' % (func.__name__, alias) 122 ): 123 func(self._service_objects[alias]) 124 125 def get_service_alias_by_class(self, service_class): 126 """Gets the aslias name of a registered service. 127 128 The same service class can be registered multiple times with different 129 aliases. When not well managed, duplication and race conditions can arise. 130 One can use this API to de-duplicate as needed. 131 132 Args: 133 service_class: class, the class of a service type. 134 135 Returns: 136 list of strings, the aliases the service is registered with. 137 """ 138 aliases = [] 139 for alias, service_object in self._service_objects.items(): 140 if isinstance(service_object, service_class): 141 aliases.append(alias) 142 return aliases 143 144 def list_live_services(self): 145 """Lists the aliases of all the services that are alive. 146 147 Order of this list is determined by the order the services are 148 registered in. 149 150 Returns: 151 list of strings, the aliases of the services that are running. 152 """ 153 aliases = [] 154 self.for_each( 155 lambda service: aliases.append(service.alias) 156 if service.is_alive 157 else None 158 ) 159 return aliases 160 161 def create_output_excerpts_all(self, test_info): 162 """Creates output excerpts from all services. 163 164 This calls `create_output_excerpts` on all registered services. 165 166 Args: 167 test_info: RuntimeTestInfo, the test info associated with the scope 168 of the excerpts. 169 170 Returns: 171 Dict, keys are the names of the services, values are the paths to 172 the excerpt files created by the corresponding services. 173 """ 174 excerpt_paths = {} 175 176 def create_output_excerpts_for_one(service): 177 if not service.is_alive: 178 return 179 paths = service.create_output_excerpts(test_info) 180 excerpt_paths[service.alias] = paths 181 182 self.for_each(create_output_excerpts_for_one) 183 return excerpt_paths 184 185 def unregister_all(self): 186 """Safely unregisters all active instances. 187 188 Errors occurred here will be recorded but not raised. 189 """ 190 aliases = list(self._service_objects.keys()) 191 for alias in aliases: 192 self.unregister(alias) 193 194 def start_all(self): 195 """Starts all inactive service instances. 196 197 Services will be started in the order they were registered. 198 """ 199 for alias, service in self._service_objects.items(): 200 if not service.is_alive: 201 with expects.expect_no_raises('Failed to start service "%s".' % alias): 202 service.start() 203 204 def start_services(self, service_alises): 205 """Starts the specified services. 206 207 Services will be started in the order specified by the input list. 208 No-op for services that are already running. 209 210 Args: 211 service_alises: list of strings, the aliases of services to start. 212 """ 213 for name in service_alises: 214 if name not in self._service_objects: 215 raise Error( 216 self._device, 217 'No service is registered under the name "%s", cannot start.' 218 % name, 219 ) 220 service = self._service_objects[name] 221 if not service.is_alive: 222 service.start() 223 224 def stop_all(self): 225 """Stops all active service instances. 226 227 Services will be stopped in the reverse order they were registered. 228 """ 229 # OrdereDict#items does not return a sequence in Python 3.4, so we have 230 # to do a list conversion here. 231 for alias, service in reversed(list(self._service_objects.items())): 232 if service.is_alive: 233 with expects.expect_no_raises('Failed to stop service "%s".' % alias): 234 service.stop() 235 236 def pause_all(self): 237 """Pauses all service instances. 238 239 Services will be paused in the reverse order they were registered. 240 """ 241 # OrdereDict#items does not return a sequence in Python 3.4, so we have 242 # to do a list conversion here. 243 for alias, service in reversed(list(self._service_objects.items())): 244 with expects.expect_no_raises('Failed to pause service "%s".' % alias): 245 service.pause() 246 247 def resume_all(self): 248 """Resumes all service instances. 249 250 Services will be resumed in the order they were registered. 251 """ 252 for alias, service in self._service_objects.items(): 253 with expects.expect_no_raises('Failed to resume service "%s".' % alias): 254 service.resume() 255 256 def resume_services(self, service_alises): 257 """Resumes the specified services. 258 259 Services will be resumed in the order specified by the input list. 260 261 Args: 262 service_alises: list of strings, the names of services to start. 263 """ 264 for name in service_alises: 265 if name not in self._service_objects: 266 raise Error( 267 self._device, 268 'No service is registered under the name "%s", cannot resume.' 269 % name, 270 ) 271 service = self._service_objects[name] 272 service.resume() 273 274 def __getattr__(self, name): 275 """Syntactic sugar to enable direct access of service objects by alias. 276 277 Args: 278 name: string, the alias a service object was registered under. 279 """ 280 if self.has_service_by_name(name): 281 return self._service_objects[name] 282 return self.__getattribute__(name) 283