/* * Copyright (C) 2019 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.car; import static android.car.CarBugreportManager.CarBugreportManagerCallback.CAR_BUGREPORT_DUMPSTATE_CONNECTION_FAILED; import static android.car.CarBugreportManager.CarBugreportManagerCallback.CAR_BUGREPORT_DUMPSTATE_FAILED; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.car.CarBugreportManager.CarBugreportManagerCallback; import android.car.ICarBugreportCallback; import android.car.ICarBugreportService; import android.content.Context; import android.content.pm.PackageManager; import android.net.LocalSocket; import android.net.LocalSocketAddress; import android.os.Binder; import android.os.Build; import android.os.Handler; import android.os.HandlerThread; import android.os.ParcelFileDescriptor; import android.os.Process; import android.os.RemoteException; import android.os.SystemClock; import android.os.SystemProperties; import android.util.IndentingPrintWriter; import android.util.Slog; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import java.io.BufferedReader; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.util.concurrent.atomic.AtomicBoolean; /** * Bugreport service for cars. */ public class CarBugreportManagerService extends ICarBugreportService.Stub implements CarServiceBase { private static final String TAG = CarLog.tagFor(CarBugreportManagerService.class); /** * {@code dumpstate} progress prefixes. * *
The protocol is described in {@code frameworks/native/cmds/bugreportz/readme.md}. */ private static final String BEGIN_PREFIX = "BEGIN:"; private static final String PROGRESS_PREFIX = "PROGRESS:"; private static final String OK_PREFIX = "OK:"; private static final String FAIL_PREFIX = "FAIL:"; /** * The services are defined in {@code packages/services/Car/cpp/bugreport/carbugreportd.rc}. */ private static final String BUGREPORTD_SERVICE = "carbugreportd"; private static final String DUMPSTATEZ_SERVICE = "cardumpstatez"; // The socket definitions must match the actual socket names defined in car_bugreportd service // definition. private static final String BUGREPORT_PROGRESS_SOCKET = "car_br_progress_socket"; private static final String BUGREPORT_OUTPUT_SOCKET = "car_br_output_socket"; private static final String BUGREPORT_EXTRA_OUTPUT_SOCKET = "car_br_extra_output_socket"; private static final int SOCKET_CONNECTION_MAX_RETRY = 10; private static final int SOCKET_CONNECTION_RETRY_DELAY_IN_MS = 5000; private final Context mContext; private final boolean mIsUserBuild; private final Object mLock = new Object(); private final HandlerThread mHandlerThread = CarServiceUtils.getHandlerThread( getClass().getSimpleName()); private final Handler mHandler = new Handler(mHandlerThread.getLooper()); private final AtomicBoolean mIsServiceRunning = new AtomicBoolean(false); private boolean mIsDumpstateDryRun = false; /** * Create a CarBugreportManagerService instance. * * @param context the context */ public CarBugreportManagerService(Context context) { // Per https://source.android.com/setup/develop/new-device, user builds are debuggable=0 this(context, !Build.IS_DEBUGGABLE); } @VisibleForTesting CarBugreportManagerService(Context context, boolean isUserBuild) { mContext = context; mIsUserBuild = isUserBuild; } @Override public void init() { // nothing to do } @Override public void release() { // nothing to do } @Override @RequiresPermission(android.Manifest.permission.DUMP) public void requestBugreport(ParcelFileDescriptor output, ParcelFileDescriptor extraOutput, ICarBugreportCallback callback, boolean dumpstateDryRun) { mContext.enforceCallingOrSelfPermission( android.Manifest.permission.DUMP, "requestBugreport"); ensureTheCallerIsSignedWithPlatformKeys(); ensureTheCallerIsDesignatedBugReportApp(); synchronized (mLock) { if (mIsServiceRunning.getAndSet(true)) { Slog.w(TAG, "Bugreport Service already running"); reportError(callback, CarBugreportManagerCallback.CAR_BUGREPORT_IN_PROGRESS); return; } requestBugReportLocked(output, extraOutput, callback, dumpstateDryRun); } } @Override @RequiresPermission(android.Manifest.permission.DUMP) public void cancelBugreport() { mContext.enforceCallingOrSelfPermission( android.Manifest.permission.DUMP, "cancelBugreport"); ensureTheCallerIsSignedWithPlatformKeys(); ensureTheCallerIsDesignatedBugReportApp(); synchronized (mLock) { if (!mIsServiceRunning.getAndSet(false)) { Slog.i(TAG, "Failed to cancel. Service is not running."); return; } Slog.i(TAG, "Cancelling the running bugreport"); mHandler.removeCallbacksAndMessages(/* token= */ null); // This tells init to cancel the services. Note that this is achieved through // setting a system property which is not thread-safe. So the lock here offers // thread-safety only among callers of the API. try { SystemProperties.set("ctl.stop", BUGREPORTD_SERVICE); } catch (RuntimeException e) { Slog.e(TAG, "Failed to stop " + BUGREPORTD_SERVICE, e); } try { // Stop DUMPSTATEZ_SERVICE service too, because stopping BUGREPORTD_SERVICE doesn't // guarantee stopping DUMPSTATEZ_SERVICE. SystemProperties.set("ctl.stop", DUMPSTATEZ_SERVICE); } catch (RuntimeException e) { Slog.e(TAG, "Failed to stop " + DUMPSTATEZ_SERVICE, e); } if (mIsDumpstateDryRun) { setDumpstateDryRun(false); } } } /** See {@code dumpstate} docs to learn about dry_run. */ private void setDumpstateDryRun(boolean dryRun) { try { SystemProperties.set("dumpstate.dry_run", dryRun ? "true" : null); } catch (RuntimeException e) { Slog.e(TAG, "Failed to set dumpstate.dry_run", e); } } private void ensureTheCallerIsSignedWithPlatformKeys() { PackageManager pm = mContext.getPackageManager(); int callingUid = Binder.getCallingUid(); if (pm.checkSignatures(Process.myUid(), callingUid) != PackageManager.SIGNATURE_MATCH) { throw new SecurityException("Caller " + pm.getNameForUid(callingUid) + " does not have the right signature"); } } /** Checks only on user builds. */ private void ensureTheCallerIsDesignatedBugReportApp() { if (!mIsUserBuild) { return; } String defaultAppPkgName = mContext.getString(R.string.config_car_bugreport_application); int callingUid = Binder.getCallingUid(); PackageManager pm = mContext.getPackageManager(); String[] packageNamesForCallerUid = pm.getPackagesForUid(callingUid); if (packageNamesForCallerUid != null) { for (String packageName : packageNamesForCallerUid) { if (defaultAppPkgName.equals(packageName)) { return; } } } throw new SecurityException("Caller " + pm.getNameForUid(callingUid) + " is not a designated bugreport app"); } @GuardedBy("mLock") private void requestBugReportLocked( ParcelFileDescriptor output, ParcelFileDescriptor extraOutput, ICarBugreportCallback callback, boolean dumpstateDryRun) { Slog.i(TAG, "Starting " + BUGREPORTD_SERVICE); mIsDumpstateDryRun = dumpstateDryRun; if (mIsDumpstateDryRun) { setDumpstateDryRun(true); } try { // This tells init to start the service. Note that this is achieved through // setting a system property which is not thread-safe. So the lock here offers // thread-safety only among callers of the API. SystemProperties.set("ctl.start", BUGREPORTD_SERVICE); } catch (RuntimeException e) { mIsServiceRunning.set(false); Slog.e(TAG, "Failed to start " + BUGREPORTD_SERVICE, e); reportError(callback, CAR_BUGREPORT_DUMPSTATE_FAILED); return; } mHandler.post(() -> { try { processBugreportSockets(output, extraOutput, callback); } finally { if (mIsDumpstateDryRun) { setDumpstateDryRun(false); } mIsServiceRunning.set(false); } }); } private void handleProgress(String line, ICarBugreportCallback callback) { String progressOverTotal = line.substring(PROGRESS_PREFIX.length()); String[] parts = progressOverTotal.split("/"); if (parts.length != 2) { Slog.w(TAG, "Invalid progress line from bugreportz: " + line); return; } float progress; float total; try { progress = Float.parseFloat(parts[0]); total = Float.parseFloat(parts[1]); } catch (NumberFormatException e) { Slog.w(TAG, "Invalid progress value: " + line, e); return; } if (total == 0) { Slog.w(TAG, "Invalid progress total value: " + line); return; } try { callback.onProgress(100f * progress / total); } catch (RemoteException e) { Slog.e(TAG, "Failed to call onProgress callback", e); } } private void handleFinished(ParcelFileDescriptor output, ParcelFileDescriptor extraOutput, ICarBugreportCallback callback) { Slog.i(TAG, "Finished reading bugreport"); // copysockettopfd calls callback.onError on error if (!copySocketToPfd(output, BUGREPORT_OUTPUT_SOCKET, callback)) { return; } if (!copySocketToPfd(extraOutput, BUGREPORT_EXTRA_OUTPUT_SOCKET, callback)) { return; } try { callback.onFinished(); } catch (RemoteException e) { Slog.e(TAG, "Failed to call onFinished callback", e); } } /** * Reads from dumpstate progress and output sockets and invokes appropriate callbacks. * *
dumpstate prints {@code BEGIN:} right away, then prints {@code PROGRESS:} as it * progresses. When it finishes or fails it prints {@code OK:pathToTheZipFile} or * {@code FAIL:message} accordingly. */ private void processBugreportSockets( ParcelFileDescriptor output, ParcelFileDescriptor extraOutput, ICarBugreportCallback callback) { LocalSocket localSocket = connectSocket(BUGREPORT_PROGRESS_SOCKET); if (localSocket == null) { reportError(callback, CAR_BUGREPORT_DUMPSTATE_CONNECTION_FAILED); return; } try (BufferedReader reader = new BufferedReader(new InputStreamReader(localSocket.getInputStream()))) { String line; while (mIsServiceRunning.get() && (line = reader.readLine()) != null) { if (line.startsWith(PROGRESS_PREFIX)) { handleProgress(line, callback); } else if (line.startsWith(FAIL_PREFIX)) { String errorMessage = line.substring(FAIL_PREFIX.length()); Slog.e(TAG, "Failed to dumpstate: " + errorMessage); reportError(callback, CAR_BUGREPORT_DUMPSTATE_FAILED); return; } else if (line.startsWith(OK_PREFIX)) { handleFinished(output, extraOutput, callback); return; } else if (!line.startsWith(BEGIN_PREFIX)) { Slog.w(TAG, "Received unknown progress line from dumpstate: " + line); } } Slog.e(TAG, "dumpstate progress unexpectedly ended"); reportError(callback, CAR_BUGREPORT_DUMPSTATE_FAILED); } catch (IOException | RuntimeException e) { Slog.i(TAG, "Failed to read from progress socket", e); reportError(callback, CAR_BUGREPORT_DUMPSTATE_CONNECTION_FAILED); } } private boolean copySocketToPfd( ParcelFileDescriptor pfd, String remoteSocket, ICarBugreportCallback callback) { LocalSocket localSocket = connectSocket(remoteSocket); if (localSocket == null) { reportError(callback, CAR_BUGREPORT_DUMPSTATE_CONNECTION_FAILED); return false; } try ( DataInputStream in = new DataInputStream(localSocket.getInputStream()); DataOutputStream out = new DataOutputStream(new ParcelFileDescriptor.AutoCloseOutputStream(pfd)) ) { rawCopyStream(out, in); } catch (IOException | RuntimeException e) { Slog.e(TAG, "Failed to grab dump state from " + BUGREPORT_OUTPUT_SOCKET, e); reportError(callback, CAR_BUGREPORT_DUMPSTATE_FAILED); return false; } return true; } private void reportError(ICarBugreportCallback callback, int errorCode) { try { callback.onError(errorCode); } catch (RemoteException e) { Slog.e(TAG, "onError() failed", e); } } @Override public void dump(IndentingPrintWriter writer) { // TODO(sgurun) implement } @Nullable private LocalSocket connectSocket(@NonNull String socketName) { LocalSocket socket = new LocalSocket(); // The dumpstate socket will be created by init upon receiving the // service request. It may not be ready by this point. So we will // keep retrying until success or reaching timeout. int retryCount = 0; while (true) { // There are a few factors impacting the socket delay: // 1. potential system slowness // 2. carbugreportd takes the screenshots early (before starting dumpstate). This // should be taken into account as the socket opens after screenshots are // captured. // Therefore we are generous in setting the timeout. Most cases should not even // come close to the timeouts, but since bugreports are taken when there is a // system issue, it is hard to guess. // The following lines waits for SOCKET_CONNECTION_RETRY_DELAY_IN_MS or until // mIsServiceRunning becomes false. for (int i = 0; i < SOCKET_CONNECTION_RETRY_DELAY_IN_MS / 50; i++) { if (!mIsServiceRunning.get()) { Slog.i(TAG, "Failed to connect to socket " + socketName + ". The service is prematurely cancelled."); return null; } SystemClock.sleep(50); // Millis. } try { socket.connect(new LocalSocketAddress(socketName, LocalSocketAddress.Namespace.RESERVED)); return socket; } catch (IOException e) { if (++retryCount >= SOCKET_CONNECTION_MAX_RETRY) { Slog.i(TAG, "Failed to connect to dumpstate socket " + socketName + " after " + retryCount + " retries", e); return null; } Slog.i(TAG, "Failed to connect to " + socketName + ". Will try again. " + e.getMessage()); } } } // does not close the reader or writer. private static void rawCopyStream(OutputStream writer, InputStream reader) throws IOException { int read; byte[] buf = new byte[8192]; while ((read = reader.read(buf, 0, buf.length)) > 0) { writer.write(buf, 0, read); } } }