8358552: EndOfFileException in System.in.read() and IO.readln() etc. in JShell

Reviewed-by: jlahoda
This commit is contained in:
Christian Stein 2025-07-07 08:59:50 +00:00
parent 1fa772e814
commit 9449fea2cd
6 changed files with 91 additions and 32 deletions

View File

@ -843,7 +843,7 @@ class ConsoleIOContext extends IOContext {
public void beforeUserCode() {
synchronized (this) {
pendingBytes = null;
pendingLine = null;
pendingLineCharacters = null;
}
input.setState(State.BUFFER);
}
@ -977,26 +977,32 @@ class ConsoleIOContext extends IOContext {
private static final Charset stdinCharset =
Charset.forName(System.getProperty("stdin.encoding"),
Charset.defaultCharset());
private String pendingLine;
private int pendingLinePointer;
private char[] pendingLineCharacters;
private int pendingLineCharactersPointer;
private byte[] pendingBytes;
private int pendingBytesPointer;
@Override
public synchronized int readUserInput() throws IOException {
if (pendingBytes == null || pendingBytes.length <= pendingBytesPointer) {
char userChar = readUserInputChar();
int userCharInput = readUserInputChar();
if (userCharInput == (-1)) {
return -1;
}
char userChar = (char) userCharInput;
StringBuilder dataToConvert = new StringBuilder();
dataToConvert.append(userChar);
if (Character.isHighSurrogate(userChar)) {
//surrogates cannot be converted independently,
//read the low surrogate and append it to dataToConvert:
char lowSurrogate = readUserInputChar();
if (Character.isLowSurrogate(lowSurrogate)) {
dataToConvert.append(lowSurrogate);
int lowSurrogateInput = readUserInputChar();
if (lowSurrogateInput == (-1)) {
//end of input, ignore at this stage
} else if (Character.isLowSurrogate((char) lowSurrogateInput)) {
dataToConvert.append((char) lowSurrogateInput);
} else {
//if not the low surrogate, rollback the reading of the character:
pendingLinePointer--;
pendingLineCharactersPointer--;
}
}
pendingBytes = dataToConvert.toString().getBytes(stdinCharset);
@ -1006,19 +1012,32 @@ class ConsoleIOContext extends IOContext {
}
@Override
public synchronized char readUserInputChar() throws IOException {
while (pendingLine == null || pendingLine.length() <= pendingLinePointer) {
pendingLine = doReadUserLine("", null) + System.getProperty("line.separator");
pendingLinePointer = 0;
public synchronized int readUserInputChar() throws IOException {
if (pendingLineCharacters != null && pendingLineCharacters.length == 0) {
return -1;
}
return pendingLine.charAt(pendingLinePointer++);
while (pendingLineCharacters == null || pendingLineCharacters.length <= pendingLineCharactersPointer) {
String readLine = doReadUserLine("", null);
if (readLine == null) {
pendingLineCharacters = new char[0];
return -1;
} else {
pendingLineCharacters = (readLine + System.getProperty("line.separator")).toCharArray();
}
pendingLineCharactersPointer = 0;
}
return pendingLineCharacters[pendingLineCharactersPointer++];
}
@Override
public synchronized String readUserLine(String prompt) throws IOException {
//TODO: correct behavior w.r.t. pre-read stuff?
if (pendingLine != null && pendingLine.length() > pendingLinePointer) {
return pendingLine.substring(pendingLinePointer);
if (pendingLineCharacters != null && pendingLineCharacters.length > pendingLineCharactersPointer) {
String result = new String(pendingLineCharacters,
pendingLineCharactersPointer,
pendingLineCharacters.length - pendingLineCharactersPointer);
pendingLineCharacters = null;
return result;
}
return doReadUserLine(prompt, null);
}
@ -1041,6 +1060,8 @@ class ConsoleIOContext extends IOContext {
return in.readLine(prompt.replace("%", "%%"), mask);
} catch (UserInterruptException ex) {
throw new InterruptedIOException();
} catch (EndOfFileException ex) {
return null; // Signal that Ctrl+D or similar happened
} finally {
in.setParser(prevParser);
in.setHistory(prevHistory);
@ -1051,7 +1072,11 @@ class ConsoleIOContext extends IOContext {
public char[] readPassword(String prompt) throws IOException {
//TODO: correct behavior w.r.t. pre-read stuff?
return doReadUserLine(prompt, '\0').toCharArray();
String line = doReadUserLine(prompt, '\0');
if (line == null) {
return null;
}
return line.toCharArray();
}
@Override

View File

@ -28,7 +28,6 @@ package jdk.internal.jshell.tool;
import java.io.IOException;
import java.io.Writer;
import java.nio.charset.Charset;
import jdk.internal.org.jline.reader.UserInterruptException;
/**
* Interface for defining user interaction with the shell.
@ -59,18 +58,18 @@ abstract class IOContext implements AutoCloseable {
public abstract int readUserInput() throws IOException;
public char readUserInputChar() throws IOException {
throw new UserInterruptException("");
public int readUserInputChar() throws IOException {
return -1;
}
public String readUserLine(String prompt) throws IOException {
userOutput().write(prompt);
userOutput().flush();
throw new UserInterruptException("");
return null;
}
public String readUserLine() throws IOException {
throw new UserInterruptException("");
return null;
}
public Writer userOutput() {
@ -80,7 +79,7 @@ abstract class IOContext implements AutoCloseable {
public char[] readPassword(String prompt) throws IOException {
userOutput().write(prompt);
userOutput().flush();
throw new UserInterruptException("");
return null;
}
public void setIndent(int indent) {}

View File

@ -4121,7 +4121,11 @@ public class JShellTool implements MessageHandler {
public int read(char[] cbuf, int off, int len) throws IOException {
if (len == 0) return 0;
try {
cbuf[off] = input.readUserInputChar();
int r = input.readUserInputChar();
if (r == (-1)) {
return -1;
}
cbuf[off] = (char) r;
return 1;
} catch (UserInterruptException ex) {
return -1;

View File

@ -182,9 +182,18 @@ public class ConsoleImpl {
reader = new Reader() {
@Override
public int read(char[] cbuf, int off, int len) throws IOException {
if (len == 0) {
return 0;
}
return sendAndReceive(() -> {
remoteInput.write(Task.READ_CHARS.ordinal());
return readChars(cbuf, off, len);
int r = readInt();
if (r == (-1)) {
return -1;
} else {
cbuf[off] = (char) r;
return 1;
}
});
}
@ -374,13 +383,9 @@ public class ConsoleImpl {
bp = 0;
}
case READ_CHARS -> {
if (bp >= 5) {
int len = readInt(1);
int c = console.reader().read();
//XXX: EOF handling!
sendChars(sinkOutput, new char[] {(char) c}, 0, 1);
bp = 0;
}
int c = console.reader().read();
sendInt(sinkOutput, c);
bp = 0;
}
case READ_LINE -> {
char[] data = readCharsOrNull(1);

View File

@ -23,7 +23,7 @@
/*
* @test
* @bug 8356165
* @bug 8356165 8358552
* @summary Check user input works properly
* @modules
* jdk.compiler/com.sun.tools.javac.api
@ -38,6 +38,7 @@
* @run testng/othervm -Dstderr.encoding=UTF-8 -Dstdin.encoding=UTF-8 -Dstdout.encoding=UTF-8 InputUITest
*/
import java.util.Map;
import java.util.function.Function;
import org.testng.annotations.Test;
@ -67,4 +68,21 @@ public class InputUITest extends UITesting {
}, false);
}
public void testCloseInputSinkWhileReadingUserInputSimulatingCtrlD() throws Exception {
var snippets = Map.of(
"System.in.read()", " ==> -1",
"System.console().reader().read()", " ==> -1",
"System.console().readLine()", " ==> null",
"System.console().readPassword()", " ==> null",
"IO.readln()", " ==> null",
"System.in.readAllBytes()", " ==> byte[0] { }"
);
for (var snippet : snippets.entrySet()) {
doRunTest((inputSink, out) -> {
inputSink.write(snippet.getKey() + "\n");
inputSink.close(); // Does not work: inputSink.write("\u0004"); // CTRL + D
waitOutput(out, patternQuote(snippet.getValue()), patternQuote("EndOfFileException"));
}, false);
}
}
}

View File

@ -106,11 +106,19 @@ public class UITesting {
});
Writer inputSink = new OutputStreamWriter(input.createOutput(), StandardCharsets.UTF_8) {
boolean closed = false;
@Override
public void write(String str) throws IOException {
if (closed) return; // prevents exception thrown due to closed writer
super.write(str);
flush();
}
@Override
public void close() throws IOException {
super.close();
closed = true;
}
};
runner.start();