diff --git a/build.gradle b/build.gradle index 4f4672e..9e4581f 100644 --- a/build.gradle +++ b/build.gradle @@ -16,7 +16,6 @@ repositories { dependencies { implementation 'io.github.wasabithumb:jtoml:1.5.2' implementation 'dev.jlibghostty:jlibghostty:0.1.0-SNAPSHOT' - implementation 'org.jetbrains.pty4j:pty4j:0.13.11' } javafx { @@ -26,10 +25,10 @@ javafx { gluonfx { compilerArgs += [ + '--features=com.gregor.jprototerm.PtyForeignRegistrationFeature', '-H:+UnlockExperimentalVMOptions', '-H:+ForeignAPISupport', '--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() ] } diff --git a/src/main/java/com/gregor/jprototerm/LinuxPty.java b/src/main/java/com/gregor/jprototerm/LinuxPty.java new file mode 100644 index 0000000..a871af0 --- /dev/null +++ b/src/main/java/com/gregor/jprototerm/LinuxPty.java @@ -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. + * + *
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. + * + *
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. + * + *
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 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);
+ }
+}
diff --git a/src/main/java/com/gregor/jprototerm/ShellSession.java b/src/main/java/com/gregor/jprototerm/ShellSession.java
index 60d988c..d787383 100644
--- a/src/main/java/com/gregor/jprototerm/ShellSession.java
+++ b/src/main/java/com/gregor/jprototerm/ShellSession.java
@@ -1,12 +1,7 @@
package com.gregor.jprototerm;
-import com.pty4j.PtyProcess;
-import com.pty4j.PtyProcessBuilder;
-import com.pty4j.WinSize;
import javafx.application.Platform;
-import java.io.IOException;
-import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
@@ -14,14 +9,12 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public final class ShellSession implements AutoCloseable {
- private final PtyProcess process;
- private final OutputStream stdin;
+ private final LinuxPty pty;
private final ExecutorService reader;
private volatile boolean closed;
- private ShellSession(PtyProcess process) {
- this.process = process;
- this.stdin = process.getOutputStream();
+ private ShellSession(LinuxPty pty) {
+ this.pty = pty;
this.reader = Executors.newSingleThreadExecutor(runnable -> {
Thread thread = new Thread(runnable, "shell-output-reader");
thread.setDaemon(true);
@@ -36,14 +29,14 @@ public final class ShellSession implements AutoCloseable {
environment.put("COLORTERM", "truecolor");
environment.putAll(envOverride);
- PtyProcess process = new PtyProcessBuilder(new String[] {shell, "-i"})
- .setEnvironment(environment)
- .setInitialColumns(columns)
- .setInitialRows(rows)
- .setDirectory(System.getProperty("user.home"))
- .start();
- return new ShellSession(process);
- } catch (IOException ex) {
+ LinuxPty pty = LinuxPty.spawn(
+ new String[] {shell, "-i"},
+ environment,
+ System.getProperty("user.home"));
+ ShellSession session = new ShellSession(pty);
+ session.resize(columns, rows);
+ return session;
+ } catch (RuntimeException ex) {
pane.write("failed to start shell: " + ex.getMessage() + "\r\n");
throw new IllegalStateException("Could not start shell " + shell, ex);
}
@@ -57,7 +50,7 @@ public final class ShellSession implements AutoCloseable {
if (closed) {
return;
}
- process.setWinSize(new WinSize(columns, rows));
+ pty.setWinSize(columns, rows);
}
public void send(String text) {
@@ -69,9 +62,8 @@ public final class ShellSession implements AutoCloseable {
return;
}
try {
- stdin.write(bytes);
- stdin.flush();
- } catch (IOException ex) {
+ pty.write(bytes);
+ } catch (RuntimeException ex) {
close();
}
}
@@ -80,7 +72,7 @@ public final class ShellSession implements AutoCloseable {
byte[] buffer = new byte[8192];
try {
int read;
- while ((read = process.getInputStream().read(buffer)) != -1) {
+ while ((read = pty.read(buffer)) != -1) {
if (!closed) {
byte[] bytes = new byte[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) {
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() {
closed = true;
reader.shutdownNow();
- process.destroy();
+ pty.close();
}
}
diff --git a/src/main/resources/META-INF/native-image/com.gregor/jprototerm/reachability-metadata.json b/src/main/resources/META-INF/native-image/com.gregor/jprototerm/reachability-metadata.json
index 7c23cb4..d02ef58 100644
--- a/src/main/resources/META-INF/native-image/com.gregor/jprototerm/reachability-metadata.json
+++ b/src/main/resources/META-INF/native-image/com.gregor/jprototerm/reachability-metadata.json
@@ -1,13 +1,3 @@
{
- "resources": [
- {
- "glob": "com/sun/jna/**"
- },
- {
- "glob": "com/pty4j/native/**"
- },
- {
- "glob": "pty4j-native/**"
- }
- ]
+ "resources": []
}
diff --git a/src/main/resources/META-INF/native-image/com.gregor/jprototerm/resource-config.json b/src/main/resources/META-INF/native-image/com.gregor/jprototerm/resource-config.json
index 0af655a..bf2f940 100644
--- a/src/main/resources/META-INF/native-image/com.gregor/jprototerm/resource-config.json
+++ b/src/main/resources/META-INF/native-image/com.gregor/jprototerm/resource-config.json
@@ -1,15 +1,5 @@
{
"resources": {
- "includes": [
- {
- "pattern": "com/sun/jna/.*"
- },
- {
- "pattern": "com/pty4j/native/.*"
- },
- {
- "pattern": "pty4j-native/.*"
- }
- ]
+ "includes": []
}
}