/* * Copyright (c) 2014-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. */ package com.facebook.stetho.dumpapp.plugins; import com.facebook.stetho.common.ExceptionUtil; import com.facebook.stetho.common.Util; import com.facebook.stetho.dumpapp.ArgsHelper; import com.facebook.stetho.dumpapp.DumpException; import com.facebook.stetho.dumpapp.DumpUsageException; import com.facebook.stetho.dumpapp.DumperContext; import com.facebook.stetho.dumpapp.DumperPlugin; import java.io.IOException; import java.io.InputStream; import java.io.PrintStream; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.util.Iterator; import java.util.concurrent.CountDownLatch; import javax.annotation.Nullable; /** * Yes, this intentionally crashes the app. Useful for testing crash recovery and crash reporter * work flows. Three separate exit strategies are supported; see help output for more details. */ public class CrashDumperPlugin implements DumperPlugin { private static final String NAME = "crash"; private static final String OPTION_THROW_DEFAULT = "java.lang.Error"; private static final String OPTION_KILL_DEFAULT = "9"; // SIGKILL private static final String OPTION_EXIT_DEFAULT = "0"; // EXIT_SUCCESS in C public CrashDumperPlugin() { } @Override public String getName() { return NAME; } @Override public void dump(DumperContext dumpContext) throws DumpException { Iterator<String> argsIter = dumpContext.getArgsAsList().iterator(); String command = ArgsHelper.nextOptionalArg(argsIter, null); if ("throw".equals(command)) { doUncaughtException(argsIter); } else if ("kill".equals(command)) { doKill(dumpContext, argsIter); } else if ("exit".equals(command)) { doSystemExit(argsIter); } else { doUsage(dumpContext.getStdout()); if (command != null) { throw new DumpUsageException("Unsupported command: " + command); } } } private void doUsage(PrintStream out) { final String cmdName = "dumpapp " + NAME; String usagePrefix = "Usage: " + cmdName + " "; String blankPrefix = " " + cmdName + " "; out.println(usagePrefix + "<command> [command-options]"); out.println(usagePrefix + "throw"); out.println(blankPrefix + "kill"); out.println(blankPrefix + "exit"); out.println(); out.println(cmdName + " throw: Throw an uncaught exception (simulates a program crash)"); out.println(" <Throwable>: Throwable class to use (default: " + OPTION_THROW_DEFAULT + ")"); out.println(); out.println(cmdName + " kill: Send a signal to this process (simulates the low memory killer)"); out.println(" <SIGNAL>: Either signal name or number to send (default: " + OPTION_KILL_DEFAULT + ")"); out.println(" See `adb shell kill -l` for more information"); out.println(); out.println(cmdName + " exit: Invoke System.exit (simulates an abnormal Android exit strategy)"); out.println(" <code>: Exit code (default: " + OPTION_EXIT_DEFAULT + ")"); } private void doSystemExit(Iterator<String> argsIter) { String exitCodeStr = ArgsHelper.nextOptionalArg(argsIter, OPTION_EXIT_DEFAULT); System.exit(Integer.parseInt(exitCodeStr)); } private void doKill(DumperContext dumpContext, Iterator<String> argsIter) throws DumpException { String signal = ArgsHelper.nextOptionalArg(argsIter, OPTION_KILL_DEFAULT); try { Process kill = new ProcessBuilder() .command("/system/bin/kill", "-" + signal, String.valueOf(android.os.Process.myPid())) .redirectErrorStream(true) .start(); // Handle kill command output gracefully in the event that the signal delivered didn't // actually take out our process... try { InputStream in = kill.getInputStream(); Util.copy(in, dumpContext.getStdout(), new byte[1024]); } finally { kill.destroy(); } } catch (IOException e) { throw new DumpException("Failed to invoke kill: " + e); } } private void doUncaughtException(Iterator<String> argsIter) throws DumpException { String throwableClassString = ArgsHelper.nextOptionalArg(argsIter, OPTION_THROW_DEFAULT); try { Class<? extends Throwable> throwableClass = (Class<? extends Throwable>)Class.forName(throwableClassString); Throwable t; Constructor<? extends Throwable> ctorWithMessage = tryGetDeclaredConstructor(throwableClass, String.class); if (ctorWithMessage != null) { t = ctorWithMessage.newInstance("Uncaught exception triggered by Stetho"); } else { Constructor<? extends Throwable> ctorParameterless = throwableClass.getDeclaredConstructor(); t = ctorParameterless.newInstance(); } Thread crashThread = new Thread(new ThrowRunnable(t)); crashThread.start(); Util.joinUninterruptibly(crashThread); } catch ( ClassNotFoundException | ClassCastException | NoSuchMethodException | IllegalAccessException | InstantiationException e) { throw new DumpException("Invalid supplied Throwable class: " + e); } catch (InvocationTargetException e) { // This means that the method invoked actually threw, independent of reflection. Best // reflect that as a normal unchecked exception in dumpapp output. throw ExceptionUtil.propagate(e.getCause()); } } @Nullable private static <T> Constructor<? extends T> tryGetDeclaredConstructor( Class<T> clazz, Class<?>... parameterTypes) { try { return clazz.getDeclaredConstructor(parameterTypes); } catch (NoSuchMethodException e) { return null; } } private static class ThrowRunnable implements Runnable { private final Throwable mThrowable; public ThrowRunnable(Throwable t) { mThrowable = t; } @Override public void run() { ExceptionUtil.<Error>sneakyThrow(mThrowable); } } }