• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2#
3#   Copyright 2020 - The Android Open Source Project
4#
5#   Licensed under the Apache License, Version 2.0 (the 'License');
6#   you may not use this file except in compliance with the License.
7#   You may obtain a copy of the License at
8#
9#       http://www.apache.org/licenses/LICENSE-2.0
10#
11#   Unless required by applicable law or agreed to in writing, software
12#   distributed under the License is distributed on an 'AS IS' BASIS,
13#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14#   See the License for the specific language governing permissions and
15#   limitations under the License.
16
17import atexit
18import json
19import logging
20import os
21import re
22import signal
23import tempfile
24import time
25
26from enum import Enum
27
28from acts import context
29from acts.libs.proc import job
30from acts.libs.proc import process
31
32
33class BitsServiceError(Exception):
34    pass
35
36
37class BitsServiceStates(Enum):
38    NOT_STARTED = 'not-started'
39    STARTED = 'started'
40    STOPPED = 'stopped'
41
42
43class BitsService(object):
44    """Helper class to start and stop a bits service
45
46    Attributes:
47        port: When the service starts the port it was assigned to is made
48        available for external agents to reference to the background service.
49        config: The BitsServiceConfig used to configure this service.
50        name: A free form string.
51        service_state: A BitsServiceState that represents the service state.
52    """
53
54    def __init__(self, config, binary, output_log_path,
55                 name='bits_service_default',
56                 timeout=None):
57        """Creates a BitsService object.
58
59        Args:
60            config: A BitsServiceConfig.
61            described in go/pixel-bits/user-guide/service/configuration.md
62            binary: Path to a bits_service binary.
63            output_log_path: Full path to where the resulting logs should be
64            stored.
65            name: Optional string to identify this service by. This
66            is used as reference in logs to tell this service apart from others
67            running in parallel.
68            timeout: Maximum time in seconds the service should be allowed
69            to run in the background after start. If left undefined the service
70            in the background will not time out.
71        """
72        self.name = name
73        self.port = None
74        self.config = config
75        self.service_state = BitsServiceStates.NOT_STARTED
76        self._timeout = timeout
77        self._binary = binary
78        self._log = logging.getLogger()
79        self._process = None
80        self._output_log = open(output_log_path, 'w')
81        self._collections_dir = tempfile.TemporaryDirectory(
82            prefix='bits_service_collections_dir_')
83        self._cleaned_up = False
84        atexit.register(self._atexit_cleanup)
85
86    def _atexit_cleanup(self):
87        if not self._cleaned_up:
88            self._log.error('Cleaning up bits_service %s at exit.', self.name)
89            self._cleanup()
90
91    def _write_extra_debug_logs(self):
92        dmesg_log = '%s.dmesg.txt' % self._output_log.name
93        dmesg = job.run(['dmesg', '-e'], ignore_status=True)
94        with open(dmesg_log, 'w') as f:
95            f.write(dmesg.stdout)
96
97        free_log = '%s.free.txt' % self._output_log.name
98        free = job.run(['free', '-m'], ignore_status=True)
99        with open(free_log, 'w') as f:
100            f.write(free.stdout)
101
102        df_log = '%s.df.txt' % self._output_log.name
103        df = job.run(['df', '-h'], ignore_status=True)
104        with open(df_log, 'w') as f:
105            f.write(df.stdout)
106
107    def _cleanup(self):
108        self._write_extra_debug_logs()
109        self.port = None
110        self._collections_dir.cleanup()
111        if self._process and self._process.is_running():
112            self._process.signal(signal.SIGINT)
113            self._log.debug('SIGINT sent to bits_service %s.' % self.name)
114            self._process.wait(kill_timeout=60.0)
115            self._log.debug('bits_service %s has been stopped.' % self.name)
116        self._output_log.close()
117        if self.config.has_monsoon:
118            job.run([self.config.monsoon_config.monsoon_binary,
119                     '--serialno',
120                     str(self.config.monsoon_config.serial_num),
121                     '--usbpassthrough',
122                     'on'],
123                    timeout=10)
124        self._cleaned_up = True
125
126    def _service_started_listener(self, line):
127        if self.service_state is BitsServiceStates.STARTED:
128            return
129        if 'Started server!' in line and self.port is not None:
130            self.service_state = BitsServiceStates.STARTED
131
132    PORT_PATTERN = re.compile(r'.*Server listening on .*:(\d+)\.$')
133
134    def _service_port_listener(self, line):
135        if self.port is not None:
136            return
137        match = self.PORT_PATTERN.match(line)
138        if match:
139            self.port = match.group(1)
140
141    def _output_callback(self, line):
142        self._output_log.write(line)
143        self._output_log.write('\n')
144        self._service_port_listener(line)
145        self._service_started_listener(line)
146
147    def _trigger_background_process(self, binary):
148        config_path = os.path.join(
149            context.get_current_context().get_full_output_path(),
150            '%s.config.json' % self.name)
151        with open(config_path, 'w') as f:
152            f.write(json.dumps(self.config.config_dic, indent=2))
153
154        cmd = [binary,
155               '--port',
156               '0',
157               '--collections_folder',
158               self._collections_dir.name,
159               '--collector_config_file',
160               config_path]
161
162        # bits_service only works on linux systems, therefore is safe to assume
163        # that 'timeout' will be available.
164        if self._timeout:
165            cmd = ['timeout',
166                   '--signal=SIGTERM',
167                   '--kill-after=60',
168                   str(self._timeout)] + cmd
169
170        self._process = process.Process(cmd)
171        self._process.set_on_output_callback(self._output_callback)
172        self._process.set_on_terminate_callback(self._on_terminate)
173        self._process.start()
174
175    def _on_terminate(self, *_):
176        self._log.error('bits_service %s stopped unexpectedly.', self.name)
177        self._cleanup()
178
179    def start(self):
180        """Starts the bits service in the background.
181
182        This function blocks until the background service signals that it has
183        successfully started. A BitsServiceError is raised if the signal is not
184        received.
185        """
186        if self.service_state is BitsServiceStates.STOPPED:
187            raise BitsServiceError(
188                'bits_service %s was already stopped. A stopped'
189                ' service can not be started again.' % self.name)
190
191        if self.service_state is BitsServiceStates.STARTED:
192            raise BitsServiceError(
193                'bits_service %s has already been started.' % self.name)
194
195        self._log.info('starting bits_service %s', self.name)
196        self._trigger_background_process(self._binary)
197
198        # wait 40 seconds for the service to be ready.
199        max_startup_wait = time.time() + 40
200        while time.time() < max_startup_wait:
201            if self.service_state is BitsServiceStates.STARTED:
202                self._log.info('bits_service %s started on port %s', self.name,
203                               self.port)
204                return
205            time.sleep(0.1)
206
207        self._log.error('bits_service %s did not start on time, starting '
208                        'service teardown and raising a BitsServiceError.')
209        self._cleanup()
210        raise BitsServiceError(
211            'bits_service %s did not start successfully' % self.name)
212
213    def stop(self):
214        """Stops the bits service."""
215        if self.service_state is BitsServiceStates.STOPPED:
216            raise BitsServiceError(
217                'bits_service %s has already been stopped.' % self.name)
218        port = self.port
219        self._log.info('stopping bits_service %s on port %s', self.name, port)
220        self.service_state = BitsServiceStates.STOPPED
221        self._cleanup()
222        self._log.info('bits_service %s on port %s was stopped', self.name,
223                       port)
224