From e15fd8e4bb8d0e67080b76addd2b86c211e5929f Mon Sep 17 00:00:00 2001 From: Francois Onimus Date: Wed, 3 Apr 2019 20:36:51 +0200 Subject: [PATCH] Improve interactive processes, add test coverage --- README.md | 29 +- .../ssh/shell/complete/DemoCommand.java | 20 +- .../ssh/shell/SshShellCommandFactory.java | 99 +- .../fonimus/ssh/shell/SshShellHelper.java | 83 +- .../fonimus/ssh/shell/SshShellUtils.java | 118 +++ ...shShellSecurityAuthenticationProvider.java | 3 +- .../ssh/shell/commands/ThreadCommand.java | 7 +- .../commands/actuator/ActuatorCommand.java | 842 +++++++++--------- .../ssh/shell/interactive/Interactive.java | 4 +- .../ssh/shell/interactive/KeyBinding.java | 6 +- .../fonimus/ssh/shell/SshShellHelperTest.java | 38 +- .../fonimus/ssh/shell/SshShellUtilsTest.java | 24 + 12 files changed, 696 insertions(+), 577 deletions(-) create mode 100644 starter/src/main/java/com/github/fonimus/ssh/shell/SshShellUtils.java create mode 100644 starter/src/test/java/com/github/fonimus/ssh/shell/SshShellUtilsTest.java diff --git a/README.md b/README.md index 7ebb7bbb..091fd8c9 100755 --- a/README.md +++ b/README.md @@ -370,6 +370,12 @@ Every **refresh delay** (here 2 seconds), `com.github.fonimus.ssh.shell.interact This can be used to display progress, monitoring, etc. +The interactive builder, [Interactive.java](./starter/src/main/java/com/github/fonimus/ssh/shell/interactive/Interactive.java) +allows you to build your interactive command. + +This builder can also take key bindings to make specific actions, whose can be made by the following builder: +[KeyBinding.java](./starter/src/main/java/com/github/fonimus/ssh/shell/interactive/KeyBinding.java). + ```java @SshShellComponent public class DemoCommand { @@ -379,31 +385,40 @@ public class DemoCommand { @ShellMethod("Interactive command") public void interactive() { + + KeyBinding binding = KeyBinding.builder() + .description("K binding example") + .key("k").input(() -> LOGGER.info("In specific action triggered by key 'k' !")).build(); + Interactive interactive = Interactive.builder().input((size, currentDelay) -> { LOGGER.info("In interactive command for input..."); List lines = new ArrayList<>(); AttributedStringBuilder sb = new AttributedStringBuilder(size.getColumns()); - sb.style(sb.style().bold()); - sb.append("Current time"); - sb.style(sb.style().boldOff()); - sb.append(" : "); + sb.append("\nCurrent time", AttributedStyle.BOLD).append(" : "); sb.append(String.format("%8tT", new Date())); + lines.add(sb.toAttributedString()); SecureRandom sr = new SecureRandom(); lines.add(new AttributedStringBuilder().append(helper.progress(sr.nextInt(100)), AttributedStyle.DEFAULT.foreground(sr.nextInt(6) + 1)).toAttributedString()); - lines.add(AttributedString.fromAnsi("Please press key 'q' to quit.")); + lines.add(AttributedString.fromAnsi(SshShellHelper.INTERACTIVE_LONG_MESSAGE + "\n")); return lines; - }) - .fullScreen(fullscreen).refreshDelay(delay).build(); + }).binding(binding).fullScreen(true|false).refreshDelay(5000).build(); + helper.interactive(interactive); } } ``` +Note: existing key bindings are: + +* `q`: to quit interactive command and go back to shell +* `+`: to increase refresh delay by 1000 milliseconds +* `-`: to decrease refresh delay by 1000 milliseconds + ### Role check If you are using *AuthenticationProvider* thanks to property `ssh.shell.authentication=security`, you can check that connected user has right authorities for command. diff --git a/samples/complete/src/main/java/com/github/fonimus/ssh/shell/complete/DemoCommand.java b/samples/complete/src/main/java/com/github/fonimus/ssh/shell/complete/DemoCommand.java index 6f615e5b..fdd99b3d 100755 --- a/samples/complete/src/main/java/com/github/fonimus/ssh/shell/complete/DemoCommand.java +++ b/samples/complete/src/main/java/com/github/fonimus/ssh/shell/complete/DemoCommand.java @@ -4,6 +4,7 @@ import com.github.fonimus.ssh.shell.auth.SshAuthentication; import com.github.fonimus.ssh.shell.commands.SshShellComponent; import com.github.fonimus.ssh.shell.interactive.Interactive; +import com.github.fonimus.ssh.shell.interactive.KeyBinding; import com.github.fonimus.ssh.shell.providers.AnyOsFileValueProvider; import org.jline.terminal.Size; import org.jline.utils.AttributedString; @@ -105,27 +106,30 @@ private void info(File file) { * @param delay delay in ms */ @ShellMethod("Interactive command") - public void interactive(boolean fullscreen, @ShellOption(defaultValue = "2000") long delay) { + public void interactive(boolean fullscreen, @ShellOption(defaultValue = "3000") long delay) { + + KeyBinding binding = KeyBinding.builder() + .description("K binding example") + .key("k").input(() -> LOGGER.info("In specific action triggered by key 'k' !")).build(); + Interactive interactive = Interactive.builder().input((size, currentDelay) -> { LOGGER.info("In interactive command for input..."); List lines = new ArrayList<>(); AttributedStringBuilder sb = new AttributedStringBuilder(size.getColumns()); - sb.style(sb.style().bold()); - sb.append("Current time"); - sb.style(sb.style().boldOff()); - sb.append(" : "); + sb.append("\nCurrent time", AttributedStyle.BOLD).append(" : "); sb.append(String.format("%8tT", new Date())); + lines.add(sb.toAttributedString()); SecureRandom sr = new SecureRandom(); lines.add(new AttributedStringBuilder().append(helper.progress(sr.nextInt(100)), AttributedStyle.DEFAULT.foreground(sr.nextInt(6) + 1)).toAttributedString()); - lines.add(AttributedString.fromAnsi("Please press key 'q' to quit.")); + lines.add(AttributedString.fromAnsi(SshShellHelper.INTERACTIVE_LONG_MESSAGE + "\n")); return lines; - }) - .fullScreen(fullscreen).refreshDelay(delay).build(); + }).binding(binding).fullScreen(fullscreen).refreshDelay(delay).build(); + helper.interactive(interactive); } diff --git a/starter/src/main/java/com/github/fonimus/ssh/shell/SshShellCommandFactory.java b/starter/src/main/java/com/github/fonimus/ssh/shell/SshShellCommandFactory.java index 6f5223aa..44cdb5e0 100644 --- a/starter/src/main/java/com/github/fonimus/ssh/shell/SshShellCommandFactory.java +++ b/starter/src/main/java/com/github/fonimus/ssh/shell/SshShellCommandFactory.java @@ -4,7 +4,6 @@ import com.github.fonimus.ssh.shell.auth.SshShellSecurityAuthenticationProvider; import lombok.extern.slf4j.Slf4j; import org.apache.sshd.common.Factory; -import org.apache.sshd.common.channel.PtyMode; import org.apache.sshd.server.ChannelSessionAware; import org.apache.sshd.server.ExitCallback; import org.apache.sshd.server.Signal; @@ -37,7 +36,6 @@ import java.io.*; import java.nio.charset.StandardCharsets; -import java.util.Map; import static com.github.fonimus.ssh.shell.SshShellHistoryAutoConfiguration.HISTORY_FILE; @@ -131,7 +129,7 @@ public void run() { resultHandler.setTerminal(terminal); Attributes attr = terminal.getAttributes(); - fill(attr, sshEnv.getPtyModes()); + SshShellUtils.fill(attr, sshEnv.getPtyModes()); terminal.setAttributes(attr); sshEnv.addSignalListener(signal -> { @@ -190,101 +188,6 @@ public void run() { } } - private void fill(Attributes attr, Map ptyModes) { - for (Map.Entry e : ptyModes.entrySet()) { - switch (e.getKey()) { - case VINTR: - attr.setControlChar(Attributes.ControlChar.VINTR, e.getValue()); - break; - case VQUIT: - attr.setControlChar(Attributes.ControlChar.VQUIT, e.getValue()); - break; - case VERASE: - attr.setControlChar(Attributes.ControlChar.VERASE, e.getValue()); - break; - case VKILL: - attr.setControlChar(Attributes.ControlChar.VKILL, e.getValue()); - break; - case VEOF: - attr.setControlChar(Attributes.ControlChar.VEOF, e.getValue()); - break; - case VEOL: - attr.setControlChar(Attributes.ControlChar.VEOL, e.getValue()); - break; - case VEOL2: - attr.setControlChar(Attributes.ControlChar.VEOL2, e.getValue()); - break; - case VSTART: - attr.setControlChar(Attributes.ControlChar.VSTART, e.getValue()); - break; - case VSTOP: - attr.setControlChar(Attributes.ControlChar.VSTOP, e.getValue()); - break; - case VSUSP: - attr.setControlChar(Attributes.ControlChar.VSUSP, e.getValue()); - break; - case VDSUSP: - attr.setControlChar(Attributes.ControlChar.VDSUSP, e.getValue()); - break; - case VREPRINT: - attr.setControlChar(Attributes.ControlChar.VREPRINT, e.getValue()); - break; - case VWERASE: - attr.setControlChar(Attributes.ControlChar.VWERASE, e.getValue()); - break; - case VLNEXT: - attr.setControlChar(Attributes.ControlChar.VLNEXT, e.getValue()); - break; - /* - case VFLUSH: - attr.setControlChar(Attributes.ControlChar.VMIN, e.getValue()); - break; - case VSWTCH: - attr.setControlChar(Attributes.ControlChar.VTIME, e.getValue()); - break; - */ - case VSTATUS: - attr.setControlChar(Attributes.ControlChar.VSTATUS, e.getValue()); - break; - case VDISCARD: - attr.setControlChar(Attributes.ControlChar.VDISCARD, e.getValue()); - break; - case ECHO: - attr.setLocalFlag(Attributes.LocalFlag.ECHO, e.getValue() != 0); - break; - case ICANON: - attr.setLocalFlag(Attributes.LocalFlag.ICANON, e.getValue() != 0); - break; - case ISIG: - attr.setLocalFlag(Attributes.LocalFlag.ISIG, e.getValue() != 0); - break; - case ICRNL: - attr.setInputFlag(Attributes.InputFlag.ICRNL, e.getValue() != 0); - break; - case INLCR: - attr.setInputFlag(Attributes.InputFlag.INLCR, e.getValue() != 0); - break; - case IGNCR: - attr.setInputFlag(Attributes.InputFlag.IGNCR, e.getValue() != 0); - break; - case OCRNL: - attr.setOutputFlag(Attributes.OutputFlag.OCRNL, e.getValue() != 0); - break; - case ONLCR: - attr.setOutputFlag(Attributes.OutputFlag.ONLCR, e.getValue() != 0); - break; - case ONLRET: - attr.setOutputFlag(Attributes.OutputFlag.ONLRET, e.getValue() != 0); - break; - case OPOST: - attr.setOutputFlag(Attributes.OutputFlag.OPOST, e.getValue() != 0); - break; - default: - // nothing - } - } - } - private void quit(int exitCode) { ec.onExit(exitCode); } diff --git a/starter/src/main/java/com/github/fonimus/ssh/shell/SshShellHelper.java b/starter/src/main/java/com/github/fonimus/ssh/shell/SshShellHelper.java index 9e6106f2..1fb0fe05 100644 --- a/starter/src/main/java/com/github/fonimus/ssh/shell/SshShellHelper.java +++ b/starter/src/main/java/com/github/fonimus/ssh/shell/SshShellHelper.java @@ -30,8 +30,6 @@ public class SshShellHelper { public static final String INTERACTIVE_SHORT_MESSAGE = "'q': quit, '+'|'-': increase|decrease refresh"; public static final String EXIT = "_EXIT"; - public static final String INCREASE_DELAY = "_INCREASE_DELAY"; - public static final String DECREASE_DELAY = "_DECREASE_DELAY"; public static final List DEFAULT_CONFIRM_WORDS = Arrays.asList("y", "yes"); @@ -336,6 +334,15 @@ public String progress(int current, int total) { // Interactive command which refreshes automatically + private static String generateId() { + return UUID.randomUUID().toString(); + } + + /** + * Interactive + * + * @param interactive interactive built command + */ public void interactive(Interactive interactive) { final long[] refreshDelay = {interactive.getRefreshDelay()}; int rows = 0; @@ -378,16 +385,18 @@ public void interactive(Interactive interactive) { usedKeys.add("q"); } if (interactive.isIncrease()) { - keys.bind(INCREASE_DELAY, "+"); - inputs.put(INCREASE_DELAY, () -> { + String id = generateId(); + keys.bind(id, "+"); + inputs.put(id, () -> { refreshDelay[0] = refreshDelay[0] + 1000; LOGGER.debug("New refresh delay is now: " + refreshDelay[0]); }); usedKeys.add("+"); } if (interactive.isDecrease()) { - keys.bind(DECREASE_DELAY, "-"); - inputs.put(DECREASE_DELAY, () -> { + String id = generateId(); + keys.bind(id, "-"); + inputs.put(id, () -> { if (refreshDelay[0] > 1000) { refreshDelay[0] = refreshDelay[0] - 1000; LOGGER.debug("New refresh delay is now: " + refreshDelay[0]); @@ -399,21 +408,24 @@ public void interactive(Interactive interactive) { } for (KeyBinding binding : interactive.getBindings()) { - if (inputs.containsKey(binding.getId())) { - LOGGER.warn("Binding now allowed: {}. Protected name.", binding.getId()); - } else { - boolean ok = true; - for (String key : binding.getKeys()) { - if (usedKeys.contains(key)) { - LOGGER.warn("Binding key now allowed: {}. Protected key.", key); - ok = false; - } - } - if (ok) { - keys.bind(binding.getId(), binding.getKeys().toArray(new String[0])); - inputs.put(binding.getId(), binding.getInput()); + List newKeys = new ArrayList<>(); + for (String key : binding.getKeys()) { + if (usedKeys.contains(key)) { + LOGGER.warn("Binding key not allowed as already used: {}.", key); + } else { + newKeys.add(key); } } + if (newKeys.isEmpty()) { + LOGGER.error("None of the keys are allowed {}, action [{}] will not be bound", + binding.getDescription(), binding.getKeys()); + } else { + String id = generateId(); + keys.bind(id, newKeys.toArray(new String[0])); + inputs.put(id, binding.getInput()); + usedKeys.addAll(newKeys); + LOGGER.debug("Binding [{}] added with keys: {}", binding.getDescription(), newKeys); + } } String op; @@ -421,13 +433,13 @@ public void interactive(Interactive interactive) { maxLines[0] = display(interactive.getInput(), display, size, refreshDelay[0]); checkInterrupted(); - op = null; - long delta = ((System.currentTimeMillis() - t0) / refreshDelay[0] + 1) * refreshDelay[0] + t0 - System.currentTimeMillis(); int ch = bindingReader.peekCharacter(delta); - if (ch == -1) { + op = null; + // 27 is escape char + if (ch == -1 || ch == 27) { op = EXIT; } else if (ch != NonBlockingReader.READ_EXPIRED) { op = bindingReader.readBinding(keys, null, false); @@ -462,6 +474,33 @@ public void interactive(Interactive interactive) { } } + // Old interactive for compatibility + + @Deprecated + public void interactive(InteractiveInput input) { + interactive(input, true); + } + + @Deprecated + public void interactive(InteractiveInput input, long delay) { + interactive(input, delay, true); + } + + @Deprecated + public void interactive(InteractiveInput input, boolean fullScreen) { + interactive(input, 1000, fullScreen); + } + + @Deprecated + public void interactive(InteractiveInput input, long delay, boolean fullScreen) { + interactive(input, delay, fullScreen, null); + } + + @Deprecated + public void interactive(InteractiveInput input, long delay, boolean fullScreen, Size size) { + interactive(Interactive.builder().input(input).refreshDelay(delay).fullScreen(fullScreen).size(size).build()); + } + private int display(InteractiveInput input, Display display, Size size, long currentDelay) { display.resize(size.getRows(), size.getColumns()); List lines = input.getLines(size, currentDelay); diff --git a/starter/src/main/java/com/github/fonimus/ssh/shell/SshShellUtils.java b/starter/src/main/java/com/github/fonimus/ssh/shell/SshShellUtils.java new file mode 100644 index 00000000..7d71da72 --- /dev/null +++ b/starter/src/main/java/com/github/fonimus/ssh/shell/SshShellUtils.java @@ -0,0 +1,118 @@ +package com.github.fonimus.ssh.shell; + +import org.apache.sshd.common.channel.PtyMode; +import org.jline.terminal.Attributes; + +import java.util.Map; + +/** + * Utility tools + */ +public final class SshShellUtils { + + private SshShellUtils() { + // private constructor + } + + /** + * Fill attributes with given modes + * + * @param attr attributes + * @param ptyModes pty modes + */ + public static void fill(Attributes attr, Map ptyModes) { + for (Map.Entry e : ptyModes.entrySet()) { + switch (e.getKey()) { + case VINTR: + attr.setControlChar(Attributes.ControlChar.VINTR, e.getValue()); + break; + case VQUIT: + attr.setControlChar(Attributes.ControlChar.VQUIT, e.getValue()); + break; + case VERASE: + attr.setControlChar(Attributes.ControlChar.VERASE, e.getValue()); + break; + case VKILL: + attr.setControlChar(Attributes.ControlChar.VKILL, e.getValue()); + break; + case VEOF: + attr.setControlChar(Attributes.ControlChar.VEOF, e.getValue()); + break; + case VEOL: + attr.setControlChar(Attributes.ControlChar.VEOL, e.getValue()); + break; + case VEOL2: + attr.setControlChar(Attributes.ControlChar.VEOL2, e.getValue()); + break; + case VSTART: + attr.setControlChar(Attributes.ControlChar.VSTART, e.getValue()); + break; + case VSTOP: + attr.setControlChar(Attributes.ControlChar.VSTOP, e.getValue()); + break; + case VSUSP: + attr.setControlChar(Attributes.ControlChar.VSUSP, e.getValue()); + break; + case VDSUSP: + attr.setControlChar(Attributes.ControlChar.VDSUSP, e.getValue()); + break; + case VREPRINT: + attr.setControlChar(Attributes.ControlChar.VREPRINT, e.getValue()); + break; + case VWERASE: + attr.setControlChar(Attributes.ControlChar.VWERASE, e.getValue()); + break; + case VLNEXT: + attr.setControlChar(Attributes.ControlChar.VLNEXT, e.getValue()); + break; + /* + case VFLUSH: + attr.setControlChar(Attributes.ControlChar.VMIN, e.getValue()); + break; + case VSWTCH: + attr.setControlChar(Attributes.ControlChar.VTIME, e.getValue()); + break; + */ + case VSTATUS: + attr.setControlChar(Attributes.ControlChar.VSTATUS, e.getValue()); + break; + case VDISCARD: + attr.setControlChar(Attributes.ControlChar.VDISCARD, e.getValue()); + break; + case ECHO: + attr.setLocalFlag(Attributes.LocalFlag.ECHO, e.getValue() != 0); + break; + case ICANON: + attr.setLocalFlag(Attributes.LocalFlag.ICANON, e.getValue() != 0); + break; + case ISIG: + attr.setLocalFlag(Attributes.LocalFlag.ISIG, e.getValue() != 0); + break; + case ICRNL: + attr.setInputFlag(Attributes.InputFlag.ICRNL, e.getValue() != 0); + break; + case INLCR: + attr.setInputFlag(Attributes.InputFlag.INLCR, e.getValue() != 0); + break; + case IGNCR: + attr.setInputFlag(Attributes.InputFlag.IGNCR, e.getValue() != 0); + break; + case OCRNL: + attr.setOutputFlag(Attributes.OutputFlag.OCRNL, e.getValue() != 0); + break; + case ONLCR: + attr.setOutputFlag(Attributes.OutputFlag.ONLCR, e.getValue() != 0); + break; + case ONLRET: + attr.setOutputFlag(Attributes.OutputFlag.ONLRET, e.getValue() != 0); + break; + case OPOST: + attr.setOutputFlag(Attributes.OutputFlag.OPOST, e.getValue() != 0); + break; + default: + // nothing to do + } + } + } + +} diff --git a/starter/src/main/java/com/github/fonimus/ssh/shell/auth/SshShellSecurityAuthenticationProvider.java b/starter/src/main/java/com/github/fonimus/ssh/shell/auth/SshShellSecurityAuthenticationProvider.java index 7595485f..e3041ba9 100644 --- a/starter/src/main/java/com/github/fonimus/ssh/shell/auth/SshShellSecurityAuthenticationProvider.java +++ b/starter/src/main/java/com/github/fonimus/ssh/shell/auth/SshShellSecurityAuthenticationProvider.java @@ -83,7 +83,8 @@ public boolean authenticate(String username, String pass, new SshAuthentication(auth.getPrincipal(), auth.getDetails(), auth.getCredentials(), authorities)); return auth.isAuthenticated(); } catch (AuthenticationException e) { - LOGGER.error("Unable to authenticate user: {}", username, e); + LOGGER.error("Unable to authenticate user [{}] : {}", username, e.getMessage()); + LOGGER.debug("Unable to authenticate user [{}]", username, e); return false; } } diff --git a/starter/src/main/java/com/github/fonimus/ssh/shell/commands/ThreadCommand.java b/starter/src/main/java/com/github/fonimus/ssh/shell/commands/ThreadCommand.java index 1089f882..f651aafb 100644 --- a/starter/src/main/java/com/github/fonimus/ssh/shell/commands/ThreadCommand.java +++ b/starter/src/main/java/com/github/fonimus/ssh/shell/commands/ThreadCommand.java @@ -147,8 +147,8 @@ public String threads(@ShellOption(defaultValue = "LIST") ThreadAction action, Interactive.InteractiveBuilder builder = Interactive.builder(); for (ThreadColumn value : ThreadColumn.values()) { String key = value == ThreadColumn.INTERRUPTED ? "t" : value.name().toLowerCase().substring(0, 1); - builder.binding(KeyBinding.builder().id("ORDER_BY_" + value.name()) - .key(key).input(() -> { + builder.binding(KeyBinding.builder().description("ORDER_" + value.name()).key(key) + .input(() -> { if (value == finalOrderBy[0]) { finalReverseOrder[0] = !finalReverseOrder[0]; } else { @@ -156,7 +156,8 @@ public String threads(@ShellOption(defaultValue = "LIST") ThreadAction action, } }).build()); } - builder.binding(KeyBinding.builder().id("REVERSE_ORDER").key("r").input(() -> finalReverseOrder[0] = !finalReverseOrder[0]).build()); + builder.binding(KeyBinding.builder().key("r").description("REVERSE") + .input(() -> finalReverseOrder[0] = !finalReverseOrder[0]).build()); helper.interactive(builder.input((size, currentDelay) -> { List lines = new ArrayList<>(size.getRows()); diff --git a/starter/src/main/java/com/github/fonimus/ssh/shell/commands/actuator/ActuatorCommand.java b/starter/src/main/java/com/github/fonimus/ssh/shell/commands/actuator/ActuatorCommand.java index f081b99a..556e8255 100644 --- a/starter/src/main/java/com/github/fonimus/ssh/shell/commands/actuator/ActuatorCommand.java +++ b/starter/src/main/java/com/github/fonimus/ssh/shell/commands/actuator/ActuatorCommand.java @@ -1,9 +1,10 @@ package com.github.fonimus.ssh.shell.commands.actuator; -import java.util.Arrays; -import java.util.List; -import java.util.Map; - +import com.github.fonimus.ssh.shell.SshShellCommandFactory; +import com.github.fonimus.ssh.shell.SshShellHelper; +import com.github.fonimus.ssh.shell.SshShellProperties; +import com.github.fonimus.ssh.shell.auth.SshAuthentication; +import com.github.fonimus.ssh.shell.commands.SshShellComponent; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.boot.actuate.audit.AuditEventsEndpoint; import org.springframework.boot.actuate.autoconfigure.condition.ConditionsReportEndpoint; @@ -34,11 +35,9 @@ import org.springframework.shell.standard.ShellMethodAvailability; import org.springframework.shell.standard.ShellOption; -import com.github.fonimus.ssh.shell.SshShellCommandFactory; -import com.github.fonimus.ssh.shell.SshShellHelper; -import com.github.fonimus.ssh.shell.SshShellProperties; -import com.github.fonimus.ssh.shell.auth.SshAuthentication; -import com.github.fonimus.ssh.shell.commands.SshShellComponent; +import java.util.Arrays; +import java.util.List; +import java.util.Map; import static com.github.fonimus.ssh.shell.SshShellProperties.SSH_SHELL_PREFIX; @@ -51,416 +50,417 @@ @ConditionalOnProperty(value = SSH_SHELL_PREFIX + ".actuator.enable", havingValue = "true", matchIfMissing = true) public class ActuatorCommand { - private ApplicationContext applicationContext; - - private Environment environment; - - private SshShellProperties properties; - - private SshShellHelper helper; - - private AuditEventsEndpoint audit; - - private BeansEndpoint beans; - - private ConditionsReportEndpoint conditions; - - private ConfigurationPropertiesReportEndpoint configprops; - - private EnvironmentEndpoint env; - - private HealthEndpoint health; - - private HttpTraceEndpoint httptrace; - - private InfoEndpoint info; - - private LoggersEndpoint loggers; - - private MetricsEndpoint metrics; - - private MappingsEndpoint mappings; - - private ScheduledTasksEndpoint scheduledtasks; - - private ShutdownEndpoint shutdown; - - private ThreadDumpEndpoint threaddump; - - public ActuatorCommand(ApplicationContext applicationContext, Environment environment, SshShellProperties properties, SshShellHelper helper, - @Lazy AuditEventsEndpoint audit, @Lazy BeansEndpoint beans, @Lazy ConditionsReportEndpoint conditions, - @Lazy ConfigurationPropertiesReportEndpoint configprops, @Lazy EnvironmentEndpoint env, @Lazy HealthEndpoint health, - @Lazy HttpTraceEndpoint httptrace, @Lazy InfoEndpoint info, @Lazy LoggersEndpoint loggers, @Lazy MetricsEndpoint metrics, - @Lazy MappingsEndpoint mappings, @Lazy ScheduledTasksEndpoint scheduledtasks, @Lazy ShutdownEndpoint shutdown, - @Lazy ThreadDumpEndpoint threaddump) { - this.applicationContext = applicationContext; - this.environment = environment; - this.properties = properties; - this.helper = helper; - this.audit = audit; - this.beans = beans; - this.conditions = conditions; - this.configprops = configprops; - this.env = env; - this.health = health; - this.httptrace = httptrace; - this.info = info; - this.loggers = loggers; - this.metrics = metrics; - this.mappings = mappings; - this.scheduledtasks = scheduledtasks; - this.shutdown = shutdown; - this.threaddump = threaddump; - } - - /** - * Audit method - * - * @param principal principal to filter with - * @param type to filter with - * @return audit - */ - @ShellMethod(key = "audit", value = "Display audit endpoint.") - @ShellMethodAvailability("auditAvailability") - public AuditEventsEndpoint.AuditEventsDescriptor audit( - @ShellOption(value = { "-p", "--principal" }, defaultValue = ShellOption.NULL, help = "Principal to filter on") String principal, - @ShellOption(value = { "-t", "--type" }, defaultValue = ShellOption.NULL, help = "Type to filter on") String type) { - return audit.events(principal, null, type); - } - - /** - * @return whether `audit` command is available - */ - public Availability auditAvailability() { - return availability("audit", AuditEventsEndpoint.class); - } - - /** - * Beans method - * - * @return beans - */ - @ShellMethod(key = "beans", value = "Display beans endpoint.") - @ShellMethodAvailability("beansAvailability") - public BeansEndpoint.ApplicationBeans beans() { - return beans.beans(); - } - - /** - * @return whether `beans` command is available - */ - public Availability beansAvailability() { - return availability("beans", BeansEndpoint.class); - } - - /** - * Conditions method - * - * @return conditions - */ - @ShellMethod(key = "conditions", value = "Display conditions endpoint.") - @ShellMethodAvailability("conditionsAvailability") - public ConditionsReportEndpoint.ApplicationConditionEvaluation conditions() { - return conditions.applicationConditionEvaluation(); - } - - /** - * @return whether `conditions` command is available - */ - public Availability conditionsAvailability() { - return availability("conditions", ConditionsReportEndpoint.class); - } - - /** - * Config props method - * - * @return configprops - */ - @ShellMethod(key = "configprops", value = "Display configprops endpoint.") - @ShellMethodAvailability("configpropsAvailability") - public ConfigurationPropertiesReportEndpoint.ApplicationConfigurationProperties configprops() { - return configprops.configurationProperties(); - } - - /** - * @return whether `configprops` command is available - */ - public Availability configpropsAvailability() { - return availability("configprops", ConfigurationPropertiesReportEndpoint.class); - } - - /** - * Environment method - * - * @param pattern pattern to filter with - * @return env - */ - @ShellMethod(key = "env", value = "Display env endpoint.") - @ShellMethodAvailability("envAvailability") - public EnvironmentEndpoint.EnvironmentDescriptor env( - @ShellOption(value = { "-p", "--pattern" }, defaultValue = ShellOption.NULL, help = "Pattern " + - "to filter on") String pattern) { - return env.environment(pattern); - } - - /** - * @return whether `env` command is available - */ - public Availability envAvailability() { - return availability("env", EnvironmentEndpoint.class); - } - - /** - * Health method - * - * @return health - */ - @ShellMethod(key = "health", value = "Display health endpoint.") - @ShellMethodAvailability("healthAvailability") - public Health health() { - return health.health(); - } - - /** - * @return whether `health` command is available - */ - public Availability healthAvailability() { - return availability("health", HealthEndpoint.class); - } - - /** - * Http traces method - * - * @return httptrace - */ - @ShellMethod(key = "httptrace", value = "Display httptrace endpoint.") - @ShellMethodAvailability("httptraceAvailability") - public HttpTraceEndpoint.HttpTraceDescriptor httptrace() { - return httptrace.traces(); - } - - /** - * @return whether `httptrace` command is available - */ - public Availability httptraceAvailability() { - return availability("httptrace", HttpTraceEndpoint.class); - } - - /** - * Info method - * - * @return info - */ - @ShellMethod(key = "info", value = "Display info endpoint.") - @ShellMethodAvailability("infoAvailability") - public Map info() { - return info.info(); - } - - /** - * @return whether `info` command is available - */ - public Availability infoAvailability() { - return availability("info", InfoEndpoint.class); - } - - /** - * Loggers method - * - * @param action action to make - * @param loggerName logger name for get or configure - * @param loggerLevel logger level for configure - * @return loggers - */ - @ShellMethod(key = "loggers", value = "Display or configure loggers.") - @ShellMethodAvailability("loggersAvailability") - public Object loggers( - @ShellOption(value = { "-a", "--action" }, help = "Action to perform", defaultValue = "list") LoggerAction action, - @ShellOption(value = { "-n", "--name" }, help = "Logger name for configuration or display", defaultValue = ShellOption.NULL) String loggerName, - @ShellOption(value = { "-l", "--level" }, help = "Logger level for configuration", defaultValue = ShellOption.NULL) LogLevel loggerLevel) { - if ((action == LoggerAction.get || action == LoggerAction.conf) && loggerName == null) { - throw new IllegalArgumentException("Logger name is mandatory for '" + action + "' action"); - } - switch (action) { - case get: - LoggersEndpoint.LoggerLevels levels = loggers.loggerLevels(loggerName); - return "Logger named [" + loggerName + "] : [configured: " + levels.getConfiguredLevel() + ", effective: " + levels.getEffectiveLevel() + "]"; - case conf: - if (loggerLevel == null) { - throw new IllegalArgumentException("Logger level is mandatory for '" + action + "' action"); - } - loggers.configureLogLevel(loggerName, loggerLevel); - return "Logger named [" + loggerName + "] now configured to level [" + loggerLevel + "]"; - default: - // list - return loggers.loggers(); - } - } - - /** - * @return whether `loggers` command is available - */ - public Availability loggersAvailability() { - return availability("loggers", LoggersEndpoint.class); - } - - /** - * Metrics method - * - * @param name metrics name to display - * @param tags tags to filter with - * @return metrics - */ - @ShellMethod(key = "metrics", value = "Display metrics endpoint.") - @ShellMethodAvailability("metricsAvailability") - public Object metrics( - @ShellOption(value = { "-n", "--name" }, help = "Metric name to get", defaultValue = ShellOption.NULL) String name, - @ShellOption(value = { "-t", "--tags" }, help = "Tags (key=value, separated by coma)", defaultValue = ShellOption.NULL) String tags - ) { - if (name != null) { - MetricsEndpoint.MetricResponse result = metrics.metric(name, tags != null ? Arrays.asList(tags.split(",") - ) : null); - if (result == null) { - String tagsStr = tags != null ? " and tags: " + tags : ""; - throw new IllegalArgumentException("No result for metrics name: " + name + tagsStr); - } - return result; - } - return metrics.listNames(); - } - - /** - * @return whether `metrics` command is available - */ - public Availability metricsAvailability() { - return availability("metrics", MetricsEndpoint.class); - } - - /** - * Mappings method - * - * @return mappings - */ - @ShellMethod(key = "mappings", value = "Display mappings endpoint.") - @ShellMethodAvailability("mappingsAvailability") - public MappingsEndpoint.ApplicationMappings mappings() { - return mappings.mappings(); - } - - /** - * @return whether `mappings` command is available - */ - public Availability mappingsAvailability() { - return availability("mappings", MappingsEndpoint.class); - } - - /** - * Sessions method - * - * @return sessions - */ - @ShellMethod(key = "sessions", value = "Display sessions endpoint.") - @ShellMethodAvailability("sessionsAvailability") - public SessionsEndpoint.SessionsReport sessions() { - return applicationContext.getBean(SessionsEndpoint.class).sessionsForUsername(null); - } - - /** - * @return whether `sessions` command is available - */ - public Availability sessionsAvailability() { - return availability("sessions", SessionsEndpoint.class); - } - - /** - * Scheduled tasks method - * - * @return scheduledtasks - */ - @ShellMethod(key = "scheduledtasks", value = "Display scheduledtasks endpoint.") - @ShellMethodAvailability("scheduledtasksAvailability") - public ScheduledTasksEndpoint.ScheduledTasksReport scheduledtasks() { - return scheduledtasks.scheduledTasks(); - } - - /** - * @return whether `scheduledtasks` command is available - */ - public Availability scheduledtasksAvailability() { - return availability("scheduledtasks", ScheduledTasksEndpoint.class); - } - - /** - * Shutdown method - * - * @return shutdown message - */ - @ShellMethod(key = "shutdown", value = "Shutdown application.") - @ShellMethodAvailability("shutdownAvailability") - public String shutdown() { - if (helper.confirm("Are you sure you want to shutdown application ? [y/N]")) { - shutdown.shutdown(); - return "Shutting down application..."; - } else { - return "Aborting shutdown"; - } - } - - /** - * @return whether `shutdown` command is available - */ - public Availability shutdownAvailability() { - return availability("shutdown", ShutdownEndpoint.class, false); - } - - /** - * Thread dump method - * - * @return threaddump - */ - @ShellMethod(key = "threaddump", value = "Display threaddump endpoint.") - @ShellMethodAvailability("threaddumpAvailability") - public ThreadDumpEndpoint.ThreadDumpDescriptor threaddump() { - return threaddump.threadDump(); - } - - /** - * @return whether `threaddump` command is available - */ - public Availability threaddumpAvailability() { - return availability("threaddump", ThreadDumpEndpoint.class); - } - - private Availability availability(String name, Class clazz) { - return availability(name, clazz, true); - } - - private Availability availability(String name, Class clazz, boolean defaultValue) { - if (!"info".equals(name)) { - SshAuthentication auth = SshShellCommandFactory.SSH_THREAD_CONTEXT.get().getAuthentication(); - List authorities = auth != null ? auth.getAuthorities() : null; - if (!helper.checkAuthorities(properties.getActuator().getAuthorizedRoles(), authorities, - properties.getAuthentication() == SshShellProperties.AuthenticationType.simple)) { - return Availability.unavailable("actuator commands are forbidden for current user"); - } - } - String property = "management.endpoint." + name + ".enabled"; - if (!environment.getProperty(property, Boolean.TYPE, defaultValue)) { - return Availability.unavailable("endpoint '" + name + "' deactivated (please check property '" + property - + "')"); - } else if (properties.getActuator().getExcludes().contains(name)) { - return Availability.unavailable("command is present in exclusion (please check property '" + - SSH_SHELL_PREFIX + ".actuator.excludes')"); - } - try { - applicationContext.getBean(clazz); - } catch (NoSuchBeanDefinitionException e) { - return Availability.unavailable(clazz.getName() + " is not in application context"); - } - return Availability.available(); - } - - public enum LoggerAction { - list, get, conf - } + private ApplicationContext applicationContext; + + private Environment environment; + + private SshShellProperties properties; + + private SshShellHelper helper; + + private AuditEventsEndpoint audit; + + private BeansEndpoint beans; + + private ConditionsReportEndpoint conditions; + + private ConfigurationPropertiesReportEndpoint configprops; + + private EnvironmentEndpoint env; + + private HealthEndpoint health; + + private HttpTraceEndpoint httptrace; + + private InfoEndpoint info; + + private LoggersEndpoint loggers; + + private MetricsEndpoint metrics; + + private MappingsEndpoint mappings; + + private ScheduledTasksEndpoint scheduledtasks; + + private ShutdownEndpoint shutdown; + + private ThreadDumpEndpoint threaddump; + + public ActuatorCommand(ApplicationContext applicationContext, Environment environment, SshShellProperties properties, SshShellHelper helper, + @Lazy AuditEventsEndpoint audit, @Lazy BeansEndpoint beans, @Lazy ConditionsReportEndpoint conditions, + @Lazy ConfigurationPropertiesReportEndpoint configprops, @Lazy EnvironmentEndpoint env, @Lazy HealthEndpoint health, + @Lazy HttpTraceEndpoint httptrace, @Lazy InfoEndpoint info, @Lazy LoggersEndpoint loggers, @Lazy MetricsEndpoint metrics, + @Lazy MappingsEndpoint mappings, @Lazy ScheduledTasksEndpoint scheduledtasks, @Lazy ShutdownEndpoint shutdown, + @Lazy ThreadDumpEndpoint threaddump) { + this.applicationContext = applicationContext; + this.environment = environment; + this.properties = properties; + this.helper = helper; + this.audit = audit; + this.beans = beans; + this.conditions = conditions; + this.configprops = configprops; + this.env = env; + this.health = health; + this.httptrace = httptrace; + this.info = info; + this.loggers = loggers; + this.metrics = metrics; + this.mappings = mappings; + this.scheduledtasks = scheduledtasks; + this.shutdown = shutdown; + this.threaddump = threaddump; + } + + /** + * Audit method + * + * @param principal principal to filter with + * @param type to filter with + * @return audit + */ + @ShellMethod(key = "audit", value = "Display audit endpoint.") + @ShellMethodAvailability("auditAvailability") + public AuditEventsEndpoint.AuditEventsDescriptor audit( + @ShellOption(value = {"-p", "--principal"}, defaultValue = ShellOption.NULL, help = "Principal to filter on") String principal, + @ShellOption(value = {"-t", "--type"}, defaultValue = ShellOption.NULL, help = "Type to filter on") String type) { + return audit.events(principal, null, type); + } + + /** + * @return whether `audit` command is available + */ + public Availability auditAvailability() { + return availability("audit", AuditEventsEndpoint.class); + } + + /** + * Beans method + * + * @return beans + */ + @ShellMethod(key = "beans", value = "Display beans endpoint.") + @ShellMethodAvailability("beansAvailability") + public BeansEndpoint.ApplicationBeans beans() { + return beans.beans(); + } + + /** + * @return whether `beans` command is available + */ + public Availability beansAvailability() { + return availability("beans", BeansEndpoint.class); + } + + /** + * Conditions method + * + * @return conditions + */ + @ShellMethod(key = "conditions", value = "Display conditions endpoint.") + @ShellMethodAvailability("conditionsAvailability") + public ConditionsReportEndpoint.ApplicationConditionEvaluation conditions() { + return conditions.applicationConditionEvaluation(); + } + + /** + * @return whether `conditions` command is available + */ + public Availability conditionsAvailability() { + return availability("conditions", ConditionsReportEndpoint.class); + } + + /** + * Config props method + * + * @return configprops + */ + @ShellMethod(key = "configprops", value = "Display configprops endpoint.") + @ShellMethodAvailability("configpropsAvailability") + public ConfigurationPropertiesReportEndpoint.ApplicationConfigurationProperties configprops() { + return configprops.configurationProperties(); + } + + /** + * @return whether `configprops` command is available + */ + public Availability configpropsAvailability() { + return availability("configprops", ConfigurationPropertiesReportEndpoint.class); + } + + /** + * Environment method + * + * @param pattern pattern to filter with + * @return env + */ + @ShellMethod(key = "env", value = "Display env endpoint.") + @ShellMethodAvailability("envAvailability") + public EnvironmentEndpoint.EnvironmentDescriptor env( + @ShellOption(value = {"-p", "--pattern"}, defaultValue = ShellOption.NULL, help = "Pattern " + + "to filter on") String pattern) { + return env.environment(pattern); + } + + /** + * @return whether `env` command is available + */ + public Availability envAvailability() { + return availability("env", EnvironmentEndpoint.class); + } + + /** + * Health method + * + * @return health + */ + @ShellMethod(key = "health", value = "Display health endpoint.") + @ShellMethodAvailability("healthAvailability") + public Health health() { + return health.health(); + } + + /** + * @return whether `health` command is available + */ + public Availability healthAvailability() { + return availability("health", HealthEndpoint.class); + } + + /** + * Http traces method + * + * @return httptrace + */ + @ShellMethod(key = "httptrace", value = "Display httptrace endpoint.") + @ShellMethodAvailability("httptraceAvailability") + public HttpTraceEndpoint.HttpTraceDescriptor httptrace() { + return httptrace.traces(); + } + + /** + * @return whether `httptrace` command is available + */ + public Availability httptraceAvailability() { + return availability("httptrace", HttpTraceEndpoint.class); + } + + /** + * Info method + * + * @return info + */ + @ShellMethod(key = "info", value = "Display info endpoint.") + @ShellMethodAvailability("infoAvailability") + public Map info() { + return info.info(); + } + + /** + * @return whether `info` command is available + */ + public Availability infoAvailability() { + return availability("info", InfoEndpoint.class); + } + + /** + * Loggers method + * + * @param action action to make + * @param loggerName logger name for get or configure + * @param loggerLevel logger level for configure + * @return loggers + */ + @ShellMethod(key = "loggers", value = "Display or configure loggers.") + @ShellMethodAvailability("loggersAvailability") + public Object loggers( + @ShellOption(value = {"-a", "--action"}, help = "Action to perform", defaultValue = "list") LoggerAction action, + @ShellOption(value = {"-n", "--name"}, help = "Logger name for configuration or display", defaultValue = ShellOption.NULL) String loggerName, + @ShellOption(value = {"-l", "--level"}, help = "Logger level for configuration", defaultValue = ShellOption.NULL) LogLevel loggerLevel) { + if ((action == LoggerAction.get || action == LoggerAction.conf) && loggerName == null) { + throw new IllegalArgumentException("Logger name is mandatory for '" + action + "' action"); + } + switch (action) { + case get: + LoggersEndpoint.LoggerLevels levels = loggers.loggerLevels(loggerName); + return "Logger named [" + loggerName + "] : [configured: " + levels.getConfiguredLevel() + ", effective: " + levels.getEffectiveLevel() + "]"; + case conf: + if (loggerLevel == null) { + throw new IllegalArgumentException("Logger level is mandatory for '" + action + "' action"); + } + loggers.configureLogLevel(loggerName, loggerLevel); + return "Logger named [" + loggerName + "] now configured to level [" + loggerLevel + "]"; + default: + // list + return loggers.loggers(); + } + } + + /** + * @return whether `loggers` command is available + */ + public Availability loggersAvailability() { + return availability("loggers", LoggersEndpoint.class); + } + + /** + * Metrics method + * + * @param name metrics name to display + * @param tags tags to filter with + * @return metrics + */ + @ShellMethod(key = "metrics", value = "Display metrics endpoint.") + @ShellMethodAvailability("metricsAvailability") + public Object metrics( + @ShellOption(value = {"-n", "--name"}, help = "Metric name to get", defaultValue = ShellOption.NULL) String name, + @ShellOption(value = {"-t", "--tags"}, help = "Tags (key=value, separated by coma)", defaultValue = ShellOption.NULL) String tags + ) { + if (name != null) { + MetricsEndpoint.MetricResponse result = metrics.metric(name, tags != null ? Arrays.asList(tags.split(",") + ) : null); + if (result == null) { + String tagsStr = tags != null ? " and tags: " + tags : ""; + throw new IllegalArgumentException("No result for metrics name: " + name + tagsStr); + } + return result; + } + return metrics.listNames(); + } + + /** + * @return whether `metrics` command is available + */ + public Availability metricsAvailability() { + return availability("metrics", MetricsEndpoint.class); + } + + /** + * Mappings method + * + * @return mappings + */ + @ShellMethod(key = "mappings", value = "Display mappings endpoint.") + @ShellMethodAvailability("mappingsAvailability") + public MappingsEndpoint.ApplicationMappings mappings() { + return mappings.mappings(); + } + + /** + * @return whether `mappings` command is available + */ + public Availability mappingsAvailability() { + return availability("mappings", MappingsEndpoint.class); + } + + /** + * Sessions method + * + * @return sessions + */ + @ShellMethod(key = "sessions", value = "Display sessions endpoint.") + @ShellMethodAvailability("sessionsAvailability") + public SessionsEndpoint.SessionsReport sessions() { + return applicationContext.getBean(SessionsEndpoint.class).sessionsForUsername(null); + } + + /** + * @return whether `sessions` command is available + */ + public Availability sessionsAvailability() { + return availability("sessions", SessionsEndpoint.class); + } + + /** + * Scheduled tasks method + * + * @return scheduledtasks + */ + @ShellMethod(key = "scheduledtasks", value = "Display scheduledtasks endpoint.") + @ShellMethodAvailability("scheduledtasksAvailability") + public ScheduledTasksEndpoint.ScheduledTasksReport scheduledtasks() { + return scheduledtasks.scheduledTasks(); + } + + /** + * @return whether `scheduledtasks` command is available + */ + public Availability scheduledtasksAvailability() { + return availability("scheduledtasks", ScheduledTasksEndpoint.class); + } + + /** + * Shutdown method + * + * @return shutdown message + */ + @ShellMethod(key = "shutdown", value = "Shutdown application.") + @ShellMethodAvailability("shutdownAvailability") + public String shutdown() { + if (helper.confirm("Are you sure you want to shutdown application ? [y/N]")) { + helper.print("Shutting down application..."); + shutdown.shutdown(); + return ""; + } else { + return "Aborting shutdown"; + } + } + + /** + * @return whether `shutdown` command is available + */ + public Availability shutdownAvailability() { + return availability("shutdown", ShutdownEndpoint.class, false); + } + + /** + * Thread dump method + * + * @return threaddump + */ + @ShellMethod(key = "threaddump", value = "Display threaddump endpoint.") + @ShellMethodAvailability("threaddumpAvailability") + public ThreadDumpEndpoint.ThreadDumpDescriptor threaddump() { + return threaddump.threadDump(); + } + + /** + * @return whether `threaddump` command is available + */ + public Availability threaddumpAvailability() { + return availability("threaddump", ThreadDumpEndpoint.class); + } + + private Availability availability(String name, Class clazz) { + return availability(name, clazz, true); + } + + private Availability availability(String name, Class clazz, boolean defaultValue) { + if (!"info".equals(name)) { + SshAuthentication auth = SshShellCommandFactory.SSH_THREAD_CONTEXT.get().getAuthentication(); + List authorities = auth != null ? auth.getAuthorities() : null; + if (!helper.checkAuthorities(properties.getActuator().getAuthorizedRoles(), authorities, + properties.getAuthentication() == SshShellProperties.AuthenticationType.simple)) { + return Availability.unavailable("actuator commands are forbidden for current user"); + } + } + String property = "management.endpoint." + name + ".enabled"; + if (!environment.getProperty(property, Boolean.TYPE, defaultValue)) { + return Availability.unavailable("endpoint '" + name + "' deactivated (please check property '" + property + + "')"); + } else if (properties.getActuator().getExcludes().contains(name)) { + return Availability.unavailable("command is present in exclusion (please check property '" + + SSH_SHELL_PREFIX + ".actuator.excludes')"); + } + try { + applicationContext.getBean(clazz); + } catch (NoSuchBeanDefinitionException e) { + return Availability.unavailable(clazz.getName() + " is not in application context"); + } + return Availability.available(); + } + + public enum LoggerAction { + list, get, conf + } } diff --git a/starter/src/main/java/com/github/fonimus/ssh/shell/interactive/Interactive.java b/starter/src/main/java/com/github/fonimus/ssh/shell/interactive/Interactive.java index f0507b2f..915d2302 100644 --- a/starter/src/main/java/com/github/fonimus/ssh/shell/interactive/Interactive.java +++ b/starter/src/main/java/com/github/fonimus/ssh/shell/interactive/Interactive.java @@ -1,7 +1,7 @@ package com.github.fonimus.ssh.shell.interactive; import lombok.Builder; -import lombok.Data; +import lombok.Getter; import lombok.NonNull; import lombok.Singular; import org.jline.terminal.Size; @@ -11,8 +11,8 @@ /** * Interactive bean */ -@Data @Builder +@Getter public class Interactive { @NonNull diff --git a/starter/src/main/java/com/github/fonimus/ssh/shell/interactive/KeyBinding.java b/starter/src/main/java/com/github/fonimus/ssh/shell/interactive/KeyBinding.java index 5a5d24e2..97abed4e 100644 --- a/starter/src/main/java/com/github/fonimus/ssh/shell/interactive/KeyBinding.java +++ b/starter/src/main/java/com/github/fonimus/ssh/shell/interactive/KeyBinding.java @@ -1,7 +1,7 @@ package com.github.fonimus.ssh.shell.interactive; import lombok.Builder; -import lombok.Data; +import lombok.Getter; import lombok.NonNull; import lombok.Singular; @@ -10,12 +10,12 @@ /** * Key binding bean */ -@Data @Builder +@Getter public class KeyBinding { @NonNull - private String id; + private String description; @NonNull private KeyBindingInput input; diff --git a/starter/src/test/java/com/github/fonimus/ssh/shell/SshShellHelperTest.java b/starter/src/test/java/com/github/fonimus/ssh/shell/SshShellHelperTest.java index f4112c77..93d37fdf 100644 --- a/starter/src/test/java/com/github/fonimus/ssh/shell/SshShellHelperTest.java +++ b/starter/src/test/java/com/github/fonimus/ssh/shell/SshShellHelperTest.java @@ -5,6 +5,8 @@ package com.github.fonimus.ssh.shell; import com.github.fonimus.ssh.shell.interactive.Interactive; +import com.github.fonimus.ssh.shell.interactive.InteractiveInput; +import com.github.fonimus.ssh.shell.interactive.KeyBinding; import org.jline.reader.impl.completer.ArgumentCompleter; import org.jline.terminal.Size; import org.jline.utils.AttributedString; @@ -167,27 +169,39 @@ void progress() { void interactive() throws Exception { // return 'q' = 113 after third times when(reader.read(100L)) - .thenReturn(0) - .thenReturn(0) - .thenReturn(113) - .thenReturn(113); + // '-','+' char + .thenReturn(43).thenReturn(45) + // 'f' char + .thenReturn(102) + // 'q' char + .thenReturn(113).thenReturn(113).thenReturn(113); when(ter.getSize()).thenReturn(new Size(13, 40)); final int[] count = {0}; - assertEquals(0, count[0]); - h.interactive(Interactive.builder().input((size, currentDelay) -> { - count[0]++; - return Collections.singletonList(AttributedString.EMPTY); - }).build()); - assertEquals(3, count[0]); + KeyBinding binding = KeyBinding.builder().key("f").description("Binding Ok").input(() -> { + + }).build(); + KeyBinding failingBinding = KeyBinding.builder().key("f").description("Failing Binding").input(() -> { + + }).build(); - h.interactive(Interactive.builder().input((size, currentDelay) -> { + InteractiveInput input = (size, currentDelay) -> { count[0]++; return Collections.singletonList(AttributedString.EMPTY); - }).fullScreen(false).build()); + }; + assertEquals(0, count[0]); + h.interactive(Interactive.builder().binding(binding).binding(failingBinding).input(input).build()); assertEquals(4, count[0]); + + h.interactive(Interactive.builder().input(input).fullScreen(false).build()); + + assertEquals(5, count[0]); + + h.interactive(input); + + assertEquals(6, count[0]); } private void verifyMessage(String message) { diff --git a/starter/src/test/java/com/github/fonimus/ssh/shell/SshShellUtilsTest.java b/starter/src/test/java/com/github/fonimus/ssh/shell/SshShellUtilsTest.java new file mode 100644 index 00000000..26d0ff67 --- /dev/null +++ b/starter/src/test/java/com/github/fonimus/ssh/shell/SshShellUtilsTest.java @@ -0,0 +1,24 @@ +package com.github.fonimus.ssh.shell; + +import org.apache.sshd.common.channel.PtyMode; +import org.jline.terminal.Attributes; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertFalse; + +class SshShellUtilsTest { + + @Test + void testFillAttributes() { + Attributes attributes = new Attributes(); + Map map = new HashMap<>(); + for (PtyMode value : PtyMode.values()) { + map.put(value, 1); + } + SshShellUtils.fill(attributes, map); + assertFalse(attributes.getControlChars().isEmpty()); + } +}