Moved shell executor from utils to it's own project

master
nathan 2017-11-26 21:57:25 +01:00
parent 122d6ea9c2
commit d9b565edde
8 changed files with 1583 additions and 0 deletions

218
LICENSE.Apachev2 Normal file
View File

@ -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.

12
ShellExecutor.iml Normal file
View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="module" module-name="Console" />
</component>
</module>

View File

@ -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 <java.home>/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<String> jvmOptions = new ArrayList<String>();
private List<String> classpathEntries = new ArrayList<String>();
// what version of java??
// so, this starts a NEW java, from an ALREADY existing java.
private List<String> mainClassArguments = new ArrayList<String>();
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<String> paths) {
this.classpathEntries.addAll(paths);
}
public final
void addJvmOption(String argument) {
this.jvmOptions.add(argument);
}
public final
void addJvmOptions(List<String> 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<String> origArguments = new ArrayList<String>(this.arguments.size());
origArguments.addAll(this.arguments);
this.arguments = new ArrayList<String>(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();
}
}

View File

@ -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
}
}

View File

@ -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();
}
}
}

View File

@ -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);
}
}

View File

@ -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:
* <pre> {@code
*
* ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(8196);
* PrintStream outputStream = new PrintStream(byteArrayOutputStream);
* ...
*
* String output = ShellProcessBuilder.getOutput(byteArrayOutputStream);
* }</pre>
*/
@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<String> arguments = new ArrayList<String>();
private Map<String, String> 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<String> 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.
* </p>
* For a process you want interactive IO with, this is required.
* </p>
* For a long-running sub-process, with no interactive IO, this is what you'd want.
* </p>
* 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<String,String> 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<String> 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<String> 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<String>();
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<String, String> 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<String, String> 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;
}
}

View File

@ -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();
}
}