Compare commits

...

215 Commits

Author SHA1 Message Date
Kannan J c7202c0db5
Bump readme (#12305) 2025-08-24 18:21:52 +05:30
Eric Anderson 028afbe352
xds: Implement equals in WRRLBConfig
Just an is a8de9f0, lack of equals causes cluster_resolver to consider every update a different configuration and restart itself.

Handling NaN should really be prevented with validation, but it looks like that
would lead to yak shaving at the moment.

b/435208946
2025-08-22 08:07:51 -07:00
John Cormie afdbecb235
binder: Move BinderTransport's inner classes to the top level (#12303)
BinderTransport.java was getting too long and deeply nested.

This is a pure refactor with no behavior changes.
2025-08-21 16:08:58 -07:00
MV Shiva 2039266ebc
xds: xdsClient caches transient error for new watchers (#12262) 2025-08-19 21:41:52 +05:30
Jiri Kaplan 43bef65cf9 netty: Support BCJSSE provider in GrpcSslContexts 2025-08-19 07:17:57 -07:00
Eric Anderson 437e03dc98
xds: Avoid PriorityLb re-enabling timer on duplicate CONNECTING (#12289)
Since c4256add4 we no longer fabricate a TRANSIENT_FAILURE update from
children. However, previously that would have set
seenReadyOrIdleSinceTransientFailure = false and prevented future timer
creation. If a LB policy gives extraneous updates with state CONNECTING,
then it was possible to re-create failOverTimer which would then wait
the 10 seconds for the child to finish CONNECTING. We only want to give
the child one opportunity after transitioning out of READY/IDLE.

https://github.com/grpc/proposal/pull/509
2025-08-19 12:53:47 +05:30
Eric Anderson 6462ef9a11 netty: Count sent RST_STREAMs against limit
Http2RstCounterEncoder has to be constructed before
NettyServerHandler/Http2ConnectionHandler so it must be static. Thus the
code/counters were moved into RstStreamCounter which then can be
constructed earlier and shared.

This depends on Netty 4.1.124 for a bug fix to actually call the
encoder:
be53dc3c9a
2025-08-18 07:23:34 -07:00
Eric Anderson 95d16d85c8 Upgrade to Netty 4.1.124.Final
This implicitly disables NettyAdaptiveCumulator (#11284), which can have a
performance impact. We delayed upgrading Netty to give time to rework
the optimization, but we've gone too long already without upgrading
which causes problems for vulnerability tracking.
2025-08-13 14:23:14 -07:00
Sangamesh f50726d32e
android: Clean up android lint and other warnings (#12143)
Worked on clearing the lint warnings (OldTargetApi, ObsoleteSdkInt,
InlinedApi, NewApi)

Fixes #12142
2025-08-11 15:18:01 -07:00
Eric Anderson 06707f7c38 xds: Use a different log name for XdsClientImpl and ControlPlaneClient
Seems like a good time to stop hating ourselves, as that seems to be the
only reason to use the same string.
2025-08-08 14:23:43 -07:00
John Cormie efcdebb904
Introduce a NameResolver for Android's `intent:` URIs (#12248)
Let grpc-binder clients find on-device services by [implicit Intent](https://developer.android.com/guide/components/intents-filters#Types) target URI, lifting the need to hard code a server's package name.
2025-08-07 08:38:44 -07:00
Eric Anderson f30964ab82
Bump versions of dependencies (#12252)
Notably, protobuf to 3.25.8, opentelemetry to 1.52.0. Protobuf in Bazel
has 25.5 in the BCR and it seems better to align the WORKSPACE
with that version. But we can't actually use 25.5 in BCR because it is
incompatible with Bazel 7.
2025-08-06 11:01:45 -07:00
MV Shiva 7040417eee
stub: use the closedTrailers in StatusException (#12259) 2025-08-06 12:24:33 +05:30
camel a40c8cf5a4
binder: Let apps call SecurityPolicy.checkAuthorization() by PeerUid (#12257)
This allows a server with access to PeerUid to check additional application-layer security policy *after* the call itself is authorized by the transport layer. Cross cutting application-layer checks could be done from a ServerInterceptor (RPC method level policy, say). Checks based on the substance of a request message could be done by the individual RPC method implementations themselves.
2025-08-05 16:47:45 -07:00
Kannan J 8b46ad58c3
Start 1.76.0 development cycle (#12258) 2025-08-05 22:00:39 +05:30
apolcyn d947c80f99
interop-testing: make soak test use logger rather than writing to stderr directly 2025-07-30 09:31:02 -07:00
Eric Anderson 6ffcbd927e Bump Gradle to 8.14.3 and upgrade plugins
The syntax changes adding `=` were to address:
https://docs.gradle.org/8.14.3/userguide/upgrading_version_8.html#groovy_space_assignment_syntax
2025-07-30 08:43:06 -07:00
MV Shiva 36fe276a50
xds: add "resource_timer_is_transient_failure" server feature (#12249) 2025-07-30 17:54:45 +05:30
Benjamin Peterson ba0a7329da
stub: simplify BlockingClientCall infinite blocking (#12217)
Move deadline computation into overloads with finite timeouts. Blocking calls without timeouts now do not have to read the clock.
2025-07-29 09:33:09 -07:00
Eric Anderson 28f14255ce Update README etc to reference 1.74.0 2025-07-29 07:51:17 -07:00
Kannan J 7e982e48a1
Xds: Aggregate cluster fixes (A75) (#12186)
Instead of representing an aggregate cluster as a single cluster whose
priorities come from different underlying clusters, represent an aggregate cluster as an instance of a priority LB policy where each child is a cds LB policy for the underlying
cluster.
2025-07-29 18:06:39 +05:30
MV Shiva c3ef1ab034
xds: Envoy proto sync to (#12224) 2025-07-28 20:56:27 +05:30
Eric Anderson 8f09b96899
bazel: Use jarjar to avoid xds deps (#12243)
Avoiding so many deps will allow us to upgrade the protos without being
forced to upgrade to protobuf-java 4.x. It also removes the remaining
non-bzlmod dependencies.

It'd be really easy to get this wrong, so we do two things 1) mirror the
gradle configuration as much as possible, as that sees a lot of testing,
and 2) run the fake control plane with the _results_ of jarjar. There's
lots of classes that we could mess up, but that at least kicks the tires.

XdsTestUtils.buildRouteConfiguration() was moved to ControlPlaneRule to
stop the unnecessary circular dependency between the classes and to
avoid the many dependencies of XdsTestUtils.

I'm totally hacking java_grpc_library to improve the dependency
situation. Long-term, I think we will stop building Java libraries with
Bazel and require users to rely entirely on Maven Central. That seems to
be the direction Bazel is going and it will greatly simplify the
problems we've seen with protobuf having a single repository for many
languages. So while the hack isn't too bad, I hope we won't have to live
with it long-term.
2025-07-28 12:30:39 +05:30
Kannan J 42e1829b37
xds: Do RLS fallback policy eagar start (#12211)
The resource subscription to the fallback target was done only at the time of falling back, which can cause rpcs to fail. This change makes the fallback target to be subscribed and cached earlier, similar to C++ and go gRPC implementations.
2025-07-24 16:58:32 +05:30
Eric Anderson c4256add4d xds: Align PriorityLB child selection with A56
The PriorityLB predates A56. tryNextPriority() now matches
ChoosePriority() from the gRFC.

The biggest change is waiting on CONNECTING children instead of failing
after the failOverTimer fires. The failOverTimer should be used to start
lower priorities more eagerly, but shouldn't cause the overall
connectivity state to become TRANSIENT_FAILURE on its own. The prior
behavior of creating the "Connection timeout for priority" failing
picker was particularly strange, because it didn't update child's
connectivity state. This previous behavior was creating errors because
of the failOverTimer with no way to diagnose what was going wrong.

b/428517222
2025-07-23 06:38:33 -07:00
Eric Anderson 6ff8ecac09 core: Don't pre-compute DEADLINE_EXCEEDED message for delayed calls
The main reason I made a change here was to fix the tense from the
deadline "will be exceeded in" to "was exceeded after". But we really
don't want to be doing the string formatting unless the deadline is
actually exceeded. There were a few more changes to make some variables
effectively final.
2025-07-22 06:56:02 -07:00
Patrick Strawderman 80217275db
api: Size Sets and Maps correctly in handling of Metadata values to be exchanged during a call (#12229)
Fix HashSet / HashMap initializations to have sufficient capacity allocated based on the number of keys to be inserted, without which it would always lead to a rehash / resize operation.
2025-07-22 09:14:08 +05:30
Eric Anderson 2e96fbf1e8 netty: Associate netty stream eagerly to avoid client hang
In #12185, RPCs were randomly hanging. In #12207 this was tracked down
to the headers promise completing successfully, but the netty stream
was null. This was because the headers write hadn't completed but
stream.close() had been called by goingAway().
2025-07-17 21:55:53 +00:00
George Gensure a37d3eb349 Guarantee missing stream promise delivery
In observed cases, whether RST_STREAM or another failure from netty or
the server, listeners can fail to be notified when a connection yields a
null stream for the selected streamId. This causes hangs in clients,
despite deadlines, with no obvious resolution.

Tests which relied upon this promise succeeding must now change.
2025-07-17 21:55:16 +00:00
Eric Anderson 1fc4ab0bb2 LBs should avoid calling LBs after lb.shutdown()
LoadBalancers shouldn't be called after shutdown(), but RingHashLb could
have enqueued work to the SynchronizationContext that executed after
shutdown(). This commit fixes problems discovered when auditing all LBs
usage of the syncContext for that type of problem.

Similarly, PickFirstLb could have requested a new connection after
shutdown(). We want to avoid that sort of thing too.

RingHashLb's test changed from CONNECTING to TRANSIENT_FAILURE to get
the latest picker. Because two subchannels have failed it will be in
TRANSIENT_FAILURE. Previously the test was using an older picker with
out-of-date subchannelView, and the verifyConnection() was too imprecise
to notice it was creating the wrong subchannel.

As discovered in b/430347751, where ClusterImplLb was seeing a new
subchannel being called after the child LB was shutdown (the shutdown
itself had been caused by RingHashConfig not implementing equals() and
was fixed by a8de9f07ab, which caused ClusterResolverLb to replace its
state):

```
java.lang.NullPointerException
	at io.grpc.xds.ClusterImplLoadBalancer$ClusterImplLbHelper.createClusterLocalityFromAttributes(ClusterImplLoadBalancer.java:322)
	at io.grpc.xds.ClusterImplLoadBalancer$ClusterImplLbHelper.createSubchannel(ClusterImplLoadBalancer.java:236)
	at io.grpc.util.ForwardingLoadBalancerHelper.createSubchannel(ForwardingLoadBalancerHelper.java:47)
	at io.grpc.util.ForwardingLoadBalancerHelper.createSubchannel(ForwardingLoadBalancerHelper.java:47)
	at io.grpc.internal.PickFirstLeafLoadBalancer.createNewSubchannel(PickFirstLeafLoadBalancer.java:527)
	at io.grpc.internal.PickFirstLeafLoadBalancer.requestConnection(PickFirstLeafLoadBalancer.java:459)
	at io.grpc.internal.PickFirstLeafLoadBalancer.acceptResolvedAddresses(PickFirstLeafLoadBalancer.java:174)
	at io.grpc.xds.LazyLoadBalancer$LazyDelegate.activate(LazyLoadBalancer.java:64)
	at io.grpc.xds.LazyLoadBalancer$LazyDelegate.requestConnection(LazyLoadBalancer.java:97)
	at io.grpc.util.ForwardingLoadBalancer.requestConnection(ForwardingLoadBalancer.java:61)
	at io.grpc.xds.RingHashLoadBalancer$RingHashPicker.lambda$pickSubchannel$0(RingHashLoadBalancer.java:440)
	at io.grpc.SynchronizationContext.drain(SynchronizationContext.java:96)
	at io.grpc.SynchronizationContext.execute(SynchronizationContext.java:128)
	at io.grpc.xds.client.XdsClientImpl$ResourceSubscriber.onData(XdsClientImpl.java:817)
```
2025-07-17 12:56:33 +00:00
MV Shiva 6935d3a115
Revert "xds: add "resource_timer_is_transient_failure" server feature (#12063)" (#12228) 2025-07-17 11:35:34 +05:30
MV Shiva d7d70c6905
xds: cncf/xds proto sync to 2025-05-02 (#12225) 2025-07-17 10:26:12 +05:30
Kannan J d352540a02
api: Add more Javadoc for NameResolver.Listener2 interface (#12220) 2025-07-16 14:39:43 +05:30
MV Shiva 5a8326f1c7
xds: add "resource_timer_is_transient_failure" server feature (#12063) 2025-07-15 15:33:02 +05:30
Eric Anderson a8de9f07ab xds: Implement equals in RingHashConfig
Lack of equals causes cluster_resolver to consider every update a
different configuration and restart itself.

b/430347751
2025-07-14 14:06:15 +00:00
Eric Anderson 9d191b31b5 xds: Check isHttp11ProxyAvailable in equals()
This fixes an equals/hashCode bug introduced in 12197065fe.

Discovered when investigating b/430347751
2025-07-14 14:05:35 +00:00
Richard Belleville 01bd63d88f
Remove inactive maintainers (#12187) 2025-07-11 15:07:00 -07:00
John Cormie 94532a6b56
binder: Introduce server pre-authorization (#12127)
grpc-binder clients authorize servers by checking the UID of the sender of the SETUP_TRANSPORT Binder transaction against some SecurityPolicy. But merely binding to an unauthorized server to learn its UID can enable "keep-alive" and "background activity launch" abuse, even if security policy ultimately decides the connection is unauthorized. Pre-authorization mitigates this kind of abuse by looking up and authorizing a candidate server Application's UID before binding to it. Pre-auth is especially important when the server's address is not fixed in advance but discovered by PackageManager lookup.
2025-07-10 14:14:36 -07:00
Eric Anderson 6dfa03c51c
core: grpc-timeout should always be positive (#12201)
PROTOCOL-HTTP2.md specifies "TimeoutValue → {positive integer as ASCII
string of at most 8 digits}". Zero is not positive, so it should be
avoided. So make sure timeouts are at least 1 nanosecond instead of 0
nanoseconds.

grpc-go recently began disallowing zero timeouts in
https://github.com/grpc/grpc-go/pull/8290 which caused a regression as
grpc-java can generate such timeouts. Apparently no gRPC implementation
had previously been checking for zero timeouts.

Instead of changing the max(0) to max(1) everywhere, just move the max
handling into TimeoutMarshaller, since every caller of TIMEOUT_KEY was
doing the same max() handling.

Before fd8fd517d (in 2016!), grpc-java actually behaved correctly, as it
failed RPCs with timeouts "<= 0". The commit changed the handling to the
max(0) handling we see now.

b/427338711
2025-07-03 11:44:04 +05:30
Abhishek Agrawal 919370172d
census: APIs for stats and tracing (#12050) 2025-07-01 20:44:28 +05:30
Eric Anderson ca99a8c478 Fix RLS regressions from XdsDepMan conversion
297ab05ef converted CDS to XdsDependencyManager. This caused three
regressions:

 * CdsLB2 as a RLS child would always fail with "Unable to find
   non-dynamic root cluster" because is_dynamic=true was missing in
   its service config
 * XdsNameResolver only propagated resolution updates when the clusters
   changed, so a CdsUpdate change would be ignored. This caused a hang
   for RLS even with is_dynamic=true. For non-RLS the lack config update
   broke the circuit breaking psm interop test. This would have been
   more severe if ClusterResolverLb had been converted to
   XdsDependenceManager, as it would have ignored EDS updates
 * RLS did not propagate resolution updates, so CdsLB2 even with
   is_dynamic=true the CdsUpdate for the new cluster would never arrive,
   causing a hang

b/428120265
b/427912384
2025-06-30 14:23:32 +00:00
John Cormie 2ee4f9b488
AndroidComponentAddress constructor can be private. (#12188) 2025-06-27 10:58:48 +05:30
John Cormie 74aee11389
Clarify requirements for creating a cross-user Channel. (#12181)
The @SystemApi runtime visibility requirement isn't really new. It has always been implicit in the required INTERACT_ACROSS_USERS permission, which (in production) can only be held by system apps.

The SDK_INT >= 30 requirement was also always present, via @RequiresApi() on  BinderChannelBuilder#bindAsUser. This change just updates its replacement APIs (AndroidComponentAddress and TARGET_ANDROID_USER) to require it too.
2025-06-26 17:43:13 -07:00
vimanikag 64322c3243
11243: RLS cleanups (#12085) 2025-06-25 10:55:00 +05:30
Eric Anderson af7efeb9f5 core: Rely on ping-pong for flow control testing
The previous code did a ping-pong to make sure the transport had enough
time to process, but then proceeded to sleep 5 seconds. That sleep would
have been needed without the ping-pong, but with the ping-pong we are
confident all events have been drained from the transport. Deleting the
unnecessary sleeps saves 10 seconds, for each of the 9 instances of this
test.
2025-06-25 04:52:46 +00:00
Eric Anderson ebc6d3e932 Start 1.75.0 development cycle 2025-06-25 04:52:17 +00:00
Eric Anderson d374b26b68 xds: Disable LOGICAL_DNS in XdsDepMan until used
ClusterResolverLb is still doing DNS itself, so disable it in XdsDepMan
until that migration has finished. EDS is fine in XdsDepman, because
XdsClient will share the result with ClusterResolverLb.
2025-06-24 14:56:16 +00:00
MV Shiva f99b2aaef8
release: Migrate artifacts publishing from legacy OSSRH to Central Portal (#12156) 2025-06-24 10:12:35 +05:30
John Cormie 30d40a6179
binder: Cancel checkAuthorization() request if still pending upon termination (#12167) 2025-06-23 12:51:40 -07:00
John Cormie 9a6bdc70af
download maven using archive/permalink url (#12169) 2025-06-23 12:00:25 -07:00
Kun Zhang e6e7bcadaf
binder: stops emulating for 21/22 Lollipop in tests
See cl/769936336 internally
2025-06-18 14:47:24 -07:00
John Cormie 922dc8a999
Mark a few test helper methods as @CanIgnoreReturnValue (#12162) 2025-06-18 10:59:21 +05:30
Eric Anderson d2d8ed8efa xds: Add logical dns cluster support to XdsDepManager
ClusterResolverLb gets the NameResolverRegistry from
LoadBalancer.Helper, so a new API was added in NameResover.Args to
propagate the same object to the name resolver tree.

RetryingNameResolver was exposed to xds. This is expected to be
temporary, as the retrying is being removed from ManagedChannelImpl and
moved into the resolvers. At that point, DnsNameResolverProvider would
wrap DnsNameResolver with a similar API to RetryingNameResolver and xds
would no longer be responsible.
2025-06-17 22:14:20 +00:00
Eric Anderson f07eb47cac
util: Deliver addresses in a random order in MultiChildLb
This should often not matter much, but in b/412468630 it was cleary
visible that child creation order can skew load for the first batch of
RPCs. This doesn't solve all the cases, as further-away backends will
still be less likely chosen initially and it is ignorant of the LB
policy. But this doesn't impact correctness, is easy, and is one fewer
cases to worry about.
2025-06-17 07:14:32 -07:00
Eric Anderson 2604ce8a55 xds: XdsNR should be subscribing to clusters with XdsDepManager
This is missing behavior defined in gRFC A74:

> As per gRFC A31, the ConfigSelector gives each RPC a ref to the
> cluster that was selected for it to ensure that the cluster is not
> removed from the xds_cluster_manager LB policy config before the RPC
> is done with its LB picks. These cluster refs will also hold a
> subscription for the cluster from the XdsDependencyManager, so that
> the XdsDependencyManager will not stop watching the cluster resource
> until the cluster is removed from the xds_cluster_manager LB policy
> config.

Without the logic, RPCs can race and see the error:

> INTERNAL: CdsLb for cluster0: Unable to find non-dynamic root cluster

Fixes #12152. This fixes the regression introduced in 297ab05e
2025-06-17 13:36:24 +00:00
vimanikag 1c43098990
util: OutlierDetection should use Ticker, not TimeProvider (#12110)
TimeProvider provides wall time. That can move forward and backward as time is adjusted. OutlierDetection is measuring durations, so it should use a monotonic clock.

Fixes #11622
2025-06-16 07:47:36 -07:00
Eric Anderson d5b4fb51c2 xds: Support tracking non-xds resources in XdsDepManager
This will be used for logical dns clusters as part of gRFC A74. Swapping
to EnumMap wasn't really necessary, but was easy given the new type
system.

I can't say I'm particularly happy with the name of the new
TrackedWatcher type, but XdsConfigWatcher prevented using "Watcher"
because it won't implement the new interface, and ResourceWatcher
already exists in XdsClient. So we have TrackedWatcher, WatcherTracer,
TypeWatchers, and TrackedWatcherType.
2025-06-16 14:32:27 +00:00
Eric Anderson 8974a306af
util: Mark OutlierDetectionLb classes final
None of these classes were intended to be extended. Even non-public
classes need final to prevent mocks from doing horrible things.
2025-06-13 10:52:00 -07:00
Eric Anderson d88ef97a87 core: Remove RetryingNR.RESOLUTION_RESULT_LISTENER_KEY
It was introduced in fcb5c54e4 because at the time we didn't change the
API to communicate the status. When onResult2() was introduced in
90d0fabb1 this hack stopped being necessary.
2025-06-13 15:20:10 +00:00
Eric Anderson 240f731e00 xds: Avoid changing cache when watching children in XdsDepManager
The watchers can be completely regular, so the base class can do the
cache management while the subclasses are only concerned with
subscribing to children.
2025-06-13 15:18:14 +00:00
MV Shiva 26bd0eee47
core: Use lazy message formatting in checkState (#12144) 2025-06-13 12:30:13 +05:30
David Sanderson 6f69363d90
bazel: Migrate java_grpc_library to use DefaultInfo (#12148)
We here address the following obstacles in grpc-java to using Bazel's
--incompatible_disable_target_default_provider_fields flag:

```
ERROR: /private/var/tmp/_bazel_dws/7fd3cd5077fbf76d9e2ae421c39ef7ed/external/googleapis+/google/devtools/build/v1/BUILD.bazel:81:18: in _java_grpc_library rule @@googleapis+//google/devtools/build/v1:build_java_grpc:
Traceback (most recent call last):
        File "/private/var/tmp/_bazel_dws/7fd3cd5077fbf76d9e2ae421c39ef7ed/external/grpc-java+/java_grpc_library.bzl", line 94, column 30, in _java_rpc_library_impl
                args.add(toolchain.plugin.files_to_run.executable, format = "--plugin=protoc-gen-rpc-plugin=%s")
Error: Accessing the default provider in this manner is deprecated and will be removed soon. It may be temporarily re-enabled by setting --incompatible_disable_target_default_provider_fields=false. See https://github.com/bazelbuild/bazel/issues/20183 for details.
ERROR: /private/var/tmp/_bazel_dws/7fd3cd5077fbf76d9e2ae421c39ef7ed/external/googleapis+/google/devtools/build/v1/BUILD.bazel:81:18: Analysis of target '@@googleapis+//google/devtools/build/v1:build_java_grpc' failed
ERROR: Analysis of target '//src:bazel' failed; build aborted: Analysis failed
```
2025-06-12 08:43:23 -07:00
Eric Anderson 6cc2ff1ced util: In OutlierDetectionLb, don't box longs if they can't be null
Changed the builder pattern to pass the builder to the constructor,
since I was changing almost all the arguments of the constructor anyway.
2025-06-12 04:02:40 +00:00
Eric Anderson 13fe008044
rls: Refactor estimatedSizeBytes updates (#12145)
Just use a regular method instead of reusing the EvictionListener API.
Fix a few comments as well. Both of these changes were based on review
comments to pre-existing code in #11203.

Contributes to #11243
2025-06-12 09:31:19 +05:30
John Cormie 30f6a4db77
google-java-format a line that was too long (#12147) 2025-06-11 12:19:19 -07:00
Eric Anderson 297ab05efe
xds: Convert CdsLb to XdsDepManager
I noticed we deviated from gRFC A37 in some ways. It turned out those
were added to the gRFC later in https://github.com/grpc/proposal/pull/344:
- NACKing empty aggregate clusters
- Failing aggregate cluster when children could not be loaded
- Recusion limit of 16. We had this behavior already, but it was
  ascribed to matching C++

There's disagreement on whether we should actually fail the aggregate
cluster for bad children, so I'm preserving the pre-existing behavior
for now.

The code is now doing a depth-first leaf traversal, not breadth-first.
This was odd to see, but the code was also pretty old, so the reasoning
seems lost to history. Since we haven't seen more than a single level of
aggregate clusters in practice, this wouldn't have been noticed by
users.

XdsDependencyManager.start() was created to guarantee that the callback
could not be called before returning from the constructor. Otherwise
XDS_CLUSTER_SUBSCRIPT_REGISTRY could potentially be null.
2025-06-11 11:56:13 -07:00
MV Shiva a16d655919
compiler: generate blocking v2 unary calls that throw StatusException (#12126) 2025-06-10 10:31:03 +05:30
Eric Anderson 6afacf589e xds: Don't cache rdsName in XdsDepManager
We can easily compute the rdsName and avoiding the state means we don't
need to override onResourceDoesNotExist() to keep the cache in-sync with
the config.
2025-06-09 14:13:59 +00:00
Eric Anderson 4ee662fbcf xds: cancelled=true on watch close in XdsDepManager
1fd29bc80 replaced cancelWatcher() with watcher.close(). But setting
cancelled was missing. Because the config update checks for shutdown,
the cancelled flag no longer avoids exceptions. But it seems best to
continue avoiding any processing after close to avoid surprises.
2025-06-09 14:13:41 +00:00
Eric Anderson 1fd29bc804 xds: Use tracing GC in XdsDepManager
Reference counting doesn't release cycles, so swap to a tracing garbage
collector. This greatly simplifies the code as well, as diffing is no
longer necessary. (If vanilla reference counting was used, diffing
wouldn't have been necessary either as you just increment all the new
objects and decrement the old ones. But that doesn't work when use a set
instead of an integer.)
2025-06-06 08:43:35 -07:00
eshitachandwani dc192f5c5e
Create SPIFFE tests config (#12133) 2025-06-06 19:24:27 +05:30
John Cormie c206428749
binder: Rationalize @ThreadSafe-ty of BinderTransport. (#12130)
- Use @BinderThread to document restrictions on methods and certain fields.
- Make TransactionHandler non-public since only Android should call it.
- Replace an unnecessary AtomicLong with a plain old long.
2025-06-05 19:07:59 -07:00
Eric Anderson 4cd7881086 xds: Fix XdsDepManager aggregate cluster child ordering and loop detection
The children of aggregate clusters have a priority order, so we can't
ever throw them in an ordinary set for later iteration.

This now detects recusion limits only after subscribing, but that
matches our existing behavior in CdsLoadBalancer2. We don't get much
value detecting the limit before subscribing and doing so makes watcher
types more different.

Loops are still a bit broken as they won't be unwatched when orphaned,
as they will form a reference loop. In CdsLoadBalancer2, duplicate
clusters had duplicate watchers so there was single-ownership and
reference cycles couldn't form. Fixing that is a bigger change.

Intermediate aggregate clusters are now included in XdsConfig, just for
simplicity. It doesn't hurt anything whether they are present or
missing. but it required updates to some tests.
2025-06-05 08:43:02 -07:00
John Cormie 4c73999102
Move all test helper classes out of AbstractTransportTest so they can be used elsewhere (#12125) 2025-06-05 15:39:51 +05:30
Eric Anderson 482dc5c1c3
xds: Don't allow hostnames in address field (#12123)
* xds: Don't allow hostnames in address field

gRFC A27 specifies they must be IPv4 or IPv6 addresses. Certainly doing
a DNS lookup hidden inside the config object is asking for trouble.

The tests were accidentally doing a lot of failing DNS requests greatly
slowing them down. On my desktop, which made the problem most obvious
with five search paths in /etc/resolv.conf, :grpc-xds:test decreased
from 66s to 29s. The majority of that is XdsDependencyManagerTest which
went from 33s to .1s, as it generated a UUID for the in-process
transport each test and then used it as a hostname, which defeated
Java's DNS (negative) cache. The slowness was noticed because
XdsDependencyManagerTest should have run quickly as a single thread
without I/O, but was particularly slow on my desktop.

The cleanup caused me to audit serverName and the weird places it went.
I think some of them were tricks for XdsClientFallbackTest to squirrel
away something distinguishing, although reusing the serverName is asking
for confusion as is including the tricks in "shared" utilities.
XdsClientFallbackTest does have some non-trivial changes, but this seems
to fix some pre-existing bugs in the tests.

* Add failing hostname unit test
2025-06-05 15:37:49 +05:30
Eric Anderson efe9ccc22c
xds: Non-SOTW resources need onError() callbacks, too (#12122)
SOTW is unique in that it can become absent after being found. But if we
NACK when initially loading the resource, we don't want to delay, depend
on the resource timeout, and then give a poor error.

This was noticed while adding the EDS restriction that address is not a
hostname and some tests started hanging instead of failing quickly.
2025-06-03 12:02:02 +05:30
Eric Anderson 48d08e643e
xds: Remove EDS maybePublishConfig() avoidance in XdsDepManager (#12121)
The optimization makes the code more complicated. Yes, we know that
maybePublishConfig() will do no work because of an outstanding watch,
but we don't do this for other new watchers created and doing so would
just make the code more bug-prone. This removes a difference in how
different watcher types are handled.
2025-06-03 11:52:38 +05:30
Eric Anderson 6bad600592
xds: Use getWatchers more often in XdsDepManager
This provides better type and missing-map handling. Note that
getWatchers() now implicitly creates the map if it doesn't exist,
instead of just returning an empty map. That makes it a bit easier to
use and more importantly avoids accidents where a bug tries to modify
the immutable map.
2025-06-02 06:42:31 -07:00
MV Shiva 379fbaa380
Update README etc to reference 1.73.0 (#12108) 2025-06-02 15:37:11 +05:30
Eric Anderson 8044a56ad2
xds: Remove timeouts from XdsDepManagerTest (#12114)
The tests are using FakeClock and inprocess transport with direct executor, so all operations should run in the test thread.
2025-06-02 12:25:27 +05:30
Eric Anderson 142e378cea
xds: Improve shutdown handling of XdsDepManager
The most important change here is to handle subscribeToCluster() calls
after shutdown(), and preventing the internal state from being heavily
confused as the assumption is there are no watchers after shutdown().

ClusterSubscription.closed isn't strictly necessary, but I don't want
the code to depend on double-deregistration being safe.
maybePublishConfig() isn't being called after shutdown(), but adding the
protection avoids a class of bugs that would cause channel panic.
2025-05-30 09:00:27 -07:00
Alex Panchenko 22cf7cf2ac
tests: Replace usages of deprecated junit ExpectedException with assertThrows (#12103) 2025-05-30 10:55:37 +05:30
Sangamesh 83538cdae3
cronet: Delete TODO for User-Agent on CronetEngine
gRPC doesn't create the CronetEngine, so even though streaming is
observing the CronetEngine's User-Agent, we don't have control of that.
In addition, CronetEngines are commonly shared between gRPC and normal
HTTP traffic, so we don't actually expect users to set gRPC in engine's
user agent. The existing behavior seems to be working as well as
feasible.

Fixes #11582
2025-05-27 10:52:47 -07:00
Eric Anderson d124007ff4
kokoro: Don't run grpc codegen in android-interop (#12098)
android-interop has been failing to build since 46485c8 because it
didn't have cmake installed and defined LDFLAGS/CXXFLAGS with pkg-config
before make_dependencies.sh had been run.

Android-interop didn't verify the codegen is up-to-date. Building the
codegen was just a relic from when android was its own separate gradle
build. Avoiding codegen means we don't have to compile absl/protobuf and
have a C++ toolchain.
2025-05-26 08:42:58 +05:30
Kannan J 46485c8b62
Upgrade Protobuf C++ to 22.5 (#11961) 2025-05-23 12:24:14 +05:30
Michael Lumish 9406d3b2a0 Rename PSM interop fallback test suite to light 2025-05-22 15:43:25 -07:00
MV Shiva 9d439d4a44
xds: Add GcpAuthenticationFilter to FilterRegistry (#12075) 2025-05-22 16:22:41 +05:30
Eric Anderson f8700a13ad
compiler: Default to @generated=omit (#12080)
After many years of issue 9179 being open, there's been nothing to show
that we need the javax.annotations.Generated annotation. Most tools use
file paths and a few check for annotations with "Generated" in the name.
ErrorProne has a few that check for javax.annotations.Generated, but
only UnnecessarilyFullyQualified looks like it'd be a problem and it is
disabled by default. We're not getting any more information, no users
have reported issues with `@generated=omit`, and the existing dependency
is annoying users, so just drop it.

Given we will still retain the GrpcGenerated annotation, it seems highly
likely things are already okay. Even if there are problems they would
probably be addressed by adding a io.grpc.stub.annotations.Generated
annotation or small tweaks. In the short-term, (non-Bazel) users can use
`@generated=javax`, but long-term we could consider removing the option
assuming we've resolved any outstanding issues.

We will want to update the examples and the README to remove the
org.apache.tomcat:annotations-api dependency after the next release.

Fixes #9179
2025-05-21 22:49:30 +05:30
Luwei Ge 2fb09578a8
xds: enableSpiffe also checks the new env var per gRFC-A87 2025-05-19 12:50:25 -07:00
Gregory Cooke 480640dc2f
alts: add experimental keepalive (#12076) 2025-05-19 08:30:10 -07:00
Abhishek Agrawal b88536a17d
kokoro: add cloud run tests config (#12065)
kokoro: add cloud run tests config
2025-05-15 10:59:52 +05:30
Eric Anderson b089761486
xds: Enable least request by default (#12054)
Enable least request by default.

It has seen good testing by users and behaved as expected.

Fixes #11996
2025-05-14 13:23:37 +05:30
MV Shiva 59adeb9d47
Start 1.74.0 development cycle (#12061) 2025-05-13 17:15:36 +05:30
vinodhabib 6baac45bd2
xds: Fix pretty-print of Cluster with WrrLocality and LB policies (#12037) 2025-05-12 12:44:14 +05:30
John Cormie 454f1c5c6a
binder: Create a Robolectric version of BinderTransportTest (#12057)
This version runs way faster than BinderTransportTest and doesn't require an actual Android device/emulator. It'll allow future tests to simulate things that are difficult/impossible on real Android, at the price of some realism.
2025-05-09 14:57:59 -07:00
John Cormie 64fe061ccd
Simplify RobolectricBinderSecurityTest (#12058)
- Use INSTRUMENTATION_TEST @LooperMode to avoid custom Executors and idleLoopers() toil.
- No android.app.Service is actually needed with Robolectric.
2025-05-09 14:52:12 -07:00
Eric Anderson 80cc988b3c
xds: Use acceptResolvedAddresses() for WeightedTarget children (#12053)
Convert the tests to use acceptResolvedAddresses() as well.
2025-05-08 11:34:16 +05:30
Eric Anderson 3f5fdf1266
core: Remove unnecessary SuppressWarnings("deprecation") (#12052) 2025-05-07 10:14:48 +05:30
Eric Anderson be0247f501 Bump com.google.protobuf gradle plugin to 0.9.5
The plugin now outputs to "generated/sources". The IDE configuration
explicitly adding the folders to the source sets hasn't been needed for
some years.
2025-05-06 06:20:33 -07:00
Kim Jin Young 12aaf88d86
Fix comment's typo (#12045) 2025-05-05 22:32:31 +05:30
Eric Anderson 25199e9df9
xds: XdsDepManager should ignore updates after shutdown
This prevents a NPE and subsequent channel panic when trying to build a
config (because there are no watchers, so waitingOnResource==false)
without any listener and route.
```
java.lang.NullPointerException: Cannot invoke "io.grpc.xds.XdsDependencyManager$RdsUpdateSupplier.getRdsUpdate()" because "routeSource" is null
    at io.grpc.xds.XdsDependencyManager.buildUpdate(XdsDependencyManager.java:295)
    at io.grpc.xds.XdsDependencyManager.maybePublishConfig(XdsDependencyManager.java:266)
    at io.grpc.xds.XdsDependencyManager$EdsWatcher.onChanged(XdsDependencyManager.java:899)
    at io.grpc.xds.XdsDependencyManager$EdsWatcher.onChanged(XdsDependencyManager.java:888)
    at io.grpc.xds.client.XdsClientImpl$ResourceSubscriber.notifyWatcher(XdsClientImpl.java:929)
    at io.grpc.xds.client.XdsClientImpl$ResourceSubscriber.lambda$onData$0(XdsClientImpl.java:837)
    at io.grpc.SynchronizationContext.drain(SynchronizationContext.java:96)
```

I think this fully-fixes the problem today, but not tomorrow.
subscribeToCluster() is racy as well, but not yet used.

This was noticed when idleTimeout was firing, with some other code
calling getState(true) to wake the channel back up. That may have made
this panic more visible than it would be otherwise, but that has not
been investigated.

b/412474567
2025-04-23 09:18:08 -07:00
Kannan J 7952afdd56
Add some documentation to StatusOr.equals regarding how underlying statuses are compared, to avoid any confusion, as suggested in issue #11949. (#12036)
Add some documentation to StatusOr.equals regarding how underlying statuses are compared, to avoid any confusion, as suggested in issue #11949.
2025-04-23 18:42:24 +05:30
Abhishek Agrawal 6cd007d0d0
xds: add the missing xds.authority metric (#12018)
This completes the [XDS client metrics](https://github.com/grpc/proposal/blob/master/A78-grpc-metrics-wrr-pf-xds.md#xdsclient) by adding the remaining grpc.xds.authority metric.
2025-04-22 14:34:51 +05:30
Eric Anderson 9619453799
Implement grpc.lb.backend_service optional label
This completes gRFC A89. 7162d2d66 and fc86084df had already implemented
the LB plumbing for the optional label on RPC metrics. This observes the
value in OpenTelemetry and adds it to WRR metrics as well.

https://github.com/grpc/proposal/blob/master/A89-backend-service-metric-label.md
2025-04-21 06:17:43 -07:00
Abhishek Agrawal 53de8a72ca
Update README etc to reference 1.72.0 (#12025) 2025-04-17 17:37:08 +05:30
MV Shiva 7a08fdb7f9
xds: float LRU cache across interceptors (#11992) 2025-04-17 07:26:40 +05:30
Kurt Alfred Kluever 84bd01454b context: Remove mention of "epoch" from Ticker.nanoTime() javadocs, plus other minor touchups
In Java, when people hear "epoch", they think "unix epoch".

cl/747082451
2025-04-15 13:00:35 -07:00
Eric Anderson 65d0bb8a4d
xds: Enable deprecation warnings
The security code referenced fields removed from gRFC A29 before it was
finalized.

Note that this fixes a bug in CommonTlsContextUtil where
CombinedValidationContext was not checked. I believe this was the only
location with such a bug as I audited all non-test usages of
has/getValidationContext() and confirmed they have have a corresponding
has/getCombinedValidationContext().
2025-04-11 08:25:21 -07:00
Eric Anderson f79ab2f16f api: Remove deprecated SubchannelPicker.requestConnection()
It has been deprecated since cec9ee368, six years ago. It was replaced
with LoadBalancer.requestConnection().
2025-04-09 12:51:33 -07:00
Eric Anderson a6aec2769e auth: Use pre-existing private key in test
Generating a KeyPair is very expensive when running with TSAN, because
TSAN keeps the JVM in interpreted mode. This speeds up the test running
on my desktop from .368s to .151s; faster, but nobody cares. With TSAN,
the speedup is from 150-500s to 4-6s. Within Google the test was timing
out because it was taking so long. While we can increase the timeout,
it seems better to speed up the test in this easy way.
2025-04-08 11:03:45 -07:00
Eric Anderson 2db4852e23 core: Loop over interceptors when computing effective interceptors
A post-merge review of 8516cfef9 suggested this change and the comment
had been lost in my inbox.
2025-04-07 21:33:55 -07:00
jiangyuan 54d37839a3
stub: trailersFromThrowable() metadata should be copied (#11979)
If the same exception is passed to multiple RPCs, then the results will
race.

Fixes #11973
2025-04-07 14:34:57 -07:00
Eric Anderson aae52de3b8 stub: Add RunWith(JUnit4) to support varied environments
Some JUnit environments require the RunWith annotation. Notably
Blaze/Bazel needs it.
2025-04-04 12:28:37 -07:00
Kannan J a13fca2bf2
xds: ClusterResolverLoadBalancer handle update for both resolved addresses and errors via ResolutionResult (#11997) 2025-04-04 22:08:29 +05:30
Alex Panchenko edc2bf7346
stub: Utility method StreamObservers.nextAndComplete() that does both onNext and onComplete (#11778) 2025-04-04 19:39:35 +05:30
Eric Anderson 5ca4d852ae
core: Avoid Set.removeAll() when passing a possibly-large List (#11994)
See #11958
2025-04-04 17:46:37 +05:30
Abhishek Agrawal d4c46a7f1f
refactor: prevents global stats config freeze in ConfiguratorRegistry.getConfigurators() (#11991) 2025-04-04 11:23:08 +05:30
MV Shiva c8d1e6e39c
xds: listener type validation (#11933) 2025-04-03 11:22:26 +05:30
MV Shiva 84c7713b2f
xds: propagate audience from cluster resource in gcp auth filter (#11972) 2025-04-02 16:29:55 +05:30
Eric Anderson 908f9f19cd
core: Delete the long-deprecated GRPC_PROXY_EXP (#11988)
"EXP" stood for experimental and all documentation that referenced it made it clear it was experimental. It's been some years since we started logging a message when it was used to say it will be deleted. There's no time like the present to delete it.
2025-04-02 16:24:32 +05:30
Eric Anderson 8ca7c4ef1f
core: Delete stale SuppressWarnings("deprecated") for ATTR_LOAD_BALANCING_CONFIG (#11982)
ATTR_LOAD_BALANCING_CONFIG was deleted in bf7a42dbd.
2025-04-02 16:22:00 +05:30
Kannan J c28a7e3e06
okhttp: Per-rpc call option authority verification (#11754) 2025-04-02 10:10:41 +05:30
Abhishek Agrawal 8f6a16f846
Start 1.73.0 development cycle (#11987) 2025-04-01 16:27:39 +05:30
Eric Anderson 2448c8b6b9
util: Replace BUFFER_PICKER with FixedResultPicker
I think at some point there were more usages in the tests. But now it
is pretty easy.

PriorityLb.ChildLbState.picker is initialized to
FixedResultPicker(NoResult). So now that GracefulSwitchLb is using the
same picker, equals() is able to de-dup an update.
2025-03-28 12:49:36 -07:00
Eric Anderson 2e260a4bbc util: Graceful switch to new LB when leaving CONNECTING
Previously it would wait for the new LB to enter READY. However, that
prevents there being an upper-bound on how long the old policy will
continue to be used. The point of graceful switch is to avoid RPCs
seeing increased latency when we swap config. We don't want it to
prevent the system from becoming eventually consistent.
2025-03-28 15:18:10 +00:00
Alex Panchenko 7507a9ec06
core: Use java.time.Time.getNano in InstantTimeProvider without reflection (#11977)
Fixes #11975
2025-03-26 13:49:21 +05:30
Abhishek Agrawal a332eddc13
fix: cleans up FileWatcherCertificateProvider in XdsSecurityClientServerTest 2025-03-26 11:43:05 +05:30
jiangyuan 350f90e1a3
services: Avoid cancellation exceptions when notifying watchers that already have their connections cancelled (#11934)
Some clients watching health status can cancel their watch and `HealthService` when trying to notify these watchers were getting CANCELLED exception because there was no cancellation  handler set on the `StreamObserver`. This change sets the cancellation handler that removes the watcher from the set of watcher clients to be notified of the health status.
2025-03-25 17:42:28 +05:30
Eric Anderson 3961a923ac
core: Log any exception during panic because of exception
panic() calls a good amount of code, so it could get another exception.
The SynchronizationContext is running on an arbitrary thread and we
don't want to propagate this secondary exception up its stack (to be
handled by its UncaughtExceptionHandler); it we wanted that we'd
propagate the original exception.

This second exception will only be seen in the logs; the first exception
was logged and will be used to fail RPCs.

Also related to http://yaqs/8493785598685872128 and b692b9d26
2025-03-24 14:32:53 -07:00
Ashley Zhang 1958e42370
xds: add support for custom per-target credentials on the transport (#11951) 2025-03-21 15:19:40 -07:00
yifeizhuang 94f8e93691
otel tracing: fix span names (#11974) 2025-03-21 15:19:25 -07:00
Alex Panchenko d60e6fc251
Replace usages of deprecated ExpectedException in grpc-api and grpc-core (#11962) 2025-03-21 13:00:24 +05:30
Eric Anderson d2d72cda83
xds: Expose filter names to filter instances (#11971)
This is to support gRFC A83 xDS GCP Authentication Filter:
> Otherwise, the filter will look in the CDS resource's metadata for a
> key corresponding to the filter's instance name.
2025-03-21 11:01:16 +05:30
Eric Anderson bb120a8cbb xds: Assert XdsNR's cluster ref counting is consistent
It is much harder to debug refcounting problems when we ignore
impossible situations. So make such impossible cases complain loudly so
the bug is obvious.
2025-03-19 13:47:02 -07:00
Eric Anderson bc3c764058 xds: Include XdsConfig as a CallOption
This allows Filters to access the xds configuration for their own
processing. From gRFC A83:

> This data is available via the XdsConfig attribute introduced in A74.
> If the xDS ConfigSelector is not already passing that attribute to the
> filters, it will need to be changed to do so.
2025-03-19 09:04:27 -07:00
Abhishek Agrawal a57c14a51e
refactor: Stops exception allocation on channel shutdown
This fixes #11955.

Stops exception allocation and its propagation on channel shutdown.
2025-03-19 09:27:34 +05:30
Eric Anderson e80c197455
xds: Use XdsDependencyManager for XdsNameResolver
Contributes to the gRFC A74 effort.
https://github.com/grpc/proposal/blob/master/A74-xds-config-tears.md

The alternative to using Mockito's ArgumentMatcher is to use Hamcrest.
However, Hamcrest did not impress me. ArgumentMatcher is trivial if you
don't care about the error message.

This fixes a pre-existing issue where ConfigSelector.releaseCluster
could revert the LB config back to using cluster manager after releasing
all RPCs using a cluster have committed.

Co-authored-by: Larry Safran <lsafran@google.com>
2025-03-18 14:05:01 -07:00
MV Shiva e388ef3975
documentation: upgrade to junit 4.13.2 (#11967) 2025-03-18 18:43:03 +05:30
Dennis Shao b69bd64ce7
Populate the pb::java feature extension to gprc proto plugin (#11885)
Populate the pb::java feature extension to the protoc plugins that require Protobuf Java feature resolution for the  edition.
2025-03-17 18:46:28 +05:30
Alex Panchenko fca1d3cf43
servlet: set description for CANCELLED status (#11927) 2025-03-12 14:09:49 +05:30
MV Shiva 2f52a00364
netty: Swap to UniformStreamByteDistributor (#11954) 2025-03-11 22:39:54 +05:30
Kannan J 2191557582
Update README etc to reference 1.71.0 (#11940) 2025-03-11 16:05:39 +05:30
Arjan Singh Bal 4933cddd00
Fix typo in dualstack example (#11916) 2025-03-11 16:05:05 +05:30
Kannan J 24b9f6ff0d
Update psm-dualstack.cfg (#11950)
120 minutes has not been sufficient, causing frequent VM timeout errors in the test runs: https://testgrid.corp.google.com/grpc-psm-java#v1.67.x&width=20&graph-metrics=test-duration-minutes&include-filter-by-regex=psm-dualstack$
2025-03-10 12:33:45 +05:30
Emmanuel Ferdman 61a110d962
examples: Update in-process sources in examples (#11952)
Update in-process sources location in examples since they have been migrated from core artifacts.
2025-03-10 05:20:20 +00:00
Eric Anderson f3f054a0a4 xds: Log cluster_manager config update before applying config
It is confusing/harder to read the logs when the
activations/deactivations because of the config happen before the log
entry describing the new config.
2025-03-07 14:37:37 -08:00
Eric Anderson d82613a74c
xds: Fix cluster selection races when updating config selector
Listener2.onResult() doesn't require running in the sync context, so
when called from the sync context it is guaranteed not to do its
processing immediately (instead, it schedules work into the sync
context).

The code was doing an update dance: 1) update service config to add new
cluster, 2) update config selector to use new cluster, 3) update service
config to remove old clusters. But the onResult() wasn't being processed
immediately, so the actual execution order was 2, 1, 3 which has a small
window where RPCs will fail. But onResult2() does run immediately. And
since ca4819ac6, updateBalancingState() updates the picker immediately.

cleanUpRoutes() was also racy because it updated the routingConfig
before swapping to the new config selector, so RPCs could fail saying
there was no route instead of the useful error message. Even with the
opposite order, some RPCs may be executing the while loop of
selectConfig(), trying to acquire a cluster. The code unreffed the
clusters before updating the routingConfig, so those RPCs could go into
a tight loop until the routingConfig was updated. Also, once the
routingConfig was updated to EMPTY those RPCs would similarly
see the wrong error message. To give the correct error message,
selectConfig() must fail such RPCs directly, and once it can do that
there's no need to stop using the config selector in error cases. This
has the benefit of fewer moving parts and more consistent threading
among cases.

The added test was able to detect the race 2% of the time. The slower
the code/machine, the more reliable the test failed. ca4819ac6 along
with this commit reduced it to 0 failures in 1000 runs.

Discovered when investigating b/394850611
2025-03-07 10:33:35 -08:00
Eric Anderson ca4819ac6d core: Apply ManagedChannelImpl's updateBalancingState() immediately
ffcc360ba adjusted updateBalancingState() to require being run within
the sync context. However, it still queued the work into the sync
context, which was unnecessary. This re-entering the sync context
unnecessarily delays the new state from being used.
2025-03-06 12:31:10 -08:00
Sergii Tkachenko a6a041e415
xds: Support filter state retention
This PR adds support filter state retention in Java. The mechanism
will be similar to the one described in [A83]
(https://github.com/grpc/proposal/blob/master/A83-xds-gcp-authn-filter.md#filter-call-credentials-cache)
for C-core, and will serve the same purpose. However, the
implementation details are very different due to the different nature
of xDS HTTP filter support in C-core and Java.

### Filter instance lifecycle
#### xDS gRPC clients
New filter instances are created per combination of:
1. `XdsNameResolver` instance,
2. Filter name+typeUrl as configured in 
   HttpConnectionManager (HCM) http_filters.

Existing client-side filter instances are shutdown:
- A single a filter instance is shutdown when an LDS update contains
  HCM that is missing filter configuration for name+typeUrl
  combination of this instance.
- All filter instances when watched LDS resource is missing from an
  LDS update.
- All filter instances name resolver shutdown.

#### xDS-enabled gRPC servers
New filter instances are created per combination of:
1. Server instance,
2. FilterChain name,
3. Filter name+typeUrl as configured in FilterChain's HCM.http_filters

Filter instances of Default Filter Chain is tracked separately per:
1. Server instance,
2. Filter name+typeUrl in default_filter_chain's HCM.http_filters.

Existing server-side filter instances are shutdown:
- A single a filter instance is shutdown when an LDS update contains
  FilterChain with HCM.http_filters that is missing configuration for
  filter name+typeUrl.
- All filter instances associated with the FilterChain when an LDS
  update no longer contains FilterChain's name.
- All filter instances when watched LDS resource is missing from an
  LDS update.
- All filter instances on server shutdown.

### Related
- Part 1: #11883
2025-03-06 10:32:08 -08:00
MV Shiva 602aece081
xds: avoid unnecessary dns lookup (#11932) 2025-03-06 16:04:53 +05:30
MV Shiva 12197065fe
xds: xDS-based HTTP CONNECT configuration (#11861) 2025-03-06 13:40:18 +05:30
MV Shiva c340f4a2f3
rls: allow maxAge to exceed 5m if staleAge is set (#11931) 2025-03-04 10:02:03 +05:30
Sergii Tkachenko 1a2285b527
xds: ensure server interceptors are created in a sync context (#11930)
`XdsServerWrapper#generatePerRouteInterceptors` was always intended
to be executed within a sync context. This PR ensures that by calling
`syncContext.throwIfNotInThisSynchronizationContext()`.

This change is needed for upcoming xDS filter state retention because
the new tests in XdsServerWrapperTest flake with this NPE:

> `Cannot invoke "io.grpc.xds.client.XdsClient$ResourceWatcher.onChanged(io.grpc.xds.client.XdsClient$ResourceUpdate)" because "this.ldsWatcher" is null`
2025-03-03 14:28:36 -08:00
Kannan J cdab410b81
netty: Per-rpc authority verification against peer cert subject names (#11724)
Per-rpc verification of authority specified via call options or set by the LB API against peer cert's subject names.
2025-02-24 20:28:11 +05:30
Eric Anderson 57124d6b29 Use acceptResolvedAddresses() in easy cases
We want to move away from handleResolvedAddresses(). These are "easy" in
that they need no logic. LBs extending ForwardingLoadBalancer had the
method duplicated from handleResolvedAddresses() and swapped away from
`super` because ForwardingLoadBalancer only forwards
handleResolvedAddresses() reliably today. Duplicating small methods was
less bug-prone than dealing with ForwardingLoadBalancer.
2025-02-20 21:25:55 -08:00
Eric Anderson 110c1ff0d6 xds: Use acceptResolvedAddresses() for PriorityLb children
PriorityLb should propagate config problems up to the name resolver so
it can refresh.
2025-02-20 16:35:54 -08:00
Eric Anderson f207be39a9 util: Remove GracefulSwitchLb.switchTo()
It was deprecated in 85e0a01ec, so has been deprecated for six
releases/over six months.
2025-02-20 16:06:37 -08:00
Daniel Liu 892144dcac
xds: explicitly set request hash key for the ring hash LB policy
Implements [gRFC A76: explicitly setting the request hash key for the
ring hash LB policy][A76]
* Explictly setting the request hash key is guarded by the
  `GRPC_EXPERIMENTAL_RING_HASH_SET_REQUEST_HASH_KEY` environment
  variable until API stabilized. 

Tested:
* Verified end-to-end by spinning up multiple gRPC servers and a gRPC
  client that injects a custom service (load balancing) config with
  `ring_hash_experimental` and a custom `request_hash_header` (with
  NO associated value in the metadata headers) which generates a random
  hash for each request to the ring hash LB. Verified picks/RPCs are
  split evenly/uniformly across all backends.
* Ran affected unit tests with thread sanitizer and 1000 iterations to
  prevent data races.

[A76]: https://github.com/grpc/proposal/blob/master/A76-ring-hash-improvements.md#explicitly-setting-the-request-hash-key
2025-02-19 20:25:33 -08:00
Riya Mehta 68d79b5130
s2a: Use protos published under com.google.s2a.proto.v2. (#11908) 2025-02-19 16:59:50 -08:00
Kannan J 60f6ea7b8e
Upgrade gradle and gradle plugin versions. (#11906)
Upgrading to Gradle 8.11.
Gradle 8.12 requires newer versions of Windows (gradle/gradle#31939) that we can look into later.
2025-02-19 17:25:54 +05:30
Sergii Tkachenko 2b87b01651
xds: Change how xDS filters are created by introducing Filter.Provider (#11883)
This is the first step towards supporting filter state retention in
Java. The mechanism will be similar to the one described in [A83]
(https://github.com/grpc/proposal/blob/master/A83-xds-gcp-authn-filter.md#filter-call-credentials-cache)
for C-core, and will serve the same purpose. However, the
implementation details are very different due to the different nature
of xDS HTTP filter support in C-core and Java.

In Java, xDS HTTP filters are backed by classes implementing
`io.grpc.xds.Filter`, from here just called "Filters". To support
Filter state retention (next PR), Java's xDS implementation must be
able to create unique Filter instances per:
- Per HCM
  `envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager`
- Per filter name as specified in
  `envoy.extensions.filters.network.http_connection_manager.v3.HttpFilter.name`

This PR **does not** implements Filter state retention, but lays the
groundwork for it by changing how filters are registered and
instantiated. To achieve this, all existing Filter classes had to be
updated to the new instantiation mechanism described below.

Prior to these this PR, Filters had no livecycle. FilterRegistry
provided singleton instances for a given typeUrl. This PR introduces
a new interface `Filter.Provider`, which instantiates Filter classes.
All functionality that doesn't need an instance of a Filter is moved
to the Filter.Provider. This includes parsing filter config proto
into FilterConfig and determining the filter kind
(client-side, server-side, or both).

This PR is limited to refactoring, and there's no changes to the
existing behavior. Note that all Filter Providers still return
singleton Filter instances. However, with this PR, it is now possible
to create Providers that return a new Filter instance each time
`newInstance` is called.
2025-02-18 10:47:01 -08:00
Eric Anderson 713607056e util: Use acceptResolvedAddresses() for MultiChildLb children
A failing Status from acceptResolvedAddresses means something is wrong
with the config, but parts of the config may still have been applied.
Thus there are now two possible flows: errors that should prevent
updateOverallBalancingState() and errors that should have no effect
other than the return code. To manage that, MultChildLb must always be
responsible for calling updateOverallBalancingState().
acceptResolvedAddressesInternal() was inlined to make that error
processing easier. No existing usages actually needed to have logic
between updating the children and regenerating the picker.

RingHashLb already was verifying that the address list was not empty, so
the short-circuiting when acceptResolvedAddressesInternal() returned an
error was impossible to trigger. WrrLb's updateWeightTask() calls the
last picker, so it can run before acceptResolvedAddressesInternal(); the
only part that matters is re-creating the weightUpdateTimer.
2025-02-18 07:33:49 -08:00
Kannan J a132123c93
Start 1.72.0 development cycle (#11907) 2025-02-18 19:46:02 +05:30
Naveen Prasanna V 16edf7ac4e
Examples: Updated HelloWorldServer to use Executor (#11850) 2025-02-18 14:40:18 +05:30
Eric Anderson 16d26726cf
s2a: Don't allow S2AStub to be set
S2AStub is an internal API and shouldn't be used outside of s2a. It is
still available for tests.

IntegrationTest was moved to io.grpc.s2a. It uses a io.grpc.s2a class,
so shouldn't be in internal.handler
2025-02-14 15:47:19 -08:00
Eric Anderson 9e54e8e5e9 servlet: Provide Gradle a filter version number
The version number is simply a unique string per version.
2025-02-14 15:45:44 -08:00
Larry Safran c1d703546a
okhttp:Use a locally specified value instead of Segment.SIZE in okhttp (#11899)
Switched to using 8192 which is the current value of Segment.SIZE and just have a test check that they are equal.  

The reason for doing this is that Segment.SIZE is Kotlin internal so shouldn't be used outside of its module.
2025-02-14 14:46:54 -08:00
Eric Anderson 57af63ad0a kokoro: Increase gradle mem in android-interop
To try to aid failure when building android-interop-testing
```
The Daemon will expire after the build after running out of JVM heap space.
The project memory settings are likely not configured or are configured to an insufficient value.
The daemon will restart for the next build, which may increase subsequent build times.
These settings can be adjusted by setting 'org.gradle.jvmargs' in 'gradle.properties'.
The currently configured max heap space is '512 MiB' and the configured max metaspace is '384 MiB'.
...
Exception in thread "Daemon client event forwarder" java.lang.OutOfMemoryError: Java heap space
...
> Task :grpc-android-interop-testing:mergeDexDebug FAILED
ERROR:D8: java.lang.OutOfMemoryError: Java heap space
com.android.builder.dexing.DexArchiveMergerException: Error while merging dex archives:
```
2025-02-14 13:20:05 -08:00
Riya Mehta a5347b2bc4
s2a: inject Optional<AccessTokenManager> in tests 2025-02-14 12:55:42 -08:00
Larry Safran 41dd0c6d73
xds:Cleanup to reduce test flakiness (#11895)
* don't process resourceDoesNotExist for watchers that have been cancelled.

* Change test to use an ArgumentMatcher instead of expecting that only the final result will be sent since depending on timing there may be configs sent for clusters being removed with their entries as errors.
2025-02-14 10:23:54 -08:00
Alex Panchenko 5a7f350537
optimize number of buffer allocations (#11879)
Currently this improves 2 flows

1. Known length message which length is greater than 1Mb. Previously the
first buffer was 1Mb, and then many buffers of 4096 bytes (from
CodedOutputStream), now subsequent buffers are also up to 1Mb

2. In case of compression, the first write is always 10 bytes buffer
(gzip header), but worth allocating more space
2025-02-14 05:59:21 -08:00
MV Shiva 7585b1607d
core: remember last pick status in no real stream (#11851) 2025-02-14 11:38:06 +05:30
Eric Anderson 122b683717 Upgrade netty-tcnative to 2.0.70 2025-02-13 12:41:56 -08:00
Larry Safran 764a4e3f08
xds: Cleanup by moving methods in XdsDependencyManager ahead of classes (#11890)
* Move private methods ahead of classes
2025-02-11 14:34:46 -08:00
Larry Safran ade2dd2038
xds: Change XdsClusterConfig to have children field instead of endpoint (#11888)
* Change XdsConfig to match spec with a `children` object holding either `a list of leaf cluster names` or `an EdsUpdate`.  Removed intermediate aggregate nodes from `XdsConfig.clusters`.
2025-02-11 12:38:52 -08:00
Kannan J fc8571a0e5
Version upgrades (#11874) 2025-02-12 01:08:46 +05:30
Naveen Prasanna V 302342cfce
core: logging the error message when onClose() itself fails (#11880) 2025-02-11 11:38:07 +05:30
Benjamin Peterson dc316f7fd9 Add missing frame release to Http2ClientStreamTransportState.
If a data frame is received before headers, processing of the frame is abandoned. The frame must be released in that case.
2025-02-10 21:40:05 -08:00
Sergii Tkachenko bd6af59221
xds: improve code readability of server FilterChain parsing
- Improve code flow and variable names
- Reduce nesting
- Add comments between logical blocks
- Add comments explaining some xDS/gRPC nuances
2025-02-10 17:14:07 -08:00
Larry Safran 67fc2e156a
Add new classes for eliminating xds config tears (#11740)
* Framework definition to support A74
2025-02-07 16:33:17 -08:00
MV Shiva 90b1c4fe94
protobuf: Stabilize marshallerWithRecursionLimit (#11884) 2025-02-07 14:16:13 +05:30
Abhishek Agrawal 44e92e2c2c
core: updates the backoff range as per the A6 redefinition (#11858)
* core: updates the backoff range being used from [0, 1] to [0.8, 1.2] as per the A6 redefinition

* adds a flag for experimental jitter

* xds: Allow FaultFilter's interceptor to be reused

This is the only usage of PickSubchannelArgs when creating a filter's
ClientInterceptor, and a follow-up commit will remove the argument and
actually reuse the interceptors. Other filter's interceptors can
already be reused.

There doesn't seem to be any significant loss of legibility by making
FaultFilter a more ordinary interceptor, but the change does cause the
ForwardingClientCall to be present when faultDelay is configured,
independent of whether the fault delay ends up being triggered.

Reusing interceptors will move more state management out of the RPC path
which will be more relevant with RLQS.

* netty: Removed 4096 min buffer size (#11856)

* netty: Removed 4096 min buffer size

* turns the flag in a var for better efficiency

---------

Co-authored-by: Eric Anderson <ejona@google.com>
2025-02-05 14:18:20 -08:00
Alex Panchenko 3142928fa3
use gradle java-platform in grpc-bom; Fixes #5530 (#11875)
* use gradle java-platform in grpc-bom; Fixes #5530

* fix withXml

* explicitly exclude grpc-compiler
2025-02-05 11:10:39 -08:00
Eric Anderson 199a7ea3e8
xds: Improve XdsNR's selectConfig() variable handling
The variables from the do-while are no longer initialized to let the
compiler verify that the loop sets each. Unnecessary comparisons to null
are also removed and is more obvious as the variables are never set to
null. Added a minor optimization of computing the RPCs path once instead
of once for each route. The variable declarations were also sorted to
match their initialization order.

This does fix an unlikely bug where if the old code could successfully
matched a route but fail to retain the cluster, then when trying a
second time if the route was _not_ matched it would re-use the prior route
and thus infinite-loop failing to retain that same cluster.

It also adds a missing cast to unsigned long for a uint32 weight. The old
code would detect if the _sum_ was negative, but a weight using 32 bits
would have been negative and never selected.
2025-02-05 10:37:22 -08:00
Eric Anderson ea3f644eef
Replace Kokoro ARM build with GitHub Actions
The Kokoro aarch64 build runs on x86 with an emulator, and has always
been flaky due to the slow execution speed. At present it is continually
failing due to deadline exceededs. GitHub Actions is running on aarch64
hardware, so is much faster (4 minutes vs 30 minutes, without including
the speedup from GitHub Action's caching).
2025-02-04 10:51:13 -08:00
Alex Panchenko 4a10a38166
servlet: remove 4096 min buffer size
Similar to 7153ff8
2025-02-04 10:49:30 -08:00
Eric Anderson b1bc0a9d24 alts: Add ClientCall support to AltsContextUtil
This adds a createFrom(Attributes) to mirror the check(Attributes) added
in ba8ab79. It also adds conveniences for ClientCall for both
createFrom() and check(). This allows getting peer information from
ClientCall and CallCredentials.RequestInfo, as was already available
from ServerCall.

The tests were reworked to test the Attribute-based methods and then
only basic tests for client/server.

Fixes #11042
2025-01-30 16:10:19 -08:00
Eric Anderson 04f1cc5845 xds: Make XdsNR.RoutingConfig.empty a constant
The field was made final in 4b52639aa but was soon reverted in 3ebb3e192
because of what I assume was a bad merge conflict resolution. The field
has contained an immutable object since its introduction in d25f5acf1,
so it is pretty likely to remain a constant in the future.
2025-01-30 15:10:12 -08:00
Eric Anderson c506190b0f
xds: Reuse filter interceptors across RPCs
This moves the interceptor creation from the ConfigSelector to the
resource update handling.

The code structure changes will make adding support for filter
lifecycles (for RLQS) a bit easier. The filter lifecycles will allow
filters to share state across interceptors, and constructing all the
interceptors on a single thread will mean filters wouldn't need to be
thread-safe (but their interceptors would be thread-safe).
2025-01-30 12:43:51 -08:00
Eric Anderson 90aefb26e7 core: Propagate authority override from LB exactly once
Setting the authority is only useful when creating a real stream, as
there will be a following pick otherwise. In addition, DelayedStream
will buffer each call to setAuthority() in a list and we don't want that
memory usage. Note that no LBs are using this feature yet, so users
would not have been exposed to the memory use.

We also needed to setAuthority() when the LB selected a subchannel on
the first pick attempt.
2025-01-30 08:08:17 -08:00
Abhishek Agrawal 7153ff8522
netty: Removed 4096 min buffer size (#11856)
* netty: Removed 4096 min buffer size
2025-01-30 12:52:37 +05:30
Eric Anderson b3db8c2489 xds: Allow FaultFilter's interceptor to be reused
This is the only usage of PickSubchannelArgs when creating a filter's
ClientInterceptor, and a follow-up commit will remove the argument and
actually reuse the interceptors. Other filter's interceptors can
already be reused.

There doesn't seem to be any significant loss of legibility by making
FaultFilter a more ordinary interceptor, but the change does cause the
ForwardingClientCall to be present when faultDelay is configured,
independent of whether the fault delay ends up being triggered.

Reusing interceptors will move more state management out of the RPC path
which will be more relevant with RLQS.
2025-01-29 14:21:53 -08:00
vinodhabib 9e8629914f
examples: Added README files for all missing Examples (#11676) 2025-01-28 13:02:00 +05:30
Larry Safran 87aa6deadf
core:Have acceptResolvedAddresses() do a seek when in CONNECTING state and cleanup removed subchannels when a seek was successful (#11849)
* Have acceptResolvedAddresses() do a seek when in CONNECTING state and cleanup removed subchannels when a seek was successful.
Move cleanup of removed subchannels into a method so it can be called from 2 places in acceptResolvedAddresses.
Since the seek could mean we never looked at the first address, if we go off the end of the index and haven't looked at the all of the addresses then instead of scheduleBackoff() we reset the index and request a connection.
2025-01-24 16:42:56 -08:00
Boddu Harshavardhan 67351c0c53
gcp-observability: Optimize GcpObservabilityTest.enableObservability execution time (#11783) 2025-01-24 16:50:41 +05:30
MV Shiva bf8eb24a30
Update README etc to reference 1.70.0 (#11854) 2025-01-24 15:21:14 +05:30
Kannan J 0f5503ebb1
xds: Include max concurrent request limit in the error status for concurre… (#11845)
Include max concurrent request limit in the error status for concurrent connections limit exceeded
2025-01-23 21:40:21 +05:30
Eric Anderson 495a8906b2 xds: Fix fallback test FakeClock TSAN failure
d65d3942e increased the test speed of
connect_then_mainServerDown_fallbackServerUp by using FakeClock.
However, it introduced a data race because FakeClock is not thread-safe.
This change injects a single thread for gRPC callbacks such that
syncContext is run on a thread under the test's control.

A simpler approach would be to expose syncContext from XdsClientImpl for
testing. However, this test is in a different package and I wanted to
avoid adding a public method.

```
  Read of size 8 at 0x00008dec9d50 by thread T25:
    #0 io.grpc.internal.FakeClock$ScheduledExecutorImpl.schedule(Lio/grpc/internal/FakeClock$ScheduledTask;JLjava/util/concurrent/TimeUnit;)V FakeClock.java:140
    #1 io.grpc.internal.FakeClock$ScheduledExecutorImpl.schedule(Ljava/lang/Runnable;JLjava/util/concurrent/TimeUnit;)Ljava/util/concurrent/ScheduledFuture; FakeClock.java:150
    #2 io.grpc.SynchronizationContext.schedule(Ljava/lang/Runnable;JLjava/util/concurrent/TimeUnit;Ljava/util/concurrent/ScheduledExecutorService;)Lio/grpc/SynchronizationContext$ScheduledHandle; SynchronizationContext.java:153
    #3 io.grpc.xds.client.ControlPlaneClient$AdsStream.handleRpcStreamClosed(Lio/grpc/Status;)V ControlPlaneClient.java:491
    #4 io.grpc.xds.client.ControlPlaneClient$AdsStream.lambda$onStatusReceived$0(Lio/grpc/Status;)V ControlPlaneClient.java:429
    #5 io.grpc.xds.client.ControlPlaneClient$AdsStream$$Lambda+0x00000001004a95d0.run()V ??
    #6 io.grpc.SynchronizationContext.drain()V SynchronizationContext.java:96
    #7 io.grpc.SynchronizationContext.execute(Ljava/lang/Runnable;)V SynchronizationContext.java:128
    #8 io.grpc.xds.client.ControlPlaneClient$AdsStream.onStatusReceived(Lio/grpc/Status;)V ControlPlaneClient.java:428
    #9 io.grpc.xds.GrpcXdsTransportFactory$EventHandlerToCallListenerAdapter.onClose(Lio/grpc/Status;Lio/grpc/Metadata;)V GrpcXdsTransportFactory.java:149
    #10 io.grpc.PartialForwardingClientCallListener.onClose(Lio/grpc/Status;Lio/grpc/Metadata;)V PartialForwardingClientCallListener.java:39
    ...

  Previous write of size 8 at 0x00008dec9d50 by thread T4 (mutexes: write M0, write M1, write M2, write M3):
    #0 io.grpc.internal.FakeClock.forwardTime(JLjava/util/concurrent/TimeUnit;)I FakeClock.java:368
    #1 io.grpc.xds.XdsClientFallbackTest.connect_then_mainServerDown_fallbackServerUp()V XdsClientFallbackTest.java:358
    ...
```
2025-01-22 16:00:00 -08:00
Eric Anderson fc86084df5 xds: Rename grpc.xds.cluster to grpc.lb.backend_service
The name is being changed to allow the value to be used in more metrics
where xds-specifics are awkward.
2025-01-17 17:16:32 -08:00
Eric Anderson a0a42fc8e6 Update README etc to reference 1.69.1 2025-01-17 13:30:22 -08:00
Eric Anderson f299ef5e58 compiler: Prepare for C++ protobuf using string_view
Protobuf is interested in using absl::string_view instead of const
std::string&. Just copy to std::string as the C++17 build isn't yet
operational and that level of performance doesn't matter.

cl/711732759 b/353571051
2025-01-17 10:44:01 -08:00
MV Shiva b44ebce45d
xds: Envoy proto sync to 2024-11-11 (#11816) 2025-01-17 14:58:52 +05:30
Larry Safran 4d8aff72dc
stub: Eliminate invalid test cases where different threads were calling close from the thread writing. (#11822)
* Eliminate invalid test cases where different threads were calling close from the thread writing.
* Remove multi-thread cancel/write test
2025-01-15 10:43:48 -08:00
Eric Anderson 87c7b7a375 interop-testing: Move soak out of AbstractInteropTest
The soak code grew considerably in 6a92a2a22e. Since it isn't a JUnit
test and doesn't resemble the other tests, it doesn't belong in
AbstractInteropTest. AbstractInteropTest has lots of users, including it
being re-compiled for use on Android, so moving it out makes the
remaining code more clear for the more common cases.
2025-01-14 13:28:10 -08:00
zbilun 87b27b1545
interop-testing: fix peer extraction issue in soak test iterations
This PR resolves an issue with peer address extraction in the soak
test. 

In current `TestServiceClient` implementation, the same
`clientCallCapture` atomic is shared across threads, leading to
incorrect peer extraction. This fix ensures that each thread uses a
local variable for capturing the client call.
2025-01-13 17:57:39 -08:00
Larry Safran 228dcf7a01
core: Alternate ipV4 and ipV6 addresses for Happy Eyeballs in PickFirstLeafLoadBalancer (#11624)
* Interweave ipV4 and ipV6 addresses as per gRFC.
2025-01-13 16:38:16 -08:00
Eric Anderson 7162d2d661 xds: Pass grpc.xds.cluster label to tracer
This is in service to gRFC A89. Since the gRFC isn't finalized this
purposefully doesn't really do anything yet. The grpc-opentelemetry
change to use this optional label will be done after the gRFC is merged.
grpc-opentelemetry currently has a hard-coded list (one entry) of labels
that it looks for, and this label will need to be added.

b/356167676
2025-01-13 11:54:36 -08:00
Larry Safran 176f3eed12
xds: Enable Xds Client Fallback by default (#11817) 2025-01-10 15:25:15 -08:00
Eric Anderson d65d3942e6 xds: Increase speed of fallback test
These changes reduce connect_then_mainServerDown_fallbackServerUp test
time from 20 seconds to 5 s by faking time for the the does-no-exist
timer.

XdsClientImpl only uses the TimeProvider for CSDS cache details, so any
implementation should be fine. FakeXdsClient provides an implementation,
so might as well use it as it is one less clock to think about.
2025-01-10 08:28:48 -08:00
Eric Anderson 82403b9bfa util: Increase modtime in AdvancedTlsX509TrustManagerTest
I noticed an old JDK 8u275 failed on the test because the modification
time's resolution was one second. A newer JDK 8u432 worked fine, so it's
not really a problem for me, but increasing the time difference is
cheap. I used two seconds as that's the resolution available on FAT
(which is unlikely to be TMPDIR, even on Windows).
2025-01-10 08:17:12 -08:00
Eric Anderson 70825adce6 Replace jsr305's GuardedBy with Error Prone's
We should avoid jsr305 and error prone's has the same semantics.
2025-01-10 08:16:48 -08:00
Eric Anderson 7b5d0692cc
Replace jsr305's CheckReturnValue with Error Prone's (#11811)
We should avoid jsr305 and error prone's has the same semantics.

Fixes #8687
2025-01-09 13:45:35 -08:00
Albumen Kevin 73721acc0d
Add UnitTest to verify updateTrustCredentials rotate (#11798)
* Add lastUpdateTime to avoid read
2025-01-09 11:53:36 -08:00
Eric Anderson e61b03cb9f netty: Fix getAttributes() data races in NettyClientTransportTest
Since approximately the LBv2 API (the current API) was introduced, gRPC
won't use a transport until it is ready. Long ago, transports could be
used before they were ready and these old tests were not waiting for the
negotiator to complete before starting. We need them to wait for the
handshake to complete to avoid a test-only data race in getAttributes()
noticed by TSAN.

Throwing away data frames in the Noop handshaker is necessary to act
like a normal handshaker; they don't allow data frames to pass until the
handshake is complete. Without the handling, it goes through invalid
code paths in NettyClientHandler where a terminated transport becomes
ready, and a similar data race.

```
  Write of size 4 at 0x00008db31e2c by thread T37:
    #0 io.grpc.netty.NettyClientHandler.handleProtocolNegotiationCompleted(Lio/grpc/Attributes;Lio/grpc/InternalChannelz$Security;)V NettyClientHandler.java:517
    #1 io.grpc.netty.ProtocolNegotiators$GrpcNegotiationHandler.userEventTriggered(Lio/netty/channel/ChannelHandlerContext;Ljava/lang/Object;)V ProtocolNegotiators.java:937
    #2 io.netty.channel.AbstractChannelHandlerContext.invokeUserEventTriggered(Ljava/lang/Object;)V AbstractChannelHandlerContext.java:398
    #3 io.netty.channel.AbstractChannelHandlerContext.invokeUserEventTriggered(Lio/netty/channel/AbstractChannelHandlerContext;Ljava/lang/Object;)V AbstractChannelHandlerContext.java:376
    #4 io.netty.channel.AbstractChannelHandlerContext.fireUserEventTriggered(Ljava/lang/Object;)Lio/netty/channel/ChannelHandlerContext; AbstractChannelHandlerContext.java:368
    #5 io.grpc.netty.ProtocolNegotiators$ProtocolNegotiationHandler.fireProtocolNegotiationEvent(Lio/netty/channel/ChannelHandlerContext;)V ProtocolNegotiators.java:1107
    #6 io.grpc.netty.ProtocolNegotiators$WaitUntilActiveHandler.channelActive(Lio/netty/channel/ChannelHandlerContext;)V ProtocolNegotiators.java:1011
    ...

  Previous read of size 4 at 0x00008db31e2c by thread T4 (mutexes: write M0, write M1, write M2, write M3):
    #0 io.grpc.netty.NettyClientHandler.getAttributes()Lio/grpc/Attributes; NettyClientHandler.java:345
    #1 io.grpc.netty.NettyClientTransport.getAttributes()Lio/grpc/Attributes; NettyClientTransport.java:387
    #2 io.grpc.netty.NettyClientTransport.newStream(Lio/grpc/MethodDescriptor;Lio/grpc/Metadata;Lio/grpc/CallOptions;[Lio/grpc/ClientStreamTracer;)Lio/grpc/internal/ClientStream; NettyClientTransport.java:198
    #3 io.grpc.netty.NettyClientTransportTest$Rpc.<init>(Lio/grpc/netty/NettyClientTransport;Lio/grpc/Metadata;)V NettyClientTransportTest.java:953
    #4 io.grpc.netty.NettyClientTransportTest.huffmanCodingShouldNotBePerformed()V NettyClientTransportTest.java:631
    ...
```

```
  Read of size 4 at 0x00008f983a3c by thread T4 (mutexes: write M0, write M1):
    #0 io.grpc.netty.NettyClientHandler.getAttributes()Lio/grpc/Attributes; NettyClientHandler.java:345
    #1 io.grpc.netty.NettyClientTransport.getAttributes()Lio/grpc/Attributes; NettyClientTransport.java:387
    #2 io.grpc.netty.NettyClientTransport.newStream(Lio/grpc/MethodDescriptor;Lio/grpc/Metadata;Lio/grpc/CallOptions;[Lio/grpc/ClientStreamTracer;)Lio/grpc/internal/ClientStream; NettyClientTransport.java:198
    #3 io.grpc.netty.NettyClientTransportTest$Rpc.<init>(Lio/grpc/netty/NettyClientTransport;Lio/grpc/Metadata;)V NettyClientTransportTest.java:973
    #4 io.grpc.netty.NettyClientTransportTest$Rpc.<init>(Lio/grpc/netty/NettyClientTransport;)V NettyClientTransportTest.java:969
    #5 io.grpc.netty.NettyClientTransportTest.handlerExceptionDuringNegotiatonPropagatesToStatus()V NettyClientTransportTest.java:425
    ...

  Previous write of size 4 at 0x00008f983a3c by thread T56:
    #0 io.grpc.netty.NettyClientHandler$FrameListener.onSettingsRead(Lio/netty/channel/ChannelHandlerContext;Lio/netty/handler/codec/http2/Http2Settings;)V NettyClientHandler.java:960
    ...
```
2025-01-07 10:25:22 -08:00
MV Shiva ae109727d3
Start 1.71.0 development cycle (#11801) 2025-01-07 14:33:09 +05:30
587 changed files with 21636 additions and 9779 deletions

41
.github/workflows/branch-testing.yml vendored Normal file
View File

@ -0,0 +1,41 @@
name: GitHub Actions Branch Testing
on:
push:
branches:
- master
- 'v1.*'
schedule:
- cron: '54 19 * * SUN' # weekly at a "random" time
permissions:
contents: read
jobs:
arm64:
runs-on: ubuntu-24.04-arm
strategy:
matrix:
jre: [17]
fail-fast: false # Should swap to true if we grow a large matrix
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
java-version: ${{ matrix.jre }}
distribution: 'temurin'
- name: Gradle cache
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Build
run: ./gradlew -Dorg.gradle.parallel=true -Dorg.gradle.jvmargs='-Xmx1g' -PskipAndroid=true -PskipCodegen=true -PerrorProne=false test

View File

@ -102,6 +102,9 @@ jobs:
- name: Run bazel build
run: bazelisk build //... --enable_bzlmod=${{ matrix.bzlmod }}
- name: Run bazel test
run: bazelisk test //... --enable_bzlmod=${{ matrix.bzlmod }}
- name: Run example bazel build
run: bazelisk build //... --enable_bzlmod=${{ matrix.bzlmod }}
working-directory: ./examples

View File

@ -33,7 +33,6 @@ java_library(
"//api",
"//protobuf",
"//stub",
"//stub:javax_annotation",
"@com_google_protobuf//:protobuf_java",
artifact("com.google.code.findbugs:jsr305"),
artifact("com.google.guava:guava"),
@ -47,7 +46,6 @@ java_library(
"//api",
"//protobuf-lite",
"//stub",
"//stub:javax_annotation",
artifact("com.google.code.findbugs:jsr305"),
artifact("com.google.guava:guava"),
],
@ -67,6 +65,5 @@ java_library(
visibility = ["//:__subpackages__"],
exports = [
artifact("com.google.auto.value:auto-value-annotations"),
artifact("org.apache.tomcat:annotations-api"), # @Generated for Java 9+
],
)

View File

@ -44,11 +44,11 @@ This section is only necessary if you are making changes to the code
generation. Most users only need to use `skipCodegen=true` as discussed above.
### Build Protobuf
The codegen plugin is C++ code and requires protobuf 21.7 or later.
The codegen plugin is C++ code and requires protobuf 22.5 or later.
For Linux, Mac and MinGW:
```
$ PROTOBUF_VERSION=21.7
$ PROTOBUF_VERSION=22.5
$ curl -LO https://github.com/protocolbuffers/protobuf/releases/download/v$PROTOBUF_VERSION/protobuf-all-$PROTOBUF_VERSION.tar.gz
$ tar xzf protobuf-all-$PROTOBUF_VERSION.tar.gz
$ cd protobuf-$PROTOBUF_VERSION

View File

@ -11,8 +11,6 @@ for general contribution guidelines.
- [ejona86](https://github.com/ejona86), Google LLC
- [jdcormie](https://github.com/jdcormie), Google LLC
- [kannanjgithub](https://github.com/kannanjgithub), Google LLC
- [larry-safran](https://github.com/larry-safran), Google LLC
- [markb74](https://github.com/markb74), Google LLC
- [ran-su](https://github.com/ran-su), Google LLC
- [sergiitk](https://github.com/sergiitk), Google LLC
- [temawi](https://github.com/temawi), Google LLC
@ -26,7 +24,9 @@ for general contribution guidelines.
- [ericgribkoff](https://github.com/ericgribkoff)
- [jiangtaoli2016](https://github.com/jiangtaoli2016)
- [jtattermusch](https://github.com/jtattermusch)
- [larry-safran](https://github.com/larry-safran)
- [louiscryan](https://github.com/louiscryan)
- [markb74](https://github.com/markb74)
- [nicolasnoble](https://github.com/nicolasnoble)
- [nmittler](https://github.com/nmittler)
- [sanjaypujare](https://github.com/sanjaypujare)

View File

@ -2,13 +2,13 @@ module(
name = "grpc-java",
compatibility_level = 0,
repo_name = "io_grpc_grpc_java",
version = "1.70.0-SNAPSHOT", # CURRENT_GRPC_VERSION
version = "1.76.0-SNAPSHOT", # CURRENT_GRPC_VERSION
)
# GRPC_DEPS_START
IO_GRPC_GRPC_JAVA_ARTIFACTS = [
"com.google.android:annotations:4.1.1.4",
"com.google.api.grpc:proto-google-common-protos:2.48.0",
"com.google.api.grpc:proto-google-common-protos:2.59.2",
"com.google.auth:google-auth-library-credentials:1.24.1",
"com.google.auth:google-auth-library-oauth2-http:1.24.1",
"com.google.auto.value:auto-value-annotations:1.11.0",
@ -18,63 +18,45 @@ IO_GRPC_GRPC_JAVA_ARTIFACTS = [
"com.google.errorprone:error_prone_annotations:2.30.0",
"com.google.guava:failureaccess:1.0.1",
"com.google.guava:guava:33.3.1-android",
"com.google.re2j:re2j:1.7",
"com.google.re2j:re2j:1.8",
"com.google.s2a.proto.v2:s2a-proto:0.1.2",
"com.google.truth:truth:1.4.2",
"com.squareup.okhttp:okhttp:2.7.5",
"com.squareup.okio:okio:2.10.0", # 3.0+ needs swapping to -jvm; need work to avoid flag-day
"io.netty:netty-buffer:4.1.110.Final",
"io.netty:netty-codec-http2:4.1.110.Final",
"io.netty:netty-codec-http:4.1.110.Final",
"io.netty:netty-codec-socks:4.1.110.Final",
"io.netty:netty-codec:4.1.110.Final",
"io.netty:netty-common:4.1.110.Final",
"io.netty:netty-handler-proxy:4.1.110.Final",
"io.netty:netty-handler:4.1.110.Final",
"io.netty:netty-resolver:4.1.110.Final",
"io.netty:netty-tcnative-boringssl-static:2.0.65.Final",
"io.netty:netty-tcnative-classes:2.0.65.Final",
"io.netty:netty-transport-native-epoll:jar:linux-x86_64:4.1.110.Final",
"io.netty:netty-transport-native-unix-common:4.1.110.Final",
"io.netty:netty-transport:4.1.110.Final",
"io.netty:netty-buffer:4.1.124.Final",
"io.netty:netty-codec-http2:4.1.124.Final",
"io.netty:netty-codec-http:4.1.124.Final",
"io.netty:netty-codec-socks:4.1.124.Final",
"io.netty:netty-codec:4.1.124.Final",
"io.netty:netty-common:4.1.124.Final",
"io.netty:netty-handler-proxy:4.1.124.Final",
"io.netty:netty-handler:4.1.124.Final",
"io.netty:netty-resolver:4.1.124.Final",
"io.netty:netty-tcnative-boringssl-static:2.0.70.Final",
"io.netty:netty-tcnative-classes:2.0.70.Final",
"io.netty:netty-transport-native-epoll:jar:linux-x86_64:4.1.124.Final",
"io.netty:netty-transport-native-unix-common:4.1.124.Final",
"io.netty:netty-transport:4.1.124.Final",
"io.opencensus:opencensus-api:0.31.0",
"io.opencensus:opencensus-contrib-grpc-metrics:0.31.0",
"io.perfmark:perfmark-api:0.27.0",
"junit:junit:4.13.2",
"org.apache.tomcat:annotations-api:6.0.53",
"org.checkerframework:checker-qual:3.12.0",
"org.codehaus.mojo:animal-sniffer-annotations:1.24",
]
# GRPC_DEPS_END
bazel_dep(name = "bazel_jar_jar", version = "0.1.7")
bazel_dep(name = "bazel_skylib", version = "1.7.1")
bazel_dep(name = "googleapis", repo_name = "com_google_googleapis", version = "0.0.0-20240326-1c8d509c5")
# CEL Spec may be removed when cncf/xds MODULE is no longer using protobuf 27.x
bazel_dep(name = "cel-spec", repo_name = "dev_cel", version = "0.15.0")
bazel_dep(name = "grpc", repo_name = "com_github_grpc_grpc", version = "1.56.3.bcr.1")
bazel_dep(name = "grpc-proto", repo_name = "io_grpc_grpc_proto", version = "0.0.0-20240627-ec30f58")
bazel_dep(name = "protobuf", repo_name = "com_google_protobuf", version = "23.1")
# Protobuf 25.5+ is incompatible with Bazel 7 with bzlmod
bazel_dep(name = "protobuf", repo_name = "com_google_protobuf", version = "24.4")
bazel_dep(name = "rules_cc", version = "0.0.9")
bazel_dep(name = "rules_java", version = "5.3.5")
bazel_dep(name = "rules_go", repo_name = "io_bazel_rules_go", version = "0.46.0")
bazel_dep(name = "rules_jvm_external", version = "6.0")
bazel_dep(name = "rules_proto", version = "5.3.0-21.7")
non_module_deps = use_extension("//:repositories.bzl", "grpc_java_repositories_extension")
use_repo(
non_module_deps,
"com_github_cncf_xds",
"envoy_api",
)
grpc_repo_deps_ext = use_extension("@com_github_grpc_grpc//bazel:grpc_deps.bzl", "grpc_repo_deps_ext")
use_repo(
grpc_repo_deps_ext,
"com_envoyproxy_protoc_gen_validate",
"opencensus_proto",
)
maven = use_extension("@rules_jvm_external//:extensions.bzl", "maven")
maven.install(
@ -201,7 +183,3 @@ maven.override(
coordinates = "io.grpc:grpc-util",
target = "@io_grpc_grpc_java//util",
)
switched_rules = use_extension("@com_google_googleapis//:extensions.bzl", "switched_rules")
switched_rules.use_languages(java = True)

View File

@ -44,8 +44,8 @@ For a guided tour, take a look at the [quick start
guide](https://grpc.io/docs/languages/java/quickstart) or the more explanatory [gRPC
basics](https://grpc.io/docs/languages/java/basics).
The [examples](https://github.com/grpc/grpc-java/tree/v1.69.0/examples) and the
[Android example](https://github.com/grpc/grpc-java/tree/v1.69.0/examples/android)
The [examples](https://github.com/grpc/grpc-java/tree/v1.75.0/examples) and the
[Android example](https://github.com/grpc/grpc-java/tree/v1.75.0/examples/android)
are standalone projects that showcase the usage of gRPC.
Download
@ -56,18 +56,18 @@ Download [the JARs][]. Or for Maven with non-Android, add to your `pom.xml`:
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-netty-shaded</artifactId>
<version>1.69.0</version>
<version>1.75.0</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-protobuf</artifactId>
<version>1.69.0</version>
<version>1.75.0</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-stub</artifactId>
<version>1.69.0</version>
<version>1.75.0</version>
</dependency>
<dependency> <!-- necessary for Java 9+ -->
<groupId>org.apache.tomcat</groupId>
@ -79,18 +79,18 @@ Download [the JARs][]. Or for Maven with non-Android, add to your `pom.xml`:
Or for Gradle with non-Android, add to your dependencies:
```gradle
runtimeOnly 'io.grpc:grpc-netty-shaded:1.69.0'
implementation 'io.grpc:grpc-protobuf:1.69.0'
implementation 'io.grpc:grpc-stub:1.69.0'
runtimeOnly 'io.grpc:grpc-netty-shaded:1.75.0'
implementation 'io.grpc:grpc-protobuf:1.75.0'
implementation 'io.grpc:grpc-stub:1.75.0'
compileOnly 'org.apache.tomcat:annotations-api:6.0.53' // necessary for Java 9+
```
For Android client, use `grpc-okhttp` instead of `grpc-netty-shaded` and
`grpc-protobuf-lite` instead of `grpc-protobuf`:
```gradle
implementation 'io.grpc:grpc-okhttp:1.69.0'
implementation 'io.grpc:grpc-protobuf-lite:1.69.0'
implementation 'io.grpc:grpc-stub:1.69.0'
implementation 'io.grpc:grpc-okhttp:1.75.0'
implementation 'io.grpc:grpc-protobuf-lite:1.75.0'
implementation 'io.grpc:grpc-stub:1.75.0'
compileOnly 'org.apache.tomcat:annotations-api:6.0.53' // necessary for Java 9+
```
@ -99,10 +99,10 @@ For [Bazel](https://bazel.build), you can either
(with the GAVs from above), or use `@io_grpc_grpc_java//api` et al (see below).
[the JARs]:
https://search.maven.org/search?q=g:io.grpc%20AND%20v:1.69.0
https://search.maven.org/search?q=g:io.grpc%20AND%20v:1.75.0
Development snapshots are available in [Sonatypes's snapshot
repository](https://oss.sonatype.org/content/repositories/snapshots/).
repository](https://central.sonatype.com/repository/maven-snapshots/).
Generated Code
--------------
@ -131,7 +131,7 @@ For protobuf-based codegen integrated with the Maven build system, you can use
<configuration>
<protocArtifact>com.google.protobuf:protoc:3.25.5:exe:${os.detected.classifier}</protocArtifact>
<pluginId>grpc-java</pluginId>
<pluginArtifact>io.grpc:protoc-gen-grpc-java:1.69.0:exe:${os.detected.classifier}</pluginArtifact>
<pluginArtifact>io.grpc:protoc-gen-grpc-java:1.75.0:exe:${os.detected.classifier}</pluginArtifact>
</configuration>
<executions>
<execution>
@ -152,7 +152,7 @@ For non-Android protobuf-based codegen integrated with the Gradle build system,
you can use [protobuf-gradle-plugin][]:
```gradle
plugins {
id 'com.google.protobuf' version '0.9.4'
id 'com.google.protobuf' version '0.9.5'
}
protobuf {
@ -161,7 +161,7 @@ protobuf {
}
plugins {
grpc {
artifact = 'io.grpc:protoc-gen-grpc-java:1.69.0'
artifact = 'io.grpc:protoc-gen-grpc-java:1.75.0'
}
}
generateProtoTasks {
@ -185,7 +185,7 @@ use protobuf-gradle-plugin but specify the 'lite' options:
```gradle
plugins {
id 'com.google.protobuf' version '0.9.4'
id 'com.google.protobuf' version '0.9.5'
}
protobuf {
@ -194,7 +194,7 @@ protobuf {
}
plugins {
grpc {
artifact = 'io.grpc:protoc-gen-grpc-java:1.69.0'
artifact = 'io.grpc:protoc-gen-grpc-java:1.75.0'
}
}
generateProtoTasks {

View File

@ -160,7 +160,7 @@ Tagging the Release
repository can then be `released`, which will begin the process of pushing
the new artifacts to Maven Central (the staging repository will be destroyed
in the process). You can see the complete process for releasing to Maven
Central on the [OSSRH site](https://central.sonatype.org/pages/releasing-the-deployment.html).
Central on the [OSSRH site](https://central.sonatype.org/publish/publish-portal-ossrh-staging-api/#deploying).
10. We have containers for each release to detect compatibility regressions with
old releases. Generate one for the new release by following the [GCR image

View File

@ -399,7 +399,9 @@ grpc-netty version | netty-handler version | netty-tcnative-boringssl-static ver
1.57.x-1.58.x | 4.1.93.Final | 2.0.61.Final
1.59.x | 4.1.97.Final | 2.0.61.Final
1.60.x-1.66.x | 4.1.100.Final | 2.0.61.Final
1.67.x | 4.1.110.Final | 2.0.65.Final
1.67.x-1.70.x | 4.1.110.Final | 2.0.65.Final
1.71.x-1.74.x | 4.1.110.Final | 2.0.70.Final
1.75.x- | 4.1.124.Final | 2.0.72.Final
_(grpc-netty-shaded avoids issues with keeping these versions in sync.)_

View File

@ -22,20 +22,19 @@ load("//:repositories.bzl", "grpc_java_repositories")
grpc_java_repositories()
load("@bazel_jar_jar//:jar_jar.bzl", "jar_jar_repositories")
jar_jar_repositories()
load("@com_google_protobuf//:protobuf_deps.bzl", "PROTOBUF_MAVEN_ARTIFACTS")
load("@com_google_protobuf//:protobuf_deps.bzl", "protobuf_deps")
protobuf_deps()
load("@envoy_api//bazel:repositories.bzl", "api_dependencies")
api_dependencies()
load("@com_google_googleapis//:repository_rules.bzl", "switched_rules_by_language")
switched_rules_by_language(
name = "com_google_googleapis_imports",
java = True,
)
maven_install(

View File

@ -18,6 +18,7 @@ java_library(
"@com_google_protobuf//:protobuf_java",
"@com_google_protobuf//:protobuf_java_util",
artifact("com.google.code.findbugs:jsr305"),
artifact("com.google.errorprone:error_prone_annotations"),
artifact("com.google.guava:guava"),
artifact("io.netty:netty-buffer"),
artifact("io.netty:netty-codec"),

View File

@ -4,9 +4,6 @@ import static io.grpc.MethodDescriptor.generateFullMethodName;
/**
*/
@javax.annotation.Generated(
value = "by gRPC proto compiler",
comments = "Source: grpc/gcp/handshaker.proto")
@io.grpc.stub.annotations.GrpcGenerated
public final class HandshakerServiceGrpc {

View File

@ -17,6 +17,7 @@
package io.grpc.alts;
import io.grpc.Attributes;
import io.grpc.ClientCall;
import io.grpc.ExperimentalApi;
import io.grpc.ServerCall;
import io.grpc.alts.internal.AltsInternalContext;
@ -29,14 +30,36 @@ public final class AltsContextUtil {
private AltsContextUtil() {}
/**
* Creates a {@link AltsContext} from ALTS context information in the {@link ServerCall}.
* Creates an {@link AltsContext} from ALTS context information in the {@link ServerCall}.
*
* @param call the {@link ServerCall} containing the ALTS information
* @return the created {@link AltsContext}
* @throws IllegalArgumentException if the {@link ServerCall} has no ALTS information.
*/
public static AltsContext createFrom(ServerCall<?, ?> call) {
Object authContext = call.getAttributes().get(AltsProtocolNegotiator.AUTH_CONTEXT_KEY);
return createFrom(call.getAttributes());
}
/**
* Creates an {@link AltsContext} from ALTS context information in the {@link ClientCall}.
*
* @param call the {@link ClientCall} containing the ALTS information
* @return the created {@link AltsContext}
* @throws IllegalArgumentException if the {@link ClientCall} has no ALTS information.
*/
public static AltsContext createFrom(ClientCall<?, ?> call) {
return createFrom(call.getAttributes());
}
/**
* Creates an {@link AltsContext} from ALTS context information in the {@link Attributes}.
*
* @param attributes the {@link Attributes} containing the ALTS information
* @return the created {@link AltsContext}
* @throws IllegalArgumentException if the {@link Attributes} has no ALTS information.
*/
public static AltsContext createFrom(Attributes attributes) {
Object authContext = attributes.get(AltsProtocolNegotiator.AUTH_CONTEXT_KEY);
if (!(authContext instanceof AltsInternalContext)) {
throw new IllegalArgumentException("No ALTS context information found");
}
@ -53,6 +76,16 @@ public final class AltsContextUtil {
return check(call.getAttributes());
}
/**
* Checks if the {@link ClientCall} contains ALTS information.
*
* @param call the {@link ClientCall} to check
* @return true, if the {@link ClientCall} contains ALTS information and false otherwise.
*/
public static boolean check(ClientCall<?, ?> call) {
return check(call.getAttributes());
}
/**
* Checks if the {@link Attributes} contains ALTS information.
*

View File

@ -21,6 +21,7 @@ import io.grpc.Channel;
import io.grpc.ClientCall;
import io.grpc.ManagedChannel;
import io.grpc.MethodDescriptor;
import io.grpc.internal.GrpcUtil;
import io.grpc.internal.SharedResourceHolder.Resource;
import io.grpc.netty.NettyChannelBuilder;
import io.netty.channel.EventLoopGroup;
@ -45,6 +46,9 @@ final class HandshakerServiceChannel {
return new ChannelResource(handshakerAddress);
}
private static final boolean EXPERIMENTAL_ALTS_HANDSHAKER_KEEPALIVE_PARAMS =
GrpcUtil.getFlag("GRPC_EXPERIMENTAL_ALTS_HANDSHAKER_KEEPALIVE_PARAMS", false);
private static class ChannelResource implements Resource<Channel> {
private final String target;
@ -57,12 +61,16 @@ final class HandshakerServiceChannel {
/* Use its own event loop thread pool to avoid blocking. */
EventLoopGroup eventGroup =
new NioEventLoopGroup(1, new DefaultThreadFactory("handshaker pool", true));
ManagedChannel channel = NettyChannelBuilder.forTarget(target)
NettyChannelBuilder channelBuilder =
NettyChannelBuilder.forTarget(target)
.channelType(NioSocketChannel.class, InetSocketAddress.class)
.directExecutor()
.eventLoopGroup(eventGroup)
.usePlaintext()
.build();
.usePlaintext();
if (EXPERIMENTAL_ALTS_HANDSHAKER_KEEPALIVE_PARAMS) {
channelBuilder.keepAliveTime(10, TimeUnit.MINUTES).keepAliveTimeout(10, TimeUnit.SECONDS);
}
ManagedChannel channel = channelBuilder.build();
return new EventLoopHoldingChannel(channel, eventGroup);
}

View File

@ -16,12 +16,12 @@
package io.grpc.alts.internal;
import com.google.errorprone.annotations.concurrent.GuardedBy;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
import java.util.LinkedList;
import java.util.Queue;
import javax.annotation.concurrent.GuardedBy;
/** Provides a semaphore primitive, without blocking waiting on permits. */
final class AsyncSemaphore {

View File

@ -24,6 +24,7 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import io.grpc.Attributes;
import io.grpc.ClientCall;
import io.grpc.ServerCall;
import io.grpc.alts.AltsContext.SecurityLevel;
import io.grpc.alts.internal.AltsInternalContext;
@ -37,27 +38,28 @@ import org.junit.runners.JUnit4;
/** Unit tests for {@link AltsContextUtil}. */
@RunWith(JUnit4.class)
public class AltsContextUtilTest {
private final ServerCall<?,?> call = mock(ServerCall.class);
@Test
public void check_noAttributeValue() {
when(call.getAttributes()).thenReturn(Attributes.newBuilder().build());
assertFalse(AltsContextUtil.check(call));
assertFalse(AltsContextUtil.check(Attributes.newBuilder().build()));
}
@Test
public void contains_unexpectedAttributeValueType() {
when(call.getAttributes()).thenReturn(Attributes.newBuilder()
public void check_unexpectedAttributeValueType() {
assertFalse(AltsContextUtil.check(Attributes.newBuilder()
.set(AltsProtocolNegotiator.AUTH_CONTEXT_KEY, new Object())
.build());
assertFalse(AltsContextUtil.check(call));
.build()));
}
@Test
public void contains_altsInternalContext() {
public void check_altsInternalContext() {
assertTrue(AltsContextUtil.check(Attributes.newBuilder()
.set(AltsProtocolNegotiator.AUTH_CONTEXT_KEY, AltsInternalContext.getDefaultInstance())
.build()));
}
@Test
public void checkServer_altsInternalContext() {
ServerCall<?,?> call = mock(ServerCall.class);
when(call.getAttributes()).thenReturn(Attributes.newBuilder()
.set(AltsProtocolNegotiator.AUTH_CONTEXT_KEY, AltsInternalContext.getDefaultInstance())
.build());
@ -66,26 +68,67 @@ public class AltsContextUtilTest {
}
@Test
public void from_altsInternalContext() {
public void checkClient_altsInternalContext() {
ClientCall<?,?> call = mock(ClientCall.class);
when(call.getAttributes()).thenReturn(Attributes.newBuilder()
.set(AltsProtocolNegotiator.AUTH_CONTEXT_KEY, AltsInternalContext.getDefaultInstance())
.build());
assertTrue(AltsContextUtil.check(call));
}
@Test
public void createFrom_altsInternalContext() {
HandshakerResult handshakerResult =
HandshakerResult.newBuilder()
.setPeerIdentity(Identity.newBuilder().setServiceAccount("remote@peer"))
.setLocalIdentity(Identity.newBuilder().setServiceAccount("local@peer"))
.build();
when(call.getAttributes()).thenReturn(Attributes.newBuilder()
.set(AltsProtocolNegotiator.AUTH_CONTEXT_KEY, new AltsInternalContext(handshakerResult))
.build());
AltsContext context = AltsContextUtil.createFrom(call);
AltsContext context = AltsContextUtil.createFrom(Attributes.newBuilder()
.set(AltsProtocolNegotiator.AUTH_CONTEXT_KEY, new AltsInternalContext(handshakerResult))
.build());
assertEquals("remote@peer", context.getPeerServiceAccount());
assertEquals("local@peer", context.getLocalServiceAccount());
assertEquals(SecurityLevel.INTEGRITY_AND_PRIVACY, context.getSecurityLevel());
}
@Test(expected = IllegalArgumentException.class)
public void from_noAttributeValue() {
when(call.getAttributes()).thenReturn(Attributes.newBuilder().build());
public void createFrom_noAttributeValue() {
AltsContextUtil.createFrom(Attributes.newBuilder().build());
}
AltsContextUtil.createFrom(call);
@Test
public void createFromServer_altsInternalContext() {
HandshakerResult handshakerResult =
HandshakerResult.newBuilder()
.setPeerIdentity(Identity.newBuilder().setServiceAccount("remote@peer"))
.setLocalIdentity(Identity.newBuilder().setServiceAccount("local@peer"))
.build();
ServerCall<?,?> call = mock(ServerCall.class);
when(call.getAttributes()).thenReturn(Attributes.newBuilder()
.set(AltsProtocolNegotiator.AUTH_CONTEXT_KEY, new AltsInternalContext(handshakerResult))
.build());
AltsContext context = AltsContextUtil.createFrom(call);
assertEquals("remote@peer", context.getPeerServiceAccount());
}
@Test
public void createFromClient_altsInternalContext() {
HandshakerResult handshakerResult =
HandshakerResult.newBuilder()
.setPeerIdentity(Identity.newBuilder().setServiceAccount("remote@peer"))
.setLocalIdentity(Identity.newBuilder().setServiceAccount("local@peer"))
.build();
ClientCall<?,?> call = mock(ClientCall.class);
when(call.getAttributes()).thenReturn(Attributes.newBuilder()
.set(AltsProtocolNegotiator.AUTH_CONTEXT_KEY, new AltsInternalContext(handshakerResult))
.build());
AltsContext context = AltsContextUtil.createFrom(call);
assertEquals("remote@peer", context.getPeerServiceAccount());
}
}

View File

@ -10,7 +10,7 @@ repositories {
}
android {
namespace 'io.grpc.android.integrationtest'
namespace = 'io.grpc.android.integrationtest'
sourceSets {
main {
java {
@ -41,7 +41,7 @@ android {
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
multiDexEnabled true
multiDexEnabled = true
}
buildTypes {
debug { minifyEnabled false }
@ -73,7 +73,6 @@ dependencies {
project(':grpc-protobuf-lite'),
project(':grpc-stub'),
project(':grpc-testing'),
libraries.hdrhistogram,
libraries.junit,
libraries.truth,
libraries.androidx.test.rules,

View File

@ -7,9 +7,6 @@ import static io.grpc.MethodDescriptor.generateFullMethodName;
* A service used to obtain stats for verifying LB behavior.
* </pre>
*/
@javax.annotation.Generated(
value = "by gRPC proto compiler",
comments = "Source: grpc/testing/test.proto")
@io.grpc.stub.annotations.GrpcGenerated
public final class LoadBalancerStatsServiceGrpc {
@ -245,8 +242,8 @@ public final class LoadBalancerStatsServiceGrpc {
* Gets the backend distribution for RPCs sent by a test client.
* </pre>
*/
public io.grpc.testing.integration.Messages.LoadBalancerStatsResponse getClientStats(io.grpc.testing.integration.Messages.LoadBalancerStatsRequest request) {
return io.grpc.stub.ClientCalls.blockingUnaryCall(
public io.grpc.testing.integration.Messages.LoadBalancerStatsResponse getClientStats(io.grpc.testing.integration.Messages.LoadBalancerStatsRequest request) throws io.grpc.StatusException {
return io.grpc.stub.ClientCalls.blockingV2UnaryCall(
getChannel(), getGetClientStatsMethod(), getCallOptions(), request);
}
@ -255,8 +252,8 @@ public final class LoadBalancerStatsServiceGrpc {
* Gets the accumulated stats for RPCs sent by a test client.
* </pre>
*/
public io.grpc.testing.integration.Messages.LoadBalancerAccumulatedStatsResponse getClientAccumulatedStats(io.grpc.testing.integration.Messages.LoadBalancerAccumulatedStatsRequest request) {
return io.grpc.stub.ClientCalls.blockingUnaryCall(
public io.grpc.testing.integration.Messages.LoadBalancerAccumulatedStatsResponse getClientAccumulatedStats(io.grpc.testing.integration.Messages.LoadBalancerAccumulatedStatsRequest request) throws io.grpc.StatusException {
return io.grpc.stub.ClientCalls.blockingV2UnaryCall(
getChannel(), getGetClientAccumulatedStatsMethod(), getCallOptions(), request);
}
}

View File

@ -4,9 +4,6 @@ import static io.grpc.MethodDescriptor.generateFullMethodName;
/**
*/
@javax.annotation.Generated(
value = "by gRPC proto compiler",
comments = "Source: grpc/testing/metrics.proto")
@io.grpc.stub.annotations.GrpcGenerated
public final class MetricsServiceGrpc {
@ -245,8 +242,8 @@ public final class MetricsServiceGrpc {
* Returns the value of one gauge
* </pre>
*/
public io.grpc.testing.integration.Metrics.GaugeResponse getGauge(io.grpc.testing.integration.Metrics.GaugeRequest request) {
return io.grpc.stub.ClientCalls.blockingUnaryCall(
public io.grpc.testing.integration.Metrics.GaugeResponse getGauge(io.grpc.testing.integration.Metrics.GaugeRequest request) throws io.grpc.StatusException {
return io.grpc.stub.ClientCalls.blockingV2UnaryCall(
getChannel(), getGetGaugeMethod(), getCallOptions(), request);
}
}

View File

@ -7,9 +7,6 @@ import static io.grpc.MethodDescriptor.generateFullMethodName;
* A service used to control reconnect server.
* </pre>
*/
@javax.annotation.Generated(
value = "by gRPC proto compiler",
comments = "Source: grpc/testing/test.proto")
@io.grpc.stub.annotations.GrpcGenerated
public final class ReconnectServiceGrpc {
@ -230,15 +227,15 @@ public final class ReconnectServiceGrpc {
/**
*/
public io.grpc.testing.integration.EmptyProtos.Empty start(io.grpc.testing.integration.Messages.ReconnectParams request) {
return io.grpc.stub.ClientCalls.blockingUnaryCall(
public io.grpc.testing.integration.EmptyProtos.Empty start(io.grpc.testing.integration.Messages.ReconnectParams request) throws io.grpc.StatusException {
return io.grpc.stub.ClientCalls.blockingV2UnaryCall(
getChannel(), getStartMethod(), getCallOptions(), request);
}
/**
*/
public io.grpc.testing.integration.Messages.ReconnectInfo stop(io.grpc.testing.integration.EmptyProtos.Empty request) {
return io.grpc.stub.ClientCalls.blockingUnaryCall(
public io.grpc.testing.integration.Messages.ReconnectInfo stop(io.grpc.testing.integration.EmptyProtos.Empty request) throws io.grpc.StatusException {
return io.grpc.stub.ClientCalls.blockingV2UnaryCall(
getChannel(), getStopMethod(), getCallOptions(), request);
}
}

View File

@ -8,9 +8,6 @@ import static io.grpc.MethodDescriptor.generateFullMethodName;
* performance with various types of payload.
* </pre>
*/
@javax.annotation.Generated(
value = "by gRPC proto compiler",
comments = "Source: grpc/testing/test.proto")
@io.grpc.stub.annotations.GrpcGenerated
public final class TestServiceGrpc {
@ -576,8 +573,8 @@ public final class TestServiceGrpc {
* One empty request followed by one empty response.
* </pre>
*/
public io.grpc.testing.integration.EmptyProtos.Empty emptyCall(io.grpc.testing.integration.EmptyProtos.Empty request) {
return io.grpc.stub.ClientCalls.blockingUnaryCall(
public io.grpc.testing.integration.EmptyProtos.Empty emptyCall(io.grpc.testing.integration.EmptyProtos.Empty request) throws io.grpc.StatusException {
return io.grpc.stub.ClientCalls.blockingV2UnaryCall(
getChannel(), getEmptyCallMethod(), getCallOptions(), request);
}
@ -586,8 +583,8 @@ public final class TestServiceGrpc {
* One request followed by one response.
* </pre>
*/
public io.grpc.testing.integration.Messages.SimpleResponse unaryCall(io.grpc.testing.integration.Messages.SimpleRequest request) {
return io.grpc.stub.ClientCalls.blockingUnaryCall(
public io.grpc.testing.integration.Messages.SimpleResponse unaryCall(io.grpc.testing.integration.Messages.SimpleRequest request) throws io.grpc.StatusException {
return io.grpc.stub.ClientCalls.blockingV2UnaryCall(
getChannel(), getUnaryCallMethod(), getCallOptions(), request);
}
@ -598,8 +595,8 @@ public final class TestServiceGrpc {
* satisfy subsequent requests.
* </pre>
*/
public io.grpc.testing.integration.Messages.SimpleResponse cacheableUnaryCall(io.grpc.testing.integration.Messages.SimpleRequest request) {
return io.grpc.stub.ClientCalls.blockingUnaryCall(
public io.grpc.testing.integration.Messages.SimpleResponse cacheableUnaryCall(io.grpc.testing.integration.Messages.SimpleRequest request) throws io.grpc.StatusException {
return io.grpc.stub.ClientCalls.blockingV2UnaryCall(
getChannel(), getCacheableUnaryCallMethod(), getCallOptions(), request);
}
@ -664,8 +661,8 @@ public final class TestServiceGrpc {
* to test the behavior when clients call unimplemented methods.
* </pre>
*/
public io.grpc.testing.integration.EmptyProtos.Empty unimplementedCall(io.grpc.testing.integration.EmptyProtos.Empty request) {
return io.grpc.stub.ClientCalls.blockingUnaryCall(
public io.grpc.testing.integration.EmptyProtos.Empty unimplementedCall(io.grpc.testing.integration.EmptyProtos.Empty request) throws io.grpc.StatusException {
return io.grpc.stub.ClientCalls.blockingV2UnaryCall(
getChannel(), getUnimplementedCallMethod(), getCallOptions(), request);
}
}

View File

@ -8,9 +8,6 @@ import static io.grpc.MethodDescriptor.generateFullMethodName;
* that case.
* </pre>
*/
@javax.annotation.Generated(
value = "by gRPC proto compiler",
comments = "Source: grpc/testing/test.proto")
@io.grpc.stub.annotations.GrpcGenerated
public final class UnimplementedServiceGrpc {
@ -199,8 +196,8 @@ public final class UnimplementedServiceGrpc {
* A call that no server should implement
* </pre>
*/
public io.grpc.testing.integration.EmptyProtos.Empty unimplementedCall(io.grpc.testing.integration.EmptyProtos.Empty request) {
return io.grpc.stub.ClientCalls.blockingUnaryCall(
public io.grpc.testing.integration.EmptyProtos.Empty unimplementedCall(io.grpc.testing.integration.EmptyProtos.Empty request) throws io.grpc.StatusException {
return io.grpc.stub.ClientCalls.blockingV2UnaryCall(
getChannel(), getUnimplementedCallMethod(), getCallOptions(), request);
}
}

View File

@ -7,9 +7,6 @@ import static io.grpc.MethodDescriptor.generateFullMethodName;
* A service to dynamically update the configuration of an xDS test client.
* </pre>
*/
@javax.annotation.Generated(
value = "by gRPC proto compiler",
comments = "Source: grpc/testing/test.proto")
@io.grpc.stub.annotations.GrpcGenerated
public final class XdsUpdateClientConfigureServiceGrpc {
@ -194,8 +191,8 @@ public final class XdsUpdateClientConfigureServiceGrpc {
* Update the tes client's configuration.
* </pre>
*/
public io.grpc.testing.integration.Messages.ClientConfigureResponse configure(io.grpc.testing.integration.Messages.ClientConfigureRequest request) {
return io.grpc.stub.ClientCalls.blockingUnaryCall(
public io.grpc.testing.integration.Messages.ClientConfigureResponse configure(io.grpc.testing.integration.Messages.ClientConfigureRequest request) throws io.grpc.StatusException {
return io.grpc.stub.ClientCalls.blockingV2UnaryCall(
getChannel(), getConfigureMethod(), getCallOptions(), request);
}
}

View File

@ -7,9 +7,6 @@ import static io.grpc.MethodDescriptor.generateFullMethodName;
* A service to remotely control health status of an xDS test server.
* </pre>
*/
@javax.annotation.Generated(
value = "by gRPC proto compiler",
comments = "Source: grpc/testing/test.proto")
@io.grpc.stub.annotations.GrpcGenerated
public final class XdsUpdateHealthServiceGrpc {
@ -230,15 +227,15 @@ public final class XdsUpdateHealthServiceGrpc {
/**
*/
public io.grpc.testing.integration.EmptyProtos.Empty setServing(io.grpc.testing.integration.EmptyProtos.Empty request) {
return io.grpc.stub.ClientCalls.blockingUnaryCall(
public io.grpc.testing.integration.EmptyProtos.Empty setServing(io.grpc.testing.integration.EmptyProtos.Empty request) throws io.grpc.StatusException {
return io.grpc.stub.ClientCalls.blockingV2UnaryCall(
getChannel(), getSetServingMethod(), getCallOptions(), request);
}
/**
*/
public io.grpc.testing.integration.EmptyProtos.Empty setNotServing(io.grpc.testing.integration.EmptyProtos.Empty request) {
return io.grpc.stub.ClientCalls.blockingUnaryCall(
public io.grpc.testing.integration.EmptyProtos.Empty setNotServing(io.grpc.testing.integration.EmptyProtos.Empty request) throws io.grpc.StatusException {
return io.grpc.stub.ClientCalls.blockingV2UnaryCall(
getChannel(), getSetNotServingMethod(), getCallOptions(), request);
}
}

View File

@ -7,9 +7,6 @@ import static io.grpc.MethodDescriptor.generateFullMethodName;
* A service used to obtain stats for verifying LB behavior.
* </pre>
*/
@javax.annotation.Generated(
value = "by gRPC proto compiler",
comments = "Source: grpc/testing/test.proto")
@io.grpc.stub.annotations.GrpcGenerated
public final class LoadBalancerStatsServiceGrpc {
@ -245,8 +242,8 @@ public final class LoadBalancerStatsServiceGrpc {
* Gets the backend distribution for RPCs sent by a test client.
* </pre>
*/
public io.grpc.testing.integration.Messages.LoadBalancerStatsResponse getClientStats(io.grpc.testing.integration.Messages.LoadBalancerStatsRequest request) {
return io.grpc.stub.ClientCalls.blockingUnaryCall(
public io.grpc.testing.integration.Messages.LoadBalancerStatsResponse getClientStats(io.grpc.testing.integration.Messages.LoadBalancerStatsRequest request) throws io.grpc.StatusException {
return io.grpc.stub.ClientCalls.blockingV2UnaryCall(
getChannel(), getGetClientStatsMethod(), getCallOptions(), request);
}
@ -255,8 +252,8 @@ public final class LoadBalancerStatsServiceGrpc {
* Gets the accumulated stats for RPCs sent by a test client.
* </pre>
*/
public io.grpc.testing.integration.Messages.LoadBalancerAccumulatedStatsResponse getClientAccumulatedStats(io.grpc.testing.integration.Messages.LoadBalancerAccumulatedStatsRequest request) {
return io.grpc.stub.ClientCalls.blockingUnaryCall(
public io.grpc.testing.integration.Messages.LoadBalancerAccumulatedStatsResponse getClientAccumulatedStats(io.grpc.testing.integration.Messages.LoadBalancerAccumulatedStatsRequest request) throws io.grpc.StatusException {
return io.grpc.stub.ClientCalls.blockingV2UnaryCall(
getChannel(), getGetClientAccumulatedStatsMethod(), getCallOptions(), request);
}
}

View File

@ -4,9 +4,6 @@ import static io.grpc.MethodDescriptor.generateFullMethodName;
/**
*/
@javax.annotation.Generated(
value = "by gRPC proto compiler",
comments = "Source: grpc/testing/metrics.proto")
@io.grpc.stub.annotations.GrpcGenerated
public final class MetricsServiceGrpc {
@ -245,8 +242,8 @@ public final class MetricsServiceGrpc {
* Returns the value of one gauge
* </pre>
*/
public io.grpc.testing.integration.Metrics.GaugeResponse getGauge(io.grpc.testing.integration.Metrics.GaugeRequest request) {
return io.grpc.stub.ClientCalls.blockingUnaryCall(
public io.grpc.testing.integration.Metrics.GaugeResponse getGauge(io.grpc.testing.integration.Metrics.GaugeRequest request) throws io.grpc.StatusException {
return io.grpc.stub.ClientCalls.blockingV2UnaryCall(
getChannel(), getGetGaugeMethod(), getCallOptions(), request);
}
}

View File

@ -7,9 +7,6 @@ import static io.grpc.MethodDescriptor.generateFullMethodName;
* A service used to control reconnect server.
* </pre>
*/
@javax.annotation.Generated(
value = "by gRPC proto compiler",
comments = "Source: grpc/testing/test.proto")
@io.grpc.stub.annotations.GrpcGenerated
public final class ReconnectServiceGrpc {
@ -230,15 +227,15 @@ public final class ReconnectServiceGrpc {
/**
*/
public io.grpc.testing.integration.EmptyProtos.Empty start(io.grpc.testing.integration.Messages.ReconnectParams request) {
return io.grpc.stub.ClientCalls.blockingUnaryCall(
public io.grpc.testing.integration.EmptyProtos.Empty start(io.grpc.testing.integration.Messages.ReconnectParams request) throws io.grpc.StatusException {
return io.grpc.stub.ClientCalls.blockingV2UnaryCall(
getChannel(), getStartMethod(), getCallOptions(), request);
}
/**
*/
public io.grpc.testing.integration.Messages.ReconnectInfo stop(io.grpc.testing.integration.EmptyProtos.Empty request) {
return io.grpc.stub.ClientCalls.blockingUnaryCall(
public io.grpc.testing.integration.Messages.ReconnectInfo stop(io.grpc.testing.integration.EmptyProtos.Empty request) throws io.grpc.StatusException {
return io.grpc.stub.ClientCalls.blockingV2UnaryCall(
getChannel(), getStopMethod(), getCallOptions(), request);
}
}

View File

@ -8,9 +8,6 @@ import static io.grpc.MethodDescriptor.generateFullMethodName;
* performance with various types of payload.
* </pre>
*/
@javax.annotation.Generated(
value = "by gRPC proto compiler",
comments = "Source: grpc/testing/test.proto")
@io.grpc.stub.annotations.GrpcGenerated
public final class TestServiceGrpc {
@ -576,8 +573,8 @@ public final class TestServiceGrpc {
* One empty request followed by one empty response.
* </pre>
*/
public io.grpc.testing.integration.EmptyProtos.Empty emptyCall(io.grpc.testing.integration.EmptyProtos.Empty request) {
return io.grpc.stub.ClientCalls.blockingUnaryCall(
public io.grpc.testing.integration.EmptyProtos.Empty emptyCall(io.grpc.testing.integration.EmptyProtos.Empty request) throws io.grpc.StatusException {
return io.grpc.stub.ClientCalls.blockingV2UnaryCall(
getChannel(), getEmptyCallMethod(), getCallOptions(), request);
}
@ -586,8 +583,8 @@ public final class TestServiceGrpc {
* One request followed by one response.
* </pre>
*/
public io.grpc.testing.integration.Messages.SimpleResponse unaryCall(io.grpc.testing.integration.Messages.SimpleRequest request) {
return io.grpc.stub.ClientCalls.blockingUnaryCall(
public io.grpc.testing.integration.Messages.SimpleResponse unaryCall(io.grpc.testing.integration.Messages.SimpleRequest request) throws io.grpc.StatusException {
return io.grpc.stub.ClientCalls.blockingV2UnaryCall(
getChannel(), getUnaryCallMethod(), getCallOptions(), request);
}
@ -598,8 +595,8 @@ public final class TestServiceGrpc {
* satisfy subsequent requests.
* </pre>
*/
public io.grpc.testing.integration.Messages.SimpleResponse cacheableUnaryCall(io.grpc.testing.integration.Messages.SimpleRequest request) {
return io.grpc.stub.ClientCalls.blockingUnaryCall(
public io.grpc.testing.integration.Messages.SimpleResponse cacheableUnaryCall(io.grpc.testing.integration.Messages.SimpleRequest request) throws io.grpc.StatusException {
return io.grpc.stub.ClientCalls.blockingV2UnaryCall(
getChannel(), getCacheableUnaryCallMethod(), getCallOptions(), request);
}
@ -664,8 +661,8 @@ public final class TestServiceGrpc {
* to test the behavior when clients call unimplemented methods.
* </pre>
*/
public io.grpc.testing.integration.EmptyProtos.Empty unimplementedCall(io.grpc.testing.integration.EmptyProtos.Empty request) {
return io.grpc.stub.ClientCalls.blockingUnaryCall(
public io.grpc.testing.integration.EmptyProtos.Empty unimplementedCall(io.grpc.testing.integration.EmptyProtos.Empty request) throws io.grpc.StatusException {
return io.grpc.stub.ClientCalls.blockingV2UnaryCall(
getChannel(), getUnimplementedCallMethod(), getCallOptions(), request);
}
}

View File

@ -8,9 +8,6 @@ import static io.grpc.MethodDescriptor.generateFullMethodName;
* that case.
* </pre>
*/
@javax.annotation.Generated(
value = "by gRPC proto compiler",
comments = "Source: grpc/testing/test.proto")
@io.grpc.stub.annotations.GrpcGenerated
public final class UnimplementedServiceGrpc {
@ -199,8 +196,8 @@ public final class UnimplementedServiceGrpc {
* A call that no server should implement
* </pre>
*/
public io.grpc.testing.integration.EmptyProtos.Empty unimplementedCall(io.grpc.testing.integration.EmptyProtos.Empty request) {
return io.grpc.stub.ClientCalls.blockingUnaryCall(
public io.grpc.testing.integration.EmptyProtos.Empty unimplementedCall(io.grpc.testing.integration.EmptyProtos.Empty request) throws io.grpc.StatusException {
return io.grpc.stub.ClientCalls.blockingV2UnaryCall(
getChannel(), getUnimplementedCallMethod(), getCallOptions(), request);
}
}

View File

@ -7,9 +7,6 @@ import static io.grpc.MethodDescriptor.generateFullMethodName;
* A service to dynamically update the configuration of an xDS test client.
* </pre>
*/
@javax.annotation.Generated(
value = "by gRPC proto compiler",
comments = "Source: grpc/testing/test.proto")
@io.grpc.stub.annotations.GrpcGenerated
public final class XdsUpdateClientConfigureServiceGrpc {
@ -194,8 +191,8 @@ public final class XdsUpdateClientConfigureServiceGrpc {
* Update the tes client's configuration.
* </pre>
*/
public io.grpc.testing.integration.Messages.ClientConfigureResponse configure(io.grpc.testing.integration.Messages.ClientConfigureRequest request) {
return io.grpc.stub.ClientCalls.blockingUnaryCall(
public io.grpc.testing.integration.Messages.ClientConfigureResponse configure(io.grpc.testing.integration.Messages.ClientConfigureRequest request) throws io.grpc.StatusException {
return io.grpc.stub.ClientCalls.blockingV2UnaryCall(
getChannel(), getConfigureMethod(), getCallOptions(), request);
}
}

View File

@ -7,9 +7,6 @@ import static io.grpc.MethodDescriptor.generateFullMethodName;
* A service to remotely control health status of an xDS test server.
* </pre>
*/
@javax.annotation.Generated(
value = "by gRPC proto compiler",
comments = "Source: grpc/testing/test.proto")
@io.grpc.stub.annotations.GrpcGenerated
public final class XdsUpdateHealthServiceGrpc {
@ -230,15 +227,15 @@ public final class XdsUpdateHealthServiceGrpc {
/**
*/
public io.grpc.testing.integration.EmptyProtos.Empty setServing(io.grpc.testing.integration.EmptyProtos.Empty request) {
return io.grpc.stub.ClientCalls.blockingUnaryCall(
public io.grpc.testing.integration.EmptyProtos.Empty setServing(io.grpc.testing.integration.EmptyProtos.Empty request) throws io.grpc.StatusException {
return io.grpc.stub.ClientCalls.blockingV2UnaryCall(
getChannel(), getSetServingMethod(), getCallOptions(), request);
}
/**
*/
public io.grpc.testing.integration.EmptyProtos.Empty setNotServing(io.grpc.testing.integration.EmptyProtos.Empty request) {
return io.grpc.stub.ClientCalls.blockingUnaryCall(
public io.grpc.testing.integration.EmptyProtos.Empty setNotServing(io.grpc.testing.integration.EmptyProtos.Empty request) throws io.grpc.StatusException {
return io.grpc.stub.ClientCalls.blockingV2UnaryCall(
getChannel(), getSetNotServingMethod(), getCallOptions(), request);
}
}

View File

@ -7,20 +7,20 @@ plugins {
description = 'gRPC: Android'
android {
namespace 'io.grpc.android'
namespace = 'io.grpc.android'
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
compileSdkVersion 34
defaultConfig {
minSdkVersion 21
minSdkVersion 22
targetSdkVersion 33
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
lintOptions { abortOnError true }
lintOptions { abortOnError = true }
publishing {
singleVariant('release') {
withSourcesJar()

View File

@ -28,6 +28,7 @@ import android.util.Log;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.errorprone.annotations.InlineMe;
import com.google.errorprone.annotations.concurrent.GuardedBy;
import io.grpc.CallOptions;
import io.grpc.ClientCall;
import io.grpc.ConnectivityState;
@ -41,7 +42,6 @@ import io.grpc.MethodDescriptor;
import io.grpc.internal.GrpcUtil;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
/**
* Builds a {@link ManagedChannel} that, when provided with a {@link Context}, will automatically
@ -217,7 +217,6 @@ public final class AndroidChannelBuilder extends ForwardingChannelBuilder<Androi
connectivityManager.registerDefaultNetworkCallback(defaultNetworkCallback);
unregisterRunnable =
new Runnable() {
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public void run() {
connectivityManager.unregisterNetworkCallback(defaultNetworkCallback);
@ -231,7 +230,6 @@ public final class AndroidChannelBuilder extends ForwardingChannelBuilder<Androi
context.registerReceiver(networkReceiver, networkIntentFilter);
unregisterRunnable =
new Runnable() {
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public void run() {
context.unregisterReceiver(networkReceiver);

View File

@ -47,6 +47,7 @@ dependencies {
testImplementation project(':grpc-core')
testImplementation project(':grpc-testing')
testImplementation libraries.guava.testlib
testImplementation libraries.truth
signature (libraries.signature.java) {
artifact {

View File

@ -16,8 +16,10 @@
package io.grpc;
import java.util.Arrays;
import static java.util.Objects.requireNonNull;
import java.util.Locale;
import java.util.Objects;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
@ -33,7 +35,7 @@ import java.util.concurrent.TimeUnit;
* passed to the various components unambiguously.
*/
public final class Deadline implements Comparable<Deadline> {
private static final SystemTicker SYSTEM_TICKER = new SystemTicker();
private static final Ticker SYSTEM_TICKER = new SystemTicker();
// nanoTime has a range of just under 300 years. Only allow up to 100 years in the past or future
// to prevent wraparound as long as process runs for less than ~100 years.
private static final long MAX_OFFSET = TimeUnit.DAYS.toNanos(100 * 365);
@ -91,7 +93,7 @@ public final class Deadline implements Comparable<Deadline> {
* @since 1.24.0
*/
public static Deadline after(long duration, TimeUnit units, Ticker ticker) {
checkNotNull(units, "units");
requireNonNull(units, "units");
return new Deadline(ticker, units.toNanos(duration), true);
}
@ -191,8 +193,8 @@ public final class Deadline implements Comparable<Deadline> {
* @return {@link ScheduledFuture} which can be used to cancel execution of the task
*/
public ScheduledFuture<?> runOnExpiration(Runnable task, ScheduledExecutorService scheduler) {
checkNotNull(task, "task");
checkNotNull(scheduler, "scheduler");
requireNonNull(task, "task");
requireNonNull(scheduler, "scheduler");
return scheduler.schedule(task, deadlineNanos - ticker.nanoTime(), TimeUnit.NANOSECONDS);
}
@ -225,37 +227,27 @@ public final class Deadline implements Comparable<Deadline> {
@Override
public int compareTo(Deadline that) {
checkTicker(that);
long diff = this.deadlineNanos - that.deadlineNanos;
if (diff < 0) {
return -1;
} else if (diff > 0) {
return 1;
}
return 0;
return Long.compare(this.deadlineNanos, that.deadlineNanos);
}
@Override
public int hashCode() {
return Arrays.asList(this.ticker, this.deadlineNanos).hashCode();
return Objects.hash(this.ticker, this.deadlineNanos);
}
@Override
public boolean equals(final Object o) {
if (o == this) {
public boolean equals(final Object object) {
if (object == this) {
return true;
}
if (!(o instanceof Deadline)) {
if (!(object instanceof Deadline)) {
return false;
}
final Deadline other = (Deadline) o;
if (this.ticker == null ? other.ticker != null : this.ticker != other.ticker) {
final Deadline that = (Deadline) object;
if (this.ticker == null ? that.ticker != null : this.ticker != that.ticker) {
return false;
}
if (this.deadlineNanos != other.deadlineNanos) {
return false;
}
return true;
return this.deadlineNanos == that.deadlineNanos;
}
/**
@ -275,24 +267,17 @@ public final class Deadline implements Comparable<Deadline> {
* @since 1.24.0
*/
public abstract static class Ticker {
/** Returns the number of nanoseconds since this source's epoch. */
/** Returns the number of nanoseconds elapsed since this ticker's reference point in time. */
public abstract long nanoTime();
}
private static class SystemTicker extends Ticker {
private static final class SystemTicker extends Ticker {
@Override
public long nanoTime() {
return System.nanoTime();
}
}
private static <T> T checkNotNull(T reference, Object errorMessage) {
if (reference == null) {
throw new NullPointerException(String.valueOf(errorMessage));
}
return reference;
}
private void checkTicker(Deadline other) {
if (ticker != other.ticker) {
throw new AssertionError(

View File

@ -21,6 +21,7 @@ import static io.grpc.TimeUtils.convertToNanos;
import com.google.common.base.MoreObjects;
import com.google.common.base.Preconditions;
import com.google.errorprone.annotations.CheckReturnValue;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
@ -28,7 +29,6 @@ import java.util.Collections;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import javax.annotation.CheckReturnValue;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;

View File

@ -16,10 +16,10 @@
package io.grpc;
import com.google.errorprone.annotations.concurrent.GuardedBy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.annotation.concurrent.GuardedBy;
/**
* A registry for {@link Configurator} instances.
@ -33,9 +33,9 @@ final class ConfiguratorRegistry {
@GuardedBy("this")
private boolean wasConfiguratorsSet;
@GuardedBy("this")
private boolean configFrozen;
@GuardedBy("this")
private List<Configurator> configurators = Collections.emptyList();
@GuardedBy("this")
private int configuratorsCallCountBeforeSet = 0;
ConfiguratorRegistry() {}
@ -56,11 +56,10 @@ final class ConfiguratorRegistry {
* @throws IllegalStateException if this method is called more than once
*/
public synchronized void setConfigurators(List<? extends Configurator> configurators) {
if (configFrozen) {
if (wasConfiguratorsSet) {
throw new IllegalStateException("Configurators are already set");
}
this.configurators = Collections.unmodifiableList(new ArrayList<>(configurators));
configFrozen = true;
wasConfiguratorsSet = true;
}
@ -68,10 +67,20 @@ final class ConfiguratorRegistry {
* Returns a list of the configurators in this registry.
*/
public synchronized List<Configurator> getConfigurators() {
configFrozen = true;
if (!wasConfiguratorsSet) {
configuratorsCallCountBeforeSet++;
}
return configurators;
}
/**
* Returns the number of times getConfigurators() was called before
* setConfigurators() was successfully invoked.
*/
public synchronized int getConfiguratorsCallCountBeforeSet() {
return configuratorsCallCountBeforeSet;
}
public synchronized boolean wasSetConfiguratorsCalled() {
return wasConfiguratorsSet;
}

View File

@ -35,7 +35,7 @@ public abstract class InternalConfigSelector {
= Attributes.Key.create("internal:io.grpc.config-selector");
// Use PickSubchannelArgs for SelectConfigArgs for now. May change over time.
/** Selects the config for an PRC. */
/** Selects the config for an RPC. */
public abstract Result selectConfig(LoadBalancer.PickSubchannelArgs args);
public static final class Result {

View File

@ -48,4 +48,8 @@ public final class InternalConfiguratorRegistry {
public static boolean wasSetConfiguratorsCalled() {
return ConfiguratorRegistry.getDefaultRegistry().wasSetConfiguratorsCalled();
}
public static int getConfiguratorsCallCountBeforeSet() {
return ConfiguratorRegistry.getDefaultRegistry().getConfiguratorsCallCountBeforeSet();
}
}

View File

@ -452,18 +452,6 @@ public abstract class LoadBalancer {
* @since 1.3.0
*/
public abstract PickResult pickSubchannel(PickSubchannelArgs args);
/**
* Tries to establish connections now so that the upcoming RPC may then just pick a ready
* connection without having to connect first.
*
* <p>No-op if unsupported.
*
* @deprecated override {@link LoadBalancer#requestConnection} instead.
* @since 1.11.0
*/
@Deprecated
public void requestConnection() {}
}
/**
@ -1201,6 +1189,10 @@ public abstract class LoadBalancer {
* Returns a {@link SynchronizationContext} that runs tasks in the same Synchronization Context
* as that the callback methods on the {@link LoadBalancer} interface are run in.
*
* <p>Work added to the synchronization context might not run immediately, so LB implementations
* must be careful to ensure that any assumptions still hold when it is executed. In particular,
* the LB might have been shut down or subchannels might have changed state.
*
* <p>Pro-tip: in order to call {@link SynchronizationContext#schedule}, you need to provide a
* {@link ScheduledExecutorService}. {@link #getScheduledExecutorService} is provided for your
* convenience.

View File

@ -18,6 +18,7 @@ package io.grpc;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.errorprone.annotations.concurrent.GuardedBy;
import java.net.SocketAddress;
import java.net.URI;
import java.net.URISyntaxException;
@ -30,7 +31,6 @@ import java.util.LinkedHashSet;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.ThreadSafe;
/**

View File

@ -22,6 +22,8 @@ import static java.nio.charset.StandardCharsets.US_ASCII;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.io.BaseEncoding;
import com.google.common.io.ByteStreams;
import java.io.ByteArrayInputStream;
@ -32,8 +34,6 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.BitSet;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
@ -325,7 +325,7 @@ public final class Metadata {
if (isEmpty()) {
return Collections.emptySet();
}
Set<String> ks = new HashSet<>(size);
Set<String> ks = Sets.newHashSetWithExpectedSize(size);
for (int i = 0; i < size; i++) {
ks.add(new String(name(i), 0 /* hibyte */));
}
@ -526,7 +526,7 @@ public final class Metadata {
public void merge(Metadata other, Set<Key<?>> keys) {
Preconditions.checkNotNull(other, "other");
// Use ByteBuffer for equals and hashCode.
Map<ByteBuffer, Key<?>> asciiKeys = new HashMap<>(keys.size());
Map<ByteBuffer, Key<?>> asciiKeys = Maps.newHashMapWithExpectedSize(keys.size());
for (Key<?> key : keys) {
asciiKeys.put(ByteBuffer.wrap(key.asciiName()), key);
}

View File

@ -20,9 +20,9 @@ import static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.base.MoreObjects;
import com.google.common.base.Preconditions;
import com.google.errorprone.annotations.CheckReturnValue;
import java.io.InputStream;
import java.util.concurrent.atomic.AtomicReferenceArray;
import javax.annotation.CheckReturnValue;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;

View File

@ -21,12 +21,12 @@ import static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import com.google.errorprone.annotations.concurrent.GuardedBy;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.annotation.concurrent.GuardedBy;
/**
* A registry for globally registered metric instruments.

View File

@ -239,6 +239,9 @@ public abstract class NameResolver {
* {@link ResolutionResult#getAddressesOrError()} is empty, {@link #onError(Status)} will be
* called.
*
* <p>Newer NameResolver implementations should prefer calling onResult2. This method exists to
* facilitate older {@link Listener} implementations to migrate to {@link Listener2}.
*
* @param resolutionResult the resolved server addresses, attributes, and Service Config.
* @since 1.21.0
*/
@ -248,6 +251,10 @@ public abstract class NameResolver {
* Handles a name resolving error from the resolver. The listener is responsible for eventually
* invoking {@link NameResolver#refresh()} to re-attempt resolution.
*
* <p>New NameResolver implementations should prefer calling onResult2 which will have the
* address resolution error in {@link ResolutionResult}'s addressesOrError. This method exists
* to facilitate older implementations using {@link Listener} to migrate to {@link Listener2}.
*
* @param error a non-OK status
* @since 1.21.0
*/
@ -255,9 +262,14 @@ public abstract class NameResolver {
public abstract void onError(Status error);
/**
* Handles updates on resolved addresses and attributes.
* Handles updates on resolved addresses and attributes. Must be called from the same
* {@link SynchronizationContext} available in {@link NameResolver.Args} that is passed
* from the channel.
*
* @param resolutionResult the resolved server addresses, attributes, and Service Config.
* @param resolutionResult the resolved server addresses or error in address resolution,
* attributes, and Service Config or error
* @return status indicating whether the resolutionResult was accepted by the listener,
* typically the result from a load balancer.
* @since 1.66
*/
public Status onResult2(ResolutionResult resolutionResult) {
@ -275,6 +287,11 @@ public abstract class NameResolver {
@Documented
public @interface ResolutionResultAttr {}
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/11989")
@ResolutionResultAttr
public static final Attributes.Key<String> ATTR_BACKEND_SERVICE =
Attributes.Key.create("io.grpc.NameResolver.ATTR_BACKEND_SERVICE");
/**
* Information that a {@link Factory} uses to create a {@link NameResolver}.
*
@ -298,6 +315,7 @@ public abstract class NameResolver {
@Nullable private final Executor executor;
@Nullable private final String overrideAuthority;
@Nullable private final MetricRecorder metricRecorder;
@Nullable private final NameResolverRegistry nameResolverRegistry;
@Nullable private final IdentityHashMap<Key<?>, Object> customArgs;
private Args(Builder builder) {
@ -311,6 +329,7 @@ public abstract class NameResolver {
this.executor = builder.executor;
this.overrideAuthority = builder.overrideAuthority;
this.metricRecorder = builder.metricRecorder;
this.nameResolverRegistry = builder.nameResolverRegistry;
this.customArgs = cloneCustomArgs(builder.customArgs);
}
@ -442,6 +461,18 @@ public abstract class NameResolver {
return metricRecorder;
}
/**
* Returns the {@link NameResolverRegistry} that the Channel uses to look for {@link
* NameResolver}s.
*
* @since 1.74.0
*/
public NameResolverRegistry getNameResolverRegistry() {
if (nameResolverRegistry == null) {
throw new IllegalStateException("NameResolverRegistry is not set in Builder");
}
return nameResolverRegistry;
}
@Override
public String toString() {
@ -456,6 +487,7 @@ public abstract class NameResolver {
.add("executor", executor)
.add("overrideAuthority", overrideAuthority)
.add("metricRecorder", metricRecorder)
.add("nameResolverRegistry", nameResolverRegistry)
.toString();
}
@ -475,6 +507,7 @@ public abstract class NameResolver {
builder.setOffloadExecutor(executor);
builder.setOverrideAuthority(overrideAuthority);
builder.setMetricRecorder(metricRecorder);
builder.setNameResolverRegistry(nameResolverRegistry);
builder.customArgs = cloneCustomArgs(customArgs);
return builder;
}
@ -503,6 +536,7 @@ public abstract class NameResolver {
private Executor executor;
private String overrideAuthority;
private MetricRecorder metricRecorder;
private NameResolverRegistry nameResolverRegistry;
private IdentityHashMap<Key<?>, Object> customArgs;
Builder() {
@ -609,6 +643,16 @@ public abstract class NameResolver {
return this;
}
/**
* See {@link Args#getNameResolverRegistry}. This is an optional field.
*
* @since 1.74.0
*/
public Builder setNameResolverRegistry(NameResolverRegistry registry) {
this.nameResolverRegistry = registry;
return this;
}
/**
* Builds an {@link Args}.
*

View File

@ -20,6 +20,7 @@ import static com.google.common.base.Preconditions.checkArgument;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableMap;
import com.google.errorprone.annotations.concurrent.GuardedBy;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
@ -31,7 +32,6 @@ import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.ThreadSafe;
/**
@ -166,6 +166,11 @@ public final class NameResolverRegistry {
} catch (ClassNotFoundException e) {
logger.log(Level.FINE, "Unable to find DNS NameResolver", e);
}
try {
list.add(Class.forName("io.grpc.binder.internal.IntentNameResolverProvider"));
} catch (ClassNotFoundException e) {
logger.log(Level.FINE, "Unable to find IntentNameResolverProvider", e);
}
return Collections.unmodifiableList(list);
}

View File

@ -18,6 +18,7 @@ package io.grpc;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.errorprone.annotations.concurrent.GuardedBy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
@ -25,7 +26,6 @@ import java.util.LinkedHashSet;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.ThreadSafe;
/**

View File

@ -23,6 +23,7 @@ import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.base.MoreObjects;
import com.google.common.base.Objects;
import com.google.errorprone.annotations.CheckReturnValue;
import io.grpc.Metadata.TrustedAsciiMarshaller;
import java.nio.ByteBuffer;
import java.util.ArrayList;
@ -30,7 +31,6 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.TreeMap;
import javax.annotation.CheckReturnValue;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;

View File

@ -66,6 +66,14 @@ public class StatusOr<T> {
return status == null ? Status.OK : status;
}
/**
* Note that StatusOr containing statuses, the equality comparision is delegated to
* {@link Status#equals} which just does a reference equality check because equality on
* Statuses is not well defined.
* Instead, do comparison based on their Code with {@link Status#getCode}. The description and
* cause of the Status are unlikely to be stable, and additional fields may be added to Status
* in the future.
*/
@Override
public boolean equals(Object other) {
if (!(other instanceof StatusOr)) {

View File

@ -85,14 +85,12 @@ public class ConfiguratorRegistryTest {
@Override
public void run() {
assertThat(ConfiguratorRegistry.getDefaultRegistry().getConfigurators()).isEmpty();
try {
ConfiguratorRegistry.getDefaultRegistry()
.setConfigurators(Arrays.asList(new NoopConfigurator()));
fail("should have failed for invoking set call after get is already called");
} catch (IllegalStateException e) {
assertThat(e).hasMessageThat().isEqualTo("Configurators are already set");
}
NoopConfigurator noopConfigurator = new NoopConfigurator();
ConfiguratorRegistry.getDefaultRegistry()
.setConfigurators(Arrays.asList(noopConfigurator));
assertThat(ConfiguratorRegistry.getDefaultRegistry().getConfigurators())
.containsExactly(noopConfigurator);
assertThat(InternalConfiguratorRegistry.getConfiguratorsCallCountBeforeSet()).isEqualTo(1);
}
}

View File

@ -16,6 +16,7 @@
package io.grpc;
import static com.google.common.truth.Truth.assertThat;
import static java.nio.charset.StandardCharsets.US_ASCII;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.Assert.assertArrayEquals;
@ -24,6 +25,7 @@ import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
@ -37,9 +39,7 @@ import java.io.InputStream;
import java.util.Arrays;
import java.util.Iterator;
import java.util.Locale;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@ -49,9 +49,6 @@ import org.junit.runners.JUnit4;
@RunWith(JUnit4.class)
public class MetadataTest {
@SuppressWarnings("deprecation") // https://github.com/grpc/grpc-java/issues/7467
@Rule public final ExpectedException thrown = ExpectedException.none();
private static final Metadata.BinaryMarshaller<Fish> FISH_MARSHALLER =
new Metadata.BinaryMarshaller<Fish>() {
@Override
@ -65,7 +62,7 @@ public class MetadataTest {
}
};
private static class FishStreamMarsaller implements Metadata.BinaryStreamMarshaller<Fish> {
private static class FishStreamMarshaller implements Metadata.BinaryStreamMarshaller<Fish> {
@Override
public InputStream toStream(Fish fish) {
return new ByteArrayInputStream(FISH_MARSHALLER.toBytes(fish));
@ -82,7 +79,7 @@ public class MetadataTest {
}
private static final Metadata.BinaryStreamMarshaller<Fish> FISH_STREAM_MARSHALLER =
new FishStreamMarsaller();
new FishStreamMarshaller();
/** A pattern commonly used to avoid unnecessary serialization of immutable objects. */
private static final class FakeFishStream extends InputStream {
@ -121,10 +118,9 @@ public class MetadataTest {
@Test
public void noPseudoHeaders() {
thrown.expect(IllegalArgumentException.class);
thrown.expectMessage("Invalid character");
Metadata.Key.of(":test-bin", FISH_MARSHALLER);
IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
() -> Metadata.Key.of(":test-bin", FISH_MARSHALLER));
assertThat(e).hasMessageThat().isEqualTo("Invalid character ':' in key name ':test-bin'");
}
@Test
@ -186,8 +182,7 @@ public class MetadataTest {
Iterator<Fish> i = metadata.getAll(KEY).iterator();
assertEquals(lance, i.next());
thrown.expect(UnsupportedOperationException.class);
i.remove();
assertThrows(UnsupportedOperationException.class, i::remove);
}
@Test
@ -271,17 +266,15 @@ public class MetadataTest {
@Test
public void shortBinaryKeyName() {
thrown.expect(IllegalArgumentException.class);
Metadata.Key.of("-bin", FISH_MARSHALLER);
assertThrows(IllegalArgumentException.class, () -> Metadata.Key.of("-bin", FISH_MARSHALLER));
}
@Test
public void invalidSuffixBinaryKeyName() {
thrown.expect(IllegalArgumentException.class);
thrown.expectMessage("Binary header is named");
Metadata.Key.of("nonbinary", FISH_MARSHALLER);
IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
() -> Metadata.Key.of("nonbinary", FISH_MARSHALLER));
assertThat(e).hasMessageThat()
.isEqualTo("Binary header is named nonbinary. It must end with -bin");
}
@Test
@ -415,7 +408,7 @@ public class MetadataTest {
h.put(KEY_STREAMED, salmon);
// Get using a different marshaller instance.
Fish fish = h.get(copyKey(KEY_STREAMED, new FishStreamMarsaller()));
Fish fish = h.get(copyKey(KEY_STREAMED, new FishStreamMarshaller()));
assertEquals(salmon, fish);
}

View File

@ -26,9 +26,7 @@ import static org.junit.Assert.assertTrue;
import io.grpc.MethodDescriptor.Marshaller;
import io.grpc.MethodDescriptor.MethodType;
import io.grpc.testing.TestMethodDescriptors;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@ -37,10 +35,6 @@ import org.junit.runners.JUnit4;
*/
@RunWith(JUnit4.class)
public class MethodDescriptorTest {
@SuppressWarnings("deprecation") // https://github.com/grpc/grpc-java/issues/7467
@Rule
public final ExpectedException thrown = ExpectedException.none();
@Test
public void createMethodDescriptor() {
MethodDescriptor<String, String> descriptor = MethodDescriptor.<String, String>newBuilder()

View File

@ -19,6 +19,7 @@ package io.grpc;
import static com.google.common.collect.Iterables.getOnlyElement;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.AdditionalAnswers.delegatesTo;
import static org.mockito.ArgumentMatchers.same;
@ -40,7 +41,6 @@ import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.mockito.ArgumentMatchers;
@ -55,10 +55,6 @@ public class ServerInterceptorsTest {
@Rule
public final MockitoRule mocks = MockitoJUnit.rule();
@SuppressWarnings("deprecation") // https://github.com/grpc/grpc-java/issues/7467
@Rule
public final ExpectedException thrown = ExpectedException.none();
@Mock
private Marshaller<String> requestMarshaller;
@ -111,21 +107,21 @@ public class ServerInterceptorsTest {
public void npeForNullServiceDefinition() {
ServerServiceDefinition serviceDef = null;
List<ServerInterceptor> interceptors = Arrays.asList();
thrown.expect(NullPointerException.class);
ServerInterceptors.intercept(serviceDef, interceptors);
assertThrows(NullPointerException.class,
() -> ServerInterceptors.intercept(serviceDef, interceptors));
}
@Test
public void npeForNullInterceptorList() {
thrown.expect(NullPointerException.class);
ServerInterceptors.intercept(serviceDefinition, (List<ServerInterceptor>) null);
assertThrows(NullPointerException.class,
() -> ServerInterceptors.intercept(serviceDefinition, (List<ServerInterceptor>) null));
}
@Test
public void npeForNullInterceptor() {
List<ServerInterceptor> interceptors = Arrays.asList((ServerInterceptor) null);
thrown.expect(NullPointerException.class);
ServerInterceptors.intercept(serviceDefinition, interceptors);
assertThrows(NullPointerException.class,
() -> ServerInterceptors.intercept(serviceDefinition, interceptors));
}
@Test

View File

@ -18,14 +18,13 @@ package io.grpc;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.fail;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@ -52,9 +51,6 @@ public class ServerServiceDefinitionTest {
= ServerMethodDefinition.create(method1, methodHandler1);
private ServerMethodDefinition<String, Integer> methodDef2
= ServerMethodDefinition.create(method2, methodHandler2);
@SuppressWarnings("deprecation") // https://github.com/grpc/grpc-java/issues/7467
@Rule
public ExpectedException thrown = ExpectedException.none();
@Test
public void noMethods() {
@ -91,9 +87,7 @@ public class ServerServiceDefinitionTest {
ServiceDescriptor sd = new ServiceDescriptor(serviceName, method1);
ServerServiceDefinition.Builder ssd = ServerServiceDefinition.builder(sd)
.addMethod(method1, methodHandler1);
thrown.expect(IllegalStateException.class);
ssd.addMethod(diffMethod1, methodHandler2)
.build();
assertThrows(IllegalStateException.class, () -> ssd.addMethod(diffMethod1, methodHandler2));
}
@Test
@ -101,8 +95,7 @@ public class ServerServiceDefinitionTest {
ServiceDescriptor sd = new ServiceDescriptor(serviceName);
ServerServiceDefinition.Builder ssd = ServerServiceDefinition.builder(sd)
.addMethod(methodDef1);
thrown.expect(IllegalStateException.class);
ssd.build();
assertThrows(IllegalStateException.class, ssd::build);
}
@Test
@ -110,16 +103,14 @@ public class ServerServiceDefinitionTest {
ServiceDescriptor sd = new ServiceDescriptor(serviceName, method1);
ServerServiceDefinition.Builder ssd = ServerServiceDefinition.builder(sd)
.addMethod(diffMethod1, methodHandler1);
thrown.expect(IllegalStateException.class);
ssd.build();
assertThrows(IllegalStateException.class, ssd::build);
}
@Test
public void buildMisaligned_missingMethod() {
ServiceDescriptor sd = new ServiceDescriptor(serviceName, method1);
ServerServiceDefinition.Builder ssd = ServerServiceDefinition.builder(sd);
thrown.expect(IllegalStateException.class);
ssd.build();
assertThrows(IllegalStateException.class, ssd::build);
}
@Test

View File

@ -16,17 +16,18 @@
package io.grpc;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import com.google.common.truth.StringSubject;
import io.grpc.MethodDescriptor.MethodType;
import io.grpc.testing.TestMethodDescriptors;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@ -36,32 +37,27 @@ import org.junit.runners.JUnit4;
@RunWith(JUnit4.class)
public class ServiceDescriptorTest {
@SuppressWarnings("deprecation") // https://github.com/grpc/grpc-java/issues/7467
@Rule
public final ExpectedException thrown = ExpectedException.none();
@Test
public void failsOnNullName() {
thrown.expect(NullPointerException.class);
thrown.expectMessage("name");
new ServiceDescriptor(null, Collections.<MethodDescriptor<?, ?>>emptyList());
List<MethodDescriptor<?, ?>> methods = Collections.emptyList();
NullPointerException e = assertThrows(NullPointerException.class,
() -> new ServiceDescriptor(null, methods));
assertThat(e).hasMessageThat().isEqualTo("name");
}
@Test
public void failsOnNullMethods() {
thrown.expect(NullPointerException.class);
thrown.expectMessage("methods");
new ServiceDescriptor("name", (Collection<MethodDescriptor<?, ?>>) null);
NullPointerException e = assertThrows(NullPointerException.class,
() -> new ServiceDescriptor("name", (Collection<MethodDescriptor<?, ?>>) null));
assertThat(e).hasMessageThat().isEqualTo("methods");
}
@Test
public void failsOnNullMethod() {
thrown.expect(NullPointerException.class);
thrown.expectMessage("method");
new ServiceDescriptor("name", Collections.<MethodDescriptor<?, ?>>singletonList(null));
List<MethodDescriptor<?, ?>> methods = Collections.singletonList(null);
NullPointerException e = assertThrows(NullPointerException.class,
() -> new ServiceDescriptor("name", methods));
assertThat(e).hasMessageThat().isEqualTo("method");
}
@Test
@ -69,15 +65,17 @@ public class ServiceDescriptorTest {
List<MethodDescriptor<?, ?>> descriptors = Collections.<MethodDescriptor<?, ?>>singletonList(
MethodDescriptor.<Void, Void>newBuilder()
.setType(MethodType.UNARY)
.setFullMethodName(MethodDescriptor.generateFullMethodName("wrongservice", "method"))
.setFullMethodName(MethodDescriptor.generateFullMethodName("wrongService", "method"))
.setRequestMarshaller(TestMethodDescriptors.voidMarshaller())
.setResponseMarshaller(TestMethodDescriptors.voidMarshaller())
.build());
thrown.expect(IllegalArgumentException.class);
thrown.expectMessage("service names");
new ServiceDescriptor("name", descriptors);
IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
() -> new ServiceDescriptor("fooService", descriptors));
StringSubject error = assertThat(e).hasMessageThat();
error.contains("service names");
error.contains("fooService");
error.contains("wrongService");
}
@Test
@ -96,10 +94,9 @@ public class ServiceDescriptorTest {
.setResponseMarshaller(TestMethodDescriptors.voidMarshaller())
.build());
thrown.expect(IllegalArgumentException.class);
thrown.expectMessage("duplicate");
new ServiceDescriptor("name", descriptors);
IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
() -> new ServiceDescriptor("name", descriptors));
assertThat(e).hasMessageThat().isEqualTo("duplicate name name/method");
}
@Test

View File

@ -0,0 +1,118 @@
/*
* Copyright 2025 The gRPC Authors
*
* 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 io.grpc;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import org.mockito.ArgumentMatcher;
/**
* Mockito matcher for {@link Status}.
*/
public final class StatusMatcher implements ArgumentMatcher<Status> {
public static StatusMatcher statusHasCode(ArgumentMatcher<Status.Code> codeMatcher) {
return new StatusMatcher(codeMatcher, null);
}
public static StatusMatcher statusHasCode(Status.Code code) {
return statusHasCode(new EqualsMatcher<>(code));
}
private final ArgumentMatcher<Status.Code> codeMatcher;
private final ArgumentMatcher<String> descriptionMatcher;
private StatusMatcher(
ArgumentMatcher<Status.Code> codeMatcher,
ArgumentMatcher<String> descriptionMatcher) {
this.codeMatcher = checkNotNull(codeMatcher, "codeMatcher");
this.descriptionMatcher = descriptionMatcher;
}
public StatusMatcher andDescription(ArgumentMatcher<String> descriptionMatcher) {
checkState(this.descriptionMatcher == null, "Already has a description matcher");
return new StatusMatcher(codeMatcher, descriptionMatcher);
}
public StatusMatcher andDescription(String description) {
return andDescription(new EqualsMatcher<>(description));
}
public StatusMatcher andDescriptionContains(String substring) {
return andDescription(new StringContainsMatcher(substring));
}
@Override
public boolean matches(Status status) {
return status != null
&& codeMatcher.matches(status.getCode())
&& (descriptionMatcher == null || descriptionMatcher.matches(status.getDescription()));
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("{code=");
sb.append(codeMatcher);
if (descriptionMatcher != null) {
sb.append(", description=");
sb.append(descriptionMatcher);
}
sb.append("}");
return sb.toString();
}
// Use instead of lambda for better error message.
static final class EqualsMatcher<T> implements ArgumentMatcher<T> {
private final T obj;
EqualsMatcher(T obj) {
this.obj = checkNotNull(obj, "obj");
}
@Override
public boolean matches(Object other) {
return obj.equals(other);
}
@Override
public String toString() {
return obj.toString();
}
}
static final class StringContainsMatcher implements ArgumentMatcher<String> {
private final String needle;
StringContainsMatcher(String needle) {
this.needle = checkNotNull(needle, "needle");
}
@Override
public boolean matches(String haystack) {
if (haystack == null) {
return false;
}
return haystack.contains(needle);
}
@Override
public String toString() {
return "contains " + needle;
}
}
}

View File

@ -0,0 +1,66 @@
/*
* Copyright 2025 The gRPC Authors
*
* 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 io.grpc;
import static com.google.common.base.Preconditions.checkNotNull;
import org.mockito.ArgumentMatcher;
/**
* Mockito matcher for {@link StatusOr}.
*/
public final class StatusOrMatcher<T> implements ArgumentMatcher<StatusOr<T>> {
public static <T> StatusOrMatcher<T> hasValue(ArgumentMatcher<T> valueMatcher) {
return new StatusOrMatcher<T>(checkNotNull(valueMatcher, "valueMatcher"), null);
}
public static <T> StatusOrMatcher<T> hasStatus(ArgumentMatcher<Status> statusMatcher) {
return new StatusOrMatcher<T>(null, checkNotNull(statusMatcher, "statusMatcher"));
}
private final ArgumentMatcher<T> valueMatcher;
private final ArgumentMatcher<Status> statusMatcher;
private StatusOrMatcher(ArgumentMatcher<T> valueMatcher, ArgumentMatcher<Status> statusMatcher) {
this.valueMatcher = valueMatcher;
this.statusMatcher = statusMatcher;
}
@Override
public boolean matches(StatusOr<T> statusOr) {
if (statusOr == null) {
return false;
}
if (statusOr.hasValue() != (valueMatcher != null)) {
return false;
}
if (valueMatcher != null) {
return valueMatcher.matches(statusOr.getValue());
} else {
return statusMatcher.matches(statusOr.getStatus());
}
}
@Override
public String toString() {
if (valueMatcher != null) {
return "{value=" + valueMatcher + "}";
} else {
return "{status=" + statusMatcher + "}";
}
}
}

View File

@ -24,9 +24,9 @@ import static java.util.concurrent.TimeUnit.SECONDS;
import com.google.common.truth.ComparableSubject;
import com.google.common.truth.FailureMetadata;
import com.google.common.truth.Subject;
import com.google.errorprone.annotations.CheckReturnValue;
import io.grpc.Deadline;
import java.util.concurrent.TimeUnit;
import javax.annotation.CheckReturnValue;
import javax.annotation.Nullable;
/** Propositions for {@link Deadline} subjects. */

View File

@ -50,10 +50,12 @@ import io.grpc.SecurityLevel;
import io.grpc.Status;
import io.grpc.internal.JsonParser;
import io.grpc.testing.TestMethodDescriptors;
import io.grpc.testing.TlsTesting;
import io.grpc.util.CertificateUtils;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
@ -342,7 +344,10 @@ public class GoogleAuthLibraryCallCredentialsTest {
@Test
public void serviceAccountToJwt() throws Exception {
KeyPair pair = KeyPairGenerator.getInstance("RSA").generateKeyPair();
PrivateKey privateKey;
try (InputStream server1Key = TlsTesting.loadCert("server1.key")) {
privateKey = CertificateUtils.getPrivateKey(server1Key);
}
HttpTransportFactory factory = Mockito.mock(HttpTransportFactory.class);
Mockito.when(factory.create()).thenThrow(new AssertionError());
@ -350,7 +355,7 @@ public class GoogleAuthLibraryCallCredentialsTest {
ServiceAccountCredentials credentials =
ServiceAccountCredentials.newBuilder()
.setClientEmail("test-email@example.com")
.setPrivateKey(pair.getPrivate())
.setPrivateKey(privateKey)
.setPrivateKeyId("test-private-key-id")
.setHttpTransportFactory(factory)
.build();
@ -390,13 +395,16 @@ public class GoogleAuthLibraryCallCredentialsTest {
@Test
public void jwtAccessCredentialsInRequestMetadata() throws Exception {
KeyPair pair = KeyPairGenerator.getInstance("RSA").generateKeyPair();
PrivateKey privateKey;
try (InputStream server1Key = TlsTesting.loadCert("server1.key")) {
privateKey = CertificateUtils.getPrivateKey(server1Key);
}
ServiceAccountCredentials credentials =
ServiceAccountCredentials.newBuilder()
.setClientId("test-client")
.setClientEmail("test-email@example.com")
.setPrivateKey(pair.getPrivate())
.setPrivateKey(privateKey)
.setPrivateKeyId("test-private-key-id")
.setQuotaProjectId("test-quota-project-id")
.build();

View File

@ -4,9 +4,6 @@ import static io.grpc.MethodDescriptor.generateFullMethodName;
/**
*/
@javax.annotation.Generated(
value = "by gRPC proto compiler",
comments = "Source: grpc/testing/services.proto")
@io.grpc.stub.annotations.GrpcGenerated
public final class BenchmarkServiceGrpc {
@ -401,8 +398,8 @@ public final class BenchmarkServiceGrpc {
* The server returns the client payload as-is.
* </pre>
*/
public io.grpc.benchmarks.proto.Messages.SimpleResponse unaryCall(io.grpc.benchmarks.proto.Messages.SimpleRequest request) {
return io.grpc.stub.ClientCalls.blockingUnaryCall(
public io.grpc.benchmarks.proto.Messages.SimpleResponse unaryCall(io.grpc.benchmarks.proto.Messages.SimpleRequest request) throws io.grpc.StatusException {
return io.grpc.stub.ClientCalls.blockingV2UnaryCall(
getChannel(), getUnaryCallMethod(), getCallOptions(), request);
}

View File

@ -4,9 +4,6 @@ import static io.grpc.MethodDescriptor.generateFullMethodName;
/**
*/
@javax.annotation.Generated(
value = "by gRPC proto compiler",
comments = "Source: grpc/testing/services.proto")
@io.grpc.stub.annotations.GrpcGenerated
public final class ReportQpsScenarioServiceGrpc {
@ -180,8 +177,8 @@ public final class ReportQpsScenarioServiceGrpc {
* Report results of a QPS test benchmark scenario.
* </pre>
*/
public io.grpc.benchmarks.proto.Control.Void reportScenario(io.grpc.benchmarks.proto.Control.ScenarioResult request) {
return io.grpc.stub.ClientCalls.blockingUnaryCall(
public io.grpc.benchmarks.proto.Control.Void reportScenario(io.grpc.benchmarks.proto.Control.ScenarioResult request) throws io.grpc.StatusException {
return io.grpc.stub.ClientCalls.blockingV2UnaryCall(
getChannel(), getReportScenarioMethod(), getCallOptions(), request);
}
}

View File

@ -4,9 +4,6 @@ import static io.grpc.MethodDescriptor.generateFullMethodName;
/**
*/
@javax.annotation.Generated(
value = "by gRPC proto compiler",
comments = "Source: grpc/testing/services.proto")
@io.grpc.stub.annotations.GrpcGenerated
public final class WorkerServiceGrpc {
@ -390,8 +387,8 @@ public final class WorkerServiceGrpc {
* Just return the core count - unary call
* </pre>
*/
public io.grpc.benchmarks.proto.Control.CoreResponse coreCount(io.grpc.benchmarks.proto.Control.CoreRequest request) {
return io.grpc.stub.ClientCalls.blockingUnaryCall(
public io.grpc.benchmarks.proto.Control.CoreResponse coreCount(io.grpc.benchmarks.proto.Control.CoreRequest request) throws io.grpc.StatusException {
return io.grpc.stub.ClientCalls.blockingV2UnaryCall(
getChannel(), getCoreCountMethod(), getCallOptions(), request);
}
@ -400,8 +397,8 @@ public final class WorkerServiceGrpc {
* Quit this worker
* </pre>
*/
public io.grpc.benchmarks.proto.Control.Void quitWorker(io.grpc.benchmarks.proto.Control.Void request) {
return io.grpc.stub.ClientCalls.blockingUnaryCall(
public io.grpc.benchmarks.proto.Control.Void quitWorker(io.grpc.benchmarks.proto.Control.Void request) throws io.grpc.StatusException {
return io.grpc.stub.ClientCalls.blockingV2UnaryCall(
getChannel(), getQuitWorkerMethod(), getCallOptions(), request);
}
}

View File

@ -6,28 +6,28 @@ plugins {
description = 'gRPC BinderChannel'
android {
namespace 'io.grpc.binder'
namespace = 'io.grpc.binder'
compileSdkVersion 34
compileOptions {
sourceCompatibility 1.8
targetCompatibility 1.8
}
defaultConfig {
minSdkVersion 21
minSdkVersion 22
targetSdkVersion 33
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
multiDexEnabled true
multiDexEnabled = true
}
lintOptions { abortOnError false }
lintOptions { abortOnError = false }
publishing {
singleVariant('release') {
withSourcesJar()
withJavadocJar()
}
}
testFixtures { enable true }
testFixtures { enable = true }
}
repositories {
@ -72,6 +72,7 @@ dependencies {
androidTestImplementation testFixtures(project(':grpc-core'))
testFixturesImplementation libraries.guava.testlib
testFixturesImplementation testFixtures(project(':grpc-core'))
}
import net.ltgt.gradle.errorprone.CheckSeverity

View File

@ -11,11 +11,13 @@
<service android:name="io.grpc.binder.HostServices$HostService1" android:exported="false">
<intent-filter>
<action android:name="action1"/>
<data android:scheme="scheme" android:host="authority" android:path="/path"/>
</intent-filter>
</service>
<service android:name="io.grpc.binder.HostServices$HostService2" android:exported="false">
<intent-filter>
<action android:name="action2"/>
<data android:scheme="scheme" android:host="authority" android:path="/path"/>
</intent-filter>
</service>
</application>

View File

@ -23,6 +23,7 @@ import static java.util.concurrent.TimeUnit.SECONDS;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.test.core.app.ApplicationProvider;
@ -39,7 +40,6 @@ import io.grpc.ForwardingServerCall.SimpleForwardingServerCall;
import io.grpc.ManagedChannel;
import io.grpc.Metadata;
import io.grpc.MethodDescriptor;
import io.grpc.NameResolverRegistry;
import io.grpc.ServerCall;
import io.grpc.ServerCall.Listener;
import io.grpc.ServerCallHandler;
@ -49,7 +49,6 @@ import io.grpc.ServerServiceDefinition;
import io.grpc.Status.Code;
import io.grpc.StatusRuntimeException;
import io.grpc.internal.GrpcUtil;
import io.grpc.internal.testing.FakeNameResolverProvider;
import io.grpc.stub.ClientCalls;
import io.grpc.stub.MetadataUtils;
import io.grpc.stub.ServerCalls;
@ -77,7 +76,6 @@ public final class BinderChannelSmokeTest {
private static final int SLIGHTLY_MORE_THAN_ONE_BLOCK = 16 * 1024 + 100;
private static final String MSG = "Some text which will be repeated many many times";
private static final String SERVER_TARGET_URI = "fake://server";
private static final Metadata.Key<PoisonParcelable> POISON_KEY =
ParcelableUtils.metadataKey("poison-bin", PoisonParcelable.CREATOR);
@ -99,7 +97,6 @@ public final class BinderChannelSmokeTest {
.setType(MethodDescriptor.MethodType.BIDI_STREAMING)
.build();
FakeNameResolverProvider fakeNameResolverProvider;
ManagedChannel channel;
AtomicReference<Metadata> headersCapture = new AtomicReference<>();
AtomicReference<PeerUid> clientUidCapture = new AtomicReference<>();
@ -138,8 +135,6 @@ public final class BinderChannelSmokeTest {
PeerUids.newPeerIdentifyingServerInterceptor());
AndroidComponentAddress serverAddress = HostServices.allocateService(appContext);
fakeNameResolverProvider = new FakeNameResolverProvider(SERVER_TARGET_URI, serverAddress);
NameResolverRegistry.getDefaultRegistry().register(fakeNameResolverProvider);
HostServices.configureService(
serverAddress,
HostServices.serviceParamsBuilder()
@ -166,7 +161,6 @@ public final class BinderChannelSmokeTest {
@After
public void tearDown() throws Exception {
channel.shutdownNow();
NameResolverRegistry.getDefaultRegistry().deregister(fakeNameResolverProvider);
HostServices.awaitServiceShutdown();
}
@ -235,7 +229,11 @@ public final class BinderChannelSmokeTest {
@Test
public void testConnectViaTargetUri() throws Exception {
channel = BinderChannelBuilder.forTarget(SERVER_TARGET_URI, appContext).build();
// Compare with the <intent-filter> mapping in AndroidManifest.xml.
channel =
BinderChannelBuilder.forTarget(
"intent://authority/path#Intent;action=action1;scheme=scheme;end;", appContext)
.build();
assertThat(doCall("Hello").get()).isEqualTo("Hello");
}
@ -245,7 +243,10 @@ public final class BinderChannelSmokeTest {
channel =
BinderChannelBuilder.forAddress(
AndroidComponentAddress.forBindIntent(
new Intent().setAction("action1").setPackage(appContext.getPackageName())),
new Intent()
.setAction("action1")
.setData(Uri.parse("scheme://authority/path"))
.setPackage(appContext.getPackageName())),
appContext)
.build();
assertThat(doCall("Hello").get()).isEqualTo("Hello");

View File

@ -29,6 +29,7 @@ import android.os.RemoteException;
import androidx.lifecycle.LifecycleService;
import com.google.auto.value.AutoValue;
import com.google.common.base.Supplier;
import com.google.errorprone.annotations.concurrent.GuardedBy;
import io.grpc.Server;
import java.io.IOException;
import java.util.HashMap;
@ -38,7 +39,6 @@ import java.util.concurrent.Executor;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
/**
* A test helper class for creating android services to host gRPC servers.

View File

@ -17,6 +17,7 @@
package io.grpc.binder.internal;
import static com.google.common.truth.Truth.assertThat;
import static java.util.concurrent.TimeUnit.SECONDS;
import android.content.Context;
import android.os.DeadObjectException;
@ -24,9 +25,9 @@ import android.os.Parcel;
import android.os.RemoteException;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.errorprone.annotations.concurrent.GuardedBy;
import com.google.protobuf.Empty;
import io.grpc.CallOptions;
import io.grpc.ClientStreamTracer;
@ -37,13 +38,13 @@ import io.grpc.ServerServiceDefinition;
import io.grpc.Status;
import io.grpc.Status.Code;
import io.grpc.binder.AndroidComponentAddress;
import io.grpc.binder.AsyncSecurityPolicy;
import io.grpc.binder.BinderServerBuilder;
import io.grpc.binder.HostServices;
import io.grpc.binder.SecurityPolicy;
import io.grpc.binder.internal.OneWayBinderProxies.BlackHoleOneWayBinderProxy;
import io.grpc.binder.internal.OneWayBinderProxies.BlockingBinderDecorator;
import io.grpc.binder.internal.OneWayBinderProxies.ThrowingOneWayBinderProxy;
import io.grpc.binder.internal.SettableAsyncSecurityPolicy.AuthRequest;
import io.grpc.internal.ClientStream;
import io.grpc.internal.ClientStreamListener;
import io.grpc.internal.ClientTransportFactory.ClientTransportOptions;
@ -62,9 +63,7 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@ -101,7 +100,7 @@ public final class BinderClientTransportTest {
.build();
AndroidComponentAddress serverAddress;
BinderTransport.BinderClientTransport transport;
BinderClientTransport transport;
BlockingSecurityPolicy blockingSecurityPolicy = new BlockingSecurityPolicy();
private final ObjectPool<ScheduledExecutorService> executorServicePool =
@ -154,23 +153,32 @@ public final class BinderClientTransportTest {
.setScheduledExecutorPool(executorServicePool)
.setOffloadExecutorPool(offloadServicePool);
@CanIgnoreReturnValue
public BinderClientTransportBuilder setSecurityPolicy(SecurityPolicy securityPolicy) {
factoryBuilder.setSecurityPolicy(securityPolicy);
return this;
}
@CanIgnoreReturnValue
public BinderClientTransportBuilder setBinderDecorator(
OneWayBinderProxy.Decorator binderDecorator) {
factoryBuilder.setBinderDecorator(binderDecorator);
return this;
}
@CanIgnoreReturnValue
public BinderClientTransportBuilder setReadyTimeoutMillis(int timeoutMillis) {
factoryBuilder.setReadyTimeoutMillis(timeoutMillis);
return this;
}
public BinderTransport.BinderClientTransport build() {
@CanIgnoreReturnValue
public BinderClientTransportBuilder setPreAuthorizeServer(boolean preAuthorizeServer) {
factoryBuilder.setPreAuthorizeServers(preAuthorizeServer);
return this;
}
public BinderClientTransport build() {
return factoryBuilder
.buildClientTransportFactory()
.newClientTransport(serverAddress, new ClientTransportOptions(), null);
@ -189,7 +197,7 @@ public final class BinderClientTransportTest {
private static void shutdownAndTerminate(ExecutorService executorService)
throws InterruptedException {
executorService.shutdownNow();
if (!executorService.awaitTermination(TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
if (!executorService.awaitTermination(TIMEOUT_SECONDS, SECONDS)) {
throw new AssertionError("executor failed to terminate promptly");
}
}
@ -370,27 +378,58 @@ public final class BinderClientTransportTest {
}
@Test
public void testBlackHoleSecurityPolicyConnectTimeout() throws Exception {
public void testBlackHoleSecurityPolicyAuthTimeout() throws Exception {
SettableAsyncSecurityPolicy securityPolicy = new SettableAsyncSecurityPolicy();
transport =
new BinderClientTransportBuilder()
.setSecurityPolicy(blockingSecurityPolicy)
.setSecurityPolicy(securityPolicy)
.setPreAuthorizeServer(false)
.setReadyTimeoutMillis(1_234)
.build();
transport.start(transportListener).run();
// Take the next authRequest but don't respond to it, in order to trigger the ready timeout.
AuthRequest authRequest = securityPolicy.takeNextAuthRequest(TIMEOUT_SECONDS, SECONDS);
Status transportStatus = transportListener.awaitShutdown();
assertThat(transportStatus.getCode()).isEqualTo(Code.DEADLINE_EXCEEDED);
assertThat(transportStatus.getDescription()).contains("1234");
transportListener.awaitTermination();
blockingSecurityPolicy.provideNextCheckAuthorizationResult(Status.OK);
// If the transport gave up waiting on auth, it should cancel its request.
assertThat(authRequest.isCancelled()).isTrue();
}
@Test
public void testAsyncSecurityPolicyFailure() throws Exception {
public void testBlackHoleSecurityPolicyPreAuthTimeout() throws Exception {
SettableAsyncSecurityPolicy securityPolicy = new SettableAsyncSecurityPolicy();
transport = new BinderClientTransportBuilder().setSecurityPolicy(securityPolicy).build();
RuntimeException exception = new NullPointerException();
securityPolicy.setAuthorizationException(exception);
transport =
new BinderClientTransportBuilder()
.setSecurityPolicy(securityPolicy)
.setPreAuthorizeServer(true)
.setReadyTimeoutMillis(1_234)
.build();
transport.start(transportListener).run();
// Take the next authRequest but don't respond to it, in order to trigger the ready timeout.
AuthRequest preAuthRequest = securityPolicy.takeNextAuthRequest(TIMEOUT_SECONDS, SECONDS);
Status transportStatus = transportListener.awaitShutdown();
assertThat(transportStatus.getCode()).isEqualTo(Code.DEADLINE_EXCEEDED);
assertThat(transportStatus.getDescription()).contains("1234");
transportListener.awaitTermination();
// If the transport gave up waiting on auth, it should cancel its request.
assertThat(preAuthRequest.isCancelled()).isTrue();
}
@Test
public void testAsyncSecurityPolicyAuthFailure() throws Exception {
SettableAsyncSecurityPolicy securityPolicy = new SettableAsyncSecurityPolicy();
transport =
new BinderClientTransportBuilder()
.setPreAuthorizeServer(false)
.setSecurityPolicy(securityPolicy)
.build();
RuntimeException exception = new NullPointerException();
transport.start(transportListener).run();
securityPolicy.takeNextAuthRequest(TIMEOUT_SECONDS, SECONDS).setResult(exception);
Status transportStatus = transportListener.awaitShutdown();
assertThat(transportStatus.getCode()).isEqualTo(Code.INTERNAL);
assertThat(transportStatus.getCause()).isEqualTo(exception);
@ -398,19 +437,72 @@ public final class BinderClientTransportTest {
}
@Test
public void testAsyncSecurityPolicySuccess() throws Exception {
public void testAsyncSecurityPolicyPreAuthFailure() throws Exception {
SettableAsyncSecurityPolicy securityPolicy = new SettableAsyncSecurityPolicy();
transport = new BinderClientTransportBuilder().setSecurityPolicy(securityPolicy).build();
securityPolicy.setAuthorizationResult(Status.PERMISSION_DENIED);
transport =
new BinderClientTransportBuilder()
.setPreAuthorizeServer(true)
.setSecurityPolicy(securityPolicy)
.build();
RuntimeException exception = new NullPointerException();
transport.start(transportListener).run();
securityPolicy.takeNextAuthRequest(TIMEOUT_SECONDS, SECONDS).setResult(exception);
Status transportStatus = transportListener.awaitShutdown();
assertThat(transportStatus.getCode()).isEqualTo(Code.PERMISSION_DENIED);
assertThat(transportStatus.getCode()).isEqualTo(Code.INTERNAL);
assertThat(transportStatus.getCause()).isEqualTo(exception);
transportListener.awaitTermination();
}
@Test
public void testAsyncSecurityPolicyAuthSuccess() throws Exception {
SettableAsyncSecurityPolicy securityPolicy = new SettableAsyncSecurityPolicy();
transport =
new BinderClientTransportBuilder()
.setPreAuthorizeServer(false)
.setSecurityPolicy(securityPolicy)
.build();
transport.start(transportListener).run();
securityPolicy
.takeNextAuthRequest(TIMEOUT_SECONDS, SECONDS)
.setResult(Status.PERMISSION_DENIED.withDescription("xyzzy"));
Status transportStatus = transportListener.awaitShutdown();
assertThat(transportStatus.getCode()).isEqualTo(Code.PERMISSION_DENIED);
assertThat(transportStatus.getDescription()).contains("xyzzy");
transportListener.awaitTermination();
}
@Test
public void testAsyncSecurityPolicyPreAuthSuccess() throws Exception {
SettableAsyncSecurityPolicy securityPolicy = new SettableAsyncSecurityPolicy();
transport =
new BinderClientTransportBuilder()
.setPreAuthorizeServer(true)
.setSecurityPolicy(securityPolicy)
.build();
transport.start(transportListener).run();
securityPolicy
.takeNextAuthRequest(TIMEOUT_SECONDS, SECONDS)
.setResult(Status.PERMISSION_DENIED.withDescription("xyzzy"));
Status transportStatus = transportListener.awaitShutdown();
assertThat(transportStatus.getCode()).isEqualTo(Code.PERMISSION_DENIED);
assertThat(transportStatus.getDescription()).contains("xyzzy");
transportListener.awaitTermination();
}
@Test
public void testAsyncSecurityPolicyCancelledUponExternalTermination() throws Exception {
SettableAsyncSecurityPolicy securityPolicy = new SettableAsyncSecurityPolicy();
transport = new BinderClientTransportBuilder().setSecurityPolicy(securityPolicy).build();
transport.start(transportListener).run();
AuthRequest authRequest = securityPolicy.takeNextAuthRequest(TIMEOUT_SECONDS, SECONDS);
transport.shutdownNow(Status.UNAVAILABLE); // 'authRequest' remains unanswered!
transportListener.awaitShutdown();
transportListener.awaitTermination();
assertThat(authRequest.isCancelled()).isTrue();
}
private static void startAndAwaitReady(
BinderTransport.BinderClientTransport transport, TestTransportListener transportListener)
throws Exception {
BinderClientTransport transport, TestTransportListener transportListener) throws Exception {
transport.start(transportListener).run();
transportListener.awaitReady();
}
@ -429,7 +521,7 @@ public final class BinderClientTransportTest {
}
public Status awaitShutdown() throws Exception {
return shutdownStatus.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
return shutdownStatus.get(TIMEOUT_SECONDS, SECONDS);
}
@Override
@ -440,7 +532,7 @@ public final class BinderClientTransportTest {
}
public void awaitTermination() throws Exception {
isTerminated.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
isTerminated.get(TIMEOUT_SECONDS, SECONDS);
}
@Override
@ -451,7 +543,7 @@ public final class BinderClientTransportTest {
}
public void awaitReady() throws Exception {
isReady.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
isReady.get(TIMEOUT_SECONDS, SECONDS);
}
@Override
@ -567,25 +659,4 @@ public final class BinderClientTransportTest {
}
}
}
/** An AsyncSecurityPolicy that lets a test specify the outcome of checkAuthorizationAsync(). */
static class SettableAsyncSecurityPolicy extends AsyncSecurityPolicy {
private SettableFuture<Status> result = SettableFuture.create();
public void clearAuthorizationResult() {
result = SettableFuture.create();
}
public boolean setAuthorizationResult(Status status) {
return result.set(status);
}
public boolean setAuthorizationException(Throwable t) {
return result.setException(t);
}
public ListenableFuture<Status> checkAuthorizationAsync(int uid) {
return Futures.nonCancellationPropagating(result);
}
}
}

View File

@ -106,8 +106,7 @@ public final class BinderTransportTest extends AbstractTransportTest {
options.setEagAttributes(eagAttrs());
options.setChannelLogger(transportLogger());
return new BinderTransport.BinderClientTransport(
builder.buildClientTransportFactory(), addr, options);
return new BinderClientTransport(builder.buildClientTransportFactory(), addr, options);
}
@Test

View File

@ -58,7 +58,7 @@ public final class AndroidComponentAddress extends SocketAddress {
@Nullable
private final UserHandle targetUser; // null means the same user that hosts this process.
protected AndroidComponentAddress(Intent bindIntent, @Nullable UserHandle targetUser) {
private AndroidComponentAddress(Intent bindIntent, @Nullable UserHandle targetUser) {
checkArgument(
bindIntent.getComponent() != null || bindIntent.getPackage() != null,
"'bindIntent' must be explicit. Specify either a package or ComponentName.");
@ -250,7 +250,22 @@ public final class AndroidComponentAddress extends SocketAddress {
return this;
}
/** See {@link AndroidComponentAddress#getTargetUser()}. */
/**
* Specifies the Android user in which the built Address' bind Intent will be evaluated.
*
* <p>Connecting to a server in a different Android user is uncommon and requires the client app
* have runtime visibility of &#064;SystemApi's and hold certain &#064;SystemApi permissions.
* The device must also be running Android SDK version 30 or higher.
*
* <p>See https://developer.android.com/guide/app-compatibility/restrictions-non-sdk-interfaces
* for details on which apps can call the underlying &#064;SystemApi's needed to make this type
* of connection.
*
* <p>One of the "android.permission.INTERACT_ACROSS_XXX" permissions is required. The exact one
* depends on the calling user's relationship to the target user, whether client and server are
* in the same or different apps, and the version of Android in use. See {@link
* Context#bindServiceAsUser}, the essential underlying Android API, for details.
*/
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/10173")
public Builder setTargetUser(@Nullable UserHandle targetUser) {
this.targetUser = targetUser;

View File

@ -18,6 +18,8 @@ package io.grpc.binder;
import android.content.Intent;
import android.os.UserHandle;
import io.grpc.Attributes;
import io.grpc.EquivalentAddressGroup;
import io.grpc.ExperimentalApi;
import io.grpc.NameResolver;
@ -32,15 +34,42 @@ public final class ApiConstants {
*/
public static final String ACTION_BIND = "grpc.io.action.BIND";
/**
* Gives a {@link NameResolver} access to its Channel's "source" {@link android.content.Context},
* the entry point to almost every other Android API.
*
* <p>This argument is set automatically by {@link BinderChannelBuilder}. Any value passed to
* {@link io.grpc.ManagedChannelBuilder#setNameResolverArg} will be ignored.
*
* <p>See {@link BinderChannelBuilder#forTarget(String, android.content.Context)} for more.
*/
public static final NameResolver.Args.Key<android.content.Context> SOURCE_ANDROID_CONTEXT =
NameResolver.Args.Key.create("source-android-context");
/**
* Specifies the Android user in which target URIs should be resolved.
*
* <p>{@link UserHandle} can't reasonably be encoded in a target URI string. Instead, all
* {@link io.grpc.NameResolverProvider}s producing {@link AndroidComponentAddress}es should let
* clients address servers in another Android user using this argument.
* <p>{@link UserHandle} can't reasonably be encoded in a target URI string. Instead, all {@link
* io.grpc.NameResolverProvider}s producing {@link AndroidComponentAddress}es should let clients
* address servers in another Android user using this argument.
*
* <p>See also {@link AndroidComponentAddress#getTargetUser()}.
* <p>Connecting to a server in a different Android user is uncommon and can only be done by a
* "system app" client with special permissions. See {@link
* AndroidComponentAddress.Builder#setTargetUser(UserHandle)} for details.
*/
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/10173")
public static final NameResolver.Args.Key<UserHandle> TARGET_ANDROID_USER =
NameResolver.Args.Key.create("target-android-user");
/**
* Lets you override a Channel's pre-auth configuration (see {@link
* BinderChannelBuilder#preAuthorizeServers(boolean)}) for a given {@link EquivalentAddressGroup}.
*
* <p>A {@link NameResolver} that discovers servers from an untrusted source like PackageManager
* can use this to force server pre-auth and prevent abuse.
*/
@EquivalentAddressGroup.Attr
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/12191")
public static final Attributes.Key<Boolean> PRE_AUTH_SERVER_OVERRIDE =
Attributes.Key.create("pre-auth-server-override");
}

View File

@ -17,11 +17,11 @@
package io.grpc.binder;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.errorprone.annotations.CheckReturnValue;
import io.grpc.ExperimentalApi;
import io.grpc.Status;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import javax.annotation.CheckReturnValue;
/**
* Decides whether a given Android UID is authorized to access some resource.
@ -67,4 +67,25 @@ public abstract class AsyncSecurityPolicy extends SecurityPolicy {
* authorized.
*/
public abstract ListenableFuture<Status> checkAuthorizationAsync(int uid);
/**
* Decides whether the given Android UID is authorized, without providing its raw integer value.
*
* <p>Calling this is equivalent to calling {@link SecurityPolicy#checkAuthorization(int)}, except
* the caller provides a {@link PeerUid} wrapper instead of the raw integer uid (known only to the
* transport). This allows a server to check additional application-layer security policy for
* itself *after* the call itself is authorized by the transport layer. Cross cutting application-
* layer checks could be done from a {@link io.grpc.ServerInterceptor}. Checks based on the
* substance of a request message could be done by the individual RPC method implementations
* themselves.
*
* <p>See #checkAuthorizationAsync(int) for details on the semantics. See {@link
* PeerUids#newPeerIdentifyingServerInterceptor()} for how to get a {@link PeerUid}.
*
* @param uid The Android UID to authenticate.
* @return A gRPC {@link Status} object, with OK indicating authorized.
*/
public final ListenableFuture<Status> checkAuthorizationAsync(PeerUid uid) {
return checkAuthorizationAsync(uid.getUid());
}
}

View File

@ -242,9 +242,9 @@ public final class BinderChannelBuilder extends ForwardingChannelBuilder<BinderC
* specify a {@link UserHandle}. If neither the Channel nor the {@link AndroidComponentAddress}
* specifies a target user, the {@link UserHandle} of the current process will be used.
*
* <p>Targeting a Service in a different Android user is uncommon and requires special permissions
* normally reserved for system apps. See {@link android.content.Context#bindServiceAsUser} for
* details.
* <p>Connecting to a server in a different Android user is uncommon and can only be done by a
* "system app" client with special permissions. See {@link
* AndroidComponentAddress.Builder#setTargetUser(UserHandle)} for details.
*
* @deprecated This method's name is misleading because it implies an impersonated client identity
* when it's actually specifying part of the server's location. It's also no longer necessary
@ -279,6 +279,35 @@ public final class BinderChannelBuilder extends ForwardingChannelBuilder<BinderC
return this;
}
/**
* Checks servers against this Channel's {@link SecurityPolicy} *before* binding.
*
* <p>Android users can be tricked into installing a malicious app with the same package name as a
* legitimate server. That's why we don't send calls to a server until it has been authorized by
* an appropriate {@link SecurityPolicy}. But merely binding to a malicious server can enable
* "keep-alive" and "background activity launch" abuse, even if it's ultimately unauthorized.
* Pre-authorization mitigates these threats by performing a preliminary {@link SecurityPolicy}
* check against a server app's PackageManager-registered identity without actually creating an
* instance of it. This is especially important for security when the server's direct address
* isn't known in advance but rather resolved via target URI or discovered by other means.
*
* <p>Note that, unlike ordinary authorization, pre-authorization is performed against the server
* app's UID, not the UID of the process hosting the bound Service. These can be different, most
* commonly due to services that set `android:isolatedProcess=true`.
*
* <p>Pre-authorization is strongly recommended but it remains optional for now because of this
* behavior change and the small performance cost.
*
* <p>The default value of this property is false but it will become true in a future release.
* Clients that require a particular behavior should configure it explicitly using this method
* rather than relying on the default.
*/
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/12191")
public BinderChannelBuilder preAuthorizeServers(boolean preAuthorize) {
transportFactoryBuilder.setPreAuthorizeServers(preAuthorize);
return this;
}
@Override
public BinderChannelBuilder idleTimeout(long value, TimeUnit unit) {
checkState(
@ -292,6 +321,8 @@ public final class BinderChannelBuilder extends ForwardingChannelBuilder<BinderC
public ManagedChannel build() {
transportFactoryBuilder.setOffloadExecutorPool(
managedChannelImplBuilder.getOffloadExecutorPool());
setNameResolverArg(
ApiConstants.SOURCE_ANDROID_CONTEXT, transportFactoryBuilder.getSourceContext());
return super.build();
}
}

View File

@ -184,7 +184,6 @@ public final class SecurityPolicies {
* Creates {@link SecurityPolicy} which checks if the app is a device owner app. See {@link
* DevicePolicyManager}.
*/
@RequiresApi(18)
public static io.grpc.binder.SecurityPolicy isDeviceOwner(Context applicationContext) {
DevicePolicyManager devicePolicyManager =
(DevicePolicyManager) applicationContext.getSystemService(Context.DEVICE_POLICY_SERVICE);
@ -199,7 +198,6 @@ public final class SecurityPolicies {
* Creates {@link SecurityPolicy} which checks if the app is a profile owner app. See {@link
* DevicePolicyManager}.
*/
@RequiresApi(21)
public static SecurityPolicy isProfileOwner(Context applicationContext) {
DevicePolicyManager devicePolicyManager =
(DevicePolicyManager) applicationContext.getSystemService(Context.DEVICE_POLICY_SERVICE);

View File

@ -16,8 +16,8 @@
package io.grpc.binder;
import com.google.errorprone.annotations.CheckReturnValue;
import io.grpc.Status;
import javax.annotation.CheckReturnValue;
/**
* Decides whether a given Android UID is authorized to access some resource.
@ -53,4 +53,25 @@ public abstract class SecurityPolicy {
* @return A gRPC {@link Status} object, with OK indicating authorized.
*/
public abstract Status checkAuthorization(int uid);
/**
* Decides whether the given Android UID is authorized, without providing its raw integer value.
*
* <p>Calling this is equivalent to calling {@link SecurityPolicy#checkAuthorization(int)}, except
* the caller provides a {@link PeerUid} wrapper instead of the raw integer uid (known only to the
* transport). This allows a server to check additional application-layer security policy for
* itself *after* the call itself is authorized by the transport layer. Cross cutting application-
* layer checks could be done from a {@link io.grpc.ServerInterceptor}. Checks based on the
* substance of a request message could be done by the individual RPC method implementations
* themselves.
*
* <p>See #checkAuthorizationAsync(int) for details on the semantics. See {@link
* PeerUids#newPeerIdentifyingServerInterceptor()} for how to get a {@link PeerUid}.
*
* @param uid The Android UID to authenticate.
* @return A gRPC {@link Status} object, with OK indicating authorized.
*/
public final Status checkAuthorization(PeerUid uid) {
return checkAuthorization(uid.getUid());
}
}

View File

@ -19,10 +19,10 @@ package io.grpc.binder;
import com.google.common.collect.ImmutableMap;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.errorprone.annotations.CheckReturnValue;
import io.grpc.Status;
import java.util.HashMap;
import java.util.Map;
import javax.annotation.CheckReturnValue;
/**
* A security policy for a gRPC server.

View File

@ -16,9 +16,9 @@
package io.grpc.binder;
import com.google.errorprone.annotations.CheckReturnValue;
import io.grpc.ExperimentalApi;
import io.grpc.Status;
import javax.annotation.CheckReturnValue;
/** Static factory methods for creating untrusted security policies. */
@CheckReturnValue

View File

@ -2,17 +2,17 @@ package io.grpc.binder.internal;
import static com.google.common.base.Preconditions.checkState;
import com.google.errorprone.annotations.concurrent.GuardedBy;
import io.grpc.Attributes;
import io.grpc.Metadata;
import io.grpc.internal.ServerListener;
import io.grpc.internal.ServerStream;
import io.grpc.internal.ServerTransport;
import io.grpc.internal.ServerTransportListener;
import javax.annotation.concurrent.GuardedBy;
/**
* Tracks which {@link BinderTransport.BinderServerTransport} are currently active and allows
* invoking a {@link Runnable} only once all transports are terminated.
* Tracks which {@link BinderServerTransport} are currently active and allows invoking a {@link
* Runnable} only once all transports are terminated.
*/
final class ActiveTransportTracker implements ServerListener {
private final ServerListener delegate;

View File

@ -16,10 +16,12 @@
package io.grpc.binder.internal;
import android.content.pm.ServiceInfo;
import android.os.IBinder;
import androidx.annotation.AnyThread;
import androidx.annotation.MainThread;
import io.grpc.Status;
import io.grpc.StatusException;
/** An interface for managing a {@code Binder} connection. */
interface Bindable {
@ -45,6 +47,19 @@ interface Bindable {
void onUnbound(Status reason);
}
/**
* Fetches details about the remote Service from PackageManager without binding to it.
*
* <p>Resolving an untrusted address before binding to it lets you screen out problematic servers
* before giving them a chance to run. However, note that the identity/existence of the resolved
* Service can change between the time this method returns and the time you actually bind/connect
* to it. For example, suppose the target package gets uninstalled or upgraded right after this
* method returns. In {@link Observer#onBound}, you should verify that the server you resolved is
* the same one you connected to.
*/
@AnyThread
ServiceInfo resolve() throws StatusException;
/**
* Attempt to bind with the remote service.
*

View File

@ -0,0 +1,441 @@
/*
* Copyright 2020 The gRPC Authors
*
* 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 io.grpc.binder.internal;
import static com.google.common.base.Preconditions.checkNotNull;
import static io.grpc.binder.ApiConstants.PRE_AUTH_SERVER_OVERRIDE;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import android.content.Context;
import android.content.pm.ServiceInfo;
import android.os.Binder;
import android.os.IBinder;
import android.os.Parcel;
import android.os.Process;
import com.google.common.base.Ticker;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.errorprone.annotations.CheckReturnValue;
import com.google.errorprone.annotations.concurrent.GuardedBy;
import io.grpc.Attributes;
import io.grpc.CallOptions;
import io.grpc.ClientStreamTracer;
import io.grpc.Grpc;
import io.grpc.Internal;
import io.grpc.InternalLogId;
import io.grpc.Metadata;
import io.grpc.MethodDescriptor;
import io.grpc.SecurityLevel;
import io.grpc.Status;
import io.grpc.StatusException;
import io.grpc.binder.AndroidComponentAddress;
import io.grpc.binder.AsyncSecurityPolicy;
import io.grpc.binder.InboundParcelablePolicy;
import io.grpc.binder.SecurityPolicy;
import io.grpc.internal.ClientStream;
import io.grpc.internal.ClientTransportFactory.ClientTransportOptions;
import io.grpc.internal.ConnectionClientTransport;
import io.grpc.internal.FailingClientStream;
import io.grpc.internal.GrpcAttributes;
import io.grpc.internal.GrpcUtil;
import io.grpc.internal.ManagedClientTransport;
import io.grpc.internal.ObjectPool;
import io.grpc.internal.StatsTraceContext;
import java.util.concurrent.Executor;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.atomic.AtomicInteger;
import javax.annotation.Nullable;
import javax.annotation.concurrent.ThreadSafe;
/** Concrete client-side transport implementation. */
@ThreadSafe
@Internal
public final class BinderClientTransport extends BinderTransport
implements ConnectionClientTransport, Bindable.Observer {
private final ObjectPool<? extends Executor> offloadExecutorPool;
private final Executor offloadExecutor;
private final SecurityPolicy securityPolicy;
private final Bindable serviceBinding;
/** Number of ongoing calls which keep this transport "in-use". */
private final AtomicInteger numInUseStreams;
private final long readyTimeoutMillis;
private final PingTracker pingTracker;
private final boolean preAuthorizeServer;
@Nullable private ManagedClientTransport.Listener clientTransportListener;
@GuardedBy("this")
private int latestCallId = FIRST_CALL_ID;
@GuardedBy("this")
private ScheduledFuture<?> readyTimeoutFuture; // != null iff timeout scheduled.
@GuardedBy("this")
@Nullable
private ListenableFuture<Status> authResultFuture; // null before we check auth.
@GuardedBy("this")
@Nullable
private ListenableFuture<Status> preAuthResultFuture; // null before we pre-auth.
/**
* Constructs a new transport instance.
*
* @param factory parameters common to all a Channel's transports
* @param targetAddress the fully resolved and load-balanced server address
* @param options other parameters that can vary as transports come and go within a Channel
*/
public BinderClientTransport(
BinderClientTransportFactory factory,
AndroidComponentAddress targetAddress,
ClientTransportOptions options) {
super(
factory.scheduledExecutorPool,
buildClientAttributes(
options.getEagAttributes(),
factory.sourceContext,
targetAddress,
factory.inboundParcelablePolicy),
factory.binderDecorator,
buildLogId(factory.sourceContext, targetAddress));
this.offloadExecutorPool = factory.offloadExecutorPool;
this.securityPolicy = factory.securityPolicy;
this.offloadExecutor = offloadExecutorPool.getObject();
this.readyTimeoutMillis = factory.readyTimeoutMillis;
Boolean preAuthServerOverride = options.getEagAttributes().get(PRE_AUTH_SERVER_OVERRIDE);
this.preAuthorizeServer =
preAuthServerOverride != null ? preAuthServerOverride : factory.preAuthorizeServers;
numInUseStreams = new AtomicInteger();
pingTracker = new PingTracker(Ticker.systemTicker(), (id) -> sendPing(id));
serviceBinding =
new ServiceBinding(
factory.mainThreadExecutor,
factory.sourceContext,
factory.channelCredentials,
targetAddress.asBindIntent(),
targetAddress.getTargetUser() != null
? targetAddress.getTargetUser()
: factory.defaultTargetUserHandle,
factory.bindServiceFlags.toInteger(),
this);
}
@Override
void releaseExecutors() {
super.releaseExecutors();
offloadExecutorPool.returnObject(offloadExecutor);
}
@Override
public synchronized void onBound(IBinder binder) {
sendSetupTransaction(binderDecorator.decorate(OneWayBinderProxy.wrap(binder, offloadExecutor)));
}
@Override
public synchronized void onUnbound(Status reason) {
shutdownInternal(reason, true);
}
@CheckReturnValue
@Override
public synchronized Runnable start(Listener clientTransportListener) {
this.clientTransportListener = checkNotNull(clientTransportListener);
return () -> {
synchronized (BinderClientTransport.this) {
if (inState(TransportState.NOT_STARTED)) {
setState(TransportState.SETUP);
try {
if (preAuthorizeServer) {
preAuthorize(serviceBinding.resolve());
} else {
serviceBinding.bind();
}
} catch (StatusException e) {
shutdownInternal(e.getStatus(), true);
return;
}
if (readyTimeoutMillis >= 0) {
readyTimeoutFuture =
getScheduledExecutorService()
.schedule(
BinderClientTransport.this::onReadyTimeout,
readyTimeoutMillis,
MILLISECONDS);
}
}
}
};
}
@GuardedBy("this")
private void preAuthorize(ServiceInfo serviceInfo) {
// It's unlikely, but the identity/existence of this Service could change by the time we
// actually connect. It doesn't matter though, because:
// - If pre-auth fails (but would succeed against the server's new state), the grpc-core layer
// will eventually retry using a new transport instance that will see the Service's new state.
// - If pre-auth succeeds (but would fail against the server's new state), we might give an
// unauthorized server a chance to run, but the connection will still fail by SecurityPolicy
// check later in handshake. Pre-auth remains effective at mitigating abuse because malware
// can't typically control the exact timing of its installation.
preAuthResultFuture = checkServerAuthorizationAsync(serviceInfo.applicationInfo.uid);
Futures.addCallback(
preAuthResultFuture,
new FutureCallback<Status>() {
@Override
public void onSuccess(Status result) {
handlePreAuthResult(result);
}
@Override
public void onFailure(Throwable t) {
handleAuthResult(t);
}
},
offloadExecutor);
}
private synchronized void handlePreAuthResult(Status authorization) {
if (inState(TransportState.SETUP)) {
if (!authorization.isOk()) {
shutdownInternal(authorization, true);
} else {
serviceBinding.bind();
}
}
}
private synchronized void onReadyTimeout() {
if (inState(TransportState.SETUP)) {
readyTimeoutFuture = null;
shutdownInternal(
Status.DEADLINE_EXCEEDED.withDescription(
"Connect timeout " + readyTimeoutMillis + "ms lapsed"),
true);
}
}
@Override
public synchronized ClientStream newStream(
final MethodDescriptor<?, ?> method,
final Metadata headers,
final CallOptions callOptions,
ClientStreamTracer[] tracers) {
if (!inState(TransportState.READY)) {
return newFailingClientStream(
isShutdown()
? shutdownStatus
: Status.INTERNAL.withDescription("newStream() before transportReady()"),
attributes,
headers,
tracers);
}
int callId = latestCallId++;
if (latestCallId == LAST_CALL_ID) {
latestCallId = FIRST_CALL_ID;
}
StatsTraceContext statsTraceContext =
StatsTraceContext.newClientContext(tracers, attributes, headers);
Inbound.ClientInbound inbound =
new Inbound.ClientInbound(
this, attributes, callId, GrpcUtil.shouldBeCountedForInUse(callOptions));
if (ongoingCalls.putIfAbsent(callId, inbound) != null) {
Status failure = Status.INTERNAL.withDescription("Clashing call IDs");
shutdownInternal(failure, true);
return newFailingClientStream(failure, attributes, headers, tracers);
} else {
if (inbound.countsForInUse() && numInUseStreams.getAndIncrement() == 0) {
clientTransportListener.transportInUse(true);
}
Outbound.ClientOutbound outbound =
new Outbound.ClientOutbound(this, callId, method, headers, statsTraceContext);
if (method.getType().clientSendsOneMessage()) {
return new SingleMessageClientStream(inbound, outbound, attributes);
} else {
return new MultiMessageClientStream(inbound, outbound, attributes);
}
}
}
@Override
protected void unregisterInbound(Inbound<?> inbound) {
if (inbound.countsForInUse() && numInUseStreams.decrementAndGet() == 0) {
clientTransportListener.transportInUse(false);
}
super.unregisterInbound(inbound);
}
@Override
public void ping(final PingCallback callback, Executor executor) {
pingTracker.startPing(callback, executor);
}
@Override
public synchronized void shutdown(Status reason) {
checkNotNull(reason, "reason");
shutdownInternal(reason, false);
}
@Override
public synchronized void shutdownNow(Status reason) {
checkNotNull(reason, "reason");
shutdownInternal(reason, true);
}
@Override
@GuardedBy("this")
void notifyShutdown(Status status) {
clientTransportListener.transportShutdown(status);
}
@Override
@GuardedBy("this")
void notifyTerminated() {
if (numInUseStreams.getAndSet(0) > 0) {
clientTransportListener.transportInUse(false);
}
if (readyTimeoutFuture != null) {
readyTimeoutFuture.cancel(false);
readyTimeoutFuture = null;
}
if (preAuthResultFuture != null) {
preAuthResultFuture.cancel(false); // No effect if already complete.
}
if (authResultFuture != null) {
authResultFuture.cancel(false); // No effect if already complete.
}
serviceBinding.unbind();
clientTransportListener.transportTerminated();
}
@Override
@GuardedBy("this")
protected void handleSetupTransport(Parcel parcel) {
int remoteUid = Binder.getCallingUid();
attributes = setSecurityAttrs(attributes, remoteUid);
if (inState(TransportState.SETUP)) {
int version = parcel.readInt();
IBinder binder = parcel.readStrongBinder();
if (version != WIRE_FORMAT_VERSION) {
shutdownInternal(Status.UNAVAILABLE.withDescription("Wire format version mismatch"), true);
} else if (binder == null) {
shutdownInternal(
Status.UNAVAILABLE.withDescription("Malformed SETUP_TRANSPORT data"), true);
} else {
authResultFuture = checkServerAuthorizationAsync(remoteUid);
Futures.addCallback(
authResultFuture,
new FutureCallback<Status>() {
@Override
public void onSuccess(Status result) {
handleAuthResult(binder, result);
}
@Override
public void onFailure(Throwable t) {
handleAuthResult(t);
}
},
offloadExecutor);
}
}
}
private ListenableFuture<Status> checkServerAuthorizationAsync(int remoteUid) {
return (securityPolicy instanceof AsyncSecurityPolicy)
? ((AsyncSecurityPolicy) securityPolicy).checkAuthorizationAsync(remoteUid)
: Futures.submit(() -> securityPolicy.checkAuthorization(remoteUid), offloadExecutor);
}
private synchronized void handleAuthResult(IBinder binder, Status authorization) {
if (inState(TransportState.SETUP)) {
if (!authorization.isOk()) {
shutdownInternal(authorization, true);
} else if (!setOutgoingBinder(OneWayBinderProxy.wrap(binder, offloadExecutor))) {
shutdownInternal(
Status.UNAVAILABLE.withDescription("Failed to observe outgoing binder"), true);
} else {
// Check state again, since a failure inside setOutgoingBinder (or a callback it
// triggers), could have shut us down.
if (!isShutdown()) {
setState(TransportState.READY);
attributes = clientTransportListener.filterTransport(attributes);
clientTransportListener.transportReady();
if (readyTimeoutFuture != null) {
readyTimeoutFuture.cancel(false);
readyTimeoutFuture = null;
}
}
}
}
}
private synchronized void handleAuthResult(Throwable t) {
shutdownInternal(
Status.INTERNAL.withDescription("Could not evaluate SecurityPolicy").withCause(t), true);
}
@GuardedBy("this")
@Override
protected void handlePingResponse(Parcel parcel) {
pingTracker.onPingResponse(parcel.readInt());
}
private static ClientStream newFailingClientStream(
Status failure, Attributes attributes, Metadata headers, ClientStreamTracer[] tracers) {
StatsTraceContext statsTraceContext =
StatsTraceContext.newClientContext(tracers, attributes, headers);
statsTraceContext.clientOutboundHeaders();
return new FailingClientStream(failure, tracers);
}
private static InternalLogId buildLogId(
Context sourceContext, AndroidComponentAddress targetAddress) {
return InternalLogId.allocate(
BinderClientTransport.class,
sourceContext.getClass().getSimpleName() + "->" + targetAddress);
}
private static Attributes buildClientAttributes(
Attributes eagAttrs,
Context sourceContext,
AndroidComponentAddress targetAddress,
InboundParcelablePolicy inboundParcelablePolicy) {
return Attributes.newBuilder()
.set(GrpcAttributes.ATTR_SECURITY_LEVEL, SecurityLevel.NONE) // Trust noone for now.
.set(GrpcAttributes.ATTR_CLIENT_EAG_ATTRS, eagAttrs)
.set(Grpc.TRANSPORT_ATTR_LOCAL_ADDR, AndroidComponentAddress.forContext(sourceContext))
.set(Grpc.TRANSPORT_ATTR_REMOTE_ADDR, targetAddress)
.set(INBOUND_PARCELABLE_POLICY, inboundParcelablePolicy)
.build();
}
private static Attributes setSecurityAttrs(Attributes attributes, int uid) {
return attributes.toBuilder()
.set(REMOTE_UID, uid)
.set(
GrpcAttributes.ATTR_SECURITY_LEVEL,
uid == Process.myUid()
? SecurityLevel.PRIVACY_AND_INTEGRITY
: SecurityLevel.INTEGRITY) // TODO: Have the SecrityPolicy decide this.
.build();
}
}

View File

@ -55,6 +55,7 @@ public final class BinderClientTransportFactory implements ClientTransportFactor
final InboundParcelablePolicy inboundParcelablePolicy;
final OneWayBinderProxy.Decorator binderDecorator;
final long readyTimeoutMillis;
final boolean preAuthorizeServers; // TODO(jdcormie): Default to true.
ScheduledExecutorService executorService;
Executor offloadExecutor;
@ -75,18 +76,19 @@ public final class BinderClientTransportFactory implements ClientTransportFactor
inboundParcelablePolicy = checkNotNull(builder.inboundParcelablePolicy);
binderDecorator = checkNotNull(builder.binderDecorator);
readyTimeoutMillis = builder.readyTimeoutMillis;
preAuthorizeServers = builder.preAuthorizeServers;
executorService = scheduledExecutorPool.getObject();
offloadExecutor = offloadExecutorPool.getObject();
}
@Override
public BinderTransport.BinderClientTransport newClientTransport(
public BinderClientTransport newClientTransport(
SocketAddress addr, ClientTransportOptions options, ChannelLogger channelLogger) {
if (closed) {
throw new IllegalStateException("The transport factory is closed.");
}
return new BinderTransport.BinderClientTransport(this, (AndroidComponentAddress) addr, options);
return new BinderClientTransport(this, (AndroidComponentAddress) addr, options);
}
@Override
@ -128,6 +130,7 @@ public final class BinderClientTransportFactory implements ClientTransportFactor
InboundParcelablePolicy inboundParcelablePolicy = InboundParcelablePolicy.DEFAULT;
OneWayBinderProxy.Decorator binderDecorator = OneWayBinderProxy.IDENTITY_DECORATOR;
long readyTimeoutMillis = 60_000;
boolean preAuthorizeServers;
@Override
public BinderClientTransportFactory buildClientTransportFactory() {
@ -139,6 +142,10 @@ public final class BinderClientTransportFactory implements ClientTransportFactor
return this;
}
public Context getSourceContext() {
return sourceContext;
}
public Builder setOffloadExecutorPool(ObjectPool<? extends Executor> offloadExecutorPool) {
this.offloadExecutorPool = checkNotNull(offloadExecutorPool, "offloadExecutorPool");
return this;
@ -216,5 +223,11 @@ public final class BinderClientTransportFactory implements ClientTransportFactor
this.readyTimeoutMillis = readyTimeoutMillis;
return this;
}
/** Whether to check server addresses against the SecurityPolicy *before* binding to them. */
public Builder setPreAuthorizeServers(boolean preAuthorizeServers) {
this.preAuthorizeServers = preAuthorizeServers;
return this;
}
}
}

View File

@ -25,6 +25,7 @@ import android.os.IBinder;
import android.os.Parcel;
import android.os.RemoteException;
import com.google.common.collect.ImmutableList;
import com.google.errorprone.annotations.concurrent.GuardedBy;
import io.grpc.Attributes;
import io.grpc.Grpc;
import io.grpc.InternalChannelz.SocketStats;
@ -48,7 +49,6 @@ import java.util.concurrent.ScheduledExecutorService;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.ThreadSafe;
/**
@ -178,8 +178,8 @@ public final class BinderServer implements InternalServer, LeakSafeOneWayBinder.
serverPolicyChecker,
checkNotNull(executor, "Not started?"));
// Create a new transport and let our listener know about it.
BinderTransport.BinderServerTransport transport =
new BinderTransport.BinderServerTransport(
BinderServerTransport transport =
new BinderServerTransport(
executorServicePool,
attrsBuilder.build(),
streamTracerFactories,

View File

@ -0,0 +1,126 @@
/*
* Copyright 2020 The gRPC Authors
*
* 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 io.grpc.binder.internal;
import android.os.IBinder;
import com.google.errorprone.annotations.concurrent.GuardedBy;
import io.grpc.Attributes;
import io.grpc.Grpc;
import io.grpc.Internal;
import io.grpc.InternalLogId;
import io.grpc.Metadata;
import io.grpc.ServerStreamTracer;
import io.grpc.Status;
import io.grpc.internal.ObjectPool;
import io.grpc.internal.ServerStream;
import io.grpc.internal.ServerTransport;
import io.grpc.internal.ServerTransportListener;
import io.grpc.internal.StatsTraceContext;
import java.util.List;
import java.util.concurrent.ScheduledExecutorService;
import javax.annotation.Nullable;
/** Concrete server-side transport implementation. */
@Internal
public final class BinderServerTransport extends BinderTransport implements ServerTransport {
private final List<ServerStreamTracer.Factory> streamTracerFactories;
@Nullable private ServerTransportListener serverTransportListener;
/**
* Constructs a new transport instance.
*
* @param binderDecorator used to decorate 'callbackBinder', for fault injection.
*/
public BinderServerTransport(
ObjectPool<ScheduledExecutorService> executorServicePool,
Attributes attributes,
List<ServerStreamTracer.Factory> streamTracerFactories,
OneWayBinderProxy.Decorator binderDecorator,
IBinder callbackBinder) {
super(executorServicePool, attributes, binderDecorator, buildLogId(attributes));
this.streamTracerFactories = streamTracerFactories;
// TODO(jdcormie): Plumb in the Server's executor() and use it here instead.
setOutgoingBinder(OneWayBinderProxy.wrap(callbackBinder, getScheduledExecutorService()));
}
public synchronized void setServerTransportListener(
ServerTransportListener serverTransportListener) {
this.serverTransportListener = serverTransportListener;
if (isShutdown()) {
setState(TransportState.SHUTDOWN_TERMINATED);
notifyTerminated();
releaseExecutors();
} else {
sendSetupTransaction();
// Check we're not shutdown again, since a failure inside sendSetupTransaction (or a callback
// it triggers), could have shut us down.
if (!isShutdown()) {
setState(TransportState.READY);
attributes = serverTransportListener.transportReady(attributes);
}
}
}
StatsTraceContext createStatsTraceContext(String methodName, Metadata headers) {
return StatsTraceContext.newServerContext(streamTracerFactories, methodName, headers);
}
synchronized Status startStream(ServerStream stream, String methodName, Metadata headers) {
if (isShutdown()) {
return Status.UNAVAILABLE.withDescription("transport is shutdown");
} else {
serverTransportListener.streamCreated(stream, methodName, headers);
return Status.OK;
}
}
@Override
@GuardedBy("this")
void notifyShutdown(Status status) {
// Nothing to do.
}
@Override
@GuardedBy("this")
void notifyTerminated() {
if (serverTransportListener != null) {
serverTransportListener.transportTerminated();
}
}
@Override
public synchronized void shutdown() {
shutdownInternal(Status.OK, false);
}
@Override
public synchronized void shutdownNow(Status reason) {
shutdownInternal(reason, true);
}
@Override
@Nullable
@GuardedBy("this")
protected Inbound<?> createInbound(int callId) {
return new Inbound.ServerInbound(this, attributes, callId);
}
private static InternalLogId buildLogId(Attributes attributes) {
return InternalLogId.allocate(
BinderServerTransport.class, "from " + attributes.get(Grpc.TRANSPORT_ATTR_REMOTE_ADDR));
}
}

View File

@ -19,68 +19,35 @@ package io.grpc.binder.internal;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.util.concurrent.Futures.immediateFuture;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import android.content.Context;
import android.os.Binder;
import android.os.DeadObjectException;
import android.os.IBinder;
import android.os.Parcel;
import android.os.Process;
import android.os.RemoteException;
import android.os.TransactionTooLargeException;
import androidx.annotation.BinderThread;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Ticker;
import com.google.common.base.Verify;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.errorprone.annotations.concurrent.GuardedBy;
import io.grpc.Attributes;
import io.grpc.CallOptions;
import io.grpc.ClientStreamTracer;
import io.grpc.Grpc;
import io.grpc.Internal;
import io.grpc.InternalChannelz.SocketStats;
import io.grpc.InternalLogId;
import io.grpc.Metadata;
import io.grpc.MethodDescriptor;
import io.grpc.SecurityLevel;
import io.grpc.ServerStreamTracer;
import io.grpc.Status;
import io.grpc.StatusException;
import io.grpc.binder.AndroidComponentAddress;
import io.grpc.binder.AsyncSecurityPolicy;
import io.grpc.binder.InboundParcelablePolicy;
import io.grpc.binder.SecurityPolicy;
import io.grpc.internal.ClientStream;
import io.grpc.internal.ClientTransportFactory.ClientTransportOptions;
import io.grpc.internal.ConnectionClientTransport;
import io.grpc.internal.FailingClientStream;
import io.grpc.internal.GrpcAttributes;
import io.grpc.internal.GrpcUtil;
import io.grpc.internal.ManagedClientTransport;
import io.grpc.internal.ObjectPool;
import io.grpc.internal.ServerStream;
import io.grpc.internal.ServerTransport;
import io.grpc.internal.ServerTransportListener;
import io.grpc.internal.StatsTraceContext;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.CheckReturnValue;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.ThreadSafe;
/**
@ -105,8 +72,7 @@ import javax.annotation.concurrent.ThreadSafe;
* https://github.com/grpc/proposal/blob/master/L73-java-binderchannel/wireformat.md
*/
@ThreadSafe
public abstract class BinderTransport
implements LeakSafeOneWayBinder.TransactionHandler, IBinder.DeathRecipient {
public abstract class BinderTransport implements IBinder.DeathRecipient {
private static final Logger logger = Logger.getLogger(BinderTransport.class.getName());
@ -168,10 +134,10 @@ public abstract class BinderTransport
private static final int RESERVED_TRANSACTIONS = 1000;
/** The first call ID we can use. */
private static final int FIRST_CALL_ID = IBinder.FIRST_CALL_TRANSACTION + RESERVED_TRANSACTIONS;
static final int FIRST_CALL_ID = IBinder.FIRST_CALL_TRANSACTION + RESERVED_TRANSACTIONS;
/** The last call ID we can use. */
private static final int LAST_CALL_ID = IBinder.LAST_CALL_TRANSACTION;
static final int LAST_CALL_ID = IBinder.LAST_CALL_TRANSACTION;
/** The states of this transport. */
protected enum TransportState {
@ -210,12 +176,14 @@ public abstract class BinderTransport
private final FlowController flowController;
/** The number of incoming bytes we've received. */
private final AtomicLong numIncomingBytes;
// Only read/written on @BinderThread.
private long numIncomingBytes;
/** The number of incoming bytes we've told our peer we've received. */
// Only read/written on @BinderThread.
private long acknowledgedIncomingBytes;
private BinderTransport(
protected BinderTransport(
ObjectPool<ScheduledExecutorService> executorServicePool,
Attributes attributes,
OneWayBinderProxy.Decorator binderDecorator,
@ -225,10 +193,9 @@ public abstract class BinderTransport
this.attributes = attributes;
this.logId = logId;
scheduledExecutorService = executorServicePool.getObject();
incomingBinder = new LeakSafeOneWayBinder(this);
incomingBinder = new LeakSafeOneWayBinder(this::handleTransaction);
ongoingCalls = new ConcurrentHashMap<>();
flowController = new FlowController(TRANSACTION_BYTES_WINDOW);
numIncomingBytes = new AtomicLong();
}
// Override in child class.
@ -299,7 +266,10 @@ public abstract class BinderTransport
@Override
public synchronized void binderDied() {
shutdownInternal(Status.UNAVAILABLE.withDescription("Peer process crashed, exited or was killed (binderDied)"), true);
shutdownInternal(
Status.UNAVAILABLE.withDescription(
"Peer process crashed, exited or was killed (binderDied)"),
true);
}
@GuardedBy("this")
@ -423,8 +393,9 @@ public abstract class BinderTransport
}
}
@Override
public final boolean handleTransaction(int code, Parcel parcel) {
@BinderThread
@VisibleForTesting
final boolean handleTransaction(int code, Parcel parcel) {
try {
return handleTransactionInternal(code, parcel);
} catch (RuntimeException e) {
@ -440,6 +411,7 @@ public abstract class BinderTransport
}
}
@BinderThread
private boolean handleTransactionInternal(int code, Parcel parcel) {
if (code < FIRST_CALL_ID) {
synchronized (this) {
@ -483,11 +455,12 @@ public abstract class BinderTransport
if (inbound != null) {
inbound.handleTransaction(parcel);
}
long nib = numIncomingBytes.addAndGet(size);
if ((nib - acknowledgedIncomingBytes) > TRANSACTION_BYTES_WINDOW_FORCE_ACK) {
numIncomingBytes += size;
if ((numIncomingBytes - acknowledgedIncomingBytes) > TRANSACTION_BYTES_WINDOW_FORCE_ACK) {
synchronized (this) {
sendAcknowledgeBytes(checkNotNull(outgoingBinder));
sendAcknowledgeBytes(checkNotNull(outgoingBinder), numIncomingBytes);
}
acknowledgedIncomingBytes = numIncomingBytes;
}
return true;
}
@ -519,10 +492,8 @@ public abstract class BinderTransport
protected void handlePingResponse(Parcel parcel) {}
@GuardedBy("this")
private void sendAcknowledgeBytes(OneWayBinderProxy iBinder) {
private void sendAcknowledgeBytes(OneWayBinderProxy iBinder, long n) {
// Send a transaction to acknowledge reception of incoming data.
long n = numIncomingBytes.get();
acknowledgedIncomingBytes = n;
try (ParcelHolder parcel = ParcelHolder.obtain()) {
parcel.get().writeLong(n);
iBinder.transact(ACKNOWLEDGE_BYTES, parcel);
@ -553,414 +524,6 @@ public abstract class BinderTransport
}
}
/** Concrete client-side transport implementation. */
@ThreadSafe
@Internal
public static final class BinderClientTransport extends BinderTransport
implements ConnectionClientTransport, Bindable.Observer {
private final ObjectPool<? extends Executor> offloadExecutorPool;
private final Executor offloadExecutor;
private final SecurityPolicy securityPolicy;
private final Bindable serviceBinding;
/** Number of ongoing calls which keep this transport "in-use". */
private final AtomicInteger numInUseStreams;
private final long readyTimeoutMillis;
private final PingTracker pingTracker;
@Nullable private ManagedClientTransport.Listener clientTransportListener;
@GuardedBy("this")
private int latestCallId = FIRST_CALL_ID;
@GuardedBy("this")
private ScheduledFuture<?> readyTimeoutFuture; // != null iff timeout scheduled.
/**
* Constructs a new transport instance.
*
* @param factory parameters common to all a Channel's transports
* @param targetAddress the fully resolved and load-balanced server address
* @param options other parameters that can vary as transports come and go within a Channel
*/
public BinderClientTransport(
BinderClientTransportFactory factory,
AndroidComponentAddress targetAddress,
ClientTransportOptions options) {
super(
factory.scheduledExecutorPool,
buildClientAttributes(
options.getEagAttributes(),
factory.sourceContext,
targetAddress,
factory.inboundParcelablePolicy),
factory.binderDecorator,
buildLogId(factory.sourceContext, targetAddress));
this.offloadExecutorPool = factory.offloadExecutorPool;
this.securityPolicy = factory.securityPolicy;
this.offloadExecutor = offloadExecutorPool.getObject();
this.readyTimeoutMillis = factory.readyTimeoutMillis;
numInUseStreams = new AtomicInteger();
pingTracker = new PingTracker(Ticker.systemTicker(), (id) -> sendPing(id));
serviceBinding =
new ServiceBinding(
factory.mainThreadExecutor,
factory.sourceContext,
factory.channelCredentials,
targetAddress.asBindIntent(),
targetAddress.getTargetUser() != null
? targetAddress.getTargetUser()
: factory.defaultTargetUserHandle,
factory.bindServiceFlags.toInteger(),
this);
}
@Override
void releaseExecutors() {
super.releaseExecutors();
offloadExecutorPool.returnObject(offloadExecutor);
}
@Override
public synchronized void onBound(IBinder binder) {
sendSetupTransaction(
binderDecorator.decorate(OneWayBinderProxy.wrap(binder, offloadExecutor)));
}
@Override
public synchronized void onUnbound(Status reason) {
shutdownInternal(reason, true);
}
@CheckReturnValue
@Override
public synchronized Runnable start(ManagedClientTransport.Listener clientTransportListener) {
this.clientTransportListener = checkNotNull(clientTransportListener);
return () -> {
synchronized (BinderClientTransport.this) {
if (inState(TransportState.NOT_STARTED)) {
setState(TransportState.SETUP);
serviceBinding.bind();
if (readyTimeoutMillis >= 0) {
readyTimeoutFuture =
getScheduledExecutorService()
.schedule(
BinderClientTransport.this::onReadyTimeout,
readyTimeoutMillis,
MILLISECONDS);
}
}
}
};
}
private synchronized void onReadyTimeout() {
if (inState(TransportState.SETUP)) {
readyTimeoutFuture = null;
shutdownInternal(
Status.DEADLINE_EXCEEDED.withDescription(
"Connect timeout " + readyTimeoutMillis + "ms lapsed"),
true);
}
}
@Override
public synchronized ClientStream newStream(
final MethodDescriptor<?, ?> method,
final Metadata headers,
final CallOptions callOptions,
ClientStreamTracer[] tracers) {
if (!inState(TransportState.READY)) {
return newFailingClientStream(
isShutdown()
? shutdownStatus
: Status.INTERNAL.withDescription("newStream() before transportReady()"),
attributes,
headers,
tracers);
}
int callId = latestCallId++;
if (latestCallId == LAST_CALL_ID) {
latestCallId = FIRST_CALL_ID;
}
StatsTraceContext statsTraceContext =
StatsTraceContext.newClientContext(tracers, attributes, headers);
Inbound.ClientInbound inbound =
new Inbound.ClientInbound(
this, attributes, callId, GrpcUtil.shouldBeCountedForInUse(callOptions));
if (ongoingCalls.putIfAbsent(callId, inbound) != null) {
Status failure = Status.INTERNAL.withDescription("Clashing call IDs");
shutdownInternal(failure, true);
return newFailingClientStream(failure, attributes, headers, tracers);
} else {
if (inbound.countsForInUse() && numInUseStreams.getAndIncrement() == 0) {
clientTransportListener.transportInUse(true);
}
Outbound.ClientOutbound outbound =
new Outbound.ClientOutbound(this, callId, method, headers, statsTraceContext);
if (method.getType().clientSendsOneMessage()) {
return new SingleMessageClientStream(inbound, outbound, attributes);
} else {
return new MultiMessageClientStream(inbound, outbound, attributes);
}
}
}
@Override
protected void unregisterInbound(Inbound<?> inbound) {
if (inbound.countsForInUse() && numInUseStreams.decrementAndGet() == 0) {
clientTransportListener.transportInUse(false);
}
super.unregisterInbound(inbound);
}
@Override
public void ping(final PingCallback callback, Executor executor) {
pingTracker.startPing(callback, executor);
}
@Override
public synchronized void shutdown(Status reason) {
checkNotNull(reason, "reason");
shutdownInternal(reason, false);
}
@Override
public synchronized void shutdownNow(Status reason) {
checkNotNull(reason, "reason");
shutdownInternal(reason, true);
}
@Override
@GuardedBy("this")
void notifyShutdown(Status status) {
clientTransportListener.transportShutdown(status);
}
@Override
@GuardedBy("this")
void notifyTerminated() {
if (numInUseStreams.getAndSet(0) > 0) {
clientTransportListener.transportInUse(false);
}
if (readyTimeoutFuture != null) {
readyTimeoutFuture.cancel(false);
readyTimeoutFuture = null;
}
serviceBinding.unbind();
clientTransportListener.transportTerminated();
}
@Override
@GuardedBy("this")
protected void handleSetupTransport(Parcel parcel) {
int remoteUid = Binder.getCallingUid();
attributes = setSecurityAttrs(attributes, remoteUid);
if (inState(TransportState.SETUP)) {
int version = parcel.readInt();
IBinder binder = parcel.readStrongBinder();
if (version != WIRE_FORMAT_VERSION) {
shutdownInternal(
Status.UNAVAILABLE.withDescription("Wire format version mismatch"), true);
} else if (binder == null) {
shutdownInternal(
Status.UNAVAILABLE.withDescription("Malformed SETUP_TRANSPORT data"), true);
} else {
ListenableFuture<Status> authFuture =
(securityPolicy instanceof AsyncSecurityPolicy)
? ((AsyncSecurityPolicy) securityPolicy).checkAuthorizationAsync(remoteUid)
: Futures.submit(
() -> securityPolicy.checkAuthorization(remoteUid), offloadExecutor);
Futures.addCallback(
authFuture,
new FutureCallback<Status>() {
@Override
public void onSuccess(Status result) {
handleAuthResult(binder, result);
}
@Override
public void onFailure(Throwable t) {
handleAuthResult(t);
}
},
offloadExecutor);
}
}
}
private synchronized void handleAuthResult(IBinder binder, Status authorization) {
if (inState(TransportState.SETUP)) {
if (!authorization.isOk()) {
shutdownInternal(authorization, true);
} else if (!setOutgoingBinder(OneWayBinderProxy.wrap(binder, offloadExecutor))) {
shutdownInternal(
Status.UNAVAILABLE.withDescription("Failed to observe outgoing binder"), true);
} else {
// Check state again, since a failure inside setOutgoingBinder (or a callback it
// triggers), could have shut us down.
if (!isShutdown()) {
setState(TransportState.READY);
attributes = clientTransportListener.filterTransport(attributes);
clientTransportListener.transportReady();
if (readyTimeoutFuture != null) {
readyTimeoutFuture.cancel(false);
readyTimeoutFuture = null;
}
}
}
}
}
private synchronized void handleAuthResult(Throwable t) {
shutdownInternal(
Status.INTERNAL.withDescription("Could not evaluate SecurityPolicy").withCause(t), true);
}
@GuardedBy("this")
@Override
protected void handlePingResponse(Parcel parcel) {
pingTracker.onPingResponse(parcel.readInt());
}
private static ClientStream newFailingClientStream(
Status failure, Attributes attributes, Metadata headers, ClientStreamTracer[] tracers) {
StatsTraceContext statsTraceContext =
StatsTraceContext.newClientContext(tracers, attributes, headers);
statsTraceContext.clientOutboundHeaders();
return new FailingClientStream(failure, tracers);
}
private static InternalLogId buildLogId(
Context sourceContext, AndroidComponentAddress targetAddress) {
return InternalLogId.allocate(
BinderClientTransport.class,
sourceContext.getClass().getSimpleName() + "->" + targetAddress);
}
private static Attributes buildClientAttributes(
Attributes eagAttrs,
Context sourceContext,
AndroidComponentAddress targetAddress,
InboundParcelablePolicy inboundParcelablePolicy) {
return Attributes.newBuilder()
.set(GrpcAttributes.ATTR_SECURITY_LEVEL, SecurityLevel.NONE) // Trust noone for now.
.set(GrpcAttributes.ATTR_CLIENT_EAG_ATTRS, eagAttrs)
.set(Grpc.TRANSPORT_ATTR_LOCAL_ADDR, AndroidComponentAddress.forContext(sourceContext))
.set(Grpc.TRANSPORT_ATTR_REMOTE_ADDR, targetAddress)
.set(INBOUND_PARCELABLE_POLICY, inboundParcelablePolicy)
.build();
}
private static Attributes setSecurityAttrs(Attributes attributes, int uid) {
return attributes.toBuilder()
.set(REMOTE_UID, uid)
.set(
GrpcAttributes.ATTR_SECURITY_LEVEL,
uid == Process.myUid()
? SecurityLevel.PRIVACY_AND_INTEGRITY
: SecurityLevel.INTEGRITY) // TODO: Have the SecrityPolicy decide this.
.build();
}
}
/** Concrete server-side transport implementation. */
@Internal
public static final class BinderServerTransport extends BinderTransport
implements ServerTransport {
private final List<ServerStreamTracer.Factory> streamTracerFactories;
@Nullable private ServerTransportListener serverTransportListener;
/**
* Constructs a new transport instance.
*
* @param binderDecorator used to decorate 'callbackBinder', for fault injection.
*/
public BinderServerTransport(
ObjectPool<ScheduledExecutorService> executorServicePool,
Attributes attributes,
List<ServerStreamTracer.Factory> streamTracerFactories,
OneWayBinderProxy.Decorator binderDecorator,
IBinder callbackBinder) {
super(executorServicePool, attributes, binderDecorator, buildLogId(attributes));
this.streamTracerFactories = streamTracerFactories;
// TODO(jdcormie): Plumb in the Server's executor() and use it here instead.
setOutgoingBinder(OneWayBinderProxy.wrap(callbackBinder, getScheduledExecutorService()));
}
public synchronized void setServerTransportListener(
ServerTransportListener serverTransportListener) {
this.serverTransportListener = serverTransportListener;
if (isShutdown()) {
setState(TransportState.SHUTDOWN_TERMINATED);
notifyTerminated();
releaseExecutors();
} else {
sendSetupTransaction();
// Check we're not shutdown again, since a failure inside sendSetupTransaction (or a
// callback it triggers), could have shut us down.
if (!isShutdown()) {
setState(TransportState.READY);
attributes = serverTransportListener.transportReady(attributes);
}
}
}
StatsTraceContext createStatsTraceContext(String methodName, Metadata headers) {
return StatsTraceContext.newServerContext(streamTracerFactories, methodName, headers);
}
synchronized Status startStream(ServerStream stream, String methodName, Metadata headers) {
if (isShutdown()) {
return Status.UNAVAILABLE.withDescription("transport is shutdown");
} else {
serverTransportListener.streamCreated(stream, methodName, headers);
return Status.OK;
}
}
@Override
@GuardedBy("this")
void notifyShutdown(Status status) {
// Nothing to do.
}
@Override
@GuardedBy("this")
void notifyTerminated() {
if (serverTransportListener != null) {
serverTransportListener.transportTerminated();
}
}
@Override
public synchronized void shutdown() {
shutdownInternal(Status.OK, false);
}
@Override
public synchronized void shutdownNow(Status reason) {
shutdownInternal(reason, true);
}
@Override
@Nullable
@GuardedBy("this")
protected Inbound<?> createInbound(int callId) {
return new Inbound.ServerInbound(this, attributes, callId);
}
private static InternalLogId buildLogId(Attributes attributes) {
return InternalLogId.allocate(
BinderServerTransport.class, "from " + attributes.get(Grpc.TRANSPORT_ATTR_REMOTE_ADDR));
}
}
private static void checkTransition(TransportState current, TransportState next) {
switch (next) {
case SETUP:

View File

@ -20,6 +20,7 @@ import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.errorprone.annotations.CheckReturnValue;
import io.grpc.Attributes;
import io.grpc.Internal;
import io.grpc.Metadata;
@ -35,7 +36,6 @@ import java.util.concurrent.CancellationException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import javax.annotation.CheckReturnValue;
import javax.annotation.Nullable;
/**

View File

@ -15,7 +15,7 @@
*/
package io.grpc.binder.internal;
import javax.annotation.concurrent.GuardedBy;
import com.google.errorprone.annotations.concurrent.GuardedBy;
/** Keeps track of the number of bytes on the wire in a single direction. */
final class FlowController {

View File

@ -20,6 +20,7 @@ import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import android.os.Parcel;
import com.google.errorprone.annotations.concurrent.GuardedBy;
import io.grpc.Attributes;
import io.grpc.Metadata;
import io.grpc.Status;
@ -34,7 +35,6 @@ import io.grpc.internal.StreamListener;
import java.io.InputStream;
import java.util.ArrayList;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
/**
* Handles incoming binder transactions for a single stream, turning those transactions into calls
@ -610,10 +610,9 @@ abstract class Inbound<L extends StreamListener> implements StreamListener.Messa
// Server-side inbound transactions.
static final class ServerInbound extends Inbound<ServerStreamListener> {
private final BinderTransport.BinderServerTransport serverTransport;
private final BinderServerTransport serverTransport;
ServerInbound(
BinderTransport.BinderServerTransport transport, Attributes attributes, int callId) {
ServerInbound(BinderServerTransport transport, Attributes attributes, int callId) {
super(transport, attributes, callId);
this.serverTransport = transport;
}

View File

@ -0,0 +1,299 @@
/*
* Copyright 2025 The gRPC Authors
*
* 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 io.grpc.binder.internal;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static io.grpc.binder.internal.SystemApis.createContextAsUser;
import android.annotation.SuppressLint;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.os.Build;
import android.os.UserHandle;
import com.google.common.collect.ImmutableMap;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import io.grpc.Attributes;
import io.grpc.EquivalentAddressGroup;
import io.grpc.NameResolver;
import io.grpc.Status;
import io.grpc.StatusException;
import io.grpc.StatusOr;
import io.grpc.SynchronizationContext;
import io.grpc.binder.AndroidComponentAddress;
import io.grpc.binder.ApiConstants;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Executor;
import javax.annotation.Nullable;
/**
* A {@link NameResolver} that resolves Android-standard "intent:" target URIs to the list of {@link
* AndroidComponentAddress} that match it by manifest intent filter.
*/
final class IntentNameResolver extends NameResolver {
private final Intent targetIntent; // Never mutated.
@Nullable private final UserHandle targetUser; // null means same user that hosts this process.
private final Context targetUserContext;
private final Executor offloadExecutor;
private final Executor sequentialExecutor;
private final SynchronizationContext syncContext;
private final ServiceConfigParser serviceConfigParser;
// Accessed only on `sequentialExecutor`
@Nullable private PackageChangeReceiver receiver; // != null when registered
// Accessed only on 'syncContext'.
private boolean shutdown;
private boolean queryNeeded;
@Nullable private Listener2 listener; // != null after start().
@Nullable private ListenableFuture<ResolutionResult> queryResultFuture; // != null when querying.
@EquivalentAddressGroup.Attr
private static final Attributes CONSTANT_EAG_ATTRS =
Attributes.newBuilder()
// Servers discovered in PackageManager are especially untrusted. After all, any app can
// declare any intent filter it wants! Require pre-authorization so that unauthorized apps
// don't even get a chance to run onCreate()/onBind().
.set(ApiConstants.PRE_AUTH_SERVER_OVERRIDE, true)
.build();
IntentNameResolver(Intent targetIntent, Args args) {
this.targetIntent = targetIntent;
this.targetUser = args.getArg(ApiConstants.TARGET_ANDROID_USER);
Context context =
checkNotNull(args.getArg(ApiConstants.SOURCE_ANDROID_CONTEXT), "SOURCE_ANDROID_CONTEXT")
.getApplicationContext();
this.targetUserContext =
targetUser != null ? createContextForTargetUserOrThrow(context, targetUser) : context;
// This Executor is nominally optional but all grpc-java Channels provide it since 1.25.
this.offloadExecutor =
checkNotNull(args.getOffloadExecutor(), "NameResolver.Args.getOffloadExecutor()");
// Ensures start()'s work runs before resolve()'s' work, and both run before shutdown()'s.
this.sequentialExecutor = MoreExecutors.newSequentialExecutor(offloadExecutor);
this.syncContext = args.getSynchronizationContext();
this.serviceConfigParser = args.getServiceConfigParser();
}
private static Context createContextForTargetUserOrThrow(Context context, UserHandle targetUser) {
try {
return createContextAsUser(context, targetUser, /* flags= */ 0); // @SystemApi since R.
} catch (ReflectiveOperationException e) {
throw new IllegalArgumentException(
"TARGET_ANDROID_USER NameResolver.Arg requires SDK_INT >= R and @SystemApi visibility");
}
}
@Override
public void start(Listener2 listener) {
checkState(this.listener == null, "Already started!");
checkState(!shutdown, "Resolver is shutdown");
this.listener = checkNotNull(listener);
sequentialExecutor.execute(this::registerReceiver);
resolve();
}
@Override
public void refresh() {
checkState(listener != null, "Not started!");
resolve();
}
private void resolve() {
syncContext.throwIfNotInThisSynchronizationContext();
if (shutdown) {
return;
}
// We can't block here in 'syncContext' so we offload PackageManager queries to an Executor.
// But offloading complicates things a bit because other calls can arrive while we wait for the
// results. We keep 'listener' up-to-date with the latest state in PackageManager by doing:
// 1. Only one query-and-report-to-listener operation at a time.
// 2. At least one query-and-report-to-listener AFTER every PackageManager state change.
if (queryResultFuture == null) {
queryResultFuture = Futures.submit(this::queryPackageManager, sequentialExecutor);
queryResultFuture.addListener(this::onQueryComplete, syncContext);
} else {
// There's already a query in-flight but (2) says we need at least one more. Our sequential
// Executor would be enough to ensure (1) but we also don't want a backlog of work to build up
// if things change rapidly. Just make a note to start a new query when this one finishes.
queryNeeded = true;
}
}
private void onQueryComplete() {
syncContext.throwIfNotInThisSynchronizationContext();
checkState(queryResultFuture != null);
checkState(queryResultFuture.isDone());
// Capture non-final `listener` here while we're on 'syncContext'.
Listener2 listener = checkNotNull(this.listener);
Futures.addCallback(
queryResultFuture, // Already isDone() so this execute()s immediately.
new FutureCallback<ResolutionResult>() {
@Override
public void onSuccess(ResolutionResult result) {
listener.onResult2(result);
}
@Override
public void onFailure(Throwable t) {
listener.onResult2(
ResolutionResult.newBuilder()
.setAddressesOrError(StatusOr.fromStatus(Status.fromThrowable(t)))
.build());
}
},
syncContext); // Already on 'syncContext' but addCallback() is faster than try/get/catch.
queryResultFuture = null;
if (queryNeeded) {
// One or more resolve() requests arrived while we were working on the last one. Just one
// follow-on query can subsume all of them.
queryNeeded = false;
resolve();
}
}
@Override
public String getServiceAuthority() {
return "localhost";
}
@Override
public void shutdown() {
syncContext.throwIfNotInThisSynchronizationContext();
if (!shutdown) {
shutdown = true;
sequentialExecutor.execute(this::maybeUnregisterReceiver);
}
}
private ResolutionResult queryPackageManager() throws StatusException {
List<ResolveInfo> queryResults = queryIntentServices(targetIntent);
// Avoid a spurious UnsafeIntentLaunchViolation later. Since S, Android's StrictMode is very
// conservative, marking any Intent parsed from a string as suspicious and complaining when you
// bind to it. But all this is pointless with grpc-binder, which already goes even further by
// not trusting addresses at all! Instead, we rely on SecurityPolicy, which won't allow a
// connection to an unauthorized server UID no matter how you got there.
Intent prototypeBindIntent = sanitize(targetIntent);
// Model each matching android.app.Service as an EAG (server) with a single address.
List<EquivalentAddressGroup> addresses = new ArrayList<>();
for (ResolveInfo resolveInfo : queryResults) {
prototypeBindIntent.setComponent(
new ComponentName(resolveInfo.serviceInfo.packageName, resolveInfo.serviceInfo.name));
addresses.add(
new EquivalentAddressGroup(
AndroidComponentAddress.newBuilder()
.setBindIntent(prototypeBindIntent) // Makes a copy.
.setTargetUser(targetUser)
.build(),
CONSTANT_EAG_ATTRS));
}
return ResolutionResult.newBuilder()
.setAddressesOrError(StatusOr.fromValue(addresses))
// Empty service config means we get the default 'pick_first' load balancing policy.
.setServiceConfig(serviceConfigParser.parseServiceConfig(ImmutableMap.of()))
.build();
}
private List<ResolveInfo> queryIntentServices(Intent intent) throws StatusException {
int flags = 0;
if (Build.VERSION.SDK_INT >= 29) {
// Don't match direct-boot-unaware Services that can't presently be created. We'll query again
// after the user is unlocked. The MATCH_DIRECT_BOOT_AUTO behavior is actually the default but
// being explicit here avoids an android.os.strictmode.ImplicitDirectBootViolation.
flags |= PackageManager.MATCH_DIRECT_BOOT_AUTO;
}
List<ResolveInfo> intentServices =
targetUserContext.getPackageManager().queryIntentServices(intent, flags);
if (intentServices == null || intentServices.isEmpty()) {
// Must be the same as when ServiceBinding's call to bindService() returns false.
throw Status.UNIMPLEMENTED
.withDescription("Service not found for intent " + intent)
.asException();
}
return intentServices;
}
// Returns a new Intent with the same action, data and categories as 'input'.
private static Intent sanitize(Intent input) {
Intent output = new Intent();
output.setAction(input.getAction());
output.setData(input.getData());
Set<String> categories = input.getCategories();
if (categories != null) {
for (String category : categories) {
output.addCategory(category);
}
}
// Don't bother copying extras and flags since AndroidComponentAddress (rightly) ignores them.
// Don't bother copying package or ComponentName either, since we're about to set that.
return output;
}
final class PackageChangeReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
// Get off the main thread and into the correct SynchronizationContext.
syncContext.executeLater(IntentNameResolver.this::resolve);
offloadExecutor.execute(syncContext::drain);
}
}
@SuppressLint("UnprotectedReceiver") // All of these are protected system broadcasts.
private void registerReceiver() {
checkState(receiver == null, "Already registered!");
receiver = new PackageChangeReceiver();
IntentFilter filter = new IntentFilter();
filter.addDataScheme("package");
filter.addAction(Intent.ACTION_PACKAGE_ADDED);
filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
filter.addAction(Intent.ACTION_PACKAGE_REPLACED);
targetUserContext.registerReceiver(receiver, filter);
if (Build.VERSION.SDK_INT >= 24) {
// Clients running in direct boot mode must refresh() when the user is unlocked because
// that's when `directBootAware=false` services become visible in queryIntentServices()
// results. ACTION_BOOT_COMPLETED would work too but it's delivered with lower priority.
targetUserContext.registerReceiver(receiver, new IntentFilter(Intent.ACTION_USER_UNLOCKED));
}
}
private void maybeUnregisterReceiver() {
if (receiver != null) { // NameResolver API contract appears to allow shutdown without start().
targetUserContext.unregisterReceiver(receiver);
receiver = null;
}
}
}

View File

@ -0,0 +1,77 @@
/*
* Copyright 2025 The gRPC Authors
*
* 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 io.grpc.binder.internal;
import static android.content.Intent.URI_INTENT_SCHEME;
import android.content.Intent;
import com.google.common.collect.ImmutableSet;
import io.grpc.NameResolver;
import io.grpc.NameResolver.Args;
import io.grpc.NameResolverProvider;
import io.grpc.binder.AndroidComponentAddress;
import java.net.SocketAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Objects;
import javax.annotation.Nullable;
/**
* A {@link NameResolverProvider} that handles Android-standard "intent:" target URIs, resolving
* them to the list of {@link AndroidComponentAddress} that match by manifest intent filter.
*/
public final class IntentNameResolverProvider extends NameResolverProvider {
static final String ANDROID_INTENT_SCHEME = "intent";
@Override
public String getDefaultScheme() {
return ANDROID_INTENT_SCHEME;
}
@Nullable
@Override
public NameResolver newNameResolver(URI targetUri, final Args args) {
if (Objects.equals(targetUri.getScheme(), ANDROID_INTENT_SCHEME)) {
return new IntentNameResolver(parseUriArg(targetUri), args);
} else {
return null;
}
}
@Override
public boolean isAvailable() {
return true;
}
@Override
public int priority() {
return 3; // Lower than DNS so we don't accidentally become the default scheme for a registry.
}
@Override
public ImmutableSet<Class<? extends SocketAddress>> getProducedSocketAddressTypes() {
return ImmutableSet.of(AndroidComponentAddress.class);
}
private static Intent parseUriArg(URI targetUri) {
try {
return Intent.parseUri(targetUri.toString(), URI_INTENT_SCHEME);
} catch (URISyntaxException e) {
throw new IllegalArgumentException(e);
}
}
}

View File

@ -19,6 +19,7 @@ package io.grpc.binder.internal;
import android.os.Binder;
import android.os.IBinder;
import android.os.Parcel;
import androidx.annotation.BinderThread;
import io.grpc.Internal;
import java.util.logging.Level;
import java.util.logging.Logger;
@ -58,6 +59,7 @@ public final class LeakSafeOneWayBinder extends Binder {
* @return the value to return from {@link Binder#onTransact}. NB: "oneway" semantics mean this
* result will not delivered to the caller of {@link IBinder#transact}
*/
@BinderThread
boolean handleTransaction(int code, Parcel data);
}

View File

@ -19,9 +19,9 @@ package io.grpc.binder.internal;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static io.grpc.internal.GrpcUtil.TIMEOUT_KEY;
import static java.lang.Math.max;
import android.os.Parcel;
import com.google.errorprone.annotations.concurrent.GuardedBy;
import io.grpc.Deadline;
import io.grpc.Metadata;
import io.grpc.MethodDescriptor;
@ -34,7 +34,6 @@ import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
/**
* Sends the set of outbound transactions for a single BinderStream (rpc).
@ -397,8 +396,7 @@ abstract class Outbound {
@GuardedBy("this")
void setDeadline(Deadline deadline) {
headers.discardAll(TIMEOUT_KEY);
long effectiveTimeoutNanos = max(0, deadline.timeRemaining(TimeUnit.NANOSECONDS));
headers.put(TIMEOUT_KEY, effectiveTimeoutNanos);
headers.put(TIMEOUT_KEY, deadline.timeRemaining(TimeUnit.NANOSECONDS));
}
}

View File

@ -17,12 +17,12 @@
package io.grpc.binder.internal;
import com.google.common.base.Ticker;
import com.google.errorprone.annotations.concurrent.GuardedBy;
import io.grpc.Status;
import io.grpc.StatusException;
import io.grpc.internal.ClientTransport.PingCallback;
import java.util.concurrent.Executor;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
/**
* Tracks an ongoing ping request for a client-side binder transport. We only handle a single active
@ -99,7 +99,7 @@ final class PingTracker {
private synchronized void fail(Status status) {
if (!done) {
done = true;
executor.execute(() -> callback.onFailure(status.asException()));
executor.execute(() -> callback.onFailure(status));
}
}

View File

@ -23,18 +23,26 @@ import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.os.Build;
import android.os.IBinder;
import android.os.UserHandle;
import androidx.annotation.AnyThread;
import androidx.annotation.MainThread;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.VerifyException;
import com.google.errorprone.annotations.concurrent.GuardedBy;
import io.grpc.Status;
import io.grpc.StatusException;
import io.grpc.binder.BinderChannelCredentials;
import java.lang.reflect.Method;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.ThreadSafe;
/**
@ -85,6 +93,8 @@ final class ServiceBinding implements Bindable, ServiceConnection {
private final Observer observer;
private final Executor mainThreadExecutor;
private static volatile Method queryIntentServicesAsUserMethod;
@GuardedBy("this")
private State state;
@ -183,18 +193,27 @@ final class ServiceBinding implements Bindable, ServiceConnection {
bindResult = context.bindService(bindIntent, conn, flags);
break;
case BIND_SERVICE_AS_USER:
bindResult = context.bindServiceAsUser(bindIntent, conn, flags, targetUserHandle);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
bindResult = context.bindServiceAsUser(bindIntent, conn, flags, targetUserHandle);
} else {
return Status.INTERNAL.withDescription("Cross user Channel requires Android R+");
}
break;
case DEVICE_POLICY_BIND_SEVICE_ADMIN:
DevicePolicyManager devicePolicyManager =
(DevicePolicyManager) context.getSystemService(Context.DEVICE_POLICY_SERVICE);
bindResult =
devicePolicyManager.bindDeviceAdminServiceAsUser(
channelCredentials.getDevicePolicyAdminComponentName(),
bindIntent,
conn,
flags,
targetUserHandle);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
bindResult =
devicePolicyManager.bindDeviceAdminServiceAsUser(
channelCredentials.getDevicePolicyAdminComponentName(),
bindIntent,
conn,
flags,
targetUserHandle);
} else {
return Status.INTERNAL.withDescription(
"Device policy admin binding requires Android R+");
}
break;
}
if (!bindResult) {
@ -247,6 +266,71 @@ final class ServiceBinding implements Bindable, ServiceConnection {
}
}
// Sadly the PackageManager#resolveServiceAsUser() API we need isn't part of the SDK or even a
// @SystemApi as of this writing. Modern Android prevents even system apps from calling it, by any
// means (https://developer.android.com/guide/app-compatibility/restrictions-non-sdk-interfaces).
// So instead we call queryIntentServicesAsUser(), which does more than we need but *is* a
// @SystemApi in all the SDK versions where we support cross-user Channels.
@Nullable
private static ResolveInfo resolveServiceAsUser(
PackageManager packageManager, Intent intent, int flags, UserHandle targetUserHandle) {
List<ResolveInfo> results =
queryIntentServicesAsUser(packageManager, intent, flags, targetUserHandle);
// The first query result is "what would be returned by resolveService", per the javadoc.
return (results != null && !results.isEmpty()) ? results.get(0) : null;
}
// The cross-user Channel feature requires the client to be a system app so we assume @SystemApi
// queryIntentServicesAsUser() is visible to us at runtime. It would be visible at build time too,
// if our host system app were written to call it directly. We only have to use reflection here
// because grpc-java is a library built outside the Android source tree where the compiler can't
// see the "non-SDK" @SystemApis that we need.
@Nullable
@SuppressWarnings("unchecked") // Safe by PackageManager#queryIntentServicesAsUser spec in AOSP.
private static List<ResolveInfo> queryIntentServicesAsUser(
PackageManager packageManager, Intent intent, int flags, UserHandle targetUserHandle) {
try {
if (queryIntentServicesAsUserMethod == null) {
synchronized (ServiceBinding.class) {
if (queryIntentServicesAsUserMethod == null) {
queryIntentServicesAsUserMethod =
PackageManager.class.getMethod(
"queryIntentServicesAsUser", Intent.class, int.class, UserHandle.class);
}
}
}
return (List<ResolveInfo>)
queryIntentServicesAsUserMethod.invoke(packageManager, intent, flags, targetUserHandle);
} catch (ReflectiveOperationException e) {
throw new VerifyException(e);
}
}
@AnyThread
@Override
public ServiceInfo resolve() throws StatusException {
checkState(sourceContext != null);
PackageManager packageManager = sourceContext.getPackageManager();
int flags = 0;
if (Build.VERSION.SDK_INT >= 29) {
// Filter out non-'directBootAware' <service>s when 'targetUserHandle' is locked. Here's why:
// Callers want 'bindIntent' to #resolve() to the same thing a follow-up call to #bind() will.
// But bindService() *always* ignores services that can't presently be created for lack of
// 'directBootAware'-ness. This flag explicitly tells resolveService() to act the same way.
flags |= PackageManager.MATCH_DIRECT_BOOT_AUTO;
}
ResolveInfo resolveInfo =
targetUserHandle != null
? resolveServiceAsUser(packageManager, bindIntent, flags, targetUserHandle)
: packageManager.resolveService(bindIntent, flags);
if (resolveInfo == null) {
throw Status.UNIMPLEMENTED // Same status code as when bindService() returns false.
.withDescription("resolveService(" + bindIntent + " / " + targetUserHandle + ") was null")
.asException();
}
return resolveInfo.serviceInfo;
}
@MainThread
private void clearReferences() {
sourceContext = null;

View File

@ -0,0 +1,60 @@
/*
* Copyright 2025 The gRPC Authors
*
* 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 io.grpc.binder.internal;
import android.content.Context;
import android.os.UserHandle;
import java.lang.reflect.Method;
/**
* A collection of static methods that wrap hidden Android "System APIs."
*
* <p>grpc-java can't call Android methods marked @SystemApi directly, even though many of our users
* are "system apps" entitled to do so. Being a library built outside the Android source tree, these
* "non-SDK" elements simply don't exist from our compiler's perspective. Instead we resort to
* reflection but use the static wrappers found here to keep call sites readable and type safe.
*
* <p>Modern Android's JRE also limits the visibility of these methods at *runtime*. Only certain
* privileged apps installed on the system image app can call them, even using reflection, and this
* wrapper doesn't change that. Callers are responsible for ensuring that the host app actually has
* the ability to call @SystemApis and all methods throw {@link ReflectiveOperationException} as a
* reminder to do that. See
* https://developer.android.com/guide/app-compatibility/restrictions-non-sdk-interfaces for more.
*/
final class SystemApis {
private static volatile Method createContextAsUserMethod;
// Not to be instantiated.
private SystemApis() {}
/**
* Returns a new Context object whose methods act as if they were running in the given user.
*
* @throws ReflectiveOperationException if SDK_INT < R or host app lacks @SystemApi visibility
*/
public static Context createContextAsUser(Context context, UserHandle userHandle, int flags)
throws ReflectiveOperationException {
if (createContextAsUserMethod == null) {
synchronized (SystemApis.class) {
if (createContextAsUserMethod == null) {
createContextAsUserMethod =
Context.class.getMethod("createContextAsUser", UserHandle.class, int.class);
}
}
}
return (Context) createContextAsUserMethod.invoke(context, userHandle, flags);
}
}

View File

@ -22,12 +22,13 @@ import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static org.robolectric.Shadows.shadowOf;
import android.app.Application;
import android.content.ComponentName;
import android.content.Intent;
import android.os.IBinder;
import android.os.Looper;
import androidx.lifecycle.LifecycleService;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.ServiceInfo;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.core.content.pm.ApplicationInfoBuilder;
import androidx.test.core.content.pm.PackageInfoBuilder;
import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
@ -42,90 +43,143 @@ import io.grpc.ServerMethodDefinition;
import io.grpc.ServerServiceDefinition;
import io.grpc.Status;
import io.grpc.StatusRuntimeException;
import io.grpc.binder.internal.MainThreadScheduledExecutorService;
import io.grpc.protobuf.lite.ProtoLiteUtils;
import io.grpc.stub.ClientCalls;
import io.grpc.stub.ServerCalls;
import java.io.IOException;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ScheduledExecutorService;
import javax.annotation.Nullable;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.android.controller.ServiceController;
import org.robolectric.ParameterizedRobolectricTestRunner;
import org.robolectric.ParameterizedRobolectricTestRunner.Parameter;
import org.robolectric.ParameterizedRobolectricTestRunner.Parameters;
import org.robolectric.annotation.LooperMode;
import org.robolectric.annotation.LooperMode.Mode;
@RunWith(RobolectricTestRunner.class)
@RunWith(ParameterizedRobolectricTestRunner.class)
@LooperMode(Mode.INSTRUMENTATION_TEST)
public final class RobolectricBinderSecurityTest {
private static final String SERVICE_NAME = "fake_service";
private static final String FULL_METHOD_NAME = "fake_service/fake_method";
private final Application context = ApplicationProvider.getApplicationContext();
private ServiceController<SomeService> controller;
private SomeService service;
private final ArrayBlockingQueue<SettableFuture<Status>> statusesToSet =
new ArrayBlockingQueue<>(128);
private ManagedChannel channel;
private Server server;
@Parameter public boolean preAuthServersParam;
@Parameters(name = "preAuthServersParam={0}")
public static ImmutableList<Boolean> data() {
return ImmutableList.of(true, false);
}
@Before
public void setUp() {
controller = Robolectric.buildService(SomeService.class);
service = controller.create().get();
ApplicationInfo serverAppInfo =
ApplicationInfoBuilder.newBuilder().setPackageName(context.getPackageName()).build();
serverAppInfo.uid = android.os.Process.myUid();
PackageInfo serverPkgInfo =
PackageInfoBuilder.newBuilder()
.setPackageName(serverAppInfo.packageName)
.setApplicationInfo(serverAppInfo)
.build();
shadowOf(context.getPackageManager()).installPackage(serverPkgInfo);
AndroidComponentAddress listenAddress = AndroidComponentAddress.forContext(service);
ScheduledExecutorService executor = service.getExecutor();
ServiceInfo serviceInfo = new ServiceInfo();
serviceInfo.name = "SomeService";
serviceInfo.packageName = serverAppInfo.packageName;
serviceInfo.applicationInfo = serverAppInfo;
shadowOf(context.getPackageManager()).addOrUpdateService(serviceInfo);
AndroidComponentAddress listenAddress =
AndroidComponentAddress.forRemoteComponent(serviceInfo.packageName, serviceInfo.name);
MethodDescriptor<Empty, Empty> methodDesc = getMethodDescriptor();
ServerCallHandler<Empty, Empty> callHandler =
ServerCalls.asyncUnaryCall(
(req, respObserver) -> {
respObserver.onNext(req);
respObserver.onCompleted();
});
ServerMethodDefinition<Empty, Empty> methodDef =
ServerMethodDefinition.create(methodDesc, callHandler);
ServerServiceDefinition def =
ServerServiceDefinition.builder(SERVICE_NAME).addMethod(methodDef).build();
IBinderReceiver binderReceiver = new IBinderReceiver();
server =
BinderServerBuilder.forAddress(listenAddress, binderReceiver)
.addService(def)
.securityPolicy(
ServerSecurityPolicy.newBuilder()
.servicePolicy(
SERVICE_NAME,
new AsyncSecurityPolicy() {
@Override
public ListenableFuture<Status> checkAuthorizationAsync(int uid) {
SettableFuture<Status> status = SettableFuture.create();
statusesToSet.add(status);
return status;
}
})
.build())
.build();
try {
server.start();
} catch (IOException e) {
throw new IllegalStateException(e);
}
shadowOf(context)
.setComponentNameAndServiceForBindServiceForIntent(
listenAddress.asBindIntent(),
listenAddress.getComponent(),
checkNotNull(binderReceiver.get()));
channel =
BinderChannelBuilder.forAddress(listenAddress, context)
.executor(executor)
.scheduledExecutorService(executor)
.offloadExecutor(executor)
.preAuthorizeServers(preAuthServersParam)
.build();
idleLoopers();
}
@After
public void tearDown() {
channel.shutdownNow();
controller.destroy();
server.shutdownNow();
}
@Test
public void testAsyncServerSecurityPolicy_failed_returnsFailureStatus() throws Exception {
ListenableFuture<Status> status = makeCall();
service.setSecurityPolicyStatusWhenReady(Status.ALREADY_EXISTS);
idleLoopers();
statusesToSet.take().set(Status.ALREADY_EXISTS);
assertThat(Futures.getDone(status).getCode()).isEqualTo(Status.Code.ALREADY_EXISTS);
assertThat(status.get().getCode()).isEqualTo(Status.Code.ALREADY_EXISTS);
}
@Test
public void testAsyncServerSecurityPolicy_failedFuture_failsWithCodeInternal() throws Exception {
ListenableFuture<Status> status = makeCall();
service.setSecurityPolicyFailed(new IllegalStateException("oops"));
idleLoopers();
statusesToSet.take().setException(new IllegalStateException("oops"));
assertThat(Futures.getDone(status).getCode()).isEqualTo(Status.Code.INTERNAL);
assertThat(status.get().getCode()).isEqualTo(Status.Code.INTERNAL);
}
@Test
public void testAsyncServerSecurityPolicy_allowed_returnsOkStatus() throws Exception {
ListenableFuture<Status> status = makeCall();
service.setSecurityPolicyStatusWhenReady(Status.OK);
idleLoopers();
statusesToSet.take().set(Status.OK);
assertThat(Futures.getDone(status).getCode()).isEqualTo(Status.Code.OK);
assertThat(status.get().getCode()).isEqualTo(Status.Code.OK);
}
private ListenableFuture<Status> makeCall() {
ClientCall<Empty, Empty> call =
channel.newCall(
getMethodDescriptor(), CallOptions.DEFAULT.withExecutor(service.getExecutor()));
ClientCall<Empty, Empty> call = channel.newCall(getMethodDescriptor(), CallOptions.DEFAULT);
ListenableFuture<Empty> responseFuture =
ClientCalls.futureUnaryCall(call, Empty.getDefaultInstance());
idleLoopers();
return Futures.catching(
Futures.transform(responseFuture, unused -> Status.OK, directExecutor()),
StatusRuntimeException.class,
@ -133,10 +187,6 @@ public final class RobolectricBinderSecurityTest {
directExecutor());
}
private static void idleLoopers() {
shadowOf(Looper.getMainLooper()).idle();
}
private static MethodDescriptor<Empty, Empty> getMethodDescriptor() {
MethodDescriptor.Marshaller<Empty> marshaller =
ProtoLiteUtils.marshaller(Empty.getDefaultInstance());
@ -147,106 +197,4 @@ public final class RobolectricBinderSecurityTest {
.setSampledToLocalTracing(true)
.build();
}
private static class SomeService extends LifecycleService {
private final IBinderReceiver binderReceiver = new IBinderReceiver();
private final ArrayBlockingQueue<SettableFuture<Status>> statusesToSet =
new ArrayBlockingQueue<>(128);
private Server server;
private final ScheduledExecutorService scheduledExecutorService =
new MainThreadScheduledExecutorService();
@Override
public void onCreate() {
super.onCreate();
MethodDescriptor<Empty, Empty> methodDesc = getMethodDescriptor();
ServerCallHandler<Empty, Empty> callHandler =
ServerCalls.asyncUnaryCall(
(req, respObserver) -> {
respObserver.onNext(req);
respObserver.onCompleted();
});
ServerMethodDefinition<Empty, Empty> methodDef =
ServerMethodDefinition.create(methodDesc, callHandler);
ServerServiceDefinition def =
ServerServiceDefinition.builder(SERVICE_NAME).addMethod(methodDef).build();
server =
BinderServerBuilder.forAddress(AndroidComponentAddress.forContext(this), binderReceiver)
.addService(def)
.securityPolicy(
ServerSecurityPolicy.newBuilder()
.servicePolicy(
SERVICE_NAME,
new AsyncSecurityPolicy() {
@Override
public ListenableFuture<Status> checkAuthorizationAsync(int uid) {
return Futures.submitAsync(
() -> {
SettableFuture<Status> status = SettableFuture.create();
statusesToSet.add(status);
return status;
},
getExecutor());
}
})
.build())
.executor(getExecutor())
.scheduledExecutorService(getExecutor())
.build();
try {
server.start();
} catch (IOException e) {
throw new IllegalStateException(e);
}
Application context = ApplicationProvider.getApplicationContext();
ComponentName componentName = new ComponentName(context, SomeService.class);
shadowOf(context)
.setComponentNameAndServiceForBindService(
componentName, checkNotNull(binderReceiver.get()));
}
/**
* Returns an {@link ScheduledExecutorService} under which all of the gRPC computations run. The
* execution of any pending tasks on this executor can be triggered via {@link #idleLoopers()}.
*/
ScheduledExecutorService getExecutor() {
return scheduledExecutorService;
}
void setSecurityPolicyStatusWhenReady(Status status) {
getNextEnqueuedStatus().set(status);
}
void setSecurityPolicyFailed(Exception e) {
getNextEnqueuedStatus().setException(e);
}
private SettableFuture<Status> getNextEnqueuedStatus() {
@Nullable SettableFuture<Status> future = statusesToSet.poll();
while (future == null) {
// Keep idling until the future is available.
idleLoopers();
future = statusesToSet.poll();
}
return checkNotNull(future);
}
@Override
public IBinder onBind(Intent intent) {
super.onBind(intent);
return checkNotNull(binderReceiver.get());
}
@Override
public void onDestroy() {
super.onDestroy();
server.shutdownNow();
}
/** A future representing a task submitted to a {@link Handler}. */
}
}

View File

@ -357,7 +357,7 @@ public final class SecurityPoliciesTest {
}
@Test
@Config(sdk = 21)
@Config(sdk = Config.OLDEST_SDK)
public void testIsProfileOwner_succeedsForProfileOwner() throws Exception {
PackageInfo info =
newBuilder().setPackageName(OTHER_UID_PACKAGE_NAME).setSignatures(SIG2).build();
@ -371,7 +371,7 @@ public final class SecurityPoliciesTest {
}
@Test
@Config(sdk = 21)
@Config(sdk = Config.OLDEST_SDK)
public void testIsProfileOwner_failsForNotProfileOwner() throws Exception {
PackageInfo info =
newBuilder().setPackageName(OTHER_UID_PACKAGE_NAME).setSignatures(SIG2).build();
@ -385,7 +385,7 @@ public final class SecurityPoliciesTest {
}
@Test
@Config(sdk = 21)
@Config(sdk = Config.OLDEST_SDK)
public void testIsProfileOwner_failsWhenNoPackagesForUid() throws Exception {
policy = SecurityPolicies.isProfileOwner(appContext);
@ -425,7 +425,7 @@ public final class SecurityPoliciesTest {
}
@Test
@Config(sdk = 21)
@Config(sdk = Config.OLDEST_SDK)
public void testIsProfileOwnerOnOrgOwned_failsForNotProfileOwner() throws Exception {
PackageInfo info =
newBuilder().setPackageName(OTHER_UID_PACKAGE_NAME).setSignatures(SIG2).build();
@ -439,7 +439,7 @@ public final class SecurityPoliciesTest {
}
@Test
@Config(sdk = 21)
@Config(sdk = Config.OLDEST_SDK)
public void testIsProfileOwnerOnOrgOwned_failsWhenNoPackagesForUid() throws Exception {
policy = SecurityPolicies.isProfileOwnerOnOrganizationOwnedDevice(appContext);

View File

@ -16,7 +16,6 @@
package io.grpc.binder.internal;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
@ -29,11 +28,9 @@ import android.os.Looper;
import android.os.Parcel;
import com.google.common.collect.ImmutableList;
import io.grpc.Attributes;
import io.grpc.Metadata;
import io.grpc.Status;
import io.grpc.internal.FixedObjectPool;
import io.grpc.internal.ServerStream;
import io.grpc.internal.ServerTransportListener;
import io.grpc.internal.MockServerTransportListener;
import java.util.concurrent.ScheduledExecutorService;
import org.junit.Before;
import org.junit.Rule;
@ -55,21 +52,22 @@ public final class BinderServerTransportTest {
@Rule public MockitoRule mocks = MockitoJUnit.rule();
private final ScheduledExecutorService executorService = new MainThreadScheduledExecutorService();
private final TestTransportListener transportListener = new TestTransportListener();
private MockServerTransportListener transportListener;
@Mock IBinder mockBinder;
BinderTransport.BinderServerTransport transport;
BinderServerTransport transport;
@Before
public void setUp() throws Exception {
transport =
new BinderTransport.BinderServerTransport(
new BinderServerTransport(
new FixedObjectPool<>(executorService),
Attributes.EMPTY,
ImmutableList.of(),
OneWayBinderProxy.IDENTITY_DECORATOR,
mockBinder);
transportListener = new MockServerTransportListener(transport);
}
@Test
@ -82,34 +80,6 @@ public final class BinderServerTransportTest {
transport.shutdownNow(Status.UNKNOWN.withDescription("reasons"));
shadowOf(Looper.getMainLooper()).idle();
assertThat(transportListener.terminated).isTrue();
}
private static final class TestTransportListener implements ServerTransportListener {
public boolean ready;
public boolean terminated;
/**
* Called when a new stream was created by the remote client.
*
* @param stream the newly created stream.
* @param method the fully qualified method name being called on the server.
* @param headers containing metadata for the call.
*/
@Override
public void streamCreated(ServerStream stream, String method, Metadata headers) {}
@Override
public Attributes transportReady(Attributes attributes) {
ready = true;
return attributes;
}
@Override
public void transportTerminated() {
checkState(!terminated, "Terminated twice");
terminated = true;
}
assertThat(transportListener.isTerminated()).isTrue();
}
}

View File

@ -0,0 +1,115 @@
/*
* Copyright 2025 The gRPC Authors
*
* 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 io.grpc.binder.internal;
import static android.os.Looper.getMainLooper;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.robolectric.Shadows.shadowOf;
import android.app.Application;
import androidx.core.content.ContextCompat;
import androidx.test.core.app.ApplicationProvider;
import io.grpc.NameResolver;
import io.grpc.NameResolver.ResolutionResult;
import io.grpc.NameResolver.ServiceConfigParser;
import io.grpc.NameResolverProvider;
import io.grpc.SynchronizationContext;
import io.grpc.binder.ApiConstants;
import java.net.URI;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoTestRule;
import org.robolectric.RobolectricTestRunner;
/** A test for IntentNameResolverProvider. */
@RunWith(RobolectricTestRunner.class)
public final class IntentNameResolverProviderTest {
private final Application appContext = ApplicationProvider.getApplicationContext();
private final SynchronizationContext syncContext = newSynchronizationContext();
private final NameResolver.Args args = newNameResolverArgs();
private NameResolverProvider provider;
@Rule public MockitoTestRule mockitoTestRule = MockitoJUnit.testRule(this);
@Mock public NameResolver.Listener2 mockListener;
@Captor public ArgumentCaptor<ResolutionResult> resultCaptor;
@Before
public void setUp() {
provider = new IntentNameResolverProvider();
}
@Test
public void testProviderScheme_returnsIntentScheme() throws Exception {
assertThat(provider.getDefaultScheme())
.isEqualTo(IntentNameResolverProvider.ANDROID_INTENT_SCHEME);
}
@Test
public void testNoResolverForUnknownScheme_returnsNull() throws Exception {
assertThat(provider.newNameResolver(new URI("random://uri"), args)).isNull();
}
@Test
public void testResolutionWithBadUri_throwsIllegalArg() throws Exception {
assertThrows(
IllegalArgumentException.class,
() -> provider.newNameResolver(new URI("intent:xxx#Intent;e.x=1;end;"), args));
}
@Test
public void testResolverForIntentScheme_returnsResolver() throws Exception {
URI uri = new URI("intent://authority/path#Intent;action=action;scheme=scheme;end");
NameResolver resolver = provider.newNameResolver(uri, args);
assertThat(resolver).isNotNull();
assertThat(resolver.getServiceAuthority()).isEqualTo("localhost");
syncContext.execute(() -> resolver.start(mockListener));
shadowOf(getMainLooper()).idle();
verify(mockListener).onResult2(resultCaptor.capture());
assertThat(resultCaptor.getValue().getAddressesOrError()).isNotNull();
syncContext.execute(resolver::shutdown);
shadowOf(getMainLooper()).idle();
}
/** Returns a new test-specific {@link NameResolver.Args} instance. */
private NameResolver.Args newNameResolverArgs() {
return NameResolver.Args.newBuilder()
.setDefaultPort(-1)
.setProxyDetector((target) -> null) // No proxies here.
.setSynchronizationContext(syncContext)
.setOffloadExecutor(ContextCompat.getMainExecutor(appContext))
.setServiceConfigParser(mock(ServiceConfigParser.class))
.setArg(ApiConstants.SOURCE_ANDROID_CONTEXT, appContext)
.build();
}
private static SynchronizationContext newSynchronizationContext() {
return new SynchronizationContext(
(thread, exception) -> {
throw new AssertionError(exception);
});
}
}

View File

@ -0,0 +1,531 @@
/*
* Copyright 2025 The gRPC Authors
*
* 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 io.grpc.binder.internal;
import static android.content.Intent.ACTION_PACKAGE_ADDED;
import static android.content.Intent.ACTION_PACKAGE_REPLACED;
import static android.os.Looper.getMainLooper;
import static android.os.Process.myUserHandle;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.robolectric.Shadows.shadowOf;
import android.app.Application;
import android.content.ComponentName;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ServiceInfo;
import android.net.Uri;
import android.os.UserHandle;
import android.os.UserManager;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import androidx.test.core.app.ApplicationProvider;
import com.google.common.collect.ImmutableList;
import io.grpc.EquivalentAddressGroup;
import io.grpc.NameResolver;
import io.grpc.NameResolver.ResolutionResult;
import io.grpc.NameResolver.ServiceConfigParser;
import io.grpc.Status;
import io.grpc.StatusOr;
import io.grpc.SynchronizationContext;
import io.grpc.binder.AndroidComponentAddress;
import io.grpc.binder.ApiConstants;
import java.lang.Thread.UncaughtExceptionHandler;
import java.net.SocketAddress;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoTestRule;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowPackageManager;
/** A test for IntentNameResolverProvider. */
@RunWith(RobolectricTestRunner.class)
public final class IntentNameResolverTest {
private static final ComponentName SOME_COMPONENT_NAME =
new ComponentName("com.foo.bar", "SomeComponent");
private static final ComponentName ANOTHER_COMPONENT_NAME =
new ComponentName("org.blah", "AnotherComponent");
private final Application appContext = ApplicationProvider.getApplicationContext();
private final SynchronizationContext syncContext = newSynchronizationContext();
private final NameResolver.Args args = newNameResolverArgs().build();
private final ShadowPackageManager shadowPackageManager =
shadowOf(appContext.getPackageManager());
@Rule public MockitoTestRule mockitoTestRule = MockitoJUnit.testRule(this);
@Mock public NameResolver.Listener2 mockListener;
@Captor public ArgumentCaptor<ResolutionResult> resultCaptor;
@Test
public void testResolverForIntentScheme_returnsResolverWithLocalHostAuthority() throws Exception {
NameResolver resolver = newNameResolver(newIntent());
assertThat(resolver).isNotNull();
assertThat(resolver.getServiceAuthority()).isEqualTo("localhost");
}
@Test
public void testResolutionWithoutServicesAvailable_returnsUnimplemented() throws Exception {
NameResolver nameResolver = newNameResolver(newIntent());
syncContext.execute(() -> nameResolver.start(mockListener));
shadowOf(getMainLooper()).idle();
verify(mockListener).onResult2(resultCaptor.capture());
assertThat(resultCaptor.getValue().getAddressesOrError().getStatus().getCode())
.isEqualTo(Status.UNIMPLEMENTED.getCode());
}
@Test
public void testResolutionWithMultipleServicesAvailable_returnsAndroidComponentAddresses()
throws Exception {
Intent intent = newIntent();
IntentFilter serviceIntentFilter = newFilterMatching(intent);
shadowPackageManager.addServiceIfNotPresent(SOME_COMPONENT_NAME);
shadowPackageManager.addIntentFilterForService(SOME_COMPONENT_NAME, serviceIntentFilter);
// Adds another valid Service
shadowPackageManager.addServiceIfNotPresent(ANOTHER_COMPONENT_NAME);
shadowPackageManager.addIntentFilterForService(ANOTHER_COMPONENT_NAME, serviceIntentFilter);
NameResolver nameResolver = newNameResolver(intent);
syncContext.execute(() -> nameResolver.start(mockListener));
shadowOf(getMainLooper()).idle();
verify(mockListener, never()).onError(any());
verify(mockListener).onResult2(resultCaptor.capture());
assertThat(getAddressesOrThrow(resultCaptor.getValue()))
.containsExactly(
toAddressList(intent.cloneFilter().setComponent(SOME_COMPONENT_NAME)),
toAddressList(intent.cloneFilter().setComponent(ANOTHER_COMPONENT_NAME)));
syncContext.execute(nameResolver::shutdown);
shadowOf(getMainLooper()).idle();
}
@Test
public void testExplicitResolutionByComponent_returnsRestrictedResults() throws Exception {
Intent intent = newIntent();
IntentFilter serviceIntentFilter = newFilterMatching(intent);
shadowPackageManager.addServiceIfNotPresent(SOME_COMPONENT_NAME);
shadowPackageManager.addIntentFilterForService(SOME_COMPONENT_NAME, serviceIntentFilter);
shadowPackageManager.addServiceIfNotPresent(ANOTHER_COMPONENT_NAME);
shadowPackageManager.addIntentFilterForService(ANOTHER_COMPONENT_NAME, serviceIntentFilter);
NameResolver nameResolver =
newNameResolver(intent.cloneFilter().setComponent(ANOTHER_COMPONENT_NAME));
syncContext.execute(() -> nameResolver.start(mockListener));
shadowOf(getMainLooper()).idle();
verify(mockListener, never()).onError(any());
verify(mockListener).onResult2(resultCaptor.capture());
assertThat(getAddressesOrThrow(resultCaptor.getValue()))
.containsExactly(toAddressList(intent.cloneFilter().setComponent(ANOTHER_COMPONENT_NAME)));
syncContext.execute(nameResolver::shutdown);
shadowOf(getMainLooper()).idle();
}
@Test
public void testExplicitResolutionByPackage_returnsRestrictedResults() throws Exception {
Intent intent = newIntent();
IntentFilter serviceIntentFilter = newFilterMatching(intent);
shadowPackageManager.addServiceIfNotPresent(SOME_COMPONENT_NAME);
shadowPackageManager.addIntentFilterForService(SOME_COMPONENT_NAME, serviceIntentFilter);
shadowPackageManager.addServiceIfNotPresent(ANOTHER_COMPONENT_NAME);
shadowPackageManager.addIntentFilterForService(ANOTHER_COMPONENT_NAME, serviceIntentFilter);
NameResolver nameResolver =
newNameResolver(intent.cloneFilter().setPackage(ANOTHER_COMPONENT_NAME.getPackageName()));
syncContext.execute(() -> nameResolver.start(mockListener));
shadowOf(getMainLooper()).idle();
verify(mockListener, never()).onError(any());
verify(mockListener).onResult2(resultCaptor.capture());
assertThat(getAddressesOrThrow(resultCaptor.getValue()))
.containsExactly(toAddressList(intent.cloneFilter().setComponent(ANOTHER_COMPONENT_NAME)));
syncContext.execute(nameResolver::shutdown);
shadowOf(getMainLooper()).idle();
}
@Test
public void testResolution_setsPreAuthEagAttribute() throws Exception {
Intent intent = newIntent();
IntentFilter serviceIntentFilter = newFilterMatching(intent);
shadowPackageManager.addServiceIfNotPresent(SOME_COMPONENT_NAME);
shadowPackageManager.addIntentFilterForService(SOME_COMPONENT_NAME, serviceIntentFilter);
NameResolver nameResolver = newNameResolver(intent);
syncContext.execute(() -> nameResolver.start(mockListener));
shadowOf(getMainLooper()).idle();
verify(mockListener).onResult2(resultCaptor.capture());
assertThat(getAddressesOrThrow(resultCaptor.getValue()))
.containsExactly(toAddressList(intent.cloneFilter().setComponent(SOME_COMPONENT_NAME)));
assertThat(
getEagsOrThrow(resultCaptor.getValue()).stream()
.map(EquivalentAddressGroup::getAttributes)
.collect(toImmutableList())
.get(0)
.get(ApiConstants.PRE_AUTH_SERVER_OVERRIDE))
.isTrue();
syncContext.execute(nameResolver::shutdown);
shadowOf(getMainLooper()).idle();
}
@Test
public void testServiceRemoved_pushesUpdatedAndroidComponentAddresses() throws Exception {
Intent intent = newIntent();
IntentFilter serviceIntentFilter = newFilterMatching(intent);
shadowPackageManager.addServiceIfNotPresent(SOME_COMPONENT_NAME);
shadowPackageManager.addIntentFilterForService(SOME_COMPONENT_NAME, serviceIntentFilter);
shadowPackageManager.addServiceIfNotPresent(ANOTHER_COMPONENT_NAME);
shadowPackageManager.addIntentFilterForService(ANOTHER_COMPONENT_NAME, serviceIntentFilter);
NameResolver nameResolver = newNameResolver(intent);
syncContext.execute(() -> nameResolver.start(mockListener));
shadowOf(getMainLooper()).idle();
verify(mockListener, never()).onError(any());
verify(mockListener).onResult2(resultCaptor.capture());
assertThat(getAddressesOrThrow(resultCaptor.getValue()))
.containsExactly(
toAddressList(intent.cloneFilter().setComponent(SOME_COMPONENT_NAME)),
toAddressList(intent.cloneFilter().setComponent(ANOTHER_COMPONENT_NAME)));
shadowPackageManager.removeService(ANOTHER_COMPONENT_NAME);
broadcastPackageChange(ACTION_PACKAGE_REPLACED, ANOTHER_COMPONENT_NAME.getPackageName());
shadowOf(getMainLooper()).idle();
verify(mockListener, never()).onError(any());
verify(mockListener, times(2)).onResult2(resultCaptor.capture());
assertThat(getAddressesOrThrow(resultCaptor.getValue()))
.containsExactly(toAddressList(intent.cloneFilter().setComponent(SOME_COMPONENT_NAME)));
syncContext.execute(nameResolver::shutdown);
shadowOf(getMainLooper()).idle();
verifyNoMoreInteractions(mockListener);
assertThat(shadowOf(appContext).getRegisteredReceivers()).isEmpty();
}
@Test
@Config(sdk = 30)
public void testTargetAndroidUser_pushesUpdatedAddresses() throws Exception {
Intent intent = newIntent();
IntentFilter serviceIntentFilter = newFilterMatching(intent);
NameResolver nameResolver =
newNameResolver(
intent,
newNameResolverArgs().setArg(ApiConstants.TARGET_ANDROID_USER, myUserHandle()).build());
syncContext.execute(() -> nameResolver.start(mockListener));
shadowOf(getMainLooper()).idle();
verify(mockListener).onResult2(resultCaptor.capture());
assertThat(resultCaptor.getValue().getAddressesOrError().getStatus().getCode())
.isEqualTo(Status.UNIMPLEMENTED.getCode());
shadowPackageManager.addServiceIfNotPresent(SOME_COMPONENT_NAME);
shadowPackageManager.addIntentFilterForService(SOME_COMPONENT_NAME, serviceIntentFilter);
broadcastPackageChange(ACTION_PACKAGE_ADDED, SOME_COMPONENT_NAME.getPackageName());
shadowOf(getMainLooper()).idle();
verify(mockListener, never()).onError(any());
verify(mockListener, times(2)).onResult2(resultCaptor.capture());
assertThat(getAddressesOrThrow(resultCaptor.getValue()))
.containsExactly(
ImmutableList.of(
AndroidComponentAddress.newBuilder()
.setTargetUser(myUserHandle())
.setBindIntent(intent.cloneFilter().setComponent(SOME_COMPONENT_NAME))
.build()));
syncContext.execute(nameResolver::shutdown);
shadowOf(getMainLooper()).idle();
verifyNoMoreInteractions(mockListener);
assertThat(shadowOf(appContext).getRegisteredReceivers()).isEmpty();
}
@Test
@Config(sdk = 29)
public void testTargetAndroidUser_notSupported_throwsWithHelpfulMessage() throws Exception {
NameResolver.Args args =
newNameResolverArgs().setArg(ApiConstants.TARGET_ANDROID_USER, myUserHandle()).build();
IllegalArgumentException iae =
assertThrows(IllegalArgumentException.class, () -> newNameResolver(newIntent(), args));
assertThat(iae.getMessage()).contains("TARGET_ANDROID_USER");
assertThat(iae.getMessage()).contains("SDK_INT >= R");
}
@Test
@Config(sdk = 29)
public void testServiceAppearsUponBootComplete_pushesUpdatedAndroidComponentAddresses()
throws Exception {
Intent intent = newIntent();
IntentFilter serviceIntentFilter = newFilterMatching(intent);
// Suppose this directBootAware=true Service appears in PackageManager before a user unlock.
shadowOf(appContext.getSystemService(UserManager.class)).setUserUnlocked(false);
ServiceInfo someServiceInfo = shadowPackageManager.addServiceIfNotPresent(SOME_COMPONENT_NAME);
someServiceInfo.directBootAware = true;
shadowPackageManager.addIntentFilterForService(SOME_COMPONENT_NAME, serviceIntentFilter);
NameResolver nameResolver = newNameResolver(intent);
syncContext.execute(() -> nameResolver.start(mockListener));
shadowOf(getMainLooper()).idle();
verify(mockListener, never()).onError(any());
verify(mockListener).onResult2(resultCaptor.capture());
assertThat(getAddressesOrThrow(resultCaptor.getValue()))
.containsExactly(toAddressList(intent.cloneFilter().setComponent(SOME_COMPONENT_NAME)));
// TODO(b/331618070): Robolectric doesn't yet support ServiceInfo.directBootAware filtering.
// Simulate support by waiting for a user unlock to add this !directBootAware Service.
ServiceInfo anotherServiceInfo =
shadowPackageManager.addServiceIfNotPresent(ANOTHER_COMPONENT_NAME);
anotherServiceInfo.directBootAware = false;
shadowPackageManager.addIntentFilterForService(ANOTHER_COMPONENT_NAME, serviceIntentFilter);
shadowOf(appContext.getSystemService(UserManager.class)).setUserUnlocked(true);
broadcastUserUnlocked(myUserHandle());
shadowOf(getMainLooper()).idle();
verify(mockListener, never()).onError(any());
verify(mockListener, times(2)).onResult2(resultCaptor.capture());
assertThat(getAddressesOrThrow(resultCaptor.getValue()))
.containsExactly(
toAddressList(intent.cloneFilter().setComponent(SOME_COMPONENT_NAME)),
toAddressList(intent.cloneFilter().setComponent(ANOTHER_COMPONENT_NAME)));
syncContext.execute(nameResolver::shutdown);
shadowOf(getMainLooper()).idle();
verifyNoMoreInteractions(mockListener);
}
@Test
public void testRefresh_returnsSameAndroidComponentAddresses() throws Exception {
Intent intent = newIntent();
IntentFilter serviceIntentFilter = newFilterMatching(intent);
shadowPackageManager.addServiceIfNotPresent(SOME_COMPONENT_NAME);
shadowPackageManager.addIntentFilterForService(SOME_COMPONENT_NAME, serviceIntentFilter);
shadowPackageManager.addServiceIfNotPresent(ANOTHER_COMPONENT_NAME);
shadowPackageManager.addIntentFilterForService(ANOTHER_COMPONENT_NAME, serviceIntentFilter);
NameResolver nameResolver = newNameResolver(intent);
syncContext.execute(() -> nameResolver.start(mockListener));
shadowOf(getMainLooper()).idle();
verify(mockListener, never()).onError(any());
verify(mockListener).onResult2(resultCaptor.capture());
assertThat(getAddressesOrThrow(resultCaptor.getValue()))
.containsExactly(
toAddressList(intent.cloneFilter().setComponent(SOME_COMPONENT_NAME)),
toAddressList(intent.cloneFilter().setComponent(ANOTHER_COMPONENT_NAME)));
syncContext.execute(nameResolver::refresh);
shadowOf(getMainLooper()).idle();
verify(mockListener, never()).onError(any());
verify(mockListener, times(2)).onResult2(resultCaptor.capture());
assertThat(getAddressesOrThrow(resultCaptor.getValue()))
.containsExactly(
toAddressList(intent.cloneFilter().setComponent(SOME_COMPONENT_NAME)),
toAddressList(intent.cloneFilter().setComponent(ANOTHER_COMPONENT_NAME)));
syncContext.execute(nameResolver::shutdown);
shadowOf(getMainLooper()).idle();
assertThat(shadowOf(appContext).getRegisteredReceivers()).isEmpty();
}
@Test
public void testRefresh_collapsesMultipleRequestsIntoOneLookup() throws Exception {
Intent intent = newIntent();
IntentFilter serviceIntentFilter = newFilterMatching(intent);
shadowPackageManager.addServiceIfNotPresent(SOME_COMPONENT_NAME);
shadowPackageManager.addIntentFilterForService(SOME_COMPONENT_NAME, serviceIntentFilter);
NameResolver nameResolver = newNameResolver(intent);
syncContext.execute(() -> nameResolver.start(mockListener)); // Should kick off the 1st lookup.
syncContext.execute(nameResolver::refresh); // Should queue a lookup to run when 1st finishes.
syncContext.execute(nameResolver::refresh); // Should be ignored since a lookup is already Q'd.
syncContext.execute(nameResolver::refresh); // Also ignored.
shadowOf(getMainLooper()).idle();
verify(mockListener, never()).onError(any());
verify(mockListener, times(2)).onResult2(resultCaptor.capture());
assertThat(getAddressesOrThrow(resultCaptor.getValue()))
.containsExactly(toAddressList(intent.cloneFilter().setComponent(SOME_COMPONENT_NAME)));
syncContext.execute(nameResolver::shutdown);
shadowOf(getMainLooper()).idle();
}
private void broadcastPackageChange(String action, String pkgName) {
Intent broadcast = new Intent();
broadcast.setAction(action);
broadcast.setData(Uri.parse("package:" + pkgName));
appContext.sendBroadcast(broadcast);
}
private void broadcastUserUnlocked(UserHandle userHandle) {
Intent unlockedBroadcast = new Intent(Intent.ACTION_USER_UNLOCKED);
unlockedBroadcast.putExtra(Intent.EXTRA_USER, userHandle);
appContext.sendBroadcast(unlockedBroadcast);
}
@Test
public void testResolutionOnResultThrows_onErrorNotCalled() throws Exception {
RetainingUncaughtExceptionHandler exceptionHandler = new RetainingUncaughtExceptionHandler();
SynchronizationContext syncContext = new SynchronizationContext(exceptionHandler);
Intent intent = newIntent();
shadowPackageManager.addServiceIfNotPresent(SOME_COMPONENT_NAME);
shadowPackageManager.addIntentFilterForService(SOME_COMPONENT_NAME, newFilterMatching(intent));
@SuppressWarnings("serial")
class SomeRuntimeException extends RuntimeException {}
doThrow(SomeRuntimeException.class).when(mockListener).onResult2(any());
NameResolver nameResolver =
newNameResolver(
intent, newNameResolverArgs().setSynchronizationContext(syncContext).build());
syncContext.execute(() -> nameResolver.start(mockListener));
shadowOf(getMainLooper()).idle();
verify(mockListener).onResult2(any());
verify(mockListener, never()).onError(any());
assertThat(exceptionHandler.uncaught).hasSize(1);
assertThat(exceptionHandler.uncaught.get(0)).isInstanceOf(SomeRuntimeException.class);
}
private static Intent newIntent() {
Intent intent = new Intent();
intent.setAction("test.action");
intent.setData(Uri.parse("grpc:ServiceName"));
return intent;
}
private static IntentFilter newFilterMatching(Intent intent) {
IntentFilter filter = new IntentFilter();
if (intent.getAction() != null) {
filter.addAction(intent.getAction());
}
Uri data = intent.getData();
if (data != null) {
if (data.getScheme() != null) {
filter.addDataScheme(data.getScheme());
}
if (data.getSchemeSpecificPart() != null) {
filter.addDataSchemeSpecificPart(data.getSchemeSpecificPart(), 0);
}
}
Set<String> categories = intent.getCategories();
if (categories != null) {
for (String category : categories) {
filter.addCategory(category);
}
}
return filter;
}
private static List<EquivalentAddressGroup> getEagsOrThrow(ResolutionResult result) {
StatusOr<List<EquivalentAddressGroup>> eags = result.getAddressesOrError();
if (!eags.hasValue()) {
throw eags.getStatus().asRuntimeException();
}
return eags.getValue();
}
// Extracts just the addresses from 'result's EquivalentAddressGroups.
private static ImmutableList<List<SocketAddress>> getAddressesOrThrow(ResolutionResult result) {
return getEagsOrThrow(result).stream()
.map(EquivalentAddressGroup::getAddresses)
.collect(toImmutableList());
}
// Converts given Intents to a list of ACAs, for convenient comparison with getAddressesOrThrow().
private static ImmutableList<AndroidComponentAddress> toAddressList(Intent... bindIntents) {
ImmutableList.Builder<AndroidComponentAddress> builder = ImmutableList.builder();
for (Intent bindIntent : bindIntents) {
builder.add(AndroidComponentAddress.forBindIntent(bindIntent));
}
return builder.build();
}
private NameResolver newNameResolver(Intent targetIntent) {
return newNameResolver(targetIntent, args);
}
private NameResolver newNameResolver(Intent targetIntent, NameResolver.Args args) {
return new IntentNameResolver(targetIntent, args);
}
/** Returns a new test-specific {@link NameResolver.Args} instance. */
private NameResolver.Args.Builder newNameResolverArgs() {
return NameResolver.Args.newBuilder()
.setDefaultPort(-1)
.setProxyDetector((target) -> null) // No proxies here.
.setSynchronizationContext(syncContext)
.setOffloadExecutor(ContextCompat.getMainExecutor(appContext))
.setArg(ApiConstants.SOURCE_ANDROID_CONTEXT, appContext)
.setServiceConfigParser(mock(ServiceConfigParser.class));
}
/**
* Returns a test {@link SynchronizationContext}.
*
* <p>Exceptions will cause the test to fail with {@link AssertionError}.
*/
private static SynchronizationContext newSynchronizationContext() {
return new SynchronizationContext(
(thread, exception) -> {
throw new AssertionError(exception);
});
}
static final class RetainingUncaughtExceptionHandler implements UncaughtExceptionHandler {
final ArrayList<Throwable> uncaught = new ArrayList<>();
@Override
public void uncaughtException(@NonNull Thread t, @NonNull Throwable e) {
uncaught.add(e);
}
}
}

View File

@ -96,7 +96,7 @@ public final class PingTrackerTest {
private int numCallbacks;
private boolean success;
private boolean failure;
private Throwable failureException;
private Status failureStatus;
private long roundtripTimeNanos;
@Override
@ -107,10 +107,10 @@ public final class PingTrackerTest {
}
@Override
public synchronized void onFailure(Throwable failureException) {
public synchronized void onFailure(Status failureStatus) {
numCallbacks += 1;
failure = true;
this.failureException = failureException;
this.failureStatus = failureStatus;
}
public void assertNotCalled() {
@ -130,13 +130,13 @@ public final class PingTrackerTest {
public void assertFailure(Status status) {
assertThat(numCallbacks).isEqualTo(1);
assertThat(failure).isTrue();
assertThat(((StatusException) failureException).getStatus()).isSameInstanceAs(status);
assertThat(failureStatus).isSameInstanceAs(status);
}
public void assertFailure(Status.Code statusCode) {
assertThat(numCallbacks).isEqualTo(1);
assertThat(failure).isTrue();
assertThat(((StatusException) failureException).getStatus().getCode()).isEqualTo(statusCode);
assertThat(failureStatus.getCode()).isEqualTo(statusCode);
}
}
}

View File

@ -0,0 +1,281 @@
/*
* Copyright 2025 The gRPC Authors
*
* 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 io.grpc.binder.internal;
import static com.google.common.truth.Truth.assertThat;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;
import static org.robolectric.Shadows.shadowOf;
import android.app.Application;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.ServiceInfo;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.core.content.pm.ApplicationInfoBuilder;
import androidx.test.core.content.pm.PackageInfoBuilder;
import com.google.common.collect.ImmutableList;
import io.grpc.Attributes;
import io.grpc.ServerStreamTracer;
import io.grpc.Status;
import io.grpc.binder.AndroidComponentAddress;
import io.grpc.binder.ApiConstants;
import io.grpc.binder.AsyncSecurityPolicy;
import io.grpc.binder.internal.SettableAsyncSecurityPolicy.AuthRequest;
import io.grpc.internal.AbstractTransportTest;
import io.grpc.internal.ClientTransportFactory.ClientTransportOptions;
import io.grpc.internal.GrpcUtil;
import io.grpc.internal.InternalServer;
import io.grpc.internal.ManagedClientTransport;
import io.grpc.internal.ObjectPool;
import io.grpc.internal.SharedResourcePool;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.ScheduledExecutorService;
import org.junit.Before;
import org.junit.Ignore;
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.ParameterizedRobolectricTestRunner;
import org.robolectric.ParameterizedRobolectricTestRunner.Parameter;
import org.robolectric.ParameterizedRobolectricTestRunner.Parameters;
import org.robolectric.annotation.LooperMode;
import org.robolectric.annotation.LooperMode.Mode;
import org.robolectric.shadows.ShadowBinder;
/**
* All of the AbstractTransportTest cases applied to {@link BinderTransport} running in a
* Robolectric environment.
*
* <p>Runs much faster than BinderTransportTest and doesn't require an Android device/emulator.
* Somewhat less realistic but allows simulating behavior that would be difficult or impossible with
* real Android.
*
* <p>NB: Unlike most robolectric tests, we run in {@link LooperMode.Mode#INSTRUMENTATION_TEST},
* meaning test cases don't run on the main thread. This supports the AbstractTransportTest approach
* where the test thread frequently blocks waiting for transport state changes to take effect.
*/
@RunWith(ParameterizedRobolectricTestRunner.class)
@LooperMode(Mode.INSTRUMENTATION_TEST)
public final class RobolectricBinderTransportTest extends AbstractTransportTest {
private final Application application = ApplicationProvider.getApplicationContext();
private final ObjectPool<ScheduledExecutorService> executorServicePool =
SharedResourcePool.forResource(GrpcUtil.TIMER_SERVICE);
private final ObjectPool<Executor> offloadExecutorPool =
SharedResourcePool.forResource(GrpcUtil.SHARED_CHANNEL_EXECUTOR);
private final ObjectPool<Executor> serverExecutorPool =
SharedResourcePool.forResource(GrpcUtil.SHARED_CHANNEL_EXECUTOR);
@Rule public MockitoRule mocks = MockitoJUnit.rule();
@Mock AsyncSecurityPolicy mockClientSecurityPolicy;
ApplicationInfo serverAppInfo;
PackageInfo serverPkgInfo;
ServiceInfo serviceInfo;
private int nextServerAddress;
@Parameter public boolean preAuthServersParam;
@Parameters(name = "preAuthServersParam={0}")
public static ImmutableList<Boolean> data() {
return ImmutableList.of(true, false);
}
@Override
public void setUp() {
serverAppInfo =
ApplicationInfoBuilder.newBuilder().setPackageName("the.server.package").build();
serverAppInfo.uid = android.os.Process.myUid();
serverPkgInfo =
PackageInfoBuilder.newBuilder()
.setPackageName(serverAppInfo.packageName)
.setApplicationInfo(serverAppInfo)
.build();
shadowOf(application.getPackageManager()).installPackage(serverPkgInfo);
serviceInfo = new ServiceInfo();
serviceInfo.name = "SomeService";
serviceInfo.packageName = serverAppInfo.packageName;
serviceInfo.applicationInfo = serverAppInfo;
shadowOf(application.getPackageManager()).addOrUpdateService(serviceInfo);
super.setUp();
}
@Before
public void requestRealisticBindServiceBehavior() {
shadowOf(application).setBindServiceCallsOnServiceConnectedDirectly(false);
shadowOf(application).setUnbindServiceCallsOnServiceDisconnected(false);
}
@Override
protected InternalServer newServer(List<ServerStreamTracer.Factory> streamTracerFactories) {
AndroidComponentAddress listenAddr =
AndroidComponentAddress.forBindIntent(
new Intent()
.setClassName(serviceInfo.packageName, serviceInfo.name)
.setAction("io.grpc.action.BIND." + nextServerAddress++));
BinderServer binderServer =
new BinderServer.Builder()
.setListenAddress(listenAddr)
.setExecutorPool(serverExecutorPool)
.setExecutorServicePool(executorServicePool)
.setStreamTracerFactories(streamTracerFactories)
.build();
shadowOf(application.getPackageManager()).addServiceIfNotPresent(listenAddr.getComponent());
shadowOf(application)
.setComponentNameAndServiceForBindServiceForIntent(
listenAddr.asBindIntent(), listenAddr.getComponent(), binderServer.getHostBinder());
return binderServer;
}
@Override
protected InternalServer newServer(
int port, List<ServerStreamTracer.Factory> streamTracerFactories) {
if (port > 0) {
// TODO: TCP ports have no place in an *abstract* transport test. Replace with SocketAddress.
throw new UnsupportedOperationException();
}
return newServer(streamTracerFactories);
}
BinderClientTransportFactory.Builder newClientTransportFactoryBuilder() {
return new BinderClientTransportFactory.Builder()
.setPreAuthorizeServers(preAuthServersParam)
.setSourceContext(application)
.setScheduledExecutorPool(executorServicePool)
.setOffloadExecutorPool(offloadExecutorPool);
}
BinderClientTransportBuilder newClientTransportBuilder() {
return new BinderClientTransportBuilder()
.setFactory(newClientTransportFactoryBuilder().buildClientTransportFactory())
.setServerAddress(server.getListenSocketAddress());
}
@Override
protected ManagedClientTransport newClientTransport(InternalServer server) {
ClientTransportOptions options = new ClientTransportOptions();
options.setEagAttributes(eagAttrs());
options.setChannelLogger(transportLogger());
return newClientTransportBuilder()
.setServerAddress(server.getListenSocketAddress())
.setOptions(options)
.build();
}
@Override
protected String testAuthority(InternalServer server) {
return ((AndroidComponentAddress) server.getListenSocketAddress()).getAuthority();
}
@Test
public void clientAuthorizesServerUidsInOrder() throws Exception {
// TODO(jdcormie): In real Android, Binder#getCallingUid is thread-local but Robolectric only
// lets us fake value this *globally*. So the ShadowBinder#setCallingUid() here unrealistically
// affects the server's view of the client's uid too. For now this doesn't matter because this
// test never exercises server SecurityPolicy.
ShadowBinder.setCallingUid(11111); // UID of the server *process*.
serverPkgInfo.applicationInfo.uid = 22222; // UID of the server *app*, which can be different.
shadowOf(application.getPackageManager()).installPackage(serverPkgInfo);
shadowOf(application.getPackageManager()).addOrUpdateService(serviceInfo);
server = newServer(ImmutableList.of());
server.start(serverListener);
SettableAsyncSecurityPolicy securityPolicy = new SettableAsyncSecurityPolicy();
client =
newClientTransportBuilder()
.setFactory(
newClientTransportFactoryBuilder()
.setSecurityPolicy(securityPolicy)
.buildClientTransportFactory())
.build();
runIfNotNull(client.start(mockClientTransportListener));
if (preAuthServersParam) {
AuthRequest preAuthRequest = securityPolicy.takeNextAuthRequest(TIMEOUT_MS, MILLISECONDS);
assertThat(preAuthRequest.uid).isEqualTo(22222);
verify(mockClientTransportListener, never()).transportReady();
preAuthRequest.setResult(Status.OK);
}
AuthRequest authRequest = securityPolicy.takeNextAuthRequest(TIMEOUT_MS, MILLISECONDS);
assertThat(authRequest.uid).isEqualTo(11111);
verify(mockClientTransportListener, never()).transportReady();
authRequest.setResult(Status.OK);
verify(mockClientTransportListener, timeout(TIMEOUT_MS)).transportReady();
}
@Test
public void eagAttributeCanOverrideChannelPreAuthServerSetting() throws Exception {
server.start(serverListener);
SettableAsyncSecurityPolicy securityPolicy = new SettableAsyncSecurityPolicy();
ClientTransportOptions options = new ClientTransportOptions();
options.setEagAttributes(
Attributes.newBuilder().set(ApiConstants.PRE_AUTH_SERVER_OVERRIDE, true).build());
client =
newClientTransportBuilder()
.setOptions(options)
.setFactory(
newClientTransportFactoryBuilder()
.setPreAuthorizeServers(preAuthServersParam) // To be overridden.
.setSecurityPolicy(securityPolicy)
.buildClientTransportFactory())
.build();
runIfNotNull(client.start(mockClientTransportListener));
AuthRequest preAuthRequest = securityPolicy.takeNextAuthRequest(TIMEOUT_MS, MILLISECONDS);
verify(mockClientTransportListener, never()).transportReady();
preAuthRequest.setResult(Status.OK);
AuthRequest authRequest = securityPolicy.takeNextAuthRequest(TIMEOUT_MS, MILLISECONDS);
verify(mockClientTransportListener, never()).transportReady();
authRequest.setResult(Status.OK);
verify(mockClientTransportListener, timeout(TIMEOUT_MS)).transportReady();
}
@Test
@Ignore("See BinderTransportTest#socketStats.")
@Override
public void socketStats() {}
@Test
@Ignore("See BinderTransportTest#flowControlPushBack")
@Override
public void flowControlPushBack() {}
@Test
@Ignore("See BinderTransportTest#serverAlreadyListening")
@Override
public void serverAlreadyListening() {}
}

Some files were not shown because too many files have changed in this diff Show More