Create thread to dump shutdown check points to file
Create new method ShutdownCheckPoints#createDumpThread that
creates a thread that calls ShutdownCheckPoints#dump on a local file and
also limits the total number of files created by deleting the oldest
ones.
Bug: 113147108
Test: atest FrameworksServicesTests:ShutdownCheckPointsTest
Change-Id: I62c8eedba2ff4c10ad66636c57de07fd1370c116
diff --git a/services/core/java/com/android/server/power/ShutdownCheckPoints.java b/services/core/java/com/android/server/power/ShutdownCheckPoints.java
index c85a77a..e6d0ddd 100644
--- a/services/core/java/com/android/server/power/ShutdownCheckPoints.java
+++ b/services/core/java/com/android/server/power/ShutdownCheckPoints.java
@@ -21,13 +21,20 @@
import android.app.IActivityManager;
import android.os.Process;
import android.os.RemoteException;
+import android.util.AtomicFile;
+import android.util.Log;
import android.util.Slog;
import com.android.internal.annotations.VisibleForTesting;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.FilenameFilter;
+import java.io.IOException;
import java.io.PrintWriter;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
@@ -45,6 +52,7 @@
private static final ShutdownCheckPoints INSTANCE = new ShutdownCheckPoints();
private static final int MAX_CHECK_POINTS = 100;
+ private static final int MAX_DUMP_FILES = 20;
private static final SimpleDateFormat DATE_FORMAT =
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS z");
@@ -64,6 +72,11 @@
}
@Override
+ public int maxDumpFiles() {
+ return MAX_DUMP_FILES;
+ }
+
+ @Override
public IActivityManager activityManager() {
return ActivityManager.getService();
}
@@ -96,6 +109,15 @@
INSTANCE.dumpInternal(printWriter);
}
+ /**
+ * Creates a {@link Thread} that calls {@link #dump(PrintWriter)} on a rotating file created
+ * from given {@code baseFile} and a timestamp suffix. Older dump files are also deleted by this
+ * thread.
+ */
+ public static Thread newDumpThread(File baseFile) {
+ return INSTANCE.newDumpThreadInternal(baseFile);
+ }
+
@VisibleForTesting
void recordCheckPointInternal() {
recordCheckPointInternal(new SystemServerCheckPoint(mInjector));
@@ -138,13 +160,21 @@
}
}
+ @VisibleForTesting
+ Thread newDumpThreadInternal(File baseFile) {
+ return new FileDumperThread(this, baseFile, mInjector.maxDumpFiles());
+ }
+
/** Injector used by {@link ShutdownCheckPoints} for testing purposes. */
@VisibleForTesting
interface Injector {
+
long currentTimeMillis();
int maxCheckPoints();
+ int maxDumpFiles();
+
IActivityManager activityManager();
}
@@ -296,4 +326,74 @@
printWriter.println(mPackageName);
}
}
+
+ /**
+ * Thread that writes {@link ShutdownCheckPoints#dumpInternal(PrintWriter)} to a new file and
+ * deletes old ones to keep the total number of files down to a given limit.
+ */
+ private static final class FileDumperThread extends Thread {
+
+ private final ShutdownCheckPoints mInstance;
+ private final File mBaseFile;
+ private final int mFileCountLimit;
+
+ FileDumperThread(ShutdownCheckPoints instance, File baseFile, int fileCountLimit) {
+ mInstance = instance;
+ mBaseFile = baseFile;
+ mFileCountLimit = fileCountLimit;
+ }
+
+ @Override
+ public void run() {
+ mBaseFile.getParentFile().mkdirs();
+ File[] checkPointFiles = listCheckPointsFiles();
+
+ int filesToDelete = checkPointFiles.length - mFileCountLimit + 1;
+ for (int i = 0; i < filesToDelete; i++) {
+ checkPointFiles[i].delete();
+ }
+
+ File nextCheckPointsFile = new File(String.format("%s-%d",
+ mBaseFile.getAbsolutePath(), System.currentTimeMillis()));
+ writeCheckpoints(nextCheckPointsFile);
+ }
+
+ private File[] listCheckPointsFiles() {
+ String filePrefix = mBaseFile.getName() + "-";
+ File[] files = mBaseFile.getParentFile().listFiles(new FilenameFilter() {
+ @Override
+ public boolean accept(File dir, String name) {
+ if (!name.startsWith(filePrefix)) {
+ return false;
+ }
+ try {
+ Long.valueOf(name.substring(filePrefix.length()));
+ } catch (NumberFormatException e) {
+ return false;
+ }
+ return true;
+ }
+ });
+ Arrays.sort(files);
+ return files;
+ }
+
+ private void writeCheckpoints(File file) {
+ AtomicFile tmpFile = new AtomicFile(mBaseFile);
+ FileOutputStream fos = null;
+ try {
+ fos = tmpFile.startWrite();
+ PrintWriter pw = new PrintWriter(fos);
+ mInstance.dumpInternal(pw);
+ pw.flush();
+ tmpFile.finishWrite(fos); // This also closes the output stream.
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to write shutdown checkpoints", e);
+ if (fos != null) {
+ tmpFile.failWrite(fos); // This also closes the output stream.
+ }
+ }
+ mBaseFile.renameTo(file);
+ }
+ }
}
diff --git a/services/core/java/com/android/server/power/ShutdownThread.java b/services/core/java/com/android/server/power/ShutdownThread.java
index 2c1fe58..2621d8b 100644
--- a/services/core/java/com/android/server/power/ShutdownThread.java
+++ b/services/core/java/com/android/server/power/ShutdownThread.java
@@ -43,7 +43,6 @@
import android.os.Vibrator;
import android.telephony.TelephonyManager;
import android.util.ArrayMap;
-import android.util.AtomicFile;
import android.util.Log;
import android.util.Slog;
import android.util.TimingsTraceLog;
@@ -57,7 +56,6 @@
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
-import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
public final class ShutdownThread extends Thread {
@@ -428,7 +426,9 @@
metricStarted(METRIC_SYSTEM_SERVER);
// Start dumping check points for this shutdown in a separate thread.
- Thread dumpCheckPointsThread = startCheckPointsDump();
+ Thread dumpCheckPointsThread = ShutdownCheckPoints.newDumpThread(
+ new File(CHECK_POINTS_FILE_BASENAME));
+ dumpCheckPointsThread.start();
BroadcastReceiver br = new BroadcastReceiver() {
@Override public void onReceive(Context context, Intent intent) {
@@ -728,25 +728,6 @@
}
}
- private Thread startCheckPointsDump() {
- Thread thread = new Thread() {
- public void run() {
- // Writes to a file with .new suffix and then renames it to final file name.
- // The final file won't be left with partial data if this thread is interrupted.
- AtomicFile file = new AtomicFile(new File(CHECK_POINTS_FILE_BASENAME));
- try (FileOutputStream fos = file.startWrite()) {
- PrintWriter pw = new PrintWriter(fos);
- ShutdownCheckPoints.dump(pw);
- file.finishWrite(fos);
- } catch (IOException e) {
- Log.e(TAG, "Cannot save shutdown checkpoints", e);
- }
- }
- };
- thread.start();
- return thread;
- }
-
private void uncrypt() {
Log.i(TAG, "Calling uncrypt and monitoring the progress...");
diff --git a/services/tests/servicestests/src/com/android/server/power/ShutdownCheckPointsTest.java b/services/tests/servicestests/src/com/android/server/power/ShutdownCheckPointsTest.java
index 7ffd0d0..a1f1056 100644
--- a/services/tests/servicestests/src/com/android/server/power/ShutdownCheckPointsTest.java
+++ b/services/tests/servicestests/src/com/android/server/power/ShutdownCheckPointsTest.java
@@ -33,9 +33,15 @@
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
+import java.io.File;
+import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Paths;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
@@ -59,7 +65,7 @@
public void setUp() {
Locale.setDefault(Locale.UK);
TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
- mTestInjector = new TestInjector(0, 100, mActivityManager);
+ mTestInjector = new TestInjector(mActivityManager);
mInstance = new ShutdownCheckPoints(mTestInjector);
}
@@ -207,6 +213,67 @@
dumpToString(limitedInstance));
}
+ @Test
+ public void testDumpToFile() throws Exception {
+ File tempDir = createTempDir();
+ File baseFile = new File(tempDir, "checkpoints");
+
+ mTestInjector.setCurrentTime(1000);
+ mInstance.recordCheckPointInternal("first.intent", "first.app");
+ dumpToFile(baseFile);
+
+ mTestInjector.setCurrentTime(2000);
+ mInstance.recordCheckPointInternal("second.intent", "second.app");
+ dumpToFile(baseFile);
+
+ File[] dumpFiles = tempDir.listFiles();
+ Arrays.sort(dumpFiles);
+
+ assertEquals(2, dumpFiles.length);
+ assertEquals(
+ "Shutdown request from INTENT at 1970-01-01 00:00:01.000 UTC (epoch=1000)\n"
+ + "Intent: first.intent\n"
+ + "Package: first.app\n\n",
+ readFileAsString(dumpFiles[0].getAbsolutePath()));
+ assertEquals(
+ "Shutdown request from INTENT at 1970-01-01 00:00:01.000 UTC (epoch=1000)\n"
+ + "Intent: first.intent\n"
+ + "Package: first.app\n\n"
+ + "Shutdown request from INTENT at 1970-01-01 00:00:02.000 UTC (epoch=2000)"
+ + "\n"
+ + "Intent: second.intent\n"
+ + "Package: second.app\n\n",
+ readFileAsString(dumpFiles[1].getAbsolutePath()));
+ }
+
+ @Test
+ public void testTooManyFilesDropsOlderOnes() throws Exception {
+ mTestInjector.setDumpFilesLimit(1);
+ ShutdownCheckPoints instance = new ShutdownCheckPoints(mTestInjector);
+ File tempDir = createTempDir();
+ File baseFile = new File(tempDir, "checkpoints");
+
+ mTestInjector.setCurrentTime(1000);
+ instance.recordCheckPointInternal("first.intent", "first.app");
+ dumpToFile(instance, baseFile);
+
+ mTestInjector.setCurrentTime(2000);
+ instance.recordCheckPointInternal("second.intent", "second.app");
+ dumpToFile(instance, baseFile);
+
+ File[] dumpFiles = tempDir.listFiles();
+ assertEquals(1, dumpFiles.length);
+ assertEquals(
+ "Shutdown request from INTENT at 1970-01-01 00:00:01.000 UTC (epoch=1000)\n"
+ + "Intent: first.intent\n"
+ + "Package: first.app\n\n"
+ + "Shutdown request from INTENT at 1970-01-01 00:00:02.000 UTC (epoch=2000)"
+ + "\n"
+ + "Intent: second.intent\n"
+ + "Package: second.app\n\n",
+ readFileAsString(dumpFiles[0].getAbsolutePath()));
+ }
+
private String dumpToString() {
return dumpToString(mInstance);
}
@@ -218,15 +285,39 @@
return sw.toString();
}
+ private void dumpToFile(File baseFile) throws InterruptedException {
+ dumpToFile(mInstance, baseFile);
+ }
+
+ private void dumpToFile(ShutdownCheckPoints instance, File baseFile)
+ throws InterruptedException {
+ Thread dumpThread = instance.newDumpThreadInternal(baseFile);
+ dumpThread.start();
+ dumpThread.join();
+ }
+
+ private String readFileAsString(String absolutePath) throws IOException {
+ return new String(Files.readAllBytes(Paths.get(absolutePath)), StandardCharsets.UTF_8);
+ }
+
+ private File createTempDir() throws IOException {
+ File tempDir = File.createTempFile("checkpoints", "out");
+ tempDir.delete();
+ tempDir.mkdir();
+ return tempDir;
+ }
+
/** Fake system dependencies for testing. */
private final class TestInjector implements ShutdownCheckPoints.Injector {
private long mNow;
- private int mLimit;
+ private int mCheckPointsLimit;
+ private int mDumpFilesLimit;
private IActivityManager mActivityManager;
- TestInjector(long now, int limit, IActivityManager activityManager) {
- mNow = now;
- mLimit = limit;
+ TestInjector(IActivityManager activityManager) {
+ mNow = 0;
+ mCheckPointsLimit = 100;
+ mDumpFilesLimit = 2;
mActivityManager = activityManager;
}
@@ -237,7 +328,12 @@
@Override
public int maxCheckPoints() {
- return mLimit;
+ return mCheckPointsLimit;
+ }
+
+ @Override
+ public int maxDumpFiles() {
+ return mDumpFilesLimit;
}
@Override
@@ -250,7 +346,11 @@
}
void setCheckPointsLimit(int limit) {
- mLimit = limit;
+ mCheckPointsLimit = limit;
+ }
+
+ void setDumpFilesLimit(int dumpFilesLimit) {
+ mDumpFilesLimit = dumpFilesLimit;
}
}
}