diff --git a/vscode-extensions/commons/application-properties-metadata/src/test/java/org/springframework/ide/vscode/boot/properties/metadata/PropertiesIndexTest.java b/vscode-extensions/commons/application-properties-metadata/src/test/java/org/springframework/ide/vscode/boot/properties/metadata/PropertiesIndexTest.java index 4c9775b14..289072cad 100644 --- a/vscode-extensions/commons/application-properties-metadata/src/test/java/org/springframework/ide/vscode/boot/properties/metadata/PropertiesIndexTest.java +++ b/vscode-extensions/commons/application-properties-metadata/src/test/java/org/springframework/ide/vscode/boot/properties/metadata/PropertiesIndexTest.java @@ -21,6 +21,8 @@ import java.nio.file.Paths; import org.junit.BeforeClass; import org.junit.Test; import org.springframework.ide.vscode.boot.properties.util.FuzzyMap; +import org.springframework.ide.vscode.util.ExternalCommand; +import org.springframework.ide.vscode.util.ExternalProcess; /** * Sanity test the boot properties index @@ -39,8 +41,8 @@ public class PropertiesIndexTest { Path testProjectPath = Paths.get(PropertiesIndexTest.class.getResource("/demo-1").toURI()); if (!Files.exists(testProjectPath.resolve("classpath.txt"))) { testProjectPath.resolve("mvnw").toFile().setExecutable(true); - int result = Runtime.getRuntime().exec("./mvnw clean package", null, testProjectPath.toFile()).waitFor(); - if (result != 0) { + ExternalProcess process = new ExternalProcess(testProjectPath.toFile(), new ExternalCommand("./mvnw", "clean", "package"), true); + if (process.getExitValue() != 0) { throw new RuntimeException("Failed to build test project"); } } diff --git a/vscode-extensions/commons/util-commons/src/main/java/org/springframework/ide/vscode/util/ExternalCommand.java b/vscode-extensions/commons/util-commons/src/main/java/org/springframework/ide/vscode/util/ExternalCommand.java new file mode 100644 index 000000000..b693ef566 --- /dev/null +++ b/vscode-extensions/commons/util-commons/src/main/java/org/springframework/ide/vscode/util/ExternalCommand.java @@ -0,0 +1,78 @@ +/******************************************************************************* + * Copyright (c) 2012, 2016 Pivotal Software, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.util; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; + +/** + * Encapsulates information about an 'external' command that can be run through the OS. + *

+ * This is a simplistic implementation. A more sophisticate implementation should allow for + * different OS's (commands may return different information depending on the OS). + * + * @author Kris De Volder + */ +public class ExternalCommand { + + private final String[] command; + + public ExternalCommand(String... command) { + ArrayList pieces = new ArrayList(command.length); + for (String piece : command) { + if (piece!=null) { + pieces.add(piece); + } + } + this.command = pieces.toArray(new String[pieces.size()]); + } + + public String[] getProgramAndArgs() { + return command; + } + + @Override + public String toString() { + StringBuffer buf = new StringBuffer(); + boolean first = true; + for (String piece : command) { + if (!first) { + buf.append(" "); + } + buf.append(piece); + first = false; + } + return buf.toString(); + } + + /** + * Just before executing the command with a ProcessBuilder instance, this method is called, + * giving the command a chance to apply some extra configuration (e.g. set some environment + * parameters). + */ + public void configure(ProcessBuilder processBuilder) { + //Default implementation does nothing. Subclasses may override. + } + + /** + * A convenient way to execute commands suitable for use in tests. The output and + * result of commands are logged to the console and if the command returns non + * 0 exit value an exception is thrown. + */ + public void exec(File workdir) throws IOException, InterruptedException { + System.out.println(">>> exec: "+this); + ExternalProcess process = new ExternalProcess(workdir, this); + System.out.println(process); +// org.junit.Assert.assertEquals(0, process.getExitValue()); + } + +} diff --git a/vscode-extensions/commons/util-commons/src/main/java/org/springframework/ide/vscode/util/ExternalProcess.java b/vscode-extensions/commons/util-commons/src/main/java/org/springframework/ide/vscode/util/ExternalProcess.java new file mode 100644 index 000000000..68c0b6030 --- /dev/null +++ b/vscode-extensions/commons/util-commons/src/main/java/org/springframework/ide/vscode/util/ExternalProcess.java @@ -0,0 +1,180 @@ +/******************************************************************************* + * Copyright (c) 2012, 2016 Pivotal Software, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.util; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintStream; + + +/** + * Convenient wrapper around a {@link Process}. Simplifies the synchronous execution of external + * commands by handling reading from out and error streams and either buffering the result output + * for later retrieval, or forwarding the to output to designated streams. + * + * @author Kris De Volder + */ +public class ExternalProcess { + + /** + * A thread that keeps reading input from a Stream until the end is reached + * or there's some error reading the Stream. + */ + public static class StreamGobler extends Thread { + + private final OutputStream echo; + private InputStream toRead; //Stream to read. This is nulled after all input has been consumed. + + /** + * Creates a StreamGobler that reads input from an input stream + * and buffers up all input it has read for later retrieval via + * the getOut() method. + */ + public StreamGobler(InputStream toRead) { + this(toRead, new ByteArrayOutputStream()); + } + + /** + * Creates a StreamGobler that reads input from an input stream + * and writes it out to an outputstream. + */ + public StreamGobler(InputStream toRead, OutputStream forwardTo) { + this.toRead = toRead; + this.echo = forwardTo; + start(); + } + + @Override + public void run() { + byte[] buf = new byte[256]; + while (toRead!=null) { + try { + int i = toRead.read(buf); + if (i==-1) { + //EOF + toRead = null; //Done! + } else { + append(buf, i); + } + } catch (IOException e) { + toRead = null; + ByteArrayOutputStream errMsg = new ByteArrayOutputStream(); + e.printStackTrace(new PrintStream(errMsg)); + append(errMsg.toByteArray()); + } + } + } + + private void append(byte[] buf) { + append(buf, buf.length); + } + + private void append(byte[] buf, int len) { + if (echo!=null) { + try { + echo.write(buf, 0, len); + } catch (IOException e) { + } + } + } + + public String getContents() throws InterruptedException { + try { + this.join(); + if (echo instanceof ByteArrayOutputStream) { + return ((ByteArrayOutputStream)echo).toString(); + } else { + return null; + } + } finally { + toRead = null; + } + } + } + + private Process process; + private StreamGobler err; // Standard error is to be read from here + private StreamGobler out; // Standard out is to be read from here + private int exitValue = -9999; + private ExternalCommand cmd; + + /** + * Creates an external process and waits for it to terminate. The output and error streams + * will be read and forwarded to System.out and System.err + */ + public ExternalProcess(File workingDir, ExternalCommand cmd) throws IOException, InterruptedException { + this(workingDir, cmd, false); + } + + private void init(File workingDir, ExternalCommand cmd, + OutputStream outStream, OutputStream errStream) throws IOException, + InterruptedException { + this.cmd = cmd; + ProcessBuilder processBuilder = new ProcessBuilder(cmd.getProgramAndArgs()); + processBuilder.directory(workingDir); + cmd.configure(processBuilder); + process = processBuilder.start(); + err = new StreamGobler(process.getErrorStream(), errStream); + out = new StreamGobler(process.getInputStream(), outStream); + exitValue = process.waitFor(); + } + + public ExternalProcess(File workingDir, ExternalCommand cmd, boolean captureStreams) throws IOException, InterruptedException { + if (captureStreams) { + init(workingDir, cmd, new ByteArrayOutputStream(), new ByteArrayOutputStream()); + } else { + init(workingDir, cmd, System.out, System.err); + } + } + + public String getOut() throws InterruptedException { + return out.getContents(); + } + + public String getErr() throws InterruptedException { + return err.getContents(); + } + + @Override + public String toString() { + StringBuffer result = new StringBuffer(); + try { + process.exitValue(); + result.append(">>>> ExternalProcess: "); + result.append(cmd+"\n"); + result.append("exitValue = "+exitValue+"\n"); + String strOut = getOut(); + if (strOut!=null) { + result.append("\n------- System.out -------\n"); + result.append(strOut); + } + String strErr = getErr(); + if (strErr!=null) { + result.append("\n------- System.err -------\n"); + result.append(getOut()); + } + result.append("<<<< ExternalProcess"); + return result.toString(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return result.toString(); + } catch (IllegalThreadStateException e) { + return "ExternalProcess(RUNNING, "+cmd+")"; + } + } + + public int getExitValue() { + return exitValue; + } +}