1#!/usr/bin/python3 2 3# Copyright (C) 2019 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 17# 18# This is an ADB proxy for Winscope. 19# 20# Requirements: python3.10 and ADB installed and in system PATH. 21# 22# Usage: 23# run: python3 winscope_proxy.py 24# 25 26import argparse 27import base64 28import gzip 29import json 30import logging 31import os 32import re 33import secrets 34import signal 35import subprocess 36import sys 37import threading 38import time 39from abc import abstractmethod 40from enum import Enum 41from http import HTTPStatus 42from http.server import HTTPServer, BaseHTTPRequestHandler 43from logging import DEBUG, INFO 44from tempfile import NamedTemporaryFile 45from typing import Callable 46 47# GLOBALS # 48 49log = None 50secret_token = None 51 52# CONFIG # 53 54def create_argument_parser() -> argparse.ArgumentParser: 55 parser = argparse.ArgumentParser(description='Proxy for go/winscope', prog='winscope_proxy') 56 57 parser.add_argument('--info', '-i', dest='loglevel', action='store_const', const=INFO) 58 parser.add_argument('--port', '-p', default=5544, action='store') 59 60 parser.set_defaults(loglevel=DEBUG) 61 62 return parser 63 64# Keep in sync with ProxyConnection#VERSION in Winscope 65VERSION = '4.0.8' 66 67PERFETTO_TRACE_CONFIG_FILE = '/data/misc/perfetto-configs/winscope-proxy-trace.conf' 68PERFETTO_DUMP_CONFIG_FILE = '/data/misc/perfetto-configs/winscope-proxy-dump.conf' 69PERFETTO_TRACE_FILE = '/data/misc/perfetto-traces/winscope-proxy-trace.perfetto-trace' 70PERFETTO_DUMP_FILE = '/data/misc/perfetto-traces/winscope-proxy-dump.perfetto-trace' 71PERFETTO_UNIQUE_SESSION_NAME = 'winscope proxy perfetto tracing' 72PERFETTO_TRACING_SESSIONS_QUERY_START = """TRACING SESSIONS: 73 74ID UID STATE BUF (#) KB DUR (s) #DS STARTED NAME 75=== === ===== ========== ======= === ======= ====\n""" 76PERFETTO_TRACING_SESSIONS_QUERY_END = """\nNOTE: Some tracing sessions are not reported in the list above.""" 77 78WINSCOPE_VERSION_HEADER = "Winscope-Proxy-Version" 79WINSCOPE_TOKEN_HEADER = "Winscope-Token" 80 81# Location to save the proxy security token 82WINSCOPE_TOKEN_LOCATION = os.path.expanduser('~/.config/winscope/.token') 83 84# Winscope traces extensions 85WINSCOPE_EXT = ".winscope" 86WINSCOPE_EXT_LEGACY = ".pb" 87WINSCOPE_EXTS = [WINSCOPE_EXT, WINSCOPE_EXT_LEGACY] 88 89# Winscope traces directories 90WINSCOPE_DIR = "/data/misc/wmtrace/" 91WINSCOPE_BACKUP_DIR = "/data/local/tmp/last_winscope_tracing_session/" 92 93# Tracing handlers 94SIGNAL_HANDLER_LOG = "/data/local/tmp/winscope_signal_handler.log" 95WINSCOPE_STATUS = "/data/local/tmp/winscope_status" 96 97# Max interval between the client keep-alive requests in seconds 98KEEP_ALIVE_INTERVAL_S = 5 99 100# Perfetto's default timeout for getting an ACK from producer processes is 5s 101# We need to be sure that the timeout is longer than that with a good margin. 102COMMAND_TIMEOUT_S = 15 103 104class File: 105 def __init__(self, file, filetype) -> None: 106 self.file = file 107 self.type = filetype 108 109 def get_filepaths(self, device_id) -> list[str]: 110 return [self.file] 111 112 def get_filetype(self) -> str: 113 return self.type 114 115 116class FileMatcher: 117 def __init__(self, path, matcher, filetype) -> None: 118 self.path = path 119 self.matcher = matcher 120 self.type = filetype 121 122 def get_filepaths(self, device_id) -> list[str]: 123 if len(self.matcher) > 0: 124 matchingFiles = call_adb( 125 f"shell su root find {self.path} -name {self.matcher}", device_id) 126 else: 127 matchingFiles = call_adb( 128 f"shell su root find {self.path}", device_id) 129 130 files = matchingFiles.split('\n')[:-1] 131 log.debug("Found files %s", files) 132 return files 133 134 def get_filetype(self) -> str: 135 return self.type 136 137 138class WinscopeFileMatcher(FileMatcher): 139 def __init__(self, path, matcher, filetype) -> None: 140 self.path = path 141 self.internal_matchers = list(map(lambda ext: FileMatcher(path, f'{matcher}{ext}', filetype), 142 WINSCOPE_EXTS)) 143 self.type = filetype 144 145 def get_filepaths(self, device_id) -> list[str]: 146 for matcher in self.internal_matchers: 147 files = matcher.get_filepaths(device_id) 148 if len(files) > 0: 149 return files 150 log.debug("No files found") 151 return [] 152 153 154def get_shell_args(device_id: str, type: str) -> list[str]: 155 shell = ['adb', '-s', device_id, 'shell'] 156 log.debug(f"Starting {type} shell {' '.join(shell)}") 157 return shell 158 159def is_perfetto_data_source_available(name: str, query_result: str) -> bool: 160 return name in query_result 161 162def is_any_perfetto_data_source_available(query_result: str) -> bool: 163 return is_perfetto_data_source_available('android.inputmethod', query_result) or \ 164 is_perfetto_data_source_available('android.protolog', query_result) or \ 165 is_perfetto_data_source_available('android.surfaceflinger.layers', query_result) or \ 166 is_perfetto_data_source_available('android.surfaceflinger.transactions', query_result) or \ 167 is_perfetto_data_source_available('com.android.wm.shell.transition', query_result) or \ 168 is_perfetto_data_source_available('android.viewcapture', query_result) or \ 169 is_perfetto_data_source_available('android.windowmanager', query_result) or \ 170 is_perfetto_data_source_available('android.input.inputevent', query_result) 171 172 173class TraceConfig: 174 def __init__(self, is_perfetto: bool) -> None: 175 self.is_perfetto = is_perfetto 176 177 @abstractmethod 178 def add(self, config: str, value: str | list[str] | None) -> None: 179 pass 180 181 @abstractmethod 182 def is_valid(self, config: str) -> bool: 183 pass 184 185 @abstractmethod 186 def execute_command(self, server, device_id): 187 pass 188 189 def get_trace_identifiers(self)-> list[str]: 190 return [""] 191 192 def get_optional_start_args(self, identifier)-> str: 193 return "" 194 195 def execute_optional_config_command(self, server, device_id, shell, command, config_key, config_value): 196 process = subprocess.Popen(shell, stdout=subprocess.PIPE, stderr=subprocess.PIPE, 197 stdin=subprocess.PIPE, start_new_session=True) 198 out, err = process.communicate(command.encode('utf-8')) 199 if process.returncode != 0: 200 raise AdbError( 201 f"Error executing command:\n {command}\n\n### OUTPUT ###{out.decode('utf-8')}\n{err.decode('utf-8')}") 202 log.debug(f"Optional trace config changed on device {device_id} {config_key}:{config_value}") 203 204 def execute_perfetto_config_command(self, server, shell, command, trace_name): 205 process = subprocess.Popen(shell, stdout=subprocess.PIPE, stderr=subprocess.PIPE, 206 stdin=subprocess.PIPE, start_new_session=True) 207 out, err = process.communicate(command.encode('utf-8')) 208 if process.returncode != 0: 209 raise AdbError( 210 f"Error executing command:\n {command}\n\n### OUTPUT ###{out.decode('utf-8')}\n{err.decode('utf-8')}") 211 log.info(f'{trace_name} (perfetto) configured to start along the other perfetto traces.') 212 213 214class SurfaceFlingerTraceConfig(TraceConfig): 215 """Handles optional configuration for Surface Flinger traces. 216 """ 217 LEGACY_FLAGS_MAP = { 218 "input": 1 << 1, 219 "composition": 1 << 2, 220 "metadata": 1 << 3, 221 "hwc": 1 << 4, 222 "tracebuffers": 1 << 5, 223 "virtualdisplays": 1 << 6 224 } 225 226 PERFETTO_FLAGS_MAP = { 227 "input": "TRACE_FLAG_INPUT", 228 "composition": "TRACE_FLAG_COMPOSITION", 229 "metadata": "TRACE_FLAG_EXTRA", 230 "hwc": "TRACE_FLAG_HWC", 231 "tracebuffers": "TRACE_FLAG_BUFFERS", 232 "virtualdisplays": "TRACE_FLAG_VIRTUAL_DISPLAYS", 233 } 234 235 def __init__(self, is_perfetto: bool) -> None: 236 super().__init__(is_perfetto) 237 self.flags = [] 238 self.perfetto_flags = [] 239 240 # defaults set for all configs 241 self.selected_configs = { 242 "sfbuffersize": "16000" 243 } 244 245 def add(self, config: str, value: str | None) -> None: 246 if config in SurfaceFlingerTraceConfig.LEGACY_FLAGS_MAP: 247 self.flags.append(config) 248 elif config in self.selected_configs: 249 self.selected_configs[config] = value 250 251 def is_valid(self, config: str) -> bool: 252 return config in SurfaceFlingerTraceConfig.LEGACY_FLAGS_MAP or config in self.selected_configs 253 254 def execute_command(self, server, device_id): 255 shell = get_shell_args(device_id, "sf config") 256 257 if self.is_perfetto: 258 self.execute_perfetto_config_command(server, shell, self._perfetto_config_command(), "SurfaceFlinger") 259 else: 260 self.execute_optional_config_command(server, device_id, shell, self._legacy_flags_command(), "sf flags", self.flags) 261 self.execute_optional_config_command(server, device_id, shell, self._legacy_buffer_size_command(), "sf buffer size", self.selected_configs["sfbuffersize"]) 262 263 def _perfetto_config_command(self) -> str: 264 flags = "\n".join([f"""trace_flags: {SurfaceFlingerTraceConfig.PERFETTO_FLAGS_MAP[flag]}""" for flag in self.flags]) 265 266 return f""" 267cat << EOF >> {PERFETTO_TRACE_CONFIG_FILE} 268data_sources: {{ 269 config {{ 270 name: "android.surfaceflinger.layers" 271 surfaceflinger_layers_config: {{ 272 mode: MODE_ACTIVE 273 {flags} 274 }} 275 }} 276}} 277EOF 278""" 279 280 def _legacy_buffer_size_command(self) -> str: 281 return f'su root service call SurfaceFlinger 1029 i32 {self.selected_configs["sfbuffersize"]}' 282 283 def _legacy_flags_command(self) -> str: 284 flags = 0 285 for flag in self.flags: 286 flags |= SurfaceFlingerTraceConfig.LEGACY_FLAGS_MAP[flag] 287 288 return f"su root service call SurfaceFlinger 1033 i32 {flags}" 289 290 291class WindowManagerTraceConfig(TraceConfig): 292 PERFETTO_LOG_LEVEL_MAP = { 293 "verbose": "LOG_LEVEL_VERBOSE", 294 "debug": "LOG_LEVEL_DEBUG", 295 "critical": "LOG_LEVEL_CRITICAL", 296 } 297 298 PERFETTO_LOG_FREQUENCY_MAP = { 299 "frame": "LOG_FREQUENCY_FRAME", 300 "transaction": "LOG_FREQUENCY_TRANSACTION", 301 } 302 303 """Handles optional selected configuration for Window Manager traces. 304 """ 305 306 def __init__(self, is_perfetto: bool) -> None: 307 super().__init__(is_perfetto) 308 # defaults set for all configs 309 self.selected_configs = { 310 "wmbuffersize": "16000", 311 "tracinglevel": "debug", 312 "tracingtype": "frame", 313 } 314 315 def add(self, config_type: str, config_value: str | None) -> None: 316 self.selected_configs[config_type] = config_value 317 318 def is_valid(self, config_type: str) -> bool: 319 return config_type in self.selected_configs 320 321 def execute_command(self, server, device_id): 322 shell = get_shell_args(device_id, "wm config") 323 324 if self.is_perfetto: 325 self.execute_perfetto_config_command(server, shell, self._perfetto_config_command(), "WM") 326 else: 327 self.execute_optional_config_command(server, device_id, shell, self._legacy_tracing_type_command(), "tracing type", self.selected_configs["tracingtype"]) 328 self.execute_optional_config_command(server, device_id, shell, self._legacy_tracing_level_command(), "tracing level", self.selected_configs["tracinglevel"]) 329 # /!\ buffer size must be configured last 330 # otherwise the other configurations might override it 331 self.execute_optional_config_command(server, device_id, shell, self._legacy_buffer_size_command(), "wm buffer size", self.selected_configs["wmbuffersize"]) 332 333 def _perfetto_config_command(self) -> str: 334 log_level = WindowManagerTraceConfig.PERFETTO_LOG_LEVEL_MAP[self.selected_configs["tracinglevel"]] 335 log_frequency = WindowManagerTraceConfig.PERFETTO_LOG_FREQUENCY_MAP[self.selected_configs["tracingtype"]] 336 return f""" 337cat << EOF >> {PERFETTO_TRACE_CONFIG_FILE} 338data_sources: {{ 339 config {{ 340 name: "android.windowmanager" 341 windowmanager_config: {{ 342 log_level: {log_level} 343 log_frequency: {log_frequency} 344 }} 345 }} 346}} 347EOF 348""" 349 350 def _legacy_tracing_type_command(self) -> str: 351 return f'su root cmd window tracing {self.selected_configs["tracingtype"]}' 352 353 def _legacy_tracing_level_command(self) -> str: 354 return f'su root cmd window tracing level {self.selected_configs["tracinglevel"]}' 355 356 def _legacy_buffer_size_command(self) -> str: 357 return f'su root cmd window tracing size {self.selected_configs["wmbuffersize"]}' 358 359 360class ViewCaptureTraceConfig(TraceConfig): 361 """Handles perfetto config for View Capture traces.""" 362 363 COMMAND = f""" 364cat << EOF >> {PERFETTO_TRACE_CONFIG_FILE} 365data_sources: {{ 366 config {{ 367 name: "android.viewcapture" 368 }} 369}} 370EOF 371 """ 372 373 def execute_command(self, server, device_id): 374 if (self.is_perfetto): 375 shell = get_shell_args(device_id, "perfetto config view capture") 376 self.execute_perfetto_config_command(server, shell, ViewCaptureTraceConfig.COMMAND, "View Capture") 377 378class TransactionsConfig(TraceConfig): 379 """Handles perfetto config for Transactions traces.""" 380 381 COMMAND = f""" 382cat << EOF >> {PERFETTO_TRACE_CONFIG_FILE} 383data_sources: {{ 384 config {{ 385 name: "android.surfaceflinger.transactions" 386 surfaceflinger_transactions_config: {{ 387 mode: MODE_ACTIVE 388 }} 389 }} 390}} 391EOF 392 """ 393 394 def execute_command(self, server, device_id): 395 if (self.is_perfetto): 396 shell = get_shell_args(device_id, "perfetto config transactions") 397 self.execute_perfetto_config_command(server, shell, TransactionsConfig.COMMAND, "SF transactions") 398 399 400class ProtoLogConfig(TraceConfig): 401 """Handles perfetto config for ProtoLog traces.""" 402 403 COMMAND = f""" 404cat << EOF >> {PERFETTO_TRACE_CONFIG_FILE} 405data_sources: {{ 406 config {{ 407 name: "android.protolog" 408 protolog_config: {{ 409 tracing_mode: ENABLE_ALL 410 }} 411 }} 412}} 413EOF 414 """ 415 416 def execute_command(self, server, device_id): 417 if (self.is_perfetto): 418 shell = get_shell_args(device_id, "perfetto config protolog") 419 self.execute_perfetto_config_command(server, shell, ProtoLogConfig.COMMAND, "ProtoLog") 420 421 422class ImeConfig(TraceConfig): 423 """Handles perfetto config for IME traces.""" 424 425 COMMAND = f""" 426cat << EOF >> {PERFETTO_TRACE_CONFIG_FILE} 427data_sources: {{ 428 config {{ 429 name: "android.inputmethod" 430 }} 431}} 432EOF 433 """ 434 435 def execute_command(self, server, device_id): 436 if (self.is_perfetto): 437 shell = get_shell_args(device_id, "perfetto config ime") 438 self.execute_perfetto_config_command(server, shell, ImeConfig.COMMAND, "IME tracing") 439 440 441class TransitionTracesConfig(TraceConfig): 442 """Handles perfetto config for Transition traces.""" 443 444 COMMAND = f""" 445cat << EOF >> {PERFETTO_TRACE_CONFIG_FILE} 446data_sources: {{ 447 config {{ 448 name: "com.android.wm.shell.transition" 449 }} 450}} 451EOF 452 """ 453 454 def execute_command(self, server, device_id): 455 if (self.is_perfetto): 456 shell = get_shell_args(device_id, "perfetto config transitions") 457 self.execute_perfetto_config_command(server, shell, TransitionTracesConfig.COMMAND, "Transitions") 458 459 460class InputConfig(TraceConfig): 461 """Handles perfetto config for Input traces.""" 462 463 COMMAND = f""" 464cat << EOF >> {PERFETTO_TRACE_CONFIG_FILE} 465data_sources: {{ 466 config {{ 467 name: "android.input.inputevent" 468 android_input_event_config {{ 469 mode: TRACE_MODE_TRACE_ALL 470 }} 471 }} 472}} 473EOF 474 """ 475 476 def execute_command(self, server, device_id): 477 if (self.is_perfetto): 478 shell = get_shell_args(device_id, "perfetto config input") 479 self.execute_perfetto_config_command(server, shell, InputConfig.COMMAND, "Input trace") 480 481 482class MediaBasedConfig(TraceConfig): 483 """Creates trace identifiers for Screen Recording traces and Screenshots.""" 484 trace_identifiers = ["active"] 485 486 def get_trace_identifiers(self): 487 return self.trace_identifiers 488 489 def is_valid(self, config_type: str) -> bool: 490 return config_type == "displays" 491 492 def add(self, config_type: str, config_value: str | list[str] | None): 493 if config_type != "displays": 494 return 495 if config_value and len(config_value) > 0: 496 if type(config_value) == str: 497 self.trace_identifiers = [config_value.split(" ")[0]] 498 else: 499 self.trace_identifiers = list(map(lambda d: d.split(" ")[0], config_value)) 500 501 def execute_command(self, server, device_id): 502 pass 503 504class ScreenRecordingConfig(MediaBasedConfig): 505 """Creates display args for Screen Recording traces.""" 506 def get_optional_start_args(self, identifier): 507 if identifier == "active": 508 return "" 509 return f"--display-id {identifier}" 510 511class ScreenshotConfig(MediaBasedConfig): 512 """Creates display args for Screenshots.""" 513 def get_optional_start_args(self, identifier): 514 if identifier == "active": 515 return "" 516 return f"-d {identifier}" 517 518 519class SurfaceFlingerDumpConfig(TraceConfig): 520 """Handles perfetto config for SurfaceFlinger dumps.""" 521 def __init__(self, is_perfetto: bool) -> None: 522 super().__init__(is_perfetto) 523 524 def add(self, config: str, value: str | None) -> None: 525 pass 526 527 def is_valid(self, config: str) -> bool: 528 return False 529 530 def execute_command(self, server, device_id): 531 shell = get_shell_args(device_id, "sf config") 532 533 if self.is_perfetto: 534 self.execute_perfetto_config_command(server, shell, self._perfetto_config_command(), "SurfaceFlinger") 535 536 def _perfetto_config_command(self) -> str: 537 return f""" 538cat << EOF >> {PERFETTO_DUMP_CONFIG_FILE} 539data_sources: {{ 540 config {{ 541 name: "android.surfaceflinger.layers" 542 surfaceflinger_layers_config: {{ 543 mode: MODE_DUMP 544 trace_flags: TRACE_FLAG_INPUT 545 trace_flags: TRACE_FLAG_COMPOSITION 546 trace_flags: TRACE_FLAG_HWC 547 trace_flags: TRACE_FLAG_BUFFERS 548 trace_flags: TRACE_FLAG_VIRTUAL_DISPLAYS 549 }} 550 }} 551}} 552EOF 553""" 554 555 def _legacy_buffer_size_command(self) -> str: 556 return f'su root service call SurfaceFlinger 1029 i32 {self.selected_configs["sfbuffersize"]}' 557 558 def _legacy_flags_command(self) -> str: 559 flags = 0 560 for flag in self.flags: 561 flags |= SurfaceFlingerTraceConfig.LEGACY_FLAGS_MAP[flag] 562 563 return f"su root service call SurfaceFlinger 1033 i32 {flags}" 564 565 566class TraceTarget: 567 """Defines a single parameter to trace. 568 569 Attributes: 570 trace_name: used as a key to access config and log statements during Start/End endpoints. 571 files: the matchers used to identify the paths on the device the trace results are saved to. 572 is_perfetto_available: callback to determine if perfetto tracing is available for target. 573 trace_start: command to start the trace from adb shell, must not block. 574 trace_stop: command to stop the trace, should block until the trace is stopped. 575 get_trace_config: getter for optional setup to execute pre-tracing adb commands and define start command arguments. 576 """ 577 578 def __init__( 579 self, 580 trace_name: str, 581 files: list[File | FileMatcher], 582 is_perfetto_available: Callable[[str], bool], 583 trace_start: str, 584 trace_stop: str, 585 get_trace_config: Callable[[bool], TraceConfig] | None = None 586 ) -> None: 587 self.trace_name = trace_name 588 if type(files) is not list: 589 files = [files] 590 self.files = files 591 self.is_perfetto_available = is_perfetto_available 592 self.trace_start = trace_start 593 self.trace_stop = trace_stop 594 self.get_trace_config = get_trace_config 595 self.status_filename = WINSCOPE_STATUS + "_" + trace_name 596 597 598# Order of files matters as they will be expected in that order and decoded in that order 599TRACE_TARGETS = { 600 "view_capture_trace": TraceTarget( 601 "view_capture_trace", 602 File('/data/misc/wmtrace/view_capture_trace.zip', "view_capture_trace.zip"), 603 lambda res: is_perfetto_data_source_available("android.viewcapture", res), 604 """ 605su root settings put global view_capture_enabled 1 606echo 'ViewCapture tracing (legacy) started.' 607 """, 608 """ 609su root sh -c 'cmd launcherapps dump-view-hierarchies >/data/misc/wmtrace/view_capture_trace.zip' 610su root settings put global view_capture_enabled 0 611echo 'ViewCapture tracing (legacy) stopped.' 612 """, 613 lambda is_perfetto: ViewCaptureTraceConfig(is_perfetto) 614 ), 615 "window_trace": TraceTarget( 616 "window_trace", 617 WinscopeFileMatcher(WINSCOPE_DIR, "wm_trace", "window_trace"), 618 lambda res: is_perfetto_data_source_available('android.windowmanager', res), 619 """ 620su root cmd window tracing start 621echo 'WM trace (legacy) started.' 622 """, 623 """ 624su root cmd window tracing stop >/dev/null 2>&1 625echo 'WM trace (legacy) stopped.' 626 """, 627 lambda is_perfetto: WindowManagerTraceConfig(is_perfetto) 628 ), 629 "layers_trace": TraceTarget( 630 "layers_trace", 631 WinscopeFileMatcher(WINSCOPE_DIR, "layers_trace", "layers_trace"), 632 lambda res: is_perfetto_data_source_available('android.surfaceflinger.layers', res), 633 """ 634su root service call SurfaceFlinger 1025 i32 1 635echo 'SF layers trace (legacy) started.' 636 """, 637 """ 638su root service call SurfaceFlinger 1025 i32 0 >/dev/null 2>&1 639echo 'SF layers trace (legacy) stopped.' 640 """, 641 lambda is_perfetto: SurfaceFlingerTraceConfig(is_perfetto) 642 ), 643 "screen_recording": TraceTarget( 644 "screen_recording", 645 File( 646 '/data/local/tmp/screen_{trace_identifier}.mp4', 647 "screen_recording_{trace_identifier}"), 648 lambda res: False, 649 ''' 650 settings put system show_touches 1 && \ 651 settings put system pointer_location 1 && \ 652 screenrecord --bugreport --bit-rate 8M {options} /data/local/tmp/screen_{trace_identifier}.mp4 & \ 653 echo "ScreenRecorder started." 654 ''', 655 '''settings put system pointer_location 0 && \ 656 settings put system show_touches 0 && \ 657 pkill -l SIGINT screenrecord >/dev/null 2>&1 658 '''.strip(), 659 lambda is_perfetto: ScreenRecordingConfig(is_perfetto) 660 ), 661 "transactions": TraceTarget( 662 "transactions", 663 WinscopeFileMatcher(WINSCOPE_DIR, "transactions_trace", "transactions"), 664 lambda res: is_perfetto_data_source_available('android.surfaceflinger.transactions', res), 665 """ 666su root service call SurfaceFlinger 1041 i32 1 667echo 'SF transactions trace (legacy) started.' 668 """, 669 "su root service call SurfaceFlinger 1041 i32 0 >/dev/null 2>&1", 670 lambda is_perfetto: TransactionsConfig(is_perfetto) 671 ), 672 "transactions_legacy": TraceTarget( 673 "transactions_legacy", 674 [ 675 WinscopeFileMatcher(WINSCOPE_DIR, "transaction_trace", "transactions_legacy"), 676 FileMatcher(WINSCOPE_DIR, f'transaction_merges_*', "transaction_merges"), 677 ], 678 lambda res: False, 679 'su root service call SurfaceFlinger 1020 i32 1\necho "SF transactions recording started."', 680 'su root service call SurfaceFlinger 1020 i32 0 >/dev/null 2>&1', 681 ), 682 "proto_log": TraceTarget( 683 "proto_log", 684 WinscopeFileMatcher(WINSCOPE_DIR, "wm_log", "proto_log"), 685 lambda res: is_perfetto_data_source_available('android.protolog', res), 686 """ 687su root cmd window logging start 688echo "ProtoLog (legacy) started." 689 """, 690 """ 691su root cmd window logging stop >/dev/null 2>&1 692echo "ProtoLog (legacy) stopped." 693 """, 694 lambda is_perfetto: ProtoLogConfig(is_perfetto) 695 ), 696 "ime": TraceTarget( 697 "ime", 698 [WinscopeFileMatcher(WINSCOPE_DIR, "ime_trace_clients", "ime_trace_clients"), 699 WinscopeFileMatcher(WINSCOPE_DIR, "ime_trace_service", "ime_trace_service"), 700 WinscopeFileMatcher(WINSCOPE_DIR, "ime_trace_managerservice", "ime_trace_managerservice")], 701 lambda res: is_perfetto_data_source_available('android.inputmethod', res), 702 """ 703su root ime tracing start 704echo "IME tracing (legacy) started." 705 """, 706 """ 707su root ime tracing stop >/dev/null 2>&1 708echo "IME tracing (legacy) stopped." 709 """, 710 lambda is_perfetto: ImeConfig(is_perfetto) 711 ), 712 "wayland_trace": TraceTarget( 713 "wayland_trace", 714 WinscopeFileMatcher("/data/misc/wltrace", "wl_trace", "wl_trace"), 715 lambda res: False, 716 'su root service call Wayland 26 i32 1 >/dev/null\necho "Wayland trace started."', 717 'su root service call Wayland 26 i32 0 >/dev/null' 718 ), 719 "eventlog": TraceTarget( 720 "eventlog", 721 WinscopeFileMatcher("/data/local/tmp", "eventlog", "eventlog"), 722 lambda res: False, 723 'rm -f /data/local/tmp/eventlog.winscope && EVENT_LOG_TRACING_START_TIME=$EPOCHREALTIME\necho "Event Log trace started."', 724 'echo "EventLog\\n" > /data/local/tmp/eventlog.winscope && su root logcat -b events -v threadtime -v printable -v uid -v nsec -v epoch -b events -t $EVENT_LOG_TRACING_START_TIME >> /data/local/tmp/eventlog.winscope', 725 ), 726 "transition_traces": TraceTarget( 727 "transition_traces", 728 [WinscopeFileMatcher(WINSCOPE_DIR, "wm_transition_trace", "wm_transition_trace"), 729 WinscopeFileMatcher(WINSCOPE_DIR, "shell_transition_trace", "shell_transition_trace")], 730 lambda res: is_perfetto_data_source_available('com.android.wm.shell.transition', res), 731 """ 732su root cmd window shell tracing start && su root dumpsys activity service SystemUIService WMShell transitions tracing start 733echo "Transition traces (legacy) started." 734 """, 735 """ 736su root cmd window shell tracing stop && su root dumpsys activity service SystemUIService WMShell transitions tracing stop >/dev/null 2>&1 737echo 'Transition traces (legacy) stopped.' 738 """, 739 lambda is_perfetto: TransitionTracesConfig(is_perfetto) 740 ), 741 "input": TraceTarget( 742 "input", 743 [WinscopeFileMatcher(WINSCOPE_DIR, "input_trace", "input_trace")], 744 lambda res: is_perfetto_data_source_available('android.input.inputevent', res), 745 "", 746 "", 747 lambda is_perfetto: InputConfig(is_perfetto) 748 ), 749 "perfetto_trace": TraceTarget( 750 "perfetto_trace", 751 File(PERFETTO_TRACE_FILE, "trace.perfetto-trace"), 752 lambda res: is_any_perfetto_data_source_available(res), 753 f""" 754cat << EOF >> {PERFETTO_TRACE_CONFIG_FILE} 755buffers: {{ 756 size_kb: 500000 757 fill_policy: RING_BUFFER 758}} 759duration_ms: 0 760file_write_period_ms: 999999999 761write_into_file: true 762unique_session_name: "{PERFETTO_UNIQUE_SESSION_NAME}" 763EOF 764 765rm -f {PERFETTO_TRACE_FILE} 766perfetto --out {PERFETTO_TRACE_FILE} --txt --config {PERFETTO_TRACE_CONFIG_FILE} --detach=WINSCOPE-PROXY-TRACING-SESSION 767echo 'Started perfetto trace.' 768""", 769 """ 770perfetto --attach=WINSCOPE-PROXY-TRACING-SESSION --stop 771echo 'Stopped perfetto trace.' 772""", 773 ), 774} 775 776 777class DumpTarget: 778 """Defines a single parameter to dump. 779 780 Attributes: 781 trace_name: used as a key to access config and log statements during Dump endpoint. 782 files: the matchers used to identify the paths on the device the dump results are saved to. 783 dump_command: command to dump state to file. 784 get_trace_config: getter for optional setup to execute pre-tracing adb commands and define start command arguments. 785 """ 786 787 def __init__( 788 self, 789 trace_name: str, 790 files: list[File | FileMatcher], 791 is_perfetto_available: Callable[[str], bool], 792 dump_command: str, 793 get_trace_config: Callable[[bool], TraceConfig] | None = None 794 ) -> None: 795 self.trace_name = trace_name 796 if type(files) is not list: 797 files = [files] 798 self.files = files 799 self.is_perfetto_available = is_perfetto_available 800 self.dump_command = dump_command 801 self.get_trace_config = get_trace_config 802 803 804DUMP_TARGETS = { 805 "window_dump": DumpTarget( 806 "window_dump", 807 File(f'/data/local/tmp/wm_dump{WINSCOPE_EXT}', "window_dump"), 808 lambda res: False, 809 f'su root dumpsys window --proto > /data/local/tmp/wm_dump{WINSCOPE_EXT}' 810 ), 811 812 "layers_dump": DumpTarget( 813 "layers_dump", 814 File(f'/data/local/tmp/sf_dump{WINSCOPE_EXT}', "layers_dump"), 815 lambda res: is_perfetto_data_source_available('android.surfaceflinger.layers', res), 816 f""" 817su root dumpsys SurfaceFlinger --proto > /data/local/tmp/sf_dump{WINSCOPE_EXT} 818 """, 819 lambda is_perfetto: SurfaceFlingerDumpConfig(is_perfetto) 820 ), 821 822 "screenshot": DumpTarget( 823 "screenshot", 824 File("/data/local/tmp/screenshot_{trace_identifier}.png", "screenshot_{trace_identifier}.png"), 825 lambda res: False, 826 "screencap -p {options}> /data/local/tmp/screenshot_{trace_identifier}.png", 827 lambda is_perfetto: ScreenshotConfig(is_perfetto) 828 ), 829 830 "perfetto_dump": DumpTarget( 831 "perfetto_dump", 832 File(PERFETTO_DUMP_FILE, "dump.perfetto-trace"), 833 lambda res: is_any_perfetto_data_source_available(res), 834 f""" 835cat << EOF >> {PERFETTO_DUMP_CONFIG_FILE} 836buffers: {{ 837 size_kb: 500000 838 fill_policy: RING_BUFFER 839}} 840duration_ms: 1 841EOF 842 843rm -f {PERFETTO_DUMP_FILE} 844perfetto --out {PERFETTO_DUMP_FILE} --txt --config {PERFETTO_DUMP_CONFIG_FILE} 845echo 'Recorded perfetto dump.' 846 """ 847 ) 848} 849 850 851# END OF CONFIG # 852 853 854def get_token() -> str: 855 """Returns saved proxy security token or creates new one""" 856 try: 857 with open(WINSCOPE_TOKEN_LOCATION, 'r') as token_file: 858 token = token_file.readline() 859 log.debug("Loaded token {} from {}".format( 860 token, WINSCOPE_TOKEN_LOCATION)) 861 return token 862 except IOError: 863 token = secrets.token_hex(32) 864 os.makedirs(os.path.dirname(WINSCOPE_TOKEN_LOCATION), exist_ok=True) 865 try: 866 with open(WINSCOPE_TOKEN_LOCATION, 'w') as token_file: 867 log.debug("Created and saved token {} to {}".format( 868 token, WINSCOPE_TOKEN_LOCATION)) 869 token_file.write(token) 870 os.chmod(WINSCOPE_TOKEN_LOCATION, 0o600) 871 except IOError: 872 log.error("Unable to save persistent token {} to {}".format( 873 token, WINSCOPE_TOKEN_LOCATION)) 874 return token 875 876 877class RequestType(Enum): 878 GET = 1 879 POST = 2 880 HEAD = 3 881 882 883def add_standard_headers(server): 884 server.send_header('Cache-Control', 'no-cache, no-store, must-revalidate') 885 server.send_header('Access-Control-Allow-Origin', '*') 886 server.send_header('Access-Control-Allow-Methods', 'POST, GET, OPTIONS') 887 server.send_header('Access-Control-Allow-Headers', 888 WINSCOPE_TOKEN_HEADER + ', Content-Type, Content-Length') 889 server.send_header('Access-Control-Expose-Headers', 890 'Winscope-Proxy-Version') 891 server.send_header(WINSCOPE_VERSION_HEADER, VERSION) 892 server.end_headers() 893 894 895class RequestEndpoint: 896 """Request endpoint to use with the RequestRouter.""" 897 898 @abstractmethod 899 def process(self, server, path): 900 pass 901 902 903class AdbError(Exception): 904 """Unsuccessful ADB operation""" 905 pass 906 907 908class BadRequest(Exception): 909 """Invalid client request""" 910 pass 911 912 913class RequestRouter: 914 """Handles HTTP request authentication and routing""" 915 916 def __init__(self, handler): 917 self.request = handler 918 self.endpoints = {} 919 920 def register_endpoint(self, method: RequestType, name: str, endpoint: RequestEndpoint): 921 self.endpoints[(method, name)] = endpoint 922 923 def _bad_request(self, error: str): 924 log.warning("Bad request: " + error) 925 self.request.respond(HTTPStatus.BAD_REQUEST, b"Bad request!\nThis is Winscope ADB proxy.\n\n" 926 + error.encode("utf-8"), 'text/txt') 927 928 def _internal_error(self, error: str): 929 log.error("Internal error: " + error) 930 self.request.respond(HTTPStatus.INTERNAL_SERVER_ERROR, 931 error.encode("utf-8"), 'text/txt') 932 933 def _bad_token(self): 934 log.warning("Bad token") 935 self.request.respond(HTTPStatus.FORBIDDEN, b"Bad Winscope authorization token!\nThis is Winscope ADB proxy.\n", 936 'text/txt') 937 938 def process(self, method: RequestType): 939 token = self.request.headers[WINSCOPE_TOKEN_HEADER] 940 if not token or token != secret_token: 941 return self._bad_token() 942 path = self.request.path.strip('/').split('/') 943 if path and len(path) > 0: 944 endpoint_name = path[0] 945 try: 946 return self.endpoints[(method, endpoint_name)].process(self.request, path[1:]) 947 except KeyError as ex: 948 if "RequestType" in repr(ex): 949 return self._bad_request("Unknown endpoint /{}/".format(endpoint_name)) 950 return self._internal_error(repr(ex)) 951 except AdbError as ex: 952 return self._internal_error(str(ex)) 953 except BadRequest as ex: 954 return self._bad_request(str(ex)) 955 except Exception as ex: 956 return self._internal_error(repr(ex)) 957 self._bad_request("No endpoint specified") 958 959 960def call_adb(params: str, device: str = None, stdin: bytes = None): 961 command = ['adb'] + (['-s', device] if device else []) + params.split(' ') 962 try: 963 log.debug("Call: " + ' '.join(command)) 964 return subprocess.check_output(command, stderr=subprocess.STDOUT, input=stdin).decode('utf-8') 965 except OSError as ex: 966 raise AdbError('Error executing adb command: {}\n{}'.format( 967 ' '.join(command), repr(ex))) 968 except subprocess.CalledProcessError as ex: 969 raise AdbError('Error executing adb command: adb {}\n{}'.format( 970 params, ex.output.decode("utf-8"))) 971 972 973def call_adb_outfile(params: str, outfile, device: str = None, stdin: bytes = None): 974 try: 975 process = subprocess.Popen(['adb'] + (['-s', device] if device else []) + params.split(' '), stdout=outfile, 976 stderr=subprocess.PIPE) 977 _, err = process.communicate(stdin) 978 outfile.seek(0) 979 if process.returncode != 0: 980 raise AdbError('Error executing adb command: adb {}\n'.format(params) + err.decode( 981 'utf-8') + '\n' + outfile.read().decode('utf-8')) 982 except OSError as ex: 983 raise AdbError( 984 'Error executing adb command: adb {}\n{}'.format(params, repr(ex))) 985 986 987class ListDevicesEndpoint(RequestEndpoint): 988 ADB_INFO_RE = re.compile("^([A-Za-z0-9._:\\-]+)\\s+(\\w+)(.*model:(\\w+))?") 989 foundDevices: dict[str | int, dict[str, bool | str]] = {} 990 991 def process(self, server, path): 992 lines = list(filter(None, call_adb('devices -l').split('\n'))) 993 devices = {} 994 for m in [ListDevicesEndpoint.ADB_INFO_RE.match(d) for d in lines[1:]]: 995 if m: 996 authorized = str(m.group(2)) != 'unauthorized' 997 try: 998 screen_record_version = call_adb('shell screenrecord --version', m.group(1)) if authorized else '0' 999 except AdbError: 1000 try: 1001 help_text = call_adb('shell screenrecord --help', m.group(1)) 1002 version_start_index = help_text.find('v') + 1 1003 screen_record_version = help_text[version_start_index:version_start_index + 3] 1004 except AdbError: 1005 screen_record_version = '0' 1006 1007 try: 1008 displays = list(filter(None, call_adb('shell su root dumpsys SurfaceFlinger --display-id', 1009 m.group(1)).split('\n'))) if authorized else [] 1010 except AdbError: 1011 displays = [] 1012 1013 devices[m.group(1)] = { 1014 'authorized': authorized, 1015 'model': m.group(4).replace('_', ' ') if m.group(4) else '', 1016 'displays': displays, 1017 'screenrecord_version': screen_record_version, 1018 } 1019 self.foundDevices = devices 1020 j = json.dumps(devices) 1021 log.info("Detected devices: " + j) 1022 server.respond(HTTPStatus.OK, j.encode("utf-8"), "text/json") 1023 1024 1025 1026class CheckWaylandServiceEndpoint(RequestEndpoint): 1027 def __init__(self, listDevicesEndpoint: ListDevicesEndpoint): 1028 self._listDevicesEndpoint = listDevicesEndpoint 1029 1030 def process(self, server, path): 1031 self._listDevicesEndpoint.process(server, path) 1032 foundDevices = self._listDevicesEndpoint.foundDevices 1033 1034 if len(foundDevices) != 1: 1035 res = 'false' 1036 else: 1037 device = list(foundDevices.values())[0] 1038 if not device.get('authorized') or not device.get('model'): 1039 res = 'false' 1040 else: 1041 raw_res = call_adb('shell service check Wayland') 1042 res = 'false' if 'not found' in raw_res else 'true' 1043 server.respond(HTTPStatus.OK, res.encode("utf-8"), "text/json") 1044 1045 1046 1047class DeviceRequestEndpoint(RequestEndpoint): 1048 def process(self, server, path): 1049 if len(path) > 0 and re.fullmatch("[A-Za-z0-9._:\\-]+", path[0]): 1050 self.process_with_device(server, path[1:], path[0]) 1051 else: 1052 raise BadRequest("Device id not specified") 1053 1054 @abstractmethod 1055 def process_with_device(self, server, path, device_id): 1056 pass 1057 1058 def get_request(self, server) -> str: 1059 try: 1060 length = int(server.headers["Content-Length"]) 1061 except KeyError as err: 1062 raise BadRequest("Missing Content-Length header\n" + str(err)) 1063 except ValueError as err: 1064 raise BadRequest("Content length unreadable\n" + str(err)) 1065 return json.loads(server.rfile.read(length).decode("utf-8")) 1066 1067 def get_targets_and_prepare_for_tracing(self, server, device_id, perfetto_config_file, targets_map: dict[str, TraceTarget | DumpTarget], perfetto_name): 1068 warnings: list[str] = [] 1069 1070 call_adb(f"shell su root rm -f {perfetto_config_file}", device_id) 1071 log.debug("Cleared perfetto config file for previous tracing session") 1072 1073 trace_requests: list[dict] = self.get_request(server) 1074 trace_types = [t.get("name") for t in trace_requests] 1075 log.debug(f"Received client request of {trace_types} for {device_id}") 1076 1077 perfetto_query_result = call_adb("shell perfetto --query", device_id) 1078 1079 too_many_perfetto_sessions = self.too_many_perfetto_sessions(perfetto_query_result) 1080 if (too_many_perfetto_sessions): 1081 warnings.append("Limit of 5 Perfetto sessions reached on device. Will attempt to collect legacy traces.") 1082 log.warning(warnings[0]) 1083 1084 trace_targets: list[tuple[DumpTarget | TraceTarget, TraceConfig | None]] = [] 1085 for t in trace_requests: 1086 try: 1087 trace_name = t.get("name") 1088 target = targets_map[trace_name] 1089 is_perfetto = (not too_many_perfetto_sessions) and target.is_perfetto_available(perfetto_query_result) 1090 config = None 1091 if target.get_trace_config is not None: 1092 config = target.get_trace_config(is_perfetto) 1093 self.apply_config(config, t.get("config"), server, device_id) 1094 is_valid_perfetto_target = trace_name == perfetto_name and not too_many_perfetto_sessions 1095 is_valid_trace_target = trace_name != perfetto_name and not is_perfetto 1096 if is_valid_perfetto_target or is_valid_trace_target: 1097 trace_targets.append((target, config)) 1098 1099 except KeyError as err: 1100 log.warning("Unsupported trace target\n" + str(err)) 1101 trace_targets = self.move_perfetto_target_to_end_of_list(trace_targets) 1102 1103 self.check_device_and_permissions(server, device_id) 1104 self.clear_last_tracing_session(device_id) 1105 1106 log.debug("Trace requested for {} with targets {}".format( 1107 device_id, ','.join([target.trace_name for target, config in trace_targets]))) 1108 1109 return trace_targets, warnings 1110 1111 def too_many_perfetto_sessions(self, perfetto_query_result: str): 1112 concurrent_sessions_start = perfetto_query_result.find(PERFETTO_TRACING_SESSIONS_QUERY_START) 1113 if concurrent_sessions_start != -1: 1114 concurrent_sessions = perfetto_query_result[concurrent_sessions_start + len(PERFETTO_TRACING_SESSIONS_QUERY_START):] 1115 log.info(f"Concurrent sessions:\n{concurrent_sessions}\n") 1116 concurrent_sessions_end = concurrent_sessions.find(PERFETTO_TRACING_SESSIONS_QUERY_END) 1117 if concurrent_sessions_end > 0: 1118 concurrent_sessions_end -= 1 1119 concurrent_sessions = concurrent_sessions[:concurrent_sessions_end] 1120 number_of_concurrent_sessions = len(concurrent_sessions.split("\n")) if len(concurrent_sessions) > 0 else 0 1121 1122 if PERFETTO_UNIQUE_SESSION_NAME in concurrent_sessions: 1123 call_adb("shell perfetto --attach=WINSCOPE-PROXY-TRACING-SESSION --stop") 1124 log.debug("Stopped already-running winscope perfetto session.") 1125 number_of_concurrent_sessions -= 1 1126 return number_of_concurrent_sessions >= 5 1127 return False 1128 1129 def apply_config(self, trace_config: TraceConfig, requested_configs: list[dict], server, device_id): 1130 for requested_config in requested_configs: 1131 config_key = requested_config.get("key") 1132 if not trace_config.is_valid(config_key): 1133 raise BadRequest( 1134 f"Unsupported config {config_key}\n") 1135 trace_config.add(config_key, requested_config.get("value")) 1136 1137 if device_id in TRACE_THREADS: 1138 BadRequest(f"Trace in progress for {device_id}") 1139 if not self.check_root(device_id): 1140 raise AdbError( 1141 f"Unable to acquire root privileges on the device - check the output of 'adb -s {device_id} shell su root id'") 1142 trace_config.execute_command(server, device_id) 1143 1144 def check_root(self, device_id): 1145 log.debug("Checking root access on {}".format(device_id)) 1146 return int(call_adb('shell su root id -u', device_id)) == 0 1147 1148 def move_perfetto_target_to_end_of_list(self, targets: list[tuple[TraceTarget, TraceConfig | None]]) -> list[tuple[TraceTarget, TraceConfig | None]]: 1149 # Make sure a perfetto target (if present) comes last in the list of targets, i.e. will 1150 # be processed last. 1151 # A perfetto target must be processed last, so that perfetto tracing is started only after 1152 # the other targets have been processed and have configured the perfetto config file. 1153 def is_perfetto_target(target: TraceTarget): 1154 return target == TRACE_TARGETS["perfetto_trace"] or target == DUMP_TARGETS["perfetto_dump"] 1155 non_perfetto_targets = [t for t in targets if not is_perfetto_target(t[0])] 1156 perfetto_targets = [t for t in targets if is_perfetto_target(t[0])] 1157 return non_perfetto_targets + perfetto_targets 1158 1159 def check_device_and_permissions(self, server, device_id): 1160 if device_id in TRACE_THREADS: 1161 log.warning("Trace already in progress for {}", device_id) 1162 server.respond(HTTPStatus.OK, b'', "text/plain") 1163 if not self.check_root(device_id): 1164 raise AdbError( 1165 "Unable to acquire root privileges on the device - check the output of 'adb -s {} shell su root id'" 1166 .format( device_id)) 1167 1168 def clear_last_tracing_session(self, device_id): 1169 call_adb(f"shell su root rm -rf {WINSCOPE_BACKUP_DIR}", device_id) 1170 call_adb(f"shell su root mkdir {WINSCOPE_BACKUP_DIR}", device_id) 1171 log.debug("Cleared previous tracing session files from device") 1172 1173 1174class FetchFilesEndpoint(DeviceRequestEndpoint): 1175 def process_with_device(self, server, path, device_id): 1176 file_buffers = self.fetch_existing_files(device_id) 1177 1178 j = json.dumps(file_buffers) 1179 server.respond(HTTPStatus.OK, j.encode("utf-8"), "text/json") 1180 1181 def fetch_existing_files(self, device_id): 1182 file_buffers = dict() 1183 file = FileMatcher(f"{WINSCOPE_BACKUP_DIR}*", "", "") 1184 try: 1185 file_paths = file.get_filepaths(device_id) 1186 for file_path in file_paths: 1187 with NamedTemporaryFile() as tmp: 1188 file_name = file_path.split('/')[-1] + ".gz" 1189 log.debug( 1190 f"Fetching file {file_path} from device to {tmp.name}") 1191 try: 1192 call_adb_outfile('exec-out su root cat ' + 1193 file_path, tmp, device_id) 1194 except AdbError as ex: 1195 log.warning(f"Unable to fetch file {file_path} - {repr(ex)}") 1196 return 1197 log.debug(f"Uploading file {tmp.name}") 1198 if file_name not in file_buffers: 1199 file_buffers[file_name] = [] 1200 buf = base64.encodebytes(gzip.compress(tmp.read())).decode("utf-8") 1201 file_buffers[file_name].append(buf) 1202 except: 1203 self.log_no_files_warning() 1204 return file_buffers 1205 1206 def log_no_files_warning(self): 1207 log.warning("Proxy didn't find any file to fetch") 1208 1209 1210TRACE_THREADS = {} 1211 1212class TraceThread(threading.Thread): 1213 def __init__(self, trace_name: str, device_id: str, command: str, trace_identifier: str, status_filename: str): 1214 self.trace_command = command 1215 self.trace_name = trace_name 1216 self.trace_identifier = trace_identifier 1217 self.status_filename = status_filename 1218 self._device_id = device_id 1219 self._keep_alive_timer = None 1220 self.out = None, 1221 self.err = None, 1222 self._command_timed_out = False 1223 self._success = False 1224 try: 1225 shell = get_shell_args(self._device_id, "trace") 1226 self.process = subprocess.Popen(shell, stdout=subprocess.PIPE, 1227 stderr=subprocess.PIPE, stdin=subprocess.PIPE, start_new_session=True) 1228 except OSError as ex: 1229 raise AdbError( 1230 'Error executing adb command for trace {}: adb shell\n{}'.format(trace_name, repr(ex))) 1231 1232 super().__init__() 1233 1234 def timeout(self): 1235 if self.is_alive(): 1236 log.warning("Keep-alive timeout for {} trace on {}".format(self.trace_name, self._device_id)) 1237 self.end_trace() 1238 1239 def reset_timer(self): 1240 log.info( 1241 "Resetting keep-alive clock for {} trace on {}".format(self.trace_name, self._device_id)) 1242 if self._keep_alive_timer: 1243 self._keep_alive_timer.cancel() 1244 self._keep_alive_timer = threading.Timer( 1245 KEEP_ALIVE_INTERVAL_S, self.timeout) 1246 self._keep_alive_timer.start() 1247 1248 def end_trace(self): 1249 if self._keep_alive_timer: 1250 self._keep_alive_timer.cancel() 1251 log.info("Sending SIGINT to the {} process on {}".format( 1252 self.trace_name, 1253 self._device_id)) 1254 self.process.send_signal(signal.SIGINT) 1255 try: 1256 log.debug("Waiting for {} trace shell to exit for {}".format( 1257 self.trace_name, 1258 self._device_id)) 1259 self.process.wait(timeout=COMMAND_TIMEOUT_S) 1260 except TimeoutError: 1261 log.error( 1262 "TIMEOUT - sending SIGKILL to the {} trace process on {}".format(self.trace_name, self._device_id)) 1263 self.process.kill() 1264 self.join() 1265 1266 def run(self): 1267 retry_interval = 0.1 1268 log.info("Trace {} started on {}".format(self.trace_name, self._device_id)) 1269 self.reset_timer() 1270 self.out, self.err = self.process.communicate(self.trace_command) 1271 log.info("Trace {} ended on {}, waiting for cleanup".format(self.trace_name, self._device_id)) 1272 time.sleep(0.2) 1273 for i in range(int(COMMAND_TIMEOUT_S / retry_interval)): 1274 if call_adb(f"shell su root cat {self.status_filename}", device=self._device_id) == 'TRACE_OK\n': 1275 log.info("Trace {} finished on {}".format( 1276 self.trace_name, 1277 self._device_id)) 1278 if self.trace_name == "perfetto_trace": 1279 self._success = True 1280 else: 1281 self._success = len(self.err) == 0 1282 return 1283 log.debug("Still waiting for cleanup on {} for {}".format(self._device_id, self.trace_name)) 1284 time.sleep(retry_interval) 1285 1286 self._command_timed_out = True 1287 1288 def success(self): 1289 return self._success 1290 1291 def timed_out(self): 1292 return self._command_timed_out 1293 1294class StartTraceEndpoint(DeviceRequestEndpoint): 1295 TRACE_COMMAND = """ 1296set -e 1297 1298echo "Starting trace..." 1299echo "TRACE_START" > {winscope_status} 1300 1301# Do not print anything to stdout/stderr in the handler 1302function stop_trace() {{ 1303 echo "start" >{signal_handler_log} 1304 1305 # redirect stdout/stderr to log file 1306 exec 1>>{signal_handler_log} 1307 exec 2>>{signal_handler_log} 1308 1309 set -x 1310 trap - EXIT HUP INT 1311 {stop_commands} 1312 echo "TRACE_OK" > {winscope_status} 1313}} 1314 1315trap stop_trace EXIT HUP INT 1316echo "Signal handler registered." 1317 1318{start_commands} 1319 1320# ADB shell does not handle hung up well and does not call HUP handler when a child is active in foreground, 1321# as a workaround we sleep for short intervals in a loop so the handler is called after a sleep interval. 1322while true; do sleep 0.1; done 1323""" 1324 1325 def process_with_device(self, server, path, device_id): 1326 trace_targets, warnings = self.get_targets_and_prepare_for_tracing( 1327 server, device_id, PERFETTO_TRACE_CONFIG_FILE, TRACE_TARGETS, "perfetto_trace") 1328 1329 for t, config in trace_targets: 1330 if config: 1331 trace_identifiers = config.get_trace_identifiers() 1332 else: 1333 trace_identifiers = [""] 1334 1335 for trace_identifier in trace_identifiers: 1336 if trace_identifier: 1337 if config: 1338 options = config.get_optional_start_args(trace_identifier) 1339 else: 1340 options = "" 1341 start_cmd = t.trace_start.format(trace_identifier=trace_identifier, options=options) 1342 else: 1343 start_cmd = t.trace_start 1344 1345 command = StartTraceEndpoint.TRACE_COMMAND.format( 1346 winscope_status=t.status_filename, 1347 signal_handler_log=SIGNAL_HANDLER_LOG, 1348 stop_commands=t.trace_stop, 1349 perfetto_config_file=PERFETTO_TRACE_CONFIG_FILE, 1350 start_commands=start_cmd, 1351 ) 1352 log.debug(f"Executing start command for {t.trace_name} on {device_id}...") 1353 thread = TraceThread(t.trace_name, device_id, command.encode('utf-8'), trace_identifier, t.status_filename) 1354 if device_id not in TRACE_THREADS: 1355 TRACE_THREADS[device_id] = [thread] 1356 else: 1357 TRACE_THREADS[device_id].append(thread) 1358 thread.start() 1359 1360 server.respond(HTTPStatus.OK, json.dumps(warnings).encode("utf-8"), "text/json") 1361 1362 1363def move_collected_files(files: list[File | FileMatcher], device_id, trace_identifier): 1364 for f in files: 1365 file_paths = f.get_filepaths(device_id) 1366 file_type = f.get_filetype().format(trace_identifier=trace_identifier) 1367 1368 for file_path in file_paths: 1369 formatted_path = file_path.format(trace_identifier=trace_identifier) 1370 log.debug(f"Moving file {formatted_path} to {WINSCOPE_BACKUP_DIR}{file_type} on device") 1371 try: 1372 call_adb( 1373 f"shell su root [ ! -f {formatted_path} ] || su root mv {formatted_path} {WINSCOPE_BACKUP_DIR}{file_type}", 1374 device_id) 1375 except AdbError as ex: 1376 log.warning(f"Unable to move file {formatted_path} - {repr(ex)}") 1377 1378 1379class EndTraceEndpoint(DeviceRequestEndpoint): 1380 def process_with_device(self, server, path, device_id): 1381 if device_id not in TRACE_THREADS: 1382 raise BadRequest("No trace in progress for {}".format(device_id)) 1383 1384 errors: list[str] = [] 1385 1386 for thread in TRACE_THREADS[device_id]: 1387 if thread.is_alive(): 1388 thread.end_trace() 1389 success = thread.success() 1390 signal_handler_log = call_adb(f"shell su root cat {SIGNAL_HANDLER_LOG}", device=device_id).encode('utf-8') 1391 1392 if (thread.timed_out()): 1393 timeout_message = "Trace {} timed out during cleanup".format(thread.trace_name) 1394 errors.append(timeout_message) 1395 log.error(timeout_message) 1396 1397 if not success: 1398 log.error("Error ending trace {} on the device".format(thread.trace_name)) 1399 errors.append("Error ending trace {} on the device: {}".format(thread.trace_name, thread.err)) 1400 1401 out = b"### Shell script's stdout ###\n" + \ 1402 (thread.out if thread.out else b'<no stdout>') + \ 1403 b"\n### Shell script's stderr ###\n" + \ 1404 (thread.err if thread.err else b'<no stderr>') + \ 1405 b"\n### Signal handler log ###\n" + \ 1406 (signal_handler_log if signal_handler_log else b'<no signal handler logs>') + \ 1407 b"\n" 1408 log.debug("### Output ###\n".format(thread.trace_name) + out.decode("utf-8")) 1409 if thread.trace_name in TRACE_TARGETS: 1410 files = TRACE_TARGETS[thread.trace_name].files 1411 move_collected_files(files, device_id, thread.trace_identifier) 1412 else: 1413 errors.append(f"File location unknown for {thread.trace_name}") 1414 1415 call_adb(f"shell su root rm {thread.status_filename}", device=device_id) 1416 TRACE_THREADS.pop(device_id) 1417 server.respond(HTTPStatus.OK, json.dumps(errors).encode("utf-8"), "text/plain") 1418 1419 1420class StatusEndpoint(DeviceRequestEndpoint): 1421 def process_with_device(self, server, path, device_id): 1422 if device_id not in TRACE_THREADS: 1423 raise BadRequest("No trace in progress for {}".format(device_id)) 1424 for thread in TRACE_THREADS[device_id]: 1425 thread.reset_timer() 1426 server.respond(HTTPStatus.OK, str(TRACE_THREADS[device_id][0].is_alive()).encode("utf-8"), "text/plain") 1427 1428 1429class DumpEndpoint(DeviceRequestEndpoint): 1430 def process_with_device(self, server, path, device_id): 1431 dump_targets, warnings = self.get_targets_and_prepare_for_tracing( 1432 server, device_id, PERFETTO_DUMP_CONFIG_FILE, DUMP_TARGETS, "perfetto_dump") 1433 1434 dump_commands = [] 1435 for t, config in dump_targets: 1436 if config: 1437 for trace_identifier in config.get_trace_identifiers(): 1438 dump_commands.append( 1439 t.dump_command.format(trace_identifier=trace_identifier, options=config.get_optional_start_args(trace_identifier))) 1440 else: 1441 dump_commands.append(t.dump_command) 1442 1443 dump_commands = '\n'.join(dump_commands) 1444 1445 shell = get_shell_args(device_id, "dump") 1446 process = subprocess.Popen(shell, stdout=subprocess.PIPE, stderr=subprocess.PIPE, 1447 stdin=subprocess.PIPE, start_new_session=True) 1448 log.info("Starting dump on device {}".format(device_id)) 1449 out, err = process.communicate(dump_commands.encode('utf-8')) 1450 if process.returncode != 0: 1451 raise AdbError("Error executing dump command." + "\n\n### OUTPUT ###" + out.decode('utf-8') + "\n" 1452 + err.decode('utf-8')) 1453 log.info("Dump finished on device {}".format(device_id)) 1454 1455 for target, config in dump_targets: 1456 if config: 1457 trace_identifiers = config.get_trace_identifiers() 1458 for trace_identifier in trace_identifiers: 1459 move_collected_files(target.files, device_id, trace_identifier) 1460 else: 1461 move_collected_files(target.files, device_id, "") 1462 1463 server.respond(HTTPStatus.OK, json.dumps(warnings).encode("utf-8"), "text/plain") 1464 1465 1466class ADBWinscopeProxy(BaseHTTPRequestHandler): 1467 def __init__(self, request, client_address, server): 1468 self.router = RequestRouter(self) 1469 listDevicesEndpoint = ListDevicesEndpoint() 1470 self.router.register_endpoint( 1471 RequestType.GET, "devices", listDevicesEndpoint) 1472 self.router.register_endpoint( 1473 RequestType.GET, "status", StatusEndpoint()) 1474 self.router.register_endpoint( 1475 RequestType.GET, "fetch", FetchFilesEndpoint()) 1476 self.router.register_endpoint(RequestType.POST, "start", StartTraceEndpoint()) 1477 self.router.register_endpoint(RequestType.POST, "end", EndTraceEndpoint()) 1478 self.router.register_endpoint(RequestType.POST, "dump", DumpEndpoint()) 1479 self.router.register_endpoint( 1480 RequestType.GET, "checkwayland", CheckWaylandServiceEndpoint(listDevicesEndpoint)) 1481 super().__init__(request, client_address, server) 1482 1483 def respond(self, code: int, data: bytes, mime: str) -> None: 1484 self.send_response(code) 1485 self.send_header('Content-type', mime) 1486 add_standard_headers(self) 1487 self.wfile.write(data) 1488 1489 def do_GET(self): 1490 self.router.process(RequestType.GET) 1491 1492 def do_POST(self): 1493 self.router.process(RequestType.POST) 1494 1495 def do_OPTIONS(self): 1496 self.send_response(HTTPStatus.OK) 1497 self.send_header('Allow', 'GET,POST') 1498 add_standard_headers(self) 1499 self.end_headers() 1500 self.wfile.write(b'GET,POST') 1501 1502 def log_request(self, code='-', size='-'): 1503 log.info('{} {} {}'.format(self.requestline, str(code), str(size))) 1504 1505 1506if __name__ == '__main__': 1507 args = create_argument_parser().parse_args() 1508 1509 logging.basicConfig(stream=sys.stderr, level=args.loglevel, 1510 format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') 1511 1512 log = logging.getLogger("ADBProxy") 1513 secret_token = get_token() 1514 1515 print("Winscope ADB Connect proxy version: " + VERSION) 1516 print('Winscope token: ' + secret_token) 1517 1518 httpd = HTTPServer(('localhost', args.port), ADBWinscopeProxy) 1519 try: 1520 httpd.serve_forever() 1521 except KeyboardInterrupt: 1522 log.info("Shutting down") 1523