From 27a7bb5b3d1c2a7ada1e491bd9e033264c574b46 Mon Sep 17 00:00:00 2001 From: Glavo Date: Fri, 29 Sep 2023 17:10:04 +0800 Subject: [PATCH] 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);