From d8bc14a8e0846ad77736b8aaaffe5e662e1daa40 Mon Sep 17 00:00:00 2001
From: Dave Brown <dave@moonspider.com>
Date: Wed, 1 Feb 2017 16:42:05 -0800
Subject: [PATCH] (enhancement) Expose dup2() from libc, Daemon optionally
 redirect streams

Allow user to redirect stdout, stderr after fork() instead of closing them.
Expose dup() and dup2() in CLibrary to accomplish the above. Add dup2()
unit test.
---
 pom.xml                                       |  9 +++
 src/main/java/com/sun/akuma/CLibrary.java     |  2 +
 src/main/java/com/sun/akuma/Daemon.java       | 56 ++++++++++++++--
 .../java/com/sun/akuma/test/Dup2Test.java     | 66 +++++++++++++++++++
 4 files changed, 129 insertions(+), 4 deletions(-)
 create mode 100644 src/test/java/com/sun/akuma/test/Dup2Test.java

diff --git a/pom.xml b/pom.xml
index 5c510cb..8a90dba 100644
--- a/pom.xml
+++ b/pom.xml
@@ -25,6 +25,15 @@
 
   <build>
     <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-compiler-plugin</artifactId>
+        <version>3.6.1</version>
+        <configuration>
+          <source>1.6</source>
+          <target>1.6</target>
+        </configuration>
+      </plugin>
       <plugin><!-- create uberjar for easy testing -->
         <artifactId>maven-assembly-plugin</artifactId>
         <executions>
