aboutsummaryrefslogtreecommitdiff
path: root/mobly/controllers/android_device_lib/service_manager.py
blob: 983cff7618fd50ced1063cae5d68659fb5b37d49 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
# Copyright 2018 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Module for the manager of services."""
# TODO(xpconanfan: move the device errors to a more generic location so
# other device controllers like iOS can share it.
import collections
import inspect

from mobly import expects
from mobly.controllers.android_device_lib import errors
from mobly.controllers.android_device_lib.services import base_service


class Error(errors.DeviceError):
  """Root error type for this module."""


class ServiceManager:
  """Manager for services of AndroidDevice.

  A service is a long running process that involves an Android device, like
  adb logcat or Snippet.
  """

  def __init__(self, device):
    self._service_objects = collections.OrderedDict()
    self._device = device

  def has_service_by_name(self, name):
    """Checks if the manager has a service registered with a specific name.

    Args:
      name: string, the name to look for.

    Returns:
      True if a service is registered with the specified name, False
      otherwise.
    """
    return name in self._service_objects

  @property
  def is_any_alive(self):
    """True if any service is alive; False otherwise."""
    for service in self._service_objects.values():
      if service.is_alive:
        return True
    return False

  def register(self, alias, service_class, configs=None, start_service=True):
    """Registers a service.

    This will create a service instance, starts the service, and adds the
    instance to the mananger.

    Args:
      alias: string, the alias for this instance.
      service_class: class, the service class to instantiate.
      configs: (optional) config object to pass to the service class's
        constructor.
      start_service: bool, whether to start the service instance or not.
        Default is True.
    """
    if not inspect.isclass(service_class):
      raise Error(self._device, '"%s" is not a class!' % service_class)
    if not issubclass(service_class, base_service.BaseService):
      raise Error(self._device,
                  'Class %s is not a subclass of BaseService!' % service_class)
    if alias in self._service_objects:
      raise Error(self._device,
                  'A service is already registered with alias "%s".' % alias)
    service_obj = service_class(self._device, configs)
    service_obj.alias = alias
    if start_service:
      service_obj.start()
    self._service_objects[alias] = service_obj

  def unregister(self, alias):
    """Unregisters a service instance.

    Stops a service and removes it from the manager.

    Args:
      alias: string, the alias of the service instance to unregister.
    """
    if alias not in self._service_objects:
      raise Error(self._device,
                  'No service is registered with alias "%s".' % alias)
    service_obj = self._service_objects.pop(alias)
    if service_obj.is_alive:
      with expects.expect_no_raises('Failed to stop service instance "%s".' %
                                    alias):
        service_obj.stop()

  def for_each(self, func):
    """Executes a function with all registered services.

    Args:
      func: function, the function to execute. This function should take
        a service object as args.
    """
    aliases = list(self._service_objects.keys())
    for alias in aliases:
      with expects.expect_no_raises('Failed to execute "%s" for service "%s".' %
                                    (func.__name__, alias)):
        func(self._service_objects[alias])

  def list_live_services(self):
    """Lists the aliases of all the services that are alive.

    Order of this list is determined by the order the services are
    registered in.

    Returns:
      list of strings, the aliases of the services that are running.
    """
    aliases = []
    self.for_each(lambda service: aliases.append(service.alias)
                  if service.is_alive else None)
    return aliases

  def create_output_excerpts_all(self, test_info):
    """Creates output excerpts from all services.

    This calls `create_output_excerpts` on all registered services.

    Args:
      test_info: RuntimeTestInfo, the test info associated with the scope
        of the excerpts.

    Returns:
      Dict, keys are the names of the services, values are the paths to
        the excerpt files created by the corresponding services.
    """
    excerpt_paths = {}

    def create_output_excerpts_for_one(service):
      if not service.is_alive:
        return
      paths = service.create_output_excerpts(test_info)
      excerpt_paths[service.alias] = paths

    self.for_each(create_output_excerpts_for_one)
    return excerpt_paths

  def unregister_all(self):
    """Safely unregisters all active instances.

    Errors occurred here will be recorded but not raised.
    """
    aliases = list(self._service_objects.keys())
    for alias in aliases:
      self.unregister(alias)

  def start_all(self):
    """Starts all inactive service instances.

    Services will be started in the order they were registered.
    """
    for alias, service in self._service_objects.items():
      if not service.is_alive:
        with expects.expect_no_raises('Failed to start service "%s".' % alias):
          service.start()

  def start_services(self, service_alises):
    """Starts the specified services.

    Services will be started in the order specified by the input list.
    No-op for services that are already running.

    Args:
      service_alises: list of strings, the aliases of services to start.
    """
    for name in service_alises:
      if name not in self._service_objects:
        raise Error(
            self._device,
            'No service is registered under the name "%s", cannot start.' %
            name)
      service = self._service_objects[name]
      if not service.is_alive:
        service.start()

  def stop_all(self):
    """Stops all active service instances.

    Services will be stopped in the reverse order they were registered.
    """
    # OrdereDict#items does not return a sequence in Python 3.4, so we have
    # to do a list conversion here.
    for alias, service in reversed(list(self._service_objects.items())):
      if service.is_alive:
        with expects.expect_no_raises('Failed to stop service "%s".' % alias):
          service.stop()

  def pause_all(self):
    """Pauses all service instances.

    Services will be paused in the reverse order they were registered.
    """
    # OrdereDict#items does not return a sequence in Python 3.4, so we have
    # to do a list conversion here.
    for alias, service in reversed(list(self._service_objects.items())):
      with expects.expect_no_raises('Failed to pause service "%s".' % alias):
        service.pause()

  def resume_all(self):
    """Resumes all service instances.

    Services will be resumed in the order they were registered.
    """
    for alias, service in self._service_objects.items():
      with expects.expect_no_raises('Failed to resume service "%s".' % alias):
        service.resume()

  def resume_services(self, service_alises):
    """Resumes the specified services.

    Services will be resumed in the order specified by the input list.

    Args:
      service_alises: list of strings, the names of services to start.
    """
    for name in service_alises:
      if name not in self._service_objects:
        raise Error(
            self._device,
            'No service is registered under the name "%s", cannot resume.' %
            name)
      service = self._service_objects[name]
      service.resume()

  def __getattr__(self, name):
    """Syntactic sugar to enable direct access of service objects by alias.

    Args:
      name: string, the alias a service object was registered under.
    """
    if self.has_service_by_name(name):
      return self._service_objects[name]
    return self.__getattribute__(name)