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

Commit 8837441d authored by Adrian Roos's avatar Adrian Roos Committed by Automerger Merge Worker
Browse files

Merge "Add CancellationSignalBeamer" into udc-dev am: c728095c

parents 4096df10 c728095c
Loading
Loading
Loading
Loading
+325 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 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 android.annotation.Nullable;
import android.system.SystemCleaner;

import java.lang.ref.Cleaner;
import java.lang.ref.Reference;
import java.util.HashMap;

/**
 * A transport for {@link CancellationSignal}, but unlike
 * {@link CancellationSignal#createTransport()} doesn't require pre-creating the transport in the
 * target process. Instead, cancellation is forwarded over the same IPC surface as the cancellable
 * request.
 *
 * <p><strong>Important:</strong> For this to work, the following invariants must be held up:
 * <ul>
 *     <li>A call to beam() <strong>MUST</strong> result in a call to close() on the result
 *     (otherwise, the token will be leaked and cancellation isn't propagated), and that call
 *     must happen after the call using the
 *     token is sent (otherwise, any concurrent cancellation may be lost). It is strongly
 *     recommended to use try-with-resources on the token.
 *     <li>The cancel(), forget() and cancellable operations transporting the token must either
 *     all be oneway on the same binder, or all be non-oneway to guarantee proper ordering.
 *     <li>A {@link CancellationSignal} <strong>SHOULD</strong> be used only once, as there
 *     can only be a single {@link android.os.CancellationSignal.OnCancelListener OnCancelListener}.
 *
 * </ul>
 * <p>Caveats:
 * <ul>
 *     <li>Cancellation is only ever dispatched after the token is closed, and thus after the
 *     call performing the cancellable operation (if the invariants are followed). The operation
 *     must therefore not block the incoming binder thread, or cancellation won't be possible.
 *     <li>Consequently, in the unlikely event that the sender dies right after beaming an already
 *     cancelled {@link CancellationSignal}, the cancellation may be lost (unlike with
 *     {@link CancellationSignal#createTransport()}).
 *     <li>The forwarding OnCancelListener is set in the implied finally phase of try-with-resources
 *         / when closing the token. If the receiver is in the same process, and the signal is
 *         already cancelled, this may invoke the target's OnCancelListener during that phase.
 * </ul>
 *
 *
 * <p>Usage:
 * <pre>
 *  // Sender:
 *
 *  class FooManager {
 *    var mCancellationSignalSender = new CancellationSignalBeamer.Sender() {
 *      &#064;Override
 *      public void onCancel(IBinder token) { remoteIFooService.onCancelToken(token); }
 *
 *      &#064;Override
 *      public void onForget(IBinder token) { remoteIFooService.onForgetToken(token); }
 *    };
 *
 *    public void doCancellableOperation(..., CancellationSignal cs) {
 *      try (var csToken = mCancellationSignalSender.beam(cs)) {
 *          remoteIFooService.doCancellableOperation(..., csToken);
 *      }
 *    }
 *  }
 *
 *  // Receiver:
 *
 *  class FooManagerService extends IFooService.Stub {
 *    var mCancellationSignalReceiver = new CancellationSignalBeamer.Receiver();
 *
 *    &#064;Override
 *    public void doCancellableOperation(..., IBinder csToken) {
 *      CancellationSignal cs = mCancellationSignalReceiver.unbeam(csToken))
 *      // ...
 *    }
 *
 *    &#064;Override
 *    public void onCancelToken(..., IBinder csToken) {
 *      mCancellationSignalReceiver.cancelToken(csToken))
 *    }
 *
 *    &#064;Override
 *    public void onForgetToken(..., IBinder csToken) {
 *      mCancellationSignalReceiver.forgetToken(csToken))
 *    }
 *  }
 *
 * </pre>
 *
 * @hide
 */
public class CancellationSignalBeamer {

    static final Cleaner sCleaner = SystemCleaner.cleaner();

    /** The sending side of an {@link CancellationSignalBeamer} */
    public abstract static class Sender {

