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