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.DurationMillisLong;
20 import android.annotation.NonNull;
21 import android.content.Context;
22 import android.os.Environment;
23 import android.os.IBinder;
24 import android.os.ParcelFileDescriptor;
25 import android.os.RemoteException;
26 import android.os.ServiceManager;
27 import android.provider.Settings.Global;
28 import android.util.Slog;
29 
30 import com.android.internal.annotations.GuardedBy;
31 import com.android.internal.annotations.VisibleForTesting;
32 import com.android.internal.util.ArrayUtils;
33 
34 import java.io.File;
35 import java.io.FileNotFoundException;
36 import java.io.FileWriter;
37 import java.io.IOException;
38 import java.util.concurrent.TimeUnit;
39 import java.util.concurrent.atomic.AtomicBoolean;
40 
41 /**
42  * Provides utils to dump/wipe pre-reboot information.
43  */
44 final class PreRebootLogger {
45     private static final String TAG = "PreRebootLogger";
46     private static final String PREREBOOT_DIR = "prereboot";
47 
48     private static final String[] BUFFERS_TO_DUMP = {"system"};
49     private static final String[] SERVICES_TO_DUMP = {Context.ROLLBACK_SERVICE, "package"};
50 
51     private static final Object sLock = new Object();
52     private static final long MAX_DUMP_TIME = TimeUnit.SECONDS.toMillis(20);
53 
54     /**
55      * Process pre-reboot information. Dump pre-reboot information to {@link #PREREBOOT_DIR} if
56      * enabled {@link Settings.Global#ADB_ENABLED} and having active staged session; wipe dumped
57      * information otherwise.
58      */
log(Context context)59     static void log(Context context) {
60         log(context, getDumpDir());
61     }
62 
63     @VisibleForTesting
log(Context context, @NonNull File dumpDir)64     static void log(Context context, @NonNull File dumpDir) {
65         if (needDump(context)) {
66             dump(dumpDir, MAX_DUMP_TIME);
67         } else {
68             wipe(dumpDir);
69         }
70     }
71 
needDump(Context context)72     private static boolean needDump(Context context) {
73         return Global.getInt(context.getContentResolver(), Global.ADB_ENABLED, 0) == 1
74                 && !context.getPackageManager().getPackageInstaller()
75                         .getActiveStagedSessions().isEmpty();
76     }
77 
78     @VisibleForTesting
dump(@onNull File dumpDir, @DurationMillisLong long maxWaitTime)79     static void dump(@NonNull File dumpDir, @DurationMillisLong long maxWaitTime) {
80         Slog.d(TAG, "Dumping pre-reboot information...");
81         final AtomicBoolean done = new AtomicBoolean(false);
82         final Thread t = new Thread(() -> {
83             synchronized (sLock) {
84                 for (String buffer : BUFFERS_TO_DUMP) {
85                     dumpLogsLocked(dumpDir, buffer);
86                 }
87                 for (String service : SERVICES_TO_DUMP) {
88                     dumpServiceLocked(dumpDir, service);
89                 }
90             }
91             done.set(true);
92         });
93         t.start();
94 
95         try {
96             t.join(maxWaitTime);
97         } catch (InterruptedException e) {
98             Slog.e(TAG, "Failed to dump pre-reboot information due to interrupted", e);
99         }
100 
101         if (!done.get()) {
102             Slog.w(TAG, "Failed to dump pre-reboot information due to timeout");
103         }
104     }
105 
wipe(@onNull File dumpDir)106     private static void wipe(@NonNull File dumpDir) {
107         Slog.d(TAG, "Wiping pre-reboot information...");
108         synchronized (sLock) {
109             for (File file : dumpDir.listFiles()) {
110                 file.delete();
111             }
112         }
113     }
114 
getDumpDir()115     private static File getDumpDir() {
116         final File dumpDir = new File(Environment.getDataMiscDirectory(), PREREBOOT_DIR);
117         if (!dumpDir.exists() || !dumpDir.isDirectory()) {
118             throw new UnsupportedOperationException("Pre-reboot dump directory not found");
119         }
120         return dumpDir;
121     }
122 
123     @GuardedBy("sLock")
dumpLogsLocked(@onNull File dumpDir, @NonNull String buffer)124     private static void dumpLogsLocked(@NonNull File dumpDir, @NonNull String buffer) {
125         try {
126             final File dumpFile = new File(dumpDir, buffer);
127             if (dumpFile.createNewFile()) {
128                 dumpFile.setWritable(true /* writable */, true /* ownerOnly */);
129             } else {
130                 // Wipes dumped information in existing file before recording new information.
131                 new FileWriter(dumpFile, false).flush();
132             }
133 
134             final String[] cmdline =
135                     {"logcat", "-d", "-b", buffer, "-f", dumpFile.getAbsolutePath()};
136             Runtime.getRuntime().exec(cmdline).waitFor();
137         } catch (IOException | InterruptedException e) {
138             Slog.e(TAG, "Failed to dump system log buffer before reboot", e);
139         }
140     }
141 
142     @GuardedBy("sLock")
dumpServiceLocked(@onNull File dumpDir, @NonNull String serviceName)143     private static void dumpServiceLocked(@NonNull File dumpDir, @NonNull String serviceName) {
144         final IBinder binder = ServiceManager.checkService(serviceName);
145         if (binder == null) {
146             return;
147         }
148 
149         try {
150             final File dumpFile = new File(dumpDir, serviceName);
151             final ParcelFileDescriptor fd = ParcelFileDescriptor.open(dumpFile,
152                     ParcelFileDescriptor.MODE_CREATE | ParcelFileDescriptor.MODE_TRUNCATE
153                             | ParcelFileDescriptor.MODE_WRITE_ONLY);
154             binder.dump(fd.getFileDescriptor(), ArrayUtils.emptyArray(String.class));
155         } catch (FileNotFoundException | RemoteException e) {
156             Slog.e(TAG, String.format("Failed to dump %s service before reboot", serviceName), e);
157         }
158     }
159 }
160