diff --git a/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/ConsoleIOContext.java b/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/ConsoleIOContext.java index c5b3dae2d58..f32d2bb3f0b 100644 --- a/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/ConsoleIOContext.java +++ b/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/ConsoleIOContext.java @@ -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 diff --git a/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/IOContext.java b/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/IOContext.java index aa1f840c9c3..e6d43bb346e 100644 --- a/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/IOContext.java +++ b/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/IOContext.java @@ -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) {} diff --git a/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/JShellTool.java b/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/JShellTool.java index 86a84953c80..795321253fd 100644 --- a/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/JShellTool.java +++ b/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/JShellTool.java @@ -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; diff --git a/src/jdk.jshell/share/classes/jdk/jshell/execution/impl/ConsoleImpl.java b/src/jdk.jshell/share/classes/jdk/jshell/execution/impl/ConsoleImpl.java index 5f4defc8afd..03f578746af 100644 --- a/src/jdk.jshell/share/classes/jdk/jshell/execution/impl/ConsoleImpl.java +++ b/src/jdk.jshell/share/classes/jdk/jshell/execution/impl/ConsoleImpl.java @@ -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); diff --git a/test/langtools/jdk/jshell/InputUITest.java b/test/langtools/jdk/jshell/InputUITest.java index d46c88aa797..f099932974d 100644 --- a/test/langtools/jdk/jshell/InputUITest.java +++ b/test/langtools/jdk/jshell/InputUITest.java @@ -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); + } + } } \ No newline at end of file diff --git a/test/langtools/jdk/jshell/UITesting.java b/test/langtools/jdk/jshell/UITesting.java index 70a1b12a943..b7bd9700d93 100644 --- a/test/langtools/jdk/jshell/UITesting.java +++ b/test/langtools/jdk/jshell/UITesting.java @@ -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();