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