blob: 52f1b7a051012697df8ee84a444fd99bf260d0e0 [file] [log] [blame]
/*
* 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 org.jclouds.cli.runner;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintStream;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.UnknownHostException;
import java.util.Collections;
import java.util.Enumeration;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeoutException;
import jline.Terminal;
import org.apache.felix.gogo.commands.Action;
import org.apache.felix.gogo.commands.Command;
import org.apache.felix.gogo.commands.CommandException;
import org.apache.felix.gogo.commands.basic.AbstractCommand;
import org.apache.felix.gogo.runtime.CommandNotFoundException;
import org.apache.felix.gogo.runtime.CommandProcessorImpl;
import org.apache.felix.gogo.runtime.threadio.ThreadIOImpl;
import org.apache.felix.service.command.CommandSession;
import org.apache.felix.service.command.Function;
import org.apache.karaf.shell.console.NameScoping;
import org.apache.karaf.shell.console.jline.Console;
import org.apache.karaf.shell.console.jline.TerminalFactory;
import org.fusesource.jansi.Ansi;
import org.fusesource.jansi.AnsiConsole;
import org.jclouds.blobstore.ContainerNotFoundException;
import org.jclouds.blobstore.KeyNotFoundException;
import org.jclouds.rest.AuthorizationException;
import org.jclouds.rest.InsufficientResourcesException;
import org.jclouds.rest.ResourceAlreadyExistsException;
import org.jclouds.rest.ResourceNotFoundException;
import org.jclouds.util.Throwables2;
import com.google.common.collect.Lists;
/**
* This is forked from Apache Karaf and aligned to the needs of jclouds cli.
*/
public class Main {
private static final String KARAF_HOME = "karaf.home";
private static final Class[] parameters = new Class[] {URL.class};
private String application = System.getProperty("karaf.name", "root");
private String user = "karaf";
private static enum Errno {
ENOENT(2),
EIO(5),
ENXIO(6),
EACCES(13),
EEXIST(17),
ETIMEDOUT(110),
EDQUOT(122),
UNKNOWN(255);
private final int errno;
Errno(int errno) {
this.errno = errno;
}
int getErrno() {
return errno;
}
}
/**
* Exit if klass has the same type or sub-type as throwable or one of its
* causes. If exiting, emit message.
*/
private static void exitIfThrowableMatches(Throwable throwable,
Class klass, Errno errno, String message) {
Throwable cause = Throwables2.getFirstThrowableOfType(throwable, klass);
if (cause == null) {
return;
}
System.err.format("%s: %s\n", message, cause.getMessage());
System.exit(errno.getErrno());
}
public static void main(String args[]) throws Exception {
Main main = new Main();
try {
main.run(args);
} catch (CommandNotFoundException cnfe) {
String str = Ansi.ansi()
.fg(Ansi.Color.RED)
.a("Command not found: ")
.a(Ansi.Attribute.INTENSITY_BOLD)
.a(cnfe.getCommand())
.a(Ansi.Attribute.INTENSITY_BOLD_OFF)
.fg(Ansi.Color.DEFAULT).toString();
System.err.println(str);
System.exit(Errno.UNKNOWN.getErrno());
} catch (CommandException ce) {
System.err.println(ce.getNiceHelp());
System.exit(Errno.UNKNOWN.getErrno());
} catch (Throwable t) {
exitIfThrowableMatches(t, AuthorizationException.class, Errno.EACCES, "Authorization error");
exitIfThrowableMatches(t, ContainerNotFoundException.class, Errno.ENOENT, "Container not found");
// FileNotFoundException must precede IOException due to inheritance
exitIfThrowableMatches(t, FileNotFoundException.class, Errno.ENOENT, "File not found");
// UnknownHostException must precede IOException due to inheritance
exitIfThrowableMatches(t, UnknownHostException.class, Errno.ENXIO, "Unknown host");
exitIfThrowableMatches(t, IOException.class, Errno.EIO, "IO error");
exitIfThrowableMatches(t, InsufficientResourcesException.class, Errno.EDQUOT, "Insufficient resources");
exitIfThrowableMatches(t, KeyNotFoundException.class, Errno.ENOENT, "Blob not found");
exitIfThrowableMatches(t, ResourceAlreadyExistsException.class, Errno.EEXIST, "Resource already exists");
// ContainerNotFoundException and KeyNotFoundException must precede ResourceNotFoundException due to inheritance
exitIfThrowableMatches(t, ResourceNotFoundException.class, Errno.ENOENT, "Resource not found");
exitIfThrowableMatches(t, TimeoutException.class, Errno.ETIMEDOUT, "Timeout");
t.printStackTrace();
System.exit(Errno.UNKNOWN.getErrno());
}
// We must explicitly exit on success since we do not close
// BlobStoreContext and ComputeServiceContext.
System.exit(0);
}
/**
* Use this method when the shell is being executed as a top level shell.
*
* @param args
* @throws Exception
*/
public void run(String args[]) throws Exception {
ThreadIOImpl threadio = new ThreadIOImpl();
threadio.start();
ClassLoader cl = Main.class.getClassLoader();
//This is a workaround for windows machines struggling with long class paths.
loadJarsFromPath(new File(System.getProperty(KARAF_HOME), "system"));
loadJarsFromPath(new File(System.getProperty(KARAF_HOME), "deploy"));
CommandProcessorImpl commandProcessor = new CommandProcessorImpl(threadio);
discoverCommands(commandProcessor, cl);
InputStream in = unwrap(System.in);
PrintStream out = wrap(unwrap(System.out));
PrintStream err = wrap(unwrap(System.err));
run(commandProcessor, args, in, out, err);
}
/**
* Loads Jars found under the specified path.
* @throws IOException
*/
public void loadJarsFromPath(File path) throws IOException {
LinkedList<File> dirs = Lists.newLinkedList();
dirs.add(path);
while (!dirs.isEmpty()) {
for (File f : dirs.poll().listFiles()) {
if (f.isDirectory()) {
dirs.add(f);
} else if (f.isFile() && f.getAbsolutePath().endsWith(".jar") && !f.getAbsolutePath().contains("pax-logging")) {
//We make sure to exclude pax logging jars when running outside of OSGi, since we use external logging jars in that case.
addURL(f.toURI().toURL());
}
}
}
}
public static void addURL(URL u) throws IOException {
URLClassLoader sysloader = (URLClassLoader) ClassLoader.getSystemClassLoader();
Class sysclass = URLClassLoader.class;
try {
Method method = sysclass.getDeclaredMethod("addURL", parameters);
method.setAccessible(true);
method.invoke(sysloader, new Object[] {u});
} catch (Throwable t) {
t.printStackTrace();
throw new IOException("Error, could not add URL to system classloader");
}
}
private void run(final CommandProcessorImpl commandProcessor, String[] args, final InputStream in, final PrintStream out, final PrintStream err) throws Exception {
if (args.length > 0) {
// Commands have the form: jclouds:category-action.
List<String> commands = Lists.newArrayList(commandProcessor.getCommands());
Collections.sort(commands);
if (!commands.contains(args[0])) {
final String INDENT = " ";
StringBuilder sb = new StringBuilder(
"Usage: jclouds {category} {action} {options/arguments}");
// list commands
sb.append("\n\nValid commands:");
for (String command : commands) {
if (command.startsWith("jclouds:")) {
command = command.substring("jclouds:".length());
int index = command.indexOf('-');
String category = command.substring(0, index);
String action = command.substring(index + 1);
sb.append('\n').append(INDENT).append(category)
.append(' ').append(action);
// TODO: expose command descriptions
}
}
// list options
sb.append("\n\nOptions:")
.append('\n').append(INDENT)
.append("--properties: File containing properties")
.append('\n').append(INDENT)
.append("--provider: The id of the provider")
.append('\n').append(INDENT)
.append("--api: The id of the api")
.append('\n').append(INDENT)
.append("--endpoint: The endpoint")
.append('\n').append(INDENT)
.append("--identity: The identity")
.append('\n').append(INDENT)
.append("--credential: The credential");
throw new CommandNotFoundException(sb.toString());
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < args.length; i++) {
if (i > 0) {
sb.append(" ");
}
sb.append(args[i]);
}
// Shell is directly executing a sub/command, we don't setup a terminal and console
// in this case, this avoids us reading from stdin un-necessarily.
CommandSession session = commandProcessor.createSession(in, out, err);
session.put("USER", user);
session.put("APPLICATION", application);
session.put(NameScoping.MULTI_SCOPE_MODE_KEY, Boolean.toString(isMultiScopeMode()));
session.execute(sb);
} else {
// We are going into full blown interactive shell mode.
final TerminalFactory terminalFactory = new TerminalFactory();
final Terminal terminal = terminalFactory.getTerminal();
Console console = createConsole(commandProcessor, in, out, err, terminal);
CommandSession session = console.getSession();
session.put("USER", user);
session.put("APPLICATION", application);
session.put(NameScoping.MULTI_SCOPE_MODE_KEY, Boolean.toString(isMultiScopeMode()));
session.put("#LINES", new Function() {
public Object execute(CommandSession session, List<Object> arguments) throws Exception {
return Integer.toString(terminal.getHeight());
}
});
session.put("#COLUMNS", new Function() {
public Object execute(CommandSession session, List<Object> arguments) throws Exception {
return Integer.toString(terminal.getWidth());
}
});
session.put(".jline.terminal", terminal);
console.run();
terminalFactory.destroy();
}
}
/**
* Allow sub classes of main to change the Console implementation used.
*
* @param commandProcessor
* @param in
* @param out
* @param err
* @param terminal
* @return
* @throws Exception
*/
protected Console createConsole(CommandProcessorImpl commandProcessor, InputStream in, PrintStream out, PrintStream err, Terminal terminal) throws Exception {
return new Console(commandProcessor, in, out, err, terminal, null);
}
/**
* Sub classes can override so that their registered commands do not conflict with the default shell
* implementation.
*
* @return
*/
public String getDiscoveryResource() {
return "META-INF/services/org/apache/karaf/shell/commands";
}
private void discoverCommands(CommandProcessorImpl commandProcessor, ClassLoader cl) throws IOException, ClassNotFoundException {
Enumeration<URL> urls = cl.getResources(getDiscoveryResource());
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
BufferedReader reader = new BufferedReader(new InputStreamReader(url.openStream()));
try {
while (true) {
String line = reader.readLine();
if (line == null) {
break;
}
line = line.trim();
if (line.isEmpty() || line.charAt(0) == '#') {
continue;
}
final Class<Action> actionClass = (Class<Action>) cl.loadClass(line);
Command cmd = actionClass.getAnnotation(Command.class);
Function function = new AbstractCommand() {
@Override
public Action createNewAction() {
try {
return ((Class<? extends Action>) actionClass).newInstance();
} catch (InstantiationException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
};
addCommand(cmd, function, commandProcessor);
}
} finally {
reader.close();
}
}
}
protected void addCommand(Command cmd, Function function, CommandProcessorImpl commandProcessor) {
try {
commandProcessor.addCommand(cmd.scope(), function, cmd.name());
} catch (Exception e) {
}
}
public String getApplication() {
return application;
}
public void setApplication(String application) {
this.application = application;
}
public String getUser() {
return user;
}
public void setUser(String user) {
this.user = user;
}
/**
* Returns whether or not we are in multi-scope mode.
* <p/>
* The default mode is multi-scoped where we prefix commands by their scope. If we are in single
* scoped mode then we don't use scope prefixes when registering or tab completing commands.
*/
public boolean isMultiScopeMode() {
return true;
}
private static PrintStream wrap(PrintStream stream) {
OutputStream o = AnsiConsole.wrapOutputStream(stream);
if (o instanceof PrintStream) {
return (PrintStream) o;
} else {
return new PrintStream(o);
}
}
private static <T> T unwrap(T stream) {
try {
Method mth = stream.getClass().getMethod("getRoot");
return (T) mth.invoke(stream);
} catch (Throwable t) {
return stream;
}
}
private static List<URL> getFiles(File base) throws MalformedURLException {
List<URL> urls = Lists.newArrayList();
getFiles(base, urls);
return urls;
}
private static void getFiles(File base, List<URL> urls) throws MalformedURLException {
for (File f : base.listFiles()) {
if (f.isDirectory()) {
getFiles(f, urls);
} else if (f.getName().endsWith(".jar")) {
urls.add(f.toURI().toURL());
}
}
}
}