[DO NOT MERGE] Update winscope proxy on R

Backport uspport for device paths with :

Fixes: 196260922
Test: open winscope and collect a trace
Change-Id: Ib2d8088e3a3452c1fea1cccb599dbd9d2ce642d4
This commit is contained in:
Nataniel Borges
2022-04-21 14:32:26 +00:00
parent 6cf816bbcf
commit 50775a7444

View File

@@ -42,12 +42,12 @@ import base64
# CONFIG # # CONFIG #
LOG_LEVEL = logging.WARNING LOG_LEVEL = logging.DEBUG
PORT = 5544 PORT = 5544
# Keep in sync with WINSCOPE_PROXY_VERSION in Winscope DataAdb.vue # Keep in sync with WINSCOPE_PROXY_VERSION in Winscope DataAdb.vue
VERSION = '0.8' VERSION = '0.5'
WINSCOPE_VERSION_HEADER = "Winscope-Proxy-Version" WINSCOPE_VERSION_HEADER = "Winscope-Proxy-Version"
WINSCOPE_TOKEN_HEADER = "Winscope-Token" WINSCOPE_TOKEN_HEADER = "Winscope-Token"
@@ -55,14 +55,6 @@ WINSCOPE_TOKEN_HEADER = "Winscope-Token"
# Location to save the proxy security token # Location to save the proxy security token
WINSCOPE_TOKEN_LOCATION = os.path.expanduser('~/.config/winscope/.token') WINSCOPE_TOKEN_LOCATION = os.path.expanduser('~/.config/winscope/.token')
# Winscope traces extensions
WINSCOPE_EXT = ".winscope"
WINSCOPE_EXT_LEGACY = ".pb"
WINSCOPE_EXTS = [WINSCOPE_EXT, WINSCOPE_EXT_LEGACY]
# Winscope traces directory
WINSCOPE_DIR = "/data/misc/wmtrace/"
# Max interval between the client keep-alive requests in seconds # Max interval between the client keep-alive requests in seconds
KEEP_ALIVE_INTERVAL_S = 5 KEEP_ALIVE_INTERVAL_S = 5
@@ -71,203 +63,50 @@ logging.basicConfig(stream=sys.stderr, level=LOG_LEVEL,
log = logging.getLogger("ADBProxy") log = logging.getLogger("ADBProxy")
class File:
def __init__(self, file, filetype) -> None:
self.file = file
self.type = filetype
def get_filepaths(self, device_id):
return [self.file]
def get_filetype(self):
return self.type
class FileMatcher:
def __init__(self, path, matcher, filetype) -> None:
self.path = path
self.matcher = matcher
self.type = filetype
def get_filepaths(self, device_id):
matchingFiles = call_adb(
f"shell su root find {self.path} -name {self.matcher}", device_id)
log.debug("Found file %s", matchingFiles.split('\n')[:-1])
return matchingFiles.split('\n')[:-1]
def get_filetype(self):
return self.type
class WinscopeFileMatcher(FileMatcher):
def __init__(self, path, matcher, filetype) -> None:
self.path = path
self.internal_matchers = list(map(lambda ext: FileMatcher(path, f'{matcher}{ext}', filetype),
WINSCOPE_EXTS))
self.type = filetype
def get_filepaths(self, device_id):
for matcher in self.internal_matchers:
files = matcher.get_filepaths(device_id)
if len(files) > 0:
return files
log.debug("No files found")
return []
class TraceTarget: class TraceTarget:
"""Defines a single parameter to trace. """Defines a single parameter to trace.
Attributes: Attributes:
file_matchers: the matchers used to identify the paths on the device the trace results are saved to. file: the path on the device the trace results are saved to.
trace_start: command to start the trace from adb shell, must not block. trace_start: command to start the trace from adb shell, must not block.
trace_stop: command to stop the trace, should block until the trace is stopped. trace_stop: command to stop the trace, should block until the trace is stopped.
""" """
def __init__(self, files, trace_start: str, trace_stop: str) -> None: def __init__(self, file: str, trace_start: str, trace_stop: str) -> None:
if type(files) is not list: self.file = file
files = [files]
self.files = files
self.trace_start = trace_start self.trace_start = trace_start
self.trace_stop = trace_stop self.trace_stop = trace_stop
# Order of files matters as they will be expected in that order and decoded in that order
TRACE_TARGETS = { TRACE_TARGETS = {
"window_trace": TraceTarget( "window_trace": TraceTarget(
WinscopeFileMatcher(WINSCOPE_DIR, "wm_trace", "window_trace"), "/data/misc/wmtrace/wm_trace.pb",
'su root cmd window tracing start\necho "WM trace started."', 'su root cmd window tracing start\necho "WM trace started."',
'su root cmd window tracing stop >/dev/null 2>&1' 'su root cmd window tracing stop >/dev/null 2>&1'
), ),
"accessibility_trace": TraceTarget(
WinscopeFileMatcher("/data/misc/a11ytrace", "a11y_trace", "accessibility_trace"),
'su root cmd accessibility start-trace\necho "Accessibility trace started."',
'su root cmd accessibility stop-trace >/dev/null 2>&1'
),
"layers_trace": TraceTarget( "layers_trace": TraceTarget(
WinscopeFileMatcher(WINSCOPE_DIR, "layers_trace", "layers_trace"), "/data/misc/wmtrace/layers_trace.pb",
'su root service call SurfaceFlinger 1025 i32 1\necho "SF trace started."', 'su root service call SurfaceFlinger 1025 i32 1\necho "SF trace started."',
'su root service call SurfaceFlinger 1025 i32 0 >/dev/null 2>&1' 'su root service call SurfaceFlinger 1025 i32 0 >/dev/null 2>&1'
), ),
"screen_recording": TraceTarget( "screen_recording": TraceTarget(
File(f'/data/local/tmp/screen.mp4', "screen_recording"), "/data/local/tmp/screen.winscope.mp4",
f'screenrecord --bit-rate 8M /data/local/tmp/screen.mp4 >/dev/null 2>&1 &\necho "ScreenRecorder started."', 'screenrecord --bit-rate 8M /data/local/tmp/screen.winscope.mp4 >/dev/null 2>&1 &\necho "ScreenRecorder started."',
'pkill -l SIGINT screenrecord >/dev/null 2>&1' 'pkill -l SIGINT screenrecord >/dev/null 2>&1'
), ),
"transaction": TraceTarget( "transaction": TraceTarget(
[ "/data/misc/wmtrace/transaction_trace.pb",
WinscopeFileMatcher(WINSCOPE_DIR, "transaction_trace", "transactions"),
FileMatcher(WINSCOPE_DIR, f'transaction_merges_*', "transaction_merges"),
],
'su root service call SurfaceFlinger 1020 i32 1\necho "SF transactions recording started."', 'su root service call SurfaceFlinger 1020 i32 1\necho "SF transactions recording started."',
'su root service call SurfaceFlinger 1020 i32 0 >/dev/null 2>&1' 'su root service call SurfaceFlinger 1020 i32 0 >/dev/null 2>&1'
), ),
"proto_log": TraceTarget( "proto_log": TraceTarget(
WinscopeFileMatcher(WINSCOPE_DIR, "wm_log", "proto_log"), "/data/misc/wmtrace/wm_log.pb",
'su root cmd window logging start\necho "WM logging started."', 'su root cmd window logging start\necho "WM logging started."',
'su root cmd window logging stop >/dev/null 2>&1' 'su root cmd window logging stop >/dev/null 2>&1'
), ),
"ime_trace_clients": TraceTarget(
WinscopeFileMatcher(WINSCOPE_DIR, "ime_trace_clients", "ime_trace_clients"),
'su root ime tracing start\necho "Clients IME trace started."',
'su root ime tracing stop >/dev/null 2>&1'
),
"ime_trace_service": TraceTarget(
WinscopeFileMatcher(WINSCOPE_DIR, "ime_trace_service", "ime_trace_service"),
'su root ime tracing start\necho "Service IME trace started."',
'su root ime tracing stop >/dev/null 2>&1'
),
"ime_trace_managerservice": TraceTarget(
WinscopeFileMatcher(WINSCOPE_DIR, "ime_trace_managerservice", "ime_trace_managerservice"),
'su root ime tracing start\necho "ManagerService IME trace started."',
'su root ime tracing stop >/dev/null 2>&1'
),
} }
class SurfaceFlingerTraceConfig:
"""Handles optional configuration for surfaceflinger traces.
"""
def __init__(self) -> None:
# default config flags CRITICAL | INPUT | SYNC
self.flags = 1 << 0 | 1 << 1 | 1 << 6
def add(self, config: str) -> None:
self.flags |= CONFIG_FLAG[config]
def is_valid(self, config: str) -> bool:
return config in CONFIG_FLAG
def command(self) -> str:
return f'su root service call SurfaceFlinger 1033 i32 {self.flags}'
class SurfaceFlingerTraceSelectedConfig:
"""Handles optional selected configuration for surfaceflinger traces.
"""
def __init__(self) -> None:
# defaults set for all configs
self.selectedConfigs = {
"sfbuffersize": "16000"
}
def add(self, configType, configValue) -> None:
self.selectedConfigs[configType] = configValue
def is_valid(self, configType) -> bool:
return configType in CONFIG_SF_SELECTION
def setBufferSize(self) -> str:
return f'su root service call SurfaceFlinger 1029 i32 {self.selectedConfigs["sfbuffersize"]}'
class WindowManagerTraceSelectedConfig:
"""Handles optional selected configuration for windowmanager traces.
"""
def __init__(self) -> None:
# defaults set for all configs
self.selectedConfigs = {
"wmbuffersize": "16000",
"tracinglevel": "all",
"tracingtype": "frame",
}
def add(self, configType, configValue) -> None:
self.selectedConfigs[configType] = configValue
def is_valid(self, configType) -> bool:
return configType in CONFIG_WM_SELECTION
def setBufferSize(self) -> str:
return f'su root cmd window tracing size {self.selectedConfigs["wmbuffersize"]}'
def setTracingLevel(self) -> str:
return f'su root cmd window tracing level {self.selectedConfigs["tracinglevel"]}'
def setTracingType(self) -> str:
return f'su root cmd window tracing {self.selectedConfigs["tracingtype"]}'
CONFIG_FLAG = {
"composition": 1 << 2,
"metadata": 1 << 3,
"hwc": 1 << 4
}
#Keep up to date with options in DataAdb.vue
CONFIG_SF_SELECTION = [
"sfbuffersize",
]
#Keep up to date with options in DataAdb.vue
CONFIG_WM_SELECTION = [
"wmbuffersize",
"tracingtype",
"tracinglevel",
]
class DumpTarget: class DumpTarget:
"""Defines a single parameter to trace. """Defines a single parameter to trace.
@@ -276,21 +115,19 @@ class DumpTarget:
dump_command: command to dump state to file. dump_command: command to dump state to file.
""" """
def __init__(self, files, dump_command: str) -> None: def __init__(self, file: str, dump_command: str) -> None:
if type(files) is not list: self.file = file
files = [files]
self.files = files
self.dump_command = dump_command self.dump_command = dump_command
DUMP_TARGETS = { DUMP_TARGETS = {
"window_dump": DumpTarget( "window_dump": DumpTarget(
File(f'/data/local/tmp/wm_dump{WINSCOPE_EXT}', "window_dump"), "/data/local/tmp/wm_dump.pb",
f'su root dumpsys window --proto > /data/local/tmp/wm_dump{WINSCOPE_EXT}' 'su root dumpsys window --proto > /data/local/tmp/wm_dump.pb'
), ),
"layers_dump": DumpTarget( "layers_dump": DumpTarget(
File(f'/data/local/tmp/sf_dump{WINSCOPE_EXT}', "layers_dump"), "/data/local/tmp/sf_dump.pb",
f'su root dumpsys SurfaceFlinger --proto > /data/local/tmp/sf_dump{WINSCOPE_EXT}' 'su root dumpsys SurfaceFlinger --proto > /data/local/tmp/sf_dump.pb'
) )
} }
@@ -303,21 +140,18 @@ def get_token() -> str:
try: try:
with open(WINSCOPE_TOKEN_LOCATION, 'r') as token_file: with open(WINSCOPE_TOKEN_LOCATION, 'r') as token_file:
token = token_file.readline() token = token_file.readline()
log.debug("Loaded token {} from {}".format( log.debug("Loaded token {} from {}".format(token, WINSCOPE_TOKEN_LOCATION))
token, WINSCOPE_TOKEN_LOCATION))
return token return token
except IOError: except IOError:
token = secrets.token_hex(32) token = secrets.token_hex(32)
os.makedirs(os.path.dirname(WINSCOPE_TOKEN_LOCATION), exist_ok=True) os.makedirs(os.path.dirname(WINSCOPE_TOKEN_LOCATION), exist_ok=True)
try: try:
with open(WINSCOPE_TOKEN_LOCATION, 'w') as token_file: with open(WINSCOPE_TOKEN_LOCATION, 'w') as token_file:
log.debug("Created and saved token {} to {}".format( log.debug("Created and saved token {} to {}".format(token, WINSCOPE_TOKEN_LOCATION))
token, WINSCOPE_TOKEN_LOCATION))
token_file.write(token) token_file.write(token)
os.chmod(WINSCOPE_TOKEN_LOCATION, 0o600) os.chmod(WINSCOPE_TOKEN_LOCATION, 0o600)
except IOError: except IOError:
log.error("Unable to save persistent token {} to {}".format( log.error("Unable to save persistent token {} to {}".format(token, WINSCOPE_TOKEN_LOCATION))
token, WINSCOPE_TOKEN_LOCATION))
return token return token
@@ -334,10 +168,8 @@ def add_standard_headers(server):
server.send_header('Cache-Control', 'no-cache, no-store, must-revalidate') server.send_header('Cache-Control', 'no-cache, no-store, must-revalidate')
server.send_header('Access-Control-Allow-Origin', '*') server.send_header('Access-Control-Allow-Origin', '*')
server.send_header('Access-Control-Allow-Methods', 'POST, GET, OPTIONS') server.send_header('Access-Control-Allow-Methods', 'POST, GET, OPTIONS')
server.send_header('Access-Control-Allow-Headers', server.send_header('Access-Control-Allow-Headers', WINSCOPE_TOKEN_HEADER + ', Content-Type, Content-Length')
WINSCOPE_TOKEN_HEADER + ', Content-Type, Content-Length') server.send_header('Access-Control-Expose-Headers', 'Winscope-Proxy-Version')
server.send_header('Access-Control-Expose-Headers',
'Winscope-Proxy-Version')
server.send_header(WINSCOPE_VERSION_HEADER, VERSION) server.send_header(WINSCOPE_VERSION_HEADER, VERSION)
server.end_headers() server.end_headers()
@@ -361,7 +193,7 @@ class BadRequest(Exception):
class RequestRouter: class RequestRouter:
"""Handles HTTP request authentication and routing""" """Handles HTTP request authenticationn and routing"""
def __init__(self, handler): def __init__(self, handler):
self.request = handler self.request = handler
@@ -377,8 +209,7 @@ class RequestRouter:
def __internal_error(self, error: str): def __internal_error(self, error: str):
log.error("Internal error: " + error) log.error("Internal error: " + error)
self.request.respond(HTTPStatus.INTERNAL_SERVER_ERROR, self.request.respond(HTTPStatus.INTERNAL_SERVER_ERROR, error.encode("utf-8"), 'text/txt')
error.encode("utf-8"), 'text/txt')
def __bad_token(self): def __bad_token(self):
log.info("Bad token") log.info("Bad token")
@@ -411,15 +242,11 @@ def call_adb(params: str, device: str = None, stdin: bytes = None):
log.debug("Call: " + ' '.join(command)) log.debug("Call: " + ' '.join(command))
return subprocess.check_output(command, stderr=subprocess.STDOUT, input=stdin).decode('utf-8') return subprocess.check_output(command, stderr=subprocess.STDOUT, input=stdin).decode('utf-8')
except OSError as ex: except OSError as ex:
log.debug('Error executing adb command: {}\n{}'.format( log.debug('Error executing adb command: {}\n{}'.format(' '.join(command), repr(ex)))
' '.join(command), repr(ex))) raise AdbError('Error executing adb command: {}\n{}'.format(' '.join(command), repr(ex)))
raise AdbError('Error executing adb command: {}\n{}'.format(
' '.join(command), repr(ex)))
except subprocess.CalledProcessError as ex: except subprocess.CalledProcessError as ex:
log.debug('Error executing adb command: {}\n{}'.format( log.debug('Error executing adb command: {}\n{}'.format(' '.join(command), ex.output.decode("utf-8")))
' '.join(command), ex.output.decode("utf-8"))) raise AdbError('Error executing adb command: adb {}\n{}'.format(params, ex.output.decode("utf-8")))
raise AdbError('Error executing adb command: adb {}\n{}'.format(
params, ex.output.decode("utf-8")))
def call_adb_outfile(params: str, outfile, device: str = None, stdin: bytes = None): def call_adb_outfile(params: str, outfile, device: str = None, stdin: bytes = None):
@@ -434,10 +261,8 @@ def call_adb_outfile(params: str, outfile, device: str = None, stdin: bytes = No
raise AdbError('Error executing adb command: adb {}\n'.format(params) + err.decode( raise AdbError('Error executing adb command: adb {}\n'.format(params) + err.decode(
'utf-8') + '\n' + outfile.read().decode('utf-8')) 'utf-8') + '\n' + outfile.read().decode('utf-8'))
except OSError as ex: except OSError as ex:
log.debug('Error executing adb command: adb {}\n{}'.format( log.debug('Error executing adb command: adb {}\n{}'.format(params, repr(ex)))
params, repr(ex))) raise AdbError('Error executing adb command: adb {}\n{}'.format(params, repr(ex)))
raise AdbError(
'Error executing adb command: adb {}\n{}'.format(params, repr(ex)))
class ListDevicesEndpoint(RequestEndpoint): class ListDevicesEndpoint(RequestEndpoint):
@@ -465,54 +290,34 @@ class DeviceRequestEndpoint(RequestEndpoint):
def process_with_device(self, server, path, device_id): def process_with_device(self, server, path, device_id):
pass pass
def get_request(self, server) -> str:
try:
length = int(server.headers["Content-Length"])
except KeyError as err:
raise BadRequest("Missing Content-Length header\n" + str(err))
except ValueError as err:
raise BadRequest("Content length unreadable\n" + str(err))
return json.loads(server.rfile.read(length).decode("utf-8"))
class FetchFileEndpoint(DeviceRequestEndpoint):
class FetchFilesEndpoint(DeviceRequestEndpoint):
def process_with_device(self, server, path, device_id): def process_with_device(self, server, path, device_id):
if len(path) != 1: if len(path) != 1:
raise BadRequest("File not specified") raise BadRequest("File not specified")
if path[0] in TRACE_TARGETS: if path[0] in TRACE_TARGETS:
files = TRACE_TARGETS[path[0]].files file_path = TRACE_TARGETS[path[0]].file
elif path[0] in DUMP_TARGETS: elif path[0] in DUMP_TARGETS:
files = DUMP_TARGETS[path[0]].files file_path = DUMP_TARGETS[path[0]].file
else: else:
raise BadRequest("Unknown file specified") raise BadRequest("Unknown file specified")
file_buffers = dict() with NamedTemporaryFile() as tmp:
log.debug("Fetching file {} from device to {}".format(file_path, tmp.name))
for f in files: call_adb_outfile('exec-out su root cat ' + file_path, tmp, device_id)
file_type = f.get_filetype() log.debug("Deleting file {} from device".format(file_path))
file_paths = f.get_filepaths(device_id) call_adb('shell su root rm ' + file_path, device_id)
server.send_response(HTTPStatus.OK)
for file_path in file_paths: server.send_header('X-Content-Type-Options', 'nosniff')
with NamedTemporaryFile() as tmp: server.send_header('Content-type', 'application/octet-stream')
log.debug( add_standard_headers(server)
f"Fetching file {file_path} from device to {tmp.name}") log.debug("Uploading file {}".format(tmp.name))
call_adb_outfile('exec-out su root cat ' + while True:
file_path, tmp, device_id) buf = tmp.read(1024)
log.debug(f"Deleting file {file_path} from device") if buf:
call_adb('shell su root rm ' + file_path, device_id) server.wfile.write(buf)
log.debug(f"Uploading file {tmp.name}") else:
if file_type not in file_buffers: break
file_buffers[file_type] = []
buf = base64.encodebytes(tmp.read()).decode("utf-8")
file_buffers[file_type].append(buf)
if (len(file_buffers) == 0):
log.error("Proxy didn't find any file to fetch")
# server.send_header('X-Content-Type-Options', 'nosniff')
# add_standard_headers(server)
j = json.dumps(file_buffers)
server.respond(HTTPStatus.OK, j.encode("utf-8"), "text/json")
def check_root(device_id): def check_root(device_id):
@@ -537,41 +342,34 @@ class TraceThread(threading.Thread):
self.process = subprocess.Popen(shell, stdout=subprocess.PIPE, self.process = subprocess.Popen(shell, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, stdin=subprocess.PIPE, start_new_session=True) stderr=subprocess.PIPE, stdin=subprocess.PIPE, start_new_session=True)
except OSError as ex: except OSError as ex:
raise AdbError( raise AdbError('Error executing adb command: adb shell\n{}'.format(repr(ex)))
'Error executing adb command: adb shell\n{}'.format(repr(ex)))
super().__init__() super().__init__()
def timeout(self): def timeout(self):
if self.is_alive(): if self.is_alive():
log.warning( log.warning("Keep-alive timeout for trace on {}".format(self._device_id))
"Keep-alive timeout for trace on {}".format(self._device_id))
self.end_trace() self.end_trace()
if self._device_id in TRACE_THREADS: if self._device_id in TRACE_THREADS:
TRACE_THREADS.pop(self._device_id) TRACE_THREADS.pop(self._device_id)
def reset_timer(self): def reset_timer(self):
log.debug( log.debug("Resetting keep-alive clock for trace on {}".format(self._device_id))
"Resetting keep-alive clock for trace on {}".format(self._device_id))
if self._keep_alive_timer: if self._keep_alive_timer:
self._keep_alive_timer.cancel() self._keep_alive_timer.cancel()
self._keep_alive_timer = threading.Timer( self._keep_alive_timer = threading.Timer(KEEP_ALIVE_INTERVAL_S, self.timeout)
KEEP_ALIVE_INTERVAL_S, self.timeout)
self._keep_alive_timer.start() self._keep_alive_timer.start()
def end_trace(self): def end_trace(self):
if self._keep_alive_timer: if self._keep_alive_timer:
self._keep_alive_timer.cancel() self._keep_alive_timer.cancel()
log.debug("Sending SIGINT to the trace process on {}".format( log.debug("Sending SIGINT to the trace process on {}".format(self._device_id))
self._device_id))
self.process.send_signal(signal.SIGINT) self.process.send_signal(signal.SIGINT)
try: try:
log.debug("Waiting for trace shell to exit for {}".format( log.debug("Waiting for trace shell to exit for {}".format(self._device_id))
self._device_id))
self.process.wait(timeout=5) self.process.wait(timeout=5)
except TimeoutError: except TimeoutError:
log.debug( log.debug("TIMEOUT - sending SIGKILL to the trace process on {}".format(self._device_id))
"TIMEOUT - sending SIGKILL to the trace process on {}".format(self._device_id))
self.process.kill() self.process.kill()
self.join() self.join()
@@ -583,10 +381,8 @@ class TraceThread(threading.Thread):
time.sleep(0.2) time.sleep(0.2)
for i in range(10): for i in range(10):
if call_adb("shell su root cat /data/local/tmp/winscope_status", device=self._device_id) == 'TRACE_OK\n': if call_adb("shell su root cat /data/local/tmp/winscope_status", device=self._device_id) == 'TRACE_OK\n':
call_adb( call_adb("shell su root rm /data/local/tmp/winscope_status", device=self._device_id)
"shell su root rm /data/local/tmp/winscope_status", device=self._device_id) log.debug("Trace finished successfully on {}".format(self._device_id))
log.debug("Trace finished successfully on {}".format(
self._device_id))
self._success = True self._success = True
break break
log.debug("Still waiting for cleanup on {}".format(self._device_id)) log.debug("Still waiting for cleanup on {}".format(self._device_id))
@@ -624,7 +420,13 @@ while true; do sleep 0.1; done
def process_with_device(self, server, path, device_id): def process_with_device(self, server, path, device_id):
try: try:
requested_types = self.get_request(server) length = int(server.headers["Content-Length"])
except KeyError as err:
raise BadRequest("Missing Content-Length header\n" + str(err))
except ValueError as err:
raise BadRequest("Content length unreadable\n" + str(err))
try:
requested_types = json.loads(server.rfile.read(length).decode("utf-8"))
requested_traces = [TRACE_TARGETS[t] for t in requested_types] requested_traces = [TRACE_TARGETS[t] for t in requested_types]
except KeyError as err: except KeyError as err:
raise BadRequest("Unsupported trace target\n" + str(err)) raise BadRequest("Unsupported trace target\n" + str(err))
@@ -638,10 +440,8 @@ while true; do sleep 0.1; done
command = StartTrace.TRACE_COMMAND.format( command = StartTrace.TRACE_COMMAND.format(
'\n'.join([t.trace_stop for t in requested_traces]), '\n'.join([t.trace_stop for t in requested_traces]),
'\n'.join([t.trace_start for t in requested_traces])) '\n'.join([t.trace_start for t in requested_traces]))
log.debug("Trace requested for {} with targets {}".format( log.debug("Trace requested for {} with targets {}".format(device_id, ','.join(requested_types)))
device_id, ','.join(requested_types))) TRACE_THREADS[device_id] = TraceThread(device_id, command.encode('utf-8'))
TRACE_THREADS[device_id] = TraceThread(
device_id, command.encode('utf-8'))
TRACE_THREADS[device_id].start() TRACE_THREADS[device_id].start()
server.respond(HTTPStatus.OK, b'', "text/plain") server.respond(HTTPStatus.OK, b'', "text/plain")
@@ -654,8 +454,7 @@ class EndTrace(DeviceRequestEndpoint):
TRACE_THREADS[device_id].end_trace() TRACE_THREADS[device_id].end_trace()
success = TRACE_THREADS[device_id].success() success = TRACE_THREADS[device_id].success()
out = TRACE_THREADS[device_id].out + \ out = TRACE_THREADS[device_id].out + b"\n" + TRACE_THREADS[device_id].err
b"\n" + TRACE_THREADS[device_id].err
command = TRACE_THREADS[device_id].trace_command command = TRACE_THREADS[device_id].trace_command
TRACE_THREADS.pop(device_id) TRACE_THREADS.pop(device_id)
if success: if success:
@@ -667,97 +466,24 @@ class EndTrace(DeviceRequestEndpoint):
"utf-8")) "utf-8"))
def execute_command(server, device_id, shell, configType, configValue):
process = subprocess.Popen(shell, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
stdin=subprocess.PIPE, start_new_session=True)
log.debug(f"Changing trace config on device {device_id} {configType}:{configValue}")
out, err = process.communicate(configValue.encode('utf-8'))
if process.returncode != 0:
raise AdbError(
f"Error executing command:\n {configValue}\n\n### OUTPUT ###{out.decode('utf-8')}\n{err.decode('utf-8')}")
log.debug(f"Changing trace config finished on device {device_id}")
server.respond(HTTPStatus.OK, b'', "text/plain")
class ConfigTrace(DeviceRequestEndpoint):
def process_with_device(self, server, path, device_id):
try:
requested_configs = self.get_request(server)
config = SurfaceFlingerTraceConfig()
for requested_config in requested_configs:
if not config.is_valid(requested_config):
raise BadRequest(
f"Unsupported config {requested_config}\n")
config.add(requested_config)
except KeyError as err:
raise BadRequest("Unsupported trace target\n" + str(err))
if device_id in TRACE_THREADS:
BadRequest(f"Trace in progress for {device_id}")
if not check_root(device_id):
raise AdbError(
f"Unable to acquire root privileges on the device - check the output of 'adb -s {device_id} shell su root id'")
command = config.command()
shell = ['adb', '-s', device_id, 'shell']
log.debug(f"Starting shell {' '.join(shell)}")
execute_command(server, device_id, shell, "sf buffer size", command)
def add_selected_request_to_config(self, server, device_id, config):
try:
requested_configs = self.get_request(server)
for requested_config in requested_configs:
if config.is_valid(requested_config):
config.add(requested_config, requested_configs[requested_config])
else:
raise BadRequest(
f"Unsupported config {requested_config}\n")
except KeyError as err:
raise BadRequest("Unsupported trace target\n" + str(err))
if device_id in TRACE_THREADS:
BadRequest(f"Trace in progress for {device_id}")
if not check_root(device_id):
raise AdbError(
f"Unable to acquire root privileges on the device - check the output of 'adb -s {device_id} shell su root id'")
return config
class SurfaceFlingerSelectedConfigTrace(DeviceRequestEndpoint):
def process_with_device(self, server, path, device_id):
config = SurfaceFlingerTraceSelectedConfig()
config = add_selected_request_to_config(self, server, device_id, config)
setBufferSize = config.setBufferSize()
shell = ['adb', '-s', device_id, 'shell']
log.debug(f"Starting shell {' '.join(shell)}")
execute_command(server, device_id, shell, "sf buffer size", setBufferSize)
class WindowManagerSelectedConfigTrace(DeviceRequestEndpoint):
def process_with_device(self, server, path, device_id):
config = WindowManagerTraceSelectedConfig()
config = add_selected_request_to_config(self, server, device_id, config)
setBufferSize = config.setBufferSize()
setTracingType = config.setTracingType()
setTracingLevel = config.setTracingLevel()
shell = ['adb', '-s', device_id, 'shell']
log.debug(f"Starting shell {' '.join(shell)}")
execute_command(server, device_id, shell, "wm buffer size", setBufferSize)
execute_command(server, device_id, shell, "tracing type", setTracingType)
execute_command(server, device_id, shell, "tracing level", setTracingLevel)
class StatusEndpoint(DeviceRequestEndpoint): class StatusEndpoint(DeviceRequestEndpoint):
def process_with_device(self, server, path, device_id): def process_with_device(self, server, path, device_id):
if device_id not in TRACE_THREADS: if device_id not in TRACE_THREADS:
raise BadRequest("No trace in progress for {}".format(device_id)) raise BadRequest("No trace in progress for {}".format(device_id))
TRACE_THREADS[device_id].reset_timer() TRACE_THREADS[device_id].reset_timer()
server.respond(HTTPStatus.OK, str( server.respond(HTTPStatus.OK, str(TRACE_THREADS[device_id].is_alive()).encode("utf-8"), "text/plain")
TRACE_THREADS[device_id].is_alive()).encode("utf-8"), "text/plain")
class DumpEndpoint(DeviceRequestEndpoint): class DumpEndpoint(DeviceRequestEndpoint):
def process_with_device(self, server, path, device_id): def process_with_device(self, server, path, device_id):
try: try:
requested_types = self.get_request(server) length = int(server.headers["Content-Length"])
except KeyError as err:
raise BadRequest("Missing Content-Length header\n" + str(err))
except ValueError as err:
raise BadRequest("Content length unreadable\n" + str(err))
try:
requested_types = json.loads(server.rfile.read(length).decode("utf-8"))
requested_traces = [DUMP_TARGETS[t] for t in requested_types] requested_traces = [DUMP_TARGETS[t] for t in requested_types]
except KeyError as err: except KeyError as err:
raise BadRequest("Unsupported trace target\n" + str(err)) raise BadRequest("Unsupported trace target\n" + str(err))
@@ -784,21 +510,12 @@ class DumpEndpoint(DeviceRequestEndpoint):
class ADBWinscopeProxy(BaseHTTPRequestHandler): class ADBWinscopeProxy(BaseHTTPRequestHandler):
def __init__(self, request, client_address, server): def __init__(self, request, client_address, server):
self.router = RequestRouter(self) self.router = RequestRouter(self)
self.router.register_endpoint( self.router.register_endpoint(RequestType.GET, "devices", ListDevicesEndpoint())
RequestType.GET, "devices", ListDevicesEndpoint()) self.router.register_endpoint(RequestType.GET, "status", StatusEndpoint())
self.router.register_endpoint( self.router.register_endpoint(RequestType.GET, "fetch", FetchFileEndpoint())
RequestType.GET, "status", StatusEndpoint())
self.router.register_endpoint(
RequestType.GET, "fetch", FetchFilesEndpoint())
self.router.register_endpoint(RequestType.POST, "start", StartTrace()) self.router.register_endpoint(RequestType.POST, "start", StartTrace())
self.router.register_endpoint(RequestType.POST, "end", EndTrace()) self.router.register_endpoint(RequestType.POST, "end", EndTrace())
self.router.register_endpoint(RequestType.POST, "dump", DumpEndpoint()) self.router.register_endpoint(RequestType.POST, "dump", DumpEndpoint())
self.router.register_endpoint(
RequestType.POST, "configtrace", ConfigTrace())
self.router.register_endpoint(
RequestType.POST, "selectedsfconfigtrace", SurfaceFlingerSelectedConfigTrace())
self.router.register_endpoint(
RequestType.POST, "selectedwmconfigtrace", WindowManagerSelectedConfigTrace())
super().__init__(request, client_address, server) super().__init__(request, client_address, server)
def respond(self, code: int, data: bytes, mime: str) -> None: def respond(self, code: int, data: bytes, mime: str) -> None: