[Thread] add CLI commands for getting/setting config

Bug: 364237115

Change-Id: Ice42d7d02cfefd733042282939635502c259d9d5
This commit is contained in:
Handa Wang 2024-10-16 14:55:30 +00:00
commit 7eb1bbd369
4 changed files with 207 additions and 0 deletions

View file

@ -19,10 +19,12 @@ package com.android.server.thread;
import android.annotation.Nullable;
import android.content.Context;
import android.net.thread.ActiveOperationalDataset;
import android.net.thread.IConfigurationReceiver;
import android.net.thread.IOperationReceiver;
import android.net.thread.IOutputReceiver;
import android.net.thread.OperationalDatasetTimestamp;
import android.net.thread.PendingOperationalDataset;
import android.net.thread.ThreadConfiguration;
import android.net.thread.ThreadNetworkException;
import android.os.Binder;
import android.os.Process;
@ -56,6 +58,7 @@ public final class ThreadNetworkShellCommand extends BasicShellCommandHandler {
private static final Duration MIGRATE_TIMEOUT = Duration.ofSeconds(2);
private static final Duration FORCE_STOP_TIMEOUT = Duration.ofSeconds(1);
private static final Duration OT_CTL_COMMAND_TIMEOUT = Duration.ofSeconds(5);
private static final Duration CONFIG_TIMEOUT = Duration.ofSeconds(1);
private static final String PERMISSION_THREAD_NETWORK_TESTING =
"android.permission.THREAD_NETWORK_TESTING";
@ -118,6 +121,8 @@ public final class ThreadNetworkShellCommand extends BasicShellCommandHandler {
pw.println(" Sets country code to <two-letter code> or left for normal value");
pw.println(" ot-ctl <subcommand>");
pw.println(" Runs ot-ctl command");
pw.println(" config [name] [value]");
pw.println(" Gets the config or sets the value for a config entry");
}
@Override
@ -144,6 +149,8 @@ public final class ThreadNetworkShellCommand extends BasicShellCommandHandler {
return forceCountryCode();
case "get-country-code":
return getCountryCode();
case "config":
return handleConfigCommand();
case "ot-ctl":
return handleOtCtlCommand();
default:
@ -261,6 +268,68 @@ public final class ThreadNetworkShellCommand extends BasicShellCommandHandler {
return 0;
}
private int handleConfigCommand() {
ensureTestingPermission();
// Get config
if (peekNextArg() == null) {
try {
final ThreadConfiguration config = getConfig();
getOutputWriter().println("Thread configuration = " + config);
} catch (AssertionError e) {
getErrorWriter().println("Failed: " + e.getMessage());
return -1;
}
return 0;
}
// Set config
final String name = getNextArg();
final String value = getNextArg();
try {
setConfig(name, value);
} catch (AssertionError | IllegalArgumentException e) {
getErrorWriter().println(e.getMessage());
return -1;
}
return 0;
}
private ThreadConfiguration getConfig() throws AssertionError {
final CompletableFuture<ThreadConfiguration> future = new CompletableFuture<>();
mControllerService.registerConfigurationCallback(
new IConfigurationReceiver.Stub() {
@Override
public void onConfigurationChanged(ThreadConfiguration config) {
future.complete(config);
}
});
try {
return future.get(CONFIG_TIMEOUT.toSeconds(), TimeUnit.SECONDS);
} catch (InterruptedException | ExecutionException | TimeoutException e) {
throw new AssertionError("Failed to get config within timeout", e);
}
}
private void setConfig(String name, String value)
throws IllegalArgumentException, AssertionError {
if (name == null || value == null) {
throw new IllegalArgumentException(
"Invalid config name = " + name + ", value=" + value);
}
final ThreadConfiguration oldConfig = getConfig();
final ThreadConfiguration.Builder newConfigBuilder =
new ThreadConfiguration.Builder(oldConfig);
switch (name) {
case "nat64" -> newConfigBuilder.setNat64Enabled(argEnabledOrDisabled(value));
case "pd" -> newConfigBuilder.setDhcpv6PdEnabled(argEnabledOrDisabled(value));
default -> throw new IllegalArgumentException("Invalid config name: " + name);
}
CompletableFuture<Void> future = new CompletableFuture();
mControllerService.setConfiguration(newConfigBuilder.build(), newOperationReceiver(future));
waitForFuture(future, CONFIG_TIMEOUT, mErrorWriter);
}
private static final class OutputReceiver extends IOutputReceiver.Stub {
private final CompletableFuture<Void> future;
private final PrintWriter outputWriter;
@ -359,6 +428,10 @@ public final class ThreadNetworkShellCommand extends BasicShellCommandHandler {
}
}
private static boolean argEnabledOrDisabled(String arg) {
return argTrueOrFalse(arg, "enabled", "disabled");
}
private boolean getNextArgRequiredTrueOrFalse(String trueString, String falseString) {
String nextArg = getNextArgRequired();
return argTrueOrFalse(nextArg, trueString, falseString);

View file

@ -19,6 +19,7 @@ package android.net.thread;
import static android.net.thread.ThreadNetworkController.STATE_DISABLED;
import static android.net.thread.ThreadNetworkController.STATE_ENABLED;
import static android.net.thread.ThreadNetworkException.ERROR_THREAD_DISABLED;
import static android.net.thread.utils.IntegrationTestUtils.DEFAULT_CONFIG;
import static android.net.thread.utils.IntegrationTestUtils.DEFAULT_DATASET;
import static com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow;
@ -79,6 +80,7 @@ public class ThreadNetworkShellCommandTest {
public void tearDown() throws Exception {
mFtd.destroy();
ensureThreadEnabled();
mController.setConfigurationAndWait(DEFAULT_CONFIG);
}
private static void ensureThreadEnabled() {
@ -179,6 +181,27 @@ public class ThreadNetworkShellCommandTest {
assertThat(result).endsWith("Done\r\n");
}
@Test
public void config_getConfig_expectedValueIsPrinted() throws Exception {
ThreadConfiguration config =
new ThreadConfiguration.Builder().setNat64Enabled(true).build();
mController.setConfigurationAndWait(config);
final String result = runThreadCommand("config");
assertThat(result).contains("Nat64Enabled=true");
}
@Test
public void config_setConfig_expectedValueIsSet() throws Exception {
ThreadConfiguration config = new ThreadConfiguration.Builder().build();
mController.setConfigurationAndWait(config);
runThreadCommand("config nat64 enabled");
assertThat(mController.getConfiguration().isNat64Enabled()).isTrue();
}
private static String runThreadCommand(String cmd) {
return runShellCommandOrThrow("cmd thread_network " + cmd);
}

View file

@ -32,6 +32,7 @@ import android.net.TestNetworkInterface
import android.net.nsd.NsdManager
import android.net.nsd.NsdServiceInfo
import android.net.thread.ActiveOperationalDataset
import android.net.thread.ThreadConfiguration
import android.net.thread.ThreadNetworkController
import android.os.Build
import android.os.Handler
@ -108,6 +109,9 @@ object IntegrationTestUtils {
val DEFAULT_DATASET: ActiveOperationalDataset =
ActiveOperationalDataset.fromThreadTlvs(DEFAULT_DATASET_TLVS)
@JvmField
val DEFAULT_CONFIG = ThreadConfiguration.Builder().build()
/**
* Waits for the given [Supplier] to be true until given timeout.
*

View file

@ -38,8 +38,11 @@ import static org.mockito.Mockito.when;
import android.content.Context;
import android.net.thread.ActiveOperationalDataset;
import android.net.thread.IConfigurationReceiver;
import android.net.thread.IOperationReceiver;
import android.net.thread.IOutputReceiver;
import android.net.thread.PendingOperationalDataset;
import android.net.thread.ThreadConfiguration;
import android.os.Binder;
import android.os.Process;
@ -320,4 +323,108 @@ public class ThreadNetworkShellCommandTest {
inOrder.verify(mOutputWriter).print("Done");
inOrder.verify(mOutputWriter).print("\r\n");
}
@Test
public void config_getConfig_testingPermissionIsChecked() {
runShellCommand("config");
verify(mContext, times(1))
.enforceCallingOrSelfPermission(
eq("android.permission.THREAD_NETWORK_TESTING"), anyString());
}
@Test
public void config_getConfig_serviceTimeOut_failsWithTimeoutError() {
runShellCommand("config");
verify(mControllerService, times(1)).registerConfigurationCallback(any());
verify(mErrorWriter, atLeastOnce()).println(contains("timeout"));
verify(mOutputWriter, never()).println();
}
@Test
public void config_getConfig_expectedValueIsPrinted() {
doAnswer(
inv -> {
((IConfigurationReceiver) inv.getArgument(0))
.onConfigurationChanged(
new ThreadConfiguration.Builder()
.setNat64Enabled(true)
.build());
return null;
})
.when(mControllerService)
.registerConfigurationCallback(any());
runShellCommand("config");
verify(mErrorWriter, never()).println();
verify(mOutputWriter, times(1)).println(contains("Nat64Enabled=true"));
}
@Test
public void config_setConfig_testingPermissionIsChecked() {
runShellCommand("config", "nat64", "enabled");
verify(mContext, times(1))
.enforceCallingOrSelfPermission(
eq("android.permission.THREAD_NETWORK_TESTING"), anyString());
}
@Test
public void config_setConfig_serviceTimeOut_failedWithTimeoutError() {
runShellCommand("config", "nat64", "enabled");
verify(mControllerService, times(1)).registerConfigurationCallback(any());
verify(mErrorWriter, atLeastOnce()).println(contains("timeout"));
verify(mOutputWriter, never()).println();
}
@Test
public void config_invalidArgument_failsWithInvalidArgumentError() {
doAnswer(
inv -> {
((IConfigurationReceiver) inv.getArgument(0))
.onConfigurationChanged(
new ThreadConfiguration.Builder().build());
return null;
})
.when(mControllerService)
.registerConfigurationCallback(any());
runShellCommand("config", "invalidName", "invalidValue");
verify(mErrorWriter, atLeastOnce()).println(contains("Invalid config"));
verify(mOutputWriter, never()).println();
}
@Test
public void config_setConfig_expectedValueIsSet() {
doAnswer(
inv -> {
((IConfigurationReceiver) inv.getArgument(0))
.onConfigurationChanged(
new ThreadConfiguration.Builder()
.setNat64Enabled(false)
.build());
return null;
})
.when(mControllerService)
.registerConfigurationCallback(any());
doAnswer(
inv -> {
((IOperationReceiver) inv.getArgument(0)).onSuccess();
return null;
})
.when(mControllerService)
.setConfiguration(any(), any());
runShellCommand("config", "nat64", "enabled");
verify(mControllerService, times(1))
.setConfiguration(
eq(new ThreadConfiguration.Builder().setNat64Enabled(true).build()), any());
verify(mErrorWriter, never()).println();
verify(mOutputWriter, never()).println();
}
}