• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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