Skip to content

Commit

Permalink
add support for handling user interrupts
Browse files Browse the repository at this point in the history
  • Loading branch information
siordache committed Feb 20, 2017
1 parent bd30e18 commit 98b5d29
Show file tree
Hide file tree
Showing 16 changed files with 368 additions and 71 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ bin/
*.iws
.idea/
out/
classes/

hs_err_pid*
_ignore*
16 changes: 13 additions & 3 deletions text-io-demo/src/main/java/org/beryx/textio/demo/TextIoDemo.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import org.beryx.textio.console.ConsoleTextTerminalProvider;
import org.beryx.textio.jline.AnsiTextTerminal;
import org.beryx.textio.jline.JLineTextTerminalProvider;
import org.beryx.textio.swing.SwingTextTerminal;
import org.beryx.textio.swing.SwingTextTerminalProvider;
import org.beryx.textio.system.SystemTextTerminal;
import org.beryx.textio.system.SystemTextTerminalProvider;
Expand Down Expand Up @@ -57,9 +58,19 @@ public String toString() {
}

public static void main(String[] args) {
System.setProperty(SwingTextTerminal.PROP_USER_INTERRUPT_KEY, "ctrl C");
TextIO textIO = chooseTextIO();

// Uncomment the line below to ignore user interrupts.
// textIO.getTextTerminal().registerUserInterruptHandler(term -> System.out.println("\n\t### User interrupt ignored."), false);

if(textIO.getTextTerminal() instanceof WebTextTerminal) {
WebTextIoExecutor webTextIoExecutor = new WebTextIoExecutor().withPort(webServerPort);
WebTextTerminal webTextTerm = (WebTextTerminal)textIO.getTextTerminal();

// Uncomment the line below to trigger a user interrupt in the web terminal by typing Ctrl+C (instead of the default Ctrl+Q).
// webTextTerm.setUserInterruptKey('C', true, false, false);

WebTextIoExecutor webTextIoExecutor = new WebTextIoExecutor(webTextTerm).withPort(webServerPort);
webTextIoExecutor.execute(SimpleApp::execute);
} else {
SimpleApp.execute(textIO);
Expand Down Expand Up @@ -125,8 +136,7 @@ private static WebTextTerminal createWebTextTerminal(TextIO textIO) {
.withDefaultValue(Service.SPARK_DEFAULT_PORT)
.read("Server port number");

// The returned WebTextTerminal is not actually used, but treated as a marker that triggers the creation of a WebTextIoExecutor.
// This WebTextIoExecutor will instantiate a new WebTextTerminal each time a client starts a new session.
// The returned WebTextTerminal is used as a template by the WebTextIoExecutor, which instantiates a new WebTextTerminal each time a client starts a new session.
return new WebTextTerminal();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,12 @@
import org.beryx.textio.TextIO;
import org.beryx.textio.web.SparkDataServer;
import org.beryx.textio.web.SparkTextIoApp;
import org.beryx.textio.web.WebTextTerminal;

import java.awt.*;
import java.net.URI;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;

import static spark.Spark.staticFiles;
Expand All @@ -31,26 +34,27 @@
* by configuring and initializing a {@link SparkDataServer}.
*/
public class WebTextIoExecutor {
private final WebTextTerminal termTemplate;
private int port = -1;

public WebTextIoExecutor(WebTextTerminal termTemplate) {
this.termTemplate = termTemplate;
}

public WebTextIoExecutor withPort(int port) {
this.port = port;
return this;
}

public void execute(Consumer<TextIO> textIoRunner) {
SparkTextIoApp app = new SparkTextIoApp(textIoRunner);
SparkTextIoApp app = new SparkTextIoApp(textIoRunner, termTemplate);
app.setMaxInactiveSeconds(600);
app.setOnDispose(sessionId -> {
new Thread(() -> {
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
stop();
}).start();
});
Consumer<String> stopServer = sessionId -> Executors.newSingleThreadScheduledExecutor().schedule(() -> {
stop();
System.exit(0);
}, 2, TimeUnit.SECONDS);
app.setOnDispose(stopServer);
app.setOnAbort(stopServer);

SparkDataServer server = app.getServer();
if(port > 0) {
Expand Down
4 changes: 3 additions & 1 deletion text-io-demo/src/main/resources/public-html/web-demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@ <h3 id="app-done"> </h3>
<script src="textterm.js"></script>
<script>
var textTerm = TextTerm.init(document.getElementById("textterm"));

textTerm.onDispose = function() {
document.getElementById("app-done").textContent = "You can now close this window.";
}
textTerm.onAbort = function() {
document.getElementById("app-done").textContent = "Program aborted by the user. You can now close this window.";
}
</script>

</body>
Expand Down
6 changes: 6 additions & 0 deletions text-io-web/src/main/java/org/beryx/textio/web/DataApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,10 @@ public interface DataApi {

/** This method is called by the web component to post the user input */
void postUserInput(String input);

/**
* This method is called by the web component in response to a user interrupt (typically triggered by typing Ctrl+Q).
* @param partialInput the partially entered input when the user interrupt occurred.
*/
void postUserInterrupt(String partialInput);
}
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,15 @@ public void init() {
post(pathForPostInput, (request, response) -> {
logger.trace("Received POST");
DataApi dataApi = getDataApi(request);
boolean userInterrupt = Boolean.parseBoolean(request.headers("textio-user-interrupt"));
String input = new String(request.body().getBytes(), "UTF-8");
logger.trace("Posting input...");
dataApi.postUserInput(input);
if(userInterrupt) {
logger.trace("Posting user interrupted input...");
dataApi.postUserInterrupt(input);
} else {
logger.trace("Posting input...");
dataApi.postUserInput(input);
}
return "OK";
});
}
Expand Down
17 changes: 14 additions & 3 deletions text-io-web/src/main/java/org/beryx/textio/web/SparkTextIoApp.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,18 @@ public class SparkTextIoApp {
private static final Logger logger = LoggerFactory.getLogger(SparkTextIoApp.class);

private final Map<String, WebTextTerminal> dataApiMap = new HashMap<>();
private final WebTextTerminal termTemplate;

private final Consumer<TextIO> textIoRunner;
private final SparkDataServer server;
private Integer maxInactiveSeconds = null;

private Consumer<String> onDispose;
private Consumer<String> onAbort;

public SparkTextIoApp(Consumer<TextIO> textIoRunner) {
public SparkTextIoApp(Consumer<TextIO> textIoRunner, WebTextTerminal termTemplate) {
this.textIoRunner = textIoRunner;
this.termTemplate = termTemplate;
this.server = new SparkDataServer(this::getDataApi);
}

Expand All @@ -50,18 +53,26 @@ public void setOnDispose(Consumer<String> onDispose) {
this.onDispose = onDispose;
}

public void setOnAbort(Consumer<String> onAbort) {
this.onAbort = onAbort;
}

public void setMaxInactiveSeconds(Integer maxInactiveSeconds) {
this.maxInactiveSeconds = maxInactiveSeconds;
}

private final WebTextTerminal getDataApi(String sessionId, Session session) {
private WebTextTerminal getDataApi(String sessionId, Session session) {
synchronized (dataApiMap) {
WebTextTerminal terminal = dataApiMap.get(sessionId);
if(terminal == null) {
terminal = new WebTextTerminal();
logger.debug("Creating terminal for sessionId: " + sessionId);
terminal = termTemplate.createCopy();
if(onDispose != null) {
terminal.setOnDispose(() -> onDispose.accept(sessionId));
}
if(onAbort != null) {
terminal.setOnAbort(() -> onAbort.accept(sessionId));
}
dataApiMap.put(sessionId, terminal);

if(maxInactiveSeconds != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,23 +20,50 @@

/**
* The data sent by the server to a polling web component.
* Includes a list of prompt messages and an action to be executed by the web component (NONE, READ, READ_MASKED, DISPOSE).
* Includes a list of settings, a list of prompt messages and an action to be executed by the web component (NONE, READ, READ_MASKED, DISPOSE or ABORT).
*/
public class TextTerminalData {
public enum Action {NONE, READ, READ_MASKED, DISPOSE}
public enum Action {NONE, READ, READ_MASKED, DISPOSE, ABORT}

public static class KeyValue {
public final String key;
public final Object value;

public KeyValue(String key, Object value) {
this.key = key;
this.value = value;
}
}

private final List<KeyValue> settings = new ArrayList<>() ;
private final List<String> messages = new ArrayList<>();
private Action action = Action.NONE;
private boolean resetRequired = true;

public TextTerminalData getCopy() {
TextTerminalData data = new TextTerminalData();
data.settings.addAll(settings);
data.messages.addAll(messages);
data.action = action;
data.resetRequired = resetRequired;
return data;
}

public void addSetting(String key, Object value) {
KeyValue keyVal = new KeyValue(key, value);
int size = settings.size();
for(int i = 0; i < size; i++) {
if(settings.get(i).key.equals(key)) {
settings.set(i, keyVal);
return;
}
}
settings.add(keyVal);
}

public List<KeyValue> getSettings() {
return settings;
}
public List<String> getMessages() {
return messages;
}
Expand Down Expand Up @@ -65,6 +92,7 @@ public boolean hasAction() {
}

public void clear() {
settings.clear();
messages.clear();
action = Action.NONE;
resetRequired = false;
Expand All @@ -73,6 +101,7 @@ public void clear() {
@Override
public String toString() {
return "resetRequired: " + resetRequired +
", settings: " + settings +
", messages: " + messages +
", action: " + action;
}
Expand Down
Loading

0 comments on commit 98b5d29

Please sign in to comment.