diff --git a/headless-services/commons/commons-boot-app-cli/.classpath b/headless-services/commons/commons-boot-app-cli/.classpath index af1430be1..950e0aca6 100644 --- a/headless-services/commons/commons-boot-app-cli/.classpath +++ b/headless-services/commons/commons-boot-app-cli/.classpath @@ -22,5 +22,10 @@ + + + + + diff --git a/headless-services/commons/commons-boot-app-cli/pom.xml b/headless-services/commons/commons-boot-app-cli/pom.xml index 8f52e59ee..26bf14df9 100644 --- a/headless-services/commons/commons-boot-app-cli/pom.xml +++ b/headless-services/commons/commons-boot-app-cli/pom.xml @@ -32,6 +32,12 @@ system ${java.home}/../lib/tools.jar + + commons-util + org.springframework.ide.vscode + ${project.version} + test + diff --git a/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/SpringBootApp.java b/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/SpringBootApp.java index 5155f5c2f..e011615d9 100644 --- a/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/SpringBootApp.java +++ b/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/SpringBootApp.java @@ -35,7 +35,6 @@ import com.sun.tools.attach.VirtualMachineDescriptor; /** * @author Martin Lippert */ -@SuppressWarnings("restriction") public class SpringBootApp { private VirtualMachine vm; @@ -321,4 +320,9 @@ public class SpringBootApp { return null; } + + @Override + public String toString() { + return "SpringBootApp [" +vmd.id() + ", "+vmd.displayName()+"]"; + } } diff --git a/headless-services/commons/commons-boot-app-cli/src/test/java/org/springframework/ide/vscode/commons/boot/app/cli/SpringBootAppTest.java b/headless-services/commons/commons-boot-app-cli/src/test/java/org/springframework/ide/vscode/commons/boot/app/cli/SpringBootAppTest.java new file mode 100644 index 000000000..13d3e5c41 --- /dev/null +++ b/headless-services/commons/commons-boot-app-cli/src/test/java/org/springframework/ide/vscode/commons/boot/app/cli/SpringBootAppTest.java @@ -0,0 +1,69 @@ +/******************************************************************************* + * Copyright (c) 2017 Pivotal, 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, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.commons.boot.app.cli; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.net.URL; +import java.util.Map; +import java.util.Optional; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Ignore; +import org.junit.Test; +import org.springframework.ide.vscode.commons.util.AsyncProcess; +import org.springframework.ide.vscode.commons.util.ExternalCommand; + +public class SpringBootAppTest { + + private static final String appName = "actuator-client-test-subject"; + private static AsyncProcess testAppRunner; + + @BeforeClass + public static void setupClass() throws Exception { + testAppRunner = startTestApplication(SpringBootAppTest.class.getResource("/"+appName+"-0.0.1-SNAPSHOT.jar")); + + //TODO: add some wait here until the boot app is 'ready'. Otherwise some of our test will just fail trying to read stuff that's not there yet. + } + + private static AsyncProcess startTestApplication(URL jarUrl) throws Exception { + File jarFile = new File(jarUrl.toURI()); + return new AsyncProcess(new File("."), new ExternalCommand( + "java", + "-Dserver.port=0", //let spring boot pick randomized free port + "-jar", + jarFile.getAbsolutePath() + )); + } + + @AfterClass + public static void tearDownClass() throws Exception { + testAppRunner.kill(); + } + + @Test + public void getAllJavaApps() throws Exception { + Map allApps = SpringBootApp.getAllRunningJavaApps(); + Optional myProcess = allApps.values().stream().filter(app -> app.getProcessName().contains(appName)).findAny(); + assertTrue(myProcess.isPresent()); + } + + @Ignore //Failing... not sure how to fix. + @Test public void getAllBootApps() throws Exception { + Map allApps = SpringBootApp.getAllRunningSpringApps(); + Optional myProcess = allApps.values().stream().filter(app -> app.getProcessName().contains(appName)).findAny(); + assertTrue(myProcess.isPresent()); + } + +} diff --git a/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/AsyncProcess.java b/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/AsyncProcess.java new file mode 100644 index 000000000..c8cec1c3b --- /dev/null +++ b/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/AsyncProcess.java @@ -0,0 +1,228 @@ +/******************************************************************************* + * 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.commons.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; +import java.time.Duration; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + + +/** + * Convenient wrapper around a {@link Process}. Simplifies the asynchronous 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. + *

+ * This provides similar apis than {@link ExternalProcess}. But has been adapted so that creation + * of the instance starts the process asynchronously. Additional apis are also provided to + * wait for the process and/or terminate it forcibly when no longer needed. + * + * @author Kris De Volder + */ +public class AsyncProcess { + + //TODO: much of this code copied from ExternalProcess. It should be relatively easy + // to make 'ExternalProcess inherit from this one and just add some additional 'waitFor' call + // in its 'init' method. + + /** + * 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 AsyncProcess(File workingDir, ExternalCommand cmd) throws IOException { + this(workingDir, cmd, false); + } + + private void init(File workingDir, ExternalCommand cmd, + OutputStream outStream, OutputStream errStream + ) throws IOException { + 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); + } + + public AsyncProcess(File workingDir, ExternalCommand cmd, boolean captureStreams) throws IOException { + 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(); + } + + /** + * Wait for the process to exit. If a timeout is specified and the process + * does not terminate within that time, the process is destroyed forcibly. + *

+ * If timeout is not specified (i.e. null) then this waits for as long as necessary + * (which is potentially forever if the process does not terminate). + */ + public int waitFor(Duration timeout) throws InterruptedException, TimeoutException { + try { + if (timeout==null) { + exitValue = process.waitFor(); + } else { + if (process.waitFor(timeout.toMillis(), TimeUnit.MILLISECONDS)) { + exitValue = process.exitValue(); + } else { + process.destroy(); + exitValue = 999; //Set some non-0 value as that is what some callers might use to determine 'failure' occurred. + throw new TimeoutException("Command timed out: "+this); + } + } + return exitValue; + } finally { + try { + //Try destroying unresponsive processes no matter what. + process.destroyForcibly(); + } catch (Exception e) { + //ignore + } + } + } + + @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("------- System.out -------\n"); + result.append(strOut); + } + String strErr = getErr(); + if (strErr!=null) { + result.append("------- System.err -------\n"); + result.append(strErr); + } + 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; + } + + public void kill() { + try { + waitFor(Duration.ofSeconds(0)); + } catch (Exception e) { + } + } +}