com.spotify.heroic.HeroicShell.java Source code

Java tutorial

Introduction

Here is the source code for com.spotify.heroic.HeroicShell.java

Source

/*
 * 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.google.common.base.Charsets;
import com.google.common.collect.ImmutableList;
import com.spotify.heroic.HeroicCore.Builder;
import com.spotify.heroic.args4j.CmdLine;
import com.spotify.heroic.shell.AbstractShellTaskParams;
import com.spotify.heroic.shell.CoreInterface;
import com.spotify.heroic.shell.RemoteCoreInterface;
import com.spotify.heroic.shell.ShellIO;
import com.spotify.heroic.shell.ShellProtocol;
import com.spotify.heroic.shell.ShellTask;
import com.spotify.heroic.shell.TaskParameters;
import com.spotify.heroic.shell.protocol.CommandDefinition;
import eu.toolchain.async.AsyncFramework;
import eu.toolchain.async.AsyncFuture;
import eu.toolchain.async.TinyAsync;
import eu.toolchain.serializer.SerializerFramework;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.kohsuke.args4j.CmdLineException;
import org.kohsuke.args4j.CmdLineParser;
import org.kohsuke.args4j.Option;

import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.Executors;

@Slf4j
public class HeroicShell {
    public static final Path[] DEFAULT_CONFIGS = new Path[] { Paths.get("heroic.yml"),
            Paths.get("/etc/heroic/heroic.yml") };

    public static final SerializerFramework serializer = ShellProtocol.setupSerializer();

    public static void main(String[] args) throws IOException {
        HeroicLogging.configure();

        Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
            try {
                log.error("Uncaught exception in thread {}, exiting...", t, e);
            } finally {
                System.exit(1);
            }
        });

        final Parameters params = new Parameters();
        final CmdLineParser parser = setupParser(params);
        final ParsedArguments parsed = ParsedArguments.parse(args);

        try {
            parser.parseArgument(parsed.primary);
        } catch (CmdLineException e) {
            log.error("Argument error", e);
            System.exit(1);
            return;
        }

        if (params.help()) {
            parser.printUsage(System.out);
            System.out.println();
            HeroicModules.printAllUsage(System.out, "-P");
            System.exit(0);
            return;
        }

        final AsyncFramework async = TinyAsync.builder().executor(Executors.newSingleThreadExecutor()).build();

        if (parsed.child.isEmpty()) {
            final CoreInterface bridge;

            try {
                bridge = setupCoreBridge(params, async);
            } catch (Exception e) {
                log.error("Failed to setup core bridge", e);
                System.exit(1);
                return;
            }
            try {
                interactive(params, bridge);
            } catch (Exception e) {
                log.error("Error when running shell", e);
                System.exit(1);
            }
            System.exit(0);
            return;
        }

        final HeroicCore.Builder builder = setupBuilder(params);

        try {
            standalone(parsed.child, builder);
        } catch (Exception e) {
            log.error("Failed to run standalone task", e);
        }

        System.exit(0);
    }

    private static CoreInterface setupCoreBridge(Parameters params, AsyncFramework async) throws Exception {
        if (params.connect != null) {
            return setupRemoteCore(params.connect, async);
        }

        return setupLocalCore(params, async);
    }

    private static CoreInterface setupRemoteCore(String connect, AsyncFramework async) throws Exception {
        return RemoteCoreInterface.fromConnectString(connect, async, serializer);
    }

    private static CoreInterface setupLocalCore(Parameters params, AsyncFramework async) throws Exception {
        final HeroicCore.Builder builder = setupBuilder(params);

        final HeroicCore core = builder.build();

        log.info("Starting local Heroic...");

        final HeroicCoreInstance instance = core.newInstance();

        instance.start().get();

        return instance.<CoreInterface>inject(comp -> new CoreInterface() {
            private final ShellTasks tasks = comp.tasks();

            @Override
            public AsyncFuture<Void> evaluate(List<String> command, ShellIO io) throws Exception {
                return tasks.evaluate(command, io);
            }

            @Override
            public List<CommandDefinition> commands() throws Exception {
                return tasks.commands();
            }

            @Override
            public void shutdown() throws Exception {
                instance.shutdown().get();
            }
        });
    }

    static void interactive(Parameters params, CoreInterface core) throws Exception {
        log.info("Setting up interactive shell...");

        Exception e = null;
        try {
            runInteractiveShell(core);
        } catch (final Exception inner) {
            e = inner;
        }

        log.info("Closing core bridge...");

        try {
            core.shutdown();
        } catch (final Exception inner) {
            if (e != null) {
                inner.addSuppressed(e);
            }
            e = inner;
        }
        if (e != null) {
            throw e;
        }
    }

    static void runInteractiveShell(final CoreInterface core) throws Exception {
        final List<CommandDefinition> commands = new ArrayList<>(core.commands());

        commands.add(new CommandDefinition("clear", ImmutableList.of(), "Clear the current shell"));
        commands.add(new CommandDefinition("timeout", ImmutableList.of(), "Get or set the current task timeout"));
        commands.add(new CommandDefinition("exit", ImmutableList.of(), "Exit the shell"));

        try (final FileInputStream input = new FileInputStream(FileDescriptor.in)) {
            final HeroicInteractiveShell interactive = HeroicInteractiveShell.buildInstance(commands, input);

            try {
                interactive.run(core);
            } finally {
                interactive.shutdown();
            }
        }
    }

    static void standalone(List<String> arguments, Builder builder) throws Exception {
        final String taskName = arguments.iterator().next();
        final List<String> rest = arguments.subList(1, arguments.size());

        log.info("Running standalone task {}", taskName);

        final HeroicCore core = builder.build();

        log.info("Starting Heroic...");
        final HeroicCoreInstance instance = core.newInstance();

        instance.start().get();

        final ShellTask task = instance.inject(c -> c.tasks().resolve(taskName));

        final TaskParameters params = task.params();

        final CmdLineParser parser = setupParser(params);

        try {
            parser.parseArgument(rest);
        } catch (CmdLineException e) {
            log.error("Error parsing arguments", e);
            System.exit(1);
            return;
        }

        if (params.help()) {
            parser.printUsage(System.err);
            HeroicModules.printAllUsage(System.err, "-P");
            System.exit(0);
            return;
        }

        try {
            final PrintWriter o = standaloneOutput(params, System.out);
            final ShellIO io = new DirectShellIO(o);

            try {
                task.run(io, params).get();
            } catch (Exception e) {
                log.error("Failed to run task", e);
            } finally {
                o.flush();
            }
        } finally {
            instance.shutdown().get();
        }
    }

    @SuppressWarnings("unchecked")
    static Class<ShellTask> resolveShellTask(final String taskName) throws ClassNotFoundException, Exception {
        final Class<?> taskType = Class.forName(taskName);

        if (!(ShellTask.class.isAssignableFrom(taskType))) {
            throw new Exception(String.format("Not an instance of ShellTask (%s)", taskName));
        }

        return (Class<ShellTask>) taskType;
    }

    static PrintWriter standaloneOutput(final TaskParameters params, final PrintStream original)
            throws IOException {
        final OutputStream out;

        if (params.output() != null && !"-".equals(params.output())) {
            out = Files.newOutputStream(Paths.get(params.output()));
        } else {
            out = original;
        }

        return new PrintWriter(new OutputStreamWriter(out, Charsets.UTF_8));
    }

    static Path parseConfigPath(String config) {
        final Path path = doParseConfigPath(config);

        if (!Files.isRegularFile(path)) {
            throw new IllegalStateException("No such file: " + path.toAbsolutePath());
        }

        return path;
    }

    static Path doParseConfigPath(String config) {
        if (config == null) {
            for (final Path p : DEFAULT_CONFIGS) {
                if (Files.isRegularFile(p)) {
                    return p;
                }
            }

            throw new IllegalStateException(
                    "No default configuration available, checked " + formatDefaults(DEFAULT_CONFIGS));
        }

        return Paths.get(config);
    }

    static String formatDefaults(Path[] defaultConfigs) {
        final List<Path> alternatives = new ArrayList<>(defaultConfigs.length);

        for (final Path path : defaultConfigs) {
            alternatives.add(path.toAbsolutePath());
        }

        return StringUtils.join(alternatives, ", ");
    }

    static HeroicCore.Builder setupBuilder(Parameters params) {
        HeroicCore.Builder builder = HeroicCore.builder().setupService(params.server)
                .disableBackends(params.disableBackends).modules(HeroicModules.ALL_MODULES).oneshot(true);

        if (params.config() != null) {
            builder.configPath(parseConfigPath(params.config()));
        }

        builder.parameters(ExtraParameters.ofList(params.parameters));

        for (final String profile : params.profiles()) {
            final HeroicProfile p = HeroicModules.PROFILES.get(profile);

            if (p == null) {
                throw new IllegalArgumentException(String.format("not a valid profile: %s", profile));
            }

            builder.profile(p);
        }

        builder.setupShellServer(params.shellServer);

        return builder;
    }

    /**
     * Setup a {@link org.kohsuke.args4j.CmdLineParser} with some useful handlers associated with
     * it.
     */
    private static CmdLineParser setupParser(final TaskParameters params) {
        return CmdLine.createParser(params);
    }

    @ToString
    public static class Parameters extends AbstractShellTaskParams {
        @Option(name = "--server", usage = "Start shell as server (enables listen port)")
        private boolean server = false;

        @Option(name = "--shell-server", usage = "Start shell with shell server (enables remote connections)")
        private boolean shellServer = false;

        @Option(name = "--disable-backends", usage = "Start core without configuring backends")
        private boolean disableBackends = false;

        @Option(name = "--connect", usage = "Connect to a remote heroic server", metaVar = "<host>[:<port>]")
        private String connect = null;

        @Option(name = "-X", usage = "Define an extra parameter", metaVar = "<key>=<value>")
        private List<String> parameters = new ArrayList<>();
    }

    @RequiredArgsConstructor
    static class ParsedArguments {
        final List<String> primary;
        final List<String> child;

        public static ParsedArguments parse(String[] args) {
            final List<String> primary = new ArrayList<>();
            final List<String> child = new ArrayList<>();

            final Iterator<String> iterator = Arrays.stream(args).iterator();

            while (iterator.hasNext()) {
                final String arg = iterator.next();

                if ("--".equals(arg)) {
                    break;
                }

                primary.add(arg);
            }

            while (iterator.hasNext()) {
                child.add(iterator.next());
            }

            return new ParsedArguments(primary, child);
        }
    }
}