        /**
         * Beams a {@link CancellationSignal} through an existing Binder interface.
         *
         * @param cs the {@code CancellationSignal} to beam, or {@code null}.
         * @return an {@link IBinder} token. MUST be {@link CloseableToken#close}d <em>after</em>
         *         the binder call transporting it to the remote process, best with
         *         try-with-resources. {@code null} if {@code cs} was {@code null}.
         */
        // TODO(b/254888024): @MustBeClosed
        @Nullable
        public CloseableToken beam(@Nullable CancellationSignal cs) {
            if (cs == null) {
                return null;
            }
            return new Token(this, cs);
        }

        /**
         * A {@link #beam}ed {@link CancellationSignal} was closed.
         *
         * MUST be forwarded to {@link Receiver#cancel} with proper ordering. See
         * {@link CancellationSignalBeamer} for details.
         */
        public abstract void onCancel(IBinder token);

        /**
         * A {@link #beam}ed {@link CancellationSignal} was GC'd.
         *
         * MUST be forwarded to {@link Receiver#forget} with proper ordering. See
         * {@link CancellationSignalBeamer} for details.
         */
        public abstract void onForget(IBinder token);

        private static class Token extends Binder implements CloseableToken, Runnable {

            private final Sender mSender;
            private Preparer mPreparer;

            private Token(Sender sender, CancellationSignal signal) {
                mSender = sender;
                mPreparer = new Preparer(sender, signal, this);
            }

            @Override
            public void close() {
                Preparer preparer = mPreparer;
                mPreparer = null;
                if (preparer != null) {
                    preparer.setup();
                }
            }

            @Override
            public void run() {
                mSender.onForget(this);
            }

            private static class Preparer implements CancellationSignal.OnCancelListener {
                private final Sender mSender;
                private final CancellationSignal mSignal;
                private final Token mToken;

                private Preparer(Sender sender, CancellationSignal signal, Token token) {
                    mSender = sender;
                    mSignal = signal;
                    mToken = token;
                }

                void setup() {
                    sCleaner.register(this, mToken);
                    mSignal.setOnCancelListener(this);
                }

                @Override
                public void onCancel() {
                    try {
                        mSender.onCancel(mToken);
                    } finally {
                        // Make sure we dispatch onCancel before the cleaner can run.
                        Reference.reachabilityFence(this);
                    }
                }
            }
        }

        /**
         * A {@link #beam}ed {@link CancellationSignal} ready for sending over Binder.
         *
         * MUST be closed <em>after</em> it is sent over binder, ideally through try-with-resources.
         */
        public interface CloseableToken extends IBinder, AutoCloseable {
            @Override
            void close(); // No throws
        }
    }

    /** The receiving side of a {@link CancellationSignalBeamer}. */
    public static class Receiver implements IBinder.DeathRecipient {
        private final HashMap<IBinder, CancellationSignal> mTokenMap = new HashMap<>();
        private final boolean mCancelOnSenderDeath;

        /**
         * Constructs a new {@code Receiver}.
         *
         * @param cancelOnSenderDeath if true, {@link CancellationSignal}s obtained from
         *   {@link #unbeam} are automatically {@link #cancel}led if the sender token
         *   {@link Binder#linkToDeath dies}; otherwise they are simnply dropped. Note: if the
         *   sending process drops all references to the {@link CancellationSignal} before
         *   process death, the cancellation is not guaranteed.
         */
        public Receiver(boolean cancelOnSenderDeath) {
            mCancelOnSenderDeath = cancelOnSenderDeath;
        }

        /**
         * Unbeams a token that was obtained via {@link Sender#beam} and turns it back into a
         * {@link CancellationSignal}.
         *
         * A subsequent call to {@link #cancel} with the same token will cancel the returned
         * {@code CancellationSignal}.
         *
         * @param token a token that was obtained from {@link Sender}, possibly in a remote process.
         * @return a {@link CancellationSignal} linked to the given token.
         */
        @Nullable
        public CancellationSignal unbeam(@Nullable IBinder token) {
            if (token == null) {
                return null;
            }
            synchronized (this) {
                CancellationSignal cs = mTokenMap.get(token);
                if (cs != null) {
                    return cs;
                }

                cs = new CancellationSignal();
                mTokenMap.put(token, cs);
                try {
                    token.linkToDeath(this, 0);
                } catch (RemoteException e) {
                    dead(token);
                }
                return cs;
            }
        }

