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 argparse
27import base64
28import json
29import logging
30import os
31import re
32import secrets
33import signal
34import subprocess
35import sys
36import threading
37import time
38from abc import abstractmethod
39from enum import Enum
40from http import HTTPStatus
41from http.server import HTTPServer, BaseHTTPRequestHandler
42from logging import DEBUG, INFO, WARNING
43from tempfile import NamedTemporaryFile
44
45# GLOBALS #
46
47log = None
48secret_token = None
49
50# CONFIG #
51
52def create_argument_parser() -> argparse.ArgumentParser:
53    parser = argparse.ArgumentParser(description='Proxy for go/winscope', prog='winscope_proxy')
54
55    parser.add_argument('--verbose', '-v', dest='loglevel', action='store_const', const=INFO)
56    parser.add_argument('--debug', '-d', dest='loglevel', action='store_const', const=DEBUG)
57    parser.add_argument('--port', '-p', default=5544, action='store')
58
59    parser.set_defaults(loglevel=WARNING)
60
61    return parser
62
63# Keep in sync with ProxyClient#VERSION in Winscope
64VERSION = '2.1.1'
65
66PERFETTO_TRACE_CONFIG_FILE = '/data/misc/perfetto-configs/winscope-proxy-trace.conf'
67PERFETTO_DUMP_CONFIG_FILE = '/data/misc/perfetto-configs/winscope-proxy-dump.conf'
68PERFETTO_SF_CONFIG_FILE = '/data/misc/perfetto-configs/winscope-proxy.surfaceflinger.conf'
69PERFETTO_TRACE_FILE = '/data/misc/perfetto-traces/winscope-proxy-trace.perfetto-trace'
70PERFETTO_DUMP_FILE = '/data/misc/perfetto-traces/winscope-proxy-dump.perfetto-trace'
71PERFETTO_UNIQUE_SESSION_NAME = 'winscope proxy perfetto tracing'
72PERFETTO_UTILS = f"""
73function is_perfetto_data_source_available {{
74    local data_source_name=$1
75    if perfetto --query | grep $data_source_name 2>&1 >/dev/null; then
76        return 0
77    else
78        return 1
79    fi
80}}
81
82function is_perfetto_tracing_session_running {{
83    if perfetto --query | grep "{PERFETTO_UNIQUE_SESSION_NAME}" 2>&1 >/dev/null; then
84        return 0
85    else
86        return 1
87    fi
88}}
89
90function is_any_perfetto_data_source_available {{
91    if is_perfetto_data_source_available android.inputmethod || \
92       is_perfetto_data_source_available android.protolog || \
93       is_perfetto_data_source_available android.surfaceflinger.layers || \
94       is_perfetto_data_source_available android.surfaceflinger.transactions || \
95       is_perfetto_data_source_available com.android.wm.shell.transition; then
96        return 0
97    else
98        return 1
99    fi
100}}
101
102function is_flag_set {{
103    local flag_name=$1
104    if dumpsys device_config | grep $flag_name=true 2>&1 >/dev/null; then
105        return 0
106    else
107        return 1
108    fi
109}}
110"""
111
112WINSCOPE_VERSION_HEADER = "Winscope-Proxy-Version"
113WINSCOPE_TOKEN_HEADER = "Winscope-Token"
114
115# Location to save the proxy security token
116WINSCOPE_TOKEN_LOCATION = os.path.expanduser('~/.config/winscope/.token')
117
118# Winscope traces extensions
119WINSCOPE_EXT = ".winscope"
120WINSCOPE_EXT_LEGACY = ".pb"
121WINSCOPE_EXTS = [WINSCOPE_EXT, WINSCOPE_EXT_LEGACY]
122
123# Winscope traces directory
124WINSCOPE_DIR = "/data/misc/wmtrace/"
125
126# Max interval between the client keep-alive requests in seconds
127KEEP_ALIVE_INTERVAL_S = 5
128
129class File:
130    def __init__(self, file, filetype) -> None:
131        self.file = file
132        self.type = filetype
133
134    def get_filepaths(self, device_id):
135        return [self.file]
136
137    def get_filetype(self):
138        return self.type
139
140
141class FileMatcher:
142    def __init__(self, path, matcher, filetype) -> None:
143        self.path = path
144        self.matcher = matcher
145        self.type = filetype
146
147    def get_filepaths(self, device_id):
148        matchingFiles = call_adb(
149            f"shell su root find {self.path} -name {self.matcher}", device_id)
150
151        log.debug("Found file %s", matchingFiles.split('\n')[:-1])
152        return matchingFiles.split('\n')[:-1]
153
154    def get_filetype(self):
155        return self.type
156
157
158class WinscopeFileMatcher(FileMatcher):
159    def __init__(self, path, matcher, filetype) -> None:
160        self.path = path
161        self.internal_matchers = list(map(lambda ext: FileMatcher(path, f'{matcher}{ext}', filetype),
162            WINSCOPE_EXTS))
163        self.type = filetype
164
165    def get_filepaths(self, device_id):
166        for matcher in self.internal_matchers:
167            files = matcher.get_filepaths(device_id)
168            if len(files) > 0:
169                return files
170        log.debug("No files found")
171        return []
172
173
174class TraceTarget:
175    """Defines a single parameter to trace.
176
177    Attributes:
178        file_matchers: the matchers used to identify the paths on the device the trace results are saved to.
179        trace_start: command to start the trace from adb shell, must not block.
180        trace_stop: command to stop the trace, should block until the trace is stopped.
181    """
182
183    def __init__(self, files, trace_start: str, trace_stop: str) -> None:
184        if type(files) is not list:
185            files = [files]
186        self.files = files
187        self.trace_start = trace_start
188        self.trace_stop = trace_stop
189
190
191# Order of files matters as they will be expected in that order and decoded in that order
192TRACE_TARGETS = {
193    "view_capture_trace": TraceTarget(
194        File('/data/misc/wmtrace/view_capture_trace.zip', "view_capture_trace.zip"),
195    f"""
196if is_flag_set windowing_tools/android.tracing.perfetto_view_capture_tracing; then
197    cat << EOF >> {PERFETTO_TRACE_CONFIG_FILE}
198data_sources: {{
199    config {{
200        name: "android.viewcapture"
201    }}
202}}
203EOF
204    echo 'ViewCapture tracing (perfetto) configured to start along the other perfetto traces'
205else
206    su root settings put global view_capture_enabled 1
207    echo 'ViewCapture tracing (legacy) started.'
208fi
209""",
210    """
211if ! is_flag_set windowing_tools/android.tracing.perfetto_view_capture_tracing; then
212    su root sh -c 'cmd launcherapps dump-view-hierarchies >/data/misc/wmtrace/view_capture_trace.zip'
213    su root settings put global view_capture_enabled 0
214    echo 'ViewCapture tracing (legacy) stopped.'
215fi
216"""
217    ),
218    "window_trace": TraceTarget(
219        WinscopeFileMatcher(WINSCOPE_DIR, "wm_trace", "window_trace"),
220        'su root cmd window tracing start\necho "WM trace started."',
221        'su root cmd window tracing stop >/dev/null 2>&1'
222    ),
223    "layers_trace": TraceTarget(
224        WinscopeFileMatcher(WINSCOPE_DIR, "layers_trace", "layers_trace"),
225        f"""
226if is_perfetto_data_source_available android.surfaceflinger.layers; then
227    cat {PERFETTO_SF_CONFIG_FILE} >> {PERFETTO_TRACE_CONFIG_FILE}
228    echo 'SF trace (perfetto) configured to start along the other perfetto traces'
229else
230    su root service call SurfaceFlinger 1025 i32 1
231    echo 'SF layers trace (legacy) started'
232fi
233        """,
234        """
235if ! is_perfetto_data_source_available android.surfaceflinger.layers; then
236    su root service call SurfaceFlinger 1025 i32 0 >/dev/null 2>&1
237    echo 'SF layers trace (legacy) stopped.'
238fi
239"""
240),
241    "screen_recording": TraceTarget(
242        File(f'/data/local/tmp/screen.mp4', "screen_recording"),
243        f'''
244        settings put system show_touches 1 && \
245        settings put system pointer_location 1 && \
246        screenrecord --bugreport --bit-rate 8M /data/local/tmp/screen.mp4 >/dev/null 2>&1 & \
247        echo "ScreenRecorder started."
248        ''',
249        '''settings put system pointer_location 0 && \
250        settings put system show_touches 0 && \
251        pkill -l SIGINT screenrecord >/dev/null 2>&1
252        '''.strip()
253    ),
254    "transactions": TraceTarget(
255        WinscopeFileMatcher(WINSCOPE_DIR, "transactions_trace", "transactions"),
256        f"""
257if is_perfetto_data_source_available android.surfaceflinger.transactions; then
258    cat << EOF >> {PERFETTO_TRACE_CONFIG_FILE}
259data_sources: {{
260    config {{
261        name: "android.surfaceflinger.transactions"
262        surfaceflinger_transactions_config: {{
263            mode: MODE_ACTIVE
264        }}
265    }}
266}}
267EOF
268    echo 'SF transactions trace (perfetto) configured to start along the other perfetto traces'
269else
270    su root service call SurfaceFlinger 1041 i32 1
271    echo 'SF transactions trace (legacy) started'
272fi
273""",
274        """
275if ! is_perfetto_data_source_available android.surfaceflinger.transactions; then
276    su root service call SurfaceFlinger 1041 i32 0 >/dev/null 2>&1
277fi
278"""
279    ),
280    "transactions_legacy": TraceTarget(
281        [
282            WinscopeFileMatcher(WINSCOPE_DIR, "transaction_trace", "transactions_legacy"),
283            FileMatcher(WINSCOPE_DIR, f'transaction_merges_*', "transaction_merges"),
284        ],
285        'su root service call SurfaceFlinger 1020 i32 1\necho "SF transactions recording started."',
286        'su root service call SurfaceFlinger 1020 i32 0 >/dev/null 2>&1'
287    ),
288    "proto_log": TraceTarget(
289        WinscopeFileMatcher(WINSCOPE_DIR, "wm_log", "proto_log"),
290        f"""
291if is_perfetto_data_source_available android.protolog && \
292    is_flag_set windowing_tools/android.tracing.perfetto_protolog_tracing; then
293    cat << EOF >> {PERFETTO_TRACE_CONFIG_FILE}
294data_sources: {{
295    config {{
296        name: "android.protolog"
297        protolog_config: {{
298            tracing_mode: ENABLE_ALL
299        }}
300    }}
301}}
302EOF
303    echo 'ProtoLog (perfetto) configured to start along the other perfetto traces'
304else
305    su root cmd window logging start
306    echo "ProtoLog (legacy) started."
307fi
308        """,
309        """
310if ! is_perfetto_data_source_available android.protolog && \
311    ! is_flag_set windowing_tools/android.tracing.perfetto_protolog_tracing; then
312    su root cmd window logging stop >/dev/null 2>&1
313    echo "ProtoLog (legacy) stopped."
314fi
315        """
316    ),
317    "ime": TraceTarget(
318        [WinscopeFileMatcher(WINSCOPE_DIR, "ime_trace_clients", "ime_trace_clients"),
319         WinscopeFileMatcher(WINSCOPE_DIR, "ime_trace_service", "ime_trace_service"),
320         WinscopeFileMatcher(WINSCOPE_DIR, "ime_trace_managerservice", "ime_trace_managerservice")],
321         f"""
322if is_flag_set windowing_tools/android.tracing.perfetto_ime; then
323    cat << EOF >> {PERFETTO_TRACE_CONFIG_FILE}
324data_sources: {{
325    config {{
326        name: "android.inputmethod"
327    }}
328}}
329EOF
330    echo 'IME tracing (perfetto) configured to start along the other perfetto traces'
331else
332    su root ime tracing start
333    echo "IME tracing (legacy) started."
334fi
335""",
336    """
337if ! is_flag_set windowing_tools/android.tracing.perfetto_ime; then
338    su root ime tracing stop >/dev/null 2>&1
339    echo "IME tracing (legacy) stopped."
340fi
341"""
342    ),
343    "wayland_trace": TraceTarget(
344        WinscopeFileMatcher("/data/misc/wltrace", "wl_trace", "wl_trace"),
345        'su root service call Wayland 26 i32 1 >/dev/null\necho "Wayland trace started."',
346        'su root service call Wayland 26 i32 0 >/dev/null'
347    ),
348    "eventlog": TraceTarget(
349        WinscopeFileMatcher("/data/local/tmp", "eventlog", "eventlog"),
350        'rm -f /data/local/tmp/eventlog.winscope && EVENT_LOG_TRACING_START_TIME=$EPOCHREALTIME\necho "Event Log trace started."',
351        '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',
352    ),
353    "transition_traces": TraceTarget(
354        [WinscopeFileMatcher(WINSCOPE_DIR, "wm_transition_trace", "wm_transition_trace"),
355         WinscopeFileMatcher(WINSCOPE_DIR, "shell_transition_trace", "shell_transition_trace")],
356         f"""
357if is_perfetto_data_source_available com.android.wm.shell.transition && \
358    is_flag_set windowing_tools/android.tracing.perfetto_transition_tracing; then
359    cat << EOF >> {PERFETTO_TRACE_CONFIG_FILE}
360data_sources: {{
361    config {{
362        name: "com.android.wm.shell.transition"
363    }}
364}}
365EOF
366    echo 'Transition trace (perfetto) configured to start along the other perfetto traces'
367else
368    su root cmd window shell tracing start && su root dumpsys activity service SystemUIService WMShell transitions tracing start
369    echo "Transition traces (legacy) started."
370fi
371        """,
372        """
373if ! is_perfetto_data_source_available com.android.wm.shell.transition && \
374    ! is_flag_set windowing_tools/android.tracing.perfetto_transition_tracing; then
375    su root cmd window shell tracing stop && su root dumpsys activity service SystemUIService WMShell transitions tracing stop >/dev/null 2>&1
376    echo 'Transition traces (legacy) stopped.'
377fi
378"""
379    ),
380    "perfetto_trace": TraceTarget(
381        File(PERFETTO_TRACE_FILE, "trace.perfetto-trace"),
382        f"""
383if is_any_perfetto_data_source_available; then
384    cat << EOF >> {PERFETTO_TRACE_CONFIG_FILE}
385buffers: {{
386    size_kb: 80000
387    fill_policy: RING_BUFFER
388}}
389duration_ms: 0
390file_write_period_ms: 999999999
391write_into_file: true
392unique_session_name: "{PERFETTO_UNIQUE_SESSION_NAME}"
393EOF
394
395    if is_perfetto_tracing_session_running; then
396        perfetto --attach=WINSCOPE-PROXY-TRACING-SESSION --stop
397        echo 'Stopped already-running winscope perfetto session'
398    fi
399
400    echo 'Concurrent Perfetto Sessions'
401    perfetto --query | sed -n '/^TRACING SESSIONS:$/,$p'
402
403    rm -f {PERFETTO_TRACE_FILE}
404    perfetto --out {PERFETTO_TRACE_FILE} --txt --config {PERFETTO_TRACE_CONFIG_FILE} --detach=WINSCOPE-PROXY-TRACING-SESSION
405    echo 'Started perfetto trace'
406fi
407""",
408        """
409if is_any_perfetto_data_source_available; then
410    perfetto --attach=WINSCOPE-PROXY-TRACING-SESSION --stop
411    echo 'Stopped perfetto trace'
412fi
413""",
414    )
415}
416
417
418class SurfaceFlingerTraceConfig:
419    """Handles optional configuration for surfaceflinger traces.
420    """
421
422    def __init__(self) -> None:
423        self.flags = []
424        self.perfetto_flags = []
425
426    def add(self, config: str) -> None:
427        self.flags.append(config)
428
429    def is_valid(self, config: str) -> bool:
430        return config in SF_LEGACY_FLAGS_MAP
431
432    def command(self) -> str:
433        legacy_flags = 0
434        for flag in self.flags:
435            legacy_flags |= SF_LEGACY_FLAGS_MAP[flag]
436
437        perfetto_flags = "\n".join([f"""trace_flags: {SF_PERFETTO_FLAGS_MAP[flag]}""" for flag in self.flags])
438
439        return f"""
440{PERFETTO_UTILS}
441
442if is_perfetto_data_source_available android.surfaceflinger.layers; then
443    cat << EOF > {PERFETTO_SF_CONFIG_FILE}
444data_sources: {{
445    config {{
446        name: "android.surfaceflinger.layers"
447        surfaceflinger_layers_config: {{
448            mode: MODE_ACTIVE
449            {perfetto_flags}
450        }}
451    }}
452}}
453EOF
454    echo 'SF trace (perfetto) configured.'
455else
456    su root service call SurfaceFlinger 1033 i32 {legacy_flags}
457    echo 'SF trace (legacy) configured'
458fi
459"""
460
461class SurfaceFlingerTraceSelectedConfig:
462    """Handles optional selected configuration for surfaceflinger traces.
463    """
464
465    def __init__(self) -> None:
466        # defaults set for all configs
467        self.selectedConfigs = {
468            "sfbuffersize": "16000"
469        }
470
471    def add(self, configType, configValue) -> None:
472        self.selectedConfigs[configType] = configValue
473
474    def is_valid(self, configType) -> bool:
475        return configType in CONFIG_SF_SELECTION
476
477    def setBufferSize(self) -> str:
478        return f'su root service call SurfaceFlinger 1029 i32 {self.selectedConfigs["sfbuffersize"]}'
479
480class WindowManagerTraceSelectedConfig:
481    """Handles optional selected configuration for windowmanager traces.
482    """
483
484    def __init__(self) -> None:
485        # defaults set for all configs
486        self.selectedConfigs = {
487            "wmbuffersize": "16000",
488            "tracinglevel": "debug",
489            "tracingtype": "frame",
490        }
491
492    def add(self, configType, configValue) -> None:
493        self.selectedConfigs[configType] = configValue
494
495    def is_valid(self, configType) -> bool:
496        return configType in CONFIG_WM_SELECTION
497
498    def setBufferSize(self) -> str:
499        return f'su root cmd window tracing size {self.selectedConfigs["wmbuffersize"]}'
500
501    def setTracingLevel(self) -> str:
502        return f'su root cmd window tracing level {self.selectedConfigs["tracinglevel"]}'
503
504    def setTracingType(self) -> str:
505        return f'su root cmd window tracing {self.selectedConfigs["tracingtype"]}'
506
507
508SF_LEGACY_FLAGS_MAP = {
509    "input": 1 << 1,
510    "composition": 1 << 2,
511    "metadata": 1 << 3,
512    "hwc": 1 << 4,
513    "tracebuffers": 1 << 5,
514    "virtualdisplays": 1 << 6
515}
516
517SF_PERFETTO_FLAGS_MAP = {
518    "input": "TRACE_FLAG_INPUT",
519    "composition": "TRACE_FLAG_COMPOSITION",
520    "metadata": "TRACE_FLAG_EXTRA",
521    "hwc": "TRACE_FLAG_HWC",
522    "tracebuffers": "TRACE_FLAG_BUFFERS",
523    "virtualdisplays": "TRACE_FLAG_VIRTUAL_DISPLAYS",
524}
525
526#Keep up to date with options in trace_collection_utils.ts
527CONFIG_SF_SELECTION = [
528    "sfbuffersize",
529]
530
531#Keep up to date with options in trace_collection_utils.ts
532CONFIG_WM_SELECTION = [
533    "wmbuffersize",
534    "tracingtype",
535    "tracinglevel",
536]
537
538class DumpTarget:
539    """Defines a single parameter to trace.
540
541    Attributes:
542        file: the path on the device the dump results are saved to.
543        dump_command: command to dump state to file.
544    """
545
546    def __init__(self, files, dump_command: str) -> None:
547        if type(files) is not list:
548            files = [files]
549        self.files = files
550        self.dump_command = dump_command
551
552
553DUMP_TARGETS = {
554    "window_dump": DumpTarget(
555        File(f'/data/local/tmp/wm_dump{WINSCOPE_EXT}', "window_dump"),
556        f'su root dumpsys window --proto > /data/local/tmp/wm_dump{WINSCOPE_EXT}'
557    ),
558
559    "layers_dump": DumpTarget(
560        File(f'/data/local/tmp/sf_dump{WINSCOPE_EXT}', "layers_dump"),
561        f"""
562if is_perfetto_data_source_available android.surfaceflinger.layers; then
563    cat << EOF >> {PERFETTO_DUMP_CONFIG_FILE}
564data_sources: {{
565    config {{
566        name: "android.surfaceflinger.layers"
567        surfaceflinger_layers_config: {{
568            mode: MODE_DUMP
569            trace_flags: TRACE_FLAG_INPUT
570            trace_flags: TRACE_FLAG_COMPOSITION
571            trace_flags: TRACE_FLAG_HWC
572            trace_flags: TRACE_FLAG_BUFFERS
573            trace_flags: TRACE_FLAG_VIRTUAL_DISPLAYS
574        }}
575    }}
576}}
577EOF
578    echo 'SF transactions trace (perfetto) configured to start along the other perfetto traces'
579else
580    su root dumpsys SurfaceFlinger --proto > /data/local/tmp/sf_dump{WINSCOPE_EXT}
581fi
582"""
583    ),
584
585    "screenshot": DumpTarget(
586        File("/data/local/tmp/screenshot.png", "screenshot.png"),
587        "screencap -p > /data/local/tmp/screenshot.png"
588    ),
589
590    "perfetto_dump": DumpTarget(
591        File(PERFETTO_DUMP_FILE, "dump.perfetto-trace"),
592        f"""
593if is_any_perfetto_data_source_available; then
594    cat << EOF >> {PERFETTO_DUMP_CONFIG_FILE}
595buffers: {{
596    size_kb: 50000
597    fill_policy: RING_BUFFER
598}}
599duration_ms: 1
600EOF
601
602    rm -f {PERFETTO_DUMP_FILE}
603    perfetto --out {PERFETTO_DUMP_FILE} --txt --config {PERFETTO_DUMP_CONFIG_FILE}
604    echo 'Recorded perfetto dump'
605fi
606        """
607    )
608}
609
610
611# END OF CONFIG #
612
613
614def get_token() -> str:
615    """Returns saved proxy security token or creates new one"""
616    try:
617        with open(WINSCOPE_TOKEN_LOCATION, 'r') as token_file:
618            token = token_file.readline()
619            log.debug("Loaded token {} from {}".format(
620                token, WINSCOPE_TOKEN_LOCATION))
621            return token
622    except IOError:
623        token = secrets.token_hex(32)
624        os.makedirs(os.path.dirname(WINSCOPE_TOKEN_LOCATION), exist_ok=True)
625        try:
626            with open(WINSCOPE_TOKEN_LOCATION, 'w') as token_file:
627                log.debug("Created and saved token {} to {}".format(
628                    token, WINSCOPE_TOKEN_LOCATION))
629                token_file.write(token)
630            os.chmod(WINSCOPE_TOKEN_LOCATION, 0o600)
631        except IOError:
632            log.error("Unable to save persistent token {} to {}".format(
633                token, WINSCOPE_TOKEN_LOCATION))
634        return token
635
636
637class RequestType(Enum):
638    GET = 1
639    POST = 2
640    HEAD = 3
641
642
643def add_standard_headers(server):
644    server.send_header('Cache-Control', 'no-cache, no-store, must-revalidate')
645    server.send_header('Access-Control-Allow-Origin', '*')
646    server.send_header('Access-Control-Allow-Methods', 'POST, GET, OPTIONS')
647    server.send_header('Access-Control-Allow-Headers',
648                       WINSCOPE_TOKEN_HEADER + ', Content-Type, Content-Length')
649    server.send_header('Access-Control-Expose-Headers',
650                       'Winscope-Proxy-Version')
651    server.send_header(WINSCOPE_VERSION_HEADER, VERSION)
652    server.end_headers()
653
654
655class RequestEndpoint:
656    """Request endpoint to use with the RequestRouter."""
657
658    @abstractmethod
659    def process(self, server, path):
660        pass
661
662
663class AdbError(Exception):
664    """Unsuccessful ADB operation"""
665    pass
666
667
668class BadRequest(Exception):
669    """Invalid client request"""
670    pass
671
672
673class RequestRouter:
674    """Handles HTTP request authentication and routing"""
675
676    def __init__(self, handler):
677        self.request = handler
678        self.endpoints = {}
679
680    def register_endpoint(self, method: RequestType, name: str, endpoint: RequestEndpoint):
681        self.endpoints[(method, name)] = endpoint
682
683    def __bad_request(self, error: str):
684        log.warning("Bad request: " + error)
685        self.request.respond(HTTPStatus.BAD_REQUEST, b"Bad request!\nThis is Winscope ADB proxy.\n\n"
686                             + error.encode("utf-8"), 'text/txt')
687
688    def __internal_error(self, error: str):
689        log.error("Internal error: " + error)
690        self.request.respond(HTTPStatus.INTERNAL_SERVER_ERROR,
691                             error.encode("utf-8"), 'text/txt')
692
693    def __bad_token(self):
694        log.info("Bad token")
695        self.request.respond(HTTPStatus.FORBIDDEN, b"Bad Winscope authorisation token!\nThis is Winscope ADB proxy.\n",
696                             'text/txt')
697
698    def process(self, method: RequestType):
699        token = self.request.headers[WINSCOPE_TOKEN_HEADER]
700        if not token or token != secret_token:
701            return self.__bad_token()
702        path = self.request.path.strip('/').split('/')
703        if path and len(path) > 0:
704            endpoint_name = path[0]
705            try:
706                return self.endpoints[(method, endpoint_name)].process(self.request, path[1:])
707            except KeyError:
708                return self.__bad_request("Unknown endpoint /{}/".format(endpoint_name))
709            except AdbError as ex:
710                return self.__internal_error(str(ex))
711            except BadRequest as ex:
712                return self.__bad_request(str(ex))
713            except Exception as ex:
714                return self.__internal_error(repr(ex))
715        self.__bad_request("No endpoint specified")
716
717
718def call_adb(params: str, device: str = None, stdin: bytes = None):
719    command = ['adb'] + (['-s', device] if device else []) + params.split(' ')
720    try:
721        log.debug("Call: " + ' '.join(command))
722        return subprocess.check_output(command, stderr=subprocess.STDOUT, input=stdin).decode('utf-8')
723    except OSError as ex:
724        log.debug('Error executing adb command: {}\n{}'.format(
725            ' '.join(command), repr(ex)))
726        raise AdbError('Error executing adb command: {}\n{}'.format(
727            ' '.join(command), repr(ex)))
728    except subprocess.CalledProcessError as ex:
729        log.debug('Error executing adb command: {}\n{}'.format(
730            ' '.join(command), ex.output.decode("utf-8")))
731        raise AdbError('Error executing adb command: adb {}\n{}'.format(
732            params, ex.output.decode("utf-8")))
733
734
735def call_adb_outfile(params: str, outfile, device: str = None, stdin: bytes = None):
736    try:
737        process = subprocess.Popen(['adb'] + (['-s', device] if device else []) + params.split(' '), stdout=outfile,
738                                   stderr=subprocess.PIPE)
739        _, err = process.communicate(stdin)
740        outfile.seek(0)
741        if process.returncode != 0:
742            log.debug('Error executing adb command: adb {}\n'.format(params) + err.decode(
743                'utf-8') + '\n' + outfile.read().decode('utf-8'))
744            raise AdbError('Error executing adb command: adb {}\n'.format(params) + err.decode(
745                'utf-8') + '\n' + outfile.read().decode('utf-8'))
746    except OSError as ex:
747        log.debug('Error executing adb command: adb {}\n{}'.format(
748            params, repr(ex)))
749        raise AdbError(
750            'Error executing adb command: adb {}\n{}'.format(params, repr(ex)))
751
752
753class CheckWaylandServiceEndpoint(RequestEndpoint):
754    _listDevicesEndpoint = None
755
756    def __init__(self, listDevicesEndpoint):
757      self._listDevicesEndpoint = listDevicesEndpoint
758
759    def process(self, server, path):
760        self._listDevicesEndpoint.process(server, path)
761        foundDevices = self._listDevicesEndpoint._foundDevices
762
763        if len(foundDevices) > 1:
764          res = 'false'
765        else:
766          raw_res = call_adb('shell service check Wayland')
767          res = 'false' if 'not found' in raw_res else 'true'
768        server.respond(HTTPStatus.OK, res.encode("utf-8"), "text/json")
769
770
771class ListDevicesEndpoint(RequestEndpoint):
772    ADB_INFO_RE = re.compile("^([A-Za-z0-9._:\\-]+)\\s+(\\w+)(.*model:(\\w+))?")
773    _foundDevices = None
774
775    def process(self, server, path):
776        lines = list(filter(None, call_adb('devices -l').split('\n')))
777        devices = {m.group(1): {
778            'authorised': str(m.group(2)) != 'unauthorized',
779            'model': m.group(4).replace('_', ' ') if m.group(4) else ''
780        } for m in [ListDevicesEndpoint.ADB_INFO_RE.match(d) for d in lines[1:]] if m}
781        self._foundDevices = devices
782        j = json.dumps(devices)
783        log.debug("Detected devices: " + j)
784        server.respond(HTTPStatus.OK, j.encode("utf-8"), "text/json")
785
786
787class DeviceRequestEndpoint(RequestEndpoint):
788    def process(self, server, path):
789        if len(path) > 0 and re.fullmatch("[A-Za-z0-9.:\\-]+", path[0]):
790            self.process_with_device(server, path[1:], path[0])
791        else:
792            raise BadRequest("Device id not specified")
793
794    @abstractmethod
795    def process_with_device(self, server, path, device_id):
796        pass
797
798    def get_request(self, server) -> str:
799        try:
800            length = int(server.headers["Content-Length"])
801        except KeyError as err:
802            raise BadRequest("Missing Content-Length header\n" + str(err))
803        except ValueError as err:
804            raise BadRequest("Content length unreadable\n" + str(err))
805        return json.loads(server.rfile.read(length).decode("utf-8"))
806
807    def move_perfetto_target_to_end_of_list(self, targets):
808        # Make sure a perfetto target (if present) comes last in the list of targets, i.e. will
809        # be processed last.
810        # A perfetto target must be processed last, so that perfetto tracing is started only after
811        # the other targets have been processed and have configured the perfetto config file.
812        def is_perfetto_target(target):
813            return target == TRACE_TARGETS["perfetto_trace"] or target == DUMP_TARGETS["perfetto_dump"]
814        non_perfetto_targets = [t for t in targets if not is_perfetto_target(t)]
815        perfetto_targets = [t for t in targets if is_perfetto_target(t)]
816        return non_perfetto_targets + perfetto_targets
817
818
819
820class FetchFilesEndpoint(DeviceRequestEndpoint):
821    def process_with_device(self, server, path, device_id):
822        if len(path) != 1:
823            raise BadRequest("File not specified")
824        if path[0] in TRACE_TARGETS:
825            files = TRACE_TARGETS[path[0]].files
826        elif path[0] in DUMP_TARGETS:
827            files = DUMP_TARGETS[path[0]].files
828        else:
829            raise BadRequest("Unknown file specified")
830
831        file_buffers = dict()
832
833        for f in files:
834            file_type = f.get_filetype()
835            file_paths = f.get_filepaths(device_id)
836
837            for file_path in file_paths:
838                with NamedTemporaryFile() as tmp:
839                    log.debug(
840                        f"Fetching file {file_path} from device to {tmp.name}")
841                    call_adb_outfile('exec-out su root cat ' +
842                                     file_path, tmp, device_id)
843                    log.debug(f"Deleting file {file_path} from device")
844                    call_adb('shell su root rm -f ' + file_path, device_id)
845                    log.debug(f"Uploading file {tmp.name}")
846                    if file_type not in file_buffers:
847                        file_buffers[file_type] = []
848                    buf = base64.encodebytes(tmp.read()).decode("utf-8")
849                    file_buffers[file_type].append(buf)
850
851        if (len(file_buffers) == 0):
852            log.error("Proxy didn't find any file to fetch")
853
854        # server.send_header('X-Content-Type-Options', 'nosniff')
855        # add_standard_headers(server)
856        j = json.dumps(file_buffers)
857        server.respond(HTTPStatus.OK, j.encode("utf-8"), "text/json")
858
859
860def check_root(device_id):
861    log.debug("Checking root access on {}".format(device_id))
862    return int(call_adb('shell su root id -u', device_id)) == 0
863
864
865TRACE_THREADS = {}
866
867
868class TraceThread(threading.Thread):
869    def __init__(self, device_id, command):
870        self._keep_alive_timer = None
871        self.trace_command = command
872        self._device_id = device_id
873        self.out = None,
874        self.err = None,
875        self._success = False
876        try:
877            shell = ['adb', '-s', self._device_id, 'shell']
878            log.debug("Starting trace shell {}".format(' '.join(shell)))
879            self.process = subprocess.Popen(shell, stdout=subprocess.PIPE,
880                                            stderr=subprocess.PIPE, stdin=subprocess.PIPE, start_new_session=True)
881        except OSError as ex:
882            raise AdbError(
883                'Error executing adb command: adb shell\n{}'.format(repr(ex)))
884
885        super().__init__()
886
887    def timeout(self):
888        if self.is_alive():
889            log.warning(
890                "Keep-alive timeout for trace on {}".format(self._device_id))
891            self.end_trace()
892            if self._device_id in TRACE_THREADS:
893                TRACE_THREADS.pop(self._device_id)
894
895    def reset_timer(self):
896        log.debug(
897            "Resetting keep-alive clock for trace on {}".format(self._device_id))
898        if self._keep_alive_timer:
899            self._keep_alive_timer.cancel()
900        self._keep_alive_timer = threading.Timer(
901            KEEP_ALIVE_INTERVAL_S, self.timeout)
902        self._keep_alive_timer.start()
903
904    def end_trace(self):
905        if self._keep_alive_timer:
906            self._keep_alive_timer.cancel()
907        log.debug("Sending SIGINT to the trace process on {}".format(
908            self._device_id))
909        self.process.send_signal(signal.SIGINT)
910        try:
911            log.debug("Waiting for trace shell to exit for {}".format(
912                self._device_id))
913            self.process.wait(timeout=5)
914        except TimeoutError:
915            log.debug(
916                "TIMEOUT - sending SIGKILL to the trace process on {}".format(self._device_id))
917            self.process.kill()
918        self.join()
919
920    def run(self):
921        log.debug("Trace started on {}".format(self._device_id))
922        self.reset_timer()
923        self.out, self.err = self.process.communicate(self.trace_command)
924        log.debug("Trace ended on {}, waiting for cleanup".format(self._device_id))
925        time.sleep(0.2)
926        for i in range(50):
927            if call_adb("shell su root cat /data/local/tmp/winscope_status", device=self._device_id) == 'TRACE_OK\n':
928                call_adb(
929                    "shell su root rm /data/local/tmp/winscope_status", device=self._device_id)
930                log.debug("Trace finished successfully on {}".format(
931                    self._device_id))
932                self._success = True
933                break
934            log.debug("Still waiting for cleanup on {}".format(self._device_id))
935            time.sleep(0.1)
936
937    def success(self):
938        return self._success
939
940
941class StartTrace(DeviceRequestEndpoint):
942    TRACE_COMMAND = """
943set -e
944
945{perfetto_utils}
946
947echo "Starting trace..."
948echo "TRACE_START" > /data/local/tmp/winscope_status
949
950# Do not print anything to stdout/stderr in the handler
951function stop_trace() {{
952  echo "start" >/data/local/tmp/winscope_signal_handler.log
953
954  # redirect stdout/stderr to log file
955  exec 1>>/data/local/tmp/winscope_signal_handler.log
956  exec 2>>/data/local/tmp/winscope_signal_handler.log
957
958  set -x
959  trap - EXIT HUP INT
960  {stop_commands}
961  echo "TRACE_OK" > /data/local/tmp/winscope_status
962}}
963
964trap stop_trace EXIT HUP INT
965echo "Signal handler registered."
966
967# Clear perfetto config file. The start commands below are going to populate it.
968rm -f {perfetto_config_file}
969
970{start_commands}
971
972# ADB shell does not handle hung up well and does not call HUP handler when a child is active in foreground,
973# as a workaround we sleep for short intervals in a loop so the handler is called after a sleep interval.
974while true; do sleep 0.1; done
975"""
976
977    def process_with_device(self, server, path, device_id):
978        try:
979            requested_types = self.get_request(server)
980            log.debug(f"Clienting requested trace types {requested_types}")
981            requested_traces = [TRACE_TARGETS[t] for t in requested_types]
982            requested_traces = self.move_perfetto_target_to_end_of_list(requested_traces)
983        except KeyError as err:
984            raise BadRequest("Unsupported trace target\n" + str(err))
985        if device_id in TRACE_THREADS:
986            log.warning("Trace already in progress for {}", device_id)
987            server.respond(HTTPStatus.OK, b'', "text/plain")
988        if not check_root(device_id):
989            raise AdbError(
990                "Unable to acquire root privileges on the device - check the output of 'adb -s {} shell su root id'".format(
991                    device_id))
992        command = StartTrace.TRACE_COMMAND.format(
993            perfetto_utils=PERFETTO_UTILS,
994            stop_commands='\n'.join([t.trace_stop for t in requested_traces]),
995            perfetto_config_file=PERFETTO_TRACE_CONFIG_FILE,
996            start_commands='\n'.join([t.trace_start for t in requested_traces]))
997        log.debug("Trace requested for {} with targets {}".format(
998            device_id, ','.join(requested_types)))
999        log.debug(f"Executing command \"{command}\" on {device_id}...")
1000        TRACE_THREADS[device_id] = TraceThread(
1001            device_id, command.encode('utf-8'))
1002        TRACE_THREADS[device_id].start()
1003        server.respond(HTTPStatus.OK, b'', "text/plain")
1004
1005
1006class EndTrace(DeviceRequestEndpoint):
1007    def process_with_device(self, server, path, device_id):
1008        if device_id not in TRACE_THREADS:
1009            raise BadRequest("No trace in progress for {}".format(device_id))
1010        if TRACE_THREADS[device_id].is_alive():
1011            TRACE_THREADS[device_id].end_trace()
1012
1013        success = TRACE_THREADS[device_id].success()
1014
1015        signal_handler_log = call_adb("shell su root cat /data/local/tmp/winscope_signal_handler.log", device=device_id).encode('utf-8')
1016
1017        out = b"### Shell script's stdout - start\n" + \
1018            TRACE_THREADS[device_id].out + \
1019            b"### Shell script's stdout - end\n" + \
1020            b"### Shell script's stderr - start\n" + \
1021            TRACE_THREADS[device_id].err + \
1022            b"### Shell script's stderr - end\n" + \
1023            b"### Signal handler log - start\n" + \
1024            signal_handler_log + \
1025            b"### Signal handler log - end\n"
1026        command = TRACE_THREADS[device_id].trace_command
1027        TRACE_THREADS.pop(device_id)
1028        if success:
1029            server.respond(HTTPStatus.OK, out, "text/plain")
1030        else:
1031            raise AdbError(
1032                "Error tracing the device\n### Output ###\n" + out.decode(
1033                    "utf-8") + "\n### Command: adb -s {} shell ###\n### Input ###\n".format(device_id) + command.decode(
1034                    "utf-8"))
1035
1036
1037def execute_command(server, device_id, shell, configType, configValue):
1038    process = subprocess.Popen(shell, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
1039                                   stdin=subprocess.PIPE, start_new_session=True)
1040    log.debug(f"Changing trace config on device {device_id} {configType}:{configValue}")
1041    out, err = process.communicate(configValue.encode('utf-8'))
1042    if process.returncode != 0:
1043        raise AdbError(
1044            f"Error executing command:\n {configValue}\n\n### OUTPUT ###{out.decode('utf-8')}\n{err.decode('utf-8')}")
1045    log.debug(f"Changing trace config finished on device {device_id}")
1046    server.respond(HTTPStatus.OK, b'', "text/plain")
1047
1048
1049class ConfigTrace(DeviceRequestEndpoint):
1050    def process_with_device(self, server, path, device_id):
1051        try:
1052            requested_configs = self.get_request(server)
1053            config = SurfaceFlingerTraceConfig()
1054            for requested_config in requested_configs:
1055                if not config.is_valid(requested_config):
1056                    raise BadRequest(
1057                        f"Unsupported config {requested_config}\n")
1058                config.add(requested_config)
1059        except KeyError as err:
1060            raise BadRequest("Unsupported trace target\n" + str(err))
1061        if device_id in TRACE_THREADS:
1062            BadRequest(f"Trace in progress for {device_id}")
1063        if not check_root(device_id):
1064            raise AdbError(
1065                f"Unable to acquire root privileges on the device - check the output of 'adb -s {device_id} shell su root id'")
1066        command = config.command()
1067        shell = ['adb', '-s', device_id, 'shell']
1068        log.debug(f"Starting shell {' '.join(shell)}")
1069        execute_command(server, device_id, shell, "sf buffer size", command)
1070
1071
1072def add_selected_request_to_config(self, server, device_id, config):
1073    try:
1074        requested_configs = self.get_request(server)
1075        for requested_config in requested_configs:
1076            if config.is_valid(requested_config):
1077                config.add(requested_config, requested_configs[requested_config])
1078            else:
1079                raise BadRequest(
1080                        f"Unsupported config {requested_config}\n")
1081    except KeyError as err:
1082        raise BadRequest("Unsupported trace target\n" + str(err))
1083    if device_id in TRACE_THREADS:
1084        BadRequest(f"Trace in progress for {device_id}")
1085    if not check_root(device_id):
1086        raise AdbError(
1087            f"Unable to acquire root privileges on the device - check the output of 'adb -s {device_id} shell su root id'")
1088    return config
1089
1090
1091class SurfaceFlingerSelectedConfigTrace(DeviceRequestEndpoint):
1092    def process_with_device(self, server, path, device_id):
1093        config = SurfaceFlingerTraceSelectedConfig()
1094        config = add_selected_request_to_config(self, server, device_id, config)
1095        setBufferSize = config.setBufferSize()
1096        shell = ['adb', '-s', device_id, 'shell']
1097        log.debug(f"Starting shell {' '.join(shell)}")
1098        execute_command(server, device_id, shell, "sf buffer size", setBufferSize)
1099
1100
1101class WindowManagerSelectedConfigTrace(DeviceRequestEndpoint):
1102    def process_with_device(self, server, path, device_id):
1103        config = WindowManagerTraceSelectedConfig()
1104        config = add_selected_request_to_config(self, server, device_id, config)
1105        setBufferSize = config.setBufferSize()
1106        setTracingType = config.setTracingType()
1107        setTracingLevel = config.setTracingLevel()
1108        shell = ['adb', '-s', device_id, 'shell']
1109        log.debug(f"Starting shell {' '.join(shell)}")
1110        execute_command(server, device_id, shell, "tracing type", setTracingType)
1111        execute_command(server, device_id, shell, "tracing level", setTracingLevel)
1112        # /!\ buffer size must be configured last
1113        # otherwise the other configurations might override it
1114        execute_command(server, device_id, shell, "wm buffer size", setBufferSize)
1115
1116
1117class StatusEndpoint(DeviceRequestEndpoint):
1118    def process_with_device(self, server, path, device_id):
1119        if device_id not in TRACE_THREADS:
1120            raise BadRequest("No trace in progress for {}".format(device_id))
1121        TRACE_THREADS[device_id].reset_timer()
1122        server.respond(HTTPStatus.OK, str(
1123            TRACE_THREADS[device_id].is_alive()).encode("utf-8"), "text/plain")
1124
1125
1126class DumpEndpoint(DeviceRequestEndpoint):
1127    def process_with_device(self, server, path, device_id):
1128        try:
1129            requested_types = self.get_request(server)
1130            requested_traces = [DUMP_TARGETS[t] for t in requested_types]
1131            requested_traces = self.move_perfetto_target_to_end_of_list(requested_traces)
1132        except KeyError as err:
1133            raise BadRequest("Unsupported trace target\n" + str(err))
1134        if device_id in TRACE_THREADS:
1135            BadRequest("Trace in progress for {}".format(device_id))
1136        if not check_root(device_id):
1137            raise AdbError(
1138                "Unable to acquire root privileges on the device - check the output of 'adb -s {} shell su root id'"
1139                .format(device_id))
1140        dump_commands = '\n'.join(t.dump_command for t in requested_traces)
1141        command = f"""
1142{PERFETTO_UTILS}
1143
1144# Clear perfetto config file. The commands below are going to populate it.
1145rm -f {PERFETTO_DUMP_CONFIG_FILE}
1146
1147{dump_commands}
1148"""
1149        shell = ['adb', '-s', device_id, 'shell']
1150        log.debug("Starting dump shell {}".format(' '.join(shell)))
1151        process = subprocess.Popen(shell, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
1152                                   stdin=subprocess.PIPE, start_new_session=True)
1153        log.debug("Starting dump on device {}".format(device_id))
1154        out, err = process.communicate(command.encode('utf-8'))
1155        if process.returncode != 0:
1156            raise AdbError("Error executing command:\n" + command + "\n\n### OUTPUT ###" + out.decode('utf-8') + "\n"
1157                           + err.decode('utf-8'))
1158        log.debug("Dump finished on device {}".format(device_id))
1159        server.respond(HTTPStatus.OK, b'', "text/plain")
1160
1161
1162class ADBWinscopeProxy(BaseHTTPRequestHandler):
1163    def __init__(self, request, client_address, server):
1164        self.router = RequestRouter(self)
1165        listDevicesEndpoint = ListDevicesEndpoint()
1166        self.router.register_endpoint(
1167            RequestType.GET, "devices", listDevicesEndpoint)
1168        self.router.register_endpoint(
1169            RequestType.GET, "status", StatusEndpoint())
1170        self.router.register_endpoint(
1171            RequestType.GET, "fetch", FetchFilesEndpoint())
1172        self.router.register_endpoint(RequestType.POST, "start", StartTrace())
1173        self.router.register_endpoint(RequestType.POST, "end", EndTrace())
1174        self.router.register_endpoint(RequestType.POST, "dump", DumpEndpoint())
1175        self.router.register_endpoint(
1176            RequestType.POST, "configtrace", ConfigTrace())
1177        self.router.register_endpoint(
1178            RequestType.POST, "selectedsfconfigtrace", SurfaceFlingerSelectedConfigTrace())
1179        self.router.register_endpoint(
1180            RequestType.POST, "selectedwmconfigtrace", WindowManagerSelectedConfigTrace())
1181        self.router.register_endpoint(
1182            RequestType.GET, "checkwayland", CheckWaylandServiceEndpoint(listDevicesEndpoint))
1183        super().__init__(request, client_address, server)
1184
1185    def respond(self, code: int, data: bytes, mime: str) -> None:
1186        self.send_response(code)
1187        self.send_header('Content-type', mime)
1188        add_standard_headers(self)
1189        self.wfile.write(data)
1190
1191    def do_GET(self):
1192        self.router.process(RequestType.GET)
1193
1194    def do_POST(self):
1195        self.router.process(RequestType.POST)
1196
1197    def do_OPTIONS(self):
1198        self.send_response(HTTPStatus.OK)
1199        self.send_header('Allow', 'GET,POST')
1200        add_standard_headers(self)
1201        self.end_headers()
1202        self.wfile.write(b'GET,POST')
1203
1204    def log_request(self, code='-', size='-'):
1205        log.info('{} {} {}'.format(self.requestline, str(code), str(size)))
1206
1207
1208if __name__ == '__main__':
1209    args = create_argument_parser().parse_args()
1210
1211    logging.basicConfig(stream=sys.stderr, level=args.loglevel,
1212                        format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
1213
1214    log = logging.getLogger("ADBProxy")
1215    secret_token = get_token()
1216
1217    print("Winscope ADB Connect proxy version: " + VERSION)
1218    print('Winscope token: ' + secret_token)
1219
1220    httpd = HTTPServer(('localhost', args.port), ADBWinscopeProxy)
1221    try:
1222        httpd.serve_forever()
1223    except KeyboardInterrupt:
1224        log.info("Shutting down")
1225