Skip to content

ConsoleAppender with withJansi=true closes the JVM's stdout file descriptor on LoggerContext.reset() #1063

Description

@wf-r

Summary

With <withJansi>true</withJansi> on a ConsoleAppender, calling LoggerContext.reset() (or stop()) closes the JVM's stdout file descriptor, not merely the appender's own stream. Everything writing to stdout afterwards is silently lost, and the condition is not recoverable within the JVM.

Originally reported at LOGBACK-1759.

Environment

logback-classic / logback-core 1.3.16, slf4j-api 2.0.18, jansi 2.4.3, Java 8, Windows.
(The same code path exists unchanged on current master.)

Analysis

  1. LoggerContext.reset() → Logger.recursiveReset() → detachAndStopAllAppenders() → Appender.stop()

  2. OutputStreamAppender.stop() calls closeOutputStream(), which does this.outputStream.close().

  3. Without Jansi this is harmless by design: ConsoleAppender.start() uses ConsoleTarget.SystemOut.getStream(), an anonymous OutputStream that overrides only write(..) and flush(). Its close() is inherited from java.io.OutputStream and is a no-op. That is the existing safety net.

  4. With withJansi=true that safety net is bypassed. ConsoleAppender.wrapWithJansi() takes the Jansi-2 branch and discards targetStream entirely:

    // check for JAnsi 2
    if (optOutMethod.isPresent()) {
        return (PrintStream) outMethod.invoke(null);   // == AnsiConsole.out()
    }
    // JAnsi 1
    return (OutputStream) method.invoke(null, new PrintStream(targetStream));

    Only the Jansi-1 fallback still wraps the non-closing ConsoleTarget stream.

  5. AnsiConsole.out() returns the process-wide singleton built by ansiStream(true):

    FileDescriptor descriptor = stdout ? FileDescriptor.out : FileDescriptor.err;
    final OutputStream out = new FastBufferedOutputStream(new FileOutputStream(descriptor));

    i.e. it sits directly on FileDescriptor.out, not on System.out.

  6. AnsiPrintStream does not override close(). So PrintStream.close() → AnsiOutputStream.close() (uninstall(); super.close();) → FileOutputStream(FileDescriptor.out).close() → fd 1 is closed at OS level.

Why reconfiguration does not repair it

AnsiConsole.out is a static field whose recreation is guarded by the initialized flag in initStreams(), and close() does not clear that flag. A subsequent JoranConfigurator.doConfigure(..) therefore hands the new ConsoleAppender back the same, already closed AnsiPrintStream. System.setOut(..) does not help either, because the descriptor itself is gone.

Reproduction

logback.xml with a single ConsoleAppender using <withJansi>true</withJansi>, then:

LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
LoggerFactory.getLogger("x").info("hello");
System.out.println("before reset");               // visible
System.out.println(FileDescriptor.out.valid());   // true
lc.reset();
System.out.println("after reset");                // lost
System.err.println(FileDescriptor.out.valid());   // false

Practical impact

Test suites that reconfigure logback per test (JUnit/ScalaTest @Before/@After) must call reset() to avoid appender duplication on re-configuration. Doing so kills stdout for the rest of the run; with an unforked build tool it also kills the build tool's own console output, so the run aborts rather than merely losing log lines.

Suggested fix

ConsoleAppender should not close a stream it does not own. The most direct fix is to override closeOutputStream() in ConsoleAppender to flush only - this mirrors java.util.logging.ConsoleHandler.close(), which explicitly flushes without closing System.err, and it makes the withJansi case behave like the already-safe ConsoleTarget case. Alternatively, wrap the stream returned by AnsiConsole.out() in a non-closing delegate.

Note that master is still affected and additionally invokes AnsiConsole.systemInstall() in wrapWithJansi(), which makes the closed singleton the process-wide System.out as well.

Workarounds

  • Set withJansi to false. On Windows 10 1511+ the console interprets VT sequences natively, and build tools that install Jansi themselves keep colouring working through ConsoleTarget → System.out.
  • Or use a ConsoleAppender subclass overriding closeOutputStream() to flush only.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions