/* * 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. * Blocks the calling thread until the callable completes execution on the main Wifi * thread. * * BEWARE OF DEADLOCKS!!! * * @param 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 call(@NonNull Supplier supplier, T valueToReturnOnTimeout) { Mutable 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 blocks 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 (not executed) 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 (not executed) 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. *

* 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. *

* 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. *

* 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. *

* 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. *

* If timeout occurs then this method returns false but the runnable * will remain posted on the handler and may already be in progress or * complete at a later time. *

* 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.) *

* * @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; } } }