Merge changes from topic "Trace Collection"
* changes: Add component tests and an upload card. Adding ability to run and end traces via proxy. Refactor code to use common connection. Migration of proxy. trace collection UI
This commit is contained in:
committed by
Android (Google) Code Review
commit
8181e2a221
1496
tools/winscope-ng/package-lock.json
generated
1496
tools/winscope-ng/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -22,13 +22,22 @@
|
||||
"@angular/core": "^14.0.1",
|
||||
"@angular/elements": "^14.0.1",
|
||||
"@angular/forms": "^14.0.0",
|
||||
"@angular/material": "^14.0.4",
|
||||
"@angular/platform-browser": "^14.0.0",
|
||||
"@angular/platform-browser-dynamic": "^14.0.0",
|
||||
"@angular/router": "^14.0.0",
|
||||
"@auth0/auth0-angular": "^1.10.0",
|
||||
"@ngrx/effects": "^14.0.2",
|
||||
"@ngrx/store": "^14.0.2",
|
||||
"@ngxs/store": "^3.7.4",
|
||||
"@types/jsbn": "^1.2.30",
|
||||
"angular2-template-loader": "^0.6.2",
|
||||
"auth0": "^2.42.0",
|
||||
"html-loader": "^3.1.0",
|
||||
"html-webpack-inline-source-plugin": "^1.0.0-beta.2",
|
||||
"html-webpack-plugin": "^5.5.0",
|
||||
"jsbn": "^1.1.0",
|
||||
"jsbn-rsa": "^1.0.4",
|
||||
"kotlin": "^1.7.0",
|
||||
"kotlin-compiler": "^1.7.0",
|
||||
"loader-utils": "^2.0.0",
|
||||
@@ -46,7 +55,11 @@
|
||||
"@angular-devkit/build-angular": "^14.0.0",
|
||||
"@angular/cli": "~14.0.0",
|
||||
"@angular/compiler-cli": "^14.0.0",
|
||||
"@ngxs/devtools-plugin": "^3.7.4",
|
||||
"@types/jasmine": "~4.0.0",
|
||||
"@types/jquery": "^3.5.14",
|
||||
"@types/node": "^18.0.4",
|
||||
"@types/w3c-web-usb": "^1.0.6",
|
||||
"@typescript-eslint/eslint-plugin": "^5.30.6",
|
||||
"@typescript-eslint/parser": "^5.30.6",
|
||||
"eslint": "^8.19.0",
|
||||
|
||||
867
tools/winscope-ng/src/adb/winscope_proxy.py
Normal file
867
tools/winscope-ng/src/adb/winscope_proxy.py
Normal file
@@ -0,0 +1,867 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
# Copyright (C) 2019 The Android Open Source Project
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
#
|
||||
# This is an ADB proxy for Winscope.
|
||||
#
|
||||
# Requirements: python3.5 and ADB installed and in system PATH.
|
||||
#
|
||||
# Usage:
|
||||
# run: python3 winscope_proxy.py
|
||||
#
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import secrets
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from abc import abstractmethod
|
||||
from enum import Enum
|
||||
from http import HTTPStatus
|
||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||
from tempfile import NamedTemporaryFile
|
||||
import base64
|
||||
|
||||
# CONFIG #
|
||||
|
||||
LOG_LEVEL = logging.DEBUG
|
||||
|
||||
PORT = 5544
|
||||
|
||||
# Keep in sync with WINSCOPE_PROXY_VERSION in Winscope DataAdb.vue
|
||||
VERSION = '0.8'
|
||||
|
||||
WINSCOPE_VERSION_HEADER = "Winscope-Proxy-Version"
|
||||
WINSCOPE_TOKEN_HEADER = "Winscope-Token"
|
||||
|
||||
# Location to save the proxy security 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
|
||||
KEEP_ALIVE_INTERVAL_S = 5
|
||||
|
||||
logging.basicConfig(stream=sys.stderr, level=LOG_LEVEL,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
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:
|
||||
"""Defines a single parameter to trace.
|
||||
|
||||
Attributes:
|
||||
file_matchers: the matchers used to identify the paths on the device the trace results are saved to.
|
||||
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.
|
||||
"""
|
||||
|
||||
def __init__(self, files, trace_start: str, trace_stop: str) -> None:
|
||||
if type(files) is not list:
|
||||
files = [files]
|
||||
self.files = files
|
||||
self.trace_start = trace_start
|
||||
self.trace_stop = trace_stop
|
||||
|
||||
# Order of files matters as they will be expected in that order and decoded in that order
|
||||
TRACE_TARGETS = {
|
||||
"window_trace": TraceTarget(
|
||||
WinscopeFileMatcher(WINSCOPE_DIR, "wm_trace", "window_trace"),
|
||||
'su root cmd window tracing start\necho "WM trace started."',
|
||||
'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(
|
||||
WinscopeFileMatcher(WINSCOPE_DIR, "layers_trace", "layers_trace"),
|
||||
'su root service call SurfaceFlinger 1025 i32 1\necho "SF trace started."',
|
||||
'su root service call SurfaceFlinger 1025 i32 0 >/dev/null 2>&1'
|
||||
),
|
||||
"screen_recording": TraceTarget(
|
||||
File(f'/data/local/tmp/screen.mp4', "screen_recording"),
|
||||
f'screenrecord --bit-rate 8M /data/local/tmp/screen.mp4 >/dev/null 2>&1 &\necho "ScreenRecorder started."',
|
||||
'pkill -l SIGINT screenrecord >/dev/null 2>&1'
|
||||
),
|
||||
"transactions": TraceTarget(
|
||||
WinscopeFileMatcher(WINSCOPE_DIR, "transactions_trace", "transactions"),
|
||||
'su root service call SurfaceFlinger 1041 i32 1\necho "SF transactions recording started."',
|
||||
'su root service call SurfaceFlinger 1041 i32 0 >/dev/null 2>&1'
|
||||
),
|
||||
"transactions_legacy": TraceTarget(
|
||||
[
|
||||
WinscopeFileMatcher(WINSCOPE_DIR, "transaction_trace", "transactions_legacy"),
|
||||
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 0 >/dev/null 2>&1'
|
||||
),
|
||||
"proto_log": TraceTarget(
|
||||
WinscopeFileMatcher(WINSCOPE_DIR, "wm_log", "proto_log"),
|
||||
'su root cmd window logging start\necho "WM logging started."',
|
||||
'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'
|
||||
),
|
||||
"wayland_trace": TraceTarget(
|
||||
WinscopeFileMatcher("/data/misc/wltrace", "wl_trace", "wl_trace"),
|
||||
'su root service call Wayland 26 i32 1 >/dev/null\necho "Wayland trace started."',
|
||||
'su root service call Wayland 26 i32 0 >/dev/null'
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
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": "debug",
|
||||
"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,
|
||||
"tracebuffers": 1 << 5
|
||||
}
|
||||
|
||||
#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:
|
||||
"""Defines a single parameter to trace.
|
||||
|
||||
Attributes:
|
||||
file: the path on the device the dump results are saved to.
|
||||
dump_command: command to dump state to file.
|
||||
"""
|
||||
|
||||
def __init__(self, files, dump_command: str) -> None:
|
||||
if type(files) is not list:
|
||||
files = [files]
|
||||
self.files = files
|
||||
self.dump_command = dump_command
|
||||
|
||||
|
||||
DUMP_TARGETS = {
|
||||
"window_dump": DumpTarget(
|
||||
File(f'/data/local/tmp/wm_dump{WINSCOPE_EXT}', "window_dump"),
|
||||
f'su root dumpsys window --proto > /data/local/tmp/wm_dump{WINSCOPE_EXT}'
|
||||
),
|
||||
"layers_dump": DumpTarget(
|
||||
File(f'/data/local/tmp/sf_dump{WINSCOPE_EXT}', "layers_dump"),
|
||||
f'su root dumpsys SurfaceFlinger --proto > /data/local/tmp/sf_dump{WINSCOPE_EXT}'
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
# END OF CONFIG #
|
||||
|
||||
|
||||
def get_token() -> str:
|
||||
"""Returns saved proxy security token or creates new one"""
|
||||
try:
|
||||
with open(WINSCOPE_TOKEN_LOCATION, 'r') as token_file:
|
||||
token = token_file.readline()
|
||||
log.debug("Loaded token {} from {}".format(
|
||||
token, WINSCOPE_TOKEN_LOCATION))
|
||||
return token
|
||||
except IOError:
|
||||
token = secrets.token_hex(32)
|
||||
os.makedirs(os.path.dirname(WINSCOPE_TOKEN_LOCATION), exist_ok=True)
|
||||
try:
|
||||
with open(WINSCOPE_TOKEN_LOCATION, 'w') as token_file:
|
||||
log.debug("Created and saved token {} to {}".format(
|
||||
token, WINSCOPE_TOKEN_LOCATION))
|
||||
token_file.write(token)
|
||||
os.chmod(WINSCOPE_TOKEN_LOCATION, 0o600)
|
||||
except IOError:
|
||||
log.error("Unable to save persistent token {} to {}".format(
|
||||
token, WINSCOPE_TOKEN_LOCATION))
|
||||
return token
|
||||
|
||||
|
||||
secret_token = get_token()
|
||||
|
||||
|
||||
class RequestType(Enum):
|
||||
GET = 1
|
||||
POST = 2
|
||||
HEAD = 3
|
||||
|
||||
|
||||
def add_standard_headers(server):
|
||||
server.send_header('Cache-Control', 'no-cache, no-store, must-revalidate')
|
||||
server.send_header('Access-Control-Allow-Origin', '*')
|
||||
server.send_header('Access-Control-Allow-Methods', 'POST, GET, OPTIONS')
|
||||
server.send_header('Access-Control-Allow-Headers',
|
||||
WINSCOPE_TOKEN_HEADER + ', Content-Type, Content-Length')
|
||||
server.send_header('Access-Control-Expose-Headers',
|
||||
'Winscope-Proxy-Version')
|
||||
server.send_header(WINSCOPE_VERSION_HEADER, VERSION)
|
||||
server.end_headers()
|
||||
|
||||
|
||||
class RequestEndpoint:
|
||||
"""Request endpoint to use with the RequestRouter."""
|
||||
|
||||
@abstractmethod
|
||||
def process(self, server, path):
|
||||
pass
|
||||
|
||||
|
||||
class AdbError(Exception):
|
||||
"""Unsuccessful ADB operation"""
|
||||
pass
|
||||
|
||||
|
||||
class BadRequest(Exception):
|
||||
"""Invalid client request"""
|
||||
pass
|
||||
|
||||
|
||||
class RequestRouter:
|
||||
"""Handles HTTP request authentication and routing"""
|
||||
|
||||
def __init__(self, handler):
|
||||
self.request = handler
|
||||
self.endpoints = {}
|
||||
|
||||
def register_endpoint(self, method: RequestType, name: str, endpoint: RequestEndpoint):
|
||||
self.endpoints[(method, name)] = endpoint
|
||||
|
||||
def __bad_request(self, error: str):
|
||||
log.warning("Bad request: " + error)
|
||||
self.request.respond(HTTPStatus.BAD_REQUEST, b"Bad request!\nThis is Winscope ADB proxy.\n\n"
|
||||
+ error.encode("utf-8"), 'text/txt')
|
||||
|
||||
def __internal_error(self, error: str):
|
||||
log.error("Internal error: " + error)
|
||||
self.request.respond(HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
error.encode("utf-8"), 'text/txt')
|
||||
|
||||
def __bad_token(self):
|
||||
log.info("Bad token")
|
||||
self.request.respond(HTTPStatus.FORBIDDEN, b"Bad Winscope authorisation token!\nThis is Winscope ADB proxy.\n",
|
||||
'text/txt')
|
||||
|
||||
def process(self, method: RequestType):
|
||||
token = self.request.headers[WINSCOPE_TOKEN_HEADER]
|
||||
if not token or token != secret_token:
|
||||
return self.__bad_token()
|
||||
path = self.request.path.strip('/').split('/')
|
||||
if path and len(path) > 0:
|
||||
endpoint_name = path[0]
|
||||
try:
|
||||
return self.endpoints[(method, endpoint_name)].process(self.request, path[1:])
|
||||
except KeyError:
|
||||
return self.__bad_request("Unknown endpoint /{}/".format(endpoint_name))
|
||||
except AdbError as ex:
|
||||
return self.__internal_error(str(ex))
|
||||
except BadRequest as ex:
|
||||
return self.__bad_request(str(ex))
|
||||
except Exception as ex:
|
||||
return self.__internal_error(repr(ex))
|
||||
self.__bad_request("No endpoint specified")
|
||||
|
||||
|
||||
def call_adb(params: str, device: str = None, stdin: bytes = None):
|
||||
command = ['adb'] + (['-s', device] if device else []) + params.split(' ')
|
||||
try:
|
||||
log.debug("Call: " + ' '.join(command))
|
||||
return subprocess.check_output(command, stderr=subprocess.STDOUT, input=stdin).decode('utf-8')
|
||||
except OSError as ex:
|
||||
log.debug('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:
|
||||
log.debug('Error executing adb command: {}\n{}'.format(
|
||||
' '.join(command), 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):
|
||||
try:
|
||||
process = subprocess.Popen(['adb'] + (['-s', device] if device else []) + params.split(' '), stdout=outfile,
|
||||
stderr=subprocess.PIPE)
|
||||
_, err = process.communicate(stdin)
|
||||
outfile.seek(0)
|
||||
if process.returncode != 0:
|
||||
log.debug('Error executing adb command: adb {}\n'.format(params) + err.decode(
|
||||
'utf-8') + '\n' + outfile.read().decode('utf-8'))
|
||||
raise AdbError('Error executing adb command: adb {}\n'.format(params) + err.decode(
|
||||
'utf-8') + '\n' + outfile.read().decode('utf-8'))
|
||||
except OSError as ex:
|
||||
log.debug('Error executing adb command: adb {}\n{}'.format(
|
||||
params, repr(ex)))
|
||||
raise AdbError(
|
||||
'Error executing adb command: adb {}\n{}'.format(params, repr(ex)))
|
||||
|
||||
|
||||
class CheckWaylandServiceEndpoint(RequestEndpoint):
|
||||
_listDevicesEndpoint = None
|
||||
|
||||
def __init__(self, listDevicesEndpoint):
|
||||
self._listDevicesEndpoint = listDevicesEndpoint
|
||||
|
||||
def process(self, server, path):
|
||||
self._listDevicesEndpoint.process(server, path)
|
||||
foundDevices = self._listDevicesEndpoint._foundDevices
|
||||
|
||||
if len(foundDevices) > 1:
|
||||
res = 'false'
|
||||
else:
|
||||
raw_res = call_adb('shell service check Wayland')
|
||||
res = 'false' if 'not found' in raw_res else 'true'
|
||||
server.respond(HTTPStatus.OK, res.encode("utf-8"), "text/json")
|
||||
|
||||
|
||||
class ListDevicesEndpoint(RequestEndpoint):
|
||||
ADB_INFO_RE = re.compile("^([A-Za-z0-9.:\\-]+)\\s+(\\w+)(.*model:(\\w+))?")
|
||||
_foundDevices = None
|
||||
|
||||
def process(self, server, path):
|
||||
lines = list(filter(None, call_adb('devices -l').split('\n')))
|
||||
devices = {m.group(1): {
|
||||
'authorised': str(m.group(2)) != 'unauthorized',
|
||||
'model': m.group(4).replace('_', ' ') if m.group(4) else ''
|
||||
} for m in [ListDevicesEndpoint.ADB_INFO_RE.match(d) for d in lines[1:]] if m}
|
||||
self._foundDevices = devices
|
||||
j = json.dumps(devices)
|
||||
log.debug("Detected devices: " + j)
|
||||
server.respond(HTTPStatus.OK, j.encode("utf-8"), "text/json")
|
||||
|
||||
|
||||
class DeviceRequestEndpoint(RequestEndpoint):
|
||||
def process(self, server, path):
|
||||
if len(path) > 0 and re.fullmatch("[A-Za-z0-9.:\\-]+", path[0]):
|
||||
self.process_with_device(server, path[1:], path[0])
|
||||
else:
|
||||
raise BadRequest("Device id not specified")
|
||||
|
||||
@abstractmethod
|
||||
def process_with_device(self, server, path, device_id):
|
||||
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 FetchFilesEndpoint(DeviceRequestEndpoint):
|
||||
def process_with_device(self, server, path, device_id):
|
||||
if len(path) != 1:
|
||||
raise BadRequest("File not specified")
|
||||
if path[0] in TRACE_TARGETS:
|
||||
files = TRACE_TARGETS[path[0]].files
|
||||
elif path[0] in DUMP_TARGETS:
|
||||
files = DUMP_TARGETS[path[0]].files
|
||||
else:
|
||||
raise BadRequest("Unknown file specified")
|
||||
|
||||
file_buffers = dict()
|
||||
|
||||
for f in files:
|
||||
file_type = f.get_filetype()
|
||||
file_paths = f.get_filepaths(device_id)
|
||||
|
||||
for file_path in file_paths:
|
||||
with NamedTemporaryFile() as tmp:
|
||||
log.debug(
|
||||
f"Fetching file {file_path} from device to {tmp.name}")
|
||||
call_adb_outfile('exec-out su root cat ' +
|
||||
file_path, tmp, device_id)
|
||||
log.debug(f"Deleting file {file_path} from device")
|
||||
call_adb('shell su root rm ' + file_path, device_id)
|
||||
log.debug(f"Uploading file {tmp.name}")
|
||||
if file_type not in file_buffers:
|
||||
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):
|
||||
log.debug("Checking root access on {}".format(device_id))
|
||||
return int(call_adb('shell su root id -u', device_id)) == 0
|
||||
|
||||
|
||||
TRACE_THREADS = {}
|
||||
|
||||
|
||||
class TraceThread(threading.Thread):
|
||||
def __init__(self, device_id, command):
|
||||
self._keep_alive_timer = None
|
||||
self.trace_command = command
|
||||
self._device_id = device_id
|
||||
self.out = None,
|
||||
self.err = None,
|
||||
self._success = False
|
||||
try:
|
||||
shell = ['adb', '-s', self._device_id, 'shell']
|
||||
log.debug("Starting trace shell {}".format(' '.join(shell)))
|
||||
self.process = subprocess.Popen(shell, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE, stdin=subprocess.PIPE, start_new_session=True)
|
||||
except OSError as ex:
|
||||
raise AdbError(
|
||||
'Error executing adb command: adb shell\n{}'.format(repr(ex)))
|
||||
|
||||
super().__init__()
|
||||
|
||||
def timeout(self):
|
||||
if self.is_alive():
|
||||
log.warning(
|
||||
"Keep-alive timeout for trace on {}".format(self._device_id))
|
||||
self.end_trace()
|
||||
if self._device_id in TRACE_THREADS:
|
||||
TRACE_THREADS.pop(self._device_id)
|
||||
|
||||
def reset_timer(self):
|
||||
log.debug(
|
||||
"Resetting keep-alive clock for trace on {}".format(self._device_id))
|
||||
if self._keep_alive_timer:
|
||||
self._keep_alive_timer.cancel()
|
||||
self._keep_alive_timer = threading.Timer(
|
||||
KEEP_ALIVE_INTERVAL_S, self.timeout)
|
||||
self._keep_alive_timer.start()
|
||||
|
||||
def end_trace(self):
|
||||
if self._keep_alive_timer:
|
||||
self._keep_alive_timer.cancel()
|
||||
log.debug("Sending SIGINT to the trace process on {}".format(
|
||||
self._device_id))
|
||||
self.process.send_signal(signal.SIGINT)
|
||||
try:
|
||||
log.debug("Waiting for trace shell to exit for {}".format(
|
||||
self._device_id))
|
||||
self.process.wait(timeout=5)
|
||||
except TimeoutError:
|
||||
log.debug(
|
||||
"TIMEOUT - sending SIGKILL to the trace process on {}".format(self._device_id))
|
||||
self.process.kill()
|
||||
self.join()
|
||||
|
||||
def run(self):
|
||||
log.debug("Trace started on {}".format(self._device_id))
|
||||
self.reset_timer()
|
||||
self.out, self.err = self.process.communicate(self.trace_command)
|
||||
log.debug("Trace ended on {}, waiting for cleanup".format(self._device_id))
|
||||
time.sleep(0.2)
|
||||
for i in range(50):
|
||||
if call_adb("shell su root cat /data/local/tmp/winscope_status", device=self._device_id) == 'TRACE_OK\n':
|
||||
call_adb(
|
||||
"shell su root rm /data/local/tmp/winscope_status", device=self._device_id)
|
||||
log.debug("Trace finished successfully on {}".format(
|
||||
self._device_id))
|
||||
self._success = True
|
||||
break
|
||||
log.debug("Still waiting for cleanup on {}".format(self._device_id))
|
||||
time.sleep(0.1)
|
||||
|
||||
def success(self):
|
||||
return self._success
|
||||
|
||||
|
||||
class StartTrace(DeviceRequestEndpoint):
|
||||
TRACE_COMMAND = """
|
||||
set -e
|
||||
|
||||
echo "Starting trace..."
|
||||
echo "TRACE_START" > /data/local/tmp/winscope_status
|
||||
|
||||
# Do not print anything to stdout/stderr in the handler
|
||||
function stop_trace() {{
|
||||
trap - EXIT HUP INT
|
||||
|
||||
{}
|
||||
|
||||
echo "TRACE_OK" > /data/local/tmp/winscope_status
|
||||
}}
|
||||
|
||||
trap stop_trace EXIT HUP INT
|
||||
echo "Signal handler registered."
|
||||
|
||||
{}
|
||||
|
||||
# ADB shell does not handle hung up well and does not call HUP handler when a child is active in foreground,
|
||||
# as a workaround we sleep for short intervals in a loop so the handler is called after a sleep interval.
|
||||
while true; do sleep 0.1; done
|
||||
"""
|
||||
|
||||
def process_with_device(self, server, path, device_id):
|
||||
try:
|
||||
requested_types = self.get_request(server)
|
||||
requested_traces = [TRACE_TARGETS[t] for t in requested_types]
|
||||
except KeyError as err:
|
||||
raise BadRequest("Unsupported trace target\n" + str(err))
|
||||
if device_id in TRACE_THREADS:
|
||||
log.warning("Trace already in progress for {}", device_id)
|
||||
server.respond(HTTPStatus.OK, b'', "text/plain")
|
||||
if not check_root(device_id):
|
||||
raise AdbError(
|
||||
"Unable to acquire root privileges on the device - check the output of 'adb -s {} shell su root id'".format(
|
||||
device_id))
|
||||
command = StartTrace.TRACE_COMMAND.format(
|
||||
'\n'.join([t.trace_stop for t in requested_traces]),
|
||||
'\n'.join([t.trace_start for t in requested_traces]))
|
||||
log.debug("Trace requested for {} with targets {}".format(
|
||||
device_id, ','.join(requested_types)))
|
||||
TRACE_THREADS[device_id] = TraceThread(
|
||||
device_id, command.encode('utf-8'))
|
||||
TRACE_THREADS[device_id].start()
|
||||
server.respond(HTTPStatus.OK, b'', "text/plain")
|
||||
|
||||
|
||||
class EndTrace(DeviceRequestEndpoint):
|
||||
def process_with_device(self, server, path, device_id):
|
||||
if device_id not in TRACE_THREADS:
|
||||
raise BadRequest("No trace in progress for {}".format(device_id))
|
||||
if TRACE_THREADS[device_id].is_alive():
|
||||
TRACE_THREADS[device_id].end_trace()
|
||||
|
||||
success = TRACE_THREADS[device_id].success()
|
||||
out = TRACE_THREADS[device_id].out + \
|
||||
b"\n" + TRACE_THREADS[device_id].err
|
||||
command = TRACE_THREADS[device_id].trace_command
|
||||
TRACE_THREADS.pop(device_id)
|
||||
if success:
|
||||
server.respond(HTTPStatus.OK, out, "text/plain")
|
||||
else:
|
||||
raise AdbError(
|
||||
"Error tracing the device\n### Output ###\n" + out.decode(
|
||||
"utf-8") + "\n### Command: adb -s {} shell ###\n### Input ###\n".format(device_id) + command.decode(
|
||||
"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):
|
||||
def process_with_device(self, server, path, device_id):
|
||||
if device_id not in TRACE_THREADS:
|
||||
raise BadRequest("No trace in progress for {}".format(device_id))
|
||||
TRACE_THREADS[device_id].reset_timer()
|
||||
server.respond(HTTPStatus.OK, str(
|
||||
TRACE_THREADS[device_id].is_alive()).encode("utf-8"), "text/plain")
|
||||
|
||||
|
||||
class DumpEndpoint(DeviceRequestEndpoint):
|
||||
def process_with_device(self, server, path, device_id):
|
||||
try:
|
||||
requested_types = self.get_request(server)
|
||||
requested_traces = [DUMP_TARGETS[t] for t in requested_types]
|
||||
except KeyError as err:
|
||||
raise BadRequest("Unsupported trace target\n" + str(err))
|
||||
if device_id in TRACE_THREADS:
|
||||
BadRequest("Trace in progress for {}".format(device_id))
|
||||
if not check_root(device_id):
|
||||
raise AdbError(
|
||||
"Unable to acquire root privileges on the device - check the output of 'adb -s {} shell su root id'"
|
||||
.format(device_id))
|
||||
command = '\n'.join(t.dump_command for t in requested_traces)
|
||||
shell = ['adb', '-s', device_id, 'shell']
|
||||
log.debug("Starting dump shell {}".format(' '.join(shell)))
|
||||
process = subprocess.Popen(shell, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
||||
stdin=subprocess.PIPE, start_new_session=True)
|
||||
log.debug("Starting dump on device {}".format(device_id))
|
||||
out, err = process.communicate(command.encode('utf-8'))
|
||||
if process.returncode != 0:
|
||||
raise AdbError("Error executing command:\n" + command + "\n\n### OUTPUT ###" + out.decode('utf-8') + "\n"
|
||||
+ err.decode('utf-8'))
|
||||
log.debug("Dump finished on device {}".format(device_id))
|
||||
server.respond(HTTPStatus.OK, b'', "text/plain")
|
||||
|
||||
|
||||
class ADBWinscopeProxy(BaseHTTPRequestHandler):
|
||||
def __init__(self, request, client_address, server):
|
||||
self.router = RequestRouter(self)
|
||||
listDevicesEndpoint = ListDevicesEndpoint()
|
||||
self.router.register_endpoint(
|
||||
RequestType.GET, "devices", listDevicesEndpoint)
|
||||
self.router.register_endpoint(
|
||||
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, "end", EndTrace())
|
||||
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())
|
||||
self.router.register_endpoint(
|
||||
RequestType.GET, "checkwayland", CheckWaylandServiceEndpoint(listDevicesEndpoint))
|
||||
super().__init__(request, client_address, server)
|
||||
|
||||
def respond(self, code: int, data: bytes, mime: str) -> None:
|
||||
self.send_response(code)
|
||||
self.send_header('Content-type', mime)
|
||||
add_standard_headers(self)
|
||||
self.wfile.write(data)
|
||||
|
||||
def do_GET(self):
|
||||
self.router.process(RequestType.GET)
|
||||
|
||||
def do_POST(self):
|
||||
self.router.process(RequestType.POST)
|
||||
|
||||
def do_OPTIONS(self):
|
||||
self.send_response(HTTPStatus.OK)
|
||||
self.send_header('Allow', 'GET,POST')
|
||||
add_standard_headers(self)
|
||||
self.end_headers()
|
||||
self.wfile.write(b'GET,POST')
|
||||
|
||||
def log_request(self, code='-', size='-'):
|
||||
log.info('{} {} {}'.format(self.requestline, str(code), str(size)))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
print("Winscope ADB Connect proxy version: " + VERSION)
|
||||
print('Winscope token: ' + secret_token)
|
||||
httpd = HTTPServer(('localhost', PORT), ADBWinscopeProxy)
|
||||
try:
|
||||
httpd.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
log.info("Shutting down")
|
||||
86
tools/winscope-ng/src/app/adb_proxy.component.spec.ts
Normal file
86
tools/winscope-ng/src/app/adb_proxy.component.spec.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright (C) 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { AdbProxyComponent } from "./adb_proxy.component";
|
||||
import { proxyClient, ProxyState } from "../trace_collection/proxy_client";
|
||||
import { MatIconModule } from "@angular/material/icon";
|
||||
import { MatFormFieldModule } from "@angular/material/form-field";
|
||||
import { MatInputModule } from "@angular/material/input";
|
||||
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
|
||||
import { MatButtonModule } from "@angular/material/button";
|
||||
import { NO_ERRORS_SCHEMA } from "@angular/core";
|
||||
|
||||
describe("AdbProxyComponent", () => {
|
||||
let fixture: ComponentFixture<AdbProxyComponent>;
|
||||
let component: AdbProxyComponent;
|
||||
let htmlElement: HTMLElement;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
MatIconModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
BrowserAnimationsModule,
|
||||
MatButtonModule
|
||||
],
|
||||
declarations: [AdbProxyComponent],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
fixture = TestBed.createComponent(AdbProxyComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.proxy = proxyClient;
|
||||
htmlElement = fixture.nativeElement;
|
||||
});
|
||||
|
||||
it("can be created", () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it("check correct icon and message displays if no proxy", () => {
|
||||
component.proxy.setState(ProxyState.NO_PROXY);
|
||||
fixture.detectChanges();
|
||||
expect(htmlElement.querySelector(".adb-info")?.innerHTML).toBe("Unable to connect to Winscope ADB proxy");
|
||||
expect(htmlElement.querySelector(".adb-icon")?.innerHTML).toBe("error");
|
||||
});
|
||||
|
||||
it("check correct icon and message displays if invalid proxy", () => {
|
||||
component.proxy.setState(ProxyState.INVALID_VERSION);
|
||||
fixture.detectChanges();
|
||||
expect(htmlElement.querySelector(".adb-info")?.innerHTML).toBe("Your local proxy version is incompatible with Winscope.");
|
||||
expect(htmlElement.querySelector(".adb-icon")?.innerHTML).toBe("update");
|
||||
});
|
||||
|
||||
it("check correct icon and message displays if unauthorised proxy", () => {
|
||||
component.proxy.setState(ProxyState.UNAUTH);
|
||||
fixture.detectChanges();
|
||||
expect(htmlElement.querySelector(".adb-info")?.innerHTML).toBe("Proxy authorisation required");
|
||||
expect(htmlElement.querySelector(".adb-icon")?.innerHTML).toBe("lock");
|
||||
});
|
||||
|
||||
it("check retry button acts as expected", async () => {
|
||||
component.proxy.setState(ProxyState.NO_PROXY);
|
||||
fixture.detectChanges();
|
||||
spyOn(component, "restart").and.callThrough();
|
||||
const button: HTMLButtonElement | null = htmlElement.querySelector(".retry");
|
||||
expect(button).toBeInstanceOf(HTMLButtonElement);
|
||||
button?.dispatchEvent(new Event("click"));
|
||||
await fixture.whenStable();
|
||||
expect(component.restart).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
103
tools/winscope-ng/src/app/adb_proxy.component.ts
Normal file
103
tools/winscope-ng/src/app/adb_proxy.component.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Copyright (C) 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { Component, Input, Output, EventEmitter } from "@angular/core";
|
||||
import { proxyClient, ProxyClient, ProxyState } from "../trace_collection/proxy_client";
|
||||
|
||||
@Component({
|
||||
selector: "adb-proxy",
|
||||
template: `
|
||||
<div *ngIf="proxy.state===states.NO_PROXY">
|
||||
<div id="icon-information">
|
||||
<mat-icon class="adb-icon">error</mat-icon>
|
||||
<span class="adb-info">Unable to connect to Winscope ADB proxy</span>
|
||||
</div>
|
||||
<div class="further-adb-info">
|
||||
<p>Launch the Winscope ADB Connect proxy to capture traces directly from your browser.</p>
|
||||
<p>Python 3.5+ and ADB are required.</p>
|
||||
<p>Run:</p>
|
||||
<pre>python3</pre>
|
||||
<pre>$ANDROID_BUILD_TOP/development/tools/winscope-ng/src/adb/winscope_proxy.py</pre>
|
||||
<p>Or get it from the AOSP repository.</p>
|
||||
</div>
|
||||
<div>
|
||||
<button mat-raised-button>
|
||||
<a href="{{downloadProxyUrl}}" target='_blank'>Download from AOSP</a>
|
||||
</button>
|
||||
<button mat-raised-button class="retry" (click)="restart()">Retry</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="proxy.state===states.INVALID_VERSION">
|
||||
<div id="icon-information">
|
||||
<mat-icon class="adb-icon">update</mat-icon>
|
||||
<span class="adb-info">Your local proxy version is incompatible with Winscope.</span>
|
||||
</div>
|
||||
<div class="further-adb-info">
|
||||
<p>Please update the proxy to version {{ proxyVersion }}.</p>
|
||||
<p>Run:</p>
|
||||
<pre>python3</pre>
|
||||
<pre>$ANDROID_BUILD_TOP/development/tools/winscope-ng/src/adb/winscope_proxy.py</pre>
|
||||
<p>Or get it from the AOSP repository.</p>
|
||||
</div>
|
||||
<div>
|
||||
<button mat-raised-button>
|
||||
<a href="{{downloadProxyUrl}}" target='_blank'>Download from AOSP</a>
|
||||
</button>
|
||||
<button mat-raised-button class="retry" (click)="restart()">Retry</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="proxy.state===states.UNAUTH">
|
||||
<div id="icon-information">
|
||||
<mat-icon class="adb-icon">lock</mat-icon>
|
||||
<span class="adb-info">Proxy authorisation required</span>
|
||||
</div>
|
||||
<div class="further-adb-info">
|
||||
<p>Enter Winscope proxy token:</p>
|
||||
<mat-form-field class="proxy-key-field">
|
||||
<input matInput [(ngModel)]="proxyKeyItem" name="proxy-key"/>
|
||||
</mat-form-field>
|
||||
<p>The proxy token is printed to console on proxy launch, copy and paste it above.</p>
|
||||
</div>
|
||||
<div>
|
||||
<button mat-raised-button class="retry" (click)="restart()">Connect</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
`,
|
||||
styles: [".proxy-key-field {width: 30rem}"]
|
||||
})
|
||||
export class AdbProxyComponent {
|
||||
@Input()
|
||||
proxy: ProxyClient = proxyClient;
|
||||
|
||||
@Output()
|
||||
proxyChange = new EventEmitter<ProxyClient>();
|
||||
|
||||
@Output()
|
||||
addKey = new EventEmitter<string>();
|
||||
|
||||
states = ProxyState;
|
||||
proxyKeyItem = "";
|
||||
readonly proxyVersion = this.proxy.VERSION;
|
||||
readonly downloadProxyUrl: string = "https://android.googlesource.com/platform/development/+/master/tools/winscope/adb_proxy/winscope_proxy.py";
|
||||
|
||||
public restart() {
|
||||
this.addKey.emit(this.proxyKeyItem);
|
||||
this.proxy.setState(this.states.CONNECTING);
|
||||
this.proxyChange.emit(this.proxy);
|
||||
}
|
||||
}
|
||||
@@ -13,21 +13,50 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { ChangeDetectionStrategy } from "@angular/core";
|
||||
import {ComponentFixture, TestBed} from "@angular/core/testing";
|
||||
import {AppComponent} from "./app.component";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { MatCardModule } from "@angular/material/card";
|
||||
import { MatButtonModule } from "@angular/material/button";
|
||||
import { MatGridListModule } from "@angular/material/grid-list";
|
||||
|
||||
import { AppComponent } from "./app.component";
|
||||
import { CollectTracesComponent } from "./collect_traces.component";
|
||||
import { UploadTracesComponent } from "./upload_traces.component";
|
||||
import { AdbProxyComponent } from "./adb_proxy.component";
|
||||
import { WebAdbComponent } from "./web_adb.component";
|
||||
import { TraceConfigComponent } from "./trace_config.component";
|
||||
|
||||
import { ComponentFixtureAutoDetect } from "@angular/core/testing";
|
||||
|
||||
|
||||
describe("AppComponent", () => {
|
||||
let fixture: ComponentFixture<AppComponent>;
|
||||
let component: AppComponent;
|
||||
let htmlElement: HTMLElement;
|
||||
|
||||
beforeAll(async () => {
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
AppComponent
|
||||
providers: [
|
||||
{ provide: ComponentFixtureAutoDetect, useValue: true }
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
MatCardModule,
|
||||
MatButtonModule,
|
||||
MatGridListModule,
|
||||
],
|
||||
declarations: [
|
||||
AppComponent,
|
||||
CollectTracesComponent,
|
||||
UploadTracesComponent,
|
||||
AdbProxyComponent,
|
||||
WebAdbComponent,
|
||||
TraceConfigComponent,
|
||||
],
|
||||
}).overrideComponent(AppComponent, {
|
||||
set: { changeDetection: ChangeDetectionStrategy.Default }
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(AppComponent);
|
||||
component = fixture.componentInstance;
|
||||
htmlElement = fixture.nativeElement;
|
||||
@@ -41,7 +70,23 @@ describe("AppComponent", () => {
|
||||
expect(component.title).toEqual("winscope-ng");
|
||||
});
|
||||
|
||||
it("renders the title", () => {
|
||||
expect(htmlElement.querySelector("div#title")?.innerHTML).toContain("Winscope Viewer 2.0");
|
||||
it("renders the page title", () => {
|
||||
expect(htmlElement.querySelector("#title")?.innerHTML).toContain("Winscope Viewer 2.0");
|
||||
});
|
||||
|
||||
it("displays correct elements when no data loaded", async () => {
|
||||
component.dataLoaded = false;
|
||||
fixture.detectChanges();
|
||||
expect(htmlElement.querySelector("#collect-traces-card")).toBeTruthy();
|
||||
expect(htmlElement.querySelector("#upload-traces-card")).toBeTruthy();
|
||||
expect(htmlElement.querySelector("#loaded-data-card")).toBeFalsy();
|
||||
});
|
||||
|
||||
it("displays correct elements when data loaded", async () => {
|
||||
component.dataLoaded = true;
|
||||
fixture.detectChanges();
|
||||
expect(htmlElement.querySelector("#collect-traces-card")).toBeFalsy();
|
||||
expect(htmlElement.querySelector("#upload-traces-card")).toBeFalsy();
|
||||
expect(htmlElement.querySelector("#loaded-data-card")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,10 +13,12 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import {Component, Injector, Inject} from "@angular/core";
|
||||
import {createCustomElement} from "@angular/elements";
|
||||
import {ViewerWindowManagerComponent} from "viewers/viewer_window_manager/viewer_window_manager.component";
|
||||
import {Core} from "./core";
|
||||
import { Component, Injector, Inject } from "@angular/core";
|
||||
import { createCustomElement } from "@angular/elements";
|
||||
import { ViewerWindowManagerComponent } from "viewers/viewer_window_manager/viewer_window_manager.component";
|
||||
import { Core } from "./core";
|
||||
import { ProxyState } from "trace_collection/proxy_client";
|
||||
import { PersistentStore } from "../common/persistent_store";
|
||||
|
||||
@Component({
|
||||
selector: "app-root",
|
||||
@@ -25,43 +27,57 @@ import {Core} from "./core";
|
||||
<span>Winscope Viewer 2.0</span>
|
||||
</div>
|
||||
|
||||
<div id="inputfile">
|
||||
<input type="file" (change)="onInputFile($event)" #fileUpload>
|
||||
<div *ngIf="!dataLoaded" fxLayout="row wrap" fxLayoutGap="10px grid" class="home">
|
||||
<mat-card class="homepage-card" id="collect-traces-card">
|
||||
<collect-traces [(core)]="core" [(dataLoaded)]="dataLoaded" [store]="store"></collect-traces>
|
||||
</mat-card>
|
||||
<mat-card class="homepage-card" id="upload-traces-card">
|
||||
<upload-traces [(core)]="core"></upload-traces>
|
||||
</mat-card>
|
||||
</div>
|
||||
|
||||
<div *ngIf="dataLoaded">
|
||||
<mat-card class="homepage-card" id="loaded-data-card">
|
||||
<mat-card-title>Loaded data</mat-card-title>
|
||||
<button mat-raised-button (click)="clearData()">Back to Home</button>
|
||||
</mat-card>
|
||||
</div>
|
||||
|
||||
<div id="timescrub">
|
||||
<button (click)="notifyCurrentTimestamp()">Update current timestamp</button>
|
||||
</div>
|
||||
|
||||
<div id="viewers">
|
||||
<button mat-raised-button (click)="notifyCurrentTimestamp()">Update current timestamp</button>
|
||||
</div>
|
||||
|
||||
<div id="timestamps">
|
||||
</div>
|
||||
`
|
||||
|
||||
<div id="viewers">
|
||||
</div>
|
||||
`,
|
||||
styles: [".home{width: 100%; display:flex; flex-direction: row; overflow: auto;}"]
|
||||
})
|
||||
export class AppComponent {
|
||||
title = "winscope-ng";
|
||||
core: Core;
|
||||
states = ProxyState;
|
||||
store: PersistentStore = new PersistentStore();
|
||||
dataLoaded = false;
|
||||
|
||||
private core!: Core;
|
||||
|
||||
constructor(@Inject(Injector) injector: Injector) {
|
||||
customElements.define("viewer-window-manager",
|
||||
createCustomElement(ViewerWindowManagerComponent, {injector}));
|
||||
constructor(
|
||||
@Inject(Injector) injector: Injector
|
||||
) {
|
||||
this.core = new Core();
|
||||
if (!customElements.get("viewer-window-manager")) {
|
||||
customElements.define("viewer-window-manager",
|
||||
createCustomElement(ViewerWindowManagerComponent, {injector}));
|
||||
}
|
||||
}
|
||||
|
||||
public async onInputFile(event: Event) {
|
||||
const files = await this.getInputFiles(event);
|
||||
onCoreChange(newCore: Core) {
|
||||
this.core = newCore;
|
||||
}
|
||||
|
||||
this.core = new Core();
|
||||
await this.core.bootstrap(files);
|
||||
|
||||
const viewersDiv = document.querySelector("div#viewers")!;
|
||||
viewersDiv.innerHTML = "";
|
||||
this.core.getViews().forEach(view => viewersDiv!.appendChild(view) );
|
||||
|
||||
const timestampsDiv = document.querySelector("div#timestamps")!;
|
||||
timestampsDiv.innerHTML = `Retrieved ${this.core.getTimestamps().length} unique timestamps`;
|
||||
onDataLoadedChange(loaded: boolean) {
|
||||
this.dataLoaded = loaded;
|
||||
}
|
||||
|
||||
public notifyCurrentTimestamp() {
|
||||
@@ -69,14 +85,8 @@ export class AppComponent {
|
||||
this.core.notifyCurrentTimestamp(dummyTimestamp);
|
||||
}
|
||||
|
||||
//TODO: extend with support for multiple files, archives, etc...
|
||||
private getInputFiles(event: Event): File[] {
|
||||
const files: any = (event?.target as HTMLInputElement)?.files;
|
||||
|
||||
if (!files || !files[0]) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [files[0]];
|
||||
public clearData() {
|
||||
this.dataLoaded = false;
|
||||
this.core.clearData();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,58 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
import { BrowserModule } from "@angular/platform-browser";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { MatCardModule } from "@angular/material/card";
|
||||
import { MatButtonModule } from "@angular/material/button";
|
||||
import { MatGridListModule } from "@angular/material/grid-list";
|
||||
import { MatListModule } from "@angular/material/list";
|
||||
import { MatProgressSpinnerModule } from "@angular/material/progress-spinner";
|
||||
import { MatProgressBarModule } from "@angular/material/progress-bar";
|
||||
import { FormsModule } from "@angular/forms";
|
||||
import { MatCheckboxModule } from "@angular/material/checkbox";
|
||||
import { MatFormFieldModule } from "@angular/material/form-field";
|
||||
import { MatIconModule } from "@angular/material/icon";
|
||||
import { MatInputModule } from "@angular/material/input";
|
||||
import { MatSelectModule } from "@angular/material/select";
|
||||
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
|
||||
import { HttpClientModule } from "@angular/common/http";
|
||||
|
||||
import { AppComponent } from "./app.component";
|
||||
import { ViewerWindowManagerComponent} from "viewers/viewer_window_manager/viewer_window_manager.component";
|
||||
import { ViewerWindowManagerComponent } from "viewers/viewer_window_manager/viewer_window_manager.component";
|
||||
import { CollectTracesComponent } from "./collect_traces.component";
|
||||
import { AdbProxyComponent } from "./adb_proxy.component";
|
||||
import { WebAdbComponent } from "./web_adb.component";
|
||||
import { TraceConfigComponent } from "./trace_config.component";
|
||||
import { UploadTracesComponent } from "./upload_traces.component";
|
||||
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent,
|
||||
ViewerWindowManagerComponent
|
||||
ViewerWindowManagerComponent,
|
||||
CollectTracesComponent,
|
||||
UploadTracesComponent,
|
||||
AdbProxyComponent,
|
||||
WebAdbComponent,
|
||||
TraceConfigComponent,
|
||||
],
|
||||
imports: [
|
||||
BrowserModule
|
||||
BrowserModule,
|
||||
HttpClientModule,
|
||||
CommonModule,
|
||||
MatCardModule,
|
||||
MatButtonModule,
|
||||
MatGridListModule,
|
||||
FormsModule,
|
||||
MatListModule,
|
||||
MatCheckboxModule,
|
||||
MatIconModule,
|
||||
MatProgressSpinnerModule,
|
||||
MatProgressBarModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatSelectModule,
|
||||
BrowserAnimationsModule,
|
||||
HttpClientModule
|
||||
],
|
||||
providers: [],
|
||||
bootstrap: [AppComponent]
|
||||
|
||||
273
tools/winscope-ng/src/app/collect_traces.component.spec.ts
Normal file
273
tools/winscope-ng/src/app/collect_traces.component.spec.ts
Normal file
@@ -0,0 +1,273 @@
|
||||
/*
|
||||
* Copyright (C) 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import {ComponentFixture, TestBed} from "@angular/core/testing";
|
||||
import {CollectTracesComponent} from "./collect_traces.component";
|
||||
import { MatIconModule } from "@angular/material/icon";
|
||||
import { MatCardModule } from "@angular/material/card";
|
||||
import { AdbProxyComponent } from "./adb_proxy.component";
|
||||
import { WebAdbComponent } from "./web_adb.component";
|
||||
import { TraceConfigComponent } from "./trace_config.component";
|
||||
import { MatListModule } from "@angular/material/list";
|
||||
import { MatButtonModule } from "@angular/material/button";
|
||||
import { MatProgressBarModule } from "@angular/material/progress-bar";
|
||||
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
|
||||
|
||||
describe("CollectTracesComponent", () => {
|
||||
let fixture: ComponentFixture<CollectTracesComponent>;
|
||||
let component: CollectTracesComponent;
|
||||
let htmlElement: HTMLElement;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [
|
||||
MatIconModule,
|
||||
MatCardModule,
|
||||
MatListModule,
|
||||
MatIconModule,
|
||||
MatButtonModule,
|
||||
MatProgressBarModule,
|
||||
BrowserAnimationsModule
|
||||
],
|
||||
declarations: [
|
||||
CollectTracesComponent,
|
||||
AdbProxyComponent,
|
||||
WebAdbComponent,
|
||||
TraceConfigComponent,
|
||||
],
|
||||
}).compileComponents();
|
||||
fixture = TestBed.createComponent(CollectTracesComponent);
|
||||
component = fixture.componentInstance;
|
||||
htmlElement = fixture.nativeElement;
|
||||
component.isAdbProxy = true;
|
||||
});
|
||||
|
||||
it("can be created", () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders the expected card title", () => {
|
||||
fixture.detectChanges();
|
||||
expect(htmlElement.querySelector("#title")?.innerHTML).toContain("Collect Traces");
|
||||
});
|
||||
|
||||
it("displays connecting message", () => {
|
||||
component.connect.isConnectingState = jasmine.createSpy().and.returnValue(true);
|
||||
fixture.detectChanges();
|
||||
expect(htmlElement.querySelector(".connecting-message")?.innerHTML).toContain("Connecting...");
|
||||
});
|
||||
|
||||
it("displays adb set up", async () => {
|
||||
component.connect.adbSuccess = jasmine.createSpy().and.returnValue(false);
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then( () => {
|
||||
expect(htmlElement.querySelector(".set-up-adb")).toBeTruthy();
|
||||
const proxyTab: HTMLButtonElement | null = htmlElement.querySelector("#proxy-tab");
|
||||
expect(proxyTab).toBeInstanceOf(HTMLButtonElement);
|
||||
const webTab: HTMLButtonElement | null = htmlElement.querySelector("#web-tab");
|
||||
expect(webTab).toBeInstanceOf(HTMLButtonElement);
|
||||
});
|
||||
});
|
||||
|
||||
it("displays adb proxy element", async () => {
|
||||
component.connect.adbSuccess = jasmine.createSpy().and.returnValue(false);
|
||||
component.isAdbProxy = true;
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then( () => {
|
||||
expect(htmlElement.querySelector("adb-proxy")).toBeTruthy();
|
||||
expect(htmlElement.querySelector("web-adb")).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
it("displays web adb element", async () => {
|
||||
component.connect.adbSuccess = jasmine.createSpy().and.returnValue(false);
|
||||
component.isAdbProxy = false;
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then( () => {
|
||||
expect(htmlElement.querySelector("adb-proxy")).toBeFalsy();
|
||||
expect(htmlElement.querySelector("web-adb")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it("changes to adb workflow tab", async () => {
|
||||
component.connect.adbSuccess = jasmine.createSpy().and.returnValue(false);
|
||||
component.isAdbProxy = true;
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
const webTab: HTMLButtonElement | null = htmlElement.querySelector("#web-tab");
|
||||
expect(webTab).toBeInstanceOf(HTMLButtonElement);
|
||||
webTab?.dispatchEvent(new Event("click"));
|
||||
fixture.whenStable().then(() => {
|
||||
expect(component.displayWebAdbTab).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("displays no connected devices", async () => {
|
||||
component.connect.isDevicesState = jasmine.createSpy().and.returnValue(true);
|
||||
component.connect.devices = jasmine.createSpy().and.returnValue({});
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then( () => {
|
||||
const el = htmlElement.querySelector("devices-connecting");
|
||||
expect(el).toBeTruthy();
|
||||
expect(el?.innerHTML).toContain("No devices detected");
|
||||
});
|
||||
});
|
||||
|
||||
it("displays connected authorised devices", async () => {
|
||||
component.connect.isDevicesState = jasmine.createSpy().and.returnValue(true);
|
||||
component.connect.devices = jasmine.createSpy().and.returnValue({"35562": {model: "Pixel 6", authorised:true}});
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then( () => {
|
||||
const el = htmlElement.querySelector("devices-connecting");
|
||||
expect(el).toBeTruthy();
|
||||
expect(el?.innerHTML).toContain("Connected devices:");
|
||||
expect(el?.innerHTML).toContain("Pixel 6");
|
||||
expect(el?.innerHTML).toContain("smartphone");
|
||||
});
|
||||
});
|
||||
|
||||
it("displays connected unauthorised devices", async () => {
|
||||
component.connect.isDevicesState = jasmine.createSpy().and.returnValue(true);
|
||||
component.connect.devices = jasmine.createSpy().and.returnValue({"35562": {model: "Pixel 6", authorised:false}});
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then( () => {
|
||||
const el = htmlElement.querySelector("devices-connecting");
|
||||
expect(el).toBeTruthy();
|
||||
expect(el?.innerHTML).toContain("Connected devices:");
|
||||
expect(el?.innerHTML).toContain("unauthorised");
|
||||
expect(el?.innerHTML).toContain("screen_lock_portrait");
|
||||
});
|
||||
});
|
||||
|
||||
it("displays trace collection config elements", async () => {
|
||||
component.connect.isStartTraceState = jasmine.createSpy().and.returnValue(true);
|
||||
const mock = {model: "Pixel 6", authorised:true};
|
||||
component.connect.devices = jasmine.createSpy().and.returnValue({"35562": mock});
|
||||
component.connect.selectedDevice = jasmine.createSpy().and.returnValue(mock);
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then( () => {
|
||||
const el = htmlElement.querySelector("trace-collection-config");
|
||||
expect(el).toBeTruthy();
|
||||
expect(el?.innerHTML).toContain("smartphone");
|
||||
expect(el?.innerHTML).toContain("Pixel 6");
|
||||
expect(el?.innerHTML).toContain("35562");
|
||||
|
||||
const traceSection = htmlElement.querySelector("trace-section");
|
||||
expect(traceSection).toBeTruthy();
|
||||
|
||||
const dumpSection = htmlElement.querySelector("dump-section");
|
||||
expect(dumpSection).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it("start trace button works as expected", async () => {
|
||||
component.connect.isStartTraceState = jasmine.createSpy().and.returnValue(true);
|
||||
const mock = {model: "Pixel 6", authorised:true};
|
||||
component.connect.devices = jasmine.createSpy().and.returnValue({"35562": mock});
|
||||
component.connect.selectedDevice = jasmine.createSpy().and.returnValue(mock);
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then( () => {
|
||||
const start: HTMLButtonElement | null = htmlElement.querySelector(".start-btn");
|
||||
expect(start).toBeInstanceOf(HTMLButtonElement);
|
||||
start?.dispatchEvent(new Event("click"));
|
||||
fixture.whenStable().then(() => {
|
||||
expect(component.startTracing).toHaveBeenCalled();
|
||||
expect(component.connect.startTrace).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("dump state button works as expected", async () => {
|
||||
component.connect.isStartTraceState = jasmine.createSpy().and.returnValue(true);
|
||||
const mock = {model: "Pixel 6", authorised:true};
|
||||
component.connect.devices = jasmine.createSpy().and.returnValue({"35562": mock});
|
||||
component.connect.selectedDevice = jasmine.createSpy().and.returnValue(mock);
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then( () => {
|
||||
const dump: HTMLButtonElement | null = htmlElement.querySelector(".dump-btn");
|
||||
expect(dump).toBeInstanceOf(HTMLButtonElement);
|
||||
dump?.dispatchEvent(new Event("click"));
|
||||
fixture.whenStable().then(() => {
|
||||
expect(component.dumpState).toHaveBeenCalled();
|
||||
expect(component.connect.dumpState).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("change device button works as expected", async () => {
|
||||
component.connect.isStartTraceState = jasmine.createSpy().and.returnValue(true);
|
||||
const mock = {model: "Pixel 6", authorised:true};
|
||||
component.connect.devices = jasmine.createSpy().and.returnValue({"35562": mock});
|
||||
component.connect.selectedDevice = jasmine.createSpy().and.returnValue(mock);
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then( () => {
|
||||
const change: HTMLButtonElement | null = htmlElement.querySelector(".change-btn");
|
||||
expect(change).toBeInstanceOf(HTMLButtonElement);
|
||||
change?.dispatchEvent(new Event("click"));
|
||||
fixture.whenStable().then(() => {
|
||||
expect(component.connect.resetLastDevice).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("displays unknown error message", () => {
|
||||
component.connect.isErrorState = jasmine.createSpy().and.returnValue(true);
|
||||
component.connect.proxy!.errorText = "bad things are happening";
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then( () => {
|
||||
const el = htmlElement.querySelector(".unknown-error");
|
||||
expect(el?.innerHTML).toContain("Error:");
|
||||
expect(el?.innerHTML).toContain("bad things are happening");
|
||||
const retry: HTMLButtonElement | null = htmlElement.querySelector(".retry-btn");
|
||||
expect(retry).toBeInstanceOf(HTMLButtonElement);
|
||||
retry?.dispatchEvent(new Event("click"));
|
||||
fixture.whenStable().then(() => {
|
||||
expect(component.connect.restart).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("displays end tracing elements", () => {
|
||||
component.connect.isEndTraceState = jasmine.createSpy().and.returnValue(true);
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then( () => {
|
||||
const el = htmlElement.querySelector(".end-tracing");
|
||||
expect(el?.innerHTML).toContain("Tracing...");
|
||||
expect(htmlElement.querySelector("mat-progress-bar")).toBeTruthy();
|
||||
|
||||
const end: HTMLButtonElement | null = htmlElement.querySelector(".end");
|
||||
expect(end).toBeInstanceOf(HTMLButtonElement);
|
||||
end?.dispatchEvent(new Event("click"));
|
||||
fixture.whenStable().then(() => {
|
||||
expect(component.endTrace).toHaveBeenCalled();
|
||||
expect(component.connect.endTrace).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("displays loading data elements", () => {
|
||||
component.connect.isLoadDataState = jasmine.createSpy().and.returnValue(true);
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
expect(htmlElement.querySelector(".load-data")?.innerHTML).toContain("Loading data...");
|
||||
expect(htmlElement.querySelector("mat-progress-bar")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
295
tools/winscope-ng/src/app/collect_traces.component.ts
Normal file
295
tools/winscope-ng/src/app/collect_traces.component.ts
Normal file
@@ -0,0 +1,295 @@
|
||||
/*
|
||||
* Copyright (C) 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { Component, Input, OnInit, Output, EventEmitter } from "@angular/core";
|
||||
import { ProxyConnection } from "trace_collection/proxy_connection";
|
||||
import { Connection } from "trace_collection/connection";
|
||||
import { setTraces } from "trace_collection/set_traces";
|
||||
import { ProxyState } from "../trace_collection/proxy_client";
|
||||
import { traceConfigurations, configMap, SelectionConfiguration, TraceConfigurationMap, EnableConfiguration } from "../trace_collection/trace_collection_utils";
|
||||
import { Core } from "app/core";
|
||||
import { PersistentStore } from "../common/persistent_store";
|
||||
|
||||
|
||||
@Component({
|
||||
selector: "collect-traces",
|
||||
template: `
|
||||
<mat-card-title id="title">Collect Traces</mat-card-title>
|
||||
<mat-card-content>
|
||||
|
||||
<div class="connecting-message" *ngIf="connect.isConnectingState()">Connecting...</div>
|
||||
|
||||
<div class="set-up-adb" *ngIf="!connect.adbSuccess()">
|
||||
<button id="proxy-tab" mat-raised-button [ngClass]="tabClass(true)" (click)="displayAdbProxyTab()">ADB Proxy</button>
|
||||
<button id="web-tab" mat-raised-button [ngClass]="tabClass(false)" (click)="displayWebAdbTab()">Web ADB</button>
|
||||
<adb-proxy *ngIf="isAdbProxy" [(proxy)]="connect.proxy!" (addKey)="onAddKey($event)"></adb-proxy>
|
||||
<web-adb *ngIf="!isAdbProxy"></web-adb>
|
||||
</div>
|
||||
|
||||
<div id="devices-connecting" *ngIf="connect.isDevicesState()">
|
||||
<div> {{ objectKeys(connect.devices()).length > 0 ? "Connected devices:" : "No devices detected" }}</div>
|
||||
<mat-list class="device-choice">
|
||||
<mat-list-item *ngFor="let deviceId of objectKeys(connect.devices())" (click)="connect.selectDevice(deviceId)">
|
||||
<mat-icon class="icon-message">
|
||||
{{ connect.devices()[deviceId].authorised ? "smartphone" : "screen_lock_portrait" }}
|
||||
</mat-icon>
|
||||
<span class="icon-message">
|
||||
{{ connect.devices()[deviceId].authorised ? connect.devices()[deviceId].model : "unauthorised" }} ({{ deviceId }})
|
||||
</span>
|
||||
</mat-list-item>
|
||||
</mat-list>
|
||||
</div>
|
||||
|
||||
<div id="trace-collection-config" *ngIf="connect.isStartTraceState()">
|
||||
<div class="device-choice">
|
||||
<mat-list class="device-choice">
|
||||
<mat-list-item>
|
||||
<mat-icon class="icon-message">smartphone</mat-icon>
|
||||
<span class="icon-message">
|
||||
{{ connect.selectedDevice().model }} ({{ connect.selectedDeviceId() }})
|
||||
</span>
|
||||
</mat-list-item>
|
||||
</mat-list>
|
||||
</div>
|
||||
|
||||
<div class="trace-section">
|
||||
<div>
|
||||
<button class="start-btn" mat-raised-button (click)="startTracing()">Start Trace</button>
|
||||
<button class="dump-btn" mat-raised-button (click)="dumpState()">Dump State</button>
|
||||
<button class="change-btn" mat-raised-button (click)="connect.resetLastDevice()">Change Device</button>
|
||||
</div>
|
||||
<h3>Trace targets:</h3>
|
||||
<trace-config
|
||||
*ngFor="let traceKey of objectKeys(setTraces.DYNAMIC_TRACES)"
|
||||
[trace]="setTraces.DYNAMIC_TRACES[traceKey]"
|
||||
></trace-config>
|
||||
</div>
|
||||
|
||||
<div class="dump-section">
|
||||
<h3>Dump targets:</h3>
|
||||
<div class="selection">
|
||||
<mat-checkbox
|
||||
*ngFor="let dumpKey of objectKeys(setTraces.DUMPS)"
|
||||
[(ngModel)]="setTraces.DUMPS[dumpKey].run"
|
||||
>{{setTraces.DUMPS[dumpKey].name}}</mat-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="unknown-error" *ngIf="connect.isErrorState()">
|
||||
<mat-icon class="icon-message">error</mat-icon>
|
||||
<span class="icon-message">Error:</span>
|
||||
<pre>
|
||||
{{ connect.proxy?.errorText }}
|
||||
</pre>
|
||||
<button class="retry-btn" mat-raised-button (click)="connect.restart()">Retry</button>
|
||||
</div>
|
||||
|
||||
<div class="end-tracing" *ngIf="connect.isEndTraceState()">
|
||||
<span>Tracing...</span>
|
||||
<mat-progress-bar md-indeterminate value="{{connect.loadProgress}}"></mat-progress-bar>
|
||||
<button class="end-btn" mat-raised-button (click)="endTrace()">End trace</button>
|
||||
</div>
|
||||
|
||||
<div class="load-data" *ngIf="connect.isLoadDataState()">
|
||||
<span>Loading data...</span>
|
||||
<mat-progress-bar md-indeterminate></mat-progress-bar>
|
||||
</div>
|
||||
|
||||
</mat-card-content>
|
||||
`,
|
||||
styles: [".device-choice {cursor: pointer}"]
|
||||
})
|
||||
export class CollectTracesComponent implements OnInit {
|
||||
objectKeys = Object.keys;
|
||||
isAdbProxy = true;
|
||||
traceConfigurations = traceConfigurations;
|
||||
connect: Connection = new ProxyConnection();
|
||||
setTraces = setTraces;
|
||||
|
||||
@Input()
|
||||
store: PersistentStore = new PersistentStore();
|
||||
|
||||
@Input()
|
||||
core: Core = new Core();
|
||||
|
||||
@Output()
|
||||
coreChange = new EventEmitter<Core>();
|
||||
|
||||
@Input()
|
||||
dataLoaded = false;
|
||||
|
||||
@Output()
|
||||
dataLoadedChange = new EventEmitter<boolean>();
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.isAdbProxy) {
|
||||
this.connect = new ProxyConnection();
|
||||
} else {
|
||||
//TODO: change to WebAdbConnection
|
||||
this.connect = new ProxyConnection();
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.connect.proxy?.removeOnProxyChange(this.onProxyChange);
|
||||
}
|
||||
|
||||
public onAddKey(key: string) {
|
||||
this.store.addToStore("adb.proxyKey", key);
|
||||
if (this.connect.setProxyKey) {
|
||||
this.connect.setProxyKey(key);
|
||||
}
|
||||
this.connect.restart();
|
||||
}
|
||||
|
||||
public onProxyChange(newState: ProxyState) {
|
||||
this.connect.onConnectChange(newState);
|
||||
}
|
||||
|
||||
public displayAdbProxyTab() {
|
||||
this.isAdbProxy = true;
|
||||
this.connect = new ProxyConnection();
|
||||
}
|
||||
|
||||
public displayWebAdbTab() {
|
||||
this.isAdbProxy = false;
|
||||
//TODO: change to WebAdbConnection
|
||||
this.connect = new ProxyConnection();
|
||||
}
|
||||
|
||||
public requestedTraces() {
|
||||
const tracesFromCollection: Array<string> = [];
|
||||
const req = Object.keys(setTraces.DYNAMIC_TRACES)
|
||||
.filter((traceKey:string) => {
|
||||
const traceConfig = setTraces.DYNAMIC_TRACES[traceKey];
|
||||
if (traceConfig.isTraceCollection) {
|
||||
traceConfig.config?.enableConfigs.forEach((innerTrace:EnableConfiguration) => {
|
||||
if (innerTrace.enabled) {
|
||||
tracesFromCollection.push(innerTrace.key);
|
||||
}
|
||||
});
|
||||
return false;
|
||||
}
|
||||
return traceConfig.run;
|
||||
});
|
||||
return req.concat(tracesFromCollection);
|
||||
}
|
||||
|
||||
public requestedDumps() {
|
||||
return Object.keys(setTraces.DUMPS)
|
||||
.filter((dumpKey:string) => {
|
||||
return setTraces.DUMPS[dumpKey].run;
|
||||
});
|
||||
}
|
||||
|
||||
public requestedEnableConfig(): Array<string> | undefined {
|
||||
const req: Array<string> = [];
|
||||
Object.keys(setTraces.DYNAMIC_TRACES)
|
||||
.forEach((traceKey:string) => {
|
||||
const trace = setTraces.DYNAMIC_TRACES[traceKey];
|
||||
if(!trace.isTraceCollection
|
||||
&& trace.run
|
||||
&& trace.config
|
||||
&& trace.config.enableConfigs) {
|
||||
trace.config.enableConfigs.forEach((con:EnableConfiguration) => {
|
||||
if (con.enabled) {
|
||||
req.push(con.key);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
if (req.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
return req;
|
||||
}
|
||||
|
||||
public requestedSelection(traceType: string): configMap | undefined {
|
||||
if (!setTraces.DYNAMIC_TRACES[traceType].run) {
|
||||
return undefined;
|
||||
}
|
||||
const selected: configMap = {};
|
||||
setTraces.DYNAMIC_TRACES[traceType].config?.selectionConfigs.forEach(
|
||||
(con: SelectionConfiguration) => {
|
||||
selected[con.key] = con.value;
|
||||
}
|
||||
);
|
||||
return selected;
|
||||
}
|
||||
|
||||
public startTracing() {
|
||||
console.log("begin tracing");
|
||||
setTraces.reqTraces = this.requestedTraces();
|
||||
const reqEnableConfig = this.requestedEnableConfig();
|
||||
const reqSelectedSfConfig = this.requestedSelection("layers_trace");
|
||||
const reqSelectedWmConfig = this.requestedSelection("window_trace");
|
||||
if (setTraces.reqTraces.length < 1) {
|
||||
this.connect.throwNoTargetsError();
|
||||
return;
|
||||
}
|
||||
this.connect.startTrace(
|
||||
reqEnableConfig,
|
||||
reqSelectedSfConfig,
|
||||
reqSelectedWmConfig
|
||||
);
|
||||
}
|
||||
|
||||
public async dumpState() {
|
||||
console.log("begin dump");
|
||||
setTraces.reqDumps = this.requestedDumps();
|
||||
await this.connect.dumpState();
|
||||
while (!setTraces.dataReady && !setTraces.dumpError) {
|
||||
await this.waitForData(1000);
|
||||
}
|
||||
if (!setTraces.dumpError) {
|
||||
await this.loadFiles();
|
||||
} else {
|
||||
this.core.clearData();
|
||||
}
|
||||
}
|
||||
|
||||
public async endTrace() {
|
||||
console.log("end tracing");
|
||||
await this.connect.endTrace();
|
||||
while (!setTraces.dataReady) {
|
||||
await this.waitForData(1000);
|
||||
}
|
||||
await this.loadFiles();
|
||||
}
|
||||
|
||||
public async loadFiles() {
|
||||
console.log("loading files", this.connect.adbData());
|
||||
await this.core.bootstrap(this.connect.adbData());
|
||||
this.dataLoaded = true;
|
||||
this.dataLoadedChange.emit(this.dataLoaded);
|
||||
this.coreChange.emit(this.core);
|
||||
console.log("finished loading data!");
|
||||
}
|
||||
|
||||
public tabClass(adbTab: boolean) {
|
||||
let isActive: string;
|
||||
if (adbTab) {
|
||||
isActive = this.isAdbProxy ? "active" : "inactive";
|
||||
} else {
|
||||
isActive = !this.isAdbProxy ? "active" : "inactive";
|
||||
}
|
||||
return ["tab", isActive];
|
||||
}
|
||||
|
||||
private waitForData(ms: number) {
|
||||
return new Promise( resolve => setTimeout(resolve, ms) );
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,8 @@
|
||||
import {TraceTypeId} from "common/trace/type_id";
|
||||
import {Parser} from "parsers/parser";
|
||||
import {ParserFactory} from "parsers/parser_factory";
|
||||
import { setTraces } from "trace_collection/set_traces";
|
||||
import { proxyClient } from "trace_collection/proxy_client";
|
||||
import {Viewer} from "viewers/viewer";
|
||||
import {ViewerFactory} from "viewers/viewer_factory";
|
||||
|
||||
@@ -29,6 +31,7 @@ class Core {
|
||||
}
|
||||
|
||||
async bootstrap(traces: Blob[]) {
|
||||
this.clearData();
|
||||
this.parsers = await new ParserFactory().createParsers(traces);
|
||||
console.log("created parsers: ", this.parsers);
|
||||
|
||||
@@ -71,6 +74,13 @@ class Core {
|
||||
viewer.notifyCurrentTraceEntries(traceEntries);
|
||||
});
|
||||
}
|
||||
|
||||
clearData() {
|
||||
this.parsers = [];
|
||||
this.viewers = [];
|
||||
setTraces.dataReady = false;
|
||||
proxyClient.adbData = [];
|
||||
}
|
||||
}
|
||||
|
||||
export { Core };
|
||||
export { Core };
|
||||
123
tools/winscope-ng/src/app/trace_config.component.spec.ts
Normal file
123
tools/winscope-ng/src/app/trace_config.component.spec.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* Copyright (C) 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { CommonModule } from "@angular/common";
|
||||
import {ComponentFixture, TestBed} from "@angular/core/testing";
|
||||
import {TraceConfigComponent} from "./trace_config.component";
|
||||
import { MatCheckboxModule } from "@angular/material/checkbox";
|
||||
import { MatFormFieldModule } from "@angular/material/form-field";
|
||||
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
|
||||
import { MatInputModule } from "@angular/material/input";
|
||||
import { MatSelectModule } from "@angular/material/select";
|
||||
import { NO_ERRORS_SCHEMA } from "@angular/core";
|
||||
|
||||
describe("TraceConfigComponent", () => {
|
||||
let fixture: ComponentFixture<TraceConfigComponent>;
|
||||
let component: TraceConfigComponent;
|
||||
let htmlElement: HTMLElement;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
MatCheckboxModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatSelectModule,
|
||||
BrowserAnimationsModule
|
||||
],
|
||||
declarations: [TraceConfigComponent],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
fixture = TestBed.createComponent(TraceConfigComponent);
|
||||
component = fixture.componentInstance;
|
||||
htmlElement = fixture.nativeElement;
|
||||
component.trace = {
|
||||
name: "layers_trace",
|
||||
run: false,
|
||||
config: {
|
||||
enableConfigs: [{
|
||||
name:"trace buffers",
|
||||
key:"tracebuffers",
|
||||
enabled:true
|
||||
}],
|
||||
selectionConfigs: [{
|
||||
key: "tracinglevel",
|
||||
name: "tracing level",
|
||||
options: [
|
||||
"verbose",
|
||||
"debug",
|
||||
"critical",
|
||||
],
|
||||
value: "debug"
|
||||
}]
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
it("can be created", () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it("check that trace checkbox ticked on default run", () => {
|
||||
component.trace.run = true;
|
||||
fixture.detectChanges();
|
||||
const box = htmlElement.querySelector(".trace-box");
|
||||
expect(box?.innerHTML).toContain("aria-checked=\"true\"");
|
||||
expect(box?.innerHTML).toContain("layers_trace");
|
||||
});
|
||||
|
||||
it("check that trace checkbox not ticked on default run", () => {
|
||||
component.trace.run = false;
|
||||
fixture.detectChanges();
|
||||
const box = htmlElement.querySelector(".trace-box");
|
||||
expect(box?.innerHTML).toContain("aria-checked=\"false\"");
|
||||
});
|
||||
|
||||
it("check that correct advanced enable config only shows", () => {
|
||||
component.trace.config!.selectionConfigs = [];
|
||||
fixture.detectChanges();
|
||||
const adv = htmlElement.querySelector(".adv-config");
|
||||
expect(adv).toBeTruthy();
|
||||
expect(adv?.innerHTML).toContain("trace buffers");
|
||||
expect(adv?.innerHTML).not.toContain("tracing level");
|
||||
});
|
||||
|
||||
it("check that correct advanced selection config shows", () => {
|
||||
component.trace.config!.enableConfigs = [];
|
||||
fixture.detectChanges();
|
||||
const adv = htmlElement.querySelector(".adv-config");
|
||||
expect(adv).toBeTruthy();
|
||||
expect(adv?.innerHTML).not.toContain("trace buffers");
|
||||
expect(adv?.innerHTML).toContain("tracing level");
|
||||
});
|
||||
|
||||
it("check that changing enable config causes box to change", async () => { spyOn(component, "changeTraceCollectionConfig");
|
||||
component.trace.config!.enableConfigs[0].enabled = false;
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
expect(htmlElement.querySelector(".enable-config")?.innerHTML).toContain("aria-checked=\"false\"");
|
||||
});
|
||||
|
||||
it("check that changing selected config causes select to change", async () => {
|
||||
fixture.detectChanges();
|
||||
expect(htmlElement.querySelector(".selection")?.innerHTML).toContain("value=\"debug\"");
|
||||
component.trace.config!.selectionConfigs[0].value = "verbose";
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
expect(htmlElement.querySelector(".selection")?.innerHTML).toContain("value=\"verbose\"");
|
||||
});
|
||||
});
|
||||
});
|
||||
97
tools/winscope-ng/src/app/trace_config.component.ts
Normal file
97
tools/winscope-ng/src/app/trace_config.component.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright (C) 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { Component, Input } from "@angular/core";
|
||||
import { EnableConfiguration, SelectionConfiguration, TraceConfiguration } from "../trace_collection/trace_collection_utils";
|
||||
|
||||
@Component({
|
||||
selector: "trace-config",
|
||||
template: `
|
||||
<div class="card-block">
|
||||
<div>
|
||||
<mat-checkbox
|
||||
class="trace-box"
|
||||
[checked]="trace.run"
|
||||
[indeterminate]="trace.isTraceCollection ? someTraces() : false"
|
||||
(change)="changeRunTrace($event.checked)"
|
||||
>{{trace.name}}</mat-checkbox>
|
||||
|
||||
<div class="adv-config" *ngIf="trace.config">
|
||||
<mat-checkbox
|
||||
*ngFor="let enableConfig of traceEnableConfigs()"
|
||||
class="enable-config"
|
||||
[disabled]="!trace.run && !trace.isTraceCollection"
|
||||
[(ngModel)]="enableConfig.enabled"
|
||||
(ngModelChange)="changeTraceCollectionConfig()"
|
||||
>{{enableConfig.name}}</mat-checkbox>
|
||||
|
||||
<div class="selection" *ngIf="trace.config.selectionConfigs">
|
||||
<mat-form-field
|
||||
appearance="fill"
|
||||
class="config-selection"
|
||||
*ngFor="let selectionConfig of traceSelectionConfigs()"
|
||||
><mat-label>{{selectionConfig.name}}</mat-label>
|
||||
<mat-select class="selected-value" [(value)]="selectionConfig.value" [disabled]="!trace.run">
|
||||
<mat-option
|
||||
*ngFor="let option of selectionConfig.options"
|
||||
value="{{option}}"
|
||||
>{{ option }}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
styles: [".adv-config {margin-left: 5rem;}"],
|
||||
})
|
||||
|
||||
export class TraceConfigComponent {
|
||||
@Input()
|
||||
trace: TraceConfiguration = {};
|
||||
|
||||
public traceEnableConfigs(): Array<EnableConfiguration> {
|
||||
if (this.trace.config) {
|
||||
return this.trace.config.enableConfigs;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public traceSelectionConfigs(): Array<SelectionConfiguration> {
|
||||
if (this.trace.config) {
|
||||
return this.trace.config.selectionConfigs;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public someTraces(): boolean {
|
||||
return this.traceEnableConfigs().filter(trace => trace.enabled).length > 0
|
||||
&& !this.trace.run;
|
||||
}
|
||||
|
||||
public changeRunTrace(run: boolean): void {
|
||||
this.trace.run = run;
|
||||
if (this.trace.isTraceCollection) {
|
||||
this.traceEnableConfigs().forEach((c: EnableConfiguration) => (c.enabled = run));
|
||||
}
|
||||
}
|
||||
|
||||
public changeTraceCollectionConfig(): void {
|
||||
if (this.trace.isTraceCollection) {
|
||||
this.trace.run = this.traceEnableConfigs().every((c: EnableConfiguration) => c.enabled);
|
||||
}
|
||||
}
|
||||
}
|
||||
42
tools/winscope-ng/src/app/upload_traces.component.spec.ts
Normal file
42
tools/winscope-ng/src/app/upload_traces.component.spec.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright (C) 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import {ComponentFixture, TestBed} from "@angular/core/testing";
|
||||
import {UploadTracesComponent} from "./upload_traces.component";
|
||||
import { MatCardModule } from "@angular/material/card";
|
||||
|
||||
describe("CollectTracesComponent", () => {
|
||||
let fixture: ComponentFixture<UploadTracesComponent>;
|
||||
let component: UploadTracesComponent;
|
||||
let htmlElement: HTMLElement;
|
||||
|
||||
beforeAll(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [MatCardModule],
|
||||
declarations: [UploadTracesComponent],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(UploadTracesComponent);
|
||||
component = fixture.componentInstance;
|
||||
htmlElement = fixture.nativeElement;
|
||||
});
|
||||
|
||||
it("can be created", () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
});
|
||||
62
tools/winscope-ng/src/app/upload_traces.component.ts
Normal file
62
tools/winscope-ng/src/app/upload_traces.component.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright (C) 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { Component, Input, Output, EventEmitter } from "@angular/core";
|
||||
import { Core } from "app/core";
|
||||
|
||||
|
||||
@Component({
|
||||
selector: "upload-traces",
|
||||
template: `
|
||||
<mat-card-title>Upload Traces</mat-card-title>
|
||||
<mat-card-content>
|
||||
<div id="inputfile">
|
||||
<input mat-input type="file" (change)="onInputFile($event)" #fileUpload>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
`,
|
||||
|
||||
})
|
||||
export class UploadTracesComponent {
|
||||
@Input()
|
||||
core: Core = new Core();
|
||||
|
||||
@Output()
|
||||
coreChange = new EventEmitter<Core>();
|
||||
|
||||
public async onInputFile(event: Event) {
|
||||
const files = this.getInputFiles(event);
|
||||
await this.core.bootstrap(files);
|
||||
|
||||
const viewersDiv = document.querySelector("div#viewers")!;
|
||||
viewersDiv.innerHTML = "";
|
||||
this.core.getViews().forEach(view => viewersDiv!.appendChild(view) );
|
||||
this.coreChange.emit(this.core);
|
||||
|
||||
const timestampsDiv = document.querySelector("div#timestamps")!;
|
||||
timestampsDiv.innerHTML = `Retrieved ${this.core.getTimestamps().length} unique timestamps`;
|
||||
}
|
||||
|
||||
//TODO: extend with support for multiple files, archives, etc...
|
||||
private getInputFiles(event: Event): File[] {
|
||||
const files: any = (event?.target as HTMLInputElement)?.files;
|
||||
|
||||
if (!files || !files[0]) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [files[0]];
|
||||
}
|
||||
}
|
||||
45
tools/winscope-ng/src/app/web_adb.component.spec.ts
Normal file
45
tools/winscope-ng/src/app/web_adb.component.spec.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright (C) 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import {ComponentFixture, TestBed} from "@angular/core/testing";
|
||||
import {WebAdbComponent} from "./web_adb.component";
|
||||
import { MatIconModule } from "@angular/material/icon";
|
||||
import { MatCardModule } from "@angular/material/card";
|
||||
|
||||
describe("WebAdbComponent", () => {
|
||||
let fixture: ComponentFixture<WebAdbComponent>;
|
||||
let component: WebAdbComponent;
|
||||
let htmlElement: HTMLElement;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [MatIconModule, MatCardModule],
|
||||
declarations: [WebAdbComponent],
|
||||
}).compileComponents();
|
||||
fixture = TestBed.createComponent(WebAdbComponent);
|
||||
component = fixture.componentInstance;
|
||||
htmlElement = fixture.nativeElement;
|
||||
});
|
||||
|
||||
it("can be created", () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders the info message", () => {
|
||||
fixture.detectChanges();
|
||||
expect(htmlElement.querySelector(".adb-info")?.innerHTML).toBe("Add new device");
|
||||
expect(htmlElement.querySelector(".adb-icon")?.innerHTML).toBe("info");
|
||||
});
|
||||
});
|
||||
37
tools/winscope-ng/src/app/web_adb.component.ts
Normal file
37
tools/winscope-ng/src/app/web_adb.component.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright (C) 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import {Component} from "@angular/core";
|
||||
|
||||
@Component({
|
||||
selector: "web-adb",
|
||||
template: `
|
||||
<div id="info-message">
|
||||
<mat-icon class="adb-icon">info</mat-icon>
|
||||
<span class="adb-info">Add new device</span>
|
||||
</div>
|
||||
<div>
|
||||
<p>Click the button below to follow instructions in the Chrome pop-up.</p>
|
||||
<p>Selecting a device will kill all existing ADB connections.</p>
|
||||
</div>
|
||||
<div>
|
||||
<button mat-raised-button>Select a device</button>
|
||||
</div>
|
||||
`,
|
||||
styles: [".icon-message {vertical-align: middle;}"]
|
||||
})
|
||||
export class WebAdbComponent {
|
||||
adbDevice = null;
|
||||
}
|
||||
23
tools/winscope-ng/src/common/persistent_store.ts
Normal file
23
tools/winscope-ng/src/common/persistent_store.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright (C) 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export class PersistentStore {
|
||||
public addToStore(key: string, value: string) {
|
||||
localStorage.setItem(key, value);
|
||||
}
|
||||
public getFromStore(key: string) {
|
||||
return localStorage.getItem(key);
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,57 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
@import "~@angular/material/prebuilt-themes/indigo-pink.css";
|
||||
@import 'https://fonts.googleapis.com/icon?family=Material+Icons';
|
||||
|
||||
#title {
|
||||
color: aqua;
|
||||
font-weight: bold;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
color:rgb(194, 65, 108);
|
||||
font-size: 20;
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.homepage-card {
|
||||
border: 1px solid rgb(129, 129, 129);
|
||||
width: 45rem;
|
||||
height: 30rem;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
mat-checkbox {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
mat-form-field {
|
||||
margin: 10px;
|
||||
height: 5px;
|
||||
}
|
||||
|
||||
mat-icon {
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.icon-message, .adb-icon, .adb-info {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.card-block {
|
||||
margin: 15px;
|
||||
}
|
||||
|
||||
button.mat-raised-button {
|
||||
background-color:rgb(194, 65, 108);
|
||||
color: white;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.tab.inactive {
|
||||
background-color:white;
|
||||
color: black;
|
||||
}
|
||||
56
tools/winscope-ng/src/trace_collection/connection.ts
Normal file
56
tools/winscope-ng/src/trace_collection/connection.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright 2022, The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { ProxyClient } from "trace_collection/proxy_client";
|
||||
import { configMap } from "./trace_collection_utils";
|
||||
|
||||
export interface Device {
|
||||
[key: string]: DeviceProperties
|
||||
}
|
||||
|
||||
export interface DeviceProperties {
|
||||
authorised: boolean;
|
||||
model: string;
|
||||
}
|
||||
|
||||
export interface Connection {
|
||||
adbSuccess: () => boolean;
|
||||
setProxyKey?(key:string): any;
|
||||
devices(): Device;
|
||||
selectedDevice(): DeviceProperties;
|
||||
selectedDeviceId(): string;
|
||||
restart(): any;
|
||||
selectDevice(id:string): any;
|
||||
state(): any;
|
||||
onConnectChange(newState: any): any;
|
||||
resetLastDevice(): any;
|
||||
isDevicesState(): boolean;
|
||||
isStartTraceState(): boolean;
|
||||
isErrorState(): boolean;
|
||||
isEndTraceState(): boolean;
|
||||
isLoadDataState(): boolean;
|
||||
isConnectingState(): boolean;
|
||||
throwNoTargetsError(): any;
|
||||
startTrace(
|
||||
reqEnableConfig?: Array<string>,
|
||||
reqSelectedSfConfig?: configMap,
|
||||
reqSelectedWmConfig?: configMap
|
||||
): any;
|
||||
endTrace(): any;
|
||||
adbData(): Array<Blob>;
|
||||
dumpState(): any;
|
||||
proxy?: ProxyClient;
|
||||
loadProgress: number;
|
||||
}
|
||||
272
tools/winscope-ng/src/trace_collection/proxy_client.ts
Normal file
272
tools/winscope-ng/src/trace_collection/proxy_client.ts
Normal file
@@ -0,0 +1,272 @@
|
||||
/*
|
||||
* Copyright 2022, The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { PersistentStore } from "../common/persistent_store";
|
||||
import { configMap, TRACES } from "./trace_collection_utils";
|
||||
import { setTraces, SetTraces } from "./set_traces";
|
||||
import { Device } from "./connection";
|
||||
import { ProxyConnection } from "./proxy_connection";
|
||||
|
||||
export enum ProxyState {
|
||||
ERROR = 0,
|
||||
CONNECTING = 1,
|
||||
NO_PROXY = 2,
|
||||
INVALID_VERSION = 3,
|
||||
UNAUTH = 4,
|
||||
DEVICES = 5,
|
||||
START_TRACE = 6,
|
||||
END_TRACE = 7,
|
||||
LOAD_DATA = 8,
|
||||
}
|
||||
|
||||
export enum ProxyEndpoint {
|
||||
DEVICES = "/devices/",
|
||||
START_TRACE = "/start/",
|
||||
END_TRACE = "/end/",
|
||||
ENABLE_CONFIG_TRACE = "/configtrace/",
|
||||
SELECTED_WM_CONFIG_TRACE = "/selectedwmconfigtrace/",
|
||||
SELECTED_SF_CONFIG_TRACE = "/selectedsfconfigtrace/",
|
||||
DUMP = "/dump/",
|
||||
FETCH = "/fetch/",
|
||||
STATUS = "/status/",
|
||||
CHECK_WAYLAND = "/checkwayland/",
|
||||
}
|
||||
|
||||
// from here, all requests to the proxy are made
|
||||
class ProxyRequest {
|
||||
async call(
|
||||
method: string,
|
||||
path: string,
|
||||
view: any,
|
||||
onSuccess: any,
|
||||
type?: XMLHttpRequestResponseType,
|
||||
jsonRequest: any = null
|
||||
) {
|
||||
const request = new XMLHttpRequest();
|
||||
const client = proxyClient;
|
||||
request.onreadystatechange = function() {
|
||||
if (this.readyState !== 4) {
|
||||
return;
|
||||
}
|
||||
if (this.status === 0) {
|
||||
client.setState(ProxyState.NO_PROXY);
|
||||
} else if (this.status === 200) {
|
||||
if (this.getResponseHeader("Winscope-Proxy-Version") !== client.VERSION) {
|
||||
client.setState(ProxyState.INVALID_VERSION);
|
||||
} else if (onSuccess) {
|
||||
onSuccess(this, view);
|
||||
}
|
||||
} else if (this.status === 403) {
|
||||
client.setState(ProxyState.UNAUTH);
|
||||
} else {
|
||||
if (this.responseType === "text" || !this.responseType) {
|
||||
client.errorText = this.responseText;
|
||||
} else if (this.responseType === "arraybuffer") {
|
||||
client.errorText = String.fromCharCode.apply(null, new Array(this.response));
|
||||
}
|
||||
client.setState(ProxyState.ERROR, client.errorText);
|
||||
}
|
||||
};
|
||||
request.responseType = type || "";
|
||||
request.open(method, client.WINSCOPE_PROXY_URL + path);
|
||||
const lastKey = client.store.getFromStore("adb.proxyKey");
|
||||
if (lastKey !== null) {
|
||||
client.proxyKey = lastKey;
|
||||
}
|
||||
request.setRequestHeader("Winscope-Token", client.proxyKey);
|
||||
if (jsonRequest) {
|
||||
const json = JSON.stringify(jsonRequest);
|
||||
request.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
|
||||
request.send(json);
|
||||
} else {
|
||||
request.send();
|
||||
}
|
||||
}
|
||||
|
||||
getDevices(view:any) {
|
||||
proxyRequest.call("GET", ProxyEndpoint.DEVICES, view, proxyRequest.onSuccessGetDevices);
|
||||
}
|
||||
|
||||
async fetchFiles(dev:string, files: Array<string>, idx: number, view:any) {
|
||||
await proxyRequest.call("GET", `${ProxyEndpoint.FETCH}${dev}/${files[idx]}/`, view,
|
||||
proxyRequest.onSuccessUpdateAdbData, "arraybuffer");
|
||||
}
|
||||
|
||||
setEnabledConfig(view:any, req: Array<string>) {
|
||||
proxyRequest.call("POST", `${ProxyEndpoint.ENABLE_CONFIG_TRACE}${view.proxy.selectedDevice}/`, view, null, undefined, req);
|
||||
}
|
||||
|
||||
setSelectedConfig(endpoint: ProxyEndpoint, view:any, req: configMap) {
|
||||
proxyRequest.call("POST", `${endpoint}${view.proxy.selectedDevice}/`, view, null, undefined, req);
|
||||
}
|
||||
|
||||
startTrace(view:any) {
|
||||
proxyRequest.call("POST", `${ProxyEndpoint.START_TRACE}${view.proxy.selectedDevice}/`, view, function(request:XMLHttpRequest, newView:ProxyConnection) {
|
||||
newView.keepAliveTrace(newView);
|
||||
}, undefined, setTraces.reqTraces);
|
||||
}
|
||||
|
||||
async endTrace(view:any) {
|
||||
await proxyRequest.call("POST", `${ProxyEndpoint.END_TRACE}${view.proxy.selectedDevice}/`, view,
|
||||
async function (request:XMLHttpRequest, newView:ProxyConnection) {
|
||||
await proxyClient.updateAdbData(setTraces.reqTraces, 0, "trace", newView);
|
||||
});
|
||||
}
|
||||
|
||||
keepTraceAlive(view:any) {
|
||||
this.call("GET", `${ProxyEndpoint.STATUS}${view.proxy.selectedDevice}/`, view, function(request:XMLHttpRequest, newView:ProxyConnection) {
|
||||
if (request.responseText !== "True") {
|
||||
newView.endTrace();
|
||||
} else if (newView.keep_alive_worker === null) {
|
||||
newView.keep_alive_worker = setInterval(newView.keepAliveTrace, 1000, newView);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async dumpState(view:any) {
|
||||
await proxyRequest.call("POST", `${ProxyEndpoint.DUMP}${view.proxy.selectedDevice}/`, view,
|
||||
async function(request:XMLHttpRequest, newView:ProxyConnection) {
|
||||
await proxyClient.updateAdbData(setTraces.reqDumps, 0, "dump", newView);
|
||||
}, undefined, setTraces.reqDumps);
|
||||
}
|
||||
|
||||
onSuccessGetDevices = function(request: XMLHttpRequest, view: ProxyClient) {
|
||||
const client = proxyClient;
|
||||
try {
|
||||
client.devices = JSON.parse(request.responseText);
|
||||
const last = client.store.getFromStore("adb.lastDevice");
|
||||
if (last && client.devices[last] &&
|
||||
client.devices[last].authorised) {
|
||||
client.selectDevice(last);
|
||||
} else {
|
||||
if (client.refresh_worker === null) {
|
||||
client.refresh_worker = setInterval(client.getDevices, 1000);
|
||||
}
|
||||
client.setState(ProxyState.DEVICES);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
client.errorText = request.responseText;
|
||||
client.setState(ProxyState.ERROR, client.errorText);
|
||||
}
|
||||
};
|
||||
|
||||
onSuccessSetAvailableTraces = function(request:XMLHttpRequest, view:SetTraces) {
|
||||
try {
|
||||
view.DYNAMIC_TRACES = TRACES["default"];
|
||||
if(request.responseText == "true") {
|
||||
view.appendOptionalTraces(view, "arc");
|
||||
}
|
||||
} catch(err) {
|
||||
proxyClient.setState(ProxyState.ERROR, request.responseText);
|
||||
}
|
||||
};
|
||||
|
||||
onSuccessUpdateAdbData = async (request: XMLHttpRequest, view: ProxyConnection) => {
|
||||
const idx = proxyClient.adbParams.idx;
|
||||
const files = proxyClient.adbParams.files;
|
||||
const traceType = proxyClient.adbParams.traceType;
|
||||
try {
|
||||
const enc = new TextDecoder("utf-8");
|
||||
const resp = enc.decode(request.response);
|
||||
const filesByType = JSON.parse(resp);
|
||||
|
||||
for (const filetype in filesByType) {
|
||||
const files = filesByType[filetype];
|
||||
for (const encodedFileBuffer of files) {
|
||||
const buffer = Uint8Array.from(atob(encodedFileBuffer), (c) => c.charCodeAt(0));
|
||||
const blob = new Blob([buffer]);
|
||||
proxyClient.adbData.push(blob);
|
||||
}
|
||||
}
|
||||
if (idx < files.length - 1) {
|
||||
proxyClient.updateAdbData(files, idx + 1, traceType, view);
|
||||
} else {
|
||||
setTraces.dataReady = true;
|
||||
}
|
||||
} catch (error) {
|
||||
proxyClient.setState(ProxyState.ERROR, request.responseText);
|
||||
}
|
||||
};
|
||||
}
|
||||
export const proxyRequest = new ProxyRequest();
|
||||
|
||||
interface AdbParams {
|
||||
files: Array<string>,
|
||||
idx: number,
|
||||
traceType: string
|
||||
}
|
||||
|
||||
// stores all the changing variables from proxy and sets up calls from ProxyRequest
|
||||
export class ProxyClient {
|
||||
readonly WINSCOPE_PROXY_URL = "http://localhost:5544";
|
||||
readonly VERSION = "0.8";
|
||||
state: ProxyState = ProxyState.CONNECTING;
|
||||
stateChangeListeners: {(param:ProxyState, errorText:string): void;}[] = [];
|
||||
refresh_worker: NodeJS.Timer | null = null;
|
||||
devices: Device = {};
|
||||
selectedDevice = "";
|
||||
errorText = "";
|
||||
adbData: Array<Blob> = [];
|
||||
proxyKey = "";
|
||||
lastDevice = "";
|
||||
store = new PersistentStore();
|
||||
adbParams: AdbParams = {
|
||||
files: [],
|
||||
idx: -1,
|
||||
traceType: "",
|
||||
};
|
||||
|
||||
setState(state:ProxyState, errorText = "") {
|
||||
this.state = state;
|
||||
this.errorText = errorText;
|
||||
for (const listener of this.stateChangeListeners) {
|
||||
listener(state, errorText);
|
||||
}
|
||||
}
|
||||
|
||||
onProxyChange(fn: (state:ProxyState, errorText:string) => void) {
|
||||
this.removeOnProxyChange(fn);
|
||||
this.stateChangeListeners.push(fn);
|
||||
}
|
||||
|
||||
removeOnProxyChange(removeFn: (state:ProxyState, errorText:string) => void) {
|
||||
this.stateChangeListeners = this.stateChangeListeners.filter(fn => fn !== removeFn);
|
||||
}
|
||||
|
||||
getDevices() {
|
||||
if (this.state !== ProxyState.DEVICES && this.state !== ProxyState.CONNECTING) {
|
||||
clearInterval(this.refresh_worker!);
|
||||
this.refresh_worker = null;
|
||||
return;
|
||||
}
|
||||
proxyRequest.getDevices(this);
|
||||
}
|
||||
|
||||
selectDevice(device_id: string) {
|
||||
this.selectedDevice = device_id;
|
||||
this.store.addToStore("adb.lastDevice", device_id);
|
||||
this.setState(ProxyState.START_TRACE);
|
||||
}
|
||||
|
||||
async updateAdbData(files:Array<string>, idx:number, traceType:string, view: ProxyConnection) {
|
||||
this.adbParams.files = files;
|
||||
this.adbParams.idx = idx;
|
||||
this.adbParams.traceType = traceType;
|
||||
await proxyRequest.fetchFiles(this.selectedDevice, files, idx, view);
|
||||
}
|
||||
}
|
||||
|
||||
export const proxyClient = new ProxyClient();
|
||||
166
tools/winscope-ng/src/trace_collection/proxy_connection.ts
Normal file
166
tools/winscope-ng/src/trace_collection/proxy_connection.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
/*
|
||||
* Copyright 2022, The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import {
|
||||
proxyRequest,
|
||||
proxyClient,
|
||||
ProxyState,
|
||||
ProxyEndpoint
|
||||
} from "trace_collection/proxy_client";
|
||||
import { setTraces } from "./set_traces";
|
||||
import { Connection, DeviceProperties } from "./connection";
|
||||
import { configMap } from "./trace_collection_utils";
|
||||
|
||||
export class ProxyConnection implements Connection {
|
||||
proxy = proxyClient;
|
||||
keep_alive_worker: any = null;
|
||||
notConnected = [
|
||||
ProxyState.NO_PROXY,
|
||||
ProxyState.UNAUTH,
|
||||
ProxyState.INVALID_VERSION,
|
||||
];
|
||||
loadProgress = 0;
|
||||
|
||||
constructor() {
|
||||
this.proxy.setState(ProxyState.CONNECTING);
|
||||
this.proxy.onProxyChange(this.onConnectChange);
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
if (urlParams.has("token")) {
|
||||
this.proxy.proxyKey = urlParams.get("token")!;
|
||||
}
|
||||
this.proxy.getDevices();
|
||||
}
|
||||
|
||||
public devices() {
|
||||
return this.proxy.devices;
|
||||
}
|
||||
|
||||
public adbData() {
|
||||
return this.proxy.adbData;
|
||||
}
|
||||
|
||||
public state() {
|
||||
return this.proxy.state;
|
||||
}
|
||||
|
||||
public isDevicesState() {
|
||||
return this.state() === ProxyState.DEVICES;
|
||||
}
|
||||
|
||||
public isStartTraceState() {
|
||||
return this.state() === ProxyState.START_TRACE;
|
||||
}
|
||||
|
||||
public isErrorState() {
|
||||
return this.state() === ProxyState.ERROR;
|
||||
}
|
||||
|
||||
public isEndTraceState() {
|
||||
return this.state() === ProxyState.END_TRACE;
|
||||
}
|
||||
|
||||
public isLoadDataState() {
|
||||
return this.state() === ProxyState.LOAD_DATA;
|
||||
}
|
||||
|
||||
public isConnectingState() {
|
||||
return this.state() === ProxyState.CONNECTING;
|
||||
}
|
||||
|
||||
public throwNoTargetsError() {
|
||||
this.proxy.setState(ProxyState.ERROR, "No targets selected");
|
||||
}
|
||||
|
||||
public setProxyKey(key: string) {
|
||||
this.proxy.proxyKey = key;
|
||||
this.restart();
|
||||
}
|
||||
|
||||
public adbSuccess() {
|
||||
return !this.notConnected.includes(this.proxy.state);
|
||||
}
|
||||
|
||||
public selectedDevice(): DeviceProperties {
|
||||
return this.proxy.devices[this.proxy.selectedDevice];
|
||||
}
|
||||
|
||||
public selectedDeviceId(): string {
|
||||
return this.proxy.selectedDevice;
|
||||
}
|
||||
|
||||
public restart() {
|
||||
this.proxy.setState(ProxyState.CONNECTING);
|
||||
}
|
||||
|
||||
public resetLastDevice() {
|
||||
this.proxy.store.addToStore("adb.lastDevice", "");
|
||||
this.restart();
|
||||
}
|
||||
|
||||
public selectDevice(id: string) {
|
||||
this.proxy.selectDevice(id);
|
||||
}
|
||||
|
||||
public keepAliveTrace(view:ProxyConnection) {
|
||||
if (!view.isEndTraceState()) {
|
||||
clearInterval(view.keep_alive_worker);
|
||||
view.keep_alive_worker = null;
|
||||
return;
|
||||
}
|
||||
proxyRequest.keepTraceAlive(view);
|
||||
}
|
||||
|
||||
public startTrace(
|
||||
reqEnableConfig?: Array<string>,
|
||||
reqSelectedSfConfig?: configMap,
|
||||
reqSelectedWmConfig?: configMap
|
||||
) {
|
||||
if (reqEnableConfig) {
|
||||
proxyRequest.setEnabledConfig(this, reqEnableConfig);
|
||||
}
|
||||
if (reqSelectedSfConfig) {
|
||||
proxyRequest.setSelectedConfig(ProxyEndpoint.SELECTED_SF_CONFIG_TRACE, this, reqSelectedSfConfig);
|
||||
}
|
||||
if (reqSelectedWmConfig) {
|
||||
proxyRequest.setSelectedConfig(ProxyEndpoint.SELECTED_WM_CONFIG_TRACE, this, reqSelectedWmConfig);
|
||||
}
|
||||
proxyClient.setState(ProxyState.END_TRACE);
|
||||
proxyRequest.startTrace(this);
|
||||
}
|
||||
|
||||
public async endTrace() {
|
||||
this.proxy.setState(ProxyState.LOAD_DATA);
|
||||
await proxyRequest.endTrace(this);
|
||||
}
|
||||
|
||||
public async dumpState() {
|
||||
if (setTraces.reqDumps.length < 1) {
|
||||
this.proxy.setState(ProxyState.ERROR, "No targets selected");
|
||||
setTraces.dumpError = true;
|
||||
return;
|
||||
}
|
||||
this.proxy.setState(ProxyState.LOAD_DATA);
|
||||
await proxyRequest.dumpState(this);
|
||||
}
|
||||
|
||||
public onConnectChange(newState: ProxyState) {
|
||||
if (newState === ProxyState.CONNECTING) {
|
||||
proxyClient.getDevices();
|
||||
}
|
||||
if (newState == ProxyState.START_TRACE) {
|
||||
setTraces.setAvailableTraces();
|
||||
}
|
||||
}
|
||||
}
|
||||
49
tools/winscope-ng/src/trace_collection/set_traces.ts
Normal file
49
tools/winscope-ng/src/trace_collection/set_traces.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2022, The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { TraceConfigurationMap, TRACES } from "./trace_collection_utils";
|
||||
import {
|
||||
proxyRequest,
|
||||
ProxyEndpoint
|
||||
} from "trace_collection/proxy_client";
|
||||
|
||||
export class SetTraces {
|
||||
DYNAMIC_TRACES = TRACES["default"];
|
||||
reqTraces: string[] = [];
|
||||
reqDumps: string[] = [];
|
||||
dataReady = false;
|
||||
dumpError = false;
|
||||
|
||||
DUMPS: TraceConfigurationMap = {
|
||||
"window_dump": {
|
||||
name: "Window Manager",
|
||||
run: true,
|
||||
},
|
||||
"layers_dump": {
|
||||
name: "Surface Flinger",
|
||||
run: true,
|
||||
}
|
||||
};
|
||||
|
||||
setAvailableTraces() {
|
||||
proxyRequest.call("GET", ProxyEndpoint.CHECK_WAYLAND, this, proxyRequest.onSuccessSetAvailableTraces);
|
||||
}
|
||||
appendOptionalTraces(view:any, device_key:string) {
|
||||
for(const key in TRACES[device_key]) {
|
||||
view.DYNAMIC_TRACES[key] = TRACES[device_key][key];
|
||||
}
|
||||
}
|
||||
}
|
||||
export const setTraces = new SetTraces();
|
||||
190
tools/winscope-ng/src/trace_collection/trace_collection_utils.ts
Normal file
190
tools/winscope-ng/src/trace_collection/trace_collection_utils.ts
Normal file
@@ -0,0 +1,190 @@
|
||||
export interface TraceConfiguration {
|
||||
name?: string,
|
||||
run?: boolean,
|
||||
isTraceCollection?: boolean,
|
||||
config?: ConfigurationOptions
|
||||
}
|
||||
|
||||
|
||||
export interface TraceConfigurationMap {
|
||||
[key: string]: TraceConfiguration
|
||||
}
|
||||
|
||||
interface ConfigurationOptions {
|
||||
enableConfigs: Array<EnableConfiguration>,
|
||||
selectionConfigs: Array<SelectionConfiguration>
|
||||
}
|
||||
|
||||
export interface EnableConfiguration {
|
||||
name: string,
|
||||
key: string,
|
||||
enabled: boolean,
|
||||
}
|
||||
|
||||
export interface SelectionConfiguration {
|
||||
key: string,
|
||||
name: string,
|
||||
options: Array<string>,
|
||||
value: string
|
||||
}
|
||||
|
||||
export type configMap = {
|
||||
[key: string]: Array<string> | string;
|
||||
}
|
||||
|
||||
const wmTraceSelectionConfigs: Array<SelectionConfiguration> = [
|
||||
{
|
||||
key: "wmbuffersize",
|
||||
name: "buffer size (KB)",
|
||||
options: [
|
||||
"4000",
|
||||
"8000",
|
||||
"16000",
|
||||
"32000",
|
||||
],
|
||||
value: "4000"
|
||||
},
|
||||
{
|
||||
key: "tracingtype",
|
||||
name: "tracing type",
|
||||
options: [
|
||||
"frame",
|
||||
"transaction",
|
||||
],
|
||||
value: "frame"
|
||||
},
|
||||
{
|
||||
key: "tracinglevel",
|
||||
name: "tracing level",
|
||||
options: [
|
||||
"verbose",
|
||||
"debug",
|
||||
"critical",
|
||||
],
|
||||
value: "verbose"
|
||||
},
|
||||
];
|
||||
|
||||
const sfTraceEnableConfigs: Array<EnableConfiguration> = [
|
||||
{
|
||||
name: "composition",
|
||||
key: "composition",
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
name: "metadata",
|
||||
key: "metadata",
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
name: "hwc",
|
||||
key: "hwc",
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
name: "trace buffers",
|
||||
key: "tracebuffers",
|
||||
enabled: true
|
||||
}
|
||||
];
|
||||
|
||||
const sfTraceSelectionConfigs: Array<SelectionConfiguration> = [
|
||||
{
|
||||
key: "sfbuffersize",
|
||||
name: "buffer size (KB)",
|
||||
options: ["4000","8000","16000","32000"],
|
||||
value: "4000"
|
||||
}
|
||||
];
|
||||
|
||||
export const traceConfigurations: TraceConfigurationMap = {
|
||||
"layers_trace": {
|
||||
name: "Surface Flinger",
|
||||
run: true,
|
||||
config: {
|
||||
enableConfigs: sfTraceEnableConfigs,
|
||||
selectionConfigs: sfTraceSelectionConfigs,
|
||||
}
|
||||
},
|
||||
"window_trace": {
|
||||
name: "Window Manager",
|
||||
run: true,
|
||||
config: {
|
||||
enableConfigs: [],
|
||||
selectionConfigs: wmTraceSelectionConfigs,
|
||||
}
|
||||
},
|
||||
"screen_recording": {
|
||||
name: "Screen Recording",
|
||||
run: true,
|
||||
},
|
||||
"ime_tracing": {
|
||||
name: "IME Tracing",
|
||||
run: true,
|
||||
isTraceCollection: true,
|
||||
config: {
|
||||
enableConfigs: [
|
||||
{
|
||||
name: "Input Method Clients",
|
||||
key: "ime_trace_clients",
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
name: "Input Method Service",
|
||||
key: "ime_trace_service",
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
name: "Input Method Manager Service",
|
||||
key: "ime_trace_managerservice",
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
selectionConfigs: []
|
||||
}
|
||||
},
|
||||
"ime_trace_clients": {
|
||||
name: "Input Method Clients",
|
||||
run: true,
|
||||
},
|
||||
"ime_trace_service": {
|
||||
name: "Input Method Service",
|
||||
run: true,
|
||||
},
|
||||
"ime_trace_managerservice": {
|
||||
name: "Input Method Manager Service",
|
||||
run: true,
|
||||
},
|
||||
"accessibility_trace": {
|
||||
name: "Accessibility",
|
||||
run: false,
|
||||
},
|
||||
"transactions": {
|
||||
name: "Transaction",
|
||||
run: false,
|
||||
},
|
||||
"proto_log": {
|
||||
name: "ProtoLog",
|
||||
run: false,
|
||||
},
|
||||
"wayland_trace": {
|
||||
name: "Wayland",
|
||||
run: false,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
export const TRACES: { [key: string]: TraceConfigurationMap; } = {
|
||||
"default": {
|
||||
"window_trace": traceConfigurations["window_trace"],
|
||||
"accessibility_trace": traceConfigurations["accessibility_trace"],
|
||||
"layers_trace": traceConfigurations["layers_trace"],
|
||||
"transactions": traceConfigurations["transactions"],
|
||||
"proto_log": traceConfigurations["proto_log"],
|
||||
"screen_recording": traceConfigurations["screen_recording"],
|
||||
"ime_tracing": traceConfigurations["ime_tracing"],
|
||||
},
|
||||
"arc": {
|
||||
"wayland_trace": traceConfigurations["wayland_trace"],
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user