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.LinkedList;
40 import java.util.List;
41 
42 /**
43  * The shutdown check points are a recording of more detailed information of the origin of calls to
44  * system shutdown and reboot framework methods.
45  *
46  * @hide
47  */
48 public final class ShutdownCheckPoints {
49 
50     private static final String TAG = "ShutdownCheckPoints";
51 
52     private static final ShutdownCheckPoints INSTANCE = new ShutdownCheckPoints();
53 
54     private static final int MAX_CHECK_POINTS = 100;
55     private static final int MAX_DUMP_FILES = 20;
56     private static final SimpleDateFormat DATE_FORMAT =
57             new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS z");
58 
59     private final LinkedList<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 LinkedList<>();
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.addLast(checkPoint);
148             if (mCheckPoints.size() > mInjector.maxCheckPoints()) mCheckPoints.removeFirst();
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 =
299                         mActivityManager.getRunningAppProcesses();
300                 for (ActivityManager.RunningAppProcessInfo processInfo : runningProcesses) {
301                     if (processInfo.pid == mCallerProcessId) {
302                         return processInfo.processName;
303                     }
304                 }
305             } catch (RemoteException e) {
306                 Slog.e(TAG, "Failed to get running app processes from ActivityManager", e);
307             }
308             return null;
309         }
310     }
311 
312     /** Representation of a shutdown call with {@link android.content.Intent}. */
313     private static class IntentCheckPoint extends CheckPoint {
314         private final String mIntentName;
315         private final String mPackageName;
316 
IntentCheckPoint( Injector injector, String intentName, String packageName, @Nullable String reason)317         IntentCheckPoint(
318                 Injector injector, String intentName, String packageName, @Nullable String reason) {
319             super(injector, reason);
320             mIntentName = intentName;
321             mPackageName = packageName;
322         }
323 
324         @Override
getOrigin()325         String getOrigin() {
326             return "INTENT";
327         }
328 
329         @Override
dumpDetails(PrintWriter printWriter)330         void dumpDetails(PrintWriter printWriter) {
331             printWriter.print("Intent: ");
332             printWriter.println(mIntentName);
333             printWriter.print("Package: ");
334             printWriter.println(mPackageName);
335         }
336     }
337 
338     /**
339      * Thread that writes {@link ShutdownCheckPoints#dumpInternal(PrintWriter)} to a new file and
340      * deletes old ones to keep the total number of files down to a given limit.
341      */
342     private static final class FileDumperThread extends Thread {
343 
344         private final ShutdownCheckPoints mInstance;
345         private final File mBaseFile;
346         private final int mFileCountLimit;
347 
FileDumperThread(ShutdownCheckPoints instance, File baseFile, int fileCountLimit)348         FileDumperThread(ShutdownCheckPoints instance, File baseFile, int fileCountLimit) {
349             mInstance = instance;
350             mBaseFile = baseFile;
351             mFileCountLimit = fileCountLimit;
352         }
353 
354         @Override
run()355         public void run() {
356             mBaseFile.getParentFile().mkdirs();
357             File[] checkPointFiles = listCheckPointsFiles();
358 
359             int filesToDelete = checkPointFiles.length - mFileCountLimit + 1;
360             for (int i = 0; i < filesToDelete; i++) {
361                 checkPointFiles[i].delete();
362             }
363 
364             File nextCheckPointsFile = new File(String.format("%s-%d",
365                     mBaseFile.getAbsolutePath(), System.currentTimeMillis()));
366             writeCheckpoints(nextCheckPointsFile);
367         }
368 
listCheckPointsFiles()369         private File[] listCheckPointsFiles() {
370             String filePrefix = mBaseFile.getName() + "-";
371             File[] files = mBaseFile.getParentFile().listFiles(new FilenameFilter() {
372                 @Override
373                 public boolean accept(File dir, String name) {
374                     if (!name.startsWith(filePrefix)) {
375                         return false;
376                     }
377                     try {
378                         Long.valueOf(name.substring(filePrefix.length()));
379                     } catch (NumberFormatException e) {
380                         return false;
381                     }
382                     return true;
383                 }
384             });
385             Arrays.sort(files);
386             return files;
387         }
388 
writeCheckpoints(File file)389         private void writeCheckpoints(File file) {
390             AtomicFile tmpFile = new AtomicFile(mBaseFile);
391             FileOutputStream fos = null;
392             try {
393                 fos = tmpFile.startWrite();
394                 PrintWriter pw = new PrintWriter(fos);
395                 mInstance.dumpInternal(pw);
396                 pw.flush();
397                 tmpFile.finishWrite(fos); // This also closes the output stream.
398             } catch (IOException e) {
399                 Log.e(TAG, "Failed to write shutdown checkpoints", e);
400                 if (fos != null) {
401                     tmpFile.failWrite(fos); // This also closes the output stream.
402                 }
403             }
404             mBaseFile.renameTo(file);
405         }
406     }
407 }
408