1 /*
2  * Copyright (C) 2020 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.server.power;
18 
19 import android.annotation.Nullable;
20 import android.app.ActivityManager;
21 import android.app.IActivityManager;
22 import android.os.Process;
23 import android.os.RemoteException;
24 import android.util.AtomicFile;
25 import android.util.Log;
26 import android.util.Slog;
27 
28 import com.android.internal.annotations.VisibleForTesting;
29 
30 import java.io.File;
31 import java.io.FileOutputStream;
32 import java.io.FilenameFilter;
33 import java.io.IOException;
34 import java.io.PrintWriter;
35 import java.text.SimpleDateFormat;
36 import java.util.ArrayList;
37 import java.util.Arrays;
38 import java.util.Date;
39 import java.util.List;
40 
41 /**
42  * The shutdown check points are a recording of more detailed information of the origin of calls to
43  * system shutdown and reboot framework methods.
44  *
45  * @hide
46  */
47 public final class ShutdownCheckPoints {
48 
49     private static final String TAG = "ShutdownCheckPoints";
50 
51     private static final ShutdownCheckPoints INSTANCE = new ShutdownCheckPoints();
52 
53     private static final int MAX_CHECK_POINTS = 100;
54     private static final int MAX_DUMP_FILES = 20;
55     private static final SimpleDateFormat DATE_FORMAT =
56             new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS z");
57     private static final File[] EMPTY_FILE_ARRAY = {};
58 
59     private final ArrayList<CheckPoint> mCheckPoints;
60     private final Injector mInjector;
61 
ShutdownCheckPoints()62     private ShutdownCheckPoints() {
63         this(new Injector() {
64             @Override
65             public long currentTimeMillis() {
66                 return System.currentTimeMillis();
67             }
68 
69             @Override
70             public int maxCheckPoints() {
71                 return MAX_CHECK_POINTS;
72             }
73 
74             @Override
75             public int maxDumpFiles() {
76                 return MAX_DUMP_FILES;
77             }
78 
79             @Override
80             public IActivityManager activityManager() {
81                 return ActivityManager.getService();
82             }
83         });
84     }
85 
86     @VisibleForTesting
ShutdownCheckPoints(Injector injector)87     ShutdownCheckPoints(Injector injector) {
88         mCheckPoints = new ArrayList<>();
89         mInjector = injector;
90     }
91 
92     /** Records the stack trace of this {@link Thread} as a shutdown check point. */
recordCheckPoint(@ullable String reason)93     public static void recordCheckPoint(@Nullable String reason) {
94         INSTANCE.recordCheckPointInternal(reason);
95     }
96 
97     /** Records the pid of the caller process as a shutdown check point. */
recordCheckPoint(int callerProcessId, @Nullable String reason)98     public static void recordCheckPoint(int callerProcessId, @Nullable String reason) {
99         INSTANCE.recordCheckPointInternal(callerProcessId, reason);
100     }
101 
102     /** Records the {@link android.content.Intent} name and package as a shutdown check point. */
recordCheckPoint( String intentName, String packageName, @Nullable String reason)103     public static void recordCheckPoint(
104             String intentName, String packageName, @Nullable String reason) {
105         INSTANCE.recordCheckPointInternal(intentName, packageName, reason);
106     }
107 
108     /** Serializes the recorded check points and writes them to given {@code printWriter}. */
dump(PrintWriter printWriter)109     public static void dump(PrintWriter printWriter) {
110         INSTANCE.dumpInternal(printWriter);
111     }
112 
113     /**
114      * Creates a {@link Thread} that calls {@link #dump(PrintWriter)} on a rotating file created
115      * from given {@code baseFile} and a timestamp suffix. Older dump files are also deleted by this
116      * thread.
117      */
newDumpThread(File baseFile)118     public static Thread newDumpThread(File baseFile) {
119         return INSTANCE.newDumpThreadInternal(baseFile);
120     }
121 
122     @VisibleForTesting
recordCheckPointInternal(@ullable String reason)123     void recordCheckPointInternal(@Nullable String reason) {
124         recordCheckPointInternal(new SystemServerCheckPoint(mInjector, reason));
125         Slog.v(TAG, "System server shutdown checkpoint recorded");
126     }
127 
128     @VisibleForTesting
recordCheckPointInternal(int callerProcessId, @Nullable String reason)129     void recordCheckPointInternal(int callerProcessId, @Nullable String reason) {
130         recordCheckPointInternal(callerProcessId == Process.myPid()
131                 ? new SystemServerCheckPoint(mInjector, reason)
132                 : new BinderCheckPoint(mInjector, callerProcessId, reason));
133         Slog.v(TAG, "Binder shutdown checkpoint recorded with pid=" + callerProcessId);
134     }
135 
136     @VisibleForTesting
recordCheckPointInternal(String intentName, String packageName, @Nullable String reason)137     void recordCheckPointInternal(String intentName, String packageName, @Nullable String reason) {
138         recordCheckPointInternal("android".equals(packageName)
139                 ? new SystemServerCheckPoint(mInjector, reason)
140                 : new IntentCheckPoint(mInjector, intentName, packageName, reason));
141         Slog.v(TAG, String.format("Shutdown intent checkpoint recorded intent=%s from package=%s",
142                 intentName, packageName));
143     }
144 
recordCheckPointInternal(CheckPoint checkPoint)145     private void recordCheckPointInternal(CheckPoint checkPoint) {
146         synchronized (mCheckPoints) {
147             mCheckPoints.add(checkPoint);
148             if (mCheckPoints.size() > mInjector.maxCheckPoints()) mCheckPoints.remove(0);
149         }
150     }
151 
152     @VisibleForTesting
dumpInternal(PrintWriter printWriter)153     void dumpInternal(PrintWriter printWriter) {
154         final List<CheckPoint> records;
155         synchronized (mCheckPoints) {
156             records = new ArrayList<>(mCheckPoints);
157         }
158         for (CheckPoint record : records) {
159             record.dump(printWriter);
160             printWriter.println();
161         }
162     }
163 
164     @VisibleForTesting
newDumpThreadInternal(File baseFile)165     Thread newDumpThreadInternal(File baseFile) {
166         return new FileDumperThread(this, baseFile, mInjector.maxDumpFiles());
167     }
168 
169     /** Injector used by {@link ShutdownCheckPoints} for testing purposes. */
170     @VisibleForTesting
171     interface Injector {
172 
currentTimeMillis()173         long currentTimeMillis();
174 
maxCheckPoints()175         int maxCheckPoints();
176 
maxDumpFiles()177         int maxDumpFiles();
178 
activityManager()179         IActivityManager activityManager();
180     }
181 
182     /** Representation of a generic shutdown call, which can be serialized. */
183     private abstract static class CheckPoint {
184 
185         private final long mTimestamp;
186         @Nullable private final String mReason;
187 
CheckPoint(Injector injector, @Nullable String reason)188         CheckPoint(Injector injector, @Nullable String reason) {
189             mTimestamp = injector.currentTimeMillis();
190             mReason = reason;
191         }
192 
dump(PrintWriter printWriter)193         final void dump(PrintWriter printWriter) {
194             printWriter.print("Shutdown request from ");
195             printWriter.print(getOrigin());
196             if (mReason != null) {
197                 printWriter.print(" for reason ");
198                 printWriter.print(mReason);
199             }
200             printWriter.print(" at ");
201             printWriter.print(DATE_FORMAT.format(new Date(mTimestamp)));
202             printWriter.println(" (epoch=" + mTimestamp + ")");
203             dumpDetails(printWriter);
204         }
205 
getOrigin()206         abstract String getOrigin();
207 
dumpDetails(PrintWriter printWriter)208         abstract void dumpDetails(PrintWriter printWriter);
209     }
210 
211     /** Representation of a shutdown call from the system server, with stack trace. */
212     private static class SystemServerCheckPoint extends CheckPoint {
213 
214         private final StackTraceElement[] mStackTraceElements;
215 
SystemServerCheckPoint(Injector injector, @Nullable String reason)216         SystemServerCheckPoint(Injector injector, @Nullable String reason) {
217             super(injector, reason);
218             mStackTraceElements = Thread.currentThread().getStackTrace();
219         }
220 
221         @Override
getOrigin()222         String getOrigin() {
223             return "SYSTEM";
224         }
225 
226         @Override
dumpDetails(PrintWriter printWriter)227         void dumpDetails(PrintWriter printWriter) {
228             String methodName = getMethodName();
229             printWriter.println(methodName == null ? "Failed to get method name" : methodName);
230             printStackTrace(printWriter);
231         }
232 
233         @Nullable
getMethodName()234         String getMethodName() {
235             int idx = findCallSiteIndex();
236             if (idx < mStackTraceElements.length) {
237                 StackTraceElement element = mStackTraceElements[idx];
238                 return String.format("%s.%s", element.getClassName(), element.getMethodName());
239             }
240             return null;
241         }
242 
printStackTrace(PrintWriter printWriter)243         void printStackTrace(PrintWriter printWriter) {
244             // Skip the call site line, as it's already considered with getMethodName.
245             for (int i = findCallSiteIndex() + 1; i < mStackTraceElements.length; i++) {
246                 printWriter.print(" at ");
247                 printWriter.println(mStackTraceElements[i]);
248             }
249         }
250 
findCallSiteIndex()251         private int findCallSiteIndex() {
252             String className = ShutdownCheckPoints.class.getCanonicalName();
253             int idx = 0;
254             // Skip system trace lines until finding ShutdownCheckPoints call site.
255             while (idx < mStackTraceElements.length
256                     && !mStackTraceElements[idx].getClassName().equals(className)) {
257                 ++idx;
258             }
259             // Skip trace lines from ShutdownCheckPoints class.
260             while (idx < mStackTraceElements.length
261                     && mStackTraceElements[idx].getClassName().equals(className)) {
262                 ++idx;
263             }
264             return idx;
265         }
266     }
267 
268     /** Representation of a shutdown call to {@link android.os.Binder}, with caller process id. */
269     private static class BinderCheckPoint extends SystemServerCheckPoint {
270         private final int mCallerProcessId;
271         private final IActivityManager mActivityManager;
272 
BinderCheckPoint(Injector injector, int callerProcessId, @Nullable String reason)273         BinderCheckPoint(Injector injector, int callerProcessId, @Nullable String reason) {
274             super(injector, reason);
275             mCallerProcessId = callerProcessId;
276             mActivityManager = injector.activityManager();
277         }
278 
279         @Override
getOrigin()280         String getOrigin() {
281             return "BINDER";
282         }
283 
284         @Override
dumpDetails(PrintWriter printWriter)285         void dumpDetails(PrintWriter printWriter) {
286             String methodName = getMethodName();
287             printWriter.println(methodName == null ? "Failed to get method name" : methodName);
288 
289             String processName = getProcessName();
290             printWriter.print("From process ");
291             printWriter.print(processName == null ? "?" : processName);
292             printWriter.println(" (pid=" + mCallerProcessId + ")");
293         }
294 
295         @Nullable
getProcessName()296         String getProcessName() {
297             try {
298                 List<ActivityManager.RunningAppProcessInfo> runningProcesses = null;
299                 if (mActivityManager != null) {
300                     runningProcesses = mActivityManager.getRunningAppProcesses();
301                 } else {
302                     Slog.v(TAG, "No ActivityManager available to find process name with pid="
303                             + mCallerProcessId);
304                 }
305                 if (runningProcesses != null) {
306                     for (ActivityManager.RunningAppProcessInfo processInfo : runningProcesses) {
307                         if (processInfo.pid == mCallerProcessId) {
308                             return processInfo.processName;
309                         }
310                     }
311                 }
312             } catch (RemoteException e) {
313                 Slog.e(TAG, "Failed to get running app processes from ActivityManager", e);
314             }
315             return null;
316         }
317     }
318 
319     /** Representation of a shutdown call with {@link android.content.Intent}. */
320     private static class IntentCheckPoint extends CheckPoint {
321         private final String mIntentName;
322         private final String mPackageName;
323 
IntentCheckPoint( Injector injector, String intentName, String packageName, @Nullable String reason)324         IntentCheckPoint(
325                 Injector injector, String intentName, String packageName, @Nullable String reason) {
326             super(injector, reason);
327             mIntentName = intentName;
328             mPackageName = packageName;
329         }
330 
331         @Override
getOrigin()332         String getOrigin() {
333             return "INTENT";
334         }
335 
336         @Override
dumpDetails(PrintWriter printWriter)337         void dumpDetails(PrintWriter printWriter) {
338             printWriter.print("Intent: ");
339             printWriter.println(mIntentName);
340             printWriter.print("Package: ");
341             printWriter.println(mPackageName);
342         }
343     }
344 
345     /**
346      * Thread that writes {@link ShutdownCheckPoints#dumpInternal(PrintWriter)} to a new file and
347      * deletes old ones to keep the total number of files down to a given limit.
348      */
349     private static final class FileDumperThread extends Thread {
350 
351         private final ShutdownCheckPoints mInstance;
352         private final File mBaseFile;
353         private final int mFileCountLimit;
354 
FileDumperThread(ShutdownCheckPoints instance, File baseFile, int fileCountLimit)355         FileDumperThread(ShutdownCheckPoints instance, File baseFile, int fileCountLimit) {
356             mInstance = instance;
357             mBaseFile = baseFile;
358             mFileCountLimit = fileCountLimit;
359         }
360 
361         @Override
run()362         public void run() {
363             mBaseFile.getParentFile().mkdirs();
364             File[] checkPointFiles = listCheckPointsFiles();
365 
366             int filesToDelete = checkPointFiles.length - mFileCountLimit + 1;
367             for (int i = 0; i < filesToDelete; i++) {
368                 checkPointFiles[i].delete();
369             }
370 
371             File nextCheckPointsFile = new File(String.format("%s-%d",
372                     mBaseFile.getAbsolutePath(), System.currentTimeMillis()));
373             writeCheckpoints(nextCheckPointsFile);
374         }
375 
listCheckPointsFiles()376         private File[] listCheckPointsFiles() {
377             String filePrefix = mBaseFile.getName() + "-";
378             File[] files = mBaseFile.getParentFile().listFiles(new FilenameFilter() {
379                 @Override
380                 public boolean accept(File dir, String name) {
381                     if (!name.startsWith(filePrefix)) {
382                         return false;
383                     }
384                     try {
385                         Long.valueOf(name.substring(filePrefix.length()));
386                     } catch (NumberFormatException e) {
387                         return false;
388                     }
389                     return true;
390                 }
391             });
392             if (files == null) {
393                 return EMPTY_FILE_ARRAY;
394             }
395             Arrays.sort(files);
396             return files;
397         }
398 
writeCheckpoints(File file)399         private void writeCheckpoints(File file) {
400             AtomicFile tmpFile = new AtomicFile(mBaseFile);
401             FileOutputStream fos = null;
402             try {
403                 fos = tmpFile.startWrite();
404                 PrintWriter pw = new PrintWriter(fos);
405                 mInstance.dumpInternal(pw);
406                 pw.flush();
407                 tmpFile.finishWrite(fos); // This also closes the output stream.
408             } catch (IOException e) {
409                 Log.e(TAG, "Failed to write shutdown checkpoints", e);
410                 if (fos != null) {
411                     tmpFile.failWrite(fos); // This also closes the output stream.
412                 }
413             }
414             mBaseFile.renameTo(file);
415         }
416     }
417 }
418