1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.sts.common; 18 19 import static org.junit.Assert.assertEquals; 20 import static org.junit.Assert.assertNotNull; 21 import static org.junit.Assert.assertTrue; 22 import static org.junit.Assert.assertFalse; 23 import static org.junit.Assume.assumeNoException; 24 import static org.junit.Assume.assumeTrue; 25 import static com.android.sts.common.CommandUtil.runAndCheck; 26 27 import java.io.ByteArrayOutputStream; 28 import java.io.File; 29 import java.io.FileWriter; 30 import java.io.InputStream; 31 import java.io.IOException; 32 import java.io.StringReader; 33 import java.net.ServerSocket; 34 import java.net.Socket; 35 import java.nio.ByteBuffer; 36 import java.nio.ByteOrder; 37 import java.util.ArrayList; 38 import java.util.Arrays; 39 import java.util.concurrent.TimeoutException; 40 import java.util.concurrent.TimeUnit; 41 import java.util.List; 42 import javax.xml.parsers.DocumentBuilder; 43 import javax.xml.parsers.DocumentBuilderFactory; 44 import javax.xml.parsers.ParserConfigurationException; 45 import javax.xml.transform.dom.DOMSource; 46 import javax.xml.transform.stream.StreamResult; 47 import javax.xml.transform.TransformerFactory; 48 import javax.xml.transform.Transformer; 49 import javax.xml.transform.TransformerException; 50 import javax.xml.xpath.XPathConstants; 51 import javax.xml.xpath.XPathFactory; 52 import javax.xml.xpath.XPathExpressionException; 53 import org.junit.rules.TestWatcher; 54 import org.junit.runner.Description; 55 import org.junit.rules.TestWatcher; 56 import org.w3c.dom.Document; 57 import org.w3c.dom.Node; 58 import org.xml.sax.InputSource; 59 import org.xml.sax.SAXException; 60 61 import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper; 62 import com.android.tradefed.device.DeviceNotAvailableException; 63 import com.android.tradefed.device.ITestDevice; 64 import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test; 65 import com.android.tradefed.log.LogUtil.CLog; 66 import com.android.tradefed.util.CommandResult; 67 import com.android.tradefed.util.CommandStatus; 68 import com.android.tradefed.util.RunUtil; 69 70 /** TestWatcher that sets up a virtual bluetooth HAL and reboots the device once done. */ 71 public class RootcanalUtils extends TestWatcher { 72 private static final String LOCK_FILENAME = "/data/local/tmp/sts_rootcanal.lck"; 73 74 private BaseHostJUnit4Test test; 75 private OverlayFsUtils overlayFsUtils; 76 RootcanalUtils(BaseHostJUnit4Test test)77 public RootcanalUtils(BaseHostJUnit4Test test) { 78 assertNotNull(test); 79 this.test = test; 80 this.overlayFsUtils = new OverlayFsUtils(test); 81 } 82 83 @Override finished(Description d)84 public void finished(Description d) { 85 ITestDevice device = test.getDevice(); 86 assertNotNull("Device not set", device); 87 try { 88 device.enableAdbRoot(); 89 ProcessUtil.killAll( 90 device, "android\\.hardware\\.bluetooth@1\\.1-service\\.sim", 10_000, false); 91 runAndCheck(device, String.format("rm -rf '%s'", LOCK_FILENAME)); 92 device.disableAdbRoot(); 93 // OverlayFsUtils' finished() will restart the device. 94 overlayFsUtils.finished(d); 95 device.waitForDeviceAvailable(); 96 CommandResult res = device.executeShellV2Command("svc bluetooth enable"); 97 if (res.getStatus() != CommandStatus.SUCCESS) { 98 CLog.e("Could not reenable Bluetooth during cleanup!"); 99 } 100 } catch (DeviceNotAvailableException e) { 101 throw new AssertionError("Device unavailable when cleaning up", e); 102 } catch (TimeoutException e) { 103 CLog.w("Could not kill rootcanal HAL during cleanup"); 104 } catch (ProcessUtil.KillException e) { 105 if (e.getReason() != ProcessUtil.KillException.Reason.NO_SUCH_PROCESS) { 106 CLog.w("Could not kill rootcanal HAL during cleanup: " + e.getMessage()); 107 } 108 } 109 } 110 111 /** 112 * Replace existing HAL with RootCanal HAL on current device. 113 * 114 * @return an instance of RootcanalController 115 */ enableRootcanal()116 public RootcanalController enableRootcanal() 117 throws DeviceNotAvailableException, IOException, InterruptedException { 118 ITestDevice device = test.getDevice(); 119 assertNotNull("Device not set", device); 120 assumeTrue( 121 "Device does not seem to have Bluetooth", 122 device.hasFeature("android.hardware.bluetooth") 123 || device.hasFeature("android.hardware.bluetooth_le")); 124 CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(test.getBuild()); 125 126 // Check and made sure we're not calling this more than once for a device 127 assertFalse("rootcanal set up called more than once", device.doesFileExist(LOCK_FILENAME)); 128 device.pushString("", LOCK_FILENAME); 129 130 // Make sure that /vendor is writable 131 try { 132 overlayFsUtils.makeWritable("/vendor", 100); 133 } catch (IllegalStateException e) { 134 CLog.w(e); 135 } 136 137 // Remove existing HAL files and push new virtual HAL files. 138 runAndCheck(device, "svc bluetooth disable"); 139 runAndCheck( 140 device, 141 "rm -f /vendor/lib64/hw/android.hardware.bluetooth@* " 142 + "/vendor/lib/hw/android.hardware.bluetooth@* " 143 + "/vendor/bin/hw/android.hardware.bluetooth@* " 144 + "/vendor/etc/init/android.hardware.bluetooth@*"); 145 146 device.pushFile( 147 buildHelper.getTestFile("android.hardware.bluetooth@1.1-service.sim"), 148 "/vendor/bin/hw/android.hardware.bluetooth@1.1-service.sim"); 149 150 // Pushing the same lib to both 32 and 64bit lib dirs because (a) it works and 151 // (b) FileUtil does not yet support "arm/lib" and "arm64/lib64" layout. 152 device.pushFile( 153 buildHelper.getTestFile("android.hardware.bluetooth@1.1-impl-sim.so"), 154 "/vendor/lib/hw/android.hardware.bluetooth@1.1-impl-sim.so"); 155 device.pushFile( 156 buildHelper.getTestFile("android.hardware.bluetooth@1.1-impl-sim.so"), 157 "/vendor/lib64/hw/android.hardware.bluetooth@1.1-impl-sim.so"); 158 device.pushFile( 159 buildHelper.getTestFile("android.hardware.bluetooth@1.1-service.sim.rc"), 160 "/vendor/etc/init/android.hardware.bluetooth@1.1-service.sim.rc"); 161 162 // Download and patch the VINTF manifest if needed. 163 tryUpdateVintfManifest(device); 164 165 // Rootcanal expects certain libraries to be in /vendor and not /system so copy them over 166 copySystemLibToVendorIfMissing("libchrome.so"); 167 copySystemLibToVendorIfMissing("android.hardware.bluetooth@1.1.so"); 168 copySystemLibToVendorIfMissing("android.hardware.bluetooth@1.0.so"); 169 170 // Fix up permissions and SELinux contexts of files pushed over 171 runAndCheck(device, "chmod 755 /vendor/bin/hw/android.hardware.bluetooth@1.1-service.sim"); 172 runAndCheck( 173 device, 174 "chcon u:object_r:hal_bluetooth_default_exec:s0 " 175 + "/vendor/bin/hw/android.hardware.bluetooth@1.1-service.sim"); 176 runAndCheck( 177 device, 178 "chmod 644 " 179 + "/vendor/etc/vintf/manifest.xml " 180 + "/vendor/lib/hw/android.hardware.bluetooth@1.1-impl-sim.so " 181 + "/vendor/lib64/hw/android.hardware.bluetooth@1.1-impl-sim.so"); 182 runAndCheck( 183 device, "chcon u:object_r:vendor_configs_file:s0 /vendor/etc/vintf/manifest.xml"); 184 runAndCheck( 185 device, 186 "chcon u:object_r:vendor_file:s0 " 187 + "/vendor/lib/hw/android.hardware.bluetooth@1.1-impl-sim.so " 188 + "/vendor/lib64/hw/android.hardware.bluetooth@1.1-impl-sim.so"); 189 190 try { 191 // Kill currently running BT HAL. 192 if (ProcessUtil.killAll(device, "android\\.hardware\\.bluetooth@.*", 10_000, false)) { 193 CLog.d("Killed existing BT HAL"); 194 } else { 195 CLog.w("No existing BT HAL was found running"); 196 } 197 198 // Kill hwservicemanager, wait for it to come back up on its own, and wait for it 199 // to finish initializing. This is needed to reload the VINTF and HAL rc information. 200 // Note that a userspace reboot would not work here because hwservicemanager starts 201 // before userdata is mounted. 202 device.setProperty("hwservicemanager.ready", "false"); 203 ProcessUtil.killAll(device, "hwservicemanager$", 10_000); 204 waitPropertyValue(device, "hwservicemanager.ready", "true", 10_000); 205 TimeUnit.SECONDS.sleep(30); 206 207 // Launch the new HAL 208 List<String> cmd = 209 List.of( 210 "adb", 211 "-s", 212 device.getSerialNumber(), 213 "shell", 214 "/vendor/bin/hw/android.hardware.bluetooth@1.1-service.sim"); 215 RunUtil.getDefault().runCmdInBackground(cmd); 216 ProcessUtil.waitProcessRunning( 217 device, "android\\.hardware\\.bluetooth@1\\.1-service\\.sim", 10_000); 218 } catch (TimeoutException e) { 219 assumeNoException("Could not start virtual BT HAL", e); 220 } catch (ProcessUtil.KillException e) { 221 assumeNoException("Failed to kill process", e); 222 } 223 224 // Reenable Bluetooth and enable RootCanal control channel 225 String checkCmd = "netstat -l -t -n -W | grep '0\\.0\\.0\\.0:6111'"; 226 while (true) { 227 runAndCheck(device, "svc bluetooth enable"); 228 runAndCheck(device, "setprop vendor.bt.rootcanal_test_console true"); 229 CommandResult res = device.executeShellV2Command(checkCmd); 230 if (res.getStatus() == CommandStatus.SUCCESS) { 231 break; 232 } 233 } 234 235 // Forward root canal control ports on the device to the host 236 int testPort = findOpenPort(); 237 device.executeAdbCommand("forward", String.format("tcp:%d", testPort), "tcp:6111"); 238 239 int hciPort = findOpenPort(); 240 device.executeAdbCommand("forward", String.format("tcp:%d", hciPort), "tcp:6211"); 241 242 return new RootcanalController(testPort, hciPort); 243 } 244 copySystemLibToVendorIfMissing(String filename)245 private void copySystemLibToVendorIfMissing(String filename) 246 throws DeviceNotAvailableException { 247 runAndCheck( 248 test.getDevice(), 249 String.format( 250 "(test -f /vendor/lib64/%1$s || cp /system/lib64/%1$s /vendor/lib64/%1$s)" 251 + " || (test -f /vendor/lib/%1$s || cp /system/lib/%1$s" 252 + " /vendor/lib/%1$s)", 253 filename)); 254 } 255 tryUpdateVintfManifest(ITestDevice device)256 private void tryUpdateVintfManifest(ITestDevice device) 257 throws DeviceNotAvailableException, IOException { 258 try { 259 String vintfManifest = device.pullFileContents("/vendor/etc/vintf/manifest.xml"); 260 DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); 261 DocumentBuilder builder = factory.newDocumentBuilder(); 262 Document doc = builder.parse(new InputSource(new StringReader(vintfManifest))); 263 String XPATH = "/manifest/hal[name=\"android.hardware.bluetooth\"][version!=\"1.1\"]"; 264 Node node = 265 (Node) 266 XPathFactory.newInstance() 267 .newXPath() 268 .evaluate(XPATH, doc, XPathConstants.NODE); 269 if (node != null) { 270 Node versionNode = 271 (Node) 272 XPathFactory.newInstance() 273 .newXPath() 274 .evaluate("version", node, XPathConstants.NODE); 275 versionNode.setTextContent("1.1"); 276 277 Node fqnameNode = 278 (Node) 279 XPathFactory.newInstance() 280 .newXPath() 281 .evaluate("fqname", node, XPathConstants.NODE); 282 String newFqname = 283 fqnameNode.getTextContent().replaceAll("@[0-9]+\\.[0-9]+(::.*)", "@1.1$1"); 284 fqnameNode.setTextContent(newFqname); 285 286 File outFile = File.createTempFile("stsrootcanal", null); 287 outFile.deleteOnExit(); 288 289 Transformer transformer = TransformerFactory.newInstance().newTransformer(); 290 DOMSource source = new DOMSource(doc); 291 StreamResult result = new StreamResult(new FileWriter(outFile)); 292 transformer.transform(source, result); 293 device.pushFile(outFile, "/vendor/etc/vintf/manifest.xml"); 294 CLog.d("Updated VINTF manifest"); 295 } else { 296 CLog.d("Not updating VINTF manifest"); 297 } 298 } catch (ParserConfigurationException 299 | SAXException 300 | XPathExpressionException 301 | TransformerException e) { 302 CLog.e("Could not parse vintf manifest: %s", e); 303 } 304 } 305 306 /** Spin wait until given property has given value. */ waitPropertyValue(ITestDevice device, String name, String value, long timeoutMs)307 private void waitPropertyValue(ITestDevice device, String name, String value, long timeoutMs) 308 throws TimeoutException, DeviceNotAvailableException, InterruptedException { 309 long endTime = System.currentTimeMillis() + timeoutMs; 310 while (true) { 311 if (value.equals(device.getProperty(name))) { 312 return; 313 } 314 if (System.currentTimeMillis() > endTime) { 315 throw new TimeoutException(); 316 } 317 TimeUnit.MILLISECONDS.sleep(250); 318 } 319 } 320 321 /** Find an open TCP port on the host */ findOpenPort()322 private static int findOpenPort() throws IOException { 323 try (ServerSocket socket = new ServerSocket(0)) { 324 socket.setReuseAddress(true); 325 return socket.getLocalPort(); 326 } 327 } 328 329 /** Class that encapsulates a virtual HCI device that can be controlled by HCI commands. */ 330 public static class HciDevice implements AutoCloseable { 331 private static final String READ_FAIL_MSG = "Failed to read HCI packet"; 332 private final Socket hciSocket; 333 HciDevice(Socket hciSocket)334 private HciDevice(Socket hciSocket) { 335 this.hciSocket = hciSocket; 336 } 337 338 @Override close()339 public void close() throws IOException { 340 hciSocket.close(); 341 } 342 343 /** 344 * Convenient wrapper around sendHciPacket to send a HCI command packet to device. 345 * 346 * @param ogf Opcode group field 347 * @param ocf Opcode command field 348 * @param params the rest of the command parameters 349 */ sendHciCmd(int ogf, int ocf, byte[] params)350 public void sendHciCmd(int ogf, int ocf, byte[] params) throws IOException { 351 assertTrue("params length must be less than 256 bytes", params.length < 256); 352 ByteBuffer cmd = ByteBuffer.allocate(4 + params.length).order(ByteOrder.LITTLE_ENDIAN); 353 int opcode = (ogf << 10) | ocf; 354 cmd.put((byte) 0x01).putShort((short) opcode).put((byte) params.length).put(params); 355 sendHciPacket(cmd.array()); 356 } 357 358 /** 359 * Send raw HCI packet to device. 360 * 361 * @param packet raw packet data to send to device 362 */ sendHciPacket(byte[] packet)363 public void sendHciPacket(byte[] packet) throws IOException { 364 CLog.d("sending HCI: %s", Arrays.toString(packet)); 365 hciSocket.getOutputStream().write(packet); 366 } 367 368 /** Read one HCI packet from device, blocking until data is available. */ readHciPacket()369 public byte[] readHciPacket() throws IOException { 370 ByteArrayOutputStream ret = new ByteArrayOutputStream(); 371 InputStream in = hciSocket.getInputStream(); 372 373 // Read the packet type 374 byte[] typeBuf = new byte[1]; 375 assertEquals(READ_FAIL_MSG, 1, in.read(typeBuf, 0, 1)); 376 ret.write(typeBuf); 377 378 // Read the header and figure out how much data to read 379 // according to BT core spec 5.2 vol 4 part A section 2 & part E section 5.4 380 byte[] hdrBuf = new byte[4]; 381 int dataLength; 382 383 switch (typeBuf[0]) { 384 case 0x01: // Command packet 385 case 0x03: // Synch data packet 386 assertEquals(READ_FAIL_MSG, 3, in.read(hdrBuf, 0, 3)); 387 ret.write(hdrBuf, 0, 3); 388 dataLength = hdrBuf[2]; 389 break; 390 391 case 0x02: // Async data packet 392 assertEquals(READ_FAIL_MSG, 4, in.read(hdrBuf, 0, 4)); 393 ret.write(hdrBuf, 0, 4); 394 dataLength = (((int) hdrBuf[2]) & 0xFF) | ((((int) hdrBuf[3]) & 0xFF) << 8); 395 break; 396 397 case 0x04: // Event 398 assertEquals(READ_FAIL_MSG, 2, in.read(hdrBuf, 0, 2)); 399 ret.write(hdrBuf, 0, 2); 400 dataLength = hdrBuf[1]; 401 break; 402 403 case 0x05: // ISO synch data packet 404 assertEquals(READ_FAIL_MSG, 4, in.read(hdrBuf, 0, 4)); 405 ret.write(hdrBuf, 0, 4); 406 dataLength = (((int) hdrBuf[2]) & 0xFF) | ((((int) hdrBuf[3]) & 0xFC) << 6); 407 break; 408 409 default: 410 throw new IOException("Unexpected packet type: " + String.valueOf(typeBuf[0])); 411 } 412 413 // Read the data payload 414 byte[] data = new byte[dataLength]; 415 assertEquals(READ_FAIL_MSG, dataLength, in.read(data, 0, dataLength)); 416 ret.write(data, 0, dataLength); 417 418 return ret.toByteArray(); 419 } 420 } 421 422 public static class RootcanalController implements AutoCloseable { 423 private final int testPort; 424 private final int hciPort; 425 private Socket rootcanalTestChannel = null; 426 private List<HciDevice> hciDevices = new ArrayList<>(); 427 RootcanalController(int testPort, int hciPort)428 private RootcanalController(int testPort, int hciPort) 429 throws IOException, InterruptedException { 430 this.testPort = testPort; 431 this.hciPort = hciPort; 432 CLog.d("Rootcanal controller initialized; testPort=%d, hciPort=%d", testPort, hciPort); 433 } 434 435 @Override close()436 public void close() throws IOException { 437 rootcanalTestChannel.close(); 438 for (HciDevice dev : hciDevices) { 439 dev.close(); 440 } 441 } 442 443 /** 444 * Create a new HCI device by connecting to rootcanal's HCI socket. 445 * 446 * @return HciDevice object that allows sending/receiving from the HCI port 447 */ createHciDevice()448 public HciDevice createHciDevice() 449 throws DeviceNotAvailableException, IOException, InterruptedException { 450 HciDevice dev = new HciDevice(new Socket("localhost", hciPort)); 451 hciDevices.add(dev); 452 return dev; 453 } 454 455 /** 456 * Send one command to rootcanal test channel. 457 * 458 * <p>Send `help` command for list of accepted commands from Rootcanal. 459 * 460 * @param cmd command to send 461 * @param args arguments for the command 462 * @return Response string from rootcanal 463 */ sendTestChannelCommand(String cmd, String... args)464 public String sendTestChannelCommand(String cmd, String... args) 465 throws IOException, InterruptedException { 466 if (rootcanalTestChannel == null) { 467 rootcanalTestChannel = new Socket("localhost", testPort); 468 CLog.d( 469 "RootCanal test channel init: " 470 + readTestChannel(rootcanalTestChannel.getInputStream())); 471 } 472 473 // Translated from system/bt/vendor_libs/test_vendor_lib/scripts/test_channel.py 474 ByteArrayOutputStream msg = new ByteArrayOutputStream(); 475 msg.write(cmd.length()); 476 msg.write(cmd.getBytes("ASCII")); 477 msg.write(args.length); 478 for (String arg : args) { 479 msg.write(arg.length()); 480 msg.write(arg.getBytes("ASCII")); 481 } 482 483 rootcanalTestChannel.getOutputStream().write(msg.toByteArray()); 484 return readTestChannel(rootcanalTestChannel.getInputStream()); 485 } 486 487 /** Read one message from rootcanal test channel. */ readTestChannel(InputStream in)488 private String readTestChannel(InputStream in) throws IOException { 489 // Translated from system/bt/vendor_libs/test_vendor_lib/scripts/test_channel.py 490 ByteBuffer sizeBuf = ByteBuffer.allocate(Integer.BYTES).order(ByteOrder.LITTLE_ENDIAN); 491 in.read(sizeBuf.array(), 0, Integer.BYTES); 492 int size = sizeBuf.getInt(); 493 494 byte[] buf = new byte[size]; 495 in.read(buf, 0, size); 496 return new String(buf); 497 } 498 } 499 } 500