1 /*
2  * Copyright (C) 2021 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 android.car.cts.app;
18 
19 import android.app.Activity;
20 import android.car.Car;
21 import android.car.watchdog.CarWatchdogManager;
22 import android.car.watchdog.IoOveruseStats;
23 import android.car.watchdog.ResourceOveruseStats;
24 import android.content.Intent;
25 import android.os.Bundle;
26 import android.os.SystemClock;
27 import android.util.Log;
28 
29 import androidx.annotation.GuardedBy;
30 
31 import java.io.File;
32 import java.io.FileDescriptor;
33 import java.io.FileOutputStream;
34 import java.io.IOException;
35 import java.io.InterruptedIOException;
36 import java.io.PrintWriter;
37 import java.nio.file.Files;
38 import java.util.concurrent.ExecutorService;
39 import java.util.concurrent.Executors;
40 
41 public final class CarWatchdogTestActivity extends Activity {
42     private static final String TAG = CarWatchdogTestActivity.class.getSimpleName();
43     private static final String BYTES_TO_KILL = "bytes_to_kill";
44     private static final long TEN_MEGABYTES = 1024 * 1024 * 10;
45     private static final long TWO_HUNDRED_MEGABYTES = 1024 * 1024 * 200;
46     private static final int DISK_DELAY_MS = 4000;
47     private static final double WARN_THRESHOLD_PERCENT = 0.8;
48     private static final double EXCEED_WARN_THRESHOLD_PERCENT = 0.9;
49 
50     private final ExecutorService mExecutor = Executors.newSingleThreadExecutor();
51     private final Object mLock = new Object();
52 
53     @GuardedBy("mLock")
54     private CarWatchdogManager mCarWatchdogManager;
55 
56     private String mDumpMessage = "";
57     private Car mCar;
58     private File mTestDir;
59 
60     @Override
onCreate(Bundle savedInstanceState)61     protected void onCreate(Bundle savedInstanceState) {
62         super.onCreate(savedInstanceState);
63 
64         initCarApi();
65         try {
66             mTestDir =
67                     Files.createTempDirectory(getFilesDir().toPath(), "testDir").toFile();
68         } catch (IOException e) {
69             setDumpMessage("ERROR: " + e.getMessage());
70             finish();
71             return;
72         }
73         mExecutor.execute(
74                 () -> {
75                     synchronized (mLock) {
76                         if (mCarWatchdogManager == null) {
77                             Log.e(TAG, "CarWatchdogManager is null.");
78                             finish();
79                             return;
80                         }
81                     }
82                     IoOveruseListener listener = addResourceOveruseListener();
83                     try {
84                         if (!writeToDisk(TEN_MEGABYTES)) {
85                             finish();
86                             return;
87                         }
88 
89                         long remainingBytes = fetchRemainingBytes(TEN_MEGABYTES);
90                         if (remainingBytes == 0) {
91                             Log.d(TAG, "Remaining bytes is 0 after writing " + TEN_MEGABYTES
92                                     + " bytes to disk.");
93                             finish();
94                             return;
95                         }
96 
97                         /*
98                          * Warning notification is received as soon as exceeding
99                          * |WARN_THRESHOLD_PERCENT|. So, set expected minimum written bytes to
100                          * |WARN_THRESHOLD_PERCENT| of the overuse threshold.
101                          */
102                         long bytesToWarnThreshold =
103                                 (long) (TWO_HUNDRED_MEGABYTES * WARN_THRESHOLD_PERCENT);
104 
105                         listener.setExpectedMinWrittenBytes(bytesToWarnThreshold);
106 
107                         long bytesToExceedWarnThreshold =
108                                 (long) Math.ceil(remainingBytes
109                                         * EXCEED_WARN_THRESHOLD_PERCENT);
110 
111                         if (!writeToDisk(bytesToExceedWarnThreshold)) {
112                             finish();
113                             return;
114                         }
115 
116                         listener.checkIsNotified();
117                     } finally {
118                         synchronized (mLock) {
119                             mCarWatchdogManager.removeResourceOveruseListener(listener);
120                         }
121                         /* Foreground mode bytes dumped after removing listener to ensure hostside
122                          * receives dump message after test is finished.
123                          */
124                         listener.dumpForegroundModeBytes();
125                     }
126                 });
127     }
128 
129     @Override
onNewIntent(Intent intent)130     protected void onNewIntent(Intent intent) {
131         super.onNewIntent(intent);
132 
133         setDumpMessage("");
134         Bundle extras = intent.getExtras();
135         if (extras == null) {
136             Log.w(TAG, "onNewIntent: empty extras");
137             return;
138         }
139         long remainingBytes = extras.getLong(BYTES_TO_KILL);
140         Log.d(TAG, "Bytes to kill: " + remainingBytes);
141         if (remainingBytes == 0) {
142             Log.w(TAG, "onNewIntent: remaining bytes is 0");
143             return;
144         }
145         mExecutor.execute(() -> {
146             synchronized (mLock) {
147                 if (mCarWatchdogManager == null) {
148                     Log.e(TAG, "onNewIntent: CarWatchdogManager is null.");
149                     finish();
150                     return;
151                 }
152             }
153             IoOveruseListener listener = addResourceOveruseListener();
154             try {
155                 listener.setExpectedMinWrittenBytes(TWO_HUNDRED_MEGABYTES);
156 
157                 writeToDisk(remainingBytes);
158 
159                 listener.checkIsNotified();
160             } finally {
161                 synchronized (mLock) {
162                     mCarWatchdogManager.removeResourceOveruseListener(listener);
163                 }
164                 /* Foreground mode bytes dumped after removing listener to ensure hostside
165                  * receives dump message after test is finished.
166                  */
167                 listener.dumpForegroundModeBytes();
168             }
169         });
170     }
171 
172     @Override
dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args)173     public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
174         if (mDumpMessage.isEmpty()) {
175             return;
176         }
177         writer.printf("%s: %s\n", TAG, mDumpMessage);
178         Log.i(TAG, "Dumping message: '" + mDumpMessage + "'");
179     }
180 
181     @Override
onDestroy()182     protected void onDestroy() {
183         if (mCar != null) {
184             mCar.disconnect();
185         }
186         if (mTestDir.delete()) {
187             Log.i(TAG, "Deleted directory '" + mTestDir.getAbsolutePath() + "' successfully");
188         } else {
189             Log.e(TAG, "Failed to delete directory '" + mTestDir.getAbsolutePath() + "'");
190         }
191         super.onDestroy();
192     }
193 
initCarApi()194     private void initCarApi() {
195         if (mCar != null && mCar.isConnected()) {
196             mCar.disconnect();
197             mCar = null;
198         }
199         mCar = Car.createCar(this, null, Car.CAR_WAIT_TIMEOUT_WAIT_FOREVER,
200                 this::initManagers);
201     }
202 
initManagers(Car car, boolean ready)203     private void initManagers(Car car, boolean ready) {
204         synchronized (mLock) {
205             if (ready) {
206                 mCarWatchdogManager = (CarWatchdogManager) car.getCarManager(
207                         Car.CAR_WATCHDOG_SERVICE);
208                 Log.d(TAG, "initManagers() completed");
209             } else {
210                 mCarWatchdogManager = null;
211                 Log.wtf(TAG, "mCarWatchdogManager set to be null");
212             }
213         }
214     }
215 
addResourceOveruseListener()216     private IoOveruseListener addResourceOveruseListener() {
217         IoOveruseListener listener = new IoOveruseListener();
218         synchronized (mLock) {
219             mCarWatchdogManager.addResourceOveruseListener(getMainExecutor(),
220                     CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO, listener);
221         }
222         return listener;
223     }
224 
writeToDisk(long bytes)225     private boolean writeToDisk(long bytes) {
226         File uniqueFile = new File(mTestDir, Long.toString(System.nanoTime()));
227         boolean result = writeToFile(uniqueFile, bytes);
228         if (uniqueFile.delete()) {
229             Log.i(TAG, "Deleted file: " + uniqueFile.getAbsolutePath());
230         } else {
231             Log.e(TAG, "Failed to delete file: " + uniqueFile.getAbsolutePath());
232         }
233         return result;
234     }
235 
writeToFile(File uniqueFile, long bytes)236     private boolean writeToFile(File uniqueFile, long bytes) {
237         long writtenBytes = 0;
238         try (FileOutputStream fos = new FileOutputStream(uniqueFile)) {
239             Log.d(TAG, "Attempting to write " + bytes + " bytes");
240             writtenBytes = writeToFos(fos, bytes);
241             if (writtenBytes < bytes) {
242                 setDumpMessage("ERROR: Failed to write '" + bytes
243                         + "' bytes to disk. '" + writtenBytes
244                         + "' bytes were successfully written, while '" + (bytes - writtenBytes)
245                         + "' bytes were pending at the moment the exception occurred.");
246                 return false;
247             }
248             fos.getFD().sync();
249             // Wait for the IO event to propagate to the system
250             Thread.sleep(DISK_DELAY_MS);
251             return true;
252         } catch (IOException | InterruptedException e) {
253             String message;
254             if (e instanceof IOException) {
255                 message = "I/O exception";
256             } else {
257                 message = "Thread interrupted";
258                 Thread.currentThread().interrupt();
259             }
260             if (writtenBytes > 0) {
261                 message += " after successfully writing to disk.";
262             }
263             Log.e(TAG, message, e);
264             setDumpMessage("ERROR: " + message);
265             return false;
266         }
267     }
268 
writeToFos(FileOutputStream fos, long remainingBytes)269     private long writeToFos(FileOutputStream fos, long remainingBytes) {
270         long totalBytesWritten = 0;
271         while (remainingBytes != 0) {
272             int writeBytes =
273                     (int) Math.min(Integer.MAX_VALUE,
274                             Math.min(Runtime.getRuntime().freeMemory(), remainingBytes));
275             try {
276                 fos.write(new byte[writeBytes]);
277             }  catch (InterruptedIOException e) {
278                 Thread.currentThread().interrupt();
279                 continue;
280             } catch (IOException e) {
281                 Log.e(TAG, "I/O exception while writing " + writeBytes + " to disk", e);
282                 return totalBytesWritten;
283             }
284             totalBytesWritten += writeBytes;
285             remainingBytes -= writeBytes;
286             if (writeBytes > 0 && remainingBytes > 0) {
287                 Log.i(TAG, "Total bytes written: " + totalBytesWritten + "/"
288                         + (totalBytesWritten + remainingBytes));
289             }
290         }
291         Log.d(TAG, "Write completed.");
292         return totalBytesWritten;
293     }
294 
fetchRemainingBytes(long minWrittenBytes)295     private long fetchRemainingBytes(long minWrittenBytes) {
296         ResourceOveruseStats stats;
297         synchronized (mLock) {
298             stats = mCarWatchdogManager.getResourceOveruseStats(
299                     CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO,
300                     CarWatchdogManager.STATS_PERIOD_CURRENT_DAY);
301         }
302         IoOveruseStats ioOveruseStats = stats.getIoOveruseStats();
303         if (ioOveruseStats == null) {
304             setDumpMessage(
305                     "ERROR: No I/O overuse stats available for the application after writing "
306                     + minWrittenBytes + " bytes.");
307             return 0;
308         }
309         if (ioOveruseStats.getTotalBytesWritten() < minWrittenBytes) {
310             setDumpMessage("ERROR: Actual written bytes to disk '" + minWrittenBytes
311                     + "' don't match written bytes '" + ioOveruseStats.getTotalBytesWritten()
312                     + "' returned by get request");
313             return 0;
314         }
315         Log.d(TAG, ioOveruseStats.toString());
316         /*
317          * Check for foreground mode bytes given CtsCarApp is running in the foreground
318          * during testing.
319          */
320         return ioOveruseStats.getRemainingWriteBytes().getForegroundModeBytes();
321     }
322 
setDumpMessage(String message)323     private void setDumpMessage(String message) {
324         if (mDumpMessage.startsWith("ERROR:")) {
325             mDumpMessage += ", " + message;
326         } else {
327             mDumpMessage = message;
328         }
329     }
330 
331     private final class IoOveruseListener
332             implements CarWatchdogManager.ResourceOveruseListener {
333         private static final int NOTIFICATION_DELAY_MS = 5000;
334 
335         private final Object mLock = new Object();
336         @GuardedBy("mLock")
337         private boolean mNotificationReceived;
338         @GuardedBy("mLock")
339         private long mForegroundModeBytes;
340 
341         private long mExpectedMinWrittenBytes;
342 
343         @Override
onOveruse(ResourceOveruseStats resourceOveruseStats)344         public void onOveruse(ResourceOveruseStats resourceOveruseStats) {
345             synchronized (mLock) {
346                 mForegroundModeBytes = -1;
347                 mNotificationReceived = true;
348                 mLock.notifyAll();
349             }
350             Log.d(TAG, resourceOveruseStats.toString());
351             if (resourceOveruseStats.getIoOveruseStats() == null) {
352                 setDumpMessage(
353                         "ERROR: No I/O overuse stats reported for the application in the overuse "
354                         + "notification.");
355                 return;
356             }
357             long reportedWrittenBytes =
358                     resourceOveruseStats.getIoOveruseStats().getTotalBytesWritten();
359             if (reportedWrittenBytes < mExpectedMinWrittenBytes) {
360                 setDumpMessage("ERROR: Actual written bytes to disk '" + mExpectedMinWrittenBytes
361                         + "' don't match written bytes '" + reportedWrittenBytes
362                         + "' reported in overuse notification");
363                 return;
364             }
365             synchronized (mLock) {
366                 mForegroundModeBytes =
367                         resourceOveruseStats.getIoOveruseStats().getRemainingWriteBytes()
368                                 .getForegroundModeBytes();
369             }
370         }
371 
dumpForegroundModeBytes()372         public void dumpForegroundModeBytes() {
373             synchronized (mLock) {
374                 setDumpMessage(
375                         "INFO: --Notification-- foregroundModeBytes = " + mForegroundModeBytes);
376             }
377         }
378 
setExpectedMinWrittenBytes(long expectedMinWrittenBytes)379         public void setExpectedMinWrittenBytes(long expectedMinWrittenBytes) {
380             mExpectedMinWrittenBytes = expectedMinWrittenBytes;
381         }
382 
checkIsNotified()383         public void checkIsNotified() {
384             synchronized (mLock) {
385                 long now = SystemClock.uptimeMillis();
386                 long deadline = now + NOTIFICATION_DELAY_MS;
387                 while (!mNotificationReceived && now < deadline) {
388                     try {
389                         mLock.wait(deadline - now);
390                     } catch (InterruptedException e) {
391                         Thread.currentThread().interrupt();
392                         continue;
393                     } finally {
394                         now = SystemClock.uptimeMillis();
395                     }
396                     break;
397                 }
398                 if (!mNotificationReceived) {
399                     setDumpMessage("ERROR: I/O Overuse notification not received.");
400                 }
401             }
402         }
403     }
404 }
405