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# Max interval between the client keep-alive requests in seconds 59KEEP_ALIVE_INTERVAL_S = 5 60 61logging.basicConfig(stream=sys.stderr, level=LOG_LEVEL, 62 format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') 63log = logging.getLogger("ADBProxy") 64 65 66class File: 67 def __init__(self, file, filetype) -> None: 68 self.file = file 69 self.type = filetype 70 71 def get_filepaths(self, device_id): 72 return [self.file] 73 74 def get_filetype(self): 75 return self.type 76 77 78class FileMatcher: 79 def __init__(self, path, matcher, filetype) -> None: 80 self.path = path 81 self.matcher = matcher 82 self.type = filetype 83 84 def get_filepaths(self, device_id): 85 matchingFiles = call_adb( 86 f"shell su root find {self.path} -name {self.matcher}", device_id) 87 88 return matchingFiles.split('\n')[:-1] 89 90 def get_filetype(self): 91 return self.type 92 93 94class TraceTarget: 95 """Defines a single parameter to trace. 96 97 Attributes: 98 file_matchers: the matchers used to identify the paths on the device the trace results are saved to. 99 trace_start: command to start the trace from adb shell, must not block. 100 trace_stop: command to stop the trace, should block until the trace is stopped. 101 """ 102 103 def __init__(self, files, trace_start: str, trace_stop: str) -> None: 104 if type(files) is not list: 105 files = [files] 106 self.files = files 107 self.trace_start = trace_start 108 self.trace_stop = trace_stop 109 110 111# Order of files matters as they will be expected in that order and decoded in that order 112TRACE_TARGETS = { 113 "window_trace": TraceTarget( 114 File("/data/misc/wmtrace/wm_trace.pb", "window_trace"), 115 'su root cmd window tracing start\necho "WM trace started."', 116 'su root cmd window tracing stop >/dev/null 2>&1' 117 ), 118 "layers_trace": TraceTarget( 119 File("/data/misc/wmtrace/layers_trace.pb", "layers_trace"), 120 'su root service call SurfaceFlinger 1025 i32 1\necho "SF trace started."', 121 'su root service call SurfaceFlinger 1025 i32 0 >/dev/null 2>&1' 122 ), 123 "screen_recording": TraceTarget( 124 File("/data/local/tmp/screen.winscope.mp4", "screen_recording"), 125 'screenrecord --bit-rate 8M /data/local/tmp/screen.winscope.mp4 >/dev/null 2>&1 &\necho "ScreenRecorder started."', 126 'pkill -l SIGINT screenrecord >/dev/null 2>&1' 127 ), 128 "transaction": TraceTarget( 129 [ 130 File("/data/misc/wmtrace/transaction_trace.pb", "transactions"), 131 FileMatcher("/data/misc/wmtrace/", "transaction_merges_*.pb", 132 "transaction_merges"), 133 ], 134 'su root service call SurfaceFlinger 1020 i32 1\necho "SF transactions recording started."', 135 'su root service call SurfaceFlinger 1020 i32 0 >/dev/null 2>&1' 136 ), 137 "proto_log": TraceTarget( 138 File("/data/misc/wmtrace/wm_log.pb", "proto_log"), 139 'su root cmd window logging start\necho "WM logging started."', 140 'su root cmd window logging stop >/dev/null 2>&1' 141 ), 142 "ime_trace_clients": TraceTarget( 143 File("/data/misc/wmtrace/ime_trace_clients.pb", "ime_trace_clients"), 144 'su root ime tracing start\necho "Clients IME trace started."', 145 'su root ime tracing stop >/dev/null 2>&1' 146 ), 147 "ime_trace_service": TraceTarget( 148 File("/data/misc/wmtrace/ime_trace_service.pb", "ime_trace_service"), 149 'su root ime tracing start\necho "Service IME trace started."', 150 'su root ime tracing stop >/dev/null 2>&1' 151 ), 152 "ime_trace_managerservice": TraceTarget( 153 File("/data/misc/wmtrace/ime_trace_managerservice.pb", "ime_trace_managerservice"), 154 'su root ime tracing start\necho "ManagerService IME trace started."', 155 'su root ime tracing stop >/dev/null 2>&1' 156 ), 157} 158 159 160class SurfaceFlingerTraceConfig: 161 """Handles optional configuration for surfaceflinger traces. 162 """ 163 164 def __init__(self) -> None: 165 # default config flags CRITICAL | INPUT | SYNC 166 self.flags = 1 << 0 | 1 << 1 | 1 << 6 167 168 def add(self, config: str) -> None: 169 self.flags |= CONFIG_FLAG[config] 170 171 def is_valid(self, config: str) -> bool: 172 return config in CONFIG_FLAG 173 174 def command(self) -> str: 175 return f'su root service call SurfaceFlinger 1033 i32 {self.flags}' 176 177 178CONFIG_FLAG = { 179 "composition": 1 << 2, 180 "metadata": 1 << 3, 181 "hwc": 1 << 4 182} 183 184 185class DumpTarget: 186 """Defines a single parameter to trace. 187 188 Attributes: 189 file: the path on the device the dump results are saved to. 190 dump_command: command to dump state to file. 191 """ 192 193 def __init__(self, files, dump_command: str) -> None: 194 if type(files) is not list: 195 files = [files] 196 self.files = files 197 self.dump_command = dump_command 198 199 200DUMP_TARGETS = { 201 "window_dump": DumpTarget( 202 File("/data/local/tmp/wm_dump.pb", "window_dump"), 203 'su root dumpsys window --proto > /data/local/tmp/wm_dump.pb' 204 ), 205 "layers_dump": DumpTarget( 206 File("/data/local/tmp/sf_dump.pb", "layers_dump"), 207 'su root dumpsys SurfaceFlinger --proto > /data/local/tmp/sf_dump.pb' 208 ) 209} 210 211 212# END OF CONFIG # 213 214 215def get_token() -> str: 216 """Returns saved proxy security token or creates new one""" 217 try: 218 with open(WINSCOPE_TOKEN_LOCATION, 'r') as token_file: 219 token = token_file.readline() 220 log.debug("Loaded token {} from {}".format( 221 token, WINSCOPE_TOKEN_LOCATION)) 222 return token 223 except IOError: 224 token = secrets.token_hex(32) 225 os.makedirs(os.path.dirname(WINSCOPE_TOKEN_LOCATION), exist_ok=True) 226 try: 227 with open(WINSCOPE_TOKEN_LOCATION, 'w') as token_file: 228 log.debug("Created and saved token {} to {}".format( 229 token, WINSCOPE_TOKEN_LOCATION)) 230 token_file.write(token) 231 os.chmod(WINSCOPE_TOKEN_LOCATION, 0o600) 232 except IOError: 233 log.error("Unable to save persistent token {} to {}".format( 234 token, WINSCOPE_TOKEN_LOCATION)) 235 return token 236 237 238secret_token = get_token() 239 240 241class RequestType(Enum): 242 GET = 1 243 POST = 2 244 HEAD = 3 245 246 247def add_standard_headers(server): 248 server.send_header('Cache-Control', 'no-cache, no-store, must-revalidate') 249 server.send_header('Access-Control-Allow-Origin', '*') 250 server.send_header('Access-Control-Allow-Methods', 'POST, GET, OPTIONS') 251 server.send_header('Access-Control-Allow-Headers', 252 WINSCOPE_TOKEN_HEADER + ', Content-Type, Content-Length') 253 server.send_header('Access-Control-Expose-Headers', 254 'Winscope-Proxy-Version') 255 server.send_header(WINSCOPE_VERSION_HEADER, VERSION) 256 server.end_headers() 257 258 259class RequestEndpoint: 260 """Request endpoint to use with the RequestRouter.""" 261 262 @abstractmethod 263 def process(self, server, path): 264 pass 265 266 267class AdbError(Exception): 268 """Unsuccessful ADB operation""" 269 pass 270 271 272class BadRequest(Exception): 273 """Invalid client request""" 274 pass 275 276 277class RequestRouter: 278 """Handles HTTP request authentication and routing""" 279 280 def __init__(self, handler): 281 self.request = handler 282 self.endpoints = {} 283 284 def register_endpoint(self, method: RequestType, name: str, endpoint: RequestEndpoint): 285 self.endpoints[(method, name)] = endpoint 286 287 def __bad_request(self, error: str): 288 log.warning("Bad request: " + error) 289 self.request.respond(HTTPStatus.BAD_REQUEST, b"Bad request!\nThis is Winscope ADB proxy.\n\n" 290 + error.encode("utf-8"), 'text/txt') 291 292 def __internal_error(self, error: str): 293 log.error("Internal error: " + error) 294 self.request.respond(HTTPStatus.INTERNAL_SERVER_ERROR, 295 error.encode("utf-8"), 'text/txt') 296 297 def __bad_token(self): 298 log.info("Bad token") 299 self.request.respond(HTTPStatus.FORBIDDEN, b"Bad Winscope authorisation token!\nThis is Winscope ADB proxy.\n", 300 'text/txt') 301 302 def process(self, method: RequestType): 303 token = self.request.headers[WINSCOPE_TOKEN_HEADER] 304 if not token or token != secret_token: 305 return self.__bad_token() 306 path = self.request.path.strip('/').split('/') 307 if path and len(path) > 0: 308 endpoint_name = path[0] 309 try: 310 return self.endpoints[(method, endpoint_name)].process(self.request, path[1:]) 311 except KeyError: 312 return self.__bad_request("Unknown endpoint /{}/".format(endpoint_name)) 313 except AdbError as ex: 314 return self.__internal_error(str(ex)) 315 except BadRequest as ex: 316 return self.__bad_request(str(ex)) 317 except Exception as ex: 318 return self.__internal_error(repr(ex)) 319 self.__bad_request("No endpoint specified") 320 321 322def call_adb(params: str, device: str = None, stdin: bytes = None): 323 command = ['adb'] + (['-s', device] if device else []) + params.split(' ') 324 try: 325 log.debug("Call: " + ' '.join(command)) 326 return subprocess.check_output(command, stderr=subprocess.STDOUT, input=stdin).decode('utf-8') 327 except OSError as ex: 328 log.debug('Error executing adb command: {}\n{}'.format( 329 ' '.join(command), repr(ex))) 330 raise AdbError('Error executing adb command: {}\n{}'.format( 331 ' '.join(command), repr(ex))) 332 except subprocess.CalledProcessError as ex: 333 log.debug('Error executing adb command: {}\n{}'.format( 334 ' '.join(command), ex.output.decode("utf-8"))) 335 raise AdbError('Error executing adb command: adb {}\n{}'.format( 336 params, ex.output.decode("utf-8"))) 337 338 339def call_adb_outfile(params: str, outfile, device: str = None, stdin: bytes = None): 340 try: 341 process = subprocess.Popen(['adb'] + (['-s', device] if device else []) + params.split(' '), stdout=outfile, 342 stderr=subprocess.PIPE) 343 _, err = process.communicate(stdin) 344 outfile.seek(0) 345 if process.returncode != 0: 346 log.debug('Error executing adb command: adb {}\n'.format(params) + err.decode( 347 'utf-8') + '\n' + outfile.read().decode('utf-8')) 348 raise AdbError('Error executing adb command: adb {}\n'.format(params) + err.decode( 349 'utf-8') + '\n' + outfile.read().decode('utf-8')) 350 except OSError as ex: 351 log.debug('Error executing adb command: adb {}\n{}'.format( 352 params, repr(ex))) 353 raise AdbError( 354 'Error executing adb command: adb {}\n{}'.format(params, repr(ex))) 355 356 357class ListDevicesEndpoint(RequestEndpoint): 358 ADB_INFO_RE = re.compile("^([A-Za-z0-9.:\\-]+)\\s+(\\w+)(.*model:(\\w+))?") 359 360 def process(self, server, path): 361 lines = list(filter(None, call_adb('devices -l').split('\n'))) 362 devices = {m.group(1): { 363 'authorised': str(m.group(2)) != 'unauthorized', 364 'model': m.group(4).replace('_', ' ') if m.group(4) else '' 365 } for m in [ListDevicesEndpoint.ADB_INFO_RE.match(d) for d in lines[1:]] if m} 366 j = json.dumps(devices) 367 log.debug("Detected devices: " + j) 368 server.respond(HTTPStatus.OK, j.encode("utf-8"), "text/json") 369 370 371class DeviceRequestEndpoint(RequestEndpoint): 372 def process(self, server, path): 373 if len(path) > 0 and re.fullmatch("[A-Za-z0-9.:\\-]+", path[0]): 374 self.process_with_device(server, path[1:], path[0]) 375 else: 376 raise BadRequest("Device id not specified") 377 378 @abstractmethod 379 def process_with_device(self, server, path, device_id): 380 pass 381 382 def get_request(self, server) -> str: 383 try: 384 length = int(server.headers["Content-Length"]) 385 except KeyError as err: 386 raise BadRequest("Missing Content-Length header\n" + str(err)) 387 except ValueError as err: 388 raise BadRequest("Content length unreadable\n" + str(err)) 389 return json.loads(server.rfile.read(length).decode("utf-8")) 390 391 392class FetchFilesEndpoint(DeviceRequestEndpoint): 393 def process_with_device(self, server, path, device_id): 394 if len(path) != 1: 395 raise BadRequest("File not specified") 396 if path[0] in TRACE_TARGETS: 397 files = TRACE_TARGETS[path[0]].files 398 elif path[0] in DUMP_TARGETS: 399 files = DUMP_TARGETS[path[0]].files 400 else: 401 raise BadRequest("Unknown file specified") 402 403 file_buffers = dict() 404 405 for f in files: 406 file_type = f.get_filetype() 407 file_paths = f.get_filepaths(device_id) 408 409 for file_path in file_paths: 410 with NamedTemporaryFile() as tmp: 411 log.debug( 412 f"Fetching file {file_path} from device to {tmp.name}") 413 call_adb_outfile('exec-out su root cat ' + 414 file_path, tmp, device_id) 415 log.debug(f"Deleting file {file_path} from device") 416 call_adb('shell su root rm ' + file_path, device_id) 417 log.debug(f"Uploading file {tmp.name}") 418 if file_type not in file_buffers: 419 file_buffers[file_type] = [] 420 buf = base64.encodebytes(tmp.read()).decode("utf-8") 421 file_buffers[file_type].append(buf) 422 423 # server.send_header('X-Content-Type-Options', 'nosniff') 424 # add_standard_headers(server) 425 j = json.dumps(file_buffers) 426 server.respond(HTTPStatus.OK, j.encode("utf-8"), "text/json") 427 428 429def check_root(device_id): 430 log.debug("Checking root access on {}".format(device_id)) 431 return call_adb('shell su root id -u', device_id) == "0\n" 432 433 434TRACE_THREADS = {} 435 436 437class TraceThread(threading.Thread): 438 def __init__(self, device_id, command): 439 self._keep_alive_timer = None 440 self.trace_command = command 441 self._device_id = device_id 442 self.out = None, 443 self.err = None, 444 self._success = False 445 try: 446 shell = ['adb', '-s', self._device_id, 'shell'] 447 log.debug("Starting trace shell {}".format(' '.join(shell))) 448 self.process = subprocess.Popen(shell, stdout=subprocess.PIPE, 449 stderr=subprocess.PIPE, stdin=subprocess.PIPE, start_new_session=True) 450 except OSError as ex: 451 raise AdbError( 452 'Error executing adb command: adb shell\n{}'.format(repr(ex))) 453 454 super().__init__() 455 456 def timeout(self): 457 if self.is_alive(): 458 log.warning( 459 "Keep-alive timeout for trace on {}".format(self._device_id)) 460 self.end_trace() 461 if self._device_id in TRACE_THREADS: 462 TRACE_THREADS.pop(self._device_id) 463 464 def reset_timer(self): 465 log.debug( 466 "Resetting keep-alive clock for trace on {}".format(self._device_id)) 467 if self._keep_alive_timer: 468 self._keep_alive_timer.cancel() 469 self._keep_alive_timer = threading.Timer( 470 KEEP_ALIVE_INTERVAL_S, self.timeout) 471 self._keep_alive_timer.start() 472 473 def end_trace(self): 474 if self._keep_alive_timer: 475 self._keep_alive_timer.cancel() 476 log.debug("Sending SIGINT to the trace process on {}".format( 477 self._device_id)) 478 self.process.send_signal(signal.SIGINT) 479 try: 480 log.debug("Waiting for trace shell to exit for {}".format( 481 self._device_id)) 482 self.process.wait(timeout=5) 483 except TimeoutError: 484 log.debug( 485 "TIMEOUT - sending SIGKILL to the trace process on {}".format(self._device_id)) 486 self.process.kill() 487 self.join() 488 489 def run(self): 490 log.debug("Trace started on {}".format(self._device_id)) 491 self.reset_timer() 492 self.out, self.err = self.process.communicate(self.trace_command) 493 log.debug("Trace ended on {}, waiting for cleanup".format(self._device_id)) 494 time.sleep(0.2) 495 for i in range(10): 496 if call_adb("shell su root cat /data/local/tmp/winscope_status", device=self._device_id) == 'TRACE_OK\n': 497 call_adb( 498 "shell su root rm /data/local/tmp/winscope_status", device=self._device_id) 499 log.debug("Trace finished successfully on {}".format( 500 self._device_id)) 501 self._success = True 502 break 503 log.debug("Still waiting for cleanup on {}".format(self._device_id)) 504 time.sleep(0.1) 505 506 def success(self): 507 return self._success 508 509 510class StartTrace(DeviceRequestEndpoint): 511 TRACE_COMMAND = """ 512set -e 513 514echo "Starting trace..." 515echo "TRACE_START" > /data/local/tmp/winscope_status 516 517# Do not print anything to stdout/stderr in the handler 518function stop_trace() {{ 519 trap - EXIT HUP INT 520 521{} 522 523 echo "TRACE_OK" > /data/local/tmp/winscope_status 524}} 525 526trap stop_trace EXIT HUP INT 527echo "Signal handler registered." 528 529{} 530 531# ADB shell does not handle hung up well and does not call HUP handler when a child is active in foreground, 532# as a workaround we sleep for short intervals in a loop so the handler is called after a sleep interval. 533while true; do sleep 0.1; done 534""" 535 536 def process_with_device(self, server, path, device_id): 537 try: 538 requested_types = self.get_request(server) 539 requested_traces = [TRACE_TARGETS[t] for t in requested_types] 540 except KeyError as err: 541 raise BadRequest("Unsupported trace target\n" + str(err)) 542 if device_id in TRACE_THREADS: 543 log.warning("Trace already in progress for {}", device_id) 544 server.respond(HTTPStatus.OK, b'', "text/plain") 545 if not check_root(device_id): 546 raise AdbError( 547 "Unable to acquire root privileges on the device - check the output of 'adb -s {} shell su root id'".format( 548 device_id)) 549 command = StartTrace.TRACE_COMMAND.format( 550 '\n'.join([t.trace_stop for t in requested_traces]), 551 '\n'.join([t.trace_start for t in requested_traces])) 552 log.debug("Trace requested for {} with targets {}".format( 553 device_id, ','.join(requested_types))) 554 TRACE_THREADS[device_id] = TraceThread( 555 device_id, command.encode('utf-8')) 556 TRACE_THREADS[device_id].start() 557 server.respond(HTTPStatus.OK, b'', "text/plain") 558 559 560class EndTrace(DeviceRequestEndpoint): 561 def process_with_device(self, server, path, device_id): 562 if device_id not in TRACE_THREADS: 563 raise BadRequest("No trace in progress for {}".format(device_id)) 564 if TRACE_THREADS[device_id].is_alive(): 565 TRACE_THREADS[device_id].end_trace() 566 567 success = TRACE_THREADS[device_id].success() 568 out = TRACE_THREADS[device_id].out + \ 569 b"\n" + TRACE_THREADS[device_id].err 570 command = TRACE_THREADS[device_id].trace_command 571 TRACE_THREADS.pop(device_id) 572 if success: 573 server.respond(HTTPStatus.OK, out, "text/plain") 574 else: 575 raise AdbError( 576 "Error tracing the device\n### Output ###\n" + out.decode( 577 "utf-8") + "\n### Command: adb -s {} shell ###\n### Input ###\n".format(device_id) + command.decode( 578 "utf-8")) 579 580 581class ConfigTrace(DeviceRequestEndpoint): 582 def process_with_device(self, server, path, device_id): 583 try: 584 requested_configs = self.get_request(server) 585 config = SurfaceFlingerTraceConfig() 586 for requested_config in requested_configs: 587 if not config.is_valid(requested_config): 588 raise BadRequest( 589 f"Unsupported config {requested_config}\n") 590 config.add(requested_config) 591 except KeyError as err: 592 raise BadRequest("Unsupported trace target\n" + str(err)) 593 if device_id in TRACE_THREADS: 594 BadRequest(f"Trace in progress for {device_id}") 595 if not check_root(device_id): 596 raise AdbError( 597 f"Unable to acquire root privileges on the device - check the output of 'adb -s {device_id} shell su root id'") 598 command = config.command() 599 shell = ['adb', '-s', device_id, 'shell'] 600 log.debug(f"Starting shell {' '.join(shell)}") 601 process = subprocess.Popen(shell, stdout=subprocess.PIPE, stderr=subprocess.PIPE, 602 stdin=subprocess.PIPE, start_new_session=True) 603 log.debug(f"Changing trace config on device {device_id} cmd:{command}") 604 out, err = process.communicate(command.encode('utf-8')) 605 if process.returncode != 0: 606 raise AdbError( 607 f"Error executing command:\n {command}\n\n### OUTPUT ###{out.decode('utf-8')}\n{err.decode('utf-8')}") 608 log.debug(f"Changing trace config finished on device {device_id}") 609 server.respond(HTTPStatus.OK, b'', "text/plain") 610 611 612class StatusEndpoint(DeviceRequestEndpoint): 613 def process_with_device(self, server, path, device_id): 614 if device_id not in TRACE_THREADS: 615 raise BadRequest("No trace in progress for {}".format(device_id)) 616 TRACE_THREADS[device_id].reset_timer() 617 server.respond(HTTPStatus.OK, str( 618 TRACE_THREADS[device_id].is_alive()).encode("utf-8"), "text/plain") 619 620 621class DumpEndpoint(DeviceRequestEndpoint): 622 def process_with_device(self, server, path, device_id): 623 try: 624 requested_types = self.get_request(server) 625 requested_traces = [DUMP_TARGETS[t] for t in requested_types] 626 except KeyError as err: 627 raise BadRequest("Unsupported trace target\n" + str(err)) 628 if device_id in TRACE_THREADS: 629 BadRequest("Trace in progress for {}".format(device_id)) 630 if not check_root(device_id): 631 raise AdbError( 632 "Unable to acquire root privileges on the device - check the output of 'adb -s {} shell su root id'" 633 .format(device_id)) 634 command = '\n'.join(t.dump_command for t in requested_traces) 635 shell = ['adb', '-s', device_id, 'shell'] 636 log.debug("Starting dump shell {}".format(' '.join(shell))) 637 process = subprocess.Popen(shell, stdout=subprocess.PIPE, stderr=subprocess.PIPE, 638 stdin=subprocess.PIPE, start_new_session=True) 639 log.debug("Starting dump on device {}".format(device_id)) 640 out, err = process.communicate(command.encode('utf-8')) 641 if process.returncode != 0: 642 raise AdbError("Error executing command:\n" + command + "\n\n### OUTPUT ###" + out.decode('utf-8') + "\n" 643 + err.decode('utf-8')) 644 log.debug("Dump finished on device {}".format(device_id)) 645 server.respond(HTTPStatus.OK, b'', "text/plain") 646 647 648class ADBWinscopeProxy(BaseHTTPRequestHandler): 649 def __init__(self, request, client_address, server): 650 self.router = RequestRouter(self) 651 self.router.register_endpoint( 652 RequestType.GET, "devices", ListDevicesEndpoint()) 653 self.router.register_endpoint( 654 RequestType.GET, "status", StatusEndpoint()) 655 self.router.register_endpoint( 656 RequestType.GET, "fetch", FetchFilesEndpoint()) 657 self.router.register_endpoint(RequestType.POST, "start", StartTrace()) 658 self.router.register_endpoint(RequestType.POST, "end", EndTrace()) 659 self.router.register_endpoint(RequestType.POST, "dump", DumpEndpoint()) 660 self.router.register_endpoint( 661 RequestType.POST, "configtrace", ConfigTrace()) 662 super().__init__(request, client_address, server) 663 664 def respond(self, code: int, data: bytes, mime: str) -> None: 665 self.send_response(code) 666 self.send_header('Content-type', mime) 667 add_standard_headers(self) 668 self.wfile.write(data) 669 670 def do_GET(self): 671 self.router.process(RequestType.GET) 672 673 def do_POST(self): 674 self.router.process(RequestType.POST) 675 676 def do_OPTIONS(self): 677 self.send_response(HTTPStatus.OK) 678 self.send_header('Allow', 'GET,POST') 679 add_standard_headers(self) 680 self.end_headers() 681 self.wfile.write(b'GET,POST') 682 683 def log_request(self, code='-', size='-'): 684 log.info('{} {} {}'.format(self.requestline, str(code), str(size))) 685 686 687if __name__ == '__main__': 688 print("Winscope ADB Connect proxy version: " + VERSION) 689 print('Winscope token: ' + secret_token) 690 httpd = HTTPServer(('localhost', PORT), ADBWinscopeProxy) 691 try: 692 httpd.serve_forever() 693 except KeyboardInterrupt: 694 log.info("Shutting down") 695