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.5 and ADB installed and in system PATH. 21# 22# Usage: 23# run: python3 winscope_proxy.py 24# 25 26import argparse 27import base64 28import json 29import logging 30import os 31import re 32import secrets 33import signal 34import subprocess 35import sys 36import threading 37import time 38from abc import abstractmethod 39from enum import Enum 40from http import HTTPStatus 41from http.server import HTTPServer, BaseHTTPRequestHandler 42from logging import DEBUG, INFO, WARNING 43from tempfile import NamedTemporaryFile 44 45# GLOBALS # 46 47log = None 48secret_token = None 49 50# CONFIG # 51 52def create_argument_parser() -> argparse.ArgumentParser: 53 parser = argparse.ArgumentParser(description='Proxy for go/winscope', prog='winscope_proxy') 54 55 parser.add_argument('--verbose', '-v', dest='loglevel', action='store_const', const=INFO) 56 parser.add_argument('--debug', '-d', dest='loglevel', action='store_const', const=DEBUG) 57 parser.add_argument('--port', '-p', default=5544, action='store') 58 59 parser.set_defaults(loglevel=WARNING) 60 61 return parser 62 63# Keep in sync with ProxyClient#VERSION in Winscope 64VERSION = '2.1.1' 65 66PERFETTO_TRACE_CONFIG_FILE = '/data/misc/perfetto-configs/winscope-proxy-trace.conf' 67PERFETTO_DUMP_CONFIG_FILE = '/data/misc/perfetto-configs/winscope-proxy-dump.conf' 68PERFETTO_SF_CONFIG_FILE = '/data/misc/perfetto-configs/winscope-proxy.surfaceflinger.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_UTILS = f""" 73function is_perfetto_data_source_available {{ 74 local data_source_name=$1 75 if perfetto --query | grep $data_source_name 2>&1 >/dev/null; then 76 return 0 77 else 78 return 1 79 fi 80}} 81 82function is_perfetto_tracing_session_running {{ 83 if perfetto --query | grep "{PERFETTO_UNIQUE_SESSION_NAME}" 2>&1 >/dev/null; then 84 return 0 85 else 86 return 1 87 fi 88}} 89 90function is_any_perfetto_data_source_available {{ 91 if is_perfetto_data_source_available android.inputmethod || \ 92 is_perfetto_data_source_available android.protolog || \ 93 is_perfetto_data_source_available android.surfaceflinger.layers || \ 94 is_perfetto_data_source_available android.surfaceflinger.transactions || \ 95 is_perfetto_data_source_available com.android.wm.shell.transition; then 96 return 0 97 else 98 return 1 99 fi 100}} 101 102function is_flag_set {{ 103 local flag_name=$1 104 if dumpsys device_config | grep $flag_name=true 2>&1 >/dev/null; then 105 return 0 106 else 107 return 1 108 fi 109}} 110""" 111 112WINSCOPE_VERSION_HEADER = "Winscope-Proxy-Version" 113WINSCOPE_TOKEN_HEADER = "Winscope-Token" 114 115# Location to save the proxy security token 116WINSCOPE_TOKEN_LOCATION = os.path.expanduser('~/.config/winscope/.token') 117 118# Winscope traces extensions 119WINSCOPE_EXT = ".winscope" 120WINSCOPE_EXT_LEGACY = ".pb" 121WINSCOPE_EXTS = [WINSCOPE_EXT, WINSCOPE_EXT_LEGACY] 122 123# Winscope traces directory 124WINSCOPE_DIR = "/data/misc/wmtrace/" 125 126# Max interval between the client keep-alive requests in seconds 127KEEP_ALIVE_INTERVAL_S = 5 128 129class File: 130 def __init__(self, file, filetype) -> None: 131 self.file = file 132 self.type = filetype 133 134 def get_filepaths(self, device_id): 135 return [self.file] 136 137 def get_filetype(self): 138 return self.type 139 140 141class FileMatcher: 142 def __init__(self, path, matcher, filetype) -> None: 143 self.path = path 144 self.matcher = matcher 145 self.type = filetype 146 147 def get_filepaths(self, device_id): 148 matchingFiles = call_adb( 149 f"shell su root find {self.path} -name {self.matcher}", device_id) 150 151 log.debug("Found file %s", matchingFiles.split('\n')[:-1]) 152 return matchingFiles.split('\n')[:-1] 153 154 def get_filetype(self): 155 return self.type 156 157 158class WinscopeFileMatcher(FileMatcher): 159 def __init__(self, path, matcher, filetype) -> None: 160 self.path = path 161 self.internal_matchers = list(map(lambda ext: FileMatcher(path, f'{matcher}{ext}', filetype), 162 WINSCOPE_EXTS)) 163 self.type = filetype 164 165 def get_filepaths(self, device_id): 166 for matcher in self.internal_matchers: 167 files = matcher.get_filepaths(device_id) 168 if len(files) > 0: 169 return files 170 log.debug("No files found") 171 return [] 172 173 174class TraceTarget: 175 """Defines a single parameter to trace. 176 177 Attributes: 178 file_matchers: the matchers used to identify the paths on the device the trace results are saved to. 179 trace_start: command to start the trace from adb shell, must not block. 180 trace_stop: command to stop the trace, should block until the trace is stopped. 181 """ 182 183 def __init__(self, files, trace_start: str, trace_stop: str) -> None: 184 if type(files) is not list: 185 files = [files] 186 self.files = files 187 self.trace_start = trace_start 188 self.trace_stop = trace_stop 189 190 191# Order of files matters as they will be expected in that order and decoded in that order 192TRACE_TARGETS = { 193 "view_capture_trace": TraceTarget( 194 File('/data/misc/wmtrace/view_capture_trace.zip', "view_capture_trace.zip"), 195 f""" 196if is_flag_set windowing_tools/android.tracing.perfetto_view_capture_tracing; then 197 cat << EOF >> {PERFETTO_TRACE_CONFIG_FILE} 198data_sources: {{ 199 config {{ 200 name: "android.viewcapture" 201 }} 202}} 203EOF 204 echo 'ViewCapture tracing (perfetto) configured to start along the other perfetto traces' 205else 206 su root settings put global view_capture_enabled 1 207 echo 'ViewCapture tracing (legacy) started.' 208fi 209""", 210 """ 211if ! is_flag_set windowing_tools/android.tracing.perfetto_view_capture_tracing; then 212 su root sh -c 'cmd launcherapps dump-view-hierarchies >/data/misc/wmtrace/view_capture_trace.zip' 213 su root settings put global view_capture_enabled 0 214 echo 'ViewCapture tracing (legacy) stopped.' 215fi 216""" 217 ), 218 "window_trace": TraceTarget( 219 WinscopeFileMatcher(WINSCOPE_DIR, "wm_trace", "window_trace"), 220 'su root cmd window tracing start\necho "WM trace started."', 221 'su root cmd window tracing stop >/dev/null 2>&1' 222 ), 223 "layers_trace": TraceTarget( 224 WinscopeFileMatcher(WINSCOPE_DIR, "layers_trace", "layers_trace"), 225 f""" 226if is_perfetto_data_source_available android.surfaceflinger.layers; then 227 cat {PERFETTO_SF_CONFIG_FILE} >> {PERFETTO_TRACE_CONFIG_FILE} 228 echo 'SF trace (perfetto) configured to start along the other perfetto traces' 229else 230 su root service call SurfaceFlinger 1025 i32 1 231 echo 'SF layers trace (legacy) started' 232fi 233 """, 234 """ 235if ! is_perfetto_data_source_available android.surfaceflinger.layers; then 236 su root service call SurfaceFlinger 1025 i32 0 >/dev/null 2>&1 237 echo 'SF layers trace (legacy) stopped.' 238fi 239""" 240), 241 "screen_recording": TraceTarget( 242 File(f'/data/local/tmp/screen.mp4', "screen_recording"), 243 f''' 244 settings put system show_touches 1 && \ 245 settings put system pointer_location 1 && \ 246 screenrecord --bugreport --bit-rate 8M /data/local/tmp/screen.mp4 >/dev/null 2>&1 & \ 247 echo "ScreenRecorder started." 248 ''', 249 '''settings put system pointer_location 0 && \ 250 settings put system show_touches 0 && \ 251 pkill -l SIGINT screenrecord >/dev/null 2>&1 252 '''.strip() 253 ), 254 "transactions": TraceTarget( 255 WinscopeFileMatcher(WINSCOPE_DIR, "transactions_trace", "transactions"), 256 f""" 257if is_perfetto_data_source_available android.surfaceflinger.transactions; then 258 cat << EOF >> {PERFETTO_TRACE_CONFIG_FILE} 259data_sources: {{ 260 config {{ 261 name: "android.surfaceflinger.transactions" 262 surfaceflinger_transactions_config: {{ 263 mode: MODE_ACTIVE 264 }} 265 }} 266}} 267EOF 268 echo 'SF transactions trace (perfetto) configured to start along the other perfetto traces' 269else 270 su root service call SurfaceFlinger 1041 i32 1 271 echo 'SF transactions trace (legacy) started' 272fi 273""", 274 """ 275if ! is_perfetto_data_source_available android.surfaceflinger.transactions; then 276 su root service call SurfaceFlinger 1041 i32 0 >/dev/null 2>&1 277fi 278""" 279 ), 280 "transactions_legacy": TraceTarget( 281 [ 282 WinscopeFileMatcher(WINSCOPE_DIR, "transaction_trace", "transactions_legacy"), 283 FileMatcher(WINSCOPE_DIR, f'transaction_merges_*', "transaction_merges"), 284 ], 285 'su root service call SurfaceFlinger 1020 i32 1\necho "SF transactions recording started."', 286 'su root service call SurfaceFlinger 1020 i32 0 >/dev/null 2>&1' 287 ), 288 "proto_log": TraceTarget( 289 WinscopeFileMatcher(WINSCOPE_DIR, "wm_log", "proto_log"), 290 f""" 291if is_perfetto_data_source_available android.protolog && \ 292 is_flag_set windowing_tools/android.tracing.perfetto_protolog_tracing; then 293 cat << EOF >> {PERFETTO_TRACE_CONFIG_FILE} 294data_sources: {{ 295 config {{ 296 name: "android.protolog" 297 protolog_config: {{ 298 tracing_mode: ENABLE_ALL 299 }} 300 }} 301}} 302EOF 303 echo 'ProtoLog (perfetto) configured to start along the other perfetto traces' 304else 305 su root cmd window logging start 306 echo "ProtoLog (legacy) started." 307fi 308 """, 309 """ 310if ! is_perfetto_data_source_available android.protolog && \ 311 ! is_flag_set windowing_tools/android.tracing.perfetto_protolog_tracing; then 312 su root cmd window logging stop >/dev/null 2>&1 313 echo "ProtoLog (legacy) stopped." 314fi 315 """ 316 ), 317 "ime": TraceTarget( 318 [WinscopeFileMatcher(WINSCOPE_DIR, "ime_trace_clients", "ime_trace_clients"), 319 WinscopeFileMatcher(WINSCOPE_DIR, "ime_trace_service", "ime_trace_service"), 320 WinscopeFileMatcher(WINSCOPE_DIR, "ime_trace_managerservice", "ime_trace_managerservice")], 321 f""" 322if is_flag_set windowing_tools/android.tracing.perfetto_ime; then 323 cat << EOF >> {PERFETTO_TRACE_CONFIG_FILE} 324data_sources: {{ 325 config {{ 326 name: "android.inputmethod" 327 }} 328}} 329EOF 330 echo 'IME tracing (perfetto) configured to start along the other perfetto traces' 331else 332 su root ime tracing start 333 echo "IME tracing (legacy) started." 334fi 335""", 336 """ 337if ! is_flag_set windowing_tools/android.tracing.perfetto_ime; then 338 su root ime tracing stop >/dev/null 2>&1 339 echo "IME tracing (legacy) stopped." 340fi 341""" 342 ), 343 "wayland_trace": TraceTarget( 344 WinscopeFileMatcher("/data/misc/wltrace", "wl_trace", "wl_trace"), 345 'su root service call Wayland 26 i32 1 >/dev/null\necho "Wayland trace started."', 346 'su root service call Wayland 26 i32 0 >/dev/null' 347 ), 348 "eventlog": TraceTarget( 349 WinscopeFileMatcher("/data/local/tmp", "eventlog", "eventlog"), 350 'rm -f /data/local/tmp/eventlog.winscope && EVENT_LOG_TRACING_START_TIME=$EPOCHREALTIME\necho "Event Log trace started."', 351 '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', 352 ), 353 "transition_traces": TraceTarget( 354 [WinscopeFileMatcher(WINSCOPE_DIR, "wm_transition_trace", "wm_transition_trace"), 355 WinscopeFileMatcher(WINSCOPE_DIR, "shell_transition_trace", "shell_transition_trace")], 356 f""" 357if is_perfetto_data_source_available com.android.wm.shell.transition && \ 358 is_flag_set windowing_tools/android.tracing.perfetto_transition_tracing; then 359 cat << EOF >> {PERFETTO_TRACE_CONFIG_FILE} 360data_sources: {{ 361 config {{ 362 name: "com.android.wm.shell.transition" 363 }} 364}} 365EOF 366 echo 'Transition trace (perfetto) configured to start along the other perfetto traces' 367else 368 su root cmd window shell tracing start && su root dumpsys activity service SystemUIService WMShell transitions tracing start 369 echo "Transition traces (legacy) started." 370fi 371 """, 372 """ 373if ! is_perfetto_data_source_available com.android.wm.shell.transition && \ 374 ! is_flag_set windowing_tools/android.tracing.perfetto_transition_tracing; then 375 su root cmd window shell tracing stop && su root dumpsys activity service SystemUIService WMShell transitions tracing stop >/dev/null 2>&1 376 echo 'Transition traces (legacy) stopped.' 377fi 378""" 379 ), 380 "perfetto_trace": TraceTarget( 381 File(PERFETTO_TRACE_FILE, "trace.perfetto-trace"), 382 f""" 383if is_any_perfetto_data_source_available; then 384 cat << EOF >> {PERFETTO_TRACE_CONFIG_FILE} 385buffers: {{ 386 size_kb: 80000 387 fill_policy: RING_BUFFER 388}} 389duration_ms: 0 390file_write_period_ms: 999999999 391write_into_file: true 392unique_session_name: "{PERFETTO_UNIQUE_SESSION_NAME}" 393EOF 394 395 if is_perfetto_tracing_session_running; then 396 perfetto --attach=WINSCOPE-PROXY-TRACING-SESSION --stop 397 echo 'Stopped already-running winscope perfetto session' 398 fi 399 400 echo 'Concurrent Perfetto Sessions' 401 perfetto --query | sed -n '/^TRACING SESSIONS:$/,$p' 402 403 rm -f {PERFETTO_TRACE_FILE} 404 perfetto --out {PERFETTO_TRACE_FILE} --txt --config {PERFETTO_TRACE_CONFIG_FILE} --detach=WINSCOPE-PROXY-TRACING-SESSION 405 echo 'Started perfetto trace' 406fi 407""", 408 """ 409if is_any_perfetto_data_source_available; then 410 perfetto --attach=WINSCOPE-PROXY-TRACING-SESSION --stop 411 echo 'Stopped perfetto trace' 412fi 413""", 414 ) 415} 416 417 418class SurfaceFlingerTraceConfig: 419 """Handles optional configuration for surfaceflinger traces. 420 """ 421 422 def __init__(self) -> None: 423 self.flags = [] 424 self.perfetto_flags = [] 425 426 def add(self, config: str) -> None: 427 self.flags.append(config) 428 429 def is_valid(self, config: str) -> bool: 430 return config in SF_LEGACY_FLAGS_MAP 431 432 def command(self) -> str: 433 legacy_flags = 0 434 for flag in self.flags: 435 legacy_flags |= SF_LEGACY_FLAGS_MAP[flag] 436 437 perfetto_flags = "\n".join([f"""trace_flags: {SF_PERFETTO_FLAGS_MAP[flag]}""" for flag in self.flags]) 438 439 return f""" 440{PERFETTO_UTILS} 441 442if is_perfetto_data_source_available android.surfaceflinger.layers; then 443 cat << EOF > {PERFETTO_SF_CONFIG_FILE} 444data_sources: {{ 445 config {{ 446 name: "android.surfaceflinger.layers" 447 surfaceflinger_layers_config: {{ 448 mode: MODE_ACTIVE 449 {perfetto_flags} 450 }} 451 }} 452}} 453EOF 454 echo 'SF trace (perfetto) configured.' 455else 456 su root service call SurfaceFlinger 1033 i32 {legacy_flags} 457 echo 'SF trace (legacy) configured' 458fi 459""" 460 461class SurfaceFlingerTraceSelectedConfig: 462 """Handles optional selected configuration for surfaceflinger traces. 463 """ 464 465 def __init__(self) -> None: 466 # defaults set for all configs 467 self.selectedConfigs = { 468 "sfbuffersize": "16000" 469 } 470 471 def add(self, configType, configValue) -> None: 472 self.selectedConfigs[configType] = configValue 473 474 def is_valid(self, configType) -> bool: 475 return configType in CONFIG_SF_SELECTION 476 477 def setBufferSize(self) -> str: 478 return f'su root service call SurfaceFlinger 1029 i32 {self.selectedConfigs["sfbuffersize"]}' 479 480class WindowManagerTraceSelectedConfig: 481 """Handles optional selected configuration for windowmanager traces. 482 """ 483 484 def __init__(self) -> None: 485 # defaults set for all configs 486 self.selectedConfigs = { 487 "wmbuffersize": "16000", 488 "tracinglevel": "debug", 489 "tracingtype": "frame", 490 } 491 492 def add(self, configType, configValue) -> None: 493 self.selectedConfigs[configType] = configValue 494 495 def is_valid(self, configType) -> bool: 496 return configType in CONFIG_WM_SELECTION 497 498 def setBufferSize(self) -> str: 499 return f'su root cmd window tracing size {self.selectedConfigs["wmbuffersize"]}' 500 501 def setTracingLevel(self) -> str: 502 return f'su root cmd window tracing level {self.selectedConfigs["tracinglevel"]}' 503 504 def setTracingType(self) -> str: 505 return f'su root cmd window tracing {self.selectedConfigs["tracingtype"]}' 506 507 508SF_LEGACY_FLAGS_MAP = { 509 "input": 1 << 1, 510 "composition": 1 << 2, 511 "metadata": 1 << 3, 512 "hwc": 1 << 4, 513 "tracebuffers": 1 << 5, 514 "virtualdisplays": 1 << 6 515} 516 517SF_PERFETTO_FLAGS_MAP = { 518 "input": "TRACE_FLAG_INPUT", 519 "composition": "TRACE_FLAG_COMPOSITION", 520 "metadata": "TRACE_FLAG_EXTRA", 521 "hwc": "TRACE_FLAG_HWC", 522 "tracebuffers": "TRACE_FLAG_BUFFERS", 523 "virtualdisplays": "TRACE_FLAG_VIRTUAL_DISPLAYS", 524} 525 526#Keep up to date with options in trace_collection_utils.ts 527CONFIG_SF_SELECTION = [ 528 "sfbuffersize", 529] 530 531#Keep up to date with options in trace_collection_utils.ts 532CONFIG_WM_SELECTION = [ 533 "wmbuffersize", 534 "tracingtype", 535 "tracinglevel", 536] 537 538class DumpTarget: 539 """Defines a single parameter to trace. 540 541 Attributes: 542 file: the path on the device the dump results are saved to. 543 dump_command: command to dump state to file. 544 """ 545 546 def __init__(self, files, dump_command: str) -> None: 547 if type(files) is not list: 548 files = [files] 549 self.files = files 550 self.dump_command = dump_command 551 552 553DUMP_TARGETS = { 554 "window_dump": DumpTarget( 555 File(f'/data/local/tmp/wm_dump{WINSCOPE_EXT}', "window_dump"), 556 f'su root dumpsys window --proto > /data/local/tmp/wm_dump{WINSCOPE_EXT}' 557 ), 558 559 "layers_dump": DumpTarget( 560 File(f'/data/local/tmp/sf_dump{WINSCOPE_EXT}', "layers_dump"), 561 f""" 562if is_perfetto_data_source_available android.surfaceflinger.layers; then 563 cat << EOF >> {PERFETTO_DUMP_CONFIG_FILE} 564data_sources: {{ 565 config {{ 566 name: "android.surfaceflinger.layers" 567 surfaceflinger_layers_config: {{ 568 mode: MODE_DUMP 569 trace_flags: TRACE_FLAG_INPUT 570 trace_flags: TRACE_FLAG_COMPOSITION 571 trace_flags: TRACE_FLAG_HWC 572 trace_flags: TRACE_FLAG_BUFFERS 573 trace_flags: TRACE_FLAG_VIRTUAL_DISPLAYS 574 }} 575 }} 576}} 577EOF 578 echo 'SF transactions trace (perfetto) configured to start along the other perfetto traces' 579else 580 su root dumpsys SurfaceFlinger --proto > /data/local/tmp/sf_dump{WINSCOPE_EXT} 581fi 582""" 583 ), 584 585 "screenshot": DumpTarget( 586 File("/data/local/tmp/screenshot.png", "screenshot.png"), 587 "screencap -p > /data/local/tmp/screenshot.png" 588 ), 589 590 "perfetto_dump": DumpTarget( 591 File(PERFETTO_DUMP_FILE, "dump.perfetto-trace"), 592 f""" 593if is_any_perfetto_data_source_available; then 594 cat << EOF >> {PERFETTO_DUMP_CONFIG_FILE} 595buffers: {{ 596 size_kb: 50000 597 fill_policy: RING_BUFFER 598}} 599duration_ms: 1 600EOF 601 602 rm -f {PERFETTO_DUMP_FILE} 603 perfetto --out {PERFETTO_DUMP_FILE} --txt --config {PERFETTO_DUMP_CONFIG_FILE} 604 echo 'Recorded perfetto dump' 605fi 606 """ 607 ) 608} 609 610 611# END OF CONFIG # 612 613 614def get_token() -> str: 615 """Returns saved proxy security token or creates new one""" 616 try: 617 with open(WINSCOPE_TOKEN_LOCATION, 'r') as token_file: 618 token = token_file.readline() 619 log.debug("Loaded token {} from {}".format( 620 token, WINSCOPE_TOKEN_LOCATION)) 621 return token 622 except IOError: 623 token = secrets.token_hex(32) 624 os.makedirs(os.path.dirname(WINSCOPE_TOKEN_LOCATION), exist_ok=True) 625 try: 626 with open(WINSCOPE_TOKEN_LOCATION, 'w') as token_file: 627 log.debug("Created and saved token {} to {}".format( 628 token, WINSCOPE_TOKEN_LOCATION)) 629 token_file.write(token) 630 os.chmod(WINSCOPE_TOKEN_LOCATION, 0o600) 631 except IOError: 632 log.error("Unable to save persistent token {} to {}".format( 633 token, WINSCOPE_TOKEN_LOCATION)) 634 return token 635 636 637class RequestType(Enum): 638 GET = 1 639 POST = 2 640 HEAD = 3 641 642 643def add_standard_headers(server): 644 server.send_header('Cache-Control', 'no-cache, no-store, must-revalidate') 645 server.send_header('Access-Control-Allow-Origin', '*') 646 server.send_header('Access-Control-Allow-Methods', 'POST, GET, OPTIONS') 647 server.send_header('Access-Control-Allow-Headers', 648 WINSCOPE_TOKEN_HEADER + ', Content-Type, Content-Length') 649 server.send_header('Access-Control-Expose-Headers', 650 'Winscope-Proxy-Version') 651 server.send_header(WINSCOPE_VERSION_HEADER, VERSION) 652 server.end_headers() 653 654 655class RequestEndpoint: 656 """Request endpoint to use with the RequestRouter.""" 657 658 @abstractmethod 659 def process(self, server, path): 660 pass 661 662 663class AdbError(Exception): 664 """Unsuccessful ADB operation""" 665 pass 666 667 668class BadRequest(Exception): 669 """Invalid client request""" 670 pass 671 672 673class RequestRouter: 674 """Handles HTTP request authentication and routing""" 675 676 def __init__(self, handler): 677 self.request = handler 678 self.endpoints = {} 679 680 def register_endpoint(self, method: RequestType, name: str, endpoint: RequestEndpoint): 681 self.endpoints[(method, name)] = endpoint 682 683 def __bad_request(self, error: str): 684 log.warning("Bad request: " + error) 685 self.request.respond(HTTPStatus.BAD_REQUEST, b"Bad request!\nThis is Winscope ADB proxy.\n\n" 686 + error.encode("utf-8"), 'text/txt') 687 688 def __internal_error(self, error: str): 689 log.error("Internal error: " + error) 690 self.request.respond(HTTPStatus.INTERNAL_SERVER_ERROR, 691 error.encode("utf-8"), 'text/txt') 692 693 def __bad_token(self): 694 log.info("Bad token") 695 self.request.respond(HTTPStatus.FORBIDDEN, b"Bad Winscope authorisation token!\nThis is Winscope ADB proxy.\n", 696 'text/txt') 697 698 def process(self, method: RequestType): 699 token = self.request.headers[WINSCOPE_TOKEN_HEADER] 700 if not token or token != secret_token: 701 return self.__bad_token() 702 path = self.request.path.strip('/').split('/') 703 if path and len(path) > 0: 704 endpoint_name = path[0] 705 try: 706 return self.endpoints[(method, endpoint_name)].process(self.request, path[1:]) 707 except KeyError: 708 return self.__bad_request("Unknown endpoint /{}/".format(endpoint_name)) 709 except AdbError as ex: 710 return self.__internal_error(str(ex)) 711 except BadRequest as ex: 712 return self.__bad_request(str(ex)) 713 except Exception as ex: 714 return self.__internal_error(repr(ex)) 715 self.__bad_request("No endpoint specified") 716 717 718def call_adb(params: str, device: str = None, stdin: bytes = None): 719 command = ['adb'] + (['-s', device] if device else []) + params.split(' ') 720 try: 721 log.debug("Call: " + ' '.join(command)) 722 return subprocess.check_output(command, stderr=subprocess.STDOUT, input=stdin).decode('utf-8') 723 except OSError as ex: 724 log.debug('Error executing adb command: {}\n{}'.format( 725 ' '.join(command), repr(ex))) 726 raise AdbError('Error executing adb command: {}\n{}'.format( 727 ' '.join(command), repr(ex))) 728 except subprocess.CalledProcessError as ex: 729 log.debug('Error executing adb command: {}\n{}'.format( 730 ' '.join(command), ex.output.decode("utf-8"))) 731 raise AdbError('Error executing adb command: adb {}\n{}'.format( 732 params, ex.output.decode("utf-8"))) 733 734 735def call_adb_outfile(params: str, outfile, device: str = None, stdin: bytes = None): 736 try: 737 process = subprocess.Popen(['adb'] + (['-s', device] if device else []) + params.split(' '), stdout=outfile, 738 stderr=subprocess.PIPE) 739 _, err = process.communicate(stdin) 740 outfile.seek(0) 741 if process.returncode != 0: 742 log.debug('Error executing adb command: adb {}\n'.format(params) + err.decode( 743 'utf-8') + '\n' + outfile.read().decode('utf-8')) 744 raise AdbError('Error executing adb command: adb {}\n'.format(params) + err.decode( 745 'utf-8') + '\n' + outfile.read().decode('utf-8')) 746 except OSError as ex: 747 log.debug('Error executing adb command: adb {}\n{}'.format( 748 params, repr(ex))) 749 raise AdbError( 750 'Error executing adb command: adb {}\n{}'.format(params, repr(ex))) 751 752 753class CheckWaylandServiceEndpoint(RequestEndpoint): 754 _listDevicesEndpoint = None 755 756 def __init__(self, listDevicesEndpoint): 757 self._listDevicesEndpoint = listDevicesEndpoint 758 759 def process(self, server, path): 760 self._listDevicesEndpoint.process(server, path) 761 foundDevices = self._listDevicesEndpoint._foundDevices 762 763 if len(foundDevices) > 1: 764 res = 'false' 765 else: 766 raw_res = call_adb('shell service check Wayland') 767 res = 'false' if 'not found' in raw_res else 'true' 768 server.respond(HTTPStatus.OK, res.encode("utf-8"), "text/json") 769 770 771class ListDevicesEndpoint(RequestEndpoint): 772 ADB_INFO_RE = re.compile("^([A-Za-z0-9._:\\-]+)\\s+(\\w+)(.*model:(\\w+))?") 773 _foundDevices = None 774 775 def process(self, server, path): 776 lines = list(filter(None, call_adb('devices -l').split('\n'))) 777 devices = {m.group(1): { 778 'authorised': str(m.group(2)) != 'unauthorized', 779 'model': m.group(4).replace('_', ' ') if m.group(4) else '' 780 } for m in [ListDevicesEndpoint.ADB_INFO_RE.match(d) for d in lines[1:]] if m} 781 self._foundDevices = devices 782 j = json.dumps(devices) 783 log.debug("Detected devices: " + j) 784 server.respond(HTTPStatus.OK, j.encode("utf-8"), "text/json") 785 786 787class DeviceRequestEndpoint(RequestEndpoint): 788 def process(self, server, path): 789 if len(path) > 0 and re.fullmatch("[A-Za-z0-9.:\\-]+", path[0]): 790 self.process_with_device(server, path[1:], path[0]) 791 else: 792 raise BadRequest("Device id not specified") 793 794 @abstractmethod 795 def process_with_device(self, server, path, device_id): 796 pass 797 798 def get_request(self, server) -> str: 799 try: 800 length = int(server.headers["Content-Length"]) 801 except KeyError as err: 802 raise BadRequest("Missing Content-Length header\n" + str(err)) 803 except ValueError as err: 804 raise BadRequest("Content length unreadable\n" + str(err)) 805 return json.loads(server.rfile.read(length).decode("utf-8")) 806 807 def move_perfetto_target_to_end_of_list(self, targets): 808 # Make sure a perfetto target (if present) comes last in the list of targets, i.e. will 809 # be processed last. 810 # A perfetto target must be processed last, so that perfetto tracing is started only after 811 # the other targets have been processed and have configured the perfetto config file. 812 def is_perfetto_target(target): 813 return target == TRACE_TARGETS["perfetto_trace"] or target == DUMP_TARGETS["perfetto_dump"] 814 non_perfetto_targets = [t for t in targets if not is_perfetto_target(t)] 815 perfetto_targets = [t for t in targets if is_perfetto_target(t)] 816 return non_perfetto_targets + perfetto_targets 817 818 819 820class FetchFilesEndpoint(DeviceRequestEndpoint): 821 def process_with_device(self, server, path, device_id): 822 if len(path) != 1: 823 raise BadRequest("File not specified") 824 if path[0] in TRACE_TARGETS: 825 files = TRACE_TARGETS[path[0]].files 826 elif path[0] in DUMP_TARGETS: 827 files = DUMP_TARGETS[path[0]].files 828 else: 829 raise BadRequest("Unknown file specified") 830 831 file_buffers = dict() 832 833 for f in files: 834 file_type = f.get_filetype() 835 file_paths = f.get_filepaths(device_id) 836 837 for file_path in file_paths: 838 with NamedTemporaryFile() as tmp: 839 log.debug( 840 f"Fetching file {file_path} from device to {tmp.name}") 841 call_adb_outfile('exec-out su root cat ' + 842 file_path, tmp, device_id) 843 log.debug(f"Deleting file {file_path} from device") 844 call_adb('shell su root rm -f ' + file_path, device_id) 845 log.debug(f"Uploading file {tmp.name}") 846 if file_type not in file_buffers: 847 file_buffers[file_type] = [] 848 buf = base64.encodebytes(tmp.read()).decode("utf-8") 849 file_buffers[file_type].append(buf) 850 851 if (len(file_buffers) == 0): 852 log.error("Proxy didn't find any file to fetch") 853 854 # server.send_header('X-Content-Type-Options', 'nosniff') 855 # add_standard_headers(server) 856 j = json.dumps(file_buffers) 857 server.respond(HTTPStatus.OK, j.encode("utf-8"), "text/json") 858 859 860def check_root(device_id): 861 log.debug("Checking root access on {}".format(device_id)) 862 return int(call_adb('shell su root id -u', device_id)) == 0 863 864 865TRACE_THREADS = {} 866 867 868class TraceThread(threading.Thread): 869 def __init__(self, device_id, command): 870 self._keep_alive_timer = None 871 self.trace_command = command 872 self._device_id = device_id 873 self.out = None, 874 self.err = None, 875 self._success = False 876 try: 877 shell = ['adb', '-s', self._device_id, 'shell'] 878 log.debug("Starting trace shell {}".format(' '.join(shell))) 879 self.process = subprocess.Popen(shell, stdout=subprocess.PIPE, 880 stderr=subprocess.PIPE, stdin=subprocess.PIPE, start_new_session=True) 881 except OSError as ex: 882 raise AdbError( 883 'Error executing adb command: adb shell\n{}'.format(repr(ex))) 884 885 super().__init__() 886 887 def timeout(self): 888 if self.is_alive(): 889 log.warning( 890 "Keep-alive timeout for trace on {}".format(self._device_id)) 891 self.end_trace() 892 if self._device_id in TRACE_THREADS: 893 TRACE_THREADS.pop(self._device_id) 894 895 def reset_timer(self): 896 log.debug( 897 "Resetting keep-alive clock for trace on {}".format(self._device_id)) 898 if self._keep_alive_timer: 899 self._keep_alive_timer.cancel() 900 self._keep_alive_timer = threading.Timer( 901 KEEP_ALIVE_INTERVAL_S, self.timeout) 902 self._keep_alive_timer.start() 903 904 def end_trace(self): 905 if self._keep_alive_timer: 906 self._keep_alive_timer.cancel() 907 log.debug("Sending SIGINT to the trace process on {}".format( 908 self._device_id)) 909 self.process.send_signal(signal.SIGINT) 910 try: 911 log.debug("Waiting for trace shell to exit for {}".format( 912 self._device_id)) 913 self.process.wait(timeout=5) 914 except TimeoutError: 915 log.debug( 916 "TIMEOUT - sending SIGKILL to the trace process on {}".format(self._device_id)) 917 self.process.kill() 918 self.join() 919 920 def run(self): 921 log.debug("Trace started on {}".format(self._device_id)) 922 self.reset_timer() 923 self.out, self.err = self.process.communicate(self.trace_command) 924 log.debug("Trace ended on {}, waiting for cleanup".format(self._device_id)) 925 time.sleep(0.2) 926 for i in range(50): 927 if call_adb("shell su root cat /data/local/tmp/winscope_status", device=self._device_id) == 'TRACE_OK\n': 928 call_adb( 929 "shell su root rm /data/local/tmp/winscope_status", device=self._device_id) 930 log.debug("Trace finished successfully on {}".format( 931 self._device_id)) 932 self._success = True 933 break 934 log.debug("Still waiting for cleanup on {}".format(self._device_id)) 935 time.sleep(0.1) 936 937 def success(self): 938 return self._success 939 940 941class StartTrace(DeviceRequestEndpoint): 942 TRACE_COMMAND = """ 943set -e 944 945{perfetto_utils} 946 947echo "Starting trace..." 948echo "TRACE_START" > /data/local/tmp/winscope_status 949 950# Do not print anything to stdout/stderr in the handler 951function stop_trace() {{ 952 echo "start" >/data/local/tmp/winscope_signal_handler.log 953 954 # redirect stdout/stderr to log file 955 exec 1>>/data/local/tmp/winscope_signal_handler.log 956 exec 2>>/data/local/tmp/winscope_signal_handler.log 957 958 set -x 959 trap - EXIT HUP INT 960 {stop_commands} 961 echo "TRACE_OK" > /data/local/tmp/winscope_status 962}} 963 964trap stop_trace EXIT HUP INT 965echo "Signal handler registered." 966 967# Clear perfetto config file. The start commands below are going to populate it. 968rm -f {perfetto_config_file} 969 970{start_commands} 971 972# ADB shell does not handle hung up well and does not call HUP handler when a child is active in foreground, 973# as a workaround we sleep for short intervals in a loop so the handler is called after a sleep interval. 974while true; do sleep 0.1; done 975""" 976 977 def process_with_device(self, server, path, device_id): 978 try: 979 requested_types = self.get_request(server) 980 log.debug(f"Clienting requested trace types {requested_types}") 981 requested_traces = [TRACE_TARGETS[t] for t in requested_types] 982 requested_traces = self.move_perfetto_target_to_end_of_list(requested_traces) 983 except KeyError as err: 984 raise BadRequest("Unsupported trace target\n" + str(err)) 985 if device_id in TRACE_THREADS: 986 log.warning("Trace already in progress for {}", device_id) 987 server.respond(HTTPStatus.OK, b'', "text/plain") 988 if not check_root(device_id): 989 raise AdbError( 990 "Unable to acquire root privileges on the device - check the output of 'adb -s {} shell su root id'".format( 991 device_id)) 992 command = StartTrace.TRACE_COMMAND.format( 993 perfetto_utils=PERFETTO_UTILS, 994 stop_commands='\n'.join([t.trace_stop for t in requested_traces]), 995 perfetto_config_file=PERFETTO_TRACE_CONFIG_FILE, 996 start_commands='\n'.join([t.trace_start for t in requested_traces])) 997 log.debug("Trace requested for {} with targets {}".format( 998 device_id, ','.join(requested_types))) 999 log.debug(f"Executing command \"{command}\" on {device_id}...") 1000 TRACE_THREADS[device_id] = TraceThread( 1001 device_id, command.encode('utf-8')) 1002 TRACE_THREADS[device_id].start() 1003 server.respond(HTTPStatus.OK, b'', "text/plain") 1004 1005 1006class EndTrace(DeviceRequestEndpoint): 1007 def process_with_device(self, server, path, device_id): 1008 if device_id not in TRACE_THREADS: 1009 raise BadRequest("No trace in progress for {}".format(device_id)) 1010 if TRACE_THREADS[device_id].is_alive(): 1011 TRACE_THREADS[device_id].end_trace() 1012 1013 success = TRACE_THREADS[device_id].success() 1014 1015 signal_handler_log = call_adb("shell su root cat /data/local/tmp/winscope_signal_handler.log", device=device_id).encode('utf-8') 1016 1017 out = b"### Shell script's stdout - start\n" + \ 1018 TRACE_THREADS[device_id].out + \ 1019 b"### Shell script's stdout - end\n" + \ 1020 b"### Shell script's stderr - start\n" + \ 1021 TRACE_THREADS[device_id].err + \ 1022 b"### Shell script's stderr - end\n" + \ 1023 b"### Signal handler log - start\n" + \ 1024 signal_handler_log + \ 1025 b"### Signal handler log - end\n" 1026 command = TRACE_THREADS[device_id].trace_command 1027 TRACE_THREADS.pop(device_id) 1028 if success: 1029 server.respond(HTTPStatus.OK, out, "text/plain") 1030 else: 1031 raise AdbError( 1032 "Error tracing the device\n### Output ###\n" + out.decode( 1033 "utf-8") + "\n### Command: adb -s {} shell ###\n### Input ###\n".format(device_id) + command.decode( 1034 "utf-8")) 1035 1036 1037def execute_command(server, device_id, shell, configType, configValue): 1038 process = subprocess.Popen(shell, stdout=subprocess.PIPE, stderr=subprocess.PIPE, 1039 stdin=subprocess.PIPE, start_new_session=True) 1040 log.debug(f"Changing trace config on device {device_id} {configType}:{configValue}") 1041 out, err = process.communicate(configValue.encode('utf-8')) 1042 if process.returncode != 0: 1043 raise AdbError( 1044 f"Error executing command:\n {configValue}\n\n### OUTPUT ###{out.decode('utf-8')}\n{err.decode('utf-8')}") 1045 log.debug(f"Changing trace config finished on device {device_id}") 1046 server.respond(HTTPStatus.OK, b'', "text/plain") 1047 1048 1049class ConfigTrace(DeviceRequestEndpoint): 1050 def process_with_device(self, server, path, device_id): 1051 try: 1052 requested_configs = self.get_request(server) 1053 config = SurfaceFlingerTraceConfig() 1054 for requested_config in requested_configs: 1055 if not config.is_valid(requested_config): 1056 raise BadRequest( 1057 f"Unsupported config {requested_config}\n") 1058 config.add(requested_config) 1059 except KeyError as err: 1060 raise BadRequest("Unsupported trace target\n" + str(err)) 1061 if device_id in TRACE_THREADS: 1062 BadRequest(f"Trace in progress for {device_id}") 1063 if not check_root(device_id): 1064 raise AdbError( 1065 f"Unable to acquire root privileges on the device - check the output of 'adb -s {device_id} shell su root id'") 1066 command = config.command() 1067 shell = ['adb', '-s', device_id, 'shell'] 1068 log.debug(f"Starting shell {' '.join(shell)}") 1069 execute_command(server, device_id, shell, "sf buffer size", command) 1070 1071 1072def add_selected_request_to_config(self, server, device_id, config): 1073 try: 1074 requested_configs = self.get_request(server) 1075 for requested_config in requested_configs: 1076 if config.is_valid(requested_config): 1077 config.add(requested_config, requested_configs[requested_config]) 1078 else: 1079 raise BadRequest( 1080 f"Unsupported config {requested_config}\n") 1081 except KeyError as err: 1082 raise BadRequest("Unsupported trace target\n" + str(err)) 1083 if device_id in TRACE_THREADS: 1084 BadRequest(f"Trace in progress for {device_id}") 1085 if not check_root(device_id): 1086 raise AdbError( 1087 f"Unable to acquire root privileges on the device - check the output of 'adb -s {device_id} shell su root id'") 1088 return config 1089 1090 1091class SurfaceFlingerSelectedConfigTrace(DeviceRequestEndpoint): 1092 def process_with_device(self, server, path, device_id): 1093 config = SurfaceFlingerTraceSelectedConfig() 1094 config = add_selected_request_to_config(self, server, device_id, config) 1095 setBufferSize = config.setBufferSize() 1096 shell = ['adb', '-s', device_id, 'shell'] 1097 log.debug(f"Starting shell {' '.join(shell)}") 1098 execute_command(server, device_id, shell, "sf buffer size", setBufferSize) 1099 1100 1101class WindowManagerSelectedConfigTrace(DeviceRequestEndpoint): 1102 def process_with_device(self, server, path, device_id): 1103 config = WindowManagerTraceSelectedConfig() 1104 config = add_selected_request_to_config(self, server, device_id, config) 1105 setBufferSize = config.setBufferSize() 1106 setTracingType = config.setTracingType() 1107 setTracingLevel = config.setTracingLevel() 1108 shell = ['adb', '-s', device_id, 'shell'] 1109 log.debug(f"Starting shell {' '.join(shell)}") 1110 execute_command(server, device_id, shell, "tracing type", setTracingType) 1111 execute_command(server, device_id, shell, "tracing level", setTracingLevel) 1112 # /!\ buffer size must be configured last 1113 # otherwise the other configurations might override it 1114 execute_command(server, device_id, shell, "wm buffer size", setBufferSize) 1115 1116 1117class StatusEndpoint(DeviceRequestEndpoint): 1118 def process_with_device(self, server, path, device_id): 1119 if device_id not in TRACE_THREADS: 1120 raise BadRequest("No trace in progress for {}".format(device_id)) 1121 TRACE_THREADS[device_id].reset_timer() 1122 server.respond(HTTPStatus.OK, str( 1123 TRACE_THREADS[device_id].is_alive()).encode("utf-8"), "text/plain") 1124 1125 1126class DumpEndpoint(DeviceRequestEndpoint): 1127 def process_with_device(self, server, path, device_id): 1128 try: 1129 requested_types = self.get_request(server) 1130 requested_traces = [DUMP_TARGETS[t] for t in requested_types] 1131 requested_traces = self.move_perfetto_target_to_end_of_list(requested_traces) 1132 except KeyError as err: 1133 raise BadRequest("Unsupported trace target\n" + str(err)) 1134 if device_id in TRACE_THREADS: 1135 BadRequest("Trace in progress for {}".format(device_id)) 1136 if not check_root(device_id): 1137 raise AdbError( 1138 "Unable to acquire root privileges on the device - check the output of 'adb -s {} shell su root id'" 1139 .format(device_id)) 1140 dump_commands = '\n'.join(t.dump_command for t in requested_traces) 1141 command = f""" 1142{PERFETTO_UTILS} 1143 1144# Clear perfetto config file. The commands below are going to populate it. 1145rm -f {PERFETTO_DUMP_CONFIG_FILE} 1146 1147{dump_commands} 1148""" 1149 shell = ['adb', '-s', device_id, 'shell'] 1150 log.debug("Starting dump shell {}".format(' '.join(shell))) 1151 process = subprocess.Popen(shell, stdout=subprocess.PIPE, stderr=subprocess.PIPE, 1152 stdin=subprocess.PIPE, start_new_session=True) 1153 log.debug("Starting dump on device {}".format(device_id)) 1154 out, err = process.communicate(command.encode('utf-8')) 1155 if process.returncode != 0: 1156 raise AdbError("Error executing command:\n" + command + "\n\n### OUTPUT ###" + out.decode('utf-8') + "\n" 1157 + err.decode('utf-8')) 1158 log.debug("Dump finished on device {}".format(device_id)) 1159 server.respond(HTTPStatus.OK, b'', "text/plain") 1160 1161 1162class ADBWinscopeProxy(BaseHTTPRequestHandler): 1163 def __init__(self, request, client_address, server): 1164 self.router = RequestRouter(self) 1165 listDevicesEndpoint = ListDevicesEndpoint() 1166 self.router.register_endpoint( 1167 RequestType.GET, "devices", listDevicesEndpoint) 1168 self.router.register_endpoint( 1169 RequestType.GET, "status", StatusEndpoint()) 1170 self.router.register_endpoint( 1171 RequestType.GET, "fetch", FetchFilesEndpoint()) 1172 self.router.register_endpoint(RequestType.POST, "start", StartTrace()) 1173 self.router.register_endpoint(RequestType.POST, "end", EndTrace()) 1174 self.router.register_endpoint(RequestType.POST, "dump", DumpEndpoint()) 1175 self.router.register_endpoint( 1176 RequestType.POST, "configtrace", ConfigTrace()) 1177 self.router.register_endpoint( 1178 RequestType.POST, "selectedsfconfigtrace", SurfaceFlingerSelectedConfigTrace()) 1179 self.router.register_endpoint( 1180 RequestType.POST, "selectedwmconfigtrace", WindowManagerSelectedConfigTrace()) 1181 self.router.register_endpoint( 1182 RequestType.GET, "checkwayland", CheckWaylandServiceEndpoint(listDevicesEndpoint)) 1183 super().__init__(request, client_address, server) 1184 1185 def respond(self, code: int, data: bytes, mime: str) -> None: 1186 self.send_response(code) 1187 self.send_header('Content-type', mime) 1188 add_standard_headers(self) 1189 self.wfile.write(data) 1190 1191 def do_GET(self): 1192 self.router.process(RequestType.GET) 1193 1194 def do_POST(self): 1195 self.router.process(RequestType.POST) 1196 1197 def do_OPTIONS(self): 1198 self.send_response(HTTPStatus.OK) 1199 self.send_header('Allow', 'GET,POST') 1200 add_standard_headers(self) 1201 self.end_headers() 1202 self.wfile.write(b'GET,POST') 1203 1204 def log_request(self, code='-', size='-'): 1205 log.info('{} {} {}'.format(self.requestline, str(code), str(size))) 1206 1207 1208if __name__ == '__main__': 1209 args = create_argument_parser().parse_args() 1210 1211 logging.basicConfig(stream=sys.stderr, level=args.loglevel, 1212 format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') 1213 1214 log = logging.getLogger("ADBProxy") 1215 secret_token = get_token() 1216 1217 print("Winscope ADB Connect proxy version: " + VERSION) 1218 print('Winscope token: ' + secret_token) 1219 1220 httpd = HTTPServer(('localhost', args.port), ADBWinscopeProxy) 1221 try: 1222 httpd.serve_forever() 1223 except KeyboardInterrupt: 1224 log.info("Shutting down") 1225