8167461: jshell tool: Scanner#next() hangs tool
PipeInputStream.read(byte[]...) should only read available bytes; properly resending exceptions for snippet's System.in and properly closing it; more reliable way to cancel user input while waiting in System.in. Reviewed-by: rfield
This commit is contained in:
parent
5f4ad59aee
commit
7ac8bf6c75
@ -31,6 +31,7 @@ import jdk.jshell.SourceCodeAnalysis.Suggestion;
|
||||
import java.awt.event.ActionListener;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InterruptedIOException;
|
||||
import java.io.PrintStream;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.lang.reflect.Method;
|
||||
@ -390,7 +391,7 @@ class ConsoleIOContext extends IOContext {
|
||||
private int inputBytesPointer;
|
||||
|
||||
@Override
|
||||
public synchronized int readUserInput() {
|
||||
public synchronized int readUserInput() throws IOException {
|
||||
while (inputBytes == null || inputBytes.length <= inputBytesPointer) {
|
||||
boolean prevHandleUserInterrupt = in.getHandleUserInterrupt();
|
||||
History prevHistory = in.getHistory();
|
||||
@ -401,12 +402,8 @@ class ConsoleIOContext extends IOContext {
|
||||
in.setHistory(userInputHistory);
|
||||
inputBytes = (in.readLine("") + System.getProperty("line.separator")).getBytes();
|
||||
inputBytesPointer = 0;
|
||||
} catch (IOException ex) {
|
||||
ex.printStackTrace();
|
||||
return -1;
|
||||
} catch (UserInterruptException ex) {
|
||||
repl.state.stop();
|
||||
return -1;
|
||||
throw new InterruptedIOException();
|
||||
} finally {
|
||||
in.setHistory(prevHistory);
|
||||
in.setHandleUserInterrupt(prevHandleUserInterrupt);
|
||||
|
@ -54,7 +54,7 @@ abstract class IOContext implements AutoCloseable {
|
||||
|
||||
public abstract void replaceLastHistoryEntry(String source);
|
||||
|
||||
public abstract int readUserInput();
|
||||
public abstract int readUserInput() throws IOException;
|
||||
|
||||
class InputInterruptedException extends Exception {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
@ -28,6 +28,7 @@ package jdk.jshell;
|
||||
import jdk.jshell.spi.ExecutionControl;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.io.InterruptedIOException;
|
||||
import java.io.PrintStream;
|
||||
import java.text.MessageFormat;
|
||||
import java.util.ArrayList;
|
||||
@ -167,6 +168,10 @@ public class JShell implements AutoCloseable {
|
||||
* user input cannot use {@code System.in} as the input stream for
|
||||
* the remote process.
|
||||
* <p>
|
||||
* The {@code read} method of the {@code InputStream} may throw the {@link InterruptedIOException}
|
||||
* to signal the user canceled the input. The currently running snippet will be automatically
|
||||
* {@link JShell#stop() stopped}.
|
||||
* <p>
|
||||
* The default, if this is not set, is to provide an empty input stream
|
||||
* -- {@code new ByteArrayInputStream(new byte[0])}.
|
||||
*
|
||||
|
@ -42,7 +42,7 @@ class PipeInputStream extends InputStream {
|
||||
|
||||
@Override
|
||||
public synchronized int read() throws IOException {
|
||||
if (start == end) {
|
||||
if (start == end && !closed) {
|
||||
inputNeeded();
|
||||
}
|
||||
while (start == end) {
|
||||
@ -62,6 +62,32 @@ class PipeInputStream extends InputStream {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized int read(byte[] b, int off, int len) throws IOException {
|
||||
if (b == null) {
|
||||
throw new NullPointerException();
|
||||
} else if (off < 0 || len < 0 || len > b.length - off) {
|
||||
throw new IndexOutOfBoundsException();
|
||||
} else if (len == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int c = read();
|
||||
if (c == -1) {
|
||||
return -1;
|
||||
}
|
||||
b[off] = (byte)c;
|
||||
|
||||
int totalRead = 1;
|
||||
while (totalRead < len && start != end) {
|
||||
int r = read();
|
||||
if (r == (-1))
|
||||
break;
|
||||
b[off + totalRead++] = (byte) r;
|
||||
}
|
||||
return totalRead;
|
||||
}
|
||||
|
||||
protected void inputNeeded() throws IOException {}
|
||||
|
||||
private synchronized void write(int b) {
|
||||
|
@ -28,6 +28,7 @@ import jdk.jshell.spi.ExecutionEnv;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InterruptedIOException;
|
||||
import java.io.ObjectInput;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.io.ObjectOutput;
|
||||
@ -42,6 +43,7 @@ import java.util.function.Consumer;
|
||||
|
||||
import com.sun.jdi.VirtualMachine;
|
||||
import jdk.jshell.spi.ExecutionControl;
|
||||
import jdk.jshell.spi.ExecutionControl.ExecutionControlException;
|
||||
|
||||
|
||||
/**
|
||||
@ -54,6 +56,10 @@ import jdk.jshell.spi.ExecutionControl;
|
||||
*/
|
||||
public class Util {
|
||||
|
||||
private static final int TAG_DATA = 0;
|
||||
private static final int TAG_CLOSED = 1;
|
||||
private static final int TAG_EXCEPTION = 2;
|
||||
|
||||
// never instanciated
|
||||
private Util() {}
|
||||
|
||||
@ -131,6 +137,25 @@ public class Util {
|
||||
inputSignal.write('1');
|
||||
inputSignal.flush();
|
||||
}
|
||||
@Override
|
||||
public synchronized int read() throws IOException {
|
||||
int tag = super.read();
|
||||
switch (tag) {
|
||||
case TAG_DATA: return super.read();
|
||||
case TAG_CLOSED: close(); return -1;
|
||||
case TAG_EXCEPTION:
|
||||
int len = (super.read() << 0) + (super.read() << 8) + (super.read() << 16) + (super.read() << 24);
|
||||
byte[] message = new byte[len];
|
||||
for (int i = 0; i < len; i++) {
|
||||
message[i] = (byte) super.read();
|
||||
}
|
||||
throw new IOException(new String(message, "UTF-8"));
|
||||
case -1:
|
||||
return -1;
|
||||
default:
|
||||
throw new IOException("Internal error: unrecognized message tag: " + tag);
|
||||
}
|
||||
}
|
||||
};
|
||||
inputs.put(e.getKey(), inputPipe.createOutput());
|
||||
e.getValue().accept(inputPipe);
|
||||
@ -163,6 +188,7 @@ public class Util {
|
||||
public static ExecutionControl remoteInputOutput(InputStream input, OutputStream output,
|
||||
Map<String, OutputStream> outputStreamMap, Map<String, InputStream> inputStreamMap,
|
||||
BiFunction<ObjectInput, ObjectOutput, ExecutionControl> factory) throws IOException {
|
||||
ExecutionControl[] result = new ExecutionControl[1];
|
||||
Map<String, OutputStream> augmentedStreamMap = new HashMap<>(outputStreamMap);
|
||||
ObjectOutput commandOut = new ObjectOutputStream(Util.multiplexingOutputStream("$command", output));
|
||||
for (Entry<String, InputStream> e : inputStreamMap.entrySet()) {
|
||||
@ -172,7 +198,28 @@ public class Util {
|
||||
@Override
|
||||
public void write(int b) throws IOException {
|
||||
//value ignored, just a trigger to read from the input
|
||||
inTarget.write(in.read());
|
||||
try {
|
||||
int r = in.read();
|
||||
if (r == (-1)) {
|
||||
inTarget.write(TAG_CLOSED);
|
||||
} else {
|
||||
inTarget.write(new byte[] {TAG_DATA, (byte) r});
|
||||
}
|
||||
} catch (InterruptedIOException exc) {
|
||||
try {
|
||||
result[0].stop();
|
||||
} catch (ExecutionControlException ex) {
|
||||
debug(ex, "$" + e.getKey() + "-input-requested.write");
|
||||
}
|
||||
} catch (IOException exc) {
|
||||
byte[] message = exc.getMessage().getBytes("UTF-8");
|
||||
inTarget.write(TAG_EXCEPTION);
|
||||
inTarget.write((message.length >> 0) & 0xFF);
|
||||
inTarget.write((message.length >> 8) & 0xFF);
|
||||
inTarget.write((message.length >> 16) & 0xFF);
|
||||
inTarget.write((message.length >> 24) & 0xFF);
|
||||
inTarget.write(message);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -180,7 +227,7 @@ public class Util {
|
||||
OutputStream commandInTarget = commandIn.createOutput();
|
||||
augmentedStreamMap.put("$command", commandInTarget);
|
||||
new DemultiplexInput(input, augmentedStreamMap, Arrays.asList(commandInTarget)).start();
|
||||
return factory.apply(new ObjectInputStream(commandIn), commandOut);
|
||||
return result[0] = factory.apply(new ObjectInputStream(commandIn), commandOut);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -198,4 +245,13 @@ public class Util {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a serious unexpected internal exception.
|
||||
*
|
||||
* @param ex the exception
|
||||
* @param where a description of the context of the exception
|
||||
*/
|
||||
private static void debug(Throwable ex, String where) {
|
||||
// Reserved for future logging
|
||||
}
|
||||
}
|
||||
|
@ -21,7 +21,10 @@
|
||||
* questions.
|
||||
*/
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.PrintStream;
|
||||
import java.io.StringWriter;
|
||||
import java.lang.reflect.Method;
|
||||
@ -83,7 +86,7 @@ public class KullaTesting {
|
||||
|
||||
private SourceCodeAnalysis analysis = null;
|
||||
private JShell state = null;
|
||||
private TestingInputStream inStream = null;
|
||||
private InputStream inStream = null;
|
||||
private ByteArrayOutputStream outStream = null;
|
||||
private ByteArrayOutputStream errStream = null;
|
||||
|
||||
@ -106,7 +109,11 @@ public class KullaTesting {
|
||||
}
|
||||
|
||||
public void setInput(String s) {
|
||||
inStream.setInput(s);
|
||||
setInput(new ByteArrayInputStream(s.getBytes()));
|
||||
}
|
||||
|
||||
public void setInput(InputStream in) {
|
||||
inStream = in;
|
||||
}
|
||||
|
||||
public String getOutput() {
|
||||
@ -159,11 +166,27 @@ public class KullaTesting {
|
||||
}
|
||||
|
||||
public void setUp(Consumer<JShell.Builder> bc) {
|
||||
inStream = new TestingInputStream();
|
||||
InputStream in = new InputStream() {
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
assertNotNull(inStream);
|
||||
return inStream.read();
|
||||
}
|
||||
@Override
|
||||
public int read(byte[] b) throws IOException {
|
||||
assertNotNull(inStream);
|
||||
return inStream.read(b);
|
||||
}
|
||||
@Override
|
||||
public int read(byte[] b, int off, int len) throws IOException {
|
||||
assertNotNull(inStream);
|
||||
return inStream.read(b, off, len);
|
||||
}
|
||||
};
|
||||
outStream = new ByteArrayOutputStream();
|
||||
errStream = new ByteArrayOutputStream();
|
||||
JShell.Builder builder = JShell.builder()
|
||||
.in(inStream)
|
||||
.in(in)
|
||||
.out(new PrintStream(outStream))
|
||||
.err(new PrintStream(errStream));
|
||||
bc.accept(builder);
|
||||
|
71
langtools/test/jdk/jshell/PipeInputStreamTest.java
Normal file
71
langtools/test/jdk/jshell/PipeInputStreamTest.java
Normal file
@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved.
|
||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||
*
|
||||
* This code is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License version 2 only, as
|
||||
* published by the Free Software Foundation.
|
||||
*
|
||||
* This code is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||
* version 2 for more details (a copy is included in the LICENSE file that
|
||||
* accompanied this code).
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License version
|
||||
* 2 along with this work; if not, write to the Free Software Foundation,
|
||||
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*
|
||||
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
|
||||
* or visit www.oracle.com if you need additional information or have any
|
||||
* questions.
|
||||
*/
|
||||
|
||||
/*
|
||||
* @test 8167461
|
||||
* @summary Verify PipeInputStream works.
|
||||
* @modules jdk.compiler/com.sun.tools.javac.util
|
||||
* jdk.jshell
|
||||
* @run testng PipeInputStreamTest
|
||||
*/
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import org.testng.annotations.Test;
|
||||
|
||||
import com.sun.tools.javac.util.Pair;
|
||||
|
||||
import static org.testng.Assert.*;
|
||||
|
||||
@Test
|
||||
public class PipeInputStreamTest {
|
||||
|
||||
public void testReadArrayNotBlocking() throws Exception {
|
||||
Pair<InputStream, OutputStream> streams = createPipeStream();
|
||||
InputStream in = streams.fst;
|
||||
OutputStream out = streams.snd;
|
||||
out.write('a');
|
||||
byte[] data = new byte[12];
|
||||
assertEquals(in.read(data), 1);
|
||||
assertEquals(data[0], 'a');
|
||||
out.write('a'); out.write('b'); out.write('c');
|
||||
assertEquals(in.read(data), 3);
|
||||
assertEquals(data[0], 'a');
|
||||
assertEquals(data[1], 'b');
|
||||
assertEquals(data[2], 'c');
|
||||
}
|
||||
|
||||
private Pair<InputStream, OutputStream> createPipeStream() throws Exception {
|
||||
Class<?> pipeStreamClass = Class.forName("jdk.jshell.execution.PipeInputStream");
|
||||
Constructor<?> c = pipeStreamClass.getDeclaredConstructor();
|
||||
c.setAccessible(true);
|
||||
Object pipeStream = c.newInstance();
|
||||
Method createOutputStream = pipeStreamClass.getDeclaredMethod("createOutput");
|
||||
createOutputStream.setAccessible(true);
|
||||
return Pair.of((InputStream) pipeStream, (OutputStream) createOutputStream.invoke(pipeStream));
|
||||
}
|
||||
|
||||
}
|
@ -23,12 +23,15 @@
|
||||
|
||||
/*
|
||||
* @test
|
||||
* @bug 8131023
|
||||
* @bug 8131023 8167461
|
||||
* @summary Verify that the user's code can read System.in
|
||||
* @build KullaTesting TestingInputStream
|
||||
* @run testng UserInputTest
|
||||
*/
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
import org.testng.annotations.Test;
|
||||
|
||||
@Test
|
||||
@ -37,8 +40,61 @@ public class UserInputTest extends KullaTesting {
|
||||
public void testReadInput() {
|
||||
setInput("AB\n");
|
||||
assertEval("System.in.read()", "65");
|
||||
setInput("BC\n");
|
||||
assertEval("System.in.read()", "66");
|
||||
setInput("CD\n");
|
||||
assertEval("System.in.read()", "67");
|
||||
}
|
||||
|
||||
public void testScanner() {
|
||||
assertEval("import java.util.Scanner;");
|
||||
assertEval("Scanner s = new Scanner(System.in);");
|
||||
setInput("12\n");
|
||||
assertEval("s.nextInt();", "12");
|
||||
}
|
||||
|
||||
public void testClose() {
|
||||
setInput(new InputStream() {
|
||||
private final byte[] data = new byte[] {0, 1, 2};
|
||||
private int cursor;
|
||||
@Override public int read() throws IOException {
|
||||
if (cursor < data.length) {
|
||||
return data[cursor++];
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
});
|
||||
assertEval("int read;", "0");
|
||||
assertEval("System.in.read();", "0");
|
||||
assertEval("System.in.read();", "1");
|
||||
assertEval("System.in.read();", "2");
|
||||
assertEval("System.in.read();", "-1");
|
||||
assertEval("System.in.read();", "-1");
|
||||
assertEval("System.in.read();", "-1");
|
||||
}
|
||||
|
||||
public void testException() {
|
||||
setInput(new InputStream() {
|
||||
private final int[] data = new int[] {0, 1, -2, 2};
|
||||
private int cursor;
|
||||
@Override public int read() throws IOException {
|
||||
if (cursor < data.length) {
|
||||
int d = data[cursor++];
|
||||
if (d == (-2)) {
|
||||
throw new IOException("Crashed");
|
||||
}
|
||||
return d;
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
});
|
||||
assertEval("int read;", "0");
|
||||
assertEval("System.in.read();", "0");
|
||||
assertEval("System.in.read();", "1");
|
||||
assertEval("java.io.IOException e;");
|
||||
assertEval("try { System.in.read(); } catch (java.io.IOException exc) { e = exc; }");
|
||||
assertEval("e", "java.io.IOException: Crashed");
|
||||
assertEval("System.in.read();", "2");
|
||||
assertEval("System.in.read();", "-1");
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user