• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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