mirror of
https://github.com/google/nomulus.git
synced 2025-05-13 16:07:15 +02:00
Add a "shell" pseudo-command to nomulus tool
Add the "shell" command which lets you run multiple other command in a single session, sparing you the initialization costs for all but the first of them. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=188712815
This commit is contained in:
parent
64986442bc
commit
f1c29633fb
6 changed files with 256 additions and 35 deletions
24
java/google/registry/tools/CommandRunner.java
Normal file
24
java/google/registry/tools/CommandRunner.java
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
// Copyright 2018 The Nomulus Authors. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// 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 google.registry.tools;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for things that run commands.
|
||||||
|
*
|
||||||
|
* <p>This exists only to allow us to test ShellCommand.
|
||||||
|
*/
|
||||||
|
interface CommandRunner {
|
||||||
|
public void run(String[] args) throws Exception;
|
||||||
|
}
|
|
@ -35,7 +35,7 @@ import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||||
|
|
||||||
/** Container class to create and run remote commands against a Datastore instance. */
|
/** Container class to create and run remote commands against a Datastore instance. */
|
||||||
@Parameters(separators = " =", commandDescription = "Command-line interface to the registry")
|
@Parameters(separators = " =", commandDescription = "Command-line interface to the registry")
|
||||||
final class RegistryCli {
|
final class RegistryCli implements AutoCloseable, CommandRunner {
|
||||||
|
|
||||||
@Parameter(
|
@Parameter(
|
||||||
names = {"-e", "--environment"},
|
names = {"-e", "--environment"},
|
||||||
|
@ -57,33 +57,54 @@ final class RegistryCli {
|
||||||
@ParametersDelegate
|
@ParametersDelegate
|
||||||
private LoggingParameters loggingParams = new LoggingParameters();
|
private LoggingParameters loggingParams = new LoggingParameters();
|
||||||
|
|
||||||
// The <? extends Class<? extends Command>> wildcard looks a little funny, but is needed so that
|
// These are created lazily on first use.
|
||||||
// we can accept maps with value types that are subtypes of Class<? extends Command> rather than
|
private AppEngineConnection connection;
|
||||||
// literally that type. For more explanation, see:
|
private RemoteApiInstaller installer;
|
||||||
// http://www.angelikalanger.com/GenericsFAQ/FAQSections/TypeArguments.html#FAQ104
|
|
||||||
void run(
|
|
||||||
String programName,
|
Map<String, Command> commandInstances;
|
||||||
String[] args,
|
Map<String, ? extends Class<? extends Command>> commands;
|
||||||
ImmutableMap<String, ? extends Class<? extends Command>> commands) throws Exception {
|
JCommander jcommander;
|
||||||
|
|
||||||
|
RegistryCli(
|
||||||
|
String programName, ImmutableMap<String, ? extends Class<? extends Command>> commands) {
|
||||||
|
this.commands = commands;
|
||||||
|
|
||||||
Security.addProvider(new BouncyCastleProvider());
|
Security.addProvider(new BouncyCastleProvider());
|
||||||
JCommander jcommander = new JCommander(this);
|
jcommander = new JCommander(this);
|
||||||
jcommander.addConverterFactory(new ParameterFactory());
|
jcommander.addConverterFactory(new ParameterFactory());
|
||||||
jcommander.setProgramName(programName);
|
jcommander.setProgramName(programName);
|
||||||
|
|
||||||
// Store the instances of each Command class here so we can retrieve the same one for the
|
// Store the instances of each Command class here so we can retrieve the same one for the
|
||||||
// called command later on. JCommander could have done this for us, but it doesn't.
|
// called command later on. JCommander could have done this for us, but it doesn't.
|
||||||
Map<String, Command> commandInstances = new HashMap<>();
|
commandInstances = new HashMap<>();
|
||||||
|
|
||||||
HelpCommand helpCommand = new HelpCommand(jcommander);
|
HelpCommand helpCommand = new HelpCommand(jcommander);
|
||||||
jcommander.addCommand("help", helpCommand);
|
jcommander.addCommand("help", helpCommand);
|
||||||
commandInstances.put("help", helpCommand);
|
commandInstances.put("help", helpCommand);
|
||||||
|
|
||||||
for (Map.Entry<String, ? extends Class<? extends Command>> entry : commands.entrySet()) {
|
// Add the shell command.
|
||||||
Command command = entry.getValue().getDeclaredConstructor().newInstance();
|
ShellCommand shellCommand = new ShellCommand(System.in, this);
|
||||||
jcommander.addCommand(entry.getKey(), command);
|
jcommander.addCommand("shell", shellCommand);
|
||||||
commandInstances.put(entry.getKey(), command);
|
commandInstances.put("shell", shellCommand);
|
||||||
}
|
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (Map.Entry<String, ? extends Class<? extends Command>> entry : commands.entrySet()) {
|
||||||
|
Command command = entry.getValue().getDeclaredConstructor().newInstance();
|
||||||
|
jcommander.addCommand(entry.getKey(), command);
|
||||||
|
commandInstances.put(entry.getKey(), command);
|
||||||
|
}
|
||||||
|
} catch (ReflectiveOperationException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The <? extends Class<? extends Command>> wildcard looks a little funny, but is needed so that
|
||||||
|
// we can accept maps with value types that are subtypes of Class<? extends Command> rather than
|
||||||
|
// literally that type. For more explanation, see:
|
||||||
|
// http://www.angelikalanger.com/GenericsFAQ/FAQSections/TypeArguments.html#FAQ104
|
||||||
|
@Override
|
||||||
|
public void run(String[] args) throws Exception {
|
||||||
try {
|
try {
|
||||||
jcommander.parse(args);
|
jcommander.parse(args);
|
||||||
} catch (ParameterException e) {
|
} catch (ParameterException e) {
|
||||||
|
@ -127,6 +148,14 @@ final class RegistryCli {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
if (installer != null) {
|
||||||
|
installer.uninstall();
|
||||||
|
installer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void runCommand(Command command) throws Exception {
|
private void runCommand(Command command) throws Exception {
|
||||||
// Create the main component and use it to inject the command class.
|
// Create the main component and use it to inject the command class.
|
||||||
RegistryToolComponent component = DaggerRegistryToolComponent.builder()
|
RegistryToolComponent component = DaggerRegistryToolComponent.builder()
|
||||||
|
@ -142,31 +171,35 @@ final class RegistryCli {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the App Engine connection, advise the user if they are not currently logged in..
|
// Get the App Engine connection, advise the user if they are not currently logged in..
|
||||||
AppEngineConnection connection = component.appEngineConnection();
|
if (connection == null) {
|
||||||
|
connection = component.appEngineConnection();
|
||||||
|
}
|
||||||
|
|
||||||
if (command instanceof ServerSideCommand) {
|
if (command instanceof ServerSideCommand) {
|
||||||
((ServerSideCommand) command).setConnection(connection);
|
((ServerSideCommand) command).setConnection(connection);
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoteApiCommands need to have the remote api installed to work.
|
// RemoteApiCommands need to have the remote api installed to work.
|
||||||
RemoteApiInstaller installer = new RemoteApiInstaller();
|
if (installer == null) {
|
||||||
RemoteApiOptions options = new RemoteApiOptions();
|
installer = new RemoteApiInstaller();
|
||||||
options.server(connection.getServer().getHost(), connection.getServer().getPort());
|
RemoteApiOptions options = new RemoteApiOptions();
|
||||||
if (connection.isLocalhost()) {
|
options.server(connection.getServer().getHost(), connection.getServer().getPort());
|
||||||
// Use dev credentials for localhost.
|
if (connection.isLocalhost()) {
|
||||||
options.useDevelopmentServerCredential();
|
// Use dev credentials for localhost.
|
||||||
} else {
|
options.useDevelopmentServerCredential();
|
||||||
options.useApplicationDefaultCredential();
|
} else {
|
||||||
|
options.useApplicationDefaultCredential();
|
||||||
|
}
|
||||||
|
installer.install(options);
|
||||||
}
|
}
|
||||||
installer.install(options);
|
|
||||||
|
|
||||||
// Ensure that all entity classes are loaded before command code runs.
|
// Ensure that all entity classes are loaded before command code runs.
|
||||||
ObjectifyService.initOfy();
|
ObjectifyService.initOfy();
|
||||||
|
|
||||||
try {
|
command.run();
|
||||||
command.run();
|
}
|
||||||
} finally {
|
|
||||||
installer.uninstall();
|
void setEnvironment(RegistryToolEnvironment environment) {
|
||||||
}
|
this.environment = environment;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -132,6 +132,8 @@ public final class RegistryTool {
|
||||||
|
|
||||||
public static void main(String[] args) throws Exception {
|
public static void main(String[] args) throws Exception {
|
||||||
RegistryToolEnvironment.parseFromArgs(args).setup();
|
RegistryToolEnvironment.parseFromArgs(args).setup();
|
||||||
new RegistryCli().run("nomulus", args, COMMAND_MAP);
|
try (RegistryCli cli = new RegistryCli("nomulus", COMMAND_MAP)) {
|
||||||
|
cli.run(args);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
93
java/google/registry/tools/ShellCommand.java
Normal file
93
java/google/registry/tools/ShellCommand.java
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
// Copyright 2018 The Nomulus Authors. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// 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 google.registry.tools;
|
||||||
|
|
||||||
|
import static java.nio.charset.StandardCharsets.US_ASCII;
|
||||||
|
|
||||||
|
import com.beust.jcommander.Parameters;
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.io.StreamTokenizer;
|
||||||
|
import java.io.StringReader;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements a tiny shell interpreter for the nomulus tool.
|
||||||
|
*
|
||||||
|
* <p>Parses a very simple command grammar. Tokens are either whitespace delimited words or
|
||||||
|
* double-quoted strings.
|
||||||
|
*/
|
||||||
|
@Parameters(commandDescription = "Run an interactive shell")
|
||||||
|
public class ShellCommand implements Command {
|
||||||
|
|
||||||
|
private final CommandRunner runner;
|
||||||
|
private final BufferedReader lineReader;
|
||||||
|
|
||||||
|
ShellCommand(InputStream in, CommandRunner runner) {
|
||||||
|
this.runner = runner;
|
||||||
|
this.lineReader = new BufferedReader(new InputStreamReader(in, US_ASCII));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Run the shell until the user presses "Ctrl-D". */
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
String line;
|
||||||
|
while ((line = getLine()) != null) {
|
||||||
|
String[] lineArgs = parseCommand(line);
|
||||||
|
try {
|
||||||
|
runner.run(lineArgs);
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.err.println("Got an exception:\n" + e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getLine() {
|
||||||
|
if (System.console() != null) {
|
||||||
|
System.err.print("nom> ");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return lineReader.readLine();
|
||||||
|
} catch (IOException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
static String[] parseCommand(String line) {
|
||||||
|
ImmutableList.Builder<String> resultBuilder = new ImmutableList.Builder<>();
|
||||||
|
|
||||||
|
// Create a tokenizer, make everything word characters except quoted strings and unprintable
|
||||||
|
// ascii chars and space (just treat them all as whitespace).
|
||||||
|
StreamTokenizer tokenizer = new StreamTokenizer(new StringReader(line));
|
||||||
|
tokenizer.resetSyntax();
|
||||||
|
tokenizer.whitespaceChars(0, ' ');
|
||||||
|
tokenizer.wordChars('!', '~');
|
||||||
|
tokenizer.quoteChar('"');
|
||||||
|
|
||||||
|
try {
|
||||||
|
while (tokenizer.nextToken() != StreamTokenizer.TT_EOF) {
|
||||||
|
resultBuilder.add(tokenizer.sval);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return resultBuilder.build().toArray(new String[0]);
|
||||||
|
}
|
||||||
|
}
|
|
@ -90,8 +90,8 @@ public class RegistryToolTest {
|
||||||
/**
|
/**
|
||||||
* Gets the set of all non-abstract classes implementing the {@link Command} interface (abstract
|
* Gets the set of all non-abstract classes implementing the {@link Command} interface (abstract
|
||||||
* class and interface subtypes of Command aren't expected to have cli commands). Note that this
|
* class and interface subtypes of Command aren't expected to have cli commands). Note that this
|
||||||
* also filters out HelpCommand, which has special handling in {@link RegistryCli} and isn't in
|
* also filters out HelpCommand and ShellCommand, which have special handling in {@link
|
||||||
* the command map.
|
* RegistryCli} and aren't in the command map.
|
||||||
*
|
*
|
||||||
* @throws IOException if reading the classpath resources fails.
|
* @throws IOException if reading the classpath resources fails.
|
||||||
*/
|
*/
|
||||||
|
@ -105,7 +105,8 @@ public class RegistryToolTest {
|
||||||
if (Command.class.isAssignableFrom(clazz)
|
if (Command.class.isAssignableFrom(clazz)
|
||||||
&& !Modifier.isAbstract(clazz.getModifiers())
|
&& !Modifier.isAbstract(clazz.getModifiers())
|
||||||
&& !Modifier.isInterface(clazz.getModifiers())
|
&& !Modifier.isInterface(clazz.getModifiers())
|
||||||
&& !clazz.equals(HelpCommand.class)) {
|
&& !clazz.equals(HelpCommand.class)
|
||||||
|
&& !clazz.equals(ShellCommand.class)) {
|
||||||
builder.add((Class<? extends Command>) clazz);
|
builder.add((Class<? extends Command>) clazz);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
68
javatests/google/registry/tools/ShellCommandTest.java
Normal file
68
javatests/google/registry/tools/ShellCommandTest.java
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
// Copyright 2018 The Nomulus Authors. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// 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 google.registry.tools;
|
||||||
|
|
||||||
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
import static java.nio.charset.StandardCharsets.US_ASCII;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import com.google.common.collect.ImmutableMap;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.junit.runners.JUnit4;
|
||||||
|
|
||||||
|
@RunWith(JUnit4.class)
|
||||||
|
public class ShellCommandTest {
|
||||||
|
|
||||||
|
CommandRunner cli = mock(CommandRunner.class);
|
||||||
|
|
||||||
|
public ShellCommandTest() {}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testParsing() {
|
||||||
|
assertThat(ShellCommand.parseCommand("foo bar 123 baz+ // comment \"string data\""))
|
||||||
|
.isEqualTo(new String[] {"foo", "bar", "123", "baz+", "//", "comment", "string data"});
|
||||||
|
assertThat(ShellCommand.parseCommand("\"got \\\" escapes?\""))
|
||||||
|
.isEqualTo(new String[] {"got \" escapes?"});
|
||||||
|
assertThat(ShellCommand.parseCommand("")).isEqualTo(new String[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCommandProcessing() {
|
||||||
|
String testData = "test1 foo bar\ntest2 foo bar\n";
|
||||||
|
ImmutableMap<String, Class<? extends Command>> commandMap = ImmutableMap.of();
|
||||||
|
MockCli cli = new MockCli();
|
||||||
|
ShellCommand shellCommand =
|
||||||
|
new ShellCommand(new ByteArrayInputStream(testData.getBytes(US_ASCII)), cli);
|
||||||
|
shellCommand.run();
|
||||||
|
assertThat(cli.calls)
|
||||||
|
.containsExactly(
|
||||||
|
ImmutableList.of("test1", "foo", "bar"), ImmutableList.of("test2", "foo", "bar"))
|
||||||
|
.inOrder();
|
||||||
|
}
|
||||||
|
|
||||||
|
static class MockCli implements CommandRunner {
|
||||||
|
public ArrayList<ImmutableList<String>> calls = new ArrayList<>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run(String[] args)
|
||||||
|
throws Exception {
|
||||||
|
calls.add(ImmutableList.copyOf(args));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue