/* * The MIT License * * Copyright (c) 2004-2010, Sun Microsystems, Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package hudson.cli.declarative; import hudson.AbortException; import hudson.Extension; import hudson.ExtensionComponent; import hudson.ExtensionFinder; import hudson.Util; import hudson.cli.CLICommand; import hudson.cli.CloneableCLICommand; import hudson.model.Hudson; import jenkins.ExtensionComponentSet; import jenkins.ExtensionRefreshException; import jenkins.model.Jenkins; import hudson.security.CliAuthenticator; import org.acegisecurity.AccessDeniedException; import org.acegisecurity.Authentication; import org.acegisecurity.BadCredentialsException; import org.acegisecurity.context.SecurityContext; import org.acegisecurity.context.SecurityContextHolder; import org.jvnet.hudson.annotation_indexer.Index; import org.jvnet.localizer.ResourceBundleHolder; import org.kohsuke.args4j.ClassParser; import org.kohsuke.args4j.CmdLineParser; import org.kohsuke.args4j.CmdLineException; import java.io.IOException; import java.io.InputStream; import java.io.PrintStream; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.Stack; import static java.util.logging.Level.SEVERE; import java.util.UUID; import java.util.logging.Level; import java.util.logging.Logger; /** * Discover {@link CLIMethod}s and register them as {@link CLICommand} implementations. * * @author Kohsuke Kawaguchi */ @Extension public class CLIRegisterer extends ExtensionFinder { @Override public ExtensionComponentSet refresh() throws ExtensionRefreshException { // TODO: this is not complex. just bit tedious. return ExtensionComponentSet.EMPTY; } public <T> Collection<ExtensionComponent<T>> find(Class<T> type, Hudson jenkins) { if (type==CLICommand.class) return (List)discover(jenkins); else return Collections.emptyList(); } /** * Finds a resolved method annotated with {@link CLIResolver}. */ private Method findResolver(Class type) throws IOException { List<Method> resolvers = Util.filter(Index.list(CLIResolver.class, Jenkins.getInstance().getPluginManager().uberClassLoader), Method.class); for ( ; type!=null; type=type.getSuperclass()) for (Method m : resolvers) if (m.getReturnType()==type) return m; return null; } private List<ExtensionComponent<CLICommand>> discover(final Jenkins hudson) { LOGGER.fine("Listing up @CLIMethod"); List<ExtensionComponent<CLICommand>> r = new ArrayList<ExtensionComponent<CLICommand>>(); try { for ( final Method m : Util.filter(Index.list(CLIMethod.class, hudson.getPluginManager().uberClassLoader),Method.class)) { try { // command name final String name = m.getAnnotation(CLIMethod.class).name(); final ResourceBundleHolder res = loadMessageBundle(m); res.format("CLI."+name+".shortDescription"); // make sure we have the resource, to fail early r.add(new ExtensionComponent<CLICommand>(new CloneableCLICommand() { @Override public String getName() { return name; } @Override public String getShortDescription() { // format by using the right locale return res.format("CLI."+name+".shortDescription"); } @Override protected CmdLineParser getCmdLineParser() { return bindMethod(new ArrayList<MethodBinder>()); } private CmdLineParser bindMethod(List<MethodBinder> binders) { registerOptionHandlers(); CmdLineParser parser = new CmdLineParser(null); // build up the call sequence Stack<Method> chains = new Stack<Method>(); Method method = m; while (true) { chains.push(method); if (Modifier.isStatic(method.getModifiers())) break; // the chain is complete. // the method in question is an instance method, so we need to resolve the instance by using another resolver Class<?> type = method.getDeclaringClass(); try { method = findResolver(type); } catch (IOException ex) { throw new RuntimeException("Unable to find the resolver method annotated with @CLIResolver for "+type, ex); } if (method==null) { throw new RuntimeException("Unable to find the resolver method annotated with @CLIResolver for "+type); } } while (!chains.isEmpty()) binders.add(new MethodBinder(chains.pop(),this,parser)); return parser; } /** * Envelope an annotated CLI command * * @param args * Arguments to the sub command. For example, if the CLI is invoked like "java -jar cli.jar foo bar zot", * then "foo" is the sub-command and the argument list is ["bar","zot"]. * @param locale * Locale of the client (which can be different from that of the server.) Good behaving command implementation * would use this locale for formatting messages. * @param stdin * Connected to the stdin of the CLI client. * @param stdout * Connected to the stdout of the CLI client. * @param stderr * Connected to the stderr of the CLI client. * @return * Exit code from the CLI command execution * * <p> * Jenkins standard exit codes from CLI: * 0 means everything went well. * 1 means further unspecified exception is thrown while performing the command. * 2 means CmdLineException is thrown while performing the command. * 3 means IllegalArgumentException is thrown while performing the command. * 4 mean IllegalStateException is thrown while performing the command. * 5 means AbortException is thrown while performing the command. * 6 means AccessDeniedException is thrown while performing the command. * 7 means BadCredentialsException is thrown while performing the command. * 8-15 are reserved for future usage * 16+ mean a custom CLI exit error code (meaning defined by the CLI command itself) * * <p> * Note: For details - see JENKINS-32273 */ @Override public int main(List<String> args, Locale locale, InputStream stdin, PrintStream stdout, PrintStream stderr) { this.stdout = stdout; this.stderr = stderr; this.locale = locale; List<MethodBinder> binders = new ArrayList<MethodBinder>(); CmdLineParser parser = bindMethod(binders); try { SecurityContext sc = SecurityContextHolder.getContext(); Authentication old = sc.getAuthentication(); try { // authentication CliAuthenticator authenticator = Jenkins.getInstance().getSecurityRealm().createCliAuthenticator(this); new ClassParser().parse(authenticator, parser); // fill up all the binders parser.parseArgument(args); Authentication auth = authenticator.authenticate(); if (auth == Jenkins.ANONYMOUS) auth = loadStoredAuthentication(); sc.setAuthentication(auth); // run the CLI with the right credential hudson.checkPermission(Jenkins.READ); // resolve them Object instance = null; for (MethodBinder binder : binders) instance = binder.call(instance); if (instance instanceof Integer) return (Integer) instance; else return 0; } catch (InvocationTargetException e) { Throwable t = e.getTargetException(); if (t instanceof Exception) throw (Exception) t; throw e; } finally { sc.setAuthentication(old); // restore } } catch (CmdLineException e) { stderr.println(""); stderr.println("ERROR: " + e.getMessage()); printUsage(stderr, parser); return 2; } catch (IllegalStateException e) { stderr.println(""); stderr.println("ERROR: " + e.getMessage()); return 4; } catch (IllegalArgumentException e) { stderr.println(""); stderr.println("ERROR: " + e.getMessage()); return 3; } catch (AbortException e) { stderr.println(""); stderr.println("ERROR: " + e.getMessage()); return 5; } catch (AccessDeniedException e) { stderr.println(""); stderr.println("ERROR: " + e.getMessage()); return 6; } catch (BadCredentialsException e) { // to the caller, we can't reveal whether the user didn't exist or the password didn't match. // do that to the server log instead String id = UUID.randomUUID().toString(); LOGGER.log(Level.INFO, "CLI login attempt failed: " + id, e); stderr.println(""); stderr.println("ERROR: Bad Credentials. Search the server log for " + id + " for more details."); return 7; } catch (Throwable e) { final String errorMsg = String.format("Unexpected exception occurred while performing %s command.", getName()); stderr.println(""); stderr.println("ERROR: " + errorMsg); LOGGER.log(Level.WARNING, errorMsg, e); e.printStackTrace(stderr); return 1; } } protected int run() throws Exception { throw new UnsupportedOperationException(); } })); } catch (ClassNotFoundException e) { LOGGER.log(SEVERE,"Failed to process @CLIMethod: "+m,e); } } } catch (IOException e) { LOGGER.log(SEVERE, "Failed to discover @CLIMethod",e); } return r; } /** * Locates the {@link ResourceBundleHolder} for this CLI method. */ private ResourceBundleHolder loadMessageBundle(Method m) throws ClassNotFoundException { Class c = m.getDeclaringClass(); Class<?> msg = c.getClassLoader().loadClass(c.getName().substring(0, c.getName().lastIndexOf(".")) + ".Messages"); return ResourceBundleHolder.get(msg); } private static final Logger LOGGER = Logger.getLogger(CLIRegisterer.class.getName()); }