1 /*
2  * Copyright (C) 2019 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.car;
18 
19 import static android.car.CarBugreportManager.CarBugreportManagerCallback.CAR_BUGREPORT_DUMPSTATE_CONNECTION_FAILED;
20 import static android.car.CarBugreportManager.CarBugreportManagerCallback.CAR_BUGREPORT_DUMPSTATE_FAILED;
21 
22 import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.DUMP_INFO;
23 
24 import android.annotation.NonNull;
25 import android.annotation.Nullable;
26 import android.annotation.RequiresPermission;
27 import android.car.CarBugreportManager.CarBugreportManagerCallback;
28 import android.car.ICarBugreportCallback;
29 import android.car.ICarBugreportService;
30 import android.car.builtin.os.BuildHelper;
31 import android.car.builtin.os.SystemPropertiesHelper;
32 import android.car.builtin.util.Slogf;
33 import android.content.Context;
34 import android.content.pm.PackageManager;
35 import android.net.LocalSocket;
36 import android.net.LocalSocketAddress;
37 import android.os.Binder;
38 import android.os.Handler;
39 import android.os.HandlerThread;
40 import android.os.ParcelFileDescriptor;
41 import android.os.Process;
42 import android.os.RemoteException;
43 import android.os.SystemClock;
44 import android.util.proto.ProtoOutputStream;
45 
46 import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport;
47 import com.android.car.internal.util.IndentingPrintWriter;
48 import com.android.internal.annotations.GuardedBy;
49 import com.android.internal.annotations.VisibleForTesting;
50 
51 import java.io.BufferedReader;
52 import java.io.DataInputStream;
53 import java.io.DataOutputStream;
54 import java.io.IOException;
55 import java.io.InputStream;
56 import java.io.InputStreamReader;
57 import java.io.OutputStream;
58 import java.util.concurrent.atomic.AtomicBoolean;
59 
60 /**
61  * Bugreport service for cars.
62  */
63 public class CarBugreportManagerService extends ICarBugreportService.Stub implements
64         CarServiceBase {
65 
66     private static final String TAG = CarLog.tagFor(CarBugreportManagerService.class);
67 
68     /**
69      * {@code dumpstate} progress prefixes.
70      *
71      * <p>The protocol is described in {@code frameworks/native/cmds/bugreportz/readme.md}.
72      */
73     private static final String BEGIN_PREFIX = "BEGIN:";
74     private static final String PROGRESS_PREFIX = "PROGRESS:";
75     private static final String OK_PREFIX = "OK:";
76     private static final String FAIL_PREFIX = "FAIL:";
77 
78     /**
79      * The services are defined in {@code packages/services/Car/cpp/bugreport/carbugreportd.rc}.
80      */
81     @VisibleForTesting
82     static final String BUGREPORTD_SERVICE = "carbugreportd";
83     @VisibleForTesting
84     static final String DUMPSTATEZ_SERVICE = "cardumpstatez";
85 
86     // The socket definitions must match the actual socket names defined in car_bugreportd service
87     // definition.
88     private static final String BUGREPORT_PROGRESS_SOCKET = "car_br_progress_socket";
89     private static final String BUGREPORT_OUTPUT_SOCKET = "car_br_output_socket";
90     private static final String BUGREPORT_EXTRA_OUTPUT_SOCKET = "car_br_extra_output_socket";
91 
92     private static final int SOCKET_CONNECTION_MAX_RETRY = 10;
93     private static final int SOCKET_CONNECTION_RETRY_DELAY_IN_MS = 5000;
94 
95     private final Context mContext;
96     private final boolean mIsUserBuild;
97     private final Object mLock = new Object();
98 
99     private final HandlerThread mHandlerThread = CarServiceUtils.getHandlerThread(
100             getClass().getSimpleName());
101     private final Handler mHandler = new Handler(mHandlerThread.getLooper());
102     @VisibleForTesting
103     final AtomicBoolean mIsServiceRunning = new AtomicBoolean(false);
104     private boolean mIsDumpstateDryRun = false;
105 
106     /**
107      * Create a CarBugreportManagerService instance.
108      *
109      * @param context the context
110      */
CarBugreportManagerService(Context context)111     public CarBugreportManagerService(Context context) {
112         // Per https://source.android.com/setup/develop/new-device, user builds are debuggable=0
113         this(context, !BuildHelper.isDebuggableBuild());
114     }
115 
116     @VisibleForTesting
CarBugreportManagerService(Context context, boolean isUserBuild)117     CarBugreportManagerService(Context context, boolean isUserBuild) {
118         mContext = context;
119         mIsUserBuild = isUserBuild;
120     }
121 
122     @Override
init()123     public void init() {
124         // nothing to do
125     }
126 
127     @Override
release()128     public void release() {
129         // To stop any pending tasks in HandlerThread
130         mIsServiceRunning.set(false);
131     }
132 
133     @Override
134     @RequiresPermission(android.Manifest.permission.DUMP)
requestBugreport(ParcelFileDescriptor output, ParcelFileDescriptor extraOutput, ICarBugreportCallback callback, boolean dumpstateDryRun)135     public void requestBugreport(ParcelFileDescriptor output, ParcelFileDescriptor extraOutput,
136             ICarBugreportCallback callback, boolean dumpstateDryRun) {
137         mContext.enforceCallingOrSelfPermission(
138                 android.Manifest.permission.DUMP, "requestBugreport");
139         ensureTheCallerIsSignedWithPlatformKeys();
140         ensureTheCallerIsDesignatedBugReportApp();
141         synchronized (mLock) {
142             if (mIsServiceRunning.getAndSet(true)) {
143                 Slogf.w(TAG, "Bugreport Service already running");
144                 reportError(callback, CarBugreportManagerCallback.CAR_BUGREPORT_IN_PROGRESS);
145                 return;
146             }
147             requestBugReportLocked(output, extraOutput, callback, dumpstateDryRun);
148         }
149     }
150 
151     @Override
152     @RequiresPermission(android.Manifest.permission.DUMP)
cancelBugreport()153     public void cancelBugreport() {
154         mContext.enforceCallingOrSelfPermission(
155                 android.Manifest.permission.DUMP, "cancelBugreport");
156         ensureTheCallerIsSignedWithPlatformKeys();
157         ensureTheCallerIsDesignatedBugReportApp();
158         synchronized (mLock) {
159             if (!mIsServiceRunning.getAndSet(false)) {
160                 Slogf.i(TAG, "Ignoring cancelBugreport. Service is not running.");
161                 return;
162             }
163             Slogf.i(TAG, "Cancelling the running bugreport");
164             mHandler.removeCallbacksAndMessages(/* token= */ null);
165             // This tells init to cancel the services. Note that this is achieved through
166             // setting a system property which is not thread-safe. So the lock here offers
167             // thread-safety only among callers of the API.
168             try {
169                 SystemPropertiesHelper.set("ctl.stop", BUGREPORTD_SERVICE);
170             } catch (RuntimeException e) {
171                 Slogf.e(TAG, "Failed to stop " + BUGREPORTD_SERVICE, e);
172             }
173             try {
174                 // Stop DUMPSTATEZ_SERVICE service too, because stopping BUGREPORTD_SERVICE doesn't
175                 // guarantee stopping DUMPSTATEZ_SERVICE.
176                 SystemPropertiesHelper.set("ctl.stop", DUMPSTATEZ_SERVICE);
177             } catch (RuntimeException e) {
178                 Slogf.e(TAG, "Failed to stop " + DUMPSTATEZ_SERVICE, e);
179             }
180             if (mIsDumpstateDryRun) {
181                 setDumpstateDryRun(false);
182             }
183         }
184     }
185 
186     /** See {@code dumpstate} docs to learn about dry_run. */
setDumpstateDryRun(boolean dryRun)187     private void setDumpstateDryRun(boolean dryRun) {
188         try {
189             SystemPropertiesHelper.set("dumpstate.dry_run", dryRun ? "true" : null);
190         } catch (RuntimeException e) {
191             Slogf.e(TAG, "Failed to set dumpstate.dry_run", e);
192         }
193     }
194 
ensureTheCallerIsSignedWithPlatformKeys()195     private void ensureTheCallerIsSignedWithPlatformKeys() {
196         PackageManager pm = mContext.getPackageManager();
197         int callingUid = Binder.getCallingUid();
198         if (pm.checkSignatures(Process.myUid(), callingUid) != PackageManager.SIGNATURE_MATCH) {
199             throw new SecurityException("Caller " + pm.getNameForUid(callingUid)
200                             + " does not have the right signature");
201         }
202     }
203 
204     /** Checks only on user builds. */
ensureTheCallerIsDesignatedBugReportApp()205     private void ensureTheCallerIsDesignatedBugReportApp() {
206         if (!mIsUserBuild) {
207             return;
208         }
209         String defaultAppPkgName = mContext.getString(R.string.config_car_bugreport_application);
210         int callingUid = Binder.getCallingUid();
211         PackageManager pm = mContext.getPackageManager();
212         String[] packageNamesForCallerUid = pm.getPackagesForUid(callingUid);
213         if (packageNamesForCallerUid != null) {
214             for (String packageName : packageNamesForCallerUid) {
215                 if (defaultAppPkgName.equals(packageName)) {
216                     return;
217                 }
218             }
219         }
220         throw new SecurityException("Caller " + pm.getNameForUid(callingUid)
221                 + " is not a designated bugreport app");
222     }
223 
224     @GuardedBy("mLock")
requestBugReportLocked( ParcelFileDescriptor output, ParcelFileDescriptor extraOutput, ICarBugreportCallback callback, boolean dumpstateDryRun)225     private void requestBugReportLocked(
226             ParcelFileDescriptor output,
227             ParcelFileDescriptor extraOutput,
228             ICarBugreportCallback callback,
229             boolean dumpstateDryRun) {
230         Slogf.i(TAG, "Starting " + BUGREPORTD_SERVICE);
231         mIsDumpstateDryRun = dumpstateDryRun;
232         if (mIsDumpstateDryRun) {
233             setDumpstateDryRun(true);
234         }
235         try {
236             // This tells init to start the service. Note that this is achieved through
237             // setting a system property which is not thread-safe. So the lock here offers
238             // thread-safety only among callers of the API.
239             SystemPropertiesHelper.set("ctl.start", BUGREPORTD_SERVICE);
240         } catch (RuntimeException e) {
241             mIsServiceRunning.set(false);
242             Slogf.e(TAG, "Failed to start " + BUGREPORTD_SERVICE, e);
243             reportError(callback, CAR_BUGREPORT_DUMPSTATE_FAILED);
244             return;
245         }
246         mHandler.post(() -> {
247             try {
248                 processBugreportSockets(output, extraOutput, callback);
249             } finally {
250                 if (mIsDumpstateDryRun) {
251                     setDumpstateDryRun(false);
252                 }
253                 mIsServiceRunning.set(false);
254             }
255         });
256     }
257 
handleProgress(String line, ICarBugreportCallback callback)258     private void handleProgress(String line, ICarBugreportCallback callback) {
259         String progressOverTotal = line.substring(PROGRESS_PREFIX.length());
260         String[] parts = progressOverTotal.split("/");
261         if (parts.length != 2) {
262             Slogf.w(TAG, "Invalid progress line from bugreportz: " + line);
263             return;
264         }
265         float progress;
266         float total;
267         try {
268             progress = Float.parseFloat(parts[0]);
269             total = Float.parseFloat(parts[1]);
270         } catch (NumberFormatException e) {
271             Slogf.w(TAG, "Invalid progress value: " + line, e);
272             return;
273         }
274         if (total == 0) {
275             Slogf.w(TAG, "Invalid progress total value: " + line);
276             return;
277         }
278         try {
279             callback.onProgress(100f * progress / total);
280         } catch (RemoteException e) {
281             Slogf.e(TAG, "Failed to call onProgress callback", e);
282         }
283     }
284 
handleFinished(ParcelFileDescriptor output, ParcelFileDescriptor extraOutput, ICarBugreportCallback callback)285     private void handleFinished(ParcelFileDescriptor output, ParcelFileDescriptor extraOutput,
286             ICarBugreportCallback callback) {
287         Slogf.i(TAG, "Finished reading bugreport");
288         // copysockettopfd calls callback.onError on error
289         if (!copySocketToPfd(output, BUGREPORT_OUTPUT_SOCKET, callback)) {
290             return;
291         }
292         if (!copySocketToPfd(extraOutput, BUGREPORT_EXTRA_OUTPUT_SOCKET, callback)) {
293             return;
294         }
295         try {
296             callback.onFinished();
297         } catch (RemoteException e) {
298             Slogf.e(TAG, "Failed to call onFinished callback", e);
299         }
300     }
301 
302     /**
303      * Reads from dumpstate progress and output sockets and invokes appropriate callbacks.
304      *
305      * <p>dumpstate prints {@code BEGIN:} right away, then prints {@code PROGRESS:} as it
306      * progresses. When it finishes or fails it prints {@code OK:pathToTheZipFile} or
307      * {@code FAIL:message} accordingly.
308      */
processBugreportSockets( ParcelFileDescriptor output, ParcelFileDescriptor extraOutput, ICarBugreportCallback callback)309     private void processBugreportSockets(
310             ParcelFileDescriptor output, ParcelFileDescriptor extraOutput,
311             ICarBugreportCallback callback) {
312         LocalSocket localSocket = connectSocket(BUGREPORT_PROGRESS_SOCKET);
313         if (localSocket == null) {
314             reportError(callback, CAR_BUGREPORT_DUMPSTATE_CONNECTION_FAILED);
315             return;
316         }
317         try (BufferedReader reader =
318                 new BufferedReader(new InputStreamReader(localSocket.getInputStream()))) {
319             String line;
320             while (mIsServiceRunning.get() && (line = reader.readLine()) != null) {
321                 if (line.startsWith(PROGRESS_PREFIX)) {
322                     handleProgress(line, callback);
323                 } else if (line.startsWith(FAIL_PREFIX)) {
324                     String errorMessage = line.substring(FAIL_PREFIX.length());
325                     Slogf.e(TAG, "Failed to dumpstate: " + errorMessage);
326                     reportError(callback, CAR_BUGREPORT_DUMPSTATE_FAILED);
327                     return;
328                 } else if (line.startsWith(OK_PREFIX)) {
329                     handleFinished(output, extraOutput, callback);
330                     return;
331                 } else if (!line.startsWith(BEGIN_PREFIX)) {
332                     Slogf.w(TAG, "Received unknown progress line from dumpstate: " + line);
333                 }
334             }
335             Slogf.e(TAG, "dumpstate progress unexpectedly ended");
336             reportError(callback, CAR_BUGREPORT_DUMPSTATE_FAILED);
337         } catch (IOException | RuntimeException e) {
338             Slogf.i(TAG, "Failed to read from progress socket", e);
339             reportError(callback, CAR_BUGREPORT_DUMPSTATE_CONNECTION_FAILED);
340         }
341     }
342 
copySocketToPfd( ParcelFileDescriptor pfd, String remoteSocket, ICarBugreportCallback callback)343     private boolean copySocketToPfd(
344             ParcelFileDescriptor pfd, String remoteSocket, ICarBugreportCallback callback) {
345         LocalSocket localSocket = connectSocket(remoteSocket);
346         if (localSocket == null) {
347             reportError(callback, CAR_BUGREPORT_DUMPSTATE_CONNECTION_FAILED);
348             return false;
349         }
350 
351         try (
352             DataInputStream in = new DataInputStream(localSocket.getInputStream());
353             DataOutputStream out =
354                     new DataOutputStream(new ParcelFileDescriptor.AutoCloseOutputStream(pfd))
355         ) {
356             rawCopyStream(out, in);
357         } catch (IOException | RuntimeException e) {
358             Slogf.e(TAG, "Failed to grab dump state from " + BUGREPORT_OUTPUT_SOCKET, e);
359             reportError(callback, CAR_BUGREPORT_DUMPSTATE_FAILED);
360             return false;
361         }
362         return true;
363     }
364 
reportError(ICarBugreportCallback callback, int errorCode)365     private void reportError(ICarBugreportCallback callback, int errorCode) {
366         try {
367             callback.onError(errorCode);
368         } catch (RemoteException e) {
369             Slogf.e(TAG, "onError() failed", e);
370         }
371     }
372 
373     @Override
374     @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO)
dump(IndentingPrintWriter writer)375     public void dump(IndentingPrintWriter writer) {
376         // TODO(sgurun) implement
377     }
378 
379     @Override
380     @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO)
dumpProto(ProtoOutputStream proto)381     public void dumpProto(ProtoOutputStream proto) {}
382 
383     @Nullable
connectSocket(@onNull String socketName)384     private LocalSocket connectSocket(@NonNull String socketName) {
385         LocalSocket socket = new LocalSocket();
386         // The dumpstate socket will be created by init upon receiving the
387         // service request. It may not be ready by this point. So we will
388         // keep retrying until success or reaching timeout.
389         int retryCount = 0;
390         while (true) {
391             // There are a few factors impacting the socket delay:
392             // 1. potential system slowness
393             // 2. carbugreportd takes the screenshots early (before starting dumpstate). This
394             //    should be taken into account as the socket opens after screenshots are
395             //    captured.
396             // Therefore we are generous in setting the timeout. Most cases should not even
397             // come close to the timeouts, but since bugreports are taken when there is a
398             // system issue, it is hard to guess.
399             // The following lines waits for SOCKET_CONNECTION_RETRY_DELAY_IN_MS or until
400             // mIsServiceRunning becomes false.
401             for (int i = 0; i < SOCKET_CONNECTION_RETRY_DELAY_IN_MS / 50; i++) {
402                 if (!mIsServiceRunning.get()) {
403                     Slogf.i(TAG, "Failed to connect to socket " + socketName
404                             + ". The service is prematurely cancelled.");
405                     return null;
406                 }
407                 SystemClock.sleep(50);  // Millis.
408             }
409 
410             try {
411                 socket.connect(new LocalSocketAddress(socketName,
412                         LocalSocketAddress.Namespace.RESERVED));
413                 return socket;
414             } catch (IOException e) {
415                 if (++retryCount >= SOCKET_CONNECTION_MAX_RETRY) {
416                     Slogf.i(TAG, "Failed to connect to dumpstate socket " + socketName
417                             + " after " + retryCount + " retries", e);
418                     return null;
419                 }
420                 Slogf.i(TAG, "Failed to connect to " + socketName + ". Will try again. "
421                         + e.getMessage());
422             }
423         }
424     }
425 
426     // does not close the reader or writer.
rawCopyStream(OutputStream writer, InputStream reader)427     private static void rawCopyStream(OutputStream writer, InputStream reader) throws IOException {
428         int read;
429         byte[] buf = new byte[8192];
430         while ((read = reader.read(buf, 0, buf.length)) > 0) {
431             writer.write(buf, 0, read);
432         }
433     }
434 }
435