/** * Copyright 2014 Google Inc. All Rights Reserved. */ package com.google.appengine.gcloudapp; import com.google.appengine.repackaged.com.google.api.client.util.Throwables; import com.google.appengine.repackaged.com.google.common.io.ByteStreams; import java.io.File; import java.io.IOException; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.List; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; /** * Runs the App Engine development server. * * @author Ludo * @goal run * @execute phase="package" * @threadSafe false */ public class GCloudAppRun extends AbstractGcloudMojo { /** * The host and port on which to start the API server (in the format host:port) * * @parameter expression="${gcloud.api_host}" */ private String api_host; /** * Additional directories containing App Engine modules to be run. * * @parameter */ private List<String> modules; /** * The host and port on which to start the local web server (in the format host:port) * * @parameter expression="${gcloud.host}" */ private String host; /** * The host and port on which to start the admin server (in the format host:port) * * @parameter expression="${gcloud.admin_host}" */ private String admin_host; /** * The default location for storing application data. Can be overridden for specific kinds of data * using --datastore-path, --blobstore-path, and/or --logs-path * * @parameter expression="${gcloud.storage_path}" */ private String storage_path; /** * The minimum verbosity of logs from your app that will be displayed in the terminal. (debug, * info, warning, critical, error) Defaults to current verbosity setting. * * @parameter expression="${gcloud.log_level}" */ private String log_level; /** * Path to a file used to store request logs (defaults to a file in --storage-path if not set) * * @parameter expression="${gcloud.logs_path}" */ private String logs_path; /** * name of the authorization domain to use (default: gmail.com) * * @parameter expression="${gcloud.auth_domain}" */ private String auth_domain; /** * the maximum number of runtime instances that can be started for a particular module - the value * can be an integer, in what case all modules are limited to that number of instances or a * comma-separated list of module:max_instances e.g. "default:5,backend:3" (default: None) * * @parameter expression="${gcloud.max_module_instances}" */ private String max_module_instances; /** * email address associated with a service account that has a downloadable key. May be None for no * local application identity. (default: None) * * @parameter expression="${gcloud.appidentity_email_address}" */ private String appidentity_email_address; /** * path to private key file associated with service account (.pem format). Must be set if * appidentity_email_address is set. (default: None) * * @parameter expression="${gcloud.appidentity_private_key_path}" */ private String appidentity_private_key_path; /** * path to gcloud_directory used to store blob contents (defaults to a subdirectory of * --storage_path if not set) (default: None) * * @parameter expression="${gcloud.blobstore_path}" */ private String blobstore_path; /** * path to a file used to store datastore contents (defaults to a file in --storage_path if not * set) (default: None) * * @parameter expression="${gcloud.datastore_path}" */ private String datastore_path; /** * clear the datastore on startup (default: False) * * @parameter expression="${gcloud.clear_datastore}" */ private boolean clear_datastore; /** * make files specified in the app.yaml "skip_files" or "static" handles readable by the * application. (default: False) * * @parameter expression="${gcloud.allow_skipped_files}" */ private boolean allow_skipped_files; /** * Enable logs collection and display in local Admin Console for Managed VM modules. * * @parameter expression="${gcloud.enable_mvm_logs}" */ private boolean enable_mvm_logs; /** * Use the "sendmail" tool to transmit e-mail sent using the Mail API (ignored if --smtp-host is * set) * * @parameter expression="${gcloud.enable_sendmail}" */ private boolean enable_sendmail; /** * Use mtime polling for detecting source code changes - useful if modifying code from a remote * machine using a distributed file system * * @parameter expression="${gcloud.use_mtime_file_watcher}" */ private boolean use_mtime_file_watcher; /** * JVM_FLAG Additional arguments to pass to the java command when launching an instance of the * app. May be specified more than once. Example: * <jvm_flag> <param>-Xmx1024m</param> * <param>-Xms256m</param> </jvm_flag>. * * @parameter */ private List<String> jvm_flag; /** * default Google Cloud Storage bucket name (default: None) * * @parameter expression="${gcloud.default_gcs_bucket_name}" */ private String default_gcs_bucket_name; /** * enable_cloud_datastore * * @parameter expression="${gcloud.enable_cloud_datastore}" */ private boolean enable_cloud_datastore; /** * datastore_consistency_policy The policy to apply when deciding whether a datastore write should * appear in global queries (default="time") * * @parameter expression="${gcloud.datastore_consistency_policy}" */ private String datastore_consistency_policy; /** * The full path to the PHP executable to use to run your PHP module * * @parameter expression="${gcloud.php_executable_path}" */ private String php_executable_path; /** * The script to run at the startup of new Python runtime instances (useful for tools such as * debuggers) * * @parameter expression="${gcloud.python_startup_script}" */ private String python_startup_script; /** * Generate an error on datastore queries that require a composite index not found in index.yaml * * @parameter expression="${gcloud.require_indexes}" */ private boolean require_indexes; /** * Logs the contents of e-mails sent using the Mail API * * @parameter expression="${gcloud.show_mail_body}" */ private boolean show_mail_body; /** * Allow TLS to be used when the SMTP server announces TLS support (ignored if --smtp-host is not * set) * * @parameter expression="${gcloud.smtp_allow_tls}" */ private boolean smtp_allow_tls; /** * The host and port of an SMTP server to use to transmit e-mail sent using the Mail API, in the * format host:port * * @parameter expression="${gcloud.smtp_host}" */ private String smtp_host; /** * Password to use when connecting to the SMTP server specified with --smtp-host * * @parameter expression="${gcloud.smtp_password}" */ private String smtp_password; /** * Username to use when connecting to the SMTP server specified with --smtp-host * * @parameter expression="${gcloud.smtp_user}" */ private String smtp_user; /** * Specify an entrypoint for custom runtime modules. This is required when such modules are * present. Include "{port}" in the string (without quotes) to pass the port number in as an * argument. For instance: --custom_entrypoint="gunicorn -b localhost:{port} * mymodule:application" * * @parameter expression="${gcloud.custom_entrypoint}" */ private String custom_entrypoint; @Override public void execute() throws MojoExecutionException, MojoFailureException { getLog().info(""); if (application_directory == null) { application_directory = maven_project.getBuild().getDirectory() + "/" + maven_project.getBuild().getFinalName(); } File appDirFile = new File(application_directory); if (!appDirFile.exists()) { File f = new File(maven_project.getBasedir(), application_directory); if (f.exists()) { application_directory = f.getAbsolutePath(); } else { 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); } // Just before starting, just to make sure, shut down any running devserver on this port. stopDevAppServer(); try { ArrayList<String> devAppServerCommand = getCommand(application_directory); startCommand(appDirFile, devAppServerCommand, WaitDirective.WAIT_SERVER_STOPPED); } catch (Exception ex) { getLog().error(ex); throw new MojoExecutionException("Execution error: " + ex); } } @Override protected ArrayList<String> getCommand(String appDir) throws MojoExecutionException { getLog().info("Running gcloud app run..."); ArrayList<String> devAppServerCommand = new ArrayList<>(); setupInitialCommands(devAppServerCommand); //devAppServerCommand.add("run"); File appDirectory = new File(appDir); File f = new File(appDirectory, "WEB-INF/appengine-web.xml"); if (!f.exists()) { // EAR project possibly, add all modules one by one: f = new File(appDirectory, "app.yaml"); boolean isAppYamlGenerated = new File(appDirectory, ".appyamlgenerated").exists(); if (f.exists() && !isAppYamlGenerated) { devAppServerCommand.add(f.getAbsolutePath()); } else { boolean oneMod = false; for (File w : appDirectory.listFiles()) { if (new File(w, "WEB-INF/appengine-web.xml").exists()) { executeAppCfgStagingCommand(w.getAbsolutePath()); devAppServerCommand.add(w.getAbsolutePath()); oneMod = true; } } if (!oneMod) { executeAppCfgStagingCommand(application_directory); devAppServerCommand.add(appDirectory.getAbsolutePath()); } } } else { f = new File(appDirectory, "app.yaml"); boolean isAppYamlGenerated = new File(appDirectory, ".appyamlgenerated").exists(); if (f.exists() && !isAppYamlGenerated) { devAppServerCommand.add(f.getAbsolutePath()); } else { // Point to our application executeAppCfgStagingCommand(application_directory); devAppServerCommand.add(appDirectory.getAbsolutePath() + "/app.yaml"); } } if ((modules != null) && !modules.isEmpty()) { for (String modDir : modules) { getLog().info("Running gcloud app run with extra module in " + modDir); devAppServerCommand.add(new File(modDir).getAbsolutePath()); } } // Add in additional options for starting the DevAppServer if (host != null) { String[] parts = host.split(":"); devAppServerCommand.add("--host"); devAppServerCommand.add(parts[0]); devAppServerCommand.add("--port"); devAppServerCommand.add(parts[1]); } if (api_host != null) { String[] parts = api_host.split(":"); devAppServerCommand.add("--api_host"); devAppServerCommand.add(parts[0]); devAppServerCommand.add("--api_port"); devAppServerCommand.add(parts[1]); } if (admin_host != null) { String[] parts = admin_host.split(":"); devAppServerCommand.add("--admin_host"); devAppServerCommand.add(parts[0]); devAppServerCommand.add("--admin_port"); devAppServerCommand.add(parts[1]); } if (storage_path != null) { devAppServerCommand.add("--storage_path=" + storage_path); } if (log_level != null) { devAppServerCommand.add("--log_level=" + log_level); } if (logs_path != null) { devAppServerCommand.add("--logs_path=" + logs_path); } if (auth_domain != null) { devAppServerCommand.add("--auth_domain=" + auth_domain); } if (max_module_instances != null) { devAppServerCommand.add("--max_module_instances=" + max_module_instances); } if (appidentity_email_address != null) { devAppServerCommand.add("--appidentity_email_address=" + appidentity_email_address); } if (appidentity_private_key_path != null) { devAppServerCommand.add("--appidentity_private_key_path=" + appidentity_private_key_path); } if (blobstore_path != null) { devAppServerCommand.add("--blobstore_path=" + blobstore_path); } if (datastore_path != null) { devAppServerCommand.add("--datastore_path=" + datastore_path); } if (clear_datastore) { devAppServerCommand.add("--clear_datastore"); } if (allow_skipped_files) { devAppServerCommand.add("--allow_skipped_files"); } if (enable_mvm_logs) { devAppServerCommand.add("--enable_mvm_logs"); } if (enable_sendmail) { devAppServerCommand.add("--enable_sendmail"); } if (use_mtime_file_watcher) { devAppServerCommand.add("--use_mtime_file_watcher"); } if ((jvm_flag != null) && !jvm_flag.isEmpty()) { for (String opt : jvm_flag) { devAppServerCommand.add("--jvm_flag=" + opt); } } if (default_gcs_bucket_name != null) { devAppServerCommand.add("--default_gcs_bucket_name=" + default_gcs_bucket_name); } if (enable_cloud_datastore) { devAppServerCommand.add("--enable_cloud_datastore"); } if (datastore_consistency_policy != null) { devAppServerCommand.add("--datastore_consistency_policy=" + datastore_consistency_policy); } if (php_executable_path != null) { devAppServerCommand.add("--php_executable_path=" + php_executable_path); } if (python_startup_script != null) { devAppServerCommand.add("--python_startup_script=" + python_startup_script); } if (require_indexes) { devAppServerCommand.add("--require_indexes"); } if (show_mail_body) { devAppServerCommand.add("--show_mail_body"); } if (smtp_allow_tls) { devAppServerCommand.add("--smt_allow_tls"); } if (smtp_host != null) { String[] parts = host.split(":"); devAppServerCommand.add("--smtp_host"); devAppServerCommand.add(parts[0]); devAppServerCommand.add("--smtp_port"); devAppServerCommand.add(parts[1]); } if (smtp_password != null) { devAppServerCommand.add("--smtp_password=" + smtp_password); } if (smtp_user != null) { devAppServerCommand.add("--smtp_user=" + smtp_user); } if (custom_entrypoint != null) { devAppServerCommand.add("--custom_entrypoint=" + custom_entrypoint); } if (runtime != null) { devAppServerCommand.add("--runtime=" + runtime); } return devAppServerCommand; } protected void stopDevAppServer() throws MojoExecutionException { HttpURLConnection connection; try { String ad = "localhost"; if (host != null) { String[] parts = host.split(":"); ad = parts[0]; } URL url = new URL("http", ad, 8000, "/quit"); connection = (HttpURLConnection) url.openConnection(); connection.setDoOutput(true); connection.setDoInput(true); connection.setRequestMethod("GET"); ByteStreams.toByteArray(connection.getInputStream()); connection.setReadTimeout(4000); connection.disconnect(); getLog().info("Shutting down Cloud SDK Server on port " + 8000 + " and waiting 4 seconds..."); Thread.sleep(4000); } catch (MalformedURLException e) { throw new MojoExecutionException( "URL malformed attempting to stop the devserver : " + e.getMessage()); } catch (IOException e) { getLog().debug( "Was not able to contact the devappserver to shut it down. Most likely this is due to it" + " simply not running anymore.", e); } catch (InterruptedException e) { Throwables.propagate(e); } } }