Skip to content

Commit 98b5d29

Browse files
committed
add support for handling user interrupts
1 parent bd30e18 commit 98b5d29

File tree

16 files changed

+368
-71
lines changed

16 files changed

+368
-71
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ bin/
2020
*.iws
2121
.idea/
2222
out/
23+
classes/
2324

2425
hs_err_pid*
2526
_ignore*

text-io-demo/src/main/java/org/beryx/textio/demo/TextIoDemo.java

+13-3
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import org.beryx.textio.console.ConsoleTextTerminalProvider;
2323
import org.beryx.textio.jline.AnsiTextTerminal;
2424
import org.beryx.textio.jline.JLineTextTerminalProvider;
25+
import org.beryx.textio.swing.SwingTextTerminal;
2526
import org.beryx.textio.swing.SwingTextTerminalProvider;
2627
import org.beryx.textio.system.SystemTextTerminal;
2728
import org.beryx.textio.system.SystemTextTerminalProvider;
@@ -57,9 +58,19 @@ public String toString() {
5758
}
5859

5960
public static void main(String[] args) {
61+
System.setProperty(SwingTextTerminal.PROP_USER_INTERRUPT_KEY, "ctrl C");
6062
TextIO textIO = chooseTextIO();
63+
64+
// Uncomment the line below to ignore user interrupts.
65+
// textIO.getTextTerminal().registerUserInterruptHandler(term -> System.out.println("\n\t### User interrupt ignored."), false);
66+
6167
if(textIO.getTextTerminal() instanceof WebTextTerminal) {
62-
WebTextIoExecutor webTextIoExecutor = new WebTextIoExecutor().withPort(webServerPort);
68+
WebTextTerminal webTextTerm = (WebTextTerminal)textIO.getTextTerminal();
69+
70+
// Uncomment the line below to trigger a user interrupt in the web terminal by typing Ctrl+C (instead of the default Ctrl+Q).
71+
// webTextTerm.setUserInterruptKey('C', true, false, false);
72+
73+
WebTextIoExecutor webTextIoExecutor = new WebTextIoExecutor(webTextTerm).withPort(webServerPort);
6374
webTextIoExecutor.execute(SimpleApp::execute);
6475
} else {
6576
SimpleApp.execute(textIO);
@@ -125,8 +136,7 @@ private static WebTextTerminal createWebTextTerminal(TextIO textIO) {
125136
.withDefaultValue(Service.SPARK_DEFAULT_PORT)
126137
.read("Server port number");
127138

128-
// The returned WebTextTerminal is not actually used, but treated as a marker that triggers the creation of a WebTextIoExecutor.
129-
// This WebTextIoExecutor will instantiate a new WebTextTerminal each time a client starts a new session.
139+
// The returned WebTextTerminal is used as a template by the WebTextIoExecutor, which instantiates a new WebTextTerminal each time a client starts a new session.
130140
return new WebTextTerminal();
131141
}
132142
}

text-io-demo/src/main/java/org/beryx/textio/demo/WebTextIoExecutor.java

+15-11
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,12 @@
1818
import org.beryx.textio.TextIO;
1919
import org.beryx.textio.web.SparkDataServer;
2020
import org.beryx.textio.web.SparkTextIoApp;
21+
import org.beryx.textio.web.WebTextTerminal;
2122

2223
import java.awt.*;
2324
import java.net.URI;
25+
import java.util.concurrent.Executors;
26+
import java.util.concurrent.TimeUnit;
2427
import java.util.function.Consumer;
2528

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

40+
public WebTextIoExecutor(WebTextTerminal termTemplate) {
41+
this.termTemplate = termTemplate;
42+
}
43+
3644
public WebTextIoExecutor withPort(int port) {
3745
this.port = port;
3846
return this;
3947
}
4048

4149
public void execute(Consumer<TextIO> textIoRunner) {
42-
SparkTextIoApp app = new SparkTextIoApp(textIoRunner);
50+
SparkTextIoApp app = new SparkTextIoApp(textIoRunner, termTemplate);
4351
app.setMaxInactiveSeconds(600);
44-
app.setOnDispose(sessionId -> {
45-
new Thread(() -> {
46-
try {
47-
Thread.sleep(1000L);
48-
} catch (InterruptedException e) {
49-
e.printStackTrace();
50-
}
51-
stop();
52-
}).start();
53-
});
52+
Consumer<String> stopServer = sessionId -> Executors.newSingleThreadScheduledExecutor().schedule(() -> {
53+
stop();
54+
System.exit(0);
55+
}, 2, TimeUnit.SECONDS);
56+
app.setOnDispose(stopServer);
57+
app.setOnAbort(stopServer);
5458

5559
SparkDataServer server = app.getServer();
5660
if(port > 0) {

text-io-demo/src/main/resources/public-html/web-demo.html

+3-1
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,12 @@ <h3 id="app-done"> </h3>
1919
<script src="textterm.js"></script>
2020
<script>
2121
var textTerm = TextTerm.init(document.getElementById("textterm"));
22-
2322
textTerm.onDispose = function() {
2423
document.getElementById("app-done").textContent = "You can now close this window.";
2524
}
25+
textTerm.onAbort = function() {
26+
document.getElementById("app-done").textContent = "Program aborted by the user. You can now close this window.";
27+
}
2628
</script>
2729

2830
</body>

text-io-web/src/main/java/org/beryx/textio/web/DataApi.java

+6
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,10 @@ public interface DataApi {
2424

2525
/** This method is called by the web component to post the user input */
2626
void postUserInput(String input);
27+
28+
/**
29+
* This method is called by the web component in response to a user interrupt (typically triggered by typing Ctrl+Q).
30+
* @param partialInput the partially entered input when the user interrupt occurred.
31+
*/
32+
void postUserInterrupt(String partialInput);
2733
}

text-io-web/src/main/java/org/beryx/textio/web/SparkDataServer.java

+8-2
Original file line numberDiff line numberDiff line change
@@ -97,9 +97,15 @@ public void init() {
9797
post(pathForPostInput, (request, response) -> {
9898
logger.trace("Received POST");
9999
DataApi dataApi = getDataApi(request);
100+
boolean userInterrupt = Boolean.parseBoolean(request.headers("textio-user-interrupt"));
100101
String input = new String(request.body().getBytes(), "UTF-8");
101-
logger.trace("Posting input...");
102-
dataApi.postUserInput(input);
102+
if(userInterrupt) {
103+
logger.trace("Posting user interrupted input...");
104+
dataApi.postUserInterrupt(input);
105+
} else {
106+
logger.trace("Posting input...");
107+
dataApi.postUserInput(input);
108+
}
103109
return "OK";
104110
});
105111
}

text-io-web/src/main/java/org/beryx/textio/web/SparkTextIoApp.java

+14-3
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,18 @@ public class SparkTextIoApp {
3030
private static final Logger logger = LoggerFactory.getLogger(SparkTextIoApp.class);
3131

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

3435
private final Consumer<TextIO> textIoRunner;
3536
private final SparkDataServer server;
3637
private Integer maxInactiveSeconds = null;
3738

3839
private Consumer<String> onDispose;
40+
private Consumer<String> onAbort;
3941

40-
public SparkTextIoApp(Consumer<TextIO> textIoRunner) {
42+
public SparkTextIoApp(Consumer<TextIO> textIoRunner, WebTextTerminal termTemplate) {
4143
this.textIoRunner = textIoRunner;
44+
this.termTemplate = termTemplate;
4245
this.server = new SparkDataServer(this::getDataApi);
4346
}
4447

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

56+
public void setOnAbort(Consumer<String> onAbort) {
57+
this.onAbort = onAbort;
58+
}
59+
5360
public void setMaxInactiveSeconds(Integer maxInactiveSeconds) {
5461
this.maxInactiveSeconds = maxInactiveSeconds;
5562
}
5663

57-
private final WebTextTerminal getDataApi(String sessionId, Session session) {
64+
private WebTextTerminal getDataApi(String sessionId, Session session) {
5865
synchronized (dataApiMap) {
5966
WebTextTerminal terminal = dataApiMap.get(sessionId);
6067
if(terminal == null) {
61-
terminal = new WebTextTerminal();
68+
logger.debug("Creating terminal for sessionId: " + sessionId);
69+
terminal = termTemplate.createCopy();
6270
if(onDispose != null) {
6371
terminal.setOnDispose(() -> onDispose.accept(sessionId));
6472
}
73+
if(onAbort != null) {
74+
terminal.setOnAbort(() -> onAbort.accept(sessionId));
75+
}
6576
dataApiMap.put(sessionId, terminal);
6677

6778
if(maxInactiveSeconds != null) {

text-io-web/src/main/java/org/beryx/textio/web/TextTerminalData.java

+31-2
Original file line numberDiff line numberDiff line change
@@ -20,23 +20,50 @@
2020

2121
/**
2222
* The data sent by the server to a polling web component.
23-
* Includes a list of prompt messages and an action to be executed by the web component (NONE, READ, READ_MASKED, DISPOSE).
23+
* 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).
2424
*/
2525
public class TextTerminalData {
26-
public enum Action {NONE, READ, READ_MASKED, DISPOSE}
26+
public enum Action {NONE, READ, READ_MASKED, DISPOSE, ABORT}
2727

28+
public static class KeyValue {
29+
public final String key;
30+
public final Object value;
31+
32+
public KeyValue(String key, Object value) {
33+
this.key = key;
34+
this.value = value;
35+
}
36+
}
37+
38+
private final List<KeyValue> settings = new ArrayList<>() ;
2839
private final List<String> messages = new ArrayList<>();
2940
private Action action = Action.NONE;
3041
private boolean resetRequired = true;
3142

3243
public TextTerminalData getCopy() {
3344
TextTerminalData data = new TextTerminalData();
45+
data.settings.addAll(settings);
3446
data.messages.addAll(messages);
3547
data.action = action;
3648
data.resetRequired = resetRequired;
3749
return data;
3850
}
3951

52+
public void addSetting(String key, Object value) {
53+
KeyValue keyVal = new KeyValue(key, value);
54+
int size = settings.size();
55+
for(int i = 0; i < size; i++) {
56+
if(settings.get(i).key.equals(key)) {
57+
settings.set(i, keyVal);
58+
return;
59+
}
60+
}
61+
settings.add(keyVal);
62+
}
63+
64+
public List<KeyValue> getSettings() {
65+
return settings;
66+
}
4067
public List<String> getMessages() {
4168
return messages;
4269
}
@@ -65,6 +92,7 @@ public boolean hasAction() {
6592
}
6693

6794
public void clear() {
95+
settings.clear();
6896
messages.clear();
6997
action = Action.NONE;
7098
resetRequired = false;
@@ -73,6 +101,7 @@ public void clear() {
73101
@Override
74102
public String toString() {
75103
return "resetRequired: " + resetRequired +
104+
", settings: " + settings +
76105
", messages: " + messages +
77106
", action: " + action;
78107
}

0 commit comments

Comments
 (0)