xref: /aosp_15_r20/development/tools/winscope/src/adb/winscope_proxy.py (revision 90c8c64db3049935a07c6143d7fd006e26f8ecca)
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.10 and ADB installed and in system PATH.
21#
22# Usage:
23#     run: python3 winscope_proxy.py
24#
25
26import argparse
27import base64
28import gzip
29import json
30import logging
31import os
32import re
33import secrets
34import signal
35import subprocess
36import sys
37import threading
38import time
39from abc import abstractmethod
40from enum import Enum
41from http import HTTPStatus
42from http.server import HTTPServer, BaseHTTPRequestHandler
43from logging import DEBUG, INFO
44from tempfile import NamedTemporaryFile
45from typing import Callable
46
47# GLOBALS #
48
49log = None
50secret_token = None
51
52# CONFIG #
53
54def create_argument_parser() -> argparse.ArgumentParser:
55    parser = argparse.ArgumentParser(description='Proxy for go/winscope', prog='winscope_proxy')
56
57    parser.add_argument('--info', '-i', dest='loglevel', action='store_const', const=INFO)
58    parser.add_argument('--port', '-p', default=5544, action='store')
59
60    parser.set_defaults(loglevel=DEBUG)
61
62    return parser
63
64# Keep in sync with ProxyConnection#VERSION in Winscope
65VERSION = '4.0.8'
66
67PERFETTO_TRACE_CONFIG_FILE = '/data/misc/perfetto-configs/winscope-proxy-trace.conf'
68PERFETTO_DUMP_CONFIG_FILE = '/data/misc/perfetto-configs/winscope-proxy-dump.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_TRACING_SESSIONS_QUERY_START = """TRACING SESSIONS:
73
74ID      UID     STATE      BUF (#) KB   DUR (s)   #DS  STARTED  NAME
75===     ===     =====      ==========   =======   ===  =======  ====\n"""
76PERFETTO_TRACING_SESSIONS_QUERY_END = """\nNOTE: Some tracing sessions are not reported in the list above."""
77
78WINSCOPE_VERSION_HEADER = "Winscope-Proxy-Version"
79WINSCOPE_TOKEN_HEADER = "Winscope-Token"
80
81# Location to save the proxy security token
82WINSCOPE_TOKEN_LOCATION = os.path.expanduser('~/.config/winscope/.token')
83
84# Winscope traces extensions
85WINSCOPE_EXT = ".winscope"
86WINSCOPE_EXT_LEGACY = ".pb"
87WINSCOPE_EXTS = [WINSCOPE_EXT, WINSCOPE_EXT_LEGACY]
88
89# Winscope traces directories
90WINSCOPE_DIR = "/data/misc/wmtrace/"
91WINSCOPE_BACKUP_DIR = "/data/local/tmp/last_winscope_tracing_session/"
92
93# Tracing handlers
94SIGNAL_HANDLER_LOG = "/data/local/tmp/winscope_signal_handler.log"
95WINSCOPE_STATUS = "/data/local/tmp/winscope_status"
96
97# Max interval between the client keep-alive requests in seconds
98KEEP_ALIVE_INTERVAL_S = 5
99
100# Perfetto's default timeout for getting an ACK from producer processes is 5s
101# We need to be sure that the timeout is longer than that with a good margin.
102COMMAND_TIMEOUT_S = 15
103
104class File:
105    def __init__(self, file, filetype) -> None:
106        self.file = file
107        self.type = filetype
108
109    def get_filepaths(self, device_id) -> list[str]:
110        return [self.file]
111
112    def get_filetype(self) -> str:
113        return self.type
114
115
116class FileMatcher:
117    def __init__(self, path, matcher, filetype) -> None:
118        self.path = path
119        self.matcher = matcher
120        self.type = filetype
121
122    def get_filepaths(self, device_id) -> list[str]:
123        if len(self.matcher) > 0:
124            matchingFiles = call_adb(
125                f"shell su root find {self.path} -name {self.matcher}", device_id)
126        else:
127            matchingFiles = call_adb(
128                f"shell su root find {self.path}", device_id)
129
130        files = matchingFiles.split('\n')[:-1]
131        log.debug("Found files %s", files)
132        return files
133
134    def get_filetype(self) -> str:
135        return self.type
136
137
138class WinscopeFileMatcher(FileMatcher):
139    def __init__(self, path, matcher, filetype) -> None:
140        self.path = path
141        self.internal_matchers = list(map(lambda ext: FileMatcher(path, f'{matcher}{ext}', filetype),
142            WINSCOPE_EXTS))
143        self.type = filetype
144
145    def get_filepaths(self, device_id) -> list[str]:
146        for matcher in self.internal_matchers:
147            files = matcher.get_filepaths(device_id)
148            if len(files) > 0:
149                return files
150        log.debug("No files found")
151        return []
152
153
154def get_shell_args(device_id: str, type: str) -> list[str]:
155    shell = ['adb', '-s', device_id, 'shell']
156    log.debug(f"Starting {type} shell {' '.join(shell)}")
157    return shell
158
159def is_perfetto_data_source_available(name: str, query_result: str) -> bool:
160    return name in query_result
161
162def is_any_perfetto_data_source_available(query_result: str) -> bool:
163    return is_perfetto_data_source_available('android.inputmethod', query_result) or \
164       is_perfetto_data_source_available('android.protolog', query_result) or \
165       is_perfetto_data_source_available('android.surfaceflinger.layers', query_result) or \
166       is_perfetto_data_source_available('android.surfaceflinger.transactions', query_result) or \
167       is_perfetto_data_source_available('com.android.wm.shell.transition', query_result) or \
168       is_perfetto_data_source_available('android.viewcapture', query_result) or \
169       is_perfetto_data_source_available('android.windowmanager', query_result) or \
170       is_perfetto_data_source_available('android.input.inputevent', query_result)
171
172
173class TraceConfig:
174    def __init__(self, is_perfetto: bool) -> None:
175        self.is_perfetto = is_perfetto
176
177    @abstractmethod
178    def add(self, config: str, value: str | list[str] | None) -> None:
179        pass
180
181    @abstractmethod
182    def is_valid(self, config: str) -> bool:
183        pass
184
185    @abstractmethod
186    def execute_command(self, server, device_id):
187        pass
188
189    def get_trace_identifiers(self)-> list[str]:
190        return [""]
191
192    def get_optional_start_args(self, identifier)-> str:
193        return ""
194
195    def execute_optional_config_command(self, server, device_id, shell, command, config_key, config_value):
196        process = subprocess.Popen(shell, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
197                                    stdin=subprocess.PIPE, start_new_session=True)
198        out, err = process.communicate(command.encode('utf-8'))
199        if process.returncode != 0:
200            raise AdbError(
201                f"Error executing command:\n {command}\n\n### OUTPUT ###{out.decode('utf-8')}\n{err.decode('utf-8')}")
202        log.debug(f"Optional trace config changed on device {device_id} {config_key}:{config_value}")
203
204    def execute_perfetto_config_command(self, server, shell, command, trace_name):
205        process = subprocess.Popen(shell, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
206                                    stdin=subprocess.PIPE, start_new_session=True)
207        out, err = process.communicate(command.encode('utf-8'))
208        if process.returncode != 0:
209            raise AdbError(
210                f"Error executing command:\n {command}\n\n### OUTPUT ###{out.decode('utf-8')}\n{err.decode('utf-8')}")
211        log.info(f'{trace_name} (perfetto) configured to start along the other perfetto traces.')
212
213
214class SurfaceFlingerTraceConfig(TraceConfig):
215    """Handles optional configuration for Surface Flinger traces.
216    """
217    LEGACY_FLAGS_MAP = {
218        "input": 1 << 1,
219        "composition": 1 << 2,
220        "metadata": 1 << 3,
221        "hwc": 1 << 4,
222        "tracebuffers": 1 << 5,
223        "virtualdisplays": 1 << 6
224    }
225
226    PERFETTO_FLAGS_MAP = {
227        "input": "TRACE_FLAG_INPUT",
228        "composition": "TRACE_FLAG_COMPOSITION",
229        "metadata": "TRACE_FLAG_EXTRA",
230        "hwc": "TRACE_FLAG_HWC",
231        "tracebuffers": "TRACE_FLAG_BUFFERS",
232        "virtualdisplays": "TRACE_FLAG_VIRTUAL_DISPLAYS",
233    }
234
235    def __init__(self, is_perfetto: bool) -> None:
236        super().__init__(is_perfetto)
237        self.flags = []
238        self.perfetto_flags = []
239
240        # defaults set for all configs
241        self.selected_configs = {
242            "sfbuffersize": "16000"
243        }
244
245    def add(self, config: str, value: str | None) -> None:
246        if config in SurfaceFlingerTraceConfig.LEGACY_FLAGS_MAP:
247            self.flags.append(config)
248        elif config in self.selected_configs:
249            self.selected_configs[config] = value
250
251    def is_valid(self, config: str) -> bool:
252        return config in SurfaceFlingerTraceConfig.LEGACY_FLAGS_MAP or config in self.selected_configs
253
254    def execute_command(self, server, device_id):
255        shell = get_shell_args(device_id, "sf config")
256
257        if self.is_perfetto:
258            self.execute_perfetto_config_command(server, shell, self._perfetto_config_command(), "SurfaceFlinger")
259        else:
260            self.execute_optional_config_command(server, device_id, shell, self._legacy_flags_command(), "sf flags", self.flags)
261            self.execute_optional_config_command(server, device_id, shell, self._legacy_buffer_size_command(), "sf buffer size", self.selected_configs["sfbuffersize"])
262
263    def _perfetto_config_command(self) -> str:
264        flags = "\n".join([f"""trace_flags: {SurfaceFlingerTraceConfig.PERFETTO_FLAGS_MAP[flag]}""" for flag in self.flags])
265
266        return f"""
267cat << EOF >> {PERFETTO_TRACE_CONFIG_FILE}
268data_sources: {{
269    config {{
270        name: "android.surfaceflinger.layers"
271        surfaceflinger_layers_config: {{
272            mode: MODE_ACTIVE
273            {flags}
274        }}
275    }}
276}}
277EOF
278"""
279
280    def _legacy_buffer_size_command(self) -> str:
281        return f'su root service call SurfaceFlinger 1029 i32 {self.selected_configs["sfbuffersize"]}'
282
283    def _legacy_flags_command(self) -> str:
284        flags = 0
285        for flag in self.flags:
286            flags |= SurfaceFlingerTraceConfig.LEGACY_FLAGS_MAP[flag]
287
288        return f"su root service call SurfaceFlinger 1033 i32 {flags}"
289
290
291class WindowManagerTraceConfig(TraceConfig):
292    PERFETTO_LOG_LEVEL_MAP = {
293       "verbose": "LOG_LEVEL_VERBOSE",
294       "debug": "LOG_LEVEL_DEBUG",
295       "critical": "LOG_LEVEL_CRITICAL",
296    }
297
298    PERFETTO_LOG_FREQUENCY_MAP = {
299       "frame": "LOG_FREQUENCY_FRAME",
300       "transaction": "LOG_FREQUENCY_TRANSACTION",
301    }
302
303    """Handles optional selected configuration for Window Manager traces.
304    """
305
306    def __init__(self, is_perfetto: bool) -> None:
307        super().__init__(is_perfetto)
308        # defaults set for all configs
309        self.selected_configs = {
310            "wmbuffersize": "16000",
311            "tracinglevel": "debug",
312            "tracingtype": "frame",
313        }
314
315    def add(self, config_type: str, config_value: str | None) -> None:
316        self.selected_configs[config_type] = config_value
317
318    def is_valid(self, config_type: str) -> bool:
319        return config_type in self.selected_configs
320
321    def execute_command(self, server, device_id):
322        shell = get_shell_args(device_id, "wm config")
323
324        if self.is_perfetto:
325            self.execute_perfetto_config_command(server, shell, self._perfetto_config_command(), "WM")
326        else:
327            self.execute_optional_config_command(server, device_id, shell, self._legacy_tracing_type_command(), "tracing type", self.selected_configs["tracingtype"])
328            self.execute_optional_config_command(server, device_id, shell, self._legacy_tracing_level_command(), "tracing level", self.selected_configs["tracinglevel"])
329            # /!\ buffer size must be configured last
330            # otherwise the other configurations might override it
331            self.execute_optional_config_command(server, device_id, shell, self._legacy_buffer_size_command(), "wm buffer size", self.selected_configs["wmbuffersize"])
332
333    def _perfetto_config_command(self) -> str:
334        log_level = WindowManagerTraceConfig.PERFETTO_LOG_LEVEL_MAP[self.selected_configs["tracinglevel"]]
335        log_frequency = WindowManagerTraceConfig.PERFETTO_LOG_FREQUENCY_MAP[self.selected_configs["tracingtype"]]
336        return f"""
337cat << EOF >> {PERFETTO_TRACE_CONFIG_FILE}
338data_sources: {{
339    config {{
340        name: "android.windowmanager"
341        windowmanager_config: {{
342            log_level: {log_level}
343            log_frequency: {log_frequency}
344        }}
345    }}
346}}
347EOF
348"""
349
350    def _legacy_tracing_type_command(self) -> str:
351        return f'su root cmd window tracing {self.selected_configs["tracingtype"]}'
352
353    def _legacy_tracing_level_command(self) -> str:
354        return f'su root cmd window tracing level {self.selected_configs["tracinglevel"]}'
355
356    def _legacy_buffer_size_command(self) -> str:
357        return f'su root cmd window tracing size {self.selected_configs["wmbuffersize"]}'
358
359
360class ViewCaptureTraceConfig(TraceConfig):
361    """Handles perfetto config for View Capture traces."""
362
363    COMMAND = f"""
364cat << EOF >> {PERFETTO_TRACE_CONFIG_FILE}
365data_sources: {{
366    config {{
367        name: "android.viewcapture"
368    }}
369}}
370EOF
371    """
372
373    def execute_command(self, server, device_id):
374        if (self.is_perfetto):
375            shell = get_shell_args(device_id, "perfetto config view capture")
376            self.execute_perfetto_config_command(server, shell, ViewCaptureTraceConfig.COMMAND, "View Capture")
377
378class TransactionsConfig(TraceConfig):
379    """Handles perfetto config for Transactions traces."""
380
381    COMMAND = f"""
382cat << EOF >> {PERFETTO_TRACE_CONFIG_FILE}
383data_sources: {{
384    config {{
385        name: "android.surfaceflinger.transactions"
386        surfaceflinger_transactions_config: {{
387            mode: MODE_ACTIVE
388        }}
389    }}
390}}
391EOF
392    """
393
394    def execute_command(self, server, device_id):
395        if (self.is_perfetto):
396            shell = get_shell_args(device_id, "perfetto config transactions")
397            self.execute_perfetto_config_command(server, shell, TransactionsConfig.COMMAND, "SF transactions")
398
399
400class ProtoLogConfig(TraceConfig):
401    """Handles perfetto config for ProtoLog traces."""
402
403    COMMAND = f"""
404cat << EOF >> {PERFETTO_TRACE_CONFIG_FILE}
405data_sources: {{
406    config {{
407        name: "android.protolog"
408        protolog_config: {{
409            tracing_mode: ENABLE_ALL
410        }}
411    }}
412}}
413EOF
414    """
415
416    def execute_command(self, server, device_id):
417        if (self.is_perfetto):
418            shell = get_shell_args(device_id, "perfetto config protolog")
419            self.execute_perfetto_config_command(server, shell, ProtoLogConfig.COMMAND, "ProtoLog")
420
421
422class ImeConfig(TraceConfig):
423    """Handles perfetto config for IME traces."""
424
425    COMMAND = f"""
426cat << EOF >> {PERFETTO_TRACE_CONFIG_FILE}
427data_sources: {{
428    config {{
429        name: "android.inputmethod"
430    }}
431}}
432EOF
433    """
434
435    def execute_command(self, server, device_id):
436        if (self.is_perfetto):
437            shell = get_shell_args(device_id, "perfetto config ime")
438            self.execute_perfetto_config_command(server, shell, ImeConfig.COMMAND, "IME tracing")
439
440
441class TransitionTracesConfig(TraceConfig):
442    """Handles perfetto config for Transition traces."""
443
444    COMMAND = f"""
445cat << EOF >> {PERFETTO_TRACE_CONFIG_FILE}
446data_sources: {{
447    config {{
448        name: "com.android.wm.shell.transition"
449    }}
450}}
451EOF
452    """
453
454    def execute_command(self, server, device_id):
455        if (self.is_perfetto):
456            shell = get_shell_args(device_id, "perfetto config transitions")
457            self.execute_perfetto_config_command(server, shell, TransitionTracesConfig.COMMAND, "Transitions")
458
459
460class InputConfig(TraceConfig):
461    """Handles perfetto config for Input traces."""
462
463    COMMAND = f"""
464cat << EOF >> {PERFETTO_TRACE_CONFIG_FILE}
465data_sources: {{
466    config {{
467        name: "android.input.inputevent"
468        android_input_event_config {{
469            mode: TRACE_MODE_TRACE_ALL
470        }}
471    }}
472}}
473EOF
474    """
475
476    def execute_command(self, server, device_id):
477        if (self.is_perfetto):
478            shell = get_shell_args(device_id, "perfetto config input")
479            self.execute_perfetto_config_command(server, shell, InputConfig.COMMAND, "Input trace")
480
481
482class MediaBasedConfig(TraceConfig):
483    """Creates trace identifiers for Screen Recording traces and Screenshots."""
484    trace_identifiers = ["active"]
485
486    def get_trace_identifiers(self):
487        return self.trace_identifiers
488
489    def is_valid(self, config_type: str) -> bool:
490        return config_type == "displays"
491
492    def add(self, config_type: str, config_value: str | list[str] | None):
493        if config_type != "displays":
494           return
495        if config_value and len(config_value) > 0:
496            if type(config_value) == str:
497                self.trace_identifiers = [config_value.split(" ")[0]]
498            else:
499                self.trace_identifiers = list(map(lambda d: d.split(" ")[0], config_value))
500
501    def execute_command(self, server, device_id):
502        pass
503
504class ScreenRecordingConfig(MediaBasedConfig):
505    """Creates display args for Screen Recording traces."""
506    def get_optional_start_args(self, identifier):
507        if identifier == "active":
508            return ""
509        return f"--display-id {identifier}"
510
511class ScreenshotConfig(MediaBasedConfig):
512    """Creates display args for Screenshots."""
513    def get_optional_start_args(self, identifier):
514        if identifier == "active":
515            return ""
516        return f"-d {identifier}"
517
518
519class SurfaceFlingerDumpConfig(TraceConfig):
520    """Handles perfetto config for SurfaceFlinger dumps."""
521    def __init__(self, is_perfetto: bool) -> None:
522        super().__init__(is_perfetto)
523
524    def add(self, config: str, value: str | None) -> None:
525        pass
526
527    def is_valid(self, config: str) -> bool:
528        return False
529
530    def execute_command(self, server, device_id):
531        shell = get_shell_args(device_id, "sf config")
532
533        if self.is_perfetto:
534            self.execute_perfetto_config_command(server, shell, self._perfetto_config_command(), "SurfaceFlinger")
535
536    def _perfetto_config_command(self) -> str:
537        return f"""
538cat << EOF >> {PERFETTO_DUMP_CONFIG_FILE}
539data_sources: {{
540    config {{
541        name: "android.surfaceflinger.layers"
542        surfaceflinger_layers_config: {{
543            mode: MODE_DUMP
544            trace_flags: TRACE_FLAG_INPUT
545            trace_flags: TRACE_FLAG_COMPOSITION
546            trace_flags: TRACE_FLAG_HWC
547            trace_flags: TRACE_FLAG_BUFFERS
548            trace_flags: TRACE_FLAG_VIRTUAL_DISPLAYS
549        }}
550    }}
551}}
552EOF
553"""
554
555    def _legacy_buffer_size_command(self) -> str:
556        return f'su root service call SurfaceFlinger 1029 i32 {self.selected_configs["sfbuffersize"]}'
557
558    def _legacy_flags_command(self) -> str:
559        flags = 0
560        for flag in self.flags:
561            flags |= SurfaceFlingerTraceConfig.LEGACY_FLAGS_MAP[flag]
562
563        return f"su root service call SurfaceFlinger 1033 i32 {flags}"
564
565
566class TraceTarget:
567    """Defines a single parameter to trace.
568
569    Attributes:
570        trace_name: used as a key to access config and log statements during Start/End endpoints.
571        files: the matchers used to identify the paths on the device the trace results are saved to.
572        is_perfetto_available: callback to determine if perfetto tracing is available for target.
573        trace_start: command to start the trace from adb shell, must not block.
574        trace_stop: command to stop the trace, should block until the trace is stopped.
575        get_trace_config: getter for optional setup to execute pre-tracing adb commands and define start command arguments.
576    """
577
578    def __init__(
579            self,
580            trace_name: str,
581            files: list[File | FileMatcher],
582            is_perfetto_available: Callable[[str], bool],
583            trace_start: str,
584            trace_stop: str,
585            get_trace_config: Callable[[bool], TraceConfig] | None = None
586        ) -> None:
587        self.trace_name = trace_name
588        if type(files) is not list:
589            files = [files]
590        self.files = files
591        self.is_perfetto_available = is_perfetto_available
592        self.trace_start = trace_start
593        self.trace_stop = trace_stop
594        self.get_trace_config = get_trace_config
595        self.status_filename = WINSCOPE_STATUS + "_" + trace_name
596
597
598# Order of files matters as they will be expected in that order and decoded in that order
599TRACE_TARGETS = {
600    "view_capture_trace": TraceTarget(
601        "view_capture_trace",
602        File('/data/misc/wmtrace/view_capture_trace.zip', "view_capture_trace.zip"),
603        lambda res: is_perfetto_data_source_available("android.viewcapture", res),
604        """
605su root settings put global view_capture_enabled 1
606echo 'ViewCapture tracing (legacy) started.'
607        """,
608        """
609su root sh -c 'cmd launcherapps dump-view-hierarchies >/data/misc/wmtrace/view_capture_trace.zip'
610su root settings put global view_capture_enabled 0
611echo 'ViewCapture tracing (legacy) stopped.'
612        """,
613        lambda is_perfetto: ViewCaptureTraceConfig(is_perfetto)
614    ),
615    "window_trace": TraceTarget(
616        "window_trace",
617        WinscopeFileMatcher(WINSCOPE_DIR, "wm_trace", "window_trace"),
618        lambda res: is_perfetto_data_source_available('android.windowmanager', res),
619        """
620su root cmd window tracing start
621echo 'WM trace (legacy) started.'
622        """,
623        """
624su root cmd window tracing stop >/dev/null 2>&1
625echo 'WM trace (legacy) stopped.'
626        """,
627        lambda is_perfetto: WindowManagerTraceConfig(is_perfetto)
628    ),
629    "layers_trace": TraceTarget(
630        "layers_trace",
631        WinscopeFileMatcher(WINSCOPE_DIR, "layers_trace", "layers_trace"),
632        lambda res: is_perfetto_data_source_available('android.surfaceflinger.layers', res),
633        """
634su root service call SurfaceFlinger 1025 i32 1
635echo 'SF layers trace (legacy) started.'
636        """,
637        """
638su root service call SurfaceFlinger 1025 i32 0 >/dev/null 2>&1
639echo 'SF layers trace (legacy) stopped.'
640        """,
641        lambda is_perfetto: SurfaceFlingerTraceConfig(is_perfetto)
642    ),
643    "screen_recording": TraceTarget(
644        "screen_recording",
645        File(
646            '/data/local/tmp/screen_{trace_identifier}.mp4',
647            "screen_recording_{trace_identifier}"),
648        lambda res: False,
649        '''
650        settings put system show_touches 1 && \
651        settings put system pointer_location 1 && \
652        screenrecord --bugreport --bit-rate 8M {options} /data/local/tmp/screen_{trace_identifier}.mp4 & \
653        echo "ScreenRecorder started."
654        ''',
655        '''settings put system pointer_location 0 && \
656        settings put system show_touches 0 && \
657        pkill -l SIGINT screenrecord >/dev/null 2>&1
658        '''.strip(),
659        lambda is_perfetto: ScreenRecordingConfig(is_perfetto)
660    ),
661    "transactions": TraceTarget(
662        "transactions",
663        WinscopeFileMatcher(WINSCOPE_DIR, "transactions_trace", "transactions"),
664        lambda res: is_perfetto_data_source_available('android.surfaceflinger.transactions', res),
665        """
666su root service call SurfaceFlinger 1041 i32 1
667echo 'SF transactions trace (legacy) started.'
668        """,
669        "su root service call SurfaceFlinger 1041 i32 0 >/dev/null 2>&1",
670        lambda is_perfetto: TransactionsConfig(is_perfetto)
671    ),
672    "transactions_legacy": TraceTarget(
673        "transactions_legacy",
674        [
675            WinscopeFileMatcher(WINSCOPE_DIR, "transaction_trace", "transactions_legacy"),
676            FileMatcher(WINSCOPE_DIR, f'transaction_merges_*', "transaction_merges"),
677        ],
678        lambda res: False,
679        'su root service call SurfaceFlinger 1020 i32 1\necho "SF transactions recording started."',
680        'su root service call SurfaceFlinger 1020 i32 0 >/dev/null 2>&1',
681    ),
682    "proto_log": TraceTarget(
683         "proto_log",
684        WinscopeFileMatcher(WINSCOPE_DIR, "wm_log", "proto_log"),
685        lambda res: is_perfetto_data_source_available('android.protolog', res),
686        """
687su root cmd window logging start
688echo "ProtoLog (legacy) started."
689        """,
690        """
691su root cmd window logging stop >/dev/null 2>&1
692echo "ProtoLog (legacy) stopped."
693        """,
694        lambda is_perfetto: ProtoLogConfig(is_perfetto)
695    ),
696    "ime": TraceTarget(
697        "ime",
698        [WinscopeFileMatcher(WINSCOPE_DIR, "ime_trace_clients", "ime_trace_clients"),
699         WinscopeFileMatcher(WINSCOPE_DIR, "ime_trace_service", "ime_trace_service"),
700         WinscopeFileMatcher(WINSCOPE_DIR, "ime_trace_managerservice", "ime_trace_managerservice")],
701        lambda res: is_perfetto_data_source_available('android.inputmethod', res),
702        """
703su root ime tracing start
704echo "IME tracing (legacy) started."
705        """,
706        """
707su root ime tracing stop >/dev/null 2>&1
708echo "IME tracing (legacy) stopped."
709        """,
710        lambda is_perfetto: ImeConfig(is_perfetto)
711    ),
712    "wayland_trace": TraceTarget(
713        "wayland_trace",
714        WinscopeFileMatcher("/data/misc/wltrace", "wl_trace", "wl_trace"),
715        lambda res: False,
716        'su root service call Wayland 26 i32 1 >/dev/null\necho "Wayland trace started."',
717        'su root service call Wayland 26 i32 0 >/dev/null'
718    ),
719    "eventlog": TraceTarget(
720        "eventlog",
721        WinscopeFileMatcher("/data/local/tmp", "eventlog", "eventlog"),
722        lambda res: False,
723        'rm -f /data/local/tmp/eventlog.winscope && EVENT_LOG_TRACING_START_TIME=$EPOCHREALTIME\necho "Event Log trace started."',
724        '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',
725    ),
726    "transition_traces": TraceTarget(
727        "transition_traces",
728        [WinscopeFileMatcher(WINSCOPE_DIR, "wm_transition_trace", "wm_transition_trace"),
729         WinscopeFileMatcher(WINSCOPE_DIR, "shell_transition_trace", "shell_transition_trace")],
730        lambda res: is_perfetto_data_source_available('com.android.wm.shell.transition', res),
731        """
732su root cmd window shell tracing start && su root dumpsys activity service SystemUIService WMShell transitions tracing start
733echo "Transition traces (legacy) started."
734        """,
735        """
736su root cmd window shell tracing stop && su root dumpsys activity service SystemUIService WMShell transitions tracing stop >/dev/null 2>&1
737echo 'Transition traces (legacy) stopped.'
738        """,
739        lambda is_perfetto: TransitionTracesConfig(is_perfetto)
740    ),
741    "input": TraceTarget(
742        "input",
743        [WinscopeFileMatcher(WINSCOPE_DIR, "input_trace", "input_trace")],
744        lambda res: is_perfetto_data_source_available('android.input.inputevent', res),
745        "",
746        "",
747        lambda is_perfetto: InputConfig(is_perfetto)
748    ),
749    "perfetto_trace": TraceTarget(
750         "perfetto_trace",
751        File(PERFETTO_TRACE_FILE, "trace.perfetto-trace"),
752        lambda res: is_any_perfetto_data_source_available(res),
753        f"""
754cat << EOF >> {PERFETTO_TRACE_CONFIG_FILE}
755buffers: {{
756    size_kb: 500000
757    fill_policy: RING_BUFFER
758}}
759duration_ms: 0
760file_write_period_ms: 999999999
761write_into_file: true
762unique_session_name: "{PERFETTO_UNIQUE_SESSION_NAME}"
763EOF
764
765rm -f {PERFETTO_TRACE_FILE}
766perfetto --out {PERFETTO_TRACE_FILE} --txt --config {PERFETTO_TRACE_CONFIG_FILE} --detach=WINSCOPE-PROXY-TRACING-SESSION
767echo 'Started perfetto trace.'
768""",
769        """
770perfetto --attach=WINSCOPE-PROXY-TRACING-SESSION --stop
771echo 'Stopped perfetto trace.'
772""",
773    ),
774}
775
776
777class DumpTarget:
778    """Defines a single parameter to dump.
779
780    Attributes:
781        trace_name: used as a key to access config and log statements during Dump endpoint.
782        files: the matchers used to identify the paths on the device the dump results are saved to.
783        dump_command: command to dump state to file.
784        get_trace_config: getter for optional setup to execute pre-tracing adb commands and define start command arguments.
785    """
786
787    def __init__(
788            self,
789            trace_name: str,
790            files: list[File | FileMatcher],
791            is_perfetto_available: Callable[[str], bool],
792            dump_command: str,
793            get_trace_config: Callable[[bool], TraceConfig] | None = None
794        ) -> None:
795        self.trace_name = trace_name
796        if type(files) is not list:
797            files = [files]
798        self.files = files
799        self.is_perfetto_available = is_perfetto_available
800        self.dump_command = dump_command
801        self.get_trace_config = get_trace_config
802
803
804DUMP_TARGETS = {
805    "window_dump": DumpTarget(
806        "window_dump",
807        File(f'/data/local/tmp/wm_dump{WINSCOPE_EXT}', "window_dump"),
808        lambda res: False,
809        f'su root dumpsys window --proto > /data/local/tmp/wm_dump{WINSCOPE_EXT}'
810    ),
811
812    "layers_dump": DumpTarget(
813        "layers_dump",
814        File(f'/data/local/tmp/sf_dump{WINSCOPE_EXT}', "layers_dump"),
815        lambda res: is_perfetto_data_source_available('android.surfaceflinger.layers', res),
816        f"""
817su root dumpsys SurfaceFlinger --proto > /data/local/tmp/sf_dump{WINSCOPE_EXT}
818        """,
819        lambda is_perfetto: SurfaceFlingerDumpConfig(is_perfetto)
820    ),
821
822    "screenshot": DumpTarget(
823        "screenshot",
824        File("/data/local/tmp/screenshot_{trace_identifier}.png", "screenshot_{trace_identifier}.png"),
825        lambda res: False,
826        "screencap -p {options}> /data/local/tmp/screenshot_{trace_identifier}.png",
827        lambda is_perfetto: ScreenshotConfig(is_perfetto)
828    ),
829
830    "perfetto_dump": DumpTarget(
831        "perfetto_dump",
832        File(PERFETTO_DUMP_FILE, "dump.perfetto-trace"),
833        lambda res: is_any_perfetto_data_source_available(res),
834        f"""
835cat << EOF >> {PERFETTO_DUMP_CONFIG_FILE}
836buffers: {{
837    size_kb: 500000
838    fill_policy: RING_BUFFER
839}}
840duration_ms: 1
841EOF
842
843rm -f {PERFETTO_DUMP_FILE}
844perfetto --out {PERFETTO_DUMP_FILE} --txt --config {PERFETTO_DUMP_CONFIG_FILE}
845echo 'Recorded perfetto dump.'
846        """
847    )
848}
849
850
851# END OF CONFIG #
852
853
854def get_token() -> str:
855    """Returns saved proxy security token or creates new one"""
856    try:
857        with open(WINSCOPE_TOKEN_LOCATION, 'r') as token_file:
858            token = token_file.readline()
859            log.debug("Loaded token {} from {}".format(
860                token, WINSCOPE_TOKEN_LOCATION))
861            return token
862    except IOError:
863        token = secrets.token_hex(32)
864        os.makedirs(os.path.dirname(WINSCOPE_TOKEN_LOCATION), exist_ok=True)
865        try:
866            with open(WINSCOPE_TOKEN_LOCATION, 'w') as token_file:
867                log.debug("Created and saved token {} to {}".format(
868                    token, WINSCOPE_TOKEN_LOCATION))
869                token_file.write(token)
870            os.chmod(WINSCOPE_TOKEN_LOCATION, 0o600)
871        except IOError:
872            log.error("Unable to save persistent token {} to {}".format(
873                token, WINSCOPE_TOKEN_LOCATION))
874        return token
875
876
877class RequestType(Enum):
878    GET = 1
879    POST = 2
880    HEAD = 3
881
882
883def add_standard_headers(server):
884    server.send_header('Cache-Control', 'no-cache, no-store, must-revalidate')
885    server.send_header('Access-Control-Allow-Origin', '*')
886    server.send_header('Access-Control-Allow-Methods', 'POST, GET, OPTIONS')
887    server.send_header('Access-Control-Allow-Headers',
888                       WINSCOPE_TOKEN_HEADER + ', Content-Type, Content-Length')
889    server.send_header('Access-Control-Expose-Headers',
890                       'Winscope-Proxy-Version')
891    server.send_header(WINSCOPE_VERSION_HEADER, VERSION)
892    server.end_headers()
893
894
895class RequestEndpoint:
896    """Request endpoint to use with the RequestRouter."""
897
898    @abstractmethod
899    def process(self, server, path):
900        pass
901
902
903class AdbError(Exception):
904    """Unsuccessful ADB operation"""
905    pass
906
907
908class BadRequest(Exception):
909    """Invalid client request"""
910    pass
911
912
913class RequestRouter:
914    """Handles HTTP request authentication and routing"""
915
916    def __init__(self, handler):
917        self.request = handler
918        self.endpoints = {}
919
920    def register_endpoint(self, method: RequestType, name: str, endpoint: RequestEndpoint):
921        self.endpoints[(method, name)] = endpoint
922
923    def _bad_request(self, error: str):
924        log.warning("Bad request: " + error)
925        self.request.respond(HTTPStatus.BAD_REQUEST, b"Bad request!\nThis is Winscope ADB proxy.\n\n"
926                             + error.encode("utf-8"), 'text/txt')
927
928    def _internal_error(self, error: str):
929        log.error("Internal error: " + error)
930        self.request.respond(HTTPStatus.INTERNAL_SERVER_ERROR,
931                             error.encode("utf-8"), 'text/txt')
932
933    def _bad_token(self):
934        log.warning("Bad token")
935        self.request.respond(HTTPStatus.FORBIDDEN, b"Bad Winscope authorization token!\nThis is Winscope ADB proxy.\n",
936                             'text/txt')
937
938    def process(self, method: RequestType):
939        token = self.request.headers[WINSCOPE_TOKEN_HEADER]
940        if not token or token != secret_token:
941            return self._bad_token()
942        path = self.request.path.strip('/').split('/')
943        if path and len(path) > 0:
944            endpoint_name = path[0]
945            try:
946                return self.endpoints[(method, endpoint_name)].process(self.request, path[1:])
947            except KeyError as ex:
948                if "RequestType" in repr(ex):
949                    return self._bad_request("Unknown endpoint /{}/".format(endpoint_name))
950                return self._internal_error(repr(ex))
951            except AdbError as ex:
952                return self._internal_error(str(ex))
953            except BadRequest as ex:
954                return self._bad_request(str(ex))
955            except Exception as ex:
956                return self._internal_error(repr(ex))
957        self._bad_request("No endpoint specified")
958
959
960def call_adb(params: str, device: str = None, stdin: bytes = None):
961    command = ['adb'] + (['-s', device] if device else []) + params.split(' ')
962    try:
963        log.debug("Call: " + ' '.join(command))
964        return subprocess.check_output(command, stderr=subprocess.STDOUT, input=stdin).decode('utf-8')
965    except OSError as ex:
966        raise AdbError('Error executing adb command: {}\n{}'.format(
967            ' '.join(command), repr(ex)))
968    except subprocess.CalledProcessError as ex:
969        raise AdbError('Error executing adb command: adb {}\n{}'.format(
970            params, ex.output.decode("utf-8")))
971
972
973def call_adb_outfile(params: str, outfile, device: str = None, stdin: bytes = None):
974    try:
975        process = subprocess.Popen(['adb'] + (['-s', device] if device else []) + params.split(' '), stdout=outfile,
976                                   stderr=subprocess.PIPE)
977        _, err = process.communicate(stdin)
978        outfile.seek(0)
979        if process.returncode != 0:
980            raise AdbError('Error executing adb command: adb {}\n'.format(params) + err.decode(
981                'utf-8') + '\n' + outfile.read().decode('utf-8'))
982    except OSError as ex:
983        raise AdbError(
984            'Error executing adb command: adb {}\n{}'.format(params, repr(ex)))
985
986
987class ListDevicesEndpoint(RequestEndpoint):
988    ADB_INFO_RE = re.compile("^([A-Za-z0-9._:\\-]+)\\s+(\\w+)(.*model:(\\w+))?")
989    foundDevices: dict[str | int, dict[str, bool | str]] = {}
990
991    def process(self, server, path):
992        lines = list(filter(None, call_adb('devices -l').split('\n')))
993        devices = {}
994        for m in [ListDevicesEndpoint.ADB_INFO_RE.match(d) for d in lines[1:]]:
995            if m:
996                authorized = str(m.group(2)) != 'unauthorized'
997                try:
998                    screen_record_version = call_adb('shell screenrecord --version', m.group(1)) if authorized else '0'
999                except AdbError:
1000                    try:
1001                        help_text = call_adb('shell screenrecord --help', m.group(1))
1002                        version_start_index = help_text.find('v') + 1
1003                        screen_record_version = help_text[version_start_index:version_start_index + 3]
1004                    except AdbError:
1005                        screen_record_version = '0'
1006
1007                try:
1008                    displays = list(filter(None, call_adb('shell su root dumpsys SurfaceFlinger --display-id',
1009                                                          m.group(1)).split('\n'))) if authorized else []
1010                except AdbError:
1011                    displays = []
1012
1013                devices[m.group(1)] = {
1014                    'authorized': authorized,
1015                    'model': m.group(4).replace('_', ' ') if m.group(4) else '',
1016                    'displays': displays,
1017                    'screenrecord_version': screen_record_version,
1018                }
1019        self.foundDevices = devices
1020        j = json.dumps(devices)
1021        log.info("Detected devices: " + j)
1022        server.respond(HTTPStatus.OK, j.encode("utf-8"), "text/json")
1023
1024
1025
1026class CheckWaylandServiceEndpoint(RequestEndpoint):
1027    def __init__(self, listDevicesEndpoint: ListDevicesEndpoint):
1028      self._listDevicesEndpoint = listDevicesEndpoint
1029
1030    def process(self, server, path):
1031        self._listDevicesEndpoint.process(server, path)
1032        foundDevices = self._listDevicesEndpoint.foundDevices
1033
1034        if len(foundDevices) != 1:
1035            res = 'false'
1036        else:
1037            device = list(foundDevices.values())[0]
1038            if not device.get('authorized') or not device.get('model'):
1039                res = 'false'
1040            else:
1041                raw_res = call_adb('shell service check Wayland')
1042                res = 'false' if 'not found' in raw_res else 'true'
1043        server.respond(HTTPStatus.OK, res.encode("utf-8"), "text/json")
1044
1045
1046
1047class DeviceRequestEndpoint(RequestEndpoint):
1048    def process(self, server, path):
1049        if len(path) > 0 and re.fullmatch("[A-Za-z0-9._:\\-]+", path[0]):
1050            self.process_with_device(server, path[1:], path[0])
1051        else:
1052            raise BadRequest("Device id not specified")
1053
1054    @abstractmethod
1055    def process_with_device(self, server, path, device_id):
1056        pass
1057
1058    def get_request(self, server) -> str:
1059        try:
1060            length = int(server.headers["Content-Length"])
1061        except KeyError as err:
1062            raise BadRequest("Missing Content-Length header\n" + str(err))
1063        except ValueError as err:
1064            raise BadRequest("Content length unreadable\n" + str(err))
1065        return json.loads(server.rfile.read(length).decode("utf-8"))
1066
1067    def get_targets_and_prepare_for_tracing(self, server, device_id, perfetto_config_file, targets_map: dict[str, TraceTarget | DumpTarget], perfetto_name):
1068        warnings: list[str] = []
1069
1070        call_adb(f"shell su root rm -f {perfetto_config_file}", device_id)
1071        log.debug("Cleared perfetto config file for previous tracing session")
1072
1073        trace_requests: list[dict] = self.get_request(server)
1074        trace_types = [t.get("name") for t in trace_requests]
1075        log.debug(f"Received client request of {trace_types} for {device_id}")
1076
1077        perfetto_query_result = call_adb("shell perfetto --query", device_id)
1078
1079        too_many_perfetto_sessions = self.too_many_perfetto_sessions(perfetto_query_result)
1080        if (too_many_perfetto_sessions):
1081            warnings.append("Limit of 5 Perfetto sessions reached on device. Will attempt to collect legacy traces.")
1082            log.warning(warnings[0])
1083
1084        trace_targets: list[tuple[DumpTarget | TraceTarget, TraceConfig | None]] = []
1085        for t in trace_requests:
1086            try:
1087                trace_name = t.get("name")
1088                target = targets_map[trace_name]
1089                is_perfetto = (not too_many_perfetto_sessions) and target.is_perfetto_available(perfetto_query_result)
1090                config = None
1091                if target.get_trace_config is not None:
1092                    config = target.get_trace_config(is_perfetto)
1093                    self.apply_config(config, t.get("config"), server, device_id)
1094                is_valid_perfetto_target = trace_name == perfetto_name and not too_many_perfetto_sessions
1095                is_valid_trace_target = trace_name != perfetto_name and not is_perfetto
1096                if is_valid_perfetto_target or is_valid_trace_target:
1097                    trace_targets.append((target, config))
1098
1099            except KeyError as err:
1100                log.warning("Unsupported trace target\n" + str(err))
1101        trace_targets = self.move_perfetto_target_to_end_of_list(trace_targets)
1102
1103        self.check_device_and_permissions(server, device_id)
1104        self.clear_last_tracing_session(device_id)
1105
1106        log.debug("Trace requested for {} with targets {}".format(
1107            device_id, ','.join([target.trace_name for target, config in trace_targets])))
1108
1109        return trace_targets, warnings
1110
1111    def too_many_perfetto_sessions(self, perfetto_query_result: str):
1112        concurrent_sessions_start = perfetto_query_result.find(PERFETTO_TRACING_SESSIONS_QUERY_START)
1113        if concurrent_sessions_start != -1:
1114            concurrent_sessions = perfetto_query_result[concurrent_sessions_start + len(PERFETTO_TRACING_SESSIONS_QUERY_START):]
1115            log.info(f"Concurrent sessions:\n{concurrent_sessions}\n")
1116            concurrent_sessions_end = concurrent_sessions.find(PERFETTO_TRACING_SESSIONS_QUERY_END)
1117            if concurrent_sessions_end > 0:
1118                concurrent_sessions_end -= 1
1119            concurrent_sessions = concurrent_sessions[:concurrent_sessions_end]
1120            number_of_concurrent_sessions = len(concurrent_sessions.split("\n")) if len(concurrent_sessions) > 0 else 0
1121
1122            if PERFETTO_UNIQUE_SESSION_NAME in concurrent_sessions:
1123                call_adb("shell perfetto --attach=WINSCOPE-PROXY-TRACING-SESSION --stop")
1124                log.debug("Stopped already-running winscope perfetto session.")
1125                number_of_concurrent_sessions -= 1
1126            return number_of_concurrent_sessions >= 5
1127        return False
1128
1129    def apply_config(self, trace_config: TraceConfig, requested_configs: list[dict], server, device_id):
1130        for requested_config in requested_configs:
1131            config_key = requested_config.get("key")
1132            if not trace_config.is_valid(config_key):
1133                raise BadRequest(
1134                    f"Unsupported config {config_key}\n")
1135            trace_config.add(config_key, requested_config.get("value"))
1136
1137        if device_id in TRACE_THREADS:
1138            BadRequest(f"Trace in progress for {device_id}")
1139        if not self.check_root(device_id):
1140            raise AdbError(
1141                f"Unable to acquire root privileges on the device - check the output of 'adb -s {device_id} shell su root id'")
1142        trace_config.execute_command(server, device_id)
1143
1144    def check_root(self, device_id):
1145        log.debug("Checking root access on {}".format(device_id))
1146        return int(call_adb('shell su root id -u', device_id)) == 0
1147
1148    def move_perfetto_target_to_end_of_list(self, targets: list[tuple[TraceTarget, TraceConfig | None]]) -> list[tuple[TraceTarget, TraceConfig | None]]:
1149        # Make sure a perfetto target (if present) comes last in the list of targets, i.e. will
1150        # be processed last.
1151        # A perfetto target must be processed last, so that perfetto tracing is started only after
1152        # the other targets have been processed and have configured the perfetto config file.
1153        def is_perfetto_target(target: TraceTarget):
1154            return target == TRACE_TARGETS["perfetto_trace"] or target == DUMP_TARGETS["perfetto_dump"]
1155        non_perfetto_targets = [t for t in targets if not is_perfetto_target(t[0])]
1156        perfetto_targets = [t for t in targets if is_perfetto_target(t[0])]
1157        return non_perfetto_targets + perfetto_targets
1158
1159    def check_device_and_permissions(self, server, device_id):
1160        if device_id in TRACE_THREADS:
1161            log.warning("Trace already in progress for {}", device_id)
1162            server.respond(HTTPStatus.OK, b'', "text/plain")
1163        if not self.check_root(device_id):
1164            raise AdbError(
1165                "Unable to acquire root privileges on the device - check the output of 'adb -s {} shell su root id'"
1166                .format( device_id))
1167
1168    def clear_last_tracing_session(self, device_id):
1169        call_adb(f"shell su root rm -rf {WINSCOPE_BACKUP_DIR}", device_id)
1170        call_adb(f"shell su root mkdir {WINSCOPE_BACKUP_DIR}", device_id)
1171        log.debug("Cleared previous tracing session files from device")
1172
1173
1174class FetchFilesEndpoint(DeviceRequestEndpoint):
1175    def process_with_device(self, server, path, device_id):
1176        file_buffers = self.fetch_existing_files(device_id)
1177
1178        j = json.dumps(file_buffers)
1179        server.respond(HTTPStatus.OK, j.encode("utf-8"), "text/json")
1180
1181    def fetch_existing_files(self, device_id):
1182        file_buffers = dict()
1183        file = FileMatcher(f"{WINSCOPE_BACKUP_DIR}*", "", "")
1184        try:
1185            file_paths = file.get_filepaths(device_id)
1186            for file_path in file_paths:
1187                with NamedTemporaryFile() as tmp:
1188                    file_name = file_path.split('/')[-1] + ".gz"
1189                    log.debug(
1190                        f"Fetching file {file_path} from device to {tmp.name}")
1191                    try:
1192                        call_adb_outfile('exec-out su root cat ' +
1193                                            file_path, tmp, device_id)
1194                    except AdbError as ex:
1195                        log.warning(f"Unable to fetch file {file_path} - {repr(ex)}")
1196                        return
1197                    log.debug(f"Uploading file {tmp.name}")
1198                    if file_name not in file_buffers:
1199                        file_buffers[file_name] = []
1200                    buf = base64.encodebytes(gzip.compress(tmp.read())).decode("utf-8")
1201                    file_buffers[file_name].append(buf)
1202        except:
1203            self.log_no_files_warning()
1204        return file_buffers
1205
1206    def log_no_files_warning(self):
1207        log.warning("Proxy didn't find any file to fetch")
1208
1209
1210TRACE_THREADS = {}
1211
1212class TraceThread(threading.Thread):
1213    def __init__(self, trace_name: str, device_id: str, command: str, trace_identifier: str, status_filename: str):
1214        self.trace_command = command
1215        self.trace_name = trace_name
1216        self.trace_identifier = trace_identifier
1217        self.status_filename = status_filename
1218        self._device_id = device_id
1219        self._keep_alive_timer = None
1220        self.out = None,
1221        self.err = None,
1222        self._command_timed_out = False
1223        self._success = False
1224        try:
1225            shell = get_shell_args(self._device_id, "trace")
1226            self.process = subprocess.Popen(shell, stdout=subprocess.PIPE,
1227                                            stderr=subprocess.PIPE, stdin=subprocess.PIPE, start_new_session=True)
1228        except OSError as ex:
1229            raise AdbError(
1230                'Error executing adb command for trace {}: adb shell\n{}'.format(trace_name, repr(ex)))
1231
1232        super().__init__()
1233
1234    def timeout(self):
1235        if self.is_alive():
1236            log.warning("Keep-alive timeout for {} trace on {}".format(self.trace_name, self._device_id))
1237            self.end_trace()
1238
1239    def reset_timer(self):
1240        log.info(
1241            "Resetting keep-alive clock for {} trace on {}".format(self.trace_name, self._device_id))
1242        if self._keep_alive_timer:
1243            self._keep_alive_timer.cancel()
1244        self._keep_alive_timer = threading.Timer(
1245            KEEP_ALIVE_INTERVAL_S, self.timeout)
1246        self._keep_alive_timer.start()
1247
1248    def end_trace(self):
1249        if self._keep_alive_timer:
1250            self._keep_alive_timer.cancel()
1251        log.info("Sending SIGINT to the {} process on {}".format(
1252            self.trace_name,
1253            self._device_id))
1254        self.process.send_signal(signal.SIGINT)
1255        try:
1256            log.debug("Waiting for {} trace shell to exit for {}".format(
1257                self.trace_name,
1258                self._device_id))
1259            self.process.wait(timeout=COMMAND_TIMEOUT_S)
1260        except TimeoutError:
1261            log.error(
1262                "TIMEOUT - sending SIGKILL to the {} trace process on {}".format(self.trace_name, self._device_id))
1263            self.process.kill()
1264        self.join()
1265
1266    def run(self):
1267        retry_interval = 0.1
1268        log.info("Trace {} started on {}".format(self.trace_name, self._device_id))
1269        self.reset_timer()
1270        self.out, self.err = self.process.communicate(self.trace_command)
1271        log.info("Trace {} ended on {}, waiting for cleanup".format(self.trace_name, self._device_id))
1272        time.sleep(0.2)
1273        for i in range(int(COMMAND_TIMEOUT_S / retry_interval)):
1274            if call_adb(f"shell su root cat {self.status_filename}", device=self._device_id) == 'TRACE_OK\n':
1275                log.info("Trace {} finished on {}".format(
1276                    self.trace_name,
1277                    self._device_id))
1278                if self.trace_name == "perfetto_trace":
1279                    self._success = True
1280                else:
1281                    self._success = len(self.err) == 0
1282                return
1283            log.debug("Still waiting for cleanup on {} for {}".format(self._device_id, self.trace_name))
1284            time.sleep(retry_interval)
1285
1286        self._command_timed_out = True
1287
1288    def success(self):
1289        return self._success
1290
1291    def timed_out(self):
1292        return self._command_timed_out
1293
1294class StartTraceEndpoint(DeviceRequestEndpoint):
1295    TRACE_COMMAND = """
1296set -e
1297
1298echo "Starting trace..."
1299echo "TRACE_START" > {winscope_status}
1300
1301# Do not print anything to stdout/stderr in the handler
1302function stop_trace() {{
1303  echo "start" >{signal_handler_log}
1304
1305  # redirect stdout/stderr to log file
1306  exec 1>>{signal_handler_log}
1307  exec 2>>{signal_handler_log}
1308
1309  set -x
1310  trap - EXIT HUP INT
1311  {stop_commands}
1312  echo "TRACE_OK" > {winscope_status}
1313}}
1314
1315trap stop_trace EXIT HUP INT
1316echo "Signal handler registered."
1317
1318{start_commands}
1319
1320# ADB shell does not handle hung up well and does not call HUP handler when a child is active in foreground,
1321# as a workaround we sleep for short intervals in a loop so the handler is called after a sleep interval.
1322while true; do sleep 0.1; done
1323"""
1324
1325    def process_with_device(self, server, path, device_id):
1326        trace_targets, warnings = self.get_targets_and_prepare_for_tracing(
1327            server, device_id, PERFETTO_TRACE_CONFIG_FILE, TRACE_TARGETS, "perfetto_trace")
1328
1329        for t, config in trace_targets:
1330            if config:
1331                trace_identifiers = config.get_trace_identifiers()
1332            else:
1333                trace_identifiers = [""]
1334
1335            for trace_identifier in trace_identifiers:
1336                if trace_identifier:
1337                    if config:
1338                        options = config.get_optional_start_args(trace_identifier)
1339                    else:
1340                        options = ""
1341                    start_cmd = t.trace_start.format(trace_identifier=trace_identifier, options=options)
1342                else:
1343                    start_cmd = t.trace_start
1344
1345                command = StartTraceEndpoint.TRACE_COMMAND.format(
1346                    winscope_status=t.status_filename,
1347                    signal_handler_log=SIGNAL_HANDLER_LOG,
1348                    stop_commands=t.trace_stop,
1349                    perfetto_config_file=PERFETTO_TRACE_CONFIG_FILE,
1350                    start_commands=start_cmd,
1351                )
1352                log.debug(f"Executing start command for {t.trace_name} on {device_id}...")
1353                thread = TraceThread(t.trace_name, device_id, command.encode('utf-8'), trace_identifier, t.status_filename)
1354                if device_id not in TRACE_THREADS:
1355                    TRACE_THREADS[device_id] = [thread]
1356                else:
1357                    TRACE_THREADS[device_id].append(thread)
1358                thread.start()
1359
1360        server.respond(HTTPStatus.OK, json.dumps(warnings).encode("utf-8"), "text/json")
1361
1362
1363def move_collected_files(files: list[File | FileMatcher], device_id, trace_identifier):
1364    for f in files:
1365        file_paths = f.get_filepaths(device_id)
1366        file_type = f.get_filetype().format(trace_identifier=trace_identifier)
1367
1368        for file_path in file_paths:
1369            formatted_path = file_path.format(trace_identifier=trace_identifier)
1370            log.debug(f"Moving file {formatted_path} to {WINSCOPE_BACKUP_DIR}{file_type} on device")
1371            try:
1372                call_adb(
1373                    f"shell su root [ ! -f {formatted_path} ] || su root mv {formatted_path} {WINSCOPE_BACKUP_DIR}{file_type}",
1374                    device_id)
1375            except AdbError as ex:
1376                log.warning(f"Unable to move file {formatted_path} - {repr(ex)}")
1377
1378
1379class EndTraceEndpoint(DeviceRequestEndpoint):
1380    def process_with_device(self, server, path, device_id):
1381        if device_id not in TRACE_THREADS:
1382            raise BadRequest("No trace in progress for {}".format(device_id))
1383
1384        errors: list[str] = []
1385
1386        for thread in TRACE_THREADS[device_id]:
1387            if thread.is_alive():
1388                thread.end_trace()
1389            success = thread.success()
1390            signal_handler_log = call_adb(f"shell su root cat {SIGNAL_HANDLER_LOG}", device=device_id).encode('utf-8')
1391
1392            if (thread.timed_out()):
1393                timeout_message = "Trace {} timed out during cleanup".format(thread.trace_name)
1394                errors.append(timeout_message)
1395                log.error(timeout_message)
1396
1397            if not success:
1398                log.error("Error ending trace {} on the device".format(thread.trace_name))
1399                errors.append("Error ending trace {} on the device: {}".format(thread.trace_name, thread.err))
1400
1401            out = b"### Shell script's stdout ###\n" + \
1402                (thread.out if thread.out else b'<no stdout>') + \
1403                b"\n### Shell script's stderr ###\n" + \
1404                (thread.err if thread.err else b'<no stderr>') + \
1405                b"\n### Signal handler log ###\n" + \
1406                (signal_handler_log if signal_handler_log else b'<no signal handler logs>') + \
1407                b"\n"
1408            log.debug("### Output ###\n".format(thread.trace_name) + out.decode("utf-8"))
1409            if thread.trace_name in TRACE_TARGETS:
1410                files = TRACE_TARGETS[thread.trace_name].files
1411                move_collected_files(files, device_id, thread.trace_identifier)
1412            else:
1413                errors.append(f"File location unknown for {thread.trace_name}")
1414
1415        call_adb(f"shell su root rm {thread.status_filename}", device=device_id)
1416        TRACE_THREADS.pop(device_id)
1417        server.respond(HTTPStatus.OK, json.dumps(errors).encode("utf-8"), "text/plain")
1418
1419
1420class StatusEndpoint(DeviceRequestEndpoint):
1421    def process_with_device(self, server, path, device_id):
1422        if device_id not in TRACE_THREADS:
1423            raise BadRequest("No trace in progress for {}".format(device_id))
1424        for thread in TRACE_THREADS[device_id]:
1425            thread.reset_timer()
1426        server.respond(HTTPStatus.OK, str(TRACE_THREADS[device_id][0].is_alive()).encode("utf-8"), "text/plain")
1427
1428
1429class DumpEndpoint(DeviceRequestEndpoint):
1430    def process_with_device(self, server, path, device_id):
1431        dump_targets, warnings = self.get_targets_and_prepare_for_tracing(
1432            server, device_id, PERFETTO_DUMP_CONFIG_FILE, DUMP_TARGETS, "perfetto_dump")
1433
1434        dump_commands = []
1435        for t, config in dump_targets:
1436            if config:
1437                for trace_identifier in config.get_trace_identifiers():
1438                    dump_commands.append(
1439                        t.dump_command.format(trace_identifier=trace_identifier, options=config.get_optional_start_args(trace_identifier)))
1440            else:
1441                dump_commands.append(t.dump_command)
1442
1443        dump_commands = '\n'.join(dump_commands)
1444
1445        shell = get_shell_args(device_id, "dump")
1446        process = subprocess.Popen(shell, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
1447                                   stdin=subprocess.PIPE, start_new_session=True)
1448        log.info("Starting dump on device {}".format(device_id))
1449        out, err = process.communicate(dump_commands.encode('utf-8'))
1450        if process.returncode != 0:
1451            raise AdbError("Error executing dump command." + "\n\n### OUTPUT ###" + out.decode('utf-8') + "\n"
1452                           + err.decode('utf-8'))
1453        log.info("Dump finished on device {}".format(device_id))
1454
1455        for target, config in dump_targets:
1456            if config:
1457                trace_identifiers = config.get_trace_identifiers()
1458                for trace_identifier in trace_identifiers:
1459                    move_collected_files(target.files, device_id, trace_identifier)
1460            else:
1461                move_collected_files(target.files, device_id, "")
1462
1463        server.respond(HTTPStatus.OK, json.dumps(warnings).encode("utf-8"), "text/plain")
1464
1465
1466class ADBWinscopeProxy(BaseHTTPRequestHandler):
1467    def __init__(self, request, client_address, server):
1468        self.router = RequestRouter(self)
1469        listDevicesEndpoint = ListDevicesEndpoint()
1470        self.router.register_endpoint(
1471            RequestType.GET, "devices", listDevicesEndpoint)
1472        self.router.register_endpoint(
1473            RequestType.GET, "status", StatusEndpoint())
1474        self.router.register_endpoint(
1475            RequestType.GET, "fetch", FetchFilesEndpoint())
1476        self.router.register_endpoint(RequestType.POST, "start", StartTraceEndpoint())
1477        self.router.register_endpoint(RequestType.POST, "end", EndTraceEndpoint())
1478        self.router.register_endpoint(RequestType.POST, "dump", DumpEndpoint())
1479        self.router.register_endpoint(
1480            RequestType.GET, "checkwayland", CheckWaylandServiceEndpoint(listDevicesEndpoint))
1481        super().__init__(request, client_address, server)
1482
1483    def respond(self, code: int, data: bytes, mime: str) -> None:
1484        self.send_response(code)
1485        self.send_header('Content-type', mime)
1486        add_standard_headers(self)
1487        self.wfile.write(data)
1488
1489    def do_GET(self):
1490        self.router.process(RequestType.GET)
1491
1492    def do_POST(self):
1493        self.router.process(RequestType.POST)
1494
1495    def do_OPTIONS(self):
1496        self.send_response(HTTPStatus.OK)
1497        self.send_header('Allow', 'GET,POST')
1498        add_standard_headers(self)
1499        self.end_headers()
1500        self.wfile.write(b'GET,POST')
1501
1502    def log_request(self, code='-', size='-'):
1503        log.info('{} {} {}'.format(self.requestline, str(code), str(size)))
1504
1505
1506if __name__ == '__main__':
1507    args = create_argument_parser().parse_args()
1508
1509    logging.basicConfig(stream=sys.stderr, level=args.loglevel,
1510                        format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
1511
1512    log = logging.getLogger("ADBProxy")
1513    secret_token = get_token()
1514
1515    print("Winscope ADB Connect proxy version: " + VERSION)
1516    print('Winscope token: ' + secret_token)
1517
1518    httpd = HTTPServer(('localhost', args.port), ADBWinscopeProxy)
1519    try:
1520        httpd.serve_forever()
1521    except KeyboardInterrupt:
1522        log.info("Shutting down")
1523