diff --git a/src/main/java/com/sun/akuma/CLibrary.java b/src/main/java/com/sun/akuma/CLibrary.java
index e6463b2..e39e63c 100644
--- a/src/main/java/com/sun/akuma/CLibrary.java
+++ b/src/main/java/com/sun/akuma/CLibrary.java
@@ -50,6 +50,8 @@ public interface CLibrary extends Library {
     int unsetenv(String name);
     void perror(String msg);
     String strerror(int errno);
+    int dup(int fd);
+    int dup2(int newfd, int oldfd);
 
     // this is listed in http://developer.apple.com/DOCUMENTATION/Darwin/Reference/ManPages/man3/sysctlbyname.3.html
     // but not in http://www.gnu.org/software/libc/manual/html_node/System-Parameters.html#index-sysctl-3493
diff --git a/src/main/java/com/sun/akuma/Daemon.java b/src/main/java/com/sun/akuma/Daemon.java
index 1d01d1a..f09b882 100644
--- a/src/main/java/com/sun/akuma/Daemon.java
+++ b/src/main/java/com/sun/akuma/Daemon.java
@@ -29,9 +29,8 @@
 import com.sun.jna.StringArray;
 import static com.sun.akuma.CLibrary.LIBC;
 
-import java.io.FileWriter;
-import java.io.IOException;
-import java.io.File;
+import java.io.*;
+import java.lang.reflect.Field;
 import java.lang.reflect.Method;
 import java.util.logging.Level;
 import java.util.logging.Logger;
@@ -185,7 +184,20 @@ public void init(String pidFile) throws Exception {
      * when they don't work correctly.
      */
     protected void closeDescriptors() throws IOException {
-        if(!Boolean.getBoolean(Daemon.class.getName()+".keepDescriptors")) {
+        final String cname = Daemon.class.getName();
+        if (Boolean.getBoolean(cname + ".redirectDescriptors")) {
+            // redirect (dup) System.out / System.err to user-specified files, or /dev/null
+            String stdoutPath = System.getProperty(cname + ".stdoutFile", "/dev/null");
+            String stderrPath = System.getProperty(cname + ".stderrFile", "/dev/null");
+            // attempt mkdirs as a nicety to caller
+            new File(stdoutPath).getParentFile().mkdirs();
+            new File(stderrPath).getParentFile().mkdirs();
+            FileOutputStream stdout = new FileOutputStream(stdoutPath);
+            FileOutputStream stderr = stdoutPath.equals(stderrPath) ? stdout : new FileOutputStream(stderrPath);
+            dupFD(stdout.getFD(), 1);
+            dupFD(stderr.getFD(), 2);
+            System.in.close();
+        } else if(!Boolean.getBoolean(cname +".keepDescriptors")) {
             System.out.close();
             System.err.close();
             System.in.close();
@@ -238,6 +250,42 @@ public static String getCurrentExecutable() {
         return System.getProperty("java.home")+"/bin/java";
     }
 
+
+     // dup2() system call: close oldfd, make oldfd refer to same resource as newfd
+     // public access to be visible to testing
+    public static int dupFD(FileDescriptor newFD, int oldfd) throws IOException {
+        /*
+           We need to get the field 'private int fd' out of java.io.FileDescriptor
+           There are two possible ways without writing JNI
+           1) sun.misc.SharedSecrets
+           2) reflection
+
+           Both are used elsewhere in this codebase (NetworkServer.java), but
+           prefer 2) b/c dubiousness of using sun.* API's, but it might be
+           rejected by a SecurityManager, if installed.
+         */
+        int newfd;
+        if (Boolean.getBoolean(Daemon.class.getName() + ".useSunMiscForDescriptors")) {
+            newfd = sun.misc.SharedSecrets.getJavaIOFileDescriptorAccess().get(newFD);
+        } else {
+            try {
+                Field fdField = FileDescriptor.class.getDeclaredField("fd");
+                fdField.setAccessible(true);
+                newfd = fdField.getInt(newFD);
+            } catch (Exception nsfe) { // NoSuchFieldException || IllegalAccessException
+                throw new RuntimeException("cannot reflect on java.io.FileDescriptor", nsfe);
+            }
+
+        }
+        int ret = LIBC.dup2(newfd, oldfd);
+        if (ret != oldfd) {
+            String msg = String.format("dup2 returned %d expected %d", ret, oldfd);
+            LIBC.perror(msg);
+            throw new IOException(msg);
+        }
+        return ret;
+    }
+
     private static String resolveSymlink(File link) throws IOException {
         String filename = link.getAbsolutePath();
 
diff --git a/src/test/java/com/sun/akuma/test/Dup2Test.java b/src/test/java/com/sun/akuma/test/Dup2Test.java
new file mode 100644
index 0000000..24a1b2e
--- /dev/null
+++ b/src/test/java/com/sun/akuma/test/Dup2Test.java
@@ -0,0 +1,66 @@
+package com.sun.akuma.test;
+
+import com.sun.akuma.Daemon;
+import junit.framework.TestCase;
+
+import java.io.*;
+import java.lang.reflect.Field;
+import java.nio.charset.Charset;
+import java.util.Random;
+import static com.sun.akuma.CLibrary.LIBC;
+
+
+public class Dup2Test extends TestCase {
+
+    static final Random rand = new Random(System.currentTimeMillis());
+
+    // Attempting to write this test using stdout / stderr works within IntelliJ, but
+    // the surefire test runner writes to a cached reference to System.out during the test
+    // and corrupts the results. So make the same test using other files.
+    public void testDup2() throws Exception {
+        /* dup fileA into fileB, so that writes to A really go to B */
+        File fileA = File.createTempFile("dup2-testA", ".txt"),
+                fileB = File.createTempFile("dup2-testB", ".txt");
+        FileOutputStream outA = new FileOutputStream(fileA),
+                outB = new FileOutputStream(fileB);
+        String textA = String.valueOf(rand.nextLong()), textB = String.valueOf(rand.nextLong()),
+                textC = String.valueOf(rand.nextLong());
+        try {
+            outA.write(textA.getBytes(Charset.defaultCharset()));
+            outB.write(textB.getBytes(Charset.defaultCharset()));
+            int bfd = getFD(outB.getFD());
+            Daemon.dupFD(outA.getFD(), bfd);
+            outB.write(textC.getBytes(Charset.defaultCharset()));
+        } finally {
+            outA.close();
+            outB.close();
+        }
+        String contentA = readFileContents(fileA);
+        String contentB = readFileContents(fileB);
+        assertEquals("expect only textB in file B", textB, contentB);
+        assertEquals("expect textA + textC in file A", textA + textC, contentA);
+    }
+
+
+    private static int getFD(FileDescriptor fd) throws Exception {
+        Field fdField = FileDescriptor.class.getDeclaredField("fd");
+        fdField.setAccessible(true);
+        return fdField.getInt(fd);
+    }
+
+    private static String readFileContents(File f) throws IOException {
+        /* assuming this project wants to target back to java6, so do not use Files.readAllBytes() */
+        StringBuilder out = new StringBuilder();
+        Reader reader = new FileReader(f);
+        char[] buf = new char[100];
+        int r;
+        try {
+            while ((r = reader.read(buf)) > 0) {
+                out.append(buf, 0, r);
+            }
+        } finally {
+            reader.close();
+        }
+        return out.toString();
+    }
+}