Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 341ff134 authored by Makoto Onuki's avatar Makoto Onuki Committed by Android (Google) Code Review
Browse files

Merge "Better handle exceptions from BG threads" into main

parents fadd2c47 b1631c5b
Loading
Loading
Loading
Loading
+12 −1
Original line number Diff line number Diff line
@@ -22,6 +22,9 @@ import android.annotation.Nullable;
import android.annotation.SystemApi;
import android.annotation.TestApi;
import android.compat.annotation.UnsupportedAppUsage;
import android.ravenwood.annotation.RavenwoodKeepWholeClass;
import android.ravenwood.annotation.RavenwoodRedirect;
import android.ravenwood.annotation.RavenwoodRedirectionClass;
import android.util.Log;
import android.util.Printer;

@@ -66,7 +69,8 @@ import java.lang.reflect.Modifier;
 * your new thread.  The given Runnable or Message will then be scheduled
 * in the Handler's message queue and processed when appropriate.
 */
@android.ravenwood.annotation.RavenwoodKeepWholeClass
@RavenwoodKeepWholeClass
@RavenwoodRedirectionClass("Handler_ravenwood")
public class Handler {
    /*
     * Set this flag to true to detect anonymous, local or member classes
@@ -789,8 +793,15 @@ public class Handler {
        return sendMessage(msg);
    }

    @RavenwoodRedirect
    private static void onBeforeEnqueue(@NonNull MessageQueue queue, @NonNull Message msg,
            long uptimeMillis) {
        // Ravenwood will check for a pending exception, and throw it if any.
    }

    private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
            long uptimeMillis) {
        onBeforeEnqueue(queue, msg, uptimeMillis);
        msg.target = this;
        msg.workSourceUid = ThreadLocalWorkSource.getUid();

+34 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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 android.os;

import android.annotation.NonNull;

import com.android.internal.util.function.TriFunction;

public class Handler_ravenwood {
    private Handler_ravenwood() {
    }

    public static volatile TriFunction<MessageQueue, Message, Long, Void>
            sPendingExceptionThrower = (a, b, c) -> null;

    static void onBeforeEnqueue(@NonNull MessageQueue queue, @NonNull Message msg,
            long uptimeMillis) {
        // Check for a pendign exception, and throw it if any.
        sPendingExceptionThrower.apply(queue, msg, uptimeMillis);
    }
}
+75 −23
Original line number Diff line number Diff line
@@ -53,6 +53,7 @@ import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.os.Environment_ravenwood;
import android.os.HandlerThread;
import android.os.Handler_ravenwood;
import android.os.Looper;
import android.os.Looper_ravenwood;
import android.os.Message;
@@ -133,12 +134,12 @@ public class RavenwoodDriver {
            !"0".equals(System.getenv("RAVENWOOD_ENABLE_TIMEOUT_STACKS"));

    /** RavenwoodCoreTest modifies it, so not final. */
    public static volatile boolean TOLERATE_UNHANDLED_ASSERTS =
    public static final boolean TOLERATE_UNHANDLED_ASSERTS =
            !"0".equals(System.getenv("RAVENWOOD_TOLERATE_UNHANDLED_ASSERTS"));

    /** RavenwoodCoreTest modifies it, so not final. */
    public static volatile boolean TOLERATE_UNHANDLED_EXCEPTIONS =
            "1".equals(System.getenv("RAVENWOOD_TOLERATE_UNHANDLED_EXCEPTIONS"));
    public static final boolean TOLERATE_UNHANDLED_EXCEPTIONS =
            !"0".equals(System.getenv("RAVENWOOD_TOLERATE_UNHANDLED_EXCEPTIONS"));

    static final int DEFAULT_TIMEOUT_SECONDS = 10;
    private static final int TIMEOUT_MILLIS = getTimeoutSeconds() * 1000;
@@ -151,7 +152,6 @@ public class RavenwoodDriver {
        return Integer.parseInt(e);
    }


