Skip to content

Commit add7dbb

Browse files
Merge pull request #88 from nihaltp/feature/timeout
feat: Add timeout command to wait for a specified duration
2 parents 896fc78 + 7d5de1a commit add7dbb

File tree

3 files changed

+191
-1
lines changed

3 files changed

+191
-1
lines changed

CheckList.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@
117117
- [x] date
118118
- [ ] shutdown
119119
- [ ] choice
120-
- [ ] timeout
120+
- [x] timeout
121121
- [ ] call
122122
- [ ] start
123123
- [ ] taskkill

src/main/java/com/mycmd/App.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ private CommandNames() {}
101101
private static final String TASKLIST = "tasklist";
102102
private static final String TELNET = "telnet";
103103
private static final String TIME = "time";
104+
private static final String TIMEOUT = "timeout";
104105
private static final String TITLE = "title";
105106
private static final String TOUCH = "touch";
106107
private static final String TREE = "tree";
@@ -138,6 +139,7 @@ private static void registerCommands(Map<String, Command> commands) {
138139
commands.put(CommandNames.TASKLIST, new TasklistCommand());
139140
commands.put(CommandNames.TELNET, new TelnetCommand());
140141
commands.put(CommandNames.TIME, new TimeCommand());
142+
commands.put(CommandNames.TIMEOUT, new TimeoutCommand());
141143
commands.put(CommandNames.TITLE, new TitleCommand());
142144
commands.put(CommandNames.TOUCH, new TouchCommand());
143145
commands.put(CommandNames.TREE, new TreeCommand());
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
package com.mycmd.commands;
2+
3+
import com.mycmd.Command;
4+
import com.mycmd.ShellContext;
5+
import java.io.IOException;
6+
import java.util.concurrent.atomic.AtomicBoolean;
7+
8+
public class TimeoutCommand implements Command {
9+
/**
10+
* Execute the timeout command.
11+
*
12+
* <p>This command will wait for the specified number of seconds before continuing. If the user
13+
* presses Enter before the timeout expires, the command will terminate immediately.
14+
*
15+
* @param args The arguments to the command.
16+
* @param context The context of the shell.
17+
* @throws IOException If an I/O error occurs.
18+
*/
19+
@Override
20+
public void execute(String[] args, ShellContext context) {
21+
int seconds = -1;
22+
boolean hasSecondsArg = false;
23+
boolean hasSlashT = false;
24+
boolean noBreak = false;
25+
26+
for (int i = 0; i < args.length; i++) {
27+
if (args[i].equalsIgnoreCase("/t")
28+
&& i + 1 < args.length
29+
&& args[i + 1].matches("[+-]?\\d+")) {
30+
if (hasSlashT) {
31+
System.out.println(
32+
"Error: Invalid syntax. '/t' option is not allowed more than '1' time(s).");
33+
return;
34+
}
35+
seconds = Integer.parseInt(args[i + 1]);
36+
i++;
37+
hasSecondsArg = true;
38+
hasSlashT = true;
39+
} else if (args[i].equalsIgnoreCase("/nobreak")) {
40+
if (noBreak) {
41+
System.out.println(
42+
"Error: Invalid syntax. '/nobreak' option is not allowed more than '1' time(s).");
43+
return;
44+
}
45+
noBreak = true;
46+
} else if (args[i].matches("[+-]?\\d+")) {
47+
if (hasSecondsArg) {
48+
System.out.println(
49+
"Error: Invalid syntax. Default option is not allowed more than '1' time(s).");
50+
return;
51+
}
52+
seconds = Integer.parseInt(args[i]);
53+
hasSecondsArg = true;
54+
} else if (args[i].equalsIgnoreCase("/t")) {
55+
System.out.println("Error: Invalid syntax. Value expected for '/t'.");
56+
return;
57+
} else {
58+
System.out.println("Error: Invalid syntax. Unrecognized argument: " + args[i]);
59+
return;
60+
}
61+
}
62+
63+
if (!hasSecondsArg) {
64+
System.out.println("Error: Invalid syntax. Seconds value is required.");
65+
return;
66+
}
67+
68+
if (seconds < -1 || seconds > 99999) {
69+
System.out.println(
70+
"Error: Invalid value for timeout specified. Valid range is 0-99999 seconds.");
71+
return;
72+
}
73+
74+
if (seconds == -1) {
75+
System.out.println();
76+
PauseCommand pauseCmd = new PauseCommand();
77+
pauseCmd.execute(new String[0], context);
78+
return;
79+
}
80+
81+
AtomicBoolean interrupted = new AtomicBoolean(false);
82+
AtomicBoolean stopInput = new AtomicBoolean(false);
83+
Thread inputThread = null;
84+
85+
if (!noBreak) {
86+
inputThread =
87+
new Thread(
88+
() -> {
89+
try {
90+
// Poll non-blocking so we can stop the thread
91+
// deterministically.
92+
while (!stopInput.get()) {
93+
if (System.in.available() > 0) {
94+
int r = System.in.read();
95+
// Treat CR or LF as Enter across platforms
96+
if (r == '\n' || r == '\r') {
97+
interrupted.set(true);
98+
break;
99+
}
100+
} else {
101+
try {
102+
Thread.sleep(25);
103+
} catch (InterruptedException ie) {
104+
Thread.currentThread().interrupt();
105+
break;
106+
}
107+
}
108+
}
109+
} catch (IOException e) {
110+
// Best-effort only; fall through.
111+
}
112+
});
113+
inputThread.setDaemon(true);
114+
inputThread.start();
115+
}
116+
System.out.println();
117+
118+
for (; seconds > 0; seconds--) {
119+
if (!noBreak && interrupted.get()) {
120+
System.out.println("\r");
121+
System.out.println();
122+
if (inputThread != null) {
123+
stopInput.set(true);
124+
try {
125+
inputThread.join(200);
126+
} catch (InterruptedException ie) {
127+
Thread.currentThread().interrupt();
128+
}
129+
}
130+
return;
131+
}
132+
133+
System.out.print(
134+
"\rWaiting for "
135+
+ seconds
136+
+ " seconds, press "
137+
+ (noBreak ? "CTRL+C to quit ..." : "enter key to continue ..."));
138+
System.out.flush();
139+
try {
140+
Thread.sleep(1000);
141+
} catch (InterruptedException e) {
142+
if (noBreak) {
143+
continue;
144+
}
145+
System.out.println();
146+
Thread.currentThread().interrupt();
147+
break;
148+
}
149+
}
150+
151+
// Normal completion: stop the input thread before draining.
152+
if (!noBreak && inputThread != null) {
153+
stopInput.set(true);
154+
try {
155+
inputThread.join(200);
156+
} catch (InterruptedException ie) {
157+
Thread.currentThread().interrupt();
158+
}
159+
}
160+
try {
161+
// Drain any remaining bytes so subsequent commands don't immediately see
162+
// leftover input. This is a best-effort drain; System.in.available() may
163+
// not be supported on all streams, but for typical console streams it helps.
164+
while (System.in.available() > 0) {
165+
System.in.read();
166+
}
167+
} catch (IOException e) {
168+
// Ignore: if we can't drain the stream it's non-fatal; any leftover input
169+
// will be handled by the next read and is acceptable.
170+
}
171+
172+
System.out.println("\r");
173+
System.out.println();
174+
}
175+
176+
@Override
177+
public String description() {
178+
return "Sets a timeout for command execution.";
179+
}
180+
181+
@Override
182+
public String usage() {
183+
return "timeout <seconds>\n"
184+
+ "timeout /t <seconds>\n"
185+
+ "timeout /t <seconds> /nobreak\n"
186+
+ "timeout /t -1";
187+
}
188+
}

0 commit comments

Comments
 (0)