From 0ec9c83ab1dfd1ca1dc19c1309e381f33ade9f09 Mon Sep 17 00:00:00 2001 From: Glavo Date: Wed, 27 Sep 2023 00:17:09 +0800 Subject: [PATCH 01/15] Support linux-riscv64 Fixes #261 --- Makefile | 7 +++++++ Makefile.common | 7 +++++++ .../org/fusesource/jansi/internal/OSInfo.java | 4 ++++ .../internal/native/Linux/armv6/libjansi.so | Bin 15088 -> 15088 bytes 4 files changed, 18 insertions(+) diff --git a/Makefile b/Makefile index 4e2b2c85..cb5c1049 100644 --- a/Makefile +++ b/Makefile @@ -17,6 +17,7 @@ include Makefile.common linux-armv6-digest:=@sha256:7bad6ab302af34bdf6634c8c2b02c8dc6ac932c67da9ecc199c549ab405e971e linux-x86-digest:=@sha256:7a8fda5ff1bb436ac1f2e7d40043deb630800fce33d123d04779d48f85702dcd +linux-riscv64-digest:=@sha256:e10e1d3588cffffaf4d0721825e4f952710ad29d4b6630ea76d353914ffdc415 windows-static-x86-digest:=@sha256:896bd4a43bbc89502904afdc8d00e6f2422f8f35852cc59777d6426bfc8491e8 windows-static-x64-digest:=@sha256:f159861bc80b29e5dafb223477167bec53ecec6cdacb051d31e90c5823542100 windows-arm64-digest:=@sha256:f4b3c1a49ec8b53418cef1499dc3f9a54a5570b7a3ecdf42fc8c83eb94b01b7d @@ -121,6 +122,12 @@ linux-ppc64: download-includes docker run -it --rm -v $$PWD:/workdir --user $$(id -u):$$(id -g) \ -e CROSS_TRIPLE=powerpc64le-linux-gnu multiarch/crossbuild$(cross-build-digest) make clean-native native OS_NAME=Linux OS_ARCH=ppc64 +target/dockcross/dockcross-linux-riscv64: dockcross + docker run --rm dockcross/linux-riscv64$(linux-riscv64-digest) > target/dockcross/dockcross-linux-riscv64 + chmod +x target/dockcross/dockcross-linux-riscv64 +linux-riscv64: download-includes target/dockcross/dockcross-linux-riscv64 + target/dockcross/dockcross-linux-riscv64 bash -c 'make clean-native native CROSS_PREFIX=riscv64-unknown-linux-gnu- OS_NAME=Linux OS_ARCH=riscv64' + target/dockcross/dockcross-windows-static-x86: dockcross docker run --rm dockcross/windows-static-x86$(windows-static-x86-digest) > target/dockcross/dockcross-windows-static-x86 chmod +x target/dockcross/dockcross-windows-static-x86 diff --git a/Makefile.common b/Makefile.common index d67eb0e6..5f24f473 100644 --- a/Makefile.common +++ b/Makefile.common @@ -80,6 +80,13 @@ Linux-ppc64_LINKFLAGS := -shared -static-libgcc Linux-ppc64_LIBNAME := libjansi.so Linux-ppc64_JANSI_FLAGS := +Linux-riscv64_CC := $(CROSS_PREFIX)gcc +Linux-riscv64_STRIP := $(CROSS_PREFIX)strip +Linux-riscv64_CCFLAGS := -I$(JAVA_HOME)/include -Itarget/inc -Itarget/inc/unix -Os -fPIC -fvisibility=hidden +Linux-riscv64_LINKFLAGS := -shared -static-libgcc +Linux-riscv64_LIBNAME := libjansi.so +Linux-riscv64_JANSI_FLAGS := + DragonFly-x86_64_CC := $(CROSS_PREFIX)cc DragonFly-x86_64_STRIP := $(CROSS_PREFIX)strip DragonFly-x86_64_CCFLAGS := -I$(JAVA_HOME)/include -Itarget/inc -Itarget/inc/unix -O2 -fPIC -fvisibility=hidden diff --git a/src/main/java/org/fusesource/jansi/internal/OSInfo.java b/src/main/java/org/fusesource/jansi/internal/OSInfo.java index 8c9999ca..fe53cbb5 100644 --- a/src/main/java/org/fusesource/jansi/internal/OSInfo.java +++ b/src/main/java/org/fusesource/jansi/internal/OSInfo.java @@ -50,6 +50,7 @@ public class OSInfo { public static final String PPC = "ppc"; public static final String PPC64 = "ppc64"; public static final String ARM64 = "arm64"; + public static final String RISCV64 = "riscv64"; private static final HashMap archMapping = new HashMap(); @@ -92,6 +93,9 @@ public class OSInfo { // aarch64 mappings archMapping.put("aarch64", ARM64); + + // riscv64 mappings + archMapping.put(RISCV64, RISCV64); } public static void main(String[] args) { diff --git a/src/main/resources/org/fusesource/jansi/internal/native/Linux/armv6/libjansi.so b/src/main/resources/org/fusesource/jansi/internal/native/Linux/armv6/libjansi.so index 3d9631eb2076f76753d826a253e0345219ac3fe8..9a240b955b4498865b434880d9de675d38a2d9b8 100755 GIT binary patch delta 364 zcmexR`k{0K3!})3s1t@E_g|mSUmCLD+fr7=$@bm`nWt9KxtJd zZ49NEp)?zm7J<@yP?{G?t3YWXD6I~qnV_@;l;(ucYz$l=#^i_6mj{VcHap6mXPjKX z$gx>K-j7klRL?-q$Sgi3$t=y>z*tkk&`{5KaMkp-~r8yup8v`eZG5MkNim_hS?_(KFC9F*S%cO*2h2GD_1_Ff`ONnp~t{x_N~{jRd3KWK~OL# Date: Thu, 28 Sep 2023 07:26:11 +0200 Subject: [PATCH 02/15] Fix wrong output encoding on Windows with JDK >= 19 (fixes #247) (#258) * Fix wrong output encoding on Windows with JDK >= 19 JDK 19 has changed the system properties used for System.out and System.err encoding, see https://www.oracle.com/java/technologies/javase/19-relnote-issues.html#JDK-8283620 * Fix bad background in logo and add output encoding system properties --- src/main/java/org/fusesource/jansi/AnsiConsole.java | 5 ++++- src/main/java/org/fusesource/jansi/AnsiMain.java | 4 ++++ src/main/resources/org/fusesource/jansi/jansi.txt | 4 ++-- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/fusesource/jansi/AnsiConsole.java b/src/main/java/org/fusesource/jansi/AnsiConsole.java index 3b7b0032..9ce1b9a3 100644 --- a/src/main/java/org/fusesource/jansi/AnsiConsole.java +++ b/src/main/java/org/fusesource/jansi/AnsiConsole.java @@ -237,7 +237,10 @@ private static AnsiPrintStream ansiStream(boolean stdout) { FileDescriptor descriptor = stdout ? FileDescriptor.out : FileDescriptor.err; final OutputStream out = new FastBufferedOutputStream(new FileOutputStream(descriptor)); - String enc = System.getProperty(stdout ? "sun.stdout.encoding" : "sun.stderr.encoding"); + String enc = System.getProperty(stdout ? "stdout.encoding" : "stderr.encoding"); + if (enc == null) { + enc = System.getProperty(stdout ? "sun.stdout.encoding" : "sun.stderr.encoding"); + } final boolean isatty; boolean isAtty; diff --git a/src/main/java/org/fusesource/jansi/AnsiMain.java b/src/main/java/org/fusesource/jansi/AnsiMain.java index c2a845d6..197c7978 100644 --- a/src/main/java/org/fusesource/jansi/AnsiMain.java +++ b/src/main/java/org/fusesource/jansi/AnsiMain.java @@ -85,6 +85,10 @@ public static void main(String... args) throws IOException { + "os.version= " + System.getProperty("os.version") + ", " + "os.arch= " + System.getProperty("os.arch")); System.out.println("file.encoding= " + System.getProperty("file.encoding")); + System.out.println("sun.stdout.encoding= " + System.getProperty("sun.stdout.encoding") + ", " + + "sun.stderr.encoding= " + System.getProperty("sun.stderr.encoding")); + System.out.println("stdout.encoding= " + System.getProperty("stdout.encoding") + ", " + "stderr.encoding= " + + System.getProperty("stderr.encoding")); System.out.println("java.version= " + System.getProperty("java.version") + ", " + "java.vendor= " + System.getProperty("java.vendor") + "," + " java.home= " + System.getProperty("java.home")); diff --git a/src/main/resources/org/fusesource/jansi/jansi.txt b/src/main/resources/org/fusesource/jansi/jansi.txt index 247afd25..a62a6f40 100644 --- a/src/main/resources/org/fusesource/jansi/jansi.txt +++ b/src/main/resources/org/fusesource/jansi/jansi.txt @@ -1,5 +1,5 @@ -[?7h -┌──┐┌─────┐ ┌─────┐ ┌──────┬──┐ +[?7h +┌──┐┌─────┐ ┌─────┐ ┌──────┬──┐ │██├┘█████└┬┘█████└┬┘██████│▐▌│ ┌──┐ │██│██▄▄▄██│██┌─┐██│██▄▄▄▄ │▄▄│ │▒▒└─┘▒█│▒█┌─┐▒█│▒█│ │▒█│ ▀▀▀▀▒█│▒█│ From 3b15c26d4b6b00d9d6959e83398f42efd878986d Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Thu, 28 Sep 2023 07:35:46 +0200 Subject: [PATCH 03/15] Support for FFM on JDK 21 (fixes #230) (#259) - the minimal build requirement is bumped to JDK 21 - FFM is the default provider if available (JDK >= 21 with --enable-preview flag) --- .github/workflows/build.yml | 2 +- pom.xml | 60 +- .../org/fusesource/jansi/AnsiConsole.java | 65 +- .../fusesource/jansi/AnsiConsoleSupport.java | 63 ++ .../jansi/AnsiConsoleSupportHolder.java | 60 ++ .../java/org/fusesource/jansi/AnsiMain.java | 64 +- .../org/fusesource/jansi/WindowsSupport.java | 21 +- .../jansi/ffm/AnsiConsoleSupportFfm.java | 166 ++++ .../org/fusesource/jansi/ffm/Kernel32.java | 858 ++++++++++++++++++ .../jansi/ffm/WindowsAnsiProcessor.java | 439 +++++++++ .../jansi/internal/AnsiConsoleSupportJni.java | 108 +++ .../fusesource/jansi/internal/CLibrary.java | 10 +- .../jansi/internal/WindowsAnsiProcessor.java | 407 +++++++++ .../jansi/io/WindowsAnsiProcessor.java | 391 +------- 14 files changed, 2243 insertions(+), 471 deletions(-) create mode 100644 src/main/java/org/fusesource/jansi/AnsiConsoleSupport.java create mode 100644 src/main/java/org/fusesource/jansi/AnsiConsoleSupportHolder.java create mode 100644 src/main/java/org/fusesource/jansi/ffm/AnsiConsoleSupportFfm.java create mode 100644 src/main/java/org/fusesource/jansi/ffm/Kernel32.java create mode 100644 src/main/java/org/fusesource/jansi/ffm/WindowsAnsiProcessor.java create mode 100644 src/main/java/org/fusesource/jansi/internal/AnsiConsoleSupportJni.java create mode 100644 src/main/java/org/fusesource/jansi/internal/WindowsAnsiProcessor.java diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 169a1924..e1d5db2a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -30,7 +30,7 @@ jobs: strategy: matrix: os: [ ubuntu-latest, windows-latest, macos-latest ] - java: [ '11', '17' ] + java: [ '21' ] steps: - uses: actions/checkout@v2 with: diff --git a/pom.xml b/pom.xml index 9cf9578f..2b8a8f61 100644 --- a/pom.xml +++ b/pom.xml @@ -160,13 +160,67 @@ + + org.apache.maven.plugins + maven-enforcer-plugin + 3.4.1 + + + enforce-java + + enforce + + + + + 21 + + + + + + org.apache.maven.plugins maven-compiler-plugin - 3.8.1 + 3.11.0 + true + ${jdkTarget} + ${jdkTarget} ${jdkTarget} + + -Xlint:-options + + + + default-compile + + + **/ffm/*.java + + + + + jdk-21 + + compile + + + 21 + + **/ffm/*.java + + + --enable-preview + + + + + default-testCompile + + org.apache.felix @@ -351,12 +405,12 @@ com.diffplug.spotless spotless-maven-plugin - 2.38.0 + 2.39.0 - 2.35.0 + 2.38.0 java|javax,org,,\# diff --git a/src/main/java/org/fusesource/jansi/AnsiConsole.java b/src/main/java/org/fusesource/jansi/AnsiConsole.java index 9ce1b9a3..ff0cc657 100644 --- a/src/main/java/org/fusesource/jansi/AnsiConsole.java +++ b/src/main/java/org/fusesource/jansi/AnsiConsole.java @@ -26,22 +26,9 @@ import java.nio.charset.UnsupportedCharsetException; import java.util.Locale; -import org.fusesource.jansi.internal.CLibrary; -import org.fusesource.jansi.internal.CLibrary.WinSize; -import org.fusesource.jansi.internal.Kernel32.CONSOLE_SCREEN_BUFFER_INFO; import org.fusesource.jansi.io.AnsiOutputStream; import org.fusesource.jansi.io.AnsiProcessor; import org.fusesource.jansi.io.FastBufferedOutputStream; -import org.fusesource.jansi.io.WindowsAnsiProcessor; - -import static org.fusesource.jansi.internal.CLibrary.ioctl; -import static org.fusesource.jansi.internal.CLibrary.isatty; -import static org.fusesource.jansi.internal.Kernel32.GetConsoleMode; -import static org.fusesource.jansi.internal.Kernel32.GetConsoleScreenBufferInfo; -import static org.fusesource.jansi.internal.Kernel32.GetStdHandle; -import static org.fusesource.jansi.internal.Kernel32.STD_ERROR_HANDLE; -import static org.fusesource.jansi.internal.Kernel32.STD_OUTPUT_HANDLE; -import static org.fusesource.jansi.internal.Kernel32.SetConsoleMode; /** * Provides consistent access to an ANSI aware console PrintStream or an ANSI codes stripping PrintStream @@ -167,6 +154,11 @@ public class AnsiConsole { */ public static final String JANSI_GRACEFUL = "jansi.graceful"; + public static final String JANSI_PROVIDERS = "jansi.providers"; + public static final String JANSI_PROVIDER_JNI = "jni"; + public static final String JANSI_PROVIDER_FFM = "ffm"; + public static final String JANSI_PROVIDERS_DEFAULT = JANSI_PROVIDER_FFM + "," + JANSI_PROVIDER_JNI; + /** * @deprecated this field will be made private in a future release, use {@link #sysOut()} instead */ @@ -249,9 +241,9 @@ private static AnsiPrintStream ansiStream(boolean stdout) { // the library can not be loaded on unsupported platforms final int fd = stdout ? STDOUT_FILENO : STDERR_FILENO; try { - // If we can detect that stdout is not a tty.. then setup - // to strip the ANSI sequences.. - isAtty = isatty(fd) != 0; + // If we can detect that stdout is not a tty, then setup + // to strip the ANSI sequences... + isAtty = getCLibrary().isTty(fd) != 0; String term = System.getenv("TERM"); String emacs = System.getenv("INSIDE_EMACS"); if (isAtty && "dumb".equals(term) && emacs != null && !emacs.contains("comint")) { @@ -277,25 +269,26 @@ private static AnsiPrintStream ansiStream(boolean stdout) { installer = uninstaller = null; width = new AnsiOutputStream.ZeroWidthSupplier(); } else if (IS_WINDOWS) { - final long console = GetStdHandle(stdout ? STD_OUTPUT_HANDLE : STD_ERROR_HANDLE); + final long console = getKernel32().getStdHandle(stdout); final int[] mode = new int[1]; - final boolean isConsole = GetConsoleMode(console, mode) != 0; - if (isConsole && SetConsoleMode(console, mode[0] | ENABLE_VIRTUAL_TERMINAL_PROCESSING) != 0) { - SetConsoleMode(console, mode[0]); // set it back for now, but we know it works + final boolean isConsole = getKernel32().getConsoleMode(console, mode) != 0; + if (isConsole && getKernel32().setConsoleMode(console, mode[0] | ENABLE_VIRTUAL_TERMINAL_PROCESSING) != 0) { + // set it back for now, but we know it works + getKernel32().setConsoleMode(console, mode[0]); processor = null; type = AnsiType.VirtualTerminal; installer = new AnsiOutputStream.IoRunnable() { @Override public void run() throws IOException { virtualProcessing++; - SetConsoleMode(console, mode[0] | ENABLE_VIRTUAL_TERMINAL_PROCESSING); + getKernel32().setConsoleMode(console, mode[0] | ENABLE_VIRTUAL_TERMINAL_PROCESSING); } }; uninstaller = new AnsiOutputStream.IoRunnable() { @Override public void run() throws IOException { if (--virtualProcessing == 0) { - SetConsoleMode(console, mode[0]); + getKernel32().setConsoleMode(console, mode[0]); } } }; @@ -311,7 +304,7 @@ public void run() throws IOException { AnsiProcessor proc; AnsiType ttype; try { - proc = new WindowsAnsiProcessor(out, console); + proc = getKernel32().newProcessor(out, console); ttype = AnsiType.Emulation; } catch (Throwable ignore) { // this happens when the stdout is being redirected to a file. @@ -323,14 +316,7 @@ public void run() throws IOException { type = ttype; installer = uninstaller = null; } - width = new AnsiOutputStream.WidthSupplier() { - @Override - public int getTerminalWidth() { - CONSOLE_SCREEN_BUFFER_INFO info = new CONSOLE_SCREEN_BUFFER_INFO(); - GetConsoleScreenBufferInfo(console, info); - return info.windowWidth(); - } - }; + width = () -> getKernel32().getTerminalWidth(console); } // We must be on some Unix variant... @@ -339,14 +325,7 @@ public int getTerminalWidth() { processor = null; type = AnsiType.Native; installer = uninstaller = null; - width = new AnsiOutputStream.WidthSupplier() { - @Override - public int getTerminalWidth() { - WinSize sz = new WinSize(); - ioctl(fd, CLibrary.TIOCGWINSZ, sz); - return sz.ws_col; - } - }; + width = () -> getCLibrary().getTerminalWidth(fd); } AnsiMode mode; @@ -556,4 +535,12 @@ static synchronized void initStreams() { initialized = true; } } + + private static AnsiConsoleSupport.Kernel32 getKernel32() { + return AnsiConsoleSupport.getInstance().getKernel32(); + } + + private static AnsiConsoleSupport.CLibrary getCLibrary() { + return AnsiConsoleSupport.getInstance().getCLibrary(); + } } diff --git a/src/main/java/org/fusesource/jansi/AnsiConsoleSupport.java b/src/main/java/org/fusesource/jansi/AnsiConsoleSupport.java new file mode 100644 index 00000000..02907c55 --- /dev/null +++ b/src/main/java/org/fusesource/jansi/AnsiConsoleSupport.java @@ -0,0 +1,63 @@ +/* + * 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; + +import java.io.IOException; +import java.io.OutputStream; + +import org.fusesource.jansi.io.AnsiProcessor; + +public interface AnsiConsoleSupport { + + interface CLibrary { + + int STDOUT_FILENO = 1; + int STDERR_FILENO = 2; + + short getTerminalWidth(int fd); + + int isTty(int fd); + } + + interface Kernel32 { + + int isTty(long console); + + int getTerminalWidth(long console); + + long getStdHandle(boolean stdout); + + int getConsoleMode(long console, int[] mode); + + int setConsoleMode(long console, int mode); + + int getLastError(); + + String getErrorMessage(int errorCode); + + AnsiProcessor newProcessor(OutputStream os, long console) throws IOException; + } + + String getProviderName(); + + CLibrary getCLibrary(); + + Kernel32 getKernel32(); + + static AnsiConsoleSupport getInstance() { + return AnsiConsoleSupportHolder.get(); + } +} diff --git a/src/main/java/org/fusesource/jansi/AnsiConsoleSupportHolder.java b/src/main/java/org/fusesource/jansi/AnsiConsoleSupportHolder.java new file mode 100644 index 00000000..082f78d0 --- /dev/null +++ b/src/main/java/org/fusesource/jansi/AnsiConsoleSupportHolder.java @@ -0,0 +1,60 @@ +/* + * 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; + +import org.fusesource.jansi.internal.AnsiConsoleSupportJni; + +import static org.fusesource.jansi.AnsiConsole.JANSI_PROVIDERS; +import static org.fusesource.jansi.AnsiConsole.JANSI_PROVIDERS_DEFAULT; +import static org.fusesource.jansi.AnsiConsole.JANSI_PROVIDER_FFM; +import static org.fusesource.jansi.AnsiConsole.JANSI_PROVIDER_JNI; + +class AnsiConsoleSupportHolder { + static volatile AnsiConsoleSupport instance; + + static AnsiConsoleSupport get() { + if (instance == null) { + synchronized (AnsiConsoleSupportHolder.class) { + if (instance == null) { + instance = doGet(); + } + } + } + return instance; + } + + static AnsiConsoleSupport doGet() { + RuntimeException error = new RuntimeException("Unable to create AnsiConsoleSupport provider"); + String[] providers = + System.getProperty(JANSI_PROVIDERS, JANSI_PROVIDERS_DEFAULT).split(","); + for (String provider : providers) { + try { + if (JANSI_PROVIDER_FFM.equals(provider)) { + return (AnsiConsoleSupport) AnsiConsoleSupport.class + .getClassLoader() + .loadClass("org.fusesource.jansi.ffm.AnsiConsoleSupportFfm") + .getConstructor() + .newInstance(); + } else if (JANSI_PROVIDER_JNI.equals(provider)) { + return new AnsiConsoleSupportJni(); + } + } catch (Throwable t) { + error.addSuppressed(t); + } + } + throw error; + } +} diff --git a/src/main/java/org/fusesource/jansi/AnsiMain.java b/src/main/java/org/fusesource/jansi/AnsiMain.java index 197c7978..24fe6f2f 100644 --- a/src/main/java/org/fusesource/jansi/AnsiMain.java +++ b/src/main/java/org/fusesource/jansi/AnsiMain.java @@ -26,7 +26,6 @@ import java.util.Properties; import org.fusesource.jansi.Ansi.Attribute; -import org.fusesource.jansi.internal.CLibrary; import org.fusesource.jansi.internal.JansiLoader; import static org.fusesource.jansi.Ansi.ansi; @@ -54,27 +53,34 @@ public static void main(String... args) throws IOException { System.out.println(); - // info on native library - System.out.println("library.jansi.path= " + System.getProperty("library.jansi.path", "")); - System.out.println("library.jansi.version= " + System.getProperty("library.jansi.version", "")); - boolean loaded = JansiLoader.initialize(); - if (loaded) { - System.out.println("Jansi native library loaded from " + JansiLoader.getNativeLibraryPath()); - if (JansiLoader.getNativeLibrarySourceUrl() != null) { - System.out.println(" which was auto-extracted from " + JansiLoader.getNativeLibrarySourceUrl()); - } - } else { - String prev = System.getProperty(AnsiConsole.JANSI_GRACEFUL); - try { - System.setProperty(AnsiConsole.JANSI_GRACEFUL, "false"); - JansiLoader.initialize(); - } catch (Throwable e) { - e.printStackTrace(System.out); - } finally { - if (prev != null) { - System.setProperty(AnsiConsole.JANSI_GRACEFUL, prev); - } else { - System.clearProperty(AnsiConsole.JANSI_GRACEFUL); + System.out.println("jansi.providers= " + + System.getProperty(AnsiConsole.JANSI_PROVIDERS, AnsiConsole.JANSI_PROVIDERS_DEFAULT)); + String provider = AnsiConsoleSupport.getInstance().getProviderName(); + System.out.println("Selected provider: " + provider); + + if (AnsiConsole.JANSI_PROVIDER_JNI.equals(provider)) { + // info on native library + System.out.println("library.jansi.path= " + System.getProperty("library.jansi.path", "")); + System.out.println("library.jansi.version= " + System.getProperty("library.jansi.version", "")); + boolean loaded = JansiLoader.initialize(); + if (loaded) { + System.out.println("Jansi native library loaded from " + JansiLoader.getNativeLibraryPath()); + if (JansiLoader.getNativeLibrarySourceUrl() != null) { + System.out.println(" which was auto-extracted from " + JansiLoader.getNativeLibrarySourceUrl()); + } + } else { + String prev = System.getProperty(AnsiConsole.JANSI_GRACEFUL); + try { + System.setProperty(AnsiConsole.JANSI_GRACEFUL, "false"); + JansiLoader.initialize(); + } catch (Throwable e) { + e.printStackTrace(System.out); + } finally { + if (prev != null) { + System.setProperty(AnsiConsole.JANSI_GRACEFUL, prev); + } else { + System.clearProperty(AnsiConsole.JANSI_GRACEFUL); + } } } } @@ -192,11 +198,21 @@ private static String getJansiVersion() { } private static void diagnoseTty(boolean stderr) { - int fd = stderr ? CLibrary.STDERR_FILENO : CLibrary.STDOUT_FILENO; - int isatty = CLibrary.LOADED ? CLibrary.isatty(fd) : 0; + int isatty; + int width; + if (AnsiConsole.IS_WINDOWS) { + long console = AnsiConsoleSupport.getInstance().getKernel32().getStdHandle(!stderr); + isatty = AnsiConsoleSupport.getInstance().getKernel32().isTty(console); + width = AnsiConsoleSupport.getInstance().getKernel32().getTerminalWidth(console); + } else { + int fd = stderr ? AnsiConsoleSupport.CLibrary.STDERR_FILENO : AnsiConsoleSupport.CLibrary.STDOUT_FILENO; + isatty = AnsiConsoleSupport.getInstance().getCLibrary().isTty(fd); + width = AnsiConsoleSupport.getInstance().getCLibrary().getTerminalWidth(fd); + } System.out.println("isatty(STD" + (stderr ? "ERR" : "OUT") + "_FILENO): " + isatty + ", System." + (stderr ? "err" : "out") + " " + ((isatty == 0) ? "is *NOT*" : "is") + " a terminal"); + System.out.println("width(STD" + (stderr ? "ERR" : "OUT") + "_FILENO): " + width); } private static void testAnsi(boolean stderr) { diff --git a/src/main/java/org/fusesource/jansi/WindowsSupport.java b/src/main/java/org/fusesource/jansi/WindowsSupport.java index 010f527e..e14854cd 100644 --- a/src/main/java/org/fusesource/jansi/WindowsSupport.java +++ b/src/main/java/org/fusesource/jansi/WindowsSupport.java @@ -15,27 +15,18 @@ */ package org.fusesource.jansi; -import java.io.UnsupportedEncodingException; - -import static org.fusesource.jansi.internal.Kernel32.FORMAT_MESSAGE_FROM_SYSTEM; -import static org.fusesource.jansi.internal.Kernel32.FormatMessageW; -import static org.fusesource.jansi.internal.Kernel32.GetLastError; - public class WindowsSupport { public static String getLastErrorMessage() { - int errorCode = GetLastError(); + int errorCode = getKernel32().getLastError(); return getErrorMessage(errorCode); } public static String getErrorMessage(int errorCode) { - int bufferSize = 160; - byte data[] = new byte[bufferSize]; - FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM, 0, errorCode, 0, data, bufferSize, null); - try { - return new String(data, "UTF-16LE").trim(); - } catch (UnsupportedEncodingException e) { - throw new IllegalStateException(e); - } + return getKernel32().getErrorMessage(errorCode); + } + + private static AnsiConsoleSupport.Kernel32 getKernel32() { + return AnsiConsoleSupport.getInstance().getKernel32(); } } diff --git a/src/main/java/org/fusesource/jansi/ffm/AnsiConsoleSupportFfm.java b/src/main/java/org/fusesource/jansi/ffm/AnsiConsoleSupportFfm.java new file mode 100644 index 00000000..b1af763f --- /dev/null +++ b/src/main/java/org/fusesource/jansi/ffm/AnsiConsoleSupportFfm.java @@ -0,0 +1,166 @@ +/* + * 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.ffm; + +import java.io.IOException; +import java.io.OutputStream; +import java.lang.foreign.Arena; +import java.lang.foreign.FunctionDescriptor; +import java.lang.foreign.GroupLayout; +import java.lang.foreign.Linker; +import java.lang.foreign.MemoryLayout; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.VarHandle; + +import org.fusesource.jansi.AnsiConsoleSupport; +import org.fusesource.jansi.io.AnsiProcessor; + +import static org.fusesource.jansi.ffm.Kernel32.*; + +public class AnsiConsoleSupportFfm implements AnsiConsoleSupport { + static GroupLayout wsLayout; + static MethodHandle ioctl; + static VarHandle ws_col; + static MethodHandle isatty; + + static { + wsLayout = MemoryLayout.structLayout( + ValueLayout.JAVA_SHORT.withName("ws_row"), + ValueLayout.JAVA_SHORT.withName("ws_col"), + ValueLayout.JAVA_SHORT, + ValueLayout.JAVA_SHORT); + ws_col = wsLayout.varHandle(MemoryLayout.PathElement.groupElement("ws_col")); + Linker linker = Linker.nativeLinker(); + ioctl = linker.downcallHandle( + linker.defaultLookup().find("ioctl").get(), + FunctionDescriptor.of( + ValueLayout.JAVA_INT, ValueLayout.JAVA_INT, ValueLayout.JAVA_LONG, ValueLayout.ADDRESS), + Linker.Option.firstVariadicArg(2)); + isatty = linker.downcallHandle( + linker.defaultLookup().find("isatty").get(), + FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.JAVA_INT)); + } + + @Override + public String getProviderName() { + return "ffm"; + } + + @Override + public CLibrary getCLibrary() { + return new CLibrary() { + static final int TIOCGWINSZ; + + static { + String osName = System.getProperty("os.name"); + if (osName.startsWith("Linux")) { + String arch = System.getProperty("os.arch"); + boolean isMipsPpcOrSparc = + arch.startsWith("mips") || arch.startsWith("ppc") || arch.startsWith("sparc"); + TIOCGWINSZ = isMipsPpcOrSparc ? 0x40087468 : 0x00005413; + } else if (osName.startsWith("Solaris") || osName.startsWith("SunOS")) { + int _TIOC = ('T' << 8); + TIOCGWINSZ = (_TIOC | 104); + } else if (osName.startsWith("Mac") || osName.startsWith("Darwin")) { + TIOCGWINSZ = 0x40087468; + } else if (osName.startsWith("FreeBSD")) { + TIOCGWINSZ = 0x40087468; + } else { + throw new UnsupportedOperationException(); + } + } + + @Override + public short getTerminalWidth(int fd) { + MemorySegment segment = Arena.ofAuto().allocate(wsLayout); + try { + int res = (int) ioctl.invoke(fd, (long) TIOCGWINSZ, segment); + return (short) ws_col.get(segment); + } catch (Throwable e) { + throw new RuntimeException("Unable to ioctl(TIOCGWINSZ)", e); + } + } + + @Override + public int isTty(int fd) { + try { + return (int) isatty.invoke(fd); + } catch (Throwable e) { + throw new RuntimeException("Unable to call isatty", e); + } + } + }; + } + + @Override + public Kernel32 getKernel32() { + return new Kernel32() { + @Override + public int isTty(long console) { + int[] mode = new int[1]; + return getConsoleMode(console, mode); + } + + @Override + public int getTerminalWidth(long console) { + CONSOLE_SCREEN_BUFFER_INFO info = new CONSOLE_SCREEN_BUFFER_INFO(); + GetConsoleScreenBufferInfo(MemorySegment.ofAddress(console), info); + return info.windowWidth(); + } + + @Override + public long getStdHandle(boolean stdout) { + return GetStdHandle(stdout ? STD_OUTPUT_HANDLE : STD_ERROR_HANDLE) + .address(); + } + + @Override + public int getConsoleMode(long console, int[] mode) { + try (Arena session = Arena.ofConfined()) { + MemorySegment written = session.allocate(ValueLayout.JAVA_INT); + int res = GetConsoleMode(MemorySegment.ofAddress(console), written); + mode[0] = written.getAtIndex(ValueLayout.JAVA_INT, 0); + return res; + } + } + + @Override + public int setConsoleMode(long console, int mode) { + return SetConsoleMode(MemorySegment.ofAddress(console), mode); + } + + @Override + public int getLastError() { + return GetLastError(); + } + + @Override + public String getErrorMessage(int errorCode) { + int bufferSize = 160; + MemorySegment data = Arena.ofAuto().allocate(bufferSize); + FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM, null, errorCode, 0, data, bufferSize, null); + return data.getUtf8String(0).trim(); + } + + @Override + public AnsiProcessor newProcessor(OutputStream os, long console) throws IOException { + return new WindowsAnsiProcessor(os, MemorySegment.ofAddress(console)); + } + }; + } +} diff --git a/src/main/java/org/fusesource/jansi/ffm/Kernel32.java b/src/main/java/org/fusesource/jansi/ffm/Kernel32.java new file mode 100644 index 00000000..fc17db68 --- /dev/null +++ b/src/main/java/org/fusesource/jansi/ffm/Kernel32.java @@ -0,0 +1,858 @@ +/* + * 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.ffm; + +import java.io.IOException; +import java.lang.foreign.AddressLayout; +import java.lang.foreign.Arena; +import java.lang.foreign.FunctionDescriptor; +import java.lang.foreign.GroupLayout; +import java.lang.foreign.Linker; +import java.lang.foreign.MemoryLayout; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.SymbolLookup; +import java.lang.foreign.ValueLayout; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.VarHandle; +import java.util.Objects; + +import static java.lang.foreign.ValueLayout.JAVA_INT; +import static java.lang.foreign.ValueLayout.OfBoolean; +import static java.lang.foreign.ValueLayout.OfByte; +import static java.lang.foreign.ValueLayout.OfChar; +import static java.lang.foreign.ValueLayout.OfDouble; +import static java.lang.foreign.ValueLayout.OfFloat; +import static java.lang.foreign.ValueLayout.OfInt; +import static java.lang.foreign.ValueLayout.OfLong; +import static java.lang.foreign.ValueLayout.OfShort; + +@SuppressWarnings({"unused", "CopyConstructorMissesField"}) +class Kernel32 { + + public static final int FORMAT_MESSAGE_FROM_SYSTEM = 0x00001000; + + public static final int INVALID_HANDLE_VALUE = -1; + public static final int STD_INPUT_HANDLE = -10; + public static final int STD_OUTPUT_HANDLE = -11; + public static final int STD_ERROR_HANDLE = -12; + + public static final int ENABLE_PROCESSED_INPUT = 0x0001; + public static final int ENABLE_LINE_INPUT = 0x0002; + public static final int ENABLE_ECHO_INPUT = 0x0004; + public static final int ENABLE_WINDOW_INPUT = 0x0008; + public static final int ENABLE_MOUSE_INPUT = 0x0010; + public static final int ENABLE_INSERT_MODE = 0x0020; + public static final int ENABLE_QUICK_EDIT_MODE = 0x0040; + public static final int ENABLE_EXTENDED_FLAGS = 0x0080; + + public static final int RIGHT_ALT_PRESSED = 0x0001; + public static final int LEFT_ALT_PRESSED = 0x0002; + public static final int RIGHT_CTRL_PRESSED = 0x0004; + public static final int LEFT_CTRL_PRESSED = 0x0008; + public static final int SHIFT_PRESSED = 0x0010; + + public static final int FOREGROUND_BLUE = 0x0001; + public static final int FOREGROUND_GREEN = 0x0002; + public static final int FOREGROUND_RED = 0x0004; + public static final int FOREGROUND_INTENSITY = 0x0008; + public static final int BACKGROUND_BLUE = 0x0010; + public static final int BACKGROUND_GREEN = 0x0020; + public static final int BACKGROUND_RED = 0x0040; + public static final int BACKGROUND_INTENSITY = 0x0080; + + // Button state + public static final int FROM_LEFT_1ST_BUTTON_PRESSED = 0x0001; + public static final int RIGHTMOST_BUTTON_PRESSED = 0x0002; + public static final int FROM_LEFT_2ND_BUTTON_PRESSED = 0x0004; + public static final int FROM_LEFT_3RD_BUTTON_PRESSED = 0x0008; + public static final int FROM_LEFT_4TH_BUTTON_PRESSED = 0x0010; + + // Event flags + public static final int MOUSE_MOVED = 0x0001; + public static final int DOUBLE_CLICK = 0x0002; + public static final int MOUSE_WHEELED = 0x0004; + public static final int MOUSE_HWHEELED = 0x0008; + + // Event types + public static final short KEY_EVENT = 0x0001; + public static final short MOUSE_EVENT = 0x0002; + public static final short WINDOW_BUFFER_SIZE_EVENT = 0x0004; + public static final short MENU_EVENT = 0x0008; + public static final short FOCUS_EVENT = 0x0010; + + public static int WaitForSingleObject(MemorySegment hHandle, int dwMilliseconds) { + MethodHandle mh$ = requireNonNull(WaitForSingleObject$MH, "WaitForSingleObject"); + try { + return (int) mh$.invokeExact(hHandle, dwMilliseconds); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + public static MemorySegment GetStdHandle(int nStdHandle) { + MethodHandle mh$ = requireNonNull(GetStdHandle$MH, "GetStdHandle"); + try { + return MemorySegment.ofAddress((long) mh$.invokeExact(nStdHandle)); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + public static int FormatMessageW( + int dwFlags, + MemorySegment lpSource, + int dwMessageId, + int dwLanguageId, + MemorySegment lpBuffer, + int nSize, + MemorySegment Arguments) { + MethodHandle mh$ = requireNonNull(FormatMessageW$MH, "FormatMessageW"); + try { + return (int) mh$.invokeExact( + dwFlags, + lpSource.address(), + dwMessageId, + dwLanguageId, + lpBuffer.address(), + nSize, + Arguments.address()); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + public static int SetConsoleTextAttribute(MemorySegment hConsoleOutput, short wAttributes) { + MethodHandle mh$ = requireNonNull(SetConsoleTextAttribute$MH, "SetConsoleTextAttribute"); + try { + return (int) mh$.invokeExact(hConsoleOutput, wAttributes); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + public static int SetConsoleMode(MemorySegment hConsoleHandle, int dwMode) { + MethodHandle mh$ = requireNonNull(SetConsoleMode$MH, "SetConsoleMode"); + try { + return (int) mh$.invokeExact(hConsoleHandle.address(), dwMode); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + public static int GetConsoleMode(MemorySegment hConsoleHandle, MemorySegment lpMode) { + MethodHandle mh$ = requireNonNull(GetConsoleMode$MH, "GetConsoleMode"); + try { + return (int) mh$.invokeExact(hConsoleHandle.address(), lpMode.address()); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + public static int SetConsoleTitleW(MemorySegment lpConsoleTitle) { + MethodHandle mh$ = requireNonNull(SetConsoleTitleW$MH, "SetConsoleTitleW"); + try { + return (int) mh$.invokeExact(lpConsoleTitle.address()); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + public static int SetConsoleCursorPosition(MemorySegment hConsoleOutput, COORD dwCursorPosition) { + MethodHandle mh$ = requireNonNull(SetConsoleCursorPosition$MH, "SetConsoleCursorPosition"); + try { + return (int) mh$.invokeExact(hConsoleOutput, dwCursorPosition.seg); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + public static int FillConsoleOutputCharacterW( + MemorySegment hConsoleOutput, + char cCharacter, + int nLength, + COORD dwWriteCoord, + MemorySegment lpNumberOfCharsWritten) { + MethodHandle mh$ = requireNonNull(FillConsoleOutputCharacterW$MH, "FillConsoleOutputCharacterW"); + try { + return (int) mh$.invokeExact( + hConsoleOutput.address(), cCharacter, nLength, dwWriteCoord.seg, lpNumberOfCharsWritten.address()); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + public static int FillConsoleOutputAttribute( + MemorySegment hConsoleOutput, + short wAttribute, + int nLength, + COORD dwWriteCoord, + MemorySegment lpNumberOfAttrsWritten) { + MethodHandle mh$ = requireNonNull(FillConsoleOutputAttribute$MH, "FillConsoleOutputAttribute"); + try { + return (int) mh$.invokeExact( + hConsoleOutput, wAttribute, nLength, dwWriteCoord.seg, lpNumberOfAttrsWritten.address()); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + public static int WriteConsoleW( + MemorySegment hConsoleOutput, + MemorySegment lpBuffer, + int nNumberOfCharsToWrite, + MemorySegment lpNumberOfCharsWritten, + MemorySegment lpReserved) { + MethodHandle mh$ = requireNonNull(WriteConsoleW$MH, "WriteConsoleW"); + try { + return (int) mh$.invokeExact( + hConsoleOutput, lpBuffer, nNumberOfCharsToWrite, lpNumberOfCharsWritten, lpReserved); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + public static int ReadConsoleInputW( + MemorySegment hConsoleInput, MemorySegment lpBuffer, int nLength, MemorySegment lpNumberOfEventsRead) { + MethodHandle mh$ = requireNonNull(ReadConsoleInputW$MH, "ReadConsoleInputW"); + try { + return (int) mh$.invokeExact( + hConsoleInput.address(), lpBuffer.address(), nLength, lpNumberOfEventsRead.address()); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + public static int PeekConsoleInputW( + MemorySegment hConsoleInput, MemorySegment lpBuffer, int nLength, MemorySegment lpNumberOfEventsRead) { + MethodHandle mh$ = requireNonNull(PeekConsoleInputW$MH, "PeekConsoleInputW"); + try { + return (int) mh$.invokeExact( + hConsoleInput.address(), lpBuffer.address(), nLength, lpNumberOfEventsRead.address()); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + public static int GetConsoleScreenBufferInfo( + MemorySegment hConsoleOutput, CONSOLE_SCREEN_BUFFER_INFO lpConsoleScreenBufferInfo) { + MethodHandle mh$ = requireNonNull(GetConsoleScreenBufferInfo$MH, "GetConsoleScreenBufferInfo"); + try { + return (int) mh$.invokeExact(hConsoleOutput.address(), lpConsoleScreenBufferInfo.seg); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + public static int ScrollConsoleScreenBuffer( + MemorySegment hConsoleOutput, + SMALL_RECT lpScrollRectangle, + SMALL_RECT lpClipRectangle, + COORD dwDestinationOrigin, + CHAR_INFO lpFill) { + MethodHandle mh$ = requireNonNull(ScrollConsoleScreenBuffer$MH, "ScrollConsoleScreenBuffer"); + try { + return (int) + mh$.invokeExact(hConsoleOutput, lpScrollRectangle, lpClipRectangle, dwDestinationOrigin, lpFill); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + public static int GetLastError(Object... x0) { + MethodHandle mh$ = requireNonNull(GetLastError$MH, "GetLastError"); + try { + return (int) mh$.invokeExact(x0); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + public static INPUT_RECORD[] readConsoleInputHelper(MemorySegment handle, int count, boolean peek) + throws IOException { + try (Arena session = Arena.ofConfined()) { + MemorySegment inputRecordPtr = session.allocateArray(INPUT_RECORD.LAYOUT, count); + MemorySegment length = session.allocate(JAVA_INT, 0); + int res = peek + ? PeekConsoleInputW(handle, inputRecordPtr, count, length) + : ReadConsoleInputW(handle, inputRecordPtr, count, length); + if (res == 0) { + throw new IOException("ReadConsoleInputW failed: " + getLastErrorMessage()); + } + int len = length.get(JAVA_INT, 0); + return inputRecordPtr + .elements(INPUT_RECORD.LAYOUT) + .map(INPUT_RECORD::new) + .limit(len) + .toArray(INPUT_RECORD[]::new); + } + } + + public static String getLastErrorMessage() { + int errorCode = GetLastError(); + return getErrorMessage(errorCode); + } + + public static String getErrorMessage(int errorCode) { + int bufferSize = 160; + MemorySegment data = Arena.ofAuto().allocate(bufferSize); + FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM, null, errorCode, 0, data, bufferSize, null); + return data.getUtf8String(0).trim(); + } + + static final OfBoolean C_BOOL$LAYOUT = ValueLayout.JAVA_BOOLEAN; + static final OfByte C_CHAR$LAYOUT = ValueLayout.JAVA_BYTE; + static final OfChar C_WCHAR$LAYOUT = ValueLayout.JAVA_CHAR.withByteAlignment(16); + static final OfShort C_SHORT$LAYOUT = ValueLayout.JAVA_SHORT.withByteAlignment(16); + static final OfShort C_WORD$LAYOUT = ValueLayout.JAVA_SHORT.withByteAlignment(16); + static final OfInt C_DWORD$LAYOUT = ValueLayout.JAVA_INT.withByteAlignment(32); + static final OfInt C_INT$LAYOUT = JAVA_INT.withByteAlignment(32); + static final OfLong C_LONG$LAYOUT = ValueLayout.JAVA_LONG.withByteAlignment(64); + static final OfLong C_LONG_LONG$LAYOUT = ValueLayout.JAVA_LONG.withByteAlignment(64); + static final OfFloat C_FLOAT$LAYOUT = ValueLayout.JAVA_FLOAT.withByteAlignment(32); + static final OfDouble C_DOUBLE$LAYOUT = ValueLayout.JAVA_DOUBLE.withByteAlignment(64); + static final AddressLayout C_POINTER$LAYOUT = ValueLayout.ADDRESS.withByteAlignment(64); + + static final MethodHandle WaitForSingleObject$MH = + downcallHandle("WaitForSingleObject", FunctionDescriptor.of(C_INT$LAYOUT, C_POINTER$LAYOUT, C_INT$LAYOUT)); + static final MethodHandle GetStdHandle$MH = + downcallHandle("GetStdHandle", FunctionDescriptor.of(C_POINTER$LAYOUT, C_INT$LAYOUT)); + static final MethodHandle FormatMessageW$MH = downcallHandle( + "FormatMessageW", + FunctionDescriptor.of( + C_INT$LAYOUT, + C_INT$LAYOUT, + C_POINTER$LAYOUT, + C_INT$LAYOUT, + C_INT$LAYOUT, + C_POINTER$LAYOUT, + C_INT$LAYOUT, + C_POINTER$LAYOUT)); + static final MethodHandle SetConsoleTextAttribute$MH = downcallHandle( + "SetConsoleTextAttribute", FunctionDescriptor.of(C_INT$LAYOUT, C_POINTER$LAYOUT, C_SHORT$LAYOUT)); + static final MethodHandle SetConsoleMode$MH = + downcallHandle("SetConsoleMode", FunctionDescriptor.of(C_INT$LAYOUT, C_POINTER$LAYOUT, C_INT$LAYOUT)); + static final MethodHandle GetConsoleMode$MH = + downcallHandle("GetConsoleMode", FunctionDescriptor.of(C_INT$LAYOUT, C_POINTER$LAYOUT, C_POINTER$LAYOUT)); + + static final MethodHandle SetConsoleTitleW$MH = + downcallHandle("SetConsoleTitleW", FunctionDescriptor.of(C_INT$LAYOUT, C_POINTER$LAYOUT)); + static final MethodHandle SetConsoleCursorPosition$MH = downcallHandle( + "SetConsoleCursorPosition", FunctionDescriptor.of(C_INT$LAYOUT, C_POINTER$LAYOUT, COORD.LAYOUT)); + static final MethodHandle FillConsoleOutputCharacterW$MH = downcallHandle( + "FillConsoleOutputCharacterW", + FunctionDescriptor.of( + C_INT$LAYOUT, C_POINTER$LAYOUT, C_SHORT$LAYOUT, C_INT$LAYOUT, COORD.LAYOUT, C_POINTER$LAYOUT)); + static final MethodHandle FillConsoleOutputAttribute$MH = downcallHandle( + "FillConsoleOutputAttribute", + FunctionDescriptor.of( + C_INT$LAYOUT, C_POINTER$LAYOUT, C_SHORT$LAYOUT, C_INT$LAYOUT, COORD.LAYOUT, C_POINTER$LAYOUT)); + static final MethodHandle WriteConsoleW$MH = downcallHandle( + "WriteConsoleW", + FunctionDescriptor.of( + C_INT$LAYOUT, + C_POINTER$LAYOUT, + C_POINTER$LAYOUT, + C_INT$LAYOUT, + C_POINTER$LAYOUT, + C_POINTER$LAYOUT)); + + static final MethodHandle ReadConsoleInputW$MH = downcallHandle( + "ReadConsoleInputW", + FunctionDescriptor.of(C_INT$LAYOUT, C_POINTER$LAYOUT, C_POINTER$LAYOUT, C_INT$LAYOUT, C_POINTER$LAYOUT)); + static final MethodHandle PeekConsoleInputW$MH = downcallHandle( + "PeekConsoleInputW", + FunctionDescriptor.of(C_INT$LAYOUT, C_POINTER$LAYOUT, C_POINTER$LAYOUT, C_INT$LAYOUT, C_POINTER$LAYOUT)); + + static final MethodHandle GetConsoleScreenBufferInfo$MH = downcallHandle( + "GetConsoleScreenBufferInfo", FunctionDescriptor.of(C_INT$LAYOUT, C_POINTER$LAYOUT, C_POINTER$LAYOUT)); + + static final MethodHandle ScrollConsoleScreenBuffer$MH = downcallHandle( + "ScrollConsoleScreenBuffer", + FunctionDescriptor.of( + C_INT$LAYOUT, + C_POINTER$LAYOUT, + C_POINTER$LAYOUT, + C_POINTER$LAYOUT, + COORD.LAYOUT, + C_POINTER$LAYOUT)); + static final MethodHandle GetLastError$MH = downcallHandle("GetLastError", FunctionDescriptor.of(C_INT$LAYOUT)); + + public static class INPUT_RECORD { + static final MemoryLayout LAYOUT = MemoryLayout.structLayout( + ValueLayout.JAVA_SHORT.withName("EventType"), + MemoryLayout.unionLayout( + KEY_EVENT_RECORD.LAYOUT.withName("KeyEvent"), + MOUSE_EVENT_RECORD.LAYOUT.withName("MouseEvent"), + WINDOW_BUFFER_SIZE_RECORD.LAYOUT.withName("WindowBufferSizeEvent"), + MENU_EVENT_RECORD.LAYOUT.withName("MenuEvent"), + FOCUS_EVENT_RECORD.LAYOUT.withName("FocusEvent")) + .withName("Event")); + static final VarHandle EventType$VH = varHandle(LAYOUT, "EventType"); + static final long Event$OFFSET = byteOffset(LAYOUT, "Event"); + + private final MemorySegment seg; + + public INPUT_RECORD() { + this(Arena.ofAuto().allocate(LAYOUT)); + } + + INPUT_RECORD(MemorySegment seg) { + this.seg = seg; + } + + public short eventType() { + return (short) EventType$VH.get(seg); + } + + public KEY_EVENT_RECORD keyEvent() { + return new KEY_EVENT_RECORD(seg, Event$OFFSET); + } + + public MOUSE_EVENT_RECORD mouseEvent() { + return new MOUSE_EVENT_RECORD(seg, Event$OFFSET); + } + + public FOCUS_EVENT_RECORD focusEvent() { + return new FOCUS_EVENT_RECORD(seg, Event$OFFSET); + } + } + + public static class MENU_EVENT_RECORD { + + static final GroupLayout LAYOUT = MemoryLayout.structLayout(C_DWORD$LAYOUT.withName("dwCommandId")); + static final VarHandle COMMAND_ID = varHandle(LAYOUT, "dwCommandId"); + + private final MemorySegment seg; + + public MENU_EVENT_RECORD() { + this(Arena.ofAuto().allocate(LAYOUT)); + } + + MENU_EVENT_RECORD(MemorySegment seg) { + this.seg = seg; + } + + public int commandId() { + return (int) MENU_EVENT_RECORD.COMMAND_ID.get(seg); + } + + public void commandId(int commandId) { + MENU_EVENT_RECORD.COMMAND_ID.set(seg, commandId); + } + } + + public static class FOCUS_EVENT_RECORD { + + static final GroupLayout LAYOUT = MemoryLayout.structLayout(C_BOOL$LAYOUT.withName("bSetFocus")); + static final VarHandle SET_FOCUS = varHandle(LAYOUT, "bSetFocus"); + + private final MemorySegment seg; + + public FOCUS_EVENT_RECORD() { + this(Arena.ofAuto().allocate(LAYOUT)); + } + + FOCUS_EVENT_RECORD(MemorySegment seg) { + this.seg = Objects.requireNonNull(seg); + } + + FOCUS_EVENT_RECORD(MemorySegment seg, long offset) { + this.seg = Objects.requireNonNull(seg).asSlice(offset, LAYOUT.byteSize()); + } + + public boolean setFocus() { + return (boolean) FOCUS_EVENT_RECORD.SET_FOCUS.get(seg); + } + + public void setFocus(boolean setFocus) { + FOCUS_EVENT_RECORD.SET_FOCUS.set(seg, setFocus); + } + } + + public static class WINDOW_BUFFER_SIZE_RECORD { + + static final GroupLayout LAYOUT = MemoryLayout.structLayout(COORD.LAYOUT.withName("size")); + static final long SIZE_OFFSET = byteOffset(LAYOUT, "size"); + + private final MemorySegment seg; + + public WINDOW_BUFFER_SIZE_RECORD() { + this(Arena.ofAuto().allocate(LAYOUT)); + } + + WINDOW_BUFFER_SIZE_RECORD(MemorySegment seg) { + this.seg = seg; + } + + public COORD size() { + return new COORD(seg, SIZE_OFFSET); + } + + public String toString() { + return "WINDOW_BUFFER_SIZE_RECORD{size=" + this.size() + '}'; + } + } + + public static class MOUSE_EVENT_RECORD { + + private static final MemoryLayout LAYOUT = MemoryLayout.structLayout( + COORD.LAYOUT.withName("dwMousePosition"), + C_DWORD$LAYOUT.withName("dwButtonState"), + C_DWORD$LAYOUT.withName("dwControlKeyState"), + C_DWORD$LAYOUT.withName("dwEventFlags")); + private static final long MOUSE_POSITION_OFFSET = byteOffset(LAYOUT, "dwMousePosition"); + private static final VarHandle BUTTON_STATE = varHandle(LAYOUT, "dwButtonState"); + private static final VarHandle CONTROL_KEY_STATE = varHandle(LAYOUT, "dwControlKeyState"); + private static final VarHandle EVENT_FLAGS = varHandle(LAYOUT, "dwEventFlags"); + + private final MemorySegment seg; + + public MOUSE_EVENT_RECORD() { + this(Arena.ofAuto().allocate(LAYOUT)); + } + + MOUSE_EVENT_RECORD(MemorySegment seg) { + this.seg = Objects.requireNonNull(seg); + } + + MOUSE_EVENT_RECORD(MemorySegment seg, long offset) { + this.seg = Objects.requireNonNull(seg).asSlice(offset, LAYOUT.byteSize()); + } + + public COORD mousePosition() { + return new COORD(seg, MOUSE_POSITION_OFFSET); + } + + public int buttonState() { + return (int) BUTTON_STATE.get(seg); + } + + public int controlKeyState() { + return (int) CONTROL_KEY_STATE.get(seg); + } + + public int eventFlags() { + return (int) EVENT_FLAGS.get(seg); + } + + public String toString() { + return "MOUSE_EVENT_RECORD{mousePosition=" + mousePosition() + ", buttonState=" + buttonState() + + ", controlKeyState=" + controlKeyState() + ", eventFlags=" + eventFlags() + '}'; + } + } + + public static class KEY_EVENT_RECORD { + + static final MemoryLayout LAYOUT = MemoryLayout.structLayout( + JAVA_INT.withName("bKeyDown"), + ValueLayout.JAVA_SHORT.withName("wRepeatCount"), + ValueLayout.JAVA_SHORT.withName("wVirtualKeyCode"), + ValueLayout.JAVA_SHORT.withName("wVirtualScanCode"), + MemoryLayout.unionLayout( + ValueLayout.JAVA_CHAR.withName("UnicodeChar"), + ValueLayout.JAVA_BYTE.withName("AsciiChar")) + .withName("uChar"), + JAVA_INT.withName("dwControlKeyState")); + static final VarHandle bKeyDown$VH = varHandle(LAYOUT, "bKeyDown"); + static final VarHandle wRepeatCount$VH = varHandle(LAYOUT, "wRepeatCount"); + static final VarHandle wVirtualKeyCode$VH = varHandle(LAYOUT, "wVirtualKeyCode"); + static final VarHandle wVirtualScanCode$VH = varHandle(LAYOUT, "wVirtualScanCode"); + static final VarHandle UnicodeChar$VH = varHandle(LAYOUT, "uChar", "UnicodeChar"); + static final VarHandle AsciiChar$VH = varHandle(LAYOUT, "uChar", "AsciiChar"); + static final VarHandle dwControlKeyState$VH = varHandle(LAYOUT, "dwControlKeyState"); + + final MemorySegment seg; + + public KEY_EVENT_RECORD() { + this(Arena.ofAuto().allocate(LAYOUT)); + } + + KEY_EVENT_RECORD(MemorySegment seg) { + this.seg = seg; + } + + KEY_EVENT_RECORD(MemorySegment seg, long offset) { + this.seg = Objects.requireNonNull(seg).asSlice(offset, LAYOUT.byteSize()); + } + + public boolean keyDown() { + return (boolean) bKeyDown$VH.get(seg); + } + + public int repeatCount() { + return (int) wRepeatCount$VH.get(seg); + } + + public short keyCode() { + return (short) wVirtualKeyCode$VH.get(seg); + } + + public short scanCode() { + return (short) wVirtualScanCode$VH.get(seg); + } + + public char uchar() { + return (char) UnicodeChar$VH.get(seg); + } + + public int controlKeyState() { + return (int) dwControlKeyState$VH.get(seg); + } + + public String toString() { + return "KEY_EVENT_RECORD{keyDown=" + this.keyDown() + ", repeatCount=" + this.repeatCount() + ", keyCode=" + + this.keyCode() + ", scanCode=" + this.scanCode() + ", uchar=" + this.uchar() + + ", controlKeyState=" + + this.controlKeyState() + '}'; + } + } + + public static class CHAR_INFO { + + static final GroupLayout LAYOUT = MemoryLayout.structLayout( + MemoryLayout.unionLayout(C_WCHAR$LAYOUT.withName("UnicodeChar"), C_CHAR$LAYOUT.withName("AsciiChar")) + .withName("Char"), + C_WORD$LAYOUT.withName("Attributes")); + static final VarHandle UnicodeChar$VH = varHandle(LAYOUT, "Char", "UnicodeChar"); + static final VarHandle Attributes$VH = varHandle(LAYOUT, "Attributes"); + + final MemorySegment seg; + + public CHAR_INFO() { + this(Arena.ofAuto().allocate(LAYOUT)); + } + + public CHAR_INFO(char c, short a) { + this(); + UnicodeChar$VH.set(seg, c); + Attributes$VH.set(seg, a); + } + + CHAR_INFO(MemorySegment seg) { + this.seg = seg; + } + + public char unicodeChar() { + return (char) UnicodeChar$VH.get(seg); + } + } + + public static class CONSOLE_SCREEN_BUFFER_INFO { + static final GroupLayout LAYOUT = MemoryLayout.structLayout( + COORD.LAYOUT.withName("dwSize"), + COORD.LAYOUT.withName("dwCursorPosition"), + C_WORD$LAYOUT.withName("wAttributes"), + SMALL_RECT.LAYOUT.withName("srWindow"), + COORD.LAYOUT.withName("dwMaximumWindowSize")); + static final long dwSize$OFFSET = byteOffset(LAYOUT, "dwSize"); + static final long dwCursorPosition$OFFSET = byteOffset(LAYOUT, "dwCursorPosition"); + static final VarHandle wAttributes$VH = varHandle(LAYOUT, "wAttributes"); + static final long srWindow$OFFSET = byteOffset(LAYOUT, "srWindow"); + + private final MemorySegment seg; + + public CONSOLE_SCREEN_BUFFER_INFO() { + this(Arena.ofAuto().allocate(LAYOUT)); + } + + CONSOLE_SCREEN_BUFFER_INFO(MemorySegment seg) { + this.seg = seg; + } + + public COORD size() { + return new COORD(seg, dwSize$OFFSET); + } + + public COORD cursorPosition() { + return new COORD(seg, dwCursorPosition$OFFSET); + } + + public short attributes() { + return (short) wAttributes$VH.get(seg); + } + + public SMALL_RECT window() { + return new SMALL_RECT(seg, srWindow$OFFSET); + } + + public int windowWidth() { + return this.window().width() + 1; + } + + public int windowHeight() { + return this.window().height() + 1; + } + + public void attributes(short attr) { + wAttributes$VH.set(seg, attr); + } + } + + public static class COORD { + + static final GroupLayout LAYOUT = + MemoryLayout.structLayout(C_SHORT$LAYOUT.withName("x"), C_SHORT$LAYOUT.withName("y")); + static final VarHandle x$VH = varHandle(LAYOUT, "x"); + static final VarHandle y$VH = varHandle(LAYOUT, "y"); + + private final MemorySegment seg; + + public COORD() { + this(Arena.ofAuto().allocate(LAYOUT)); + } + + public COORD(short x, short y) { + this(Arena.ofAuto().allocate(LAYOUT)); + x(x); + y(y); + } + + public COORD(COORD from) { + this(Arena.ofAuto().allocate(LAYOUT).copyFrom(Objects.requireNonNull(from).seg)); + } + + COORD(MemorySegment seg) { + this.seg = seg; + } + + COORD(MemorySegment seg, long offset) { + this.seg = Objects.requireNonNull(seg).asSlice(offset, LAYOUT.byteSize()); + } + + public short x() { + return (short) COORD.x$VH.get(seg); + } + + public void x(short x) { + COORD.x$VH.set(seg, x); + } + + public short y() { + return (short) COORD.y$VH.get(seg); + } + + public void y(short y) { + COORD.y$VH.set(seg, y); + } + + public COORD copy() { + return new COORD(this); + } + } + + public static class SMALL_RECT { + + static final GroupLayout LAYOUT = MemoryLayout.structLayout( + C_SHORT$LAYOUT.withName("Left"), + C_SHORT$LAYOUT.withName("Top"), + C_SHORT$LAYOUT.withName("Right"), + C_SHORT$LAYOUT.withName("Bottom")); + static final VarHandle Left$VH = varHandle(LAYOUT, "Left"); + static final VarHandle Top$VH = varHandle(LAYOUT, "Top"); + static final VarHandle Right$VH = varHandle(LAYOUT, "Right"); + static final VarHandle Bottom$VH = varHandle(LAYOUT, "Bottom"); + + private final MemorySegment seg; + + public SMALL_RECT() { + this(Arena.ofAuto().allocate(LAYOUT)); + } + + public SMALL_RECT(SMALL_RECT from) { + this(Arena.ofAuto().allocate(LAYOUT).copyFrom(from.seg)); + } + + SMALL_RECT(MemorySegment seg, long offset) { + this(seg.asSlice(offset, LAYOUT.byteSize())); + } + + SMALL_RECT(MemorySegment seg) { + this.seg = seg; + } + + public short left() { + return (short) Left$VH.get(seg); + } + + public short top() { + return (short) Top$VH.get(seg); + } + + public short right() { + return (short) Right$VH.get(seg); + } + + public short bottom() { + return (short) Bottom$VH.get(seg); + } + + public short width() { + return (short) (this.right() - this.left()); + } + + public short height() { + return (short) (this.bottom() - this.top()); + } + + public void left(short l) { + Left$VH.set(seg, l); + } + + public void top(short t) { + Top$VH.set(seg, t); + } + + public SMALL_RECT copy() { + return new SMALL_RECT(this); + } + } + + private static final Linker LINKER = Linker.nativeLinker(); + + private static final SymbolLookup SYMBOL_LOOKUP; + + static { + SymbolLookup loaderLookup = SymbolLookup.loaderLookup(); + SYMBOL_LOOKUP = + name -> loaderLookup.find(name).or(() -> LINKER.defaultLookup().find(name)); + } + + static MethodHandle downcallHandle(String name, FunctionDescriptor fdesc) { + return SYMBOL_LOOKUP + .find(name) + .map(addr -> LINKER.downcallHandle(addr, fdesc)) + .orElse(null); + } + + static T requireNonNull(T obj, String symbolName) { + if (obj == null) { + throw new UnsatisfiedLinkError("unresolved symbol: " + symbolName); + } + return obj; + } + + static VarHandle varHandle(MemoryLayout layout, String e1) { + return layout.varHandle(MemoryLayout.PathElement.groupElement(e1)); + } + + static VarHandle varHandle(MemoryLayout layout, String e1, String e2) { + return layout.varHandle(MemoryLayout.PathElement.groupElement(e1), MemoryLayout.PathElement.groupElement(e2)); + } + + static long byteOffset(MemoryLayout layout, String e1) { + return layout.byteOffset(MemoryLayout.PathElement.groupElement(e1)); + } +} diff --git a/src/main/java/org/fusesource/jansi/ffm/WindowsAnsiProcessor.java b/src/main/java/org/fusesource/jansi/ffm/WindowsAnsiProcessor.java new file mode 100644 index 00000000..25d20030 --- /dev/null +++ b/src/main/java/org/fusesource/jansi/ffm/WindowsAnsiProcessor.java @@ -0,0 +1,439 @@ +/* + * 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.ffm; + +import java.io.IOException; +import java.io.OutputStream; +import java.lang.foreign.Arena; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; + +import org.fusesource.jansi.WindowsSupport; +import org.fusesource.jansi.io.AnsiProcessor; +import org.fusesource.jansi.io.Colors; + +import static org.fusesource.jansi.ffm.Kernel32.*; + +/** + * A Windows ANSI escape processor, that uses JNA to access native platform + * API's to change the console attributes (see + * Jansi native Kernel32). + *

