/* * Copyright (c) 2011-2015 The original author or authors * ------------------------------------------------------ * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * and Apache License v2.0 which accompanies this distribution. * * The Eclipse Public License is available at * http://www.eclipse.org/legal/epl-v10.html * * The Apache License v2.0 is available at * http://www.opensource.org/licenses/apache2.0.php * * You may elect to redistribute this code under either of these licenses. */ package io.vertx.core.impl.launcher.commands; import io.vertx.core.DeploymentOptions; import io.vertx.core.Handler; import io.vertx.core.cli.CLIException; import io.vertx.core.cli.CommandLine; import io.vertx.core.cli.annotations.*; import io.vertx.core.impl.launcher.VertxLifecycleHooks; import io.vertx.core.json.DecodeException; import io.vertx.core.json.JsonObject; import io.vertx.core.spi.launcher.ExecutionContext; import java.io.File; import java.io.FileNotFoundException; import java.util.ArrayList; import java.util.List; import java.util.Scanner; import java.util.UUID; import java.util.stream.Collectors; /** * The vert.x run command that lets you execute verticles. * * @author Clement Escoffier <clement@apache.org> */ @Name("run") @Summary("Runs a verticle called <main-verticle> in its own instance of vert.x.") public class RunCommand extends BareCommand { protected DeploymentOptions deploymentOptions; protected boolean cluster; protected boolean ha; protected int instances; protected String config; protected boolean worker; protected String mainVerticle; protected List<String> redeploy; protected String vertxApplicationBackgroundId; protected String onRedeployCommand; protected Watcher watcher; private long redeployScanPeriod; private long redeployGracePeriod; private long redeployTerminationPeriod; /** * Enables / disables the high-availability. * * @param ha whether or not to enable the HA. */ @Option(longName = "ha", acceptValue = false, flag = true) @Description("If specified the verticle will be deployed as a high availability (HA) deployment. This means it can " + "fail over to any other nodes in the cluster started with the same HA group.") public void setHighAvailability(boolean ha) { this.ha = ha; } /** * Enables / disables the clustering. * * @param cluster whether or not to start vert.x in clustered mode. */ @Option(longName = "cluster", acceptValue = false, flag = true) @Description("If specified then the vert.x instance will form a cluster with any other vert.x instances on the " + "network.") public void setCluster(boolean cluster) { this.cluster = cluster; } /** * Whether or not the verticle is deployed as a worker verticle. * * @param worker {@code true} to deploy the verticle as worker, {@code false} otherwise */ @Option(longName = "worker", acceptValue = false) @Description("If specified then the verticle is a worker verticle.") public void setWorker(boolean worker) { this.worker = worker; } /** * Sets the number of instance of the verticle to create. * * @param instances the number of instances */ @Option(longName = "instances", argName = "instances") @DefaultValue("1") @Description("Specifies how many instances of the verticle will be deployed. Defaults to 1.") public void setInstances(int instances) { this.instances = instances; } /** * The main verticle configuration, it can be a json file or a json string. * * @param configuration the configuration */ @Option(longName = "conf", argName = "config") @Description("Specifies configuration that should be provided to the verticle. <config> should reference either a " + "text file containing a valid JSON object which represents the configuration OR be a JSON string.") public void setConfig(String configuration) { if (configuration != null) { // For inlined configuration remove first and end single and double quotes if any this.config = configuration.trim() .replaceAll("^\"|\"$", "") .replaceAll("^'|'$", ""); } else { this.config = null; } } /** * Sets the main verticle that is deployed. * * @param verticle the verticle */ @Argument(index = 0, argName = "main-verticle", required = true) @Description("The main verticle to deploy, it can be a fully qualified class name or a file.") public void setMainVerticle(String verticle) { this.mainVerticle = verticle; } @Option(longName = "redeploy", argName = "includes") @Description("Enable automatic redeployment of the application. This option takes a set on includes as parameter " + "indicating which files need to be watched. Patterns are separated by a comma.") @ParsedAsList public void setRedeploy(List<String> redeploy) { this.redeploy = redeploy; } /** * Sets the user command executed during redeployment. * * @param command the on redeploy command * @deprecated Use 'on-redeploy' instead. It will be removed in vert.x 3.3 */ @Option(longName = "onRedeploy", argName = "cmd") @Description("Optional shell command executed when a redeployment is triggered (deprecated - will be removed in 3" + ".3, use 'on-redeploy' instead") @Hidden @Deprecated public void setOnRedeployCommandOld(String command) { out.println("[WARNING] the 'onRedeploy' option is deprecated, and will be removed in vert.x 3.3. Use " + "'on-redeploy' instead."); setOnRedeployCommand(command); } @Option(longName = "on-redeploy", argName = "cmd") @Description("Optional shell command executed when a redeployment is triggered") public void setOnRedeployCommand(String command) { this.onRedeployCommand = command; } @Option(longName = "redeploy-scan-period", argName = "period") @Description("When redeploy is enabled, this option configures the file system scanning period to detect file " + "changes. The time is given in milliseconds. 250 ms by default.") @DefaultValue("250") public void setRedeployScanPeriod(long period) { this.redeployScanPeriod = period; } @Option(longName = "redeploy-grace-period", argName = "period") @Description("When redeploy is enabled, this option configures the grace period between 2 redeployments. The time " + "is given in milliseconds. 1000 ms by default.") @DefaultValue("1000") public void setRedeployGracePeriod(long period) { this.redeployGracePeriod = period; } @Option(longName = "redeploy-termination-period", argName = "period") @Description("When redeploy is enabled, this option configures the time waited to be sure that the previous " + "version of the application has been stopped. It is useful on Windows, where the 'terminate' command may take time to be " + "executed.The time is given in milliseconds. 0 ms by default.") @DefaultValue("0") public void setRedeployStopWaitingTime(long period) { this.redeployTerminationPeriod = period; } /** * Validates the command line parameters. * * @param context - the execution context * @throws CLIException - validation failed */ @Override public void setUp(ExecutionContext context) throws CLIException { super.setUp(context); // If cluster-host and / or port is set, cluster need to have been explicitly set io.vertx.core.cli.Option clusterHostOption = executionContext.cli().getOption("cluster-host"); io.vertx.core.cli.Option clusterPortOption = executionContext.cli().getOption("cluster-port"); CommandLine commandLine = executionContext.commandLine(); if ((!isClustered()) && (commandLine.isOptionAssigned(clusterHostOption) || commandLine.isOptionAssigned(clusterPortOption))) { throw new CLIException("The option -cluster-host and -cluster-port requires -cluster to be enabled"); } // If quorum and / or ha-group, ha need to have been explicitly set io.vertx.core.cli.Option haGroupOption = executionContext.cli().getOption("hagroup"); io.vertx.core.cli.Option quorumOption = executionContext.cli().getOption("quorum"); if (!ha && (commandLine.isOptionAssigned(haGroupOption) || commandLine.isOptionAssigned(quorumOption))) { throw new CLIException("The option -hagroup and -quorum requires -ha to be enabled"); } } /** * @return whether the {@code cluster} option or the {@code ha} option are enabled. Also {@code true} when a custom * launcher modifies the Vert.x options to set `clustered` to {@code true} */ @Override public boolean isClustered() { return cluster || ha || (options != null && options.isClustered()); } @Override public boolean getHA() { return ha; } /** * Starts vert.x and deploy the verticle. */ @Override public void run() { if (redeploy == null || redeploy.isEmpty()) { JsonObject conf = getConfiguration(); if (conf == null) { conf = new JsonObject(); } afterConfigParsed(conf); super.run(); if (vertx == null) { // Already logged. ExecUtils.exitBecauseOfVertxInitializationIssue(); } deploymentOptions = new DeploymentOptions(); configureFromSystemProperties(deploymentOptions, DEPLOYMENT_OPTIONS_PROP_PREFIX); deploymentOptions.setConfig(conf).setWorker(worker).setHa(ha).setInstances(instances); beforeDeployingVerticle(deploymentOptions); deploy(); } else { // redeploy is set, start the redeployment infrastructure (watcher). initializeRedeployment(); } } /** * Initializes the redeployment cycle. In "redeploy mode", the application is launched as background, and is * restarted after every change. A {@link Watcher} instance is responsible for monitoring files and triggering the * redeployment. */ protected synchronized void initializeRedeployment() { if (watcher != null) { throw new IllegalStateException("Redeployment already started ? The watcher already exists"); } // Compute the application id. We append "-redeploy" to ease the identification in the process list. vertxApplicationBackgroundId = UUID.randomUUID().toString() + "-redeploy"; watcher = new Watcher(getCwd(), redeploy, this::startAsBackgroundApplication, // On deploy this::stopBackgroundApplication, // On undeploy onRedeployCommand, // In between command redeployGracePeriod, // The redeploy grace period redeployScanPeriod); // The redeploy scan period // Close the watcher when the JVM is terminating. // Notice that the vert.x finalizer is not registered when we run in redeploy mode. Runtime.getRuntime().addShutdownHook(new Thread() { public void run() { shutdownRedeployment(); } }); // Start the watching process, it triggers the initial deployment. watcher.watch(); } /** * Stop the redeployment if started. */ protected synchronized void shutdownRedeployment() { if (watcher != null) { watcher.close(); watcher = null; } } /** * On-Undeploy action invoked while redeploying. It just stops the application launched in background. * * @param onCompletion an optional on-completion handler. If set it must be invoked at the end of this method. */ protected synchronized void stopBackgroundApplication(Handler<Void> onCompletion) { executionContext.execute("stop", vertxApplicationBackgroundId, "--redeploy"); if (redeployTerminationPeriod > 0) { try { Thread.sleep(redeployTerminationPeriod); } catch (InterruptedException e) { // Ignore the exception. Thread.currentThread().interrupt(); } } if (onCompletion != null) { onCompletion.handle(null); } } /** * On-Deploy action invoked while redeploying. It just starts the application in background, copying all input * parameters. In addition, the vertx application id is set. * * @param onCompletion an optional on-completion handler. If set it must be invoked at the end of this method. */ protected void startAsBackgroundApplication(Handler<Void> onCompletion) { // We need to copy all options and arguments. List<String> args = new ArrayList<>(); // Prepend the command. args.add("run"); args.add("--vertx-id=" + vertxApplicationBackgroundId); args.addAll(executionContext.commandLine().allArguments()); // No need to add the main-verticle as it's part of the allArguments list. if (cluster) { args.add("--cluster"); } if (clusterHost != null) { args.add("--cluster-host=" + clusterHost); } if (clusterPort != 0) { args.add("--cluster-port=" + clusterPort); } if (ha) { args.add("--ha"); } if (haGroup != null && !haGroup.equals("__DEFAULT__")) { args.add("--hagroup=" + haGroup); } if (quorum != -1) { args.add("--quorum=" + quorum); } if (classpath != null && !classpath.isEmpty()) { args.add("--classpath=" + classpath.stream().collect(Collectors.joining(File.pathSeparator))); } if (config != null) { // Pass the configuration in 2 steps to quote correctly the configuration if it's an inlined json string args.add("--conf"); args.add(config); } if (instances != 1) { args.add("--instances=" + instances); } if (worker) { args.add("--worker"); } if (systemProperties != null) { args.addAll(systemProperties.stream().map(s -> "-D" + s).collect(Collectors.toList())); } // Enable stream redirection args.add("--redirect-output"); executionContext.execute("start", args.toArray(new String[args.size()])); if (onCompletion != null) { onCompletion.handle(null); } } protected void deploy() { deploy(mainVerticle, vertx, deploymentOptions, res -> { if (res.failed()) { res.cause().printStackTrace(); handleDeployFailed(res.cause()); } }); } private void handleDeployFailed(Throwable cause) { if (executionContext.main() instanceof VertxLifecycleHooks) { ((VertxLifecycleHooks) executionContext.main()).handleDeployFailed(vertx, mainVerticle, deploymentOptions, cause); } else { ExecUtils.exitBecauseOfVertxDeploymentIssue(); } } protected JsonObject getConfiguration() { JsonObject conf; if (config != null) { try (Scanner scanner = new Scanner(new File(config), "UTF-8").useDelimiter("\\A")) { String sconf = scanner.next(); try { conf = new JsonObject(sconf); } catch (DecodeException e) { log.error("Configuration file " + sconf + " does not contain a valid JSON object"); return null; } } catch (FileNotFoundException e) { try { conf = new JsonObject(config); } catch (DecodeException e2) { // The configuration is not printed for security purpose, it can contain sensitive data. log.error("The -conf option does not point to an existing file or is not a valid JSON object"); e2.printStackTrace(); return null; } } } else { conf = null; } return conf; } protected void beforeDeployingVerticle(DeploymentOptions deploymentOptions) { final Object main = executionContext.main(); if (main instanceof VertxLifecycleHooks) { ((VertxLifecycleHooks) main).beforeDeployingVerticle(deploymentOptions); } } protected void afterConfigParsed(JsonObject config) { final Object main = executionContext.main(); if (main instanceof VertxLifecycleHooks) { ((VertxLifecycleHooks) main).afterConfigParsed(config); } } }