1 /* 2 * Copyright (C) 2018 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.framework.multidexlegacytestservices.test2; 18 19 import android.app.ActivityManager; 20 import android.content.Context; 21 import android.content.Intent; 22 import android.util.Log; 23 24 import androidx.test.InstrumentationRegistry; 25 import androidx.test.runner.AndroidJUnit4; 26 27 import junit.framework.Assert; 28 29 import org.junit.Before; 30 import org.junit.Test; 31 import org.junit.runner.RunWith; 32 33 import java.io.File; 34 import java.io.FileFilter; 35 import java.io.IOException; 36 import java.io.RandomAccessFile; 37 import java.util.concurrent.TimeoutException; 38 39 /** 40 * Run the tests with: <code>adb shell am instrument -w 41 * com.android.framework.multidexlegacytestservices.test2/androidx.test.runner.AndroidJUnitRunner 42 * </code> 43 */ 44 @RunWith(AndroidJUnit4.class) 45 public class ServicesTests { 46 private static final String TAG = "ServicesTests"; 47 48 static { Log.i(TAG, R)49 Log.i(TAG, "Initializing"); 50 } 51 52 private class ExtensionFilter implements FileFilter { 53 private final String ext; 54 ExtensionFilter(String ext)55 public ExtensionFilter(String ext) { 56 this.ext = ext; 57 } 58 59 @Override accept(File file)60 public boolean accept(File file) { 61 return file.getName().endsWith(ext); 62 } 63 } 64 65 private class ExtractedZipFilter extends ExtensionFilter { ExtractedZipFilter()66 public ExtractedZipFilter() { 67 super(".zip"); 68 } 69 70 @Override accept(File file)71 public boolean accept(File file) { 72 return super.accept(file) && !file.getName().startsWith("tmp-"); 73 } 74 } 75 76 private static final int ENDHDR = 22; 77 78 private static final String SERVICE_BASE_ACTION = 79 "com.android.framework.multidexlegacytestservices.action.Service"; 80 private static final int MIN_SERVICE = 1; 81 private static final int MAX_SERVICE = 19; 82 private static final String COMPLETION_SUCCESS = "Success"; 83 84 private File targetFilesDir; 85 86 @Before setup()87 public void setup() throws Exception { 88 Log.i(TAG, "setup"); 89 killServices(); 90 91 File applicationDataDir = 92 new File(InstrumentationRegistry.getTargetContext().getApplicationInfo().dataDir); 93 clearDirContent(applicationDataDir); 94 targetFilesDir = InstrumentationRegistry.getTargetContext().getFilesDir(); 95 96 Log.i(TAG, "setup done"); 97 } 98 99 @Test testStressConcurentLaunch()100 public void testStressConcurentLaunch() throws Exception { 101 startServices(); 102 waitServicesCompletion(); 103 String completionStatus = getServicesCompletionStatus(); 104 if (completionStatus != COMPLETION_SUCCESS) { 105 Assert.fail(completionStatus); 106 } 107 } 108 109 @Test testRecoverFromZipCorruption()110 public void testRecoverFromZipCorruption() throws Exception { 111 int serviceId = 1; 112 // Ensure extraction. 113 initServicesWorkFiles(); 114 startService(serviceId); 115 waitServicesCompletion(serviceId); 116 117 // Corruption of the extracted zips. 118 tamperAllExtractedZips(); 119 120 killServices(); 121 checkRecover(); 122 } 123 124 @Test testRecoverFromDexCorruption()125 public void testRecoverFromDexCorruption() throws Exception { 126 int serviceId = 1; 127 // Ensure extraction. 128 initServicesWorkFiles(); 129 startService(serviceId); 130 waitServicesCompletion(serviceId); 131 132 // Corruption of the odex files. 133 tamperAllOdex(); 134 135 killServices(); 136 checkRecover(); 137 } 138 139 @Test testRecoverFromZipCorruptionStressTest()140 public void testRecoverFromZipCorruptionStressTest() throws Exception { 141 Thread startServices = 142 new Thread() { 143 @Override 144 public void run() { 145 startServices(); 146 } 147 }; 148 149 startServices.start(); 150 151 // Start services lasts more than 80s, lets cause a few corruptions during this interval. 152 for (int i = 0; i < 80; i++) { 153 Thread.sleep(1000); 154 tamperAllExtractedZips(); 155 } 156 startServices.join(); 157 try { 158 waitServicesCompletion(); 159 } catch (TimeoutException e) { 160 // Can happen. 161 } 162 163 killServices(); 164 checkRecover(); 165 } 166 167 @Test testRecoverFromDexCorruptionStressTest()168 public void testRecoverFromDexCorruptionStressTest() throws Exception { 169 Thread startServices = 170 new Thread() { 171 @Override 172 public void run() { 173 startServices(); 174 } 175 }; 176 177 startServices.start(); 178 179 // Start services lasts more than 80s, lets cause a few corruptions during this interval. 180 for (int i = 0; i < 80; i++) { 181 Thread.sleep(1000); 182 tamperAllOdex(); 183 } 184 startServices.join(); 185 try { 186 waitServicesCompletion(); 187 } catch (TimeoutException e) { 188 // Will probably happen most of the time considering what we're doing... 189 } 190 191 killServices(); 192 checkRecover(); 193 } 194 clearDirContent(File dir)195 private static void clearDirContent(File dir) { 196 for (File subElement : dir.listFiles()) { 197 if (subElement.isDirectory()) { 198 clearDirContent(subElement); 199 } 200 if (!subElement.delete()) { 201 throw new AssertionError("Failed to clear '" + subElement.getAbsolutePath() + "'"); 202 } 203 } 204 } 205 startServices()206 private void startServices() { 207 Log.i(TAG, "start services"); 208 initServicesWorkFiles(); 209 for (int i = MIN_SERVICE; i <= MAX_SERVICE; i++) { 210 startService(i); 211 try { 212 Thread.sleep((i - 1) * (1 << (i / 5))); 213 } catch (InterruptedException e) { 214 } 215 } 216 } 217 startService(int serviceId)218 private void startService(int serviceId) { 219 Log.i(TAG, "start service " + serviceId); 220 InstrumentationRegistry.getContext().startService(new Intent(SERVICE_BASE_ACTION + serviceId)); 221 } 222 initServicesWorkFiles()223 private void initServicesWorkFiles() { 224 for (int i = MIN_SERVICE; i <= MAX_SERVICE; i++) { 225 File resultFile = new File(targetFilesDir, "Service" + i); 226 resultFile.delete(); 227 Assert.assertFalse( 228 "Failed to delete result file '" + resultFile.getAbsolutePath() + "'.", 229 resultFile.exists()); 230 File completeFile = new File(targetFilesDir, "Service" + i + ".complete"); 231 completeFile.delete(); 232 Assert.assertFalse( 233 "Failed to delete completion file '" + completeFile.getAbsolutePath() + "'.", 234 completeFile.exists()); 235 } 236 } 237 waitServicesCompletion()238 private void waitServicesCompletion() throws TimeoutException { 239 Log.i(TAG, "start sleeping"); 240 int attempt = 0; 241 int maxAttempt = 50; // 10 is enough for a nexus S 242 do { 243 try { 244 Thread.sleep(5000); 245 } catch (InterruptedException e) { 246 } 247 attempt++; 248 if (attempt >= maxAttempt) { 249 throw new TimeoutException(); 250 } 251 } while (!areAllServicesCompleted()); 252 } 253 waitServicesCompletion(int serviceId)254 private void waitServicesCompletion(int serviceId) throws TimeoutException { 255 Log.i(TAG, "start sleeping"); 256 int attempt = 0; 257 int maxAttempt = 50; // 10 is enough for a nexus S 258 do { 259 try { 260 Thread.sleep(5000); 261 } catch (InterruptedException e) { 262 } 263 attempt++; 264 if (attempt >= maxAttempt) { 265 throw new TimeoutException(); 266 } 267 } while (isServiceRunning(serviceId)); 268 } 269 getServicesCompletionStatus()270 private String getServicesCompletionStatus() { 271 String status = COMPLETION_SUCCESS; 272 for (int i = MIN_SERVICE; i <= MAX_SERVICE; i++) { 273 File resultFile = new File(targetFilesDir, "Service" + i); 274 if (!resultFile.isFile()) { 275 status = "Service" + i + " never completed."; 276 break; 277 } 278 if (resultFile.length() != 8) { 279 status = "Service" + i + " was restarted."; 280 break; 281 } 282 } 283 Log.i(TAG, "Services completion status: " + status); 284 return status; 285 } 286 getServiceCompletionStatus(int serviceId)287 private String getServiceCompletionStatus(int serviceId) { 288 String status = COMPLETION_SUCCESS; 289 File resultFile = new File(targetFilesDir, "Service" + serviceId); 290 if (!resultFile.isFile()) { 291 status = "Service" + serviceId + " never completed."; 292 } else if (resultFile.length() != 8) { 293 status = "Service" + serviceId + " was restarted."; 294 } 295 Log.i(TAG, "Service " + serviceId + " completion status: " + status); 296 return status; 297 } 298 areAllServicesCompleted()299 private boolean areAllServicesCompleted() { 300 for (int i = MIN_SERVICE; i <= MAX_SERVICE; i++) { 301 if (isServiceRunning(i)) { 302 return false; 303 } 304 } 305 return true; 306 } 307 isServiceRunning(int i)308 private boolean isServiceRunning(int i) { 309 File completeFile = new File(targetFilesDir, "Service" + i + ".complete"); 310 return !completeFile.exists(); 311 } 312 getSecondaryFolder()313 private File getSecondaryFolder() { 314 File dir = 315 new File( 316 new File( 317 InstrumentationRegistry.getTargetContext().getApplicationInfo().dataDir, 318 "code_cache"), 319 "secondary-dexes"); 320 Assert.assertTrue(dir.getAbsolutePath(), dir.isDirectory()); 321 return dir; 322 } 323 tamperAllExtractedZips()324 private void tamperAllExtractedZips() throws IOException { 325 // First attempt was to just overwrite zip entries but keep central directory, this was no 326 // trouble for Dalvik that was just ignoring those zip and using the odex files. 327 Log.i(TAG, "Tamper extracted zip files by overwriting all content by '\\0's."); 328 byte[] zeros = new byte[4 * 1024]; 329 // Do not tamper tmp zip during their extraction. 330 for (File zip : getSecondaryFolder().listFiles(new ExtractedZipFilter())) { 331 long fileLength = zip.length(); 332 Assert.assertTrue(fileLength > ENDHDR); 333 zip.setWritable(true); 334 RandomAccessFile raf = new RandomAccessFile(zip, "rw"); 335 try { 336 int index = 0; 337 while (index < fileLength) { 338 int length = (int) Math.min(zeros.length, fileLength - index); 339 raf.write(zeros, 0, length); 340 index += length; 341 } 342 } finally { 343 raf.close(); 344 } 345 } 346 } 347 tamperAllOdex()348 private void tamperAllOdex() throws IOException { 349 Log.i(TAG, "Tamper odex files by overwriting some content by '\\0's."); 350 byte[] zeros = new byte[4 * 1024]; 351 // I think max size would be 40 (u1[8] + 8 u4) but it's a test so lets take big margins. 352 int savedSizeForOdexHeader = 80; 353 for (File odex : getSecondaryFolder().listFiles(new ExtensionFilter(".dex"))) { 354 long fileLength = odex.length(); 355 Assert.assertTrue(fileLength > zeros.length + savedSizeForOdexHeader); 356 odex.setWritable(true); 357 RandomAccessFile raf = new RandomAccessFile(odex, "rw"); 358 try { 359 raf.seek(savedSizeForOdexHeader); 360 raf.write(zeros, 0, zeros.length); 361 } finally { 362 raf.close(); 363 } 364 } 365 } 366 checkRecover()367 private void checkRecover() throws TimeoutException { 368 Log.i(TAG, "Check recover capability"); 369 int serviceId = 1; 370 // Start one service and check it was able to run correctly even if a previous run failed. 371 initServicesWorkFiles(); 372 startService(serviceId); 373 waitServicesCompletion(serviceId); 374 String completionStatus = getServiceCompletionStatus(serviceId); 375 if (completionStatus != COMPLETION_SUCCESS) { 376 Assert.fail(completionStatus); 377 } 378 } 379 killServices()380 private void killServices() { 381 ((ActivityManager) 382 InstrumentationRegistry.getContext().getSystemService(Context.ACTIVITY_SERVICE)) 383 .killBackgroundProcesses("com.android.framework.multidexlegacytestservices"); 384 } 385 } 386