The native library used is named jansi and is loaded using HawtJNI Runtime + * Library + * + * @since 1.19 + * @author Hiram Chirino + * @author Joris Kuipers + */ +public class WindowsAnsiProcessor extends AnsiProcessor { + + private final MemorySegment console; + + private static final short FOREGROUND_BLACK = 0; + private static final short FOREGROUND_YELLOW = (short) (FOREGROUND_RED | FOREGROUND_GREEN); + private static final short FOREGROUND_MAGENTA = (short) (FOREGROUND_BLUE | FOREGROUND_RED); + private static final short FOREGROUND_CYAN = (short) (FOREGROUND_BLUE | FOREGROUND_GREEN); + private static final short FOREGROUND_WHITE = (short) (FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE); + + private static final short BACKGROUND_BLACK = 0; + private static final short BACKGROUND_YELLOW = (short) (BACKGROUND_RED | BACKGROUND_GREEN); + private static final short BACKGROUND_MAGENTA = (short) (BACKGROUND_BLUE | BACKGROUND_RED); + private static final short BACKGROUND_CYAN = (short) (BACKGROUND_BLUE | BACKGROUND_GREEN); + private static final short BACKGROUND_WHITE = (short) (BACKGROUND_RED | BACKGROUND_GREEN | BACKGROUND_BLUE); + + private static final short[] ANSI_FOREGROUND_COLOR_MAP = { + FOREGROUND_BLACK, + FOREGROUND_RED, + FOREGROUND_GREEN, + FOREGROUND_YELLOW, + FOREGROUND_BLUE, + FOREGROUND_MAGENTA, + FOREGROUND_CYAN, + FOREGROUND_WHITE, + }; + + private static final short[] ANSI_BACKGROUND_COLOR_MAP = { + BACKGROUND_BLACK, + BACKGROUND_RED, + BACKGROUND_GREEN, + BACKGROUND_YELLOW, + BACKGROUND_BLUE, + BACKGROUND_MAGENTA, + BACKGROUND_CYAN, + BACKGROUND_WHITE, + }; + + private final CONSOLE_SCREEN_BUFFER_INFO info = new CONSOLE_SCREEN_BUFFER_INFO(); + private final short originalColors; + + private boolean negative; + private short savedX = -1; + private short savedY = -1; + + public WindowsAnsiProcessor(OutputStream ps, MemorySegment console) throws IOException { + super(ps); + this.console = console; + getConsoleInfo(); + originalColors = info.attributes(); + } + + public WindowsAnsiProcessor(OutputStream ps, boolean stdout) throws IOException { + this(ps, GetStdHandle(stdout ? STD_OUTPUT_HANDLE : STD_ERROR_HANDLE)); + } + + public WindowsAnsiProcessor(OutputStream ps) throws IOException { + this(ps, true); + } + + private void getConsoleInfo() throws IOException { + os.flush(); + if (GetConsoleScreenBufferInfo(console, info) == 0) { + throw new IOException("Could not get the screen info: " + WindowsSupport.getLastErrorMessage()); + } + if (negative) { + info.attributes(invertAttributeColors(info.attributes())); + } + } + + private void applyAttribute() throws IOException { + os.flush(); + short attributes = info.attributes(); + if (negative) { + attributes = invertAttributeColors(attributes); + } + if (SetConsoleTextAttribute(console, attributes) == 0) { + throw new IOException(WindowsSupport.getLastErrorMessage()); + } + } + + private short invertAttributeColors(short attributes) { + // Swap the the Foreground and Background bits. + int fg = 0x000F & attributes; + fg <<= 4; + int bg = 0X00F0 & attributes; + bg >>= 4; + attributes = (short) ((attributes & 0xFF00) | fg | bg); + return attributes; + } + + private void applyCursorPosition() throws IOException { + if (SetConsoleCursorPosition(console, info.cursorPosition().copy()) == 0) { + throw new IOException(WindowsSupport.getLastErrorMessage()); + } + } + + @Override + protected void processEraseScreen(int eraseOption) throws IOException { + getConsoleInfo(); + try (Arena session = Arena.ofConfined()) { + MemorySegment written = session.allocate(ValueLayout.JAVA_INT); + switch (eraseOption) { + case ERASE_SCREEN: + COORD topLeft = new COORD(); + topLeft.x((short) 0); + topLeft.y(info.window().top()); + int screenLength = info.window().height() * info.size().x(); + FillConsoleOutputAttribute(console, info.attributes(), screenLength, topLeft, written); + FillConsoleOutputCharacterW(console, ' ', screenLength, topLeft, written); + break; + case ERASE_SCREEN_TO_BEGINING: + COORD topLeft2 = new COORD(); + topLeft2.x((short) 0); + topLeft2.y(info.window().top()); + int lengthToCursor = + (info.cursorPosition().y() - info.window().top()) + * info.size().x() + + info.cursorPosition().x(); + FillConsoleOutputAttribute(console, info.attributes(), lengthToCursor, topLeft2, written); + FillConsoleOutputCharacterW(console, ' ', lengthToCursor, topLeft2, written); + break; + case ERASE_SCREEN_TO_END: + int lengthToEnd = + (info.window().bottom() - info.cursorPosition().y()) + * info.size().x() + + (info.size().x() - info.cursorPosition().x()); + FillConsoleOutputAttribute( + console, + info.attributes(), + lengthToEnd, + info.cursorPosition().copy(), + written); + FillConsoleOutputCharacterW( + console, ' ', lengthToEnd, info.cursorPosition().copy(), written); + break; + default: + break; + } + } + } + + @Override + protected void processEraseLine(int eraseOption) throws IOException { + getConsoleInfo(); + try (Arena session = Arena.ofConfined()) { + MemorySegment written = session.allocate(ValueLayout.JAVA_INT); + switch (eraseOption) { + case ERASE_LINE: + COORD leftColCurrRow = info.cursorPosition().copy(); + leftColCurrRow.x((short) 0); + FillConsoleOutputAttribute( + console, info.attributes(), info.size().x(), leftColCurrRow, written); + FillConsoleOutputCharacterW(console, ' ', info.size().x(), leftColCurrRow, written); + break; + case ERASE_LINE_TO_BEGINING: + COORD leftColCurrRow2 = info.cursorPosition().copy(); + leftColCurrRow2.x((short) 0); + FillConsoleOutputAttribute( + console, info.attributes(), info.cursorPosition().x(), leftColCurrRow2, written); + FillConsoleOutputCharacterW( + console, ' ', info.cursorPosition().x(), leftColCurrRow2, written); + break; + case ERASE_LINE_TO_END: + int lengthToLastCol = + info.size().x() - info.cursorPosition().x(); + FillConsoleOutputAttribute( + console, + info.attributes(), + lengthToLastCol, + info.cursorPosition().copy(), + written); + FillConsoleOutputCharacterW( + console, ' ', lengthToLastCol, info.cursorPosition().copy(), written); + break; + default: + break; + } + } + } + + @Override + protected void processCursorLeft(int count) throws IOException { + getConsoleInfo(); + info.cursorPosition().x((short) Math.max(0, info.cursorPosition().x() - count)); + applyCursorPosition(); + } + + @Override + protected void processCursorRight(int count) throws IOException { + getConsoleInfo(); + info.cursorPosition() + .x((short) Math.min(info.window().width(), info.cursorPosition().x() + count)); + applyCursorPosition(); + } + + @Override + protected void processCursorDown(int count) throws IOException { + getConsoleInfo(); + info.cursorPosition().y((short) + Math.min(Math.max(0, info.size().y() - 1), info.cursorPosition().y() + count)); + applyCursorPosition(); + } + + @Override + protected void processCursorUp(int count) throws IOException { + getConsoleInfo(); + info.cursorPosition() + .y((short) Math.max(info.window().top(), info.cursorPosition().y() - count)); + applyCursorPosition(); + } + + @Override + protected void processCursorTo(int row, int col) throws IOException { + getConsoleInfo(); + info.cursorPosition().y((short) Math.max( + info.window().top(), Math.min(info.size().y(), info.window().top() + row - 1))); + info.cursorPosition().x((short) Math.max(0, Math.min(info.window().width(), col - 1))); + applyCursorPosition(); + } + + @Override + protected void processCursorToColumn(int x) throws IOException { + getConsoleInfo(); + info.cursorPosition().x((short) Math.max(0, Math.min(info.window().width(), x - 1))); + applyCursorPosition(); + } + + @Override + protected void processCursorUpLine(int count) throws IOException { + getConsoleInfo(); + info.cursorPosition().x((short) 0); + info.cursorPosition() + .y((short) Math.max(info.window().top(), info.cursorPosition().y() - count)); + applyCursorPosition(); + } + + @Override + protected void processCursorDownLine(int count) throws IOException { + getConsoleInfo(); + info.cursorPosition().x((short) 0); + info.cursorPosition() + .y((short) Math.max(info.window().top(), info.cursorPosition().y() + count)); + applyCursorPosition(); + } + + @Override + protected void processSetForegroundColor(int color, boolean bright) throws IOException { + info.attributes((short) ((info.attributes() & ~0x0007) | ANSI_FOREGROUND_COLOR_MAP[color])); + if (bright) { + info.attributes((short) (info.attributes() | FOREGROUND_INTENSITY)); + } + applyAttribute(); + } + + @Override + protected void processSetForegroundColorExt(int paletteIndex) throws IOException { + int round = Colors.roundColor(paletteIndex, 16); + processSetForegroundColor(round >= 8 ? round - 8 : round, round >= 8); + } + + @Override + protected void processSetForegroundColorExt(int r, int g, int b) throws IOException { + int round = Colors.roundRgbColor(r, g, b, 16); + processSetForegroundColor(round >= 8 ? round - 8 : round, round >= 8); + } + + @Override + protected void processSetBackgroundColor(int color, boolean bright) throws IOException { + info.attributes((short) ((info.attributes() & ~0x0070) | ANSI_BACKGROUND_COLOR_MAP[color])); + if (bright) { + info.attributes((short) (info.attributes() | BACKGROUND_INTENSITY)); + } + applyAttribute(); + } + + @Override + protected void processSetBackgroundColorExt(int paletteIndex) throws IOException { + int round = Colors.roundColor(paletteIndex, 16); + processSetBackgroundColor(round >= 8 ? round - 8 : round, round >= 8); + } + + @Override + protected void processSetBackgroundColorExt(int r, int g, int b) throws IOException { + int round = Colors.roundRgbColor(r, g, b, 16); + processSetBackgroundColor(round >= 8 ? round - 8 : round, round >= 8); + } + + @Override + protected void processDefaultTextColor() throws IOException { + info.attributes((short) ((info.attributes() & ~0x000F) | (originalColors & 0xF))); + info.attributes((short) (info.attributes() & ~FOREGROUND_INTENSITY)); + applyAttribute(); + } + + @Override + protected void processDefaultBackgroundColor() throws IOException { + info.attributes((short) ((info.attributes() & ~0x00F0) | (originalColors & 0xF0))); + info.attributes((short) (info.attributes() & ~BACKGROUND_INTENSITY)); + applyAttribute(); + } + + @Override + protected void processAttributeReset() throws IOException { + info.attributes((short) ((info.attributes() & ~0x00FF) | originalColors)); + this.negative = false; + applyAttribute(); + } + + @Override + protected void processSetAttribute(int attribute) throws IOException { + switch (attribute) { + case ATTRIBUTE_INTENSITY_BOLD: + info.attributes((short) (info.attributes() | FOREGROUND_INTENSITY)); + applyAttribute(); + break; + case ATTRIBUTE_INTENSITY_NORMAL: + info.attributes((short) (info.attributes() & ~FOREGROUND_INTENSITY)); + applyAttribute(); + break; + + // Yeah, setting the background intensity is not underlining.. but it's best we can do + // using the Windows console API + case ATTRIBUTE_UNDERLINE: + info.attributes((short) (info.attributes() | BACKGROUND_INTENSITY)); + applyAttribute(); + break; + case ATTRIBUTE_UNDERLINE_OFF: + info.attributes((short) (info.attributes() & ~BACKGROUND_INTENSITY)); + applyAttribute(); + break; + + case ATTRIBUTE_NEGATIVE_ON: + negative = true; + applyAttribute(); + break; + case ATTRIBUTE_NEGATIVE_OFF: + negative = false; + applyAttribute(); + break; + default: + break; + } + } + + @Override + protected void processSaveCursorPosition() throws IOException { + getConsoleInfo(); + savedX = info.cursorPosition().x(); + savedY = info.cursorPosition().y(); + } + + @Override + protected void processRestoreCursorPosition() throws IOException { + // restore only if there was a save operation first + if (savedX != -1 && savedY != -1) { + os.flush(); + info.cursorPosition().x(savedX); + info.cursorPosition().y(savedY); + applyCursorPosition(); + } + } + + @Override + protected void processInsertLine(int optionInt) throws IOException { + getConsoleInfo(); + SMALL_RECT scroll = info.window().copy(); + scroll.top(info.cursorPosition().y()); + COORD org = new COORD(); + org.x((short) 0); + org.y((short) (info.cursorPosition().y() + optionInt)); + CHAR_INFO info = new CHAR_INFO(' ', originalColors); + if (ScrollConsoleScreenBuffer(console, scroll, scroll, org, info) == 0) { + throw new IOException(WindowsSupport.getLastErrorMessage()); + } + } + + @Override + protected void processDeleteLine(int optionInt) throws IOException { + getConsoleInfo(); + SMALL_RECT scroll = info.window().copy(); + scroll.top(info.cursorPosition().y()); + COORD org = new COORD(); + org.x((short) 0); + org.y((short) (info.cursorPosition().y() - optionInt)); + CHAR_INFO info = new CHAR_INFO(' ', originalColors); + if (ScrollConsoleScreenBuffer(console, scroll, scroll, org, info) == 0) { + throw new IOException(WindowsSupport.getLastErrorMessage()); + } + } + + @Override + protected void processChangeWindowTitle(String title) { + try (Arena session = Arena.ofConfined()) { + MemorySegment str = session.allocateUtf8String(title); + SetConsoleTitleW(str); + } + } +} diff --git a/src/main/java/org/fusesource/jansi/internal/AnsiConsoleSupportJni.java b/src/main/java/org/fusesource/jansi/internal/AnsiConsoleSupportJni.java new file mode 100644 index 00000000..83f1a31e --- /dev/null +++ b/src/main/java/org/fusesource/jansi/internal/AnsiConsoleSupportJni.java @@ -0,0 +1,108 @@ +/* + * 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; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; + +import org.fusesource.jansi.AnsiConsoleSupport; +import org.fusesource.jansi.io.AnsiProcessor; +import org.fusesource.jansi.io.WindowsAnsiProcessor; + +import static org.fusesource.jansi.internal.Kernel32.FORMAT_MESSAGE_FROM_SYSTEM; +import static org.fusesource.jansi.internal.Kernel32.FormatMessageW; +import static org.fusesource.jansi.internal.Kernel32.GetConsoleMode; +import static org.fusesource.jansi.internal.Kernel32.GetConsoleScreenBufferInfo; +import static org.fusesource.jansi.internal.Kernel32.GetLastError; +import static org.fusesource.jansi.internal.Kernel32.GetStdHandle; +import static org.fusesource.jansi.internal.Kernel32.STD_ERROR_HANDLE; +import static org.fusesource.jansi.internal.Kernel32.STD_OUTPUT_HANDLE; +import static org.fusesource.jansi.internal.Kernel32.SetConsoleMode; + +public class AnsiConsoleSupportJni implements AnsiConsoleSupport { + + @Override + public String getProviderName() { + return "jni"; + } + + @Override + public CLibrary getCLibrary() { + return new CLibrary() { + @Override + public short getTerminalWidth(int fd) { + return org.fusesource.jansi.internal.CLibrary.getTerminalWidth(fd); + } + + @Override + public int isTty(int fd) { + return org.fusesource.jansi.internal.CLibrary.isatty(fd); + } + }; + } + + @Override + public Kernel32 getKernel32() { + return new Kernel32() { + @Override + public int isTty(long console) { + int[] mode = new int[1]; + return GetConsoleMode(console, mode); + } + + @Override + public int getTerminalWidth(long console) { + org.fusesource.jansi.internal.Kernel32.CONSOLE_SCREEN_BUFFER_INFO info = + new org.fusesource.jansi.internal.Kernel32.CONSOLE_SCREEN_BUFFER_INFO(); + GetConsoleScreenBufferInfo(console, info); + return info.windowWidth(); + } + + public long getStdHandle(boolean stdout) { + return GetStdHandle(stdout ? STD_OUTPUT_HANDLE : STD_ERROR_HANDLE); + } + + @Override + public int getConsoleMode(long console, int[] mode) { + return GetConsoleMode(console, mode); + } + + @Override + public int setConsoleMode(long console, int mode) { + return SetConsoleMode(console, mode); + } + + @Override + public int getLastError() { + return GetLastError(); + } + + @Override + public String getErrorMessage(int errorCode) { + int bufferSize = 160; + byte[] data = new byte[bufferSize]; + FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM, 0, errorCode, 0, data, bufferSize, null); + return new String(data, StandardCharsets.UTF_16LE).trim(); + } + + @Override + public AnsiProcessor newProcessor(OutputStream os, long console) throws IOException { + return new WindowsAnsiProcessor(os, console); + } + }; + } +} diff --git a/src/main/java/org/fusesource/jansi/internal/CLibrary.java b/src/main/java/org/fusesource/jansi/internal/CLibrary.java index 2e2285c3..24e6ddfb 100644 --- a/src/main/java/org/fusesource/jansi/internal/CLibrary.java +++ b/src/main/java/org/fusesource/jansi/internal/CLibrary.java @@ -44,10 +44,6 @@ public class CLibrary { // Constants // - public static int STDOUT_FILENO = 1; - - public static int STDERR_FILENO = 2; - public static boolean HAVE_ISATTY; public static boolean HAVE_TTYNAME; @@ -103,6 +99,12 @@ public class CLibrary { public static native int ioctl(int filedes, long request, WinSize params); + public static short getTerminalWidth(int fd) { + WinSize sz = new WinSize(); + ioctl(fd, TIOCGWINSZ, sz); + return sz.ws_col; + } + /** * Window sizes. * diff --git a/src/main/java/org/fusesource/jansi/internal/WindowsAnsiProcessor.java b/src/main/java/org/fusesource/jansi/internal/WindowsAnsiProcessor.java new file mode 100644 index 00000000..e8e64aa9 --- /dev/null +++ b/src/main/java/org/fusesource/jansi/internal/WindowsAnsiProcessor.java @@ -0,0 +1,407 @@ +/* + * 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; + +import java.io.IOException; +import java.io.OutputStream; + +import org.fusesource.jansi.WindowsSupport; +import org.fusesource.jansi.io.AnsiProcessor; +import org.fusesource.jansi.io.Colors; + +import static org.fusesource.jansi.internal.Kernel32.*; + +/** + * A Windows ANSI escape processor, that uses JNA to access native platform + * API's to change the console attributes (see + * Jansi native Kernel32). + *

The native library used is named jansi and is loaded using HawtJNI Runtime + * Library + * + * @since 1.19 + * @author Hiram Chirino + * @author Joris Kuipers + */ +public class WindowsAnsiProcessor extends AnsiProcessor { + + private final long console; + + private static final short FOREGROUND_BLACK = 0; + private static final short FOREGROUND_YELLOW = (short) (FOREGROUND_RED | FOREGROUND_GREEN); + private static final short FOREGROUND_MAGENTA = (short) (FOREGROUND_BLUE | FOREGROUND_RED); + private static final short FOREGROUND_CYAN = (short) (FOREGROUND_BLUE | FOREGROUND_GREEN); + private static final short FOREGROUND_WHITE = (short) (FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE); + + private static final short BACKGROUND_BLACK = 0; + private static final short BACKGROUND_YELLOW = (short) (BACKGROUND_RED | BACKGROUND_GREEN); + private static final short BACKGROUND_MAGENTA = (short) (BACKGROUND_BLUE | BACKGROUND_RED); + private static final short BACKGROUND_CYAN = (short) (BACKGROUND_BLUE | BACKGROUND_GREEN); + private static final short BACKGROUND_WHITE = (short) (BACKGROUND_RED | BACKGROUND_GREEN | BACKGROUND_BLUE); + + private static final short[] ANSI_FOREGROUND_COLOR_MAP = { + FOREGROUND_BLACK, + FOREGROUND_RED, + FOREGROUND_GREEN, + FOREGROUND_YELLOW, + FOREGROUND_BLUE, + FOREGROUND_MAGENTA, + FOREGROUND_CYAN, + FOREGROUND_WHITE, + }; + + private static final short[] ANSI_BACKGROUND_COLOR_MAP = { + BACKGROUND_BLACK, + BACKGROUND_RED, + BACKGROUND_GREEN, + BACKGROUND_YELLOW, + BACKGROUND_BLUE, + BACKGROUND_MAGENTA, + BACKGROUND_CYAN, + BACKGROUND_WHITE, + }; + + private final CONSOLE_SCREEN_BUFFER_INFO info = new CONSOLE_SCREEN_BUFFER_INFO(); + private final short originalColors; + + private boolean negative; + private short savedX = -1; + private short savedY = -1; + + public WindowsAnsiProcessor(OutputStream ps, long console) throws IOException { + super(ps); + this.console = console; + getConsoleInfo(); + originalColors = info.attributes; + } + + public WindowsAnsiProcessor(OutputStream ps, boolean stdout) throws IOException { + this(ps, GetStdHandle(stdout ? STD_OUTPUT_HANDLE : STD_ERROR_HANDLE)); + } + + public WindowsAnsiProcessor(OutputStream ps) throws IOException { + this(ps, true); + } + + private void getConsoleInfo() throws IOException { + os.flush(); + if (GetConsoleScreenBufferInfo(console, info) == 0) { + throw new IOException("Could not get the screen info: " + WindowsSupport.getLastErrorMessage()); + } + if (negative) { + info.attributes = invertAttributeColors(info.attributes); + } + } + + private void applyAttribute() throws IOException { + os.flush(); + short attributes = info.attributes; + if (negative) { + attributes = invertAttributeColors(attributes); + } + if (SetConsoleTextAttribute(console, attributes) == 0) { + throw new IOException(WindowsSupport.getLastErrorMessage()); + } + } + + private short invertAttributeColors(short attributes) { + // Swap the the Foreground and Background bits. + int fg = 0x000F & attributes; + fg <<= 4; + int bg = 0X00F0 & attributes; + bg >>= 4; + attributes = (short) ((attributes & 0xFF00) | fg | bg); + return attributes; + } + + private void applyCursorPosition() throws IOException { + if (SetConsoleCursorPosition(console, info.cursorPosition.copy()) == 0) { + throw new IOException(WindowsSupport.getLastErrorMessage()); + } + } + + @Override + protected void processEraseScreen(int eraseOption) throws IOException { + getConsoleInfo(); + int[] written = new int[1]; + switch (eraseOption) { + case ERASE_SCREEN: + COORD topLeft = new COORD(); + topLeft.x = 0; + topLeft.y = info.window.top; + int screenLength = info.window.height() * info.size.x; + FillConsoleOutputAttribute(console, info.attributes, screenLength, topLeft, written); + FillConsoleOutputCharacterW(console, ' ', screenLength, topLeft, written); + break; + case ERASE_SCREEN_TO_BEGINING: + COORD topLeft2 = new COORD(); + topLeft2.x = 0; + topLeft2.y = info.window.top; + int lengthToCursor = (info.cursorPosition.y - info.window.top) * info.size.x + info.cursorPosition.x; + FillConsoleOutputAttribute(console, info.attributes, lengthToCursor, topLeft2, written); + FillConsoleOutputCharacterW(console, ' ', lengthToCursor, topLeft2, written); + break; + case ERASE_SCREEN_TO_END: + int lengthToEnd = (info.window.bottom - info.cursorPosition.y) * info.size.x + + (info.size.x - info.cursorPosition.x); + FillConsoleOutputAttribute(console, info.attributes, lengthToEnd, info.cursorPosition.copy(), written); + FillConsoleOutputCharacterW(console, ' ', lengthToEnd, info.cursorPosition.copy(), written); + break; + default: + break; + } + } + + @Override + protected void processEraseLine(int eraseOption) throws IOException { + getConsoleInfo(); + int[] written = new int[1]; + switch (eraseOption) { + case ERASE_LINE: + COORD leftColCurrRow = info.cursorPosition.copy(); + leftColCurrRow.x = 0; + FillConsoleOutputAttribute(console, info.attributes, info.size.x, leftColCurrRow, written); + FillConsoleOutputCharacterW(console, ' ', info.size.x, leftColCurrRow, written); + break; + case ERASE_LINE_TO_BEGINING: + COORD leftColCurrRow2 = info.cursorPosition.copy(); + leftColCurrRow2.x = 0; + FillConsoleOutputAttribute(console, info.attributes, info.cursorPosition.x, leftColCurrRow2, written); + FillConsoleOutputCharacterW(console, ' ', info.cursorPosition.x, leftColCurrRow2, written); + break; + case ERASE_LINE_TO_END: + int lengthToLastCol = info.size.x - info.cursorPosition.x; + FillConsoleOutputAttribute( + console, info.attributes, lengthToLastCol, info.cursorPosition.copy(), written); + FillConsoleOutputCharacterW(console, ' ', lengthToLastCol, info.cursorPosition.copy(), written); + break; + default: + break; + } + } + + @Override + protected void processCursorLeft(int count) throws IOException { + getConsoleInfo(); + info.cursorPosition.x = (short) Math.max(0, info.cursorPosition.x - count); + applyCursorPosition(); + } + + @Override + protected void processCursorRight(int count) throws IOException { + getConsoleInfo(); + info.cursorPosition.x = (short) Math.min(info.window.width(), info.cursorPosition.x + count); + applyCursorPosition(); + } + + @Override + protected void processCursorDown(int count) throws IOException { + getConsoleInfo(); + info.cursorPosition.y = (short) Math.min(Math.max(0, info.size.y - 1), info.cursorPosition.y + count); + applyCursorPosition(); + } + + @Override + protected void processCursorUp(int count) throws IOException { + getConsoleInfo(); + info.cursorPosition.y = (short) Math.max(info.window.top, info.cursorPosition.y - count); + applyCursorPosition(); + } + + @Override + protected void processCursorTo(int row, int col) throws IOException { + getConsoleInfo(); + info.cursorPosition.y = (short) Math.max(info.window.top, Math.min(info.size.y, info.window.top + row - 1)); + info.cursorPosition.x = (short) Math.max(0, Math.min(info.window.width(), col - 1)); + applyCursorPosition(); + } + + @Override + protected void processCursorToColumn(int x) throws IOException { + getConsoleInfo(); + info.cursorPosition.x = (short) Math.max(0, Math.min(info.window.width(), x - 1)); + applyCursorPosition(); + } + + @Override + protected void processCursorUpLine(int count) throws IOException { + getConsoleInfo(); + info.cursorPosition.x = 0; + info.cursorPosition.y = (short) Math.max(info.window.top, info.cursorPosition.y - count); + applyCursorPosition(); + } + + @Override + protected void processCursorDownLine(int count) throws IOException { + getConsoleInfo(); + info.cursorPosition.x = 0; + info.cursorPosition.y = (short) Math.max(info.window.top, info.cursorPosition.y + count); + applyCursorPosition(); + } + + @Override + protected void processSetForegroundColor(int color, boolean bright) throws IOException { + info.attributes = (short) ((info.attributes & ~0x0007) | ANSI_FOREGROUND_COLOR_MAP[color]); + if (bright) { + info.attributes |= FOREGROUND_INTENSITY; + } + applyAttribute(); + } + + @Override + protected void processSetForegroundColorExt(int paletteIndex) throws IOException { + int round = Colors.roundColor(paletteIndex, 16); + processSetForegroundColor(round >= 8 ? round - 8 : round, round >= 8); + } + + @Override + protected void processSetForegroundColorExt(int r, int g, int b) throws IOException { + int round = Colors.roundRgbColor(r, g, b, 16); + processSetForegroundColor(round >= 8 ? round - 8 : round, round >= 8); + } + + @Override + protected void processSetBackgroundColor(int color, boolean bright) throws IOException { + info.attributes = (short) ((info.attributes & ~0x0070) | ANSI_BACKGROUND_COLOR_MAP[color]); + if (bright) { + info.attributes |= BACKGROUND_INTENSITY; + } + applyAttribute(); + } + + @Override + protected void processSetBackgroundColorExt(int paletteIndex) throws IOException { + int round = Colors.roundColor(paletteIndex, 16); + processSetBackgroundColor(round >= 8 ? round - 8 : round, round >= 8); + } + + @Override + protected void processSetBackgroundColorExt(int r, int g, int b) throws IOException { + int round = Colors.roundRgbColor(r, g, b, 16); + processSetBackgroundColor(round >= 8 ? round - 8 : round, round >= 8); + } + + @Override + protected void processDefaultTextColor() throws IOException { + info.attributes = (short) ((info.attributes & ~0x000F) | (originalColors & 0xF)); + info.attributes = (short) (info.attributes & ~FOREGROUND_INTENSITY); + applyAttribute(); + } + + @Override + protected void processDefaultBackgroundColor() throws IOException { + info.attributes = (short) ((info.attributes & ~0x00F0) | (originalColors & 0xF0)); + info.attributes = (short) (info.attributes & ~BACKGROUND_INTENSITY); + applyAttribute(); + } + + @Override + protected void processAttributeReset() throws IOException { + info.attributes = (short) ((info.attributes & ~0x00FF) | originalColors); + this.negative = false; + applyAttribute(); + } + + @Override + protected void processSetAttribute(int attribute) throws IOException { + switch (attribute) { + case ATTRIBUTE_INTENSITY_BOLD: + info.attributes = (short) (info.attributes | FOREGROUND_INTENSITY); + applyAttribute(); + break; + case ATTRIBUTE_INTENSITY_NORMAL: + info.attributes = (short) (info.attributes & ~FOREGROUND_INTENSITY); + applyAttribute(); + break; + + // Yeah, setting the background intensity is not underlining.. but it's best we can do + // using the Windows console API + case ATTRIBUTE_UNDERLINE: + info.attributes = (short) (info.attributes | BACKGROUND_INTENSITY); + applyAttribute(); + break; + case ATTRIBUTE_UNDERLINE_OFF: + info.attributes = (short) (info.attributes & ~BACKGROUND_INTENSITY); + applyAttribute(); + break; + + case ATTRIBUTE_NEGATIVE_ON: + negative = true; + applyAttribute(); + break; + case ATTRIBUTE_NEGATIVE_OFF: + negative = false; + applyAttribute(); + break; + default: + break; + } + } + + @Override + protected void processSaveCursorPosition() throws IOException { + getConsoleInfo(); + savedX = info.cursorPosition.x; + savedY = info.cursorPosition.y; + } + + @Override + protected void processRestoreCursorPosition() throws IOException { + // restore only if there was a save operation first + if (savedX != -1 && savedY != -1) { + os.flush(); + info.cursorPosition.x = savedX; + info.cursorPosition.y = savedY; + applyCursorPosition(); + } + } + + @Override + protected void processInsertLine(int optionInt) throws IOException { + getConsoleInfo(); + SMALL_RECT scroll = info.window.copy(); + scroll.top = info.cursorPosition.y; + COORD org = new COORD(); + org.x = 0; + org.y = (short) (info.cursorPosition.y + optionInt); + CHAR_INFO info = new CHAR_INFO(); + info.attributes = originalColors; + info.unicodeChar = ' '; + if (ScrollConsoleScreenBuffer(console, scroll, scroll, org, info) == 0) { + throw new IOException(WindowsSupport.getLastErrorMessage()); + } + } + + @Override + protected void processDeleteLine(int optionInt) throws IOException { + getConsoleInfo(); + SMALL_RECT scroll = info.window.copy(); + scroll.top = info.cursorPosition.y; + COORD org = new COORD(); + org.x = 0; + org.y = (short) (info.cursorPosition.y - optionInt); + CHAR_INFO info = new CHAR_INFO(); + info.attributes = originalColors; + info.unicodeChar = ' '; + if (ScrollConsoleScreenBuffer(console, scroll, scroll, org, info) == 0) { + throw new IOException(WindowsSupport.getLastErrorMessage()); + } + } + + @Override + protected void processChangeWindowTitle(String label) { + SetConsoleTitle(label); + } +} diff --git a/src/main/java/org/fusesource/jansi/io/WindowsAnsiProcessor.java b/src/main/java/org/fusesource/jansi/io/WindowsAnsiProcessor.java index 74a178a5..dccb8403 100644 --- a/src/main/java/org/fusesource/jansi/io/WindowsAnsiProcessor.java +++ b/src/main/java/org/fusesource/jansi/io/WindowsAnsiProcessor.java @@ -18,31 +18,6 @@ import java.io.IOException; import java.io.OutputStream; -import org.fusesource.jansi.WindowsSupport; -import org.fusesource.jansi.internal.Kernel32.CONSOLE_SCREEN_BUFFER_INFO; -import org.fusesource.jansi.internal.Kernel32.COORD; - -import static org.fusesource.jansi.internal.Kernel32.BACKGROUND_BLUE; -import static org.fusesource.jansi.internal.Kernel32.BACKGROUND_GREEN; -import static org.fusesource.jansi.internal.Kernel32.BACKGROUND_INTENSITY; -import static org.fusesource.jansi.internal.Kernel32.BACKGROUND_RED; -import static org.fusesource.jansi.internal.Kernel32.CHAR_INFO; -import static org.fusesource.jansi.internal.Kernel32.FOREGROUND_BLUE; -import static org.fusesource.jansi.internal.Kernel32.FOREGROUND_GREEN; -import static org.fusesource.jansi.internal.Kernel32.FOREGROUND_INTENSITY; -import static org.fusesource.jansi.internal.Kernel32.FOREGROUND_RED; -import static org.fusesource.jansi.internal.Kernel32.FillConsoleOutputAttribute; -import static org.fusesource.jansi.internal.Kernel32.FillConsoleOutputCharacterW; -import static org.fusesource.jansi.internal.Kernel32.GetConsoleScreenBufferInfo; -import static org.fusesource.jansi.internal.Kernel32.GetStdHandle; -import static org.fusesource.jansi.internal.Kernel32.SMALL_RECT; -import static org.fusesource.jansi.internal.Kernel32.STD_ERROR_HANDLE; -import static org.fusesource.jansi.internal.Kernel32.STD_OUTPUT_HANDLE; -import static org.fusesource.jansi.internal.Kernel32.ScrollConsoleScreenBuffer; -import static org.fusesource.jansi.internal.Kernel32.SetConsoleCursorPosition; -import static org.fusesource.jansi.internal.Kernel32.SetConsoleTextAttribute; -import static org.fusesource.jansi.internal.Kernel32.SetConsoleTitle; - /** * A Windows ANSI escape processor, that uses JNA to access native platform * API's to change the console attributes (see @@ -51,374 +26,20 @@ * Library * * @since 1.19 + * @author Hiram Chirino + * @author Joris Kuipers */ -public final class WindowsAnsiProcessor extends AnsiProcessor { - - private final long console; - - private static final short FOREGROUND_BLACK = 0; - private static final short FOREGROUND_YELLOW = (short) (FOREGROUND_RED | FOREGROUND_GREEN); - private static final short FOREGROUND_MAGENTA = (short) (FOREGROUND_BLUE | FOREGROUND_RED); - private static final short FOREGROUND_CYAN = (short) (FOREGROUND_BLUE | FOREGROUND_GREEN); - private static final short FOREGROUND_WHITE = (short) (FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE); - - private static final short BACKGROUND_BLACK = 0; - private static final short BACKGROUND_YELLOW = (short) (BACKGROUND_RED | BACKGROUND_GREEN); - private static final short BACKGROUND_MAGENTA = (short) (BACKGROUND_BLUE | BACKGROUND_RED); - private static final short BACKGROUND_CYAN = (short) (BACKGROUND_BLUE | BACKGROUND_GREEN); - private static final short BACKGROUND_WHITE = (short) (BACKGROUND_RED | BACKGROUND_GREEN | BACKGROUND_BLUE); - - private static final short[] ANSI_FOREGROUND_COLOR_MAP = { - FOREGROUND_BLACK, - FOREGROUND_RED, - FOREGROUND_GREEN, - FOREGROUND_YELLOW, - FOREGROUND_BLUE, - FOREGROUND_MAGENTA, - FOREGROUND_CYAN, - FOREGROUND_WHITE, - }; - - private static final short[] ANSI_BACKGROUND_COLOR_MAP = { - BACKGROUND_BLACK, - BACKGROUND_RED, - BACKGROUND_GREEN, - BACKGROUND_YELLOW, - BACKGROUND_BLUE, - BACKGROUND_MAGENTA, - BACKGROUND_CYAN, - BACKGROUND_WHITE, - }; - - private final CONSOLE_SCREEN_BUFFER_INFO info = new CONSOLE_SCREEN_BUFFER_INFO(); - private final short originalColors; - - private boolean negative; - private short savedX = -1; - private short savedY = -1; +public final class WindowsAnsiProcessor extends org.fusesource.jansi.internal.WindowsAnsiProcessor { public WindowsAnsiProcessor(OutputStream ps, long console) throws IOException { - super(ps); - this.console = console; - getConsoleInfo(); - originalColors = info.attributes; + super(ps, console); } public WindowsAnsiProcessor(OutputStream ps, boolean stdout) throws IOException { - this(ps, GetStdHandle(stdout ? STD_OUTPUT_HANDLE : STD_ERROR_HANDLE)); + super(ps, stdout); } public WindowsAnsiProcessor(OutputStream ps) throws IOException { - this(ps, true); - } - - private void getConsoleInfo() throws IOException { - os.flush(); - if (GetConsoleScreenBufferInfo(console, info) == 0) { - throw new IOException("Could not get the screen info: " + WindowsSupport.getLastErrorMessage()); - } - if (negative) { - info.attributes = invertAttributeColors(info.attributes); - } - } - - private void applyAttribute() throws IOException { - os.flush(); - short attributes = info.attributes; - if (negative) { - attributes = invertAttributeColors(attributes); - } - if (SetConsoleTextAttribute(console, attributes) == 0) { - throw new IOException(WindowsSupport.getLastErrorMessage()); - } - } - - private short invertAttributeColors(short attributes) { - // Swap the the Foreground and Background bits. - int fg = 0x000F & attributes; - fg <<= 4; - int bg = 0X00F0 & attributes; - bg >>= 4; - attributes = (short) ((attributes & 0xFF00) | fg | bg); - return attributes; - } - - private void applyCursorPosition() throws IOException { - if (SetConsoleCursorPosition(console, info.cursorPosition.copy()) == 0) { - throw new IOException(WindowsSupport.getLastErrorMessage()); - } - } - - @Override - protected void processEraseScreen(int eraseOption) throws IOException { - getConsoleInfo(); - int[] written = new int[1]; - switch (eraseOption) { - case ERASE_SCREEN: - COORD topLeft = new COORD(); - topLeft.x = 0; - topLeft.y = info.window.top; - int screenLength = info.window.height() * info.size.x; - FillConsoleOutputAttribute(console, info.attributes, screenLength, topLeft, written); - FillConsoleOutputCharacterW(console, ' ', screenLength, topLeft, written); - break; - case ERASE_SCREEN_TO_BEGINING: - COORD topLeft2 = new COORD(); - topLeft2.x = 0; - topLeft2.y = info.window.top; - int lengthToCursor = (info.cursorPosition.y - info.window.top) * info.size.x + info.cursorPosition.x; - FillConsoleOutputAttribute(console, info.attributes, lengthToCursor, topLeft2, written); - FillConsoleOutputCharacterW(console, ' ', lengthToCursor, topLeft2, written); - break; - case ERASE_SCREEN_TO_END: - int lengthToEnd = (info.window.bottom - info.cursorPosition.y) * info.size.x - + (info.size.x - info.cursorPosition.x); - FillConsoleOutputAttribute(console, info.attributes, lengthToEnd, info.cursorPosition.copy(), written); - FillConsoleOutputCharacterW(console, ' ', lengthToEnd, info.cursorPosition.copy(), written); - break; - default: - break; - } - } - - @Override - protected void processEraseLine(int eraseOption) throws IOException { - getConsoleInfo(); - int[] written = new int[1]; - switch (eraseOption) { - case ERASE_LINE: - COORD leftColCurrRow = info.cursorPosition.copy(); - leftColCurrRow.x = 0; - FillConsoleOutputAttribute(console, info.attributes, info.size.x, leftColCurrRow, written); - FillConsoleOutputCharacterW(console, ' ', info.size.x, leftColCurrRow, written); - break; - case ERASE_LINE_TO_BEGINING: - COORD leftColCurrRow2 = info.cursorPosition.copy(); - leftColCurrRow2.x = 0; - FillConsoleOutputAttribute(console, info.attributes, info.cursorPosition.x, leftColCurrRow2, written); - FillConsoleOutputCharacterW(console, ' ', info.cursorPosition.x, leftColCurrRow2, written); - break; - case ERASE_LINE_TO_END: - int lengthToLastCol = info.size.x - info.cursorPosition.x; - FillConsoleOutputAttribute( - console, info.attributes, lengthToLastCol, info.cursorPosition.copy(), written); - FillConsoleOutputCharacterW(console, ' ', lengthToLastCol, info.cursorPosition.copy(), written); - break; - default: - break; - } - } - - @Override - protected void processCursorLeft(int count) throws IOException { - getConsoleInfo(); - info.cursorPosition.x = (short) Math.max(0, info.cursorPosition.x - count); - applyCursorPosition(); - } - - @Override - protected void processCursorRight(int count) throws IOException { - getConsoleInfo(); - info.cursorPosition.x = (short) Math.min(info.window.width(), info.cursorPosition.x + count); - applyCursorPosition(); - } - - @Override - protected void processCursorDown(int count) throws IOException { - getConsoleInfo(); - info.cursorPosition.y = (short) Math.min(Math.max(0, info.size.y - 1), info.cursorPosition.y + count); - applyCursorPosition(); - } - - @Override - protected void processCursorUp(int count) throws IOException { - getConsoleInfo(); - info.cursorPosition.y = (short) Math.max(info.window.top, info.cursorPosition.y - count); - applyCursorPosition(); - } - - @Override - protected void processCursorTo(int row, int col) throws IOException { - getConsoleInfo(); - info.cursorPosition.y = (short) Math.max(info.window.top, Math.min(info.size.y, info.window.top + row - 1)); - info.cursorPosition.x = (short) Math.max(0, Math.min(info.window.width(), col - 1)); - applyCursorPosition(); - } - - @Override - protected void processCursorToColumn(int x) throws IOException { - getConsoleInfo(); - info.cursorPosition.x = (short) Math.max(0, Math.min(info.window.width(), x - 1)); - applyCursorPosition(); - } - - @Override - protected void processCursorUpLine(int count) throws IOException { - getConsoleInfo(); - info.cursorPosition.x = 0; - info.cursorPosition.y = (short) Math.max(info.window.top, info.cursorPosition.y - count); - applyCursorPosition(); - } - - @Override - protected void processCursorDownLine(int count) throws IOException { - getConsoleInfo(); - info.cursorPosition.x = 0; - info.cursorPosition.y = (short) Math.max(info.window.top, info.cursorPosition.y + count); - applyCursorPosition(); - } - - @Override - protected void processSetForegroundColor(int color, boolean bright) throws IOException { - info.attributes = (short) ((info.attributes & ~0x0007) | ANSI_FOREGROUND_COLOR_MAP[color]); - if (bright) { - info.attributes |= FOREGROUND_INTENSITY; - } - applyAttribute(); - } - - @Override - protected void processSetForegroundColorExt(int paletteIndex) throws IOException { - int round = Colors.roundColor(paletteIndex, 16); - processSetForegroundColor(round >= 8 ? round - 8 : round, round >= 8); - } - - @Override - protected void processSetForegroundColorExt(int r, int g, int b) throws IOException { - int round = Colors.roundRgbColor(r, g, b, 16); - processSetForegroundColor(round >= 8 ? round - 8 : round, round >= 8); - } - - @Override - protected void processSetBackgroundColor(int color, boolean bright) throws IOException { - info.attributes = (short) ((info.attributes & ~0x0070) | ANSI_BACKGROUND_COLOR_MAP[color]); - if (bright) { - info.attributes |= BACKGROUND_INTENSITY; - } - applyAttribute(); - } - - @Override - protected void processSetBackgroundColorExt(int paletteIndex) throws IOException { - int round = Colors.roundColor(paletteIndex, 16); - processSetBackgroundColor(round >= 8 ? round - 8 : round, round >= 8); - } - - @Override - protected void processSetBackgroundColorExt(int r, int g, int b) throws IOException { - int round = Colors.roundRgbColor(r, g, b, 16); - processSetBackgroundColor(round >= 8 ? round - 8 : round, round >= 8); - } - - @Override - protected void processDefaultTextColor() throws IOException { - info.attributes = (short) ((info.attributes & ~0x000F) | (originalColors & 0xF)); - info.attributes = (short) (info.attributes & ~FOREGROUND_INTENSITY); - applyAttribute(); - } - - @Override - protected void processDefaultBackgroundColor() throws IOException { - info.attributes = (short) ((info.attributes & ~0x00F0) | (originalColors & 0xF0)); - info.attributes = (short) (info.attributes & ~BACKGROUND_INTENSITY); - applyAttribute(); - } - - @Override - protected void processAttributeReset() throws IOException { - info.attributes = (short) ((info.attributes & ~0x00FF) | originalColors); - this.negative = false; - applyAttribute(); - } - - @Override - protected void processSetAttribute(int attribute) throws IOException { - switch (attribute) { - case ATTRIBUTE_INTENSITY_BOLD: - info.attributes = (short) (info.attributes | FOREGROUND_INTENSITY); - applyAttribute(); - break; - case ATTRIBUTE_INTENSITY_NORMAL: - info.attributes = (short) (info.attributes & ~FOREGROUND_INTENSITY); - applyAttribute(); - break; - - // Yeah, setting the background intensity is not underlining.. but it's best we can do - // using the Windows console API - case ATTRIBUTE_UNDERLINE: - info.attributes = (short) (info.attributes | BACKGROUND_INTENSITY); - applyAttribute(); - break; - case ATTRIBUTE_UNDERLINE_OFF: - info.attributes = (short) (info.attributes & ~BACKGROUND_INTENSITY); - applyAttribute(); - break; - - case ATTRIBUTE_NEGATIVE_ON: - negative = true; - applyAttribute(); - break; - case ATTRIBUTE_NEGATIVE_OFF: - negative = false; - applyAttribute(); - break; - default: - break; - } - } - - @Override - protected void processSaveCursorPosition() throws IOException { - getConsoleInfo(); - savedX = info.cursorPosition.x; - savedY = info.cursorPosition.y; - } - - @Override - protected void processRestoreCursorPosition() throws IOException { - // restore only if there was a save operation first - if (savedX != -1 && savedY != -1) { - os.flush(); - info.cursorPosition.x = savedX; - info.cursorPosition.y = savedY; - applyCursorPosition(); - } - } - - @Override - protected void processInsertLine(int optionInt) throws IOException { - getConsoleInfo(); - SMALL_RECT scroll = info.window.copy(); - scroll.top = info.cursorPosition.y; - COORD org = new COORD(); - org.x = 0; - org.y = (short) (info.cursorPosition.y + optionInt); - CHAR_INFO info = new CHAR_INFO(); - info.attributes = originalColors; - info.unicodeChar = ' '; - if (ScrollConsoleScreenBuffer(console, scroll, scroll, org, info) == 0) { - throw new IOException(WindowsSupport.getLastErrorMessage()); - } - } - - @Override - protected void processDeleteLine(int optionInt) throws IOException { - getConsoleInfo(); - SMALL_RECT scroll = info.window.copy(); - scroll.top = info.cursorPosition.y; - COORD org = new COORD(); - org.x = 0; - org.y = (short) (info.cursorPosition.y - optionInt); - CHAR_INFO info = new CHAR_INFO(); - info.attributes = originalColors; - info.unicodeChar = ' '; - if (ScrollConsoleScreenBuffer(console, scroll, scroll, org, info) == 0) { - throw new IOException(WindowsSupport.getLastErrorMessage()); - } - } - - @Override - protected void processChangeWindowTitle(String label) { - SetConsoleTitle(label); + super(ps); } } From 27a7bb5b3d1c2a7ada1e491bd9e033264c574b46 Mon Sep 17 00:00:00 2001 From: Glavo Date: Fri, 29 Sep 2023 17:10:04 +0800 Subject: [PATCH 04/15] Fix FFM backend on Windows (#263) --- .../org/fusesource/jansi/AnsiConsole.java | 5 +- .../jansi/ffm/AnsiConsoleSupportFfm.java | 94 ++----- .../org/fusesource/jansi/ffm/Kernel32.java | 229 ++++++++---------- .../fusesource/jansi/ffm/PosixCLibrary.java | 84 +++++++ .../jansi/ffm/WindowsAnsiProcessor.java | 82 +++---- .../fusesource/jansi/ffm/WindowsCLibrary.java | 116 +++++++++ .../org/fusesource/jansi/internal/OSInfo.java | 12 +- 7 files changed, 362 insertions(+), 260 deletions(-) create mode 100644 src/main/java/org/fusesource/jansi/ffm/PosixCLibrary.java create mode 100644 src/main/java/org/fusesource/jansi/ffm/WindowsCLibrary.java diff --git a/src/main/java/org/fusesource/jansi/AnsiConsole.java b/src/main/java/org/fusesource/jansi/AnsiConsole.java index ff0cc657..d8de9dc9 100644 --- a/src/main/java/org/fusesource/jansi/AnsiConsole.java +++ b/src/main/java/org/fusesource/jansi/AnsiConsole.java @@ -24,8 +24,8 @@ import java.io.UnsupportedEncodingException; import java.nio.charset.Charset; import java.nio.charset.UnsupportedCharsetException; -import java.util.Locale; +import org.fusesource.jansi.internal.OSInfo; import org.fusesource.jansi.io.AnsiOutputStream; import org.fusesource.jansi.io.AnsiProcessor; import org.fusesource.jansi.io.FastBufferedOutputStream; @@ -194,8 +194,7 @@ public static int getTerminalWidth() { return w; } - static final boolean IS_WINDOWS = - System.getProperty("os.name").toLowerCase(Locale.ENGLISH).contains("win"); + static final boolean IS_WINDOWS = OSInfo.isWindows(); static final boolean IS_CYGWIN = IS_WINDOWS && System.getenv("PWD") != null && System.getenv("PWD").startsWith("/"); diff --git a/src/main/java/org/fusesource/jansi/ffm/AnsiConsoleSupportFfm.java b/src/main/java/org/fusesource/jansi/ffm/AnsiConsoleSupportFfm.java index b1af763f..2033fad8 100644 --- a/src/main/java/org/fusesource/jansi/ffm/AnsiConsoleSupportFfm.java +++ b/src/main/java/org/fusesource/jansi/ffm/AnsiConsoleSupportFfm.java @@ -18,44 +18,16 @@ import java.io.IOException; import java.io.OutputStream; import java.lang.foreign.Arena; -import java.lang.foreign.FunctionDescriptor; -import java.lang.foreign.GroupLayout; -import java.lang.foreign.Linker; -import java.lang.foreign.MemoryLayout; import java.lang.foreign.MemorySegment; import java.lang.foreign.ValueLayout; -import java.lang.invoke.MethodHandle; -import java.lang.invoke.VarHandle; import org.fusesource.jansi.AnsiConsoleSupport; +import org.fusesource.jansi.internal.OSInfo; import org.fusesource.jansi.io.AnsiProcessor; import static org.fusesource.jansi.ffm.Kernel32.*; public class AnsiConsoleSupportFfm implements AnsiConsoleSupport { - static GroupLayout wsLayout; - static MethodHandle ioctl; - static VarHandle ws_col; - static MethodHandle isatty; - - static { - wsLayout = MemoryLayout.structLayout( - ValueLayout.JAVA_SHORT.withName("ws_row"), - ValueLayout.JAVA_SHORT.withName("ws_col"), - ValueLayout.JAVA_SHORT, - ValueLayout.JAVA_SHORT); - ws_col = wsLayout.varHandle(MemoryLayout.PathElement.groupElement("ws_col")); - Linker linker = Linker.nativeLinker(); - ioctl = linker.downcallHandle( - linker.defaultLookup().find("ioctl").get(), - FunctionDescriptor.of( - ValueLayout.JAVA_INT, ValueLayout.JAVA_INT, ValueLayout.JAVA_LONG, ValueLayout.ADDRESS), - Linker.Option.firstVariadicArg(2)); - isatty = linker.downcallHandle( - linker.defaultLookup().find("isatty").get(), - FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.JAVA_INT)); - } - @Override public String getProviderName() { return "ffm"; @@ -63,48 +35,11 @@ public String getProviderName() { @Override public CLibrary getCLibrary() { - return new CLibrary() { - static final int TIOCGWINSZ; - - static { - String osName = System.getProperty("os.name"); - if (osName.startsWith("Linux")) { - String arch = System.getProperty("os.arch"); - boolean isMipsPpcOrSparc = - arch.startsWith("mips") || arch.startsWith("ppc") || arch.startsWith("sparc"); - TIOCGWINSZ = isMipsPpcOrSparc ? 0x40087468 : 0x00005413; - } else if (osName.startsWith("Solaris") || osName.startsWith("SunOS")) { - int _TIOC = ('T' << 8); - TIOCGWINSZ = (_TIOC | 104); - } else if (osName.startsWith("Mac") || osName.startsWith("Darwin")) { - TIOCGWINSZ = 0x40087468; - } else if (osName.startsWith("FreeBSD")) { - TIOCGWINSZ = 0x40087468; - } else { - throw new UnsupportedOperationException(); - } - } - - @Override - public short getTerminalWidth(int fd) { - MemorySegment segment = Arena.ofAuto().allocate(wsLayout); - try { - int res = (int) ioctl.invoke(fd, (long) TIOCGWINSZ, segment); - return (short) ws_col.get(segment); - } catch (Throwable e) { - throw new RuntimeException("Unable to ioctl(TIOCGWINSZ)", e); - } - } - - @Override - public int isTty(int fd) { - try { - return (int) isatty.invoke(fd); - } catch (Throwable e) { - throw new RuntimeException("Unable to call isatty", e); - } - } - }; + if (OSInfo.isWindows()) { + return new WindowsCLibrary(); + } else { + return new PosixCLibrary(); + } } @Override @@ -118,9 +53,11 @@ public int isTty(long console) { @Override public int getTerminalWidth(long console) { - CONSOLE_SCREEN_BUFFER_INFO info = new CONSOLE_SCREEN_BUFFER_INFO(); - GetConsoleScreenBufferInfo(MemorySegment.ofAddress(console), info); - return info.windowWidth(); + try (Arena arena = Arena.ofConfined()) { + CONSOLE_SCREEN_BUFFER_INFO info = new CONSOLE_SCREEN_BUFFER_INFO(arena); + GetConsoleScreenBufferInfo(MemorySegment.ofAddress(console), info); + return info.windowWidth(); + } } @Override @@ -131,8 +68,8 @@ public long getStdHandle(boolean stdout) { @Override public int getConsoleMode(long console, int[] mode) { - try (Arena session = Arena.ofConfined()) { - MemorySegment written = session.allocate(ValueLayout.JAVA_INT); + try (Arena arena = Arena.ofConfined()) { + MemorySegment written = arena.allocate(ValueLayout.JAVA_INT); int res = GetConsoleMode(MemorySegment.ofAddress(console), written); mode[0] = written.getAtIndex(ValueLayout.JAVA_INT, 0); return res; @@ -151,10 +88,7 @@ public int getLastError() { @Override public String getErrorMessage(int errorCode) { - int bufferSize = 160; - MemorySegment data = Arena.ofAuto().allocate(bufferSize); - FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM, null, errorCode, 0, data, bufferSize, null); - return data.getUtf8String(0).trim(); + return org.fusesource.jansi.ffm.Kernel32.getErrorMessage(errorCode); } @Override diff --git a/src/main/java/org/fusesource/jansi/ffm/Kernel32.java b/src/main/java/org/fusesource/jansi/ffm/Kernel32.java index fc17db68..0cc409ac 100644 --- a/src/main/java/org/fusesource/jansi/ffm/Kernel32.java +++ b/src/main/java/org/fusesource/jansi/ffm/Kernel32.java @@ -27,20 +27,13 @@ import java.lang.foreign.ValueLayout; import java.lang.invoke.MethodHandle; import java.lang.invoke.VarHandle; +import java.nio.charset.StandardCharsets; import java.util.Objects; -import static java.lang.foreign.ValueLayout.JAVA_INT; -import static java.lang.foreign.ValueLayout.OfBoolean; -import static java.lang.foreign.ValueLayout.OfByte; -import static java.lang.foreign.ValueLayout.OfChar; -import static java.lang.foreign.ValueLayout.OfDouble; -import static java.lang.foreign.ValueLayout.OfFloat; -import static java.lang.foreign.ValueLayout.OfInt; -import static java.lang.foreign.ValueLayout.OfLong; -import static java.lang.foreign.ValueLayout.OfShort; +import static java.lang.foreign.ValueLayout.*; -@SuppressWarnings({"unused", "CopyConstructorMissesField"}) -class Kernel32 { +@SuppressWarnings("unused") +final class Kernel32 { public static final int FORMAT_MESSAGE_FROM_SYSTEM = 0x00001000; @@ -105,7 +98,7 @@ public static int WaitForSingleObject(MemorySegment hHandle, int dwMilliseconds) public static MemorySegment GetStdHandle(int nStdHandle) { MethodHandle mh$ = requireNonNull(GetStdHandle$MH, "GetStdHandle"); try { - return MemorySegment.ofAddress((long) mh$.invokeExact(nStdHandle)); + return (MemorySegment) mh$.invokeExact(nStdHandle); } catch (Throwable ex$) { throw new AssertionError("should not reach here", ex$); } @@ -121,14 +114,7 @@ public static int FormatMessageW( MemorySegment Arguments) { MethodHandle mh$ = requireNonNull(FormatMessageW$MH, "FormatMessageW"); try { - return (int) mh$.invokeExact( - dwFlags, - lpSource.address(), - dwMessageId, - dwLanguageId, - lpBuffer.address(), - nSize, - Arguments.address()); + return (int) mh$.invokeExact(dwFlags, lpSource, dwMessageId, dwLanguageId, lpBuffer, nSize, Arguments); } catch (Throwable ex$) { throw new AssertionError("should not reach here", ex$); } @@ -146,7 +132,7 @@ public static int SetConsoleTextAttribute(MemorySegment hConsoleOutput, short wA public static int SetConsoleMode(MemorySegment hConsoleHandle, int dwMode) { MethodHandle mh$ = requireNonNull(SetConsoleMode$MH, "SetConsoleMode"); try { - return (int) mh$.invokeExact(hConsoleHandle.address(), dwMode); + return (int) mh$.invokeExact(hConsoleHandle, dwMode); } catch (Throwable ex$) { throw new AssertionError("should not reach here", ex$); } @@ -155,7 +141,7 @@ public static int SetConsoleMode(MemorySegment hConsoleHandle, int dwMode) { public static int GetConsoleMode(MemorySegment hConsoleHandle, MemorySegment lpMode) { MethodHandle mh$ = requireNonNull(GetConsoleMode$MH, "GetConsoleMode"); try { - return (int) mh$.invokeExact(hConsoleHandle.address(), lpMode.address()); + return (int) mh$.invokeExact(hConsoleHandle, lpMode); } catch (Throwable ex$) { throw new AssertionError("should not reach here", ex$); } @@ -164,7 +150,7 @@ public static int GetConsoleMode(MemorySegment hConsoleHandle, MemorySegment lpM public static int SetConsoleTitleW(MemorySegment lpConsoleTitle) { MethodHandle mh$ = requireNonNull(SetConsoleTitleW$MH, "SetConsoleTitleW"); try { - return (int) mh$.invokeExact(lpConsoleTitle.address()); + return (int) mh$.invokeExact(lpConsoleTitle); } catch (Throwable ex$) { throw new AssertionError("should not reach here", ex$); } @@ -187,8 +173,7 @@ public static int FillConsoleOutputCharacterW( MemorySegment lpNumberOfCharsWritten) { MethodHandle mh$ = requireNonNull(FillConsoleOutputCharacterW$MH, "FillConsoleOutputCharacterW"); try { - return (int) mh$.invokeExact( - hConsoleOutput.address(), cCharacter, nLength, dwWriteCoord.seg, lpNumberOfCharsWritten.address()); + return (int) mh$.invokeExact(hConsoleOutput, cCharacter, nLength, dwWriteCoord.seg, lpNumberOfCharsWritten); } catch (Throwable ex$) { throw new AssertionError("should not reach here", ex$); } @@ -202,8 +187,7 @@ public static int FillConsoleOutputAttribute( MemorySegment lpNumberOfAttrsWritten) { MethodHandle mh$ = requireNonNull(FillConsoleOutputAttribute$MH, "FillConsoleOutputAttribute"); try { - return (int) mh$.invokeExact( - hConsoleOutput, wAttribute, nLength, dwWriteCoord.seg, lpNumberOfAttrsWritten.address()); + return (int) mh$.invokeExact(hConsoleOutput, wAttribute, nLength, dwWriteCoord.seg, lpNumberOfAttrsWritten); } catch (Throwable ex$) { throw new AssertionError("should not reach here", ex$); } @@ -228,8 +212,7 @@ public static int ReadConsoleInputW( MemorySegment hConsoleInput, MemorySegment lpBuffer, int nLength, MemorySegment lpNumberOfEventsRead) { MethodHandle mh$ = requireNonNull(ReadConsoleInputW$MH, "ReadConsoleInputW"); try { - return (int) mh$.invokeExact( - hConsoleInput.address(), lpBuffer.address(), nLength, lpNumberOfEventsRead.address()); + return (int) mh$.invokeExact(hConsoleInput, lpBuffer, nLength, lpNumberOfEventsRead); } catch (Throwable ex$) { throw new AssertionError("should not reach here", ex$); } @@ -239,8 +222,7 @@ public static int PeekConsoleInputW( MemorySegment hConsoleInput, MemorySegment lpBuffer, int nLength, MemorySegment lpNumberOfEventsRead) { MethodHandle mh$ = requireNonNull(PeekConsoleInputW$MH, "PeekConsoleInputW"); try { - return (int) mh$.invokeExact( - hConsoleInput.address(), lpBuffer.address(), nLength, lpNumberOfEventsRead.address()); + return (int) mh$.invokeExact(hConsoleInput, lpBuffer, nLength, lpNumberOfEventsRead); } catch (Throwable ex$) { throw new AssertionError("should not reach here", ex$); } @@ -250,7 +232,7 @@ public static int GetConsoleScreenBufferInfo( MemorySegment hConsoleOutput, CONSOLE_SCREEN_BUFFER_INFO lpConsoleScreenBufferInfo) { MethodHandle mh$ = requireNonNull(GetConsoleScreenBufferInfo$MH, "GetConsoleScreenBufferInfo"); try { - return (int) mh$.invokeExact(hConsoleOutput.address(), lpConsoleScreenBufferInfo.seg); + return (int) mh$.invokeExact(hConsoleOutput, lpConsoleScreenBufferInfo.seg); } catch (Throwable ex$) { throw new AssertionError("should not reach here", ex$); } @@ -271,10 +253,28 @@ public static int ScrollConsoleScreenBuffer( } } - public static int GetLastError(Object... x0) { + public static int GetLastError() { MethodHandle mh$ = requireNonNull(GetLastError$MH, "GetLastError"); try { - return (int) mh$.invokeExact(x0); + return (int) mh$.invokeExact(); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + public static int GetFileType(MemorySegment hFile) { + MethodHandle mh$ = requireNonNull(GetFileType$MH, "GetFileType"); + try { + return (int) mh$.invokeExact(hFile); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + public static MemorySegment _get_osfhandle(int fd) { + MethodHandle mh$ = requireNonNull(_get_osfhandle$MH, "_get_osfhandle"); + try { + return (MemorySegment) mh$.invokeExact(fd); } catch (Throwable ex$) { throw new AssertionError("should not reach here", ex$); } @@ -282,9 +282,9 @@ public static int GetLastError(Object... x0) { public static INPUT_RECORD[] readConsoleInputHelper(MemorySegment handle, int count, boolean peek) throws IOException { - try (Arena session = Arena.ofConfined()) { - MemorySegment inputRecordPtr = session.allocateArray(INPUT_RECORD.LAYOUT, count); - MemorySegment length = session.allocate(JAVA_INT, 0); + try (Arena arena = Arena.ofConfined()) { + MemorySegment inputRecordPtr = arena.allocateArray(INPUT_RECORD.LAYOUT, count); + MemorySegment length = arena.allocate(JAVA_INT, 0); int res = peek ? PeekConsoleInputW(handle, inputRecordPtr, count, length) : ReadConsoleInputW(handle, inputRecordPtr, count, length); @@ -307,23 +307,39 @@ public static String getLastErrorMessage() { public static String getErrorMessage(int errorCode) { int bufferSize = 160; - MemorySegment data = Arena.ofAuto().allocate(bufferSize); - FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM, null, errorCode, 0, data, bufferSize, null); - return data.getUtf8String(0).trim(); - } - - static final OfBoolean C_BOOL$LAYOUT = ValueLayout.JAVA_BOOLEAN; - static final OfByte C_CHAR$LAYOUT = ValueLayout.JAVA_BYTE; - static final OfChar C_WCHAR$LAYOUT = ValueLayout.JAVA_CHAR.withByteAlignment(16); - static final OfShort C_SHORT$LAYOUT = ValueLayout.JAVA_SHORT.withByteAlignment(16); - static final OfShort C_WORD$LAYOUT = ValueLayout.JAVA_SHORT.withByteAlignment(16); - static final OfInt C_DWORD$LAYOUT = ValueLayout.JAVA_INT.withByteAlignment(32); - static final OfInt C_INT$LAYOUT = JAVA_INT.withByteAlignment(32); - static final OfLong C_LONG$LAYOUT = ValueLayout.JAVA_LONG.withByteAlignment(64); - static final OfLong C_LONG_LONG$LAYOUT = ValueLayout.JAVA_LONG.withByteAlignment(64); - static final OfFloat C_FLOAT$LAYOUT = ValueLayout.JAVA_FLOAT.withByteAlignment(32); - static final OfDouble C_DOUBLE$LAYOUT = ValueLayout.JAVA_DOUBLE.withByteAlignment(64); - static final AddressLayout C_POINTER$LAYOUT = ValueLayout.ADDRESS.withByteAlignment(64); + try (Arena arena = Arena.ofConfined()) { + MemorySegment data = arena.allocate(bufferSize); + FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM, null, errorCode, 0, data, bufferSize, null); + return new String(data.toArray(JAVA_BYTE), StandardCharsets.UTF_16LE).trim(); + } + } + + private static final SymbolLookup SYMBOL_LOOKUP; + + static { + System.loadLibrary("Kernel32"); + SYMBOL_LOOKUP = SymbolLookup.loaderLookup().or(Linker.nativeLinker().defaultLookup()); + } + + static MethodHandle downcallHandle(String name, FunctionDescriptor fdesc) { + return SYMBOL_LOOKUP + .find(name) + .map(addr -> Linker.nativeLinker().downcallHandle(addr, fdesc)) + .orElse(null); + } + + static final OfBoolean C_BOOL$LAYOUT = JAVA_BOOLEAN; + static final OfByte C_CHAR$LAYOUT = JAVA_BYTE; + static final OfChar C_WCHAR$LAYOUT = JAVA_CHAR; + static final OfShort C_SHORT$LAYOUT = JAVA_SHORT; + static final OfShort C_WORD$LAYOUT = JAVA_SHORT; + static final OfInt C_DWORD$LAYOUT = JAVA_INT; + static final OfInt C_INT$LAYOUT = JAVA_INT; + static final OfLong C_LONG$LAYOUT = JAVA_LONG; + static final OfLong C_LONG_LONG$LAYOUT = JAVA_LONG; + static final OfFloat C_FLOAT$LAYOUT = JAVA_FLOAT; + static final OfDouble C_DOUBLE$LAYOUT = JAVA_DOUBLE; + static final AddressLayout C_POINTER$LAYOUT = ADDRESS; static final MethodHandle WaitForSingleObject$MH = downcallHandle("WaitForSingleObject", FunctionDescriptor.of(C_INT$LAYOUT, C_POINTER$LAYOUT, C_INT$LAYOUT)); @@ -389,8 +405,12 @@ public static String getErrorMessage(int errorCode) { COORD.LAYOUT, C_POINTER$LAYOUT)); static final MethodHandle GetLastError$MH = downcallHandle("GetLastError", FunctionDescriptor.of(C_INT$LAYOUT)); + static final MethodHandle GetFileType$MH = + downcallHandle("GetFileType", FunctionDescriptor.of(C_INT$LAYOUT, C_POINTER$LAYOUT)); + static final MethodHandle _get_osfhandle$MH = + downcallHandle("_get_osfhandle", FunctionDescriptor.of(C_POINTER$LAYOUT, C_INT$LAYOUT)); - public static class INPUT_RECORD { + public static final class INPUT_RECORD { static final MemoryLayout LAYOUT = MemoryLayout.structLayout( ValueLayout.JAVA_SHORT.withName("EventType"), MemoryLayout.unionLayout( @@ -405,10 +425,6 @@ public static class INPUT_RECORD { private final MemorySegment seg; - public INPUT_RECORD() { - this(Arena.ofAuto().allocate(LAYOUT)); - } - INPUT_RECORD(MemorySegment seg) { this.seg = seg; } @@ -430,17 +446,13 @@ public FOCUS_EVENT_RECORD focusEvent() { } } - public static class MENU_EVENT_RECORD { + public static final class MENU_EVENT_RECORD { static final GroupLayout LAYOUT = MemoryLayout.structLayout(C_DWORD$LAYOUT.withName("dwCommandId")); static final VarHandle COMMAND_ID = varHandle(LAYOUT, "dwCommandId"); private final MemorySegment seg; - public MENU_EVENT_RECORD() { - this(Arena.ofAuto().allocate(LAYOUT)); - } - MENU_EVENT_RECORD(MemorySegment seg) { this.seg = seg; } @@ -454,17 +466,13 @@ public void commandId(int commandId) { } } - public static class FOCUS_EVENT_RECORD { + public static final class FOCUS_EVENT_RECORD { static final GroupLayout LAYOUT = MemoryLayout.structLayout(C_BOOL$LAYOUT.withName("bSetFocus")); static final VarHandle SET_FOCUS = varHandle(LAYOUT, "bSetFocus"); private final MemorySegment seg; - public FOCUS_EVENT_RECORD() { - this(Arena.ofAuto().allocate(LAYOUT)); - } - FOCUS_EVENT_RECORD(MemorySegment seg) { this.seg = Objects.requireNonNull(seg); } @@ -482,17 +490,13 @@ public void setFocus(boolean setFocus) { } } - public static class WINDOW_BUFFER_SIZE_RECORD { + public static final class WINDOW_BUFFER_SIZE_RECORD { static final GroupLayout LAYOUT = MemoryLayout.structLayout(COORD.LAYOUT.withName("size")); static final long SIZE_OFFSET = byteOffset(LAYOUT, "size"); private final MemorySegment seg; - public WINDOW_BUFFER_SIZE_RECORD() { - this(Arena.ofAuto().allocate(LAYOUT)); - } - WINDOW_BUFFER_SIZE_RECORD(MemorySegment seg) { this.seg = seg; } @@ -506,7 +510,7 @@ public String toString() { } } - public static class MOUSE_EVENT_RECORD { + public static final class MOUSE_EVENT_RECORD { private static final MemoryLayout LAYOUT = MemoryLayout.structLayout( COORD.LAYOUT.withName("dwMousePosition"), @@ -520,10 +524,6 @@ public static class MOUSE_EVENT_RECORD { private final MemorySegment seg; - public MOUSE_EVENT_RECORD() { - this(Arena.ofAuto().allocate(LAYOUT)); - } - MOUSE_EVENT_RECORD(MemorySegment seg) { this.seg = Objects.requireNonNull(seg); } @@ -554,7 +554,7 @@ public String toString() { } } - public static class KEY_EVENT_RECORD { + public static final class KEY_EVENT_RECORD { static final MemoryLayout LAYOUT = MemoryLayout.structLayout( JAVA_INT.withName("bKeyDown"), @@ -576,10 +576,6 @@ public static class KEY_EVENT_RECORD { final MemorySegment seg; - public KEY_EVENT_RECORD() { - this(Arena.ofAuto().allocate(LAYOUT)); - } - KEY_EVENT_RECORD(MemorySegment seg) { this.seg = seg; } @@ -620,7 +616,7 @@ public String toString() { } } - public static class CHAR_INFO { + public static final class CHAR_INFO { static final GroupLayout LAYOUT = MemoryLayout.structLayout( MemoryLayout.unionLayout(C_WCHAR$LAYOUT.withName("UnicodeChar"), C_CHAR$LAYOUT.withName("AsciiChar")) @@ -631,12 +627,12 @@ public static class CHAR_INFO { final MemorySegment seg; - public CHAR_INFO() { - this(Arena.ofAuto().allocate(LAYOUT)); + public CHAR_INFO(Arena arena) { + this(arena.allocate(LAYOUT)); } - public CHAR_INFO(char c, short a) { - this(); + public CHAR_INFO(Arena arena, char c, short a) { + this(arena); UnicodeChar$VH.set(seg, c); Attributes$VH.set(seg, a); } @@ -650,7 +646,7 @@ public char unicodeChar() { } } - public static class CONSOLE_SCREEN_BUFFER_INFO { + public static final class CONSOLE_SCREEN_BUFFER_INFO { static final GroupLayout LAYOUT = MemoryLayout.structLayout( COORD.LAYOUT.withName("dwSize"), COORD.LAYOUT.withName("dwCursorPosition"), @@ -664,8 +660,8 @@ public static class CONSOLE_SCREEN_BUFFER_INFO { private final MemorySegment seg; - public CONSOLE_SCREEN_BUFFER_INFO() { - this(Arena.ofAuto().allocate(LAYOUT)); + public CONSOLE_SCREEN_BUFFER_INFO(Arena arena) { + this(arena.allocate(LAYOUT)); } CONSOLE_SCREEN_BUFFER_INFO(MemorySegment seg) { @@ -701,7 +697,7 @@ public void attributes(short attr) { } } - public static class COORD { + public static final class COORD { static final GroupLayout LAYOUT = MemoryLayout.structLayout(C_SHORT$LAYOUT.withName("x"), C_SHORT$LAYOUT.withName("y")); @@ -710,20 +706,16 @@ public static class COORD { private final MemorySegment seg; - public COORD() { - this(Arena.ofAuto().allocate(LAYOUT)); + public COORD(Arena arena) { + this(arena.allocate(LAYOUT)); } - public COORD(short x, short y) { - this(Arena.ofAuto().allocate(LAYOUT)); + public COORD(Arena arena, short x, short y) { + this(arena.allocate(LAYOUT)); x(x); y(y); } - public COORD(COORD from) { - this(Arena.ofAuto().allocate(LAYOUT).copyFrom(Objects.requireNonNull(from).seg)); - } - COORD(MemorySegment seg) { this.seg = seg; } @@ -748,12 +740,12 @@ public void y(short y) { COORD.y$VH.set(seg, y); } - public COORD copy() { - return new COORD(this); + public COORD copy(Arena arena) { + return new COORD(arena.allocate(LAYOUT).copyFrom(seg)); } } - public static class SMALL_RECT { + public static final class SMALL_RECT { static final GroupLayout LAYOUT = MemoryLayout.structLayout( C_SHORT$LAYOUT.withName("Left"), @@ -767,14 +759,6 @@ public static class SMALL_RECT { private final MemorySegment seg; - public SMALL_RECT() { - this(Arena.ofAuto().allocate(LAYOUT)); - } - - public SMALL_RECT(SMALL_RECT from) { - this(Arena.ofAuto().allocate(LAYOUT).copyFrom(from.seg)); - } - SMALL_RECT(MemorySegment seg, long offset) { this(seg.asSlice(offset, LAYOUT.byteSize())); } @@ -815,28 +799,11 @@ public void top(short t) { Top$VH.set(seg, t); } - public SMALL_RECT copy() { - return new SMALL_RECT(this); + public SMALL_RECT copy(Arena arena) { + return new SMALL_RECT(arena.allocate(LAYOUT).copyFrom(seg)); } } - private static final Linker LINKER = Linker.nativeLinker(); - - private static final SymbolLookup SYMBOL_LOOKUP; - - static { - SymbolLookup loaderLookup = SymbolLookup.loaderLookup(); - SYMBOL_LOOKUP = - name -> loaderLookup.find(name).or(() -> LINKER.defaultLookup().find(name)); - } - - static MethodHandle downcallHandle(String name, FunctionDescriptor fdesc) { - return SYMBOL_LOOKUP - .find(name) - .map(addr -> LINKER.downcallHandle(addr, fdesc)) - .orElse(null); - } - static T requireNonNull(T obj, String symbolName) { if (obj == null) { throw new UnsatisfiedLinkError("unresolved symbol: " + symbolName); diff --git a/src/main/java/org/fusesource/jansi/ffm/PosixCLibrary.java b/src/main/java/org/fusesource/jansi/ffm/PosixCLibrary.java new file mode 100644 index 00000000..bd4f1f73 --- /dev/null +++ b/src/main/java/org/fusesource/jansi/ffm/PosixCLibrary.java @@ -0,0 +1,84 @@ +/* + * 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.ffm; + +import java.lang.foreign.*; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.VarHandle; + +import org.fusesource.jansi.AnsiConsoleSupport; + +final class PosixCLibrary implements AnsiConsoleSupport.CLibrary { + private static final int TIOCGWINSZ; + private static final GroupLayout wsLayout; + private static final MethodHandle ioctl; + private static final VarHandle ws_col; + private static final MethodHandle isatty; + + static { + String osName = System.getProperty("os.name"); + if (osName.startsWith("Linux")) { + String arch = System.getProperty("os.arch"); + boolean isMipsPpcOrSparc = arch.startsWith("mips") || arch.startsWith("ppc") || arch.startsWith("sparc"); + TIOCGWINSZ = isMipsPpcOrSparc ? 0x40087468 : 0x00005413; + } else if (osName.startsWith("Solaris") || osName.startsWith("SunOS")) { + int _TIOC = ('T' << 8); + TIOCGWINSZ = (_TIOC | 104); + } else if (osName.startsWith("Mac") || osName.startsWith("Darwin")) { + TIOCGWINSZ = 0x40087468; + } else if (osName.startsWith("FreeBSD")) { + TIOCGWINSZ = 0x40087468; + } else { + throw new UnsupportedOperationException(); + } + + wsLayout = MemoryLayout.structLayout( + ValueLayout.JAVA_SHORT.withName("ws_row"), + ValueLayout.JAVA_SHORT.withName("ws_col"), + ValueLayout.JAVA_SHORT, + ValueLayout.JAVA_SHORT); + ws_col = wsLayout.varHandle(MemoryLayout.PathElement.groupElement("ws_col")); + Linker linker = Linker.nativeLinker(); + ioctl = linker.downcallHandle( + linker.defaultLookup().find("ioctl").get(), + FunctionDescriptor.of( + ValueLayout.JAVA_INT, ValueLayout.JAVA_INT, ValueLayout.JAVA_LONG, ValueLayout.ADDRESS), + Linker.Option.firstVariadicArg(2)); + isatty = linker.downcallHandle( + linker.defaultLookup().find("isatty").get(), + FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.JAVA_INT)); + } + + @Override + public short getTerminalWidth(int fd) { + try (Arena arena = Arena.ofConfined()) { + MemorySegment segment = arena.allocate(wsLayout); + int res = (int) ioctl.invoke(fd, (long) TIOCGWINSZ, segment); + return (short) ws_col.get(segment); + } catch (Throwable e) { + throw new RuntimeException("Unable to ioctl(TIOCGWINSZ)", e); + } + } + + @Override + public int isTty(int fd) { + try { + return (int) isatty.invoke(fd); + } catch (Throwable e) { + throw new RuntimeException("Unable to call isatty", e); + } + } +} diff --git a/src/main/java/org/fusesource/jansi/ffm/WindowsAnsiProcessor.java b/src/main/java/org/fusesource/jansi/ffm/WindowsAnsiProcessor.java index 25d20030..e933ff0a 100644 --- a/src/main/java/org/fusesource/jansi/ffm/WindowsAnsiProcessor.java +++ b/src/main/java/org/fusesource/jansi/ffm/WindowsAnsiProcessor.java @@ -20,6 +20,7 @@ import java.lang.foreign.Arena; import java.lang.foreign.MemorySegment; import java.lang.foreign.ValueLayout; +import java.nio.charset.StandardCharsets; import org.fusesource.jansi.WindowsSupport; import org.fusesource.jansi.io.AnsiProcessor; @@ -76,7 +77,7 @@ public class WindowsAnsiProcessor extends AnsiProcessor { BACKGROUND_WHITE, }; - private final CONSOLE_SCREEN_BUFFER_INFO info = new CONSOLE_SCREEN_BUFFER_INFO(); + private final CONSOLE_SCREEN_BUFFER_INFO info = new CONSOLE_SCREEN_BUFFER_INFO(Arena.ofAuto()); private final short originalColors; private boolean negative; @@ -130,7 +131,7 @@ private short invertAttributeColors(short attributes) { } private void applyCursorPosition() throws IOException { - if (SetConsoleCursorPosition(console, info.cursorPosition().copy()) == 0) { + if (SetConsoleCursorPosition(console, info.cursorPosition()) == 0) { throw new IOException(WindowsSupport.getLastErrorMessage()); } } @@ -138,11 +139,11 @@ private void applyCursorPosition() throws IOException { @Override protected void processEraseScreen(int eraseOption) throws IOException { getConsoleInfo(); - try (Arena session = Arena.ofConfined()) { - MemorySegment written = session.allocate(ValueLayout.JAVA_INT); + try (Arena arena = Arena.ofConfined()) { + MemorySegment written = arena.allocate(ValueLayout.JAVA_INT); switch (eraseOption) { case ERASE_SCREEN: - COORD topLeft = new COORD(); + COORD topLeft = new COORD(arena); topLeft.x((short) 0); topLeft.y(info.window().top()); int screenLength = info.window().height() * info.size().x(); @@ -150,7 +151,7 @@ protected void processEraseScreen(int eraseOption) throws IOException { FillConsoleOutputCharacterW(console, ' ', screenLength, topLeft, written); break; case ERASE_SCREEN_TO_BEGINING: - COORD topLeft2 = new COORD(); + COORD topLeft2 = new COORD(arena); topLeft2.x((short) 0); topLeft2.y(info.window().top()); int lengthToCursor = @@ -165,14 +166,8 @@ protected void processEraseScreen(int eraseOption) throws IOException { (info.window().bottom() - info.cursorPosition().y()) * info.size().x() + (info.size().x() - info.cursorPosition().x()); - FillConsoleOutputAttribute( - console, - info.attributes(), - lengthToEnd, - info.cursorPosition().copy(), - written); - FillConsoleOutputCharacterW( - console, ' ', lengthToEnd, info.cursorPosition().copy(), written); + FillConsoleOutputAttribute(console, info.attributes(), lengthToEnd, info.cursorPosition(), written); + FillConsoleOutputCharacterW(console, ' ', lengthToEnd, info.cursorPosition(), written); break; default: break; @@ -183,18 +178,18 @@ protected void processEraseScreen(int eraseOption) throws IOException { @Override protected void processEraseLine(int eraseOption) throws IOException { getConsoleInfo(); - try (Arena session = Arena.ofConfined()) { - MemorySegment written = session.allocate(ValueLayout.JAVA_INT); + try (Arena arena = Arena.ofConfined()) { + MemorySegment written = arena.allocate(ValueLayout.JAVA_INT); switch (eraseOption) { case ERASE_LINE: - COORD leftColCurrRow = info.cursorPosition().copy(); + COORD leftColCurrRow = info.cursorPosition().copy(arena); leftColCurrRow.x((short) 0); FillConsoleOutputAttribute( console, info.attributes(), info.size().x(), leftColCurrRow, written); FillConsoleOutputCharacterW(console, ' ', info.size().x(), leftColCurrRow, written); break; case ERASE_LINE_TO_BEGINING: - COORD leftColCurrRow2 = info.cursorPosition().copy(); + COORD leftColCurrRow2 = info.cursorPosition().copy(arena); leftColCurrRow2.x((short) 0); FillConsoleOutputAttribute( console, info.attributes(), info.cursorPosition().x(), leftColCurrRow2, written); @@ -205,13 +200,8 @@ protected void processEraseLine(int eraseOption) throws IOException { int lengthToLastCol = info.size().x() - info.cursorPosition().x(); FillConsoleOutputAttribute( - console, - info.attributes(), - lengthToLastCol, - info.cursorPosition().copy(), - written); - FillConsoleOutputCharacterW( - console, ' ', lengthToLastCol, info.cursorPosition().copy(), written); + console, info.attributes(), lengthToLastCol, info.cursorPosition(), written); + FillConsoleOutputCharacterW(console, ' ', lengthToLastCol, info.cursorPosition(), written); break; default: break; @@ -404,35 +394,41 @@ protected void processRestoreCursorPosition() throws IOException { @Override protected void processInsertLine(int optionInt) throws IOException { getConsoleInfo(); - SMALL_RECT scroll = info.window().copy(); - scroll.top(info.cursorPosition().y()); - COORD org = new COORD(); - org.x((short) 0); - org.y((short) (info.cursorPosition().y() + optionInt)); - CHAR_INFO info = new CHAR_INFO(' ', originalColors); - if (ScrollConsoleScreenBuffer(console, scroll, scroll, org, info) == 0) { - throw new IOException(WindowsSupport.getLastErrorMessage()); + try (Arena arena = Arena.ofConfined()) { + SMALL_RECT scroll = info.window().copy(arena); + scroll.top(info.cursorPosition().y()); + COORD org = new COORD(arena); + org.x((short) 0); + org.y((short) (info.cursorPosition().y() + optionInt)); + CHAR_INFO info = new CHAR_INFO(arena, ' ', originalColors); + if (ScrollConsoleScreenBuffer(console, scroll, scroll, org, info) == 0) { + throw new IOException(WindowsSupport.getLastErrorMessage()); + } } } @Override protected void processDeleteLine(int optionInt) throws IOException { getConsoleInfo(); - SMALL_RECT scroll = info.window().copy(); - scroll.top(info.cursorPosition().y()); - COORD org = new COORD(); - org.x((short) 0); - org.y((short) (info.cursorPosition().y() - optionInt)); - CHAR_INFO info = new CHAR_INFO(' ', originalColors); - if (ScrollConsoleScreenBuffer(console, scroll, scroll, org, info) == 0) { - throw new IOException(WindowsSupport.getLastErrorMessage()); + try (Arena arena = Arena.ofConfined()) { + SMALL_RECT scroll = info.window().copy(arena); + scroll.top(info.cursorPosition().y()); + COORD org = new COORD(arena); + org.x((short) 0); + org.y((short) (info.cursorPosition().y() - optionInt)); + CHAR_INFO info = new CHAR_INFO(arena, ' ', originalColors); + if (ScrollConsoleScreenBuffer(console, scroll, scroll, org, info) == 0) { + throw new IOException(WindowsSupport.getLastErrorMessage()); + } } } @Override protected void processChangeWindowTitle(String title) { - try (Arena session = Arena.ofConfined()) { - MemorySegment str = session.allocateUtf8String(title); + try (Arena arena = Arena.ofConfined()) { + byte[] bytes = title.getBytes(StandardCharsets.UTF_16LE); + MemorySegment str = arena.allocate(bytes.length + 2); + MemorySegment.copy(bytes, 0, str, ValueLayout.JAVA_BYTE, 0, bytes.length); SetConsoleTitleW(str); } } diff --git a/src/main/java/org/fusesource/jansi/ffm/WindowsCLibrary.java b/src/main/java/org/fusesource/jansi/ffm/WindowsCLibrary.java new file mode 100644 index 00000000..c68854bf --- /dev/null +++ b/src/main/java/org/fusesource/jansi/ffm/WindowsCLibrary.java @@ -0,0 +1,116 @@ +/* + * 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.ffm; + +import java.lang.foreign.*; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.VarHandle; +import java.nio.charset.StandardCharsets; + +import org.fusesource.jansi.AnsiConsoleSupport; + +import static java.lang.foreign.ValueLayout.*; + +final class WindowsCLibrary implements AnsiConsoleSupport.CLibrary { + + private static final int FILE_TYPE_CHAR = 0x0002; + + private static final int ObjectNameInformation = 1; + + private static final MethodHandle NtQueryObject; + private static final VarHandle UNICODE_STRING_LENGTH; + private static final VarHandle UNICODE_STRING_BUFFER; + + static { + MethodHandle ntQueryObjectHandle = null; + try { + SymbolLookup ntDll = SymbolLookup.libraryLookup("ntdll", Arena.ofAuto()); + + ntQueryObjectHandle = ntDll.find("NtQueryObject") + .map(addr -> Linker.nativeLinker() + .downcallHandle( + addr, + FunctionDescriptor.of(JAVA_INT, ADDRESS, JAVA_INT, ADDRESS, JAVA_LONG, ADDRESS))) + .orElse(null); + } catch (Throwable ignored) { + } + + NtQueryObject = ntQueryObjectHandle; + + StructLayout unicodeStringLayout; + if (ADDRESS.byteSize() == 8) { + unicodeStringLayout = MemoryLayout.structLayout( + JAVA_SHORT.withName("Length"), + JAVA_SHORT.withName("MaximumLength"), + MemoryLayout.paddingLayout(4), + ADDRESS.withTargetLayout(JAVA_BYTE).withName("Buffer")); + } else { + // 32 Bit + unicodeStringLayout = MemoryLayout.structLayout( + JAVA_SHORT.withName("Length"), + JAVA_SHORT.withName("MaximumLength"), + ADDRESS.withTargetLayout(JAVA_BYTE).withName("Buffer")); + } + + UNICODE_STRING_LENGTH = unicodeStringLayout.varHandle(PathElement.groupElement("Length")); + UNICODE_STRING_BUFFER = unicodeStringLayout.varHandle(PathElement.groupElement("Buffer")); + } + + @Override + public short getTerminalWidth(int fd) { + throw new UnsupportedOperationException("Windows does not support ioctl"); + } + + @Override + public int isTty(int fd) { + try (Arena arena = Arena.ofConfined()) { + // check if fd is a pipe + MemorySegment h = Kernel32._get_osfhandle(fd); + int t = Kernel32.GetFileType(h); + if (t == FILE_TYPE_CHAR) { + // check that this is a real tty because the /dev/null + // and /dev/zero streams are also of type FILE_TYPE_CHAR + return Kernel32.GetConsoleMode(h, arena.allocate(JAVA_INT)); + } + + if (NtQueryObject == null) { + return 0; + } + + final int BUFFER_SIZE = 1024; + + MemorySegment buffer = arena.allocate(BUFFER_SIZE); + MemorySegment result = arena.allocate(JAVA_LONG); + + int res = (int) NtQueryObject.invokeExact(h, ObjectNameInformation, buffer, BUFFER_SIZE - 2, result); + if (res != 0) { + return 0; + } + + int stringLength = Short.toUnsignedInt((Short) UNICODE_STRING_LENGTH.get(buffer)); + MemorySegment stringBuffer = ((MemorySegment) UNICODE_STRING_BUFFER.get(buffer)).reinterpret(stringLength); + + String str = new String(stringBuffer.toArray(JAVA_BYTE), StandardCharsets.UTF_16LE).trim(); + if (str.startsWith("msys-") || str.startsWith("cygwin-") || str.startsWith("-pty")) { + return 1; + } + + return 0; + } catch (Throwable e) { + throw new AssertionError("should not reach here", e); + } + } +} diff --git a/src/main/java/org/fusesource/jansi/internal/OSInfo.java b/src/main/java/org/fusesource/jansi/internal/OSInfo.java index fe53cbb5..6957f8c7 100644 --- a/src/main/java/org/fusesource/jansi/internal/OSInfo.java +++ b/src/main/java/org/fusesource/jansi/internal/OSInfo.java @@ -120,8 +120,14 @@ public static String getOSName() { return translateOSNameToFolderName(System.getProperty("os.name")); } + public static boolean isWindows() { + return System.getProperty("os.name").toLowerCase(Locale.ROOT).contains("win"); + } + public static boolean isAndroid() { - return System.getProperty("java.runtime.name", "").toLowerCase().contains("android"); + return System.getProperty("java.runtime.name", "") + .toLowerCase(Locale.ROOT) + .contains("android"); } public static boolean isAlpine() { @@ -131,7 +137,7 @@ public static boolean isAlpine() { InputStream in = p.getInputStream(); try { - return readFully(in).toLowerCase().contains("alpine"); + return readFully(in).toLowerCase(Locale.ROOT).contains("alpine"); } finally { in.close(); } @@ -207,7 +213,7 @@ public static String getArchName() { if (osArch.startsWith("arm")) { osArch = resolveArmArchType(); } else { - String lc = osArch.toLowerCase(Locale.US); + String lc = osArch.toLowerCase(Locale.ROOT); if (archMapping.containsKey(lc)) return archMapping.get(lc); } return translateArchNameToFolderName(osArch); From b4aa44880ce00d5c8508715b21d284e7cb98eada Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Fri, 29 Sep 2023 11:10:24 +0200 Subject: [PATCH 05/15] Send both SCO and DEC command for save/restore cursor position (fixes #226) (#262) --- src/main/java/org/fusesource/jansi/Ansi.java | 32 ++++++++++++++++++-- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/fusesource/jansi/Ansi.java b/src/main/java/org/fusesource/jansi/Ansi.java index c1054742..803604cb 100644 --- a/src/main/java/org/fusesource/jansi/Ansi.java +++ b/src/main/java/org/fusesource/jansi/Ansi.java @@ -716,19 +716,45 @@ public Ansi scrollDown(final int rows) { return rows > 0 ? appendEscapeSequence('T', rows) : rows < 0 ? scrollUp(-rows) : this; } + @Deprecated + public Ansi restorCursorPosition() { + return restoreCursorPosition(); + } + public Ansi saveCursorPosition() { + saveCursorPositionSCO(); + return saveCursorPositionDEC(); + } + + // SCO command + public Ansi saveCursorPositionSCO() { return appendEscapeSequence('s'); } - @Deprecated - public Ansi restorCursorPosition() { - return appendEscapeSequence('u'); + // DEC command + public Ansi saveCursorPositionDEC() { + builder.append(FIRST_ESC_CHAR); + builder.append('7'); + return this; } public Ansi restoreCursorPosition() { + restoreCursorPositionSCO(); + return restoreCursorPositionDEC(); + } + + // SCO command + public Ansi restoreCursorPositionSCO() { return appendEscapeSequence('u'); } + // DEC command + public Ansi restoreCursorPositionDEC() { + builder.append(FIRST_ESC_CHAR); + builder.append('8'); + return this; + } + public Ansi reset() { return a(Attribute.RESET); } From 3c6f950fb523332f1abb6fdf4b2605aaed440e7e Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Fri, 29 Sep 2023 14:01:28 +0200 Subject: [PATCH 06/15] Fix rebuilding the project --- pom.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/pom.xml b/pom.xml index 2b8a8f61..49bf9c81 100644 --- a/pom.xml +++ b/pom.xml @@ -266,6 +266,7 @@ org.fusesource.jansi.io; + true From 764a42207f12cb44009d4dd4896834977ac21c9d Mon Sep 17 00:00:00 2001 From: Glavo Date: Mon, 2 Oct 2023 22:43:19 +0800 Subject: [PATCH 07/15] Modernize code (#267) --- src/main/java/org/fusesource/jansi/Ansi.java | 9 ++---- .../org/fusesource/jansi/AnsiConsole.java | 21 +++++--------- .../java/org/fusesource/jansi/AnsiMain.java | 4 ++- .../jansi/internal/JansiLoader.java | 29 +++---------------- .../org/fusesource/jansi/internal/OSInfo.java | 20 ++++++------- .../fusesource/jansi/io/AnsiOutputStream.java | 8 +++-- .../jansi/io/FastBufferedOutputStream.java | 2 +- .../java/org/fusesource/jansi/AnsiTest.java | 22 ++++---------- .../org/fusesource/jansi/EncodingTest.java | 10 +++---- .../jansi/io/AnsiOutputStreamTest.java | 4 +-- 10 files changed, 45 insertions(+), 84 deletions(-) diff --git a/src/main/java/org/fusesource/jansi/Ansi.java b/src/main/java/org/fusesource/jansi/Ansi.java index 803604cb..576d8d53 100644 --- a/src/main/java/org/fusesource/jansi/Ansi.java +++ b/src/main/java/org/fusesource/jansi/Ansi.java @@ -149,17 +149,14 @@ public int value() { } } + @FunctionalInterface public interface Consumer { void apply(Ansi ansi); } public static final String DISABLE = Ansi.class.getName() + ".disable"; - private static Callable detector = new Callable() { - public Boolean call() throws Exception { - return !Boolean.getBoolean(DISABLE); - } - }; + private static Callable detector = () -> !Boolean.getBoolean(DISABLE); public static void setDetector(final Callable detector) { if (detector == null) throw new IllegalArgumentException(); @@ -374,7 +371,7 @@ public Ansi reset() { } private final StringBuilder builder; - private final ArrayList attributeOptions = new ArrayList(5); + private final ArrayList attributeOptions = new ArrayList<>(5); public Ansi() { this(new StringBuilder(80)); diff --git a/src/main/java/org/fusesource/jansi/AnsiConsole.java b/src/main/java/org/fusesource/jansi/AnsiConsole.java index d8de9dc9..dbe9d268 100644 --- a/src/main/java/org/fusesource/jansi/AnsiConsole.java +++ b/src/main/java/org/fusesource/jansi/AnsiConsole.java @@ -276,19 +276,13 @@ private static AnsiPrintStream ansiStream(boolean stdout) { getKernel32().setConsoleMode(console, mode[0]); processor = null; type = AnsiType.VirtualTerminal; - installer = new AnsiOutputStream.IoRunnable() { - @Override - public void run() throws IOException { - virtualProcessing++; - getKernel32().setConsoleMode(console, mode[0] | ENABLE_VIRTUAL_TERMINAL_PROCESSING); - } + installer = () -> { + virtualProcessing++; + getKernel32().setConsoleMode(console, mode[0] | ENABLE_VIRTUAL_TERMINAL_PROCESSING); }; - uninstaller = new AnsiOutputStream.IoRunnable() { - @Override - public void run() throws IOException { - if (--virtualProcessing == 0) { - getKernel32().setConsoleMode(console, mode[0]); - } + uninstaller = () -> { + if (--virtualProcessing == 0) { + getKernel32().setConsoleMode(console, mode[0]); } }; } else if ((IS_CONEMU || IS_CYGWIN || IS_MSYSTEM) && !isConsole) { @@ -427,8 +421,7 @@ static boolean getBoolean(String name) { try { String val = System.getProperty(name); result = val.isEmpty() || Boolean.parseBoolean(val); - } catch (IllegalArgumentException e) { - } catch (NullPointerException e) { + } catch (IllegalArgumentException | NullPointerException ignored) { } return result; } diff --git a/src/main/java/org/fusesource/jansi/AnsiMain.java b/src/main/java/org/fusesource/jansi/AnsiMain.java index 24fe6f2f..a3518ad1 100644 --- a/src/main/java/org/fusesource/jansi/AnsiMain.java +++ b/src/main/java/org/fusesource/jansi/AnsiMain.java @@ -23,11 +23,13 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.io.PrintStream; +import java.nio.charset.StandardCharsets; import java.util.Properties; import org.fusesource.jansi.Ansi.Attribute; import org.fusesource.jansi.internal.JansiLoader; +import static java.nio.charset.StandardCharsets.UTF_8; import static org.fusesource.jansi.Ansi.ansi; /** @@ -294,7 +296,7 @@ private static String getPomPropertiesVersion(String path) throws IOException { private static void printJansiLogoDemo() throws IOException { BufferedReader in = - new BufferedReader(new InputStreamReader(AnsiMain.class.getResourceAsStream("jansi.txt"), "UTF-8")); + new BufferedReader(new InputStreamReader(AnsiMain.class.getResourceAsStream("jansi.txt"), UTF_8)); try { String l; while ((l = in.readLine()) != null) { diff --git a/src/main/java/org/fusesource/jansi/internal/JansiLoader.java b/src/main/java/org/fusesource/jansi/internal/JansiLoader.java index f705620c..1b4494f3 100644 --- a/src/main/java/org/fusesource/jansi/internal/JansiLoader.java +++ b/src/main/java/org/fusesource/jansi/internal/JansiLoader.java @@ -37,8 +37,9 @@ import java.io.FilenameFilter; import java.io.IOException; import java.io.InputStream; -import java.io.OutputStream; import java.net.URL; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; import java.util.Arrays; import java.util.LinkedList; import java.util.List; @@ -198,9 +199,7 @@ private static boolean extractAndLoadLibraryFile( if (!extractedLckFile.exists()) { new FileOutputStream(extractedLckFile).close(); } - try (OutputStream out = new FileOutputStream(extractedLibFile)) { - copy(in, out); - } + Files.copy(in, extractedLibFile.toPath(), StandardCopyOption.REPLACE_EXISTING); } finally { // Delete the extracted lib file on JVM exit. extractedLibFile.deleteOnExit(); @@ -239,14 +238,6 @@ private static String randomUUID() { return Long.toHexString(new Random().nextLong()); } - private static void copy(InputStream in, OutputStream out) throws IOException { - byte[] buf = new byte[8192]; - int n; - while ((n = in.read(buf)) > 0) { - out.write(buf, 0, n); - } - } - /** * Loads native library using the given path and name of the library. * @@ -358,7 +349,7 @@ private static void loadJansiNativeLibrary() throws Exception { throw new Exception(String.format( "No native library found for os.name=%s, os.arch=%s, paths=[%s]", - OSInfo.getOSName(), OSInfo.getArchName(), join(triedPaths, File.pathSeparator))); + OSInfo.getOSName(), OSInfo.getArchName(), String.join(File.pathSeparator, triedPaths))); } private static boolean hasResource(String path) { @@ -401,16 +392,4 @@ public static String getVersion() { } return version; } - - private static String join(List list, String separator) { - StringBuilder sb = new StringBuilder(); - boolean first = true; - for (String item : list) { - if (first) first = false; - else sb.append(separator); - - sb.append(item); - } - return sb.toString(); - } } diff --git a/src/main/java/org/fusesource/jansi/internal/OSInfo.java b/src/main/java/org/fusesource/jansi/internal/OSInfo.java index 6957f8c7..14b7b0ec 100644 --- a/src/main/java/org/fusesource/jansi/internal/OSInfo.java +++ b/src/main/java/org/fusesource/jansi/internal/OSInfo.java @@ -34,6 +34,8 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Paths; import java.util.HashMap; import java.util.Locale; @@ -132,19 +134,15 @@ public static boolean isAndroid() { public static boolean isAlpine() { try { - Process p = Runtime.getRuntime().exec("cat /etc/os-release | grep ^ID"); - p.waitFor(); - - InputStream in = p.getInputStream(); - try { - return readFully(in).toLowerCase(Locale.ROOT).contains("alpine"); - } finally { - in.close(); + for (String line : Files.readAllLines(Paths.get("/etc/os-release"))) { + if (line.startsWith("ID") && line.toLowerCase(Locale.ROOT).contains("alpine")) { + return true; + } } - - } catch (Throwable e) { - return false; + } catch (Throwable ignored) { } + + return false; } static String getHardwareName() { diff --git a/src/main/java/org/fusesource/jansi/io/AnsiOutputStream.java b/src/main/java/org/fusesource/jansi/io/AnsiOutputStream.java index d925d728..9c45afef 100644 --- a/src/main/java/org/fusesource/jansi/io/AnsiOutputStream.java +++ b/src/main/java/org/fusesource/jansi/io/AnsiOutputStream.java @@ -25,6 +25,8 @@ import org.fusesource.jansi.AnsiMode; import org.fusesource.jansi.AnsiType; +import static java.nio.charset.StandardCharsets.US_ASCII; + /** * A ANSI print stream extracts ANSI escape codes written to * an output stream and calls corresponding AnsiProcessor.process* methods. @@ -38,12 +40,14 @@ */ public class AnsiOutputStream extends FilterOutputStream { - public static final byte[] RESET_CODE = "\033[0m".getBytes(); + public static final byte[] RESET_CODE = "\033[0m".getBytes(US_ASCII); + @FunctionalInterface public interface IoRunnable { void run() throws IOException; } + @FunctionalInterface public interface WidthSupplier { int getTerminalWidth(); } @@ -79,7 +83,7 @@ public int getTerminalWidth() { private final byte[] buffer = new byte[MAX_ESCAPE_SEQUENCE_LENGTH]; private int pos = 0; private int startOfValue; - private final ArrayList options = new ArrayList(); + private final ArrayList options = new ArrayList<>(); private int state = LOOKING_FOR_FIRST_ESC_CHAR; private final Charset cs; diff --git a/src/main/java/org/fusesource/jansi/io/FastBufferedOutputStream.java b/src/main/java/org/fusesource/jansi/io/FastBufferedOutputStream.java index 823e8019..e436c35e 100644 --- a/src/main/java/org/fusesource/jansi/io/FastBufferedOutputStream.java +++ b/src/main/java/org/fusesource/jansi/io/FastBufferedOutputStream.java @@ -24,7 +24,7 @@ */ public class FastBufferedOutputStream extends FilterOutputStream { - protected final byte buf[] = new byte[8192]; + protected final byte[] buf = new byte[8192]; protected int count; public FastBufferedOutputStream(OutputStream out) { diff --git a/src/test/java/org/fusesource/jansi/AnsiTest.java b/src/test/java/org/fusesource/jansi/AnsiTest.java index 2e4a6453..824c8d0d 100644 --- a/src/test/java/org/fusesource/jansi/AnsiTest.java +++ b/src/test/java/org/fusesource/jansi/AnsiTest.java @@ -21,6 +21,8 @@ import org.junit.jupiter.params.provider.CsvSource; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; /** * Tests for the {@link Ansi} class. @@ -30,20 +32,10 @@ public class AnsiTest { @Test public void testSetEnabled() throws Exception { Ansi.setEnabled(false); - new Thread() { - @Override - public void run() { - assertEquals(false, Ansi.isEnabled()); - } - }.run(); + new Thread(() -> assertFalse(Ansi.isEnabled())).run(); Ansi.setEnabled(true); - new Thread() { - @Override - public void run() { - assertEquals(true, Ansi.isEnabled()); - } - }.run(); + new Thread(() -> assertTrue(Ansi.isEnabled())).run(); } @Test @@ -59,11 +51,7 @@ public void testApply() { assertEquals( "test", Ansi.ansi() - .apply(new Ansi.Consumer() { - public void apply(Ansi ansi) { - ansi.a("test"); - } - }) + .apply(ansi -> ansi.a("test")) .toString()); } diff --git a/src/test/java/org/fusesource/jansi/EncodingTest.java b/src/test/java/org/fusesource/jansi/EncodingTest.java index 4cccc88f..d6befd2d 100644 --- a/src/test/java/org/fusesource/jansi/EncodingTest.java +++ b/src/test/java/org/fusesource/jansi/EncodingTest.java @@ -18,7 +18,7 @@ import java.io.ByteArrayOutputStream; import java.io.PrintStream; import java.io.UnsupportedEncodingException; -import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.util.concurrent.atomic.AtomicReference; import org.fusesource.jansi.io.AnsiOutputStream; @@ -32,7 +32,7 @@ public class EncodingTest { @Test public void testEncoding8859() throws UnsupportedEncodingException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); - final AtomicReference newLabel = new AtomicReference(); + final AtomicReference newLabel = new AtomicReference<>(); PrintStream ansi = new AnsiPrintStream( new AnsiOutputStream( baos, @@ -46,7 +46,7 @@ protected void processChangeWindowTitle(String label) { }, AnsiType.Emulation, AnsiColors.TrueColor, - Charset.forName("ISO-8859-1"), + StandardCharsets.ISO_8859_1, null, null, false), @@ -61,7 +61,7 @@ protected void processChangeWindowTitle(String label) { @Test public void testEncodingUtf8() throws UnsupportedEncodingException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); - final AtomicReference newLabel = new AtomicReference(); + final AtomicReference newLabel = new AtomicReference<>(); PrintStream ansi = new PrintStream( new AnsiOutputStream( baos, @@ -75,7 +75,7 @@ protected void processChangeWindowTitle(String label) { }, AnsiType.Emulation, AnsiColors.TrueColor, - Charset.forName("UTF-8"), + StandardCharsets.UTF_8, null, null, false), diff --git a/src/test/java/org/fusesource/jansi/io/AnsiOutputStreamTest.java b/src/test/java/org/fusesource/jansi/io/AnsiOutputStreamTest.java index 49e6dae1..96353408 100644 --- a/src/test/java/org/fusesource/jansi/io/AnsiOutputStreamTest.java +++ b/src/test/java/org/fusesource/jansi/io/AnsiOutputStreamTest.java @@ -17,7 +17,7 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import org.fusesource.jansi.AnsiColors; import org.fusesource.jansi.AnsiMode; @@ -38,7 +38,7 @@ void canHandleSgrsWithMultipleOptions() throws IOException { null, AnsiType.Emulation, AnsiColors.TrueColor, - Charset.forName("UTF-8"), + StandardCharsets.UTF_8, null, null, false); From ffd16888db1f9c5406648e62e0d5f57a03e3eb1d Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Mon, 2 Oct 2023 16:44:11 +0200 Subject: [PATCH 08/15] Fix terminal width support on MINGW (fixes #233) (#264) --- .../org/fusesource/jansi/AnsiConsole.java | 11 +- .../java/org/fusesource/jansi/AnsiMain.java | 15 +- .../jansi/internal/MingwSupport.java | 137 ++++++++++++++++++ 3 files changed, 161 insertions(+), 2 deletions(-) create mode 100644 src/main/java/org/fusesource/jansi/internal/MingwSupport.java diff --git a/src/main/java/org/fusesource/jansi/AnsiConsole.java b/src/main/java/org/fusesource/jansi/AnsiConsole.java index dbe9d268..749c39cb 100644 --- a/src/main/java/org/fusesource/jansi/AnsiConsole.java +++ b/src/main/java/org/fusesource/jansi/AnsiConsole.java @@ -25,6 +25,7 @@ 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.io.AnsiOutputStream; import org.fusesource.jansi.io.AnsiProcessor; @@ -285,11 +286,19 @@ private static AnsiPrintStream ansiStream(boolean stdout) { getKernel32().setConsoleMode(console, mode[0]); } }; + width = () -> getKernel32().getTerminalWidth(console); } else if ((IS_CONEMU || IS_CYGWIN || IS_MSYSTEM) && !isConsole) { // ANSI-enabled ConEmu, Cygwin or MSYS(2) on Windows... processor = null; type = AnsiType.Native; installer = uninstaller = null; + MingwSupport mingw = new MingwSupport(); + String name = mingw.getConsoleName(stdout); + if (name != null && !name.isEmpty()) { + width = () -> mingw.getTerminalWidth(name); + } else { + width = () -> -1; + } } else { // On Windows, when no ANSI-capable terminal is used, we know the console does not natively interpret // ANSI @@ -308,8 +317,8 @@ private static AnsiPrintStream ansiStream(boolean stdout) { processor = proc; type = ttype; installer = uninstaller = null; + width = () -> getKernel32().getTerminalWidth(console); } - width = () -> getKernel32().getTerminalWidth(console); } // We must be on some Unix variant... diff --git a/src/main/java/org/fusesource/jansi/AnsiMain.java b/src/main/java/org/fusesource/jansi/AnsiMain.java index a3518ad1..bb6ab3cc 100644 --- a/src/main/java/org/fusesource/jansi/AnsiMain.java +++ b/src/main/java/org/fusesource/jansi/AnsiMain.java @@ -28,6 +28,7 @@ import org.fusesource.jansi.Ansi.Attribute; import org.fusesource.jansi.internal.JansiLoader; +import org.fusesource.jansi.internal.MingwSupport; import static java.nio.charset.StandardCharsets.UTF_8; import static org.fusesource.jansi.Ansi.ansi; @@ -205,7 +206,19 @@ private static void diagnoseTty(boolean stderr) { if (AnsiConsole.IS_WINDOWS) { long console = AnsiConsoleSupport.getInstance().getKernel32().getStdHandle(!stderr); isatty = AnsiConsoleSupport.getInstance().getKernel32().isTty(console); - width = AnsiConsoleSupport.getInstance().getKernel32().getTerminalWidth(console); + if ((AnsiConsole.IS_CONEMU || AnsiConsole.IS_CYGWIN || AnsiConsole.IS_MSYSTEM) && isatty == 0) { + MingwSupport mingw = new MingwSupport(); + String name = mingw.getConsoleName(!stderr); + if (name != null && !name.isEmpty()) { + isatty = 1; + width = mingw.getTerminalWidth(name); + } else { + isatty = 0; + width = 0; + } + } else { + width = AnsiConsoleSupport.getInstance().getKernel32().getTerminalWidth(console); + } } else { int fd = stderr ? AnsiConsoleSupport.CLibrary.STDERR_FILENO : AnsiConsoleSupport.CLibrary.STDOUT_FILENO; isatty = AnsiConsoleSupport.getInstance().getCLibrary().isTty(fd); diff --git a/src/main/java/org/fusesource/jansi/internal/MingwSupport.java b/src/main/java/org/fusesource/jansi/internal/MingwSupport.java new file mode 100644 index 00000000..be0c54a2 --- /dev/null +++ b/src/main/java/org/fusesource/jansi/internal/MingwSupport.java @@ -0,0 +1,137 @@ +/* + * 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; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileDescriptor; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * 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 + * obtain the terminal name and width. + */ +public class MingwSupport { + + private final String sttyCommand; + private final String ttyCommand; + private final Pattern columnsPatterns; + + public MingwSupport() { + String tty = null; + String stty = null; + 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()) { + tty = ttyFile.getAbsolutePath(); + } + File sttyFile = new File(p, "stty.exe"); + if (stty == null && sttyFile.canExecute()) { + stty = sttyFile.getAbsolutePath(); + } + } + } + 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"); + } + + public String getConsoleName(boolean stdout) { + try { + Process p = new ProcessBuilder(ttyCommand) + .redirectInput(getRedirect(stdout ? FileDescriptor.out : FileDescriptor.err)) + .start(); + String result = waitAndCapture(p); + if (p.exitValue() == 0) { + return result.trim(); + } + } catch (Throwable t) { + if ("java.lang.reflect.InaccessibleObjectException" + .equals(t.getClass().getName())) { + System.err.println("MINGW support requires --add-opens java.base/java.lang=ALL-UNNAMED"); + } + // ignore + } + return null; + } + + public int getTerminalWidth(String name) { + try { + Process p = new ProcessBuilder(sttyCommand, "-F", name, "-a").start(); + String result = waitAndCapture(p); + if (p.exitValue() != 0) { + throw new IOException("Error executing '" + sttyCommand + "': " + result); + } + Matcher matcher = columnsPatterns.matcher(result); + if (matcher.find()) { + return Integer.parseInt(matcher.group(1)); + } + throw new IOException("Unable to parse columns"); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static String waitAndCapture(Process p) throws IOException, InterruptedException { + ByteArrayOutputStream bout = new ByteArrayOutputStream(); + try (InputStream in = p.getInputStream(); + InputStream err = p.getErrorStream()) { + int c; + while ((c = in.read()) != -1) { + bout.write(c); + } + while ((c = err.read()) != -1) { + bout.write(c); + } + p.waitFor(); + } + return bout.toString(); + } + + /** + * This requires --add-opens java.base/java.lang=ALL-UNNAMED + */ + private 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 + Class rpi = Class.forName("java.lang.ProcessBuilder$RedirectPipeImpl"); + Constructor cns = rpi.getDeclaredConstructor(); + cns.setAccessible(true); + ProcessBuilder.Redirect input = (ProcessBuilder.Redirect) cns.newInstance(); + Field f = rpi.getDeclaredField("fd"); + f.setAccessible(true); + f.set(input, fd); + return input; + } +} From a8961cb811a7b641d0ea2b973a2cf8fc00af0b45 Mon Sep 17 00:00:00 2001 From: Glavo Date: Mon, 2 Oct 2023 23:03:59 +0800 Subject: [PATCH 09/15] Refactoring AnsiConsoleSupport (#266) --- .../org/fusesource/jansi/AnsiConsole.java | 24 ++-- .../jansi/AnsiConsoleSupportHolder.java | 60 --------- .../java/org/fusesource/jansi/AnsiMain.java | 18 +-- .../org/fusesource/jansi/WindowsSupport.java | 10 +- .../{ => internal}/AnsiConsoleSupport.java | 6 +- .../internal/AnsiConsoleSupportHolder.java | 127 ++++++++++++++++++ .../ffm/AnsiConsoleSupportImpl.java} | 19 ++- .../jansi/{ => internal}/ffm/Kernel32.java | 5 +- .../{ => internal}/ffm/PosixCLibrary.java | 4 +- .../ffm/WindowsAnsiProcessor.java | 4 +- .../{ => internal}/ffm/WindowsCLibrary.java | 4 +- .../AnsiConsoleSupportImpl.java} | 6 +- .../java/org/fusesource/jansi/AnsiTest.java | 6 +- 13 files changed, 183 insertions(+), 110 deletions(-) delete mode 100644 src/main/java/org/fusesource/jansi/AnsiConsoleSupportHolder.java rename src/main/java/org/fusesource/jansi/{ => internal}/AnsiConsoleSupport.java (91%) create mode 100644 src/main/java/org/fusesource/jansi/internal/AnsiConsoleSupportHolder.java rename src/main/java/org/fusesource/jansi/{ffm/AnsiConsoleSupportFfm.java => internal/ffm/AnsiConsoleSupportImpl.java} (82%) rename src/main/java/org/fusesource/jansi/{ => internal}/ffm/Kernel32.java (99%) rename src/main/java/org/fusesource/jansi/{ => internal}/ffm/PosixCLibrary.java (97%) rename src/main/java/org/fusesource/jansi/{ => internal}/ffm/WindowsAnsiProcessor.java (99%) rename src/main/java/org/fusesource/jansi/{ => internal}/ffm/WindowsCLibrary.java (97%) rename src/main/java/org/fusesource/jansi/internal/{AnsiConsoleSupportJni.java => jni/AnsiConsoleSupportImpl.java} (95%) diff --git a/src/main/java/org/fusesource/jansi/AnsiConsole.java b/src/main/java/org/fusesource/jansi/AnsiConsole.java index 749c39cb..0e1d5df3 100644 --- a/src/main/java/org/fusesource/jansi/AnsiConsole.java +++ b/src/main/java/org/fusesource/jansi/AnsiConsole.java @@ -31,6 +31,9 @@ import org.fusesource.jansi.io.AnsiProcessor; import org.fusesource.jansi.io.FastBufferedOutputStream; +import static org.fusesource.jansi.internal.AnsiConsoleSupportHolder.getCLibrary; +import static org.fusesource.jansi.internal.AnsiConsoleSupportHolder.getKernel32; + /** * Provides consistent access to an ANSI aware console PrintStream or an ANSI codes stripping PrintStream * if not on a terminal (see @@ -155,10 +158,21 @@ public class AnsiConsole { */ public static final String JANSI_GRACEFUL = "jansi.graceful"; + /** + * The {@code jansi.providers} system property can be set to control which internal provider + * will be used. If this property is not set, the {@code ffm} provider will be used if available, + * else the {@code jni} one will be used. If set, this property is interpreted as a comma + * separated list of provider names to try in order. + */ public static final String JANSI_PROVIDERS = "jansi.providers"; + /** + * The name of the {@code jni} provider. + */ public static final String JANSI_PROVIDER_JNI = "jni"; + /** + * The name of the {@code ffm} provider. + */ public static final String JANSI_PROVIDER_FFM = "ffm"; - public static final String JANSI_PROVIDERS_DEFAULT = JANSI_PROVIDER_FFM + "," + JANSI_PROVIDER_JNI; /** * @deprecated this field will be made private in a future release, use {@link #sysOut()} instead @@ -536,12 +550,4 @@ static synchronized void initStreams() { initialized = true; } } - - private static AnsiConsoleSupport.Kernel32 getKernel32() { - return AnsiConsoleSupport.getInstance().getKernel32(); - } - - private static AnsiConsoleSupport.CLibrary getCLibrary() { - return AnsiConsoleSupport.getInstance().getCLibrary(); - } } diff --git a/src/main/java/org/fusesource/jansi/AnsiConsoleSupportHolder.java b/src/main/java/org/fusesource/jansi/AnsiConsoleSupportHolder.java deleted file mode 100644 index 082f78d0..00000000 --- a/src/main/java/org/fusesource/jansi/AnsiConsoleSupportHolder.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * 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; - -import org.fusesource.jansi.internal.AnsiConsoleSupportJni; - -import static org.fusesource.jansi.AnsiConsole.JANSI_PROVIDERS; -import static org.fusesource.jansi.AnsiConsole.JANSI_PROVIDERS_DEFAULT; -import static org.fusesource.jansi.AnsiConsole.JANSI_PROVIDER_FFM; -import static org.fusesource.jansi.AnsiConsole.JANSI_PROVIDER_JNI; - -class AnsiConsoleSupportHolder { - static volatile AnsiConsoleSupport instance; - - static AnsiConsoleSupport get() { - if (instance == null) { - synchronized (AnsiConsoleSupportHolder.class) { - if (instance == null) { - instance = doGet(); - } - } - } - return instance; - } - - static AnsiConsoleSupport doGet() { - RuntimeException error = new RuntimeException("Unable to create AnsiConsoleSupport provider"); - String[] providers = - System.getProperty(JANSI_PROVIDERS, JANSI_PROVIDERS_DEFAULT).split(","); - for (String provider : providers) { - try { - if (JANSI_PROVIDER_FFM.equals(provider)) { - return (AnsiConsoleSupport) AnsiConsoleSupport.class - .getClassLoader() - .loadClass("org.fusesource.jansi.ffm.AnsiConsoleSupportFfm") - .getConstructor() - .newInstance(); - } else if (JANSI_PROVIDER_JNI.equals(provider)) { - return new AnsiConsoleSupportJni(); - } - } catch (Throwable t) { - error.addSuppressed(t); - } - } - throw error; - } -} diff --git a/src/main/java/org/fusesource/jansi/AnsiMain.java b/src/main/java/org/fusesource/jansi/AnsiMain.java index bb6ab3cc..e824322d 100644 --- a/src/main/java/org/fusesource/jansi/AnsiMain.java +++ b/src/main/java/org/fusesource/jansi/AnsiMain.java @@ -23,10 +23,11 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.io.PrintStream; -import java.nio.charset.StandardCharsets; import java.util.Properties; import org.fusesource.jansi.Ansi.Attribute; +import org.fusesource.jansi.internal.AnsiConsoleSupport; +import org.fusesource.jansi.internal.AnsiConsoleSupportHolder; import org.fusesource.jansi.internal.JansiLoader; import org.fusesource.jansi.internal.MingwSupport; @@ -56,9 +57,8 @@ public static void main(String... args) throws IOException { System.out.println(); - System.out.println("jansi.providers= " - + System.getProperty(AnsiConsole.JANSI_PROVIDERS, AnsiConsole.JANSI_PROVIDERS_DEFAULT)); - String provider = AnsiConsoleSupport.getInstance().getProviderName(); + System.out.println("jansi.providers= " + System.getProperty(AnsiConsole.JANSI_PROVIDERS, "")); + String provider = AnsiConsoleSupportHolder.getProviderName(); System.out.println("Selected provider: " + provider); if (AnsiConsole.JANSI_PROVIDER_JNI.equals(provider)) { @@ -204,8 +204,8 @@ private static void diagnoseTty(boolean stderr) { int isatty; int width; if (AnsiConsole.IS_WINDOWS) { - long console = AnsiConsoleSupport.getInstance().getKernel32().getStdHandle(!stderr); - isatty = AnsiConsoleSupport.getInstance().getKernel32().isTty(console); + 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); @@ -217,12 +217,12 @@ private static void diagnoseTty(boolean stderr) { width = 0; } } else { - width = AnsiConsoleSupport.getInstance().getKernel32().getTerminalWidth(console); + width = AnsiConsoleSupportHolder.getKernel32().getTerminalWidth(console); } } else { int fd = stderr ? AnsiConsoleSupport.CLibrary.STDERR_FILENO : AnsiConsoleSupport.CLibrary.STDOUT_FILENO; - isatty = AnsiConsoleSupport.getInstance().getCLibrary().isTty(fd); - width = AnsiConsoleSupport.getInstance().getCLibrary().getTerminalWidth(fd); + isatty = AnsiConsoleSupportHolder.getCLibrary().isTty(fd); + width = AnsiConsoleSupportHolder.getCLibrary().getTerminalWidth(fd); } System.out.println("isatty(STD" + (stderr ? "ERR" : "OUT") + "_FILENO): " + isatty + ", System." diff --git a/src/main/java/org/fusesource/jansi/WindowsSupport.java b/src/main/java/org/fusesource/jansi/WindowsSupport.java index e14854cd..cfc0f9bc 100644 --- a/src/main/java/org/fusesource/jansi/WindowsSupport.java +++ b/src/main/java/org/fusesource/jansi/WindowsSupport.java @@ -15,18 +15,16 @@ */ package org.fusesource.jansi; +import org.fusesource.jansi.internal.AnsiConsoleSupportHolder; + public class WindowsSupport { public static String getLastErrorMessage() { - int errorCode = getKernel32().getLastError(); + int errorCode = AnsiConsoleSupportHolder.getKernel32().getLastError(); return getErrorMessage(errorCode); } public static String getErrorMessage(int errorCode) { - return getKernel32().getErrorMessage(errorCode); - } - - private static AnsiConsoleSupport.Kernel32 getKernel32() { - return AnsiConsoleSupport.getInstance().getKernel32(); + return AnsiConsoleSupportHolder.getKernel32().getErrorMessage(errorCode); } } diff --git a/src/main/java/org/fusesource/jansi/AnsiConsoleSupport.java b/src/main/java/org/fusesource/jansi/internal/AnsiConsoleSupport.java similarity index 91% rename from src/main/java/org/fusesource/jansi/AnsiConsoleSupport.java rename to src/main/java/org/fusesource/jansi/internal/AnsiConsoleSupport.java index 02907c55..4868207d 100644 --- a/src/main/java/org/fusesource/jansi/AnsiConsoleSupport.java +++ b/src/main/java/org/fusesource/jansi/internal/AnsiConsoleSupport.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.fusesource.jansi; +package org.fusesource.jansi.internal; import java.io.IOException; import java.io.OutputStream; @@ -56,8 +56,4 @@ interface Kernel32 { CLibrary getCLibrary(); Kernel32 getKernel32(); - - static AnsiConsoleSupport getInstance() { - return AnsiConsoleSupportHolder.get(); - } } diff --git a/src/main/java/org/fusesource/jansi/internal/AnsiConsoleSupportHolder.java b/src/main/java/org/fusesource/jansi/internal/AnsiConsoleSupportHolder.java new file mode 100644 index 00000000..eb049f98 --- /dev/null +++ b/src/main/java/org/fusesource/jansi/internal/AnsiConsoleSupportHolder.java @@ -0,0 +1,127 @@ +/* + * 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; + +import static org.fusesource.jansi.AnsiConsole.JANSI_PROVIDERS; + +public final class AnsiConsoleSupportHolder { + + private static final String PROVIDER_NAME; + private static final AnsiConsoleSupport.CLibrary CLIBRARY; + private static final AnsiConsoleSupport.Kernel32 KERNEL32; + private static final Throwable ERR; + + private static AnsiConsoleSupport getDefaultProvider() { + try { + // Call the specialized constructor to check whether the module has native access enabled + // If not, fallback to JNI to avoid the JDK printing warnings in stderr + return (AnsiConsoleSupport) Class.forName("org.fusesource.jansi.internal.ffm.AnsiConsoleSupportImpl") + .getConstructor(boolean.class) + .newInstance(true); + } catch (Throwable ignored) { + } + + return new org.fusesource.jansi.internal.jni.AnsiConsoleSupportImpl(); + } + + private static AnsiConsoleSupport findProvider(String providerList) { + String[] providers = providerList.split(","); + + RuntimeException error = null; + + for (String provider : providers) { + try { + return (AnsiConsoleSupport) + Class.forName("org.fusesource.jansi.internal." + provider + ".AnsiConsoleSupportImpl") + .getConstructor() + .newInstance(); + } catch (Throwable t) { + if (error == null) { + error = new RuntimeException("Unable to create AnsiConsoleSupport provider"); + } + + error.addSuppressed(t); + } + } + + // User does not specify any provider, falling back to the default + if (error == null) { + return getDefaultProvider(); + } + + throw error; + } + + static { + String providerList = System.getProperty(JANSI_PROVIDERS); + + AnsiConsoleSupport ansiConsoleSupport = null; + Throwable err = null; + + try { + if (providerList == null) { + ansiConsoleSupport = getDefaultProvider(); + } else { + ansiConsoleSupport = findProvider(providerList); + } + } catch (Throwable e) { + err = e; + } + + String providerName = null; + AnsiConsoleSupport.CLibrary clib = null; + AnsiConsoleSupport.Kernel32 kernel32 = null; + + if (ansiConsoleSupport != null) { + try { + providerName = ansiConsoleSupport.getProviderName(); + clib = ansiConsoleSupport.getCLibrary(); + kernel32 = OSInfo.isWindows() ? ansiConsoleSupport.getKernel32() : null; + } catch (Throwable e) { + err = e; + } + } + + PROVIDER_NAME = providerName; + CLIBRARY = clib; + KERNEL32 = kernel32; + ERR = err; + } + + public static String getProviderName() { + return PROVIDER_NAME; + } + + public static AnsiConsoleSupport.CLibrary getCLibrary() { + if (CLIBRARY == null) { + throw new RuntimeException("Unable to get the instance of CLibrary", ERR); + } + + return CLIBRARY; + } + + public static AnsiConsoleSupport.Kernel32 getKernel32() { + if (KERNEL32 == null) { + if (OSInfo.isWindows()) { + throw new RuntimeException("Unable to get the instance of Kernel32", ERR); + } else { + throw new UnsupportedOperationException("Not Windows"); + } + } + + return KERNEL32; + } +} diff --git a/src/main/java/org/fusesource/jansi/ffm/AnsiConsoleSupportFfm.java b/src/main/java/org/fusesource/jansi/internal/ffm/AnsiConsoleSupportImpl.java similarity index 82% rename from src/main/java/org/fusesource/jansi/ffm/AnsiConsoleSupportFfm.java rename to src/main/java/org/fusesource/jansi/internal/ffm/AnsiConsoleSupportImpl.java index 2033fad8..bc252b41 100644 --- a/src/main/java/org/fusesource/jansi/ffm/AnsiConsoleSupportFfm.java +++ b/src/main/java/org/fusesource/jansi/internal/ffm/AnsiConsoleSupportImpl.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.fusesource.jansi.ffm; +package org.fusesource.jansi.internal.ffm; import java.io.IOException; import java.io.OutputStream; @@ -21,13 +21,22 @@ import java.lang.foreign.MemorySegment; import java.lang.foreign.ValueLayout; -import org.fusesource.jansi.AnsiConsoleSupport; +import org.fusesource.jansi.internal.AnsiConsoleSupport; import org.fusesource.jansi.internal.OSInfo; import org.fusesource.jansi.io.AnsiProcessor; -import static org.fusesource.jansi.ffm.Kernel32.*; +import static org.fusesource.jansi.internal.ffm.Kernel32.*; + +public final class AnsiConsoleSupportImpl implements AnsiConsoleSupport { + + public AnsiConsoleSupportImpl() {} + + public AnsiConsoleSupportImpl(boolean checkNativeAccess) { + if (checkNativeAccess && !AnsiConsoleSupportImpl.class.getModule().isNativeAccessEnabled()) { + throw new UnsupportedOperationException("Native access is not enabled for the current module"); + } + } -public class AnsiConsoleSupportFfm implements AnsiConsoleSupport { @Override public String getProviderName() { return "ffm"; @@ -88,7 +97,7 @@ public int getLastError() { @Override public String getErrorMessage(int errorCode) { - return org.fusesource.jansi.ffm.Kernel32.getErrorMessage(errorCode); + return org.fusesource.jansi.internal.ffm.Kernel32.getErrorMessage(errorCode); } @Override diff --git a/src/main/java/org/fusesource/jansi/ffm/Kernel32.java b/src/main/java/org/fusesource/jansi/internal/ffm/Kernel32.java similarity index 99% rename from src/main/java/org/fusesource/jansi/ffm/Kernel32.java rename to src/main/java/org/fusesource/jansi/internal/ffm/Kernel32.java index 0cc409ac..a657e096 100644 --- a/src/main/java/org/fusesource/jansi/ffm/Kernel32.java +++ b/src/main/java/org/fusesource/jansi/internal/ffm/Kernel32.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.fusesource.jansi.ffm; +package org.fusesource.jansi.internal.ffm; import java.io.IOException; import java.lang.foreign.AddressLayout; @@ -309,7 +309,8 @@ public static String getErrorMessage(int errorCode) { int bufferSize = 160; try (Arena arena = Arena.ofConfined()) { MemorySegment data = arena.allocate(bufferSize); - FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM, null, errorCode, 0, data, bufferSize, null); + FormatMessageW( + FORMAT_MESSAGE_FROM_SYSTEM, MemorySegment.NULL, errorCode, 0, data, bufferSize, MemorySegment.NULL); return new String(data.toArray(JAVA_BYTE), StandardCharsets.UTF_16LE).trim(); } } diff --git a/src/main/java/org/fusesource/jansi/ffm/PosixCLibrary.java b/src/main/java/org/fusesource/jansi/internal/ffm/PosixCLibrary.java similarity index 97% rename from src/main/java/org/fusesource/jansi/ffm/PosixCLibrary.java rename to src/main/java/org/fusesource/jansi/internal/ffm/PosixCLibrary.java index bd4f1f73..30e959b9 100644 --- a/src/main/java/org/fusesource/jansi/ffm/PosixCLibrary.java +++ b/src/main/java/org/fusesource/jansi/internal/ffm/PosixCLibrary.java @@ -13,13 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.fusesource.jansi.ffm; +package org.fusesource.jansi.internal.ffm; import java.lang.foreign.*; import java.lang.invoke.MethodHandle; import java.lang.invoke.VarHandle; -import org.fusesource.jansi.AnsiConsoleSupport; +import org.fusesource.jansi.internal.AnsiConsoleSupport; final class PosixCLibrary implements AnsiConsoleSupport.CLibrary { private static final int TIOCGWINSZ; diff --git a/src/main/java/org/fusesource/jansi/ffm/WindowsAnsiProcessor.java b/src/main/java/org/fusesource/jansi/internal/ffm/WindowsAnsiProcessor.java similarity index 99% rename from src/main/java/org/fusesource/jansi/ffm/WindowsAnsiProcessor.java rename to src/main/java/org/fusesource/jansi/internal/ffm/WindowsAnsiProcessor.java index e933ff0a..cc6789e9 100644 --- a/src/main/java/org/fusesource/jansi/ffm/WindowsAnsiProcessor.java +++ b/src/main/java/org/fusesource/jansi/internal/ffm/WindowsAnsiProcessor.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.fusesource.jansi.ffm; +package org.fusesource.jansi.internal.ffm; import java.io.IOException; import java.io.OutputStream; @@ -26,7 +26,7 @@ import org.fusesource.jansi.io.AnsiProcessor; import org.fusesource.jansi.io.Colors; -import static org.fusesource.jansi.ffm.Kernel32.*; +import static org.fusesource.jansi.internal.ffm.Kernel32.*; /** * A Windows ANSI escape processor, that uses JNA to access native platform diff --git a/src/main/java/org/fusesource/jansi/ffm/WindowsCLibrary.java b/src/main/java/org/fusesource/jansi/internal/ffm/WindowsCLibrary.java similarity index 97% rename from src/main/java/org/fusesource/jansi/ffm/WindowsCLibrary.java rename to src/main/java/org/fusesource/jansi/internal/ffm/WindowsCLibrary.java index c68854bf..2acfedb3 100644 --- a/src/main/java/org/fusesource/jansi/ffm/WindowsCLibrary.java +++ b/src/main/java/org/fusesource/jansi/internal/ffm/WindowsCLibrary.java @@ -13,14 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.fusesource.jansi.ffm; +package org.fusesource.jansi.internal.ffm; import java.lang.foreign.*; import java.lang.invoke.MethodHandle; import java.lang.invoke.VarHandle; import java.nio.charset.StandardCharsets; -import org.fusesource.jansi.AnsiConsoleSupport; +import org.fusesource.jansi.internal.AnsiConsoleSupport; import static java.lang.foreign.ValueLayout.*; diff --git a/src/main/java/org/fusesource/jansi/internal/AnsiConsoleSupportJni.java b/src/main/java/org/fusesource/jansi/internal/jni/AnsiConsoleSupportImpl.java similarity index 95% rename from src/main/java/org/fusesource/jansi/internal/AnsiConsoleSupportJni.java rename to src/main/java/org/fusesource/jansi/internal/jni/AnsiConsoleSupportImpl.java index 83f1a31e..ea3bc2fc 100644 --- a/src/main/java/org/fusesource/jansi/internal/AnsiConsoleSupportJni.java +++ b/src/main/java/org/fusesource/jansi/internal/jni/AnsiConsoleSupportImpl.java @@ -13,13 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.fusesource.jansi.internal; +package org.fusesource.jansi.internal.jni; import java.io.IOException; import java.io.OutputStream; import java.nio.charset.StandardCharsets; -import org.fusesource.jansi.AnsiConsoleSupport; +import org.fusesource.jansi.internal.AnsiConsoleSupport; import org.fusesource.jansi.io.AnsiProcessor; import org.fusesource.jansi.io.WindowsAnsiProcessor; @@ -33,7 +33,7 @@ import static org.fusesource.jansi.internal.Kernel32.STD_OUTPUT_HANDLE; import static org.fusesource.jansi.internal.Kernel32.SetConsoleMode; -public class AnsiConsoleSupportJni implements AnsiConsoleSupport { +public final class AnsiConsoleSupportImpl implements AnsiConsoleSupport { @Override public String getProviderName() { diff --git a/src/test/java/org/fusesource/jansi/AnsiTest.java b/src/test/java/org/fusesource/jansi/AnsiTest.java index 824c8d0d..332fb4b5 100644 --- a/src/test/java/org/fusesource/jansi/AnsiTest.java +++ b/src/test/java/org/fusesource/jansi/AnsiTest.java @@ -48,11 +48,7 @@ public void testClone() throws CloneNotSupportedException { @Test public void testApply() { - assertEquals( - "test", - Ansi.ansi() - .apply(ansi -> ansi.a("test")) - .toString()); + assertEquals("test", Ansi.ansi().apply(ansi -> ansi.a("test")).toString()); } @ParameterizedTest From 41c2ac608577e800bf9f68640ffdf07e9d52bca7 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Wed, 4 Oct 2023 00:21:49 +0200 Subject: [PATCH 10/15] Use verify goal in CI (#271) --- .github/workflows/build.yml | 2 +- pom.xml | 43 +++++++++++++++++++++++++------------ 2 files changed, 30 insertions(+), 15 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e1d5db2a..891f5f24 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -46,4 +46,4 @@ jobs: key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} restore-keys: ${{ runner.os }}-m2 - name: mvn verify - run: mvn test + run: mvn verify -Dnosign diff --git a/pom.xml b/pom.xml index 49bf9c81..8c7d919d 100644 --- a/pom.xml +++ b/pom.xml @@ -379,20 +379,6 @@ maven-release-plugin 3.0.0-M1 - - org.apache.maven.plugins - maven-gpg-plugin - 1.6 - - - sign-artifacts - - sign - - verify - - - org.sonatype.plugins nexus-staging-maven-plugin @@ -465,4 +451,33 @@ + + + sign + + + !nosign + + + + + + org.apache.maven.plugins + maven-gpg-plugin + 1.6 + + + sign-artifacts + + sign + + verify + + + + + + + + From 1e4edb57b1eb1211b5ca96b3f12ee5dab0195762 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Wed, 4 Oct 2023 07:48:29 +0200 Subject: [PATCH 11/15] Fix JVM crash when running java (fixes #216) (#265) --- .../java/org/fusesource/jansi/AnsiMain.java | 1 + src/main/native/jansi_isatty.c | 35 +++++++++-------- .../internal/native/Windows/arm64/libjansi.so | Bin 82432 -> 82432 bytes .../internal/native/Windows/x86/jansi.dll | Bin 115972 -> 115972 bytes .../internal/native/Windows/x86_64/jansi.dll | Bin 130522 -> 130522 bytes .../java/org/fusesource/jansi/AnsiTest.java | 36 ++++++++++++++++++ 6 files changed, 57 insertions(+), 15 deletions(-) diff --git a/src/main/java/org/fusesource/jansi/AnsiMain.java b/src/main/java/org/fusesource/jansi/AnsiMain.java index e824322d..dace3a4d 100644 --- a/src/main/java/org/fusesource/jansi/AnsiMain.java +++ b/src/main/java/org/fusesource/jansi/AnsiMain.java @@ -101,6 +101,7 @@ public static void main(String... args) throws IOException { System.out.println("java.version= " + System.getProperty("java.version") + ", " + "java.vendor= " + System.getProperty("java.vendor") + "," + " java.home= " + System.getProperty("java.home")); + System.out.println("Console: " + System.console()); System.out.println(); diff --git a/src/main/native/jansi_isatty.c b/src/main/native/jansi_isatty.c index 0fc7d348..807cbe65 100644 --- a/src/main/native/jansi_isatty.c +++ b/src/main/native/jansi_isatty.c @@ -57,8 +57,8 @@ JNIEXPORT jint JNICALL CLibrary_NATIVE(isatty) /* check if fd is a pipe */ HANDLE h = (HANDLE) _get_osfhandle(arg0); - DWORD t = GetFileType(h); - if (t == FILE_TYPE_CHAR) { + DWORD t = h != NULL ? GetFileType(h) : 0; + if (h != NULL && t == FILE_TYPE_CHAR) { // check that this is a real tty because the /dev/null // and /dev/zero streams are also of type FILE_TYPE_CHAR rc = GetConsoleMode(h, &mode) != 0; @@ -84,20 +84,25 @@ JNIEXPORT jint JNICALL CLibrary_NATIVE(isatty) else { name = nameinfo->Name.Buffer; - name[nameinfo->Name.Length / 2] = 0; - - //fprintf( stderr, "Standard stream %d: pipe name: %S\n", arg0, name); - - /* - * Check if this could be a MSYS2 pty pipe ('msys-XXXX-ptyN-XX') - * or a cygwin pty pipe ('cygwin-XXXX-ptyN-XX') - */ - if ((wcsstr(name, L"msys-") || wcsstr(name, L"cygwin-")) && wcsstr(name, L"-pty")) { - rc = 1; - } else { - // This is definitely not a tty - rc = 0; + if (name == NULL) { + rc = 0; } + else { + name[nameinfo->Name.Length / 2] = 0; + + //fprintf( stderr, "Standard stream %d: pipe name: %S\n", arg0, name); + + /* + * Check if this could be a MSYS2 pty pipe ('msys-XXXX-ptyN-XX') + * or a cygwin pty pipe ('cygwin-XXXX-ptyN-XX') + */ + if ((wcsstr(name, L"msys-") || wcsstr(name, L"cygwin-")) && wcsstr(name, L"-pty")) { + rc = 1; + } else { + // This is definitely not a tty + rc = 0; + } + } } } } diff --git a/src/main/resources/org/fusesource/jansi/internal/native/Windows/arm64/libjansi.so b/src/main/resources/org/fusesource/jansi/internal/native/Windows/arm64/libjansi.so index b7e3527be2c3c2265bc0627550936e0912d729f0..cce0178d3bf0e4baf2fdc7a421a9800a5b46f87a 100755 GIT binary patch delta 2948 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 diff --git a/src/main/resources/org/fusesource/jansi/internal/native/Windows/x86/jansi.dll b/src/main/resources/org/fusesource/jansi/internal/native/Windows/x86/jansi.dll index 298e99d272d065f1b47c1364b9876d54469159c2..8843d024ecd6698e57e83af2870fe985e48bf351 100755 GIT binary patch delta 1301 zcmY+@e{2(F7zgn8N(ZELv|$iKnFH23Hpb?AckkQYdq3V&HZfu*!WMzefud|#6C#67 zLDUXiH#%dc-kK(e%o%@?!2(>e#rk6Hoosb+E~Cvg?9g(JL#|r$^n!8+chkI)UF6`_87sZ3xIE(#?{cot zXkhmyBkrx{PMAM?=eoEf=9KT*`SS;3=}1xB889mS`P|pW^ZsJ)3*&wNw(al!J#*_; z>|!MGdh8~9svFIC`qOno&N2Xo$^*`Ad#~<_=LRxQ=L5iw@XcEIj#bI22!kMSNs6RL&Rgl^%Oa7q{y zMum)!B}!tQ*d`tmPl=z3!{VrTS^QP}Q~X!-pnFgODn=?QN0n#`YCw&s8O6}s=rB5t zQs`TB4gG~2cnS95Vl3jd_+h*SKZT#e5gfyB;3GKNjZfngK8r{2Mf?+<#&d$pf=X~x zaBt8En!)da?vNbX96Au{3Jr!PLmsI_!jdL!kZPrf^pI!l(Wi+@}qJ^xvtD8 zZgsKhR|{3FYU&2HR&7!f>T&g)I;GyHJ)|{hpRo1JXgb|R+i5R7PyeC!>J@s2-mQ<% zuHpdzs@N)W*%H>m_3%j;h6mwMco{nQrF<2ea*F?r=Y@?zg3a~^VS$)M#YXW1^O;6G znT1(0C)xc3SC*XE%(Xgp=Oy2(R*}>$BP2x=EthA=y zVbW=n9wtuqRo*@MGbS(EzQ~9tg@2{x5_Hb zOy(9^va@zl=(#Xzz5w`!`$wW0b S?wAmhC&OHR|C=>jq~Sk+Y(5qM delta 1260 zcmY+@ZEO@p7zgm#^YEI3!qFF_Egq$Jf~6eH?#|rp-0sXayofCXQK&T~(s)?qDphMw zQ^c~Qp_FSOG%m1d1f&6Kpb6rgv6gk^SSJ;~VwczC}w>y|TW>y7?g)hu^1EoU0*%Mfv)p~36-o1{UcTHcg zFh5=lKX!CP1JX$;BmE^odAV%J-SU3<6ZyD&QXY|ikbjr| zls#wy@}mGkNJ9(IV)PPXp zWxi6WtWuInzj9IWkP;#iLKc!|NsQEy)nqN%LM+llddcTxkenqMa*I3~eL7kbT^s!@ zIvmYYp}ItUPu->V80taws5+#cQ!l7j)$6KT8>ba&MOsM1+I+22tJgMbd$nWQkJ{su z(t5g^_R;G!!fIIy>tSE98!TU+r?=@{`kDKqC<9Jf| z0B-vM5I_-d@k;<$#3er2E-@|_9FpWRT4c|Bz-7}EyYzCo>yQC1Z}A8F;I0va)3AeX za+w;ihw`!kc*h|jE=Pj)Oa+&$sdlO5a@!&6xcoiMp4pXc01$i9PKSBuV}}giFLTHZ zE>}wIRXO7U=$&qtSzH=R?eY|t*bKWY8xIVS6SC7f9-1-JF59?VcF18aWBI>)(8dgx yHyq-g0KgfC6ms!P_9~rAZp;5PGf~@=;PYQ;ZL1c}xxSOFLv=!NU*|F*5&s8PPbvlg diff --git a/src/main/resources/org/fusesource/jansi/internal/native/Windows/x86_64/jansi.dll b/src/main/resources/org/fusesource/jansi/internal/native/Windows/x86_64/jansi.dll index 8649e32c88fed17481773595dfd386a621f1c7f0..aeec4e3a270b82ee210d8ffdcbe7bfe0a24bb6f8 100755 GIT binary patch delta 9148 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 delta 9152 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 diff --git a/src/test/java/org/fusesource/jansi/AnsiTest.java b/src/test/java/org/fusesource/jansi/AnsiTest.java index 332fb4b5..1fbd5bc3 100644 --- a/src/test/java/org/fusesource/jansi/AnsiTest.java +++ b/src/test/java/org/fusesource/jansi/AnsiTest.java @@ -15,8 +15,16 @@ */ package org.fusesource.jansi; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.nio.file.Path; +import java.nio.file.Paths; + import org.fusesource.jansi.Ansi.Color; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledOnOs; +import org.junit.jupiter.api.condition.OS; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; @@ -165,6 +173,34 @@ public void testColorDisabled() { } } + @Test + @EnabledOnOs(OS.WINDOWS) + @Disabled("Does not really fail: launch `javaw -jar jansi-xxx.jar` directly instead") + public void testAnsiMainWithNoConsole() throws Exception { + Path javaHome = Paths.get(System.getProperty("java.home")); + Path java = javaHome.resolve("bin\\javaw.exe"); + String cp = System.getProperty("java.class.path"); + + Process process = new ProcessBuilder() + .command(java.toString(), "-cp", cp, AnsiMain.class.getName()) + .start(); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (InputStream in = process.getInputStream()) { + byte[] buffer = new byte[8192]; + while (true) { + int nb = in.read(buffer); + if (nb > 0) { + baos.write(buffer, 0, nb); + } else { + break; + } + } + } + + assertTrue(baos.toString().contains("test on System.out"), baos.toString()); + } + private static void assertAnsi(String expected, Ansi actual) { assertEquals(expected.replace("ESC", "\033"), actual.toString()); } From 9d60bc56f4c5603ee6c5a76398d8b82640b8d891 Mon Sep 17 00:00:00 2001 From: Glavo Date: Wed, 4 Oct 2023 16:12:44 +0800 Subject: [PATCH 12/15] Support using FFM in native-image (#269) --- pom.xml | 8 ++ .../jansi/internal/AnsiConsoleSupport.java | 42 ++++++- .../internal/AnsiConsoleSupportHolder.java | 64 ++++------ .../jansi/internal/NativeImageFeature.java | 84 +++++++++++++ .../org/fusesource/jansi/internal/OSInfo.java | 117 ++++++++++-------- .../internal/ffm/AnsiConsoleSupportImpl.java | 19 ++- .../jansi/internal/ffm/Kernel32.java | 9 +- .../ffm/NativeImageDowncallRegister.java | 103 +++++++++++++++ .../jansi/internal/ffm/PosixCLibrary.java | 13 +- .../internal/jni/AnsiConsoleSupportImpl.java | 11 +- .../jansi/native-image.properties | 1 + .../native-image/jansi/resource-config.json | 3 +- 12 files changed, 350 insertions(+), 124 deletions(-) create mode 100644 src/main/java/org/fusesource/jansi/internal/NativeImageFeature.java create mode 100644 src/main/java/org/fusesource/jansi/internal/ffm/NativeImageDowncallRegister.java create mode 100644 src/main/resources/META-INF/native-image/jansi/native-image.properties diff --git a/pom.xml b/pom.xml index 8c7d919d..2c79c2db 100644 --- a/pom.xml +++ b/pom.xml @@ -109,6 +109,13 @@ + + org.graalvm.sdk + nativeimage + 23.1.0 + provided + true + org.junit.jupiter junit-jupiter @@ -260,6 +267,7 @@ 9 + org.fusesource.jansi.AnsiMain org.fusesource.jansi org.fusesource.jansi; diff --git a/src/main/java/org/fusesource/jansi/internal/AnsiConsoleSupport.java b/src/main/java/org/fusesource/jansi/internal/AnsiConsoleSupport.java index 4868207d..64087fa1 100644 --- a/src/main/java/org/fusesource/jansi/internal/AnsiConsoleSupport.java +++ b/src/main/java/org/fusesource/jansi/internal/AnsiConsoleSupport.java @@ -20,9 +20,9 @@ import org.fusesource.jansi.io.AnsiProcessor; -public interface AnsiConsoleSupport { +public abstract class AnsiConsoleSupport { - interface CLibrary { + public interface CLibrary { int STDOUT_FILENO = 1; int STDERR_FILENO = 2; @@ -32,7 +32,7 @@ interface CLibrary { int isTty(int fd); } - interface Kernel32 { + public interface Kernel32 { int isTty(long console); @@ -51,9 +51,39 @@ interface Kernel32 { AnsiProcessor newProcessor(OutputStream os, long console) throws IOException; } - String getProviderName(); + private final String providerName; + private CLibrary cLibrary; + private Kernel32 kernel32; - CLibrary getCLibrary(); + protected AnsiConsoleSupport(String providerName) { + this.providerName = providerName; + } + + public final String getProviderName() { + return providerName; + } + + protected abstract CLibrary createCLibrary(); + + protected abstract Kernel32 createKernel32(); + + public final CLibrary getCLibrary() { + if (cLibrary == null) { + cLibrary = createCLibrary(); + } - Kernel32 getKernel32(); + return cLibrary; + } + + public final Kernel32 getKernel32() { + if (kernel32 == null) { + if (!OSInfo.isWindows()) { + throw new RuntimeException("Kernel32 is not available on this platform"); + } + + kernel32 = createKernel32(); + } + + return kernel32; + } } diff --git a/src/main/java/org/fusesource/jansi/internal/AnsiConsoleSupportHolder.java b/src/main/java/org/fusesource/jansi/internal/AnsiConsoleSupportHolder.java index eb049f98..9e66e579 100644 --- a/src/main/java/org/fusesource/jansi/internal/AnsiConsoleSupportHolder.java +++ b/src/main/java/org/fusesource/jansi/internal/AnsiConsoleSupportHolder.java @@ -19,19 +19,20 @@ public final class AnsiConsoleSupportHolder { - private static final String PROVIDER_NAME; - private static final AnsiConsoleSupport.CLibrary CLIBRARY; - private static final AnsiConsoleSupport.Kernel32 KERNEL32; - private static final Throwable ERR; + static final AnsiConsoleSupport PROVIDER; + + static final Throwable ERR; private static AnsiConsoleSupport getDefaultProvider() { - try { - // Call the specialized constructor to check whether the module has native access enabled - // If not, fallback to JNI to avoid the JDK printing warnings in stderr - return (AnsiConsoleSupport) Class.forName("org.fusesource.jansi.internal.ffm.AnsiConsoleSupportImpl") - .getConstructor(boolean.class) - .newInstance(true); - } catch (Throwable ignored) { + if (!OSInfo.isInImageCode()) { + try { + // Call the specialized constructor to check whether the module has native access enabled + // If not, fallback to JNI to avoid the JDK printing warnings in stderr + return (AnsiConsoleSupport) Class.forName("org.fusesource.jansi.internal.ffm.AnsiConsoleSupportImpl") + .getConstructor(boolean.class) + .newInstance(true); + } catch (Throwable ignored) { + } } return new org.fusesource.jansi.internal.jni.AnsiConsoleSupportImpl(); @@ -81,47 +82,26 @@ private static AnsiConsoleSupport findProvider(String providerList) { err = e; } - String providerName = null; - AnsiConsoleSupport.CLibrary clib = null; - AnsiConsoleSupport.Kernel32 kernel32 = null; + PROVIDER = ansiConsoleSupport; + ERR = err; + } - if (ansiConsoleSupport != null) { - try { - providerName = ansiConsoleSupport.getProviderName(); - clib = ansiConsoleSupport.getCLibrary(); - kernel32 = OSInfo.isWindows() ? ansiConsoleSupport.getKernel32() : null; - } catch (Throwable e) { - err = e; - } + public static AnsiConsoleSupport getProvider() { + if (PROVIDER == null) { + throw new RuntimeException("No provider available", ERR); } - - PROVIDER_NAME = providerName; - CLIBRARY = clib; - KERNEL32 = kernel32; - ERR = err; + return PROVIDER; } public static String getProviderName() { - return PROVIDER_NAME; + return getProvider().getProviderName(); } public static AnsiConsoleSupport.CLibrary getCLibrary() { - if (CLIBRARY == null) { - throw new RuntimeException("Unable to get the instance of CLibrary", ERR); - } - - return CLIBRARY; + return getProvider().getCLibrary(); } public static AnsiConsoleSupport.Kernel32 getKernel32() { - if (KERNEL32 == null) { - if (OSInfo.isWindows()) { - throw new RuntimeException("Unable to get the instance of Kernel32", ERR); - } else { - throw new UnsupportedOperationException("Not Windows"); - } - } - - return KERNEL32; + return getProvider().getKernel32(); } } diff --git a/src/main/java/org/fusesource/jansi/internal/NativeImageFeature.java b/src/main/java/org/fusesource/jansi/internal/NativeImageFeature.java new file mode 100644 index 00000000..27063a11 --- /dev/null +++ b/src/main/java/org/fusesource/jansi/internal/NativeImageFeature.java @@ -0,0 +1,84 @@ +/* + * 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; + +import java.util.Objects; + +import org.fusesource.jansi.AnsiConsole; +import org.graalvm.nativeimage.hosted.Feature; +import org.graalvm.nativeimage.hosted.RuntimeClassInitialization; +import org.graalvm.nativeimage.hosted.RuntimeSystemProperties; + +public class NativeImageFeature implements Feature { + @Override + public String getURL() { + return "https://github.com/fusesource/jansi"; + } + + @Override + public void duringSetup(DuringSetupAccess access) { + RuntimeClassInitialization.initializeAtBuildTime(AnsiConsoleSupportHolder.class); + + String providers = System.getProperty(AnsiConsole.JANSI_PROVIDERS); + if (providers != null) { + try { + RuntimeSystemProperties.register(AnsiConsole.JANSI_PROVIDERS, providers); + } catch (Throwable ignored) { + // GraalVM version < 23.0 + // No need to worry as we select the provider at build time + } + } + + String provider = Objects.requireNonNull(AnsiConsoleSupportHolder.getProviderName(), "No provider available"); + if (provider.equals(AnsiConsole.JANSI_PROVIDER_JNI)) { + String jansiNativeLibraryName = System.mapLibraryName("jansi"); + if (jansiNativeLibraryName.endsWith(".dylib")) { + jansiNativeLibraryName = jansiNativeLibraryName.replace(".dylib", ".jnilib"); + } + + String packagePath = JansiLoader.class.getPackage().getName().replace('.', '/'); + + try { + Class moduleClass = Class.forName("java.lang.Module"); + Class rraClass = Class.forName("org.graalvm.nativeimage.hosted.RuntimeResourceAccess"); + + Object module = Class.class.getMethod("getModule").invoke(JansiLoader.class); + rraClass.getMethod("addResource", moduleClass, String.class) + .invoke( + null, + module, + String.format( + "%s/native/%s/%s", + packagePath, + OSInfo.getNativeLibFolderPathForCurrentOS(), + jansiNativeLibraryName)); + + } catch (Throwable ignored) { + // GraalVM version < 22.3 + // Users need to manually add the JNI library as resources + } + } else if (provider.equals(AnsiConsole.JANSI_PROVIDER_FFM)) { + try { + // FFM is only available in JDK 21+, so we need to compile it separately + Class.forName("org.fusesource.jansi.internal.ffm.NativeImageDowncallRegister") + .getMethod("registerForDowncall") + .invoke(null); + } catch (Throwable e) { + throw new RuntimeException(e); + } + } + } +} diff --git a/src/main/java/org/fusesource/jansi/internal/OSInfo.java b/src/main/java/org/fusesource/jansi/internal/OSInfo.java index 14b7b0ec..2d0ce235 100644 --- a/src/main/java/org/fusesource/jansi/internal/OSInfo.java +++ b/src/main/java/org/fusesource/jansi/internal/OSInfo.java @@ -36,12 +36,10 @@ import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Paths; -import java.util.HashMap; import java.util.Locale; /** * Provides OS name and architecture name. - * */ public class OSInfo { @@ -54,52 +52,6 @@ public class OSInfo { public static final String ARM64 = "arm64"; public static final String RISCV64 = "riscv64"; - private static final HashMap archMapping = new HashMap(); - - static { - // x86 mappings - archMapping.put(X86, X86); - archMapping.put("i386", X86); - archMapping.put("i486", X86); - archMapping.put("i586", X86); - archMapping.put("i686", X86); - archMapping.put("pentium", X86); - - // x86_64 mappings - archMapping.put(X86_64, X86_64); - archMapping.put("amd64", X86_64); - archMapping.put("em64t", X86_64); - archMapping.put("universal", X86_64); // Needed for openjdk7 in Mac - - // Itenium 64-bit mappings - archMapping.put(IA64, IA64); - archMapping.put("ia64w", IA64); - - // Itenium 32-bit mappings, usually an HP-UX construct - archMapping.put(IA64_32, IA64_32); - archMapping.put("ia64n", IA64_32); - - // PowerPC mappings - archMapping.put(PPC, PPC); - archMapping.put("power", PPC); - archMapping.put("powerpc", PPC); - archMapping.put("power_pc", PPC); - archMapping.put("power_rs", PPC); - - // TODO: PowerPC 64bit mappings - archMapping.put(PPC64, PPC64); - archMapping.put("power64", PPC64); - archMapping.put("powerpc64", PPC64); - archMapping.put("power_pc64", PPC64); - archMapping.put("power_rs64", PPC64); - - // aarch64 mappings - archMapping.put("aarch64", ARM64); - - // riscv64 mappings - archMapping.put(RISCV64, RISCV64); - } - public static void main(String[] args) { if (args.length >= 1) { if ("--os".equals(args[0])) { @@ -114,6 +66,63 @@ public static void main(String[] args) { System.out.print(getNativeLibFolderPathForCurrentOS()); } + private static String mapArchName(String arch) { + switch (arch.toLowerCase(Locale.ROOT)) { + // x86 mappings + case X86: + case "i386": + case "i486": + case "i586": + case "i686": + case "pentium": + return X86; + + // x86_64 mappings + case X86_64: + case "amd64": + case "em64t": + case "universal": // Needed for openjdk7 in Mac + return X86_64; + + // Itenium 64-bit mappings + case IA64: + case "ia64w": + return IA64; + + // Itenium 32-bit mappings, usually an HP-UX construct + case IA64_32: + case "ia64n": + return IA64_32; + + // PowerPC mappings + case PPC: + case "power": + case "powerpc": + case "power_pc": + case "power_rs": + return PPC; + + // TODO: PowerPC 64bit mappings + case PPC64: + case "power64": + case "powerpc64": + case "power_pc64": + case "power_rs64": + return PPC64; + + // aarch64 mappings + case "aarch64": + return ARM64; + + // riscv64 mappings + case RISCV64: + return RISCV64; + + default: + return null; + } + } + public static String getNativeLibFolderPathForCurrentOS() { return getOSName() + "/" + getArchName(); } @@ -145,6 +154,10 @@ public static boolean isAlpine() { return false; } + public static boolean isInImageCode() { + return System.getProperty("org.graalvm.nativeimage.imagecode") != null; + } + static String getHardwareName() { try { Process p = Runtime.getRuntime().exec("uname -m"); @@ -169,7 +182,7 @@ private static String readFully(InputStream in) throws IOException { while ((readLen = in.read(buf, 0, buf.length)) >= 0) { b.write(buf, 0, readLen); } - return b.toString(); + return b.toString("UTF-8"); } static String resolveArmArchType() { @@ -211,8 +224,10 @@ public static String getArchName() { if (osArch.startsWith("arm")) { osArch = resolveArmArchType(); } else { - String lc = osArch.toLowerCase(Locale.ROOT); - if (archMapping.containsKey(lc)) return archMapping.get(lc); + String arch = mapArchName(osArch); + if (arch != null) { + return arch; + } } return translateArchNameToFolderName(osArch); } 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 bc252b41..b36bc692 100644 --- a/src/main/java/org/fusesource/jansi/internal/ffm/AnsiConsoleSupportImpl.java +++ b/src/main/java/org/fusesource/jansi/internal/ffm/AnsiConsoleSupportImpl.java @@ -27,23 +27,22 @@ import static org.fusesource.jansi.internal.ffm.Kernel32.*; -public final class AnsiConsoleSupportImpl implements AnsiConsoleSupport { +public final class AnsiConsoleSupportImpl extends AnsiConsoleSupport { - public AnsiConsoleSupportImpl() {} + public AnsiConsoleSupportImpl() { + super("ffm"); + } public AnsiConsoleSupportImpl(boolean checkNativeAccess) { + this(); if (checkNativeAccess && !AnsiConsoleSupportImpl.class.getModule().isNativeAccessEnabled()) { - throw new UnsupportedOperationException("Native access is not enabled for the current module"); + throw new UnsupportedOperationException( + "Native access is not enabled for the current module: " + AnsiConsoleSupportImpl.class.getModule()); } } @Override - public String getProviderName() { - return "ffm"; - } - - @Override - public CLibrary getCLibrary() { + protected CLibrary createCLibrary() { if (OSInfo.isWindows()) { return new WindowsCLibrary(); } else { @@ -52,7 +51,7 @@ public CLibrary getCLibrary() { } @Override - public Kernel32 getKernel32() { + protected Kernel32 createKernel32() { return new Kernel32() { @Override public int isTty(long console) { diff --git a/src/main/java/org/fusesource/jansi/internal/ffm/Kernel32.java b/src/main/java/org/fusesource/jansi/internal/ffm/Kernel32.java index a657e096..bed95e19 100644 --- a/src/main/java/org/fusesource/jansi/internal/ffm/Kernel32.java +++ b/src/main/java/org/fusesource/jansi/internal/ffm/Kernel32.java @@ -244,7 +244,7 @@ public static int ScrollConsoleScreenBuffer( SMALL_RECT lpClipRectangle, COORD dwDestinationOrigin, CHAR_INFO lpFill) { - MethodHandle mh$ = requireNonNull(ScrollConsoleScreenBuffer$MH, "ScrollConsoleScreenBuffer"); + MethodHandle mh$ = requireNonNull(ScrollConsoleScreenBufferW$MH, "ScrollConsoleScreenBuffer"); try { return (int) mh$.invokeExact(hConsoleOutput, lpScrollRectangle, lpClipRectangle, dwDestinationOrigin, lpFill); @@ -318,8 +318,9 @@ public static String getErrorMessage(int errorCode) { private static final SymbolLookup SYMBOL_LOOKUP; static { + System.loadLibrary("msvcrt"); System.loadLibrary("Kernel32"); - SYMBOL_LOOKUP = SymbolLookup.loaderLookup().or(Linker.nativeLinker().defaultLookup()); + SYMBOL_LOOKUP = SymbolLookup.loaderLookup(); } static MethodHandle downcallHandle(String name, FunctionDescriptor fdesc) { @@ -396,8 +397,8 @@ static MethodHandle downcallHandle(String name, FunctionDescriptor fdesc) { static final MethodHandle GetConsoleScreenBufferInfo$MH = downcallHandle( "GetConsoleScreenBufferInfo", FunctionDescriptor.of(C_INT$LAYOUT, C_POINTER$LAYOUT, C_POINTER$LAYOUT)); - static final MethodHandle ScrollConsoleScreenBuffer$MH = downcallHandle( - "ScrollConsoleScreenBuffer", + static final MethodHandle ScrollConsoleScreenBufferW$MH = downcallHandle( + "ScrollConsoleScreenBufferW", FunctionDescriptor.of( C_INT$LAYOUT, C_POINTER$LAYOUT, diff --git a/src/main/java/org/fusesource/jansi/internal/ffm/NativeImageDowncallRegister.java b/src/main/java/org/fusesource/jansi/internal/ffm/NativeImageDowncallRegister.java new file mode 100644 index 00000000..3052b40f --- /dev/null +++ b/src/main/java/org/fusesource/jansi/internal/ffm/NativeImageDowncallRegister.java @@ -0,0 +1,103 @@ +/* + * 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.ffm; + +import java.lang.foreign.AddressLayout; +import java.lang.foreign.FunctionDescriptor; +import java.lang.foreign.MemoryLayout; +import java.lang.foreign.StructLayout; +import java.lang.foreign.ValueLayout; + +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.hosted.RuntimeForeignAccess; + +import static java.lang.foreign.ValueLayout.*; + +public final class NativeImageDowncallRegister { + + private static void registerForDowncall(MemoryLayout resLayout, MemoryLayout... argLayouts) { + RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.of(resLayout, argLayouts)); + } + + public static void registerForDowncall() { + if (Platform.includedIn(Platform.WINDOWS.class)) { + final OfShort C_SHORT$LAYOUT = JAVA_SHORT; + final OfInt C_INT$LAYOUT = JAVA_INT; + final AddressLayout C_POINTER$LAYOUT = ADDRESS; + + StructLayout COORD$LAYOUT = + MemoryLayout.structLayout(C_SHORT$LAYOUT.withName("x"), C_SHORT$LAYOUT.withName("y")); + + // WaitForSingleObject + registerForDowncall(C_INT$LAYOUT, C_POINTER$LAYOUT, C_INT$LAYOUT); + // GetStdHandle + registerForDowncall(C_POINTER$LAYOUT, C_INT$LAYOUT); + // FormatMessageW + registerForDowncall( + C_INT$LAYOUT, + C_INT$LAYOUT, + C_POINTER$LAYOUT, + C_INT$LAYOUT, + C_INT$LAYOUT, + C_POINTER$LAYOUT, + C_INT$LAYOUT, + C_POINTER$LAYOUT); + // SetConsoleTextAttribute + registerForDowncall(C_INT$LAYOUT, C_POINTER$LAYOUT, C_SHORT$LAYOUT); + // SetConsoleMode + registerForDowncall(C_INT$LAYOUT, C_POINTER$LAYOUT, C_INT$LAYOUT); + // GetConsoleMode + registerForDowncall(C_INT$LAYOUT, C_POINTER$LAYOUT, C_POINTER$LAYOUT); + // SetConsoleTitleW + registerForDowncall(C_INT$LAYOUT, C_POINTER$LAYOUT); + // SetConsoleCursorPosition + registerForDowncall(C_INT$LAYOUT, C_POINTER$LAYOUT, COORD$LAYOUT); + // FillConsoleOutputCharacterW + registerForDowncall( + C_INT$LAYOUT, C_POINTER$LAYOUT, C_SHORT$LAYOUT, C_INT$LAYOUT, COORD$LAYOUT, C_POINTER$LAYOUT); + // FillConsoleOutputAttribute + registerForDowncall( + C_INT$LAYOUT, C_POINTER$LAYOUT, C_SHORT$LAYOUT, C_INT$LAYOUT, COORD$LAYOUT, C_POINTER$LAYOUT); + // WriteConsoleW + registerForDowncall( + C_INT$LAYOUT, C_POINTER$LAYOUT, C_POINTER$LAYOUT, C_INT$LAYOUT, C_POINTER$LAYOUT, C_POINTER$LAYOUT); + // ReadConsoleInputW + registerForDowncall(C_INT$LAYOUT, C_POINTER$LAYOUT, C_POINTER$LAYOUT, C_INT$LAYOUT, C_POINTER$LAYOUT); + // PeekConsoleInputW + registerForDowncall(C_INT$LAYOUT, C_POINTER$LAYOUT, C_POINTER$LAYOUT, C_INT$LAYOUT, C_POINTER$LAYOUT); + // GetConsoleScreenBufferInfo + registerForDowncall(C_INT$LAYOUT, C_POINTER$LAYOUT, C_POINTER$LAYOUT); + // ScrollConsoleScreenBuffer + registerForDowncall( + C_INT$LAYOUT, C_POINTER$LAYOUT, C_POINTER$LAYOUT, C_POINTER$LAYOUT, COORD$LAYOUT, C_POINTER$LAYOUT); + // GetLastError + registerForDowncall(C_INT$LAYOUT); + // GetFileType + registerForDowncall(C_INT$LAYOUT, C_POINTER$LAYOUT); + // _get_osfhandle + registerForDowncall(C_POINTER$LAYOUT, C_INT$LAYOUT); + // NtQueryObject + registerForDowncall(JAVA_INT, ADDRESS, JAVA_INT, ADDRESS, JAVA_LONG, ADDRESS); + } else if (Platform.includedIn(Platform.LINUX.class) || Platform.includedIn(Platform.DARWIN.class)) { + // ioctl + registerForDowncall(ValueLayout.JAVA_INT, ValueLayout.JAVA_INT, ValueLayout.JAVA_LONG, ValueLayout.ADDRESS); + // isatty + registerForDowncall(ValueLayout.JAVA_INT, ValueLayout.JAVA_INT); + } else { + throw new UnsupportedOperationException("Unsupported platform"); + } + } +} diff --git a/src/main/java/org/fusesource/jansi/internal/ffm/PosixCLibrary.java b/src/main/java/org/fusesource/jansi/internal/ffm/PosixCLibrary.java index 30e959b9..817e03e4 100644 --- a/src/main/java/org/fusesource/jansi/internal/ffm/PosixCLibrary.java +++ b/src/main/java/org/fusesource/jansi/internal/ffm/PosixCLibrary.java @@ -20,6 +20,7 @@ import java.lang.invoke.VarHandle; import org.fusesource.jansi.internal.AnsiConsoleSupport; +import org.fusesource.jansi.internal.OSInfo; final class PosixCLibrary implements AnsiConsoleSupport.CLibrary { private static final int TIOCGWINSZ; @@ -52,14 +53,20 @@ final class PosixCLibrary implements AnsiConsoleSupport.CLibrary { ValueLayout.JAVA_SHORT); ws_col = wsLayout.varHandle(MemoryLayout.PathElement.groupElement("ws_col")); Linker linker = Linker.nativeLinker(); + SymbolLookup lookup; + if (OSInfo.isInImageCode()) { + lookup = SymbolLookup.loaderLookup(); + } else { + lookup = linker.defaultLookup(); + } + 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)); isatty = linker.downcallHandle( - linker.defaultLookup().find("isatty").get(), - FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.JAVA_INT)); + lookup.find("isatty").get(), FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.JAVA_INT)); } @Override 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 ea3bc2fc..e3ae4d67 100644 --- a/src/main/java/org/fusesource/jansi/internal/jni/AnsiConsoleSupportImpl.java +++ b/src/main/java/org/fusesource/jansi/internal/jni/AnsiConsoleSupportImpl.java @@ -33,15 +33,14 @@ import static org.fusesource.jansi.internal.Kernel32.STD_OUTPUT_HANDLE; import static org.fusesource.jansi.internal.Kernel32.SetConsoleMode; -public final class AnsiConsoleSupportImpl implements AnsiConsoleSupport { +public final class AnsiConsoleSupportImpl extends AnsiConsoleSupport { - @Override - public String getProviderName() { - return "jni"; + public AnsiConsoleSupportImpl() { + super("jni"); } @Override - public CLibrary getCLibrary() { + protected CLibrary createCLibrary() { return new CLibrary() { @Override public short getTerminalWidth(int fd) { @@ -56,7 +55,7 @@ public int isTty(int fd) { } @Override - public Kernel32 getKernel32() { + protected Kernel32 createKernel32() { return new Kernel32() { @Override public int isTty(long console) { diff --git a/src/main/resources/META-INF/native-image/jansi/native-image.properties b/src/main/resources/META-INF/native-image/jansi/native-image.properties new file mode 100644 index 00000000..6cc8f4e4 --- /dev/null +++ b/src/main/resources/META-INF/native-image/jansi/native-image.properties @@ -0,0 +1 @@ +Args=--features=org.fusesource.jansi.internal.NativeImageFeature \ No newline at end of file diff --git a/src/main/resources/META-INF/native-image/jansi/resource-config.json b/src/main/resources/META-INF/native-image/jansi/resource-config.json index e062c81e..794d8996 100644 --- a/src/main/resources/META-INF/native-image/jansi/resource-config.json +++ b/src/main/resources/META-INF/native-image/jansi/resource-config.json @@ -1,7 +1,6 @@ { "resources": [ {"pattern": "org/fusesource/jansi/jansi.properties"}, - {"pattern": "org/fusesource/jansi/jansi.txt"}, - {"pattern": "org/fusesource/jansi/internal/native/.*"} + {"pattern": "org/fusesource/jansi/jansi.txt"} ] } \ No newline at end of file From d38ecc3816729b5a648d38b533f7f2629e30f4c5 Mon Sep 17 00:00:00 2001 From: Glavo Date: Sat, 7 Oct 2023 00:16:27 +0800 Subject: [PATCH 13/15] fix --- Makefile | 4 ++-- Makefile.common | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index cb5c1049..dfb87bff 100644 --- a/Makefile +++ b/Makefile @@ -79,8 +79,8 @@ NATIVE_DLL:=$(NATIVE_DIR)/$(LIBNAME) # For cross-compilation, install docker. See also https://github.com/dockcross/dockcross # Disabled linux-armv6 build because of this issue; https://github.com/dockcross/dockcross/issues/190 -native-all: linux-x86 linux-x86_64 linux-arm linux-armv6 linux-armv7 \ - linux-arm64 linux-ppc64 win-x86 win-x86_64 win-arm64 mac-x86 mac-x86_64 mac-arm64 freebsd-x86 freebsd-x86_64 +native-all: linux-x86 linux-x86_64 linux-arm linux-armv6 linux-armv7 linux-arm64 linux-ppc64 linux-riscv64 \ + win-x86 win-x86_64 win-arm64 mac-x86 mac-x86_64 mac-arm64 freebsd-x86 freebsd-x86_64 native: $(NATIVE_DLL) diff --git a/Makefile.common b/Makefile.common index 5f24f473..9aa8b928 100644 --- a/Makefile.common +++ b/Makefile.common @@ -14,7 +14,7 @@ # os=Default is meant to be generic unix/linux -known_targets := Linux-x86 Linux-x86_64 Linux-arm Linux-armv6 Linux-armv7 Linux-android-arm Linux-ppc64 Mac-x86 Mac-x86_64 Mac-arm64 DragonFly-x86_64 FreeBSD-x86_64 OpenBSD-x86_64 Windows-x86 Windows-x86_64 SunOS-sparcv9 HPUX-ia64_32 +known_targets := Linux-x86 Linux-x86_64 Linux-arm Linux-armv6 Linux-armv7 Linux-android-arm Linux-arm64 Linux-ppc64 Linux-riscv64 Mac-x86 Mac-x86_64 Mac-arm64 DragonFly-x86_64 FreeBSD-x86_64 OpenBSD-x86_64 Windows-x86 Windows-x86_64 SunOS-sparcv9 HPUX-ia64_32 target := $(OS_NAME)-$(OS_ARCH) ifeq (,$(findstring $(strip $(target)),$(known_targets))) @@ -68,7 +68,7 @@ Linux-armv7_JANSI_FLAGS := Linux-arm64_CC := $(CROSS_PREFIX)gcc Linux-arm64_STRIP := $(CROSS_PREFIX)strip -Linux-arm64_CCFLAGS := -I$(JAVA_HOME)/include -Itarget/inc -Itarget/inc/unix -Os -mfloat-abi=hard -mfpu=vfp -fPIC -fvisibility=hidden +Linux-arm64_CCFLAGS := -I$(JAVA_HOME)/include -Itarget/inc -Itarget/inc/unix -Os -march=armv8-a -fPIC -fvisibility=hidden Linux-arm64_LINKFLAGS := -shared -static-libgcc Linux-arm64_LIBNAME := libjansi.so Linux-arm64_JANSI_FLAGS := @@ -82,7 +82,7 @@ Linux-ppc64_JANSI_FLAGS := Linux-riscv64_CC := $(CROSS_PREFIX)gcc Linux-riscv64_STRIP := $(CROSS_PREFIX)strip -Linux-riscv64_CCFLAGS := -I$(JAVA_HOME)/include -Itarget/inc -Itarget/inc/unix -Os -fPIC -fvisibility=hidden +Linux-riscv64_CCFLAGS := -I$(JAVA_HOME)/include -Itarget/inc -Itarget/inc/unix -Os -march=rv64g -fPIC -fvisibility=hidden Linux-riscv64_LINKFLAGS := -shared -static-libgcc Linux-riscv64_LIBNAME := libjansi.so Linux-riscv64_JANSI_FLAGS := From 4681720ddf1812a38440e112a2d050fafa9e75d9 Mon Sep 17 00:00:00 2001 From: Glavo Date: Sat, 7 Oct 2023 16:54:49 +0800 Subject: [PATCH 14/15] rv64i --- Makefile.common | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile.common b/Makefile.common index 9aa8b928..8983caa6 100644 --- a/Makefile.common +++ b/Makefile.common @@ -82,7 +82,7 @@ Linux-ppc64_JANSI_FLAGS := Linux-riscv64_CC := $(CROSS_PREFIX)gcc Linux-riscv64_STRIP := $(CROSS_PREFIX)strip -Linux-riscv64_CCFLAGS := -I$(JAVA_HOME)/include -Itarget/inc -Itarget/inc/unix -Os -march=rv64g -fPIC -fvisibility=hidden +Linux-riscv64_CCFLAGS := -I$(JAVA_HOME)/include -Itarget/inc -Itarget/inc/unix -Os -march=rv64i -fPIC -fvisibility=hidden Linux-riscv64_LINKFLAGS := -shared -static-libgcc Linux-riscv64_LIBNAME := libjansi.so Linux-riscv64_JANSI_FLAGS := From 302257733582ec2b156115a0bf3e7dfc64329bd4 Mon Sep 17 00:00:00 2001 From: Glavo Date: Sat, 7 Oct 2023 17:01:31 +0800 Subject: [PATCH 15/15] rv64g --- Makefile.common | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile.common b/Makefile.common index 8983caa6..9aa8b928 100644 --- a/Makefile.common +++ b/Makefile.common @@ -82,7 +82,7 @@ Linux-ppc64_JANSI_FLAGS := Linux-riscv64_CC := $(CROSS_PREFIX)gcc Linux-riscv64_STRIP := $(CROSS_PREFIX)strip -Linux-riscv64_CCFLAGS := -I$(JAVA_HOME)/include -Itarget/inc -Itarget/inc/unix -Os -march=rv64i -fPIC -fvisibility=hidden +Linux-riscv64_CCFLAGS := -I$(JAVA_HOME)/include -Itarget/inc -Itarget/inc/unix -Os -march=rv64g -fPIC -fvisibility=hidden Linux-riscv64_LINKFLAGS := -shared -static-libgcc Linux-riscv64_LIBNAME := libjansi.so Linux-riscv64_JANSI_FLAGS :=