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