        /**
         * Forgets state associated with the given token (if any).
         *
         * Subsequent calls to {@link #cancel} or binder death notifications on the token will not
         * have any effect.
         *
         * This MUST be invoked when forwarding {@link Sender#onForget}, otherwise the token and
         * {@link CancellationSignal} will leak if the token was ever {@link #unbeam}ed.
         *
         * Optionally, the receiving service logic may also invoke this if it can guarantee that
         * the unbeamed CancellationSignal isn't needed anymore (i.e. the cancellable operation
         * using the CancellationSignal has been fully completed).
         *
         * @param token the token to forget. No-op if {@code null}.
         */
        public void forget(@Nullable IBinder token) {
            synchronized (this) {
                if (mTokenMap.remove(token) != null) {
                    token.unlinkToDeath(this, 0);
                }
            }
        }

        /**
         * Cancels the {@link CancellationSignal} associated with the given token (if any).
         *
         * This MUST be invoked when forwarding {@link Sender#onCancel}, otherwise the token and
         * {@link CancellationSignal} will leak if the token was ever {@link #unbeam}ed.
         *
         * Optionally, the receiving service logic may also invoke this if it can guarantee that
         * the unbeamed CancellationSignal isn't needed anymore (i.e. the cancellable operation
         * using the CancellationSignal has been fully completed).
         *
         * @param token the token to forget. No-op if {@code null}.
         */
        public void cancel(@Nullable IBinder token) {
            CancellationSignal cs;
            synchronized (this) {
                cs = mTokenMap.get(token);
                if (cs != null) {
                    forget(token);
                } else {
                    return;
                }
            }
            cs.cancel();
        }

        private void dead(@NonNull IBinder token) {
            if (mCancelOnSenderDeath) {
                cancel(token);
            } else {
                forget(token);
            }
        }

        @Override
        public void binderDied(@NonNull IBinder who) {
            dead(who);
        }

        @Override
        public void binderDied() {
            throw new RuntimeException("unreachable");
        }
    }
}
+224 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 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 static android.os.CancellationSignalBeamer.Sender;

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

import android.content.Context;
import android.os.CancellationSignalBeamer.Receiver;
import android.util.PollingCheck;
import android.util.PollingCheck.PollingCheckCondition;

import androidx.test.filters.SmallTest;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;

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

import java.io.File;
import java.io.IOException;
import java.lang.ref.Cleaner;
import java.lang.ref.Reference;
import java.util.concurrent.CountDownLatch;

@RunWith(AndroidJUnit4.class)
@SmallTest
public class CancellationSignalBeamerTest {

    private CancellationSignal mSenderSignal = new CancellationSignal();
    private CancellationSignal mReceivedSignal;
    private Context mContext = InstrumentationRegistry.getInstrumentation().getContext();

    @Test
    public void testBeam_null() {
        try (var token = mSender.beam(null)) {
            assertThat(token).isNull();
            invokeGenericService(token);
        }
        assertThat(mReceivedSignal).isNull();
    }

    @Test
    public void testBeam_nonNull() {
        try (var token = mSender.beam(mSenderSignal)) {
            assertThat(token).isNotNull();
            invokeGenericService(token);
        }
        assertThat(mReceivedSignal).isNotNull();
    }

    @Test
    public void testBeam_async() {
        IBinder outerToken;
        try (var token = mSender.beam(mSenderSignal)) {
            assertThat(token).isNotNull();
            outerToken = token;
        }
        invokeGenericService(outerToken);
        assertThat(mReceivedSignal).isNotNull();
    }

    @Test
    public void testCancelOnSentSignal_cancelsReceivedSignal() {
        try (var token = mSender.beam(mSenderSignal)) {
            invokeGenericService(token);
        }
        mSenderSignal.cancel();
        assertThat(mReceivedSignal.isCanceled()).isTrue();
    }

    @Test
    public void testSendingCancelledSignal_cancelsReceivedSignal() {
        mSenderSignal.cancel();
        try (var token = mSender.beam(mSenderSignal)) {
            invokeGenericService(token);
        }
        assertThat(mReceivedSignal.isCanceled()).isTrue();
    }

