blob: 64ec5b2cb6fc554566292f743d4c629d505db74a [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.server.wifi;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.Handler;
import android.os.Looper;
import android.os.SystemClock;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.wifi.util.GeneralUtil.Mutable;
import java.util.function.Supplier;
import javax.annotation.concurrent.ThreadSafe;
/**
* Runs code on one of the Wifi service threads from another thread (For ex: incoming AIDL call from
* a binder thread), in order to prevent race conditions.
* Note: This is a utility class and each wifi service may have separate instances of this class on
* their corresponding main thread for servicing incoming AIDL calls.
*/
@ThreadSafe
public class WifiThreadRunner {
private static final String TAG = "WifiThreadRunner";
/** Max wait time for posting blocking runnables */
private static final int RUN_WITH_SCISSORS_TIMEOUT_MILLIS = 4000;
/** For test only */
private boolean mTimeoutsAreErrors = false;
private volatile Thread mDispatchThread = null;
private final Handler mHandler;
public WifiThreadRunner(Handler handler) {
mHandler = handler;
}
/**
* Synchronously runs code on the main Wifi thread and return a value.
* <b>Blocks</b> the calling thread until the callable completes execution on the main Wifi
* thread.
*
* BEWARE OF DEADLOCKS!!!
*
* @param <T> the return type
* @param supplier the lambda that should be run on the main Wifi thread
* e.g. wifiThreadRunner.call(() -> mWifiApConfigStore.getApConfiguration())
* or wifiThreadRunner.call(mWifiApConfigStore::getApConfiguration)
* @param valueToReturnOnTimeout If the lambda provided could not be run within the timeout (
* {@link #RUN_WITH_SCISSORS_TIMEOUT_MILLIS}), will return this provided value
* instead.
* @return value retrieved from Wifi thread, or |valueToReturnOnTimeout| if the call failed.
* Beware of NullPointerExceptions when expecting a primitive (e.g. int, long) return
* type, it may still return null and throw a NullPointerException when auto-unboxing!
* Recommend capturing the return value in an Integer or Long instead and explicitly
* handling nulls.
*/
@Nullable
public <T> T call(@NonNull Supplier<T> supplier, T valueToReturnOnTimeout) {
Mutable<T> result = new Mutable<>();
boolean runWithScissorsSuccess = runWithScissors(mHandler,
() -> result.value = supplier.get(),
RUN_WITH_SCISSORS_TIMEOUT_MILLIS);
if (runWithScissorsSuccess) {
return result.value;
} else {
Throwable callerThreadThrowable = new Throwable("Caller thread Stack trace:");
Throwable wifiThreadThrowable = new Throwable("Wifi thread Stack trace:");
wifiThreadThrowable.setStackTrace(mHandler.getLooper().getThread().getStackTrace());
Log.e(TAG, "WifiThreadRunner.call() timed out!", callerThreadThrowable);
Log.e(TAG, "WifiThreadRunner.call() timed out!", wifiThreadThrowable);
if (mTimeoutsAreErrors) {
throw new RuntimeException("WifiThreadRunner.call() timed out!");
}
return valueToReturnOnTimeout;
}
}
/**
* Runs a Runnable on the main Wifi thread and <b>blocks</b> the calling thread until the
* Runnable completes execution on the main Wifi thread.
*
* BEWARE OF DEADLOCKS!!!
*
* @return true if the runnable executed successfully, false otherwise
*/
public boolean run(@NonNull Runnable runnable) {
boolean runWithScissorsSuccess =
runWithScissors(mHandler, runnable, RUN_WITH_SCISSORS_TIMEOUT_MILLIS);
if (runWithScissorsSuccess) {
return true;
} else {
Throwable callerThreadThrowable = new Throwable("Caller thread Stack trace:");
Throwable wifiThreadThrowable = new Throwable("Wifi thread Stack trace:");
wifiThreadThrowable.setStackTrace(mHandler.getLooper().getThread().getStackTrace());
Log.e(TAG, "WifiThreadRunner.run() timed out!", callerThreadThrowable);
Log.e(TAG, "WifiThreadRunner.run() timed out!", wifiThreadThrowable);
if (mTimeoutsAreErrors) {
throw new RuntimeException("WifiThreadRunner.run() timed out!");
}
return false;
}
}
/**
* Sets whether or not a RuntimeError should be thrown when a timeout occurs.
*
* For test purposes only!
*/
@VisibleForTesting
public void setTimeoutsAreErrors(boolean timeoutsAreErrors) {
Log.d(TAG, "setTimeoutsAreErrors " + timeoutsAreErrors);
mTimeoutsAreErrors = timeoutsAreErrors;
}
/**
* Prepares to run on a different thread.
*
* Useful when TestLooper#startAutoDispatch is used to test code that uses #call or #run,
* because in this case the messages are dispatched by a thread that is not actually the
* thread associated with the looper. Should be called before each call
* to TestLooper#startAutoDispatch, without intervening calls to other TestLooper dispatch
* methods.
*
* For test purposes only!
*/
@VisibleForTesting
public void prepareForAutoDispatch() {
mHandler.postAtFrontOfQueue(() -> {
mDispatchThread = Thread.currentThread();
});
}
/**
* Asynchronously runs a Runnable on the main Wifi thread.
*
* @return true if the runnable was successfully posted <b>(not executed)</b> to the main Wifi
* thread, false otherwise
*/
public boolean post(@NonNull Runnable runnable) {
return mHandler.post(runnable);
}
/**
* Asynchronously runs a Runnable on the main Wifi thread with delay.
*
* @param runnable The Runnable that will be executed.
* @param delayMillis The delay (in milliseconds) until the Runnable
* will be executed.
* @return true if the runnable was successfully posted <b>(not executed)</b> to the main Wifi
* thread, false otherwise
*/
public boolean postDelayed(@NonNull Runnable runnable, long delayMillis) {
return mHandler.postDelayed(runnable, delayMillis);
}
/**
* Remove any pending posts of Runnable r that are in the message queue.
*
* @param r The Runnable that will be removed.
*/
public final void removeCallbacks(@NonNull Runnable r) {
mHandler.removeCallbacks(r);
}
// Note: @hide methods copied from android.os.Handler
/**
* Runs the specified task synchronously.
* <p>
* If the current thread is the same as the handler thread, then the runnable
* runs immediately without being enqueued. Otherwise, posts the runnable
* to the handler and waits for it to complete before returning.
* </p><p>
* This method is dangerous! Improper use can result in deadlocks.
* Never call this method while any locks are held or use it in a
* possibly re-entrant manner.
* </p><p>
* This method is occasionally useful in situations where a background thread
* must synchronously await completion of a task that must run on the
* handler's thread. However, this problem is often a symptom of bad design.
* Consider improving the design (if possible) before resorting to this method.
* </p><p>
* One example of where you might want to use this method is when you just
* set up a Handler thread and need to perform some initialization steps on
* it before continuing execution.
* </p><p>
* If timeout occurs then this method returns <code>false</code> but the runnable
* will remain posted on the handler and may already be in progress or
* complete at a later time.
* </p><p>
* When using this method, be sure to use {@link Looper#quitSafely} when
* quitting the looper. Otherwise {@link #runWithScissors} may hang indefinitely.
* (TODO: We should fix this by making MessageQueue aware of blocking runnables.)
* </p>
*
* @param r The Runnable that will be executed synchronously.
* @param timeout The timeout in milliseconds, or 0 to wait indefinitely.
*
* @return Returns true if the Runnable was successfully executed.
* Returns false on failure, usually because the
* looper processing the message queue is exiting.
*
* @hide This method is prone to abuse and should probably not be in the API.
* If we ever do make it part of the API, we might want to rename it to something
* less funny like runUnsafe().
*/
private boolean runWithScissors(@NonNull Handler handler, @NonNull Runnable r,
long timeout) {
if (r == null) {
throw new IllegalArgumentException("runnable must not be null");
}
if (timeout < 0) {
throw new IllegalArgumentException("timeout must be non-negative");
}
if (Looper.myLooper() == handler.getLooper()) {
r.run();
return true;
}
if (Thread.currentThread() == mDispatchThread) {
r.run();
return true;
}
BlockingRunnable br = new BlockingRunnable(r);
return br.postAndWait(handler, timeout);
}
private static final class BlockingRunnable implements Runnable {
private final Runnable mTask;
private boolean mDone;
BlockingRunnable(Runnable task) {
mTask = task;
}
@Override
public void run() {
try {
mTask.run();
} finally {
synchronized (this) {
mDone = true;
notifyAll();
}
}
}
public boolean postAndWait(Handler handler, long timeout) {
if (!handler.post(this)) {
return false;
}
synchronized (this) {
if (timeout > 0) {
final long expirationTime = SystemClock.uptimeMillis() + timeout;
while (!mDone) {
long delay = expirationTime - SystemClock.uptimeMillis();
if (delay <= 0) {
return false; // timeout
}
try {
wait(delay);
} catch (InterruptedException ex) {
}
}
} else {
while (!mDone) {
try {
wait();
} catch (InterruptedException ex) {
}
}
}
}
return true;
}
}
}