Skip to content

8361613: System.console() should only be available for interactive terminal #26273

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/java.base/share/classes/java/lang/System.java
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@
import java.util.ResourceBundle;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.ScheduledExecutorService;
import java.util.function.Supplier;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Stream;
Expand Down Expand Up @@ -238,10 +237,11 @@ public static void setErr(PrintStream err) {
private static volatile Console cons;

/**
* Returns the unique {@link java.io.Console Console} object associated
* Returns the unique {@link Console Console} object associated
* with the current Java virtual machine, if any.
*
* @return The system console, if any, otherwise {@code null}.
* @see Console
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The method declaration already links to Console so I don't think we need another link in the "See also" section.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I missed it, but do we have anything to make it clear that it returns null if either stdin or stdout are redirected?

Copy link

@xuemingshen-oracle xuemingshen-oracle Jul 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we do have wordings like " If the virtual machine is started from an interactive command line without redirecting the standard input AND output streams then its console will exist ..." and "If no console device is
available then an invocation of that method will return null" from the very beginning. not very "straightforward" but i think it's clear enough?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think adding @see tag would be more helpful.

As to the spec wording wrt stdin/out, there is another issue filed to make it clearer: JDK-8361972. This PR addresses the implementation part only so that it can be backported to prior LTSes without spec change.

*
* @since 1.6
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public class JdkConsoleProviderImpl implements JdkConsoleProvider {
*/
@Override
public JdkConsole console(boolean isTTY, Charset inCharset, Charset outCharset) {
return new LazyDelegatingJdkConsoleImpl(inCharset, outCharset);
return isTTY ? new LazyDelegatingJdkConsoleImpl(inCharset, outCharset) : null;
}

private static class LazyDelegatingJdkConsoleImpl implements JdkConsole {
Expand Down
69 changes: 51 additions & 18 deletions test/jdk/java/io/Console/DefaultCharsetTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,33 +21,66 @@
* questions.
*/

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import java.nio.file.Files;
import java.nio.file.Paths;

import jdk.test.lib.process.OutputAnalyzer;
import jdk.test.lib.process.ProcessTools;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

import static jdk.test.lib.Utils.*;

/**
* @test
* @bug 8341975 8351435
* @bug 8341975 8351435 8361613
* @summary Tests the default charset. It should honor `stdout.encoding`
* which should be the same as System.out.charset()
* @modules jdk.internal.le
* @run junit/othervm -Djdk.console=jdk.internal.le -Dstdout.encoding=UTF-8 DefaultCharsetTest
* @run junit/othervm -Djdk.console=jdk.internal.le -Dstdout.encoding=ISO-8859-1 DefaultCharsetTest
* @run junit/othervm -Djdk.console=jdk.internal.le -Dstdout.encoding=US-ASCII DefaultCharsetTest
* @run junit/othervm -Djdk.console=jdk.internal.le -Dstdout.encoding=foo DefaultCharsetTest
* @run junit/othervm -Djdk.console=jdk.internal.le DefaultCharsetTest
* @requires (os.family == "linux") | (os.family == "mac")
* @library /test/lib
* @build jdk.test.lib.Utils
* jdk.test.lib.JDKToolFinder
* jdk.test.lib.process.ProcessTools
* @run junit DefaultCharsetTest
*/
public class DefaultCharsetTest {
@Test
public void testDefaultCharset() {
@BeforeAll
static void checkExpectAvailability() {
// check "expect" command availability
var expect = Paths.get("/usr/bin/expect");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We only need to check "expect" availability once, so we should move this check to a @BeforeAll static method. It's also more clear that this check is a precondition, and not part of the actual test. Applies to the other locations, but primarily the other parameterized tests.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. Modified to use @BeforeAll. For ModuleSelectionTest, one of the test is not using expect, so I left it as it is. In addition to that, I removed the @requires condition to allow that test to run on windows.

Assumptions.assumeTrue(Files.exists(expect) && Files.isExecutable(expect),
"'" + expect + "' not found. Test ignored.");
}
@ParameterizedTest
@ValueSource(strings = {"UTF-8", "ISO-8859-1", "US-ASCII", "foo", ""})
void testDefaultCharset(String stdoutEncoding) throws Exception {
// invoking "expect" command
OutputAnalyzer oa = ProcessTools.executeProcess(
"expect",
"-n",
TEST_SRC + "/defaultCharset.exp",
TEST_CLASSES,
TEST_JDK + "/bin/java",
"-Dstdout.encoding=" + stdoutEncoding,
getClass().getName());
oa.reportDiagnosticSummary();
oa.shouldHaveExitValue(0);
}

public static void main(String... args) {
var stdoutEncoding = System.getProperty("stdout.encoding");
var sysoutCharset = System.out.charset();
var consoleCharset = System.console().charset();
System.out.println("""
stdout.encoding = %s
System.out.charset() = %s
System.console().charset() = %s
""".formatted(stdoutEncoding, sysoutCharset.name(), consoleCharset.name()));
assertEquals(consoleCharset, sysoutCharset,
"Charsets for System.out and Console differ for stdout.encoding: %s".formatted(stdoutEncoding));
System.out.printf("""
stdout.encoding = %s
System.out.charset() = %s
System.console().charset() = %s
""", stdoutEncoding, sysoutCharset.name(), consoleCharset.name());
if (!consoleCharset.equals(sysoutCharset)) {
System.err.printf("Charsets for System.out and Console differ for stdout.encoding: %s%n", stdoutEncoding);
System.exit(-1);
}
}
}
125 changes: 71 additions & 54 deletions test/jdk/java/io/Console/LocaleTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,28 +21,40 @@
* questions.
*/

import java.io.File;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.Locale;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.function.Predicate;

import jdk.test.lib.process.OutputAnalyzer;
import jdk.test.lib.process.ProcessTools;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.Test;

import static jdk.test.lib.Utils.*;

/**
* @test
* @bug 8330276 8351435
* @bug 8330276 8351435 8361613
* @summary Tests Console methods that have Locale as an argument
* @requires (os.family == "linux") | (os.family == "mac")
* @library /test/lib
* @modules jdk.internal.le jdk.localedata
* @build jdk.test.lib.Utils
* jdk.test.lib.JDKToolFinder
* jdk.test.lib.process.ProcessTools
* @modules jdk.localedata
* @run junit LocaleTest
*/
public class LocaleTest {
private static Calendar TODAY = new GregorianCalendar(2024, Calendar.APRIL, 22);
private static String FORMAT = "%1$tY-%1$tB-%1$te %1$tA";
private static final Calendar TODAY = new GregorianCalendar(2024, Calendar.APRIL, 22);
private static final String FORMAT = "%1$tY-%1$tB-%1$te %1$tA";
// We want to limit the expected strings within US-ASCII charset, as
// the native encoding is determined as such, which is used by
// the `Process` class under jtreg environment.
private static List<String> EXPECTED = List.of(
private static final List<String> EXPECTED = List.of(
String.format(Locale.UK, FORMAT, TODAY),
String.format(Locale.FRANCE, FORMAT, TODAY),
String.format(Locale.GERMANY, FORMAT, TODAY),
Expand All @@ -53,56 +65,61 @@ public class LocaleTest {
String.format((Locale)null, FORMAT, TODAY)
);

public static void main(String... args) throws Throwable {
if (args.length == 0) {
// no arg will launch the child process that actually perform tests
var pb = ProcessTools.createTestJavaProcessBuilder(
"-Djdk.console=jdk.internal.le",
"LocaleTest", "dummy");
var input = new File(System.getProperty("test.src", "."), "input.txt");
pb.redirectInput(input);
var oa = ProcessTools.executeProcess(pb);
if (oa.getExitValue() == -1) {
System.out.println("System.console() returns null. Ignoring the test.");
} else {
var output = oa.asLines();
var resultText =
"""
Actual output: %s
Expected output: %s
""".formatted(output, EXPECTED);
if (!output.equals(EXPECTED)) {
throw new RuntimeException("Standard out had unexpected strings:\n" + resultText);
} else {
oa.shouldHaveExitValue(0);
System.out.println("Formatting with explicit Locale succeeded.\n" + resultText);
}
}
@Test
void testLocale() throws Exception {
// check "expect" command availability
var expect = Paths.get("/usr/bin/expect");
Assumptions.assumeTrue(Files.exists(expect) && Files.isExecutable(expect),
"'" + expect + "' not found. Test ignored.");

// invoking "expect" command
OutputAnalyzer oa = ProcessTools.executeProcess(
"expect",
"-n",
TEST_SRC + "/locale.exp",
TEST_CLASSES,
TEST_JDK + "/bin/java",
getClass().getName());

var stdout =
oa.stdoutAsLines().stream().filter(Predicate.not(String::isEmpty)).toList();
var resultText =
"""
Actual output: %s
Expected output: %s
""".formatted(stdout, EXPECTED);
if (!stdout.equals(EXPECTED)) {
throw new RuntimeException("Standard out had unexpected strings:\n" + resultText);
} else {
var con = System.console();
if (con != null) {
// tests these additional methods that take a Locale
con.format(Locale.UK, FORMAT, TODAY);
con.printf("\n");
con.printf(Locale.FRANCE, FORMAT, TODAY);
con.printf("\n");
con.readLine(Locale.GERMANY, FORMAT, TODAY);
con.printf("\n");
con.readPassword(Locale.of("es"), FORMAT, TODAY);
con.printf("\n");
oa.shouldHaveExitValue(0);
System.out.println("Formatting with explicit Locale succeeded.\n" + resultText);
}
}

public static void main(String... args) throws Throwable {
var con = System.console();
if (con != null) {
// tests these additional methods that take a Locale
con.format(Locale.UK, FORMAT, TODAY);
con.printf("\n");
con.printf(Locale.FRANCE, FORMAT, TODAY);
con.printf("\n");
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);
con.printf("\n");
con.printf((Locale)null, FORMAT, TODAY);
con.printf("\n");
con.readLine((Locale)null, FORMAT, TODAY);
con.printf("\n");
con.readPassword((Locale)null, FORMAT, TODAY);
} else {
// Exit with -1
System.exit(-1);
}
// tests null locale
con.format((Locale)null, FORMAT, TODAY);
con.printf("\n");
con.printf((Locale)null, FORMAT, TODAY);
con.printf("\n");
con.readLine((Locale)null, FORMAT, TODAY);
con.printf("\n");
con.readPassword((Locale)null, FORMAT, TODAY);
} else {
// Exit with -1
System.exit(-1);
}
}
}
69 changes: 58 additions & 11 deletions test/jdk/java/io/Console/ModuleSelectionTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,21 +23,71 @@

/**
* @test
* @bug 8295803 8299689 8351435
* @bug 8295803 8299689 8351435 8361613
* @summary Tests System.console() returns correct Console (or null) from the expected
* module.
* @modules java.base/java.io:+open
* @run main/othervm ModuleSelectionTest java.base
* @run main/othervm -Djdk.console=jdk.internal.le ModuleSelectionTest jdk.internal.le
* @run main/othervm -Djdk.console=java.base ModuleSelectionTest java.base
* @run main/othervm --limit-modules java.base ModuleSelectionTest java.base
* @library /test/lib
* @build jdk.test.lib.Utils
* jdk.test.lib.process.ProcessTools
* @run junit ModuleSelectionTest
*/

import java.io.Console;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.stream.Stream;

import jdk.test.lib.process.OutputAnalyzer;
import jdk.test.lib.process.ProcessTools;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

import static jdk.test.lib.Utils.*;

public class ModuleSelectionTest {
private static Stream<Arguments> options() {
return Stream.of(
Arguments.of("-Djdk.console=foo", "java.base"),
Arguments.of("-Djdk.console=java.base", "java.base"),
Arguments.of("-Djdk.console=jdk.internal.le", "jdk.internal.le"),
Arguments.of("--limit-modules java.base", "java.base")
);
}

@ParameterizedTest
@MethodSource("options")
void testNonTTY(String opts) throws Exception {
opts = opts +
" --add-opens java.base/java.io=ALL-UNNAMED ModuleSelectionTest null";
OutputAnalyzer output = ProcessTools.executeTestJava(opts.split(" "));
output.reportDiagnosticSummary();
output.shouldHaveExitValue(0);
}

@ParameterizedTest
@MethodSource("options")
void testTTY(String opts, String expected) throws Exception {
// check "expect" command availability
var expect = Paths.get("/usr/bin/expect");
Assumptions.assumeTrue(Files.exists(expect) && Files.isExecutable(expect),
"'" + expect + "' not found. Test ignored.");

opts = "expect -n " + TEST_SRC + "/moduleSelection.exp " +
TEST_CLASSES + " " +
expected + " " +
TEST_JDK + "/bin/java" +
" --add-opens java.base/java.io=ALL-UNNAMED "
+ opts;
// invoking "expect" command
OutputAnalyzer output = ProcessTools.executeProcess(opts.split(" "));
output.reportDiagnosticSummary();
output.shouldHaveExitValue(0);
}

public static void main(String... args) throws Throwable {
var con = System.console();
var pc = Class.forName("java.io.ProxyingConsole");
Expand All @@ -49,10 +99,7 @@ public static void main(String... args) throws Throwable {
.findGetter(pc, "delegate", jdkc)
.invoke(con) : null;

var expected = switch (args[0]) {
case "java.base" -> istty ? "java.base" : "null";
default -> args[0];
};
var expected = args[0];
var actual = con == null ? "null" : impl.getClass().getModule().getName();

if (!actual.equals(expected)) {
Expand All @@ -62,7 +109,7 @@ public static void main(String... args) throws Throwable {
Actual: %s
""".formatted(expected, actual));
} else {
System.out.printf("%s is the expected implementation. (tty: %s)\n", impl, istty);
System.out.printf("%s is the expected implementation. (tty: %s)\n", actual, istty);
}
}
}
Loading