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 list_live_services(self):
126    """Lists the aliases of all the services that are alive.
127
128    Order of this list is determined by the order the services are
129    registered in.
130
131    Returns:
132      list of strings, the aliases of the services that are running.
133    """
134    aliases = []
135    self.for_each(
136        lambda service: aliases.append(service.alias)
137        if service.is_alive
138        else None
139    )
140    return aliases
141
142  def create_output_excerpts_all(self, test_info):
143    """Creates output excerpts from all services.
144
145    This calls `create_output_excerpts` on all registered services.
146
147    Args:
148      test_info: RuntimeTestInfo, the test info associated with the scope
149        of the excerpts.
150
151    Returns:
152      Dict, keys are the names of the services, values are the paths to
153        the excerpt files created by the corresponding services.
154    """
155    excerpt_paths = {}
156
157    def create_output_excerpts_for_one(service):
158      if not service.is_alive:
159        return
160      paths = service.create_output_excerpts(test_info)
161      excerpt_paths[service.alias] = paths
162
163    self.for_each(create_output_excerpts_for_one)
164    return excerpt_paths
165
166  def unregister_all(self):
167    """Safely unregisters all active instances.
168
169    Errors occurred here will be recorded but not raised.
170    """
171    aliases = list(self._service_objects.keys())
172    for alias in aliases:
173      self.unregister(alias)
174
175  def start_all(self):
176    """Starts all inactive service instances.
177
178    Services will be started in the order they were registered.
179    """
180    for alias, service in self._service_objects.items():
181      if not service.is_alive:
182        with expects.expect_no_raises('Failed to start service "%s".' % alias):
183          service.start()
184
185  def start_services(self, service_alises):
186    """Starts the specified services.
187
188    Services will be started in the order specified by the input list.
189    No-op for services that are already running.
190
191    Args:
192      service_alises: list of strings, the aliases of services to start.
193    """
194    for name in service_alises:
195      if name not in self._service_objects:
196        raise Error(
197            self._device,
198            'No service is registered under the name "%s", cannot start.'
199            % name,
200        )
201      service = self._service_objects[name]
202      if not service.is_alive:
203        service.start()
204
205  def stop_all(self):
206    """Stops all active service instances.
207
208    Services will be stopped in the reverse order they were registered.
209    """
210    # OrdereDict#items does not return a sequence in Python 3.4, so we have
211    # to do a list conversion here.
212    for alias, service in reversed(list(self._service_objects.items())):
213      if service.is_alive:
214        with expects.expect_no_raises('Failed to stop service "%s".' % alias):
215          service.stop()
216
217  def pause_all(self):
218    """Pauses all service instances.
219
220    Services will be paused in the reverse order they were registered.
221    """
222    # OrdereDict#items does not return a sequence in Python 3.4, so we have
223    # to do a list conversion here.
224    for alias, service in reversed(list(self._service_objects.items())):
225      with expects.expect_no_raises('Failed to pause service "%s".' % alias):
226        service.pause()
227
228  def resume_all(self):
229    """Resumes all service instances.
230
231    Services will be resumed in the order they were registered.
232    """
233    for alias, service in self._service_objects.items():
234      with expects.expect_no_raises('Failed to resume service "%s".' % alias):
235        service.resume()
236
237  def resume_services(self, service_alises):
238    """Resumes the specified services.
239
240    Services will be resumed in the order specified by the input list.
241
242    Args:
243      service_alises: list of strings, the names of services to start.
244    """
245    for name in service_alises:
246      if name not in self._service_objects:
247        raise Error(
248            self._device,
249            'No service is registered under the name "%s", cannot resume.'
250            % name,
251        )
252      service = self._service_objects[name]
253      service.resume()
254
255  def __getattr__(self, name):
256    """Syntactic sugar to enable direct access of service objects by alias.
257
258    Args:
259      name: string, the alias a service object was registered under.
260    """
261    if self.has_service_by_name(name):
262      return self._service_objects[name]
263    return self.__getattribute__(name)
264