Skip to content
Merged
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
28 changes: 24 additions & 4 deletions builtins/src/main/java/org/jline/builtins/ScreenTerminal.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

import org.jline.utils.Colors;
Expand Down Expand Up @@ -1617,6 +1618,28 @@
}
}

/**
* Waits for the screen to become dirty, up to the given timeout.
* Uses a while loop to guard against spurious wakeups.
*
* @param timeout maximum time to wait in milliseconds; if {@code <= 0}, returns immediately
* @return true if the screen is dirty
* @throws InterruptedException if interrupted while waiting
*/
public synchronized boolean waitDirty(long timeout) throws InterruptedException {
if (!dirty.get() && timeout > 0) {
long deadlineNanos = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(timeout);
while (!dirty.get()) {
long remainingMillis = TimeUnit.NANOSECONDS.toMillis(deadlineNanos - System.nanoTime());
if (remainingMillis <= 0) {
break;
}
wait(remainingMillis);
}
}
return dirty.compareAndSet(true, false);
}

protected synchronized void setDirty() {
dirty.set(true);
notifyAll();
Expand Down Expand Up @@ -1918,11 +1941,8 @@
}
}

public synchronized String dump(long timeout, boolean forceDump) throws InterruptedException {

Check failure on line 1944 in builtins/src/main/java/org/jline/builtins/ScreenTerminal.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this method to reduce its Cognitive Complexity from 55 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=jline_jline3&issues=AZ3O_uZaX4c4cpGbXttf&open=AZ3O_uZaX4c4cpGbXttf&pullRequest=1841
if (!dirty.get() && timeout > 0) {
wait(timeout);
}
if (dirty.compareAndSet(true, false) || forceDump) {
if (waitDirty(timeout) || forceDump) {
StringBuilder sb = new StringBuilder();
int prev_attr = -1;
int cx = Math.min(this.cx, width - 1);
Expand Down
62 changes: 62 additions & 0 deletions builtins/src/test/java/org/jline/builtins/ScreenTerminalTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
*/
package org.jline.builtins;

import java.time.Duration;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;
Expand All @@ -17,6 +19,66 @@
*/
public class ScreenTerminalTest {

/**
* isDirty() returns true after construction (initial dirty state),
* then false on subsequent call, then true again after write.
*/
@Test
public void testDirtyFlag() {

Check warning on line 27 in builtins/src/test/java/org/jline/builtins/ScreenTerminalTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this 'public' modifier.

See more on https://sonarcloud.io/project/issues?id=jline_jline3&issues=AZ3O_uZ6X4c4cpGbXttg&open=AZ3O_uZ6X4c4cpGbXttg&pullRequest=1841
ScreenTerminal terminal = new ScreenTerminal(80, 24);

assertTrue(terminal.isDirty(), "Should be dirty after construction");
assertFalse(terminal.isDirty(), "Should not be dirty after consuming");

terminal.write("X");
assertTrue(terminal.isDirty(), "Should be dirty after write");
assertFalse(terminal.isDirty(), "Should not be dirty after consuming again");
}

/**
* dump(timeout, forceDump=true) must still wait for the timeout before dumping
* when the screen is not dirty. This prevents busy-loop spinning when callers
* use forceDump in a loop.
* Regression: #1768
*/
@Test
void testForceDumpWaitsForTimeout() {
ScreenTerminal terminal = new ScreenTerminal(10, 3);
terminal.write("Hello");

// Consume the dirty flag
terminal.isDirty();

long minWaitMs = 200;
long start = System.nanoTime();
assertTimeoutPreemptively(Duration.ofSeconds(5), () -> {
terminal.dump(minWaitMs, true);
});
long elapsedMs = (System.nanoTime() - start) / 1_000_000;

assertTrue(
elapsedMs >= minWaitMs / 2,
"forceDump should still wait for timeout when not dirty, but returned in " + elapsedMs + "ms");
}

/**
* waitDirty(0) must return immediately rather than blocking indefinitely.
* Regression: Object.wait(0) waits forever, so timeout==0 must skip the wait.
*/
@Test
void testWaitDirtyZeroTimeoutReturnsImmediately() {
ScreenTerminal terminal = new ScreenTerminal(80, 24);

// Consume the initial dirty flag
terminal.isDirty();

// waitDirty(0) on a non-dirty screen must return false immediately
assertTimeoutPreemptively(Duration.ofSeconds(1), () -> {
boolean result = terminal.waitDirty(0);
assertFalse(result, "Non-dirty screen should return false");
});
}

/**
* Test for issue #1206: Missing history length check in ScreenTerminal
* This test verifies that when the terminal is resized, history lines are properly
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1177,13 +1177,18 @@ private Object doc(CommandInput input) {
}

private boolean urlExists(String weburl) {
HttpURLConnection huc = null;
try {
URL url = URI.create(weburl).toURL();
HttpURLConnection huc = (HttpURLConnection) url.openConnection();
huc = (HttpURLConnection) url.openConnection();
huc.setRequestMethod("HEAD");
return huc.getResponseCode() == HttpURLConnection.HTTP_OK;
} catch (Exception e) {
return false;
} finally {
if (huc != null) {
huc.disconnect();
}
}
}

Expand Down
9 changes: 5 additions & 4 deletions jansi-core/src/main/java/org/jline/jansi/AnsiConsole.java
Original file line number Diff line number Diff line change
Expand Up @@ -193,10 +193,11 @@ public static void setTerminal(Terminal terminal) {
static synchronized void doInstall() {
try {
if (terminal == null) {
TerminalBuilder builder = TerminalBuilder.builder()
.system(true)
.name("jansi")
.providers(System.getProperty(JANSI_PROVIDERS));
TerminalBuilder builder = TerminalBuilder.builder().system(true).name("jansi");
String providers = System.getProperty(JANSI_PROVIDERS);
if (providers != null) {
builder.providers(providers);
}
String graceful = System.getProperty(JANSI_GRACEFUL);
if (graceful != null) {
builder.dumb(Boolean.parseBoolean(graceful));
Expand Down
4 changes: 2 additions & 2 deletions reader/src/main/java/org/jline/keymap/KeyMap.java
Original file line number Diff line number Diff line change
Expand Up @@ -621,7 +621,7 @@ private static <T> T unbind(KeyMap<T> map, CharSequence keySeq) {
if (keySeq != null && keySeq.length() > 0) {
for (int i = 0; i < keySeq.length() - 1; i++) {
char c = keySeq.charAt(i);
if (c > map.mapping.length) {
if (c >= map.mapping.length) {
return null;
}
if (!(map.mapping[c] instanceof KeyMap)) {
Expand All @@ -631,7 +631,7 @@ private static <T> T unbind(KeyMap<T> map, CharSequence keySeq) {
map = (KeyMap<T>) map.mapping[c];
}
char c = keySeq.charAt(keySeq.length() - 1);
if (c > map.mapping.length) {
if (c >= map.mapping.length) {
return null;
}
if (map.mapping[c] instanceof KeyMap) {
Expand Down
4 changes: 3 additions & 1 deletion reader/src/main/java/org/jline/reader/impl/KillRing.java
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,9 @@ public String yankPop() {
lastKill = false;
if (lastYank) {
prev();
return slots[head];
if (head >= 0) {
return slots[head];
}
}
return null;
}
Expand Down
27 changes: 27 additions & 0 deletions reader/src/test/java/org/jline/keymap/KeyMapTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,33 @@
assertEquals(1, remaining[0]);
}

@Test
public void testUnbindBoundaryChar() {

Check warning on line 126 in reader/src/test/java/org/jline/keymap/KeyMapTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this 'public' modifier.

See more on https://sonarcloud.io/project/issues?id=jline_jline3&issues=AZ3O_uagX4c4cpGbXtti&open=AZ3O_uagX4c4cpGbXtti&pullRequest=1841
// KEYMAP_LENGTH is 128, so a char with value 128 is at the boundary
// (i.e., index == mapping.length). Before the fix, unbind() used ">"
// instead of ">=" for the bounds check, which would let c == mapping.length
// pass the check and then throw ArrayIndexOutOfBoundsException.
KeyMap<String> map = new KeyMap<>();
char boundaryChar = (char) KeyMap.KEYMAP_LENGTH;

// Unbinding a single-char sequence at the boundary should return null gracefully
// (no ArrayIndexOutOfBoundsException)
map.unbind(String.valueOf(boundaryChar));

// Unbinding a multi-char sequence where the final char is at the boundary
map.bind("action", "a");
map.unbind("a" + boundaryChar);

// Unbinding a multi-char sequence where an intermediate char is at the boundary
map.unbind(boundaryChar + "a");

// Verify that normal unbind still works correctly
map.bind("test", "x");
assertEquals("test", map.getBound("x"));
map.unbind("x");
assertNull(map.getBound("x"));
}

@Test
public void testSort() {
List<String> strings = new ArrayList<>();
Expand Down
12 changes: 12 additions & 0 deletions reader/src/test/java/org/jline/reader/impl/KillRingTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,18 @@
assertEquals(yanked, "baz");
}

@Test
public void testYankPopOnEmptyRingDoesNotThrow() {

Check warning on line 131 in reader/src/test/java/org/jline/reader/impl/KillRingTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this 'public' modifier.

See more on https://sonarcloud.io/project/issues?id=jline_jline3&issues=AZ3O_uaOX4c4cpGbXtth&open=AZ3O_uaOX4c4cpGbXtth&pullRequest=1841
// When the ring is empty, yank() sets lastYank=true and returns null.
// A subsequent yankPop() calls prev() which sets head to -1 when all
// slots are null. Without a bounds check this would throw
// ArrayIndexOutOfBoundsException.
KillRing killRing = new KillRing();
killRing.yank(); // sets lastYank = true, returns null
String yanked = killRing.yankPop();
assertNull(yanked);
}

// Those tests are run using a buffer.

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,14 @@
import org.junit.jupiter.api.condition.EnabledOnOs;
import org.junit.jupiter.api.condition.OS;

import static org.junit.jupiter.api.Assertions.assertNotNull;

public class FfmTest {

@Test
@DisabledOnOs(OS.WINDOWS) // non system terminals are not supported on windows
public void testNewTerminalWithNull() throws IOException {
Terminal terminal = new FfmTerminalProvider()
try (Terminal terminal = new FfmTerminalProvider()
.newTerminal(
"name",
"xterm",
Expand All @@ -36,14 +38,15 @@ public void testNewTerminalWithNull() throws IOException {
Terminal.SignalHandler.SIG_DFL,
false,
null,
null);
// terminal.close();
null)) {
assertNotNull(terminal);
}
}

@Test
@DisabledOnOs(OS.WINDOWS) // non system terminals are not supported on windows
public void testNewTerminalNoNull() throws IOException {
Terminal terminal = new FfmTerminalProvider()
try (Terminal terminal = new FfmTerminalProvider()
.newTerminal(
"name",
"xterm",
Expand All @@ -53,9 +56,10 @@ public void testNewTerminalNoNull() throws IOException {
Terminal.SignalHandler.SIG_DFL,
false,
new Attributes(),
new Size());
Size size = terminal.getSize();
// terminal.close();
new Size())) {
assertNotNull(terminal);
assertNotNull(terminal.getSize());
}
}

@Test
Expand Down
4 changes: 2 additions & 2 deletions terminal/src/main/java/org/jline/terminal/Size.java
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ public int getColumns() {
* @see #getColumns()
*/
public void setColumns(int columns) {
cols = (short) columns;
cols = columns;
}

/**
Expand Down Expand Up @@ -132,7 +132,7 @@ public int getRows() {
* @see #getRows()
*/
public void setRows(int rows) {
this.rows = (short) rows;
this.rows = rows;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@
}
}

private int parseColorResponse(NonBlockingReader reader, int colorType) throws IOException {
int parseColorResponse(NonBlockingReader reader, int colorType) throws IOException {

Check failure on line 174 in terminal/src/main/java/org/jline/terminal/impl/AbstractPosixTerminal.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this method to reduce its Cognitive Complexity from 28 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=jline_jline3&issues=AZ3O_uYPX4c4cpGbXtte&open=AZ3O_uYPX4c4cpGbXtte&pullRequest=1841
if (reader.peek(50) < 0) {
return -1;
}
Expand Down Expand Up @@ -209,6 +209,9 @@
List<String> rgb = new ArrayList<>();
while (true) {
int c = reader.read(10);
if (c < 0) {
return -1;
}
if (c == '\007') {
rgb.add(sb.toString());
break;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ public void raise(Signal signal) {
public void processInputByte(int c) throws IOException {
boolean flushOut = doProcessInputByte(c);
slaveInputPipe.flush();
if (flushOut) {
if (flushOut && masterOutput != null) {
masterOutput.flush();
}
}
Expand All @@ -270,7 +270,7 @@ public void processInputBytes(byte[] input, int offset, int length) throws IOExc
flushOut |= doProcessInputByte(input[offset + i]);
}
slaveInputPipe.flush();
if (flushOut) {
if (flushOut && masterOutput != null) {
masterOutput.flush();
}
}
Expand Down Expand Up @@ -329,6 +329,9 @@ protected boolean doProcessInputByte(int c) throws IOException {
* @throws IOException if anything wrong happens
*/
protected void processOutputByte(int c) throws IOException {
if (masterOutput == null) {
return;
}
if (attributes.getOutputFlag(OutputFlag.OPOST)) {
if (c == '\n') {
if (attributes.getOutputFlag(OutputFlag.ONLCR)) {
Expand Down Expand Up @@ -395,12 +398,16 @@ public void write(byte[] b, int off, int len) throws IOException {

@Override
public void flush() throws IOException {
masterOutput.flush();
if (masterOutput != null) {
masterOutput.flush();
}
}

@Override
public void close() throws IOException {
masterOutput.close();
if (masterOutput != null) {
masterOutput.close();
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,20 @@ public PrintWriter writer() {

@Override
protected void doClose() throws IOException {
// Close the slave output to signal EOF on the master side, which
// reliably unblocks pumpOut's masterInput.read() on all platforms
// (including macOS where closing the master fd from another thread
// may not unblock a blocked read on the same fd).
try {
output.close();
} catch (IOException e) {
// ignore
}
try {
masterInput.close();
} catch (IOException e) {
// ignore
}
super.doClose();
reader.close();
}
Expand Down
Loading
Loading