blob: 1934eb46146fc5a5dc651a2467d38afffa40e7a5 [file] [log] [blame]
/*
* 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.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertFalse;
import static org.junit.Assume.assumeNoException;
import static org.junit.Assume.assumeTrue;
import static com.android.sts.common.CommandUtil.runAndCheck;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileWriter;
import java.io.InputStream;
import java.io.IOException;
import java.io.StringReader;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.Arrays;
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 = "/data/local/tmp/sts_rootcanal.lck";
private BaseHostJUnit4Test test;
private OverlayFsUtils overlayFsUtils;
public RootcanalUtils(BaseHostJUnit4Test test) {
assertNotNull(test);
this.test = test;
this.overlayFsUtils = new OverlayFsUtils(test);
}
@Override
public void finished(Description d) {
ITestDevice device = test.getDevice();
assertNotNull("Device not set", device);
try {
device.enableAdbRoot();
ProcessUtil.killAll(
device, "android\\.hardware\\.bluetooth@1\\.1-service\\.sim", 10_000, false);
runAndCheck(device, String.format("rm -rf '%s'", LOCK_FILENAME));
device.disableAdbRoot();
// OverlayFsUtils' finished() will restart the device.
overlayFsUtils.finished(d);
device.waitForDeviceAvailable();
CommandResult res = device.executeShellV2Command("svc bluetooth enable");
if (res.getStatus() != CommandStatus.SUCCESS) {
CLog.e("Could not reenable Bluetooth during cleanup!");
}
} catch (DeviceNotAvailableException e) {
throw new AssertionError("Device unavailable when cleaning up", e);
} catch (TimeoutException e) {
CLog.w("Could not kill rootcanal HAL during cleanup");
}
}
/**
* Replace existing HAL with RootCanal HAL on current device.
*
* @return an instance of RootcanalController
*/
public RootcanalController enableRootcanal()
throws DeviceNotAvailableException, IOException, InterruptedException {
ITestDevice device = test.getDevice();
assertNotNull("Device not set", device);
assumeTrue(
"Device does not seem to have Bluetooth",
device.hasFeature("android.hardware.bluetooth")
|| device.hasFeature("android.hardware.bluetooth_le"));
CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(test.getBuild());
// 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 {
overlayFsUtils.makeWritable("/vendor", 100);
} catch (IllegalStateException e) {
CLog.w(e);
}
// Remove existing HAL files and push new virtual HAL files.
runAndCheck(device, "svc bluetooth disable");
runAndCheck(
device,
"rm -f /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);
// Rootcanal expects certain libraries to be in /vendor and not /system so copy them over
copySystemLibToVendorIfMissing("libchrome.so");
copySystemLibToVendorIfMissing("android.hardware.bluetooth@1.1.so");
copySystemLibToVendorIfMissing("android.hardware.bluetooth@1.0.so");
// Fix up permissions and SELinux contexts of files pushed over
runAndCheck(device, "chmod 755 /vendor/bin/hw/android.hardware.bluetooth@1.1-service.sim");
runAndCheck(
device,
"chcon u:object_r:hal_bluetooth_default_exec:s0 "
+ "/vendor/bin/hw/android.hardware.bluetooth@1.1-service.sim");
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");
runAndCheck(
device, "chcon u:object_r:vendor_configs_file:s0 /vendor/etc/vintf/manifest.xml");
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");
try {
// 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);
} catch (TimeoutException e) {
assumeNoException("Could not start virtual BT HAL", e);
}
// Reenable Bluetooth and enable RootCanal control channel
String checkCmd = "netstat -l -t -n -W | grep '0\\.0\\.0\\.0:6111'";
while (true) {
runAndCheck(device, "svc bluetooth enable");
runAndCheck(device, "setprop vendor.bt.rootcanal_test_console true");
CommandResult res = device.executeShellV2Command(checkCmd);
if (res.getStatus() == CommandStatus.SUCCESS) {
break;
}
}
// Forward root canal control ports on the device to the host
int testPort = findOpenPort();
device.executeAdbCommand("forward", String.format("tcp:%d", testPort), "tcp:6111");
int hciPort = findOpenPort();
device.executeAdbCommand("forward", String.format("tcp:%d", hciPort), "tcp:6211");
return new RootcanalController(testPort, hciPort);
}
private void copySystemLibToVendorIfMissing(String filename)
throws DeviceNotAvailableException {
runAndCheck(
test.getDevice(),
String.format(
"(test -f /vendor/lib64/%1$s || cp /system/lib64/%1$s /vendor/lib64/%1$s)"
+ " || (test -f /vendor/lib/%1$s || cp /system/lib/%1$s"
+ " /vendor/lib/%1$s)",
filename));
}
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);
}
}
/** Find an open TCP port on the host */
private static int findOpenPort() throws IOException {
try (ServerSocket socket = new ServerSocket(0)) {
socket.setReuseAddress(true);
return socket.getLocalPort();
}
}
/** Class that encapsulates a virtual HCI device that can be controlled by HCI commands. */
public static class HciDevice implements AutoCloseable {
private static final String READ_FAIL_MSG = "Failed to read HCI packet";
private final Socket hciSocket;
private HciDevice(Socket hciSocket) {
this.hciSocket = hciSocket;
}
@Override
public void close() throws IOException {
hciSocket.close();
}
/**
* Convenient wrapper around sendHciPacket to send a HCI command packet to device.
*
* @param ogf Opcode group field
* @param ocf Opcode command field
* @param params the rest of the command parameters
*/
public void sendHciCmd(int ogf, int ocf, byte[] params) throws IOException {
assertTrue("params length must be less than 256 bytes", params.length < 256);
ByteBuffer cmd = ByteBuffer.allocate(4 + params.length).order(ByteOrder.LITTLE_ENDIAN);
int opcode = (ogf << 10) | ocf;
cmd.put((byte) 0x01).putShort((short) opcode).put((byte) params.length).put(params);
sendHciPacket(cmd.array());
}
/**
* Send raw HCI packet to device.
*
* @param packet raw packet data to send to device
*/
public void sendHciPacket(byte[] packet) throws IOException {
CLog.d("sending HCI: %s", Arrays.toString(packet));
hciSocket.getOutputStream().write(packet);
}
/** Read one HCI packet from device, blocking until data is available. */
public byte[] readHciPacket() throws IOException {
ByteArrayOutputStream ret = new ByteArrayOutputStream();
InputStream in = hciSocket.getInputStream();
// Read the packet type
byte[] typeBuf = new byte[1];
assertEquals(READ_FAIL_MSG, 1, in.read(typeBuf, 0, 1));
ret.write(typeBuf);
// Read the header and figure out how much data to read
// according to BT core spec 5.2 vol 4 part A section 2 & part E section 5.4
byte[] hdrBuf = new byte[4];
int dataLength;
switch (typeBuf[0]) {
case 0x01: // Command packet
case 0x03: // Synch data packet
assertEquals(READ_FAIL_MSG, 3, in.read(hdrBuf, 0, 3));
ret.write(hdrBuf, 0, 3);
dataLength = hdrBuf[2];
break;
case 0x02: // Async data packet
assertEquals(READ_FAIL_MSG, 4, in.read(hdrBuf, 0, 4));
ret.write(hdrBuf, 0, 4);
dataLength = (((int) hdrBuf[2]) & 0xFF) | ((((int) hdrBuf[3]) & 0xFF) << 8);
break;
case 0x04: // Event
assertEquals(READ_FAIL_MSG, 2, in.read(hdrBuf, 0, 2));
ret.write(hdrBuf, 0, 2);
dataLength = hdrBuf[1];
break;
case 0x05: // ISO synch data packet
assertEquals(READ_FAIL_MSG, 4, in.read(hdrBuf, 0, 4));
ret.write(hdrBuf, 0, 4);
dataLength = (((int) hdrBuf[2]) & 0xFF) | ((((int) hdrBuf[3]) & 0xFC) << 6);
break;
default:
throw new IOException("Unexpected packet type: " + String.valueOf(typeBuf[0]));
}
// Read the data payload
byte[] data = new byte[dataLength];
assertEquals(READ_FAIL_MSG, dataLength, in.read(data, 0, dataLength));
ret.write(data, 0, dataLength);
return ret.toByteArray();
}
}
public static class RootcanalController implements AutoCloseable {
private final int testPort;
private final int hciPort;
private Socket rootcanalTestChannel = null;
private List<HciDevice> hciDevices = new ArrayList<>();
private RootcanalController(int testPort, int hciPort)
throws IOException, InterruptedException {
this.testPort = testPort;
this.hciPort = hciPort;
CLog.d("Rootcanal controller initialized; testPort=%d, hciPort=%d", testPort, hciPort);
}
@Override
public void close() throws IOException {
rootcanalTestChannel.close();
for (HciDevice dev : hciDevices) {
dev.close();
}
}
/**
* Create a new HCI device by connecting to rootcanal's HCI socket.
*
* @return HciDevice object that allows sending/receiving from the HCI port
*/
public HciDevice createHciDevice()
throws DeviceNotAvailableException, IOException, InterruptedException {
HciDevice dev = new HciDevice(new Socket("localhost", hciPort));
hciDevices.add(dev);
return dev;
}
/**
* Send one command to rootcanal test channel.
*
* <p>Send `help` command for list of accepted commands from Rootcanal.
*
* @param cmd command to send
* @param args arguments for the command
* @return Response string from rootcanal
*/
public String sendTestChannelCommand(String cmd, String... args)
throws IOException, InterruptedException {
if (rootcanalTestChannel == null) {
rootcanalTestChannel = new Socket("localhost", testPort);
CLog.d(
"RootCanal test channel init: "
+ readTestChannel(rootcanalTestChannel.getInputStream()));
}
// Translated from system/bt/vendor_libs/test_vendor_lib/scripts/test_channel.py
ByteArrayOutputStream msg = new ByteArrayOutputStream();
msg.write(cmd.length());
msg.write(cmd.getBytes("ASCII"));
msg.write(args.length);
for (String arg : args) {
msg.write(arg.length());
msg.write(arg.getBytes("ASCII"));
}
rootcanalTestChannel.getOutputStream().write(msg.toByteArray());
return readTestChannel(rootcanalTestChannel.getInputStream());
}
/** Read one message from rootcanal test channel. */
private String readTestChannel(InputStream in) throws IOException {
// Translated from system/bt/vendor_libs/test_vendor_lib/scripts/test_channel.py
ByteBuffer sizeBuf = ByteBuffer.allocate(Integer.BYTES).order(ByteOrder.LITTLE_ENDIAN);
in.read(sizeBuf.array(), 0, Integer.BYTES);
int size = sizeBuf.getInt();
byte[] buf = new byte[size];
in.read(buf, 0, size);
return new String(buf);
}
}
}