/*
* 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.collect.ImmutableList;
import com.spotify.heroic.shell.CoreInterface;
import com.spotify.heroic.shell.QuoteParser;
import com.spotify.heroic.shell.ShellIO;
import com.spotify.heroic.shell.Tasks;
import com.spotify.heroic.shell.protocol.CommandDefinition;
import eu.toolchain.async.AsyncFuture;
import jline.console.ConsoleReader;
import jline.console.UserInterruptException;
import jline.console.completer.StringsCompleter;
import jline.console.history.FileHistory;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
@Slf4j
@RequiredArgsConstructor
public class HeroicInteractiveShell {
final ConsoleReader reader;
final List<CommandDefinition> commands;
final FileHistory history;
boolean running = true;
// mutable state, a.k.a. settings
int timeout = 10;
public void run(final CoreInterface core) throws Exception {
final PrintWriter out = new PrintWriter(reader.getOutput());
while (running) {
final String raw;
try {
raw = reader.readLine();
} catch (UserInterruptException e) {
out.println("Interrupted");
break;
}
if (raw == null) {
break;
}
final List<List<String>> lines;
try {
lines = QuoteParser.parse(raw);
} catch (Exception e) {
log.error("Line syntax invalid", e);
return;
}
lines.forEach(command -> {
if (command.isEmpty()) {
return;
}
final String commandName = command.iterator().next();
if ("exit".equals(commandName)) {
running = false;
return;
}
if ("help".equals(commandName)) {
printTasksHelp(out);
return;
}
if ("clear".equals(commandName)) {
try {
reader.clearScreen();
} catch (IOException e) {
log.error("Failed to clear screen", e);
}
return;
}
if ("timeout".equals(commandName)) {
internalTimeoutTask(out, command);
return;
}
final ShellIO io = new DirectShellIO(out);
final long start = System.nanoTime();
try {
runTask(command, io, core);
} catch (Exception e) {
e.printStackTrace();
}
final long diff = System.nanoTime() - start;
out.println(String.format("time: %s", Tasks.formatTimeNanos(diff)));
});
}
out.println();
out.println("Exiting...");
}
public void shutdown() throws IOException {
if (history != null) {
history.flush();
}
reader.shutdown();
}
void printTasksHelp(PrintWriter out) {
out.println("Available commands:");
for (final CommandDefinition c : commands) {
out.println(String.format("%s - %s", c.getName(), c.getUsage()));
if (!c.getAliases().isEmpty()) {
out.println(String.format(" aliases: %s", StringUtils.join(", ", c.getAliases())));
}
}
}
void internalTimeoutTask(PrintWriter out, List<String> args) {
if (args.size() < 2) {
if (this.timeout == 0) {
out.println("timeout disabled");
} else {
out.println(String.format("timeout = %d", this.timeout));
}
return;
}
final int timeout;
try {
timeout = Integer.parseInt(args.get(1));
} catch (Exception e) {
out.println(String.format("not a valid integer value: %s", args.get(1)));
return;
}
if (timeout <= 0) {
out.println("Timeout disabled");
this.timeout = 0;
} else {
out.println(String.format("Timeout updated to %d seconds", timeout));
this.timeout = timeout;
}
}
void runTask(List<String> command, final ShellIO io, final CoreInterface core)
throws Exception {
final AsyncFuture<Void> t;
try {
t = core.evaluate(command, io);
} catch (Exception e) {
io.out().println("Command failed");
e.printStackTrace(io.out());
return;
}
if (t == null) {
io.out().flush();
return;
}
try {
awaitFinished(t);
} catch (TimeoutException e) {
io.out().println(String.format("Command timed out (current timeout = %ds)", timeout));
t.cancel(true);
} catch (Exception e) {
io.out().println("Command failed");
e.printStackTrace(io.out());
return;
}
io.out().flush();
}
Void awaitFinished(final AsyncFuture<Void> t)
throws InterruptedException, ExecutionException, TimeoutException {
if (timeout > 0) {
return t.get(timeout, TimeUnit.SECONDS);
}
log.warn(String.format("Waiting forever for task (timeout = %d)", timeout));
return t.get();
}
public static HeroicInteractiveShell buildInstance(
final List<CommandDefinition> commands, FileInputStream input
) throws Exception {
final ConsoleReader reader = new ConsoleReader("heroicsh", input, System.out, null);
final FileHistory history = setupHistory(reader);
if (history != null) {
reader.setHistory(history);
}
reader.setPrompt(String.format("heroic> "));
reader.addCompleter(new StringsCompleter(
ImmutableList.copyOf(commands.stream().map((d) -> d.getName()).iterator())));
reader.setHandleUserInterrupt(true);
return new HeroicInteractiveShell(reader, commands, history);
}
private static FileHistory setupHistory(final ConsoleReader reader) throws IOException {
final String home = System.getProperty("user.home");
if (home == null) {
return null;
}
return new FileHistory(new File(home, ".heroicsh-history"));
}
}