Skip to content

Commit 3a0dc53

Browse files
committed
Add onReady callback to AeshConsoleRunner (#603)
Fires a Runnable callback after readline is armed and ready to accept input, but before connection.openBlocking() blocks the calling thread. This gives test frameworks and embedders a reliable signal that the REPL is ready to receive commands, eliminating the need for Thread.sleep() between starting the REPL and sending the first command. ReadlineConsole: onReadyCallback field + setOnReadyCallback(Runnable) AeshConsoleRunner: onReady(Runnable) builder method wired via start()
1 parent 3cd7ec4 commit 3a0dc53

3 files changed

Lines changed: 78 additions & 0 deletions

File tree

aesh/src/main/java/org/aesh/AeshConsoleRunner.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ public class AeshConsoleRunner {
4848
private Prompt prompt;
4949
private ReadlineConsole console;
5050
private Connection connection;
51+
private Runnable onReady;
5152

5253
private AeshConsoleRunner() {
5354
}
@@ -196,6 +197,33 @@ public AeshConsoleRunner promptSupplier(Supplier<Prompt> supplier) {
196197
return this;
197198
}
198199

200+
/**
201+
* Set a callback that fires once after readline is armed and ready to
202+
* accept input, but before the blocking input loop starts. Intended for
203+
* test frameworks and embedders that need a reliable "REPL is ready"
204+
* signal before sending the first command.
205+
*
206+
* <p>
207+
* Example:
208+
*
209+
* <pre>{@code
210+
* CountDownLatch ready = new CountDownLatch(1);
211+
* new Thread(() -> AeshConsoleRunner.builder()
212+
* .command(MyCommand.class)
213+
* .onReady(ready::countDown)
214+
* .start()).start();
215+
* ready.await(5, TimeUnit.SECONDS); // guaranteed to be armed now
216+
* }</pre>
217+
*
218+
* @param callback the callback to invoke when the console is ready
219+
* @return this builder
220+
* @since 3.17
221+
*/
222+
public AeshConsoleRunner onReady(Runnable callback) {
223+
this.onReady = callback;
224+
return this;
225+
}
226+
199227
public AeshConsoleRunner addExitCommand() {
200228
ensureRegistryBuilderInitialized();
201229
try {
@@ -211,6 +239,8 @@ public void start() {
211239
init();
212240
if (prompt != null)
213241
console.setPrompt(prompt);
242+
if (onReady != null)
243+
console.setOnReadyCallback(onReady);
214244
try {
215245
console.start();
216246
} catch (IOException e) {

aesh/src/main/java/org/aesh/console/ReadlineConsole.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ public class ReadlineConsole implements Console, Consumer<Connection> {
115115

116116
private ShellImpl shell;
117117
private CommandContext commandContext;
118+
private Runnable onReadyCallback;
118119

119120
private final EnumMap<ReadlineFlag, Integer> readlineFlags = new EnumMap<>(ReadlineFlag.class);
120121

@@ -268,6 +269,9 @@ public void accept(Connection connection) {
268269

269270
this.runtime = generateRuntime();
270271
read(this.connection, readline);
272+
// Notify embedders/test frameworks that readline is armed and ready (#603)
273+
if (onReadyCallback != null)
274+
onReadyCallback.run();
271275
this.connection.openBlocking();
272276
}
273277

@@ -495,6 +499,19 @@ public void setPrompt(String prompt) {
495499
this.prompt = new Prompt(prompt);
496500
}
497501

502+
/**
503+
* Set a callback that fires once after readline is armed and ready to
504+
* accept input, but before the blocking input loop starts.
505+
* Intended for test frameworks and embedders that need a reliable
506+
* "REPL is ready" signal.
507+
*
508+
* @param callback the callback to invoke when the console is ready
509+
* @since 3.17
510+
*/
511+
public void setOnReadyCallback(Runnable callback) {
512+
this.onReadyCallback = callback;
513+
}
514+
498515
@Override
499516
public AeshContext context() {
500517
return context;

aesh/src/test/java/org/aesh/AeshConsoleRunnerTest.java

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@
2222
import static org.aesh.terminal.utils.Config.getLineSeparator;
2323
import static org.junit.Assert.assertTrue;
2424

25+
import java.util.concurrent.CountDownLatch;
26+
import java.util.concurrent.TimeUnit;
27+
2528
import org.aesh.command.Command;
2629
import org.aesh.command.CommandDefinition;
2730
import org.aesh.command.CommandResult;
@@ -202,6 +205,34 @@ public CommandResult execute(CommandInvocation commandInvocation) {
202205
}
203206
}
204207

208+
@Test
209+
public void testOnReadyFiresWhenConsoleIsArmed() throws Exception {
210+
TestConnection connection = new TestConnection();
211+
CountDownLatch readyLatch = new CountDownLatch(1);
212+
CountDownLatch commandLatch = new CountDownLatch(1);
213+
214+
// Start the blocking REPL on a background thread
215+
Thread replThread = new Thread(() -> AeshConsoleRunner.builder()
216+
.connection(connection)
217+
.command(HelloCommand.class)
218+
.addExitCommand()
219+
.commandExecutionListener((line, result, durationMs) -> commandLatch.countDown())
220+
.onReady(readyLatch::countDown)
221+
.start());
222+
replThread.setDaemon(true);
223+
replThread.start();
224+
225+
// onReady must fire before the REPL thread blocks in openBlocking()
226+
assertTrue("onReady should fire within 5 seconds", readyLatch.await(5, TimeUnit.SECONDS));
227+
228+
// Readline is now armed — send a command immediately, no sleep needed
229+
connection.read("hello" + getLineSeparator());
230+
assertTrue("Command should execute", commandLatch.await(5, TimeUnit.SECONDS));
231+
assertTrue("Output should contain greeting", connection.getOutputBuffer().contains("Hello"));
232+
233+
connection.read("exit" + getLineSeparator());
234+
}
235+
205236
private static void assertClosedWithin(TestConnection connection, long timeoutMs) throws InterruptedException {
206237
long deadline = System.currentTimeMillis() + timeoutMs;
207238
while (!connection.closed() && System.currentTimeMillis() < deadline) {

0 commit comments

Comments
 (0)