bash
复制代码
#!/usr/bin/env python3
# Copyright (C) 2021 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import atexit
import argparse
import datetime
import hashlib
import http.server
import os
import re
import shutil
import signal
import socketserver
import subprocess
import sys
import time
import webbrowser
import threading
import random # 【补充缺失的 import】
import platform # 【补充缺失的 import】
# ----- Amalgamator: begin of python/perfetto/prebuilts/manifests/tracebox.py
TRACEBOX_MANIFEST = [{
'arch': 'mac-amd64',
'file_name': 'tracebox',
'file_size': 1646808,
'url': 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/mac-amd64/tracebox',
'sha256': '85b3060ed4d49e2c8d69dbb4d6ff26ab662f9b28c0032791674c90683dd33d39',
'platform': 'darwin',
'machine': ['x86_64']
}, {
'arch': 'mac-arm64',
'file_name': 'tracebox',
'file_size': 1508856,
'url': 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/mac-arm64/tracebox',
'sha256': 'ea2cce845daf0eba469ff356b3bcefc8e9a384084569271a470b58a9dcbf8def',
'platform': 'darwin',
'machine': ['arm64']
}, {
'arch': 'linux-amd64',
'file_name': 'tracebox',
'file_size': 2415168,
'url': 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/linux-amd64/tracebox',
'sha256': '5361676fb3c2490ae2136ab7a37dcd9e4ee5a2a6c0ba722facf3215a23a8c633',
'platform': 'linux',
'machine': ['x86_64']
}, {
'arch': 'linux-arm',
'file_name': 'tracebox',
'file_size': 1478024,
'url': 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/linux-arm/tracebox',
'sha256': '18db321576be555d8c9281df9fc03aa6b3b4358ae2424ffbd65fc120cd650b8b',
'platform': 'linux',
'machine': ['armv6l', 'armv7l', 'armv8l']
}, {
'arch': 'linux-arm64',
'file_name': 'tracebox',
'file_size': 2304384,
'url': 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/linux-arm64/tracebox',
'sha256': '70c9e2b63eb92a82db65916c346b09867bfedc0c4593974c019102f485c0dc9d',
'platform': 'linux',
'machine': ['aarch64']
}, {
'arch': 'android-arm',
'file_name': 'tracebox',
'file_size': 1354916,
'url': 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-arm/tracebox',
'sha256': '724a1cb4774bdf8a64beb37194f7394df5a052c36369ea52f64fe519fcb40117'
}, {
'arch': 'android-arm64',
'file_name': 'tracebox',
'file_size': 2142008,
'url': 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-arm64/tracebox',
'sha256': '7616bfc3be1269c3ac1eec5a1f868fb65c2830ed001b5fbcc3800c909c676848'
}, {
'arch': 'android-x86',
'file_name': 'tracebox',
'file_size': 2341884,
'url': 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-x86/tracebox',
'sha256': '29124bee9bf4e2e296b0c96071b8c9706b57d963cbf0359d6afd95a9049b2b82'
}, {
'arch': 'android-x64',
'file_name': 'tracebox',
'file_size': 2178416,
'url': 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-x64/tracebox',
'sha256': '826fffce1e138c1d5ac107492ee696c09ad83f9ae9aa647c810d71084f797509'
}]
# ----- Amalgamator: end of python/perfetto/prebuilts/manifests/tracebox.py
def download_or_get_cached(file_name, url, sha256):
dir = os.path.join(
os.path.expanduser('~'), '.local', 'share', 'perfetto', 'prebuilts')
os.makedirs(dir, exist_ok=True)
bin_path = os.path.join(dir, file_name)
sha256_path = os.path.join(dir, file_name + '.sha256')
needs_download = True
if os.path.exists(bin_path) and os.path.exists(sha256_path):
with open(sha256_path, 'rb') as f:
digest = f.read().decode()
if digest == sha256:
needs_download = False
if needs_download:
tmp_path = '%s.%d.tmp' % (bin_path, random.randint(0, 100000))
print('Downloading ' + url)
subprocess.check_call(['curl', '-f', '-L', '-#', '-o', tmp_path, url])
with open(tmp_path, 'rb') as fd:
actual_sha256 = hashlib.sha256(fd.read()).hexdigest()
if actual_sha256 != sha256:
raise Exception('Checksum mismatch for %s (actual: %s, expected: %s)' %
(url, actual_sha256, sha256))
os.chmod(tmp_path, 0o755)
os.replace(tmp_path, bin_path)
with open(tmp_path, 'w') as f:
f.write(sha256)
os.replace(tmp_path, sha256_path)
return bin_path
def get_perfetto_prebuilt(manifest, soft_fail=False, arch=None):
plat = sys.platform.lower()
machine = platform.machine().lower()
manifest_entry = None
for entry in manifest:
if arch:
if entry.get('arch') == arch:
manifest_entry = entry
break
continue
if entry.get('platform') == plat and machine in entry.get('machine', []):
manifest_entry = entry
break
if manifest_entry is None:
if soft_fail:
return None
raise Exception(
('No prebuilts available for %s-%s\n' % (plat, machine)) +
'See https://perfetto.dev/docs/contributing/build-instructions')
return download_or_get_cached(
file_name=manifest_entry['file_name'],
url=manifest_entry['url'],
sha256=manifest_entry['sha256'])
def run_perfetto_prebuilt(manifest):
bin_path = get_perfetto_prebuilt(manifest)
if sys.platform.lower() == 'win32':
sys.exit(subprocess.check_call([bin_path, *sys.argv[1:]]))
os.execv(bin_path, [bin_path] + sys.argv[1:])
def repo_root():
path = os.path.dirname(os.path.abspath(__file__))
last_dir = ''
while path and path != last_dir:
if os.path.exists(os.path.join(path, 'perfetto.rc')):
return path
last_dir = path
path = os.path.dirname(path)
return None
def repo_dir(rel_path):
return os.path.join(repo_root() or '', rel_path)
HERMETIC_ADB_PATH = repo_dir('buildtools/android_sdk/platform-tools/adb')
ABI_TO_ARCH = {
'armeabi-v7a': 'arm',
'arm64-v8a': 'arm64',
'x86': 'x86',
'x86_64': 'x64',
}
MAX_ADB_FAILURES = 15
devnull = open(os.devnull, 'rb')
adb_path = None
procs = []
class ANSI:
END = '\033[0m'
BOLD = '\033[1m'
RED = '\033[91m'
BLACK = '\033[30m'
BLUE = '\033[94m'
BG_YELLOW = '\033[43m'
BG_BLUE = '\033[44m'
GREEN = '\033[92m'
YELLOW = '\033[93m' # 【新增颜色】
# 【修改优化:结构化日志输出格式 [INFO] / [ERROR] 等】
def log_msg(msg, color=ANSI.END, prefix="[INFO]"):
"""Format logs with timestamp prefix."""
ts = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
# 结构化输出
print(f"{color}{prefix} [{ts}] {msg}{ANSI.END}")
class HttpHandler(http.server.SimpleHTTPRequestHandler):
def end_headers(self):
self.send_header('Access-Control-Allow-Origin', self.server.allow_origin)
self.send_header('Cache-Control', 'no-cache')
super().end_headers()
def do_GET(self):
if self.path != '/' + self.server.expected_fname:
self.send_error(404, "File not found")
return
self.server.fname_get_completed = True
super().do_GET()
def do_POST(self):
self.send_error(404, "File not found")
# 【新增这一段】屏蔽默认的 HTTP 请求日志输出
def log_message(self, format, *args):
pass
def select_device(serial_arg):
"""Handle multiple devices selection."""
proc = subprocess.Popen([adb_path, 'devices'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out, _ = proc.communicate()
lines = out.decode('utf-8').strip().split('\n')[1:]
devices = [line.split()[0] for line in lines if 'device' in line and not line.startswith('*')]
if serial_arg:
if serial_arg not in devices and devices:
log_msg(f'Specified device {serial_arg} not found in connected devices.', ANSI.BG_YELLOW, "[WARN]")
return serial_arg
if len(devices) == 0:
log_msg('No adb devices found. Please connect a device.', ANSI.RED, "[ERROR]")
sys.exit(1)
elif len(devices) == 1:
return devices[0]
else:
# 【优化易用性】默认选择第一个设备,不再阻塞脚本运行
log_msg(f'Multiple devices detected. Auto-selecting the first one: {devices[0]}', ANSI.YELLOW, "[WARN]")
return devices[0]
# 【新增优化:生成性能分析模板配置】
def generate_scene_config(args):
duration_ms = 10000
if args.time and args.time.endswith('s'):
duration_ms = int(args.time[:-1]) * 1000
buffer_kb = 32768
if args.buffer and args.buffer.endswith('mb'):
buffer_kb = int(args.buffer[:-2]) * 1024
# 定义模板
categories = []
if args.scene == 'jank':
categories = ["gfx", "input", "view", "wm", "am", "sm", "hal", "res", "dalvik", "rs", "bionic", "binder_driver", "binder_lock"]
elif args.scene == 'launch':
categories = ["am", "wm", "gfx", "view", "binder_driver", "sched", "freq", "idle", "res", "dalvik", "ss"]
elif args.scene == 'cpu':
categories = ["sched", "freq", "idle"]
# 生成动态 TextProto
config_text = f"""
buffers: {{ size_kb: {buffer_kb} fill_policy: RING_BUFFER }}
data_sources: {{
config {{
name: "linux.sys_stats"
sys_stats_config {{ stat_period_ms: 250 stat_counters: STAT_CPU_TIMES stat_counters: STAT_FORK_COUNT }}
}}
}}
data_sources: {{
config {{
name: "linux.ftrace"
ftrace_config {{
ftrace_events: "sched/sched_switch"
ftrace_events: "power/cpu_frequency"
ftrace_events: "power/cpu_idle"
ftrace_events: "sched/sched_wakeup"
ftrace_events: "sched/sched_wakeup_new"
ftrace_events: "sched/sched_waking"
"""
for cat in categories:
config_text += f' atrace_categories: "{cat}"\n'
if args.pkg:
config_text += f' atrace_apps: "{args.pkg}"\n'
config_text += f""" }}
}}
}}
duration_ms: {duration_ms}
"""
config_path = "auto_generated_scene.pbtx"
with open(config_path, "w") as f:
f.write(config_text)
return config_path
def setup_arguments():
atexit.register(kill_all_subprocs_on_exit)
default_out_dir_str = './'
default_out_dir = os.path.expanduser(default_out_dir_str)
examples = '\n'.join([
ANSI.BOLD + 'Examples' + ANSI.END, ' -t 10s -b 32mb sched gfx wm -a*',
' -t 5s sched/sched_switch raw_syscalls/sys_enter raw_syscalls/sys_exit',
' -c /path/to/full-textual-trace.config', '',
ANSI.BOLD + 'Long traces' + ANSI.END,
'If you want to record a hours long trace and stream it into a file ',
'you need to pass a full trace config and set write_into_file = true.',
'See https://perfetto.dev/docs/concepts/config#long-traces .'
])
parser = argparse.ArgumentParser(
epilog=examples, formatter_class=argparse.RawTextHelpFormatter)
help = 'Output file or directory (default: %s)' % default_out_dir_str
parser.add_argument('-o', '--out', default=default_out_dir, help=help)
help = 'Don\'t open or serve the trace'
parser.add_argument('-n', '--no-open', action='store_true', help=help)
help = 'Don\'t open in browser, but still serve trace (good for remote use)'
parser.add_argument('--no-open-browser', action='store_true', help=help)
help = 'The web address used to open trace files'
parser.add_argument('--origin', default='https://ui.perfetto.dev', help=help)
help = 'Force the use of the sideloaded binaries rather than system daemons'
parser.add_argument('--sideload', action='store_true', help=help)
help = ('Sideload the given binary rather than downloading it. ' +
'Implies --sideload')
parser.add_argument('--sideload-path', default=None, help=help)
help = 'Ignores any tracing guardrails which might be used'
parser.add_argument('--no-guardrails', action='store_true', help=help)
help = 'Don\'t run `adb root` run as user (only when sideloading)'
parser.add_argument('-u', '--user', action='store_true', help=help)
help = 'Specify the ADB device serial'
parser.add_argument('--serial', '-s', default=None, help=help)
# 【新增优化:自动化与场景扩展参数】
grp_auto = parser.add_argument_group('Automation Options')
grp_auto.add_argument('--scene', choices=['jank', 'launch', 'cpu', 'none'], default='none', help='预设性能分析场景(自动生成配置)')
grp_auto.add_argument('--pkg', default='', help='目标包名 (配合 --scene 使用)')
grp_auto.add_argument('--loop', type=int, default=1, help='循环抓取次数 (用于复现难复现的问题)')
grp = parser.add_argument_group(
'Short options: (only when not using -c/--config)')
help = 'Trace duration N[s,m,h] (default: 10s)' # 【优化:修改默认时长为 10s 提升易用性】
grp.add_argument('-t', '--time', default='10s', help=help)
help = 'Ring buffer size N[mb,gb] (default: 32mb)'
grp.add_argument('-b', '--buffer', default='32mb', help=help)
help = ('Android (atrace) app names. Can be specified multiple times.\n-a*' +
'for all apps (without space between a and * or bash will expand it)')
grp.add_argument(
'-a',
'--app',
metavar='com.myapp',
action='append',
default=[],
help=help)
help = 'sched, gfx, am, wm (see --list)'
grp.add_argument('events', metavar='Atrace events', nargs='*', help=help)
help = 'sched/sched_switch kmem/kmem (see --list-ftrace)'
grp.add_argument('_', metavar='Ftrace events', nargs='*', help=help)
help = 'Lists all the categories available'
grp.add_argument('--list', action='store_true', help=help)
help = 'Lists all the ftrace events available'
grp.add_argument('--list-ftrace', action='store_true', help=help)
section = ('Full trace config (only when not using short options)')
grp = parser.add_argument_group(section)
help = 'Can be generated with https://ui.perfetto.dev/#!/record'
grp.add_argument('-c', '--config', default=None, help=help)
help = 'Parse input from --config as binary proto (default: parse as text)'
grp.add_argument('--bin', action='store_true', help=help)
help = ('Pass the trace through the trace reporter API. Only works when '
'using the full trace config (-c) with the reporter package name '
"'android.perfetto.cts.reporter' and the reporter class name "
"'android.perfetto.cts.reporter.PerfettoReportService' with the "
'reporter installed on the device (see '
'tools/install_test_reporter_app.py).')
grp.add_argument('--reporter-api', action='store_true', help=help)
args = parser.parse_args()
args.sideload = args.sideload or args.sideload_path is not None
# 【新增优化:配置加载的优先级与容错机制】
if args.config is None:
# 优先级 1:如果指定了场景模板,使用动态生成的配置
if args.scene != 'none':
args.config = generate_scene_config(args)
args.bin = False # 强制为 text 模式
log_msg(f"已应用场景模板: {args.scene}, 包名: {args.pkg or '全局'}", ANSI.GREEN, "[INFO]")
else:
# 优先级 2:优先检测并使用当前目录下的 syshealth_google.pbtx
default_pbtx = os.path.join(os.getcwd(), 'syshealth_google.pbtx')
if os.path.exists(default_pbtx):
args.config = default_pbtx
log_msg(f"自动加载当前目录配置: syshealth_google.pbtx", ANSI.GREEN, "[INFO]")
else:
# 优先级 3 (容错兜底):既没传参,也没场景,当前目录也没默认文件,进入交互式提示
ts = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
log_msg("未检测到预设配置(--scene)或默认配置文件(syshealth_google.pbtx)。", ANSI.YELLOW, "[WARN]")
user_input = input(f"[{ts}] 请输入 config 文件的绝对/相对路径 (直接按回车将退出): ").strip()
if user_input:
args.config = user_input
else:
log_msg("未提供有效的配置文件,抓取取消。", ANSI.RED, "[ERROR]")
sys.exit(1)
# Setup adb_path first, then run smart device selection
find_adb()
args.serial = select_device(args.serial)
os.environ["ANDROID_SERIAL"] = args.serial
if args.list:
adb('shell', 'atrace', '--list_categories').wait()
sys.exit(0)
if args.list_ftrace:
adb('shell', 'cat /d/tracing/available_events | tr : /').wait()
sys.exit(0)
# 【容错增强:最后校验配置文件的有效性】
if not os.path.exists(args.config):
log_msg(f"致命错误:配置文件不存在 -> {args.config}", ANSI.RED, "[ERROR]")
sys.exit(1)
return args
class SignalException(Exception):
pass
def signal_handler(sig, frame):
raise SignalException('Received signal ' + str(sig))
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
def start_trace(args, print_log=True):
perfetto_cmd = 'perfetto'
device_dir = '/data/misc/perfetto-traces/'
# 【修改点 1】更改 remote_config_path 为官方推荐的具有 SELinux 读取权限的目录
remote_config_path = '/data/misc/perfetto-configs/perfetto.conf'
if print_log:
log_msg(f"设备信息:{args.serial}", ANSI.END, "[INFO]")
log_msg(f"包名:{args.pkg if hasattr(args, 'pkg') and args.pkg else '未指定'}", ANSI.END, "[INFO]")
# 【修改点 2】确保目标配置目录存在,防止某些精简系统下找不到文件夹
adb('shell', 'mkdir -p /data/misc/perfetto-configs').wait()
# Check existing traces
if print_log: log_msg("Checking for existing Perfetto traces...")
check_cmd = adb('shell', 'ls /data/misc/perfetto-traces/*.pftrace 2>/dev/null', stdout=subprocess.PIPE)
out, _ = check_cmd.communicate()
if out.strip():
if print_log: log_msg("Existing Perfetto trace found. Cleaning up...", ANSI.BG_YELLOW, "[WARN]")
adb('shell', 'rm -f /data/misc/perfetto-traces/*.pftrace').wait()
probe_cmd = 'getprop ro.build.version.sdk; getprop ro.product.cpu.abi; whoami'
probe = adb('shell', probe_cmd, stdout=subprocess.PIPE)
lines = probe.communicate()[0].decode().strip().split('\n')
lines = [x.strip() for x in lines]
if probe.returncode != 0:
log_msg('未检测到设备,或 ADB 异常', ANSI.RED, "[ERROR]")
sys.exit(1)
api_level = int(lines[0])
abi = lines[1]
arch = ABI_TO_ARCH.get(abi)
if arch is None:
log_msg('Unsupported ABI: ' + abi, ANSI.RED, "[ERROR]")
sys.exit(1)
shell_user = lines[2]
if api_level < 29 or args.sideload:
tracebox_bin = args.sideload_path
if tracebox_bin is None:
tracebox_bin = get_perfetto_prebuilt(
TRACEBOX_MANIFEST, arch='android-' + arch)
perfetto_cmd = '/data/local/tmp/tracebox'
exit_code = adb('push', '--sync', tracebox_bin, perfetto_cmd).wait()
exit_code |= adb('shell', 'chmod 755 ' + perfetto_cmd).wait()
if exit_code != 0:
log_msg('ADB push failed', ANSI.RED, "[ERROR]")
sys.exit(1)
device_dir = '/data/local/tmp/'
if shell_user != 'root' and not args.user:
adb('root').wait()
# Push Config
if print_log: log_msg(f"开始下发配置到 {remote_config_path}...", ANSI.END, "[INFO]")
push_cmd = adb('push', args.config, remote_config_path, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
push_out, _ = push_cmd.communicate()
# 【修改优化:文件命名改为 设备号_时间.perfetto-trace,格式更清爽】
tstamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
# 使用 args.serial 获取设备号
fname = f'{args.serial}_{tstamp}.perfetto-trace'
device_file = device_dir + fname
# Construct Perfetto command using the pushed config
cmd = [perfetto_cmd, '--background', '-c', remote_config_path]
if not args.bin:
cmd.append('--txt')
if args.no_guardrails:
cmd.append('--no-guardrails')
if args.reporter_api:
adb('shell', 'rm /sdcard/Android/data/android.perfetto.cts.reporter/files/*').wait()
cmd.append('--upload')
else:
cmd.extend(['-o', device_file])
if args.out.endswith('/') or os.path.isdir(args.out):
host_dir = args.out
host_file = os.path.join(args.out, fname)
else:
host_file = args.out
host_dir = os.path.dirname(host_file)
if host_dir == '':
host_dir = '.'
host_file = './' + host_file
if not os.path.exists(host_dir):
shutil.os.makedirs(host_dir)
if print_log: log_msg("开始抓取 trace...", ANSI.GREEN, "[INFO]")
# Run Perfetto
proc = adb('shell', *cmd, stdout=subprocess.PIPE)
proc_out = proc.communicate()[0].decode().strip()
match = re.search(r'^(\d+)$', proc_out, re.M)
if match is None:
log_msg('Failed to read the pid from perfetto --background', ANSI.RED, "[ERROR]")
print(proc_out)
sys.exit(1)
bg_pid = match.group(1)
exit_code = proc.wait()
if exit_code != 0:
log_msg('Perfetto 权限不足或调用失败', ANSI.RED, "[ERROR]")
sys.exit(1)
# Interactive Live Timer Thread setup
stop_event = threading.Event()
def wait_for_stop():
try:
input() # Wait for user to press Enter
except EOFError:
pass
stop_event.set()
threading.Thread(target=wait_for_stop, daemon=True).start()
start_time = time.time()
adb_failure_count = 0
ctrl_c_count = 0
while not stop_event.is_set():
try:
poll = adb('shell', f'test -d /proc/{bg_pid} && echo RUN || echo TERM', stdout=subprocess.PIPE)
poll_res = poll.communicate()[0].decode().strip()
if poll_res == 'TERM':
break
if poll_res == 'RUN':
if adb_failure_count > 0:
adb_failure_count = 0
print()
log_msg('ADB connection re-established', ANSI.BLUE, "[INFO]")
duration = int(time.time() - start_time)
# 将时间部分加上 ANSI.GREEN 和 ANSI.END
sys.stdout.write(f"\r{ANSI.BLUE}[INFO]{ANSI.END} Tracing duration: {ANSI.GREEN}{duration}s{ANSI.END} | Press Enter to stop... ")
sys.stdout.flush()
# Wait a second before updating again, break early if stopped
if stop_event.wait(1.0):
break
except (KeyboardInterrupt, SignalException):
sig = 'TERM' if ctrl_c_count == 0 else 'KILL'
ctrl_c_count += 1
print()
log_msg(f"手动终止 (SIG{sig})", ANSI.BLACK + ANSI.BG_YELLOW, "[WARN]")
adb('shell', f'kill -{sig} {bg_pid}').wait()
break
except Exception:
adb_failure_count += 1
if adb_failure_count >= MAX_ADB_FAILURES:
print()
log_msg('ADB 中断,设备掉线', ANSI.RED, "[ERROR]")
sys.exit(1)
time.sleep(2)
print() # Clear line after \r
# Terminate manually if user pressed Enter
if stop_event.is_set():
adb('shell', f'kill -TERM {bg_pid} >/dev/null 2>&1').wait()
if args.reporter_api:
if print_log:
log_msg('Waiting a few seconds to allow reporter to copy trace')
time.sleep(5)
ret = adb('shell', 'cp /sdcard/Android/data/android.perfetto.cts.reporter/files/* ' + device_file).wait()
if ret != 0:
log_msg('Failed to extract reporter trace', ANSI.RED, "[ERROR]")
sys.exit(1)
if print_log:
log_msg(f"正在导出到本地: {host_file}", ANSI.BOLD, "[INFO]")
adb('pull', device_file, host_file).wait()
adb('shell', 'rm -f ' + device_file).wait()
adb('shell', 'rm -f ' + remote_config_path).wait() # Clean up pushed config
# 【优化:成功提示】
log_msg(f"trace 已保存: {host_file}", ANSI.GREEN, "[SUCCESS]")
if not args.no_open and print_log:
log_msg(f"Opening the trace ({host_file}) in the browser", ANSI.END, "[INFO]")
open_browser = not args.no_open_browser
open_trace_in_browser(host_file, open_browser, args.origin)
return host_file
# 【修改优化:主函数增加循环逻辑】
def main():
args = setup_arguments()
# 支持自动化循环抓取
for i in range(args.loop):
if args.loop > 1:
log_msg(f"=== 开始第 {i+1}/{args.loop} 次循环抓取 ===", ANSI.BOLD, "[INFO]")
# 如果有循环,只在最后一次自动打开浏览器
print_log_and_open = (i == args.loop - 1)
if not print_log_and_open:
args.no_open = True
start_trace(args, print_log=True)
if args.loop > 1 and i < args.loop - 1:
time.sleep(2) # 循环之间的缓冲时间
# 清理自动生成的临时配置
if args.scene != 'none' and os.path.exists("auto_generated_scene.pbtx"):
os.remove("auto_generated_scene.pbtx")
def find_adb():
global adb_path
for path in ['adb', HERMETIC_ADB_PATH]:
try:
subprocess.call([path, '--version'], stdout=devnull, stderr=devnull)
adb_path = path
break
except OSError:
continue
if adb_path is None:
sdk_url = 'https://developer.android.com/studio/releases/platform-tools'
log_msg('Could not find a suitable adb binary in the PATH. ', ANSI.RED, "[ERROR]")
log_msg('You can download adb from %s' % sdk_url, ANSI.RED, "[ERROR]")
sys.exit(1)
# 【新增优化:重写 TCPServer 错误处理,屏蔽 Ctrl+C 导致的堆栈打印】
class QuietTCPServer(socketserver.TCPServer):
def handle_error(self, request, client_address):
# 如果是用户手动中断 (SignalException),则静默处理
if sys.exc_info()[0] is SignalException:
pass
else:
super().handle_error(request, client_address)
def open_trace_in_browser(path, open_browser, origin):
PORT = 9001
path = os.path.abspath(path)
os.chdir(os.path.dirname(path))
fname = os.path.basename(path)
QuietTCPServer.allow_reuse_address = True
try:
with QuietTCPServer(('127.0.0.1', PORT), HttpHandler) as httpd:
address = f'{origin}/#!/?url=http://127.0.0.1:{PORT}/{fname}&referrer=record_android_trace'
if open_browser:
webbrowser.open_new_tab(address)
else:
print(f'Open URL in browser: {address}')
httpd.expected_fname = fname
httpd.fname_get_completed = None
httpd.allow_origin = origin
# 捕获并等待浏览器请求
while httpd.fname_get_completed is None:
httpd.handle_request()
except SignalException:
# 捕获 Ctrl+C 并平滑退出本地服务
print("\n")
log_msg("本地 UI 代理服务已安全关闭。", ANSI.BLUE, "[INFO]")
def adb(*args, stdin=devnull, stdout=None, stderr=None):
cmd = [adb_path, *args]
setpgrp = None
if os.name != 'nt':
setpgrp = lambda: os.setpgrp()
proc = subprocess.Popen(
cmd, stdin=stdin, stdout=stdout, stderr=stderr, preexec_fn=setpgrp)
procs.append(proc)
return proc
def kill_all_subprocs_on_exit():
for p in [p for p in procs if p.poll() is None]:
p.kill()
if __name__ == '__main__':
sys.exit(main())