// =================================================================================================
// Copyright 2011 Twitter, Inc.
// -------------------------------------------------------------------------------------------------
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this work except in compliance with the License.
// You may obtain a copy of the License in the LICENSE file, or 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.twitter.common.application;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.inject.Guice;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.Module;
import com.google.inject.Stage;
import com.google.inject.util.Modules;
import com.twitter.common.application.modules.AppLauncherModule;
import com.twitter.common.application.modules.LifecycleModule;
import com.twitter.common.args.Arg;
import com.twitter.common.args.ArgFilters;
import com.twitter.common.args.ArgScanner;
import com.twitter.common.args.ArgScanner.ArgScanException;
import com.twitter.common.args.CmdLine;
import com.twitter.common.args.constraints.NotNull;
import com.twitter.common.base.ExceptionalCommand;
/**
* An application launcher that sets up a framework for pluggable binding modules. This class
* should be called directly as the main class, with a command line argument {@code -app_class}
* which is the canonical class name of the application to execute.
*
* If your application uses command line arguments all {@link Arg} fields annotated with
* {@link CmdLine} will be discovered and command line arguments will be validated against this set,
* parsed and applied.
*
* A bootstrap module will be automatically applied ({@link AppLauncherModule}), which provides
* overridable default bindings for things like quit/abort hooks and a health check function.
* A {@link LifecycleModule} is also automatically applied to perform startup and shutdown
* actions.
*
* @author William Farner
*/
public final class AppLauncher {
private static final Logger LOG = Logger.getLogger(AppLauncher.class.getName());
private static final String APP_CLASS_NAME = "app_class";
@NotNull
@CmdLine(name = APP_CLASS_NAME,
help = "Fully-qualified name of the application class, which must implement Runnable.")
private static final Arg<Class<? extends Application>> APP_CLASS = Arg.create();
@CmdLine(name = "guice_stage",
help = "Guice development stage to create injector with.")
private static final Arg<Stage> GUICE_STAGE = Arg.create(Stage.DEVELOPMENT);
private static final Predicate<Field> SELECT_APP_CLASS =
ArgFilters.selectCmdLineArg(AppLauncher.class, APP_CLASS_NAME);
@Inject @StartupStage private ExceptionalCommand startupCommand;
@Inject private Lifecycle lifecycle;
private AppLauncher() {
// This should not be invoked directly.
}
private void run(Application application) {
try {
configureInjection(application);
LOG.info("Executing startup actions.");
// We're an app framework and this is the outer shell - it makes sense to handle all errors
// before exiting.
// SUPPRESS CHECKSTYLE:OFF IllegalCatch
try {
startupCommand.execute();
} catch (Exception e) {
LOG.log(Level.SEVERE, "Startup action failed, quitting.", e);
throw Throwables.propagate(e);
}
// SUPPRESS CHECKSTYLE:ON IllegalCatch
try {
application.run();
} finally {
LOG.info("Application run() exited.");
}
} finally {
if (lifecycle != null) {
lifecycle.shutdown();
}
}
}
private void configureInjection(Application application) {
Iterable<Module> modules = ImmutableList.<Module>builder()
.add(new LifecycleModule())
.add(new AppLauncherModule())
.addAll(application.getModules())
.build();
Injector injector = Guice.createInjector(GUICE_STAGE.get(),
Modules.override(Modules.combine(modules)).with(application.getOverridingModules()));
injector.injectMembers(this);
injector.injectMembers(application);
}
public static void main(String... args) throws IllegalAccessException, InstantiationException {
// TODO(John Sirois): Support a META-INF/MANIFEST.MF App-Class attribute to allow java -jar
parseArgs(ArgFilters.SELECT_ALL, Arrays.asList(args));
new AppLauncher().run(APP_CLASS.get().newInstance());
}
/**
* A convenience for main wrappers. Equivalent to:
* <pre>
* AppLauncher.launch(appClass, ArgFilters.SELECT_ALL, Arrays.asList(args));
* </pre>
*
* @param appClass The application class to instantiate and launch.
* @param args The command line arguments to parse.
* @see ArgFilters
*/
public static void launch(Class<? extends Application> appClass, String... args) {
launch(appClass, ArgFilters.SELECT_ALL, Arrays.asList(args));
}
/**
* A convenience for main wrappers. Equivalent to:
* <pre>
* AppLauncher.launch(appClass, argFilter, Arrays.asList(args));
* </pre>
*
* @param appClass The application class to instantiate and launch.
* @param argFilter A filter that selects the {@literal @CmdLine} {@link Arg}s to enable for
* parsing.
* @param args The command line arguments to parse.
* @see ArgFilters
*/
public static void launch(Class<? extends Application> appClass, Predicate<Field> argFilter,
String... args) {
launch(appClass, argFilter, Arrays.asList(args));
}
/**
* Used to launch an application with a restricted set of {@literal @CmdLine} {@link Arg}s
* considered for parsing. This is useful if the classpath includes annotated fields you do not
* wish arguments to be parsed for.
*
* @param appClass The application class to instantiate and launch.
* @param argFilter A filter that selects the {@literal @CmdLine} {@link Arg}s to enable for
* parsing.
* @param args The command line arguments to parse.
* @see ArgFilters
*/
public static void launch(Class<? extends Application> appClass, Predicate<Field> argFilter,
List<String> args) {
Preconditions.checkNotNull(appClass);
Preconditions.checkNotNull(argFilter);
Preconditions.checkNotNull(args);
parseArgs(Predicates.<Field>and(Predicates.not(SELECT_APP_CLASS), argFilter), args);
try {
new AppLauncher().run(appClass.newInstance());
} catch (InstantiationException e) {
throw new IllegalStateException(e);
} catch (IllegalAccessException e) {
throw new IllegalStateException(e);
}
}
private static void parseArgs(Predicate<Field> filter, List<String> args) {
try {
if (!new ArgScanner().parse(filter, args)) {
System.exit(0);
}
} catch (ArgScanException e) {
exit("Failed to scan arguments", e);
} catch (IllegalArgumentException e) {
exit("Failed to apply arguments", e);
}
}
private static void exit(String message, Exception error) {
LOG.log(Level.SEVERE, message + "\n" + error, error);
System.exit(1);
}
}