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