1 /*
2  * Copyright (C) 2019 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.tests.rollback.host;
18 
19 import static com.android.tests.rollback.host.WatchdogEventLogger.Subject.assertThat;
20 
21 import static com.google.common.truth.Truth.assertThat;
22 import static com.google.common.truth.Truth.assertWithMessage;
23 
24 import static org.junit.Assume.assumeTrue;
25 
26 import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
27 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
28 import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
29 import com.android.tradefed.util.CommandResult;
30 import com.android.tradefed.util.CommandStatus;
31 
32 import org.junit.After;
33 import org.junit.Before;
34 import org.junit.Rule;
35 import org.junit.Test;
36 import org.junit.runner.RunWith;
37 
38 import java.io.File;
39 import java.util.concurrent.TimeUnit;
40 
41 /**
42  * Runs the staged rollback tests.
43  *
44  * TODO(gavincorkery): Support the verification of logging parents in Watchdog metrics.
45  */
46 @RunWith(DeviceJUnit4ClassRunner.class)
47 public class StagedRollbackTest extends BaseHostJUnit4Test {
48     private static final String TAG = "StagedRollbackTest";
49     private static final int NATIVE_CRASHES_THRESHOLD = 5;
50 
51     /**
52      * Runs the given phase of a test by calling into the device.
53      * Throws an exception if the test phase fails.
54      * <p>
55      * For example, <code>runPhase("testApkOnlyEnableRollback");</code>
56      */
runPhase(String phase)57     private void runPhase(String phase) throws Exception {
58         assertThat(runDeviceTests("com.android.tests.rollback",
59                     "com.android.tests.rollback.StagedRollbackTest",
60                     phase)).isTrue();
61     }
62 
63     private static final String APK_IN_APEX_TESTAPEX_NAME = "com.android.apex.apkrollback.test";
64     private static final String TESTAPP_A = "com.android.cts.install.lib.testapp.A";
65 
66     private static final String REASON_APP_CRASH = "REASON_APP_CRASH";
67     private static final String REASON_NATIVE_CRASH = "REASON_NATIVE_CRASH";
68 
69     private static final String ROLLBACK_INITIATE = "ROLLBACK_INITIATE";
70     private static final String ROLLBACK_BOOT_TRIGGERED = "ROLLBACK_BOOT_TRIGGERED";
71     private static final String ROLLBACK_SUCCESS = "ROLLBACK_SUCCESS";
72 
73     private WatchdogEventLogger mLogger = new WatchdogEventLogger();
74 
75     @Rule
76     public AbandonSessionsRule mHostTestRule = new AbandonSessionsRule(this);
77 
78     @Before
setUp()79     public void setUp() throws Exception {
80         deleteFiles("/system/apex/" + APK_IN_APEX_TESTAPEX_NAME + "*.apex",
81                 "/data/apex/active/" + APK_IN_APEX_TESTAPEX_NAME + "*.apex");
82         runPhase("expireRollbacks");
83         mLogger.start(getDevice());
84         getDevice().uninstallPackage("com.android.cts.install.lib.testapp.A");
85         getDevice().uninstallPackage("com.android.cts.install.lib.testapp.B");
86         getDevice().uninstallPackage("com.android.cts.install.lib.testapp.C");
87     }
88 
89     @After
tearDown()90     public void tearDown() throws Exception {
91         getDevice().uninstallPackage("com.android.cts.install.lib.testapp.A");
92         getDevice().uninstallPackage("com.android.cts.install.lib.testapp.B");
93         getDevice().uninstallPackage("com.android.cts.install.lib.testapp.C");
94         mLogger.stop();
95         runPhase("expireRollbacks");
96         deleteFiles("/system/apex/" + APK_IN_APEX_TESTAPEX_NAME + "*.apex",
97                 "/data/apex/active/" + APK_IN_APEX_TESTAPEX_NAME + "*.apex",
98                 apexDataDirDeSys(APK_IN_APEX_TESTAPEX_NAME) + "*",
99                 apexDataDirCe(APK_IN_APEX_TESTAPEX_NAME, 0) + "*",
100                 "/system/apex/test.rebootless_apex_v*.apex",
101                 "/data/apex/active/test.apex.rebootless*.apex");
102     }
103 
104     /**
105      * Deletes files and reboots the device if necessary.
106      * @param files the paths of files which might contain wildcards
107      */
deleteFiles(String... files)108     private void deleteFiles(String... files) throws Exception {
109         try {
110             getDevice().enableAdbRoot();
111             boolean found = false;
112             for (String file : files) {
113                 CommandResult result = getDevice().executeShellV2Command("ls " + file);
114                 if (result.getStatus() == CommandStatus.SUCCESS) {
115                     found = true;
116                     break;
117                 }
118             }
119 
120             if (found) {
121                 getDevice().remountSystemWritable();
122                 for (String file : files) {
123                     getDevice().executeShellCommand("rm -rf " + file);
124                 }
125                 getDevice().reboot();
126             }
127         } finally {
128             getDevice().disableAdbRoot();
129         }
130     }
131 
waitForDeviceNotAvailable(long timeout, TimeUnit unit)132     private void waitForDeviceNotAvailable(long timeout, TimeUnit unit) {
133         assertWithMessage("waitForDeviceNotAvailable() timed out in %s %s", timeout, unit)
134                 .that(getDevice().waitForDeviceNotAvailable(unit.toMillis(timeout))).isTrue();
135     }
136 
137     /**
138      * Tests rolling back user data where there are multiple rollbacks for that package.
139      */
140     @Test
testPreviouslyAbandonedRollbacks()141     public void testPreviouslyAbandonedRollbacks() throws Exception {
142         runPhase("testPreviouslyAbandonedRollbacks_Phase1_InstallAndAbandon");
143         getDevice().reboot();
144         runPhase("testPreviouslyAbandonedRollbacks_Phase2_Rollback");
145         getDevice().reboot();
146         runPhase("testPreviouslyAbandonedRollbacks_Phase3_VerifyRollback");
147     }
148 
149     /**
150      * Tests we can enable rollback for a allowlisted app.
151      */
152     @Test
testRollbackAllowlistedApp()153     public void testRollbackAllowlistedApp() throws Exception {
154         assumeTrue(hasMainlineModule());
155         runPhase("testRollbackAllowlistedApp_Phase1_Install");
156         getDevice().reboot();
157         runPhase("testRollbackAllowlistedApp_Phase2_VerifyInstall");
158     }
159 
160     /**
161      * Tests that RollbackPackageHealthObserver is observing apk-in-apex.
162      */
163     @Test
testRollbackApexWithApkCrashing()164     public void testRollbackApexWithApkCrashing() throws Exception {
165         pushTestApex(APK_IN_APEX_TESTAPEX_NAME + "_v1.apex");
166 
167         // Install an apex with apk that crashes
168         runPhase("testRollbackApexWithApkCrashing_Phase1_Install");
169         getDevice().reboot();
170         // Verify apex was installed and then crash the apk
171         runPhase("testRollbackApexWithApkCrashing_Phase2_Crash");
172         // Launch the app to crash to trigger rollback
173         startActivity(TESTAPP_A);
174         // Wait for reboot to happen
175         waitForDeviceNotAvailable(2, TimeUnit.MINUTES);
176         getDevice().waitForDeviceAvailable();
177         // Verify rollback occurred due to crash of apk-in-apex
178         runPhase("testRollbackApexWithApkCrashing_Phase3_VerifyRollback");
179 
180         assertThat(mLogger).eventOccurred(ROLLBACK_INITIATE, null, REASON_APP_CRASH, TESTAPP_A);
181         assertThat(mLogger).eventOccurred(ROLLBACK_BOOT_TRIGGERED, null, null, null);
182         assertThat(mLogger).eventOccurred(ROLLBACK_SUCCESS, null, null, null);
183     }
184 
185     /**
186      * Tests rollback is supported correctly for rebootless apex
187      */
188     @Test
testRollbackRebootlessApex()189     public void testRollbackRebootlessApex() throws Exception {
190         pushTestApex("test.rebootless_apex_v1.apex");
191         runPhase("testRollbackRebootlessApex");
192     }
193 
194     /**
195      * Tests only rebootless apex (if any) is rolled back when native crash happens
196      */
197     @Test
testNativeWatchdogTriggersRebootlessApexRollback()198     public void testNativeWatchdogTriggersRebootlessApexRollback() throws Exception {
199         pushTestApex("test.rebootless_apex_v1.apex");
200         runPhase("testNativeWatchdogTriggersRebootlessApexRollback_Phase1_Install");
201         crashProcess("system_server", NATIVE_CRASHES_THRESHOLD);
202         getDevice().waitForDeviceAvailable();
203         runPhase("testNativeWatchdogTriggersRebootlessApexRollback_Phase2_Verify");
204     }
205 
206     /**
207      * Tests that packages are monitored across multiple reboots.
208      */
209     @Test
testWatchdogMonitorsAcrossReboots()210     public void testWatchdogMonitorsAcrossReboots() throws Exception {
211         runPhase("testWatchdogMonitorsAcrossReboots_Phase1_Install");
212 
213         // The first reboot will make the rollback available.
214         // Information about which packages are monitored will be persisted to a file before the
215         // second reboot, and read from disk after the second reboot.
216         getDevice().reboot();
217         getDevice().reboot();
218 
219         runPhase("testWatchdogMonitorsAcrossReboots_Phase2_VerifyInstall");
220 
221         // Launch the app to crash to trigger rollback
222         startActivity(TESTAPP_A);
223         // Wait for reboot to happen
224         waitForDeviceNotAvailable(2, TimeUnit.MINUTES);
225         getDevice().waitForDeviceAvailable();
226 
227         runPhase("testWatchdogMonitorsAcrossReboots_Phase3_VerifyRollback");
228     }
229 
pushTestApex(String fileName)230     private void pushTestApex(String fileName) throws Exception {
231         CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(getBuild());
232         final File apex = buildHelper.getTestFile(fileName);
233         try {
234             getDevice().enableAdbRoot();
235             getDevice().remountSystemWritable();
236             assertThat(getDevice().pushFile(apex, "/system/apex/" + fileName)).isTrue();
237         } finally {
238             getDevice().disableAdbRoot();
239         }
240         getDevice().reboot();
241     }
242 
apexDataDirDeSys(String apexName)243     private static String apexDataDirDeSys(String apexName) {
244         return String.format("/data/misc/apexdata/%s", apexName);
245     }
246 
apexDataDirCe(String apexName, int userId)247     private static String apexDataDirCe(String apexName, int userId) {
248         return String.format("/data/misc_ce/%d/apexdata/%s", userId, apexName);
249     }
250 
startActivity(String packageName)251     private void startActivity(String packageName) throws Exception {
252         String cmd = "am start -S -a android.intent.action.MAIN "
253                 + "-c android.intent.category.LAUNCHER " + packageName;
254         getDevice().executeShellCommand(cmd);
255     }
256 
257     /**
258      * True if this build has mainline modules installed.
259      */
hasMainlineModule()260     private boolean hasMainlineModule() throws Exception {
261         try {
262             runPhase("hasMainlineModule");
263             return true;
264         } catch (AssertionError ignore) {
265             return false;
266         }
267     }
268 
crashProcess(String processName, int numberOfCrashes)269     private void crashProcess(String processName, int numberOfCrashes) throws Exception {
270         String pid = "";
271         String lastPid = "invalid";
272         for (int i = 0; i < numberOfCrashes; ++i) {
273             // This condition makes sure before we kill the process, the process is running AND
274             // the last crash was finished.
275             while ("".equals(pid) || lastPid.equals(pid)) {
276                 pid = getDevice().executeShellCommand("pidof " + processName);
277             }
278             getDevice().executeShellCommand("kill " + pid);
279             lastPid = pid;
280         }
281     }
282 }
283