/**
* Copyright 2014 Google Inc. All Rights Reserved.
*/
package com.google.appengine.gcloudapp;
import static com.google.common.base.Charsets.UTF_8;
import com.google.appengine.SdkResolver;
import com.google.appengine.Utils;
import com.google.appengine.repackaged.com.google.common.io.Files;
import com.google.appengine.tools.admin.AppCfg;
import com.google.apphosting.utils.config.AppEngineApplicationXml;
import com.google.apphosting.utils.config.AppEngineApplicationXmlReader;
import com.google.apphosting.utils.config.AppEngineWebXml;
import com.google.apphosting.utils.config.AppEngineWebXmlReader;
import com.google.apphosting.utils.config.EarHelper;
import com.google.common.base.Charsets;
import com.google.common.base.Joiner;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.URL;
import java.net.URLConnection;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.concurrent.CountDownLatch;
import org.apache.commons.io.FileUtils;
import org.apache.maven.model.Plugin;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.project.MavenProject;
import org.codehaus.plexus.util.xml.Xpp3Dom;
import org.eclipse.aether.RepositorySystem;
import org.eclipse.aether.RepositorySystemSession;
import org.eclipse.aether.repository.RemoteRepository;
import org.ini4j.Ini;
/**
* @author ludo
*/
public abstract class AbstractGcloudMojo extends AbstractMojo {
/**
* @parameter property="project"
* @required
* @readonly
*/
protected MavenProject maven_project;
/**
* gcloud installation gcloud_directory
*
* @parameter expression="${gcloud.gcloud_directory}"
*/
protected String gcloud_directory;
/**
* docker_host
*
* @parameter expression="${gcloud.docker_host}" default-value="ENV_or_default"
*/
protected String docker_host;
/**
* docker_tls_verify
*
* @parameter expression="${gcloud.docker_tls_verify}" default-value="ENV_or_default"
*/
protected String docker_tls_verify;
/**
* docker_host_cert_path
*
* @parameter expression="${gcloud.docker_cert_path}" default-value="ENV_or_default"
*/
protected String docker_cert_path;
/**
* Override the default verbosity for this command. This must be a standard logging verbosity
* level: [debug, info, warning, error, critical, none] (Default: [info]).
*
* @parameter expression="${gcloud.verbosity}"
*/
protected String verbosity;
/**
* Google Cloud Platform gcloud_project to use for this invocation.
*
* @parameter expression="${gcloud.gcloud_project}"
*/
protected String gcloud_project;
/**
* version The version of the app that will be created or replaced by this deployment.
*
* @parameter expression="${gcloud.version}"
*/
protected String version;
/**
* Quiet mode, if true does not ask to perform the action.
*
* @parameter expression="${gcloud.quiet}" default-value="true"
*/
protected boolean quiet = true;
/**
* The location of the appengine application to run.
*
* @parameter expression="${gcloud.application_directory}"
*/
protected String application_directory;
/**
* Use this option if you are deploying using a remote docker host.
*
* @parameter expression="${gcloud.remote}"
*/
protected boolean remote;
/**
* Perform a hosted (´remote´) or local Docker build. To perform a local build, you must have your
* local docker environment configured correctly.
*
* @parameter expression="${gcloud.docker_build}"
*/
protected String docker_build;
/**
* The directory for the Staging phase. It has to be under target/ and is deleted at each run or
* deploy command.
*
* @parameter expression="${gcloud.staging_directory}" default-value="${project.build.directory}/appengine-staging"
*/
protected String staging_directory;
/**
* specify the default runtime you would like to use. Valid runtimes are ['java', 'php55',
* 'python', 'custom', 'python-compat', 'java7', 'python27', 'go']. (default: )
*
* @parameter expression="${gcloud.runtime}"
*/
protected String runtime;
/**
* server The App Engine server to connect to.
*
* @parameter expression="${gcloud.server}"
*/
protected String server;
/**
* force Force deploying, overriding any previous in-progress deployments to this version.
*
* @parameter expression="${gcloud.force}"
*/
protected boolean force;
/**
* Set the encoding to be used when compiling Java source files (default "UTF-8")
*
* @parameter expression="${gcloud.compile_encoding}"
*/
protected String compile_encoding;
/**
* Delete the JSP source files after compilation
*
* @parameter expression="${gcloud.delete_jsps}"
*/
protected boolean delete_jsps;
/**
* Do not jar the classes generated from JSPs
*
* @parameter expression="${gcloud.disable_jar_jsps}"
*/
protected boolean disable_jar_jsps;
/**
* Jar the WEB-INF/classes content
*
* @parameter expression="${gcloud.enable_jar_classes}"
*/
protected boolean enable_jar_classes;
/**
* Split large jar files (> 32M) into smaller fragments
*
* @parameter expression="${gcloud.enable_jar_splitting}"
*/
protected boolean enable_jar_splitting;
/**
* Do not use symbolic links when making the temporary (staging) gcloud_directory used in
* uploading Java apps
*
* @parameter expression="${gcloud.no_symlinks}"
*/
protected boolean no_symlinks;
/**
* Do not delete temporary (staging) gcloud_directory used in uploading Java apps
*
* @parameter expression="${gcloud.retain_upload_dir}"
*/
protected boolean retain_upload_dir;
/**
* When --enable-jar-splitting is specified and --jar-splitting-excludes specifies a
* comma-separated list of suffixes, a file in a jar whose name ends with one of the suffixes will
* not be included in the split jar fragments
*
* @parameter expression="${gcloud.jar_splitting_excludes}"
*/
protected String jar_splitting_excludes;
/**
* Tell if the command will be for run or deploy. Default is false: command is for `gcloud run`.
*/
protected boolean deployCommand = false;
/**
* Directory containing the App Engine app.yaml/Dockerfile files.
*
* @parameter expression="${gcloud.appengine_config_directory}"
* default-value="${project.basedir}/src/main/appengine"
*/
protected String appengine_config_directory;
/**
* Defines which gcloud app command you want (i.e preview or beta or nothing).
* By default, the plugin is excecuting "gcloud app", but you can select the
* prefix to execute "gcloud preview app" or "gcloud beta app".
*
* @parameter expression="${gcloud.gcloud_app_prefix}"
*/
protected String gcloud_app_prefix;
protected abstract ArrayList<String> getCommand(String appDir) throws MojoExecutionException;
protected ArrayList<String> setupInitialCommands(ArrayList<String> commands)
throws MojoExecutionException {
String pythonLocation = Utils.getPythonExecutableLocation();
commands.add(pythonLocation);
if (Utils.canDisableImportOfPythonModuleSite()) {
commands.add("-S");
}
if (gcloud_directory == null) {
gcloud_directory = Utils.getCloudSDKLocation();
}
File s = new File(gcloud_directory);
File script = new File(s, "/lib/googlecloudsdk/gcloud/gcloud.py");
if (!script.exists()) {
script = new File(s, "/lib/gcloud.py");
}
if (!script.exists()) {
getLog().error("Cannot determine the default location of the Google Cloud SDK.");
getLog().error(
"If you need to install the Google Cloud SDK, follow the instructions located at "
+ "https://cloud.google.com/appengine/docs/java/managed-vms");
getLog()
.error("You can then set it via <gcloud_directory> </gcloud_directory> in the pom.xml");
throw new MojoExecutionException("Unknown Google Cloud SDK location: " + gcloud_directory);
}
if (deployCommand) {
commands.add(script.getAbsolutePath());
if (quiet) {
commands.add("--quiet");
}
if (verbosity != null) {
commands.add("--verbosity=" + verbosity);
}
if (gcloud_project != null) {
commands.add("--project=" + gcloud_project);
}
if (gcloud_app_prefix != null) {
commands.add(gcloud_app_prefix);
}
commands.add("app");
} else { // run command
File devServer = new File(gcloud_directory + "/platform/google_appengine/dev_appserver.py");
// Check if we need to install the app-engine-java component!
if (!devServer.exists()) {
installJavaAppEngineComponent(pythonLocation);
}
commands.add(gcloud_directory + "/platform/google_appengine/dev_appserver.py");
commands.add("--skip_sdk_update_check=true");
if (verbosity != null) {
commands.add("--dev_appserver_log_level=" + verbosity);
}
if (gcloud_project != null) {
commands.add("-A");
commands.add(gcloud_project);
} else {
commands.add("-A");
commands.add("app"); // local sdk default project name
}
}
return commands;
}
protected enum WaitDirective {
WAIT_SERVER_STARTED,
WAIT_SERVER_STOPPED
}
protected void startCommand(File appDirFile, ArrayList<String> devAppServerCommand,
WaitDirective waitDirective) throws MojoExecutionException {
getLog().info("Running " + Joiner.on(" ").join(devAppServerCommand));
Thread stdOutThread;
Thread stdErrThread;
try {
ProcessBuilder processBuilder = new ProcessBuilder(devAppServerCommand);
processBuilder.directory(appDirFile);
processBuilder.redirectErrorStream(true);
Map<String, String> env = processBuilder.environment();
String env_docker_host = env.get("DOCKER_HOST");
String docker_host_tls_verify = env.get("DOCKER_TLS_VERIFY");
String docker_host_cert_path = env.get("DOCKER_CERT_PATH");
boolean userDefined = (env_docker_host != null)
|| (docker_host_tls_verify != null)
|| (docker_host_cert_path != null);
if (!userDefined) {
if ("ENV_or_default".equals(docker_host)) {
if (env_docker_host == null) {
if (env.get("DEVSHELL_CLIENT_PORT") != null) {
// we know we have a good chance to be in an old Google devshell:
env_docker_host = "unix:///var/run/docker.sock";
} else {
// we assume docker machine environment (Windows, Mac, and some Linux)
env_docker_host = "tcp://192.168.99.100:2376";
}
}
} else {
env_docker_host = docker_host;
}
env.put("DOCKER_HOST", env_docker_host);
// we handle TLS extra variables only when we are tcp:
if (env_docker_host.startsWith("tcp")) {
if ("ENV_or_default".equals(docker_tls_verify)) {
if (env.get("DOCKER_TLS_VERIFY") == null) {
env.put("DOCKER_TLS_VERIFY", "1");
}
} else {
env.put("DOCKER_TLS_VERIFY", docker_tls_verify);
}
// do not set the cert path if we do a dockerless deploy command:
boolean dockerless = deployCommand && remote;
if (!dockerless) {
if ("ENV_or_default".equals(docker_cert_path)) {
if (env.get("DOCKER_CERT_PATH") == null) {
env.put("DOCKER_CERT_PATH",
System.getProperty("user.home")
+ File.separator
+ ".docker"
+ File.separator
+ "machine"
+ File.separator
+ "machines"
+ File.separator
+ "default"
);
}
} else {
env.put("DOCKER_CERT_PATH", docker_cert_path);
}
}
}
}
// for the docker library path:
env.put("PYTHONPATH", gcloud_directory + "/platform/google_appengine/lib/docker");
final Process devServerProcess = processBuilder.start();
final CountDownLatch waitStartedLatch = new CountDownLatch(1);
final Scanner stdOut = new Scanner(devServerProcess.getInputStream());
stdOutThread = new Thread("standard-out-redirection-devappserver") {
@Override
public void run() {
boolean serverStartedOK = false;
try {
long healthCount = 0;
while (stdOut.hasNextLine() && !Thread.interrupted()) {
String line = stdOut.nextLine();
// emit this every 30 times, no need for more...
if (line.contains("GET /_ah/health?IsLastSuccessful=yes HTTP/1.1\" 200 2")) {
waitStartedLatch.countDown();
if (healthCount % 20 == 0) {
getLog().info(line);
}
healthCount++;
} else if (line.contains("Dev App Server is now running")) {
// App Engine V1
waitStartedLatch.countDown();
serverStartedOK = true;
} else if (line.contains("INFO:oejs.Server:main: Started")) {
// App Engine V2
waitStartedLatch.countDown();
serverStartedOK = true;
} else {
getLog().info(line);
}
}
} finally {
waitStartedLatch.countDown();
if ((!serverStartedOK) && (!deployCommand)) {
throw new RuntimeException("The Java Dev Server has stopped.");
}
}
}
};
stdOutThread.setDaemon(true);
stdOutThread.start();
final Scanner stdErr = new Scanner(devServerProcess.getErrorStream());
stdErrThread = new Thread("standard-err-redirection-devappserver") {
@Override
public void run() {
while (stdErr.hasNextLine() && !Thread.interrupted()) {
getLog().error(stdErr.nextLine());
}
}
};
stdErrThread.setDaemon(true);
stdErrThread.start();
if (waitDirective == WaitDirective.WAIT_SERVER_STOPPED) {
Runtime.getRuntime().addShutdownHook(new Thread("destroy-devappserver") {
@Override
public void run() {
if (devServerProcess != null) {
devServerProcess.destroy();
}
}
});
devServerProcess.waitFor();
int status = devServerProcess.exitValue();
if (status != 0) {
getLog().error("Error: gcloud app command with exit code : " + status);
throw new MojoExecutionException("Error: gcloud app command exit code is: " + status);
}
} else if (waitDirective == WaitDirective.WAIT_SERVER_STARTED) {
waitStartedLatch.await();
getLog().info("");
getLog().info("App Engine Dev Server started in Async mode and running.");
getLog().info("you can stop it with this command: mvn gcloud:run_stop");
}
} catch (IOException e) {
throw new MojoExecutionException("Could not start the dev app server", e);
} catch (InterruptedException e) {
}
}
protected String getApplicationDirectory() throws MojoExecutionException {
if (application_directory != null) {
return application_directory;
}
application_directory =
maven_project.getBuild().getDirectory() + "/" + maven_project.getBuild().getFinalName();
File appDirFile = new File(application_directory);
if (!appDirFile.exists()) {
throw new MojoExecutionException(
"The application directory does not exist : " + application_directory);
}
if (!appDirFile.isDirectory()) {
throw new MojoExecutionException(
"The application directory is not a directory : " + application_directory);
}
return application_directory;
}
protected String getProjectIdfromMetaData() {
try {
URL url = new URL("http://metadata/computeMetadata/v1/project/project-id");
URLConnection connection = url.openConnection();
connection.setRequestProperty("X-Google-Metadata-Request", "True");
try (BufferedReader reader
= new BufferedReader(new InputStreamReader(
connection.getInputStream(), UTF_8))) {
return reader.readLine();
}
} catch (IOException ignore) {
// return null if can't determine
return null;
}
}
protected String getAppId() throws MojoExecutionException {
if (gcloud_project != null) {
return gcloud_project;
}
try { // Check for Cloud SDK properties:
String userHome;
if (System.getProperty("os.name").contains("Windows")) {
userHome = System.getenv("APPDATA");
} else {
userHome = System.getProperty("user.home") + "/.config";
}
// Default value:
File cloudSDKProperties = new File(userHome + "/gcloud/properties");
// But can be overridden: take this one if it is:
String env = System.getenv("CLOUDSDK_CONFIG");
if (env != null) {
cloudSDKProperties = new File(env, "properties");
}
if (cloudSDKProperties.exists()) {
org.ini4j.Ini ini = new org.ini4j.Ini();
ini.load(new FileReader(cloudSDKProperties));
Ini.Section section = ini.get("core");
String project = section.get("project");
if (project != null) {
getLog().info("Getting project name: " + project
+ " from gcloud settings.");
return project;
}
}
// now try the metadata server location:
String project = getProjectIdfromMetaData();
if (project != null) {
getLog().info("Getting project name: " + project
+ " from the metadata server.");
return project;
}
} catch (IOException ioe) {
// nothing for now. Trying to read appengine-web.xml.
}
String appDir = getApplicationDirectory();
if (EarHelper.isEar(appDir)) { // EAR project
AppEngineApplicationXmlReader reader
= new AppEngineApplicationXmlReader();
AppEngineApplicationXml appEngineApplicationXml = reader.processXml(
getInputStream(new File(appDir, "META-INF/appengine-application.xml")));
return appEngineApplicationXml.getApplicationId();
}
if (new File(appDir, "WEB-INF/appengine-web.xml").exists()) {
return getAppEngineWebXml(appDir).getAppId();
} else {
return null;
}
}
private static InputStream getInputStream(File file) {
try {
return new FileInputStream(file);
} catch (FileNotFoundException fnfe) {
throw new IllegalStateException("File should exist - '" + file + "'");
}
}
protected AppEngineWebXml getAppEngineWebXml(String webAppDir) throws MojoExecutionException {
AppEngineWebXmlReader reader = new AppEngineWebXmlReader(webAppDir);
AppEngineWebXml appengineWebXml = reader.readAppEngineWebXml();
return appengineWebXml;
}
/**
* The entry point to Aether, i.e. the component doing all the work.
*
* @component
*/
protected RepositorySystem repoSystem;
/**
* The current repository/network configuration of Maven.
*
* @parameter default-value="${repositorySystemSession}"
* @readonly
*/
protected RepositorySystemSession repoSession;
/**
* The project's remote repositories to use for the resolution of project dependencies.
*
* @parameter default-value="${project.remoteProjectRepositories}"
* @readonly
*/
protected List<RemoteRepository> projectRepos;
/**
* The project's remote repositories to use for the resolution of plugins and their dependencies.
*
* @parameter default-value="${project.remotePluginRepositories}"
* @readonly
*/
protected List<RemoteRepository> pluginRepos;
protected void resolveAndSetSdkRoot() throws MojoExecutionException {
File sdkBaseDir = SdkResolver
.getSdk(maven_project, repoSystem, repoSession, pluginRepos, projectRepos);
try {
System.setProperty("appengine.sdk.root", sdkBaseDir.getCanonicalPath());
} catch (IOException e) {
throw new MojoExecutionException("Could not open SDK zip archive.", e);
}
}
/**
* @return the java version used the pom (target) and 1.7 if not present.
*/
protected String getJavaVersion() {
String javaVersion = "1.7";
Plugin p = maven_project.getPlugin("org.apache.maven.plugins:maven-compiler-plugin");
if (p != null) {
Xpp3Dom config = (Xpp3Dom) p.getConfiguration();
if (config == null) {
return javaVersion;
}
Xpp3Dom domVersion = config.getChild("target");
if (domVersion != null) {
javaVersion = domVersion.getValue();
}
}
return javaVersion;
}
protected File executeAppCfgStagingCommand(String appDir)
throws MojoExecutionException {
ArrayList<String> arguments = new ArrayList<>();
File destinationDir = new File(staging_directory);
if (!destinationDir.getParentFile().getAbsolutePath()
.equals(maven_project.getBuild().getDirectory())) {
throw new MojoExecutionException(
"Does not want to delete a directory no under the target directory" + staging_directory);
}
try {
FileUtils.deleteDirectory(destinationDir);
} catch (IOException ex) {
throw new MojoExecutionException("Cannot delete staging directory.", ex);
}
getLog().info("Creating staging directory in: " + destinationDir.getAbsolutePath());
resolveAndSetSdkRoot();
AppEngineWebXml appengineWeb = getAppEngineWebXml(appDir);
if ("true".equals(appengineWeb.getBetaSettings().get("java_quickstart"))) {
arguments.add("--enable_quickstart");
}
arguments.add("--disable_update_check");
File appDirFile = new File(appDir);
if (!new File(appDirFile, ".appyamlgenerated").exists()) {
PrintWriter out;
try {
out = new PrintWriter(new File(appDirFile, ".appyamlgenerated"));
DateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
Date date = new Date();
System.out.println(dateFormat.format(date));
out.println("generated by the Maven Plugin on " + dateFormat.format(date));
out.close();
} catch (FileNotFoundException ex) {
throw new MojoExecutionException("Error: generating .appyamlgenerated " + ex);
}
}
arguments.add("-A");
arguments.add("notused");
if (version != null && !version.isEmpty()) {
arguments.add("-V");
arguments.add(version);
}
if (server != null && !server.isEmpty()) {
arguments.add("-s");
arguments.add(server);
}
if (gcloud_project != null) {
arguments.add("-A");
arguments.add(gcloud_project);
}
if (enable_jar_splitting) {
arguments.add("--enable_jar_splitting");
}
if (jar_splitting_excludes != null && !jar_splitting_excludes.isEmpty()) {
arguments.add("--jar_splitting_excludes=" + jar_splitting_excludes);
}
if (retain_upload_dir) {
arguments.add("--retain_upload_dir");
}
if (compile_encoding != null) {
arguments.add("--compile-encoding=" + compile_encoding);
}
if (force) {
arguments.add("-f");
}
if (delete_jsps) {
arguments.add("--delete_jsps");
}
if (enable_jar_classes) {
arguments.add("--enable_jar_classes");
}
if (runtime != null) {
arguments.add("--runtime=" + runtime);
}
/* Complicated matrix...
vm:true java7 in pom ok ->runtime:java7
vm:false java8 in pom ok ->error for now (unless override in runtime flag)
vm:true java8 in pm , generate a dockerfile and runtime:custom
env:2 java 7in pom ->ok, runtime is java
env:2 java8 in pom ->ok runtime is java
*/
boolean isVm = appengineWeb.getUseVm();
boolean isStandard = ("1".equals(appengineWeb.getEnv())
|| "std".equals(appengineWeb.getEnv())) && isVm == false;
boolean isFlex = "2".equals(appengineWeb.getEnv())
|| "flex".equals(appengineWeb.getEnv())
|| "flexible".equals(appengineWeb.getEnv());
// config error: vm false
if (isStandard && getJavaVersion().equals("1.8")) {
throw new MojoExecutionException(
"For now, Standard GAE runtime only works with Java7, but the pom.xml is targetting 1.8");
}
// Forcing Java runtime for Flex or 1.8 , otherwise it is default Java7
// FYI: the 'java' runtime for Managed VMs is the real java8
if (isFlex) {
arguments.add("-R");
arguments.add("-r");
arguments.add("java");
} else if (isVm && getJavaVersion().equals("1.8")) {
arguments.add("-R");
arguments.add("-r");
arguments.add("java");
}
arguments.add("stage");
arguments.add(appDir);
arguments.add(destinationDir.getAbsolutePath());
getLog().info("Running appcfg " + Joiner.on(" ").join(arguments));
AppCfg.main(arguments.toArray(new String[arguments.size()]));
// For now, treat custom as java7 so that the app run command works.
try {
File fileAppYaml = new File(destinationDir, "/app.yaml");
String content = Files.toString(fileAppYaml, Charsets.UTF_8);
if (isVm && getJavaVersion().equals("1.8")) {
content = content.replace("runtime: java", "runtime: custom");
Files.write(content, fileAppYaml, Charsets.UTF_8);
File dockerFile = new File(destinationDir, "/Dockerfile");
if (!dockerFile.exists()) {
Files.write("FROM gcr.io/google_appengine/jetty9-compat\nADD . /app\n", dockerFile,
Charsets.UTF_8);
}
}
} catch (IOException ioe) {
System.out.println("Error " + ioe);
}
File[] yamlFiles = new File(destinationDir, "/WEB-INF/appengine-generated").listFiles();
for (File f : yamlFiles) {
try {
Files.copy(f, new File(appDir, f.getName()));
} catch (IOException ex) {
throw new MojoExecutionException("Error: copying yaml file " + ex);
}
}
File qs = new File(destinationDir, "/WEB-INF/quickstart-web.xml");
if (qs.exists()) {
try {
Files.copy(qs, new File(appDir, "/WEB-INF/quickstart-web.xml"));
} catch (IOException ex) {
throw new MojoExecutionException("Error: copying WEB-INF/quickstart-web.xml" + ex);
}
}
// Delete the xml as we have now the index.yaml equivalent
File index = new File(appDir, "/WEB-INF/datastore-indexes.xml");
if (index.exists()) {
index.delete();
}
return destinationDir;
}
/**
* Executes the gcloud components update app-engine-java command to install the extra component
* needed for the Maven plugin.
*/
private void installJavaAppEngineComponent(String pythonLocation) throws MojoExecutionException {
ArrayList<String> installCommand = new ArrayList<>();
installCommand.add(pythonLocation);
if (Utils.canDisableImportOfPythonModuleSite()) {
installCommand.add("-S");
}
installCommand.add(gcloud_directory + "/lib/gcloud.py");
installCommand.add("components");
installCommand.add("update");
installCommand.add("app-engine-java");
installCommand.add("--quiet");
ProcessBuilder pb = new ProcessBuilder(installCommand);
getLog().info("Installing the Cloud SDK app-engine-java component");
getLog().info("Please, be patient, it takes a while on slow network...");
try {
Process process = pb.start();
final Scanner stdOut = new Scanner(process.getInputStream());
new Thread("standard-out-redirection") {
@Override
public void run() {
while (stdOut.hasNextLine() && !Thread.interrupted()) {
getLog().info(stdOut.nextLine());
}
}
};
process.waitFor();
getLog().info("Cloud SDK app-engine-java component installed.");
} catch (IOException | InterruptedException ex) {
throw new MojoExecutionException("Error: cannot execute gcloud command " + ex);
}
}
}