Skip to content

Commit 98e5943

Browse files
committed
ConsoleService: guard against infinite loops
The design of the ConsoleArgument handlers allows each handler to remove one or more arguments from the beginning of the linked argument list, which provides for a nice sequential processing sequence. However, it is very easy to code a ConsoleArgument and then forget to remove any elements from the list. Previously, such a bug would cause the ConsoleService's processArgs method to never return, instead calling the same handler over and over. This change adds a rudimentary safeguard against such an occurrence: if the list of arguments is totally unchanged after a ConsoleArgument plugin has supposedly handled it, then we warn the user about the improper handling, remove the first argument from the list, and proceed. In this way, infinite loops should be a lot less likely; a bogus ConsoleArgument plugin would now need to mutate the list (e.g., add an argument rather than remove one) in order to cause trouble.
1 parent 3e33685 commit 98e5943

File tree

2 files changed

+67
-0
lines changed

2 files changed

+67
-0
lines changed

src/main/java/org/scijava/console/DefaultConsoleService.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import java.io.PrintStream;
3636
import java.util.ArrayList;
3737
import java.util.LinkedList;
38+
import java.util.List;
3839

3940
import org.scijava.Context;
4041
import org.scijava.console.OutputEvent.Source;
@@ -82,6 +83,8 @@ public void processArgs(final String... args) {
8283
argList.add(arg);
8384
}
8485

86+
final List<String> previousArgs = new ArrayList<>();
87+
8588
while (!argList.isEmpty()) {
8689
final ConsoleArgument handler = getHandler(argList);
8790
if (handler == null) {
@@ -90,7 +93,22 @@ public void processArgs(final String... args) {
9093
log.warn("Ignoring invalid argument: " + arg);
9194
continue;
9295
}
96+
97+
// keep a copy of the argument list prior to handling
98+
previousArgs.clear();
99+
previousArgs.addAll(argList);
100+
101+
// process the argument
93102
handler.handle(argList);
103+
104+
// verify that the handler did something to the list;
105+
// this guards against bugs which would cause infinite loops
106+
if (sameElements(previousArgs, argList)) {
107+
// skip improperly handled argument
108+
final String arg = argList.removeFirst();
109+
log.warn("Plugin '" + handler.getClass().getName() +
110+
"' failed to handle argument: " + arg);
111+
}
94112
}
95113
}
96114

@@ -174,6 +192,23 @@ private MultiPrintStream multiPrintStream(final PrintStream ps) {
174192
return new MultiPrintStream(ps);
175193
}
176194

195+
/**
196+
* Gets whether two lists have exactly the same elements in them.
197+
* <p>
198+
* We cannot use {@link List#equals(Object)} because want to check for
199+
* identical references, not per-element object equality.
200+
* </p>
201+
*/
202+
private boolean sameElements(final List<String> l1,
203+
final List<String> l2)
204+
{
205+
if (l1.size() != l2.size()) return false;
206+
for (int i = 0; i < l1.size(); i++) {
207+
if (l1.get(i) != l2.get(i)) return false;
208+
}
209+
return true;
210+
}
211+
177212
// -- Helper classes --
178213

179214
/**

src/test/java/org/scijava/console/ConsoleServiceTest.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
package org.scijava.console;
3333

3434
import static org.junit.Assert.assertEquals;
35+
import static org.junit.Assert.assertFalse;
3536
import static org.junit.Assert.assertNotNull;
3637
import static org.junit.Assert.assertTrue;
3738

@@ -72,10 +73,23 @@ public void tearDown() {
7273
/** Tests {@link ConsoleService#processArgs(String...)}. */
7374
@Test
7475
public void testProcessArgs() {
76+
assertFalse(consoleService.getInstance(FooArgument.class).argsHandled);
7577
consoleService.processArgs("--foo", "--bar");
7678
assertTrue(consoleService.getInstance(FooArgument.class).argsHandled);
7779
}
7880

81+
/**
82+
* Tests that {@link ConsoleService#processArgs(String...)} does not result in
83+
* an infinite loop when a buggy {@link ConsoleArgument} forgets to remove its
84+
* handled argument from the list.
85+
*/
86+
@Test
87+
public void testInfiniteLoopAvoidance() {
88+
assertFalse(consoleService.getInstance(BrokenArgument.class).argsHandled);
89+
consoleService.processArgs("--broken");
90+
assertTrue(consoleService.getInstance(BrokenArgument.class).argsHandled);
91+
}
92+
7993
/**
8094
* Tests the {@link OutputListener}-related API:
8195
* <ul>
@@ -225,6 +239,24 @@ public void handle(final LinkedList<String> args) {
225239
}
226240
}
227241

242+
@Plugin(type = ConsoleArgument.class)
243+
public static class BrokenArgument extends AbstractConsoleArgument {
244+
245+
private boolean argsHandled;
246+
247+
public BrokenArgument() {
248+
super(1, "--broken");
249+
}
250+
251+
@Override
252+
public void handle(final LinkedList<String> args) {
253+
// NB: Does not shorten the list. This is an intentional "bug" which
254+
// would naively result in an infinite loop. We want to test that
255+
// the ConsoleService is at least minorly resilient to this problem.
256+
argsHandled = true;
257+
}
258+
}
259+
228260
private static class OutputTracker implements OutputListener {
229261

230262
private final Collection<OutputEvent> events;

0 commit comments

Comments
 (0)