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