    private static final ScheduledExecutorService sTimeoutExecutor =
            Executors.newScheduledThreadPool(1, (Runnable r) -> {
                Thread t = Executors.defaultThreadFactory().newThread(r);
@@ -171,7 +171,7 @@ public class RavenwoodDriver {
    private static final boolean DIE_ON_UNCAUGHT_EXCEPTION = false;

    /**
     * This is an "recoverable" uncaught exception from a BG thread. When we detect one,
     * This is a "recoverable" uncaught exception from a BG thread. When we detect one,
     * we just make the current test failed, but continue running the subsequent tests normally.
     */
    private static final AtomicReference<Throwable> sPendingRecoverableUncaughtException =
@@ -367,10 +367,12 @@ public class RavenwoodDriver {
        RavenwoodRuntimeState.sPid = sMyPid;
        RavenwoodRuntimeState.sTargetSdkLevel = sTargetSdkLevel;

        ServiceManager.init$ravenwood();
        LocalServices.removeAllServicesForTest();

        ActivityManager.init$ravenwood(SYSTEM.getIdentifier());
        RavenwoodUtils.sPendingExceptionThrower =
                RavenwoodDriver::maybeThrowPendingRecoverableUncaughtExceptionNoClear;
        Handler_ravenwood.sPendingExceptionThrower = (a, b, c) -> {
            maybeThrowPendingRecoverableUncaughtExceptionNoClear();
            return null;
        };

        final var main = new HandlerThread(MAIN_THREAD_NAME);
        sMainThread = main;
@@ -378,6 +380,12 @@ public class RavenwoodDriver {
        Looper_ravenwood.sDispatcher = RavenwoodDriver::dispatchMessage;
        Looper.setMainLooperForTest(main.getLooper());


        ServiceManager.init$ravenwood();
        LocalServices.removeAllServicesForTest();

        ActivityManager.init$ravenwood(SYSTEM.getIdentifier());

        final boolean isSelfInstrumenting =
                Objects.equals(sTestPackageName, sTargetPackageName);

@@ -498,7 +506,7 @@ public class RavenwoodDriver {

        SystemProperties.clearChangeCallbacksForTest();

        maybeThrowPendingRecoverableUncaughtException();
        maybeThrowPendingRecoverableUncaughtExceptionAndClear();
    }

    /**
@@ -523,7 +531,7 @@ public class RavenwoodDriver {
     */
    public static void exitTestMethod(Description description) {
        cancelTimeout();
        maybeThrowPendingRecoverableUncaughtException();
        maybeThrowPendingRecoverableUncaughtExceptionAndClear();
        maybeThrowUnrecoverableUncaughtExceptionIfDetected();
    }

@@ -607,6 +615,9 @@ public class RavenwoodDriver {
     * Return if an exception is benign and okay to continue running the remaining tests.
     */
    private static boolean isThrowableRecoverable(Throwable th) {
        if (th instanceof RavenwoodRecoverableExceptionWrapper) {
            return true;
        }
        if (TOLERATE_UNHANDLED_EXCEPTIONS) {
            return true;
        }
@@ -617,15 +628,33 @@ public class RavenwoodDriver {
        return false;
    }

    private static Exception makeRecoverableExceptionInstance(Throwable inner) {
        var outer = new Exception(String.format("Exception detected on thread %s: "
                + " *** Continuing running the remaining tests ***",
                Thread.currentThread().getName()), inner);
    private static class RavenwoodRecoverableExceptionWrapper extends Exception {
        RavenwoodRecoverableExceptionWrapper(String message, Throwable cause) {
            super(message, cause);
        }

        @Override
        public String getMessage() {
            return super.getMessage() + " : " + getCause().getMessage();
        }
    }

    private static Throwable makeRecoverableExceptionInstance(Throwable th) {
        if (th instanceof RavenwoodRecoverableExceptionWrapper) {
            return th;
        }
        var outer = new RavenwoodRecoverableExceptionWrapper(
                "Exception detected on thread " + Thread.currentThread().getName() + ": "
                + " *** Continuing running the remaining test ***", th);
        Log.e(TAG, outer.getMessage(), outer);
        return outer;
    }

    private static void dispatchMessage(Message msg) {
        // If there's already an exception caught and pending, don't run any more messages.
        if (hasPendingRecoverableUncaughtException()) {
            return;
        }
        try {
            msg.getTarget().dispatchMessage(msg);
        } catch (Throwable th) {
@@ -633,8 +662,7 @@ public class RavenwoodDriver {
                    Thread.currentThread());
            sStdErr.println(desc);
            if (isThrowableRecoverable(th)) {
                sPendingRecoverableUncaughtException.compareAndSet(null,
                        makeRecoverableExceptionInstance(th));
                setPendingRecoverableUncaughtException(th);
                return;
            }
            throw th;
@@ -642,19 +670,44 @@ public class RavenwoodDriver {
    }

    /**
     * A callback when a test class finishes its execution, mostly only for debugging.
     * A callback when a test class finishes its execution.
     */
    public static void exitTestClass() {
        maybeThrowPendingRecoverableUncaughtException();
        maybeThrowPendingRecoverableUncaughtExceptionAndClear();
    }

    private static void setPendingRecoverableUncaughtException(Throwable th) {
        sPendingRecoverableUncaughtException.compareAndSet(null,
                makeRecoverableExceptionInstance(th));
    }

    private static boolean hasPendingRecoverableUncaughtException() {
        return sPendingRecoverableUncaughtException.get() != null;
    }

    private static void maybeThrowPendingRecoverableUncaughtException() {
        final Throwable pending = sPendingRecoverableUncaughtException.getAndSet(null);
    private static Throwable getPendingRecoverableUncaughtException(boolean clear) {
        if (clear) {
            return sPendingRecoverableUncaughtException.getAndSet(null);
        } else {
            return sPendingRecoverableUncaughtException.get();
        }
    }

    private static void maybeThrowPendingRecoverableUncaughtException(boolean clear) {
        final Throwable pending = getPendingRecoverableUncaughtException(clear);
        if (pending != null) {
            SneakyThrow.sneakyThrow(pending);
        }
    }

    private static void maybeThrowPendingRecoverableUncaughtExceptionAndClear() {
        maybeThrowPendingRecoverableUncaughtException(true);
    }

    private static void maybeThrowPendingRecoverableUncaughtExceptionNoClear() {
        maybeThrowPendingRecoverableUncaughtException(false);
    }

    /**
     * Prints the stack trace from all threads.
     */
@@ -753,8 +806,7 @@ public class RavenwoodDriver {

    private static void onUncaughtException(Thread thread, Throwable inner) {
        if (isThrowableRecoverable(inner)) {
            sPendingRecoverableUncaughtException.compareAndSet(null,
                    makeRecoverableExceptionInstance(inner));
            setPendingRecoverableUncaughtException(inner);
            return;
        }
        var msg = String.format(
+63 −4
Original line number Diff line number Diff line
@@ -19,6 +19,8 @@ import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.Handler;
import android.os.Looper;
import android.os.MessageQueue;
import android.util.Log;

import com.android.ravenwood.common.RavenwoodCommonUtils;
import com.android.ravenwood.common.SneakyThrow;
@@ -36,6 +38,8 @@ public class RavenwoodUtils {
    private RavenwoodUtils() {
    }

    private static final int DEFAULT_TIMEOUT_SECONDS = 10;

    /**
     * Load a JNI library respecting {@code java.library.path}
     * (which reflects {@code LD_LIBRARY_PATH}).
@@ -88,7 +92,7 @@ public class RavenwoodUtils {
            latch.countDown();
        });
        try {
            latch.await(30, TimeUnit.SECONDS);
            latch.await(DEFAULT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            throw new RuntimeException("Interrupted while waiting on the Runnable", e);
        }
@@ -123,8 +127,57 @@ public class RavenwoodUtils {
     * Run a Runnable on main thread and wait for it to complete.
     */
    @Nullable
    public static void runOnMainThreadSync(@NonNull Runnable r) {
        runOnHandlerSync(getMainHandler(), r);
    public static void runOnMainThreadSync(@NonNull ThrowingRunnable r) {
        runOnHandlerSync(getMainHandler(), () -> {
            r.run();
            return null;
        });
    }

    /**
     * Set by {@link RavenwoodDriver} to run code before {@link #waitForLooperDone(Looper)}.
     */
    static volatile Runnable sPendingExceptionThrower = () -> {};

    /**
     * Wait for a looper to be idle.
     *
     * When running on Ravenwood, this will also throw the pending exception, if any.
     */
    public static void waitForLooperDone(Looper looper) {
        var idler = new Idler();
        looper.getQueue().addIdleHandler(idler);
        idler.waitForIdle();

        sPendingExceptionThrower.run();
    }

    /**
     * Wait for a looper to be idle.
     *
     * When running on Ravenwood, this will also throw the pending exception, if any.
     */
    public static void waitForMainLooperDone() {
        waitForLooperDone(Looper.getMainLooper());
    }

    private static class Idler implements MessageQueue.IdleHandler {
        private final CountDownLatch mLatch = new CountDownLatch(1);

        @Override
        public boolean queueIdle() {
            mLatch.countDown();
            return false; // One-shot idle handler returns true.
        }

        public boolean waitForIdle() {
            try {
                return mLatch.await(DEFAULT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
            } catch (InterruptedException e) {
                Log.w("Idler", "Interrupted");
                return false;
            }
        }
    }

    /**
@@ -157,9 +210,15 @@ public class RavenwoodUtils {
        };
    }

    /** Used by {@link #memoize(ThrowingSupplier)}  */
    public interface ThrowingRunnable {
        /** run the code. */
        void run() throws Exception;
    }

    /** Used by {@link #memoize(ThrowingSupplier)}  */
    public interface ThrowingSupplier<T> {
        /** */
        /** run the code. */
        T get() throws Exception;
    }
}
+30 −94
Original line number Diff line number Diff line
@@ -15,150 +15,86 @@
 */
package com.android.ravenwoodtest.runnercallbacktests;

import static com.google.common.truth.Truth.assertThat;

import static org.junit.Assert.fail;

import android.os.Handler;
import android.os.Looper;
import android.platform.test.annotations.NoRavenizer;
import android.platform.test.ravenwood.RavenwoodDriver;
import android.platform.test.ravenwood.RavenwoodUtils;

import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;

import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;

import java.util.concurrent.atomic.AtomicInteger;


@NoRavenizer // This class shouldn't be executed with RavenwoodAwareTestRunner.
public class RavenwoodRunnerExecutionTest extends RavenwoodRunnerTestBase {
    private static boolean sOrigTolerateUnhandledAsserts;
    private static boolean sOrigTolerateUnhandledExceptions;

    /** Save the TOLERATE_* flags and set them to false. */
    private static void initTolerateFlags() {
        sOrigTolerateUnhandledAsserts =
                RavenwoodDriver.TOLERATE_UNHANDLED_ASSERTS;
        sOrigTolerateUnhandledExceptions =
                RavenwoodDriver.TOLERATE_UNHANDLED_EXCEPTIONS;

        RavenwoodDriver.TOLERATE_UNHANDLED_ASSERTS = false;
        RavenwoodDriver.TOLERATE_UNHANDLED_EXCEPTIONS = false;
    }

    /** Restore the original TOLERATE_* flags. */
    private static void restoreTolerateFlags() {
        RavenwoodDriver.TOLERATE_UNHANDLED_ASSERTS =
                sOrigTolerateUnhandledAsserts;
        RavenwoodDriver.TOLERATE_UNHANDLED_EXCEPTIONS =
                sOrigTolerateUnhandledExceptions;
    }

    private static void ensureMainThreadAlive() {
        var value = new AtomicInteger(0);
        RavenwoodUtils.runOnMainThreadSync(() -> value.set(1));
        assertThat(value.get()).isEqualTo(1);
    }

    /**
     * Make sure TOLERATE_UNHANDLED_ASSERTS works.
     * Test around exceptions on handler threads.
     */
    @RunWith(AndroidJUnit4.class)
    // CHECKSTYLE:OFF
    @Expected("""
    testRunStarted: classes
    testSuiteStarted: classes
    testSuiteStarted: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerExecutionTest$MainThreadAssertionFailureTest
    testStarted: test1(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerExecutionTest$MainThreadAssertionFailureTest)
    testFailure: Exception detected on thread Ravenwood:Main:  *** Continuing running the remaining tests ***
    testFinished: test1(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerExecutionTest$MainThreadAssertionFailureTest)
    testSuiteFinished: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerExecutionTest$MainThreadAssertionFailureTest
    testSuiteStarted: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerExecutionTest$MainThreadExceptionAndwaitForMainLooperDoneTest
    testStarted: test1(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerExecutionTest$MainThreadExceptionAndwaitForMainLooperDoneTest)
    testFailure: Exception detected on thread Ravenwood:Main:  *** Continuing running the remaining test *** : Intentional exception on the main thread!
    testFinished: test1(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerExecutionTest$MainThreadExceptionAndwaitForMainLooperDoneTest)
    testSuiteFinished: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerExecutionTest$MainThreadExceptionAndwaitForMainLooperDoneTest
    testSuiteFinished: classes
    testRunFinished: 1,1,0,0
    """)
    // CHECKSTYLE:ON
    public static class MainThreadAssertionFailureTest {
        @BeforeClass
        public static void beforeClass() {
            initTolerateFlags();

            // Comment it out to test the false case.
            RavenwoodDriver.TOLERATE_UNHANDLED_ASSERTS = true;
        }

        @AfterClass
        public static void afterClass() {
            restoreTolerateFlags();
        }
    public static class MainThreadExceptionAndwaitForMainLooperDoneTest {

        @Test
        public void test1() throws Exception {
            var h = new Handler(Looper.getMainLooper());
            h.post(() -> fail("failed on the man thread"));
            h.post(() -> {
                throw new RuntimeException("Intentional exception on the main thread!");
            });

            // If the flag isn't set to true, then the looper would be dead, so don't do it.
            if (RavenwoodDriver.TOLERATE_UNHANDLED_ASSERTS) {
                InstrumentationRegistry.getInstrumentation().waitForIdleSync();
                ensureMainThreadAlive();
            } else {
                // waitForIdleSync() won't work, so just wait for a bit...
                Thread.sleep(5_000);
            }
            // This will wait for the looper idle, and checks for a pending exception and throws
            // if any. So the remaining code shouldn't be executed.
            RavenwoodUtils.waitForMainLooperDone();

            fail("Shouldn't reach here");
        }
    }

    /**
     * Make sure TOLERATE_UNHANDLED_EXCEPTIONS works.
     * Test around exceptions on handler threads.
     */
    @RunWith(AndroidJUnit4.class)
    // CHECKSTYLE:OFF
    @Expected("""
    testRunStarted: classes
    testSuiteStarted: classes
    testSuiteStarted: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerExecutionTest$MainThreadRuntimeExceptionTest
    testStarted: test1(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerExecutionTest$MainThreadRuntimeExceptionTest)
    testFailure: Exception detected on thread Ravenwood:Main:  *** Continuing running the remaining tests ***
    testFinished: test1(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerExecutionTest$MainThreadRuntimeExceptionTest)
    testSuiteFinished: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerExecutionTest$MainThreadRuntimeExceptionTest
    testSuiteStarted: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerExecutionTest$MainThreadExceptionAndPostTest
    testStarted: test1(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerExecutionTest$MainThreadExceptionAndPostTest)
    testFailure: Exception detected on thread Ravenwood:Main:  *** Continuing running the remaining test *** : Intentional exception on the main thread!
    testFinished: test1(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerExecutionTest$MainThreadExceptionAndPostTest)
    testSuiteFinished: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerExecutionTest$MainThreadExceptionAndPostTest
    testSuiteFinished: classes
    testRunFinished: 1,1,0,0
    """)
    // CHECKSTYLE:ON
    public static class MainThreadRuntimeExceptionTest {
        @BeforeClass
        public static void beforeClass() {
            initTolerateFlags();

            // Comment it out to test the false case.
            RavenwoodDriver.TOLERATE_UNHANDLED_EXCEPTIONS = true;
        }

        @AfterClass
        public static void afterClass() {
            restoreTolerateFlags();
        }

    public static class MainThreadExceptionAndPostTest {
        @Test
        public void test1() throws Exception {
            var h = new Handler(Looper.getMainLooper());
            h.post(() -> {
                throw new RuntimeException("exception on the man thread");
                throw new RuntimeException("Intentional exception on the main thread!");
            });

            // If the flag isn't set to true, then the looper would be dead, so don't do it.
            if (RavenwoodDriver.TOLERATE_UNHANDLED_EXCEPTIONS) {
                InstrumentationRegistry.getInstrumentation().waitForIdleSync();
                ensureMainThreadAlive();
            } else {
                // waitForIdleSync() won't work, so just wait for a bit...
                Thread.sleep(5_000);
            }
            // Because the above exception happens first, this message shouldn't be
            // executed, because before Looper executes each message, we check for a pending
            // exception and prevents running farther messages.
            h.post(() -> {
                setError(new RuntimeException("Shouldn't reach here"));
            });
            RavenwoodUtils.waitForMainLooperDone();
        }
    }
}
Loading