From 2fb2f00abb440cf654d24ff26e7867649a31747d Mon Sep 17 00:00:00 2001 From: Jan Lahoda Date: Mon, 24 Mar 2025 10:56:49 +0100 Subject: [PATCH 1/6] 8352693: Use a simpler console reader instead of JLine for System.console() --- make/modules/jdk.internal.le/Lib.gmk | 45 ++ .../jdk/internal/io/BaseJdkConsoleImpl.java | 204 +++++++++ .../jdk/internal/io/JdkConsoleImpl.java | 247 +++-------- src/java.base/share/classes/module-info.java | 4 +- .../jdk/internal/console/JdkConsoleImpl.java | 86 ++++ .../console/JdkConsoleProviderImpl.java | 39 ++ .../internal/console/LastErrorException.java | 36 ++ .../internal/console/SimpleConsoleReader.java | 324 +++++++++++++++ .../org/jline/JdkConsoleProviderImpl.java | 308 -------------- .../share/classes/module-info.java | 2 +- .../internal/console/NativeConsoleReader.java | 63 +++ .../unix/native/lible/NativeConsoleReader.cpp | 110 +++++ .../internal/console/NativeConsoleReader.java | 49 +++ .../jdk/internal/console/WindowsTerminal.java | 219 ++++++++++ .../windows/native/lible/WindowsTerminal.cpp | 193 +++++++++ .../java/io/Console/ConsolePromptTest.java | 75 ++-- test/jdk/java/io/Console/LocaleTest.java | 1 - test/jdk/java/io/Console/consolePrompt.exp | 30 ++ .../io/JdkConsoleImplConsoleTest.java | 393 ++++++++++++++++++ .../jline/AbstractWindowsTerminalTest.java | 3 +- .../jline/JLineConsoleProviderTest.java | 4 +- .../jline/LazyJdkConsoleProvider.java | 97 ----- 22 files changed, 1902 insertions(+), 630 deletions(-) create mode 100644 make/modules/jdk.internal.le/Lib.gmk create mode 100644 src/java.base/share/classes/jdk/internal/io/BaseJdkConsoleImpl.java create mode 100644 src/jdk.internal.le/share/classes/jdk/internal/console/JdkConsoleImpl.java create mode 100644 src/jdk.internal.le/share/classes/jdk/internal/console/JdkConsoleProviderImpl.java create mode 100644 src/jdk.internal.le/share/classes/jdk/internal/console/LastErrorException.java create mode 100644 src/jdk.internal.le/share/classes/jdk/internal/console/SimpleConsoleReader.java delete mode 100644 src/jdk.internal.le/share/classes/jdk/internal/org/jline/JdkConsoleProviderImpl.java create mode 100644 src/jdk.internal.le/unix/classes/jdk/internal/console/NativeConsoleReader.java create mode 100644 src/jdk.internal.le/unix/native/lible/NativeConsoleReader.cpp create mode 100644 src/jdk.internal.le/windows/classes/jdk/internal/console/NativeConsoleReader.java create mode 100644 src/jdk.internal.le/windows/classes/jdk/internal/console/WindowsTerminal.java create mode 100644 src/jdk.internal.le/windows/native/lible/WindowsTerminal.cpp create mode 100644 test/jdk/java/io/Console/consolePrompt.exp create mode 100644 test/jdk/jdk/internal/io/JdkConsoleImplConsoleTest.java delete mode 100644 test/jdk/jdk/internal/jline/LazyJdkConsoleProvider.java diff --git a/make/modules/jdk.internal.le/Lib.gmk b/make/modules/jdk.internal.le/Lib.gmk new file mode 100644 index 0000000000000..6a5e9ec663f4d --- /dev/null +++ b/make/modules/jdk.internal.le/Lib.gmk @@ -0,0 +1,45 @@ +# +# Copyright (c) 2015, 2025, 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. Oracle designates this +# particular file as subject to the "Classpath" exception as provided +# by Oracle in the LICENSE file that accompanied this code. +# +# 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. +# + +include LibCommon.gmk + +################################################################################ + +ifeq ($(call isTargetOs, linux macosx windows), true) + + $(eval $(call SetupJdkLibrary, BUILD_LIBLE, \ + NAME := le, \ + TOOLCHAIN := TOOLCHAIN_LINK_CXX, \ + OPTIMIZATION := LOW, \ + JDK_LIBS := java.base:libjava, \ + LIBS_unix := $(JDKLIB_LIBS) $(LIBCXX), \ + LIBS_windows := $(JDKLIB_LIBS) user32.lib, \ + )) + + TARGETS += $(BUILD_LIBLE) + +endif + +################################################################################ diff --git a/src/java.base/share/classes/jdk/internal/io/BaseJdkConsoleImpl.java b/src/java.base/share/classes/jdk/internal/io/BaseJdkConsoleImpl.java new file mode 100644 index 0000000000000..cba512ef11284 --- /dev/null +++ b/src/java.base/share/classes/jdk/internal/io/BaseJdkConsoleImpl.java @@ -0,0 +1,204 @@ +/* + * Copyright (c) 2022, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ + +package jdk.internal.io; + +import java.io.IOError; +import java.io.IOException; +import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.PrintWriter; +import java.io.Reader; +import java.io.Writer; +import java.nio.charset.Charset; +import java.util.Formatter; +import java.util.Locale; +import java.util.Objects; + +import sun.nio.cs.StreamDecoder; +import sun.nio.cs.StreamEncoder; + +/** + * Base for JDK's JdkConsole implementations. + */ +public abstract class BaseJdkConsoleImpl implements JdkConsole { + @Override + public PrintWriter writer() { + return pw; + } + + @Override + public Reader reader() { + return reader; + } + + @Override + public JdkConsole println(Object obj) { + pw.println(obj); + // automatic flushing covers println + return this; + } + + @Override + public JdkConsole print(Object obj) { + pw.print(obj); + pw.flush(); // automatic flushing does not cover print + return this; + } + + @Override + public String readln(String prompt) { + String line = null; + synchronized (writeLock) { + synchronized(readLock) { + pw.print(prompt); + pw.flush(); // automatic flushing does not cover print + try { + char[] ca = readline(false); + if (ca != null) + line = new String(ca); + } catch (IOException x) { + throw new IOError(x); + } + } + } + return line; + } + + @Override + public String readln() { + String line = null; + synchronized(readLock) { + try { + char[] ca = readline(false); + if (ca != null) + line = new String(ca); + } catch (IOException x) { + throw new IOError(x); + } + } + return line; + } + + @Override + public JdkConsole format(Locale locale, String format, Object ... args) { + formatter.format(locale, format, args).flush(); + return this; + } + + @Override + public String readLine(Locale locale, String format, Object ... args) { + String line = null; + synchronized (writeLock) { + synchronized(readLock) { + if (!format.isEmpty()) + pw.format(locale, format, args); + try { + char[] ca = readline(false); + if (ca != null) + line = new String(ca); + } catch (IOException x) { + throw new IOError(x); + } + } + } + return line; + } + + @Override + public String readLine() { + return readLine(Locale.getDefault(Locale.Category.FORMAT), ""); + } + + @Override + public char[] readPassword(Locale locale, String format, Object ... args) { + char[] passwd = null; + synchronized (writeLock) { + synchronized(readLock) { + try { + if (!format.isEmpty()) + pw.format(locale, format, args); + passwd = readline(true); + } catch (IOException x) { + throw new IOError(x); + } + pw.println(); + } + } + return passwd; + } + + @Override + public char[] readPassword() { + return readPassword(Locale.getDefault(Locale.Category.FORMAT), ""); + } + + @Override + public void flush() { + pw.flush(); + } + + @Override + public Charset charset() { + return charset; + } + + protected Reader wrapReader(Reader baseReader) { + return baseReader; + } + + protected final Charset charset; + protected final Object readLock; + protected final Object writeLock; + protected final Reader reader; + protected final Writer out; + protected final PrintWriter pw; + protected final Formatter formatter; + + protected abstract char[] readline(boolean password) throws IOException; + + @SuppressWarnings("this-escape") + public BaseJdkConsoleImpl(Charset charset) { + Objects.requireNonNull(charset); + this.charset = charset; + readLock = new Object(); + writeLock = new Object(); + out = StreamEncoder.forOutputStreamWriter( + new FileOutputStream(FileDescriptor.out), + writeLock, + charset); + pw = new PrintWriter(out, true) { + public void close() { + } + }; + formatter = new Formatter(out); + StreamDecoder plainReader = StreamDecoder.forInputStreamReader( + new FileInputStream(FileDescriptor.in), + readLock, + charset); + reader = wrapReader(plainReader); + } +} diff --git a/src/java.base/share/classes/jdk/internal/io/JdkConsoleImpl.java b/src/java.base/share/classes/jdk/internal/io/JdkConsoleImpl.java index 3c0afd2005cb1..56c9393069c2c 100644 --- a/src/java.base/share/classes/jdk/internal/io/JdkConsoleImpl.java +++ b/src/java.base/share/classes/jdk/internal/io/JdkConsoleImpl.java @@ -27,164 +27,17 @@ import java.io.IOError; import java.io.IOException; -import java.io.FileDescriptor; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.PrintWriter; import java.io.Reader; -import java.io.Writer; import java.nio.charset.Charset; import java.util.Arrays; -import java.util.Formatter; -import java.util.Locale; -import java.util.Objects; import jdk.internal.access.SharedSecrets; import sun.nio.cs.StreamDecoder; -import sun.nio.cs.StreamEncoder; /** * JdkConsole implementation based on the platform's TTY. */ -public final class JdkConsoleImpl implements JdkConsole { - @Override - public PrintWriter writer() { - return pw; - } - - @Override - public Reader reader() { - return reader; - } - - @Override - public JdkConsole println(Object obj) { - pw.println(obj); - // automatic flushing covers println - return this; - } - - @Override - public JdkConsole print(Object obj) { - pw.print(obj); - pw.flush(); // automatic flushing does not cover print - return this; - } - - @Override - public String readln(String prompt) { - String line = null; - synchronized (writeLock) { - synchronized(readLock) { - pw.print(prompt); - pw.flush(); // automatic flushing does not cover print - try { - char[] ca = readline(false); - if (ca != null) - line = new String(ca); - } catch (IOException x) { - throw new IOError(x); - } - } - } - return line; - } - - @Override - public String readln() { - String line = null; - synchronized(readLock) { - try { - char[] ca = readline(false); - if (ca != null) - line = new String(ca); - } catch (IOException x) { - throw new IOError(x); - } - } - return line; - } - - @Override - public JdkConsole format(Locale locale, String format, Object ... args) { - formatter.format(locale, format, args).flush(); - return this; - } - - @Override - public String readLine(Locale locale, String format, Object ... args) { - String line = null; - synchronized (writeLock) { - synchronized(readLock) { - if (!format.isEmpty()) - pw.format(locale, format, args); - try { - char[] ca = readline(false); - if (ca != null) - line = new String(ca); - } catch (IOException x) { - throw new IOError(x); - } - } - } - return line; - } - - @Override - public String readLine() { - return readLine(Locale.getDefault(Locale.Category.FORMAT), ""); - } - - @Override - public char[] readPassword(Locale locale, String format, Object ... args) { - char[] passwd = null; - synchronized (writeLock) { - synchronized(readLock) { - installShutdownHook(); - try { - synchronized(restoreEchoLock) { - restoreEcho = echo(false); - } - } catch (IOException x) { - throw new IOError(x); - } - IOError ioe = null; - try { - if (!format.isEmpty()) - pw.format(locale, format, args); - passwd = readline(true); - } catch (IOException x) { - ioe = new IOError(x); - } finally { - try { - synchronized(restoreEchoLock) { - if (restoreEcho) { - restoreEcho = echo(true); - } - } - } catch (IOException x) { - if (ioe == null) - ioe = new IOError(x); - else - ioe.addSuppressed(x); - } - if (ioe != null) { - Arrays.fill(passwd, ' '); - try { - if (reader instanceof LineReader lr) { - lr.zeroOut(); - } - } catch (IOException _) { - // ignore - } - throw ioe; - } - } - pw.println(); - } - } - return passwd; - } +public final class JdkConsoleImpl extends BaseJdkConsoleImpl { private void installShutdownHook() { if (shutdownHookInstalled) @@ -213,35 +66,62 @@ public void run() { shutdownHookInstalled = true; } - @Override - public char[] readPassword() { - return readPassword(Locale.getDefault(Locale.Category.FORMAT), ""); - } - - @Override - public void flush() { - pw.flush(); - } - - @Override - public Charset charset() { - return charset; - } - - private final Charset charset; - private final Object readLock; - private final Object writeLock; // Must not block while holding this. It is used in the shutdown hook. private final Object restoreEchoLock; - private final Reader reader; - private final Writer out; - private final PrintWriter pw; - private final Formatter formatter; private char[] rcb; private boolean restoreEcho; private boolean shutdownHookInstalled; - private char[] readline(boolean zeroOut) throws IOException { + protected char[] readline(boolean password) throws IOException { + if (!password) { + return doReadline(password); + } + + //reading password, disable echo, and ensure it is re-enabled: + installShutdownHook(); + try { + synchronized(restoreEchoLock) { + restoreEcho = echo(false); + } + } catch (IOException x) { + throw new IOError(x); + } + IOError ioe = null; + char[] passwd = null; + try { + passwd = doReadline(true); + } catch (IOException x) { + ioe = new IOError(x); + } finally { + try { + synchronized(restoreEchoLock) { + if (restoreEcho) { + restoreEcho = echo(true); + } + } + } catch (IOException x) { + if (ioe == null) + ioe = new IOError(x); + else + ioe.addSuppressed(x); + } + if (ioe != null) { + Arrays.fill(passwd, ' '); + try { + if (reader instanceof LineReader lr) { + lr.zeroOut(); + } + } catch (IOException _) { + // ignore + } + throw ioe; + } + } + + return passwd; + } + + private char[] doReadline(boolean password) throws IOException { int len = reader.read(rcb, 0, rcb.length); if (len < 0) return null; //EOL @@ -255,7 +135,7 @@ else if (rcb[len-1] == '\n') { char[] b = new char[len]; if (len > 0) { System.arraycopy(rcb, 0, b, 0, len); - if (zeroOut) { + if (password) { Arrays.fill(rcb, 0, len, ' '); if (reader instanceof LineReader lr) { lr.zeroOut(); @@ -398,25 +278,14 @@ public int read(char[] cbuf, int offset, int length) } } + @Override + protected Reader wrapReader(Reader baseReader) { + return new LineReader(baseReader); + } + public JdkConsoleImpl(Charset charset) { - Objects.requireNonNull(charset); - this.charset = charset; - readLock = new Object(); - writeLock = new Object(); + super(charset); restoreEchoLock = new Object(); - out = StreamEncoder.forOutputStreamWriter( - new FileOutputStream(FileDescriptor.out), - writeLock, - charset); - pw = new PrintWriter(out, true) { - public void close() { - } - }; - formatter = new Formatter(out); - reader = new LineReader(StreamDecoder.forInputStreamReader( - new FileInputStream(FileDescriptor.in), - readLock, - charset)); rcb = new char[1024]; } } diff --git a/src/java.base/share/classes/module-info.java b/src/java.base/share/classes/module-info.java index 22f40d9cead1d..e38672e302ed5 100644 --- a/src/java.base/share/classes/module-info.java +++ b/src/java.base/share/classes/module-info.java @@ -169,6 +169,7 @@ java.management, java.rmi, jdk.charsets, + jdk.internal.le, jdk.jartool, jdk.jlink, jdk.jfr, @@ -299,7 +300,8 @@ jdk.net, jdk.sctp; exports sun.nio.cs to - jdk.charsets; + jdk.charsets, + jdk.internal.le; exports sun.nio.fs to jdk.net; exports sun.reflect.annotation to diff --git a/src/jdk.internal.le/share/classes/jdk/internal/console/JdkConsoleImpl.java b/src/jdk.internal.le/share/classes/jdk/internal/console/JdkConsoleImpl.java new file mode 100644 index 0000000000000..14d72834f771d --- /dev/null +++ b/src/jdk.internal.le/share/classes/jdk/internal/console/JdkConsoleImpl.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2022, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ + +package jdk.internal.console; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.Locale; +import jdk.internal.console.SimpleConsoleReader.CleanableBuffer; + +import jdk.internal.io.BaseJdkConsoleImpl; + +/** + * JdkConsole implementation based on the platform's TTY, with basic keyboard navigation. + */ +public final class JdkConsoleImpl extends BaseJdkConsoleImpl { + + private final boolean isTTY; + + @Override + public char[] readPassword() { + return readPassword(Locale.getDefault(Locale.Category.FORMAT), ""); + } + + @Override + public void flush() { + pw.flush(); + } + + @Override + public Charset charset() { + return charset; + } + + protected char[] readline(boolean password) throws IOException { + if (isTTY) { + return NativeConsoleReader.readline(reader, out, password); + } else { + //dumb input: + CleanableBuffer buffer = new CleanableBuffer(); + + try { + int r; + + OUTER: while ((r = reader.read()) != (-1)) { + switch (r) { + case '\r', '\n' -> { break OUTER; } + default -> buffer.insert(buffer.length(), (char) r); + } + } + + return buffer.getData(); + } finally { + buffer.zeroOut(); + } + } + } + + public JdkConsoleImpl(boolean isTTY, Charset charset) { + super(charset); + this.isTTY = isTTY; + } + +} diff --git a/src/jdk.internal.le/share/classes/jdk/internal/console/JdkConsoleProviderImpl.java b/src/jdk.internal.le/share/classes/jdk/internal/console/JdkConsoleProviderImpl.java new file mode 100644 index 0000000000000..020d7d5fd2da5 --- /dev/null +++ b/src/jdk.internal.le/share/classes/jdk/internal/console/JdkConsoleProviderImpl.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2022, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.console; + +import java.nio.charset.Charset; +import jdk.internal.io.JdkConsole; +import jdk.internal.io.JdkConsoleProvider; + +public class JdkConsoleProviderImpl implements JdkConsoleProvider { + + @Override + public JdkConsole console(boolean isTTY, Charset charset) { + //only supported on Linux, Mac OS/X and Windows: + return new JdkConsoleImpl(isTTY, charset); + } + +} diff --git a/src/jdk.internal.le/share/classes/jdk/internal/console/LastErrorException.java b/src/jdk.internal.le/share/classes/jdk/internal/console/LastErrorException.java new file mode 100644 index 0000000000000..8dcf1ff1e0737 --- /dev/null +++ b/src/jdk.internal.le/share/classes/jdk/internal/console/LastErrorException.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2018, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.console; + +@SuppressWarnings("serial") +public class LastErrorException extends RuntimeException{ + + public final long lastError; + + public LastErrorException(long lastError) { + this.lastError = lastError; + } + +} diff --git a/src/jdk.internal.le/share/classes/jdk/internal/console/SimpleConsoleReader.java b/src/jdk.internal.le/share/classes/jdk/internal/console/SimpleConsoleReader.java new file mode 100644 index 0000000000000..0b4a27557b3b5 --- /dev/null +++ b/src/jdk.internal.le/share/classes/jdk/internal/console/SimpleConsoleReader.java @@ -0,0 +1,324 @@ +/* + * Copyright (c) 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ + +package jdk.internal.console; + +import java.io.IOException; +import java.io.Reader; +import java.io.Writer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.function.IntSupplier; +import jdk.internal.org.jline.utils.WCWidth; + +public class SimpleConsoleReader { + + //public, to simplify access from tests: + public static char[] doRead(Reader reader, Writer out, boolean password, int firstLineOffset, IntSupplier terminalWidthSupplier) throws IOException { + CleanableBuffer result = new CleanableBuffer(); + try { + doReadImpl(reader, out, password, firstLineOffset, terminalWidthSupplier, result); + return result.getData(); + } finally { + result.zeroOut(); + } + } + + private static void doReadImpl(Reader reader, Writer out, boolean password, int firstLineOffset, IntSupplier terminalWidthSupplier, CleanableBuffer result) throws IOException { + int caret = 0; + int r; + PaintState prevState = new PaintState(); + + READ: while (true) { + //paint: + if (firstLineOffset != (-1) && !password) { + prevState = repaint(out, firstLineOffset, terminalWidthSupplier, result.data, result.length, caret, prevState); + } + + //read + r = reader.read(); + switch (r) { + case -1: continue READ; + case '\n', '\r': break READ; + case 4: break READ; //EOF/Ctrl-D + case 127: + //backspace: + if (caret > 0) { + result.delete(caret - 1, caret); + caret--; + } + continue READ; + case '\033': + r = reader.read(); + switch (r) { + case '[': + r = reader.read(); + + StringBuilder firstNumber = new StringBuilder(); + + r = readNumber(reader, r, firstNumber); + + String modifier; + String key; + + switch (r) { + case '~' -> { + key = firstNumber.toString(); + modifier = "1"; + } + case ';' -> { + key = firstNumber.toString(); + + StringBuilder modifierBuilder = new StringBuilder(); + + r = readNumber(reader, reader.read(), modifierBuilder); + modifier = modifierBuilder.toString(); + + if (r == 'R') { + firstLineOffset = Integer.parseInt(modifier) - 1; + continue READ; + } + + if (r != '~') { + //TODO: unexpected, anything that can be done? + } + } + default -> { + key = Character.toString(r); + modifier = firstNumber.isEmpty() ? "1" + : firstNumber.toString(); + } + } + + if ("1".equals(modifier)) { + switch (key) { + case "C": if (caret < result.length()) caret++; break; + case "D": if (caret > 0) caret--; break; + case "1", "H": caret = 0; break; + case "4", "F": caret = result.length(); break; + case "3": + //delete + result.delete(caret, caret + 1); + continue READ; + } + } + } + continue READ; + } + + caret += result.insert(caret, (char) r); + } + + if (!password) { + //show the final state: + repaint(out, firstLineOffset, terminalWidthSupplier, result.data, result.length, caret, prevState); + } + + out.append("\n\r").flush(); + } + + private static PaintState repaint(Writer out, int firstLineOffset, IntSupplier terminalWidthSupplier, int[] toDisplay, int toDisplayLength, int caret, PaintState prevPaintState) throws IOException { + //for simplicity, repaint the whole input buffer + //for more efficiency, could compute smaller (ideally minimal) changes, + //and apply them instead of repainting everything: + record DisplayLine(int lineStartIndex, int lineLength) {} + int terminalWidth = terminalWidthSupplier.getAsInt(); + List toDisplayLines = new ArrayList<>(); + int lineOffset = firstLineOffset; + int lineStartIndex = 0; + + while (lineStartIndex < toDisplayLength) { + int currentLineColumns = terminalWidth - lineOffset; + int currentLineEnd = lineStartIndex; + + while (currentLineEnd < toDisplayLength) { + currentLineColumns -= WCWidth.wcwidth(toDisplay[currentLineEnd]); + + if (currentLineColumns < 0) { + break; + } + + currentLineEnd++; + } + + toDisplayLines.add(new DisplayLine(lineStartIndex, currentLineEnd - lineStartIndex)); + lineStartIndex = currentLineEnd; + lineOffset = 0; + } + + for (int i = prevPaintState.caretLine() + 1; i < prevPaintState.lines(); i++) { + out.append("\033[B"); + } + for (int i = prevPaintState.lines() - 1; i >= 0; i--) { + if (i == 0) { + out.append("\033[" + (firstLineOffset + 1) + "G"); + out.append("\033[K"); + } else { + out.append("\r"); + out.append("\033[K"); + out.append("\033[A"); + } + } + + char[] toPrint = new char[2]; + + for (Iterator it = toDisplayLines.iterator(); it.hasNext();) { + DisplayLine line = it.next(); + for (int o = 0; o < line.lineLength(); o++) { + int printLength = Character.toChars(toDisplay[line.lineStartIndex() + o], toPrint, 0); + + out.write(toPrint, 0, printLength); + } + + if (it.hasNext()) { + out.append("\n\r"); + } + } + + Arrays.fill(toPrint, '\0'); + + int prevCaretLine = prevPaintState.lines(); + + if (caret < toDisplayLength) { + int currentPos = toDisplayLength; + + prevCaretLine = prevPaintState.lines() - 1; + + while (caret < currentPos - toDisplayLines.get(prevCaretLine).lineLength()) { + out.append("\033[A"); + currentPos -= toDisplayLines.get(prevCaretLine).lineLength(); + prevCaretLine--; + } + + int currentLineStart = currentPos - toDisplayLines.get(prevCaretLine).lineLength(); + int linePosition = caret - currentLineStart; + + if (prevCaretLine == 0) { + linePosition += firstLineOffset; + } + + out.append("\033[" + (linePosition + 1) + "G"); + } + out.flush(); + return new PaintState(toDisplayLines.size(), prevCaretLine); + } + + private static int readNumber(Reader reader, int r, StringBuilder number) throws IOException { + while (Character.isDigit(r)) { + number.append((char) r); + r = reader.read(); + } + return r; + } + + public record Size(int width, int height) {} + record PaintState(int lines, int caretLine) { + + public PaintState() { + this(1, 0); + } + + } + + static final class CleanableBuffer { + private char pendingHighSurrogate; + private int pendingSurrogateCaret = -1; + private int[] data = new int[16]; + private int length; + + public void delete(int from, int to) { + System.arraycopy(data, to, data, from, length - to); + length--; + } + + public int length() { + return length; + } + + public int insert(int caret, char c) { + if (Character.isHighSurrogate(c)) { + if (pendingSurrogateCaret != (-1)) { + doInsert(pendingSurrogateCaret, (int) c); + } + pendingHighSurrogate = c; + pendingSurrogateCaret = caret; + return 0; + } else if (Character.isLowSurrogate(c)) { + if (pendingSurrogateCaret == (-1)) { + doInsert(caret, (int) c); + } else if (pendingSurrogateCaret != caret) { + doInsert(pendingSurrogateCaret, (int) pendingHighSurrogate); + doInsert(caret, (int) c); + } else { + doInsert(caret, Character.toCodePoint(pendingHighSurrogate, c)); + } + pendingHighSurrogate = '\0'; + pendingSurrogateCaret = -1; + return 1; + } else { + doInsert(caret, (int) c); + return 1; + } + } + + private void doInsert(int caret, int codePoint) { + while (length + 1 >= data.length) { + int[] newData = Arrays.copyOf(data, data.length * 2); + + zeroOut(); + data = newData; + } + + System.arraycopy(data, caret, data, caret + 1, length - caret); + data[caret] = codePoint; + length++; + } + + + public char[] getData() { + if (length == 0) { + return null; + } + + char[] tempResult = new char[2 * length]; + int target = 0; + + for (int source = 0; source < length; source++) { + target += Character.toChars(data[source], tempResult, target); + } + + char[] result = Arrays.copyOf(tempResult, target); + Arrays.fill(tempResult, '\0'); + + return result; + } + + public void zeroOut() { + Arrays.fill(data, 0); + } + } +} diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/JdkConsoleProviderImpl.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/JdkConsoleProviderImpl.java deleted file mode 100644 index 11de96b7fc339..0000000000000 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/JdkConsoleProviderImpl.java +++ /dev/null @@ -1,308 +0,0 @@ -/* - * Copyright (c) 2022, 2024, 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. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * 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. - */ - -package jdk.internal.org.jline; - -import java.io.IOException; -import java.io.PrintWriter; -import java.io.Reader; -import java.nio.charset.Charset; -import java.util.Locale; - -import jdk.internal.io.JdkConsole; -import jdk.internal.io.JdkConsoleProvider; -import jdk.internal.org.jline.reader.EndOfFileException; -import jdk.internal.org.jline.reader.LineReader; -import jdk.internal.org.jline.reader.LineReaderBuilder; -import jdk.internal.org.jline.terminal.Terminal; -import jdk.internal.org.jline.terminal.TerminalBuilder; -import jdk.internal.org.jline.terminal.TerminalBuilder.SystemOutput; - -/** - * JdkConsole/Provider implementations for jline - */ -public class JdkConsoleProviderImpl implements JdkConsoleProvider { - - /** - * {@inheritDoc} - */ - @Override - public JdkConsole console(boolean isTTY, Charset charset) { - return new LazyDelegatingJdkConsoleImpl(charset); - } - - private static class LazyDelegatingJdkConsoleImpl implements JdkConsole { - private final Charset charset; - private volatile boolean jlineInitialized; - private volatile JdkConsole delegate; - - public LazyDelegatingJdkConsoleImpl(Charset charset) { - this.charset = charset; - this.delegate = new jdk.internal.io.JdkConsoleImpl(charset); - } - - @Override - public PrintWriter writer() { - return getDelegate(true).writer(); - } - - @Override - public Reader reader() { - return getDelegate(true).reader(); - } - - @Override - public JdkConsole println(Object obj) { - JdkConsole delegate = getDelegate(false); - - delegate.println(obj); - flushOldDelegateIfNeeded(delegate); - - return this; - } - - @Override - public JdkConsole print(Object obj) { - JdkConsole delegate = getDelegate(false); - - delegate.print(obj); - flushOldDelegateIfNeeded(delegate); - - return this; - } - - @Override - public String readln(String prompt) { - return getDelegate(true).readln(prompt); - } - - @Override - public String readln() { - return getDelegate(true).readln(); - } - - @Override - public JdkConsole format(Locale locale, String format, Object... args) { - JdkConsole delegate = getDelegate(false); - - delegate.format(locale, format, args); - flushOldDelegateIfNeeded(delegate); - - return this; - } - - @Override - public String readLine(Locale locale, String format, Object... args) { - return getDelegate(true).readLine(locale, format, args); - } - - @Override - public String readLine() { - return getDelegate(true).readLine(); - } - - @Override - public char[] readPassword(Locale locale, String format, Object... args) { - return getDelegate(true).readPassword(locale, format, args); - } - - @Override - public char[] readPassword() { - return getDelegate(true).readPassword(); - } - - @Override - public void flush() { - getDelegate(false).flush(); - } - - @Override - public Charset charset() { - return charset; - } - - private void flushOldDelegateIfNeeded(JdkConsole oldDelegate) { - if (oldDelegate != getDelegate(false)) { - //if the delegate changed in the mean time, make sure the original - //delegate is flushed: - oldDelegate.flush(); - } - } - - private JdkConsole getDelegate(boolean needsJLine) { - if (!needsJLine || jlineInitialized) { - return delegate; - } - - return initializeJLineDelegate(); - } - - private synchronized JdkConsole initializeJLineDelegate() { - JdkConsole newDelegate = delegate; - - if (jlineInitialized) { - return newDelegate; - } - - try { - Terminal terminal = TerminalBuilder.builder().encoding(charset) - .exec(false) - .nativeSignals(false) - .systemOutput(SystemOutput.SysOut) - .build(); - newDelegate = new JdkConsoleImpl(terminal); - } catch (IllegalStateException ise) { - //cannot create a non-dumb, non-exec terminal, - //use the standard Console: - } catch (IOException ioe) { - //something went wrong, keep the existing delegate - } - - delegate = newDelegate; - jlineInitialized = true; - - return newDelegate; - } - } - - /** - * An implementation of JdkConsole, which act as a delegate for the - * public Console class. - */ - private static class JdkConsoleImpl implements JdkConsole { - private final Terminal terminal; - private volatile LineReader jline; - - @Override - public PrintWriter writer() { - return terminal.writer(); - } - - @Override - public Reader reader() { - return terminal.reader(); - } - - @Override - public JdkConsole println(Object obj) { - writer().println(obj); - writer().flush(); - return this; - } - - @Override - public JdkConsole print(Object obj) { - writer().print(obj); - writer().flush(); - return this; - } - - @Override - public String readln(String prompt) { - try { - initJLineIfNeeded(); - return jline.readLine(prompt == null ? "null" : prompt.replace("%", "%%")); - } catch (EndOfFileException eofe) { - return null; - } - } - - @Override - public String readln() { - return readLine(); - } - - @Override - public JdkConsole format(Locale locale, String format, Object ... args) { - writer().format(locale, format, args).flush(); - return this; - } - - @Override - public String readLine(Locale locale, String format, Object ... args) { - try { - initJLineIfNeeded(); - return jline.readLine(String.format(locale, format, args).replace("%", "%%")); - } catch (EndOfFileException eofe) { - return null; - } - } - - @Override - public String readLine() { - try { - initJLineIfNeeded(); - return jline.readLine(); - } catch (EndOfFileException eofe) { - return null; - } - } - - @Override - public char[] readPassword(Locale locale, String format, Object ... args) { - try { - initJLineIfNeeded(); - return jline.readLine(String.format(locale, format, args).replace("%", "%%"), '\0') - .toCharArray(); - } catch (EndOfFileException eofe) { - return null; - } finally { - jline.zeroOut(); - } - } - - @Override - public char[] readPassword() { - return readPassword(Locale.getDefault(Locale.Category.FORMAT), ""); - } - - @Override - public void flush() { - terminal.flush(); - } - - @Override - public Charset charset() { - return terminal.encoding(); - } - - public JdkConsoleImpl(Terminal terminal) { - this.terminal = terminal; - } - - private void initJLineIfNeeded() { - LineReader jline = this.jline; - if (jline == null) { - synchronized (this) { - jline = this.jline; - if (jline == null) { - jline = LineReaderBuilder.builder().terminal(terminal).build(); - this.jline = jline; - } - } - } - } - } -} diff --git a/src/jdk.internal.le/share/classes/module-info.java b/src/jdk.internal.le/share/classes/module-info.java index fdc973d36ca23..36cfde860a029 100644 --- a/src/jdk.internal.le/share/classes/module-info.java +++ b/src/jdk.internal.le/share/classes/module-info.java @@ -50,6 +50,6 @@ // Console provides jdk.internal.io.JdkConsoleProvider with - jdk.internal.org.jline.JdkConsoleProviderImpl; + jdk.internal.console.JdkConsoleProviderImpl; } diff --git a/src/jdk.internal.le/unix/classes/jdk/internal/console/NativeConsoleReader.java b/src/jdk.internal.le/unix/classes/jdk/internal/console/NativeConsoleReader.java new file mode 100644 index 0000000000000..ec29e66f3bd2a --- /dev/null +++ b/src/jdk.internal.le/unix/classes/jdk/internal/console/NativeConsoleReader.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.console; + +import java.io.IOException; +import java.io.Reader; +import java.io.Writer; + +public class NativeConsoleReader { + + public static char[] readline(Reader reader, Writer out, boolean password) throws IOException { + byte[] originalTermios = switchToRaw(); + Thread restoreConsole = new Thread(() -> { + restore(originalTermios); + }); + try { + Runtime.getRuntime().addShutdownHook(restoreConsole); + int width = terminalWidth(); + out.append("\033[6n").flush(); //ask the terminal to provide cursor location + return SimpleConsoleReader.doRead(reader, out, password, -1, () -> width); + } finally { + restoreConsole.run(); + Runtime.getRuntime().removeShutdownHook(restoreConsole); + } + } + + static { + loadNativeLibrary(); + } + + @SuppressWarnings("restricted") + private static void loadNativeLibrary() { + System.loadLibrary("le"); + initIDs(); + } + + private static native void initIDs(); + private static native byte[] switchToRaw(); + private static native void restore(byte[] termios); + private static native int terminalWidth(); +} diff --git a/src/jdk.internal.le/unix/native/lible/NativeConsoleReader.cpp b/src/jdk.internal.le/unix/native/lible/NativeConsoleReader.cpp new file mode 100644 index 0000000000000..2b9e9129cace7 --- /dev/null +++ b/src/jdk.internal.le/unix/native/lible/NativeConsoleReader.cpp @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2023, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ + +#include "jni.h" +#include "jni_util.h" +#include "jdk_internal_console_NativeConsoleReader.h" + +#include +#include +#include +#include +#include + +static jclass lastErrorExceptionClass; +static jmethodID lastErrorExceptionConstructor; + +static void throw_errno(JNIEnv *env); + +JNIEXPORT void JNICALL Java_jdk_internal_console_NativeConsoleReader_initIDs + (JNIEnv *env, jclass) { + jclass cls; + cls = env->FindClass("jdk/internal/console/LastErrorException"); + CHECK_NULL(cls); + lastErrorExceptionClass = (jclass) env->NewGlobalRef(cls); + lastErrorExceptionConstructor = env->GetMethodID(lastErrorExceptionClass, "", "(J)V"); + CHECK_NULL(lastErrorExceptionConstructor); +} + +JNIEXPORT jbyteArray JNICALL Java_jdk_internal_console_NativeConsoleReader_switchToRaw + (JNIEnv *env, jclass) { + int fd = 0; + termios data; + + if (tcgetattr(fd, &data) != 0) { + throw_errno(env); + return NULL; + } + + size_t termios_size = sizeof(termios); + jbyteArray result = env->NewByteArray(termios_size); + env->SetByteArrayRegion(result, 0, termios_size, (jbyte *) &data); + + data.c_iflag &= ~(BRKINT | IGNPAR | ICRNL | IXON | IMAXBEL) | IXOFF; + data.c_lflag &= ~(ICANON | ECHO); + + if (tcsetattr(fd, TCSADRAIN, &data) != 0) { + throw_errno(env); + return NULL; + } + + return result; +} + +JNIEXPORT void JNICALL Java_jdk_internal_console_NativeConsoleReader_restore + (JNIEnv *env, jclass, jbyteArray storedData) { + int fd = 0; + termios data; + + size_t termios_size = sizeof(termios); + env->GetByteArrayRegion(storedData, 0, termios_size, (jbyte*) &data); + + if (tcsetattr(fd, TCSADRAIN, &data) != 0) { + throw_errno(env); + } +} + +JNIEXPORT jint JNICALL Java_jdk_internal_console_NativeConsoleReader_terminalWidth + (JNIEnv *env, jclass) { + int fd = 0; + winsize ws; + + if (ioctl(fd, TIOCGWINSZ, &ws) != 0) { + throw_errno(env); + return -1; + } + + return ws.ws_col; +} + +/* + * Throws LastErrorException based on the errno: + */ +static void throw_errno(JNIEnv *env) { + jobject exc = env->NewObject(lastErrorExceptionClass, + lastErrorExceptionConstructor, + errno); + env->Throw((jthrowable) exc); +} diff --git a/src/jdk.internal.le/windows/classes/jdk/internal/console/NativeConsoleReader.java b/src/jdk.internal.le/windows/classes/jdk/internal/console/NativeConsoleReader.java new file mode 100644 index 0000000000000..f1e7f3708f8a2 --- /dev/null +++ b/src/jdk.internal.le/windows/classes/jdk/internal/console/NativeConsoleReader.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.console; + +import java.io.Reader; +import java.io.Writer; +import java.io.IOException; +import java.util.concurrent.atomic.AtomicInteger; +import static jdk.internal.console.WindowsTerminal.*; + +public class NativeConsoleReader { + + public static char[] readline(Reader reader, Writer out, boolean password) throws IOException { + byte[] originalModes = switchToRaw(); + try { + AtomicInteger width = new AtomicInteger(terminalWidth()); + int firstLineOffset = cursorX(); + Reader in = new ConsoleInputStream(() -> { + width.set(terminalWidth()); + }); + return SimpleConsoleReader.doRead(in, out, password, firstLineOffset, () -> width.get()); + } finally { + restore(originalModes); + } + } + +} diff --git a/src/jdk.internal.le/windows/classes/jdk/internal/console/WindowsTerminal.java b/src/jdk.internal.le/windows/classes/jdk/internal/console/WindowsTerminal.java new file mode 100644 index 0000000000000..3abd06e97f030 --- /dev/null +++ b/src/jdk.internal.le/windows/classes/jdk/internal/console/WindowsTerminal.java @@ -0,0 +1,219 @@ +/* + * Copyright (c) 2002-2023, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package jdk.internal.console; + +import java.io.Reader; +import java.io.IOException; + +//partly based on AbstractWindowsTerminal from JLine: +public class WindowsTerminal { + + public static final int SHIFT_FLAG = 0x01; + public static final int ALT_FLAG = 0x02; + public static final int CTRL_FLAG = 0x04; + + public static final int RIGHT_ALT_PRESSED = 0x0001; + public static final int LEFT_ALT_PRESSED = 0x0002; + public static final int RIGHT_CTRL_PRESSED = 0x0004; + public static final int LEFT_CTRL_PRESSED = 0x0008; + public static final int SHIFT_PRESSED = 0x0010; + + static { + loadNativeLibrary(); + } + + @SuppressWarnings("restricted") + private static void loadNativeLibrary() { + System.loadLibrary("le"); + initIDs(); + } + + private static native void initIDs(); + static native byte[] switchToRaw(); + static native void restore(byte[] originalModes); + static native int terminalWidth(); + static native int cursorX(); + private static native Object readEvent(); + + public record KeyEvent(boolean keyDown, short keyCode, char uchar, int controlKeyState) {} + public record WindowSizeEvent() {} + + static final class ConsoleInputStream extends Reader { + + private final Runnable refreshWidth; + private char[] backlog; + private int backlogIndex; + + public ConsoleInputStream(Runnable refreshWidth) { + this.refreshWidth = refreshWidth; + } + + @Override + public int read(char[] cbuf, int off, int len) throws IOException { + if (len == 0) { + return 0; + } + + while (backlog == null || backlogIndex >= backlog.length) { + Object event = readEvent(); + switch (event) { + case null -> {} //continue + case KeyEvent keyEvent -> { + processKeyEvent( + keyEvent.keyDown(), keyEvent.keyCode(), keyEvent.uchar(), keyEvent.controlKeyState()); + } + case WindowSizeEvent evt -> { + refreshWidth.run(); + return -1; + } + default -> throw new IllegalStateException("No other instances should be provided! Got: " + event.getClass()); + } + } + cbuf[0] = backlog[backlogIndex++]; + return 1; + } + + protected void processKeyEvent( + final boolean isKeyDown, final short virtualKeyCode, char ch, final int controlKeyState) + throws IOException { + StringBuilder data = new StringBuilder(); + final boolean isCtrl = (controlKeyState & (RIGHT_CTRL_PRESSED | LEFT_CTRL_PRESSED)) > 0; + final boolean isAlt = (controlKeyState & (RIGHT_ALT_PRESSED | LEFT_ALT_PRESSED)) > 0; + final boolean isShift = (controlKeyState & SHIFT_PRESSED) > 0; + // key down event + if (isKeyDown && ch != '\3') { + // Pressing "Alt Gr" is translated to Alt-Ctrl, hence it has to be checked that Ctrl is _not_ pressed, + // otherwise inserting of "Alt Gr" codes on non-US keyboards would yield errors + if (ch != 0 + && (controlKeyState + & (RIGHT_ALT_PRESSED | LEFT_ALT_PRESSED | RIGHT_CTRL_PRESSED | LEFT_CTRL_PRESSED)) + == (RIGHT_ALT_PRESSED | LEFT_CTRL_PRESSED)) { + data.append(ch); + } else { + final String keySeq = getEscapeSequence( + virtualKeyCode, (isCtrl ? CTRL_FLAG : 0) + (isAlt ? ALT_FLAG : 0) + (isShift ? SHIFT_FLAG : 0)); + if (keySeq != null) { + data.append(keySeq); + } else { + /* uchar value in Windows when CTRL is pressed: + * 1). Ctrl + <0x41 to 0x5e> : uchar= - 'A' + 1 + * 2). Ctrl + Backspace(0x08) : uchar=0x7f + * 3). Ctrl + Enter(0x0d) : uchar=0x0a + * 4). Ctrl + Space(0x20) : uchar=0x20 + * 5). Ctrl + : uchar=0 + * 6). Ctrl + Alt + : uchar=0 + */ + if (ch > 0) { + if (isAlt) { + data.append('\033'); + } + if (isCtrl && ch != ' ' && ch != '\n' && ch != 0x7f) { + data.append((char) (ch == '?' ? 0x7f : Character.toUpperCase(ch) & 0x1f)); + } else if (isCtrl && ch == '\n') { + //simulate Alt-Enter: + data.append('\033'); + data.append('\r'); + } else { + data.append(ch); + } + } else if (isCtrl) { // Handles the ctrl key events(uchar=0) + if (virtualKeyCode >= 'A' && virtualKeyCode <= 'Z') { + ch = (char) (virtualKeyCode - 0x40); + } else if (virtualKeyCode == 191) { // ? + ch = 127; + } + if (ch > 0) { + if (isAlt) { + data.append('\033'); + } + data.append(ch); + } + } + } + } + } else if (isKeyDown && ch == '\3') { + data.append('\3'); + } + // key up event + else { + // support ALT+NumPad input method + if (virtualKeyCode == 0x12 /*VK_MENU ALT key*/ && ch > 0) { + data.append(ch); // no such combination in Windows + } + } + backlog = new char[data.length()]; + for (int i = 0; i < data.length(); i++) { + backlog[i] = data.charAt(i); + } + backlogIndex = 0; + } + + protected String getEscapeSequence(short keyCode, int keyState) { + // virtual keycodes: http://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx + // TODO: numpad keys, modifiers + String escapeSequence = null; + switch (keyCode) { + case 0x08: // VK_BACK BackSpace + escapeSequence = "\u007F"; + break; + case 0x09: + return null; + case 0x23: // VK_END + escapeSequence = "\033[F"; + break; + case 0x24: // VK_HOME + escapeSequence = "\033[H"; + break; + case 0x25: // VK_LEFT + escapeSequence = "\033[D"; + break; + case 0x27: // VK_RIGHT + escapeSequence = "\033[C"; + break; + case 0x2E: // VK_DELETE + escapeSequence = "\033[3~"; + break; + case 0x21: // VK_PRIOR PageUp + case 0x22: // VK_NEXT PageDown + case 0x26: // VK_UP + case 0x28: // VK_DOWN + case 0x2D: // VK_INSERT + + case 0x70: // VK_F1 + case 0x71: // VK_F2 + case 0x72: // VK_F3 + case 0x73: // VK_F4 + case 0x74: // VK_F5 + case 0x75: // VK_F6 + case 0x76: // VK_F7 + case 0x77: // VK_F8 + case 0x78: // VK_F9 + case 0x79: // VK_F10 + case 0x7A: // VK_F11 + case 0x7B: // VK_F12 + return ""; + case 0x5D: // VK_CLOSE_BRACKET(Menu key) + case 0x5B: // VK_OPEN_BRACKET(Window key) + default: + return null; + } + if (keyState != 0) { + //with modifiers - ignore: + return ""; + } + return escapeSequence; + } + + @Override + public void close() throws IOException { + } + + } + +} diff --git a/src/jdk.internal.le/windows/native/lible/WindowsTerminal.cpp b/src/jdk.internal.le/windows/native/lible/WindowsTerminal.cpp new file mode 100644 index 0000000000000..045914c33e431 --- /dev/null +++ b/src/jdk.internal.le/windows/native/lible/WindowsTerminal.cpp @@ -0,0 +1,193 @@ +/* + * Copyright (c) 2023, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ + +#include "jni.h" +#include "jni_util.h" +#include "jdk_internal_console_WindowsTerminal.h" + +#include +#include +//#include +//#include + +static jclass lastErrorExceptionClass; +static jmethodID lastErrorExceptionConstructor; + +static jclass KEY_EVENT_Class; +static jmethodID KEY_EVENT_Constructor; + +static jclass WINDOW_SIZE_EVENT_Class; +static jmethodID WINDOW_SIZE_EVENT_Constructor; + +static void throw_errno(JNIEnv *env); + +JNIEXPORT void JNICALL Java_jdk_internal_console_WindowsTerminal_initIDs + (JNIEnv *env, jclass) { + jclass cls; + cls = env->FindClass("jdk/internal/console/LastErrorException"); + CHECK_NULL(cls); + lastErrorExceptionClass = (jclass) env->NewGlobalRef(cls); + lastErrorExceptionConstructor = env->GetMethodID(lastErrorExceptionClass, "", "(J)V"); + CHECK_NULL(lastErrorExceptionConstructor); + KEY_EVENT_Class = (jclass) env->NewGlobalRef(env->FindClass("jdk/internal/console/WindowsTerminal$KeyEvent")); + CHECK_NULL(KEY_EVENT_Class); + KEY_EVENT_Constructor = env->GetMethodID(KEY_EVENT_Class, "", "(ZSCI)V"); + CHECK_NULL(KEY_EVENT_Constructor); + WINDOW_SIZE_EVENT_Class = (jclass) env->NewGlobalRef(env->FindClass("jdk/internal/console/WindowsTerminal$WindowSizeEvent")); + CHECK_NULL(WINDOW_SIZE_EVENT_Class); + WINDOW_SIZE_EVENT_Constructor = env->GetMethodID(WINDOW_SIZE_EVENT_Class, "", "()V"); + CHECK_NULL(WINDOW_SIZE_EVENT_Constructor); +} + +JNIEXPORT jbyteArray JNICALL Java_jdk_internal_console_WindowsTerminal_switchToRaw + (JNIEnv *env, jclass) { + HANDLE inHandle = GetStdHandle(STD_INPUT_HANDLE); + DWORD origInMode; + + if (!GetConsoleMode(inHandle, &origInMode)) { + throw_errno(env); + return NULL; + } + + HANDLE outHandle = GetStdHandle(STD_OUTPUT_HANDLE); + DWORD origOutMode; + + if (!GetConsoleMode(outHandle, &origOutMode)) { + throw_errno(env); + return NULL; + } + + if (!SetConsoleMode(inHandle, ENABLE_PROCESSED_INPUT)) { + throw_errno(env); + return NULL; + } + + if (!SetConsoleMode(outHandle, ENABLE_VIRTUAL_TERMINAL_PROCESSING | ENABLE_PROCESSED_OUTPUT)) { + throw_errno(env); + return NULL; + } + + jsize dword_size = (jsize) sizeof(DWORD); + jbyteArray result = env->NewByteArray(2 * dword_size); + + env->SetByteArrayRegion(result, 0, dword_size, (jbyte *) &origInMode); + env->SetByteArrayRegion(result, dword_size, dword_size, (jbyte *) &origOutMode); + + return result; +} + +JNIEXPORT void JNICALL Java_jdk_internal_console_WindowsTerminal_restore + (JNIEnv *env, jclass, jbyteArray storedData) { + jsize dword_size = (jsize) sizeof(DWORD); + DWORD origInMode; + DWORD origOutMode; + + env->GetByteArrayRegion(storedData, 0, dword_size, (jbyte*) &origInMode); + env->GetByteArrayRegion(storedData, dword_size, dword_size, (jbyte*) &origOutMode); + + HANDLE inHandle = GetStdHandle(STD_INPUT_HANDLE); + + if (!SetConsoleMode(inHandle, origInMode)) { + throw_errno(env); + return ; + } + + HANDLE outHandle = GetStdHandle(STD_OUTPUT_HANDLE); + + if (!SetConsoleMode(outHandle, origOutMode)) { + throw_errno(env); + return ; + } +} + +JNIEXPORT jint JNICALL Java_jdk_internal_console_WindowsTerminal_terminalWidth + (JNIEnv *env, jclass) { + HANDLE h = GetStdHandle(STD_OUTPUT_HANDLE); + CONSOLE_SCREEN_BUFFER_INFO buffer; + if (!GetConsoleScreenBufferInfo(h, &buffer)) { + DWORD error = GetLastError(); + jobject exc = env->NewObject(lastErrorExceptionClass, + lastErrorExceptionConstructor, + (jlong) error); + env->Throw((jthrowable) exc); + return -1; + } + return buffer.dwSize.X; +} + +JNIEXPORT jint JNICALL Java_jdk_internal_console_WindowsTerminal_cursorX + (JNIEnv *env, jclass) { + HANDLE h = GetStdHandle(STD_OUTPUT_HANDLE); + CONSOLE_SCREEN_BUFFER_INFO buffer; + if (!GetConsoleScreenBufferInfo(h, &buffer)) { + DWORD error = GetLastError(); + jobject exc = env->NewObject(lastErrorExceptionClass, + lastErrorExceptionConstructor, + (jlong) error); + env->Throw((jthrowable) exc); + return -1; + } + return buffer.dwCursorPosition.X; +} + +JNIEXPORT jobject JNICALL Java_jdk_internal_console_WindowsTerminal_readEvent + (JNIEnv *env, jclass) { + HANDLE h = GetStdHandle(STD_INPUT_HANDLE); + INPUT_RECORD buffer; + DWORD numberOfEventsRead; + if (!ReadConsoleInputW(h, &buffer, 1, &numberOfEventsRead)) { + throw_errno(env); + return NULL; + } + switch (buffer.EventType) { + case KEY_EVENT: { + jobject keyEvent = env->NewObject(KEY_EVENT_Class, + KEY_EVENT_Constructor, + buffer.Event.KeyEvent.bKeyDown, + buffer.Event.KeyEvent.wVirtualKeyCode, + buffer.Event.KeyEvent.uChar.UnicodeChar, + buffer.Event.KeyEvent.dwControlKeyState); + return keyEvent; + } + case WINDOW_BUFFER_SIZE_EVENT: { + jobject windowSizeEvent = env->NewObject(WINDOW_SIZE_EVENT_Class, + WINDOW_SIZE_EVENT_Constructor); + + return windowSizeEvent; + } + } + return NULL; +} + +/* + * Throws LastErrorException based on GetLastError: + */ +static void throw_errno(JNIEnv *env) { + DWORD error = GetLastError(); + jobject exc = env->NewObject(lastErrorExceptionClass, + lastErrorExceptionConstructor, + (jlong) error); + env->Throw((jthrowable) exc); +} diff --git a/test/jdk/java/io/Console/ConsolePromptTest.java b/test/jdk/java/io/Console/ConsolePromptTest.java index 3cab25b5a1ca3..e3a3be477760f 100644 --- a/test/jdk/java/io/Console/ConsolePromptTest.java +++ b/test/jdk/java/io/Console/ConsolePromptTest.java @@ -26,26 +26,40 @@ * @bug 8331681 * @summary Verify the java.base's console provider handles the prompt correctly. * @library /test/lib - * @run main/othervm --limit-modules java.base ConsolePromptTest - * @run main/othervm -Djdk.console=java.base ConsolePromptTest */ import java.lang.reflect.Method; -import java.util.Objects; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; import jdk.test.lib.process.OutputAnalyzer; import jdk.test.lib.process.ProcessTools; public class ConsolePromptTest { + private static final List> VARIANTS = List.of( + List.of("--limit-modules", "java.base"), + List.of("-Djdk.console=java.base") + ); + public static void main(String... args) throws Throwable { for (Method m : ConsolePromptTest.class.getDeclaredMethods()) { if (m.getName().startsWith("test")) { - m.invoke(new ConsolePromptTest()); + for (List variant : VARIANTS) { + m.invoke(new ConsolePromptTest(variant)); + } } } } + private final List extraParams; + + public ConsolePromptTest(List extraParams) { + this.extraParams = extraParams; + } + void testCorrectOutputReadLine() throws Exception { doRunConsoleTest("testCorrectOutputReadLine", "inp", "%s"); } @@ -57,34 +71,35 @@ void testCorrectOutputReadPassword() throws Exception { void doRunConsoleTest(String testName, String input, String expectedOut) throws Exception { - ProcessBuilder builder = - ProcessTools.createTestJavaProcessBuilder(ConsoleTest.class.getName(), - testName); - OutputAnalyzer output = ProcessTools.executeProcess(builder, input); - - output.waitFor(); - - if (output.getExitValue() != 0) { - throw new AssertionError("Unexpected return value: " + output.getExitValue() + - ", actualOut: " + output.getStdout() + - ", actualErr: " + output.getStderr()); + // check "expect" command availability + var expect = Paths.get("/usr/bin/expect"); + if (!Files.exists(expect) || !Files.isExecutable(expect)) { + System.out.println("'expect' command not found. Test ignored."); + return; } - String actualOut = output.getStdout(); - - if (!Objects.equals(expectedOut, actualOut)) { - throw new AssertionError("Unexpected stdout content. " + - "Expected: '" + expectedOut + "'" + - ", got: '" + actualOut + "'"); - } - - String expectedErr = ""; - String actualErr = output.getStderr(); - - if (!Objects.equals(expectedErr, actualErr)) { - throw new AssertionError("Unexpected stderr content. " + - "Expected: '" + expectedErr + "'" + - ", got: '" + actualErr + "'"); + // invoking "expect" command + var testSrc = System.getProperty("test.src", "."); + var jdkDir = System.getProperty("test.jdk"); + + List command = new ArrayList<>(); + + command.add("expect"); + command.add("-n"); + command.add(testSrc + "/consolePrompt.exp"); + command.add(expectedOut); + command.add(jdkDir + "/bin/java"); + command.addAll(extraParams); + command.add("-cp"); + command.add(System.getProperty("java.class.path")); + command.add(ConsoleTest.class.getName()); + command.add(testName); + + OutputAnalyzer output = ProcessTools.executeProcess(command.toArray(String[]::new)); + output.reportDiagnosticSummary(); + var eval = output.getExitValue(); + if (eval != 0) { + throw new RuntimeException("Test failed. Exit value from 'expect' command: " + eval); } } diff --git a/test/jdk/java/io/Console/LocaleTest.java b/test/jdk/java/io/Console/LocaleTest.java index 1fe725a3ab4e7..6ec1ec26c78ac 100644 --- a/test/jdk/java/io/Console/LocaleTest.java +++ b/test/jdk/java/io/Console/LocaleTest.java @@ -88,7 +88,6 @@ public static void main(String... args) throws Throwable { con.readLine(Locale.GERMANY, FORMAT, TODAY); con.printf("\n"); con.readPassword(Locale.of("es"), FORMAT, TODAY); - con.printf("\n"); // tests null locale con.format((Locale)null, FORMAT, TODAY); diff --git a/test/jdk/java/io/Console/consolePrompt.exp b/test/jdk/java/io/Console/consolePrompt.exp new file mode 100644 index 0000000000000..dc69bdd86812a --- /dev/null +++ b/test/jdk/java/io/Console/consolePrompt.exp @@ -0,0 +1,30 @@ +# +# Copyright (c) 2025, 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. +# + +set java [lrange $argv 1 end] +set expected [lindex $argv 0] + +eval spawn $java +expect -- "$expected" +send -- "\n" +expect eof diff --git a/test/jdk/jdk/internal/io/JdkConsoleImplConsoleTest.java b/test/jdk/jdk/internal/io/JdkConsoleImplConsoleTest.java new file mode 100644 index 0000000000000..ea89deea00d01 --- /dev/null +++ b/test/jdk/jdk/internal/io/JdkConsoleImplConsoleTest.java @@ -0,0 +1,393 @@ +/* + * Copyright (c) 2025, 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 + * @bug 8352693 + * @summary Test simple console reader. + * @modules jdk.internal.le/jdk.internal.console + * @run main JdkConsoleImplConsoleTest + */ + +import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; +import java.io.StringWriter; +import java.io.Writer; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +import jdk.internal.console.SimpleConsoleReader; + +public class JdkConsoleImplConsoleTest { + public static void main(String... args) throws IOException { + new JdkConsoleImplConsoleTest().run(); + } + + private void run() throws IOException { + testNavigation(); + testTerminalHandling(); + testSurrogates(); + testWraps(); + } + + private void testNavigation() throws IOException { + String input = """ + 12345\033[D\033[D\033[3~6\033[1~7\033[4~8\033[H9\033[FA\r + """; + String expectedResult = "97123658A"; + char[] read = SimpleConsoleReader.doRead(new StringReader(input), new StringWriter(), false, 0, () -> Integer.MAX_VALUE); + assertEquals(expectedResult, new String(read)); + } + + private void testTerminalHandling() throws IOException { + Terminal terminal = new Terminal(5, 5); + Thread.ofVirtual().start(() -> { + try { + SimpleConsoleReader.doRead(terminal.getInput(), terminal.getOutput(), false, 0, () -> terminal.width); + } catch (IOException ex) { + throw new IllegalStateException(ex); + } + }); + + terminal.typed("123456"); + assertEquals(""" + 12345 + 6 + + + """, + terminal.getDisplay()); + terminal.typed("\033[D\033[D\033[DN"); + assertEquals(""" + 123N4 + 56 + + + """, + terminal.getDisplay()); + } + + private void testSurrogates() throws IOException { + { + String input = """ + 1\uD83D\uDE032 + """; + String expectedResult = "1\uD83D\uDE032"; + char[] read = SimpleConsoleReader.doRead(new StringReader(input), new StringWriter(), false, 0, () -> Integer.MAX_VALUE); + assertEquals(expectedResult, new String(read)); + } + { + String input = """ + 1\uD83D\uDE032\u007F\u007F3 + """; + String expectedResult = "13"; + char[] read = SimpleConsoleReader.doRead(new StringReader(input), new StringWriter(), false, 0, () -> Integer.MAX_VALUE); + assertEquals(expectedResult, new String(read)); + } + + { + Terminal terminal = new Terminal(5, 5); + Thread.ofVirtual().start(() -> { + try { + SimpleConsoleReader.doRead(terminal.getInput(), terminal.getOutput(), false, 0, () -> terminal.width); + } catch (IOException ex) { + throw new IllegalStateException(ex); + } + }); + + terminal.typed("12\uD83D\uDE03456"); + assertEquals(""" + 12\uD83D\uDE034 + 56 + + + """, + terminal.getDisplay()); + terminal.typed("\033[D\033[D\033[D\033[DN"); + assertEquals(""" + 12N\uD83D\uDE03 + 456 + + + """, + terminal.getDisplay()); + } + + { + Terminal terminal = new Terminal(5, 5); + Thread.ofVirtual().start(() -> { + try { + SimpleConsoleReader.doRead(terminal.getInput(), terminal.getOutput(), false, 0, () -> terminal.width); + } catch (IOException ex) { + throw new IllegalStateException(ex); + } + }); + + terminal.typed("12\uD83F\uDEEF456"); + assertEquals(""" + 12\uD83F\uDEEF45 + 6 + + + """, + terminal.getDisplay()); + terminal.typed("\033[D\033[D\033[D\033[DN"); + assertEquals(""" + 12N\uD83F\uDEEF4 + 56 + + + """, + terminal.getDisplay()); + } + } + + private void testWraps() throws IOException { + { + Terminal terminal = new Terminal(5, 5); + Thread.ofVirtual().start(() -> { + try { + SimpleConsoleReader.doRead(terminal.getInput(), terminal.getOutput(), false, 0, () -> terminal.width); + } catch (IOException ex) { + throw new IllegalStateException(ex); + } + }); + + terminal.typed("12345ABCDEabc"); + assertEquals(""" + 12345 + ABCDE + abc + + """, + terminal.getDisplay()); + } + } + + private static void assertEquals(Object expected, Object actual) { + if (!Objects.equals(expected, actual)) { + throw new AssertionError("expected: " + expected + + "actual: " + actual); + } + } + + private static class Terminal { + private final Map bindings = new HashMap<>(); + private final int width; + private final int[][] buffer; + private final StringBuilder pendingOutput = new StringBuilder(); + private final StringBuilder pendingInput = new StringBuilder(); + private final Object emptyInputLock = new Object(); + private Map currentBindings = bindings; + private int cursorX; + private int cursorY; + + public Terminal(int width, int height) { + this.width = width; + this.buffer = new int[height][]; + + for (int i = 0; i < height; i++) { + this.buffer[i] = createLine(); + } + + cursorX = 1; + cursorY = 1; + + // addKeyBinding("\033[D", () -> cursorX = Math.max(cursorX - 1, 0)); + addKeyBinding("\033[A", () -> cursorY = Math.max(cursorY - 1, 1)); + addKeyBinding("\033[B", () -> cursorY = Math.min(cursorY + 1, buffer.length)); + addKeyBinding("\033[1G", () -> cursorX = 1); + addKeyBinding("\033[2G", () -> cursorX = 2); + addKeyBinding("\033[3G", () -> cursorX = 3); + addKeyBinding("\033[4G", () -> cursorX = 4); + addKeyBinding("\033[5G", () -> cursorX = 5); + addKeyBinding("\033[K", () -> Arrays.fill(buffer[cursorY - 1], cursorX - 1, buffer[cursorY - 1].length, ' ')); + addKeyBinding("\n", () -> { + cursorY++; + if (cursorY > buffer.length) { + throw new AssertionError("scrolling via \\n not implemented!"); + } + }); + addKeyBinding("\r", () -> cursorX = 1); + } + + private int[] createLine() { + int[] line = new int[width]; + + Arrays.fill(line, ' '); + + return line; + } + + private void addKeyBinding(String sequence, Runnable action) { + Map pending = bindings; + + for (int i = 0; i < sequence.length() - 1; i++) { + pending = (Map) pending.computeIfAbsent(sequence.charAt(i), _ -> new HashMap<>()); + } + + if (pending.put(sequence.charAt(sequence.length() - 1), action) != null) { + throw new IllegalStateException(); + } + } + + private void handleOutput(char c) { + pendingOutput.append(c); + + Object nestedBindings = currentBindings.get(c); + + switch (nestedBindings) { + case null -> { + for (int i = 0; i < pendingOutput.length(); i++) { + if (cursorX > buffer[0].length) { //(width) + cursorX = 1; + cursorY++; + scrollIfNeeded(); + } + + char currentChar = pendingOutput.charAt(i); + + if (Character.isLowSurrogate(currentChar) && + cursorX > 1 && + Character.isHighSurrogate((char) buffer[cursorY - 1][cursorX - 2])) { + buffer[cursorY - 1][cursorX - 2] = Character.toCodePoint((char) buffer[cursorY - 1][cursorX - 2], currentChar); + } else { + buffer[cursorY - 1][cursorX - 1] = currentChar; + + cursorX++; + } + } + + pendingOutput.delete(0, pendingOutput.length()); + currentBindings = bindings; + } + + case Runnable r -> { + r.run(); + pendingOutput.delete(0, pendingOutput.length()); + currentBindings = bindings; + } + + case Map nextBindings -> { + currentBindings = nextBindings; + } + + default -> throw new IllegalStateException(); + } + } + + private void scrollIfNeeded() { + if (cursorY > buffer.length) { + for (int j = 0; j < buffer.length - 1; j++) { + buffer[j] = buffer[j + 1]; + } + + buffer[buffer.length - 1] = createLine(); + cursorY--; + } + } + + public Writer getOutput() { + return new Writer() { + @Override + public void write(char[] cbuf, int off, int len) throws IOException { + for (int i = 0; i < len; i++) { + handleOutput(cbuf[i + off]); + } + } + + @Override + public void flush() throws IOException {} + + @Override + public void close() throws IOException {} + + }; + } + + public Reader getInput() { + return new Reader() { + @Override + public int read(char[] cbuf, int off, int len) throws IOException { + if (len == 0) { + return 0; + } + + synchronized (pendingInput) { + while (pendingInput.isEmpty()) { + synchronized (emptyInputLock) { + emptyInputLock.notifyAll(); + } + try { + pendingInput.wait(); + } catch (InterruptedException ex) { + } + } + + cbuf[off] = pendingInput.charAt(0); + pendingInput.delete(0, 1); + + return 1; + } + } + + @Override + public void close() throws IOException {} + }; + } + + public void typed(String text) { + synchronized (pendingInput) { + pendingInput.append(text); + pendingInput.notifyAll(); + } + synchronized (emptyInputLock) { + try { + emptyInputLock.wait(); + } catch (InterruptedException ex) { + } + } + } + + public String getDisplay() { + return Arrays.stream(buffer) + .map(this::line2String) + .map(l -> l.replaceAll(" +$", "")) + .collect(Collectors.joining("\n")); + } + private String line2String(int[] line) { + char[] chars = new char[2 * line.length]; + int idx = 0; + + for (int codePoint : line) { + idx += Character.toChars(codePoint, chars, idx); + } + + return new String(chars, 0, idx); + } + } +} diff --git a/test/jdk/jdk/internal/jline/AbstractWindowsTerminalTest.java b/test/jdk/jdk/internal/jline/AbstractWindowsTerminalTest.java index 513c315682a08..f71c7dd81ed51 100644 --- a/test/jdk/jdk/internal/jline/AbstractWindowsTerminalTest.java +++ b/test/jdk/jdk/internal/jline/AbstractWindowsTerminalTest.java @@ -25,8 +25,7 @@ * @test * @bug 8218287 * @summary Verify the wrapper input stream is used when using Terminal.reader() - * @modules jdk.internal.le/jdk.internal.org.jline - * jdk.internal.le/jdk.internal.org.jline.terminal + * @modules jdk.internal.le/jdk.internal.org.jline.terminal * jdk.internal.le/jdk.internal.org.jline.terminal.impl * jdk.internal.le/jdk.internal.org.jline.terminal.spi * jdk.internal.le/jdk.internal.org.jline.utils diff --git a/test/jdk/jdk/internal/jline/JLineConsoleProviderTest.java b/test/jdk/jdk/internal/jline/JLineConsoleProviderTest.java index 5e69cdf4f4bfd..64b11b0c55ebc 100644 --- a/test/jdk/jdk/internal/jline/JLineConsoleProviderTest.java +++ b/test/jdk/jdk/internal/jline/JLineConsoleProviderTest.java @@ -38,6 +38,8 @@ public class JLineConsoleProviderTest { + private static final String NL = System.getProperty("line.separator"); + public static void main(String... args) throws Throwable { for (Method m : JLineConsoleProviderTest.class.getDeclaredMethods()) { if (m.getName().startsWith("test")) { @@ -51,7 +53,7 @@ void testCorrectOutputReadLine() throws Exception { } void testCorrectOutputReadPassword() throws Exception { - doRunConsoleTest("testCorrectOutputReadPassword", "inp", "%s"); + doRunConsoleTest("testCorrectOutputReadPassword", "inp", "%s" + NL); //see BaseJdkConsoleImpl.readPassword re the NL } void doRunConsoleTest(String testName, diff --git a/test/jdk/jdk/internal/jline/LazyJdkConsoleProvider.java b/test/jdk/jdk/internal/jline/LazyJdkConsoleProvider.java deleted file mode 100644 index 42579a8eba9e1..0000000000000 --- a/test/jdk/jdk/internal/jline/LazyJdkConsoleProvider.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright (c) 2024, 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 - * @bug 8333086 - * @summary Verify the JLine backend is not initialized for simple printing. - * @enablePreview - * @modules jdk.internal.le/jdk.internal.org.jline.reader - * jdk.internal.le/jdk.internal.org.jline.terminal - * @library /test/lib - * @run main LazyJdkConsoleProvider - */ - -import java.io.IO; -import jdk.internal.org.jline.reader.LineReader; -import jdk.internal.org.jline.terminal.Terminal; - -import jdk.test.lib.process.OutputAnalyzer; -import jdk.test.lib.process.ProcessTools; - -public class LazyJdkConsoleProvider { - - public static void main(String... args) throws Throwable { - switch (args.length > 0 ? args[0] : "default") { - case "write" -> { - System.console().println("Hello!"); - System.console().print("Hello!"); - System.console().format("\nHello!\n"); - System.console().flush(); - IO.println("Hello!"); - IO.print("Hello!"); - } - case "read" -> System.console().readLine("Hello!"); - case "IO-read" -> { - IO.readln("Hello!"); - } - case "default" -> { - new LazyJdkConsoleProvider().runTest(); - } - } - } - - void runTest() throws Exception { - record TestCase(String testKey, String expected, String notExpected) {} - TestCase[] testCases = new TestCase[] { - new TestCase("write", null, Terminal.class.getName()), - new TestCase("read", LineReader.class.getName(), null), - new TestCase("IO-read", LineReader.class.getName(), null) - }; - for (TestCase tc : testCases) { - ProcessBuilder builder = - ProcessTools.createTestJavaProcessBuilder("--enable-preview", - "-verbose:class", - "-Djdk.console=jdk.internal.le", - LazyJdkConsoleProvider.class.getName(), - tc.testKey()); - OutputAnalyzer output = ProcessTools.executeProcess(builder, ""); - - output.waitFor(); - - if (output.getExitValue() != 0) { - throw new AssertionError("Unexpected return value: " + output.getExitValue() + - ", actualOut: " + output.getStdout() + - ", actualErr: " + output.getStderr()); - } - if (tc.expected() != null) { - output.shouldContain(tc.expected()); - } - - if (tc.notExpected() != null) { - output.shouldNotContain(tc.notExpected()); - } - } - } - -} From 77c41e464cc1a820a71d8269eef17e6937479ca8 Mon Sep 17 00:00:00 2001 From: Jan Lahoda Date: Wed, 26 Mar 2025 09:22:47 +0100 Subject: [PATCH 2/6] Removing trailing whitespace --- .../classes/jdk/internal/console/JdkConsoleProviderImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/jdk.internal.le/share/classes/jdk/internal/console/JdkConsoleProviderImpl.java b/src/jdk.internal.le/share/classes/jdk/internal/console/JdkConsoleProviderImpl.java index 020d7d5fd2da5..2184b1fa94714 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/console/JdkConsoleProviderImpl.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/console/JdkConsoleProviderImpl.java @@ -35,5 +35,5 @@ public JdkConsole console(boolean isTTY, Charset charset) { //only supported on Linux, Mac OS/X and Windows: return new JdkConsoleImpl(isTTY, charset); } - + } From 91fa1b28e0436e55eed9cdee07e0e28c25bf666a Mon Sep 17 00:00:00 2001 From: Jan Lahoda Date: Mon, 31 Mar 2025 16:34:15 +0200 Subject: [PATCH 3/6] Reflecting review feedback. --- make/modules/jdk.internal.le/Lib.gmk | 7 +++--- src/java.base/share/classes/module-info.java | 4 +--- .../internal/console/SimpleConsoleReader.java | 24 +++++++++++++++---- .../windows/native/lible/WindowsTerminal.cpp | 2 -- .../jline/JLineConsoleProviderTest.java | 3 ++- 5 files changed, 26 insertions(+), 14 deletions(-) diff --git a/make/modules/jdk.internal.le/Lib.gmk b/make/modules/jdk.internal.le/Lib.gmk index 6a5e9ec663f4d..efe84664f9554 100644 --- a/make/modules/jdk.internal.le/Lib.gmk +++ b/make/modules/jdk.internal.le/Lib.gmk @@ -32,10 +32,9 @@ ifeq ($(call isTargetOs, linux macosx windows), true) $(eval $(call SetupJdkLibrary, BUILD_LIBLE, \ NAME := le, \ TOOLCHAIN := TOOLCHAIN_LINK_CXX, \ - OPTIMIZATION := LOW, \ - JDK_LIBS := java.base:libjava, \ - LIBS_unix := $(JDKLIB_LIBS) $(LIBCXX), \ - LIBS_windows := $(JDKLIB_LIBS) user32.lib, \ + OPTIMIZATION := SIZE, \ + EXTRA_HEADER_DIRS := java.base:libjava, \ + LIBS_windows := user32.lib, \ )) TARGETS += $(BUILD_LIBLE) diff --git a/src/java.base/share/classes/module-info.java b/src/java.base/share/classes/module-info.java index e38672e302ed5..22f40d9cead1d 100644 --- a/src/java.base/share/classes/module-info.java +++ b/src/java.base/share/classes/module-info.java @@ -169,7 +169,6 @@ java.management, java.rmi, jdk.charsets, - jdk.internal.le, jdk.jartool, jdk.jlink, jdk.jfr, @@ -300,8 +299,7 @@ jdk.net, jdk.sctp; exports sun.nio.cs to - jdk.charsets, - jdk.internal.le; + jdk.charsets; exports sun.nio.fs to jdk.net; exports sun.reflect.annotation to diff --git a/src/jdk.internal.le/share/classes/jdk/internal/console/SimpleConsoleReader.java b/src/jdk.internal.le/share/classes/jdk/internal/console/SimpleConsoleReader.java index 0b4a27557b3b5..aabe021211885 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/console/SimpleConsoleReader.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/console/SimpleConsoleReader.java @@ -38,7 +38,11 @@ public class SimpleConsoleReader { //public, to simplify access from tests: - public static char[] doRead(Reader reader, Writer out, boolean password, int firstLineOffset, IntSupplier terminalWidthSupplier) throws IOException { + public static char[] doRead(Reader reader, + Writer out, + boolean password, + int firstLineOffset, + IntSupplier terminalWidthSupplier) throws IOException { CleanableBuffer result = new CleanableBuffer(); try { doReadImpl(reader, out, password, firstLineOffset, terminalWidthSupplier, result); @@ -48,7 +52,12 @@ public static char[] doRead(Reader reader, Writer out, boolean password, int fir } } - private static void doReadImpl(Reader reader, Writer out, boolean password, int firstLineOffset, IntSupplier terminalWidthSupplier, CleanableBuffer result) throws IOException { + private static void doReadImpl(Reader reader, + Writer out, + boolean password, + int firstLineOffset, + IntSupplier terminalWidthSupplier, + CleanableBuffer result) throws IOException { int caret = 0; int r; PaintState prevState = new PaintState(); @@ -56,7 +65,8 @@ private static void doReadImpl(Reader reader, Writer out, boolean password, int READ: while (true) { //paint: if (firstLineOffset != (-1) && !password) { - prevState = repaint(out, firstLineOffset, terminalWidthSupplier, result.data, result.length, caret, prevState); + prevState = repaint(out, firstLineOffset, terminalWidthSupplier, + result.data, result.length, caret, prevState); } //read @@ -141,7 +151,13 @@ private static void doReadImpl(Reader reader, Writer out, boolean password, int out.append("\n\r").flush(); } - private static PaintState repaint(Writer out, int firstLineOffset, IntSupplier terminalWidthSupplier, int[] toDisplay, int toDisplayLength, int caret, PaintState prevPaintState) throws IOException { + private static PaintState repaint(Writer out, + int firstLineOffset, + IntSupplier terminalWidthSupplier, + int[] toDisplay, + int toDisplayLength, + int caret, + PaintState prevPaintState) throws IOException { //for simplicity, repaint the whole input buffer //for more efficiency, could compute smaller (ideally minimal) changes, //and apply them instead of repainting everything: diff --git a/src/jdk.internal.le/windows/native/lible/WindowsTerminal.cpp b/src/jdk.internal.le/windows/native/lible/WindowsTerminal.cpp index 045914c33e431..b5e6502bb5cb0 100644 --- a/src/jdk.internal.le/windows/native/lible/WindowsTerminal.cpp +++ b/src/jdk.internal.le/windows/native/lible/WindowsTerminal.cpp @@ -29,8 +29,6 @@ #include #include -//#include -//#include static jclass lastErrorExceptionClass; static jmethodID lastErrorExceptionConstructor; diff --git a/test/jdk/jdk/internal/jline/JLineConsoleProviderTest.java b/test/jdk/jdk/internal/jline/JLineConsoleProviderTest.java index 64b11b0c55ebc..59ef8fcb87b3a 100644 --- a/test/jdk/jdk/internal/jline/JLineConsoleProviderTest.java +++ b/test/jdk/jdk/internal/jline/JLineConsoleProviderTest.java @@ -53,7 +53,8 @@ void testCorrectOutputReadLine() throws Exception { } void testCorrectOutputReadPassword() throws Exception { - doRunConsoleTest("testCorrectOutputReadPassword", "inp", "%s" + NL); //see BaseJdkConsoleImpl.readPassword re the NL + doRunConsoleTest("testCorrectOutputReadPassword", "inp", + "%s" + NL); //see BaseJdkConsoleImpl.readPassword re the NL } void doRunConsoleTest(String testName, From c6256eeeabfd73e7e67bf7f8c18c337e6bb92534 Mon Sep 17 00:00:00 2001 From: Jan Lahoda Date: Mon, 31 Mar 2025 16:53:48 +0200 Subject: [PATCH 4/6] If there's no native library available, fall back to the standard provider. --- .../console/JdkConsoleProviderImpl.java | 4 ++++ .../internal/console/NativeConsoleReader.java | 17 ++++++++++++++++- .../internal/console/NativeConsoleReader.java | 4 ++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/jdk.internal.le/share/classes/jdk/internal/console/JdkConsoleProviderImpl.java b/src/jdk.internal.le/share/classes/jdk/internal/console/JdkConsoleProviderImpl.java index 2184b1fa94714..756e05e779757 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/console/JdkConsoleProviderImpl.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/console/JdkConsoleProviderImpl.java @@ -32,6 +32,10 @@ public class JdkConsoleProviderImpl implements JdkConsoleProvider { @Override public JdkConsole console(boolean isTTY, Charset charset) { + if (!NativeConsoleReader.isSupported()) { + return null; + } + //only supported on Linux, Mac OS/X and Windows: return new JdkConsoleImpl(isTTY, charset); } diff --git a/src/jdk.internal.le/unix/classes/jdk/internal/console/NativeConsoleReader.java b/src/jdk.internal.le/unix/classes/jdk/internal/console/NativeConsoleReader.java index ec29e66f3bd2a..b32220c75adf8 100644 --- a/src/jdk.internal.le/unix/classes/jdk/internal/console/NativeConsoleReader.java +++ b/src/jdk.internal.le/unix/classes/jdk/internal/console/NativeConsoleReader.java @@ -30,6 +30,12 @@ public class NativeConsoleReader { + private static final boolean supported; + + public static boolean isSupported() { + return supported; + } + public static char[] readline(Reader reader, Writer out, boolean password) throws IOException { byte[] originalTermios = switchToRaw(); Thread restoreConsole = new Thread(() -> { @@ -47,7 +53,16 @@ public static char[] readline(Reader reader, Writer out, boolean password) throw } static { - loadNativeLibrary(); + boolean initialized; + + try { + loadNativeLibrary(); + initialized = true; + } catch (UnsatisfiedLinkError err) { + initialized = false; + } + + supported = initialized; } @SuppressWarnings("restricted") diff --git a/src/jdk.internal.le/windows/classes/jdk/internal/console/NativeConsoleReader.java b/src/jdk.internal.le/windows/classes/jdk/internal/console/NativeConsoleReader.java index f1e7f3708f8a2..182da621ed86f 100644 --- a/src/jdk.internal.le/windows/classes/jdk/internal/console/NativeConsoleReader.java +++ b/src/jdk.internal.le/windows/classes/jdk/internal/console/NativeConsoleReader.java @@ -32,6 +32,10 @@ public class NativeConsoleReader { + public static boolean isSupported() { + return supported; + } + public static char[] readline(Reader reader, Writer out, boolean password) throws IOException { byte[] originalModes = switchToRaw(); try { From d5176d68e8cc73914a0efef45a60f0ff03885bcc Mon Sep 17 00:00:00 2001 From: Jan Lahoda Date: Thu, 3 Apr 2025 10:56:22 +0200 Subject: [PATCH 5/6] Using control characters to get backspace control character. --- .../internal/console/SimpleConsoleReader.java | 40 ++++++++++++------- .../internal/console/NativeConsoleReader.java | 17 ++++++-- .../unix/native/lible/NativeConsoleReader.cpp | 12 +++++- .../internal/console/NativeConsoleReader.java | 8 +++- .../jdk/internal/console/WindowsTerminal.java | 5 ++- .../io/JdkConsoleImplConsoleTest.java | 40 +++++++++++++++---- 6 files changed, 94 insertions(+), 28 deletions(-) diff --git a/src/jdk.internal.le/share/classes/jdk/internal/console/SimpleConsoleReader.java b/src/jdk.internal.le/share/classes/jdk/internal/console/SimpleConsoleReader.java index aabe021211885..82483a52ac9b5 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/console/SimpleConsoleReader.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/console/SimpleConsoleReader.java @@ -41,11 +41,10 @@ public class SimpleConsoleReader { public static char[] doRead(Reader reader, Writer out, boolean password, - int firstLineOffset, - IntSupplier terminalWidthSupplier) throws IOException { + TerminalConfiguration terminalConfig) throws IOException { CleanableBuffer result = new CleanableBuffer(); try { - doReadImpl(reader, out, password, firstLineOffset, terminalWidthSupplier, result); + doReadImpl(reader, out, password, terminalConfig, result); return result.getData(); } finally { result.zeroOut(); @@ -55,33 +54,38 @@ public static char[] doRead(Reader reader, private static void doReadImpl(Reader reader, Writer out, boolean password, - int firstLineOffset, - IntSupplier terminalWidthSupplier, + TerminalConfiguration terminalConfiguration, CleanableBuffer result) throws IOException { int caret = 0; int r; PaintState prevState = new PaintState(); + int firstLineOffset = terminalConfiguration.firstLineOffset(); READ: while (true) { //paint: if (firstLineOffset != (-1) && !password) { - prevState = repaint(out, firstLineOffset, terminalWidthSupplier, + prevState = repaint(out, firstLineOffset, + terminalConfiguration.terminalWidthSupplier(), result.data, result.length, caret, prevState); } //read r = reader.read(); + + if (r == terminalConfiguration.eraseControlCharacter()) { + //backspace: + if (caret > 0) { + result.delete(caret - 1, caret); + caret--; + } + continue READ; + } else if (r == terminalConfiguration.eofControlCharacter()) { + break READ; + } + switch (r) { case -1: continue READ; case '\n', '\r': break READ; - case 4: break READ; //EOF/Ctrl-D - case 127: - //backspace: - if (caret > 0) { - result.delete(caret - 1, caret); - caret--; - } - continue READ; case '\033': r = reader.read(); switch (r) { @@ -145,7 +149,9 @@ private static void doReadImpl(Reader reader, if (!password) { //show the final state: - repaint(out, firstLineOffset, terminalWidthSupplier, result.data, result.length, caret, prevState); + repaint(out, firstLineOffset, + terminalConfiguration.terminalWidthSupplier(), + result.data, result.length, caret, prevState); } out.append("\n\r").flush(); @@ -252,6 +258,10 @@ private static int readNumber(Reader reader, int r, StringBuilder number) throws } public record Size(int width, int height) {} + public record TerminalConfiguration(int firstLineOffset, + int eofControlCharacter, + int eraseControlCharacter, + IntSupplier terminalWidthSupplier) {} record PaintState(int lines, int caretLine) { public PaintState() { diff --git a/src/jdk.internal.le/unix/classes/jdk/internal/console/NativeConsoleReader.java b/src/jdk.internal.le/unix/classes/jdk/internal/console/NativeConsoleReader.java index b32220c75adf8..3aba58100ab8c 100644 --- a/src/jdk.internal.le/unix/classes/jdk/internal/console/NativeConsoleReader.java +++ b/src/jdk.internal.le/unix/classes/jdk/internal/console/NativeConsoleReader.java @@ -27,9 +27,14 @@ import java.io.IOException; import java.io.Reader; import java.io.Writer; +import jdk.internal.console.SimpleConsoleReader.TerminalConfiguration; public class NativeConsoleReader { + private static final int CONTROL_EOF_OFFSET = 0; + private static final int CONTROL_ERASE_OFFSET = 1; + private static final int CONTROL_CHARS_LEN = 2; + private static final boolean supported; public static boolean isSupported() { @@ -37,7 +42,8 @@ public static boolean isSupported() { } public static char[] readline(Reader reader, Writer out, boolean password) throws IOException { - byte[] originalTermios = switchToRaw(); + int[] controlCharacters = new int[CONTROL_CHARS_LEN]; + byte[] originalTermios = switchToRaw(controlCharacters); Thread restoreConsole = new Thread(() -> { restore(originalTermios); }); @@ -45,7 +51,12 @@ public static char[] readline(Reader reader, Writer out, boolean password) throw Runtime.getRuntime().addShutdownHook(restoreConsole); int width = terminalWidth(); out.append("\033[6n").flush(); //ask the terminal to provide cursor location - return SimpleConsoleReader.doRead(reader, out, password, -1, () -> width); + TerminalConfiguration terminalConfig = new TerminalConfiguration( + -1, + controlCharacters[CONTROL_EOF_OFFSET], + controlCharacters[CONTROL_ERASE_OFFSET], + () -> width); + return SimpleConsoleReader.doRead(reader, out, password, terminalConfig); } finally { restoreConsole.run(); Runtime.getRuntime().removeShutdownHook(restoreConsole); @@ -72,7 +83,7 @@ private static void loadNativeLibrary() { } private static native void initIDs(); - private static native byte[] switchToRaw(); + private static native byte[] switchToRaw(int[] controlCharacters); private static native void restore(byte[] termios); private static native int terminalWidth(); } diff --git a/src/jdk.internal.le/unix/native/lible/NativeConsoleReader.cpp b/src/jdk.internal.le/unix/native/lible/NativeConsoleReader.cpp index 2b9e9129cace7..299e64f9846c6 100644 --- a/src/jdk.internal.le/unix/native/lible/NativeConsoleReader.cpp +++ b/src/jdk.internal.le/unix/native/lible/NativeConsoleReader.cpp @@ -33,6 +33,9 @@ #include #include +#define CONTROL_EOF_OFFSET 0 +#define CONTROL_ERASE_OFFSET 1 + static jclass lastErrorExceptionClass; static jmethodID lastErrorExceptionConstructor; @@ -49,7 +52,7 @@ JNIEXPORT void JNICALL Java_jdk_internal_console_NativeConsoleReader_initIDs } JNIEXPORT jbyteArray JNICALL Java_jdk_internal_console_NativeConsoleReader_switchToRaw - (JNIEnv *env, jclass) { + (JNIEnv *env, jclass, jintArray controlCharacters) { int fd = 0; termios data; @@ -70,6 +73,13 @@ JNIEXPORT jbyteArray JNICALL Java_jdk_internal_console_NativeConsoleReader_switc return NULL; } + jint controlChars[2] = { + data.c_cc[VEOF], + data.c_cc[VERASE], + }; + + env->SetIntArrayRegion(controlCharacters, 0, 2, controlChars); + return result; } diff --git a/src/jdk.internal.le/windows/classes/jdk/internal/console/NativeConsoleReader.java b/src/jdk.internal.le/windows/classes/jdk/internal/console/NativeConsoleReader.java index 182da621ed86f..22715b52fd040 100644 --- a/src/jdk.internal.le/windows/classes/jdk/internal/console/NativeConsoleReader.java +++ b/src/jdk.internal.le/windows/classes/jdk/internal/console/NativeConsoleReader.java @@ -28,6 +28,7 @@ import java.io.Writer; import java.io.IOException; import java.util.concurrent.atomic.AtomicInteger; +import jdk.internal.console.SimpleConsoleReader.TerminalConfiguration; import static jdk.internal.console.WindowsTerminal.*; public class NativeConsoleReader { @@ -44,7 +45,12 @@ public static char[] readline(Reader reader, Writer out, boolean password) throw Reader in = new ConsoleInputStream(() -> { width.set(terminalWidth()); }); - return SimpleConsoleReader.doRead(in, out, password, firstLineOffset, () -> width.get()); + TerminalConfiguration terminalConfig = new TerminalConfiguration( + firstLineOffset, + VEOF, + VERASE, + () -> width.get()); + return SimpleConsoleReader.doRead(in, out, password, terminalConfig); } finally { restore(originalModes); } diff --git a/src/jdk.internal.le/windows/classes/jdk/internal/console/WindowsTerminal.java b/src/jdk.internal.le/windows/classes/jdk/internal/console/WindowsTerminal.java index 3abd06e97f030..eea88df28ba64 100644 --- a/src/jdk.internal.le/windows/classes/jdk/internal/console/WindowsTerminal.java +++ b/src/jdk.internal.le/windows/classes/jdk/internal/console/WindowsTerminal.java @@ -24,6 +24,9 @@ public class WindowsTerminal { public static final int LEFT_CTRL_PRESSED = 0x0008; public static final int SHIFT_PRESSED = 0x0010; + public static final int VEOF = 4; + public static final int VERASE = 127; + static { loadNativeLibrary(); } @@ -160,7 +163,7 @@ protected String getEscapeSequence(short keyCode, int keyState) { String escapeSequence = null; switch (keyCode) { case 0x08: // VK_BACK BackSpace - escapeSequence = "\u007F"; + escapeSequence = Character.toString(VERASE); break; case 0x09: return null; diff --git a/test/jdk/jdk/internal/io/JdkConsoleImplConsoleTest.java b/test/jdk/jdk/internal/io/JdkConsoleImplConsoleTest.java index ea89deea00d01..47295968cdb57 100644 --- a/test/jdk/jdk/internal/io/JdkConsoleImplConsoleTest.java +++ b/test/jdk/jdk/internal/io/JdkConsoleImplConsoleTest.java @@ -41,6 +41,7 @@ import java.util.stream.Collectors; import jdk.internal.console.SimpleConsoleReader; +import jdk.internal.console.SimpleConsoleReader.TerminalConfiguration; public class JdkConsoleImplConsoleTest { public static void main(String... args) throws IOException { @@ -59,7 +60,10 @@ private void testNavigation() throws IOException { 12345\033[D\033[D\033[3~6\033[1~7\033[4~8\033[H9\033[FA\r """; String expectedResult = "97123658A"; - char[] read = SimpleConsoleReader.doRead(new StringReader(input), new StringWriter(), false, 0, () -> Integer.MAX_VALUE); + char[] read = SimpleConsoleReader.doRead(new StringReader(input), + new StringWriter(), + false, + createTerminalConfig(Integer.MAX_VALUE)); assertEquals(expectedResult, new String(read)); } @@ -67,7 +71,10 @@ private void testTerminalHandling() throws IOException { Terminal terminal = new Terminal(5, 5); Thread.ofVirtual().start(() -> { try { - SimpleConsoleReader.doRead(terminal.getInput(), terminal.getOutput(), false, 0, () -> terminal.width); + SimpleConsoleReader.doRead(terminal.getInput(), + terminal.getOutput(), + false, + createTerminalConfig(terminal.width)); } catch (IOException ex) { throw new IllegalStateException(ex); } @@ -97,7 +104,10 @@ private void testSurrogates() throws IOException { 1\uD83D\uDE032 """; String expectedResult = "1\uD83D\uDE032"; - char[] read = SimpleConsoleReader.doRead(new StringReader(input), new StringWriter(), false, 0, () -> Integer.MAX_VALUE); + char[] read = SimpleConsoleReader.doRead(new StringReader(input), + new StringWriter(), + false, + createTerminalConfig(Integer.MAX_VALUE)); assertEquals(expectedResult, new String(read)); } { @@ -105,7 +115,10 @@ private void testSurrogates() throws IOException { 1\uD83D\uDE032\u007F\u007F3 """; String expectedResult = "13"; - char[] read = SimpleConsoleReader.doRead(new StringReader(input), new StringWriter(), false, 0, () -> Integer.MAX_VALUE); + char[] read = SimpleConsoleReader.doRead(new StringReader(input), + new StringWriter(), + false, + createTerminalConfig(Integer.MAX_VALUE)); assertEquals(expectedResult, new String(read)); } @@ -113,7 +126,10 @@ private void testSurrogates() throws IOException { Terminal terminal = new Terminal(5, 5); Thread.ofVirtual().start(() -> { try { - SimpleConsoleReader.doRead(terminal.getInput(), terminal.getOutput(), false, 0, () -> terminal.width); + SimpleConsoleReader.doRead(terminal.getInput(), + terminal.getOutput(), + false, + createTerminalConfig(terminal.width)); } catch (IOException ex) { throw new IllegalStateException(ex); } @@ -141,7 +157,10 @@ private void testSurrogates() throws IOException { Terminal terminal = new Terminal(5, 5); Thread.ofVirtual().start(() -> { try { - SimpleConsoleReader.doRead(terminal.getInput(), terminal.getOutput(), false, 0, () -> terminal.width); + SimpleConsoleReader.doRead(terminal.getInput(), + terminal.getOutput(), + false, + createTerminalConfig(terminal.width)); } catch (IOException ex) { throw new IllegalStateException(ex); } @@ -171,7 +190,10 @@ private void testWraps() throws IOException { Terminal terminal = new Terminal(5, 5); Thread.ofVirtual().start(() -> { try { - SimpleConsoleReader.doRead(terminal.getInput(), terminal.getOutput(), false, 0, () -> terminal.width); + SimpleConsoleReader.doRead(terminal.getInput(), + terminal.getOutput(), + false, + createTerminalConfig(terminal.width)); } catch (IOException ex) { throw new IllegalStateException(ex); } @@ -188,6 +210,10 @@ private void testWraps() throws IOException { } } + private static TerminalConfiguration createTerminalConfig(int width) { + return new TerminalConfiguration(0, 4, 127, () -> width); + } + private static void assertEquals(Object expected, Object actual) { if (!Objects.equals(expected, actual)) { throw new AssertionError("expected: " + expected + From d75641e28a723f6cbaca57dafe21dc38f95b20ad Mon Sep 17 00:00:00 2001 From: Jan Lahoda Date: Wed, 16 Apr 2025 10:48:40 +0200 Subject: [PATCH 6/6] Reflecting review feedback: Adding makefile comment as suggested --- make/modules/jdk.internal.le/Lib.gmk | 3 +++ .../classes/jdk/internal/console/NativeConsoleReader.java | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/make/modules/jdk.internal.le/Lib.gmk b/make/modules/jdk.internal.le/Lib.gmk index efe84664f9554..4c572a443459e 100644 --- a/make/modules/jdk.internal.le/Lib.gmk +++ b/make/modules/jdk.internal.le/Lib.gmk @@ -28,6 +28,9 @@ include LibCommon.gmk ################################################################################ ifeq ($(call isTargetOs, linux macosx windows), true) + ############################################################################## + ## Build lible + ############################################################################## $(eval $(call SetupJdkLibrary, BUILD_LIBLE, \ NAME := le, \ diff --git a/src/jdk.internal.le/windows/classes/jdk/internal/console/NativeConsoleReader.java b/src/jdk.internal.le/windows/classes/jdk/internal/console/NativeConsoleReader.java index 22715b52fd040..558b8d83e644b 100644 --- a/src/jdk.internal.le/windows/classes/jdk/internal/console/NativeConsoleReader.java +++ b/src/jdk.internal.le/windows/classes/jdk/internal/console/NativeConsoleReader.java @@ -34,7 +34,7 @@ public class NativeConsoleReader { public static boolean isSupported() { - return supported; + return true; } public static char[] readline(Reader reader, Writer out, boolean password) throws IOException {