Source code

Revision control

Copy as Markdown

Other Tools

/* Any copyright is dedicated to the Public Domain.
package org.mozilla.geckoview.test;
import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.geckoview.GeckoRuntime;
import org.mozilla.geckoview.test.util.UiThreadUtils;
public class TestCrashHandler extends Service {
private static final int MSG_EVAL_NEXT_CRASH_DUMP = 1;
private static final int MSG_CRASH_DUMP_EVAL_RESULT = 2;
private static final String LOGTAG = "TestCrashHandler";
public static final class EvalResult {
private static final String BUNDLE_KEY_RESULT = "TestCrashHandler.EvalResult.mResult";
private static final String BUNDLE_KEY_MSG = "TestCrashHandler.EvalResult.mMsg";
public EvalResult(final boolean result, final String msg) {
mResult = result;
mMsg = msg;
}
public EvalResult(final Bundle bundle) {
mResult = bundle.getBoolean(BUNDLE_KEY_RESULT, false);
mMsg = bundle.getString(BUNDLE_KEY_MSG);
}
public Bundle asBundle() {
final Bundle bundle = new Bundle();
bundle.putBoolean(BUNDLE_KEY_RESULT, mResult);
bundle.putString(BUNDLE_KEY_MSG, mMsg);
return bundle;
}
public boolean mResult;
public String mMsg;
}
public static final class Client {
private static final String LOGTAG = "TestCrashHandler.Client";
private class Receiver extends Handler {
public Receiver(final Looper looper) {
super(looper);
}
@Override
public void handleMessage(final Message msg) {
if (msg.what == MSG_CRASH_DUMP_EVAL_RESULT) {
setEvalResult(new EvalResult(msg.getData()));
return;
}
super.handleMessage(msg);
}
}
private Receiver mReceiver;
private boolean mDoUnbind = false;
private Messenger mService = null;
private Messenger mMessenger;
private Context mContext;
private HandlerThread mThread;
private EvalResult mResult = null;
private ServiceConnection mConnection =
new ServiceConnection() {
@Override
public void onServiceConnected(final ComponentName className, final IBinder service) {
mService = new Messenger(service);
}
@Override
public void onServiceDisconnected(final ComponentName className) {
disconnect();
}
};
public Client(final Context context) {
mContext = context;
mThread = new HandlerThread("TestCrashHandler.Client");
mThread.start();
mReceiver = new Receiver(mThread.getLooper());
mMessenger = new Messenger(mReceiver);
}
/**
* Tests should call this to notify the crash handler that the next crash it sees is intentional
* and that its intent should be checked for correctness.
*
* @param expectedProcessType The type of process the incoming crash is expected to be for.
* @param expectedRemoteType The type of content process the incoming crash is expected to be
* for.
*/
public void setEvalNextCrashDump(
final String expectedProcessType, final String expectedRemoteType) {
setEvalResult(null);
mReceiver.post(
new Runnable() {
@Override
public void run() {
final Bundle bundle = new Bundle();
bundle.putString(GeckoRuntime.EXTRA_CRASH_PROCESS_TYPE, expectedProcessType);
bundle.putString(GeckoRuntime.EXTRA_CRASH_REMOTE_TYPE, expectedRemoteType);
final Message msg = Message.obtain(null, MSG_EVAL_NEXT_CRASH_DUMP, bundle);
msg.replyTo = mMessenger;
try {
mService.send(msg);
} catch (final RemoteException e) {
throw new RuntimeException(e.getMessage());
}
}
});
}
public boolean connect(final long timeoutMillis) {
final Intent intent = new Intent(mContext, TestCrashHandler.class);
mDoUnbind =
mContext.bindService(
intent, mConnection, Context.BIND_AUTO_CREATE | Context.BIND_IMPORTANT);
if (!mDoUnbind) {
return false;
}
UiThreadUtils.waitForCondition(() -> mService != null, timeoutMillis);
return mService != null;
}
public void disconnect() {
if (mDoUnbind) {
mContext.unbindService(mConnection);
mService = null;
mDoUnbind = false;
}
mThread.quitSafely();
}
private synchronized void setEvalResult(final EvalResult result) {
mResult = result;
}
private synchronized EvalResult getEvalResult() {
return mResult;
}
/**
* Tests should call this method after initiating the intentional crash to wait for the result
* from the crash handler.
*
* @param timeoutMillis timeout in milliseconds
* @return EvalResult containing the boolean result of the test and an error message.
*/
public EvalResult getEvalResult(final long timeoutMillis) {
UiThreadUtils.waitForCondition(() -> getEvalResult() != null, timeoutMillis);
return getEvalResult();
}
}
private static final class MessageHandler extends Handler {
private Messenger mReplyToMessenger;
private String mExpectedProcessType;
private String mExpectedRemoteType;
MessageHandler() {}
@Override
public void handleMessage(final Message msg) {
if (msg.what == MSG_EVAL_NEXT_CRASH_DUMP) {
mReplyToMessenger = msg.replyTo;
Bundle bundle = (Bundle) msg.obj;
mExpectedProcessType = bundle.getString(GeckoRuntime.EXTRA_CRASH_PROCESS_TYPE);
mExpectedRemoteType = bundle.getString(GeckoRuntime.EXTRA_CRASH_REMOTE_TYPE);
return;
}
super.handleMessage(msg);
}
public void reportResult(final EvalResult result) {
if (mReplyToMessenger == null) {
return;
}
final Message msg = Message.obtain(null, MSG_CRASH_DUMP_EVAL_RESULT);
msg.setData(result.asBundle());
try {
mReplyToMessenger.send(msg);
} catch (final RemoteException e) {
throw new RuntimeException(e.getMessage());
}
mReplyToMessenger = null;
}
public String getExpectedProcessType() {
return mExpectedProcessType;
}
public String getExpectedRemoteType() {
return mExpectedRemoteType;
}
}
private Messenger mMessenger;
private MessageHandler mMsgHandler;
public TestCrashHandler() {}
private static JSONObject readExtraFile(final String filePath) throws IOException, JSONException {
final byte[] buffer = new byte[4096];
final FileInputStream inputStream = new FileInputStream(filePath);
final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
int bytesRead = 0;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
final String contents = new String(outputStream.toByteArray(), "UTF-8");
return new JSONObject(contents);
}
private EvalResult evalCrashInfo(final Intent intent) {
if (!intent.getAction().equals(GeckoRuntime.ACTION_CRASHED)) {
return new EvalResult(false, "Action should match");
}
final File dumpFile = new File(intent.getStringExtra(GeckoRuntime.EXTRA_MINIDUMP_PATH));
final boolean dumpFileExists = dumpFile.exists();
dumpFile.delete();
final File extrasFile = new File(intent.getStringExtra(GeckoRuntime.EXTRA_EXTRAS_PATH));
final boolean extrasFileExists = extrasFile.exists();
try {
final JSONObject annotations = readExtraFile(extrasFile.getPath());
final String moz_crash_reason = annotations.getString("MozCrashReason");
if (!moz_crash_reason.startsWith("MOZ_CRASH(")) {
return new EvalResult(false, "Missing or invalid child crash annotations");
}
extrasFile.delete();
} catch (final Exception e) {
return new EvalResult(false, e.toString());
}
if (!dumpFileExists) {
return new EvalResult(false, "Dump file should exist");
}
if (!extrasFileExists) {
return new EvalResult(false, "Extras file should exist");
}
final String expectedProcessType = mMsgHandler.getExpectedProcessType();
final String processType = intent.getStringExtra(GeckoRuntime.EXTRA_CRASH_PROCESS_TYPE);
if (processType == null) {
return new EvalResult(false, "Intent missing process type");
}
if (!processType.equals(expectedProcessType)) {
return new EvalResult(
false, "Expected process type " + expectedProcessType + ", found " + processType);
}
final String expectedRemoteType = mMsgHandler.getExpectedRemoteType();
final String remoteType = intent.getStringExtra(GeckoRuntime.EXTRA_CRASH_REMOTE_TYPE);
if ((remoteType == null && expectedRemoteType != null)
|| (remoteType != null && !remoteType.equals(expectedRemoteType))) {
return new EvalResult(
false, "Expected remote type " + expectedRemoteType + ", found " + remoteType);
}
return new EvalResult(true, "Crash Dump OK");
}
@Override
public synchronized int onStartCommand(final Intent intent, final int flags, final int startId) {
if (mMsgHandler != null) {
mMsgHandler.reportResult(evalCrashInfo(intent));
// We must manually call stopSelf() here to ensure the Service gets killed once the client
// unbinds. If we don't, then when the next client attempts to bind for a different test,
// onBind() will not be called, and mMsgHandler will not get set.
stopSelf();
return Service.START_NOT_STICKY;
}
// We don't want to do anything, this handler only exists
// so we produce a crash dump which is picked up by the
// test harness.
System.exit(0);
return Service.START_NOT_STICKY;
}
@Override
public synchronized IBinder onBind(final Intent intent) {
mMsgHandler = new MessageHandler();
mMessenger = new Messenger(mMsgHandler);
return mMessenger.getBinder();
}
@Override
public synchronized boolean onUnbind(final Intent intent) {
mMsgHandler = null;
mMessenger = null;
return false;
}
}