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