diff --git a/LICENSE.Apachev2 b/LICENSE.Apachev2 new file mode 100644 index 0000000..79d1b97 --- /dev/null +++ b/LICENSE.Apachev2 @@ -0,0 +1,218 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. \ No newline at end of file diff --git a/ShellExecutor.iml b/ShellExecutor.iml new file mode 100644 index 0000000..7595edd --- /dev/null +++ b/ShellExecutor.iml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/src/dorkbox/executor/JvmExecutor.java b/src/dorkbox/executor/JvmExecutor.java new file mode 100644 index 0000000..afec468 --- /dev/null +++ b/src/dorkbox/executor/JvmExecutor.java @@ -0,0 +1,275 @@ +/* + * 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.executor; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintStream; +import java.util.ArrayList; +import java.util.List; + +/** + * This will FORK the java process initially used to start the currently running JVM. Changing the java executable will change this behaviors + */ +public +class JvmExecutor extends ShellExecutor { + + /** + * Gets the version number. + */ + public static + String getVersion() { + return "1.0"; + } + + /** + * Reconstructs the path to the JVM used to launch this process. It will always use the "console" version, even on windows. + */ + public static + String getJvmPath() { + // use the VM in which we're already running + String jvmPath = checkJvmPath(System.getProperty("java.home")); + + // then throw up our hands and hope for the best + if (jvmPath == null) { + System.err.println("Unable to find java JVM [java.home=" + System.getProperty("java.home") + "]!"); + jvmPath = "java"; + } + + // Oddly, the Mac OS X specific java flag -Xdock:name will only work if java is launched + // from /usr/bin/java, and not if launched by directly referring to /bin/java, + // even though the former is a symlink to the latter! To work around this, see if the + // desired jvm is in fact pointed to by /usr/bin/java and, if so, use that instead. + if (isMacOsX) { + try { + File binDir = new File("/usr/bin"); + File javaParentDir = new File(jvmPath).getParentFile().getCanonicalFile(); + + if (javaParentDir.equals(binDir)) { + jvmPath = "/usr/bin/java"; + } + } catch (IOException ignored) { + } + } + + return jvmPath; + } + + /** + * Checks whether a Java Virtual Machine can be located in the supplied path. + * + * @param jvmLocation the location of the JVM to check + */ + private static + String checkJvmPath(String jvmLocation) { + // linux does this... + String vmbase = jvmLocation + File.separator + "bin" + File.separator; + String vmpath = vmbase + "java"; + + if (new File(vmpath).exists()) { + return vmpath; + } + + // windows does this + // open a console on windows (alternatively could open "javaw.exe", but we want the ability to redirect IO to the process. + vmpath = vmbase + "java.exe"; + + if (new File(vmpath).exists()) { + return vmpath; + } + + return null; + } + // this is NOT related to JAVA_HOME, but is instead the location of the JRE that was used to launch java initially. + private String javaLocation = getJvmPath(); + private String mainClass; + + private int initialHeapSizeInMegabytes = 0; + private int maximumHeapSizeInMegabytes = 0; + + private List jvmOptions = new ArrayList(); + private List classpathEntries = new ArrayList(); + + // what version of java?? + // so, this starts a NEW java, from an ALREADY existing java. + private List mainClassArguments = new ArrayList(); + private String jarFile; + + public + JvmExecutor() { + super(null, null, null); + } + + public + JvmExecutor(InputStream in, PrintStream out, PrintStream err) { + super(in, out, err); + } + + public final + void setMainClass(String mainClass) { + this.mainClass = mainClass; + } + + public final + void setInitialHeapSizeInMegabytes(int startingHeapSizeInMegabytes) { + this.initialHeapSizeInMegabytes = startingHeapSizeInMegabytes; + } + + public final + void setMaximumHeapSizeInMegabytes(int maximumHeapSizeInMegabytes) { + this.maximumHeapSizeInMegabytes = maximumHeapSizeInMegabytes; + } + + public final + void addJvmClasspath(String classpathEntry) { + this.classpathEntries.add(classpathEntry); + } + + public final + void addJvmClasspaths(List paths) { + this.classpathEntries.addAll(paths); + } + + public final + void addJvmOption(String argument) { + this.jvmOptions.add(argument); + } + + public final + void addJvmOptions(List paths) { + this.jvmOptions.addAll(paths); + } + + public final + void setJarFile(String jarFile) { + this.jarFile = jarFile; + } + + private + String getClasspath() { + StringBuilder builder = new StringBuilder(); + int count = 0; + final int totalSize = this.classpathEntries.size(); + final String pathseparator = File.pathSeparator; + + // DO NOT QUOTE the elements in the classpath! + for (String classpathEntry : this.classpathEntries) { + try { + // fix a nasty problem when spaces aren't properly escaped! + classpathEntry = classpathEntry.replaceAll(" ", "\\ "); + + // make sure the classpath is ABSOLUTE pathname + classpathEntry = new File(classpathEntry).getAbsolutePath(); + + builder.append(classpathEntry); + count++; + } catch (Exception e) { + e.printStackTrace(); + } + + if (count < totalSize) { + builder.append(pathseparator); // ; on windows, : on linux + } + } + return builder.toString(); + } + + /** + * Specify the JAVA executable to launch this process. By default, this will use the same java executable + * as was used to start the current JVM. + */ + public + void setJava(String javaLocation) { + this.javaLocation = javaLocation; + } + + @SuppressWarnings({"UseBulkOperation", "ManualArrayToCollectionCopy"}) + @Override + public + int start() { + setExecutable(this.javaLocation); + + // save off the original arguments + List origArguments = new ArrayList(this.arguments.size()); + origArguments.addAll(this.arguments); + this.arguments = new ArrayList(0); + + + // two versions, java vs not-java + if (initialHeapSizeInMegabytes != 0) { + this.arguments.add("-Xms" + this.initialHeapSizeInMegabytes + "M"); + } + if (maximumHeapSizeInMegabytes != 0) { + this.arguments.add("-Xmx" + this.maximumHeapSizeInMegabytes + "M"); + } + + // always run the server version + this.arguments.add("-server"); + + for (String option : this.jvmOptions) { + this.arguments.add(option); + } + + // same as -cp + String classpath = getClasspath(); + + // two more versions. jar vs class + if (this.jarFile != null) { + this.arguments.add("-jar"); + this.arguments.add(this.jarFile); + + // interesting note. You CANNOT have a classpath specified on the commandline + // when using JARs!! It must be set in the jar's MANIFEST. + if (!classpath.isEmpty()) { + throw new IllegalArgumentException("WHOOPS. You CANNOT have a classpath specified on the commandline when using JARs, " + + " It must be set in the JARs MANIFEST instead."); + } + } + // if we are running classes! + else if (this.mainClass != null) { + if (!classpath.isEmpty()) { + this.arguments.add("-classpath"); + this.arguments.add(classpath); + } + + // main class must happen AFTER the classpath! + this.arguments.add(this.mainClass); + } + else { + throw new IllegalArgumentException("You must specify a jar or main class when running a java process!"); + } + + + for (String arg : this.mainClassArguments) { + 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) { + this.arguments.add(s); + } + } + else { + this.arguments.add(arg); + } + } + + this.arguments.addAll(origArguments); + + return super.start(); + } +} diff --git a/src/dorkbox/executor/NullOutputStream.java b/src/dorkbox/executor/NullOutputStream.java new file mode 100644 index 0000000..fde5679 --- /dev/null +++ b/src/dorkbox/executor/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.executor; + +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/src/dorkbox/executor/ProcessProxy.java b/src/dorkbox/executor/ProcessProxy.java new file mode 100644 index 0000000..6e433de --- /dev/null +++ b/src/dorkbox/executor/ProcessProxy.java @@ -0,0 +1,215 @@ +/* + * 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.executor; + +import java.io.BufferedReader; +import java.io.FileInputStream; +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.lang.reflect.Field; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; + +import dorkbox.console.Console; +import dorkbox.console.input.Terminal; + +public +class ProcessProxy extends Thread { + + private final InputStream is; + private final OutputStream os; + + private final boolean isSystemIn; + + private final CountDownLatch startUpLatch = new CountDownLatch(1); + private final CountDownLatch shutDownLatch = new CountDownLatch(1); + + // when reading from the stdin and outputting to the process + public + ProcessProxy(String processName, InputStream inputStreamFromConsole, OutputStream outputStreamToProcess) { + boolean isSystemIn = false; + + // basic check to see if we are System.in + if (inputStreamFromConsole.equals(System.in)) { + + // more exact check: basically unwrap everything and see if it's a FileInputStream + try { + Field in = FilterInputStream.class.getDeclaredField("in"); + in.setAccessible(true); + + Object unwrapped = in.get(inputStreamFromConsole); + + while (unwrapped instanceof FilterInputStream) { + unwrapped = in.get(unwrapped); + } + + isSystemIn = unwrapped instanceof FileInputStream; + if (isSystemIn) { + inputStreamFromConsole = (InputStream) unwrapped; + } + } catch (Exception ignored) { + } + } + + // if we are actually System.in, we want to use the Console.in INSTEAD, because it will let us do things we could otherwise not do. + this.isSystemIn = isSystemIn; + + this.is = inputStreamFromConsole; + this.os = outputStreamToProcess; + + setName(processName); + setDaemon(true); + } + + private AtomicBoolean running = new AtomicBoolean(false); + + @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 { + startUpLatch.await(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + public + void close() { + // this.interrupt(); + running.set(false); + + try { + shutDownLatch.await(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + @Override + public + void run() { + // if we are system in, we can ONLY read the line input, unless the Console project is present! + if (isSystemIn) { + + } + + + + // we rely on buferredReader.ready(), so that we can know if there is input or not (and read/block/etc if necessary) + final BufferedReader reader = new BufferedReader(new InputStreamReader(this.is)); + + + Terminal in = Console.in(); + + + + running.set(true); + + final OutputStream os = this.os; + // final BufferedReader reader = this.reader; + final long timeout = 200L; + + startUpLatch.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) { + while (!reader.ready()) { + Thread.sleep(timeout); + + if (!running.get()) { + if (isSystemIn) { + System.err.println("DONE sysin " + this); + // should attempt to process anything more. + return; + } + + // should process whatever is left. + System.err.println("DONE a " + this); + break; + } + } + + // just read so it won't block. + reader.readLine(); + } + else { + while (running.get()) { + try { + while (!reader.ready()) { + Thread.sleep(timeout); + + if (!running.get()) { + if (isSystemIn) { + System.err.println("DONE sysin " + this); + // should attempt to process anything more. + return; + } + + // should process whatever is left. + System.err.println("DONE a " + this); + break; + } + } + } catch (InterruptedException ignored) { + } + + + while ((readInt = reader.read()) != -1) { + System.err.println("."); + 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.notifyAll(); + } + } + } + } + + + } + } catch (Exception ignore) { + ignore.printStackTrace(); + } finally { + System.err.println("DONE c " + this); + + try { + // this.reader.close(); + if (os != null) { + os.flush(); // this goes to the console, so we don't want to close it! + } + } catch (IOException e) { + e.printStackTrace(); + } + + shutDownLatch.countDown(); + } + } +} diff --git a/src/dorkbox/executor/ShellAsyncExecutor.java b/src/dorkbox/executor/ShellAsyncExecutor.java new file mode 100644 index 0000000..141b7f8 --- /dev/null +++ b/src/dorkbox/executor/ShellAsyncExecutor.java @@ -0,0 +1,62 @@ +/* + * Copyright 2017 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.executor; + +public +class ShellAsyncExecutor extends ShellExecutor { + /** + * This is a convenience method to easily create a default process. Will immediately return, and does not wait for the process to finish + * + * @param executableName the name of the executable to run + * @param args the arguments for the executable + * + * @return true if the process ran successfully (exit value was 0), otherwise false + */ + public static + boolean run(String executableName, String... args) { + ShellAsyncExecutor shell = new ShellAsyncExecutor(); + shell.setExecutable(executableName); + shell.addArguments(args); + + return shell.start() == 0; + } + + /** + * This is a convenience method to easily create a default process. Will immediately return, and does not wait for the process to finish + * + * @param executableName the name of the executable to run + * @param args the arguments for the executable + * + * @return true if the process ran successfully (exit value was 0), otherwise false + */ + public static + boolean runShell(String executableName, String... args) { + ShellAsyncExecutor shell = new ShellAsyncExecutor(); + shell.setExecutable(executableName); + shell.addArguments(args); + shell.executeAsShellCommand(); + + return shell.start() == 0; + } + + @Override + public + int start() { + // always have to make sure separate threads are started, otherwise the calling process can hang. + createReadWriterThreads(); + return super.start(false); + } +} diff --git a/src/dorkbox/executor/ShellExecutor.java b/src/dorkbox/executor/ShellExecutor.java new file mode 100644 index 0000000..844e7dd --- /dev/null +++ b/src/dorkbox/executor/ShellExecutor.java @@ -0,0 +1,681 @@ +/* + * 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.executor; + +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 java.util.Map; + +/** + * 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);
+ * }
+ */ +@SuppressWarnings({"UnusedReturnValue", "unused", "ManualArrayToCollectionCopy", "UseBulkOperation", "Convert2Diamond", "Convert2Lambda", + "Anonymous2MethodRef", "WeakerAccess"}) +public +class ShellExecutor { + + // TODO: Add the ability to get the process PID via java for mac/windows/linux. Linux is avail from jvm, windows needs JNA + + static final String LINE_SEPARATOR = System.getProperty("line.separator"); + static final boolean isWindows; + static final boolean isMacOsX; + + static { + String osName = System.getProperty("os.name"); + isWindows = osName.startsWith("windows"); + isMacOsX = osName.startsWith("mac") || osName.startsWith("darwin"); + } + + + + private static String defaultShell = null; + + private final PrintStream outputStream; + private final PrintStream outputErrorStream; + private final InputStream inputStream; + + protected List arguments = new ArrayList(); + private Map environment = null; + 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 executeAsShell; + private String pipeToNullString = ""; + private ByteArrayOutputStream byteArrayOutputStream; + + private List fullCommand; + + /** + * Gets the version number. + */ + public static + String getVersion() { + return "1.0"; + } + + /** + * This is a convenience method to easily create a default process. Will block until the process is finished running + * + * @param executableName the name of the executable to run + * @param args the arguments for the executable + * + * @return true if the process ran successfully (exit value was 0), otherwise false + */ + public static boolean run(String executableName, String... args) { + ShellExecutor shell = new ShellExecutor(); + shell.setExecutable(executableName); + shell.addArguments(args); + + // blocks until finished + return shell.start() == 0; + } + + /** + * This is a convenience method to easily create a default process. Will immediately return, and does not wait for the process to finish + * + * @param executableName the name of the executable to run + * @param args the arguments for the executable + * + * @return true if the process ran successfully (exit value was 0), otherwise false + */ + public static + boolean runShell(String executableName, String... args) { + ShellExecutor shell = new ShellExecutor(); + shell.setExecutable(executableName); + shell.addArguments(args); + shell.executeAsShellCommand(); + + // blocks until finished + return shell.start() == 0; + } + + /** + * This will cause the spawned process to pipe it's output to a String, so it can be retrieved. + */ + public + ShellExecutor() { + byteArrayOutputStream = new ByteArrayOutputStream(8196); + PrintStream outputStream = new PrintStream(byteArrayOutputStream); + + this.inputStream = null; + this.outputStream = outputStream; + this.outputErrorStream = outputStream; + } + + public + ShellExecutor(final PrintStream out) { + this.inputStream = null; + this.outputStream = out; + this.outputErrorStream = out; + } + + public + ShellExecutor(final InputStream in, final PrintStream out) { + this.inputStream = in; + this.outputStream = out; + this.outputErrorStream = out; + } + + public + ShellExecutor(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 + ShellExecutor createReadWriterThreads() { + createReadWriterThreads = true; + return this; + } + + /** + * When launched from eclipse, the working directory is USUALLY the root of the project folder + */ + public final + ShellExecutor setWorkingDirectory(final String workingDirectory) { + // MUST be absolute path!! + this.workingDirectory = new File(workingDirectory).getAbsolutePath(); + return this; + } + + /** + * The Shell's execution environment variables. Set to `null` to only use the default environment variables (From what + * {@link System#getenv} returns) + */ + public final + ShellExecutor setEnvironment(final Map environment) { + this.environment = environment; + return this; + } + + public final + ShellExecutor addArgument(final String argument) { + this.arguments.add(argument); + return this; + } + + public final + ShellExecutor addArguments(final String... args) { + for (String path : args) { + this.arguments.add(path); + } + return this; + } + + public final + ShellExecutor addArguments(final List paths) { + this.arguments.addAll(paths); + return this; + } + + public final + ShellExecutor setExecutable(final String executableName) { + this.executableName = executableName; + return this; + } + + public + ShellExecutor setExecutableDirectory(final String executableDirectory) { + // MUST be absolute path!! + this.executableDirectory = new File(executableDirectory).getAbsolutePath(); + return this; + } + + /** + * This will execute as a shell command (bash/cmd/etc) instead of as a forked process. + */ + public + ShellExecutor executeAsShellCommand() { + this.executeAsShell = true; + return this; + } + + + + + /** + * Sends all output data for this process to "null" in a cross platform method + */ + public + ShellExecutor pipeOutputToNull() throws IllegalArgumentException { + if (outputStream != null || outputErrorStream != null) { + throw new IllegalArgumentException("Cannot pipe shell command to 'null' if an output stream is specified"); + } + + if (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() { + return start(true); + } + + public + int start(final boolean waitForProcesses) { + fullCommand = new ArrayList(); + if (executeAsShell) { + if (isWindows) { + fullCommand.add("cmd"); + fullCommand.add("/c"); + } + else { + if (defaultShell == null) { + String[] shells = new String[] {"/bin/bash", "/usr/bin/bash", + "/bin/pfbash", "/usr/bin/pfbash", + "/bin/csh", "/usr/bin/csh", + "/bin/pfcsh", "/usr/bin/pfcsh", + "/bin/jsh", "/usr/bin/jsh", + "/bin/ksh", "/usr/bin/ksh", + "/bin/pfksh", "/usr/bin/pfksh", + "/bin/ksh93", "/usr/bin/ksh93", + "/bin/pfksh93", "/usr/bin/pfksh93", + "/bin/pfsh", "/usr/bin/pfsh", + "/bin/tcsh", "/usr/bin/tcsh", + "/bin/pftcsh", "/usr/bin/pftcsh", + "/usr/xpg4/bin/sh", "/usr/xp4/bin/pfsh", + "/bin/zsh", "/usr/bin/zsh", + "/bin/pfzsh", "/usr/bin/pfzsh", + "/bin/sh", "/usr/bin/sh",}; + + for (String shell : shells) { + if (new File(shell).canExecute()) { + defaultShell = shell; + break; + } + } + } + + if (defaultShell == null) { + throw new RuntimeException("Unable to determine the default shell for the linux/unix environment."); + } + + // *nix + fullCommand.add(defaultShell); + fullCommand.add("-c"); + } + + // fullCommand.add(this.executableName); // done elsewhere! + } 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 (executeAsShell && !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); + + stringBuilder.append(this.executableName).append(" "); + + 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) { + s = s.trim(); + if (!s.isEmpty()) { + 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)); + } + + + // These env variables are a copy of System.getenv() + Map environment = processBuilder.environment(); + + // Make sure all shell calls are LANG=en_US.UTF-8 THIS CAN BE OVERRIDDEN + if (isMacOsX) { + // Enable LANG overrides + environment.put("SOFTWARE", ""); + } + + // "export LANG=en_US.UTF-8" + environment.put("LANG", "C"); + + if (this.environment != null) { + for (Map.Entry e : this.environment.entrySet()) { + environment.put(e.getKey(), e.getValue()); + } + } + + // 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:"); + ex.printStackTrace(this.outputErrorStream); + } else { + System.err.println("There was a problem executing the program. Details:"); + ex.printStackTrace(); + } + + 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() { + try { + // 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(); + if (createReadWriterThreads) { + writeToProcess_input.join(); + } + } + + readFromProcess_output.close(); + if (createReadWriterThreads) { + readFromProcess_output.join(); + } + + if (readFromProcess_error != null) { + readFromProcess_error.close(); + if (createReadWriterThreads) { + readFromProcess_error.join(); + } + } + } catch (InterruptedException e) { + Thread.currentThread() + .interrupt(); + } + + // forcibly terminate the process when it's streams have closed. + // this is for cleanup ONLY, not to actually do anything. + ShellExecutor.this.process.destroy(); + } + }); + hook.setName("ShellExecutor Shutdown Hook for " + this.executableName); + + // 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; + + if (waitForProcesses) { + try { + this.process.waitFor(); + exitValue = this.process.exitValue(); + hook.run(); + } 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; + } + + /** + * There will never be a trailing newline character at the end of this output. + * + * @return A string representing the output of the process, null if the thread for this was interrupted or the output wasn't saved + */ + public + String getOutput() { + if (byteArrayOutputStream != null) { + return getOutput(byteArrayOutputStream); + } + + return null; + } + + /** + * 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. + * + * @return A string representing the output of the process, null if the thread for this was interrupted or the output wasn't saved + */ + public + String getOutputLineBuffered() { + if (byteArrayOutputStream != null) { + return getOutputLineBuffered(byteArrayOutputStream); + } + + return null; + } + + /** + * 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 ShellExecutor#ShellExecutor(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(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 ShellExecutor#ShellExecutor(PrintStream)} (or similar + * calls) + * + * @return A string representing the output of the process, null if the thread for this was interrupted + */ + public static + String getOutputLineBuffered(final ByteArrayOutputStream byteArrayOutputStream) { + String s; + synchronized (byteArrayOutputStream) { + try { + byteArrayOutputStream.wait(); + } catch (InterruptedException ignored) { + return null; + } + + s = byteArrayOutputStream.toString(); + byteArrayOutputStream.reset(); + } + + return s; + } +} diff --git a/src/dorkbox/executor/TeeOutputStream.java b/src/dorkbox/executor/TeeOutputStream.java new file mode 100644 index 0000000..665d39a --- /dev/null +++ b/src/dorkbox/executor/TeeOutputStream.java @@ -0,0 +1,74 @@ +/* + * 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.executor; + +import java.io.IOException; +import java.io.OutputStream; + +public +class TeeOutputStream extends OutputStream { + private final OutputStream out; + private final OutputStream tee; + + public + TeeOutputStream(OutputStream out, OutputStream tee) { + if (out == null) { + throw new NullPointerException(); + } + else if (tee == null) { + throw new NullPointerException(); + } + else { + this.out = out; + this.tee = tee; + } + } + + @Override + public + void write(int b) throws IOException { + this.out.write(b); + this.tee.write(b); + } + + @Override + public + void write(byte[] b) throws IOException { + this.out.write(b); + this.tee.write(b); + } + + @Override + public + void write(byte[] b, int off, int len) throws IOException { + this.out.write(b, off, len); + this.tee.write(b, off, len); + } + + @Override + public + void flush() throws IOException { + this.out.flush(); + this.tee.flush(); + } + + @Override + public + void close() throws IOException { + this.out.close(); + this.tee.close(); + } +}