From f1cd225af7295dd5021663470e9b590720f1a901 Mon Sep 17 00:00:00 2001 From: NIHAL T P Date: Tue, 28 Oct 2025 23:38:17 +0530 Subject: [PATCH 1/5] feat: Add timeout command to wait for a specified duration Closes #86 --- src/main/java/com/mycmd/App.java | 1 + .../com/mycmd/commands/TimeoutCommand.java | 130 ++++++++++++++++++ 2 files changed, 131 insertions(+) create mode 100644 src/main/java/com/mycmd/commands/TimeoutCommand.java diff --git a/src/main/java/com/mycmd/App.java b/src/main/java/com/mycmd/App.java index a789e0d..5a2b804 100644 --- a/src/main/java/com/mycmd/App.java +++ b/src/main/java/com/mycmd/App.java @@ -108,5 +108,6 @@ private static void registerCommands(Map commands) { commands.put("set", new SetCommand()); commands.put("systeminfo", new SysteminfoCommand()); commands.put("pause", new PauseCommand()); + commands.put("timeout", new TimeoutCommand()); } } diff --git a/src/main/java/com/mycmd/commands/TimeoutCommand.java b/src/main/java/com/mycmd/commands/TimeoutCommand.java new file mode 100644 index 0000000..ddc7f86 --- /dev/null +++ b/src/main/java/com/mycmd/commands/TimeoutCommand.java @@ -0,0 +1,130 @@ +package com.mycmd.commands; + +import com.mycmd.Command; +import com.mycmd.ShellContext; +import java.io.IOException; + +public class TimeoutCommand implements Command { + @Override + public void execute(String[] args, ShellContext context) { + int seconds = -1; + boolean hasSecondsArg = false; + boolean hasSlashT = false; + boolean noBreak = false; + + for (int i = 0; i < args.length; i++) { + if (args[i].equalsIgnoreCase("/t") + && i + 1 < args.length + && args[i + 1].matches("[+-]?\\d+")) { + if (hasSlashT) { + System.out.println( + "Error: Invalid syntax. '/t' option is not allowed more than '1' time(s)."); + return; + } + seconds = Integer.parseInt(args[i + 1]); + i++; + hasSecondsArg = true; + hasSlashT = true; + } else if (args[i].equalsIgnoreCase("/nobreak")) { + if (noBreak) { + System.out.println( + "Error: Invalid syntax. '/nobreak' option is not allowed more than '1' time(s)."); + return; + } + noBreak = true; + } else if (args[i].matches("[+-]?\\d+")) { + if (hasSecondsArg) { + System.out.println( + "Error: Invalid syntax. Default option is not allowed more than '1' time(s)."); + return; + } + seconds = Integer.parseInt(args[i]); + hasSecondsArg = true; + } else if (args[i].equalsIgnoreCase("/t")) { + System.out.println("Error: Invalid syntax. Value expected for '/t'."); + return; + } + } + + if (!hasSecondsArg) { + System.out.println("Error: Invalid syntax. Seconds value is required."); + return; + } + + if (seconds < -1 || seconds > 99999) { + System.out.println( + "Error: Invalid value for timeout specified. Valid range is 0-99999 seconds."); + return; + } + + if (seconds == -1) { + System.out.println(); + PauseCommand pauseCmd = new PauseCommand(); + pauseCmd.execute(new String[0], context); + return; + } + + Thread inputThread = + new Thread( + () -> { + try { + System.in.read(); + } catch (IOException e) { + // Ignore + } + }); + inputThread.setDaemon(true); + + if (!noBreak) { + inputThread.start(); + } + System.out.println(); + + for (; seconds > 0; seconds--) { + if (!noBreak && !inputThread.isAlive()) { + System.out.println("\r"); + System.out.println(); + return; + } + + System.out.print( + "\rWaiting for " + + seconds + + " seconds, press " + + (noBreak ? "CTRL+C to quit ..." : "enter key to continue ...")); + System.out.flush(); + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + if (noBreak) { + continue; + } + System.out.println(); + Thread.currentThread().interrupt(); + break; + } + } + try { + while (System.in.available() > 0) { + System.in.read(); + } + } catch (IOException e) { + // Ignore + } + System.out.println("\r"); + System.out.println(); + } + + @Override + public String description() { + return "Sets a timeout for command execution."; + } + + @Override + public String usage() { + return "timeout \n" + + "timeout /t \n" + + "timeout /t /nobreak\n" + + "timeout /t -1"; + } +} From 7252ece813b279cf70e137766956c710b41d4bff Mon Sep 17 00:00:00 2001 From: NIHAL T P Date: Tue, 28 Oct 2025 23:58:36 +0530 Subject: [PATCH 2/5] refactor: Improve input handling in TimeoutCommand for better interrupt detection --- .../com/mycmd/commands/TimeoutCommand.java | 43 +++++++++++++------ 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/mycmd/commands/TimeoutCommand.java b/src/main/java/com/mycmd/commands/TimeoutCommand.java index ddc7f86..9e8b0ce 100644 --- a/src/main/java/com/mycmd/commands/TimeoutCommand.java +++ b/src/main/java/com/mycmd/commands/TimeoutCommand.java @@ -64,26 +64,39 @@ public void execute(String[] args, ShellContext context) { return; } - Thread inputThread = - new Thread( - () -> { - try { - System.in.read(); - } catch (IOException e) { - // Ignore - } - }); - inputThread.setDaemon(true); + AtomicBoolean interrupted = new AtomicBoolean(false); + Thread inputThread = null; if (!noBreak) { + inputThread = + new Thread( + () -> { + try { + int r; + // Read until a newline is encountered so we only exit on Enter + while ((r = System.in.read()) != -1) { + if (r == '\n') { + interrupted.set(true); + break; + } + } + } catch (IOException e) { + // Ignore: if System.in is closed or an I/O error occurs we + // cannot reliably wait for Enter; treat as no-interrupt. + } + }); + inputThread.setDaemon(true); inputThread.start(); } System.out.println(); for (; seconds > 0; seconds--) { - if (!noBreak && !inputThread.isAlive()) { + if (!noBreak && interrupted.get()) { System.out.println("\r"); System.out.println(); + if (inputThread != null && inputThread.isAlive()) { + inputThread.interrupt(); + } return; } @@ -104,13 +117,19 @@ public void execute(String[] args, ShellContext context) { break; } } + try { + // Drain any remaining bytes so subsequent commands don't immediately see + // leftover input. This is a best-effort drain; System.in.available() may + // not be supported on all streams, but for typical console streams it helps. while (System.in.available() > 0) { System.in.read(); } } catch (IOException e) { - // Ignore + // Ignore: if we can't drain the stream it's non-fatal; any leftover input + // will be handled by the next read and is acceptable. } + System.out.println("\r"); System.out.println(); } From 1fd6ecadc547011ca1d3f91f02239f1a273d24d6 Mon Sep 17 00:00:00 2001 From: NIHAL T P Date: Tue, 28 Oct 2025 23:59:58 +0530 Subject: [PATCH 3/5] refactor: Improve input handling in TimeoutCommand for better interrupt detection --- CheckList.md | 2 +- .../com/mycmd/commands/TimeoutCommand.java | 43 +++++++++++++------ 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/CheckList.md b/CheckList.md index e212e2d..f9f7649 100644 --- a/CheckList.md +++ b/CheckList.md @@ -117,7 +117,7 @@ - [x] date - [ ] shutdown - [ ] choice -- [ ] timeout +- [x] timeout - [ ] call - [ ] start - [ ] taskkill diff --git a/src/main/java/com/mycmd/commands/TimeoutCommand.java b/src/main/java/com/mycmd/commands/TimeoutCommand.java index ddc7f86..9e8b0ce 100644 --- a/src/main/java/com/mycmd/commands/TimeoutCommand.java +++ b/src/main/java/com/mycmd/commands/TimeoutCommand.java @@ -64,26 +64,39 @@ public void execute(String[] args, ShellContext context) { return; } - Thread inputThread = - new Thread( - () -> { - try { - System.in.read(); - } catch (IOException e) { - // Ignore - } - }); - inputThread.setDaemon(true); + AtomicBoolean interrupted = new AtomicBoolean(false); + Thread inputThread = null; if (!noBreak) { + inputThread = + new Thread( + () -> { + try { + int r; + // Read until a newline is encountered so we only exit on Enter + while ((r = System.in.read()) != -1) { + if (r == '\n') { + interrupted.set(true); + break; + } + } + } catch (IOException e) { + // Ignore: if System.in is closed or an I/O error occurs we + // cannot reliably wait for Enter; treat as no-interrupt. + } + }); + inputThread.setDaemon(true); inputThread.start(); } System.out.println(); for (; seconds > 0; seconds--) { - if (!noBreak && !inputThread.isAlive()) { + if (!noBreak && interrupted.get()) { System.out.println("\r"); System.out.println(); + if (inputThread != null && inputThread.isAlive()) { + inputThread.interrupt(); + } return; } @@ -104,13 +117,19 @@ public void execute(String[] args, ShellContext context) { break; } } + try { + // Drain any remaining bytes so subsequent commands don't immediately see + // leftover input. This is a best-effort drain; System.in.available() may + // not be supported on all streams, but for typical console streams it helps. while (System.in.available() > 0) { System.in.read(); } } catch (IOException e) { - // Ignore + // Ignore: if we can't drain the stream it's non-fatal; any leftover input + // will be handled by the next read and is acceptable. } + System.out.println("\r"); System.out.println(); } From 28f1eaa1c6313dff6fa73fb2f8fb5c1173c12b26 Mon Sep 17 00:00:00 2001 From: NIHAL T P Date: Wed, 29 Oct 2025 00:02:59 +0530 Subject: [PATCH 4/5] Merge branch 'feature/timeout' of https://github.com/nihaltp/MyCMD into feature/timeout --- src/main/java/com/mycmd/commands/TimeoutCommand.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/mycmd/commands/TimeoutCommand.java b/src/main/java/com/mycmd/commands/TimeoutCommand.java index 9e8b0ce..9bfb4da 100644 --- a/src/main/java/com/mycmd/commands/TimeoutCommand.java +++ b/src/main/java/com/mycmd/commands/TimeoutCommand.java @@ -3,6 +3,7 @@ import com.mycmd.Command; import com.mycmd.ShellContext; import java.io.IOException; +import java.util.concurrent.atomic.AtomicBoolean; public class TimeoutCommand implements Command { @Override From 7d5de1aae6b6c28763fa59e3f918f077c4841af7 Mon Sep 17 00:00:00 2001 From: NIHAL T P Date: Wed, 29 Oct 2025 00:16:27 +0530 Subject: [PATCH 5/5] refactor: Enhance input validation and interrupt handling in TimeoutCommand --- .../com/mycmd/commands/TimeoutCommand.java | 58 +++++++++++++++---- 1 file changed, 48 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/mycmd/commands/TimeoutCommand.java b/src/main/java/com/mycmd/commands/TimeoutCommand.java index 9bfb4da..fd429f8 100644 --- a/src/main/java/com/mycmd/commands/TimeoutCommand.java +++ b/src/main/java/com/mycmd/commands/TimeoutCommand.java @@ -6,6 +6,16 @@ import java.util.concurrent.atomic.AtomicBoolean; public class TimeoutCommand implements Command { + /** + * Execute the timeout command. + * + *

