diff --git a/.travis.yml b/.travis.yml index d7b848431..d9d9ed2fd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,14 @@ sudo: false language: java jdk: - openjdk7 +install: + # download Cloud SDK + - wget https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/google-cloud-sdk-124.0.0-linux-x86_64.tar.gz + - tar -xzvf google-cloud-sdk-124.0.0-linux-x86_64.tar.gz + # update all Cloud SDK components + - gcloud components update --quiet + # add App Engine component to Cloud SDK + - gcloud components install app-engine-java --quiet cache: directories: - $HOME/.m2 @@ -16,5 +24,7 @@ script: env: global: + - PATH=$PWD/google-cloud-sdk/bin:$PATH + - CLOUDSDK_CORE_DISABLE_USAGE_REPORTING=true - secure: "da45ryIB/m37Gkpon/Uy6PPl1AhsUMdxgG7aeQIM1alh3Np4JwqKHobdP8tC0Qc/vb5fqfisEZEGPukh9Kxw95pRATO/QrQrsxYISMXUUc+Dlvo8WLeDq5F2LdUSn3933H9p5Mk8bKrZql+Jb+Il5S+B3Ib8uO+e3L4itrGKu5dw6i8TAxMggUjK8L6RuRYZeXMNiw3iaLjlHhNVEZ7F7RQ/gsHT2LhzybY6gfVJU+8AHhwEv+Tuapz5QCYTah6pwKqP+EQPhFlfrow9zdBfQm7m4h+uU+TB67VzZi46pAx+drC1quW+HZllutMHKM+cR13HsTET9qbFmV00cr2gZ3UXMVPX+PG1johZj0gs8s/S0+MVh6IQ0QLMWSqgJH/ULlyoU9PLE+PMs8A2qprO8NALuS3GXgvMQ8tHGuAzUVht/CxH5WTxW8nGFv8lwCIrT+m0hWi26gXWpNItC4N5GawoJQp+eWW0dO2Uko34kC3CIkLppRGxgzQWQQHkR+Hh+Yi3AzZsUzz30YRezF+G1RuQ68Va9yZ9qgei3h0Ppx2OIZ6fyiGg6Z8MHzOA8AULoe/Sz7CsfCiOyjTWmccRSl79wc0kpZwrF2qLO46LD3DgpGcfcKEB/cqcdwj/SA4QJoRcwUPSqy2eXb6ILhHA9UGlSBwluPZ2tSXDh2zxCL0=" - secure: "Z/Haq0MHdfBpJKntdYZFdK3uxUUk+jjiNzuuOt2hLa7P5XxpoHsboRykWSY41tWD5NnXfXoJvcHPASwhz2OesQcyNCCEHxsikkNRk+XUgFjyu8I3Ohm8Fya1k9gn+Xlz9uDBtoGN1e2SHhfEY+a9a5nGvv4aLVblBewt9J14su8fc4WSyDYggoZnUjB69DzIJrBL/ol0VOqlo8/vrcbWRbB/hlJ3QwKsFkehIhx5//GVdEJGqM2CZNJqG/bVLaSIoqQPqd6v3eyemnS2O5bXNIoNEq9yUWGSm1PfnagBYMzkHcqNrg5D1aYI+vFFeZe4PAsBaE6Xw/trCMtdRr5uDKXdNW8s8zwMKfOYru3zY9KTq2N5Fl/HgazGfimpmAjXqpH74+WaITmQmCZ6LuLb7hgltF+f2OY71bHJI7lGeuCraiwb6ECFIZiF9+FA6m0DXU6fCsajoThUtkRSpTMxDTAMzlrH/Kw2SzwEfuGk3evsb937pLUkL1kNxSc6GXhdyRrLTSrYgoqavjtNUN5S7v7MqrmO4R6UF1ZtJOAxWJ1a4W/kQP6S/36u8dZhCWbds78EocOH/+fsJ8vlCmTAnFEbtZkSpmFHjGrpPu0gVYdG+u96eAtKv90p2IF8bwCzFQRh1U25OlcxHKZfDgGzplR1Cy8DdJoNcH3KJKQd1Cw=" diff --git a/pom.xml b/pom.xml index 6289064e0..cc6218b63 100644 --- a/pom.xml +++ b/pom.xml @@ -93,6 +93,11 @@ jsr305 3.0.1 + + org.json + json + 20160810 + diff --git a/src/main/java/com/google/cloud/tools/appengine/cloudsdk/AppEngineComponentsNotInstalledException.java b/src/main/java/com/google/cloud/tools/appengine/cloudsdk/AppEngineComponentsNotInstalledException.java new file mode 100644 index 000000000..e1ae963a0 --- /dev/null +++ b/src/main/java/com/google/cloud/tools/appengine/cloudsdk/AppEngineComponentsNotInstalledException.java @@ -0,0 +1,14 @@ +package com.google.cloud.tools.appengine.cloudsdk; + +import com.google.cloud.tools.appengine.api.AppEngineException; + +/** + * User needs to run gcloud components install app-engine-java. + */ +public class AppEngineComponentsNotInstalledException extends AppEngineException { + + AppEngineComponentsNotInstalledException(String message) { + super(message); + } + +} diff --git a/src/main/java/com/google/cloud/tools/appengine/cloudsdk/CloudSdk.java b/src/main/java/com/google/cloud/tools/appengine/cloudsdk/CloudSdk.java index 5e495d93b..32ef36836 100644 --- a/src/main/java/com/google/cloud/tools/appengine/cloudsdk/CloudSdk.java +++ b/src/main/java/com/google/cloud/tools/appengine/cloudsdk/CloudSdk.java @@ -30,6 +30,11 @@ import com.google.common.collect.Lists; import com.google.common.collect.Maps; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONTokener; + import java.io.File; import java.nio.file.Files; import java.nio.file.InvalidPathException; @@ -70,7 +75,7 @@ public class CloudSdk { @Nullable private final File appCommandCredentialFile; private final String appCommandOutputFormat; - private final WaitingProcessOutputLineListener runDevAppServerWaitListener; + private final WaitingProcessOutputLineListener outputLineListener; private CloudSdk(Path sdkPath, String appCommandMetricsEnvironment, @@ -78,14 +83,14 @@ private CloudSdk(Path sdkPath, @Nullable File appCommandCredentialFile, String appCommandOutputFormat, ProcessRunner processRunner, - WaitingProcessOutputLineListener runDevAppServerWaitListener) { + WaitingProcessOutputLineListener outputLineListener) { this.sdkPath = sdkPath; this.appCommandMetricsEnvironment = appCommandMetricsEnvironment; this.appCommandMetricsEnvironmentVersion = appCommandMetricsEnvironmentVersion; this.appCommandCredentialFile = appCommandCredentialFile; this.appCommandOutputFormat = appCommandOutputFormat; this.processRunner = processRunner; - this.runDevAppServerWaitListener = runDevAppServerWaitListener; + this.outputLineListener = outputLineListener; // Populate jar locations. // TODO(joaomartins): Consider case where SDK doesn't contain these jars. Only App Engine @@ -100,7 +105,7 @@ private CloudSdk(Path sdkPath, /** * Uses the process runner to execute the gcloud app command with the provided arguments. * - * @param args The arguments to pass to "gcloud app" command. + * @param args the arguments to pass to "gcloud app" command */ public void runAppCommand(List args) throws ProcessRunnerException { List command = new ArrayList<>(); @@ -108,7 +113,6 @@ public void runAppCommand(List args) throws ProcessRunnerException { command.add("app"); command.addAll(args); - command.add("--quiet"); command.addAll(GcloudArgs.get("format", appCommandOutputFormat)); Map environment = Maps.newHashMap(); @@ -125,6 +129,38 @@ public void runAppCommand(List args) throws ProcessRunnerException { logCommand(command); processRunner.setEnvironment(environment); processRunner.run(command.toArray(new String[command.size()])); + processRunner.run(command.toArray(new String[command.size()])); + } + + /** + * Checks whether the specified component is installed in the local environment. + * + * @return true iff the specified component is installed in the local environment + */ + public boolean isComponentInstalled(String id) throws ProcessRunnerException { + List command = new ArrayList<>(); + command.add(getGCloudPath().toString()); + command.add("components"); + command.add("list"); + command.add("--format=json"); + command.add("--filter=id:" + id); + + String json = processRunner.runSynchronously(command.toArray(new String[command.size()])); + + try { + JSONTokener tokener = new JSONTokener(json); + JSONArray array = new JSONArray(tokener); + if (array.length() == 0) { + return false; + } + JSONObject object = array.getJSONObject(0); + JSONObject state = object.getJSONObject("state"); + String name = state.getString("name"); + return "Installed".equals(name); + } catch (JSONException | NullPointerException ex) { + throw new AppEngineException( + "Could not determine whether App Engine Java component is installed", ex); + } } /** @@ -149,8 +185,8 @@ public void runDevAppServerCommand(List args) throws ProcessRunnerExcept processRunner.run(command.toArray(new String[command.size()])); // wait for start if configured - if (runDevAppServerWaitListener != null) { - runDevAppServerWaitListener.await(); + if (outputLineListener != null) { + outputLineListener.await(); } } @@ -231,9 +267,12 @@ public Path getJarPath(String jarName) { } /** - * Checks whether the configured Cloud SDK Path is valid. + * Checks whether the Cloud SDK is installed in the supplied path + * and the App Engine Java Components have been installed. * - * @throws AppEngineException when there is a validation error. + * @throws AppEngineComponentsNotInstalledException the App Engine Java + * components have not been installed + * @throws AppEngineException if a necessary component of the Cloud SDK cannot be found */ public void validate() throws AppEngineException { if (sdkPath == null) { @@ -253,9 +292,8 @@ public void validate() throws AppEngineException { + getDevAppServerPath() + "' is not a file."); } if (!Files.isDirectory(getJavaAppEngineSdkPath())) { - throw new AppEngineException( - "Validation Error: Java App Engine SDK location '" - + getJavaAppEngineSdkPath() + "' is not a directory."); + throw new AppEngineComponentsNotInstalledException( + "Validation Error: App Engine Java component not installed"); } if (!Files.isRegularFile(JAR_LOCATIONS.get(JAVA_TOOLS_JAR))) { throw new AppEngineException( @@ -266,7 +304,7 @@ public void validate() throws AppEngineException { @VisibleForTesting WaitingProcessOutputLineListener getRunDevAppServerWaitListener() { - return runDevAppServerWaitListener; + return outputLineListener; } public static class Builder { @@ -528,4 +566,5 @@ public int compare(CloudSdkResolver o1, CloudSdkResolver o2) { } } + } diff --git a/src/main/java/com/google/cloud/tools/appengine/cloudsdk/internal/process/AccumulatingLineListener.java b/src/main/java/com/google/cloud/tools/appengine/cloudsdk/internal/process/AccumulatingLineListener.java new file mode 100644 index 000000000..764babc0a --- /dev/null +++ b/src/main/java/com/google/cloud/tools/appengine/cloudsdk/internal/process/AccumulatingLineListener.java @@ -0,0 +1,38 @@ +/* + * Copyright 2016 Google Inc. + * + * 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 com.google.cloud.tools.appengine.cloudsdk.internal.process; + +import com.google.cloud.tools.appengine.cloudsdk.process.ProcessOutputLineListener; + +class AccumulatingLineListener implements ProcessOutputLineListener { + + private StringBuilder output = new StringBuilder(); + + @Override + public void onOutputLine(String line) { + output.append(line + "\n"); + } + + String getOutput() { + return output.toString(); + } + + public void clear() { + output = new StringBuilder(); + } + +} diff --git a/src/main/java/com/google/cloud/tools/appengine/cloudsdk/internal/process/DefaultProcessRunner.java b/src/main/java/com/google/cloud/tools/appengine/cloudsdk/internal/process/DefaultProcessRunner.java index 50e0d471c..816803a24 100644 --- a/src/main/java/com/google/cloud/tools/appengine/cloudsdk/internal/process/DefaultProcessRunner.java +++ b/src/main/java/com/google/cloud/tools/appengine/cloudsdk/internal/process/DefaultProcessRunner.java @@ -85,8 +85,9 @@ public DefaultProcessRunner(boolean async, *

