diff options
| author | 2024-11-21 10:39:24 -0800 | |
|---|---|---|
| committer | 2024-11-22 08:40:04 -0800 | |
| commit | c8bebaf2bab50b2297a2e1a1e69716317624856c (patch) | |
| tree | 9d5bdaf96cb76ab0dfbe88782d4212f80f9ef388 | |
| parent | 7980a97fe0bfcf7a42bc308119995728b1ec41d5 (diff) | |
Create a PIC nonce-watcher for external clients
This creates a NonceWatcher feature that reports if a PIC nonce has
changed its value. The feature is only effective in the same process
as the nonce server (e.g., system_server for MODULE_SYSTEM nonces),
but it is very fast in-process.
Clients wait for a nonce change by blocking on a semaphore. This
isolates the PIC invalidation hot path from delays in the client
behavior.
Flag: android.app.pic_uses_shared_memory
Bug: 360897450
Test: atest
* FrameworksCoreTests:PropertyInvalidatedCacheTests
* FrameworksCoreTests:IpcDataCacheTest
* CtsOsTestCases:IpcDataCacheTest
Change-Id: I56d89df8301e397860836dd06bf9fed14bc13d45
| -rw-r--r-- | core/java/android/app/PropertyInvalidatedCache.java | 208 | ||||
| -rw-r--r-- | core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java | 69 |
2 files changed, 256 insertions, 21 deletions
diff --git a/core/java/android/app/PropertyInvalidatedCache.java b/core/java/android/app/PropertyInvalidatedCache.java index e218418336c5..3973c58c0708 100644 --- a/core/java/android/app/PropertyInvalidatedCache.java +++ b/core/java/android/app/PropertyInvalidatedCache.java @@ -61,6 +61,8 @@ import java.util.Random; import java.util.Set; import java.util.WeakHashMap; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; /** @@ -680,12 +682,17 @@ public class PropertyInvalidatedCache<Query, Result> { @GuardedBy("mLock") private boolean mTestMode = false; - /** - * The local value of the handler, used during testing but also used directly by the - * NonceLocal handler. - */ + // This is the local value of the nonce, as last set by the NonceHandler. It is always + // updated by the setNonce() operation. The getNonce() operation returns this value in + // NonceLocal handlers and handlers in test mode. + @GuardedBy("mLock") + protected long mShadowNonce = NONCE_UNSET; + + // A list of watchers to be notified of changes. This is null until at least one watcher + // registers. Checking for null is meant to be the fastest way the handler can determine + // that there are no watchers to be notified. @GuardedBy("mLock") - protected long mTestNonce = NONCE_UNSET; + private ArrayList<Semaphore> mWatchers; /** * The methods to get and set a nonce from whatever storage is being used. mLock may be @@ -701,27 +708,60 @@ public class PropertyInvalidatedCache<Query, Result> { /** * Get a nonce from storage. If the handler is in test mode, the nonce is returned from - * the local mTestNonce. + * the local mShadowNonce. */ long getNonce() { synchronized (mLock) { - if (mTestMode) return mTestNonce; + if (mTestMode) return mShadowNonce; } return getNonceInternal(); } /** - * Write a nonce to storage. If the handler is in test mode, the nonce is written to the - * local mTestNonce and storage is not affected. + * Write a nonce to storage. The nonce is always written to the local mShadowNonce. If + * the handler is not in test mode the nonce is also written to storage. */ void setNonce(long val) { synchronized (mLock) { - if (mTestMode) { - mTestNonce = val; - return; + mShadowNonce = val; + if (!mTestMode) { + setNonceInternal(val); + } + wakeAllWatchersLocked(); + } + } + + @GuardedBy("mLock") + private void wakeAllWatchersLocked() { + if (mWatchers != null) { + for (int i = 0; i < mWatchers.size(); i++) { + mWatchers.get(i).release(); + } + } + } + + /** + * Register a watcher to be notified when a nonce changes. There is no check for + * duplicates. In general, this method is called only from {@link NonceWatcher}. + */ + void registerWatcher(Semaphore s) { + synchronized (mLock) { + if (mWatchers == null) { + mWatchers = new ArrayList<>(); + } + mWatchers.add(s); + } + } + + /** + * Unregister a watcher. Nothing happens if the watcher is not registered. + */ + void unregisterWatcher(Semaphore s) { + synchronized (mLock) { + if (mWatchers != null) { + mWatchers.remove(s); } } - setNonceInternal(val); } /** @@ -854,7 +894,7 @@ public class PropertyInvalidatedCache<Query, Result> { void setTestMode(boolean mode) { synchronized (mLock) { mTestMode = mode; - mTestNonce = NONCE_UNSET; + mShadowNonce = NONCE_UNSET; } } @@ -1028,7 +1068,7 @@ public class PropertyInvalidatedCache<Query, Result> { /** * SystemProperties and shared storage are protected and cannot be written by random * processes. So, for testing purposes, the NonceLocal handler stores the nonce locally. The - * NonceLocal uses the mTestNonce in the superclass, regardless of test mode. + * NonceLocal uses the mShadowNonce in the superclass, regardless of test mode. */ private static class NonceLocal extends NonceHandler { // The saved nonce. @@ -1040,16 +1080,130 @@ public class PropertyInvalidatedCache<Query, Result> { @Override long getNonceInternal() { - return mTestNonce; + return mShadowNonce; } @Override void setNonceInternal(long value) { - mTestNonce = value; + mShadowNonce = value; + } + } + + /** + * A NonceWatcher lets an external client test if a nonce value has changed from the last time + * the watcher was checked. + * @hide + */ + public static class NonceWatcher implements AutoCloseable { + // The handler for the key. + private final NonceHandler mHandler; + + // The last-seen value. This is initialized to "unset". + private long mLastSeen = NONCE_UNSET; + + // The semaphore that the watcher waits on. A permit is released every time the nonce + // changes. Permits are acquired in the wait method. + private final Semaphore mSem = new Semaphore(0); + + /** + * Create a watcher for a handler. The last-seen value is not set here and will be + * "unset". Therefore, a call to isChanged() will return true if the nonce has ever been + * set, no matter when the watcher is first created. Clients may want to flush that + * change by calling isChanged() immediately after constructing the object. + */ + private NonceWatcher(@NonNull NonceHandler handler) { + mHandler = handler; + mHandler.registerWatcher(mSem); + } + + /** + * Unregister to be notified when a nonce changes. NonceHandler allows a call to + * unregisterWatcher with a semaphore that is not registered, so there is no check inside + * this method to guard against multiple closures. + */ + @Override + public void close() { + mHandler.unregisterWatcher(mSem); + } + + /** + * Return the last seen value of the nonce. This does not update that value. Only + * {@link #isChanged()} updates the value. + */ + public long lastSeen() { + return mLastSeen; + } + + /** + * Return true if the nonce has changed from the last time isChanged() was called. The + * method is not thread safe. + * @hide + */ + public boolean isChanged() { + long current = mHandler.getNonce(); + if (current != mLastSeen) { + mLastSeen = current; + return true; + } + return false; + } + + /** + * Wait for the nonce value to change. It is not guaranteed that the nonce has changed when + * this returns: clients must confirm with {@link #isChanged}. The wait operation is only + * effective in a process that writes the nonces. The function returns the number of times + * the nonce had changed since the last call to the method. + * @hide + */ + public int waitForChange() throws InterruptedException { + mSem.acquire(1); + return 1 + mSem.drainPermits(); + } + + /** + * Wait for the nonce value to change. It is not guaranteed that the nonce has changed when + * this returns: clients must confirm with {@link #isChanged}. The wait operation is only + * effective in a process that writes the nonces. The function returns the number of times + * the nonce changed since the last call to the method. A return value of zero means the + * timeout expired. Beware that a timeout of 0 means the function will not wait at all. + * @hide + */ + public int waitForChange(long timeout, TimeUnit timeUnit) throws InterruptedException { + if (mSem.tryAcquire(1, timeout, timeUnit)) { + return 1 + mSem.drainPermits(); + } else { + return 0; + } + } + + /** + * Wake the watcher by releasing the semaphore. This can be used to wake clients that are + * blocked in {@link #waitForChange} without affecting the underlying nonce. + * @hide + */ + public void wakeUp() { + mSem.release(); } } /** + * Return a NonceWatcher for the cache. + * @hide + */ + public NonceWatcher getNonceWatcher() { + return new NonceWatcher(mNonce); + } + + /** + * Return a NonceWatcher for the given property. If a handler does not exist for the + * property, one is created. This throws if the property name is not a valid cache key. + * @hide + */ + public static NonceWatcher getNonceWatcher(@NonNull String propertyName) { + return new NonceWatcher(getNonceHandler(propertyName)); + } + + /** * Complete key prefixes. */ private static final String PREFIX_TEST = CACHE_KEY_PREFIX + "." + MODULE_TEST + "."; @@ -1663,6 +1817,26 @@ public class PropertyInvalidatedCache<Query, Result> { } /** + * Non-static version of corkInvalidations() for situations in which the cache instance is + * available. This is slightly faster than than the static versions because it does not have + * to look up the NonceHandler for a given property name. + * @hide + */ + public void corkInvalidations() { + mNonce.cork(); + } + + /** + * Non-static version of uncorkInvalidations() for situations in which the cache instance is + * available. This is slightly faster than than the static versions because it does not have + * to look up the NonceHandler for a given property name. + * @hide + */ + public void uncorkInvalidations() { + mNonce.uncork(); + } + + /** * Invalidate caches in all processes that are keyed for the module and api. * @hide */ diff --git a/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java b/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java index 2fc72e1d3994..177c7f0b2f27 100644 --- a/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java +++ b/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java @@ -26,6 +26,7 @@ import static android.app.PropertyInvalidatedCache.NonceStore.INVALID_NONCE_INDE import static com.android.internal.os.Flags.FLAG_APPLICATION_SHARED_MEMORY_ENABLED; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertSame; @@ -34,6 +35,8 @@ import static org.junit.Assert.fail; import android.annotation.SuppressLint; import android.app.PropertyInvalidatedCache.Args; +import android.app.PropertyInvalidatedCache.NonceWatcher; +import android.app.PropertyInvalidatedCache.NonceStore; import android.os.Binder; import com.android.internal.os.ApplicationSharedMemory; @@ -45,11 +48,15 @@ import android.platform.test.ravenwood.RavenwoodRule; import androidx.test.filters.SmallTest; +import com.android.internal.os.ApplicationSharedMemory; + import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; +import java.util.concurrent.TimeUnit; + /** * Test for verifying the behavior of {@link PropertyInvalidatedCache}. This test does * not use any actual binder calls - it is entirely self-contained. This test also relies @@ -490,6 +497,62 @@ public class PropertyInvalidatedCacheTests { } } + // Verify that NonceWatcher change reporting works properly + @Test + public void testNonceWatcherChanged() { + // Create a cache that will write a system nonce. + TestCache sysCache = new TestCache(MODULE_SYSTEM, "watcher1"); + sysCache.testPropertyName(); + + try (NonceWatcher watcher1 = sysCache.getNonceWatcher()) { + + // The property has never been invalidated so it is still unset. + assertFalse(watcher1.isChanged()); + + // Invalidate the cache. The first call to isChanged will return true but the second + // call will return false; + sysCache.invalidateCache(); + assertTrue(watcher1.isChanged()); + assertFalse(watcher1.isChanged()); + + // Invalidate the cache. The first call to isChanged will return true but the second + // call will return false; + sysCache.invalidateCache(); + sysCache.invalidateCache(); + assertTrue(watcher1.isChanged()); + assertFalse(watcher1.isChanged()); + + NonceWatcher watcher2 = sysCache.getNonceWatcher(); + // This watcher return isChanged() immediately because the nonce is not UNSET. + assertTrue(watcher2.isChanged()); + } + } + + // Verify that NonceWatcher wait-for-change works properly + @Test + public void testNonceWatcherWait() throws Exception { + // Create a cache that will write a system nonce. + TestCache sysCache = new TestCache(MODULE_TEST, "watcher1"); + + // Use the watcher outside a try-with-resources block. + NonceWatcher watcher1 = sysCache.getNonceWatcher(); + + // Invalidate the cache and then "wait". + sysCache.invalidateCache(); + assertEquals(watcher1.waitForChange(), 1); + + // Invalidate the cache three times and then "wait". + sysCache.invalidateCache(); + sysCache.invalidateCache(); + sysCache.invalidateCache(); + assertEquals(watcher1.waitForChange(), 3); + + // Wait for a change. It won't happen, but the code will time out after 10ms. + assertEquals(watcher1.waitForChange(10, TimeUnit.MILLISECONDS), 0); + + watcher1.close(); + } + // Verify the behavior of shared memory nonce storage. This does not directly test the cache // storing nonces in shared memory. @RequiresFlagsEnabled(FLAG_APPLICATION_SHARED_MEMORY_ENABLED) @@ -502,10 +565,8 @@ public class PropertyInvalidatedCacheTests { // Create a server-side store and a client-side store. The server's store is mutable and // the client's store is not mutable. - PropertyInvalidatedCache.NonceStore server = - new PropertyInvalidatedCache.NonceStore(shmem.getSystemNonceBlock(), true); - PropertyInvalidatedCache.NonceStore client = - new PropertyInvalidatedCache.NonceStore(shmem.getSystemNonceBlock(), false); + NonceStore server = new NonceStore(shmem.getSystemNonceBlock(), true); + NonceStore client = new NonceStore(shmem.getSystemNonceBlock(), false); final String name1 = "name1"; assertEquals(server.getHandleForName(name1), INVALID_NONCE_INDEX); |