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.Assert.fail; 25 import static org.junit.Assume.assumeTrue; 26 27 import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper; 28 import com.android.ddmlib.Log; 29 import com.android.tradefed.device.DeviceNotAvailableException; 30 import com.android.tradefed.device.IFileEntry; 31 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner; 32 import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test; 33 import com.android.tradefed.util.CommandResult; 34 import com.android.tradefed.util.CommandStatus; 35 36 import org.junit.After; 37 import org.junit.Before; 38 import org.junit.Rule; 39 import org.junit.Test; 40 import org.junit.runner.RunWith; 41 42 import java.io.File; 43 import java.time.Instant; 44 import java.util.Collections; 45 import java.util.Date; 46 import java.util.List; 47 import java.util.concurrent.TimeUnit; 48 import java.util.stream.Collectors; 49 50 /** 51 * Runs the staged rollback tests. 52 * 53 * TODO(gavincorkery): Support the verification of logging parents in Watchdog metrics. 54 */ 55 @RunWith(DeviceJUnit4ClassRunner.class) 56 public class StagedRollbackTest extends BaseHostJUnit4Test { 57 private static final String TAG = "StagedRollbackTest"; 58 private static final int NATIVE_CRASHES_THRESHOLD = 5; 59 60 /** 61 * Runs the given phase of a test by calling into the device. 62 * Throws an exception if the test phase fails. 63 * <p> 64 * For example, <code>runPhase("testApkOnlyEnableRollback");</code> 65 */ runPhase(String phase)66 private void runPhase(String phase) throws Exception { 67 assertThat(runDeviceTests("com.android.tests.rollback", 68 "com.android.tests.rollback.StagedRollbackTest", 69 phase)).isTrue(); 70 } 71 72 private static final String APK_IN_APEX_TESTAPEX_NAME = "com.android.apex.apkrollback.test"; 73 private static final String TESTAPP_A = "com.android.cts.install.lib.testapp.A"; 74 75 private static final String TEST_SUBDIR = "/subdir/"; 76 77 private static final String TEST_FILENAME_1 = "test_file.txt"; 78 private static final String TEST_STRING_1 = "hello this is a test"; 79 private static final String TEST_FILENAME_2 = "another_file.txt"; 80 private static final String TEST_STRING_2 = "this is a different file"; 81 private static final String TEST_FILENAME_3 = "also.xyz"; 82 private static final String TEST_STRING_3 = "also\n a\n test\n string"; 83 private static final String TEST_FILENAME_4 = "one_more.test"; 84 private static final String TEST_STRING_4 = "once more unto the test"; 85 86 private static final String REASON_APP_CRASH = "REASON_APP_CRASH"; 87 private static final String REASON_NATIVE_CRASH = "REASON_NATIVE_CRASH"; 88 89 private static final String ROLLBACK_INITIATE = "ROLLBACK_INITIATE"; 90 private static final String ROLLBACK_BOOT_TRIGGERED = "ROLLBACK_BOOT_TRIGGERED"; 91 private static final String ROLLBACK_SUCCESS = "ROLLBACK_SUCCESS"; 92 93 private WatchdogEventLogger mLogger = new WatchdogEventLogger(); 94 95 @Rule 96 public AbandonSessionsRule mHostTestRule = new AbandonSessionsRule(this); 97 98 @Before setUp()99 public void setUp() throws Exception { 100 deleteFiles("/system/apex/" + APK_IN_APEX_TESTAPEX_NAME + "*.apex", 101 "/data/apex/active/" + APK_IN_APEX_TESTAPEX_NAME + "*.apex"); 102 runPhase("expireRollbacks"); 103 mLogger.start(getDevice()); 104 getDevice().uninstallPackage("com.android.cts.install.lib.testapp.A"); 105 getDevice().uninstallPackage("com.android.cts.install.lib.testapp.B"); 106 getDevice().uninstallPackage("com.android.cts.install.lib.testapp.C"); 107 } 108 109 @After tearDown()110 public void tearDown() throws Exception { 111 getDevice().uninstallPackage("com.android.cts.install.lib.testapp.A"); 112 getDevice().uninstallPackage("com.android.cts.install.lib.testapp.B"); 113 getDevice().uninstallPackage("com.android.cts.install.lib.testapp.C"); 114 mLogger.stop(); 115 runPhase("expireRollbacks"); 116 deleteFiles("/system/apex/" + APK_IN_APEX_TESTAPEX_NAME + "*.apex", 117 "/data/apex/active/" + APK_IN_APEX_TESTAPEX_NAME + "*.apex", 118 apexDataDirDeSys(APK_IN_APEX_TESTAPEX_NAME) + "*", 119 apexDataDirCe(APK_IN_APEX_TESTAPEX_NAME, 0) + "*"); 120 } 121 122 /** 123 * Deletes files and reboots the device if necessary. 124 * @param files the paths of files which might contain wildcards 125 */ deleteFiles(String... files)126 private void deleteFiles(String... files) throws Exception { 127 boolean found = false; 128 for (String file : files) { 129 CommandResult result = getDevice().executeShellV2Command("ls " + file); 130 if (result.getStatus() == CommandStatus.SUCCESS) { 131 found = true; 132 break; 133 } 134 } 135 136 if (found) { 137 try { 138 getDevice().enableAdbRoot(); 139 getDevice().remountSystemWritable(); 140 for (String file : files) { 141 getDevice().executeShellCommand("rm -rf " + file); 142 } 143 } finally { 144 getDevice().disableAdbRoot(); 145 } 146 getDevice().reboot(); 147 } 148 } 149 waitForDeviceNotAvailable(long timeout, TimeUnit unit)150 private void waitForDeviceNotAvailable(long timeout, TimeUnit unit) { 151 assertWithMessage("waitForDeviceNotAvailable() timed out in %s %s", timeout, unit) 152 .that(getDevice().waitForDeviceNotAvailable(unit.toMillis(timeout))).isTrue(); 153 } 154 155 /** 156 * Tests watchdog triggered staged rollbacks involving only apks. 157 */ 158 @Test testBadApkOnly()159 public void testBadApkOnly() throws Exception { 160 runPhase("testBadApkOnly_Phase1_Install"); 161 getDevice().reboot(); 162 runPhase("testBadApkOnly_Phase2_VerifyInstall"); 163 164 // Launch the app to crash to trigger rollback 165 startActivity(TESTAPP_A); 166 // Wait for reboot to happen 167 waitForDeviceNotAvailable(2, TimeUnit.MINUTES); 168 169 getDevice().waitForDeviceAvailable(); 170 171 runPhase("testBadApkOnly_Phase3_VerifyRollback"); 172 173 assertThat(mLogger).eventOccurred(ROLLBACK_INITIATE, null, REASON_APP_CRASH, TESTAPP_A); 174 assertThat(mLogger).eventOccurred(ROLLBACK_BOOT_TRIGGERED, null, null, null); 175 assertThat(mLogger).eventOccurred(ROLLBACK_SUCCESS, null, null, null); 176 } 177 178 @Test testNativeWatchdogTriggersRollback()179 public void testNativeWatchdogTriggersRollback() throws Exception { 180 runPhase("testNativeWatchdogTriggersRollback_Phase1_Install"); 181 182 // Reboot device to activate staged package 183 getDevice().reboot(); 184 185 runPhase("testNativeWatchdogTriggersRollback_Phase2_VerifyInstall"); 186 187 // crash system_server enough times to trigger a rollback 188 crashProcess("system_server", NATIVE_CRASHES_THRESHOLD); 189 190 // Rollback should be committed automatically now. 191 // Give time for rollback to be committed. This could take a while, 192 // because we need all of the following to happen: 193 // 1. system_server comes back up and boot completes. 194 // 2. Rollback health observer detects updatable crashing signal. 195 // 3. Staged rollback session becomes ready. 196 // 4. Device actually reboots. 197 // So we give a generous timeout here. 198 waitForDeviceNotAvailable(5, TimeUnit.MINUTES); 199 getDevice().waitForDeviceAvailable(); 200 201 // verify rollback committed 202 runPhase("testNativeWatchdogTriggersRollback_Phase3_VerifyRollback"); 203 204 assertThat(mLogger).eventOccurred(ROLLBACK_INITIATE, null, REASON_NATIVE_CRASH, null); 205 assertThat(mLogger).eventOccurred(ROLLBACK_BOOT_TRIGGERED, null, null, null); 206 assertThat(mLogger).eventOccurred(ROLLBACK_SUCCESS, null, null, null); 207 } 208 209 @Test testNativeWatchdogTriggersRollbackForAll()210 public void testNativeWatchdogTriggersRollbackForAll() throws Exception { 211 // This test requires committing multiple staged rollbacks 212 assumeTrue(isCheckpointSupported()); 213 214 // Install a package with rollback enabled. 215 runPhase("testNativeWatchdogTriggersRollbackForAll_Phase1_InstallA"); 216 getDevice().reboot(); 217 218 // Once previous staged install is applied, install another package 219 runPhase("testNativeWatchdogTriggersRollbackForAll_Phase2_InstallB"); 220 getDevice().reboot(); 221 222 // Verify the new staged install has also been applied successfully. 223 runPhase("testNativeWatchdogTriggersRollbackForAll_Phase3_VerifyInstall"); 224 225 // crash system_server enough times to trigger a rollback 226 crashProcess("system_server", NATIVE_CRASHES_THRESHOLD); 227 228 // Rollback should be committed automatically now. 229 // Give time for rollback to be committed. This could take a while, 230 // because we need all of the following to happen: 231 // 1. system_server comes back up and boot completes. 232 // 2. Rollback health observer detects updatable crashing signal. 233 // 3. Staged rollback session becomes ready. 234 // 4. Device actually reboots. 235 // So we give a generous timeout here. 236 waitForDeviceNotAvailable(5, TimeUnit.MINUTES); 237 getDevice().waitForDeviceAvailable(); 238 239 // verify all available rollbacks have been committed 240 runPhase("testNativeWatchdogTriggersRollbackForAll_Phase4_VerifyRollback"); 241 242 assertThat(mLogger).eventOccurred(ROLLBACK_INITIATE, null, REASON_NATIVE_CRASH, null); 243 assertThat(mLogger).eventOccurred(ROLLBACK_BOOT_TRIGGERED, null, null, null); 244 assertThat(mLogger).eventOccurred(ROLLBACK_SUCCESS, null, null, null); 245 } 246 247 /** 248 * Tests rolling back user data where there are multiple rollbacks for that package. 249 */ 250 @Test testPreviouslyAbandonedRollbacks()251 public void testPreviouslyAbandonedRollbacks() throws Exception { 252 runPhase("testPreviouslyAbandonedRollbacks_Phase1_InstallAndAbandon"); 253 getDevice().reboot(); 254 runPhase("testPreviouslyAbandonedRollbacks_Phase2_Rollback"); 255 getDevice().reboot(); 256 runPhase("testPreviouslyAbandonedRollbacks_Phase3_VerifyRollback"); 257 } 258 259 /** 260 * Tests we can enable rollback for a allowlisted app. 261 */ 262 @Test testRollbackAllowlistedApp()263 public void testRollbackAllowlistedApp() throws Exception { 264 assumeTrue(hasMainlineModule()); 265 runPhase("testRollbackAllowlistedApp_Phase1_Install"); 266 getDevice().reboot(); 267 runPhase("testRollbackAllowlistedApp_Phase2_VerifyInstall"); 268 } 269 270 @Test testRollbackDataPolicy()271 public void testRollbackDataPolicy() throws Exception { 272 List<String> before = getSnapshotDirectories("/data/misc_ce/0/rollback"); 273 274 runPhase("testRollbackDataPolicy_Phase1_Install"); 275 getDevice().reboot(); 276 runPhase("testRollbackDataPolicy_Phase2_Rollback"); 277 getDevice().reboot(); 278 runPhase("testRollbackDataPolicy_Phase3_VerifyRollback"); 279 280 // Verify snapshots are deleted after restoration 281 List<String> after = getSnapshotDirectories("/data/misc_ce/0/rollback"); 282 // Only check directories newly created during the test 283 after.removeAll(before); 284 // There should be only one /data/misc_ce/0/rollback/<rollbackId> created during test 285 assertThat(after).hasSize(1); 286 assertDirectoryIsEmpty(after.get(0)); 287 } 288 289 /** 290 * Tests that userdata of apk-in-apex is restored when apex is rolled back. 291 */ 292 @Test testRollbackApexWithApk()293 public void testRollbackApexWithApk() throws Exception { 294 pushTestApex(); 295 runPhase("testRollbackApexWithApk_Phase1_Install"); 296 getDevice().reboot(); 297 runPhase("testRollbackApexWithApk_Phase2_Rollback"); 298 getDevice().reboot(); 299 runPhase("testRollbackApexWithApk_Phase3_VerifyRollback"); 300 } 301 302 /** 303 * Tests that RollbackPackageHealthObserver is observing apk-in-apex. 304 */ 305 @Test testRollbackApexWithApkCrashing()306 public void testRollbackApexWithApkCrashing() throws Exception { 307 pushTestApex(); 308 309 // Install an apex with apk that crashes 310 runPhase("testRollbackApexWithApkCrashing_Phase1_Install"); 311 getDevice().reboot(); 312 // Verify apex was installed and then crash the apk 313 runPhase("testRollbackApexWithApkCrashing_Phase2_Crash"); 314 // Launch the app to crash to trigger rollback 315 startActivity(TESTAPP_A); 316 // Wait for reboot to happen 317 waitForDeviceNotAvailable(2, TimeUnit.MINUTES); 318 getDevice().waitForDeviceAvailable(); 319 // Verify rollback occurred due to crash of apk-in-apex 320 runPhase("testRollbackApexWithApkCrashing_Phase3_VerifyRollback"); 321 322 assertThat(mLogger).eventOccurred(ROLLBACK_INITIATE, null, REASON_APP_CRASH, TESTAPP_A); 323 assertThat(mLogger).eventOccurred(ROLLBACK_BOOT_TRIGGERED, null, null, null); 324 assertThat(mLogger).eventOccurred(ROLLBACK_SUCCESS, null, null, null); 325 } 326 327 /** 328 * Tests that data in DE_sys apex data directory is restored when apex is rolled back. 329 */ 330 @Test testRollbackApexDataDirectories_DeSys()331 public void testRollbackApexDataDirectories_DeSys() throws Exception { 332 List<String> before = getSnapshotDirectories("/data/misc/apexrollback"); 333 pushTestApex(); 334 335 // Push files to apex data directory 336 String oldFilePath1 = apexDataDirDeSys(APK_IN_APEX_TESTAPEX_NAME) + "/" + TEST_FILENAME_1; 337 String oldFilePath2 = 338 apexDataDirDeSys(APK_IN_APEX_TESTAPEX_NAME) + TEST_SUBDIR + TEST_FILENAME_2; 339 runAsRoot(() -> { 340 pushString(TEST_STRING_1, oldFilePath1); 341 pushString(TEST_STRING_2, oldFilePath2); 342 }); 343 344 // Install new version of the APEX with rollback enabled 345 runPhase("testRollbackApexDataDirectories_Phase1_Install"); 346 getDevice().reboot(); 347 348 // Replace files in data directory 349 String newFilePath3 = apexDataDirDeSys(APK_IN_APEX_TESTAPEX_NAME) + "/" + TEST_FILENAME_3; 350 String newFilePath4 = 351 apexDataDirDeSys(APK_IN_APEX_TESTAPEX_NAME) + TEST_SUBDIR + TEST_FILENAME_4; 352 runAsRoot(() -> { 353 getDevice().deleteFile(oldFilePath1); 354 getDevice().deleteFile(oldFilePath2); 355 pushString(TEST_STRING_3, newFilePath3); 356 pushString(TEST_STRING_4, newFilePath4); 357 }); 358 359 // Roll back the APEX 360 runPhase("testRollbackApexDataDirectories_Phase2_Rollback"); 361 getDevice().reboot(); 362 363 // Verify that old files have been restored and new files are gone 364 runAsRoot(() -> { 365 assertFileContents(TEST_STRING_1, oldFilePath1); 366 assertFileContents(TEST_STRING_2, oldFilePath2); 367 assertFileNotExists(newFilePath3); 368 assertFileNotExists(newFilePath4); 369 }); 370 371 // Verify snapshots are deleted after restoration 372 List<String> after = getSnapshotDirectories("/data/misc/apexrollback"); 373 // Only check directories newly created during the test 374 after.removeAll(before); 375 // There should be only one /data/misc/apexrollback/<rollbackId> created during test 376 assertThat(after).hasSize(1); 377 assertDirectoryIsEmpty(after.get(0)); 378 } 379 380 /** 381 * Tests that data in DE (user) apex data directory is restored when apex is rolled back. 382 */ 383 @Test testRollbackApexDataDirectories_DeUser()384 public void testRollbackApexDataDirectories_DeUser() throws Exception { 385 List<String> before = getSnapshotDirectories("/data/misc_de/0/apexrollback"); 386 pushTestApex(); 387 388 // Push files to apex data directory 389 String oldFilePath1 = apexDataDirDeUser( 390 APK_IN_APEX_TESTAPEX_NAME, 0) + "/" + TEST_FILENAME_1; 391 String oldFilePath2 = 392 apexDataDirDeUser(APK_IN_APEX_TESTAPEX_NAME, 0) + TEST_SUBDIR + TEST_FILENAME_2; 393 runAsRoot(() -> { 394 pushString(TEST_STRING_1, oldFilePath1); 395 pushString(TEST_STRING_2, oldFilePath2); 396 }); 397 398 // Install new version of the APEX with rollback enabled 399 runPhase("testRollbackApexDataDirectories_Phase1_Install"); 400 getDevice().reboot(); 401 402 // Replace files in data directory 403 String newFilePath3 = 404 apexDataDirDeUser(APK_IN_APEX_TESTAPEX_NAME, 0) + "/" + TEST_FILENAME_3; 405 String newFilePath4 = 406 apexDataDirDeUser(APK_IN_APEX_TESTAPEX_NAME, 0) + TEST_SUBDIR + TEST_FILENAME_4; 407 runAsRoot(() -> { 408 getDevice().deleteFile(oldFilePath1); 409 getDevice().deleteFile(oldFilePath2); 410 pushString(TEST_STRING_3, newFilePath3); 411 pushString(TEST_STRING_4, newFilePath4); 412 }); 413 414 // Roll back the APEX 415 runPhase("testRollbackApexDataDirectories_Phase2_Rollback"); 416 getDevice().reboot(); 417 418 // Verify that old files have been restored and new files are gone 419 runAsRoot(() -> { 420 assertFileContents(TEST_STRING_1, oldFilePath1); 421 assertFileContents(TEST_STRING_2, oldFilePath2); 422 assertFileNotExists(newFilePath3); 423 assertFileNotExists(newFilePath4); 424 }); 425 426 // Verify snapshots are deleted after restoration 427 List<String> after = getSnapshotDirectories("/data/misc_de/0/apexrollback"); 428 // Only check directories newly created during the test 429 after.removeAll(before); 430 // There should be only one /data/misc_de/0/apexrollback/<rollbackId> created during test 431 assertThat(after).hasSize(1); 432 assertDirectoryIsEmpty(after.get(0)); 433 } 434 435 /** 436 * Tests that data in CE apex data directory is restored when apex is rolled back. 437 */ 438 @Test testRollbackApexDataDirectories_Ce()439 public void testRollbackApexDataDirectories_Ce() throws Exception { 440 List<String> before = getSnapshotDirectories("/data/misc_ce/0/apexrollback"); 441 pushTestApex(); 442 443 // Push files to apex data directory 444 String oldFilePath1 = apexDataDirCe(APK_IN_APEX_TESTAPEX_NAME, 0) + "/" + TEST_FILENAME_1; 445 String oldFilePath2 = 446 apexDataDirCe(APK_IN_APEX_TESTAPEX_NAME, 0) + TEST_SUBDIR + TEST_FILENAME_2; 447 runAsRoot(() -> { 448 pushString(TEST_STRING_1, oldFilePath1); 449 pushString(TEST_STRING_2, oldFilePath2); 450 }); 451 452 // Install new version of the APEX with rollback enabled 453 runPhase("testRollbackApexDataDirectories_Phase1_Install"); 454 getDevice().reboot(); 455 456 // Replace files in data directory 457 String newFilePath3 = apexDataDirCe(APK_IN_APEX_TESTAPEX_NAME, 0) + "/" + TEST_FILENAME_3; 458 String newFilePath4 = 459 apexDataDirCe(APK_IN_APEX_TESTAPEX_NAME, 0) + TEST_SUBDIR + TEST_FILENAME_4; 460 runAsRoot(() -> { 461 getDevice().deleteFile(oldFilePath1); 462 getDevice().deleteFile(oldFilePath2); 463 pushString(TEST_STRING_3, newFilePath3); 464 pushString(TEST_STRING_4, newFilePath4); 465 }); 466 467 // Roll back the APEX 468 runPhase("testRollbackApexDataDirectories_Phase2_Rollback"); 469 getDevice().reboot(); 470 471 // Verify that old files have been restored and new files are gone 472 runAsRoot(() -> { 473 assertFileContents(TEST_STRING_1, oldFilePath1); 474 assertFileContents(TEST_STRING_2, oldFilePath2); 475 assertFileNotExists(newFilePath3); 476 assertFileNotExists(newFilePath4); 477 }); 478 479 // Verify snapshots are deleted after restoration 480 List<String> after = getSnapshotDirectories("/data/misc_ce/0/apexrollback"); 481 // Only check directories newly created during the test 482 after.removeAll(before); 483 // There should be only one /data/misc_ce/0/apexrollback/<rollbackId> created during test 484 assertThat(after).hasSize(1); 485 assertDirectoryIsEmpty(after.get(0)); 486 } 487 488 /** 489 * Tests that data in DE apk data directory is restored when apk is rolled back. 490 */ 491 @Test testRollbackApkDataDirectories_De()492 public void testRollbackApkDataDirectories_De() throws Exception { 493 // Install version 1 of TESTAPP_A 494 runPhase("testRollbackApkDataDirectories_Phase1_InstallV1"); 495 496 // Push files to apk data directory 497 String oldFilePath1 = apkDataDirDe(TESTAPP_A, 0) + "/" + TEST_FILENAME_1; 498 String oldFilePath2 = apkDataDirDe(TESTAPP_A, 0) + TEST_SUBDIR + TEST_FILENAME_2; 499 runAsRoot(() -> { 500 pushString(TEST_STRING_1, oldFilePath1); 501 pushString(TEST_STRING_2, oldFilePath2); 502 }); 503 504 // Install version 2 of TESTAPP_A with rollback enabled 505 runPhase("testRollbackApkDataDirectories_Phase2_InstallV2"); 506 getDevice().reboot(); 507 508 // Replace files in data directory 509 String newFilePath3 = apkDataDirDe(TESTAPP_A, 0) + "/" + TEST_FILENAME_3; 510 String newFilePath4 = apkDataDirDe(TESTAPP_A, 0) + TEST_SUBDIR + TEST_FILENAME_4; 511 runAsRoot(() -> { 512 getDevice().deleteFile(oldFilePath1); 513 getDevice().deleteFile(oldFilePath2); 514 pushString(TEST_STRING_3, newFilePath3); 515 pushString(TEST_STRING_4, newFilePath4); 516 }); 517 518 // Roll back the APK 519 runPhase("testRollbackApkDataDirectories_Phase3_Rollback"); 520 getDevice().reboot(); 521 522 // Verify that old files have been restored and new files are gone 523 runAsRoot(() -> { 524 assertFileContents(TEST_STRING_1, oldFilePath1); 525 assertFileContents(TEST_STRING_2, oldFilePath2); 526 assertFileNotExists(newFilePath3); 527 assertFileNotExists(newFilePath4); 528 }); 529 } 530 531 @Test testExpireApexRollback()532 public void testExpireApexRollback() throws Exception { 533 List<String> before = getSnapshotDirectories("/data/misc_ce/0/apexrollback"); 534 pushTestApex(); 535 536 // Push files to apex data directory 537 String oldFilePath1 = apexDataDirCe(APK_IN_APEX_TESTAPEX_NAME, 0) + "/" + TEST_FILENAME_1; 538 String oldFilePath2 = 539 apexDataDirCe(APK_IN_APEX_TESTAPEX_NAME, 0) + TEST_SUBDIR + TEST_FILENAME_2; 540 runAsRoot(() -> { 541 pushString(TEST_STRING_1, oldFilePath1); 542 pushString(TEST_STRING_2, oldFilePath2); 543 }); 544 545 // Install new version of the APEX with rollback enabled 546 runPhase("testRollbackApexDataDirectories_Phase1_Install"); 547 getDevice().reboot(); 548 549 List<String> after = getSnapshotDirectories("/data/misc_ce/0/apexrollback"); 550 // Only check directories newly created during the test 551 after.removeAll(before); 552 // There should be only one /data/misc_ce/0/apexrollback/<rollbackId> created during test 553 assertThat(after).hasSize(1); 554 // Expire all rollbacks and check CE snapshot directories are deleted 555 runPhase("expireRollbacks"); 556 runAsRoot(() -> { 557 for (String dir : after) { 558 assertFileNotExists(dir); 559 } 560 }); 561 } 562 563 /** 564 * Tests that packages are monitored across multiple reboots. 565 */ 566 @Test testWatchdogMonitorsAcrossReboots()567 public void testWatchdogMonitorsAcrossReboots() throws Exception { 568 runPhase("testWatchdogMonitorsAcrossReboots_Phase1_Install"); 569 570 // The first reboot will make the rollback available. 571 // Information about which packages are monitored will be persisted to a file before the 572 // second reboot, and read from disk after the second reboot. 573 getDevice().reboot(); 574 getDevice().reboot(); 575 576 runPhase("testWatchdogMonitorsAcrossReboots_Phase2_VerifyInstall"); 577 578 // Launch the app to crash to trigger rollback 579 startActivity(TESTAPP_A); 580 // Wait for reboot to happen 581 waitForDeviceNotAvailable(2, TimeUnit.MINUTES); 582 getDevice().waitForDeviceAvailable(); 583 584 runPhase("testWatchdogMonitorsAcrossReboots_Phase3_VerifyRollback"); 585 } 586 587 /** 588 * Tests an available rollback shouldn't be deleted when its session expires. 589 */ 590 @Test testExpireSession()591 public void testExpireSession() throws Exception { 592 runPhase("testExpireSession_Phase1_Install"); 593 getDevice().reboot(); 594 runPhase("testExpireSession_Phase2_VerifyInstall"); 595 596 // Advance system clock by 7 days to expire the staged session 597 Instant t1 = Instant.ofEpochMilli(getDevice().getDeviceDate()); 598 Instant t2 = t1.plusMillis(TimeUnit.DAYS.toMillis(7)); 599 runAsRoot(() -> getDevice().setDate(Date.from(t2))); 600 601 // Somehow we need to wait for a while before reboot. Otherwise the change to the 602 // system clock will be reset after reboot. 603 Thread.sleep(3000); 604 getDevice().reboot(); 605 runPhase("testExpireSession_Phase3_VerifyRollback"); 606 } 607 pushTestApex()608 private void pushTestApex() throws Exception { 609 CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(getBuild()); 610 final String fileName = APK_IN_APEX_TESTAPEX_NAME + "_v1.apex"; 611 final File apex = buildHelper.getTestFile(fileName); 612 try { 613 getDevice().enableAdbRoot(); 614 getDevice().remountSystemWritable(); 615 assertThat(getDevice().pushFile(apex, "/system/apex/" + fileName)).isTrue(); 616 } finally { 617 getDevice().disableAdbRoot(); 618 } 619 getDevice().reboot(); 620 } 621 pushString(String contents, String path)622 private void pushString(String contents, String path) throws Exception { 623 assertWithMessage("Failed to push file to device, content=%s path=%s", contents, path) 624 .that(getDevice().pushString(contents, path)).isTrue(); 625 } 626 assertFileContents(String expectedContents, String path)627 private void assertFileContents(String expectedContents, String path) throws Exception { 628 String actualContents = getDevice().pullFileContents(path); 629 assertWithMessage("Failed to retrieve file=%s", path).that(actualContents).isNotNull(); 630 assertWithMessage("Mismatched file contents, path=%s", path) 631 .that(actualContents).isEqualTo(expectedContents); 632 } 633 assertFileNotExists(String path)634 private void assertFileNotExists(String path) throws Exception { 635 assertWithMessage("File shouldn't exist, path=%s", path) 636 .that(getDevice().getFileEntry(path)).isNull(); 637 } 638 apexDataDirDeSys(String apexName)639 private static String apexDataDirDeSys(String apexName) { 640 return String.format("/data/misc/apexdata/%s", apexName); 641 } 642 apexDataDirDeUser(String apexName, int userId)643 private static String apexDataDirDeUser(String apexName, int userId) { 644 return String.format("/data/misc_de/%d/apexdata/%s", userId, apexName); 645 } 646 apexDataDirCe(String apexName, int userId)647 private static String apexDataDirCe(String apexName, int userId) { 648 return String.format("/data/misc_ce/%d/apexdata/%s", userId, apexName); 649 } 650 apkDataDirDe(String apexName, int userId)651 private static String apkDataDirDe(String apexName, int userId) { 652 return String.format("/data/user_de/%d/%s", userId, apexName); 653 } 654 getSnapshotDirectories(String baseDir)655 private List<String> getSnapshotDirectories(String baseDir) throws Exception { 656 try { 657 getDevice().enableAdbRoot(); 658 IFileEntry f = getDevice().getFileEntry(baseDir); 659 if (f == null) { 660 Log.d(TAG, "baseDir doesn't exist: " + baseDir); 661 return Collections.EMPTY_LIST; 662 } 663 List<String> list = f.getChildren(false) 664 .stream().filter(entry -> entry.getName().matches("\\d+(-prerestore)?")) 665 .map(entry -> entry.getFullPath()) 666 .collect(Collectors.toList()); 667 Log.d(TAG, "getSnapshotDirectories=" + list); 668 return list; 669 } finally { 670 getDevice().disableAdbRoot(); 671 } 672 } 673 assertDirectoryIsEmpty(String path)674 private void assertDirectoryIsEmpty(String path) throws Exception { 675 try { 676 getDevice().enableAdbRoot(); 677 IFileEntry file = getDevice().getFileEntry(path); 678 assertWithMessage("Not a directory: " + path).that(file.isDirectory()).isTrue(); 679 assertWithMessage("Directory not empty: " + path) 680 .that(file.getChildren(false)).isEmpty(); 681 } catch (DeviceNotAvailableException e) { 682 fail("Can't access directory: " + path); 683 } finally { 684 getDevice().disableAdbRoot(); 685 } 686 } 687 startActivity(String packageName)688 private void startActivity(String packageName) throws Exception { 689 String cmd = "am start -S -a android.intent.action.MAIN " 690 + "-c android.intent.category.LAUNCHER " + packageName; 691 getDevice().executeShellCommand(cmd); 692 } 693 crashProcess(String processName, int numberOfCrashes)694 private void crashProcess(String processName, int numberOfCrashes) throws Exception { 695 String pid = ""; 696 String lastPid = "invalid"; 697 for (int i = 0; i < numberOfCrashes; ++i) { 698 // This condition makes sure before we kill the process, the process is running AND 699 // the last crash was finished. 700 while ("".equals(pid) || lastPid.equals(pid)) { 701 pid = getDevice().executeShellCommand("pidof " + processName); 702 } 703 getDevice().executeShellCommand("kill " + pid); 704 lastPid = pid; 705 } 706 } 707 isCheckpointSupported()708 private boolean isCheckpointSupported() throws Exception { 709 try { 710 runPhase("isCheckpointSupported"); 711 return true; 712 } catch (AssertionError ignore) { 713 return false; 714 } 715 } 716 717 /** 718 * True if this build has mainline modules installed. 719 */ hasMainlineModule()720 private boolean hasMainlineModule() throws Exception { 721 try { 722 runPhase("hasMainlineModule"); 723 return true; 724 } catch (AssertionError ignore) { 725 return false; 726 } 727 } 728 729 @FunctionalInterface 730 private interface ExceptionalRunnable { run()731 void run() throws Exception; 732 } 733 runAsRoot(ExceptionalRunnable runnable)734 private void runAsRoot(ExceptionalRunnable runnable) throws Exception { 735 try { 736 getDevice().enableAdbRoot(); 737 runnable.run(); 738 } finally { 739 getDevice().disableAdbRoot(); 740 } 741 } 742 } 743