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