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.fs.common;
18 
19 import static com.android.microdroid.test.host.LogArchiver.archiveLogThenDelete;
20 import static com.android.tradefed.device.TestDevice.MicrodroidBuilder;
21 import static com.android.tradefed.testtype.DeviceJUnit4ClassRunner.TestLogData;
22 
23 import static com.google.common.truth.Truth.assertThat;
24 
25 import static org.junit.Assert.assertNotNull;
26 import static org.junit.Assert.fail;
27 
28 import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
29 import com.android.compatibility.common.util.PollingCheck;
30 import com.android.microdroid.test.host.CommandRunner;
31 import com.android.tradefed.build.IBuildInfo;
32 import com.android.tradefed.device.DeviceNotAvailableException;
33 import com.android.tradefed.device.ITestDevice;
34 import com.android.tradefed.device.TestDevice;
35 import com.android.tradefed.invoker.TestInformation;
36 import com.android.tradefed.log.LogUtil.CLog;
37 import com.android.tradefed.util.CommandResult;
38 
39 import org.junit.runner.Description;
40 import org.junit.runners.model.Statement;
41 
42 import java.io.File;
43 import java.io.FileNotFoundException;
44 import java.util.concurrent.ExecutorService;
45 import java.util.concurrent.Executors;
46 import java.util.concurrent.Future;
47 import java.util.concurrent.atomic.AtomicBoolean;
48 
49 /** Custom TestRule for AuthFs tests. */
50 public class AuthFsTestRule extends TestLogData {
51     /** FUSE's magic from statfs(2) */
52     public static final String FUSE_SUPER_MAGIC_HEX = "65735546";
53 
54     /** VM config entry path in the test APK */
55     private static final String VM_CONFIG_PATH_IN_APK = "assets/vm_config.json";
56 
57     /** Test directory on Android where data are located */
58     public static final String TEST_DIR = "/data/local/tmp/authfs";
59 
60     /** File name of the test APK */
61     private static final String TEST_APK_NAME = "MicrodroidTestApp.apk";
62 
63     /** Output directory where the test can generate output on Android */
64     public static final String TEST_OUTPUT_DIR = "/data/local/tmp/authfs/output_dir";
65 
66     /** Mount point of authfs on Microdroid during the test */
67     public static final String MOUNT_DIR = "/data/local/tmp/mnt";
68 
69     /** VM's log file */
70     private static final String LOG_PATH = TEST_OUTPUT_DIR + "/log.txt";
71 
72     /** Path to open_then_run on Android */
73     private static final String OPEN_THEN_RUN_BIN = "/data/local/tmp/open_then_run";
74 
75     /** Path to fd_server on Android */
76     private static final String FD_SERVER_BIN = "/apex/com.android.virt/bin/fd_server";
77 
78     /** Path to authfs on Microdroid */
79     private static final String AUTHFS_BIN = "/system/bin/authfs";
80 
81     /** Plenty of time for authfs to get ready */
82     private static final int AUTHFS_INIT_TIMEOUT_MS = 3000;
83 
84     private static final int VMADDR_CID_HOST = 2;
85 
86     private static TestInformation sTestInfo;
87     private static ITestDevice sMicrodroidDevice;
88     private static CommandRunner sAndroid;
89     private static CommandRunner sMicrodroid;
90 
91     private ExecutorService mThreadPool;
92 
setUpAndroid(TestInformation testInfo)93     public static void setUpAndroid(TestInformation testInfo) throws Exception {
94         assertNotNull(testInfo.getDevice());
95         if (!(testInfo.getDevice() instanceof TestDevice)) {
96             CLog.w("Unexpected type of ITestDevice. Skipping.");
97             return;
98         }
99         sTestInfo = testInfo;
100         TestDevice androidDevice = getDevice();
101         sAndroid = new CommandRunner(androidDevice);
102     }
103 
tearDownAndroid()104     public static void tearDownAndroid() {
105         sAndroid = null;
106     }
107 
108     /** This method is supposed to be called after {@link #setUpTest()}. */
getAndroid()109     public static CommandRunner getAndroid() {
110         assertThat(sAndroid).isNotNull();
111         return sAndroid;
112     }
113 
114     /** This method is supposed to be called after {@link #setUpTest()}. */
getMicrodroid()115     public static CommandRunner getMicrodroid() {
116         assertThat(sMicrodroid).isNotNull();
117         return sMicrodroid;
118     }
119 
getMicrodroidDevice()120     public static ITestDevice getMicrodroidDevice() {
121         assertThat(sMicrodroidDevice).isNotNull();
122         return sMicrodroidDevice;
123     }
124 
startMicrodroid(boolean protectedVm)125     public static void startMicrodroid(boolean protectedVm) throws DeviceNotAvailableException {
126         CLog.i("Starting the shared VM");
127         assertThat(sMicrodroidDevice).isNull();
128         sMicrodroidDevice =
129                 MicrodroidBuilder.fromFile(
130                                 findTestFile(sTestInfo.getBuildInfo(), TEST_APK_NAME),
131                                 VM_CONFIG_PATH_IN_APK)
132                         .debugLevel("full")
133                         .protectedVm(protectedVm)
134                         .build(getDevice());
135 
136         // From this point on, we need to tear down the Microdroid instance
137         sMicrodroid = new CommandRunner(sMicrodroidDevice);
138 
139         sMicrodroid.runForResult("mkdir -p " + MOUNT_DIR);
140 
141         // Root because authfs (started from shell in this test) currently require root to open
142         // /dev/fuse and mount the FUSE.
143         assertThat(sMicrodroidDevice.enableAdbRoot()).isTrue();
144     }
145 
shutdownMicrodroid()146     public static void shutdownMicrodroid() throws DeviceNotAvailableException {
147         if (sMicrodroidDevice != null) {
148             getDevice().shutdownMicrodroid(sMicrodroidDevice);
149             sMicrodroidDevice = null;
150             sMicrodroid = null;
151         }
152     }
153 
154     @Override
apply(final Statement base, Description description)155     public Statement apply(final Statement base, Description description) {
156         return super.apply(
157                 new Statement() {
158                     @Override
159                     public void evaluate() throws Throwable {
160                         setUpTest();
161                         base.evaluate();
162                         tearDownTest(description.getMethodName());
163                     }
164                 },
165                 description);
166     }
167 
168     public void runFdServerOnAndroid(String helperFlags, String fdServerFlags)
169             throws DeviceNotAvailableException {
170         String cmd =
171                 "cd "
172                         + TEST_DIR
173                         + " && "
174                         + OPEN_THEN_RUN_BIN
175                         + " "
176                         + helperFlags
177                         + " -- "
178                         + FD_SERVER_BIN
179                         + " "
180                         + fdServerFlags;
181         Future<?> unusedFuture = mThreadPool.submit(() -> runForResult(sAndroid, cmd, "fd_server"));
182     }
183 
184     public void killFdServerOnAndroid() throws DeviceNotAvailableException {
185         sAndroid.tryRun("killall fd_server");
186     }
187 
188     public void runAuthFsOnMicrodroid(String flags) {
189         String cmd = AUTHFS_BIN + " " + MOUNT_DIR + " " + flags + " --cid " + VMADDR_CID_HOST;
190 
191         AtomicBoolean starting = new AtomicBoolean(true);
192         Future<?> unusedFuture =
193                 mThreadPool.submit(
194                         () -> {
195                             // authfs may fail to start if fd_server is not yet listening on the
196                             // vsock
197                             // ("Error: Invalid raw AIBinder"). Just restart if that happens.
198                             while (starting.get()) {
199                                 runForResult(sMicrodroid, cmd, "authfs");
200                             }
201                         });
202         try {
203             PollingCheck.waitFor(
204                     AUTHFS_INIT_TIMEOUT_MS, () -> isMicrodroidDirectoryOnFuse(MOUNT_DIR));
205         } catch (Exception e) {
206             // Convert the broad Exception into an unchecked exception to avoid polluting all other
207             // methods. waitFor throws Exception because the callback, Callable#call(), has a
208             // signature to throw an Exception.
209             throw new RuntimeException(e);
210         } finally {
211             starting.set(false);
212         }
213     }
214 
215     public static File findTestFile(IBuildInfo buildInfo, String fileName) {
216         try {
217             return (new CompatibilityBuildHelper(buildInfo)).getTestFile(fileName);
218         } catch (FileNotFoundException e) {
219             fail("Missing test file: " + fileName);
220             return null;
221         }
222     }
223 
224     public static TestDevice getDevice() {
225         return (TestDevice) sTestInfo.getDevice();
226     }
227 
228     private void runForResult(CommandRunner cmdRunner, String cmd, String serviceName) {
229         try {
230             CLog.i("Starting " + serviceName);
231             CommandResult result = cmdRunner.runForResult(cmd);
232             CLog.w(serviceName + " has stopped: " + result);
233         } catch (DeviceNotAvailableException e) {
234             CLog.e("Error running " + serviceName, e);
235             throw new RuntimeException(e);
236         }
237     }
238 
239     private boolean isMicrodroidDirectoryOnFuse(String path) throws DeviceNotAvailableException {
240         String fs_type = sMicrodroid.tryRun("stat -f -c '%t' " + path);
241         return FUSE_SUPER_MAGIC_HEX.equals(fs_type);
242     }
243 
244     public void setUpTest() throws Exception {
245         mThreadPool = Executors.newCachedThreadPool();
246         if (sAndroid != null) {
247             sAndroid.run("mkdir -p " + TEST_OUTPUT_DIR);
248         }
249     }
250 
251     private void tearDownTest(String testName) throws Exception {
252         if (sMicrodroid != null) {
253             sMicrodroid.tryRun("killall authfs");
254             sMicrodroid.tryRun("umount " + MOUNT_DIR);
255         }
256 
257         assertNotNull(sAndroid);
258         killFdServerOnAndroid();
259 
260         // Even though we only run one VM for the whole class, and could have collect the VM log
261         // after all tests are done, TestLogData doesn't seem to work at class level. Hence,
262         // collect recent logs manually for each test method.
263         String vmRecentLog = TEST_OUTPUT_DIR + "/vm_recent.log";
264         sAndroid.tryRun("tail -n 50 " + LOG_PATH + " > " + vmRecentLog);
265         archiveLogThenDelete(this, getDevice(), vmRecentLog, "vm_recent.log-" + testName);
266 
267         sAndroid.run("rm -rf " + TEST_OUTPUT_DIR);
268 
269         if (mThreadPool != null) {
270             mThreadPool.shutdownNow();
271             mThreadPool = null;
272         }
273     }
274 }
275