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. */
|
||||
@Parameters(separators = " =", commandDescription = "Command-line interface to the registry")
|
||||
final class RegistryCli {
|
||||
final class RegistryCli implements AutoCloseable, CommandRunner {
|
||||
|
||||
@Parameter(
|
||||
names = {"-e", "--environment"},
|
||||
|
@ -57,33 +57,54 @@ final class RegistryCli {
|
|||
@ParametersDelegate
|
||||
private LoggingParameters loggingParams = new LoggingParameters();
|
||||
|
||||
// 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
|
||||
void run(
|
||||
String programName,
|
||||
String[] args,
|
||||
ImmutableMap<String, ? extends Class<? extends Command>> commands) throws Exception {
|
||||
// These are created lazily on first use.
|
||||
private AppEngineConnection connection;
|
||||
private RemoteApiInstaller installer;
|
||||
|
||||
|
||||
Map<String, Command> commandInstances;
|
||||
Map<String, ? extends Class<? extends Command>> commands;
|
||||
JCommander jcommander;
|
||||
|
||||
RegistryCli(
|
||||
String programName, ImmutableMap<String, ? extends Class<? extends Command>> commands) {
|
||||
this.commands = commands;
|
||||
|
||||
Security.addProvider(new BouncyCastleProvider());
|
||||
JCommander jcommander = new JCommander(this);
|
||||
jcommander = new JCommander(this);
|
||||
jcommander.addConverterFactory(new ParameterFactory());
|
||||
jcommander.setProgramName(programName);
|
||||
|
||||
// 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.
|
||||
Map<String, Command> commandInstances = new HashMap<>();
|
||||
commandInstances = new HashMap<>();
|
||||
|
||||
HelpCommand helpCommand = new HelpCommand(jcommander);
|
||||
jcommander.addCommand("help", helpCommand);
|
||||
commandInstances.put("help", helpCommand);
|
||||
|
||||
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);
|
||||
}
|
||||
// Add the shell command.
|
||||
ShellCommand shellCommand = new ShellCommand(System.in, this);
|
||||
jcommander.addCommand("shell", shellCommand);
|
||||
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 {
|
||||
jcommander.parse(args);
|
||||
} 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 {
|
||||
// Create the main component and use it to inject the command class.
|
||||
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..
|
||||
AppEngineConnection connection = component.appEngineConnection();
|
||||
if (connection == null) {
|
||||
connection = component.appEngineConnection();
|
||||
}
|
||||
|
||||
if (command instanceof ServerSideCommand) {
|
||||
((ServerSideCommand) command).setConnection(connection);
|
||||
}
|
||||
|
||||
// RemoteApiCommands need to have the remote api installed to work.
|
||||
RemoteApiInstaller installer = new RemoteApiInstaller();
|
||||
RemoteApiOptions options = new RemoteApiOptions();
|
||||
options.server(connection.getServer().getHost(), connection.getServer().getPort());
|
||||
if (connection.isLocalhost()) {
|
||||
// Use dev credentials for localhost.
|
||||
options.useDevelopmentServerCredential();
|
||||
} else {
|
||||
options.useApplicationDefaultCredential();
|
||||
if (installer == null) {
|
||||
installer = new RemoteApiInstaller();
|
||||
RemoteApiOptions options = new RemoteApiOptions();
|
||||
options.server(connection.getServer().getHost(), connection.getServer().getPort());
|
||||
if (connection.isLocalhost()) {
|
||||
// Use dev credentials for localhost.
|
||||
options.useDevelopmentServerCredential();
|
||||
} else {
|
||||
options.useApplicationDefaultCredential();
|
||||
}
|
||||
installer.install(options);
|
||||
}
|
||||
installer.install(options);
|
||||
|
||||
// Ensure that all entity classes are loaded before command code runs.
|
||||
ObjectifyService.initOfy();
|
||||
|
||||
try {
|
||||
command.run();
|
||||
} finally {
|
||||
installer.uninstall();
|
||||
}
|
||||
command.run();
|
||||
}
|
||||
|
||||
void setEnvironment(RegistryToolEnvironment environment) {
|
||||
this.environment = environment;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -132,6 +132,8 @@ public final class RegistryTool {
|
|||
|
||||
public static void main(String[] args) throws Exception {
|
||||
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
|
||||
* 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
|
||||
* the command map.
|
||||
* also filters out HelpCommand and ShellCommand, which have special handling in {@link
|
||||
* RegistryCli} and aren't in the command map.
|
||||
*
|
||||
* @throws IOException if reading the classpath resources fails.
|
||||
*/
|
||||
|
@ -105,7 +105,8 @@ public class RegistryToolTest {
|
|||
if (Command.class.isAssignableFrom(clazz)
|
||||
&& !Modifier.isAbstract(clazz.getModifiers())
|
||||
&& !Modifier.isInterface(clazz.getModifiers())
|
||||
&& !clazz.equals(HelpCommand.class)) {
|
||||
&& !clazz.equals(HelpCommand.class)
|
||||
&& !clazz.equals(ShellCommand.class)) {
|
||||
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