blob: 26a306bb859915e9b2a4eb60c8de597dbc8073de [file] [log] [blame]
/*
* 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 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.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.Log;
import android.util.Slog;
import com.android.internal.annotations.GuardedBy;
import libcore.io.IoUtils;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
/**
* Bugreport service for cars. Should *only* be used on userdebug or eng builds.
*/
public class CarBugreportManagerService extends ICarBugreportService.Stub implements
CarServiceBase {
private static final String TAG = "CarBugreportMgrService";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
private final Context mContext;
private final Object mLock = new Object();
private HandlerThread mHandlerThread = null;
private Handler mHandler;
private boolean mIsServiceRunning;
// The socket at /dev/socket/dumpstate to communicate with dumpstate.
private static final String DUMPSTATE_SOCKET = "dumpstate";
// 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 int SOCKET_CONNECTION_MAX_RETRY = 10;
/**
* Create a CarBugreportManagerService instance.
*
* @param context the context
*/
public CarBugreportManagerService(Context context) {
mContext = context;
}
@Override
public void init() {
mHandlerThread = new HandlerThread(TAG);
mHandlerThread.start();
mHandler = new Handler(mHandlerThread.getLooper());
}
@Override
public void release() {
mHandlerThread.quitSafely();
}
@Override
@RequiresPermission(android.Manifest.permission.DUMP)
public void requestZippedBugreport(ParcelFileDescriptor data, ParcelFileDescriptor progress,
ICarBugreportCallback callback) {
// Check the caller has proper permission
mContext.enforceCallingOrSelfPermission(android.Manifest.permission.DUMP,
"requestZippedBugreport");
// Check the caller is signed with platform keys
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");
}
// Check the caller is the default designated bugreport app
String defaultAppPkgName = mContext.getString(R.string.default_car_bugreport_application);
String[] packageNamesForCallerUid = pm.getPackagesForUid(callingUid);
boolean found = false;
if (packageNamesForCallerUid != null) {
for (String packageName : packageNamesForCallerUid) {
if (defaultAppPkgName.equals(packageName)) {
found = true;
break;
}
}
}
if (!found) {
throw new SecurityException("Caller " + pm.getNameForUid(callingUid)
+ " is not a designated bugreport app");
}
synchronized (mLock) {
requestZippedBugReportLocked(data, progress, callback);
}
}
@GuardedBy("mLock")
private void requestZippedBugReportLocked(ParcelFileDescriptor data,
ParcelFileDescriptor progress, ICarBugreportCallback callback) {
if (mIsServiceRunning) {
Slog.w(TAG, "Bugreport Service already running");
reportError(callback, CarBugreportManagerCallback.CAR_BUGREPORT_IN_PROGRESS);
return;
}
mIsServiceRunning = true;
mHandler.post(() -> startBugreportd(data, progress, callback));
}
private void startBugreportd(ParcelFileDescriptor data, ParcelFileDescriptor progress,
ICarBugreportCallback callback) {
readBugreport(data, progress, callback);
synchronized (mLock) {
mIsServiceRunning = false;
}
}
private void readBugreport(ParcelFileDescriptor output, ParcelFileDescriptor progress,
ICarBugreportCallback callback) {
Slog.i(TAG, "Starting car-bugreportd");
try {
SystemProperties.set("ctl.start", "car-bugreportd");
} catch (RuntimeException e) {
Slog.e(TAG, "Failed to start car-bugreportd", e);
reportError(callback, CarBugreportManagerCallback.CAR_BUGREPORT_DUMPSTATE_FAILED);
return;
}
// The native service first generates the progress data. Once it writes the progress
// data fully, it closes the socket and writes the zip file. So we read both files
// sequentially here.
if (!readSocket(BUGREPORT_PROGRESS_SOCKET, progress, callback)) {
Slog.e(TAG, "failed reading bugreport progress socket");
reportError(callback, CarBugreportManagerCallback.CAR_BUGREPORT_DUMPSTATE_FAILED);
return;
}
if (!readSocket(BUGREPORT_OUTPUT_SOCKET, output, callback)) {
Slog.i(TAG, "failed reading bugreport output socket");
reportError(callback, CarBugreportManagerCallback.CAR_BUGREPORT_DUMPSTATE_FAILED);
return;
}
Slog.i(TAG, "finished reading bugreport");
try {
callback.onFinished();
} catch (RemoteException e) {
Slog.e(TAG, "onFinished() failed: " + e.getMessage());
}
}
private boolean readSocket(String name, ParcelFileDescriptor pfd,
ICarBugreportCallback callback) {
LocalSocket localSocket;
try {
localSocket = connectSocket(name);
} catch (IOException e) {
Slog.e(TAG, "Failed connecting to socket " + name, e);
reportError(callback,
CarBugreportManagerCallback.CAR_BUGREPORT_DUMPSTATE_CONNECTION_FAILED);
// Early out if connection to socket fails.
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 " + name, e);
reportError(callback, CarBugreportManagerCallback.CAR_BUGREPORT_DUMPSTATE_FAILED);
return false;
}
return true;
}
/**
* This API and all descendants will be removed once the clients transition to new method
*/
@Override
@RequiresPermission(android.Manifest.permission.DUMP)
public void requestBugreport(ParcelFileDescriptor pfd, ICarBugreportCallback callback) {
if (mHandler == null) {
// bugreport manager service is only available if the build is not a user build.
reportError(callback, CarBugreportManagerCallback.CAR_BUGREPORT_SERVICE_NOT_AVAILABLE);
return;
}
mContext.enforceCallingOrSelfPermission(android.Manifest.permission.DUMP,
"requestBugreport");
synchronized (mLock) {
requestBugReportLocked(pfd, callback);
}
}
@GuardedBy("mLock")
private void requestBugReportLocked(ParcelFileDescriptor pfd, ICarBugreportCallback callback) {
if (mIsServiceRunning) {
Slog.w(TAG, "Bugreport Service already running");
reportError(callback, CarBugreportManagerCallback.CAR_BUGREPORT_IN_PROGRESS);
return;
}
mIsServiceRunning = true;
mHandler.post(() -> dumpStateToFileWrapper(pfd, callback));
}
private void reportError(ICarBugreportCallback callback, int errorCode) {
try {
callback.onError(errorCode);
} catch (RemoteException e) {
Slog.e(TAG, "onError() failed: " + e.getMessage());
}
}
private void dumpStateToFileWrapper(ParcelFileDescriptor pfd, ICarBugreportCallback callback) {
dumpStateToFile(pfd, callback);
synchronized (mLock) {
mIsServiceRunning = false;
}
}
@Override
public void dump(PrintWriter writer) {
// TODO(sgurun) implement
}
private void dumpStateToFile(ParcelFileDescriptor pfd, ICarBugreportCallback callback) {
if (DEBUG) {
Slog.d(TAG, "Dumpstate to file");
}
OutputStream out = null;
InputStream in = null;
LocalSocket localSocket;
try {
SystemProperties.set("ctl.start", "dumpstate");
} catch (RuntimeException e) {
Slog.e(TAG, "Failed to start dumpstate", e);
reportError(callback, CarBugreportManagerCallback.CAR_BUGREPORT_DUMPSTATE_FAILED);
return;
}
try {
localSocket = connectSocket(DUMPSTATE_SOCKET);
} catch (IOException e) {
Slog.e(TAG, "Timed out connecting to dumpstate socket", e);
reportError(callback,
CarBugreportManagerCallback.CAR_BUGREPORT_DUMPSTATE_CONNECTION_FAILED);
// Early out if connection to socket fails.
return;
}
try {
in = new DataInputStream(localSocket.getInputStream());
out = new DataOutputStream(new ParcelFileDescriptor.AutoCloseOutputStream(pfd));
rawCopyStream(out, in);
} catch (IOException | RuntimeException e) {
Slog.e(TAG, "Failed to grab dump state", e);
reportError(callback, CarBugreportManagerCallback.CAR_BUGREPORT_DUMPSTATE_FAILED);
return;
} finally {
IoUtils.closeQuietly(in);
IoUtils.closeQuietly(out);
}
try {
callback.onFinished();
} catch (RemoteException e) {
Slog.e(TAG, "onFinished() failed: " + e.getMessage());
}
}
private LocalSocket connectSocket(String socketName) throws IOException {
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) {
// First connection always fails so we just 1 second before trying to connect for the
// first time too.
SystemClock.sleep(/* ms= */ 1000);
try {
socket.connect(new LocalSocketAddress(socketName,
LocalSocketAddress.Namespace.RESERVED));
return socket;
} catch (IOException e) {
if (++retryCount >= SOCKET_CONNECTION_MAX_RETRY) {
throw e;
}
Log.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);
}
}
}