    @Test
    public void testUnbeam_null() {
        assertThat(mReceiver.unbeam(null)).isNull();
    }

    @Test
    public void testForget_null() {
        mReceiver.forget(null);
    }

    @Test
    public void testCancel_null() {
        mReceiver.cancel(null);
    }

    @Test
    public void testForget_withUnknownToken() {
        mReceiver.forget(new Binder());
    }

    @Test
    public void testCancel_withUnknownToken() {
        mReceiver.cancel(new Binder());
    }

    @Test
    public void testBinderDied_withUnknownToken() {
        mReceiver.binderDied(new Binder());
    }

    @Test
    public void testReceiverWithCancelOnSenderDead_cancelsOnSenderDeath() {
        var receiver = new Receiver(true /* cancelOnSenderDeath */);
        var token = new Binder();
        var signal = receiver.unbeam(token);
        receiver.binderDied(token);
        assertThat(signal.isCanceled()).isTrue();
    }

    @Test
    public void testReceiverWithoutCancelOnSenderDead_doesntCancelOnSenderDeath() {
        var receiver = new Receiver(false /* cancelOnSenderDeath */);
        var token = new Binder();
        var signal = receiver.unbeam(token);
        receiver.binderDied(token);
        assertThat(signal.isCanceled()).isFalse();
    }

    @Test
    public void testDroppingSentSignal_dropsReceivedSignal() throws Exception {
        // In a multiprocess scenario, sending token over Binder might leak the token
        // on both ends if we create a reference cycle. Simulate that worst-case scenario
        // here by leaking it directly, then test that cleanup of the signals still works.
        var receivedSignalCleaned = new CountDownLatch(1);
        var tokenRef = new Object[1];
        // Reference the cancellation signals in a separate method scope, so we don't
        // accidentally leak them on the stack / in a register.
        Runnable r = () -> {
            try (var token = mSender.beam(mSenderSignal)) {
                tokenRef[0] = token;
                invokeGenericService(token);
            }
            mSenderSignal = null;

            Cleaner.create().register(mReceivedSignal, receivedSignalCleaned::countDown);
            mReceivedSignal = null;
        };
        r.run();

        waitForWithGc(() -> receivedSignalCleaned.getCount() == 0);

        Reference.reachabilityFence(tokenRef[0]);
    }

    @Test
    public void testRepeatedBeaming_doesntLeak() throws Exception {
        var receivedSignalCleaned = new CountDownLatch(1);
        var tokenRef = new Object[1];
        // Reference the cancellation signals in a separate method scope, so we don't
        // accidentally leak them on the stack / in a register.
        Runnable r = () -> {
            try (var token = mSender.beam(mSenderSignal)) {
                tokenRef[0] = token;
                invokeGenericService(token);
            }
            // Beaming again leaves mReceivedSignal dangling, so it should be collected.
            mSender.beam(mSenderSignal).close();

            Cleaner.create().register(mReceivedSignal, receivedSignalCleaned::countDown);
            mReceivedSignal = null;
        };
        r.run();

        waitForWithGc(() -> receivedSignalCleaned.getCount() == 0);

        Reference.reachabilityFence(tokenRef[0]);
    }

    private void waitForWithGc(PollingCheckCondition condition) throws IOException {
        try {
            PollingCheck.waitFor(() -> {
                Runtime.getRuntime().gc();
                return condition.canProceed();
            });
        } catch (AssertionError e) {
            File heap = new File(mContext.getExternalFilesDir(null), "dump.hprof");
            Debug.dumpHprofData(heap.getAbsolutePath());
            throw e;
        }
    }

    private void invokeGenericService(IBinder cancellationSignalToken) {
        mReceivedSignal = mReceiver.unbeam(cancellationSignalToken);
    }

    private final Sender mSender = new Sender() {
        @Override
        public void onCancel(IBinder token) {
            mReceiver.cancel(token);
        }

        @Override
        public void onForget(IBinder token) {
            mReceiver.forget(token);
        }
    };

    private final Receiver mReceiver = new Receiver(false);
}