If any output listeners were configured, output will go to them only. Otherwise, process * output will be redirected to the caller via inheritIO. * - * @param command The shell command to execute + * @param command the shell command to execute */ + @Override public void run(String[] command) throws ProcessRunnerException { try { // Configure process builder. @@ -131,10 +132,61 @@ public void run(String[] command) throws ProcessRunnerException { throw new ProcessRunnerException(e); } } + + /** + * Executes a not-too-long-lived shell command synchornously and returns stdout. + * + *

If any output listeners were configured, output will go to them only. Otherwise, process + * output will be redirected to the caller via inheritIO. + * + * @param command the shell command to execute + * @return everything printed on stdout + */ + @Override + public String runSynchronously(String[] command) throws ProcessRunnerException { + try { + // Configure process builder. + final ProcessBuilder processBuilder = new ProcessBuilder(); + + // If there are no listeners, we might still want to redirect stdout and stderr to the parent + // process, or not. + if (stdErrLineListeners.isEmpty() && inheritProcessOutput) { + processBuilder.redirectError(Redirect.INHERIT); + } + if (environment != null) { + processBuilder.environment().putAll(environment); + } + + processBuilder.command(command); + + Process process = processBuilder.start(); + AccumulatingLineListener stdOut = new AccumulatingLineListener(); + stdOutLineListeners.add(stdOut); + // Only handle stdout or stderr if there are listeners. + handleStdOut(process); + if (!stdErrLineListeners.isEmpty()) { + handleErrOut(process); + } + + for (ProcessStartListener startListener : startListeners) { + startListener.onStart(process); + } + + shutdownProcessHook(process); + syncRun(process); + stdOutLineListeners.remove(stdOut); + + return stdOut.getOutput(); + + } catch (IOException | InterruptedException | IllegalThreadStateException e) { + throw new ProcessRunnerException(e); + } + } /** * Environment variables to append to the current system environment variables. */ + @Override public void setEnvironment(Map environment) { this.environment = environment; } @@ -142,6 +194,7 @@ public void setEnvironment(Map environment) { private void handleStdOut(final Process process) { final Scanner stdOut = new Scanner(process.getInputStream(), Charsets.UTF_8.name()); Thread stdOutThread = new Thread("standard-out") { + @Override public void run() { while (stdOut.hasNextLine() && !Thread.interrupted()) { String line = stdOut.nextLine(); @@ -159,6 +212,7 @@ public void run() { private void handleErrOut(final Process process) { final Scanner stdErr = new Scanner(process.getErrorStream(), Charsets.UTF_8.name()); Thread stdErrThread = new Thread("standard-err") { + @Override public void run() { while (stdErr.hasNextLine() && !Thread.interrupted()) { String line = stdErr.nextLine(); diff --git a/src/main/java/com/google/cloud/tools/appengine/cloudsdk/internal/process/ProcessRunner.java b/src/main/java/com/google/cloud/tools/appengine/cloudsdk/internal/process/ProcessRunner.java index f261a66f2..440398040 100644 --- a/src/main/java/com/google/cloud/tools/appengine/cloudsdk/internal/process/ProcessRunner.java +++ b/src/main/java/com/google/cloud/tools/appengine/cloudsdk/internal/process/ProcessRunner.java @@ -22,6 +22,8 @@ public interface ProcessRunner { void run(String[] command) throws ProcessRunnerException; + + String runSynchronously(String[] command) throws ProcessRunnerException; void setEnvironment(Map environment); diff --git a/src/test/java/com/google/cloud/tools/appengine/cloudsdk/CloudSdkEnvironmentTest.java b/src/test/java/com/google/cloud/tools/appengine/cloudsdk/CloudSdkEnvironmentTest.java new file mode 100644 index 000000000..df78b4394 --- /dev/null +++ b/src/test/java/com/google/cloud/tools/appengine/cloudsdk/CloudSdkEnvironmentTest.java @@ -0,0 +1,40 @@ +package com.google.cloud.tools.appengine.cloudsdk; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.nio.file.Files; + +import org.junit.Test; + +import com.google.cloud.tools.appengine.cloudsdk.internal.process.ProcessRunnerException; + +/** + * Integration tests for {@link CloudSdk} that require an installed CloudSdk instance. + */ +public class CloudSdkEnvironmentTest { + + private CloudSdk sdk = new CloudSdk.Builder().build(); + + @Test + public void testGetSdkPath() { + assertTrue(Files.exists(sdk.getSdkPath())); + } + + @Test + public void testIsComponentInstalled_true() throws ProcessRunnerException { + assertTrue(sdk.isComponentInstalled("app-engine-java")); + } + + @Test + public void testIsComponentInstalled_False() throws ProcessRunnerException { + assertFalse(sdk.isComponentInstalled("no-such-component")); + } + + @Test + public void testIsComponentInstalled_sequential() throws ProcessRunnerException { + assertTrue(sdk.isComponentInstalled("app-engine-java")); + assertFalse(sdk.isComponentInstalled("no-such-component")); + } + +} diff --git a/src/test/java/com/google/cloud/tools/appengine/cloudsdk/CloudSdkTest.java b/src/test/java/com/google/cloud/tools/appengine/cloudsdk/CloudSdkTest.java index 4319dac03..cb454d67a 100644 --- a/src/test/java/com/google/cloud/tools/appengine/cloudsdk/CloudSdkTest.java +++ b/src/test/java/com/google/cloud/tools/appengine/cloudsdk/CloudSdkTest.java @@ -5,9 +5,8 @@ import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.when; -import com.google.cloud.tools.appengine.cloudsdk.CloudSdk.Builder; - import com.google.cloud.tools.appengine.api.AppEngineException; +import com.google.cloud.tools.appengine.cloudsdk.CloudSdk.Builder; import com.google.cloud.tools.appengine.cloudsdk.process.ProcessOutputLineListener; import org.junit.Test; import org.junit.runner.RunWith;