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
-
LoggerContext.reset() → Logger.recursiveReset() → detachAndStopAllAppenders() → Appender.stop()
-
OutputStreamAppender.stop() calls closeOutputStream(), which does this.outputStream.close().
-
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.
-
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.
-
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.
-
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.
Summary
With
<withJansi>true</withJansi>on aConsoleAppender, callingLoggerContext.reset()(orstop()) 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
LoggerContext.reset()→Logger.recursiveReset()→detachAndStopAllAppenders()→Appender.stop()OutputStreamAppender.stop()callscloseOutputStream(), which doesthis.outputStream.close().Without Jansi this is harmless by design:
ConsoleAppender.start()usesConsoleTarget.SystemOut.getStream(), an anonymousOutputStreamthat overrides onlywrite(..)andflush(). Itsclose()is inherited fromjava.io.OutputStreamand is a no-op. That is the existing safety net.With
withJansi=truethat safety net is bypassed.ConsoleAppender.wrapWithJansi()takes the Jansi-2 branch and discardstargetStreamentirely:Only the Jansi-1 fallback still wraps the non-closing
ConsoleTargetstream.AnsiConsole.out()returns the process-wide singleton built byansiStream(true):i.e. it sits directly on
FileDescriptor.out, not onSystem.out.AnsiPrintStreamdoes not overrideclose(). SoPrintStream.close()→AnsiOutputStream.close()(uninstall(); super.close();) →FileOutputStream(FileDescriptor.out).close()→ fd 1 is closed at OS level.Why reconfiguration does not repair it
AnsiConsole.outis a static field whose recreation is guarded by theinitializedflag ininitStreams(), andclose()does not clear that flag. A subsequentJoranConfigurator.doConfigure(..)therefore hands the newConsoleAppenderback the same, already closedAnsiPrintStream.System.setOut(..)does not help either, because the descriptor itself is gone.Reproduction
logback.xmlwith a singleConsoleAppenderusing<withJansi>true</withJansi>, then:Practical impact
Test suites that reconfigure logback per test (JUnit/ScalaTest
@Before/@After) must callreset()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
ConsoleAppendershould not close a stream it does not own. The most direct fix is to overridecloseOutputStream()inConsoleAppenderto flush only - this mirrorsjava.util.logging.ConsoleHandler.close(), which explicitly flushes without closingSystem.err, and it makes thewithJansicase behave like the already-safeConsoleTargetcase. Alternatively, wrap the stream returned byAnsiConsole.out()in a non-closing delegate.Note that
masteris still affected and additionally invokesAnsiConsole.systemInstall()inwrapWithJansi(), which makes the closed singleton the process-wideSystem.outas well.Workarounds
withJansitofalse. On Windows 10 1511+ the console interprets VT sequences natively, and build tools that install Jansi themselves keep colouring working throughConsoleTarget→System.out.ConsoleAppendersubclass overridingcloseOutputStream()to flush only.