More work in progress.
[tripe-android] / jni.c
diff --git a/jni.c b/jni.c
index f65e464..9aa79f8 100644 (file)
--- a/jni.c
+++ b/jni.c
@@ -39,6 +39,7 @@
 #include <jni.h>
 
 #include <sys/types.h>
+#include <sys/select.h>
 #include <sys/socket.h>
 #include <sys/stat.h>
 #include <sys/sysmacros.h>
@@ -192,6 +193,31 @@ static void except_syserror(JNIEnv *jni, const char *clsname,
   va_end(ap);
 }
 
+static int set_nonblocking(JNIEnv *jni, int fd, int nb)
+{
+  int f0 = fcntl(fd, F_GETFL), f1;
+  if (f0 < 0) goto err;
+  if (nb) f1 = f0 | O_NONBLOCK;
+  else f1 = f0&~O_NONBLOCK;
+  if (fcntl(fd, F_SETFL, f1)) goto err;
+  return (f0 & O_NONBLOCK);
+err:
+    except_syserror(jni, SYSERR, errno,
+                   "failed to set descriptor nonblocking");
+    return (-1);
+}
+
+static int set_closeonexec(JNIEnv *jni, int fd)
+{
+  int f = fcntl(fd, F_GETFD);
+  if (f < 0 || fcntl(fd, F_SETFD, f | FD_CLOEXEC)) {
+    except_syserror(jni, SYSERR, errno,
+                   "failed to set descriptor close-on-exec");
+    return (-1);
+  }
+  return (0);
+}
+
 /*----- Wrapping native types ---------------------------------------------*/
 
 /* There's no way defined in the JNI to stash a C pointer in a Java object.
@@ -808,7 +834,7 @@ JNIEXPORT jboolean JNIFUNC(isatty)(JNIEnv *jni, jobject cls, jobject jfd)
 
 /* Java has these already, as methods on `java.io.File' objects.  Alas, these
  * methods are useless at reporting errors: they tend to return a `boolean'
- * success/ fail indicator, and throw away any more detailed information.
+ * success/fail indicator, and throw away any more detailed information.
  * There's better functionality in `java.nio.file.Files', but that only turns
  * up in Android API 26 (in 7.0 Nougat).  There's `android.system.Os', which
  * has a bunch of POSIX-shaped functions -- but they're only in Android API
@@ -1102,6 +1128,96 @@ end:
   put_cstring(jni, path, pathstr);
 }
 
+/*----- Triggers ----------------------------------------------------------*/
+
+/* A trigger is a gadget for waking up a thread which is blocking on I/O,
+ * and it's used to implement interruptability.
+ *
+ * Really, a trigger is a pipe.  A `blocking' I/O operation secretly uses
+ * select(2) to block on the descriptor of interest /and/ the read side of
+ * the trigger pipe.  To wake up a thread that's blocked, we just write a
+ * byte (nobody cares /which/ byte) to the write end.
+ */
+
+struct trigger {
+  struct native_base _base;
+  int rfd, wfd;
+};
+static const struct native_type trigger_type =
+       { "trigger", sizeof(struct trigger), 0x65ffd8b4 };
+
+JNIEXPORT wrapper JNICALL JNIFUNC(makeTrigger)(JNIEnv *jni, jobject cls)
+{
+  struct trigger trig;
+  int fd[2];
+  int i;
+  wrapper ret = 0;
+
+  fd[0] = fd[1] = -1;
+  if (pipe(fd)) {
+    except_syserror(jni, SYSERR, errno, "failed to create pipe");
+    goto end;
+  }
+  for (i = 0; i < 2; i++) {
+    if (set_nonblocking(jni, fd[i], 1) < 0 || set_closeonexec(jni, fd[i]))
+      goto end;
+  }
+
+  INIT_NATIVE(trigger, &trig);
+  trig.rfd = fd[0]; fd[0] = -1;
+  trig.wfd = fd[1]; fd[1] = -1;
+  ret = wrap(jni, &trigger_type, &trig);
+
+end:
+  for (i = 0; i < 2; i++)
+    if (fd[i] != -1) close(fd[i]);
+  return (ret);
+}
+
+JNIEXPORT void JNICALL JNIFUNC(destroyTrigger)(JNIEnv *jni, jobject cls,
+                                              wrapper wtrig)
+{
+  struct trigger trig;
+
+  if (unwrap(jni, &trig, &trigger_type, wtrig)) return;
+  if (trig.rfd != -1) { close(trig.rfd); trig.rfd = -1; }
+  if (trig.wfd != -1) { close(trig.wfd); trig.wfd = -1; }
+  update_wrapper(jni, &trigger_type, wtrig, &trig);
+}
+
+JNIEXPORT void JNICALL JNIFUNC(resetTrigger)(JNIEnv *jni, jobject cls,
+                                            wrapper wtrig)
+{
+  struct trigger trig;
+  char buf[64];
+  ssize_t n;
+
+  if (unwrap(jni, &trig, &trigger_type, wtrig)) return;
+  for (;;) {
+    n = read(trig.rfd, buf, sizeof(buf));
+    if (n > 0) continue;
+    assert(n < 0);
+    if (errno == EAGAIN || errno == EWOULDBLOCK) break;
+    else {
+      except_syserror(jni, SYSERR, errno, "failed to reset trigger");
+      break;
+    }
+  }
+}
+
+JNIEXPORT void JNICALL JNIFUNC(trigger)(JNIEnv *jni, jobject cls,
+                                       wrapper wtrig)
+{
+  struct trigger trig;
+  ssize_t n;
+  char c = 0;
+
+  if (unwrap(jni, &trig, &trigger_type, wtrig)) return;
+  n = write(trig.wfd, &c, 1);
+  if (n < 0 && errno != EAGAIN && errno != EWOULDBLOCK)
+    except_syserror(jni, SYSERR, errno, "failed to pull trigger");
+}
+
 /*----- A server connection, using a Unix-domain socket -------------------*/
 
 struct conn {
@@ -1116,14 +1232,21 @@ static const struct native_type conn_type =
        { "conn", sizeof(struct conn), 0xed030167 };
 
 JNIEXPORT wrapper JNICALL JNIFUNC(connect)(JNIEnv *jni, jobject cls,
-                                          jobject path)
+                                          jobject path, wrapper wtrig)
 {
   struct conn conn;
+  struct trigger trig;
   struct sockaddr_un sun;
+  int rc, maxfd;
+  fd_set rfds, wfds;
   const char *pathstr = 0;
-  jobject ret = 0;
+  int err;
+  socklen_t sz;
+  wrapper ret = 0;
+  int nb;
   int fd = -1;
 
+  if (unwrap(jni, &trig, &trigger_type, wtrig)) goto end;
   pathstr = get_cstring(jni, path); if (!pathstr) goto end;
   if (strlen(pathstr) >= sizeof(sun.sun_path)) {
     except(jni, ARGERR,
@@ -1132,12 +1255,33 @@ JNIEXPORT wrapper JNICALL JNIFUNC(connect)(JNIEnv *jni, jobject cls,
   }
 
   INIT_NATIVE(conn, &conn);
-  fd = socket(SOCK_STREAM, PF_UNIX, 0); if (fd < 0) goto err;
+  fd = socket(PF_UNIX, SOCK_STREAM, 0); if (fd < 0) goto err;
+  nb = set_nonblocking(jni, fd, 1); if (nb < 0) goto end;
 
   sun.sun_family = AF_UNIX;
   strcpy(sun.sun_path, (char *)pathstr);
-  if (connect(fd, (struct sockaddr *)&sun, sizeof(sun))) goto err;
+  if (!connect(fd, (struct sockaddr *)&sun, sizeof(sun))) goto connected;
+  else if (errno != EINPROGRESS) goto err;
+
+  maxfd = trig.rfd;
+  if (maxfd < fd) maxfd = fd;
+  for (;;) {
+    FD_ZERO(&rfds); FD_SET(trig.rfd, &rfds);
+    FD_ZERO(&wfds); FD_SET(fd, &wfds);
+    rc = select(maxfd + 1, &rfds, &wfds, 0, 0); if (rc < 0) goto err;
+    if (FD_ISSET(trig.rfd, &rfds)) goto end;
+    if (FD_ISSET(fd, &wfds)) {
+      sz = sizeof(sun);
+      if (!getpeername(fd, (struct sockaddr *)&sun, &sz)) goto connected;
+      else if (errno != ENOTCONN) goto err;
+      sz = sizeof(err);
+      if (!getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &sz)) errno = err;
+      goto err;
+    }
+  }
 
+connected:
+  if (set_nonblocking(jni, fd, nb) < 0) goto end;
   conn.fd = fd; fd = -1;
   conn.f = 0;
   ret = wrap(jni, &conn_type, &conn);
@@ -1147,7 +1291,7 @@ err:
   except_syserror(jni, SYSERR, errno,
                  "failed to connect to Unix-domain socket `%s'", pathstr);
 end:
-  if (fd == -1) close(fd);
+  if (fd != -1) close(fd);
   put_cstring(jni, path, pathstr);
   return (ret);
 }
@@ -1181,28 +1325,40 @@ static int check_buffer_bounds(JNIEnv *jni, const char *what,
 
 JNIEXPORT void JNICALL JNIFUNC(send)(JNIEnv *jni, jobject cls,
                                     wrapper wconn, jbyteArray buf,
-                                    jint start, jint len)
+                                    jint start, jint len,
+                                    wrapper wtrig)
 {
   struct conn conn;
+  struct trigger trig;
+  int rc, maxfd;
   ssize_t n;
+  fd_set rfds, wfds;
   jbyte *p = 0;
 
   if (unwrap(jni, &conn, &conn_type, wconn)) goto end;
+  if (unwrap(jni, &trig, &trigger_type, wtrig)) goto end;
   if (check_buffer_bounds(jni, "send", buf, start, len)) goto end;
 
   p = (*jni)->GetByteArrayElements(jni, buf, 0);
   if (!p) goto end;
 
+  maxfd = trig.rfd;
+  if (maxfd < conn.fd) maxfd = conn.fd;
   while (len) {
-    n = send(conn.fd, p + start, len, 0);
-    if (n < 0) {
-      except_syserror(jni, SYSERR,
-                     errno, "failed to send on connection");
-      goto end;
+    FD_ZERO(&rfds); FD_SET(trig.rfd, &rfds);
+    FD_ZERO(&wfds); FD_SET(conn.fd, &wfds);
+    rc = select(maxfd + 1, &rfds, &wfds, 0, 0); if (rc < 0) goto err;
+    if (FD_ISSET(trig.rfd, &rfds)) break;
+    if (FD_ISSET(conn.fd, &wfds)) {
+      n = send(conn.fd, p + start, len, 0);
+      if (n >= 0) { start += n; len -= n; }
+      else if (errno != EAGAIN && errno != EWOULDBLOCK) goto err;
     }
-    start += n; len -= n;
   }
+  goto end;
 
+err:
+  except_syserror(jni, SYSERR, errno, "failed to send on connection");
 end:
   if (p) (*jni)->ReleaseByteArrayElements(jni, buf, p, JNI_ABORT);
   return;
@@ -1210,26 +1366,42 @@ end:
 
 JNIEXPORT jint JNICALL JNIFUNC(recv)(JNIEnv *jni, jobject cls,
                                     wrapper wconn, jbyteArray buf,
-                                    jint start, jint len)
+                                    jint start, jint len,
+                                    wrapper wtrig)
 {
   struct conn conn;
+  struct trigger trig;
+  int maxfd;
+  fd_set rfds;
   jbyte *p = 0;
   jint rc = -1;
 
   if (unwrap(jni, &conn, &conn_type, wconn)) goto end;
+  if (unwrap(jni, &trig, &trigger_type, wtrig)) goto end;
   if (check_buffer_bounds(jni, "send", buf, start, len)) goto end;
 
   p = (*jni)->GetByteArrayElements(jni, buf, 0);
   if (!p) goto end;
 
-  rc = recv(conn.fd, p + start, len, 0);
-  if (rc < 0) {
-    except_syserror(jni, SYSERR,
-                   errno, "failed to read from connection");
-    goto end;
+  maxfd = trig.rfd;
+  if (maxfd < conn.fd) maxfd = conn.fd;
+  for (;;) {
+    FD_ZERO(&rfds); FD_SET(trig.rfd, &rfds); FD_SET(conn.fd, &rfds);
+    rc = select(maxfd + 1, &rfds, 0, 0, 0); if (rc < 0) goto err;
+    if (FD_ISSET(trig.rfd, &rfds)) {
+      break;
+    }
+    if (FD_ISSET(conn.fd, &rfds)) {
+      rc = recv(conn.fd, p + start, len, 0);
+      if (rc >= 0) break;
+      else if (errno != EAGAIN && errno != EWOULDBLOCK) goto err;
+    }
   }
   if (!rc) rc = -1;
+  goto end;
 
+err:
+  except_syserror(jni, SYSERR, errno, "failed to read from connection");
 end:
   if (p) (*jni)->ReleaseByteArrayElements(jni, buf, p, 0);
   return (rc);