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