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