/*
* Copyright (c) 2015 Spotify AB.
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License 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.spotify.heroic;
import com.spotify.heroic.HeroicCore.Builder;
import com.spotify.heroic.args4j.CmdLine;
import com.spotify.heroic.reflection.ResourceException;
import com.spotify.heroic.reflection.ResourceFileLoader;
import com.spotify.heroic.reflection.ResourceInstance;
import eu.toolchain.async.Transform;
import lombok.Data;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.CmdLineException;
import org.kohsuke.args4j.CmdLineParser;
import org.kohsuke.args4j.Option;
import java.lang.Thread.UncaughtExceptionHandler;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
@Slf4j
public class HeroicService {
public static interface Configuration {
void configure(HeroicCore.Builder builder, Parameters parameters);
}
public static final String CONFIGURATION_RESOURCE =
"com.spotify.heroic.HeroicService.Configuration";
/**
* Default configuration path.
*/
public static final Path DEFAULT_CONFIG = Paths.get("heroic.yml");
public static void main(final String[] args) throws Exception {
main(args, null);
}
public static void main(final String[] args, final Configuration configuration)
throws Exception {
HeroicLogging.configure();
Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
try {
log.error("Uncaught exception in thread {}, exiting...", t, e);
} finally {
System.exit(1);
}
}
});
final Parameters params = parseArguments(args);
if (params == null) {
System.exit(0);
return;
}
final HeroicCore.Builder builder = HeroicCore.builder();
configureBuilder(builder, params);
try {
loadResourceConfigurations(builder, params);
} catch (final ResourceException e) {
log.error("Failed to load configurators from resource: {}", e.getMessage(), e);
throw e;
}
final HeroicCore core = builder.build();
final HeroicCoreInstance instance;
try {
instance = core.newInstance();
} catch (final Exception e) {
log.error("Failed to create Heroic instance", e);
System.exit(1);
return;
}
try {
instance.start().get();
} catch (final Exception e) {
log.error("Failed to start Heroic instance", e);
System.exit(1);
return;
}
final Thread shutdown = new Thread(instance::shutdown);
shutdown.setName("heroic-shutdown-hook");
/* setup shutdown hook */
Runtime.getRuntime().addShutdownHook(shutdown);
/* block until core stops */
instance.join().get();
log.info("Shutting down, bye bye!");
System.exit(0);
}
private static void loadResourceConfigurations(final Builder builder, final Parameters params)
throws ResourceException {
final ClassLoader loader = Configuration.class.getClassLoader();
final List<ResourceInstance<Configuration>> configs =
ResourceFileLoader.loadInstances(CONFIGURATION_RESOURCE, loader, Configuration.class);
for (final ResourceInstance<Configuration> config : configs) {
config.invoke(new Transform<Configuration, Void>() {
@Override
public Void transform(Configuration result) throws Exception {
result.configure(builder, params);
return null;
}
});
}
}
private static void configureBuilder(
final HeroicCore.Builder builder, final Parameters params
) {
final Path config = getConfigPath(params.extra);
// setup as a service accepting http requests.
builder.setupService(true);
// do not require a shell server by default.
builder.setupShellServer(false);
if (config != null) {
builder.configPath(config);
}
if (params.id != null) {
builder.id(params.id);
}
if (params.port != null) {
builder.port(params.port);
}
if (params.host != null) {
builder.host(params.host);
}
if (params.startupPing != null) {
builder.startupPing(params.startupPing);
}
if (params.startupId != null) {
builder.startupId(params.startupId);
}
for (final String profile : params.profiles) {
builder.profile(setupProfile(profile));
}
builder.parameters(ExtraParameters.ofList(params.parameters));
builder.modules(HeroicModules.ALL_MODULES);
}
private static HeroicProfile setupProfile(final String profile) {
final HeroicProfile p = HeroicModules.PROFILES.get(profile);
if (p == null) {
throw new IllegalArgumentException("No such profile: " + profile);
}
log.info("Enabling Profile: {}", profile);
return p;
}
private static Path getConfigPath(final List<String> extra) {
if (extra.size() > 0) {
return Paths.get(extra.get(0));
}
if (Files.isRegularFile(DEFAULT_CONFIG)) {
return DEFAULT_CONFIG;
}
return null;
}
private static Parameters parseArguments(final String[] args) {
final Parameters params = new Parameters();
final CmdLineParser parser = CmdLine.createParser(params);
try {
parser.parseArgument(args);
} catch (final CmdLineException e) {
log.error("Error parsing arguments", e);
return null;
}
if (params.help) {
parser.printUsage(System.out);
System.out.println();
HeroicModules.printAllUsage(System.out, "-P");
return null;
}
return params;
}
// @formatter:off
@ToString
@Data
public static class Parameters {
@Option(name = "-P", aliases = {"--profile"},
usage = "Activate a pre-defined profile instead of a configuration file. Profiles" +
" are pre-defined configurations, useful for messing around with the " +
"system.")
private List<String> profiles = new ArrayList<>();
@Option(name = "--port", usage = "Port number to bind to")
private Integer port = null;
@Option(name = "--host", usage = "Host to bind to")
private String host = null;
@Option(name = "--id", usage = "Heroic identifier")
private String id = null;
@Option(name = "-h", aliases = {"--help"}, help = true, usage = "Display help.")
private boolean help;
@Option(name = "--startup-ping",
usage = "Send a JSON frame to the given URI containing information about this " +
"host after it has started.")
private String startupPing;
@Option(name = "--startup-id", usage = "Explicit id of a specific startup instance.")
private String startupId;
@Option(name = "-X", usage = "Define an extra parameter", metaVar = "<key>=<value>")
private List<String> parameters = new ArrayList<>();
@Argument
private List<String> extra = new ArrayList<>();
}
// @formatter:on
}