diff --git a/src/main/java/org/fusesource/jansi/AnsiConsole.java b/src/main/java/org/fusesource/jansi/AnsiConsole.java index 0e1d5df3..5033596c 100644 --- a/src/main/java/org/fusesource/jansi/AnsiConsole.java +++ b/src/main/java/org/fusesource/jansi/AnsiConsole.java @@ -25,8 +25,8 @@ import java.nio.charset.Charset; import java.nio.charset.UnsupportedCharsetException; -import org.fusesource.jansi.internal.MingwSupport; import org.fusesource.jansi.internal.OSInfo; +import org.fusesource.jansi.internal.stty.Stty; import org.fusesource.jansi.io.AnsiOutputStream; import org.fusesource.jansi.io.AnsiProcessor; import org.fusesource.jansi.io.FastBufferedOutputStream; @@ -173,6 +173,22 @@ public class AnsiConsole { * The name of the {@code ffm} provider. */ public static final String JANSI_PROVIDER_FFM = "ffm"; + /** + * The name of the {@code stty} provider. + */ + public static final String JANSI_PROVIDER_STTY = "stty"; + + /** + * The name of the {@code native-image} provider. + *
This provider uses the + * Native Image C API + * to call native functions, so it is only available when building to native image. + * Additionally, this provider currently does not support Windows. + *
Note: This is not the only provider available on Native Image, + * and it is usually recommended to use ffm or jni provider. + * This provider is mainly used when building static native images linked to musl libc. + */ + public static final String JANSI_PROVIDER_NATIVE_IMAGE = "native-image"; /** * @deprecated this field will be made private in a future release, use {@link #sysOut()} instead @@ -306,10 +322,9 @@ private static AnsiPrintStream ansiStream(boolean stdout) { processor = null; type = AnsiType.Native; installer = uninstaller = null; - MingwSupport mingw = new MingwSupport(); - String name = mingw.getConsoleName(stdout); + String name = Stty.getConsoleName(stdout); if (name != null && !name.isEmpty()) { - width = () -> mingw.getTerminalWidth(name); + width = () -> Stty.getTerminalWidth(name); } else { width = () -> -1; } diff --git a/src/main/java/org/fusesource/jansi/AnsiMain.java b/src/main/java/org/fusesource/jansi/AnsiMain.java index dace3a4d..5250ef32 100644 --- a/src/main/java/org/fusesource/jansi/AnsiMain.java +++ b/src/main/java/org/fusesource/jansi/AnsiMain.java @@ -29,7 +29,7 @@ import org.fusesource.jansi.internal.AnsiConsoleSupport; import org.fusesource.jansi.internal.AnsiConsoleSupportHolder; import org.fusesource.jansi.internal.JansiLoader; -import org.fusesource.jansi.internal.MingwSupport; +import org.fusesource.jansi.internal.stty.Stty; import static java.nio.charset.StandardCharsets.UTF_8; import static org.fusesource.jansi.Ansi.ansi; @@ -208,11 +208,10 @@ private static void diagnoseTty(boolean stderr) { long console = AnsiConsoleSupportHolder.getKernel32().getStdHandle(!stderr); isatty = AnsiConsoleSupportHolder.getKernel32().isTty(console); if ((AnsiConsole.IS_CONEMU || AnsiConsole.IS_CYGWIN || AnsiConsole.IS_MSYSTEM) && isatty == 0) { - MingwSupport mingw = new MingwSupport(); - String name = mingw.getConsoleName(!stderr); + String name = Stty.getConsoleName(!stderr); if (name != null && !name.isEmpty()) { isatty = 1; - width = mingw.getTerminalWidth(name); + width = Stty.getTerminalWidth(name); } else { isatty = 0; width = 0; diff --git a/src/main/java/org/fusesource/jansi/internal/AnsiConsoleSupportHolder.java b/src/main/java/org/fusesource/jansi/internal/AnsiConsoleSupportHolder.java index 9e66e579..f5b5faa8 100644 --- a/src/main/java/org/fusesource/jansi/internal/AnsiConsoleSupportHolder.java +++ b/src/main/java/org/fusesource/jansi/internal/AnsiConsoleSupportHolder.java @@ -44,11 +44,10 @@ private static AnsiConsoleSupport findProvider(String providerList) { RuntimeException error = null; for (String provider : providers) { + String className = "org.fusesource.jansi.internal." + provider.replace("-", "") + ".AnsiConsoleSupportImpl"; try { return (AnsiConsoleSupport) - Class.forName("org.fusesource.jansi.internal." + provider + ".AnsiConsoleSupportImpl") - .getConstructor() - .newInstance(); + Class.forName(className).getConstructor().newInstance(); } catch (Throwable t) { if (error == null) { error = new RuntimeException("Unable to create AnsiConsoleSupport provider"); diff --git a/src/main/java/org/fusesource/jansi/internal/OSInfo.java b/src/main/java/org/fusesource/jansi/internal/OSInfo.java index 2d0ce235..cfa65e9e 100644 --- a/src/main/java/org/fusesource/jansi/internal/OSInfo.java +++ b/src/main/java/org/fusesource/jansi/internal/OSInfo.java @@ -135,6 +135,10 @@ public static boolean isWindows() { return System.getProperty("os.name").toLowerCase(Locale.ROOT).contains("win"); } + public static boolean isMacOS() { + return System.getProperty("os.name").toLowerCase(Locale.ROOT).contains("mac"); + } + public static boolean isAndroid() { return System.getProperty("java.runtime.name", "") .toLowerCase(Locale.ROOT) diff --git a/src/main/java/org/fusesource/jansi/internal/ffm/AnsiConsoleSupportImpl.java b/src/main/java/org/fusesource/jansi/internal/ffm/AnsiConsoleSupportImpl.java index b36bc692..3c55481d 100644 --- a/src/main/java/org/fusesource/jansi/internal/ffm/AnsiConsoleSupportImpl.java +++ b/src/main/java/org/fusesource/jansi/internal/ffm/AnsiConsoleSupportImpl.java @@ -21,6 +21,7 @@ import java.lang.foreign.MemorySegment; import java.lang.foreign.ValueLayout; +import org.fusesource.jansi.AnsiConsole; import org.fusesource.jansi.internal.AnsiConsoleSupport; import org.fusesource.jansi.internal.OSInfo; import org.fusesource.jansi.io.AnsiProcessor; @@ -30,7 +31,7 @@ public final class AnsiConsoleSupportImpl extends AnsiConsoleSupport { public AnsiConsoleSupportImpl() { - super("ffm"); + super(AnsiConsole.JANSI_PROVIDER_FFM); } public AnsiConsoleSupportImpl(boolean checkNativeAccess) { diff --git a/src/main/java/org/fusesource/jansi/internal/jni/AnsiConsoleSupportImpl.java b/src/main/java/org/fusesource/jansi/internal/jni/AnsiConsoleSupportImpl.java index e3ae4d67..3b7c1db6 100644 --- a/src/main/java/org/fusesource/jansi/internal/jni/AnsiConsoleSupportImpl.java +++ b/src/main/java/org/fusesource/jansi/internal/jni/AnsiConsoleSupportImpl.java @@ -19,6 +19,7 @@ import java.io.OutputStream; import java.nio.charset.StandardCharsets; +import org.fusesource.jansi.AnsiConsole; import org.fusesource.jansi.internal.AnsiConsoleSupport; import org.fusesource.jansi.io.AnsiProcessor; import org.fusesource.jansi.io.WindowsAnsiProcessor; @@ -36,7 +37,7 @@ public final class AnsiConsoleSupportImpl extends AnsiConsoleSupport { public AnsiConsoleSupportImpl() { - super("jni"); + super(AnsiConsole.JANSI_PROVIDER_JNI); } @Override diff --git a/src/main/java/org/fusesource/jansi/internal/nativeimage/AnsiConsoleSupportImpl.java b/src/main/java/org/fusesource/jansi/internal/nativeimage/AnsiConsoleSupportImpl.java new file mode 100644 index 00000000..9113247e --- /dev/null +++ b/src/main/java/org/fusesource/jansi/internal/nativeimage/AnsiConsoleSupportImpl.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2009-2023 the original author(s). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.fusesource.jansi.internal.nativeimage; + +import org.fusesource.jansi.AnsiConsole; +import org.fusesource.jansi.internal.AnsiConsoleSupport; +import org.fusesource.jansi.internal.OSInfo; +import org.fusesource.jansi.internal.stty.SttyCLibrary; +import org.graalvm.nativeimage.c.type.CTypeConversion; + +public final class AnsiConsoleSupportImpl extends AnsiConsoleSupport { + + public AnsiConsoleSupportImpl() { + super(AnsiConsole.JANSI_PROVIDER_NATIVE_IMAGE); + + if (!OSInfo.isInImageCode()) { + throw new UnsupportedOperationException("This provider is only available in native images"); + } + + if (OSInfo.isWindows()) { + throw new UnsupportedOperationException("This provider is currently unavailable on Windows"); + } + } + + @Override + protected CLibrary createCLibrary() { + String stdoutTty = CTypeConversion.toJavaString(PosixCLibrary.ttyname(CLibrary.STDOUT_FILENO)); + String stderrTty = CTypeConversion.toJavaString(PosixCLibrary.ttyname(CLibrary.STDERR_FILENO)); + + return new SttyCLibrary(stdoutTty, stderrTty) { + @Override + public int isTty(int fd) { + return PosixCLibrary.isatty(fd); + } + }; + } + + @Override + protected Kernel32 createKernel32() { + throw new UnsupportedOperationException(); + } +} diff --git a/src/main/java/org/fusesource/jansi/internal/nativeimage/PosixCLibrary.java b/src/main/java/org/fusesource/jansi/internal/nativeimage/PosixCLibrary.java new file mode 100644 index 00000000..17d329c2 --- /dev/null +++ b/src/main/java/org/fusesource/jansi/internal/nativeimage/PosixCLibrary.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2009-2023 the original author(s). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.fusesource.jansi.internal.nativeimage; + +import org.graalvm.nativeimage.c.CContext; +import org.graalvm.nativeimage.c.function.CFunction; +import org.graalvm.nativeimage.c.type.CCharPointer; + +@CContext(PosixDirectives.class) +final class PosixCLibrary { + @CFunction(transition = CFunction.Transition.NO_TRANSITION) + public static native int isatty(int fd); + + @CFunction(transition = CFunction.Transition.NO_TRANSITION) + public static native CCharPointer ttyname(int fd); +} diff --git a/src/main/java/org/fusesource/jansi/internal/nativeimage/PosixDirectives.java b/src/main/java/org/fusesource/jansi/internal/nativeimage/PosixDirectives.java new file mode 100644 index 00000000..597c745c --- /dev/null +++ b/src/main/java/org/fusesource/jansi/internal/nativeimage/PosixDirectives.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2009-2023 the original author(s). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.fusesource.jansi.internal.nativeimage; + +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.c.CContext; + +public class PosixDirectives implements CContext.Directives { + @Override + public boolean isInConfiguration() { + return Platform.includedIn(Platform.LINUX.class) || Platform.includedIn(Platform.MACOS.class); + } +} diff --git a/src/main/java/org/fusesource/jansi/internal/stty/AnsiConsoleSupportImpl.java b/src/main/java/org/fusesource/jansi/internal/stty/AnsiConsoleSupportImpl.java new file mode 100644 index 00000000..5fe6d1c2 --- /dev/null +++ b/src/main/java/org/fusesource/jansi/internal/stty/AnsiConsoleSupportImpl.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2009-2023 the original author(s). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.fusesource.jansi.internal.stty; + +import org.fusesource.jansi.AnsiConsole; +import org.fusesource.jansi.internal.AnsiConsoleSupport; + +public final class AnsiConsoleSupportImpl extends AnsiConsoleSupport { + public AnsiConsoleSupportImpl() { + super(AnsiConsole.JANSI_PROVIDER_STTY); + } + + @Override + protected CLibrary createCLibrary() { + return new SttyCLibrary(Stty.getConsoleName(true), Stty.getConsoleName(false)); + } + + @Override + protected Kernel32 createKernel32() { + throw new UnsupportedOperationException(); + } +} diff --git a/src/main/java/org/fusesource/jansi/internal/MingwSupport.java b/src/main/java/org/fusesource/jansi/internal/stty/Stty.java similarity index 51% rename from src/main/java/org/fusesource/jansi/internal/MingwSupport.java rename to src/main/java/org/fusesource/jansi/internal/stty/Stty.java index be0c54a2..447bc4de 100644 --- a/src/main/java/org/fusesource/jansi/internal/MingwSupport.java +++ b/src/main/java/org/fusesource/jansi/internal/stty/Stty.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.fusesource.jansi.internal; +package org.fusesource.jansi.internal.stty; import java.io.ByteArrayOutputStream; import java.io.File; @@ -22,53 +22,62 @@ import java.io.InputStream; import java.lang.reflect.Constructor; import java.lang.reflect.Field; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.fusesource.jansi.internal.OSInfo; + /** - * Support for MINGW terminals. - * Those terminals do not use the underlying windows terminal and there's no CLibrary available - * in these environments. We have to rely on calling {@code stty.exe} and {@code tty.exe} to + * Support for POSIX-compliant or MinGW terminals. + *
+ * MinGW terminals do not use the underlying Windows terminal, and there's no CLibrary available + * in the environments. We have to rely on calling {@code stty} and {@code tty} to * obtain the terminal name and width. */ -public class MingwSupport { +public final class Stty { + + private static final String TTY_COMMAND; + private static final String STTY_COMMAND; + private static final String STTY_F_COMMAND; + private static final Pattern COLUMNS_PATTERN = Pattern.compile("\\d+ (\\d+)"); - private final String sttyCommand; - private final String ttyCommand; - private final Pattern columnsPatterns; + static { + boolean isWindows = OSInfo.isWindows(); - public MingwSupport() { - String tty = null; - String stty = null; + String tty = isWindows ? "tty.exe" : "tty"; + String stty = isWindows ? "stty.exe" : "stty"; String path = System.getenv("PATH"); if (path != null) { String[] paths = path.split(File.pathSeparator); + for (String p : paths) { - File ttyFile = new File(p, "tty.exe"); - if (tty == null && ttyFile.canExecute()) { + File ttyFile = new File(p, tty); + if (ttyFile.canExecute()) { tty = ttyFile.getAbsolutePath(); + break; } - File sttyFile = new File(p, "stty.exe"); - if (stty == null && sttyFile.canExecute()) { + } + + for (String p : paths) { + File sttyFile = new File(p, stty); + if (sttyFile.canExecute()) { stty = sttyFile.getAbsolutePath(); + break; } } } - if (tty == null) { - tty = "tty.exe"; - } - if (stty == null) { - stty = "stty.exe"; - } - ttyCommand = tty; - sttyCommand = stty; - // Compute patterns - columnsPatterns = Pattern.compile("\\b" + "columns" + "\\s+(\\d+)\\b"); + + TTY_COMMAND = tty; + STTY_COMMAND = stty; + STTY_F_COMMAND = OSInfo.isMacOS() ? "-f" : "-F"; } - public String getConsoleName(boolean stdout) { + public static String getConsoleName(boolean stdout) { try { - Process p = new ProcessBuilder(ttyCommand) + Process p = new ProcessBuilder(TTY_COMMAND) .redirectInput(getRedirect(stdout ? FileDescriptor.out : FileDescriptor.err)) .start(); String result = waitAndCapture(p); @@ -76,24 +85,51 @@ public String getConsoleName(boolean stdout) { return result.trim(); } } catch (Throwable t) { + if (!OSInfo.isWindows() && !OSInfo.isMacOS()) { + Path fd = Paths.get("/proc/self/fd/" + (stdout ? "1" : "2")); + + try { + Path target = Files.readSymbolicLink(fd); + String targetName = target.toString(); + if (targetName.startsWith("/dev/tty") || targetName.startsWith("/dev/pts/")) { + return targetName; + } else if (targetName.startsWith("pipe:") + || targetName.equals("/dev/null") + || Files.isRegularFile(target)) { + return null; + } + } catch (Throwable ignored) { + } + } + if ("java.lang.reflect.InaccessibleObjectException" .equals(t.getClass().getName())) { - System.err.println("MINGW support requires --add-opens java.base/java.lang=ALL-UNNAMED"); + String moduleName = null; + try { + Object module = Class.class.getMethod("getModule").invoke(Stty.class); + moduleName = (String) Class.forName("java.lang.Module") + .getMethod("getName") + .invoke(module); + } catch (Throwable ignored) { + } + + System.err.println("WARNING: MinGW support requires --add-opens=java.base/java.lang=" + + (moduleName == null ? "ALL-UNNAMED" : moduleName)); } // ignore } return null; } - public int getTerminalWidth(String name) { + public static int getTerminalWidth(String name) { try { - Process p = new ProcessBuilder(sttyCommand, "-F", name, "-a").start(); + Process p = new ProcessBuilder(STTY_COMMAND, STTY_F_COMMAND, name, "size").start(); String result = waitAndCapture(p); if (p.exitValue() != 0) { - throw new IOException("Error executing '" + sttyCommand + "': " + result); + throw new IOException("Error executing '" + STTY_COMMAND + "': " + result); } - Matcher matcher = columnsPatterns.matcher(result); - if (matcher.find()) { + Matcher matcher = COLUMNS_PATTERN.matcher(result.trim()); + if (matcher.matches()) { return Integer.parseInt(matcher.group(1)); } throw new IOException("Unable to parse columns"); @@ -115,13 +151,13 @@ private static String waitAndCapture(Process p) throws IOException, InterruptedE } p.waitFor(); } - return bout.toString(); + return bout.toString("UTF-8"); } /** * This requires --add-opens java.base/java.lang=ALL-UNNAMED */ - private ProcessBuilder.Redirect getRedirect(FileDescriptor fd) throws ReflectiveOperationException { + private static ProcessBuilder.Redirect getRedirect(FileDescriptor fd) throws ReflectiveOperationException { // This is not really allowed, but this is the only way to redirect the output or error stream // to the input. This is definitely not something you'd usually want to do, but in the case of // the `tty` utility, it provides a way to get @@ -134,4 +170,6 @@ private ProcessBuilder.Redirect getRedirect(FileDescriptor fd) throws Reflective f.set(input, fd); return input; } + + private Stty() {} } diff --git a/src/main/java/org/fusesource/jansi/internal/stty/SttyCLibrary.java b/src/main/java/org/fusesource/jansi/internal/stty/SttyCLibrary.java new file mode 100644 index 00000000..ccff4aaf --- /dev/null +++ b/src/main/java/org/fusesource/jansi/internal/stty/SttyCLibrary.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2009-2023 the original author(s). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.fusesource.jansi.internal.stty; + +import org.fusesource.jansi.internal.AnsiConsoleSupport; + +public class SttyCLibrary implements AnsiConsoleSupport.CLibrary { + + private final String stdoutTty; + private final String stderrTty; + + public SttyCLibrary(String stdoutTty, String stderrTty) { + this.stdoutTty = stdoutTty; + this.stderrTty = stderrTty; + } + + @Override + public short getTerminalWidth(int fd) { + String ttyName = null; + if (fd == STDOUT_FILENO) { + ttyName = stdoutTty; + } else if (fd == STDERR_FILENO) { + ttyName = stderrTty; + } + + if (ttyName == null || ttyName.isEmpty()) { + return 0; + } + + int width = Stty.getTerminalWidth(ttyName); + return width >= 0 && width <= Short.MAX_VALUE ? (short) width : 0; + } + + @Override + public int isTty(int fd) { + return (fd == STDOUT_FILENO && stdoutTty != null) || (fd == STDERR_FILENO && stderrTty != null) ? 1 : 0; + } +}