diff options
author | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2022-03-02 00:08:25 +0000 |
---|---|---|
committer | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2022-03-02 00:08:25 +0000 |
commit | edaa34263b850e15c4b844ebea33b65528b7f62f (patch) | |
tree | 35483a3e16fa3dc5171d328d853191ecb59b0e1d | |
parent | 67449b327db1b2609d70c1c8688217cc5d433ab4 (diff) | |
parent | dcf4a961732046efca31b5480ece3af86fde1978 (diff) | |
download | platform_testing-edaa34263b850e15c4b844ebea33b65528b7f62f.tar.gz |
Snap for 8237733 from dcf4a961732046efca31b5480ece3af86fde1978 to sc-qpr3-release
Change-Id: I244cc5d6008c8e0af32a6990888b3f478286f8a5
6 files changed, 347 insertions, 12 deletions
diff --git a/libraries/sts-common-util/host-side/Android.bp b/libraries/sts-common-util/host-side/Android.bp index 49f51f4d7..f151cd374 100644 --- a/libraries/sts-common-util/host-side/Android.bp +++ b/libraries/sts-common-util/host-side/Android.bp @@ -28,6 +28,7 @@ java_library_host { libs: [ "compatibility-tradefed", + "guava", "tradefed", ], } diff --git a/libraries/sts-common-util/host-side/rootcanal/Android.bp b/libraries/sts-common-util/host-side/rootcanal/Android.bp new file mode 100644 index 000000000..a5b7495d0 --- /dev/null +++ b/libraries/sts-common-util/host-side/rootcanal/Android.bp @@ -0,0 +1,11 @@ +sh_test { + name: "sts-rootcanal-sidebins", + src: "empty.sh", + test_suites: ["sts"], + data_bins: [ + "android.hardware.bluetooth@1.1-service.sim", + "android.hardware.bluetooth@1.1-impl-sim" + ], + data: ["android.hardware.bluetooth@1.1-service.sim.rc"], +} + diff --git a/libraries/sts-common-util/host-side/rootcanal/android.hardware.bluetooth@1.1-service.sim.rc b/libraries/sts-common-util/host-side/rootcanal/android.hardware.bluetooth@1.1-service.sim.rc new file mode 100644 index 000000000..262684136 --- /dev/null +++ b/libraries/sts-common-util/host-side/rootcanal/android.hardware.bluetooth@1.1-service.sim.rc @@ -0,0 +1,4 @@ +service vendor.bluetooth-1-1 /vendor/bin/hw/android.hardware.bluetooth@1.1-service.sim + class hal + user bluetooth + group bluetooth diff --git a/libraries/sts-common-util/host-side/rootcanal/empty.sh b/libraries/sts-common-util/host-side/rootcanal/empty.sh new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/libraries/sts-common-util/host-side/rootcanal/empty.sh diff --git a/libraries/sts-common-util/host-side/src/com/android/sts/common/OverlayFsUtils.java b/libraries/sts-common-util/host-side/src/com/android/sts/common/OverlayFsUtils.java index 816420f49..a21ce577b 100644 --- a/libraries/sts-common-util/host-side/src/com/android/sts/common/OverlayFsUtils.java +++ b/libraries/sts-common-util/host-side/src/com/android/sts/common/OverlayFsUtils.java @@ -17,38 +17,68 @@ package com.android.sts.common; import static org.junit.Assert.assertTrue; - -import com.android.tradefed.device.DeviceNotAvailableException; -import com.android.tradefed.device.ITestDevice; +import static org.junit.Assert.assertNotNull; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.nio.file.Paths; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.junit.rules.TestWatcher; +import org.junit.runner.Description; + +import com.android.tradefed.device.DeviceNotAvailableException; +import com.android.tradefed.device.ITestDevice; +import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test; +import com.android.tradefed.util.CommandResult; +import com.android.tradefed.util.CommandStatus; +import com.google.common.hash.Hashing; + +/** TestWatcher that enables writing to read-only partitions and reboots device when done. */ +public class OverlayFsUtils extends TestWatcher { + private static final String OVERLAYFS_PREFIX = "overlay_sts_"; + + private final BaseHostJUnit4Test test; -public class OverlayFsUtils { // output of `stat`, e.g. "root shell 755 u:object_r:vendor_file:s0" static final Pattern PERM_PATTERN = Pattern.compile( "^(?<user>[a-zA-Z0-9_-]+) (?<group>[a-zA-Z0-9_-]+) (?<perm>[0-7]+)" + " (?<secontext>.*)$"); + public OverlayFsUtils(BaseHostJUnit4Test test) { + assertNotNull("Need to pass in a valid testcase object.", test); + this.test = test; + } + /** * Mounts an OverlayFS dir over the top most common dir in the list. * * <p>The directory should be writable after this returns successfully. To cleanup, reboot the * device as unfortunately unmounting overlayfs is complicated. * - * @param device The test device to setup overlayfs for. * @param dir The directory to make writable. Directories with single quotes are not supported. */ - public static void makeWritable(ITestDevice device, String dir) - throws DeviceNotAvailableException, IOException { - // TODO(duytruong): This should ideally be made into a TestRule that also handles cleanups - // However, test devices initiation is done in one of the @Before, after a rule's setup. + public void makeWritable(final String dir) + throws DeviceNotAvailableException, IOException, IllegalStateException { + ITestDevice device = test.getDevice(); + assertNotNull("device not set.", device); + assertTrue("dir needs to be an absolute path.", dir.startsWith("/")); + + // Check and make sure we have not already mounted over this dir. We do that by hashing + // the lower dir path and put that as part of the device ID for `mount`. + String dirHash = Hashing.md5().hashString(dir, StandardCharsets.UTF_8).toString(); + String id = OVERLAYFS_PREFIX + dirHash; + CommandResult res = device.executeShellV2Command("mount | grep -q " + id); + if (res.getStatus() == CommandStatus.SUCCESS) { + // a mount with the same ID already exists + throw new IllegalStateException(dir + " has already been made writable."); + } + assertTrue("Can't acquire root for " + device.getSerialNumber(), device.enableAdbRoot()); + // Match permissions of upper dir to lower dir String statOut = CommandUtil.runAndCheck(device, "stat -c '%U %G %a %C' '" + dir + "'").getStdout(); Matcher m = PERM_PATTERN.matcher(statOut); @@ -58,7 +88,7 @@ public class OverlayFsUtils { String unixPerm = m.group("perm"); String seContext = m.group("secontext"); - Path tempdir = Paths.get("/mnt", "stsoverlayfs", dir); + Path tempdir = Paths.get("/mnt", "stsoverlayfs", id); String upperdir = tempdir.resolve("upper").toString(); String workdir = tempdir.resolve("workdir").toString(); @@ -69,8 +99,29 @@ public class OverlayFsUtils { String mountCmd = String.format( - "mount -t overlay overlay -o lowerdir='%s',upperdir='%s',workdir='%s' '%s'", - dir, upperdir, workdir, dir); + "mount -t overlay '%s' -o lowerdir='%s',upperdir='%s',workdir='%s' '%s'", + id, dir, upperdir, workdir, dir); CommandUtil.runAndCheck(device, mountCmd); } + + public boolean anyOverlayFsMounted() throws DeviceNotAvailableException { + ITestDevice device = test.getDevice(); + assertNotNull("Device not set", device); + CommandResult res = device.executeShellV2Command("mount | grep -q " + OVERLAYFS_PREFIX); + return res.getStatus() == CommandStatus.SUCCESS; + } + + @Override + public void finished(Description d) { + ITestDevice device = test.getDevice(); + assertNotNull("Device not set", device); + try { + if (anyOverlayFsMounted()) { + device.rebootUntilOnline(); + device.waitForDeviceAvailable(); + } + } catch (DeviceNotAvailableException e) { + throw new AssertionError("Device unavailable when cleaning up", e); + } + } } diff --git a/libraries/sts-common-util/host-side/src/com/android/sts/common/RootcanalUtils.java b/libraries/sts-common-util/host-side/src/com/android/sts/common/RootcanalUtils.java new file mode 100644 index 000000000..d3d23c30c --- /dev/null +++ b/libraries/sts-common-util/host-side/src/com/android/sts/common/RootcanalUtils.java @@ -0,0 +1,268 @@ +/* + * 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. + */ + +package com.android.sts.common; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertFalse; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.StringReader; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.TimeUnit; +import java.util.List; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathFactory; +import javax.xml.xpath.XPathExpressionException; +import org.junit.rules.TestWatcher; +import org.junit.runner.Description; +import org.junit.rules.TestWatcher; +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper; +import com.android.tradefed.device.DeviceNotAvailableException; +import com.android.tradefed.device.ITestDevice; +import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test; +import com.android.tradefed.log.LogUtil.CLog; +import com.android.tradefed.util.CommandResult; +import com.android.tradefed.util.CommandStatus; +import com.android.tradefed.util.RunUtil; + +/** TestWatcher that sets up a virtual bluetooth HAL and reboots the device once done. */ +public class RootcanalUtils extends TestWatcher { + private static final String LOCK_FILENAME = "/mnt/sts_rootcanal.lck"; + + private BaseHostJUnit4Test test; + + public RootcanalUtils(BaseHostJUnit4Test test) { + assertNotNull(test); + this.test = test; + } + + @Override + public void finished(Description d) { + ITestDevice device = test.getDevice(); + assertNotNull("Device not set", device); + try { + // No need to call OverlayFsUtils' finished() because we're restarting anyway + device.rebootUntilOnline(); + device.waitForDeviceAvailable(); + } catch (DeviceNotAvailableException e) { + throw new AssertionError("Device unavailable when cleaning up", e); + } + } + + /** Replace existing HAL with RootCanal HAL on current device. */ + public void enableRootcanal() + throws DeviceNotAvailableException, IOException, InterruptedException, + TimeoutException { + enableRootcanal(6111); + } + + /** + * Replace existing HAL with RootCanal HAL on current device. + * + * @param port host TCP port to adb-forward to rootcanal control port. + */ + public void enableRootcanal(int port) + throws DeviceNotAvailableException, IOException, InterruptedException, + TimeoutException { + ITestDevice device = test.getDevice(); + CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(test.getBuild()); + assertNotNull("Device not set", device); + assertNotNull("Build not set", buildHelper); + + // Check and made sure we're not calling this more than once for a device + assertFalse("rootcanal set up called more than once", device.doesFileExist(LOCK_FILENAME)); + device.pushString("", LOCK_FILENAME); + + // Make sure that /vendor is writable + try { + new OverlayFsUtils(test).makeWritable("/vendor"); + } catch (IllegalStateException e) { + CLog.w(e); + } + + // Remove existing HAL files and push new virtual HAL files. + CommandUtil.runAndCheck(device, "svc bluetooth disable"); + CommandUtil.runAndCheck( + device, + "rm /vendor/lib64/hw/android.hardware.bluetooth@* " + + "/vendor/lib/hw/android.hardware.bluetooth@* " + + "/vendor/bin/hw/android.hardware.bluetooth@* " + + "/vendor/etc/init/android.hardware.bluetooth@*"); + + device.pushFile( + buildHelper.getTestFile("android.hardware.bluetooth@1.1-service.sim"), + "/vendor/bin/hw/android.hardware.bluetooth@1.1-service.sim"); + + // Pushing the same lib to both 32 and 64bit lib dirs because (a) it works and + // (b) FileUtil does not yet support "arm/lib" and "arm64/lib64" layout. + device.pushFile( + buildHelper.getTestFile("android.hardware.bluetooth@1.1-impl-sim.so"), + "/vendor/lib/hw/android.hardware.bluetooth@1.1-impl-sim.so"); + device.pushFile( + buildHelper.getTestFile("android.hardware.bluetooth@1.1-impl-sim.so"), + "/vendor/lib64/hw/android.hardware.bluetooth@1.1-impl-sim.so"); + device.pushFile( + buildHelper.getTestFile("android.hardware.bluetooth@1.1-service.sim.rc"), + "/vendor/etc/init/android.hardware.bluetooth@1.1-service.sim.rc"); + + // Download and patch the VINTF manifest if needed. + tryUpdateVintfManifest(device); + + // Fix up permissions and SELinux contexts of files pushed over + CommandUtil.runAndCheck(device, "cp /system/lib64/libchrome.so /vendor/lib64/libchrome.so"); + CommandUtil.runAndCheck( + device, "chmod 755 /vendor/bin/hw/android.hardware.bluetooth@1.1-service.sim"); + CommandUtil.runAndCheck( + device, + "chcon u:object_r:hal_bluetooth_default_exec:s0 " + + "/vendor/bin/hw/android.hardware.bluetooth@1.1-service.sim"); + CommandUtil.runAndCheck( + device, + "chmod 644 " + + "/vendor/etc/vintf/manifest.xml " + + "/vendor/lib/hw/android.hardware.bluetooth@1.1-impl-sim.so " + + "/vendor/lib64/hw/android.hardware.bluetooth@1.1-impl-sim.so"); + CommandUtil.runAndCheck( + device, "chcon u:object_r:vendor_configs_file:s0 /vendor/etc/vintf/manifest.xml"); + CommandUtil.runAndCheck( + device, + "chcon u:object_r:vendor_file:s0 " + + "/vendor/lib/hw/android.hardware.bluetooth@1.1-impl-sim.so " + + "/vendor/lib64/hw/android.hardware.bluetooth@1.1-impl-sim.so"); + + // Kill currently running BT HAL. + if (ProcessUtil.killAll(device, "android\\.hardware\\.bluetooth@.*", 10_000, false)) { + CLog.d("Killed existing BT HAL"); + } else { + CLog.w("No existing BT HAL was found running"); + } + + // Kill hwservicemanager, wait for it to come back up on its own, and wait for it + // to finish initializing. This is needed to reload the VINTF and HAL rc information. + // Note that a userspace reboot would not work here because hwservicemanager starts + // before userdata is mounted. + device.setProperty("hwservicemanager.ready", "false"); + ProcessUtil.killAll(device, "hwservicemanager$", 10_000); + waitPropertyValue(device, "hwservicemanager.ready", "true", 10_000); + TimeUnit.SECONDS.sleep(30); + + // Launch the new HAL + List<String> cmd = + List.of( + "adb", + "-s", + device.getSerialNumber(), + "shell", + "/vendor/bin/hw/android.hardware.bluetooth@1.1-service.sim"); + RunUtil.getDefault().runCmdInBackground(cmd); + ProcessUtil.waitProcessRunning( + device, "android\\.hardware\\.bluetooth@1\\.1-service\\.sim", 10_000); + + // Reenable Bluetooth and enable RootCanal control channel + String checkCmd = "netstat -l -t -n -W | grep '0\\.0\\.0\\.0:6111'"; + while (true) { + CommandUtil.runAndCheck(device, "svc bluetooth enable"); + CommandUtil.runAndCheck(device, "setprop vendor.bt.rootcanal_test_console true"); + CommandResult res = device.executeShellV2Command(checkCmd); + if (res.getStatus() == CommandStatus.SUCCESS) { + break; + } + } + + device.executeAdbCommand("forward", "tcp:6111", String.format("tcp:%d", port)); + } + + private void tryUpdateVintfManifest(ITestDevice device) + throws DeviceNotAvailableException, IOException { + try { + String vintfManifest = device.pullFileContents("/vendor/etc/vintf/manifest.xml"); + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + DocumentBuilder builder = factory.newDocumentBuilder(); + Document doc = builder.parse(new InputSource(new StringReader(vintfManifest))); + String XPATH = "/manifest/hal[name=\"android.hardware.bluetooth\"][version!=\"1.1\"]"; + Node node = + (Node) + XPathFactory.newInstance() + .newXPath() + .evaluate(XPATH, doc, XPathConstants.NODE); + if (node != null) { + Node versionNode = + (Node) + XPathFactory.newInstance() + .newXPath() + .evaluate("version", node, XPathConstants.NODE); + versionNode.setTextContent("1.1"); + + Node fqnameNode = + (Node) + XPathFactory.newInstance() + .newXPath() + .evaluate("fqname", node, XPathConstants.NODE); + String newFqname = + fqnameNode.getTextContent().replaceAll("@[0-9]+\\.[0-9]+(::.*)", "@1.1$1"); + fqnameNode.setTextContent(newFqname); + + File outFile = File.createTempFile("stsrootcanal", null); + outFile.deleteOnExit(); + + Transformer transformer = TransformerFactory.newInstance().newTransformer(); + DOMSource source = new DOMSource(doc); + StreamResult result = new StreamResult(new FileWriter(outFile)); + transformer.transform(source, result); + device.pushFile(outFile, "/vendor/etc/vintf/manifest.xml"); + CLog.d("Updated VINTF manifest"); + } else { + CLog.d("Not updating VINTF manifest"); + } + } catch (ParserConfigurationException + | SAXException + | XPathExpressionException + | TransformerException e) { + CLog.e("Could not parse vintf manifest: %s", e); + } + } + + /** Spin wait until given property has given value. */ + private void waitPropertyValue(ITestDevice device, String name, String value, long timeoutMs) + throws TimeoutException, DeviceNotAvailableException, InterruptedException { + long endTime = System.currentTimeMillis() + timeoutMs; + while (true) { + if (value.equals(device.getProperty(name))) { + return; + } + if (System.currentTimeMillis() > endTime) { + throw new TimeoutException(); + } + TimeUnit.MILLISECONDS.sleep(250); + } + } +} |