This command will wait for the specified number of seconds before continuing. If the user + * presses Enter before the timeout expires, the command will terminate immediately. + * + * @param args The arguments to the command. + * @param context The context of the shell. + * @throws IOException If an I/O error occurs. + */ @Override public void execute(String[] args, ShellContext context) { int seconds = -1; @@ -44,6 +54,9 @@ public void execute(String[] args, ShellContext context) { } else if (args[i].equalsIgnoreCase("/t")) { System.out.println("Error: Invalid syntax. Value expected for '/t'."); return; + } else { + System.out.println("Error: Invalid syntax. Unrecognized argument: " + args[i]); + return; } } @@ -66,6 +79,7 @@ public void execute(String[] args, ShellContext context) { } AtomicBoolean interrupted = new AtomicBoolean(false); + AtomicBoolean stopInput = new AtomicBoolean(false); Thread inputThread = null; if (!noBreak) { @@ -73,17 +87,27 @@ public void execute(String[] args, ShellContext context) { new Thread( () -> { try { - int r; - // Read until a newline is encountered so we only exit on Enter - while ((r = System.in.read()) != -1) { - if (r == '\n') { - interrupted.set(true); - break; + // Poll non-blocking so we can stop the thread + // deterministically. + while (!stopInput.get()) { + if (System.in.available() > 0) { + int r = System.in.read(); + // Treat CR or LF as Enter across platforms + if (r == '\n' || r == '\r') { + interrupted.set(true); + break; + } + } else { + try { + Thread.sleep(25); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + break; + } } } } catch (IOException e) { - // Ignore: if System.in is closed or an I/O error occurs we - // cannot reliably wait for Enter; treat as no-interrupt. + // Best-effort only; fall through. } }); inputThread.setDaemon(true); @@ -95,8 +119,13 @@ public void execute(String[] args, ShellContext context) { if (!noBreak && interrupted.get()) { System.out.println("\r"); System.out.println(); - if (inputThread != null && inputThread.isAlive()) { - inputThread.interrupt(); + if (inputThread != null) { + stopInput.set(true); + try { + inputThread.join(200); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + } } return; } @@ -119,6 +148,15 @@ public void execute(String[] args, ShellContext context) { } } + // Normal completion: stop the input thread before draining. + if (!noBreak && inputThread != null) { + stopInput.set(true); + try { + inputThread.join(200); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + } + } try { // Drain any remaining bytes so subsequent commands don't immediately see // leftover input. This is a best-effort drain; System.in.available() may