diff --git a/CHANGELOG.md b/CHANGELOG.md index 1848c9d..e27c26b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ on [Keep a CHANGELOG](http://keepachangelog.com/). This project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] +### Added +JMX client can now attach to a running command by using the command line switch '--attach ' ## [2.4.1] - 2019-11-20 ### Fixed diff --git a/README.md b/README.md index d3e9481..24e8a8f 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ From Framework 6.0.0 and above - **-cn,--context-name** _The name of the context on which to run the command. Required_ - **-c,--command** _The name of the System Command to run_ +- **-a,--attach** _Attach to as running system command by specifying its command id_ - **-h,--host** _Hostname or IP address of the Wildfly server. Defaults to localhost_ - **-p,--port** _Wildfly management port. Defaults to 9990 (the default for Wildfly)_ - **-u, --username** _Optional username for Wildfly management security_ @@ -23,6 +24,17 @@ From Framework 6.0.0 and above _java -jar framework-jmx-command-client.jar --context-name example-single --command PING_ Would run the command 'PING' against a local wildfly instance with no authentication with a example-single.war deployed + +### Attaching to a running command +Sometimes the JMX client may fail (or be accidentally closed). As the command running on Wildfly +is asynchronous, then it would still be running on the server even if the client failed. In that +case you may want to re-attach to the running command by supplying the command id: + +_java -jar framework-jmx-command-client.jar --attach _ + +The command id is always logged by the client when a command is first sent. Failing that +it can be seen in the system_command_status table (which lists the status of all commands +sent to wildfly) ###### Note: If you are running wildfly on your local machine and running as the same user as the one you are diff --git a/src/main/java/uk/gov/justice/framework/command/client/CommandExecutor.java b/src/main/java/uk/gov/justice/framework/command/client/CommandExecutor.java index dc2e3ae..4d63650 100644 --- a/src/main/java/uk/gov/justice/framework/command/client/CommandExecutor.java +++ b/src/main/java/uk/gov/justice/framework/command/client/CommandExecutor.java @@ -1,12 +1,15 @@ package uk.gov.justice.framework.command.client; +import static java.util.UUID.fromString; + import uk.gov.justice.framework.command.client.io.CommandPrinter; -import uk.gov.justice.framework.command.client.io.ToConsolePrinter; +import uk.gov.justice.framework.command.client.jmx.SystemCommandAttacher; import uk.gov.justice.framework.command.client.jmx.SystemCommandInvoker; import uk.gov.justice.services.jmx.api.command.SystemCommandDetails; import uk.gov.justice.services.jmx.system.command.client.connection.JmxParameters; import java.util.List; +import java.util.UUID; import javax.inject.Inject; @@ -17,6 +20,9 @@ public class CommandExecutor { @Inject private SystemCommandInvoker systemCommandInvoker; + @Inject + private SystemCommandAttacher systemCommandAttacher; + @Inject private CommandPrinter commandPrinter; @@ -27,9 +33,14 @@ public void executeCommand( if (commandLine.hasOption("list")) { commandPrinter.printSystemCommands(systemCommandDetails); + } else if (commandLine.hasOption("attach")) { + final UUID commandId = fromString(commandLine.getOptionValue("attach")); + systemCommandAttacher.attachToRunningCommand(commandId, jmxParameters); } else { final String commandName = commandLine.getOptionValue("command"); systemCommandInvoker.runSystemCommand(commandName, jmxParameters); } } + + } diff --git a/src/main/java/uk/gov/justice/framework/command/client/CommandIdProvider.java b/src/main/java/uk/gov/justice/framework/command/client/CommandIdProvider.java new file mode 100644 index 0000000..848eca7 --- /dev/null +++ b/src/main/java/uk/gov/justice/framework/command/client/CommandIdProvider.java @@ -0,0 +1,36 @@ +package uk.gov.justice.framework.command.client; + +import static java.util.Optional.empty; +import static java.util.Optional.of; +import static java.util.UUID.fromString; + +import uk.gov.justice.framework.command.client.io.ToConsolePrinter; + +import java.util.Optional; +import java.util.UUID; + +import javax.inject.Inject; + +import org.apache.commons.cli.CommandLine; + +public class CommandIdProvider { + + @Inject + private ToConsolePrinter toConsolePrinter; + + public Optional getCommandId(final CommandLine commandLine) { + final String commandId = commandLine.getOptionValue("attach"); + + if (commandId == null) { + toConsolePrinter.println("Please specify a command id to attach to"); + } else { + try { + return of(fromString(commandId)); + } catch (final IllegalArgumentException e) { + toConsolePrinter.printf("Command id '%s' is not a UUID", commandId); + } + } + + return empty(); + } +} diff --git a/src/main/java/uk/gov/justice/framework/command/client/MainApplication.java b/src/main/java/uk/gov/justice/framework/command/client/MainApplication.java index 8687200..2d302ee 100644 --- a/src/main/java/uk/gov/justice/framework/command/client/MainApplication.java +++ b/src/main/java/uk/gov/justice/framework/command/client/MainApplication.java @@ -5,7 +5,6 @@ import uk.gov.justice.framework.command.client.cdi.producers.OptionsFactory; import uk.gov.justice.framework.command.client.jmx.ListCommandsInvoker; import uk.gov.justice.framework.command.client.startup.CommandLineArgumentParser; -import uk.gov.justice.services.jmx.api.command.SystemCommand; import uk.gov.justice.services.jmx.api.command.SystemCommandDetails; import uk.gov.justice.services.jmx.system.command.client.connection.JmxParameters; diff --git a/src/main/java/uk/gov/justice/framework/command/client/jmx/CommandPoller.java b/src/main/java/uk/gov/justice/framework/command/client/jmx/CommandPoller.java index c1ad186..9f7fcfa 100644 --- a/src/main/java/uk/gov/justice/framework/command/client/jmx/CommandPoller.java +++ b/src/main/java/uk/gov/justice/framework/command/client/jmx/CommandPoller.java @@ -2,11 +2,11 @@ import static java.lang.String.format; import static java.time.Duration.between; +import static org.apache.commons.lang3.time.DurationFormatUtils.formatDuration; import uk.gov.justice.framework.command.client.io.ToConsolePrinter; import uk.gov.justice.framework.command.client.util.Sleeper; import uk.gov.justice.framework.command.client.util.UtcClock; -import uk.gov.justice.services.jmx.api.command.SystemCommand; import uk.gov.justice.services.jmx.api.mbean.SystemCommanderMBean; import java.time.ZonedDateTime; @@ -28,9 +28,11 @@ public class CommandPoller { @Inject private ToConsolePrinter toConsolePrinter; - public void runUntilComplete(final SystemCommanderMBean systemCommanderMBean, final UUID commandId, final String commandName) { + public void runUntilComplete(final SystemCommanderMBean systemCommanderMBean, final RunContext runContext) { - final ZonedDateTime startTime = clock.now(); + final UUID commandId = runContext.getCommandId(); + final String commandName = runContext.getCommandName(); + final ZonedDateTime startTime = runContext.getStartTime(); int count = 0; while (! commandChecker.commandComplete(systemCommanderMBean, commandId, startTime)) { @@ -38,10 +40,12 @@ public void runUntilComplete(final SystemCommanderMBean systemCommanderMBean, fi count++; if (count % 10 == 0) { - final long seconds = between(startTime, clock.now()).getSeconds(); - toConsolePrinter.println(format("%s running for %d seconds", commandName, seconds)); + final long durationMillis = between(startTime, clock.now()).toMillis(); + + final String duration = formatDuration(durationMillis, "HH:mm:ss"); + + toConsolePrinter.println(format("%s running for %s (hh:mm:ss)", commandName, duration)); } } } - } diff --git a/src/main/java/uk/gov/justice/framework/command/client/jmx/ListCommandsInvoker.java b/src/main/java/uk/gov/justice/framework/command/client/jmx/ListCommandsInvoker.java index e0c24a3..9b39c25 100644 --- a/src/main/java/uk/gov/justice/framework/command/client/jmx/ListCommandsInvoker.java +++ b/src/main/java/uk/gov/justice/framework/command/client/jmx/ListCommandsInvoker.java @@ -3,7 +3,6 @@ import static java.util.Optional.of; import uk.gov.justice.framework.command.client.io.ToConsolePrinter; -import uk.gov.justice.services.jmx.api.command.SystemCommand; import uk.gov.justice.services.jmx.api.command.SystemCommandDetails; import uk.gov.justice.services.jmx.api.mbean.SystemCommanderMBean; import uk.gov.justice.services.jmx.system.command.client.SystemCommanderClient; diff --git a/src/main/java/uk/gov/justice/framework/command/client/jmx/RunContext.java b/src/main/java/uk/gov/justice/framework/command/client/jmx/RunContext.java new file mode 100644 index 0000000..d389c88 --- /dev/null +++ b/src/main/java/uk/gov/justice/framework/command/client/jmx/RunContext.java @@ -0,0 +1,54 @@ +package uk.gov.justice.framework.command.client.jmx; + +import java.time.ZonedDateTime; +import java.util.Objects; +import java.util.UUID; + +public class RunContext { + + private final UUID commandId; + private final String commandName; + private final ZonedDateTime startTime; + + public RunContext(final UUID commandId, final String commandName, final ZonedDateTime startTime) { + this.commandId = commandId; + this.commandName = commandName; + this.startTime = startTime; + } + + public UUID getCommandId() { + return commandId; + } + + public String getCommandName() { + return commandName; + } + + public ZonedDateTime getStartTime() { + return startTime; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (!(o instanceof RunContext)) return false; + final RunContext that = (RunContext) o; + return Objects.equals(commandId, that.commandId) && + Objects.equals(commandName, that.commandName) && + Objects.equals(startTime, that.startTime); + } + + @Override + public int hashCode() { + return Objects.hash(commandId, commandName, startTime); + } + + @Override + public String toString() { + return "RunContext{" + + "commandId=" + commandId + + ", commandName='" + commandName + '\'' + + ", startTime=" + startTime + + '}'; + } +} diff --git a/src/main/java/uk/gov/justice/framework/command/client/jmx/SystemCommandAttachRunner.java b/src/main/java/uk/gov/justice/framework/command/client/jmx/SystemCommandAttachRunner.java new file mode 100644 index 0000000..e56a990 --- /dev/null +++ b/src/main/java/uk/gov/justice/framework/command/client/jmx/SystemCommandAttachRunner.java @@ -0,0 +1,47 @@ +package uk.gov.justice.framework.command.client.jmx; + +import static uk.gov.justice.services.jmx.api.domain.CommandState.COMMAND_IN_PROGRESS; +import static uk.gov.justice.services.jmx.api.domain.CommandState.COMMAND_RECEIVED; + +import uk.gov.justice.framework.command.client.io.ToConsolePrinter; +import uk.gov.justice.services.jmx.api.CommandNotFoundException; +import uk.gov.justice.services.jmx.api.domain.CommandState; +import uk.gov.justice.services.jmx.api.domain.SystemCommandStatus; +import uk.gov.justice.services.jmx.api.mbean.SystemCommanderMBean; + +import java.time.ZonedDateTime; +import java.util.UUID; + +import javax.inject.Inject; + +public class SystemCommandAttachRunner { + + @Inject + private CommandPoller commandPoller; + + @Inject + private ToConsolePrinter toConsolePrinter; + + public void attach(final UUID commandId, final SystemCommanderMBean systemCommanderMBean) { + try { + final SystemCommandStatus commandStatus = systemCommanderMBean.getCommandStatus(commandId); + final String commandName = commandStatus.getSystemCommandName(); + final CommandState commandState = commandStatus.getCommandState(); + + if (commandState == COMMAND_IN_PROGRESS || commandState == COMMAND_RECEIVED) { + final ZonedDateTime startTime = commandStatus.getStatusChangedAt(); + final RunContext runContext = new RunContext( + commandId, + commandName, + startTime + ); + + commandPoller.runUntilComplete(systemCommanderMBean, runContext); + } else { + toConsolePrinter.printf("Cannot attach to Command with id '%s'. Command is not running. Current command status: %s", commandId, commandState); + } + } catch (final CommandNotFoundException e) { + toConsolePrinter.printf("No system command exists with id '%s", commandId); + } + } +} diff --git a/src/main/java/uk/gov/justice/framework/command/client/jmx/SystemCommandAttacher.java b/src/main/java/uk/gov/justice/framework/command/client/jmx/SystemCommandAttacher.java new file mode 100644 index 0000000..2713c2e --- /dev/null +++ b/src/main/java/uk/gov/justice/framework/command/client/jmx/SystemCommandAttacher.java @@ -0,0 +1,44 @@ +package uk.gov.justice.framework.command.client.jmx; + +import uk.gov.justice.framework.command.client.io.ToConsolePrinter; +import uk.gov.justice.services.jmx.api.SystemCommandInvocationFailedException; +import uk.gov.justice.services.jmx.api.mbean.SystemCommanderMBean; +import uk.gov.justice.services.jmx.system.command.client.SystemCommanderClient; +import uk.gov.justice.services.jmx.system.command.client.SystemCommanderClientFactory; +import uk.gov.justice.services.jmx.system.command.client.connection.JmxParameters; + +import java.util.UUID; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; + +@ApplicationScoped +public class SystemCommandAttacher { + + @Inject + private SystemCommanderClientFactory systemCommanderClientFactory; + + @Inject + private SystemCommandAttachRunner systemCommandAttachRunner; + + @Inject + private ToConsolePrinter toConsolePrinter; + + public void attachToRunningCommand(final UUID commandId, final JmxParameters jmxParameters) { + + final String contextName = jmxParameters.getContextName(); + toConsolePrinter.printf("Attaching to system command with id'%s'", commandId); + toConsolePrinter.printf("Connecting to %s context at '%s' on port %d", contextName, jmxParameters.getHost(), jmxParameters.getPort()); + + jmxParameters.getCredentials().ifPresent(credentials -> toConsolePrinter.printf("Connecting with credentials for user '%s'", credentials.getUsername())); + + try (final SystemCommanderClient systemCommanderClient = systemCommanderClientFactory.create(jmxParameters)) { + final SystemCommanderMBean systemCommanderMBean = systemCommanderClient.getRemote(contextName); + systemCommandAttachRunner.attach(commandId, systemCommanderMBean); + } catch (final SystemCommandInvocationFailedException e) { + toConsolePrinter.printf("The command with id '%s' failed: %s", commandId, e.getMessage()); + toConsolePrinter.println(e.getServerStackTrace()); + throw e; + } + } +} diff --git a/src/main/java/uk/gov/justice/framework/command/client/jmx/SystemCommandInvoker.java b/src/main/java/uk/gov/justice/framework/command/client/jmx/SystemCommandInvoker.java index 471d77b..11c9b99 100644 --- a/src/main/java/uk/gov/justice/framework/command/client/jmx/SystemCommandInvoker.java +++ b/src/main/java/uk/gov/justice/framework/command/client/jmx/SystemCommandInvoker.java @@ -1,9 +1,9 @@ package uk.gov.justice.framework.command.client.jmx; import uk.gov.justice.framework.command.client.io.ToConsolePrinter; +import uk.gov.justice.framework.command.client.util.UtcClock; import uk.gov.justice.services.jmx.api.SystemCommandInvocationFailedException; import uk.gov.justice.services.jmx.api.UnrunnableSystemCommandException; -import uk.gov.justice.services.jmx.api.command.SystemCommand; import uk.gov.justice.services.jmx.api.mbean.SystemCommanderMBean; import uk.gov.justice.services.jmx.system.command.client.SystemCommanderClient; import uk.gov.justice.services.jmx.system.command.client.SystemCommanderClientFactory; @@ -26,6 +26,10 @@ public class SystemCommandInvoker { @Inject private ToConsolePrinter toConsolePrinter; + @Inject + private UtcClock clock; + + public void runSystemCommand(final String commandName, final JmxParameters jmxParameters) { final String contextName = jmxParameters.getContextName(); @@ -42,7 +46,14 @@ public void runSystemCommand(final String commandName, final JmxParameters jmxPa final SystemCommanderMBean systemCommanderMBean = systemCommanderClient.getRemote(contextName); final UUID commandId = systemCommanderMBean.call(commandName); toConsolePrinter.printf("System command '%s' with id '%s' successfully sent to %s", commandName, commandId, contextName); - commandPoller.runUntilComplete(systemCommanderMBean, commandId, commandName); + + final RunContext runContext = new RunContext( + commandId, + commandName, + clock.now() + ); + + commandPoller.runUntilComplete(systemCommanderMBean, runContext); } catch (final UnrunnableSystemCommandException e) { toConsolePrinter.printf("The command '%s' is not supported on this %s context", commandName, contextName); diff --git a/src/main/java/uk/gov/justice/framework/command/client/startup/CommandLineArgumentParser.java b/src/main/java/uk/gov/justice/framework/command/client/startup/CommandLineArgumentParser.java index 3d27324..9ed342d 100644 --- a/src/main/java/uk/gov/justice/framework/command/client/startup/CommandLineArgumentParser.java +++ b/src/main/java/uk/gov/justice/framework/command/client/startup/CommandLineArgumentParser.java @@ -33,11 +33,11 @@ public Optional parse(final String[] args) { try { final CommandLine commandLine = basicParser.parse(optionsFactory.createOptions(), args); - if (commandLine.hasOption("command") || commandLine.hasOption("list")) { + if (commandLine.hasOption("command") || commandLine.hasOption("attach") || commandLine.hasOption("list")) { return of(commandLine); } - toConsolePrinter.println("No system command specifed."); + toConsolePrinter.println("No system command specified."); return empty(); diff --git a/src/test/java/uk/gov/justice/framework/command/client/CommandExecutorTest.java b/src/test/java/uk/gov/justice/framework/command/client/CommandExecutorTest.java index 0927258..a2b1639 100644 --- a/src/test/java/uk/gov/justice/framework/command/client/CommandExecutorTest.java +++ b/src/test/java/uk/gov/justice/framework/command/client/CommandExecutorTest.java @@ -1,21 +1,20 @@ package uk.gov.justice.framework.command.client; import static java.util.Arrays.asList; -import static java.util.Optional.empty; -import static java.util.Optional.of; +import static java.util.UUID.randomUUID; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; import uk.gov.justice.framework.command.client.io.CommandPrinter; -import uk.gov.justice.framework.command.client.io.ToConsolePrinter; +import uk.gov.justice.framework.command.client.jmx.SystemCommandAttacher; import uk.gov.justice.framework.command.client.jmx.SystemCommandInvoker; -import uk.gov.justice.services.jmx.api.command.SystemCommand; import uk.gov.justice.services.jmx.api.command.SystemCommandDetails; import uk.gov.justice.services.jmx.system.command.client.connection.JmxParameters; import java.util.List; +import java.util.UUID; import org.apache.commons.cli.CommandLine; import org.junit.Test; @@ -24,7 +23,6 @@ import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; - @RunWith(MockitoJUnitRunner.class) public class CommandExecutorTest { @@ -32,7 +30,7 @@ public class CommandExecutorTest { private SystemCommandInvoker systemCommandInvoker; @Mock - private ToConsolePrinter toConsolePrinter; + private SystemCommandAttacher systemCommandAttacher; @Mock private CommandPrinter commandPrinter; @@ -53,6 +51,7 @@ public void shouldLookupSystemCommandByNameAndExecute() throws Exception { final List systemCommands = asList(systemCommandDetails_1, systemCommandDetails_2); when(commandLine.hasOption("list")).thenReturn(false); + when(commandLine.hasOption("attach")).thenReturn(false); when(commandLine.getOptionValue("command")).thenReturn(commandName); commandExecutor.executeCommand(commandLine, jmxParameters, systemCommands); @@ -60,6 +59,27 @@ public void shouldLookupSystemCommandByNameAndExecute() throws Exception { verify(systemCommandInvoker).runSystemCommand(commandName, jmxParameters); } + @Test + public void shouldAttachToCurrentlyRunningCommand() throws Exception { + + final UUID commandId = randomUUID(); + + final SystemCommandDetails systemCommandDetails_1 = mock(SystemCommandDetails.class); + final SystemCommandDetails systemCommandDetails_2 = mock(SystemCommandDetails.class); + + final CommandLine commandLine = mock(CommandLine.class); + final JmxParameters jmxParameters = mock(JmxParameters.class); + final List systemCommands = asList(systemCommandDetails_1, systemCommandDetails_2); + + when(commandLine.hasOption("list")).thenReturn(false); + when(commandLine.hasOption("attach")).thenReturn(true); + when(commandLine.getOptionValue("attach")).thenReturn(commandId.toString()); + + commandExecutor.executeCommand(commandLine, jmxParameters, systemCommands); + + verify(systemCommandAttacher).attachToRunningCommand(commandId, jmxParameters); + } + @Test public void shouldListSystemCommandsIfCommandLineOptionIsList() throws Exception { diff --git a/src/test/java/uk/gov/justice/framework/command/client/CommandIdProviderTest.java b/src/test/java/uk/gov/justice/framework/command/client/CommandIdProviderTest.java new file mode 100644 index 0000000..35dbd17 --- /dev/null +++ b/src/test/java/uk/gov/justice/framework/command/client/CommandIdProviderTest.java @@ -0,0 +1,77 @@ +package uk.gov.justice.framework.command.client; + +import static java.util.UUID.randomUUID; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +import uk.gov.justice.framework.command.client.io.ToConsolePrinter; + +import java.util.Optional; +import java.util.UUID; + +import org.apache.commons.cli.CommandLine; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class CommandIdProviderTest { + + @Mock + private ToConsolePrinter toConsolePrinter; + + @InjectMocks + private CommandIdProvider commandIdProvider; + + @Test + public void shouldGetTheCommandIdFromTheCommandLine() throws Exception { + + final UUID commandId = randomUUID(); + + final CommandLine commandLine = mock(CommandLine.class); + + when(commandLine.getOptionValue("attach")).thenReturn(commandId.toString()); + + final Optional commandIdOptional = commandIdProvider.getCommandId(commandLine); + + if (commandIdOptional.isPresent()) { + assertThat(commandIdOptional.get(), is(commandId)); + } else { + fail(); + } + + verifyZeroInteractions(toConsolePrinter); + } + + @Test + public void shouldLogAndReturnEmptyIfNoCommandIdFound() throws Exception { + + final CommandLine commandLine = mock(CommandLine.class); + + when(commandLine.getOptionValue("attach")).thenReturn(null); + + assertThat(commandIdProvider.getCommandId(commandLine).isPresent(), is(false)); + + verify(toConsolePrinter).println("Please specify a command id to attach to"); + } + + @Test + public void shouldLogAndReturnEmptyIfCommandIdIsNotAUuid() throws Exception { + + final String dodgyCommandId = "something silly"; + final CommandLine commandLine = mock(CommandLine.class); + + when(commandLine.getOptionValue("attach")).thenReturn(dodgyCommandId); + + assertThat(commandIdProvider.getCommandId(commandLine).isPresent(), is(false)); + + verify(toConsolePrinter).printf("Command id '%s' is not a UUID", dodgyCommandId); + } +} diff --git a/src/test/java/uk/gov/justice/framework/command/client/CommandLineArgumentParserTest.java b/src/test/java/uk/gov/justice/framework/command/client/CommandLineArgumentParserTest.java index 9bb7ab4..d916812 100644 --- a/src/test/java/uk/gov/justice/framework/command/client/CommandLineArgumentParserTest.java +++ b/src/test/java/uk/gov/justice/framework/command/client/CommandLineArgumentParserTest.java @@ -48,6 +48,29 @@ public void shouldParseTheCommandLineArgumentsAndReturnTheCommandLineObjectIfCom when(optionsFactory.createOptions()).thenReturn(options); when(basicParser.parse(options, args)).thenReturn(commandLine); when(commandLine.hasOption("command")).thenReturn(true); + when(commandLine.hasOption("attach")).thenReturn(false); + when(commandLine.hasOption("list")).thenReturn(false); + + final Optional commandLineOptional = commandLineArgumentParser.parse(args); + + if (commandLineOptional.isPresent()) { + assertThat(commandLineOptional.get(), is(commandLine)); + } else { + fail(); + } + } + + @Test + public void shouldParseTheCommandLineArgumentsAndReturnTheCommandLineObjectIfAttachIsSpecified() throws Exception { + + final String[] args = {"some", "args"}; + final CommandLine commandLine = mock(CommandLine.class); + final Options options = mock(Options.class); + + when(optionsFactory.createOptions()).thenReturn(options); + when(basicParser.parse(options, args)).thenReturn(commandLine); + when(commandLine.hasOption("command")).thenReturn(false); + when(commandLine.hasOption("attach")).thenReturn(true); when(commandLine.hasOption("list")).thenReturn(false); final Optional commandLineOptional = commandLineArgumentParser.parse(args); @@ -69,6 +92,7 @@ public void shouldParseTheCommandLineArgumentsAndReturnTheCommandLineObjectIfLis when(optionsFactory.createOptions()).thenReturn(options); when(basicParser.parse(optionsFactory.createOptions(), args)).thenReturn(commandLine); when(commandLine.hasOption("command")).thenReturn(false); + when(commandLine.hasOption("attach")).thenReturn(false); when(commandLine.hasOption("list")).thenReturn(true); final Optional commandLineOptional = commandLineArgumentParser.parse(args); @@ -90,13 +114,14 @@ public void shouldReturnEmptyIfNeitherTheCommandOptionNorTheListOptionIsSpecifie when(optionsFactory.createOptions()).thenReturn(options); when(basicParser.parse(optionsFactory.createOptions(), args)).thenReturn(commandLine); when(commandLine.hasOption("command")).thenReturn(false); + when(commandLine.hasOption("attach")).thenReturn(false); when(commandLine.hasOption("list")).thenReturn(false); final Optional commandLineOptional = commandLineArgumentParser.parse(args); assertThat(commandLineOptional.isPresent(), is(false)); - verify(toConsolePrinter).println("No system command specifed."); + verify(toConsolePrinter).println("No system command specified."); } @Test diff --git a/src/test/java/uk/gov/justice/framework/command/client/MainApplicationTest.java b/src/test/java/uk/gov/justice/framework/command/client/MainApplicationTest.java index f4d4787..fda4afd 100644 --- a/src/test/java/uk/gov/justice/framework/command/client/MainApplicationTest.java +++ b/src/test/java/uk/gov/justice/framework/command/client/MainApplicationTest.java @@ -15,7 +15,6 @@ import uk.gov.justice.framework.command.client.cdi.producers.OptionsFactory; import uk.gov.justice.framework.command.client.jmx.ListCommandsInvoker; import uk.gov.justice.framework.command.client.startup.CommandLineArgumentParser; -import uk.gov.justice.services.jmx.api.command.SystemCommand; import uk.gov.justice.services.jmx.api.command.SystemCommandDetails; import uk.gov.justice.services.jmx.system.command.client.connection.JmxParameters; diff --git a/src/test/java/uk/gov/justice/framework/command/client/io/CommandPrinterTest.java b/src/test/java/uk/gov/justice/framework/command/client/io/CommandPrinterTest.java index 9eed76f..c36c08c 100644 --- a/src/test/java/uk/gov/justice/framework/command/client/io/CommandPrinterTest.java +++ b/src/test/java/uk/gov/justice/framework/command/client/io/CommandPrinterTest.java @@ -8,7 +8,6 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import uk.gov.justice.services.jmx.api.command.SystemCommand; import uk.gov.justice.services.jmx.api.command.SystemCommandDetails; import java.util.List; diff --git a/src/test/java/uk/gov/justice/framework/command/client/jmx/CommandPollerTest.java b/src/test/java/uk/gov/justice/framework/command/client/jmx/CommandPollerTest.java index 6f8828e..a77164a 100644 --- a/src/test/java/uk/gov/justice/framework/command/client/jmx/CommandPollerTest.java +++ b/src/test/java/uk/gov/justice/framework/command/client/jmx/CommandPollerTest.java @@ -43,15 +43,20 @@ public void shouldCheckCommandUntilComplete() throws Exception { final UUID commandId = UUID.randomUUID(); final String commandName = "CATCHUP"; - final ZonedDateTime startTime = new UtcClock().now(); + final RunContext runContext = new RunContext( + commandId, + commandName, + startTime + ); + final SystemCommanderMBean systemCommanderMBean = mock(SystemCommanderMBean.class); when(clock.now()).thenReturn(startTime); when(commandChecker.commandComplete(systemCommanderMBean, commandId, startTime)).thenReturn(false, false, true); - commandPoller.runUntilComplete(systemCommanderMBean, commandId, commandName); + commandPoller.runUntilComplete(systemCommanderMBean, runContext); verify(sleeper, times(2)).sleepFor(1_000); verifyZeroInteractions(toConsolePrinter); @@ -62,18 +67,24 @@ public void shouldLogProgressEveryTenthCall() throws Exception { final UUID commandId = UUID.randomUUID(); final String commandName = "CATCHUP"; - final ZonedDateTime startTime = new UtcClock().now(); + + final RunContext runContext = new RunContext( + commandId, + commandName, + startTime + ); + final ZonedDateTime now = startTime.plusSeconds(10); final SystemCommanderMBean systemCommanderMBean = mock(SystemCommanderMBean.class); - when(clock.now()).thenReturn(startTime, now); + when(clock.now()).thenReturn(now); when(commandChecker.commandComplete(systemCommanderMBean, commandId, startTime)).thenReturn(false, false, false, false, false, false, false, false, false, false, true); - commandPoller.runUntilComplete(systemCommanderMBean, commandId, commandName); + commandPoller.runUntilComplete(systemCommanderMBean, runContext); verify(sleeper, times(10)).sleepFor(1_000); - verify(toConsolePrinter).println("CATCHUP running for 10 seconds"); + verify(toConsolePrinter).println("CATCHUP running for 00:00:10 (hh:mm:ss)"); } } diff --git a/src/test/java/uk/gov/justice/framework/command/client/jmx/SystemCommandAttachRunnerTest.java b/src/test/java/uk/gov/justice/framework/command/client/jmx/SystemCommandAttachRunnerTest.java new file mode 100644 index 0000000..c52afdd --- /dev/null +++ b/src/test/java/uk/gov/justice/framework/command/client/jmx/SystemCommandAttachRunnerTest.java @@ -0,0 +1,157 @@ +package uk.gov.justice.framework.command.client.jmx; + +import static java.util.UUID.randomUUID; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; +import static uk.gov.justice.services.jmx.api.domain.CommandState.COMMAND_COMPLETE; +import static uk.gov.justice.services.jmx.api.domain.CommandState.COMMAND_FAILED; +import static uk.gov.justice.services.jmx.api.domain.CommandState.COMMAND_IN_PROGRESS; +import static uk.gov.justice.services.jmx.api.domain.CommandState.COMMAND_RECEIVED; + +import uk.gov.justice.framework.command.client.io.ToConsolePrinter; +import uk.gov.justice.framework.command.client.util.UtcClock; +import uk.gov.justice.services.jmx.api.CommandNotFoundException; +import uk.gov.justice.services.jmx.api.domain.SystemCommandStatus; +import uk.gov.justice.services.jmx.api.mbean.SystemCommanderMBean; + +import java.time.ZonedDateTime; +import java.util.UUID; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class SystemCommandAttachRunnerTest { + + @Mock + private CommandPoller commandPoller; + + @Mock + private ToConsolePrinter toConsolePrinter; + + @InjectMocks + private SystemCommandAttachRunner systemCommandAttachRunner; + + @Test + public void shouldAttachToRunningCommandAndPollIfCommandStateIsInProgress() throws Exception { + + final UUID commandId = randomUUID(); + final String commandName = "SOME_COMMAND"; + final ZonedDateTime startTime = new UtcClock().now(); + final RunContext runContext = new RunContext(commandId, commandName, startTime); + + final SystemCommanderMBean systemCommanderMBean = mock(SystemCommanderMBean.class); + final SystemCommandStatus commandStatus = mock(SystemCommandStatus.class); + + when(systemCommanderMBean.getCommandStatus(commandId)).thenReturn(commandStatus); + when(commandStatus.getSystemCommandName()).thenReturn(commandName); + when(commandStatus.getCommandState()).thenReturn(COMMAND_IN_PROGRESS); + when(commandStatus.getStatusChangedAt()).thenReturn(startTime); + + systemCommandAttachRunner.attach(commandId, systemCommanderMBean); + + verify(commandPoller).runUntilComplete(systemCommanderMBean, runContext); + verifyZeroInteractions(toConsolePrinter); + } + + @Test + public void shouldAttachToRunningCommandAndPollIfCommandStateIsReceived() throws Exception { + + final UUID commandId = randomUUID(); + final String commandName = "SOME_COMMAND"; + final ZonedDateTime startTime = new UtcClock().now(); + final RunContext runContext = new RunContext(commandId, commandName, startTime); + + final SystemCommanderMBean systemCommanderMBean = mock(SystemCommanderMBean.class); + final SystemCommandStatus commandStatus = mock(SystemCommandStatus.class); + + when(systemCommanderMBean.getCommandStatus(commandId)).thenReturn(commandStatus); + when(commandStatus.getSystemCommandName()).thenReturn(commandName); + when(commandStatus.getCommandState()).thenReturn(COMMAND_RECEIVED); + when(commandStatus.getStatusChangedAt()).thenReturn(startTime); + + systemCommandAttachRunner.attach(commandId, systemCommanderMBean); + + verify(commandPoller).runUntilComplete(systemCommanderMBean, runContext); + verifyZeroInteractions(toConsolePrinter); + } + + @Test + public void shouldNotAttachToCommandIfCommandStateIsComplete() throws Exception { + + final UUID commandId = randomUUID(); + final String commandName = "SOME_COMMAND"; + + final SystemCommanderMBean systemCommanderMBean = mock(SystemCommanderMBean.class); + final SystemCommandStatus commandStatus = mock(SystemCommandStatus.class); + + when(systemCommanderMBean.getCommandStatus(commandId)).thenReturn(commandStatus); + when(commandStatus.getSystemCommandName()).thenReturn(commandName); + when(commandStatus.getCommandState()).thenReturn(COMMAND_COMPLETE); + + systemCommandAttachRunner.attach(commandId, systemCommanderMBean); + + verify(toConsolePrinter).printf("Cannot attach to Command with id '%s'. Command is not running. Current command status: %s", commandId, COMMAND_COMPLETE); + verifyZeroInteractions(commandPoller); + } + + @Test + public void shouldNotAttachToCommandIfCommandStateIsFailed() throws Exception { + + final UUID commandId = randomUUID(); + final String commandName = "SOME_COMMAND"; + + final SystemCommanderMBean systemCommanderMBean = mock(SystemCommanderMBean.class); + final SystemCommandStatus commandStatus = mock(SystemCommandStatus.class); + + when(systemCommanderMBean.getCommandStatus(commandId)).thenReturn(commandStatus); + when(commandStatus.getSystemCommandName()).thenReturn(commandName); + when(commandStatus.getCommandState()).thenReturn(COMMAND_FAILED); + + systemCommandAttachRunner.attach(commandId, systemCommanderMBean); + + verify(toConsolePrinter).printf("Cannot attach to Command with id '%s'. Command is not running. Current command status: %s", commandId, COMMAND_FAILED); + verifyZeroInteractions(commandPoller); + } + + @Test + public void shouldNotAttachIfNoCommandFound() throws Exception { + + final UUID commandId = randomUUID(); + final String commandName = "SOME_COMMAND"; + final ZonedDateTime startTime = new UtcClock().now(); + final RunContext runContext = new RunContext(commandId, commandName, startTime); + + final SystemCommanderMBean systemCommanderMBean = mock(SystemCommanderMBean.class); + final SystemCommandStatus commandStatus = mock(SystemCommandStatus.class); + + when(systemCommanderMBean.getCommandStatus(commandId)).thenReturn(commandStatus); + when(commandStatus.getSystemCommandName()).thenReturn(commandName); + when(commandStatus.getCommandState()).thenReturn(COMMAND_IN_PROGRESS); + when(commandStatus.getStatusChangedAt()).thenReturn(startTime); + + systemCommandAttachRunner.attach(commandId, systemCommanderMBean); + + verify(commandPoller).runUntilComplete(systemCommanderMBean, runContext); + verifyZeroInteractions(toConsolePrinter); + } + + @Test + public void shouldPrintAndDoNothingIfCommandNotFound() throws Exception { + + final UUID commandId = randomUUID(); + final SystemCommanderMBean systemCommanderMBean = mock(SystemCommanderMBean.class); + + when(systemCommanderMBean.getCommandStatus(commandId)).thenThrow(new CommandNotFoundException("Oops")); + + systemCommandAttachRunner.attach(commandId, systemCommanderMBean); + + verify(toConsolePrinter).printf("No system command exists with id '%s", commandId); + verifyZeroInteractions(commandPoller); + } +} diff --git a/src/test/java/uk/gov/justice/framework/command/client/jmx/SystemCommandAttacherTest.java b/src/test/java/uk/gov/justice/framework/command/client/jmx/SystemCommandAttacherTest.java new file mode 100644 index 0000000..1acc67f --- /dev/null +++ b/src/test/java/uk/gov/justice/framework/command/client/jmx/SystemCommandAttacherTest.java @@ -0,0 +1,105 @@ +package uk.gov.justice.framework.command.client.jmx; + +import static java.util.Optional.empty; +import static java.util.UUID.randomUUID; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.Matchers.sameInstance; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import uk.gov.justice.framework.command.client.io.ToConsolePrinter; +import uk.gov.justice.services.jmx.api.SystemCommandInvocationFailedException; +import uk.gov.justice.services.jmx.api.mbean.SystemCommanderMBean; +import uk.gov.justice.services.jmx.system.command.client.SystemCommanderClient; +import uk.gov.justice.services.jmx.system.command.client.SystemCommanderClientFactory; +import uk.gov.justice.services.jmx.system.command.client.connection.JmxParameters; + +import java.util.UUID; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class SystemCommandAttacherTest { + + @Mock + private ToConsolePrinter toConsolePrinter; + + @Mock + private SystemCommanderClientFactory systemCommanderClientFactory; + + @Mock + private SystemCommandAttachRunner systemCommandAttachRunner; + + @InjectMocks + private SystemCommandAttacher systemCommandAttacher; + + @Test + public void shouldAttachToRunningSystemCommand() throws Exception { + + final String contextName = "my-context"; + final String host = "localhost"; + final int port = 92834; + final UUID commandId = randomUUID(); + + final JmxParameters jmxParameters = mock(JmxParameters.class); + final SystemCommanderClient systemCommanderClient = mock(SystemCommanderClient.class); + final SystemCommanderMBean systemCommanderMBean = mock(SystemCommanderMBean.class); + + when(jmxParameters.getContextName()).thenReturn(contextName); + when(jmxParameters.getHost()).thenReturn(host); + when(jmxParameters.getPort()).thenReturn(port); + when(jmxParameters.getCredentials()).thenReturn(empty()); + when(systemCommanderClientFactory.create(jmxParameters)).thenReturn(systemCommanderClient); + when(systemCommanderClient.getRemote(contextName)).thenReturn(systemCommanderMBean); + + systemCommandAttacher.attachToRunningCommand(commandId, jmxParameters); + + verify(toConsolePrinter).printf("Attaching to system command with id'%s'", commandId); + verify(toConsolePrinter).printf("Connecting to %s context at '%s' on port %d", contextName, host, port); + verify(systemCommandAttachRunner).attach(commandId, systemCommanderMBean); + } + + @Test + public void shouldThrowExceptionIfAttachingToSystemCommandFails() throws Exception { + + final String stackTrace = "stack trace"; + final String errorMessage = "Ooops"; + + final SystemCommandInvocationFailedException systemCommandInvocationFailedException = new SystemCommandInvocationFailedException(errorMessage, stackTrace); + + final String contextName = "my-context"; + final String host = "localhost"; + final int port = 7239; + final UUID commandId = randomUUID(); + + final JmxParameters jmxParameters = mock(JmxParameters.class); + final SystemCommanderClient systemCommanderClient = mock(SystemCommanderClient.class); + final SystemCommanderMBean systemCommanderMBean = mock(SystemCommanderMBean.class); + + when(jmxParameters.getContextName()).thenReturn(contextName); + when(jmxParameters.getHost()).thenReturn(host); + when(jmxParameters.getPort()).thenReturn(port); + when(jmxParameters.getCredentials()).thenReturn(empty()); + when(systemCommanderClientFactory.create(jmxParameters)).thenReturn(systemCommanderClient); + when(systemCommanderClient.getRemote(contextName)).thenReturn(systemCommanderMBean); + doThrow(systemCommandInvocationFailedException).when(systemCommandAttachRunner).attach(commandId, systemCommanderMBean); + + try { + systemCommandAttacher.attachToRunningCommand(commandId, jmxParameters); + } catch (final SystemCommandInvocationFailedException expected) { + assertThat(expected, is(sameInstance(systemCommandInvocationFailedException))); + } + + verify(toConsolePrinter).printf("Attaching to system command with id'%s'", commandId); + verify(toConsolePrinter).printf("Connecting to %s context at '%s' on port %d", contextName, host, port); + verify(toConsolePrinter).printf("The command with id '%s' failed: %s", commandId, errorMessage); + verify(toConsolePrinter).println(stackTrace); + } +} diff --git a/src/test/java/uk/gov/justice/framework/command/client/jmx/SystemCommandInvokerTest.java b/src/test/java/uk/gov/justice/framework/command/client/jmx/SystemCommandInvokerTest.java index c017d74..f3ef6cb 100644 --- a/src/test/java/uk/gov/justice/framework/command/client/jmx/SystemCommandInvokerTest.java +++ b/src/test/java/uk/gov/justice/framework/command/client/jmx/SystemCommandInvokerTest.java @@ -9,6 +9,7 @@ import static org.mockito.Mockito.when; import uk.gov.justice.framework.command.client.io.ToConsolePrinter; +import uk.gov.justice.framework.command.client.util.UtcClock; import uk.gov.justice.services.jmx.api.SystemCommandInvocationFailedException; import uk.gov.justice.services.jmx.api.UnrunnableSystemCommandException; import uk.gov.justice.services.jmx.api.mbean.SystemCommanderMBean; @@ -17,6 +18,7 @@ import uk.gov.justice.services.jmx.system.command.client.connection.Credentials; import uk.gov.justice.services.jmx.system.command.client.connection.JmxParameters; +import java.time.ZonedDateTime; import java.util.UUID; import org.junit.Test; @@ -38,17 +40,27 @@ public class SystemCommandInvokerTest { @Mock private ToConsolePrinter toConsolePrinter; + @Mock + private UtcClock clock; + @InjectMocks private SystemCommandInvoker systemCommandInvoker; @Test - public void shouldMakeJmxCallToRetrieveTheListOfCommands() throws Exception { + public void shouldInvokeSystemCommand() throws Exception { final String contextName = "my-context"; final String host = "localhost"; final int port = 92834; final String commandName = "SOME_COMMAND"; final UUID commandId = randomUUID(); + final ZonedDateTime startTime = new UtcClock().now(); + + final RunContext runContext = new RunContext( + commandId, + commandName, + startTime + ); final JmxParameters jmxParameters = mock(JmxParameters.class); final SystemCommanderClient systemCommanderClient = mock(SystemCommanderClient.class); @@ -61,6 +73,7 @@ public void shouldMakeJmxCallToRetrieveTheListOfCommands() throws Exception { when(systemCommanderClientFactory.create(jmxParameters)).thenReturn(systemCommanderClient); when(systemCommanderClient.getRemote(contextName)).thenReturn(systemCommanderMBean); when(systemCommanderMBean.call(commandName)).thenReturn(commandId); + when(clock.now()).thenReturn(startTime); systemCommandInvoker.runSystemCommand(commandName, jmxParameters); @@ -78,7 +91,7 @@ public void shouldMakeJmxCallToRetrieveTheListOfCommands() throws Exception { inOrder.verify(systemCommanderClient).getRemote(contextName); inOrder.verify(systemCommanderMBean).call(commandName); inOrder.verify(toConsolePrinter).printf("System command '%s' with id '%s' successfully sent to %s", commandName, commandId, contextName); - inOrder.verify(commandPoller).runUntilComplete(systemCommanderMBean, commandId, commandName); + inOrder.verify(commandPoller).runUntilComplete(systemCommanderMBean, runContext); } @Test @@ -90,6 +103,13 @@ public void shouldLogIfUsingCredentials() throws Exception { final int port = 92834; final String commandName = "SOME_COMMAND"; final UUID commandId = randomUUID(); + final ZonedDateTime startTime = new UtcClock().now(); + + final RunContext runContext = new RunContext( + commandId, + commandName, + startTime + ); final Credentials credentials = mock(Credentials.class); final JmxParameters jmxParameters = mock(JmxParameters.class); @@ -104,6 +124,7 @@ public void shouldLogIfUsingCredentials() throws Exception { when(systemCommanderClientFactory.create(jmxParameters)).thenReturn(systemCommanderClient); when(systemCommanderClient.getRemote(contextName)).thenReturn(systemCommanderMBean); when(systemCommanderMBean.call(commandName)).thenReturn(commandId); + when(clock.now()).thenReturn(startTime); systemCommandInvoker.runSystemCommand(commandName, jmxParameters); @@ -122,7 +143,7 @@ public void shouldLogIfUsingCredentials() throws Exception { inOrder.verify(systemCommanderClient).getRemote(contextName); inOrder.verify(systemCommanderMBean).call(commandName); inOrder.verify(toConsolePrinter).printf("System command '%s' with id '%s' successfully sent to %s", commandName, commandId, contextName); - inOrder.verify(commandPoller).runUntilComplete(systemCommanderMBean, commandId, commandName); + inOrder.verify(commandPoller).runUntilComplete(systemCommanderMBean, runContext); } @Test(expected = UnrunnableSystemCommandException.class)