aQute.libg.command.Command Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of biz.aQute.bndlib Show documentation
Show all versions of biz.aQute.bndlib Show documentation
bndlib: A Swiss Army Knife for OSGi
package aQute.libg.command;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.stream.Collectors.toList;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Pattern;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import aQute.lib.io.IO;
import aQute.libg.qtokens.QuotedTokenizer;
import aQute.service.reporter.Reporter;
public class Command {
private final static Logger logger = LoggerFactory.getLogger(Command.class);
private final static int TIMEDOUT = 126 - 3;
boolean trace;
Reporter reporter;
List arguments = new ArrayList<>();
Map variables = new LinkedHashMap<>();
long timeout = 0;
File cwd = new File("").getAbsoluteFile();
volatile Process process;
volatile boolean timedout;
private boolean useThreadForInput;
public Command(String fullCommand) {
this();
full(fullCommand);
}
public Command() {}
public int execute(Appendable stdout, Appendable stderr) throws Exception {
return execute((InputStream) null, stdout, stderr);
}
public int execute(String input, Appendable stdout, Appendable stderr) throws Exception {
InputStream in = input == null ? null : IO.stream(input, UTF_8);
return execute(in, stdout, stderr);
}
public static boolean needsWindowsQuoting(String s) {
int len = s.length();
if (len == 0) // empty string have to be quoted
return true;
for (int i = 0; i < len; i++) {
switch (s.charAt(i)) {
case ' ' :
case '\t' :
case '\\' :
case '"' :
return true;
}
}
return false;
}
private static final Pattern escapedDoubleQuote = Pattern.compile("([\\\\]*)\"");
private static final Pattern trailingBackslash = Pattern.compile("([\\\\]*)\\z");
public static String windowsQuote(String s) {
if (!needsWindowsQuoting(s))
return s;
s = escapedDoubleQuote.matcher(s)
.replaceAll("$1$1\\\\\"");
s = trailingBackslash.matcher(s)
.replaceAll("$1$1");
return "\"" + s + "\"";
}
public int execute(final InputStream in, Appendable stdout, Appendable stderr) throws Exception {
// Test System.in once since other threads can change during execution
final boolean systemIn = in == System.in;
logger.debug("executing cmd: {}", getArguments());
ProcessBuilder p = new ProcessBuilder(getArguments());
if (IO.isWindows()) {
// [cs] Arguments on windows aren't processed correctly.
// So we need to perform some escaping.
// http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6511002
// http://msdn.microsoft.com/en-us/library/17w5ykft.aspx
p.command(p.command()
.stream()
.map(Command::windowsQuote)
.collect(toList()));
}
p.environment()
.putAll(variables);
p.directory(cwd);
if (systemIn) {
p.redirectInput(ProcessBuilder.Redirect.INHERIT);
}
Process process = this.process = p.start();
// Make sure the command will not linger when we go
Thread hook = new Thread(process::destroy, getArguments().toString());
Runtime.getRuntime()
.addShutdownHook(hook);
ScheduledExecutorService scheduler = null;
if (timeout != 0) {
scheduler = Executors.newScheduledThreadPool(1);
scheduler.schedule(() -> {
timedout = true;
process.destroy();
}, timeout, TimeUnit.MILLISECONDS);
}
final OutputStream stdin = process.getOutputStream();
Thread inThread = null;
final AtomicBoolean finished = new AtomicBoolean(false);
try (InputStream out = process.getInputStream(); InputStream err = process.getErrorStream()) {
Thread outThread = new Thread(collector(out, stdout), "Write Output Thread");
outThread.setDaemon(true);
Thread errThread = new Thread(collector(err, stderr), "Write Error Thread");
errThread.setDaemon(true);
outThread.start();
errThread.start();
if (in != null) {
if (systemIn || useThreadForInput) {
inThread = new Thread(() -> {
try {
while (!finished.get()) {
int n = in.available();
if (n == 0) {
Thread.sleep(100);
} else {
int c = in.read();
if (c < 0) {
break; // finally will close stdin
}
stdin.write(c);
if (c == '\n') {
stdin.flush();
}
}
}
} catch (InterruptedIOException e) {
// Ignore here
} catch (Exception e) {
logger.debug("stdin copy exception in thread", e);
} finally {
IO.close(stdin);
}
}, "Read Input Thread");
inThread.setDaemon(true);
inThread.start();
} else {
try {
IO.copy(in, stdin);
} catch (Exception e) {
logger.debug("stdin copy exception", e);
} finally {
IO.close(stdin);
}
}
}
logger.debug("exited process");
errThread.join();
outThread.join();
logger.debug("stdout/stderr streams have finished");
} finally {
if (scheduler != null) {
scheduler.shutdownNow();
}
Runtime.getRuntime()
.removeShutdownHook(hook);
}
int exitValue = process.waitFor();
finished.set(true);
if (inThread != null) {
if (!systemIn) {
IO.close(in);
}
inThread.interrupt();
}
logger.debug("cmd {} executed with result={}, result: {}/{}, timedout={}", getArguments(), exitValue, stdout,
stderr, timedout);
if (timedout)
return TIMEDOUT;
return exitValue;
}
public void add(String arg) {
arguments.add(arg);
}
public void add(String... args) {
Collections.addAll(arguments, args);
}
public void addAll(Collection args) {
arguments.addAll(args);
}
public void setTimeout(long duration, TimeUnit unit) {
timeout = unit.toMillis(duration);
}
public void setTrace() {
this.trace = true;
}
public void setReporter(Reporter reporter) {
this.reporter = reporter;
}
public void setCwd(File dir) {
if (!dir.isDirectory())
throw new IllegalArgumentException("Working directory must be a directory: " + dir);
this.cwd = dir;
}
public void cancel() {
process.destroy();
}
private Runnable collector(InputStream in, Appendable sb) {
return () -> {
try {
for (int c; (c = in.read()) >= 0;) {
sb.append((char) c);
}
} catch (IOException e) {
// We assume the socket is closed
} catch (Exception e) {
try {
sb.append("\n**************************************\n");
sb.append(e.toString());
sb.append("\n**************************************\n");
} catch (IOException e1) {}
logger.debug("cmd exec", e);
}
};
}
public Command var(String name, String value) {
variables.put(name, value);
return this;
}
public Command arg(String arg) {
add(arg);
return this;
}
public Command arg(String... args) {
add(args);
return this;
}
public Command full(String full) {
arguments.clear();
new QuotedTokenizer(full, " \t", false, true).stream()
.filter(token -> !token.isEmpty())
.forEachOrdered(this::add);
return this;
}
public void inherit() {
ProcessBuilder pb = new ProcessBuilder();
var(pb.environment());
}
public String var(String name) {
return variables.get(name);
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
String del = "";
for (String argument : getArguments()) {
sb.append(del);
sb.append(argument);
del = " ";
}
return sb.toString();
}
public List getArguments() {
return arguments;
}
public void setUseThreadForInput(boolean useThreadForInput) {
this.useThreadForInput = useThreadForInput;
}
public void var(Map env) {
for (Map.Entry e : env.entrySet()) {
var(e.getKey(), e.getValue());
}
}
}