blob: d9069a4b586c605afedbc3e5b8cb4fb85c631f0a [file] [log] [blame]
package brooklyn.cli;
import groovy.lang.GroovyClassLoader;
import groovy.lang.GroovyShell;
import io.airlift.command.Arguments;
import io.airlift.command.Cli;
import io.airlift.command.Cli.CliBuilder;
import io.airlift.command.Command;
import io.airlift.command.Help;
import io.airlift.command.Option;
import io.airlift.command.OptionType;
import io.airlift.command.ParseException;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Callable;
import javax.inject.Inject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import brooklyn.BrooklynVersion;
import brooklyn.catalog.BrooklynCatalog;
import brooklyn.entity.Application;
import brooklyn.entity.Entity;
import brooklyn.entity.basic.AbstractApplication;
import brooklyn.entity.basic.AbstractEntity;
import brooklyn.entity.basic.ApplicationBuilder;
import brooklyn.entity.basic.Entities;
import brooklyn.entity.basic.StartableApplication;
import brooklyn.entity.proxying.EntitySpec;
import brooklyn.entity.trait.Startable;
import brooklyn.launcher.BrooklynLauncher;
import brooklyn.launcher.BrooklynServerDetails;
import brooklyn.launcher.FatalConfigurationRuntimeException;
import brooklyn.launcher.PersistMode;
import brooklyn.management.ManagementContext;
import brooklyn.management.ha.HighAvailabilityMode;
import brooklyn.util.ResourceUtils;
import brooklyn.util.exceptions.Exceptions;
import brooklyn.util.guava.Maybe;
import brooklyn.util.javalang.Enums;
import brooklyn.util.net.Networking;
import brooklyn.util.text.StringEscapes.JavaStringEscapes;
import brooklyn.util.text.Strings;
import com.google.common.annotations.Beta;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Objects;
import com.google.common.base.Objects.ToStringHelper;
import com.google.common.collect.ImmutableList;
/**
* This class is the primary CLI for brooklyn.
* Run with the `help` argument for help.
* <p>
* This class is designed for subclassing, with subclasses typically:
* <li> providing their own static {@link #main(String...)} (of course) which need simply invoke
* {@link #execCli(String[])} with the arguments
* <li> returning their CLI name (e.g. "start.sh") in an overridden {@link #cliScriptName()}
* <li> providing an overridden {@link LaunchCommand} via {@link #cliLaunchCommand()} if desired
* <li> providing any other CLI customisations by overriding {@link #cliBuilder()}
* (typically calling the parent and then customizing the builder)
* <li> populating a custom catalog using {@link LaunchCommand#populateCatalog(BrooklynCatalog)}
*/
public class Main {
// Launch banner
public static final String BANNER =
" _ _ _ \n" +
"| |__ _ __ ___ ___ | | _| |_ _ _ __ (R)\n" +
"| '_ \\| '__/ _ \\ / _ \\| |/ / | | | | '_ \\ \n" +
"| |_) | | | (_) | (_) | <| | |_| | | | |\n" +
"|_.__/|_| \\___/ \\___/|_|\\_\\_|\\__, |_| |_|\n" +
" |___/ "+BrooklynVersion.get()+"\n";
// Error codes
public static final int SUCCESS = 0;
public static final int PARSE_ERROR = 1;
public static final int EXECUTION_ERROR = 2;
public static final int CONFIGURATION_ERROR = 3;
public static final Logger log = LoggerFactory.getLogger(Main.class);
public static void main(String... args) {
new Main().execCli(args);
}
/** abstract superclass for commands defining global options, but not arguments,
* as that prevents Help from being injectable in the {@link HelpCommand} subclass */
public static abstract class BrooklynCommand implements Callable<Void> {
@Option(type = OptionType.GLOBAL, name = { "-v", "--verbose" }, description = "Verbose mode")
public boolean verbose = false;
@Option(type = OptionType.GLOBAL, name = { "-q", "--quiet" }, description = "Quiet mode")
public boolean quiet = false;
public ToStringHelper string() {
return Objects.toStringHelper(getClass())
.add("verbose", verbose)
.add("quiet", quiet);
}
@Override
public String toString() {
return string().toString();
}
}
/** common superclass for commands, defining global options (in our super) and extracting the arguments */
public static abstract class BrooklynCommandCollectingArgs extends BrooklynCommand {
/** extra arguments */
@Arguments
public List<String> arguments = new ArrayList<String>();
/** @return true iff there are arguments; it also sys.errs a warning in that case */
protected boolean warnIfNoArguments() {
if (arguments.isEmpty()) return false;
System.err.println("Invalid subcommand arguments: "+Strings.join(arguments, " "));
return true;
}
/** throw {@link ParseException} iff there are arguments */
protected void failIfNoArguments() {
if (arguments.isEmpty()) return ;
throw new ParseException("Invalid subcommand arguments '"+Strings.join(arguments, " ")+"'");
}
@Override
public ToStringHelper string() {
return super.string()
.add("arguments", arguments);
}
}
@Command(name = "help", description = "Display help for available commands")
public static class HelpCommand extends BrooklynCommand {
@Inject
public Help help;
@Override
public Void call() throws Exception {
if (log.isDebugEnabled()) log.debug("Invoked help command: {}", this);
return help.call();
}
}
@Command(name = "info", description = "Display information about brooklyn")
public static class InfoCommand extends BrooklynCommandCollectingArgs {
@Override
public Void call() throws Exception {
if (log.isDebugEnabled()) log.debug("Invoked info command: {}", this);
warnIfNoArguments();
// Get current version
String version = BrooklynVersion.get();
// Display info text
System.out.println(BANNER);
System.out.println("Version: " + version);
System.out.println("Website: http://brooklyn.io/");
System.out.println("Source: https://github.com/brooklyncentral/brooklyn/");
System.out.println();
System.out.println("Copyright 2011-2014 by Cloudsoft Corp.");
System.out.println("Licensed under the Apache 2.0 License");
System.out.println();
return null;
}
}
@Command(name = "launch", description = "Starts a server, optionally with applications")
public static class LaunchCommand extends BrooklynCommandCollectingArgs {
@Option(name = { "--localBrooklynProperties" }, title = "local brooklyn.properties file",
description = "local brooklyn.properties file, specific to this launch (appending to and overriding global properties)")
public String localBrooklynProperties;
@Option(name = { "-a", "--app" }, title = "application class or file",
description = "The Application to start. " +
"For example, my.AppName, file://my/app.yaml, or classpath://my/AppName.groovy -- "
+ "note that a BROOKLYN_CLASSPATH environment variable may be required to "
+ "load classes from other locations")
public String app;
@Beta
@Option(name = { "-s", "--script" }, title = "script URI",
description = "EXPERIMENTAL. URI for a Groovy script to parse and load." +
" This script will run before starting the app.")
public String script = null;
@Option(name = { "-l", "--location", "--locations" }, title = "location list",
description = "Specifies the locations where the application will be launched. " +
"You can specify more than one location as a comma-separated list of values " +
"(or as a JSON array, if the values are complex)")
public String locations;
@Option(name = { "-p", "--port" }, title = "port number",
description = "Specifies the port to be used by the Brooklyn Management Console")
public String port = "8081+";
@Option(name = { "-nc", "--noConsole" },
description = "Whether to start the web console")
public boolean noConsole = false;
@Option(name = { "-b", "--bindAddress" },
description = "Specifies the IP address of the NIC to bind the Brooklyn Management Console to")
public String bindAddress = null;
@Option(name = { "--noConsoleSecurity" },
description = "Whether to disable security for the web console with no security (i.e. no authentication required)")
public Boolean noConsoleSecurity = false;
@Option(name = { "-ns", "--noShutdownOnExit" },
description = "Whether to stop the application when the JVM exits")
public boolean noShutdownOnExit = false;
/**
* Note that this is a temporary workaround to allow for running the
* brooklyn-whirr example.
*
* This will be replaced by more powerful CLI control for running
* processes, to send shutdown and other commands to brooklyn.
*
* Without using this flag you get a
* java.util.concurrent.RejectedExecutionException because the brooklyn
* and whirr shutdown hooks get executed in parallel. This is how it
* looks like: {@linktourl https://gist.github.com/47066f72d6f6f79b953e}
*/
@Beta
@Option(name = { "-sk", "--stopOnKeyPress" },
description = "After the application gets started, brooklyn will wait for a key press to stop it.")
public boolean stopOnKeyPress = false;
final static String PERSIST_OPTION = "--persist";
protected final static String PERSIST_OPTION_DISABLED = "disabled";
protected final static String PERSIST_OPTION_AUTO = "auto";
protected final static String PERSIST_OPTION_REBIND = "rebind";
protected final static String PERSIST_OPTION_CLEAN = "clean";
// TODO currently defaults to disabled; want it to default to on, when we're ready
// TODO how to force a line-split per option?!
// Looks like java.io.airlift.airline.UsagePrinter is splitting the description by word, and
// wrapping it automatically.
// See https://github.com/airlift/airline/issues/30
@Option(name = { PERSIST_OPTION }, allowedValues = { PERSIST_OPTION_DISABLED, PERSIST_OPTION_AUTO, PERSIST_OPTION_REBIND, PERSIST_OPTION_CLEAN },
title = "persistance mode",
description =
"The persistence mode. Possible values are: \n"+
"disabled: will not read or persist any state; \n"+
"auto: will rebind to any existing state, or start up fresh if no state; \n"+
"rebind: will rebind to the existing state, or fail if no state available; \n"+
"clean: will start up fresh (not using any existing state)")
public String persist = PERSIST_OPTION_DISABLED;
@Option(name = { "--persistenceDir" }, title = "persistence dir",
description = "the directory to read/write persisted state")
public String persistenceDir;
final static String HA_OPTION = "--highAvailability";
protected final static String HA_OPTION_DISABLED = "disabled";
protected final static String HA_OPTION_AUTO = "auto";
protected final static String HA_OPTION_MASTER = "master";
protected final static String HA_OPTION_STANDBY = "standby";
static { Enums.checkAllEnumeratedIgnoreCase(HighAvailabilityMode.class, HA_OPTION_AUTO, HA_OPTION_DISABLED, HA_OPTION_MASTER, HA_OPTION_STANDBY); }
@Option(name = { HA_OPTION }, allowedValues = { HA_OPTION_DISABLED, HA_OPTION_AUTO, HA_OPTION_MASTER, HA_OPTION_STANDBY },
title = "high availability mode",
description =
"The high availability mode. Possible values are: \n"+
"disabled: management node works in isolation - will not cooperate with any other standby/master nodes in management plane; \n"+
"auto: will look for other management nodes, and will allocate itself as standby or master based on other nodes' states; \n"+
"master: will startup as master - if there is already a master then fails immediately; \n"+
"standby: will start up as standby - if there is not already a master then fails immediately")
public String highAvailability = HA_OPTION_AUTO;
@VisibleForTesting
protected ManagementContext explicitManagementContext;
@Override
public Void call() throws Exception {
// Configure launcher
BrooklynLauncher launcher;
failIfNoArguments();
try {
if (log.isDebugEnabled()) log.debug("Invoked launch command {}", this);
if (!quiet) System.out.println(BANNER);
if (verbose) {
if (app != null) {
System.out.println("Launching brooklyn app: " + app + " in " + locations);
} else {
System.out.println("Launching brooklyn server (no app)");
}
}
computeLocations();
PersistMode persistMode = computePersistMode();
HighAvailabilityMode highAvailabilityMode = computeHighAvailabilityMode(persistMode);
ResourceUtils utils = ResourceUtils.create(this);
ClassLoader parent = utils.getLoader();
GroovyClassLoader loader = new GroovyClassLoader(parent);
// First, run a setup script if the user has provided one
if (script != null) {
execGroovyScript(utils, loader, script);
}
launcher = createLauncher();
launchApp(launcher, utils, loader);
launcher.persistMode(persistMode);
if (persistMode != PersistMode.DISABLED && Strings.isNonBlank(persistenceDir)) {
launcher.persistenceDir(persistenceDir);
}
launcher.highAvailabilityMode(highAvailabilityMode);
} catch (FatalConfigurationRuntimeException e) {
throw e;
} catch (Exception e) {
throw new FatalConfigurationRuntimeException("Fatal error configuring Brooklyn launch: "+e.getMessage(), e);
}
// Launch server
try {
launcher.start();
} catch (FatalConfigurationRuntimeException e) {
// rely on caller logging this propagated exception
throw e;
} catch (Exception e) {
// Don't terminate the JVM; leave it as-is until someone explicitly stops it
Exceptions.propagateIfFatal(e);
log.error("Error launching brooklyn: "+Exceptions.collapseText(e), e);
}
BrooklynServerDetails server = launcher.getServerDetails();
ManagementContext ctx = server.getManagementContext();
populateCatalog(launcher.getServerDetails().getManagementContext().getCatalog());
if (verbose) {
Entities.dumpInfo(launcher.getApplications());
}
waitAfterLaunch(ctx);
return null;
}
protected void computeLocations() {
boolean hasLocations = !Strings.isBlank(locations);
if (app != null) {
if (hasLocations && isYamlApp()) {
log.info("YAML app combined with command line locations; YAML locations will take precedence; this behaviour may change in subsequent versions");
} else if (!hasLocations && isYamlApp()) {
log.info("No locations supplied; defaulting to locations defined in YAML (if any)");
} else if (!hasLocations) {
log.info("No locations supplied; starting with no locations");
}
} else if (hasLocations) {
log.error("Locations specified without any applications; ignoring locations");
}
}
protected boolean isYamlApp() {
return app != null && app.endsWith(".yaml");
}
protected PersistMode computePersistMode() {
Maybe<PersistMode> persistMode = Enums.valueOfIgnoreCase(PersistMode.class, persist);
if (!persistMode.isPresent()) {
if (Strings.isBlank(persist)) {
throw new FatalConfigurationRuntimeException("Persist mode must not be blank");
} else {
throw new FatalConfigurationRuntimeException("Illegal persist setting: "+persist);
}
}
if (persistMode.get() == PersistMode.DISABLED) {
if (Strings.isNonBlank(persistenceDir)) {
throw new FatalConfigurationRuntimeException("Cannot specify peristanceDir when persist is disabled");
}
}
return persistMode.get();
}
protected HighAvailabilityMode computeHighAvailabilityMode(PersistMode persistMode) {
Maybe<HighAvailabilityMode> highAvailabilityMode = Enums.valueOfIgnoreCase(HighAvailabilityMode.class, highAvailability);
if (!highAvailabilityMode.isPresent()) {
if (Strings.isBlank(highAvailability)) {
throw new FatalConfigurationRuntimeException("High availability mode must not be blank");
} else {
throw new FatalConfigurationRuntimeException("Illegal highAvailability setting: "+highAvailability);
}
}
if (highAvailabilityMode.get() != HighAvailabilityMode.DISABLED) {
if (persistMode == PersistMode.DISABLED) {
if (highAvailabilityMode.get() == HighAvailabilityMode.AUTO)
return HighAvailabilityMode.DISABLED;
throw new FatalConfigurationRuntimeException("Cannot specify highAvailability when persistence is disabled");
} else if (persistMode == PersistMode.CLEAN && highAvailabilityMode.get() == HighAvailabilityMode.STANDBY) {
throw new FatalConfigurationRuntimeException("Cannot specify highAvailability STANDBY when persistence is CLEAN");
}
}
return highAvailabilityMode.get();
}
@VisibleForTesting
/** forces the launcher to use the given management context, when programmatically invoked;
* mainly used when testing to inject a safe (and fast) mgmt context */
public void useManagementContext(ManagementContext mgmt) {
explicitManagementContext = mgmt;
}
protected BrooklynLauncher createLauncher() {
BrooklynLauncher launcher;
launcher = BrooklynLauncher.newInstance();
launcher.localBrooklynPropertiesFile(localBrooklynProperties)
.webconsolePort(port)
.webconsole(!noConsole)
.shutdownOnExit(!noShutdownOnExit)
.locations(Strings.isBlank(locations) ? ImmutableList.<String>of() : JavaStringEscapes.unwrapJsonishListIfPossible(locations));
if (noConsoleSecurity) {
launcher.installSecurityFilter(false);
}
if (Strings.isNonEmpty(bindAddress)) {
InetAddress ip = Networking.getInetAddressWithFixedName(bindAddress);
launcher.bindAddress(ip);
}
if (explicitManagementContext!=null) {
launcher.managementContext(explicitManagementContext);
}
return launcher;
}
/** method intended for subclassing, to add items to the catalog */
protected void populateCatalog(BrooklynCatalog catalog) {
// Force load of catalog (so web console is up to date)
catalog.getCatalogItems();
// nothing else added here
}
/** convenience for subclasses to specify that an app should run,
* throwing the right (caught) error if another app has already been specified */
protected void setAppToLaunch(String className) {
if (app!=null) {
if (app.equals(className)) return;
throw new FatalConfigurationRuntimeException("Cannot specify app '"+className+"' when '"+app+"' is already specified; "
+ "remove one or more conflicting CLI arguments.");
}
app = className;
}
protected void launchApp(BrooklynLauncher launcher, ResourceUtils utils, GroovyClassLoader loader)
throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException {
if (app != null) {
// Create the instance of the brooklyn app
log.debug("Loading the user's application: {}", app);
if (isYamlApp()) {
log.debug("Loading application as YAML spec: {}", app);
String content = utils.getResourceAsString(app);
launcher.application(content);
} else {
Object loadedApp = loadApplicationFromClasspathOrParse(utils, loader, app);
if (loadedApp instanceof ApplicationBuilder) {
launcher.application((ApplicationBuilder)loadedApp);
} else if (loadedApp instanceof Application) {
launcher.application((AbstractApplication)loadedApp);
} else {
throw new FatalConfigurationRuntimeException("Unexpected application type "+(loadedApp==null ? null : loadedApp.getClass())+", for app "+loadedApp);
}
}
}
}
protected void waitAfterLaunch(ManagementContext ctx) throws IOException {
if (stopOnKeyPress) {
// Wait for the user to type a key
log.info("Server started. Press return to stop.");
System.in.read();
stopAllApps(ctx.getApplications());
} else {
// Block forever so that Brooklyn doesn't exit (until someone does cntrl-c or kill)
log.info("Launched Brooklyn; will now block until shutdown issued. Shutdown via GUI or API or process interrupt.");
waitUntilInterrupted();
}
}
protected void waitUntilInterrupted() {
Object mutex = new Object();
synchronized (mutex) {
try {
while (!Thread.currentThread().isInterrupted()) {
mutex.wait();
log.debug("Spurious wake in brooklyn Main while waiting for interrupt, how about that!");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return; // exit gracefully
}
}
}
protected void execGroovyScript(ResourceUtils utils, GroovyClassLoader loader, String script) {
log.debug("Running the user provided script: {}", script);
String content = utils.getResourceAsString(script);
GroovyShell shell = new GroovyShell(loader);
shell.evaluate(content);
}
/**
* Helper method that gets an instance of a brooklyn {@link AbstractApplication} or an {@link ApplicationBuilder}.
* Guaranteed to be non-null result of one of those types (throwing exception if app not appropriate).
*/
@SuppressWarnings("unchecked")
protected Object loadApplicationFromClasspathOrParse(ResourceUtils utils, GroovyClassLoader loader, String app)
throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException {
Class<?> tempclazz;
log.debug("Loading application as class on classpath: {}", app);
try {
tempclazz = loader.loadClass(app, true, false);
} catch (ClassNotFoundException cnfe) { // Not a class on the classpath
log.debug("Loading \"{}\" as class on classpath failed, now trying as .groovy source file", app);
String content = utils.getResourceAsString(app);
tempclazz = loader.parseClass(content);
}
final Class<?> clazz = tempclazz;
// Instantiate an app builder (wrapping app class in ApplicationBuilder, if necessary)
if (ApplicationBuilder.class.isAssignableFrom(clazz)) {
Constructor<?> constructor = clazz.getConstructor();
return (ApplicationBuilder) constructor.newInstance();
} else if (StartableApplication.class.isAssignableFrom(clazz)) {
EntitySpec<? extends StartableApplication> appSpec;
if (tempclazz.isInterface())
appSpec = EntitySpec.create((Class<? extends StartableApplication>) clazz);
else
appSpec = EntitySpec.create(StartableApplication.class, (Class<? extends StartableApplication>) clazz);
return new ApplicationBuilder(appSpec) {
@Override protected void doBuild() {
}};
} else if (AbstractApplication.class.isAssignableFrom(clazz)) {
// TODO If this application overrides init() then in trouble, as that won't get called!
// TODO grr; what to do about non-startable applications?
// without this we could return ApplicationBuilder rather than Object
Constructor<?> constructor = clazz.getConstructor();
return (AbstractApplication) constructor.newInstance();
} else if (AbstractEntity.class.isAssignableFrom(clazz)) {
// TODO Should we really accept any entity type, and just wrap it in an app? That's not documented!
return new ApplicationBuilder() {
@Override protected void doBuild() {
addChild(EntitySpec.create(Entity.class).impl((Class<? extends AbstractEntity>)clazz).additionalInterfaces(clazz.getInterfaces()));
}};
} else if (Entity.class.isAssignableFrom(clazz)) {
return new ApplicationBuilder() {
@Override protected void doBuild() {
addChild(EntitySpec.create((Class<? extends Entity>)clazz));
}};
} else {
throw new FatalConfigurationRuntimeException("Application class "+clazz+" must extend one of ApplicationBuilder or AbstractApplication");
}
}
@VisibleForTesting
protected void stopAllApps(Collection<? extends Application> applications) {
for (Application application : applications) {
try {
if (application instanceof Startable) {
((Startable)application).stop();
}
} catch (Exception e) {
log.error("Error stopping "+application+": "+e, e);
}
}
}
@Override
public ToStringHelper string() {
return super.string()
.add("app", app)
.add("script", script)
.add("location", locations)
.add("port", port)
.add("bindAddress", bindAddress)
.add("noConsole", noConsole)
.add("noConsoleSecurity", noConsoleSecurity)
.add("noShutdownOnExit", noShutdownOnExit)
.add("stopOnKeyPress", stopOnKeyPress)
.add("localBrooklynProperties", localBrooklynProperties)
.add("persist", persist)
.add("persistenceDir", persistenceDir)
.add("highAvailability", highAvailability);
}
}
/** method intended for overriding when the script filename is different
* @return the name of the script the user has invoked */
protected String cliScriptName() {
return "brooklyn";
}
/** method intended for overriding when a different {@link Cli} is desired,
* or when the subclass wishes to change any of the arguments */
protected CliBuilder<BrooklynCommand> cliBuilder() {
@SuppressWarnings({ "unchecked" })
CliBuilder<BrooklynCommand> builder = Cli.<BrooklynCommand>builder(cliScriptName())
.withDescription("Brooklyn Management Service")
.withCommands(
HelpCommand.class,
InfoCommand.class,
cliLaunchCommand()
);
return builder;
}
/** method intended for overriding when a custom {@link LaunchCommand} is being specified */
protected Class<? extends BrooklynCommand> cliLaunchCommand() {
return LaunchCommand.class;
}
protected void execCli(String ...args) {
execCli(cliBuilder().build(), args);
}
protected void execCli(Cli<BrooklynCommand> parser, String ...args) {
try {
log.debug("Parsing command line arguments: {}", Arrays.asList(args));
BrooklynCommand command = parser.parse(args);
log.debug("Executing command: {}", command);
command.call();
System.exit(SUCCESS);
} catch (ParseException pe) { // looks like the user typed it wrong
System.err.println("Parse error: " + pe.getMessage()); // display
// error
System.err.println(getUsageInfo(parser)); // display cli help
System.exit(PARSE_ERROR);
} catch (FatalConfigurationRuntimeException e) {
log.error("Configuration error: " + e.getMessage(), e);
System.err.println("Configuration error: " + e.getMessage());
System.exit(CONFIGURATION_ERROR);
} catch (Exception e) { // unexpected error during command execution
log.error("Execution error: " + e.getMessage(), e);
System.err.println("Execution error: " + e.getMessage());
e.printStackTrace();
System.exit(EXECUTION_ERROR);
}
}
protected String getUsageInfo(Cli<BrooklynCommand> parser) {
StringBuilder help = new StringBuilder();
help.append("\n");
Help.help(parser.getMetadata(), Collections.<String>emptyList(), help);
return help.toString();
}
}