diff --git a/Dorkbox-Util/src/dorkbox/util/process/ProcessProxy.java b/Dorkbox-Util/src/dorkbox/util/process/ProcessProxy.java index 1ac316d..76e8032 100644 --- a/Dorkbox-Util/src/dorkbox/util/process/ProcessProxy.java +++ b/Dorkbox-Util/src/dorkbox/util/process/ProcessProxy.java @@ -85,8 +85,15 @@ class ProcessProxy extends Thread { while ((readInt = is.read()) != -1) { os.write(readInt); - // always flush after a write - os.flush(); + + // flush the output on new line. (same for both windows '\r\n' and linux '\n') + if (readInt == '\n') { + os.flush(); + + synchronized (os) { + os.notify(); + } + } } } } catch (Exception ignore) { diff --git a/Dorkbox-Util/src/dorkbox/util/process/ShellProcessBuilder.java b/Dorkbox-Util/src/dorkbox/util/process/ShellProcessBuilder.java index 5cdb556..36bff7f 100644 --- a/Dorkbox-Util/src/dorkbox/util/process/ShellProcessBuilder.java +++ b/Dorkbox-Util/src/dorkbox/util/process/ShellProcessBuilder.java @@ -15,8 +15,6 @@ */ package dorkbox.util.process; -import dorkbox.util.OS; - import java.io.ByteArrayOutputStream; import java.io.File; import java.io.InputStream; @@ -25,6 +23,8 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import dorkbox.util.OS; + /** * If you want to save off the output from the process, set a PrintStream to the following: *
 {@code
@@ -39,6 +39,8 @@ import java.util.List;
 public
 class ShellProcessBuilder {
 
+    // TODO: Add the ability to get the process PID via java for mac/windows/linux. Linux is avail from jvm, windows needs JNA
+
     private final PrintStream outputStream;
     private final PrintStream outputErrorStream;
     private final InputStream inputStream;
@@ -47,14 +49,17 @@ class ShellProcessBuilder {
     private String workingDirectory = null;
     private String executableName = null;
     private String executableDirectory = null;
-    private Process process = null;
 
-    // true if we want to save off (usually for debugging) the initial output from this
-    private boolean debugInfo = false;
+    private Process process = null;
+    private ProcessProxy writeToProcess_input = null;
+    private ProcessProxy readFromProcess_output = null;
+    private ProcessProxy readFromProcess_error = null;
+
     private boolean createReadWriterThreads = false;
 
     private boolean isShell;
     private String pipeToNullString = "";
+    private List fullCommand;
 
     /**
      * This will cause the spawned process to pipe it's output to null.
@@ -65,17 +70,17 @@ class ShellProcessBuilder {
     }
 
     public
-    ShellProcessBuilder(PrintStream out) {
+    ShellProcessBuilder(final PrintStream out) {
         this(null, out, out);
     }
 
     public
-    ShellProcessBuilder(InputStream in, PrintStream out) {
+    ShellProcessBuilder(final InputStream in, final PrintStream out) {
         this(in, out, out);
     }
 
     public
-    ShellProcessBuilder(InputStream in, PrintStream out, PrintStream err) {
+    ShellProcessBuilder(final InputStream in, final PrintStream out, final PrintStream err) {
         this.inputStream = in;
         this.outputStream = out;
         this.outputErrorStream = err;
@@ -101,20 +106,20 @@ class ShellProcessBuilder {
      * When launched from eclipse, the working directory is USUALLY the root of the project folder
      */
     public final
-    ShellProcessBuilder setWorkingDirectory(String workingDirectory) {
+    ShellProcessBuilder setWorkingDirectory(final String workingDirectory) {
         // MUST be absolute path!!
         this.workingDirectory = new File(workingDirectory).getAbsolutePath();
         return this;
     }
 
     public final
-    ShellProcessBuilder addArgument(String argument) {
+    ShellProcessBuilder addArgument(final String argument) {
         this.arguments.add(argument);
         return this;
     }
 
     public final
-    ShellProcessBuilder addArguments(String... paths) {
+    ShellProcessBuilder addArguments(final String... paths) {
         for (String path : paths) {
             this.arguments.add(path);
         }
@@ -122,32 +127,33 @@ class ShellProcessBuilder {
     }
 
     public final
-    ShellProcessBuilder addArguments(List paths) {
+    ShellProcessBuilder addArguments(final List paths) {
         this.arguments.addAll(paths);
         return this;
     }
 
     public final
-    ShellProcessBuilder setExecutable(String executableName) {
+    ShellProcessBuilder setExecutable(final String executableName) {
         this.executableName = executableName;
         return this;
     }
 
     public
-    ShellProcessBuilder setExecutableDirectory(String executableDirectory) {
+    ShellProcessBuilder setExecutableDirectory(final String executableDirectory) {
         // MUST be absolute path!!
         this.executableDirectory = new File(executableDirectory).getAbsolutePath();
         return this;
     }
 
+    /**
+     * Sends all output data for this process to "null" in a cross platform method
+     */
     public
-    ShellProcessBuilder addDebugInfo() {
-        this.debugInfo = true;
-        return this;
-    }
+    ShellProcessBuilder pipeOutputToNull() throws IllegalArgumentException {
+        if (outputStream != null || outputErrorStream != null) {
+            throw new IllegalArgumentException("Cannot pipe shell command to 'null' if an output stream is specified");
+        }
 
-    public
-    ShellProcessBuilder pipeOutputToNull() {
         if (OS.isWindows()) {
             // >NUL on windows
             pipeToNullString = ">NUL";
@@ -160,10 +166,30 @@ class ShellProcessBuilder {
         return this;
     }
 
+    /**
+     * @return the executable command issued to the shell
+     */
+    public
+    String getCommand() {
+        StringBuilder execCommand = new StringBuilder();
+
+        Iterator iterator = fullCommand.iterator();
+        while (iterator.hasNext()) {
+            String s = iterator.next();
+
+            execCommand.append(s);
+
+            if (iterator.hasNext()) {
+                execCommand.append(" ");
+            }
+        }
+
+        return execCommand.toString();
+    }
 
     public
     int start() {
-        List argumentsList = new ArrayList();
+        fullCommand = new ArrayList();
 
         // if no executable, then use the command shell
         if (this.executableName == null) {
@@ -173,8 +199,8 @@ class ShellProcessBuilder {
                 // windows
                 this.executableName = "cmd";
 
-                argumentsList.add(this.executableName);
-                argumentsList.add("/c");
+                fullCommand.add(this.executableName);
+                fullCommand.add("/c");
             }
             else {
                 // *nix
@@ -185,8 +211,8 @@ class ShellProcessBuilder {
                     this.executableName = "/bin/sh";
                 }
 
-                argumentsList.add(this.executableName);
-                argumentsList.add("-c");
+                fullCommand.add(this.executableName);
+                fullCommand.add("-c");
             }
         }
         else {
@@ -203,9 +229,9 @@ class ShellProcessBuilder {
                     this.executableDirectory += File.separator;
                 }
 
-                argumentsList.add(0, this.executableDirectory + this.executableName);
+                fullCommand.add(0, this.executableDirectory + this.executableName);
             } else {
-                argumentsList.add(this.executableName);
+                fullCommand.add(this.executableName);
             }
         }
 
@@ -231,55 +257,30 @@ class ShellProcessBuilder {
                 }
             }
 
-            argumentsList.add(stringBuilder.toString());
+            fullCommand.add(stringBuilder.toString());
 
         } else {
             for (String arg : this.arguments) {
-                argumentsList.add(arg);
+                if (arg.contains(" ")) {
+                    // individual arguments MUST be in their own element in order to be processed properly
+                    // (this is how it works on the command line!)
+                    String[] split = arg.split(" ");
+                    for (String s : split) {
+                        fullCommand.add(s);
+                    }
+                } else {
+                    fullCommand.add(arg);
+                }
             }
 
             if (pipeToNull) {
-                argumentsList.add(pipeToNullString);
+                fullCommand.add(pipeToNullString);
             }
         }
 
 
 
-        if (this.debugInfo) {
-            if (outputErrorStream != null) {
-                this.outputErrorStream.print("Executing: ");
-            } else {
-                System.err.print("Executing: ");
-            }
-
-            Iterator iterator = argumentsList.iterator();
-            while (iterator.hasNext()) {
-                String s = iterator.next();
-                if (outputErrorStream != null) {
-                    this.outputErrorStream.print(s);
-                } else {
-                    System.err.print(s);
-                }
-                if (iterator.hasNext()) {
-                    if (outputErrorStream != null) {
-                        this.outputErrorStream.print(" ");
-                    } else {
-                        System.err.print(" ");
-                    }
-                }
-            }
-
-            if (outputErrorStream != null) {
-                this.outputErrorStream.print(OS.LINE_SEPARATOR);
-            } else {
-                System.err.print(OS.LINE_SEPARATOR);
-            }
-        }
-
-
-
-
-        ProcessBuilder processBuilder = new ProcessBuilder(argumentsList);
+        ProcessBuilder processBuilder = new ProcessBuilder(fullCommand);
         if (this.workingDirectory != null) {
             processBuilder.directory(new File(this.workingDirectory));
         }
@@ -315,10 +316,6 @@ class ShellProcessBuilder {
         }
 
         if (this.process != null) {
-            ProcessProxy writeToProcess_input = null;
-            ProcessProxy readFromProcess_output = null;
-            ProcessProxy readFromProcess_error = null;
-
             if (this.outputErrorStream == null && this.outputStream == null) {
                 if (!pipeToNull) {
                     NullOutputStream nullOutputStream = new NullOutputStream();
@@ -360,24 +357,26 @@ class ShellProcessBuilder {
 
 
             // the process can be killed in two ways
-            // If not in eclipse, by this shutdown hook. (clicking the red square to terminate a process will not run it's shutdown hooks)
+            // If not in IDE, by this shutdown hook. (clicking the red square to terminate a process will not run it's shutdown hooks)
             // Typing "exit" will always terminate the process
             Thread hook = new Thread(new Runnable() {
                 @Override
                 public
                 void run() {
-                    if (ShellProcessBuilder.this.debugInfo) {
-                        final PrintStream errorStream = ShellProcessBuilder.this.outputErrorStream;
-                        if (errorStream != null) {
-                            errorStream.println("Terminating process: " + ShellProcessBuilder.this.executableName);
-                        }
-                    }
                     ShellProcessBuilder.this.process.destroy();
                 }
             });
+            hook.setName("ShellProcess Shutdown Hook");
+
             // add a shutdown hook to make sure that we properly terminate our spawned processes.
-            Runtime.getRuntime()
-                   .addShutdownHook(hook);
+            // hook is NOT set to daemon mode, because this is run during shutdown
+            // add a shutdown hook to make sure that we properly terminate our spawned processes.
+            try {
+                Runtime.getRuntime()
+                       .addShutdownHook(hook);
+            } catch (IllegalStateException ignored) {
+                // can happen, safe to ignore
+            }
 
             if (writeToProcess_input != null) {
                 if (createReadWriterThreads) {
@@ -433,8 +432,11 @@ class ShellProcessBuilder {
             }
 
             // remove the shutdown hook now that we've shutdown.
-            Runtime.getRuntime()
-                   .removeShutdownHook(hook);
+            try {
+                Runtime.getRuntime().removeShutdownHook(hook);
+            } catch (IllegalStateException ignored) {
+                // can happen, safe to ignore
+            }
 
             return exitValue;
         }
@@ -444,12 +446,12 @@ class ShellProcessBuilder {
     }
 
     /**
-     * Converts the baos to a string in a safe way. There might be a trailing newline character at the end of this output.
+     * Converts the baos to a string in a safe way. There will never be a trailing newline character at the end of this output.
      *
      * @param byteArrayOutputStream the baos that is used in the {@link ShellProcessBuilder#ShellProcessBuilder(PrintStream)} (or similar
      *                              calls)
      *
-     * @return A string representing the output of the process
+     * @return A string representing the output of the process, null if the thread for this was interrupted
      */
     public static
     String getOutput(final ByteArrayOutputStream byteArrayOutputStream) {
@@ -459,6 +461,39 @@ class ShellProcessBuilder {
             byteArrayOutputStream.reset();
         }
 
+        // remove trailing newline character(s)
+        int endIndex = s.lastIndexOf(OS.LINE_SEPARATOR);
+        if (endIndex > -1) {
+            return s.substring(0, endIndex);
+        }
+
+        return s;
+    }
+
+    /**
+     * Converts the baos to a string in a safe way. There will never be a trailing newline character at the end of this output. This will
+     * block until there is a line of input available.
+     *
+     * @param byteArrayOutputStream the baos that is used in the {@link ShellProcessBuilder#ShellProcessBuilder(PrintStream)} (or similar
+     *                              calls)
+     *
+     * @return A string representing the output of the process, null if the thread for this was interrupted
+     */
+    public String
+    getOutputLineBuffered(final ByteArrayOutputStream byteArrayOutputStream) {
+        String s;
+
+        synchronized (byteArrayOutputStream) {
+            try {
+                byteArrayOutputStream.wait();
+            } catch (InterruptedException ignored) {
+                return null;
+            }
+
+            s = byteArrayOutputStream.toString();
+            byteArrayOutputStream.reset();
+        }
+
         return s;
     }
 }