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 os 21import sys 22import subprocess 23import time 24import traceback 25 26# May import this package in the workstation with: 27# pip install paramiko 28from paramiko import SSHClient 29from paramiko import AutoAddPolicy 30 31# This script works on Linux workstation. 32# We haven't tested on Windows/macOS. 33 34# Demonstration of tracing QNX host and get tracelogger output as a binary file. 35# 36# Prerequirements for run the script: 37# Install traceprinter utility in QNX Software and setup proper path for it. 38# One can Install QNX Software Center from the following location: 39# https://www.qnx.com/download/group.html?programid=29178 40# 41# Define an environment varialbe QNX_DEV_DIR, the script will read this environment variable. 42# export QNX_DEV_DIR=/to/qns_dev_dir/ 43# Make symbolic link or copy traceprinter, qnx_perfetto.py under this directory. 44# 45# Usage: 46# python3 tracing_agent.py --guest_serial 10.42.0.235 --host_ip 47# 10.42.0.235 --host_tracing_file_name qnx.trace --out_dir test_trace 48# --duration 2 --host_username root 49# 50 51def parseArguments(): 52 parser = argparse.ArgumentParser( 53 prog = 'vm_tracing_driver.py', 54 description='VM Tracing Driver') 55 parser.add_argument('--guest_serial', required=True, 56 help = 'guest VM serial number') 57 parser.add_argument('--guest_config', required=True, 58 help = 'guest VM configuration file') 59 parser.add_argument('--host_ip', required=True, 60 help = 'host IP address') 61 #TODO(b/267675642): read user name from user ssh_config. 62 parser.add_argument('--host_username', required=True, 63 help = 'host username') 64 parser.add_argument('--host_tracing_file_name', required=True, 65 help = 'host trace file name') 66 parser.add_argument('--out_dir', required=True, 67 help = 'directory to store output file') 68 parser.add_argument('--duration', type=int, required=True, 69 help = 'tracing time') 70 parser.add_argument('--verbose', action='store_true') 71 return parser.parse_args() 72 73def subprocessRun(cmd): 74 print(f'Subprocess executing command {cmd}') 75 subprocess.run(cmd, check=True) 76 77# This is the base class for tracing agent. 78class TracingAgent: 79 # child class should extend this function 80 def __init__(self, name='TracingAgent'): 81 self.name = name 82 self.thread = Thread(target=self.run) 83 self.error_msg = None 84 85 # abstract method 86 # Start tracing on the device. 87 # Raise exception when there is an error. 88 def startTracing(self): 89 pass 90 91 # abstract method 92 # Copy tracing file from device to worksstation. 93 # Raise exception when there is an error. 94 def copyTracingFile(self): 95 pass 96 97 # abstract method 98 # Parse tracing file to perfetto input format. 99 # Raise exception when there is an error. 100 def parseTracingFile(self): 101 pass 102 103 def verbose_print(self, str): 104 if self.verbose: 105 print(str) 106 107 def run(self): 108 try: 109 print(f'**********start tracing on {self.name} vm') 110 self.startTracing() 111 112 print(f'**********copy tracing file from {self.name} vm') 113 self.copyTracingFile() 114 115 print(f'**********parse tracing file of {self.name} vm') 116 self.parseTracingFile() 117 except Exception as e: 118 traceresult = traceback.format_exc() 119 self.error_msg = f'Caught an exception: {traceback.format_exc()}' 120 sys.exit() 121 122 def start(self): 123 self.thread.start() 124 125 def join(self): 126 self.thread.join() 127 # Check if the thread exit cleanly or not. 128 # If the thread doesn't exit cleanly, will throw an exception in the main process. 129 if self.error_msg != None: 130 sys.exit(self.error_msg) 131 132 print(f'**********tracing done on {self.name}') 133 134# HostTracingAgent for QNX 135class QnxTracingAgent(TracingAgent): 136 def __init__(self, args): 137 self.verbose = args.verbose 138 self.ip = args.host_ip 139 super().__init__(f'qnx host at ssh://{self.ip}') 140 self.username = args.host_username 141 self.out_dir = args.out_dir 142 self.duration = args.duration 143 self.tracing_kev_file_path = os.path.join(args.out_dir, f'{args.host_tracing_file_name}.kev') 144 self.tracing_printer_file_path = os.path.join(args.out_dir, args.host_tracing_file_name) 145 146 # setup a sshclient 147 self.client = SSHClient() 148 self.client.load_system_host_keys() 149 self.client.set_missing_host_key_policy(AutoAddPolicy()) 150 self.client.connect(self.ip, username=self.username) 151 152 # create directory at the host to store tracing config and tracing output 153 if self.doesDirExist(self.out_dir) == False: 154 mkdir_cmd = f'mkdir {self.out_dir}' 155 self.clientExecuteCmd(mkdir_cmd) 156 157 # TODO(b/267675642): 158 # read the trace configuration file to get the tracing parameters 159 160 def clientExecuteCmd(self, cmd_str): 161 self.verbose_print(f'sshclient executing command {cmd_str}') 162 (stdin, stdout, stderr) = self.client.exec_command(cmd_str) 163 if stdout.channel.recv_exit_status(): 164 raise Exception(stderr.read()) 165 elif stderr.channel.recv_exit_status(): 166 raise Exception(stderr.read()) 167 168 def doesDirExist(self, dirpath): 169 cmd = f'ls -d {dirpath}' 170 (stdin, stdout, stderr) = self.client.exec_command(cmd) 171 error_str = stderr.read() 172 if len(error_str) == 0: 173 return True 174 return False 175 176 def startTracing(self): 177 tracing_cmd = f'on -p15 tracelogger -s {self.duration} -f {self.tracing_kev_file_path}' 178 self.clientExecuteCmd(tracing_cmd) 179 180 def copyTracingFile(self): 181 # copy tracing output file from host to workstation 182 os.makedirs(self.out_dir, exist_ok=True) 183 scp_cmd = ['scp', '-F', '/dev/null', 184 f'{self.username}@{self.ip}:{self.tracing_kev_file_path}', 185 f'{self.tracing_kev_file_path}'] 186 subprocessRun(scp_cmd) 187 188 def parseTracingFile(self): 189 # using traceprinter to convert binary file to text file 190 # for traceprinter options, reference: 191 # http://www.qnx.com/developers/docs/7.0.0/index.html#com.qnx.doc.neutrino.utilities/topic/t/traceprinter.html 192 qnx_dev_dir = os.environ.get('QNX_DEV_DIR') 193 traceprinter = os.path.join(qnx_dev_dir, 'traceprinter') 194 traceprinter_cmd = [traceprinter, 195 '-p', '%C %t %Z %z', 196 '-f', f'{self.tracing_kev_file_path}', 197 '-o', f'{self.tracing_printer_file_path}'] 198 subprocessRun(traceprinter_cmd) 199 200 # convert tracing file in text format to json format: 201 qnx2perfetto = os.path.join(qnx_dev_dir, 'qnx_perfetto.py') 202 convert_cmd = [qnx2perfetto, 203 f'{self.tracing_printer_file_path}'] 204 subprocessRun(convert_cmd) 205 206class AndroidTracingAgent(TracingAgent): 207 def __init__(self, args): 208 self.verbose = args.verbose 209 self.vm_trace_file = 'guest.trace' 210 self.vm_config = 'guest_config.pbtx' 211 self.ip = args.guest_serial 212 self.out_dir = args.out_dir 213 self.trace_file_path = os.path.join(args.out_dir, self.vm_trace_file) 214 self.config_file_path = args.guest_config 215 self.vm_config_file_path = os.path.join('/data/misc/perfetto-configs/', self.vm_config) 216 self.vm_trace_file_path = os.path.join('/data/misc/perfetto-traces/', self.vm_trace_file) 217 super().__init__(f'android vm at adb://{self.ip}') 218 219 subprocessRun(['adb', 'connect', self.ip]) 220 subprocessRun(['adb', 'root']) 221 subprocessRun(['adb', 'remount']) 222 223 def copyConfigFile(self): 224 subprocessRun(['adb', 'push', self.config_file_path, self.vm_config_file_path]) 225 226 def startTracing(self): 227 subprocessRun(['adb', 'shell', '-t', 'perfetto', 228 '--txt', '-c', self.vm_config_file_path, 229 '--out', self.vm_trace_file_path]) 230 231 def copyTracingFile(self): 232 os.makedirs(self.out_dir, exist_ok=True) 233 subprocessRun(['adb', 'pull', self.vm_trace_file_path, self.trace_file_path]) 234 235def main(): 236 args = parseArguments() 237 host_agent = QnxTracingAgent(args) 238 guest_agent = AndroidTracingAgent(args) 239 240 host_agent.start() 241 guest_agent.start() 242 host_agent.join() 243 guest_agent.join() 244 245if __name__ == "__main__": 246 main() 247