1#!/usr/bin/env python3 2# 3# Copyright (C) 2023 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 argparse 18from datetime import datetime 19from threading import Thread 20import json 21import os 22import re 23import sys 24import subprocess 25import time 26import traceback 27 28# May import this package in the workstation with: 29# pip install paramiko 30from paramiko import SSHClient 31from paramiko import AutoAddPolicy 32 33import calculate_time_offset 34from remote_slay import slay_process 35import update_trace 36 37# This script works on Linux workstation. 38# We haven't tested on Windows/macOS. 39 40# Demonstration of tracing QNX host and get tracelogger output as a binary file. 41# 42# Prerequirements for run the script: 43# Install traceprinter utility in QNX Software and setup proper path for it. 44# One can Install QNX Software Center from the following location: 45# https://www.qnx.com/download/group.html?programid=29178 46# 47# Define an environment varialbe QNX_DEV_DIR, the script will read this environment variable. 48# export QNX_DEV_DIR=/to/qns_dev_dir/ 49# Make symbolic link or copy traceprinter, qnx_perfetto.py under this directory. 50# 51# Install perfetto SDK from https://github.com/google/perfetto/releases. 52# Define an environment varialbe PERFETTO_DEV_DIR, the script will read this environment variable. 53# export PERFETTO_DEV_DIR=/to/perfetto_sdk_dir/ 54# 55# Usage: 56# python3 tracing_agent.py --guest_serial 10.42.0.235 --host_ip 57# 10.42.0.235 --host_tracing_file_name qnx.trace --out_dir test_trace 58# --duration 2 --host_username root 59# 60 61QNX_DEV_DIR_ENV_NAME = 'QNX_DEV_DIR' 62qnx_dev_dir = os.environ.get(QNX_DEV_DIR_ENV_NAME) 63 64PERFETTO_DEV_DIR_ENV_NAME = 'PERFETTO_DEV_DIR' 65perfetto_dev_dir = os.environ.get(PERFETTO_DEV_DIR_ENV_NAME) 66 67def parseArguments(): 68 parser = argparse.ArgumentParser( 69 prog = 'vm_tracing_driver.py', 70 description='VM Tracing Driver') 71 parser.add_argument('--guest_serial', required=True, 72 help = 'guest VM serial number') 73 parser.add_argument('--guest_config', required=True, 74 help = 'guest VM configuration file') 75 parser.add_argument('--host_ip', required=True, 76 help = 'host IP address') 77 #TODO(b/267675642): read user name from user ssh_config. 78 parser.add_argument('--host_username', required=True, 79 help = 'host username') 80 parser.add_argument('--host_tracing_file_name', required=True, 81 help = 'host trace file name') 82 parser.add_argument('--out_dir', required=True, 83 help = 'directory to store output file') 84 parser.add_argument('--duration', type=int, required=True, 85 help = 'tracing time') 86 parser.add_argument('--verbose', action='store_true') 87 return parser.parse_args() 88 89def subprocessRun(cmd): 90 print(f'Subprocess executing command {cmd}') 91 return subprocess.run(cmd, stdout=subprocess.PIPE, check=True) 92 93# This is the base class for tracing agent. 94class TracingAgent: 95 # child class should extend this function 96 def __init__(self, name='TracingAgent'): 97 self.name = name 98 self.thread = Thread(target=self.run) 99 self.error_msg = None 100 101 # abstract method 102 # Start tracing on the device. 103 # Raise exception when there is an error. 104 def startTracing(self): 105 pass 106 107 # abstract method 108 # Copy tracing file from device to worksstation. 109 # Raise exception when there is an error. 110 def copyTracingFile(self): 111 pass 112 113 # abstract method 114 # Parse tracing file to perfetto input format. 115 # Raise exception when there is an error. 116 def parseTracingFile(self): 117 pass 118 119 def verbose_print(self, str): 120 if self.verbose: 121 print(str) 122 123 def run(self): 124 try: 125 print(f'**********start tracing on {self.name} vm') 126 self.startTracing() 127 128 print(f'**********copy tracing file from {self.name} vm') 129 self.copyTracingFile() 130 131 print(f'**********parse tracing file of {self.name} vm') 132 self.parseTracingFile() 133 except Exception as e: 134 traceresult = traceback.format_exc() 135 self.error_msg = f'Caught an exception: {traceback.format_exc()}' 136 sys.exit() 137 138 def start(self): 139 self.thread.start() 140 141 def join(self): 142 self.thread.join() 143 # Check if the thread exit cleanly or not. 144 # If the thread doesn't exit cleanly, will throw an exception in the main process. 145 if self.error_msg != None: 146 sys.exit(self.error_msg) 147 148 print(f'**********tracing done on {self.name}') 149 150# HostTracingAgent for QNX 151class QnxTracingAgent(TracingAgent): 152 def __init__(self, args): 153 self.verbose = args.verbose 154 self.ip = args.host_ip 155 super().__init__(f'qnx host at ssh://{self.ip}') 156 self.username = args.host_username 157 self.out_dir = args.out_dir 158 self.duration = args.duration 159 self.tracing_kev_file_path = os.path.join(args.out_dir, f'{args.host_tracing_file_name}.kev') 160 self.tracing_printer_file_path = os.path.join(args.out_dir, f'{args.host_tracing_file_name}.printer') 161 162 # setup a sshclient 163 self.client = SSHClient() 164 self.client.load_system_host_keys() 165 self.client.set_missing_host_key_policy(AutoAddPolicy()) 166 self.client.connect(self.ip, username=self.username) 167 168 # create directory at the host to store tracing config and tracing output 169 if self.doesDirExist(self.out_dir) == False: 170 mkdir_cmd = f'mkdir {self.out_dir}' 171 self.clientExecuteCmd(mkdir_cmd) 172 173 # TODO(b/267675642): 174 # read the trace configuration file to get the tracing parameters 175 176 if not slay_process(self.client, "memdump_tracelogger"): 177 print("Warning: could not kill memdump_tracelogger on QNX." 178 "If there is a resource busy error reported by QNX, " 179 "execute slay memdump_tracelogger on QNX.") 180 181 def clientExecuteCmd(self, cmd_str): 182 self.verbose_print(f'sshclient executing command {cmd_str}') 183 (stdin, stdout, stderr) = self.client.exec_command(cmd_str) 184 if stdout.channel.recv_exit_status(): 185 raise Exception(stderr.read()) 186 elif stderr.channel.recv_exit_status(): 187 raise Exception(stderr.read()) 188 189 def doesDirExist(self, dirpath): 190 cmd = f'ls -d {dirpath}' 191 (stdin, stdout, stderr) = self.client.exec_command(cmd) 192 error_str = stderr.read() 193 if len(error_str) == 0: 194 return True 195 return False 196 197 def startTracing(self): 198 tracing_cmd = f'on -p15 tracelogger -s {self.duration} -f {self.tracing_kev_file_path}' 199 self.clientExecuteCmd(tracing_cmd) 200 201 def copyTracingFile(self): 202 # copy tracing output file from host to workstation 203 os.makedirs(self.out_dir, exist_ok=True) 204 scp_cmd = ['scp', '-F', '/dev/null', 205 f'{self.username}@{self.ip}:{self.tracing_kev_file_path}', 206 f'{self.tracing_kev_file_path}'] 207 subprocessRun(scp_cmd) 208 209 def parseTracingFile(self): 210 # using traceprinter to convert binary file to text file 211 # for traceprinter options, reference: 212 # http://www.qnx.com/developers/docs/7.0.0/index.html#com.qnx.doc.neutrino.utilities/topic/t/traceprinter.html 213 global qnx_dev_dir 214 traceprinter = os.path.join(qnx_dev_dir, 'host/linux/x86_64/usr/bin/', 'traceprinter') 215 traceprinter_cmd = [traceprinter, 216 '-p', '%C %t %Z %z', 217 '-f', f'{self.tracing_kev_file_path}', 218 '-o', f'{self.tracing_printer_file_path}'] 219 subprocessRun(traceprinter_cmd) 220 221 # convert tracing file in text format to json format: 222 qnx2perfetto = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'qnx_perfetto.py') 223 convert_cmd = [qnx2perfetto, 224 f'{self.tracing_printer_file_path}'] 225 subprocessRun(convert_cmd) 226 227class AndroidTracingAgent(TracingAgent): 228 def __init__(self, args): 229 self.verbose = args.verbose 230 self.vm_trace_file = 'guest.trace' 231 self.vm_config = 'guest_config.pbtx' 232 self.ip = args.guest_serial 233 self.out_dir = args.out_dir 234 self.trace_file_path = os.path.join(args.out_dir, self.vm_trace_file) 235 self.config_file_path = args.guest_config 236 self.vm_config_file_path = os.path.join('/data/misc/perfetto-configs/', self.vm_config) 237 self.vm_trace_file_path = os.path.join('/data/misc/perfetto-traces/', self.vm_trace_file) 238 super().__init__(f'android vm at adb://{self.ip}') 239 240 self.adb(['connect']) 241 self.adb(['root']) 242 self.adb(['remount']) 243 244 def copyConfigFile(self): 245 self.adb(['push', self.config_file_path, self.vm_config_file_path]) 246 247 def startTracing(self): 248 self.copyConfigFile() 249 self.adb(['shell', '-t', 'perfetto', 250 '--txt', '-c', self.vm_config_file_path, 251 '--out', self.vm_trace_file_path]) 252 253 def copyTracingFile(self): 254 os.makedirs(self.out_dir, exist_ok=True) 255 self.adb(['pull', self.vm_trace_file_path, self.trace_file_path]) 256 257 def adb(self, cmd): 258 adb_cmd = ['adb'] 259 if cmd == ['connect']: 260 adb_cmd.extend(['connect', self.ip]) 261 else: 262 adb_cmd.extend(['-s', self.ip]) 263 adb_cmd.extend(cmd) 264 265 return subprocessRun(adb_cmd) 266 267def merge_files(in_file1, in_file2, out_file): 268 try: 269 with open(in_file1, 'r') as f: 270 trace_dict1 = json.loads(f.read()) 271 272 with open(in_file2, 'r') as f: 273 trace_dict2 = json.loads(f.read()) 274 275 trace_dict1.update(trace_dict2) 276 with open(out_file, 'w') as f: 277 json.dump(trace_dict1, f) 278 print(f"Updated trace data saved to {out_file}") 279 except Exception as e: 280 sys.exit(f'merge_files failure due to: {e}') 281 282def update_and_merge_files(args, host_agent, guest_agent): 283 # calculate the time offset 284 try: 285 time_offset = calculate_time_offset.CalculateTimeOffset( 286 args.host_username, args.host_ip, args.guest_serial, "CLOCK_REALTIME", "trace") 287 except Exception as e: 288 sys.exit(f'Exception: catch calculate_time_offset exception {e}') 289 290 # update the timestamp and process id in the host json file 291 host_json_file = '{}.json'.format(host_agent.tracing_printer_file_path) 292 if not update_trace.update_trace_file(host_json_file, time_offset): 293 sys.exit('Error: update_trace_file') 294 295 # convert guest trace file to .json format 296 global perfetto_dev_dir 297 traceconv_cmd = os.path.join(perfetto_dev_dir, 'traceconv') 298 guest_json_file = '{}.json'.format(guest_agent.trace_file_path) 299 subprocessRun([traceconv_cmd, 'json', guest_agent.trace_file_path, guest_json_file]) 300 301 # merge host and guest trace files 302 merged_file_path = os.path.join(guest_agent.out_dir, 'merged_guest_host.json') 303 host_update_json_file = '{}_updated.json'.format(host_agent.tracing_printer_file_path) 304 merge_files(host_json_file, guest_json_file, merged_file_path) 305 306def main(): 307 if perfetto_dev_dir is None: 308 sys.exit(f'The {PERFETTO_DEV_DIR_ENV_NAME} variable is not defined') 309 310 if qnx_dev_dir is None: 311 sys.exit(f'The {QNX_DEV_DIR_ENV_NAME} variable is not defined') 312 313 args = parseArguments() 314 host_agent = QnxTracingAgent(args) 315 guest_agent = AndroidTracingAgent(args) 316 317 host_agent.start() 318 guest_agent.start() 319 host_agent.join() 320 guest_agent.join() 321 update_and_merge_files(args, host_agent, guest_agent) 322 323if __name__ == "__main__": 324 main() 325