diff options
author | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2023-03-05 08:20:21 +0000 |
---|---|---|
committer | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2023-03-05 08:20:21 +0000 |
commit | 31800f017ed0cca5c124a21595b06b16eb24aa93 (patch) | |
tree | ca60cf7e0a692f5afd6070e6c63bae4338c995b1 | |
parent | 50c59f2053d0bc7acbf2929d28ffa7a28ee1718e (diff) | |
parent | 5ead40a30a4041f779a622d9a7271b347028cb62 (diff) | |
download | net-busytown-mac-infra-release.tar.gz |
Snap for 9680074 from 5ead40a30a4041f779a622d9a7271b347028cb62 to busytown-mac-infra-releasebusytown-mac-infra-release
Change-Id: I0e10ef242403377479206174090741902568a4c6
15 files changed, 1091 insertions, 96 deletions
diff --git a/client-libs/netd/com/android/net/module/util/BaseNetdEventListener.java b/client-libs/netd/com/android/net/module/util/BaseNetdEventListener.java new file mode 100644 index 00000000..4180732f --- /dev/null +++ b/client-libs/netd/com/android/net/module/util/BaseNetdEventListener.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.net.module.util; + +import android.net.metrics.INetdEventListener; + +/** + * Base {@link INetdEventListener} that provides no-op implementations which can + * be overridden. + */ +public class BaseNetdEventListener extends INetdEventListener.Stub { + + @Override + public void onDnsEvent(int netId, int eventType, int returnCode, + int latencyMs, String hostname, String[] ipAddresses, + int ipAddressesCount, int uid) { } + + @Override + public void onPrivateDnsValidationEvent(int netId, String ipAddress, + String hostname, boolean validated) { } + + @Override + public void onConnectEvent(int netId, int error, int latencyMs, + String ipAddr, int port, int uid) { } + + @Override + public void onWakeupEvent(String prefix, int uid, int ethertype, + int ipNextHeader, byte[] dstHw, String srcIp, String dstIp, + int srcPort, int dstPort, long timestampNs) { } + + @Override + public void onTcpSocketStatsEvent(int[] networkIds, int[] sentPackets, + int[] lostPackets, int[] rttUs, int[] sentAckDiffMs) { } + + @Override + public void onNat64PrefixEvent(int netId, boolean added, + String prefixString, int prefixLength) { } + + @Override + public int getInterfaceVersion() { + return INetdEventListener.VERSION; + } + + @Override + public String getInterfaceHash() { + return INetdEventListener.HASH; + } +} diff --git a/common/device/com/android/net/module/util/DeviceConfigUtils.java b/common/device/com/android/net/module/util/DeviceConfigUtils.java index 1225aa7e..f8f250d7 100644 --- a/common/device/com/android/net/module/util/DeviceConfigUtils.java +++ b/common/device/com/android/net/module/util/DeviceConfigUtils.java @@ -194,7 +194,7 @@ public final class DeviceConfigUtils { return isFeatureEnabled(context, packageVersion, namespace, name, defaultEnabled); } catch (PackageManager.NameNotFoundException e) { Log.e(TAG, "Could not find the module name", e); - return defaultEnabled; + return false; } } diff --git a/common/device/com/android/net/module/util/async/OsAccess.java b/common/device/com/android/net/module/util/async/OsAccess.java new file mode 100644 index 00000000..df0ded21 --- /dev/null +++ b/common/device/com/android/net/module/util/async/OsAccess.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.net.module.util.async; + +import android.os.ParcelFileDescriptor; +import android.system.StructPollfd; + +import java.io.FileDescriptor; +import java.io.IOException; + +/** + * Provides access to all relevant OS functions.. + * + * @hide + */ +public abstract class OsAccess { + /** Closes the given file, suppressing IO exceptions. */ + public abstract void close(ParcelFileDescriptor fd); + + /** Returns file name for debugging purposes. */ + public abstract String getFileDebugName(ParcelFileDescriptor fd); + + /** Returns inner FileDescriptor instance. */ + public abstract FileDescriptor getInnerFileDescriptor(ParcelFileDescriptor fd); + + /** + * Reads available data from the given non-blocking file descriptor. + * + * Returns zero if there's no data to read at this moment. + * Returns -1 if the file has reached its end or the input stream has been closed. + * Otherwise returns the number of bytes read. + */ + public abstract int read(FileDescriptor fd, byte[] buffer, int pos, int len) + throws IOException; + + /** + * Writes data into the given non-blocking file descriptor. + * + * Returns zero if there's no buffer space to write to at this moment. + * Otherwise returns the number of bytes written. + */ + public abstract int write(FileDescriptor fd, byte[] buffer, int pos, int len) + throws IOException; + + public abstract long monotonicTimeMillis(); + public abstract void setNonBlocking(FileDescriptor fd) throws IOException; + public abstract ParcelFileDescriptor[] pipe() throws IOException; + + public abstract int poll(StructPollfd[] fds, int timeoutMs) throws IOException; + public abstract short getPollInMask(); + public abstract short getPollOutMask(); +} diff --git a/common/device/com/android/net/module/util/structs/IaPdOption.java b/common/device/com/android/net/module/util/structs/IaPdOption.java new file mode 100644 index 00000000..5a09c40b --- /dev/null +++ b/common/device/com/android/net/module/util/structs/IaPdOption.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.net.module.util.structs; + +import static com.android.net.module.util.NetworkStackConstants.DHCP6_OPTION_IA_PD; + +import com.android.net.module.util.Struct; +import com.android.net.module.util.Struct.Field; +import com.android.net.module.util.Struct.Type; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * DHCPv6 IA_PD option. + * https://tools.ietf.org/html/rfc8415. This does not contain any option. + * + * 0 1 2 3 + * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | OPTION_IA_PD | option-len | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | IAID (4 octets) | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | T1 | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | T2 | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * . . + * . IA_PD-options . + * . . + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * + */ +public class IaPdOption extends Struct { + public static final int LENGTH = 12; // option length excluding IA_PD options + + @Field(order = 0, type = Type.S16) + public short code; + @Field(order = 1, type = Type.S16) + public short length; + @Field(order = 2, type = Type.U32) + public long id; + @Field(order = 3, type = Type.U32) + public long t1; + @Field(order = 4, type = Type.U32) + public long t2; + + IaPdOption(final short code, final short length, final long id, final long t1, + final long t2) { + this.code = code; + this.length = length; + this.id = id; + this.t1 = t1; + this.t2 = t2; + } + + /** + * Build an IA_PD option from the required specific parameters. + */ + public static ByteBuffer build(final short length, final long id, final long t1, + final long t2) { + final IaPdOption option = new IaPdOption((short) DHCP6_OPTION_IA_PD, + length /* 12 + IA_PD options length */, id, t1, t2); + return ByteBuffer.wrap(option.writeToBytes(ByteOrder.BIG_ENDIAN)); + } +} diff --git a/common/device/com/android/net/module/util/structs/IaPrefixOption.java b/common/device/com/android/net/module/util/structs/IaPrefixOption.java new file mode 100644 index 00000000..1ac21ffb --- /dev/null +++ b/common/device/com/android/net/module/util/structs/IaPrefixOption.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.net.module.util.structs; + +import static com.android.net.module.util.NetworkStackConstants.DHCP6_OPTION_IAPREFIX; + +import com.android.net.module.util.Struct; +import com.android.net.module.util.Struct.Field; +import com.android.net.module.util.Struct.Type; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * DHCPv6 IA Prefix Option. + * https://tools.ietf.org/html/rfc8415. This does not contain any option. + * + * 0 1 2 3 + * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | OPTION_IAPREFIX | option-len | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | preferred-lifetime | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | valid-lifetime | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | prefix-length | | + * +-+-+-+-+-+-+-+-+ IPv6-prefix | + * | (16 octets) | + * | | + * | | + * | | + * | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | | . + * +-+-+-+-+-+-+-+-+ . + * . IAprefix-options . + * . . + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + */ +public class IaPrefixOption extends Struct { + public static final int LENGTH = 25; // option length excluding IAprefix-options + + @Field(order = 0, type = Type.S16) + public short code; + @Field(order = 1, type = Type.S16) + public short length; + @Field(order = 2, type = Type.U32) + public long preferred; + @Field(order = 3, type = Type.U32) + public long valid; + @Field(order = 4, type = Type.U8) + public short prefixLen; + @Field(order = 5, type = Type.ByteArray, arraysize = 16) + public byte[] prefix; + + IaPrefixOption(final short code, final short length, final long preferred, + final long valid, final short prefixLen, final byte[] prefix) { + this.code = code; + this.length = length; + this.preferred = preferred; + this.valid = valid; + this.prefixLen = prefixLen; + this.prefix = prefix.clone(); + } + + /** + * Build an IA_PD prefix option with given specific parameters. + */ + public static ByteBuffer build(final short length, final long preferred, final long valid, + final short prefixLen, final byte[] prefix) { + final IaPrefixOption option = new IaPrefixOption((byte) DHCP6_OPTION_IAPREFIX, + length /* 25 + IAPrefix options length */, preferred, valid, prefixLen, prefix); + return ByteBuffer.wrap(option.writeToBytes(ByteOrder.BIG_ENDIAN)); + } +} diff --git a/common/framework/com/android/net/module/util/NetworkStackConstants.java b/common/framework/com/android/net/module/util/NetworkStackConstants.java index 7b3943ce..1d88d6ef 100644 --- a/common/framework/com/android/net/module/util/NetworkStackConstants.java +++ b/common/framework/com/android/net/module/util/NetworkStackConstants.java @@ -214,11 +214,14 @@ public final class NetworkStackConstants { * * See also: * - https://datatracker.ietf.org/doc/html/rfc8415 + * - https://www.iana.org/assignments/dhcpv6-parameters/dhcpv6-parameters.xhtml */ public static final int DHCP6_CLIENT_PORT = 546; public static final int DHCP6_SERVER_PORT = 547; public static final Inet6Address ALL_DHCP_RELAY_AGENTS_AND_SERVERS = (Inet6Address) InetAddresses.parseNumericAddress("ff02::1:2"); + public static final int DHCP6_OPTION_IA_PD = 25; + public static final int DHCP6_OPTION_IAPREFIX = 26; /** * IEEE802.11 standard constants. diff --git a/common/tests/unit/src/com/android/net/module/util/DeviceConfigUtilsTest.java b/common/tests/unit/src/com/android/net/module/util/DeviceConfigUtilsTest.java index 302388db..a8e7993a 100644 --- a/common/tests/unit/src/com/android/net/module/util/DeviceConfigUtilsTest.java +++ b/common/tests/unit/src/com/android/net/module/util/DeviceConfigUtilsTest.java @@ -223,13 +223,9 @@ public class DeviceConfigUtilsTest { TEST_EXPERIMENT_FLAG)); assertFalse(DeviceConfigUtils.isFeatureEnabled(mContext, TEST_NAME_SPACE, TEST_EXPERIMENT_FLAG, TEST_APEX_NAME, false /* defaultEnabled */)); - assertTrue(DeviceConfigUtils.isFeatureEnabled(mContext, TEST_NAME_SPACE, - TEST_EXPERIMENT_FLAG, TEST_APEX_NAME, true /* defaultEnabled */)); doThrow(NameNotFoundException.class).when(mPm).getModuleInfo(anyString(), anyInt()); assertFalse(DeviceConfigUtils.isFeatureEnabled(mContext, TEST_NAME_SPACE, TEST_EXPERIMENT_FLAG, TEST_APEX_NAME, false /* defaultEnabled */)); - assertTrue(DeviceConfigUtils.isFeatureEnabled(mContext, TEST_NAME_SPACE, - TEST_EXPERIMENT_FLAG, TEST_APEX_NAME, true /* defaultEnabled */)); } diff --git a/common/tests/unit/src/com/android/testutils/HandlerUtilsTest.kt b/common/tests/unit/src/com/android/testutils/HandlerUtilsTest.kt index 46a3588f..30e0dafe 100644 --- a/common/tests/unit/src/com/android/testutils/HandlerUtilsTest.kt +++ b/common/tests/unit/src/com/android/testutils/HandlerUtilsTest.kt @@ -19,6 +19,7 @@ package com.android.testutils import android.os.Handler import android.os.HandlerThread import kotlin.test.assertEquals +import kotlin.test.assertFailsWith import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.JUnit4 @@ -72,5 +73,9 @@ class HandlerUtilsTest { assertEquals(attempt, x) handler.post { assertEquals(attempt, x) } } + + assertFailsWith<IllegalArgumentException> { + visibleOnHandlerThread(handler) { throw IllegalArgumentException() } + } } } diff --git a/common/tests/unit/src/com/android/testutils/TestableNetworkCallbackTest.kt b/common/tests/unit/src/com/android/testutils/TestableNetworkCallbackTest.kt index eed31e0f..ec7cdbdf 100644 --- a/common/tests/unit/src/com/android/testutils/TestableNetworkCallbackTest.kt +++ b/common/tests/unit/src/com/android/testutils/TestableNetworkCallbackTest.kt @@ -34,6 +34,7 @@ import com.android.testutils.RecorderCallback.CallbackEntry.Companion.NETWORK_CA import com.android.testutils.RecorderCallback.CallbackEntry.Companion.RESUMED import com.android.testutils.RecorderCallback.CallbackEntry.Companion.SUSPENDED import com.android.testutils.RecorderCallback.CallbackEntry.Companion.UNAVAILABLE +import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged import kotlin.reflect.KClass import kotlin.test.assertEquals import kotlin.test.assertFails @@ -140,20 +141,28 @@ class TestableNetworkCallbackTest { val meteredNc = NetworkCapabilities() val unmeteredNc = NetworkCapabilities().addCapability(NOT_METERED) // Check that expecting caps (with or without) fails when no callback has been received. - assertFails { mCallback.expectCapabilitiesWith(NOT_METERED, matcher, SHORT_TIMEOUT_MS) } - assertFails { mCallback.expectCapabilitiesWithout(NOT_METERED, matcher, SHORT_TIMEOUT_MS) } + assertFails { + mCallback.expectCaps(matcher, SHORT_TIMEOUT_MS) { it.hasCapability(NOT_METERED) } + } + assertFails { + mCallback.expectCaps(matcher, SHORT_TIMEOUT_MS) { !it.hasCapability(NOT_METERED) } + } // Add NOT_METERED and check that With succeeds and Without fails. mCallback.onCapabilitiesChanged(net, unmeteredNc) - mCallback.expectCapabilitiesWith(NOT_METERED, matcher) + mCallback.expectCaps(matcher) { it.hasCapability(NOT_METERED) } mCallback.onCapabilitiesChanged(net, unmeteredNc) - assertFails { mCallback.expectCapabilitiesWithout(NOT_METERED, matcher, SHORT_TIMEOUT_MS) } + assertFails { + mCallback.expectCaps(matcher, SHORT_TIMEOUT_MS) { !it.hasCapability(NOT_METERED) } + } // Don't add NOT_METERED and check that With fails and Without succeeds. mCallback.onCapabilitiesChanged(net, meteredNc) - assertFails { mCallback.expectCapabilitiesWith(NOT_METERED, matcher, SHORT_TIMEOUT_MS) } + assertFails { + mCallback.expectCaps(matcher, SHORT_TIMEOUT_MS) { it.hasCapability(NOT_METERED) } + } mCallback.onCapabilitiesChanged(net, meteredNc) - mCallback.expectCapabilitiesWithout(NOT_METERED, matcher) + mCallback.expectCaps(matcher) { !it.hasCapability(NOT_METERED) } } @Test @@ -179,37 +188,35 @@ class TestableNetworkCallbackTest { } @Test - fun testCapabilitiesThat() { + fun testExpectCaps() { val net = Network(101) val netCaps = NetworkCapabilities().addCapability(NOT_METERED).addTransportType(WIFI) // Check that expecting capabilitiesThat anything fails when no callback has been received. - assertFails { mCallback.expectCapabilitiesThat(net, SHORT_TIMEOUT_MS) { true } } + assertFails { mCallback.expectCaps(net, SHORT_TIMEOUT_MS) { true } } // Basic test for true and false mCallback.onCapabilitiesChanged(net, netCaps) - mCallback.expectCapabilitiesThat(net) { true } + mCallback.expectCaps(net) { true } mCallback.onCapabilitiesChanged(net, netCaps) - assertFails { mCallback.expectCapabilitiesThat(net, SHORT_TIMEOUT_MS) { false } } + assertFails { mCallback.expectCaps(net, SHORT_TIMEOUT_MS) { false } } // Try a positive and a negative case mCallback.onCapabilitiesChanged(net, netCaps) - mCallback.expectCapabilitiesThat(net) { caps -> - caps.hasCapability(NOT_METERED) && - caps.hasTransport(WIFI) && - !caps.hasTransport(CELLULAR) + mCallback.expectCaps(net) { + it.hasCapability(NOT_METERED) && it.hasTransport(WIFI) && !it.hasTransport(CELLULAR) } mCallback.onCapabilitiesChanged(net, netCaps) - assertFails { mCallback.expectCapabilitiesThat(net, SHORT_TIMEOUT_MS) { caps -> - caps.hasTransport(CELLULAR) - } } + assertFails { mCallback.expectCaps(net, SHORT_TIMEOUT_MS) { it.hasTransport(CELLULAR) } } // Try a matching callback on the wrong network mCallback.onCapabilitiesChanged(net, netCaps) - assertFails { mCallback.expectCapabilitiesThat(Network(100), SHORT_TIMEOUT_MS) { true } } + assertFails { + mCallback.expectCaps(Network(100), SHORT_TIMEOUT_MS) { true } + } } @Test - fun testLinkPropertiesThat() { + fun testLinkPropertiesCallbacks() { val net = Network(112) val linkAddress = LinkAddress("fe80::ace:d00d/64") val mtu = 1984 @@ -220,30 +227,30 @@ class TestableNetworkCallbackTest { } // Check that expecting linkPropsThat anything fails when no callback has been received. - assertFails { mCallback.expectLinkPropertiesThat(net, SHORT_TIMEOUT_MS) { true } } + assertFails { mCallback.expect<LinkPropertiesChanged>(net, SHORT_TIMEOUT_MS) { true } } // Basic test for true and false mCallback.onLinkPropertiesChanged(net, linkProps) - mCallback.expectLinkPropertiesThat(net) { true } + mCallback.expect<LinkPropertiesChanged>(net) { true } mCallback.onLinkPropertiesChanged(net, linkProps) - assertFails { mCallback.expectLinkPropertiesThat(net, SHORT_TIMEOUT_MS) { false } } + assertFails { mCallback.expect<LinkPropertiesChanged>(net, SHORT_TIMEOUT_MS) { false } } // Try a positive and negative case mCallback.onLinkPropertiesChanged(net, linkProps) - mCallback.expectLinkPropertiesThat(net) { lp -> - lp.interfaceName == TEST_INTERFACE_NAME && - lp.linkAddresses.contains(linkAddress) && - lp.mtu == mtu + mCallback.expect<LinkPropertiesChanged>(net) { + it.lp.interfaceName == TEST_INTERFACE_NAME && + it.lp.linkAddresses.contains(linkAddress) && + it.lp.mtu == mtu } mCallback.onLinkPropertiesChanged(net, linkProps) - assertFails { mCallback.expectLinkPropertiesThat(net, SHORT_TIMEOUT_MS) { lp -> - lp.interfaceName != TEST_INTERFACE_NAME + assertFails { mCallback.expect<LinkPropertiesChanged>(net, SHORT_TIMEOUT_MS) { + it.lp.interfaceName != TEST_INTERFACE_NAME } } // Try a matching callback on the wrong network mCallback.onLinkPropertiesChanged(net, linkProps) - assertFails { mCallback.expectLinkPropertiesThat(Network(114), SHORT_TIMEOUT_MS) { lp -> - lp.interfaceName == TEST_INTERFACE_NAME + assertFails { mCallback.expect<LinkPropertiesChanged>(Network(114), SHORT_TIMEOUT_MS) { + it.lp.interfaceName == TEST_INTERFACE_NAME } } } diff --git a/common/testutils/Android.bp b/common/testutils/Android.bp index c9b3d070..bcf89b34 100644 --- a/common/testutils/Android.bp +++ b/common/testutils/Android.bp @@ -36,6 +36,7 @@ java_library { "libnanohttpd", "net-tests-utils-host-device-common", "net-utils-device-common", + "net-utils-device-common-async", "net-utils-device-common-netlink", "modules-utils-build_system", ], diff --git a/common/testutils/devicetests/com/android/testutils/HandlerUtils.kt b/common/testutils/devicetests/com/android/testutils/HandlerUtils.kt index 68713490..aa252a56 100644 --- a/common/testutils/devicetests/com/android/testutils/HandlerUtils.kt +++ b/common/testutils/devicetests/com/android/testutils/HandlerUtils.kt @@ -65,11 +65,13 @@ fun waitForIdleSerialExecutor(executor: Executor, timeoutMs: Long) { */ fun visibleOnHandlerThread(handler: Handler, r: ThrowingRunnable) { val cv = ConditionVariable() + var e: Exception? = null handler.post { try { r.run() } catch (exception: Exception) { Log.e(TAG, "visibleOnHandlerThread caught exception", exception) + e = exception } cv.open() } @@ -77,4 +79,5 @@ fun visibleOnHandlerThread(handler: Handler, r: ThrowingRunnable) { // and this thread also has seen the change (since cv.open() happens-before cv.block() // returns). cv.block() + e?.let { throw it } } diff --git a/common/testutils/devicetests/com/android/testutils/TestableNetworkCallback.kt b/common/testutils/devicetests/com/android/testutils/TestableNetworkCallback.kt index 68d5fa91..485799c1 100644 --- a/common/testutils/devicetests/com/android/testutils/TestableNetworkCallback.kt +++ b/common/testutils/devicetests/com/android/testutils/TestableNetworkCallback.kt @@ -83,7 +83,7 @@ open class RecorderCallback private constructor( ) : CallbackEntry() data class BlockedStatusInt( override val network: Network, - val blocked: Int + val reason: Int ) : CallbackEntry() // Convenience constants for expecting a type companion object { @@ -414,20 +414,6 @@ open class TestableNetworkCallback private constructor( crossinline predicate: (T) -> Boolean = { true } ) = history.poll(timeoutMs, from) { it is T && predicate(it) } as T? - inline fun expectCapabilitiesThat( - net: Network, - tmt: Long = defaultTimeoutMs, - valid: (NetworkCapabilities) -> Boolean - ): CapabilitiesChanged = - expect(net, tmt, "Capabilities don't match expectations") { valid(it.caps) } - - inline fun expectLinkPropertiesThat( - net: Network, - tmt: Long = defaultTimeoutMs, - valid: (LinkProperties) -> Boolean - ): LinkPropertiesChanged = - expect(net, tmt, "LinkProperties don't match expectations") { valid(it.lp) } - // Expects onAvailable and the callbacks that follow it. These are: // - onSuspended, iff the network was suspended when the callbacks fire. // - onCapabilitiesChanged. @@ -448,18 +434,18 @@ open class TestableNetworkCallback private constructor( tmt: Long = defaultTimeoutMs ) { expectAvailableCallbacksCommon(net, suspended, validated, tmt) - expectBlockedStatusCallback(blocked, net, tmt) + expect<BlockedStatus>(net, tmt) { it.blocked == blocked } } fun expectAvailableCallbacks( net: Network, suspended: Boolean, validated: Boolean, - blockedStatus: Int, + blockedReason: Int, tmt: Long ) { expectAvailableCallbacksCommon(net, suspended, validated, tmt) - expectBlockedStatusCallback(blockedStatus, net) + expect<BlockedStatusInt>(net) { it.reason == blockedReason } } private fun expectAvailableCallbacksCommon( @@ -472,10 +458,8 @@ open class TestableNetworkCallback private constructor( if (suspended) { expect<Suspended>(net, tmt) } - expectCapabilitiesThat(net, tmt) { - validated == null || validated == it.hasCapability( - NET_CAPABILITY_VALIDATED - ) + expect<CapabilitiesChanged>(net, tmt) { + validated == null || validated == it.caps.hasCapability(NET_CAPABILITY_VALIDATED) } expect<LinkPropertiesChanged>(net, tmt) } @@ -488,16 +472,6 @@ open class TestableNetworkCallback private constructor( tmt: Long = defaultTimeoutMs ) = expectAvailableCallbacks(net, suspended = true, validated = validated, tmt = tmt) - fun expectBlockedStatusCallback(blocked: Boolean, net: Network, tmt: Long = defaultTimeoutMs) = - expect<BlockedStatus>(net, tmt, "Unexpected blocked status") { - it.blocked == blocked - } - - fun expectBlockedStatusCallback(blocked: Int, net: Network, tmt: Long = defaultTimeoutMs) = - expect<BlockedStatusInt>(net, tmt, "Unexpected blocked status") { - it.blocked == blocked - } - // Expects the available callbacks (where the onCapabilitiesChanged must contain the // VALIDATED capability), plus another onCapabilitiesChanged which is identical to the // one we just sent. @@ -514,17 +488,17 @@ open class TestableNetworkCallback private constructor( // when a network connects and satisfies a callback, and then immediately validates. fun expectAvailableThenValidatedCallbacks(net: Network, tmt: Long = defaultTimeoutMs) { expectAvailableCallbacks(net, validated = false, tmt = tmt) - expectCapabilitiesThat(net, tmt) { it.hasCapability(NET_CAPABILITY_VALIDATED) } + expectCaps(net, tmt) { it.hasCapability(NET_CAPABILITY_VALIDATED) } } fun expectAvailableThenValidatedCallbacks( net: Network, - blockedStatus: Int, + blockedReason: Int, tmt: Long = defaultTimeoutMs ) { expectAvailableCallbacks(net, validated = false, suspended = false, - blockedStatus = blockedStatus, tmt = tmt) - expectCapabilitiesThat(net, tmt) { it.hasCapability(NET_CAPABILITY_VALIDATED) } + blockedReason = blockedReason, tmt = tmt) + expectCaps(net, tmt) { it.hasCapability(NET_CAPABILITY_VALIDATED) } } // Temporary Java compat measure : have MockNetworkAgent implement this so that all existing @@ -571,38 +545,26 @@ open class TestableNetworkCallback private constructor( } @JvmOverloads - fun expectLinkPropertiesThat( + fun expectCaps( n: HasNetwork, tmt: Long = defaultTimeoutMs, - valid: (LinkProperties) -> Boolean - ) = expectLinkPropertiesThat(n.network, tmt, valid) + valid: (NetworkCapabilities) -> Boolean = { true } + ) = expect<CapabilitiesChanged>(n.network, tmt) { valid(it.caps) }.caps @JvmOverloads - fun expectCapabilitiesThat( - n: HasNetwork, + fun expectCaps( + n: Network, tmt: Long = defaultTimeoutMs, valid: (NetworkCapabilities) -> Boolean - ) = expectCapabilitiesThat(n.network, tmt, valid) + ) = expect<CapabilitiesChanged>(n, tmt) { valid(it.caps) }.caps - @JvmOverloads - fun expectCapabilitiesWith( - capability: Int, - n: HasNetwork, - timeoutMs: Long = defaultTimeoutMs - ): NetworkCapabilities { - return expectCapabilitiesThat(n.network, timeoutMs) { it.hasCapability(capability) }.caps - } - - @JvmOverloads - fun expectCapabilitiesWithout( - capability: Int, + fun expectCaps( n: HasNetwork, - timeoutMs: Long = defaultTimeoutMs - ): NetworkCapabilities { - return expectCapabilitiesThat(n.network, timeoutMs) { !it.hasCapability(capability) }.caps - } + valid: (NetworkCapabilities) -> Boolean + ) = expect<CapabilitiesChanged>(n.network) { valid(it.caps) }.caps - fun expectBlockedStatusCallback(expectBlocked: Boolean, n: HasNetwork) { - expectBlockedStatusCallback(expectBlocked, n.network, defaultTimeoutMs) - } + fun expectCaps( + tmt: Long, + valid: (NetworkCapabilities) -> Boolean + ) = expect<CapabilitiesChanged>(ANY_NETWORK, tmt) { valid(it.caps) }.caps } diff --git a/common/testutils/devicetests/com/android/testutils/async/FakeOsAccess.java b/common/testutils/devicetests/com/android/testutils/async/FakeOsAccess.java new file mode 100644 index 00000000..1b8e26b3 --- /dev/null +++ b/common/testutils/devicetests/com/android/testutils/async/FakeOsAccess.java @@ -0,0 +1,568 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.testutils; + +import android.os.ParcelFileDescriptor; +import android.system.StructPollfd; +import android.util.Log; + +import com.android.net.module.util.async.CircularByteBuffer; +import com.android.net.module.util.async.OsAccess; + +import java.io.FileDescriptor; +import java.io.InterruptedIOException; +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.concurrent.TimeUnit; + +public class FakeOsAccess extends OsAccess { + public static final boolean ENABLE_FINE_DEBUG = true; + + public static final int DEFAULT_FILE_DATA_QUEUE_SIZE = 8 * 1024; + + private enum FileType { PAIR, PIPE } + + // Common poll() constants: + private static final short POLLIN = 0x0001; + private static final short POLLOUT = 0x0004; + private static final short POLLERR = 0x0008; + private static final short POLLHUP = 0x0010; + + private static final Constructor<FileDescriptor> FD_CONSTRUCTOR; + private static final Field FD_FIELD_DESCRIPTOR; + private static final Field PFD_FIELD_DESCRIPTOR; + private static final Field PFD_FIELD_GUARD; + private static final Method CLOSE_GUARD_METHOD_CLOSE; + + private final int mReadQueueSize = DEFAULT_FILE_DATA_QUEUE_SIZE; + private final int mWriteQueueSize = DEFAULT_FILE_DATA_QUEUE_SIZE; + private final HashMap<Integer, File> mFiles = new HashMap<>(); + private final byte[] mTmpBuffer = new byte[1024]; + private final long mStartTime; + private final String mLogTag; + private int mFileNumberGen = 3; + private boolean mHasRateLimitedData; + + public FakeOsAccess(String logTag) { + mLogTag = logTag; + mStartTime = monotonicTimeMillis(); + } + + @Override + public long monotonicTimeMillis() { + return System.nanoTime() / 1000000; + } + + @Override + public FileDescriptor getInnerFileDescriptor(ParcelFileDescriptor fd) { + try { + return (FileDescriptor) PFD_FIELD_DESCRIPTOR.get(fd); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public void close(ParcelFileDescriptor fd) { + if (fd != null) { + close(getInnerFileDescriptor(fd)); + + try { + // Reduce CloseGuard warnings. + Object guard = PFD_FIELD_GUARD.get(fd); + CLOSE_GUARD_METHOD_CLOSE.invoke(guard); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + + public synchronized void close(FileDescriptor fd) { + if (fd != null) { + File file = getFileOrNull(fd); + if (file != null) { + file.decreaseRefCount(); + mFiles.remove(getFileDescriptorNumber(fd)); + setFileDescriptorNumber(fd, -1); + notifyAll(); + } + } + } + + private File getFile(String func, FileDescriptor fd) throws IOException { + File file = getFileOrNull(fd); + if (file == null) { + throw newIOException(func, "Unknown file descriptor: " + getFileDebugName(fd)); + } + return file; + } + + private File getFileOrNull(FileDescriptor fd) { + return mFiles.get(getFileDescriptorNumber(fd)); + } + + @Override + public String getFileDebugName(ParcelFileDescriptor fd) { + return (fd != null ? getFileDebugName(getInnerFileDescriptor(fd)) : "null"); + } + + public String getFileDebugName(FileDescriptor fd) { + if (fd == null) { + return "null"; + } + + final int fdNumber = getFileDescriptorNumber(fd); + File file = mFiles.get(fdNumber); + + StringBuilder sb = new StringBuilder(); + if (file != null) { + if (file.name != null) { + sb.append(file.name); + sb.append("/"); + } + sb.append(file.type); + sb.append("/"); + } else { + sb.append("BADFD/"); + } + sb.append(fdNumber); + return sb.toString(); + } + + public synchronized void setFileName(FileDescriptor fd, String name) { + File file = getFileOrNull(fd); + if (file != null) { + file.name = name; + } + } + + @Override + public synchronized void setNonBlocking(FileDescriptor fd) throws IOException { + File file = getFile("fcntl", fd); + file.isBlocking = false; + } + + @Override + public synchronized int read(FileDescriptor fd, byte[] buffer, int pos, int len) + throws IOException { + checkBoundaries("read", buffer, pos, len); + + File file = getFile("read", fd); + if (file.readQueue == null) { + throw newIOException("read", "File not readable"); + } + file.checkNonBlocking("read"); + + if (len == 0) { + return 0; + } + + final int availSize = file.readQueue.size(); + if (availSize == 0) { + if (file.isEndOfStream) { + // Java convention uses -1 to indicate end of stream. + return -1; + } + return 0; // EAGAIN + } + + final int readCount = Math.min(len, availSize); + file.readQueue.readBytes(buffer, pos, readCount); + maybeTransferData(file); + return readCount; + } + + @Override + public synchronized int write(FileDescriptor fd, byte[] buffer, int pos, int len) + throws IOException { + checkBoundaries("write", buffer, pos, len); + + File file = getFile("write", fd); + if (file.writeQueue == null) { + throw newIOException("read", "File not writable"); + } + if (file.type == FileType.PIPE && file.sink.openCount == 0) { + throw newIOException("write", "The other end of pipe is closed"); + } + file.checkNonBlocking("write"); + + if (len == 0) { + return 0; + } + + final int originalFreeSize = file.writeQueue.freeSize(); + if (originalFreeSize == 0) { + return 0; // EAGAIN + } + + final int writeCount = Math.min(len, originalFreeSize); + file.writeQueue.writeBytes(buffer, pos, writeCount); + maybeTransferData(file); + + if (file.writeQueue.freeSize() < originalFreeSize) { + final int additionalQueuedCount = originalFreeSize - file.writeQueue.freeSize(); + Log.i(mLogTag, logStr("Delaying transfer of " + additionalQueuedCount + + " bytes, queued=" + file.writeQueue.size() + ", type=" + file.type + + ", src_red=" + file.outboundLimiter + ", dst_red=" + file.sink.inboundLimiter)); + } + + return writeCount; + } + + private void maybeTransferData(File file) { + boolean hasChanges = copyFileBuffers(file, file.sink); + hasChanges = copyFileBuffers(file.source, file) || hasChanges; + + if (hasChanges) { + // TODO(b/245971639): Avoid notifying if no-one is polling. + notifyAll(); + } + } + + private boolean copyFileBuffers(File src, File dst) { + if (src.writeQueue == null || dst.readQueue == null) { + return false; + } + + final int originalCopyCount = Math.min(mTmpBuffer.length, + Math.min(src.writeQueue.size(), dst.readQueue.freeSize())); + + final int allowedCopyCount = RateLimiter.limit( + src.outboundLimiter, dst.inboundLimiter, originalCopyCount); + + if (allowedCopyCount < originalCopyCount) { + if (ENABLE_FINE_DEBUG) { + Log.i(mLogTag, logStr("Delaying transfer of " + + (originalCopyCount - allowedCopyCount) + " bytes, original=" + + originalCopyCount + ", allowed=" + allowedCopyCount + + ", type=" + src.type)); + } + if (originalCopyCount > 0) { + mHasRateLimitedData = true; + } + if (allowedCopyCount == 0) { + return false; + } + } + + boolean hasChanges = false; + if (allowedCopyCount > 0) { + if (dst.readQueue.size() == 0 || src.writeQueue.freeSize() == 0) { + hasChanges = true; // Read queue had no data, or write queue was full. + } + src.writeQueue.readBytes(mTmpBuffer, 0, allowedCopyCount); + dst.readQueue.writeBytes(mTmpBuffer, 0, allowedCopyCount); + } + + if (!dst.isEndOfStream && src.openCount == 0 + && src.writeQueue.size() == 0 && dst.readQueue.size() == 0) { + dst.isEndOfStream = true; + hasChanges = true; + } + + return hasChanges; + } + + public void clearInboundRateLimit(FileDescriptor fd) { + setInboundRateLimit(fd, Integer.MAX_VALUE); + } + + public void clearOutboundRateLimit(FileDescriptor fd) { + setOutboundRateLimit(fd, Integer.MAX_VALUE); + } + + public synchronized void setInboundRateLimit(FileDescriptor fd, int bytesPerSecond) { + File file = getFileOrNull(fd); + if (file != null) { + file.inboundLimiter.setBytesPerSecond(bytesPerSecond); + maybeTransferData(file); + } + } + + public synchronized void setOutboundRateLimit(FileDescriptor fd, int bytesPerSecond) { + File file = getFileOrNull(fd); + if (file != null) { + file.outboundLimiter.setBytesPerSecond(bytesPerSecond); + maybeTransferData(file); + } + } + + public synchronized ParcelFileDescriptor[] socketpair() throws IOException { + int fdNumber1 = getNextFd("socketpair"); + int fdNumber2 = getNextFd("socketpair"); + + File file1 = new File(FileType.PAIR, mReadQueueSize, mWriteQueueSize); + File file2 = new File(FileType.PAIR, mReadQueueSize, mWriteQueueSize); + + return registerFilePair(fdNumber1, file1, fdNumber2, file2); + } + + @Override + public synchronized ParcelFileDescriptor[] pipe() throws IOException { + int fdNumber1 = getNextFd("pipe"); + int fdNumber2 = getNextFd("pipe"); + + File file1 = new File(FileType.PIPE, mReadQueueSize, 0); + File file2 = new File(FileType.PIPE, 0, mWriteQueueSize); + + return registerFilePair(fdNumber1, file1, fdNumber2, file2); + } + + private ParcelFileDescriptor[] registerFilePair( + int fdNumber1, File file1, int fdNumber2, File file2) { + file1.sink = file2; + file1.source = file2; + file2.sink = file1; + file2.source = file1; + + mFiles.put(fdNumber1, file1); + mFiles.put(fdNumber2, file2); + return new ParcelFileDescriptor[] { + newParcelFileDescriptor(fdNumber1), newParcelFileDescriptor(fdNumber2)}; + } + + @Override + public short getPollInMask() { + return POLLIN; + } + + @Override + public short getPollOutMask() { + return POLLOUT; + } + + @Override + public synchronized int poll(StructPollfd[] fds, int timeoutMs) throws IOException { + if (timeoutMs < 0) { + timeoutMs = (int) TimeUnit.HOURS.toMillis(1); // Make "infinite" equal to 1 hour. + } + + if (fds == null || fds.length > 1000) { + throw newIOException("poll", "Invalid fds param"); + } + for (StructPollfd pollFd : fds) { + getFile("poll", pollFd.fd); + } + + int waitCallCount = 0; + final long deadline = monotonicTimeMillis() + timeoutMs; + while (true) { + if (mHasRateLimitedData) { + mHasRateLimitedData = false; + for (File file : mFiles.values()) { + if (file.inboundLimiter.getLastRequestReduction() != 0) { + copyFileBuffers(file.source, file); + } + if (file.outboundLimiter.getLastRequestReduction() != 0) { + copyFileBuffers(file, file.sink); + } + } + } + + final int readyCount = calculateReadyCount(fds); + if (readyCount > 0) { + if (ENABLE_FINE_DEBUG) { + Log.v(mLogTag, logStr("Poll returns " + readyCount + + " after " + waitCallCount + " wait calls")); + } + return readyCount; + } + + long remainingTimeoutMs = deadline - monotonicTimeMillis(); + if (remainingTimeoutMs <= 0) { + if (ENABLE_FINE_DEBUG) { + Log.v(mLogTag, logStr("Poll timeout " + timeoutMs + + "ms after " + waitCallCount + " wait calls")); + } + return 0; + } + + if (mHasRateLimitedData) { + remainingTimeoutMs = Math.min(RateLimiter.BUCKET_DURATION_MS, remainingTimeoutMs); + } + + try { + wait(remainingTimeoutMs); + } catch (InterruptedException e) { + // Ignore and retry + } + waitCallCount++; + } + } + + private int calculateReadyCount(StructPollfd[] fds) { + int fdCount = 0; + for (StructPollfd pollFd : fds) { + pollFd.revents = 0; + + File file = getFileOrNull(pollFd.fd); + if (file == null) { + Log.w(mLogTag, logStr("Ignoring FD concurrently closed by a buggy app: " + + getFileDebugName(pollFd.fd))); + continue; + } + + if (ENABLE_FINE_DEBUG) { + Log.v(mLogTag, logStr("calculateReadyCount fd=" + getFileDebugName(pollFd.fd) + + ", events=" + pollFd.events + ", eof=" + file.isEndOfStream + + ", r=" + (file.readQueue != null ? file.readQueue.size() : -1) + + ", w=" + (file.writeQueue != null ? file.writeQueue.freeSize() : -1))); + } + + if ((pollFd.events & POLLIN) != 0) { + if (file.readQueue != null && file.readQueue.size() != 0) { + pollFd.revents |= POLLIN; + } + if (file.isEndOfStream) { + pollFd.revents |= POLLHUP; + } + } + + if ((pollFd.events & POLLOUT) != 0) { + if (file.type == FileType.PIPE && file.sink.openCount == 0) { + pollFd.revents |= POLLERR; + } + if (file.writeQueue != null && file.writeQueue.freeSize() != 0) { + pollFd.revents |= POLLOUT; + } + } + + if (pollFd.revents != 0) { + fdCount++; + } + } + return fdCount; + } + + private int getNextFd(String func) throws IOException { + if (mFileNumberGen > 100000) { + throw newIOException(func, "Too many files open"); + } + + return mFileNumberGen++; + } + + private static IOException newIOException(String func, String message) { + return new IOException(message + ", func=" + func); + } + + public static void checkBoundaries(String func, byte[] buffer, int pos, int len) + throws IOException { + if (((buffer.length | pos | len) < 0 || pos > buffer.length - len)) { + throw newIOException(func, "Invalid array bounds"); + } + } + + private ParcelFileDescriptor newParcelFileDescriptor(int fdNumber) { + try { + return new ParcelFileDescriptor(newFileDescriptor(fdNumber)); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private FileDescriptor newFileDescriptor(int fdNumber) { + try { + return FD_CONSTRUCTOR.newInstance(Integer.valueOf(fdNumber)); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public int getFileDescriptorNumber(FileDescriptor fd) { + try { + return (Integer) FD_FIELD_DESCRIPTOR.get(fd); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private void setFileDescriptorNumber(FileDescriptor fd, int fdNumber) { + try { + FD_FIELD_DESCRIPTOR.set(fd, Integer.valueOf(fdNumber)); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private String logStr(String message) { + return "[FakeOs " + (monotonicTimeMillis() - mStartTime) + "] " + message; + } + + private class File { + final FileType type; + final CircularByteBuffer readQueue; + final CircularByteBuffer writeQueue; + final RateLimiter inboundLimiter = new RateLimiter(FakeOsAccess.this, Integer.MAX_VALUE); + final RateLimiter outboundLimiter = new RateLimiter(FakeOsAccess.this, Integer.MAX_VALUE); + String name; + int openCount = 1; + boolean isBlocking = true; + File sink; + File source; + boolean isEndOfStream; + + File(FileType type, int readQueueSize, int writeQueueSize) { + this.type = type; + readQueue = (readQueueSize > 0 ? new CircularByteBuffer(readQueueSize) : null); + writeQueue = (writeQueueSize > 0 ? new CircularByteBuffer(writeQueueSize) : null); + } + + void decreaseRefCount() { + if (openCount <= 0) { + throw new IllegalStateException(); + } + openCount--; + } + + void checkNonBlocking(String func) throws IOException { + if (isBlocking) { + throw newIOException(func, "File in blocking mode"); + } + } + } + + static { + try { + FD_CONSTRUCTOR = FileDescriptor.class.getDeclaredConstructor(int.class); + FD_CONSTRUCTOR.setAccessible(true); + + Field descriptorIntField; + try { + descriptorIntField = FileDescriptor.class.getDeclaredField("descriptor"); + } catch (NoSuchFieldException e) { + descriptorIntField = FileDescriptor.class.getDeclaredField("fd"); + } + FD_FIELD_DESCRIPTOR = descriptorIntField; + FD_FIELD_DESCRIPTOR.setAccessible(true); + + PFD_FIELD_DESCRIPTOR = ParcelFileDescriptor.class.getDeclaredField("mFd"); + PFD_FIELD_DESCRIPTOR.setAccessible(true); + + PFD_FIELD_GUARD = ParcelFileDescriptor.class.getDeclaredField("mGuard"); + PFD_FIELD_GUARD.setAccessible(true); + + CLOSE_GUARD_METHOD_CLOSE = Class.forName("dalvik.system.CloseGuard") + .getDeclaredMethod("close"); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/common/testutils/devicetests/com/android/testutils/async/RateLimiter.java b/common/testutils/devicetests/com/android/testutils/async/RateLimiter.java new file mode 100644 index 00000000..137873db --- /dev/null +++ b/common/testutils/devicetests/com/android/testutils/async/RateLimiter.java @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.testutils; + +import com.android.net.module.util.async.OsAccess; + +import java.util.Arrays; + +/** + * Limits the number of bytes processed to the given maximum of bytes per second. + * + * The limiter tracks the total for the past second, along with sums for each 10ms + * in the past second, allowing the total to be adjusted as the time passes. + */ +public final class RateLimiter { + private static final int PERIOD_DURATION_MS = 1000; + private static final int BUCKET_COUNT = 100; + + public static final int BUCKET_DURATION_MS = PERIOD_DURATION_MS / BUCKET_COUNT; + + private final OsAccess mOsAccess; + private final int[] mStatBuckets = new int[BUCKET_COUNT]; + private int mMaxPerPeriodBytes; + private int mMaxPerBucketBytes; + private int mRecordedPeriodBytes; + private long mLastLimitTimestamp; + private int mLastRequestReduction; + + public RateLimiter(OsAccess osAccess, int bytesPerSecond) { + mOsAccess = osAccess; + setBytesPerSecond(bytesPerSecond); + clear(); + } + + public int getBytesPerSecond() { + return mMaxPerPeriodBytes; + } + + public void setBytesPerSecond(int bytesPerSecond) { + mMaxPerPeriodBytes = bytesPerSecond; + mMaxPerBucketBytes = Math.max(1, (mMaxPerPeriodBytes / BUCKET_COUNT) * 2); + } + + public void clear() { + mLastLimitTimestamp = mOsAccess.monotonicTimeMillis(); + mRecordedPeriodBytes = 0; + Arrays.fill(mStatBuckets, 0); + } + + public static int limit(RateLimiter limiter1, RateLimiter limiter2, int requestedBytes) { + final long now = limiter1.mOsAccess.monotonicTimeMillis(); + final int allowedCount = Math.min(limiter1.calculateLimit(now, requestedBytes), + limiter2.calculateLimit(now, requestedBytes)); + limiter1.recordBytes(now, requestedBytes, allowedCount); + limiter2.recordBytes(now, requestedBytes, allowedCount); + return allowedCount; + } + + public int limit(int requestedBytes) { + final long now = mOsAccess.monotonicTimeMillis(); + final int allowedCount = calculateLimit(now, requestedBytes); + recordBytes(now, requestedBytes, allowedCount); + return allowedCount; + } + + public int getLastRequestReduction() { + return mLastRequestReduction; + } + + public boolean acceptAllOrNone(int requestedBytes) { + final long now = mOsAccess.monotonicTimeMillis(); + final int allowedCount = calculateLimit(now, requestedBytes); + if (allowedCount < requestedBytes) { + return false; + } + recordBytes(now, requestedBytes, allowedCount); + return true; + } + + private int calculateLimit(long now, int requestedBytes) { + // First remove all stale bucket data and adjust the total. + final long currentBucketAbsIdx = now / BUCKET_DURATION_MS; + final long staleCutoffIdx = currentBucketAbsIdx - BUCKET_COUNT; + for (long i = mLastLimitTimestamp / BUCKET_DURATION_MS; i < staleCutoffIdx; i++) { + final int idx = (int) (i % BUCKET_COUNT); + mRecordedPeriodBytes -= mStatBuckets[idx]; + mStatBuckets[idx] = 0; + } + + final int bucketIdx = (int) (currentBucketAbsIdx % BUCKET_COUNT); + final int maxAllowed = Math.min(mMaxPerPeriodBytes - mRecordedPeriodBytes, + Math.min(mMaxPerBucketBytes - mStatBuckets[bucketIdx], requestedBytes)); + return Math.max(0, maxAllowed); + } + + private void recordBytes(long now, int requestedBytes, int actualBytes) { + mStatBuckets[(int) ((now / BUCKET_DURATION_MS) % BUCKET_COUNT)] += actualBytes; + mRecordedPeriodBytes += actualBytes; + mLastRequestReduction = requestedBytes - actualBytes; + mLastLimitTimestamp = now; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("{max="); + sb.append(mMaxPerPeriodBytes); + sb.append(",max_bucket="); + sb.append(mMaxPerBucketBytes); + sb.append(",total="); + sb.append(mRecordedPeriodBytes); + sb.append(",last_red="); + sb.append(mLastRequestReduction); + sb.append('}'); + return sb.toString(); + } +} diff --git a/common/testutils/devicetests/com/android/testutils/filters/CtsNetTestCasesMaxTargetSdk33.kt b/common/testutils/devicetests/com/android/testutils/filters/CtsNetTestCasesMaxTargetSdk33.kt new file mode 100644 index 00000000..5af890ff --- /dev/null +++ b/common/testutils/devicetests/com/android/testutils/filters/CtsNetTestCasesMaxTargetSdk33.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.testutils.filters + +/** + * Only run this test in the CtsNetTestCasesMaxTargetSdk33 suite. + */ +annotation class CtsNetTestCasesMaxTargetSdk33(val reason: String) |