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