Fix #1575: Enforce terminal closure with backward compatibility (v3)#1577
Fix #1575: Enforce terminal closure with backward compatibility (v3)#1577gnodet merged 13 commits intojline:jline-3.xfrom
Conversation
…s (JLine 3.x) - Add closed state checking to NonBlockingReader and NonBlockingInputStream base classes - Soft mode is default (backward compatible): logs WARNING instead of throwing exception - Strict mode can be enabled with -Djline.terminal.strictClose=true - Update all concrete implementations to call checkClosed() in read methods - Remove ClosedChecking* wrapper classes (no longer needed) - Update terminal implementations to use base classes directly - Add StrictCloseTest with tests marked as @disabled (strict mode not default in 3.x) - HeldStreamReferenceTest tests soft mode behavior (default in 3.x)
…OSE constant - Added PROP_STRICT_CLOSE constant to TerminalBuilder for system property name - Updated NonBlockingInputStream and NonBlockingReader to use the constant - Added checkClosed() method to AbstractTerminal for consistent error handling - Replaced inline closed checks in Terminal implementations with checkClosed() calls - Reduces code duplication and improves maintainability
🤖 Augment PR SummarySummary: This PR addresses #1575 by enforcing terminal closure while keeping JLine 3.x backward-compatible by default. Changes:
Technical Notes: Strict mode is opt-in via 🤖 Was this summary useful? React with 👍 or 👎 |
| } | ||
|
|
||
| protected void doClose() throws IOException { | ||
| closed = true; |
There was a problem hiding this comment.
Because closed is set before status.close(), any Status cleanup that writes via terminal.puts()/writer() can now throw IllegalStateException (many writer()/output()/... methods call checkClosed()), causing Terminal.close() to fail partway through. This can leave resources/hooks unclosed when getStatus() was used.
Severity: high
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.
There was a problem hiding this comment.
✅ RESOLVED - This issue has been fixed. The closed = true assignment has been moved to AFTER status.close() (line 176 in AbstractTerminal.java). Additionally, we removed checkClosed() calls from getter methods like writer() and reader(), so status.close() can safely call terminal.puts()/writer() during cleanup without throwing exceptions.
| * @throws IOException if anything wrong happens | ||
| */ | ||
| public synchronized int read(long timeout, boolean isPeek) throws IOException { | ||
| checkClosed(); |
There was a problem hiding this comment.
read() now calls checkClosed(), but this class’s close() override never sets NonBlockingInputStream.closed, so checkClosed() won’t ever detect closure (and strict/soft close behavior won’t engage). This seems likely to make -Djline.terminal.strictClose ineffective for the most common input stream implementation.
Severity: medium
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.
There was a problem hiding this comment.
✅ RESOLVED - This issue has been fixed. NonBlockingInputStreamImpl.close() now calls super.close() at line 78, which properly sets the closed flag in the parent NonBlockingInputStream class. The checkClosed() method will now correctly detect closure and enforce strict/soft close behavior.
| * read timed out. | ||
| */ | ||
| protected synchronized int read(long timeout, boolean isPeek) throws IOException { | ||
| checkClosed(); |
There was a problem hiding this comment.
read() now calls checkClosed(), but NonBlockingReaderImpl.close() doesn’t set NonBlockingReader.closed, so the new strict/soft close behavior may never trigger for this implementation. That can also make the warning/ClosedException behavior inconsistent across NonBlockingReader implementations.
Severity: medium
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.
There was a problem hiding this comment.
✅ RESOLVED - This issue has been fixed. NonBlockingReaderImpl.close() now calls super.close() at line 81, which properly sets the closed flag in the parent NonBlockingReader class. The checkClosed() method will now correctly detect closure and enforce strict/soft close behavior consistently across all NonBlockingReader implementations.
| needOpt = null; | ||
| } else if (!arg.startsWith("-") | ||
| || (arg.length() > 1 && Character.isDigit(arg.charAt(1))) | ||
| || (arg.length() > 2 && Character.isDigit(arg.charAt(1)) && Character.isDigit(arg.charAt(2))) |
There was a problem hiding this comment.
The new numeric-argument heuristic only treats -<digit><digit>... as a non-option; negative numbers like -1.5 or -1e3 would now be parsed as options, which may break existing callers passing numeric args. If that’s not intended, the digit check may need to account for additional numeric formats.
Severity: medium
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.
There was a problem hiding this comment.
❌ NOT PART OF THIS PR - The file builtins/src/main/java/org/jline/builtins/Options.java is not modified in this PR. This PR focuses on terminal closure behavior and the jline.terminal.strictClose property. This comment appears to be related to a different change or may have been flagged incorrectly.
| LineReader reader1 = LineReaderBuilder.builder().build(); | ||
| assertFalse(reader1.isSet(LineReader.Option.DISABLE_EVENT_EXPANSION)); | ||
|
|
||
| System.setProperty("org.jline.reader.props.disable-event-expansion", "on"); |
There was a problem hiding this comment.
This test clears org.jline.reader.props.disable-event-expansion in finally, which can erase a pre-existing value set by the build environment and introduce cross-test interference. It may be safer to capture and restore the previous value instead of always clearing.
Severity: low
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.
There was a problem hiding this comment.
❌ NOT PART OF THIS PR - The file reader/src/test/java/org/jline/reader/impl/SystemOptionsTest.java is not modified in this PR. This PR focuses on terminal closure behavior and the jline.terminal.strictClose property. This comment appears to be related to a different change or may have been flagged incorrectly.
…m close() methods - Move 'closed = true' to after status.close() in AbstractTerminal.doClose() to prevent IllegalStateException during cleanup - Add super.close() calls in NonBlockingInputStreamImpl and NonBlockingReaderImpl to properly set the closed flag for strict/soft close behavior - Apply code formatting fixes
- Fix DumbTerminal anonymous NonBlockingInputStream to properly close underlying stream This ensures the background thread in NonBlockingInputStreamImpl is shut down when the terminal closes, preventing JVM hangs - Add checkClosed() calls to NonBlockingInputStream and NonBlockingReader subclasses for consistency: - NonBlockingPumpInputStream.readBuffered() - NonBlockingInputStreamReader.read() (inner class in NonBlocking.java) This ensures all stream implementations consistently check for closed state before performing operations, providing uniform behavior across the codebase.
Wrap system streams (FileDescriptor.in/out/err) in non-closeable wrappers to prevent closing the underlying file descriptors when the terminal is closed. This ensures that: - System.in/out/err remain usable after closing a system terminal - Multiple system terminals can be created sequentially - Surefire communication channel is not corrupted by closing System.out Fixes SystemOutCloseTest failure on GitHub Actions.
Changed strictClose from a static final field to an instance final field in NonBlockingInputStream and NonBlockingReader. The value is now determined at construction time from the system property, allowing tests to set the property before creating streams. This improves testability and allows different stream instances to have different strictClose settings based on when they were created. Benefits: - Tests can verify both strict and soft close mode behavior - Different stream instances can have different strictClose settings - More flexible and testable design
|
augment review |
| * System property to control strict close mode for terminals and streams. | ||
| * <p> | ||
| * In JLine 3.x, soft mode is enabled by default for backward compatibility. | ||
| * When set to "true", accessing closed terminals or streams will throw a ClosedException. |
There was a problem hiding this comment.
The Javadoc says strict close makes "closed terminals" throw ClosedException, but terminal-level checks in AbstractTerminal.checkClosed() throw IllegalStateException regardless of PROP_STRICT_CLOSE (the property only affects NonBlocking* streams). Consider aligning this doc with the actual exception/behavior.
Severity: low
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.
|
|
||
| @Override | ||
| public int read(long timeout, boolean isPeek) throws IOException { | ||
| checkClosed(); |
There was a problem hiding this comment.
checkClosed() is now called, but close() in this class doesn't call super.close(), so the closed flag used by checkClosed() is never set and strict/soft-close behavior won't trigger here. This makes strict-close enforcement/warnings inconsistent for NonBlockingReaderInputStream/NonBlockingInputStreamReader.
Severity: medium
Other Locations
terminal/src/main/java/org/jline/utils/NonBlocking.java:182
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.
|
|
||
| @Override | ||
| public synchronized int read(long timeout, boolean isPeek) throws IOException { | ||
| checkClosed(); |
There was a problem hiding this comment.
This class has its own closed field/close() implementation and does not update the inherited NonBlockingInputStream.closed, so the newly-added checkClosed() call won’t ever observe closure (and strict/soft-close behavior won’t apply). This also creates two independent "closed" states (wait() uses the local one, checkClosed() uses the inherited one).
Severity: medium
Other Locations
terminal/src/main/java/org/jline/utils/NonBlockingPumpInputStream.java:108terminal/src/main/java/org/jline/utils/NonBlockingPumpReader.java:71
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.
| * In JLine 3.x, the default behavior is "soft close": accessing streams after terminal closure | ||
| * logs a WARNING but does not throw an exception, preserving backward compatibility. | ||
| * When the system property {@code jline.terminal.strictClose=true} is set, accessing closed | ||
| * terminal streams throws {@code IllegalStateException}. |
There was a problem hiding this comment.
This Javadoc says strict close makes accessing closed terminal streams throw IllegalStateException, but stream-level strict-close appears to throw ClosedException (and terminal-level access throws IllegalStateException regardless of the property). It may be worth clarifying which APIs throw which exception in strict mode.
Severity: low
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.
| } | ||
|
|
||
| /** | ||
| * Demonstrates the enhanced behavior: held references also throw after terminal close. |
There was a problem hiding this comment.
This demonstration claims held references "should now throw" IllegalStateException, but the implemented behavior is soft-close warnings by default and ClosedException for strict mode (and PrintWriter typically won’t throw on writes even when closed). As-is, running this demo would likely misrepresent the new close semantics.
Severity: low
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.
- Fix TerminalBuilder javadoc to clarify terminal-level vs stream-level exceptions - Fix NonBlocking wrapper classes to call super.close() so checkClosed() works properly - Fix NonBlockingPumpInputStream and NonBlockingPumpReader to call super.close() to update inherited closed field - Update HeldStreamReferenceTest javadoc to clarify exception types These changes improve documentation accuracy and ensure the checkClosed() mechanism works correctly across all NonBlocking implementations.
…iplineTerminal - Removed slaveInputPump field which was redundant with slaveInput - Changed slaveInput type from NonBlockingInputStream to NonBlockingPumpInputStream - Removed empty try block in doClose() This simplifies the code while maintaining the same functionality.
Since reader.close() delegates to input.close() and close() is idempotent, we can simplify the cleanup logic by just calling reader.close(). This is clearer and more maintainable than manually closing the input and shutting down the reader separately. This brings PosixSysTerminal in line with PosixPtyTerminal and DumbTerminal which already use this simplified pattern.
Updated documentation to match v4 with comprehensive explanation of: - Two levels of closure enforcement (terminal-level vs stream-level) - Property values and their behavior (strict vs soft mode) - Default values for JLine 3.x vs 4.x - Example code demonstrating the difference - Cross-references to related classes
Changed from boolean strictClose property to string-based closeMode with three modes: - strict: Throw ClosedException - warn: Log warning but continue (default for v3) - lenient: Silently allow access (no warning, no exception) Changes: - Renamed PROP_STRICT_CLOSE to PROP_CLOSE_MODE (old constant deprecated) - Property name: jline.terminal.strictClose -> jline.terminal.closeMode - Backward compatibility: old property still works (true->strict, false->warn) - Updated NonBlockingInputStream and NonBlockingReader to use enum-based mode - Updated all tests to use new property name and values - Added comprehensive javadoc explaining all three modes
Since this hasn't been released yet, we can remove the deprecated constant and backward compatibility code entirely. This simplifies the implementation to only use the new PROP_CLOSE_MODE property.
Summary
Fixes #1575 by implementing terminal closure enforcement in JLine 3.x with backward compatibility by default. After a terminal is closed, accessing its streams will log warnings (soft mode) or throw exceptions (strict mode via system property).
Problem
As reported in #1575, terminals could still be used after calling
close(), leading to unexpected behavior:Solution
Implemented two-level closure enforcement with backward compatibility:
1. Terminal-Level Checks
closedflag toAbstractTerminalcheckClosed()method that throwsIllegalStateException2. Stream-Level Checks (Backward Compatible)
closedflag andcheckClosed()toNonBlockingInputStreamandNonBlockingReader-Djline.terminal.strictClose=trueto throwClosedExceptionBehavior in JLine 3.x (This PR)
Soft mode by default (backward compatible) - accessing closed streams logs warnings:
Opt-in strict mode - set
-Djline.terminal.strictClose=trueto throw exceptions:Changes
System Property Constant
TerminalBuilder.java
PROP_STRICT_CLOSE = "jline.terminal.strictClose"constantPROP_*constants in TerminalBuilder)Core Implementation
AbstractTerminal.java
protected volatile boolean closedfieldcheckClosed()methodclosed = trueindoClose()Terminal Implementations (6 files)
PosixSysTerminal.java- Added checks to 4 methodsAbstractWindowsTerminal.java- Added check to 1 methodPosixPtyTerminal.java- Added checks to 4 methodsAbstractPosixTerminal.java- Added check to 1 methodDumbTerminal.java- Added checks to 8 methodsLineDisciplineTerminal.java- Added checks to 8 methodsNonBlocking Stream Classes
NonBlockingInputStream.java- Addedclosedflag,checkClosed()with STRICT_CLOSE logic, usesPROP_STRICT_CLOSEconstantNonBlockingReader.java- Addedclosedflag,checkClosed()with STRICT_CLOSE logic, usesPROP_STRICT_CLOSEconstantTests
StrictCloseTest.java (new)
-Djline.terminal.strictClose=true)ClosedExceptionin strict modeCode Quality Improvements
As part of this fix, also refactored repeated code:
PROP_STRICT_CLOSEconstant in TerminalBuilderif (closed)checks with calls tocheckClosed()Statistics
Backward Compatibility
✅ JLine 3.x: This maintains backward compatibility by default.
Default behavior (soft mode):
IllegalStateException✓Opt-in strict mode (
-Djline.terminal.strictClose=true):IllegalStateException✓ClosedException✓Migration recommendation:
Testing
Run strict mode tests:
mvn test -Dtest=StrictCloseTest -Djline.terminal.strictClose=trueRelated