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 json 27import logging 28import os 29import re 30import secrets 31import signal 32import subprocess 33import sys 34import threading 35import time 36from abc import abstractmethod 37from enum import Enum 38from http import HTTPStatus 39from http.server import HTTPServer, BaseHTTPRequestHandler 40from tempfile import NamedTemporaryFile 41import base64 42 43# CONFIG # 44 45LOG_LEVEL = logging.DEBUG 46 47PORT = 5544 48 49# Keep in sync with ProxyClient#VERSION in Winscope 50VERSION = '1.0' 51 52WINSCOPE_VERSION_HEADER = "Winscope-Proxy-Version" 53WINSCOPE_TOKEN_HEADER = "Winscope-Token" 54 55# Location to save the proxy security token 56WINSCOPE_TOKEN_LOCATION = os.path.expanduser('~/.config/winscope/.token') 57 58# Winscope traces extensions 59WINSCOPE_EXT = ".winscope" 60WINSCOPE_EXT_LEGACY = ".pb" 61WINSCOPE_EXTS = [WINSCOPE_EXT, WINSCOPE_EXT_LEGACY] 62 63# Winscope traces directory 64WINSCOPE_DIR = "/data/misc/wmtrace/" 65 66# Max interval between the client keep-alive requests in seconds 67KEEP_ALIVE_INTERVAL_S = 5 68 69logging.basicConfig(stream=sys.stderr, level=LOG_LEVEL, 70 format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') 71log = logging.getLogger("ADBProxy") 72 73 74class File: 75 def __init__(self, file, filetype) -> None: 76 self.file = file 77 self.type = filetype 78 79 def get_filepaths(self, device_id): 80 return [self.file] 81 82 def get_filetype(self): 83 return self.type 84 85 86class FileMatcher: 87 def __init__(self, path, matcher, filetype) -> None: 88 self.path = path 89 self.matcher = matcher 90 self.type = filetype 91 92 def get_filepaths(self, device_id): 93 matchingFiles = call_adb( 94 f"shell su root find {self.path} -name {self.matcher}", device_id) 95 96 log.debug("Found file %s", matchingFiles.split('\n')[:-1]) 97 return matchingFiles.split('\n')[:-1] 98 99 def get_filetype(self): 100 return self.type 101 102 103class WinscopeFileMatcher(FileMatcher): 104 def __init__(self, path, matcher, filetype) -> None: 105 self.path = path 106 self.internal_matchers = list(map(lambda ext: FileMatcher(path, f'{matcher}{ext}', filetype), 107 WINSCOPE_EXTS)) 108 self.type = filetype 109 110 def get_filepaths(self, device_id): 111 for matcher in self.internal_matchers: 112 files = matcher.get_filepaths(device_id) 113 if len(files) > 0: 114 return files 115 log.debug("No files found") 116 return [] 117 118 119class TraceTarget: 120 """Defines a single parameter to trace. 121 122 Attributes: 123 file_matchers: the matchers used to identify the paths on the device the trace results are saved to. 124 trace_start: command to start the trace from adb shell, must not block. 125 trace_stop: command to stop the trace, should block until the trace is stopped. 126 """ 127 128 def __init__(self, files, trace_start: str, trace_stop: str) -> None: 129 if type(files) is not list: 130 files = [files] 131 self.files = files 132 self.trace_start = trace_start 133 self.trace_stop = trace_stop 134 135# Order of files matters as they will be expected in that order and decoded in that order 136TRACE_TARGETS = { 137 "window_trace": TraceTarget( 138 WinscopeFileMatcher(WINSCOPE_DIR, "wm_trace", "window_trace"), 139 'su root cmd window tracing start\necho "WM trace started."', 140 'su root cmd window tracing stop >/dev/null 2>&1' 141 ), 142 "accessibility_trace": TraceTarget( 143 WinscopeFileMatcher("/data/misc/a11ytrace", "a11y_trace", "accessibility_trace"), 144 'su root cmd accessibility start-trace\necho "Accessibility trace started."', 145 'su root cmd accessibility stop-trace >/dev/null 2>&1' 146 ), 147 "layers_trace": TraceTarget( 148 WinscopeFileMatcher(WINSCOPE_DIR, "layers_trace", "layers_trace"), 149 'su root service call SurfaceFlinger 1025 i32 1\necho "SF trace started."', 150 'su root service call SurfaceFlinger 1025 i32 0 >/dev/null 2>&1' 151 ), 152 "screen_recording": TraceTarget( 153 File(f'/data/local/tmp/screen.mp4', "screen_recording"), 154 f'screenrecord --bit-rate 8M /data/local/tmp/screen.mp4 >/dev/null 2>&1 &\necho "ScreenRecorder started."', 155 'pkill -l SIGINT screenrecord >/dev/null 2>&1' 156 ), 157 "transactions": TraceTarget( 158 WinscopeFileMatcher(WINSCOPE_DIR, "transactions_trace", "transactions"), 159 'su root service call SurfaceFlinger 1041 i32 1\necho "SF transactions recording started."', 160 'su root service call SurfaceFlinger 1041 i32 0 >/dev/null 2>&1' 161 ), 162 "transactions_legacy": TraceTarget( 163 [ 164 WinscopeFileMatcher(WINSCOPE_DIR, "transaction_trace", "transactions_legacy"), 165 FileMatcher(WINSCOPE_DIR, f'transaction_merges_*', "transaction_merges"), 166 ], 167 'su root service call SurfaceFlinger 1020 i32 1\necho "SF transactions recording started."', 168 'su root service call SurfaceFlinger 1020 i32 0 >/dev/null 2>&1' 169 ), 170 "proto_log": TraceTarget( 171 WinscopeFileMatcher(WINSCOPE_DIR, "wm_log", "proto_log"), 172 'su root cmd window logging start\necho "WM logging started."', 173 'su root cmd window logging stop >/dev/null 2>&1' 174 ), 175 "ime_trace_clients": TraceTarget( 176 WinscopeFileMatcher(WINSCOPE_DIR, "ime_trace_clients", "ime_trace_clients"), 177 'su root ime tracing start\necho "Clients IME trace started."', 178 'su root ime tracing stop >/dev/null 2>&1' 179 ), 180 "ime_trace_service": TraceTarget( 181 WinscopeFileMatcher(WINSCOPE_DIR, "ime_trace_service", "ime_trace_service"), 182 'su root ime tracing start\necho "Service IME trace started."', 183 'su root ime tracing stop >/dev/null 2>&1' 184 ), 185 "ime_trace_managerservice": TraceTarget( 186 WinscopeFileMatcher(WINSCOPE_DIR, "ime_trace_managerservice", "ime_trace_managerservice"), 187 'su root ime tracing start\necho "ManagerService IME trace started."', 188 'su root ime tracing stop >/dev/null 2>&1' 189 ), 190 "wayland_trace": TraceTarget( 191 WinscopeFileMatcher("/data/misc/wltrace", "wl_trace", "wl_trace"), 192 'su root service call Wayland 26 i32 1 >/dev/null\necho "Wayland trace started."', 193 'su root service call Wayland 26 i32 0 >/dev/null' 194 ), 195 "eventlog": TraceTarget( 196 WinscopeFileMatcher("/data/local/tmp", "eventlog", "eventlog"), 197 'rm -f /data/local/tmp/eventlog.winscope && EVENT_LOG_TRACING_START_TIME=$EPOCHREALTIME\necho "Event Log trace started."', 198 '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', 199 ), 200 "transition_traces": TraceTarget( 201 [WinscopeFileMatcher(WINSCOPE_DIR, "wm_transition_trace", "wm_transition_trace"), 202 WinscopeFileMatcher(WINSCOPE_DIR, "shell_transition_trace", "shell_transition_trace")], 203 'su root cmd window shell tracing start && su root dumpsys activity service SystemUIService WMShell transitions tracing start\necho "Transition traces started."', 204 'su root cmd window shell tracing stop && su root dumpsys activity service SystemUIService WMShell transitions tracing stop >/dev/null 2>&1' 205 ), 206} 207 208 209class SurfaceFlingerTraceConfig: 210 """Handles optional configuration for surfaceflinger traces. 211 """ 212 213 def __init__(self) -> None: 214 self.flags = 0 215 216 def add(self, config: str) -> None: 217 self.flags |= CONFIG_FLAG[config] 218 219 def is_valid(self, config: str) -> bool: 220 return config in CONFIG_FLAG 221 222 def command(self) -> str: 223 return f'su root service call SurfaceFlinger 1033 i32 {self.flags}' 224 225class SurfaceFlingerTraceSelectedConfig: 226 """Handles optional selected configuration for surfaceflinger traces. 227 """ 228 229 def __init__(self) -> None: 230 # defaults set for all configs 231 self.selectedConfigs = { 232 "sfbuffersize": "16000" 233 } 234 235 def add(self, configType, configValue) -> None: 236 self.selectedConfigs[configType] = configValue 237 238 def is_valid(self, configType) -> bool: 239 return configType in CONFIG_SF_SELECTION 240 241 def setBufferSize(self) -> str: 242 return f'su root service call SurfaceFlinger 1029 i32 {self.selectedConfigs["sfbuffersize"]}' 243 244class WindowManagerTraceSelectedConfig: 245 """Handles optional selected configuration for windowmanager traces. 246 """ 247 248 def __init__(self) -> None: 249 # defaults set for all configs 250 self.selectedConfigs = { 251 "wmbuffersize": "16000", 252 "tracinglevel": "debug", 253 "tracingtype": "frame", 254 } 255 256 def add(self, configType, configValue) -> None: 257 self.selectedConfigs[configType] = configValue 258 259 def is_valid(self, configType) -> bool: 260 return configType in CONFIG_WM_SELECTION 261 262 def setBufferSize(self) -> str: 263 return f'su root cmd window tracing size {self.selectedConfigs["wmbuffersize"]}' 264 265 def setTracingLevel(self) -> str: 266 return f'su root cmd window tracing level {self.selectedConfigs["tracinglevel"]}' 267 268 def setTracingType(self) -> str: 269 return f'su root cmd window tracing {self.selectedConfigs["tracingtype"]}' 270 271 272CONFIG_FLAG = { 273 "input": 1 << 1, 274 "composition": 1 << 2, 275 "metadata": 1 << 3, 276 "hwc": 1 << 4, 277 "tracebuffers": 1 << 5, 278 "virtualdisplays": 1 << 6 279} 280 281#Keep up to date with options in DataAdb.vue 282CONFIG_SF_SELECTION = [ 283 "sfbuffersize", 284] 285 286#Keep up to date with options in DataAdb.vue 287CONFIG_WM_SELECTION = [ 288 "wmbuffersize", 289 "tracingtype", 290 "tracinglevel", 291] 292 293class DumpTarget: 294 """Defines a single parameter to trace. 295 296 Attributes: 297 file: the path on the device the dump results are saved to. 298 dump_command: command to dump state to file. 299 """ 300 301 def __init__(self, files, dump_command: str) -> None: 302 if type(files) is not list: 303 files = [files] 304 self.files = files 305 self.dump_command = dump_command 306 307 308DUMP_TARGETS = { 309 "window_dump": DumpTarget( 310 File(f'/data/local/tmp/wm_dump{WINSCOPE_EXT}', "window_dump"), 311 f'su root dumpsys window --proto > /data/local/tmp/wm_dump{WINSCOPE_EXT}' 312 ), 313 "layers_dump": DumpTarget( 314 File(f'/data/local/tmp/sf_dump{WINSCOPE_EXT}', "layers_dump"), 315 f'su root dumpsys SurfaceFlinger --proto > /data/local/tmp/sf_dump{WINSCOPE_EXT}' 316 ) 317} 318 319 320# END OF CONFIG # 321 322 323def get_token() -> str: 324 """Returns saved proxy security token or creates new one""" 325 try: 326 with open(WINSCOPE_TOKEN_LOCATION, 'r') as token_file: 327 token = token_file.readline() 328 log.debug("Loaded token {} from {}".format( 329 token, WINSCOPE_TOKEN_LOCATION)) 330 return token 331 except IOError: 332 token = secrets.token_hex(32) 333 os.makedirs(os.path.dirname(WINSCOPE_TOKEN_LOCATION), exist_ok=True) 334 try: 335 with open(WINSCOPE_TOKEN_LOCATION, 'w') as token_file: 336 log.debug("Created and saved token {} to {}".format( 337 token, WINSCOPE_TOKEN_LOCATION)) 338 token_file.write(token) 339 os.chmod(WINSCOPE_TOKEN_LOCATION, 0o600) 340 except IOError: 341 log.error("Unable to save persistent token {} to {}".format( 342 token, WINSCOPE_TOKEN_LOCATION)) 343 return token 344 345 346secret_token = get_token() 347 348 349class RequestType(Enum): 350 GET = 1 351 POST = 2 352 HEAD = 3 353 354 355def add_standard_headers(server): 356 server.send_header('Cache-Control', 'no-cache, no-store, must-revalidate') 357 server.send_header('Access-Control-Allow-Origin', '*') 358 server.send_header('Access-Control-Allow-Methods', 'POST, GET, OPTIONS') 359 server.send_header('Access-Control-Allow-Headers', 360 WINSCOPE_TOKEN_HEADER + ', Content-Type, Content-Length') 361 server.send_header('Access-Control-Expose-Headers', 362 'Winscope-Proxy-Version') 363 server.send_header(WINSCOPE_VERSION_HEADER, VERSION) 364 server.end_headers() 365 366 367class RequestEndpoint: 368 """Request endpoint to use with the RequestRouter.""" 369 370 @abstractmethod 371 def process(self, server, path): 372 pass 373 374 375class AdbError(Exception): 376 """Unsuccessful ADB operation""" 377 pass 378 379 380class BadRequest(Exception): 381 """Invalid client request""" 382 pass 383 384 385class RequestRouter: 386 """Handles HTTP request authentication and routing""" 387 388 def __init__(self, handler): 389 self.request = handler 390 self.endpoints = {} 391 392 def register_endpoint(self, method: RequestType, name: str, endpoint: RequestEndpoint): 393 self.endpoints[(method, name)] = endpoint 394 395 def __bad_request(self, error: str): 396 log.warning("Bad request: " + error) 397 self.request.respond(HTTPStatus.BAD_REQUEST, b"Bad request!\nThis is Winscope ADB proxy.\n\n" 398 + error.encode("utf-8"), 'text/txt') 399 400 def __internal_error(self, error: str): 401 log.error("Internal error: " + error) 402 self.request.respond(HTTPStatus.INTERNAL_SERVER_ERROR, 403 error.encode("utf-8"), 'text/txt') 404 405 def __bad_token(self): 406 log.info("Bad token") 407 self.request.respond(HTTPStatus.FORBIDDEN, b"Bad Winscope authorisation token!\nThis is Winscope ADB proxy.\n", 408 'text/txt') 409 410 def process(self, method: RequestType): 411 token = self.request.headers[WINSCOPE_TOKEN_HEADER] 412 if not token or token != secret_token: 413 return self.__bad_token() 414 path = self.request.path.strip('/').split('/') 415 if path and len(path) > 0: 416 endpoint_name = path[0] 417 try: 418 return self.endpoints[(method, endpoint_name)].process(self.request, path[1:]) 419 except KeyError: 420 return self.__bad_request("Unknown endpoint /{}/".format(endpoint_name)) 421 except AdbError as ex: 422 return self.__internal_error(str(ex)) 423 except BadRequest as ex: 424 return self.__bad_request(str(ex)) 425 except Exception as ex: 426 return self.__internal_error(repr(ex)) 427 self.__bad_request("No endpoint specified") 428 429 430def call_adb(params: str, device: str = None, stdin: bytes = None): 431 command = ['adb'] + (['-s', device] if device else []) + params.split(' ') 432 try: 433 log.debug("Call: " + ' '.join(command)) 434 return subprocess.check_output(command, stderr=subprocess.STDOUT, input=stdin).decode('utf-8') 435 except OSError as ex: 436 log.debug('Error executing adb command: {}\n{}'.format( 437 ' '.join(command), repr(ex))) 438 raise AdbError('Error executing adb command: {}\n{}'.format( 439 ' '.join(command), repr(ex))) 440 except subprocess.CalledProcessError as ex: 441 log.debug('Error executing adb command: {}\n{}'.format( 442 ' '.join(command), ex.output.decode("utf-8"))) 443 raise AdbError('Error executing adb command: adb {}\n{}'.format( 444 params, ex.output.decode("utf-8"))) 445 446 447def call_adb_outfile(params: str, outfile, device: str = None, stdin: bytes = None): 448 try: 449 process = subprocess.Popen(['adb'] + (['-s', device] if device else []) + params.split(' '), stdout=outfile, 450 stderr=subprocess.PIPE) 451 _, err = process.communicate(stdin) 452 outfile.seek(0) 453 if process.returncode != 0: 454 log.debug('Error executing adb command: adb {}\n'.format(params) + err.decode( 455 'utf-8') + '\n' + outfile.read().decode('utf-8')) 456 raise AdbError('Error executing adb command: adb {}\n'.format(params) + err.decode( 457 'utf-8') + '\n' + outfile.read().decode('utf-8')) 458 except OSError as ex: 459 log.debug('Error executing adb command: adb {}\n{}'.format( 460 params, repr(ex))) 461 raise AdbError( 462 'Error executing adb command: adb {}\n{}'.format(params, repr(ex))) 463 464 465class CheckWaylandServiceEndpoint(RequestEndpoint): 466 _listDevicesEndpoint = None 467 468 def __init__(self, listDevicesEndpoint): 469 self._listDevicesEndpoint = listDevicesEndpoint 470 471 def process(self, server, path): 472 self._listDevicesEndpoint.process(server, path) 473 foundDevices = self._listDevicesEndpoint._foundDevices 474 475 if len(foundDevices) > 1: 476 res = 'false' 477 else: 478 raw_res = call_adb('shell service check Wayland') 479 res = 'false' if 'not found' in raw_res else 'true' 480 server.respond(HTTPStatus.OK, res.encode("utf-8"), "text/json") 481 482 483class ListDevicesEndpoint(RequestEndpoint): 484 ADB_INFO_RE = re.compile("^([A-Za-z0-9.:\\-]+)\\s+(\\w+)(.*model:(\\w+))?") 485 _foundDevices = None 486 487 def process(self, server, path): 488 lines = list(filter(None, call_adb('devices -l').split('\n'))) 489 devices = {m.group(1): { 490 'authorised': str(m.group(2)) != 'unauthorized', 491 'model': m.group(4).replace('_', ' ') if m.group(4) else '' 492 } for m in [ListDevicesEndpoint.ADB_INFO_RE.match(d) for d in lines[1:]] if m} 493 self._foundDevices = devices 494 j = json.dumps(devices) 495 log.debug("Detected devices: " + j) 496 server.respond(HTTPStatus.OK, j.encode("utf-8"), "text/json") 497 498 499class DeviceRequestEndpoint(RequestEndpoint): 500 def process(self, server, path): 501 if len(path) > 0 and re.fullmatch("[A-Za-z0-9.:\\-]+", path[0]): 502 self.process_with_device(server, path[1:], path[0]) 503 else: 504 raise BadRequest("Device id not specified") 505 506 @abstractmethod 507 def process_with_device(self, server, path, device_id): 508 pass 509 510 def get_request(self, server) -> str: 511 try: 512 length = int(server.headers["Content-Length"]) 513 except KeyError as err: 514 raise BadRequest("Missing Content-Length header\n" + str(err)) 515 except ValueError as err: 516 raise BadRequest("Content length unreadable\n" + str(err)) 517 return json.loads(server.rfile.read(length).decode("utf-8")) 518 519 520class FetchFilesEndpoint(DeviceRequestEndpoint): 521 def process_with_device(self, server, path, device_id): 522 if len(path) != 1: 523 raise BadRequest("File not specified") 524 if path[0] in TRACE_TARGETS: 525 files = TRACE_TARGETS[path[0]].files 526 elif path[0] in DUMP_TARGETS: 527 files = DUMP_TARGETS[path[0]].files 528 else: 529 raise BadRequest("Unknown file specified") 530 531 file_buffers = dict() 532 533 for f in files: 534 file_type = f.get_filetype() 535 file_paths = f.get_filepaths(device_id) 536 537 for file_path in file_paths: 538 with NamedTemporaryFile() as tmp: 539 log.debug( 540 f"Fetching file {file_path} from device to {tmp.name}") 541 call_adb_outfile('exec-out su root cat ' + 542 file_path, tmp, device_id) 543 log.debug(f"Deleting file {file_path} from device") 544 call_adb('shell su root rm ' + file_path, device_id) 545 log.debug(f"Uploading file {tmp.name}") 546 if file_type not in file_buffers: 547 file_buffers[file_type] = [] 548 buf = base64.encodebytes(tmp.read()).decode("utf-8") 549 file_buffers[file_type].append(buf) 550 551 if (len(file_buffers) == 0): 552 log.error("Proxy didn't find any file to fetch") 553 554 # server.send_header('X-Content-Type-Options', 'nosniff') 555 # add_standard_headers(server) 556 j = json.dumps(file_buffers) 557 server.respond(HTTPStatus.OK, j.encode("utf-8"), "text/json") 558 559 560def check_root(device_id): 561 log.debug("Checking root access on {}".format(device_id)) 562 return int(call_adb('shell su root id -u', device_id)) == 0 563 564 565TRACE_THREADS = {} 566 567 568class TraceThread(threading.Thread): 569 def __init__(self, device_id, command): 570 self._keep_alive_timer = None 571 self.trace_command = command 572 self._device_id = device_id 573 self.out = None, 574 self.err = None, 575 self._success = False 576 try: 577 shell = ['adb', '-s', self._device_id, 'shell'] 578 log.debug("Starting trace shell {}".format(' '.join(shell))) 579 self.process = subprocess.Popen(shell, stdout=subprocess.PIPE, 580 stderr=subprocess.PIPE, stdin=subprocess.PIPE, start_new_session=True) 581 except OSError as ex: 582 raise AdbError( 583 'Error executing adb command: adb shell\n{}'.format(repr(ex))) 584 585 super().__init__() 586 587 def timeout(self): 588 if self.is_alive(): 589 log.warning( 590 "Keep-alive timeout for trace on {}".format(self._device_id)) 591 self.end_trace() 592 if self._device_id in TRACE_THREADS: 593 TRACE_THREADS.pop(self._device_id) 594 595 def reset_timer(self): 596 log.debug( 597 "Resetting keep-alive clock for trace on {}".format(self._device_id)) 598 if self._keep_alive_timer: 599 self._keep_alive_timer.cancel() 600 self._keep_alive_timer = threading.Timer( 601 KEEP_ALIVE_INTERVAL_S, self.timeout) 602 self._keep_alive_timer.start() 603 604 def end_trace(self): 605 if self._keep_alive_timer: 606 self._keep_alive_timer.cancel() 607 log.debug("Sending SIGINT to the trace process on {}".format( 608 self._device_id)) 609 self.process.send_signal(signal.SIGINT) 610 try: 611 log.debug("Waiting for trace shell to exit for {}".format( 612 self._device_id)) 613 self.process.wait(timeout=5) 614 except TimeoutError: 615 log.debug( 616 "TIMEOUT - sending SIGKILL to the trace process on {}".format(self._device_id)) 617 self.process.kill() 618 self.join() 619 620 def run(self): 621 log.debug("Trace started on {}".format(self._device_id)) 622 self.reset_timer() 623 self.out, self.err = self.process.communicate(self.trace_command) 624 log.debug("Trace ended on {}, waiting for cleanup".format(self._device_id)) 625 time.sleep(0.2) 626 for i in range(50): 627 if call_adb("shell su root cat /data/local/tmp/winscope_status", device=self._device_id) == 'TRACE_OK\n': 628 call_adb( 629 "shell su root rm /data/local/tmp/winscope_status", device=self._device_id) 630 log.debug("Trace finished successfully on {}".format( 631 self._device_id)) 632 self._success = True 633 break 634 log.debug("Still waiting for cleanup on {}".format(self._device_id)) 635 time.sleep(0.1) 636 637 def success(self): 638 return self._success 639 640 641class StartTrace(DeviceRequestEndpoint): 642 TRACE_COMMAND = """ 643set -e 644 645echo "Starting trace..." 646echo "TRACE_START" > /data/local/tmp/winscope_status 647 648# Do not print anything to stdout/stderr in the handler 649function stop_trace() {{ 650 echo "start" >/data/local/tmp/winscope_signal_handler.log 651 652 # redirect stdout/stderr to log file 653 exec 1>>/data/local/tmp/winscope_signal_handler.log 654 exec 2>>/data/local/tmp/winscope_signal_handler.log 655 656 set -x 657 trap - EXIT HUP INT 658 {} 659 echo "TRACE_OK" > /data/local/tmp/winscope_status 660}} 661 662trap stop_trace EXIT HUP INT 663echo "Signal handler registered." 664 665{} 666 667# ADB shell does not handle hung up well and does not call HUP handler when a child is active in foreground, 668# as a workaround we sleep for short intervals in a loop so the handler is called after a sleep interval. 669while true; do sleep 0.1; done 670""" 671 672 def process_with_device(self, server, path, device_id): 673 try: 674 requested_types = self.get_request(server) 675 log.debug(f"Clienting requested trace types {requested_types}") 676 requested_traces = [TRACE_TARGETS[t] for t in requested_types] 677 except KeyError as err: 678 raise BadRequest("Unsupported trace target\n" + str(err)) 679 if device_id in TRACE_THREADS: 680 log.warning("Trace already in progress for {}", device_id) 681 server.respond(HTTPStatus.OK, b'', "text/plain") 682 if not check_root(device_id): 683 raise AdbError( 684 "Unable to acquire root privileges on the device - check the output of 'adb -s {} shell su root id'".format( 685 device_id)) 686 command = StartTrace.TRACE_COMMAND.format( 687 '\n'.join([t.trace_stop for t in requested_traces]), 688 '\n'.join([t.trace_start for t in requested_traces])) 689 log.debug("Trace requested for {} with targets {}".format( 690 device_id, ','.join(requested_types))) 691 log.debug(f"Executing command \"{command}\" on {device_id}...") 692 TRACE_THREADS[device_id] = TraceThread( 693 device_id, command.encode('utf-8')) 694 TRACE_THREADS[device_id].start() 695 server.respond(HTTPStatus.OK, b'', "text/plain") 696 697 698class EndTrace(DeviceRequestEndpoint): 699 def process_with_device(self, server, path, device_id): 700 if device_id not in TRACE_THREADS: 701 raise BadRequest("No trace in progress for {}".format(device_id)) 702 if TRACE_THREADS[device_id].is_alive(): 703 TRACE_THREADS[device_id].end_trace() 704 705 success = TRACE_THREADS[device_id].success() 706 707 signal_handler_log = call_adb("shell su root cat /data/local/tmp/winscope_signal_handler.log", device=device_id).encode('utf-8') 708 709 out = b"### Shell script's stdout - start\n" + \ 710 TRACE_THREADS[device_id].out + \ 711 b"### Shell script's stdout - end\n" + \ 712 b"### Shell script's stderr - start\n" + \ 713 TRACE_THREADS[device_id].err + \ 714 b"### Shell script's stderr - end\n" + \ 715 b"### Signal handler log - start\n" + \ 716 signal_handler_log + \ 717 b"### Signal handler log - end\n" 718 command = TRACE_THREADS[device_id].trace_command 719 TRACE_THREADS.pop(device_id) 720 if success: 721 server.respond(HTTPStatus.OK, out, "text/plain") 722 else: 723 raise AdbError( 724 "Error tracing the device\n### Output ###\n" + out.decode( 725 "utf-8") + "\n### Command: adb -s {} shell ###\n### Input ###\n".format(device_id) + command.decode( 726 "utf-8")) 727 728 729def execute_command(server, device_id, shell, configType, configValue): 730 process = subprocess.Popen(shell, stdout=subprocess.PIPE, stderr=subprocess.PIPE, 731 stdin=subprocess.PIPE, start_new_session=True) 732 log.debug(f"Changing trace config on device {device_id} {configType}:{configValue}") 733 out, err = process.communicate(configValue.encode('utf-8')) 734 if process.returncode != 0: 735 raise AdbError( 736 f"Error executing command:\n {configValue}\n\n### OUTPUT ###{out.decode('utf-8')}\n{err.decode('utf-8')}") 737 log.debug(f"Changing trace config finished on device {device_id}") 738 server.respond(HTTPStatus.OK, b'', "text/plain") 739 740 741class ConfigTrace(DeviceRequestEndpoint): 742 def process_with_device(self, server, path, device_id): 743 try: 744 requested_configs = self.get_request(server) 745 config = SurfaceFlingerTraceConfig() 746 for requested_config in requested_configs: 747 if not config.is_valid(requested_config): 748 raise BadRequest( 749 f"Unsupported config {requested_config}\n") 750 config.add(requested_config) 751 except KeyError as err: 752 raise BadRequest("Unsupported trace target\n" + str(err)) 753 if device_id in TRACE_THREADS: 754 BadRequest(f"Trace in progress for {device_id}") 755 if not check_root(device_id): 756 raise AdbError( 757 f"Unable to acquire root privileges on the device - check the output of 'adb -s {device_id} shell su root id'") 758 command = config.command() 759 shell = ['adb', '-s', device_id, 'shell'] 760 log.debug(f"Starting shell {' '.join(shell)}") 761 execute_command(server, device_id, shell, "sf buffer size", command) 762 763 764def add_selected_request_to_config(self, server, device_id, config): 765 try: 766 requested_configs = self.get_request(server) 767 for requested_config in requested_configs: 768 if config.is_valid(requested_config): 769 config.add(requested_config, requested_configs[requested_config]) 770 else: 771 raise BadRequest( 772 f"Unsupported config {requested_config}\n") 773 except KeyError as err: 774 raise BadRequest("Unsupported trace target\n" + str(err)) 775 if device_id in TRACE_THREADS: 776 BadRequest(f"Trace in progress for {device_id}") 777 if not check_root(device_id): 778 raise AdbError( 779 f"Unable to acquire root privileges on the device - check the output of 'adb -s {device_id} shell su root id'") 780 return config 781 782 783class SurfaceFlingerSelectedConfigTrace(DeviceRequestEndpoint): 784 def process_with_device(self, server, path, device_id): 785 config = SurfaceFlingerTraceSelectedConfig() 786 config = add_selected_request_to_config(self, server, device_id, config) 787 setBufferSize = config.setBufferSize() 788 shell = ['adb', '-s', device_id, 'shell'] 789 log.debug(f"Starting shell {' '.join(shell)}") 790 execute_command(server, device_id, shell, "sf buffer size", setBufferSize) 791 792 793class WindowManagerSelectedConfigTrace(DeviceRequestEndpoint): 794 def process_with_device(self, server, path, device_id): 795 config = WindowManagerTraceSelectedConfig() 796 config = add_selected_request_to_config(self, server, device_id, config) 797 setBufferSize = config.setBufferSize() 798 setTracingType = config.setTracingType() 799 setTracingLevel = config.setTracingLevel() 800 shell = ['adb', '-s', device_id, 'shell'] 801 log.debug(f"Starting shell {' '.join(shell)}") 802 execute_command(server, device_id, shell, "wm buffer size", setBufferSize) 803 execute_command(server, device_id, shell, "tracing type", setTracingType) 804 execute_command(server, device_id, shell, "tracing level", setTracingLevel) 805 806 807class StatusEndpoint(DeviceRequestEndpoint): 808 def process_with_device(self, server, path, device_id): 809 if device_id not in TRACE_THREADS: 810 raise BadRequest("No trace in progress for {}".format(device_id)) 811 TRACE_THREADS[device_id].reset_timer() 812 server.respond(HTTPStatus.OK, str( 813 TRACE_THREADS[device_id].is_alive()).encode("utf-8"), "text/plain") 814 815 816class DumpEndpoint(DeviceRequestEndpoint): 817 def process_with_device(self, server, path, device_id): 818 try: 819 requested_types = self.get_request(server) 820 requested_traces = [DUMP_TARGETS[t] for t in requested_types] 821 except KeyError as err: 822 raise BadRequest("Unsupported trace target\n" + str(err)) 823 if device_id in TRACE_THREADS: 824 BadRequest("Trace in progress for {}".format(device_id)) 825 if not check_root(device_id): 826 raise AdbError( 827 "Unable to acquire root privileges on the device - check the output of 'adb -s {} shell su root id'" 828 .format(device_id)) 829 command = '\n'.join(t.dump_command for t in requested_traces) 830 shell = ['adb', '-s', device_id, 'shell'] 831 log.debug("Starting dump shell {}".format(' '.join(shell))) 832 process = subprocess.Popen(shell, stdout=subprocess.PIPE, stderr=subprocess.PIPE, 833 stdin=subprocess.PIPE, start_new_session=True) 834 log.debug("Starting dump on device {}".format(device_id)) 835 out, err = process.communicate(command.encode('utf-8')) 836 if process.returncode != 0: 837 raise AdbError("Error executing command:\n" + command + "\n\n### OUTPUT ###" + out.decode('utf-8') + "\n" 838 + err.decode('utf-8')) 839 log.debug("Dump finished on device {}".format(device_id)) 840 server.respond(HTTPStatus.OK, b'', "text/plain") 841 842 843class ADBWinscopeProxy(BaseHTTPRequestHandler): 844 def __init__(self, request, client_address, server): 845 self.router = RequestRouter(self) 846 listDevicesEndpoint = ListDevicesEndpoint() 847 self.router.register_endpoint( 848 RequestType.GET, "devices", listDevicesEndpoint) 849 self.router.register_endpoint( 850 RequestType.GET, "status", StatusEndpoint()) 851 self.router.register_endpoint( 852 RequestType.GET, "fetch", FetchFilesEndpoint()) 853 self.router.register_endpoint(RequestType.POST, "start", StartTrace()) 854 self.router.register_endpoint(RequestType.POST, "end", EndTrace()) 855 self.router.register_endpoint(RequestType.POST, "dump", DumpEndpoint()) 856 self.router.register_endpoint( 857 RequestType.POST, "configtrace", ConfigTrace()) 858 self.router.register_endpoint( 859 RequestType.POST, "selectedsfconfigtrace", SurfaceFlingerSelectedConfigTrace()) 860 self.router.register_endpoint( 861 RequestType.POST, "selectedwmconfigtrace", WindowManagerSelectedConfigTrace()) 862 self.router.register_endpoint( 863 RequestType.GET, "checkwayland", CheckWaylandServiceEndpoint(listDevicesEndpoint)) 864 super().__init__(request, client_address, server) 865 866 def respond(self, code: int, data: bytes, mime: str) -> None: 867 self.send_response(code) 868 self.send_header('Content-type', mime) 869 add_standard_headers(self) 870 self.wfile.write(data) 871 872 def do_GET(self): 873 self.router.process(RequestType.GET) 874 875 def do_POST(self): 876 self.router.process(RequestType.POST) 877 878 def do_OPTIONS(self): 879 self.send_response(HTTPStatus.OK) 880 self.send_header('Allow', 'GET,POST') 881 add_standard_headers(self) 882 self.end_headers() 883 self.wfile.write(b'GET,POST') 884 885 def log_request(self, code='-', size='-'): 886 log.info('{} {} {}'.format(self.requestline, str(code), str(size))) 887 888 889if __name__ == '__main__': 890 print("Winscope ADB Connect proxy version: " + VERSION) 891 print('Winscope token: ' + secret_token) 892 httpd = HTTPServer(('localhost', PORT), ADBWinscopeProxy) 893 try: 894 httpd.serve_forever() 895 except KeyboardInterrupt: 896 log.info("Shutting down")