309 lines
12 KiB
Java
309 lines
12 KiB
Java
/*
|
|
* 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);
|
|
}
|
|
|
|
/**
|
|
* Check if there are any pending posts of messages with callback r in the message queue.
|
|
*
|
|
* @param r The Runnable that will be used to query.
|
|
* @return true if exists, otherwise false.
|
|
*/
|
|
public final boolean hasCallbacks(@NonNull Runnable r) {
|
|
return mHandler.hasCallbacks(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;
|
|
}
|
|
}
|
|
}
|