• Home
  • History
  • Annotate
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 #!/usr/bin/python3
2 
3 # Copyright (C) 2019 The Android Open Source Project
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
8 #
9 #      http://www.apache.org/licenses/LICENSE-2.0
10 #
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
16 
17 #
18 # This is an ADB proxy for Winscope.
19 #
20 # Requirements: python3.5 and ADB installed and in system PATH.
21 #
22 # Usage:
23 #     run: python3 winscope_proxy.py
24 #
25 
26 import argparse
27 import base64
28 import json
29 import logging
30 import os
31 import re
32 import secrets
33 import signal
34 import subprocess
35 import sys
36 import threading
37 import time
38 from abc import abstractmethod
39 from enum import Enum
40 from http import HTTPStatus
41 from http.server import HTTPServer, BaseHTTPRequestHandler
42 from logging import DEBUG, INFO, WARNING
43 from tempfile import NamedTemporaryFile
44 
45 # GLOBALS #
46 
47 log = None
48 secret_token = None
49 
50 # CONFIG #
51 
52 def 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
64 VERSION = '2.1.1'
65 
66 PERFETTO_TRACE_CONFIG_FILE = '/data/misc/perfetto-configs/winscope-proxy-trace.conf'
67 PERFETTO_DUMP_CONFIG_FILE = '/data/misc/perfetto-configs/winscope-proxy-dump.conf'
68 PERFETTO_SF_CONFIG_FILE = '/data/misc/perfetto-configs/winscope-proxy.surfaceflinger.conf'
69 PERFETTO_TRACE_FILE = '/data/misc/perfetto-traces/winscope-proxy-trace.perfetto-trace'
70 PERFETTO_DUMP_FILE = '/data/misc/perfetto-traces/winscope-proxy-dump.perfetto-trace'
71 PERFETTO_UNIQUE_SESSION_NAME = 'winscope proxy perfetto tracing'
72 PERFETTO_UTILS = f"""
73 function 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 
82 function 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 
90 function 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 
102 function 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 
112 WINSCOPE_VERSION_HEADER = "Winscope-Proxy-Version"
113 WINSCOPE_TOKEN_HEADER = "Winscope-Token"
114 
115 # Location to save the proxy security token
116 WINSCOPE_TOKEN_LOCATION = os.path.expanduser('~/.config/winscope/.token')
117 
118 # Winscope traces extensions
119 WINSCOPE_EXT = ".winscope"
120 WINSCOPE_EXT_LEGACY = ".pb"
121 WINSCOPE_EXTS = [WINSCOPE_EXT, WINSCOPE_EXT_LEGACY]
122 
123 # Winscope traces directory
124 WINSCOPE_DIR = "/data/misc/wmtrace/"
125 
126 # Max interval between the client keep-alive requests in seconds
127 KEEP_ALIVE_INTERVAL_S = 5
128 
129 class 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 
141 class 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 
158 class 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 
174 class 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
192 TRACE_TARGETS = {
193     "view_capture_trace": TraceTarget(
194         File('/data/misc/wmtrace/view_capture_trace.zip', "view_capture_trace.zip"),
195     f"""
196 if is_flag_set windowing_tools/android.tracing.perfetto_view_capture_tracing; then
197     cat << EOF >> {PERFETTO_TRACE_CONFIG_FILE}
198 data_sources: {{
199     config {{
200         name: "android.viewcapture"
201     }}
202 }}
203 EOF
204     echo 'ViewCapture tracing (perfetto) configured to start along the other perfetto traces'
205 else
206     su root settings put global view_capture_enabled 1
207     echo 'ViewCapture tracing (legacy) started.'
208 fi
209 """,
210     """
211 if ! 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.'
215 fi
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"""
226 if 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'
229 else
230     su root service call SurfaceFlinger 1025 i32 1
231     echo 'SF layers trace (legacy) started'
232 fi
233         """,
234         """
235 if ! 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.'
238 fi
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"""
257 if is_perfetto_data_source_available android.surfaceflinger.transactions; then
258     cat << EOF >> {PERFETTO_TRACE_CONFIG_FILE}
259 data_sources: {{
260     config {{
261         name: "android.surfaceflinger.transactions"
262         surfaceflinger_transactions_config: {{
263             mode: MODE_ACTIVE
264         }}
265     }}
266 }}
267 EOF
268     echo 'SF transactions trace (perfetto) configured to start along the other perfetto traces'
269 else
270     su root service call SurfaceFlinger 1041 i32 1
271     echo 'SF transactions trace (legacy) started'
272 fi
273 """,
274         """
275 if ! is_perfetto_data_source_available android.surfaceflinger.transactions; then
276     su root service call SurfaceFlinger 1041 i32 0 >/dev/null 2>&1
277 fi
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"""
291 if 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}
294 data_sources: {{
295     config {{
296         name: "android.protolog"
297         protolog_config: {{
298             tracing_mode: ENABLE_ALL
299         }}
300     }}
301 }}
302 EOF
303     echo 'ProtoLog (perfetto) configured to start along the other perfetto traces'
304 else
305     su root cmd window logging start
306     echo "ProtoLog (legacy) started."
307 fi
308         """,
309         """
310 if ! 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."
314 fi
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"""
322 if is_flag_set windowing_tools/android.tracing.perfetto_ime; then
323     cat << EOF >> {PERFETTO_TRACE_CONFIG_FILE}
324 data_sources: {{
325     config {{
326         name: "android.inputmethod"
327     }}
328 }}
329 EOF
330     echo 'IME tracing (perfetto) configured to start along the other perfetto traces'
331 else
332     su root ime tracing start
333     echo "IME tracing (legacy) started."
334 fi
335 """,
336     """
337 if ! 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."
340 fi
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"""
357 if 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}
360 data_sources: {{
361     config {{
362         name: "com.android.wm.shell.transition"
363     }}
364 }}
365 EOF
366     echo 'Transition trace (perfetto) configured to start along the other perfetto traces'
367 else
368     su root cmd window shell tracing start && su root dumpsys activity service SystemUIService WMShell transitions tracing start
369     echo "Transition traces (legacy) started."
370 fi
371         """,
372         """
373 if ! 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.'
377 fi
378 """
379     ),
380     "perfetto_trace": TraceTarget(
381         File(PERFETTO_TRACE_FILE, "trace.perfetto-trace"),
382         f"""
383 if is_any_perfetto_data_source_available; then
384     cat << EOF >> {PERFETTO_TRACE_CONFIG_FILE}
385 buffers: {{
386     size_kb: 80000
387     fill_policy: RING_BUFFER
388 }}
389 duration_ms: 0
390 file_write_period_ms: 999999999
391 write_into_file: true
392 unique_session_name: "{PERFETTO_UNIQUE_SESSION_NAME}"
393 EOF
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'
406 fi
407 """,
408         """
409 if is_any_perfetto_data_source_available; then
410     perfetto --attach=WINSCOPE-PROXY-TRACING-SESSION --stop
411     echo 'Stopped perfetto trace'
412 fi
413 """,
414     )
415 }
416 
417 
418 class 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 
442 if is_perfetto_data_source_available android.surfaceflinger.layers; then
443     cat << EOF > {PERFETTO_SF_CONFIG_FILE}
444 data_sources: {{
445     config {{
446         name: "android.surfaceflinger.layers"
447         surfaceflinger_layers_config: {{
448             mode: MODE_ACTIVE
449             {perfetto_flags}
450         }}
451     }}
452 }}
453 EOF
454     echo 'SF trace (perfetto) configured.'
455 else
456     su root service call SurfaceFlinger 1033 i32 {legacy_flags}
457     echo 'SF trace (legacy) configured'
458 fi
459 """
460 
461 class 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 
480 class 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 
508 SF_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 
517 SF_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
527 CONFIG_SF_SELECTION = [
528     "sfbuffersize",
529 ]
530 
531 #Keep up to date with options in trace_collection_utils.ts
532 CONFIG_WM_SELECTION = [
533     "wmbuffersize",
534     "tracingtype",
535     "tracinglevel",
536 ]
537 
538 class 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 
553 DUMP_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"""
562 if is_perfetto_data_source_available android.surfaceflinger.layers; then
563     cat << EOF >> {PERFETTO_DUMP_CONFIG_FILE}
564 data_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 }}
577 EOF
578     echo 'SF transactions trace (perfetto) configured to start along the other perfetto traces'
579 else
580     su root dumpsys SurfaceFlinger --proto > /data/local/tmp/sf_dump{WINSCOPE_EXT}
581 fi
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"""
593 if is_any_perfetto_data_source_available; then
594     cat << EOF >> {PERFETTO_DUMP_CONFIG_FILE}
595 buffers: {{
596     size_kb: 50000
597     fill_policy: RING_BUFFER
598 }}
599 duration_ms: 1
600 EOF
601 
602     rm -f {PERFETTO_DUMP_FILE}
603     perfetto --out {PERFETTO_DUMP_FILE} --txt --config {PERFETTO_DUMP_CONFIG_FILE}
604     echo 'Recorded perfetto dump'
605 fi
606         """
607     )
608 }
609 
610 
611 # END OF CONFIG #
612 
613 
614 def 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 
637 class RequestType(Enum):
638     GET = 1
639     POST = 2
640     HEAD = 3
641 
642 
643 def 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 
655 class RequestEndpoint:
656     """Request endpoint to use with the RequestRouter."""
657 
658     @abstractmethod
659     def process(self, server, path):
660         pass
661 
662 
663 class AdbError(Exception):
664     """Unsuccessful ADB operation"""
665     pass
666 
667 
668 class BadRequest(Exception):
669     """Invalid client request"""
670     pass
671 
672 
673 class 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 
718 def 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 
735 def 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 
753 class 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 
771 class 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 
787 class 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 
820 class 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 
860 def 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 
865 TRACE_THREADS = {}
866 
867 
868 class 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 
941 class StartTrace(DeviceRequestEndpoint):
942     TRACE_COMMAND = """
943 set -e
944 
945 {perfetto_utils}
946 
947 echo "Starting trace..."
948 echo "TRACE_START" > /data/local/tmp/winscope_status
949 
950 # Do not print anything to stdout/stderr in the handler
951 function 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 
964 trap stop_trace EXIT HUP INT
965 echo "Signal handler registered."
966 
967 # Clear perfetto config file. The start commands below are going to populate it.
968 rm -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.
974 while 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 
1006 class 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 
1037 def 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 
1049 class 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 
1072 def 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 
1091 class 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 
1101 class 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 
1117 class 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 
1126 class 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.
1145 rm -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 
1162 class 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 
1208 if __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