From 25b6c60699c51ed8fde55b5b38d0869d8d1aec4d Mon Sep 17 00:00:00 2001 From: nathan Date: Mon, 26 Dec 2016 23:13:08 +0100 Subject: [PATCH] Reverted change made of OS utils --- .../util/process/NullOutputStream.java | 46 ++ .../dorkbox/util/process/ProcessProxy.java | 102 ++++ .../util/process/ShellProcessBuilder.java | 499 ++++++++++++++++++ 3 files changed, 647 insertions(+) create mode 100644 Dorkbox-Util/src/dorkbox/util/process/NullOutputStream.java create mode 100644 Dorkbox-Util/src/dorkbox/util/process/ProcessProxy.java create mode 100644 Dorkbox-Util/src/dorkbox/util/process/ShellProcessBuilder.java diff --git a/Dorkbox-Util/src/dorkbox/util/process/NullOutputStream.java b/Dorkbox-Util/src/dorkbox/util/process/NullOutputStream.java new file mode 100644 index 0000000..0a04de3 --- /dev/null +++ b/Dorkbox-Util/src/dorkbox/util/process/NullOutputStream.java @@ -0,0 +1,46 @@ +/* + * Copyright 2010 dorkbox, llc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dorkbox.util.process; + +import java.io.IOException; +import java.io.OutputStream; + +public +class NullOutputStream extends OutputStream { + @Override + public + void write(int i) throws IOException { + //do nothing + } + + @Override + public + void write(byte[] b) throws IOException { + //do nothing + } + + @Override + public + void write(byte[] b, int off, int len) throws IOException { + //do nothing + } + + @Override + public + void flush() throws IOException { + //do nothing + } +} diff --git a/Dorkbox-Util/src/dorkbox/util/process/ProcessProxy.java b/Dorkbox-Util/src/dorkbox/util/process/ProcessProxy.java new file mode 100644 index 0000000..76e8032 --- /dev/null +++ b/Dorkbox-Util/src/dorkbox/util/process/ProcessProxy.java @@ -0,0 +1,102 @@ +/* + * Copyright 2010 dorkbox, llc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dorkbox.util.process; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.concurrent.CountDownLatch; + +public +class ProcessProxy extends Thread { + + private final InputStream is; + private final OutputStream os; + private final CountDownLatch countDownLatch = new CountDownLatch(1); + + // when reading from the stdin and outputting to the process + public + ProcessProxy(String processName, InputStream inputStreamFromConsole, OutputStream outputStreamToProcess) { + this.is = inputStreamFromConsole; + this.os = outputStreamToProcess; + + setName(processName); + setDaemon(true); + } + + public + void close() { + this.interrupt(); + try { + if (os != null) { + os.flush(); // this goes to the console, so we don't want to close it! + } + this.is.close(); + } catch (IOException e) { + } + } + + @Override + public synchronized + void start() { + super.start(); + + // now we have to for it to actually start up. The process can run & complete before this starts, resulting in no input/output + // captured + try { + countDownLatch.await(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + @Override + public + void run() { + final OutputStream os = this.os; + final InputStream is = this.is; + + countDownLatch.countDown(); + + try { + // this thread will read until there is no more data to read. (this is generally what you want) + // the stream will be closed when the process closes it (usually on exit) + int readInt; + + if (os == null) { + // just read so it won't block. + while (is.read() != -1) { + } + } + else { + while ((readInt = is.read()) != -1) { + os.write(readInt); + + + // 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 new file mode 100644 index 0000000..36bff7f --- /dev/null +++ b/Dorkbox-Util/src/dorkbox/util/process/ShellProcessBuilder.java @@ -0,0 +1,499 @@ +/* + * Copyright 2010 dorkbox, llc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dorkbox.util.process; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.InputStream; +import java.io.PrintStream; +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
+ *
+ * ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(8196);
+ * PrintStream outputStream = new PrintStream(byteArrayOutputStream);
+ * ...
+ *
+ * String output = ShellProcessBuilder.getOutput(byteArrayOutputStream);
+ * }
+ */ +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; + + protected List arguments = new ArrayList(); + private String workingDirectory = null; + private String executableName = null; + private String executableDirectory = null; + + 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. + */ + public + ShellProcessBuilder() { + this(null, null, null); + } + + public + ShellProcessBuilder(final PrintStream out) { + this(null, out, out); + } + + public + ShellProcessBuilder(final InputStream in, final PrintStream out) { + this(in, out, out); + } + + public + ShellProcessBuilder(final InputStream in, final PrintStream out, final PrintStream err) { + this.inputStream = in; + this.outputStream = out; + this.outputErrorStream = err; + } + + /** + * Creates extra reader/writer threads for the sub-process. This is useful depending on how the sub-process is designed to run. + *

+ * For a process you want interactive IO with, this is required. + *

+ * For a long-running sub-process, with no interactive IO, this is what you'd want. + *

+ * For a run-and-get-the-results process, this isn't recommended. + * + */ + public final + ShellProcessBuilder createReadWriterThreads() { + createReadWriterThreads = true; + return this; + } + + /** + * When launched from eclipse, the working directory is USUALLY the root of the project folder + */ + public final + ShellProcessBuilder setWorkingDirectory(final String workingDirectory) { + // MUST be absolute path!! + this.workingDirectory = new File(workingDirectory).getAbsolutePath(); + return this; + } + + public final + ShellProcessBuilder addArgument(final String argument) { + this.arguments.add(argument); + return this; + } + + public final + ShellProcessBuilder addArguments(final String... paths) { + for (String path : paths) { + this.arguments.add(path); + } + return this; + } + + public final + ShellProcessBuilder addArguments(final List paths) { + this.arguments.addAll(paths); + return this; + } + + public final + ShellProcessBuilder setExecutable(final String executableName) { + this.executableName = executableName; + return this; + } + + public + 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 pipeOutputToNull() throws IllegalArgumentException { + if (outputStream != null || outputErrorStream != null) { + throw new IllegalArgumentException("Cannot pipe shell command to 'null' if an output stream is specified"); + } + + if (OS.isWindows()) { + // >NUL on windows + pipeToNullString = ">NUL"; + } + else { + // we will "pipe" it to /dev/null on *nix + pipeToNullString = ">/dev/null 2>&1"; + } + + 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() { + fullCommand = new ArrayList(); + + // if no executable, then use the command shell + if (this.executableName == null) { + isShell = true; + + if (OS.isWindows()) { + // windows + this.executableName = "cmd"; + + fullCommand.add(this.executableName); + fullCommand.add("/c"); + } + else { + // *nix + this.executableName = "/bin/bash"; + + File file = new File(this.executableName); + if (!file.canExecute()) { + this.executableName = "/bin/sh"; + } + + fullCommand.add(this.executableName); + fullCommand.add("-c"); + } + } + else { + // shell and working/exe directory are mutually exclusive + + if (this.workingDirectory != null) { + if (!this.workingDirectory.endsWith(File.separator)) { + this.workingDirectory += File.separator; + } + } + + if (this.executableDirectory != null) { + if (!this.executableDirectory.endsWith(File.separator)) { + this.executableDirectory += File.separator; + } + + fullCommand.add(0, this.executableDirectory + this.executableName); + } else { + fullCommand.add(this.executableName); + } + } + + + // if we don't want output... + boolean pipeToNull = !pipeToNullString.isEmpty(); + + if (isShell && !OS.isWindows()) { + // when a shell AND on *nix, we have to place ALL the args into a single "arg" that is passed in + final StringBuilder stringBuilder = new StringBuilder(1024); + + for (String arg : this.arguments) { + stringBuilder.append(arg).append(" "); + } + + if (!arguments.isEmpty()) { + if (pipeToNull) { + stringBuilder.append(pipeToNullString); + } + else { + // delete last " " + stringBuilder.delete(stringBuilder.length() - 1, stringBuilder.length()); + } + } + + fullCommand.add(stringBuilder.toString()); + + } else { + for (String arg : this.arguments) { + 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) { + fullCommand.add(pipeToNullString); + } + } + + + + ProcessBuilder processBuilder = new ProcessBuilder(fullCommand); + if (this.workingDirectory != null) { + processBuilder.directory(new File(this.workingDirectory)); + } + + // combine these so output is properly piped to null. + if (pipeToNull || this.outputErrorStream == null) { + processBuilder.redirectErrorStream(true); + } + + try { + this.process = processBuilder.start(); + } catch (Exception ex) { + if (outputErrorStream != null) { + this.outputErrorStream.println("There was a problem executing the program. Details:"); + } else { + System.err.println("There was a problem executing the program. Details:"); + } + ex.printStackTrace(this.outputErrorStream); + + if (this.process != null) { + try { + this.process.destroy(); + this.process = null; + } catch (Exception e) { + if (outputErrorStream != null) { + this.outputErrorStream.println("Error destroying process:"); + } else { + System.err.println("Error destroying process:"); + } + e.printStackTrace(this.outputErrorStream); + } + } + } + + if (this.process != null) { + if (this.outputErrorStream == null && this.outputStream == null) { + if (!pipeToNull) { + NullOutputStream nullOutputStream = new NullOutputStream(); + + // readers (read process -> write console) + // have to keep the output buffers from filling in the target process. + readFromProcess_output = new ProcessProxy("Process Reader: " + this.executableName, + this.process.getInputStream(), + nullOutputStream); + } + } + // we want to pipe our input/output from process to ourselves + else { + /** + * Proxy the System.out and System.err from the spawned process back + * to the user's window. This is important or the spawned process could block. + */ + // readers (read process -> write console) + readFromProcess_output = new ProcessProxy("Process Reader: " + this.executableName, + this.process.getInputStream(), + this.outputStream); + + if (this.outputErrorStream != this.outputStream) { + readFromProcess_error = new ProcessProxy("Process Reader: " + this.executableName, + this.process.getErrorStream(), + this.outputErrorStream); + } + } + + if (this.inputStream != null) { + /** + * Proxy System.in from the user's window to the spawned process + */ + // writer (read console -> write process) + writeToProcess_input = new ProcessProxy("Process Writer: " + this.executableName, + this.inputStream, + this.process.getOutputStream()); + } + + + // the process can be killed in two ways + // 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() { + ShellProcessBuilder.this.process.destroy(); + } + }); + hook.setName("ShellProcess Shutdown Hook"); + + // add a shutdown hook to make sure that we properly terminate our spawned processes. + // 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) { + writeToProcess_input.start(); + } + else { + writeToProcess_input.run(); + } + } + + if (createReadWriterThreads) { + readFromProcess_output.start(); + } + else { + readFromProcess_output.run(); + } + if (readFromProcess_error != null) { + if (createReadWriterThreads) { + readFromProcess_error.start(); + } + else { + readFromProcess_error.run(); + } + } + + int exitValue = 0; + + try { + this.process.waitFor(); + + exitValue = this.process.exitValue(); + + // wait for the READER threads to die (meaning their streams have closed/EOF'd) + if (writeToProcess_input != null) { + // the INPUT (from stdin). It should be via the InputConsole, but if it's in eclipse,etc -- then this doesn't do anything + // We are done reading input, since our program has closed... + writeToProcess_input.close(); + writeToProcess_input.join(); + } + readFromProcess_output.close(); + readFromProcess_output.join(); + if (readFromProcess_error != null) { + readFromProcess_error.close(); + readFromProcess_error.join(); + } + + // forcibly terminate the process when it's streams have closed. + // this is for cleanup ONLY, not to actually do anything. + this.process.destroy(); + } catch (InterruptedException e) { + Thread.currentThread() + .interrupt(); + } + + // remove the shutdown hook now that we've shutdown. + try { + Runtime.getRuntime().removeShutdownHook(hook); + } catch (IllegalStateException ignored) { + // can happen, safe to ignore + } + + return exitValue; + } + + // 1 means a problem + return 1; + } + + /** + * 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, null if the thread for this was interrupted + */ + public static + String getOutput(final ByteArrayOutputStream byteArrayOutputStream) { + String s; + synchronized (byteArrayOutputStream) { + s = byteArrayOutputStream.toString(); + 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; + } +}