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