diff --git a/terminal-ffm/src/main/java/org/jline/terminal/impl/ffm/CLibrary.java b/terminal-ffm/src/main/java/org/jline/terminal/impl/ffm/CLibrary.java index d042fc01d..ee2396e22 100644 --- a/terminal-ffm/src/main/java/org/jline/terminal/impl/ffm/CLibrary.java +++ b/terminal-ffm/src/main/java/org/jline/terminal/impl/ffm/CLibrary.java @@ -8,19 +8,36 @@ */ package org.jline.terminal.impl.ffm; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; import java.lang.foreign.*; import java.lang.invoke.MethodHandle; import java.lang.invoke.VarHandle; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; import java.util.EnumMap; import java.util.EnumSet; +import java.util.List; +import java.util.Optional; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import java.util.stream.Stream; import org.jline.terminal.Attributes; import org.jline.terminal.Size; import org.jline.terminal.spi.Pty; import org.jline.terminal.spi.TerminalProvider; +import org.jline.utils.OSUtils; @SuppressWarnings("preview") class CLibrary { + + private static final Logger logger = Logger.getLogger("org.jline"); + // Window sizes. // @see IOCTL_TTY(2) man-page static class winsize { @@ -379,44 +396,126 @@ public Attributes asAttributes() { static MethodHandle tcsetattr; static MethodHandle tcgetattr; static MethodHandle ttyname_r; + static LinkageError openptyError; static { // methods Linker linker = Linker.nativeLinker(); + SymbolLookup lookup = SymbolLookup.loaderLookup().or(linker.defaultLookup()); // https://man7.org/linux/man-pages/man2/ioctl.2.html ioctl = linker.downcallHandle( - linker.defaultLookup().find("ioctl").get(), + lookup.find("ioctl").get(), FunctionDescriptor.of( ValueLayout.JAVA_INT, ValueLayout.JAVA_INT, ValueLayout.JAVA_LONG, ValueLayout.ADDRESS), Linker.Option.firstVariadicArg(2)); // https://www.man7.org/linux/man-pages/man3/isatty.3.html isatty = linker.downcallHandle( - linker.defaultLookup().find("isatty").get(), - FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.JAVA_INT)); - // https://man7.org/linux/man-pages/man3/openpty.3.html - openpty = linker.downcallHandle( - linker.defaultLookup().find("openpty").get(), - FunctionDescriptor.of( - ValueLayout.JAVA_INT, - ValueLayout.ADDRESS, - ValueLayout.ADDRESS, - ValueLayout.ADDRESS, - ValueLayout.ADDRESS, - ValueLayout.ADDRESS)); + lookup.find("isatty").get(), FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.JAVA_INT)); // https://man7.org/linux/man-pages/man3/tcsetattr.3p.html tcsetattr = linker.downcallHandle( - linker.defaultLookup().find("tcsetattr").get(), + lookup.find("tcsetattr").get(), FunctionDescriptor.of( ValueLayout.JAVA_INT, ValueLayout.JAVA_INT, ValueLayout.JAVA_INT, ValueLayout.ADDRESS)); // https://man7.org/linux/man-pages/man3/tcgetattr.3p.html tcgetattr = linker.downcallHandle( - linker.defaultLookup().find("tcgetattr").get(), + lookup.find("tcgetattr").get(), FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.JAVA_INT, ValueLayout.ADDRESS)); // https://man7.org/linux/man-pages/man3/ttyname.3.html ttyname_r = linker.downcallHandle( - linker.defaultLookup().find("ttyname_r").get(), + lookup.find("ttyname_r").get(), FunctionDescriptor.of( ValueLayout.JAVA_INT, ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.JAVA_LONG)); + // https://man7.org/linux/man-pages/man3/openpty.3.html + LinkageError error = null; + Optional openPtyAddr = lookup.find("openpty"); + if (openPtyAddr.isEmpty()) { + StringBuilder sb = new StringBuilder(); + sb.append("Unable to find openpty native method in static libraries and unable to load the util library."); + List suppressed = new ArrayList<>(); + try { + System.loadLibrary("util"); + openPtyAddr = lookup.find("openpty"); + } catch (Throwable t) { + suppressed.add(t); + } + if (openPtyAddr.isEmpty()) { + String libUtilPath = System.getProperty("org.jline.ffm.libutil"); + if (libUtilPath != null && !libUtilPath.isEmpty()) { + try { + System.load(libUtilPath); + openPtyAddr = lookup.find("openpty"); + } catch (Throwable t) { + suppressed.add(t); + } + } + } + if (openPtyAddr.isEmpty() && OSUtils.IS_LINUX) { + String hwName; + try { + Process p = Runtime.getRuntime().exec(new String[] {"uname", "-m"}); + p.waitFor(); + try (InputStream in = p.getInputStream()) { + hwName = readFully(in).trim(); + Path libDir = Paths.get("/usr/lib", hwName + "-linux-gnu"); + try (Stream stream = Files.list(libDir)) { + List libs = stream.filter( + l -> l.getFileName().toString().startsWith("libutil.so.")) + .collect(Collectors.toList()); + for (Path lib : libs) { + try { + System.load(lib.toString()); + openPtyAddr = lookup.find("openpty"); + if (openPtyAddr.isPresent()) { + break; + } + } catch (Throwable t) { + suppressed.add(t); + } + } + } + } + } catch (Throwable t) { + suppressed.add(t); + } + } + if (openPtyAddr.isEmpty()) { + for (Throwable t : suppressed) { + sb.append("\n\t- ").append(t.toString()); + } + error = new LinkageError(sb.toString()); + suppressed.forEach(error::addSuppressed); + if (logger.isLoggable(Level.FINE)) { + logger.log(Level.WARNING, error.getMessage(), error); + } else { + logger.log(Level.WARNING, error.getMessage()); + } + } + } + if (openPtyAddr.isPresent()) { + openpty = linker.downcallHandle( + openPtyAddr.get(), + FunctionDescriptor.of( + ValueLayout.JAVA_INT, + ValueLayout.ADDRESS, + ValueLayout.ADDRESS, + ValueLayout.ADDRESS, + ValueLayout.ADDRESS, + ValueLayout.ADDRESS)); + openptyError = null; + } else { + openpty = null; + openptyError = error; + } + } + + private static String readFully(InputStream in) throws IOException { + int readLen = 0; + ByteArrayOutputStream b = new ByteArrayOutputStream(); + byte[] buf = new byte[32]; + while ((readLen = in.read(buf, 0, buf.length)) >= 0) { + b.write(buf, 0, readLen); + } + return b.toString(); } static Size getTerminalSize(int fd) { @@ -484,6 +583,9 @@ static String ttyName(int fd) { } static Pty openpty(TerminalProvider provider, Attributes attr, Size size) { + if (openptyError != null) { + throw openptyError; + } try { java.lang.foreign.MemorySegment buf = java.lang.foreign.Arena.ofAuto().allocate(64); diff --git a/terminal-jansi/src/test/java/org/jline/terminal/impl/jansi/JansiTerminalProviderTest.java b/terminal-jansi/src/test/java/org/jline/terminal/impl/jansi/JansiTerminalProviderTest.java index c4258c9cc..a20547c18 100644 --- a/terminal-jansi/src/test/java/org/jline/terminal/impl/jansi/JansiTerminalProviderTest.java +++ b/terminal-jansi/src/test/java/org/jline/terminal/impl/jansi/JansiTerminalProviderTest.java @@ -8,9 +8,19 @@ */ package org.jline.terminal.impl.jansi; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; +import java.nio.charset.Charset; + +import org.jline.terminal.Terminal; +import org.jline.terminal.spi.SystemStream; import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; public class JansiTerminalProviderTest { @@ -19,4 +29,29 @@ public void testJansiVersion() { assertEquals(2, JansiTerminalProvider.JANSI_MAJOR_VERSION); assertEquals(4, JansiTerminalProvider.JANSI_MINOR_VERSION); } + + @Test + void testIsSystemStream() { + assertDoesNotThrow(() -> new JansiTerminalProvider().isSystemStream(SystemStream.Output)); + } + + @Test + void testNewTerminal() throws IOException { + PipedOutputStream pos = new PipedOutputStream(); + PipedInputStream pis = new PipedInputStream(pos); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + Terminal terminal = new JansiTerminalProvider() + .newTerminal( + "name", + "xterm", + pis, + baos, + Charset.defaultCharset(), + Terminal.SignalHandler.SIG_DFL, + true, + null, + null); + assertNotNull(terminal); + } } diff --git a/terminal-jna/src/test/java/org/jline/terminal/impl/jna/JnaTerminalProviderTest.java b/terminal-jna/src/test/java/org/jline/terminal/impl/jna/JnaTerminalProviderTest.java new file mode 100644 index 000000000..f10dd4c54 --- /dev/null +++ b/terminal-jna/src/test/java/org/jline/terminal/impl/jna/JnaTerminalProviderTest.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2002-2021, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.terminal.impl.jna; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; +import java.nio.charset.Charset; + +import org.jline.terminal.Terminal; +import org.jline.terminal.spi.SystemStream; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class JnaTerminalProviderTest { + + @Test + void testIsSystemStream() { + assertDoesNotThrow(() -> new JnaTerminalProvider().isSystemStream(SystemStream.Output)); + } + + @Test + void testNewTerminal() throws IOException { + PipedOutputStream pos = new PipedOutputStream(); + PipedInputStream pis = new PipedInputStream(pos); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + Terminal terminal = new JnaTerminalProvider() + .newTerminal( + "name", + "xterm", + pis, + baos, + Charset.defaultCharset(), + Terminal.SignalHandler.SIG_DFL, + true, + null, + null); + assertNotNull(terminal); + } +} diff --git a/terminal-jni/src/test/java/org/jline/terminal/impl/jni/JniTerminalProviderTest.java b/terminal-jni/src/test/java/org/jline/terminal/impl/jni/JniTerminalProviderTest.java new file mode 100644 index 000000000..0e6f938c8 --- /dev/null +++ b/terminal-jni/src/test/java/org/jline/terminal/impl/jni/JniTerminalProviderTest.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2002-2021, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.terminal.impl.jni; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; +import java.nio.charset.Charset; + +import org.jline.terminal.Terminal; +import org.jline.terminal.spi.SystemStream; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class JniTerminalProviderTest { + + @Test + void testIsSystemStream() { + assertDoesNotThrow(() -> new JniTerminalProvider().isSystemStream(SystemStream.Output)); + } + + @Test + void testNewTerminal() throws IOException { + PipedOutputStream pos = new PipedOutputStream(); + PipedInputStream pis = new PipedInputStream(pos); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + Terminal terminal = new JniTerminalProvider() + .newTerminal( + "name", + "xterm", + pis, + baos, + Charset.defaultCharset(), + Terminal.SignalHandler.SIG_DFL, + true, + null, + null); + assertNotNull(terminal); + } +} diff --git a/terminal/src/main/java/org/jline/utils/OSUtils.java b/terminal/src/main/java/org/jline/utils/OSUtils.java index d1f3ae436..1d9f52429 100644 --- a/terminal/src/main/java/org/jline/utils/OSUtils.java +++ b/terminal/src/main/java/org/jline/utils/OSUtils.java @@ -12,9 +12,18 @@ public class OSUtils { + public static final boolean IS_LINUX = + System.getProperty("os.name").toLowerCase().contains("linux"); + public static final boolean IS_WINDOWS = System.getProperty("os.name").toLowerCase().contains("win"); + public static final boolean IS_OSX = + System.getProperty("os.name").toLowerCase().contains("mac"); + + public static final boolean IS_AIX = + System.getProperty("os.name").toLowerCase().contains("aix"); + public static final boolean IS_CYGWIN = IS_WINDOWS && System.getenv("PWD") != null && System.getenv("PWD").startsWith("/"); @@ -36,11 +45,6 @@ public class OSUtils { public static final boolean IS_CONEMU = IS_WINDOWS && System.getenv("ConEmuPID") != null; - public static final boolean IS_OSX = - System.getProperty("os.name").toLowerCase().contains("mac"); - - public static final boolean IS_AIX = System.getProperty("os.name").equals("AIX"); - public static String TTY_COMMAND; public static String STTY_COMMAND; public static String STTY_F_OPTION;