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