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