Skip to content

Commit

Permalink
Improve interactive processes, add test coverage
Browse files Browse the repository at this point in the history
  • Loading branch information
fonimus committed Apr 3, 2019
1 parent b61e87e commit e15fd8e
Show file tree
Hide file tree
Showing 12 changed files with 696 additions and 577 deletions.
29 changes: 22 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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<AttributedString> 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<AttributedString> 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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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 -> {
Expand Down Expand Up @@ -190,101 +188,6 @@ public void run() {
}
}

private void fill(Attributes attr, Map<PtyMode, Integer> ptyModes) {
for (Map.Entry<PtyMode, Integer> 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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> DEFAULT_CONFIRM_WORDS = Arrays.asList("y", "yes");

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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]);
Expand All @@ -399,35 +408,38 @@ 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<String> 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;
do {
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);
Expand Down Expand Up @@ -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<AttributedString> lines = input.getLines(size, currentDelay);
Expand Down
Loading

0 comments on commit e15fd8e

Please sign in to comment.