get rid of pty4j

This commit is contained in:
Gregor Lohaus
2026-05-29 11:06:46 +02:00
parent c9fb8b5f0a
commit 08ad025f76
6 changed files with 463 additions and 49 deletions

View File

@@ -16,7 +16,6 @@ repositories {
dependencies { dependencies {
implementation 'io.github.wasabithumb:jtoml:1.5.2' implementation 'io.github.wasabithumb:jtoml:1.5.2'
implementation 'dev.jlibghostty:jlibghostty:0.1.0-SNAPSHOT' implementation 'dev.jlibghostty:jlibghostty:0.1.0-SNAPSHOT'
implementation 'org.jetbrains.pty4j:pty4j:0.13.11'
} }
javafx { javafx {
@@ -26,10 +25,10 @@ javafx {
gluonfx { gluonfx {
compilerArgs += [ compilerArgs += [
'--features=com.gregor.jprototerm.PtyForeignRegistrationFeature',
'-H:+UnlockExperimentalVMOptions', '-H:+UnlockExperimentalVMOptions',
'-H:+ForeignAPISupport', '-H:+ForeignAPISupport',
'--enable-native-access=ALL-UNNAMED', '--enable-native-access=ALL-UNNAMED',
'-H:IncludeResources=com/sun/jna/.*|com/pty4j/native/.*|pty4j-native/.*',
"-H:ConfigurationFileDirectories=${file('src/main/resources/META-INF/native-image/com.gregor/jprototerm').absolutePath}".toString() "-H:ConfigurationFileDirectories=${file('src/main/resources/META-INF/native-image/com.gregor/jprototerm').absolutePath}".toString()
] ]
} }

View File

@@ -0,0 +1,404 @@
package com.gregor.jprototerm;
import java.lang.foreign.AddressLayout;
import java.lang.foreign.Arena;
import java.lang.foreign.FunctionDescriptor;
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.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* A Linux PTY backed by libc via the Foreign Function & Memory API.
*
* <p>This replaces pty4j (which loads a JNA JNI shim that does not initialise under a
* GraalVM native image). It uses {@code posix_openpt}/{@code posix_spawnp} rather than
* {@code fork}/{@code forkpty}: doing work between {@code fork} and {@code exec} inside a
* multithreaded JVM is unsafe (only async-signal-safe calls are permitted), whereas
* {@code posix_spawn} performs the dangerous part in libc with no Java on the stack.
*
* <p>The child gets a fresh session via {@code POSIX_SPAWN_SETSID}; it then opens the slave
* PTY itself (as fd 0, without {@code O_NOCTTY}) so the slave becomes its controlling
* terminal. glibc applies attribute flags (the setsid) before file actions, so the open
* happens in the new session.
*
* <p>FFM downcall descriptors are registered for native image by
* {@link PtyForeignRegistrationFeature}; keep the two in sync.
*/
public final class LinuxPty implements AutoCloseable {
static final Linker LINKER = Linker.nativeLinker();
private static final SymbolLookup LIBC = LINKER.defaultLookup();
static final AddressLayout C_POINTER = (AddressLayout) LINKER.canonicalLayouts().get("void*");
static final ValueLayout.OfShort C_SHORT = (ValueLayout.OfShort) LINKER.canonicalLayouts().get("short");
static final ValueLayout.OfInt C_INT = (ValueLayout.OfInt) LINKER.canonicalLayouts().get("int");
static final ValueLayout.OfLong C_LONG = (ValueLayout.OfLong) LINKER.canonicalLayouts().get("long");
static final ValueLayout.OfLong C_SIZE_T = (ValueLayout.OfLong) LINKER.canonicalLayouts().get("size_t");
// Function descriptors. Mirrored in PtyForeignRegistrationFeature.
static final FunctionDescriptor FD_INT_INT = FunctionDescriptor.of(C_INT, C_INT);
static final FunctionDescriptor FD_PTSNAME_R = FunctionDescriptor.of(C_INT, C_INT, C_POINTER, C_SIZE_T);
static final FunctionDescriptor FD_RW = FunctionDescriptor.of(C_LONG, C_INT, C_POINTER, C_SIZE_T);
static final FunctionDescriptor FD_IOCTL = FunctionDescriptor.of(C_INT, C_INT, C_LONG, C_POINTER);
static final FunctionDescriptor FD_KILL = FunctionDescriptor.of(C_INT, C_INT, C_INT);
static final FunctionDescriptor FD_WAITPID = FunctionDescriptor.of(C_INT, C_INT, C_POINTER, C_INT);
static final FunctionDescriptor FD_SPAWN = FunctionDescriptor.of(
C_INT, C_POINTER, C_POINTER, C_POINTER, C_POINTER, C_POINTER, C_POINTER);
static final FunctionDescriptor FD_FA_INIT = FunctionDescriptor.of(C_INT, C_POINTER);
static final FunctionDescriptor FD_FA_ADDCLOSE = FunctionDescriptor.of(C_INT, C_POINTER, C_INT);
static final FunctionDescriptor FD_FA_ADDDUP2 = FunctionDescriptor.of(C_INT, C_POINTER, C_INT, C_INT);
static final FunctionDescriptor FD_FA_ADDOPEN =
FunctionDescriptor.of(C_INT, C_POINTER, C_INT, C_POINTER, C_INT, C_INT);
static final FunctionDescriptor FD_FA_ADDCHDIR = FunctionDescriptor.of(C_INT, C_POINTER, C_POINTER);
static final FunctionDescriptor FD_ATTR_SETFLAGS = FunctionDescriptor.of(C_INT, C_POINTER, C_SHORT);
// Linux constants (x86-64 / arm64).
private static final int O_RDWR = 0x0002;
private static final int O_NOCTTY = 0x0100;
private static final long TIOCSWINSZ = 0x5414L;
private static final short POSIX_SPAWN_SETSID = 0x80;
private static final int SIGHUP = 1;
private static final int SIGKILL = 9;
private static final int WNOHANG = 1;
// struct winsize { unsigned short ws_row, ws_col, ws_xpixel, ws_ypixel; }
private static final MemoryLayout WINSIZE = MemoryLayout.structLayout(
C_SHORT.withName("ws_row"),
C_SHORT.withName("ws_col"),
C_SHORT.withName("ws_xpixel"),
C_SHORT.withName("ws_ypixel"));
// posix_spawn_file_actions_t / posix_spawnattr_t are opaque; over-allocate generously.
private static final long SPAWN_ACTIONS_SIZE = 256;
private static final long SPAWN_ATTR_SIZE = 512;
private static final MethodHandle POSIX_OPENPT = handle("posix_openpt", FD_INT_INT);
private static final MethodHandle GRANTPT = handle("grantpt", FD_INT_INT);
private static final MethodHandle UNLOCKPT = handle("unlockpt", FD_INT_INT);
private static final MethodHandle PTSNAME_R = handle("ptsname_r", FD_PTSNAME_R);
private static final MethodHandle CLOSE = handle("close", FD_INT_INT);
private static final MethodHandle READ = handle("read", FD_RW);
private static final MethodHandle WRITE = handle("write", FD_RW);
private static final MethodHandle IOCTL = handle("ioctl", FD_IOCTL, Linker.Option.firstVariadicArg(2));
private static final MethodHandle KILL = handle("kill", FD_KILL);
private static final MethodHandle WAITPID = handle("waitpid", FD_WAITPID);
private static final MethodHandle POSIX_SPAWNP = handle("posix_spawnp", FD_SPAWN);
private static final MethodHandle FA_INIT = handle("posix_spawn_file_actions_init", FD_FA_INIT);
private static final MethodHandle FA_DESTROY = handle("posix_spawn_file_actions_destroy", FD_FA_INIT);
private static final MethodHandle FA_ADDCLOSE = handle("posix_spawn_file_actions_addclose", FD_FA_ADDCLOSE);
private static final MethodHandle FA_ADDDUP2 = handle("posix_spawn_file_actions_adddup2", FD_FA_ADDDUP2);
private static final MethodHandle FA_ADDOPEN = handle("posix_spawn_file_actions_addopen", FD_FA_ADDOPEN);
private static final MethodHandle FA_ADDCHDIR = handle("posix_spawn_file_actions_addchdir_np", FD_FA_ADDCHDIR);
private static final MethodHandle ATTR_INIT = handle("posix_spawnattr_init", FD_FA_INIT);
private static final MethodHandle ATTR_DESTROY = handle("posix_spawnattr_destroy", FD_FA_INIT);
private static final MethodHandle ATTR_SETFLAGS = handle("posix_spawnattr_setflags", FD_ATTR_SETFLAGS);
private final Arena arena = Arena.ofShared();
private final MemorySegment readBuffer = arena.allocate(65536);
private final Object writeLock = new Object();
private final int masterFd;
private final int pid;
private volatile boolean closed;
private LinuxPty(int masterFd, int pid) {
this.masterFd = masterFd;
this.pid = pid;
}
/**
* Opens a PTY and spawns {@code argv} attached to its slave end.
*
* @param argv command and arguments (e.g. {@code {"/bin/zsh", "-i"}})
* @param environment environment for the child, as KEY=VALUE pairs
* @param workingDirectory directory the child starts in, or {@code null} to inherit
*/
public static LinuxPty spawn(String[] argv, Map<String, String> environment, String workingDirectory) {
Arena setup = Arena.ofConfined();
try {
int master = check(callInt(POSIX_OPENPT, O_RDWR | O_NOCTTY), "posix_openpt");
try {
check(callInt(GRANTPT, master), "grantpt");
check(callInt(UNLOCKPT, master), "unlockpt");
MemorySegment nameBuf = setup.allocate(256);
check(callPtsnameR(master, nameBuf), "ptsname_r");
String slavePath = nameBuf.getString(0);
MemorySegment actions = setup.allocate(SPAWN_ACTIONS_SIZE);
MemorySegment attr = setup.allocate(SPAWN_ATTR_SIZE);
check(callInt(FA_INIT, actions), "posix_spawn_file_actions_init");
check(callInt(ATTR_INIT, attr), "posix_spawnattr_init");
try {
check(callInt(ATTR_SETFLAGS, attr, POSIX_SPAWN_SETSID), "posix_spawnattr_setflags");
if (workingDirectory != null) {
MemorySegment dir = setup.allocateFrom(workingDirectory);
check(callAddChdir(actions, dir), "posix_spawn_file_actions_addchdir_np");
}
// Open the slave as fd 0 in the new session -> controlling terminal, then fan out.
MemorySegment slave = setup.allocateFrom(slavePath);
check(callAddOpen(actions, 0, slave, O_RDWR, 0), "posix_spawn_file_actions_addopen");
check(callAddDup2(actions, 0, 1), "posix_spawn_file_actions_adddup2");
check(callAddDup2(actions, 0, 2), "posix_spawn_file_actions_adddup2");
check(callAddClose(actions, master), "posix_spawn_file_actions_addclose");
MemorySegment argvSeg = cStringArray(setup, List.of(argv));
MemorySegment envpSeg = cStringArray(setup, toEnvList(environment));
MemorySegment path = setup.allocateFrom(argv[0]);
MemorySegment pidOut = setup.allocate(C_INT);
int rc = callSpawn(pidOut, path, actions, attr, argvSeg, envpSeg);
if (rc != 0) {
throw new IllegalStateException("posix_spawnp failed for " + argv[0] + " (rc=" + rc + ")");
}
return new LinuxPty(master, pidOut.get(C_INT, 0));
} finally {
callInt(ATTR_DESTROY, attr);
callInt(FA_DESTROY, actions);
}
} catch (RuntimeException ex) {
callInt(CLOSE, master);
throw ex;
}
} finally {
setup.close();
}
}
/** Reads available output into {@code dst}; returns bytes read, or -1 at EOF. */
public int read(byte[] dst) {
if (closed) {
return -1;
}
long n = callLong(READ, masterFd, readBuffer, Math.min(dst.length, readBuffer.byteSize()));
if (n <= 0) {
return -1;
}
MemorySegment.copy(readBuffer, ValueLayout.JAVA_BYTE, 0, dst, 0, (int) n);
return (int) n;
}
/** Writes all of {@code data} to the master end. */
public void write(byte[] data) {
if (closed || data.length == 0) {
return;
}
synchronized (writeLock) {
try (Arena a = Arena.ofConfined()) {
MemorySegment buf = a.allocate(data.length);
MemorySegment.copy(data, 0, buf, ValueLayout.JAVA_BYTE, 0, data.length);
long offset = 0;
while (offset < data.length) {
long n = callLong(WRITE, masterFd, buf.asSlice(offset), data.length - offset);
if (n < 0) {
throw new IllegalStateException("write to pty failed");
}
offset += n;
}
}
}
}
/** Resizes the terminal window. */
public void setWinSize(int columns, int rows) {
if (closed) {
return;
}
try (Arena a = Arena.ofConfined()) {
MemorySegment ws = a.allocate(WINSIZE);
ws.set(C_SHORT, 0, (short) rows);
ws.set(C_SHORT, 2, (short) columns);
ws.set(C_SHORT, 4, (short) 0);
ws.set(C_SHORT, 6, (short) 0);
callIoctl(masterFd, TIOCSWINSZ, ws);
}
}
@Override
public void close() {
if (closed) {
return;
}
closed = true;
callKill(pid, SIGHUP);
callInt(CLOSE, masterFd);
reap();
arena.close();
}
private void reap() {
try (Arena a = Arena.ofConfined()) {
MemorySegment status = a.allocate(C_INT);
// Closing the master sends EOF/SIGHUP; an interactive shell exits promptly.
for (int attempt = 0; attempt < 50; attempt++) {
int r = callWaitpid(pid, status, WNOHANG);
if (r != 0) {
return; // reaped, or no such child
}
if (attempt == 25) {
callKill(pid, SIGKILL);
}
try {
Thread.sleep(2);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
return;
}
}
}
}
// --- typed invokeExact wrappers ---------------------------------------------------------
private static int callInt(MethodHandle handle, int arg) {
try {
return (int) handle.invokeExact(arg);
} catch (Throwable t) {
throw sneaky(t);
}
}
private static int callInt(MethodHandle handle, MemorySegment arg) {
try {
return (int) handle.invokeExact(arg);
} catch (Throwable t) {
throw sneaky(t);
}
}
private static int callInt(MethodHandle handle, MemorySegment a, short b) {
try {
return (int) handle.invokeExact(a, b);
} catch (Throwable t) {
throw sneaky(t);
}
}
private static long callLong(MethodHandle handle, int fd, MemorySegment buf, long len) {
try {
return (long) handle.invokeExact(fd, buf, len);
} catch (Throwable t) {
throw sneaky(t);
}
}
private static int callPtsnameR(int fd, MemorySegment buf) {
try {
return (int) PTSNAME_R.invokeExact(fd, buf, buf.byteSize());
} catch (Throwable t) {
throw sneaky(t);
}
}
private static int callAddChdir(MemorySegment actions, MemorySegment path) {
try {
return (int) FA_ADDCHDIR.invokeExact(actions, path);
} catch (Throwable t) {
throw sneaky(t);
}
}
private static int callAddOpen(MemorySegment actions, int fd, MemorySegment path, int oflag, int mode) {
try {
return (int) FA_ADDOPEN.invokeExact(actions, fd, path, oflag, mode);
} catch (Throwable t) {
throw sneaky(t);
}
}
private static int callAddDup2(MemorySegment actions, int fd, int newFd) {
try {
return (int) FA_ADDDUP2.invokeExact(actions, fd, newFd);
} catch (Throwable t) {
throw sneaky(t);
}
}
private static int callAddClose(MemorySegment actions, int fd) {
try {
return (int) FA_ADDCLOSE.invokeExact(actions, fd);
} catch (Throwable t) {
throw sneaky(t);
}
}
private static int callSpawn(MemorySegment pid, MemorySegment path, MemorySegment actions,
MemorySegment attr, MemorySegment argv, MemorySegment envp) {
try {
return (int) POSIX_SPAWNP.invokeExact(pid, path, actions, attr, argv, envp);
} catch (Throwable t) {
throw sneaky(t);
}
}
private static void callIoctl(int fd, long request, MemorySegment arg) {
try {
int unused = (int) IOCTL.invokeExact(fd, request, arg);
} catch (Throwable t) {
throw sneaky(t);
}
}
private static void callKill(int pid, int signal) {
try {
int unused = (int) KILL.invokeExact(pid, signal);
} catch (Throwable t) {
throw sneaky(t);
}
}
private static int callWaitpid(int pid, MemorySegment status, int options) {
try {
return (int) WAITPID.invokeExact(pid, status, options);
} catch (Throwable t) {
throw sneaky(t);
}
}
// --- helpers ----------------------------------------------------------------------------
private static MethodHandle handle(String symbol, FunctionDescriptor descriptor, Linker.Option... options) {
MemorySegment address = LIBC.find(symbol)
.orElseThrow(() -> new IllegalStateException("libc symbol not found: " + symbol));
return LINKER.downcallHandle(address, descriptor, options);
}
private static MemorySegment cStringArray(Arena arena, List<String> values) {
MemorySegment array = arena.allocate(C_POINTER, values.size() + 1L);
for (int i = 0; i < values.size(); i++) {
array.setAtIndex(C_POINTER, i, arena.allocateFrom(values.get(i)));
}
array.setAtIndex(C_POINTER, values.size(), MemorySegment.NULL);
return array;
}
private static List<String> toEnvList(Map<String, String> environment) {
List<String> out = new ArrayList<>(environment.size());
for (Map.Entry<String, String> entry : environment.entrySet()) {
out.add(entry.getKey() + "=" + entry.getValue());
}
return out;
}
private static int check(int rc, String what) {
if (rc < 0) {
throw new IllegalStateException(what + " failed (rc=" + rc + ")");
}
return rc;
}
private static RuntimeException sneaky(Throwable t) {
if (t instanceof RuntimeException re) {
return re;
}
if (t instanceof Error e) {
throw e;
}
return new IllegalStateException(t);
}
}

View File

@@ -0,0 +1,39 @@
package com.gregor.jprototerm;
import org.graalvm.nativeimage.hosted.Feature;
import org.graalvm.nativeimage.hosted.RuntimeForeignAccess;
import java.lang.foreign.Linker;
/**
* Registers the FFM downcall descriptors used by {@link LinuxPty} for GraalVM releases that
* do not consume {@code foreign.downcalls} from reachability-metadata.json. Mirrors
* jlibghostty's {@code GhosttyForeignRegistrationFeature}.
*
* <p>Wired in via {@code --features=com.gregor.jprototerm.PtyForeignRegistrationFeature}
* in the gluonfx compiler args.
*/
public final class PtyForeignRegistrationFeature implements Feature {
@Override
public void duringSetup(DuringSetupAccess access) {
downcall(LinuxPty.FD_INT_INT); // posix_openpt / grantpt / unlockpt / close
downcall(LinuxPty.FD_PTSNAME_R); // ptsname_r
downcall(LinuxPty.FD_RW); // read / write
downcall(LinuxPty.FD_KILL); // kill
downcall(LinuxPty.FD_WAITPID); // waitpid
downcall(LinuxPty.FD_SPAWN); // posix_spawnp
downcall(LinuxPty.FD_FA_INIT); // *_init / *_destroy
downcall(LinuxPty.FD_FA_ADDCLOSE); // posix_spawn_file_actions_addclose
downcall(LinuxPty.FD_FA_ADDDUP2); // posix_spawn_file_actions_adddup2
downcall(LinuxPty.FD_FA_ADDOPEN); // posix_spawn_file_actions_addopen
downcall(LinuxPty.FD_FA_ADDCHDIR); // posix_spawn_file_actions_addchdir_np
downcall(LinuxPty.FD_ATTR_SETFLAGS); // posix_spawnattr_setflags
// ioctl(int, unsigned long, ...) is variadic; register with the same linker option.
RuntimeForeignAccess.registerForDowncall(LinuxPty.FD_IOCTL, Linker.Option.firstVariadicArg(2));
}
private static void downcall(java.lang.foreign.FunctionDescriptor descriptor) {
RuntimeForeignAccess.registerForDowncall(descriptor);
}
}

View File

@@ -1,12 +1,7 @@
package com.gregor.jprototerm; package com.gregor.jprototerm;
import com.pty4j.PtyProcess;
import com.pty4j.PtyProcessBuilder;
import com.pty4j.WinSize;
import javafx.application.Platform; import javafx.application.Platform;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
@@ -14,14 +9,12 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
public final class ShellSession implements AutoCloseable { public final class ShellSession implements AutoCloseable {
private final PtyProcess process; private final LinuxPty pty;
private final OutputStream stdin;
private final ExecutorService reader; private final ExecutorService reader;
private volatile boolean closed; private volatile boolean closed;
private ShellSession(PtyProcess process) { private ShellSession(LinuxPty pty) {
this.process = process; this.pty = pty;
this.stdin = process.getOutputStream();
this.reader = Executors.newSingleThreadExecutor(runnable -> { this.reader = Executors.newSingleThreadExecutor(runnable -> {
Thread thread = new Thread(runnable, "shell-output-reader"); Thread thread = new Thread(runnable, "shell-output-reader");
thread.setDaemon(true); thread.setDaemon(true);
@@ -36,14 +29,14 @@ public final class ShellSession implements AutoCloseable {
environment.put("COLORTERM", "truecolor"); environment.put("COLORTERM", "truecolor");
environment.putAll(envOverride); environment.putAll(envOverride);
PtyProcess process = new PtyProcessBuilder(new String[] {shell, "-i"}) LinuxPty pty = LinuxPty.spawn(
.setEnvironment(environment) new String[] {shell, "-i"},
.setInitialColumns(columns) environment,
.setInitialRows(rows) System.getProperty("user.home"));
.setDirectory(System.getProperty("user.home")) ShellSession session = new ShellSession(pty);
.start(); session.resize(columns, rows);
return new ShellSession(process); return session;
} catch (IOException ex) { } catch (RuntimeException ex) {
pane.write("failed to start shell: " + ex.getMessage() + "\r\n"); pane.write("failed to start shell: " + ex.getMessage() + "\r\n");
throw new IllegalStateException("Could not start shell " + shell, ex); throw new IllegalStateException("Could not start shell " + shell, ex);
} }
@@ -57,7 +50,7 @@ public final class ShellSession implements AutoCloseable {
if (closed) { if (closed) {
return; return;
} }
process.setWinSize(new WinSize(columns, rows)); pty.setWinSize(columns, rows);
} }
public void send(String text) { public void send(String text) {
@@ -69,9 +62,8 @@ public final class ShellSession implements AutoCloseable {
return; return;
} }
try { try {
stdin.write(bytes); pty.write(bytes);
stdin.flush(); } catch (RuntimeException ex) {
} catch (IOException ex) {
close(); close();
} }
} }
@@ -80,7 +72,7 @@ public final class ShellSession implements AutoCloseable {
byte[] buffer = new byte[8192]; byte[] buffer = new byte[8192];
try { try {
int read; int read;
while ((read = process.getInputStream().read(buffer)) != -1) { while ((read = pty.read(buffer)) != -1) {
if (!closed) { if (!closed) {
byte[] bytes = new byte[read]; byte[] bytes = new byte[read];
System.arraycopy(buffer, 0, bytes, 0, read); System.arraycopy(buffer, 0, bytes, 0, read);
@@ -91,7 +83,7 @@ public final class ShellSession implements AutoCloseable {
}); });
} }
} }
} catch (IOException ex) { } catch (RuntimeException ex) {
if (!closed) { if (!closed) {
Platform.runLater(() -> pane.write("\r\nshell output stopped: " + ex.getMessage() + "\r\n")); Platform.runLater(() -> pane.write("\r\nshell output stopped: " + ex.getMessage() + "\r\n"));
} }
@@ -102,6 +94,6 @@ public final class ShellSession implements AutoCloseable {
public void close() { public void close() {
closed = true; closed = true;
reader.shutdownNow(); reader.shutdownNow();
process.destroy(); pty.close();
} }
} }

View File

@@ -1,13 +1,3 @@
{ {
"resources": [ "resources": []
{
"glob": "com/sun/jna/**"
},
{
"glob": "com/pty4j/native/**"
},
{
"glob": "pty4j-native/**"
}
]
} }

View File

@@ -1,15 +1,5 @@
{ {
"resources": { "resources": {
"includes": [ "includes": []
{
"pattern": "com/sun/jna/.*"
},
{
"pattern": "com/pty4j/native/.*"
},
{
"pattern": "pty4j-native/.*"
}
]
} }
} }