| # |
| # Copyright (C) 2024 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 math |
| import os |
| import subprocess |
| import time |
| |
| from validation_error import ValidationError |
| |
| ADB_ROOT_TIMED_OUT_LIMIT_SECS = 5 |
| ADB_BOOT_COMPLETED_TIMED_OUT_LIMIT_SECS = 30 |
| POLLING_INTERVAL_SECS = 0.5 |
| SIMPLEPERF_TRACE_FILE = "/data/misc/perfetto-traces/perf.data" |
| |
| class AdbDevice: |
| """ |
| Class representing a device. APIs interact with the current device through |
| the adb bridge. |
| """ |
| def __init__(self, serial): |
| self.serial = serial |
| |
| @staticmethod |
| def get_adb_devices(): |
| """ |
| Returns a list of devices connected to the adb bridge. |
| The output of the command 'adb devices' is expected to be of the form: |
| List of devices attached |
| SOMEDEVICE1234 device |
| device2:5678 device |
| """ |
| command_output = subprocess.run(["adb", "devices"], capture_output=True) |
| output_lines = command_output.stdout.decode("utf-8").split("\n") |
| devices = [] |
| for line in output_lines[:-2]: |
| if line[0] == "*" or line == "List of devices attached": |
| continue |
| words_in_line = line.split('\t') |
| if words_in_line[1] == "device": |
| devices.append(words_in_line[0]) |
| return devices |
| |
| def check_device_connection(self): |
| devices = self.get_adb_devices() |
| if len(devices) == 0: |
| return ValidationError("There are currently no devices connected.", None) |
| if self.serial is not None: |
| if self.serial not in devices: |
| return ValidationError(("Device with serial %s is not connected." |
| % self.serial), None) |
| elif "ANDROID_SERIAL" in os.environ: |
| if os.environ["ANDROID_SERIAL"] not in devices: |
| return ValidationError(("Device with serial %s is set as environment" |
| " variable, ANDROID_SERIAL, but is not" |
| " connected." |
| % os.environ["ANDROID_SERIAL"]), None) |
| self.serial = os.environ["ANDROID_SERIAL"] |
| elif len(devices) == 1: |
| self.serial = devices[0] |
| else: |
| return ValidationError(("There is more than one device currently" |
| " connected."), |
| ("Run one of the following commands to choose one" |
| " of the connected devices:\n\t torq --serial %s" |
| % "\n\t torq --serial ".join(devices))) |
| return None |
| |
| @staticmethod |
| def poll_is_task_completed(timed_out_limit, interval, check_is_completed): |
| start_time = time.time() |
| while True: |
| time.sleep(interval) |
| if check_is_completed(): |
| return True |
| if time.time() - start_time > timed_out_limit: |
| return False |
| |
| def root_device(self): |
| subprocess.run(["adb", "-s", self.serial, "root"]) |
| if not self.poll_is_task_completed(ADB_ROOT_TIMED_OUT_LIMIT_SECS, |
| POLLING_INTERVAL_SECS, |
| lambda: self.serial in |
| self.get_adb_devices()): |
| raise Exception(("Device with serial %s took too long to reconnect after" |
| " being rooted." % self.serial)) |
| |
| def remove_file(self, file_path): |
| subprocess.run(["adb", "-s", self.serial, "shell", "rm", "-f", file_path]) |
| |
| def start_perfetto_trace(self, config): |
| return subprocess.Popen(("adb -s %s shell perfetto -c - --txt -o" |
| " /data/misc/perfetto-traces/" |
| "trace.perfetto-trace %s" |
| % (self.serial, config)), shell=True) |
| |
| def start_simpleperf_trace(self, command): |
| events_param = "-e " + ",".join(command.simpleperf_event) |
| return subprocess.Popen(("adb -s %s shell simpleperf record -a -f 1000 " |
| "--post-unwind=yes -m 8192 -g --duration %d" |
| " %s -o %s" |
| % (self.serial, |
| int(math.ceil(command.dur_ms/1000)), |
| events_param, SIMPLEPERF_TRACE_FILE)), |
| shell=True) |
| |
| def pull_file(self, file_path, host_file): |
| subprocess.run(["adb", "-s", self.serial, "pull", file_path, host_file]) |
| |
| def get_all_users(self): |
| command_output = subprocess.run(["adb", "-s", self.serial, "shell", "pm", |
| "list", "users"], capture_output=True) |
| output_lines = command_output.stdout.decode("utf-8").split("\n")[1:-1] |
| return [int((line.split("{", 1)[1]).split(":", 1)[0]) for line in |
| output_lines] |
| |
| def user_exists(self, user): |
| users = self.get_all_users() |
| if user not in users: |
| return ValidationError(("User ID %s does not exist on device with serial" |
| " %s." % (user, self.serial)), |
| ("Select from one of the following user IDs on" |
| " device with serial %s: %s" |
| % (self.serial, ", ".join(map(str, users))))) |
| return None |
| |
| def get_current_user(self): |
| command_output = subprocess.run(["adb", "-s", self.serial, "shell", "am", |
| "get-current-user"], capture_output=True) |
| return int(command_output.stdout.decode("utf-8").split()[0]) |
| |
| def perform_user_switch(self, user): |
| subprocess.run(["adb", "-s", self.serial, "shell", "am", "switch-user", |
| str(user)]) |
| |
| def write_to_file(self, file_path, host_file_string): |
| subprocess.run(("adb -s %s shell 'cat > %s %s'" |
| % (self.serial, file_path, host_file_string)), shell=True) |
| |
| def set_prop(self, prop, value): |
| subprocess.run(["adb", "-s", self.serial, "shell", "setprop", prop, value]) |
| |
| def reboot(self): |
| subprocess.run(["adb", "-s", self.serial, "reboot"]) |
| if not self.poll_is_task_completed(ADB_ROOT_TIMED_OUT_LIMIT_SECS, |
| POLLING_INTERVAL_SECS, |
| lambda: self.serial not in |
| self.get_adb_devices()): |
| raise Exception(("Device with serial %s took too long to start" |
| " rebooting." % self.serial)) |
| |
| def wait_for_device(self): |
| subprocess.run(["adb", "-s", self.serial, "wait-for-device"]) |
| |
| def is_boot_completed(self): |
| command_output = subprocess.run(["adb", "-s", self.serial, "shell", |
| "getprop", "sys.boot_completed"], |
| capture_output=True) |
| return command_output.stdout.decode("utf-8").strip() == "1" |
| |
| def wait_for_boot_to_complete(self): |
| if not self.poll_is_task_completed(ADB_BOOT_COMPLETED_TIMED_OUT_LIMIT_SECS, |
| POLLING_INTERVAL_SECS, |
| self.is_boot_completed): |
| raise Exception(("Device with serial %s took too long to finish" |
| " rebooting." % self.serial)) |
| |
| def get_packages(self): |
| return [package.removeprefix("package:") for package in subprocess.run( |
| ["adb", "-s", self.serial, "shell", "pm", "list", "packages"], |
| capture_output=True).stdout.decode("utf-8").splitlines()] |
| |
| def get_pid(self, package): |
| return subprocess.run("adb -s %s shell pidof %s" % (self.serial, package), |
| shell=True, capture_output=True |
| ).stdout.decode("utf-8").split("\n")[0] |
| |
| def is_package_running(self, package): |
| return self.get_pid(package) != "" |
| |
| def start_package(self, package): |
| if subprocess.run( |
| ["adb", "-s", self.serial, "shell", "am", "start", package], |
| capture_output=True).stderr.decode("utf-8").split("\n")[0] != "": |
| return ValidationError(("Cannot start package %s on device with" |
| " serial %s because %s is a service package," |
| " which doesn't implement a MAIN activity." |
| % (package, self.serial, package)), None) |
| return None |
| |
| def kill_pid(self, package): |
| pid = self.get_pid(package) |
| if pid != "": |
| subprocess.run(["adb", "-s", self.serial, "shell", "kill", "-9", pid]) |
| |
| def force_stop_package(self, package): |
| subprocess.run(["adb", "-s", self.serial, "shell", "am", "force-stop", |
| package]) |
| |
| def get_prop(self, prop): |
| return subprocess.run( |
| ["adb", "-s", self.serial, "shell", "getprop", prop], |
| capture_output=True).stdout.decode("utf-8").split("\n")[0] |
| |
| def get_android_sdk_version(self): |
| return int(self.get_prop("ro.build.version.sdk")) |
| |
| def simpleperf_event_exists(self, simpleperf_events): |
| events_copy = simpleperf_events.copy() |
| grep_command = "grep" |
| for event in simpleperf_events: |
| grep_command += " -e " + event.lower() |
| |
| output = subprocess.run(["adb", "-s", self.serial, "shell", |
| "simpleperf", "list", "|", grep_command], |
| capture_output=True) |
| |
| if output is None or len(output.stdout) == 0: |
| raise Exception("Error while validating simpleperf events.") |
| lines = output.stdout.decode("utf-8").split("\n") |
| |
| # Anything that does not start with two spaces is not a command. |
| # Any command with a space will have the command before the first space. |
| for line in lines: |
| if len(line) <= 3 or line[:2] != " " or line[2] == "#": |
| # Line doesn't contain a simpleperf event |
| continue |
| event = line[2:].split(" ")[0] |
| if event in events_copy: |
| events_copy.remove(event) |
| if len(events_copy) == 0: |
| # All of the events exist, exit early |
| break |
| |
| if len(events_copy) > 0: |
| return ValidationError("The following simpleperf event(s) are invalid:" |
| " %s." |
| % events_copy, |
| "Run adb shell simpleperf list to" |
| " see valid simpleperf events.") |
| return None |