mirror of https://github.com/grpc/grpc-java.git
binder: Dispatch transact() calls on an Executor when FLAG_ONEWAY would not be respected. (#8987)
Fixes #8914
This commit is contained in:
parent
0628cab226
commit
2bf0a1f271
|
|
@ -115,6 +115,11 @@ public final class BinderTransportTest extends AbstractTransportTest {
|
||||||
@Override
|
@Override
|
||||||
public void flowControlPushBack() throws Exception {}
|
public void flowControlPushBack() throws Exception {}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Ignore("Not yet implemented. See https://github.com/grpc/grpc-java/issues/8931")
|
||||||
|
@Override
|
||||||
|
public void serverNotListening() throws Exception {}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Ignore("This test isn't appropriate for BinderTransport.")
|
@Ignore("This test isn't appropriate for BinderTransport.")
|
||||||
@Override
|
@Override
|
||||||
|
|
|
||||||
|
|
@ -201,7 +201,7 @@ public abstract class BinderTransport
|
||||||
@Nullable
|
@Nullable
|
||||||
protected Status shutdownStatus;
|
protected Status shutdownStatus;
|
||||||
|
|
||||||
@Nullable private IBinder outgoingBinder;
|
@Nullable private OneWayBinderProxy outgoingBinder;
|
||||||
|
|
||||||
private final FlowController flowController;
|
private final FlowController flowController;
|
||||||
|
|
||||||
|
|
@ -278,10 +278,10 @@ public abstract class BinderTransport
|
||||||
}
|
}
|
||||||
|
|
||||||
@GuardedBy("this")
|
@GuardedBy("this")
|
||||||
protected boolean setOutgoingBinder(IBinder binder) {
|
protected boolean setOutgoingBinder(OneWayBinderProxy binder) {
|
||||||
this.outgoingBinder = binder;
|
this.outgoingBinder = binder;
|
||||||
try {
|
try {
|
||||||
binder.linkToDeath(this, 0);
|
binder.getDelegate().linkToDeath(this, 0);
|
||||||
return true;
|
return true;
|
||||||
} catch (RemoteException re) {
|
} catch (RemoteException re) {
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -326,19 +326,13 @@ public abstract class BinderTransport
|
||||||
}
|
}
|
||||||
|
|
||||||
@GuardedBy("this")
|
@GuardedBy("this")
|
||||||
final void sendSetupTransaction(IBinder iBinder) {
|
final void sendSetupTransaction(OneWayBinderProxy iBinder) {
|
||||||
Parcel parcel = Parcel.obtain();
|
try (ParcelHolder parcel = ParcelHolder.obtain()) {
|
||||||
try {
|
parcel.get().writeInt(WIRE_FORMAT_VERSION);
|
||||||
parcel.writeInt(WIRE_FORMAT_VERSION);
|
parcel.get().writeStrongBinder(incomingBinder);
|
||||||
parcel.writeStrongBinder(incomingBinder);
|
iBinder.transact(SETUP_TRANSPORT, parcel);
|
||||||
if (!iBinder.transact(SETUP_TRANSPORT, parcel, null, IBinder.FLAG_ONEWAY)) {
|
|
||||||
shutdownInternal(
|
|
||||||
Status.UNAVAILABLE.withDescription("Failed sending SETUP_TRANSPORT transaction"), true);
|
|
||||||
}
|
|
||||||
} catch (RemoteException re) {
|
} catch (RemoteException re) {
|
||||||
shutdownInternal(statusFromRemoteException(re), true);
|
shutdownInternal(statusFromRemoteException(re), true);
|
||||||
} finally {
|
|
||||||
parcel.recycle();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -346,19 +340,16 @@ public abstract class BinderTransport
|
||||||
private final void sendShutdownTransaction() {
|
private final void sendShutdownTransaction() {
|
||||||
if (outgoingBinder != null) {
|
if (outgoingBinder != null) {
|
||||||
try {
|
try {
|
||||||
outgoingBinder.unlinkToDeath(this, 0);
|
outgoingBinder.getDelegate().unlinkToDeath(this, 0);
|
||||||
} catch (NoSuchElementException e) {
|
} catch (NoSuchElementException e) {
|
||||||
// Ignore.
|
// Ignore.
|
||||||
}
|
}
|
||||||
Parcel parcel = Parcel.obtain();
|
try (ParcelHolder parcel = ParcelHolder.obtain()) {
|
||||||
try {
|
|
||||||
// Send empty flags to avoid a memory leak linked to empty parcels (b/207778694).
|
// Send empty flags to avoid a memory leak linked to empty parcels (b/207778694).
|
||||||
parcel.writeInt(0);
|
parcel.get().writeInt(0);
|
||||||
outgoingBinder.transact(SHUTDOWN_TRANSPORT, parcel, null, IBinder.FLAG_ONEWAY);
|
outgoingBinder.transact(SHUTDOWN_TRANSPORT, parcel);
|
||||||
} catch (RemoteException re) {
|
} catch (RemoteException re) {
|
||||||
// Ignore.
|
// Ignore.
|
||||||
} finally {
|
|
||||||
parcel.recycle();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -369,14 +360,11 @@ public abstract class BinderTransport
|
||||||
} else if (outgoingBinder == null) {
|
} else if (outgoingBinder == null) {
|
||||||
throw Status.FAILED_PRECONDITION.withDescription("Transport not ready.").asException();
|
throw Status.FAILED_PRECONDITION.withDescription("Transport not ready.").asException();
|
||||||
} else {
|
} else {
|
||||||
Parcel parcel = Parcel.obtain();
|
try (ParcelHolder parcel = ParcelHolder.obtain()) {
|
||||||
try {
|
parcel.get().writeInt(id);
|
||||||
parcel.writeInt(id);
|
outgoingBinder.transact(PING, parcel);
|
||||||
outgoingBinder.transact(PING, parcel, null, IBinder.FLAG_ONEWAY);
|
|
||||||
} catch (RemoteException re) {
|
} catch (RemoteException re) {
|
||||||
throw statusFromRemoteException(re).asException();
|
throw statusFromRemoteException(re).asException();
|
||||||
} finally {
|
|
||||||
parcel.recycle();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -401,12 +389,10 @@ public abstract class BinderTransport
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final void sendTransaction(int callId, Parcel parcel) throws StatusException {
|
final void sendTransaction(int callId, ParcelHolder parcel) throws StatusException {
|
||||||
int dataSize = parcel.dataSize();
|
int dataSize = parcel.get().dataSize();
|
||||||
try {
|
try {
|
||||||
if (!outgoingBinder.transact(callId, parcel, null, IBinder.FLAG_ONEWAY)) {
|
outgoingBinder.transact(callId, parcel);
|
||||||
throw Status.UNAVAILABLE.withDescription("Failed sending transaction").asException();
|
|
||||||
}
|
|
||||||
} catch (RemoteException re) {
|
} catch (RemoteException re) {
|
||||||
throw statusFromRemoteException(re).asException();
|
throw statusFromRemoteException(re).asException();
|
||||||
}
|
}
|
||||||
|
|
@ -416,16 +402,13 @@ public abstract class BinderTransport
|
||||||
}
|
}
|
||||||
|
|
||||||
final void sendOutOfBandClose(int callId, Status status) {
|
final void sendOutOfBandClose(int callId, Status status) {
|
||||||
Parcel parcel = Parcel.obtain();
|
try (ParcelHolder parcel = ParcelHolder.obtain()) {
|
||||||
try {
|
parcel.get().writeInt(0); // Placeholder for flags. Will be filled in below.
|
||||||
parcel.writeInt(0); // Placeholder for flags. Will be filled in below.
|
int flags = TransactionUtils.writeStatus(parcel.get(), status);
|
||||||
int flags = TransactionUtils.writeStatus(parcel, status);
|
TransactionUtils.fillInFlags(parcel.get(), flags | TransactionUtils.FLAG_OUT_OF_BAND_CLOSE);
|
||||||
TransactionUtils.fillInFlags(parcel, flags | TransactionUtils.FLAG_OUT_OF_BAND_CLOSE);
|
|
||||||
sendTransaction(callId, parcel);
|
sendTransaction(callId, parcel);
|
||||||
} catch (StatusException e) {
|
} catch (StatusException e) {
|
||||||
logger.log(Level.WARNING, "Failed sending oob close transaction", e);
|
logger.log(Level.WARNING, "Failed sending oob close transaction", e);
|
||||||
} finally {
|
|
||||||
parcel.recycle();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -496,10 +479,12 @@ public abstract class BinderTransport
|
||||||
protected void handleSetupTransport(Parcel parcel) {}
|
protected void handleSetupTransport(Parcel parcel) {}
|
||||||
|
|
||||||
@GuardedBy("this")
|
@GuardedBy("this")
|
||||||
private final void handlePing(Parcel parcel) {
|
private final void handlePing(Parcel requestParcel) {
|
||||||
|
int id = requestParcel.readInt();
|
||||||
if (transportState == TransportState.READY) {
|
if (transportState == TransportState.READY) {
|
||||||
try {
|
try (ParcelHolder replyParcel = ParcelHolder.obtain()) {
|
||||||
outgoingBinder.transact(PING_RESPONSE, parcel, null, IBinder.FLAG_ONEWAY);
|
replyParcel.get().writeInt(id);
|
||||||
|
outgoingBinder.transact(PING_RESPONSE, replyParcel);
|
||||||
} catch (RemoteException re) {
|
} catch (RemoteException re) {
|
||||||
// Ignore.
|
// Ignore.
|
||||||
}
|
}
|
||||||
|
|
@ -510,21 +495,15 @@ public abstract class BinderTransport
|
||||||
protected void handlePingResponse(Parcel parcel) {}
|
protected void handlePingResponse(Parcel parcel) {}
|
||||||
|
|
||||||
@GuardedBy("this")
|
@GuardedBy("this")
|
||||||
private void sendAcknowledgeBytes(IBinder iBinder) {
|
private void sendAcknowledgeBytes(OneWayBinderProxy iBinder) {
|
||||||
// Send a transaction to acknowledge reception of incoming data.
|
// Send a transaction to acknowledge reception of incoming data.
|
||||||
long n = numIncomingBytes.get();
|
long n = numIncomingBytes.get();
|
||||||
acknowledgedIncomingBytes = n;
|
acknowledgedIncomingBytes = n;
|
||||||
Parcel parcel = Parcel.obtain();
|
try (ParcelHolder parcel = ParcelHolder.obtain()) {
|
||||||
try {
|
parcel.get().writeLong(n);
|
||||||
parcel.writeLong(n);
|
iBinder.transact(ACKNOWLEDGE_BYTES, parcel);
|
||||||
if (!iBinder.transact(ACKNOWLEDGE_BYTES, parcel, null, IBinder.FLAG_ONEWAY)) {
|
|
||||||
shutdownInternal(
|
|
||||||
Status.UNAVAILABLE.withDescription("Failed sending ack bytes transaction"), true);
|
|
||||||
}
|
|
||||||
} catch (RemoteException re) {
|
} catch (RemoteException re) {
|
||||||
shutdownInternal(statusFromRemoteException(re), true);
|
shutdownInternal(statusFromRemoteException(re), true);
|
||||||
} finally {
|
|
||||||
parcel.recycle();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -607,7 +586,7 @@ public abstract class BinderTransport
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public synchronized void onBound(IBinder binder) {
|
public synchronized void onBound(IBinder binder) {
|
||||||
sendSetupTransaction(binder);
|
sendSetupTransaction(OneWayBinderProxy.wrap(binder, offloadExecutor));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -748,7 +727,7 @@ public abstract class BinderTransport
|
||||||
if (inState(TransportState.SETUP)) {
|
if (inState(TransportState.SETUP)) {
|
||||||
if (!authorization.isOk()) {
|
if (!authorization.isOk()) {
|
||||||
shutdownInternal(authorization, true);
|
shutdownInternal(authorization, true);
|
||||||
} else if (!setOutgoingBinder(binder)) {
|
} else if (!setOutgoingBinder(OneWayBinderProxy.wrap(binder, offloadExecutor))) {
|
||||||
shutdownInternal(
|
shutdownInternal(
|
||||||
Status.UNAVAILABLE.withDescription("Failed to observe outgoing binder"), true);
|
Status.UNAVAILABLE.withDescription("Failed to observe outgoing binder"), true);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -827,7 +806,8 @@ public abstract class BinderTransport
|
||||||
IBinder callbackBinder) {
|
IBinder callbackBinder) {
|
||||||
super(executorServicePool, attributes, buildLogId(attributes));
|
super(executorServicePool, attributes, buildLogId(attributes));
|
||||||
this.streamTracerFactories = streamTracerFactories;
|
this.streamTracerFactories = streamTracerFactories;
|
||||||
setOutgoingBinder(callbackBinder);
|
// TODO(jdcormie): Plumb in the Server's executor() and use it here instead.
|
||||||
|
setOutgoingBinder(OneWayBinderProxy.wrap(callbackBinder, getScheduledExecutorService()));
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized void setServerTransportListener(ServerTransportListener serverTransportListener) {
|
public synchronized void setServerTransportListener(ServerTransportListener serverTransportListener) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,142 @@
|
||||||
|
package io.grpc.binder.internal;
|
||||||
|
|
||||||
|
import android.os.Binder;
|
||||||
|
import android.os.IBinder;
|
||||||
|
import android.os.Parcel;
|
||||||
|
import android.os.RemoteException;
|
||||||
|
import io.grpc.internal.SerializingExecutor;
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wraps an {@link IBinder} with a safe and uniformly asynchronous transaction API.
|
||||||
|
*
|
||||||
|
* <p>When the target of your bindService() call is hosted in a different process, Android supplies
|
||||||
|
* you with an {@link IBinder} that proxies your transactions to the remote {@link
|
||||||
|
* android.os.Binder} instance. But when the target Service is hosted in the same process, Android
|
||||||
|
* supplies you with that local instance of {@link android.os.Binder} directly. This in-process
|
||||||
|
* implementation of {@link IBinder} is problematic for clients that want "oneway" transaction
|
||||||
|
* semantics because its transact() method simply invokes onTransact() on the caller's thread, even
|
||||||
|
* when the {@link IBinder#FLAG_ONEWAY} flag is set. Even though this behavior is documented, its
|
||||||
|
* consequences with respect to reentrancy, locking, and transaction dispatch order can be
|
||||||
|
* surprising and dangerous.
|
||||||
|
*
|
||||||
|
* <p>Wrap your {@link IBinder}s with an instance of this class to ensure the following
|
||||||
|
* out-of-process "oneway" semantics are always in effect:
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>transact() merely enqueues the transaction for processing. It doesn't wait for onTransact()
|
||||||
|
* to complete.
|
||||||
|
* <li>transact() may fail for programming errors or transport-layer errors that are immediately
|
||||||
|
* obvious on the caller's side, but never for an Exception or false return value from
|
||||||
|
* onTransact().
|
||||||
|
* <li>onTransact() runs without holding any of the locks held by the thread calling transact().
|
||||||
|
* <li>onTransact() calls are dispatched one at a time in the same happens-before order as the
|
||||||
|
* corresponding calls to transact().
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>NB: One difference that this class can't conceal is that calls to onTransact() are serialized
|
||||||
|
* per {@link OneWayBinderProxy} instance, not per instance of the wrapped {@link IBinder}. An
|
||||||
|
* android.os.Binder with in-process callers could still receive concurrent calls to onTransact() on
|
||||||
|
* different threads if callers used different {@link OneWayBinderProxy} instances or if that Binder
|
||||||
|
* also had out-of-process callers.
|
||||||
|
*/
|
||||||
|
public abstract class OneWayBinderProxy {
|
||||||
|
private static final Logger logger = Logger.getLogger(OneWayBinderProxy.class.getName());
|
||||||
|
protected final IBinder delegate;
|
||||||
|
|
||||||
|
private OneWayBinderProxy(IBinder iBinder) {
|
||||||
|
this.delegate = iBinder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a new instance of {@link OneWayBinderProxy} that wraps {@code iBinder}.
|
||||||
|
*
|
||||||
|
* @param iBinder the binder to wrap
|
||||||
|
* @param inProcessThreadHopExecutor a non-direct Executor used to dispatch calls to onTransact(),
|
||||||
|
* if necessary
|
||||||
|
* @return a new instance of {@link OneWayBinderProxy}
|
||||||
|
*/
|
||||||
|
public static OneWayBinderProxy wrap(IBinder iBinder, Executor inProcessThreadHopExecutor) {
|
||||||
|
return (iBinder instanceof Binder)
|
||||||
|
? new InProcessImpl(iBinder, inProcessThreadHopExecutor)
|
||||||
|
: new OutOfProcessImpl(iBinder);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enqueues a transaction for the wrapped {@link IBinder} with guaranteed "oneway" semantics.
|
||||||
|
*
|
||||||
|
* <p>NB: Unlike {@link IBinder#transact}, implementations of this method take ownership of the
|
||||||
|
* {@code data} Parcel. When this method returns, {@code data} will normally be empty, but callers
|
||||||
|
* should still unconditionally {@link ParcelHolder#close()} it to avoid a leak in case they or
|
||||||
|
* the implementation throws before ownership is transferred.
|
||||||
|
*
|
||||||
|
* @param code identifies the type of this transaction
|
||||||
|
* @param data a non-empty container of the Parcel to be sent
|
||||||
|
* @throws RemoteException if the transaction could not even be queued for dispatch on the server.
|
||||||
|
* Failures from {@link Binder#onTransact} are *never* reported this way.
|
||||||
|
*/
|
||||||
|
public abstract void transact(int code, ParcelHolder data) throws RemoteException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the wrapped {@link IBinder} for the purpose of calling methods other than {@link
|
||||||
|
* IBinder#transact(int, Parcel, Parcel, int)}.
|
||||||
|
*/
|
||||||
|
public IBinder getDelegate() {
|
||||||
|
return delegate;
|
||||||
|
}
|
||||||
|
|
||||||
|
static class OutOfProcessImpl extends OneWayBinderProxy {
|
||||||
|
OutOfProcessImpl(IBinder iBinder) {
|
||||||
|
super(iBinder);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void transact(int code, ParcelHolder data) throws RemoteException {
|
||||||
|
if (!transactAndRecycleParcel(code, data.release())) {
|
||||||
|
// This cannot happen (see g/android-binder/c/jM4NvS234Rw) but, just in case, let the caller
|
||||||
|
// handle it along with all the other possible transport-layer errors.
|
||||||
|
throw new RemoteException("BinderProxy#transact(" + code + ", FLAG_ONEWAY) returned false");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected boolean transactAndRecycleParcel(int code, Parcel data) throws RemoteException {
|
||||||
|
try {
|
||||||
|
return delegate.transact(code, data, null, IBinder.FLAG_ONEWAY);
|
||||||
|
} finally {
|
||||||
|
data.recycle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class InProcessImpl extends OneWayBinderProxy {
|
||||||
|
private final SerializingExecutor executor;
|
||||||
|
|
||||||
|
InProcessImpl(IBinder binder, Executor executor) {
|
||||||
|
super(binder);
|
||||||
|
this.executor = new SerializingExecutor(executor);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void transact(int code, ParcelHolder wrappedParcel) {
|
||||||
|
// Transfer ownership, taking care to handle any RuntimeException from execute().
|
||||||
|
Parcel parcel = wrappedParcel.get();
|
||||||
|
executor.execute(
|
||||||
|
() -> {
|
||||||
|
try {
|
||||||
|
if (!transactAndRecycleParcel(code, parcel)) {
|
||||||
|
// onTransact() in our same process returned this. Ignore it, just like Android
|
||||||
|
// would have if the android.os.Binder was in another process.
|
||||||
|
logger.log(Level.FINEST, "A oneway transaction was not understood - ignoring");
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// onTransact() in our same process threw this. Ignore it, just like Android would
|
||||||
|
// have if the android.os.Binder was in another process.
|
||||||
|
logger.log(Level.FINEST, "A oneway transaction threw - ignoring", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
wrappedParcel.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -221,15 +221,14 @@ abstract class Outbound {
|
||||||
@GuardedBy("this")
|
@GuardedBy("this")
|
||||||
@SuppressWarnings("fallthrough")
|
@SuppressWarnings("fallthrough")
|
||||||
protected final void sendInternal() throws StatusException {
|
protected final void sendInternal() throws StatusException {
|
||||||
Parcel parcel = Parcel.obtain();
|
try (ParcelHolder parcel = ParcelHolder.obtain()) {
|
||||||
int flags = 0;
|
int flags = 0;
|
||||||
parcel.writeInt(0); // Placeholder for flags. Will be filled in below.
|
parcel.get().writeInt(0); // Placeholder for flags. Will be filled in below.
|
||||||
parcel.writeInt(transactionIndex++);
|
parcel.get().writeInt(transactionIndex++);
|
||||||
try {
|
|
||||||
switch (outboundState) {
|
switch (outboundState) {
|
||||||
case INITIAL:
|
case INITIAL:
|
||||||
flags |= TransactionUtils.FLAG_PREFIX;
|
flags |= TransactionUtils.FLAG_PREFIX;
|
||||||
flags |= writePrefix(parcel);
|
flags |= writePrefix(parcel.get());
|
||||||
onOutboundState(State.PREFIX_SENT);
|
onOutboundState(State.PREFIX_SENT);
|
||||||
if (!messageAvailable() && !suffixReady) {
|
if (!messageAvailable() && !suffixReady) {
|
||||||
break;
|
break;
|
||||||
|
|
@ -239,7 +238,7 @@ abstract class Outbound {
|
||||||
InputStream messageStream = peekNextMessage();
|
InputStream messageStream = peekNextMessage();
|
||||||
if (messageStream != null) {
|
if (messageStream != null) {
|
||||||
flags |= TransactionUtils.FLAG_MESSAGE_DATA;
|
flags |= TransactionUtils.FLAG_MESSAGE_DATA;
|
||||||
flags |= writeMessageData(parcel, messageStream);
|
flags |= writeMessageData(parcel.get(), messageStream);
|
||||||
} else {
|
} else {
|
||||||
checkState(suffixReady);
|
checkState(suffixReady);
|
||||||
}
|
}
|
||||||
|
|
@ -252,20 +251,19 @@ abstract class Outbound {
|
||||||
// Fall-through.
|
// Fall-through.
|
||||||
case ALL_MESSAGES_SENT:
|
case ALL_MESSAGES_SENT:
|
||||||
flags |= TransactionUtils.FLAG_SUFFIX;
|
flags |= TransactionUtils.FLAG_SUFFIX;
|
||||||
flags |= writeSuffix(parcel);
|
flags |= writeSuffix(parcel.get());
|
||||||
onOutboundState(State.SUFFIX_SENT);
|
onOutboundState(State.SUFFIX_SENT);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new AssertionError();
|
throw new AssertionError();
|
||||||
}
|
}
|
||||||
TransactionUtils.fillInFlags(parcel, flags);
|
TransactionUtils.fillInFlags(parcel.get(), flags);
|
||||||
|
int dataSize = parcel.get().dataSize();
|
||||||
transport.sendTransaction(callId, parcel);
|
transport.sendTransaction(callId, parcel);
|
||||||
statsTraceContext.outboundWireSize(parcel.dataSize());
|
statsTraceContext.outboundWireSize(dataSize);
|
||||||
statsTraceContext.outboundUncompressedSize(parcel.dataSize());
|
statsTraceContext.outboundUncompressedSize(dataSize);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw Status.INTERNAL.withCause(e).asException();
|
throw Status.INTERNAL.withCause(e).asException();
|
||||||
} finally {
|
|
||||||
parcel.recycle();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
package io.grpc.binder.internal;
|
||||||
|
|
||||||
|
import static com.google.common.base.Preconditions.checkState;
|
||||||
|
|
||||||
|
import android.os.Parcel;
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
import java.io.Closeable;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wraps a {@link Parcel} from the static {@link Parcel#obtain()} pool with methods that make it
|
||||||
|
* easy to eventually {@link Parcel#recycle()} it.
|
||||||
|
*/
|
||||||
|
class ParcelHolder implements Closeable {
|
||||||
|
|
||||||
|
@Nullable private Parcel parcel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new instance that owns a {@link Parcel} newly obtained from Android's object pool.
|
||||||
|
*/
|
||||||
|
public static ParcelHolder obtain() {
|
||||||
|
return new ParcelHolder(Parcel.obtain());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Creates a new instance taking ownership of the specified {@code parcel}. */
|
||||||
|
public ParcelHolder(Parcel parcel) {
|
||||||
|
this.parcel = parcel;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the wrapped {@link Parcel} if we still own it.
|
||||||
|
*
|
||||||
|
* @throws IllegalStateException if ownership has already been given up by {@link #release()}
|
||||||
|
*/
|
||||||
|
public Parcel get() {
|
||||||
|
checkState(parcel != null, "get() after close()/release()");
|
||||||
|
return parcel;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the wrapped {@link Parcel} and releases ownership of it.
|
||||||
|
*
|
||||||
|
* @throws IllegalStateException if ownership has already been given up by {@link #release()}
|
||||||
|
*/
|
||||||
|
public Parcel release() {
|
||||||
|
Parcel result = get();
|
||||||
|
this.parcel = null;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recycles the wrapped {@link Parcel} to Android's object pool, if we still own it.
|
||||||
|
*
|
||||||
|
* <p>Otherwise, this method has no effect.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
if (parcel != null) {
|
||||||
|
parcel.recycle();
|
||||||
|
parcel = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true iff this container no longer owns a {@link Parcel}.
|
||||||
|
*
|
||||||
|
* <p>{@link #isEmpty()} is true after all call to {@link #close()} or {@link #release()}.
|
||||||
|
*
|
||||||
|
* <p>Typically only used for debugging or testing since Parcel-owning code should be calling
|
||||||
|
* {@link #close()} unconditionally.
|
||||||
|
*/
|
||||||
|
@VisibleForTesting
|
||||||
|
public boolean isEmpty() {
|
||||||
|
return parcel == null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,197 @@
|
||||||
|
package io.grpc.binder.internal;
|
||||||
|
|
||||||
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyInt;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import android.os.Binder;
|
||||||
|
import android.os.IBinder;
|
||||||
|
import android.os.Parcel;
|
||||||
|
import android.os.RemoteException;
|
||||||
|
import io.grpc.binder.internal.OneWayBinderProxy.InProcessImpl;
|
||||||
|
import io.grpc.binder.internal.OneWayBinderProxy.OutOfProcessImpl;
|
||||||
|
import java.util.ArrayDeque;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Queue;
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
import java.util.concurrent.RejectedExecutionException;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.MockitoJUnit;
|
||||||
|
import org.mockito.junit.MockitoRule;
|
||||||
|
import org.robolectric.RobolectricTestRunner;
|
||||||
|
|
||||||
|
/** Unit tests for the {@link OneWayBinderProxy} implementations. */
|
||||||
|
@RunWith(RobolectricTestRunner.class)
|
||||||
|
public class OneWayBinderProxyTest {
|
||||||
|
@Rule public final MockitoRule mockito = MockitoJUnit.rule();
|
||||||
|
|
||||||
|
QueuingExecutor queuingExecutor = new QueuingExecutor();
|
||||||
|
|
||||||
|
@Mock IBinder mockBinder;
|
||||||
|
|
||||||
|
RecordingBinder recordingBinder = new RecordingBinder();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldProxyInProcessTransactionsOnExecutor() throws RemoteException {
|
||||||
|
InProcessImpl proxy = new InProcessImpl(recordingBinder, queuingExecutor);
|
||||||
|
try (ParcelHolder parcel = ParcelHolder.obtain()) {
|
||||||
|
parcel.get().writeInt(123);
|
||||||
|
proxy.transact(456, parcel);
|
||||||
|
assertThat(parcel.isEmpty()).isTrue();
|
||||||
|
assertThat(recordingBinder.txnLog).isEmpty();
|
||||||
|
queuingExecutor.runAllQueued();
|
||||||
|
assertThat(recordingBinder.txnLog).hasSize(1);
|
||||||
|
assertThat(recordingBinder.txnLog.get(0).argument).isEqualTo(123);
|
||||||
|
assertThat(recordingBinder.txnLog.get(0).code).isEqualTo(456);
|
||||||
|
assertThat(recordingBinder.txnLog.get(0).flags).isEqualTo(IBinder.FLAG_ONEWAY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldNotLeakParcelsInCaseOfRejectedExecution() throws RemoteException {
|
||||||
|
InProcessImpl proxy = new InProcessImpl(recordingBinder, queuingExecutor);
|
||||||
|
queuingExecutor.shutdown();
|
||||||
|
try (ParcelHolder parcel = ParcelHolder.obtain()) {
|
||||||
|
parcel.get().writeInt(123);
|
||||||
|
assertThrows(RejectedExecutionException.class, () -> proxy.transact(123, parcel));
|
||||||
|
assertThat(parcel.isEmpty()).isFalse(); // Parcel didn't leak because we still own it.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldProxyOutOfProcessTransactionsSynchronously() throws RemoteException {
|
||||||
|
OutOfProcessImpl proxy = new OutOfProcessImpl(recordingBinder);
|
||||||
|
try (ParcelHolder parcel = ParcelHolder.obtain()) {
|
||||||
|
parcel.get().writeInt(123);
|
||||||
|
proxy.transact(456, parcel);
|
||||||
|
assertThat(parcel.isEmpty()).isTrue();
|
||||||
|
assertThat(recordingBinder.txnLog).hasSize(1);
|
||||||
|
assertThat(recordingBinder.txnLog.get(0).argument).isEqualTo(123);
|
||||||
|
assertThat(recordingBinder.txnLog.get(0).code).isEqualTo(456);
|
||||||
|
assertThat(recordingBinder.txnLog.get(0).flags).isEqualTo(IBinder.FLAG_ONEWAY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldIgnoreInProcessRemoteExceptions() throws RemoteException {
|
||||||
|
when(mockBinder.transact(anyInt(), any(), any(), anyInt())).thenThrow(RemoteException.class);
|
||||||
|
InProcessImpl proxy = new InProcessImpl(mockBinder, queuingExecutor);
|
||||||
|
try (ParcelHolder parcel = ParcelHolder.obtain()) {
|
||||||
|
proxy.transact(123, parcel); // Doesn't throw.
|
||||||
|
verify(mockBinder, never()).transact(anyInt(), any(), any(), anyInt());
|
||||||
|
queuingExecutor.runAllQueued();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldExposeOutOfProcessRemoteExceptions() throws RemoteException {
|
||||||
|
when(mockBinder.transact(anyInt(), any(), any(), anyInt())).thenThrow(RemoteException.class);
|
||||||
|
OutOfProcessImpl proxy = new OutOfProcessImpl(mockBinder);
|
||||||
|
try (ParcelHolder parcel = ParcelHolder.obtain()) {
|
||||||
|
assertThrows(RemoteException.class, () -> proxy.transact(123, parcel));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldIgnoreUnknownTransactionReturnValueInProcess() throws RemoteException {
|
||||||
|
when(mockBinder.transact(anyInt(), any(), any(), anyInt())).thenReturn(false);
|
||||||
|
InProcessImpl proxy = new InProcessImpl(mockBinder, queuingExecutor);
|
||||||
|
try (ParcelHolder parcel = ParcelHolder.obtain()) {
|
||||||
|
proxy.transact(123, parcel); // Doesn't throw.
|
||||||
|
verify(mockBinder, never()).transact(anyInt(), any(), any(), anyInt());
|
||||||
|
queuingExecutor.runAllQueued();
|
||||||
|
verify(mockBinder).transact(eq(123), any(), any(), anyInt());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldReportImpossibleUnknownTransactionReturnValueOutOfProcess()
|
||||||
|
throws RemoteException {
|
||||||
|
when(mockBinder.transact(anyInt(), any(), any(), anyInt())).thenReturn(false);
|
||||||
|
OutOfProcessImpl proxy = new OutOfProcessImpl(mockBinder);
|
||||||
|
try (ParcelHolder parcel = ParcelHolder.obtain()) {
|
||||||
|
assertThrows(RemoteException.class, () -> proxy.transact(123, parcel));
|
||||||
|
verify(mockBinder).transact(eq(123), any(), any(), anyInt());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** An Executor that queues up Runnables for later manual execution by a unit test. */
|
||||||
|
static class QueuingExecutor implements Executor {
|
||||||
|
private final Queue<Runnable> runnables = new ArrayDeque<>();
|
||||||
|
private volatile boolean isShutdown;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute(Runnable r) {
|
||||||
|
if (isShutdown) {
|
||||||
|
throw new RejectedExecutionException();
|
||||||
|
}
|
||||||
|
runnables.add(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void runAllQueued() {
|
||||||
|
Runnable next = null;
|
||||||
|
while ((next = runnables.poll()) != null) {
|
||||||
|
next.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void shutdown() {
|
||||||
|
isShutdown = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** An immutable record of a call to {@link IBinder#transact(int, Parcel, Parcel, int)}. */
|
||||||
|
static class TransactionRecord {
|
||||||
|
private final int code;
|
||||||
|
private final int argument;
|
||||||
|
private final int flags;
|
||||||
|
|
||||||
|
private TransactionRecord(int code, int argument, int flags) {
|
||||||
|
this.code = code;
|
||||||
|
this.argument = argument;
|
||||||
|
this.flags = flags;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A {@link Binder} that simply records every transaction it receives. */
|
||||||
|
static class RecordingBinder extends Binder {
|
||||||
|
private final ArrayList<TransactionRecord> txnLog = new ArrayList<>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean onTransact(int code, Parcel data, Parcel reply, int flags)
|
||||||
|
throws RemoteException {
|
||||||
|
txnLog.add(new TransactionRecord(code, data.readInt(), flags));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ThrowingRunnable {
|
||||||
|
void run() throws Throwable;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(jdcormie): Replace with Assert.assertThrows() once we upgrade to junit 4.13.
|
||||||
|
private static <T extends Throwable> T assertThrows(
|
||||||
|
Class<T> expectedThrowable, ThrowingRunnable runnable) {
|
||||||
|
try {
|
||||||
|
runnable.run();
|
||||||
|
} catch (Throwable actualThrown) {
|
||||||
|
if (expectedThrowable.isInstance(actualThrown)) {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
T retVal = (T) actualThrown;
|
||||||
|
return retVal;
|
||||||
|
} else {
|
||||||
|
AssertionError assertionError = new AssertionError("Unexpected type thrown");
|
||||||
|
assertionError.initCause(actualThrown);
|
||||||
|
throw assertionError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new AssertionError("Expected " + expectedThrowable + " but nothing was thrown");
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue