PageRenderTime 58ms CodeModel.GetById 27ms app.highlight 27ms RepoModel.GetById 1ms app.codeStats 0ms

/hudson-core/src/main/java/hudson/cli/CLICommand.java

http://github.com/hudson/hudson
Java | 393 lines | 173 code | 39 blank | 181 comment | 8 complexity | 90623ded6ccd2fe273ca581248e26f5b MD5 | raw file
  1/*
  2 * The MIT License
  3 *
  4 * Copyright (c) 2004-2010, Sun Microsystems, Inc.
  5 *
  6 * Permission is hereby granted, free of charge, to any person obtaining a copy
  7 * of this software and associated documentation files (the "Software"), to deal
  8 * in the Software without restriction, including without limitation the rights
  9 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 10 * copies of the Software, and to permit persons to whom the Software is
 11 * furnished to do so, subject to the following conditions:
 12 *
 13 * The above copyright notice and this permission notice shall be included in
 14 * all copies or substantial portions of the Software.
 15 *
 16 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 17 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 18 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 19 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 20 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 21 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 22 * THE SOFTWARE.
 23 */
 24package hudson.cli;
 25
 26import hudson.AbortException;
 27import hudson.Extension;
 28import hudson.ExtensionList;
 29import hudson.ExtensionPoint;
 30import hudson.cli.declarative.CLIMethod;
 31import hudson.ExtensionPoint.LegacyInstancesAreScopedToHudson;
 32import hudson.cli.declarative.OptionHandlerExtension;
 33import hudson.model.Hudson;
 34import hudson.remoting.Callable;
 35import hudson.remoting.Channel;
 36import hudson.remoting.ChannelProperty;
 37import hudson.security.CliAuthenticator;
 38import hudson.security.SecurityRealm;
 39import org.acegisecurity.Authentication;
 40import org.acegisecurity.context.SecurityContext;
 41import org.acegisecurity.context.SecurityContextHolder;
 42import org.jvnet.hudson.annotation_indexer.Index;
 43import org.jvnet.tiger_types.Types;
 44import org.kohsuke.args4j.ClassParser;
 45import org.kohsuke.args4j.CmdLineException;
 46import org.kohsuke.args4j.CmdLineParser;
 47import org.kohsuke.args4j.spi.OptionHandler;
 48
 49import java.io.BufferedInputStream;
 50import java.io.IOException;
 51import java.io.InputStream;
 52import java.io.PrintStream;
 53import java.lang.reflect.Type;
 54import java.util.List;
 55import java.util.Locale;
 56import java.util.logging.Logger;
 57
 58/**
 59 * Base class for Hudson CLI.
 60 *
 61 * <h2>How does a CLI command work</h2>
 62 * <p>
 63 * The users starts {@linkplain CLI the "CLI agent"} on a remote system, by specifying arguments, like
 64 * <tt>"java -jar hudson-cli.jar command arg1 arg2 arg3"</tt>. The CLI agent creates
 65 * a remoting channel with the server, and it sends the entire arguments to the server, along with
 66 * the remoted stdin/out/err.
 67 *
 68 * <p>
 69 * The Hudson master then picks the right {@link CLICommand} to execute, clone it, and
 70 * calls {@link #main(List, Locale, InputStream, PrintStream, PrintStream)} method.
 71 *
 72 * <h2>Note for CLI command implementor</h2>
 73 * Start with <a href="http://wiki.hudson-ci.org/display/HUDSON/Writing+CLI+commands">this document</a>
 74 * to get the general idea of CLI.
 75 *
 76 * <ul>
 77 * <li>
 78 * Put {@link Extension} on your implementation to have it discovered by Hudson.
 79 *
 80 * <li>
 81 * Use <a href="http://java.net/projects/args4j/">args4j</a> annotation on your implementation to define
 82 * options and arguments (however, if you don't like that, you could override
 83 * the {@link #main(List, Locale, InputStream, PrintStream, PrintStream)} method directly.
 84 *
 85 * <li>
 86 * stdin, stdout, stderr are remoted, so proper buffering is necessary for good user experience.
 87 *
 88 * <li>
 89 * Send {@link Callable} to a CLI agent by using {@link #channel} to get local interaction,
 90 * such as uploading a file, asking for a password, etc.
 91 *
 92 * </ul>
 93 *
 94 * @author Kohsuke Kawaguchi
 95 * @since 1.302
 96 * @see CLIMethod
 97 */
 98@LegacyInstancesAreScopedToHudson
 99public abstract class CLICommand implements ExtensionPoint, Cloneable {
100    /**
101     * Connected to stdout and stderr of the CLI agent that initiated the session.
102     * IOW, if you write to these streams, the person who launched the CLI command
103     * will see the messages in his terminal.
104     *
105     * <p>
106     * (In contrast, calling {@code System.out.println(...)} would print out
107     * the message to the server log file, which is probably not what you want.
108     */
109    public transient PrintStream stdout,stderr;
110
111    /**
112     * Connected to stdin of the CLI agent.
113     *
114     * <p>
115     * This input stream is buffered to hide the latency in the remoting.
116     */
117    public transient InputStream stdin;
118
119    /**
120     * {@link Channel} that represents the CLI JVM. You can use this to
121     * execute {@link Callable} on the CLI JVM, among other things.
122     */
123    public transient Channel channel;
124
125    /**
126     * The locale of the client. Messages should be formatted with this resource.
127     */
128    public transient Locale locale;
129
130
131    /**
132     * Gets the command name.
133     *
134     * <p>
135     * For example, if the CLI is invoked as <tt>java -jar cli.jar foo arg1 arg2 arg4</tt>,
136     * on the server side {@link CLICommand} that returns "foo" from {@link #getName()}
137     * will be invoked.
138     *
139     * <p>
140     * By default, this method creates "foo-bar-zot" from "FooBarZotCommand".
141     */
142    public String getName() {
143        String name = getClass().getName();
144        name = name.substring(name.lastIndexOf('.')+1); // short name
145        name = name.substring(name.lastIndexOf('$')+1);
146        if(name.endsWith("Command"))
147            name = name.substring(0,name.length()-7); // trim off the command
148
149        // convert "FooBarZot" into "foo-bar-zot"
150        // Locale is fixed so that "CreateInstance" always become "create-instance" no matter where this is run.
151        return name.replaceAll("([a-z0-9])([A-Z])","$1-$2").toLowerCase(Locale.ENGLISH);
152    }
153
154    /**
155     * Gets the quick summary of what this command does.
156     * Used by the help command to generate the list of commands.
157     */
158    public abstract String getShortDescription();
159
160    public int main(List<String> args, Locale locale, InputStream stdin, PrintStream stdout, PrintStream stderr) {
161        this.stdin = new BufferedInputStream(stdin);
162        this.stdout = stdout;
163        this.stderr = stderr;
164        this.locale = locale;
165        this.channel = Channel.current();
166        registerOptionHandlers();
167        CmdLineParser p = new CmdLineParser(this);
168
169        // add options from the authenticator
170        SecurityContext sc = SecurityContextHolder.getContext();
171        Authentication old = sc.getAuthentication();
172
173        CliAuthenticator authenticator = Hudson.getInstance().getSecurityRealm().createCliAuthenticator(this);
174        new ClassParser().parse(authenticator,p);
175
176        try {
177            p.parseArgument(args.toArray(new String[args.size()]));
178            Authentication auth = authenticator.authenticate();
179            if (auth==Hudson.ANONYMOUS)
180                auth = loadStoredAuthentication();
181            sc.setAuthentication(auth); // run the CLI with the right credential
182            if (!(this instanceof LoginCommand || this instanceof HelpCommand))
183                Hudson.getInstance().checkPermission(Hudson.READ);
184            return run();
185        } catch (CmdLineException e) {
186            stderr.println(e.getMessage());
187            printUsage(stderr, p);
188            return -1;
189        } catch (AbortException e) {
190            // signals an error without stack trace
191            stderr.println(e.getMessage());
192            return -1;
193        } catch (Exception e) {
194            e.printStackTrace(stderr);
195            return -1;
196        } finally {
197            sc.setAuthentication(old); // restore
198        }
199    }
200
201    /**
202     * Loads the persisted authentication information from {@link ClientAuthenticationCache}.
203     */
204    protected Authentication loadStoredAuthentication() throws InterruptedException {
205        try {
206            return new ClientAuthenticationCache(channel).get();
207        } catch (IOException e) {
208            stderr.println("Failed to access the stored credential");
209            e.printStackTrace(stderr);  // recover
210            return Hudson.ANONYMOUS;
211        }
212    }
213
214    /**
215     * Determines if the user authentication is attempted through CLI before running this command.
216     *
217     * <p>
218     * If your command doesn't require any authentication whatsoever, and if you don't even want to let the user
219     * authenticate, then override this method to always return false &mdash; doing so will result in all the commands
220     * running as anonymous user credential.
221     *
222     * <p>
223     * Note that even if this method returns true, the user can still skip aut 
224     *
225     * @param auth
226     *      Always non-null.
227     *      If the underlying transport had already performed authentication, this object is something other than
228     *      {@link Hudson#ANONYMOUS}.
229     */
230    protected boolean shouldPerformAuthentication(Authentication auth) {
231        return auth==Hudson.ANONYMOUS;
232    }
233
234    /**
235     * Returns the identity of the client as determined at the CLI transport level.
236     *
237     * <p>
238     * When the CLI connection to the server is tunneled over HTTP, that HTTP connection
239     * can authenticate the client, just like any other HTTP connections to the server
240     * can authenticate the client. This method returns that information, if one is available.
241     * By generalizing it, this method returns the identity obtained at the transport-level authentication.
242     *
243     * <p>
244     * For example, imagine if the current {@link SecurityRealm} is doing Kerberos authentication,
245     * then this method can return a valid identity of the client.
246     *
247     * <p>
248     * If the transport doesn't do authentication, this method returns {@link Hudson#ANONYMOUS}.
249     */
250    public Authentication getTransportAuthentication() {
251        Authentication a = channel.getProperty(TRANSPORT_AUTHENTICATION);
252        if (a==null)    a = Hudson.ANONYMOUS;
253        return a;
254    }
255
256    /**
257     * Executes the command, and return the exit code.
258     *
259     * @return
260     *      0 to indicate a success, otherwise an error code.
261     * @throws AbortException
262     *      If the processing should be aborted. Hudson will report the error message
263     *      without stack trace, and then exits this command.
264     * @throws Exception
265     *      All the other exceptions cause the stack trace to be dumped, and then
266     *      the command exits with an error code.
267     */
268    protected abstract int run() throws Exception;
269
270    protected void printUsage(PrintStream stderr, CmdLineParser p) {
271        stderr.println("java -jar hudson-cli.jar "+getName()+" args...");
272        printUsageSummary(stderr);
273        p.printUsage(stderr);
274    }
275
276    /**
277     * Called while producing usage. This is a good method to override
278     * to render the general description of the command that goes beyond
279     * a single-line summary. 
280     */
281    protected void printUsageSummary(PrintStream stderr) {
282        stderr.println(getShortDescription());
283    }
284
285    /**
286     * Convenience method for subtypes to obtain the system property of the client.
287     */
288    protected String getClientSystemProperty(String name) throws IOException, InterruptedException {
289        return channel.call(new GetSystemProperty(name));
290    }
291
292    private static final class GetSystemProperty implements Callable<String, IOException> {
293        private final String name;
294
295        private GetSystemProperty(String name) {
296            this.name = name;
297        }
298
299        public String call() throws IOException {
300            return System.getProperty(name);
301        }
302
303        private static final long serialVersionUID = 1L;
304    }
305
306    /**
307     * Convenience method for subtypes to obtain environment variables of the client.
308     */
309    protected String getClientEnvironmentVariable(String name) throws IOException, InterruptedException {
310        return channel.call(new GetEnvironmentVariable(name));
311    }
312
313    private static final class GetEnvironmentVariable implements Callable<String, IOException> {
314        private final String name;
315
316        private GetEnvironmentVariable(String name) {
317            this.name = name;
318        }
319
320        public String call() throws IOException {
321            return System.getenv(name);
322        }
323
324        private static final long serialVersionUID = 1L;
325    }
326
327    /**
328     * Creates a clone to be used to execute a command.
329     */
330    protected CLICommand createClone() {
331        try {
332            return getClass().newInstance();
333        } catch (IllegalAccessException e) {
334            throw new AssertionError(e);
335        } catch (InstantiationException e) {
336            throw new AssertionError(e);
337        }
338    }
339
340    /**
341     * Auto-discovers {@link OptionHandler}s and add them to the given command line parser.
342     */
343    protected void registerOptionHandlers() {
344        try {
345            for (Class c : Index.list(OptionHandlerExtension.class,Hudson.getInstance().pluginManager.uberClassLoader,Class.class)) {
346                Type t = Types.getBaseClass(c, OptionHandler.class);
347                CmdLineParser.registerHandler(Types.erasure(Types.getTypeArgument(t,0)), c);
348            }
349        } catch (IOException e) {
350            throw new Error(e);
351        }
352    }
353
354    /**
355     * Returns all the registered {@link CLICommand}s.
356     */
357    public static ExtensionList<CLICommand> all() {
358        return Hudson.getInstance().getExtensionList(CLICommand.class);
359    }
360
361    /**
362     * Obtains a copy of the command for invocation.
363     */
364    public static CLICommand clone(String name) {
365        for (CLICommand cmd : all())
366            if(name.equals(cmd.getName()))
367                return cmd.createClone();
368        return null;
369    }
370
371    private static final Logger LOGGER = Logger.getLogger(CLICommand.class.getName());
372
373    /**
374     * Key for {@link Channel#getProperty(Object)} that links to the {@link Authentication} object
375     * which captures the identity of the client given by the transport layer.
376     */
377    public static final ChannelProperty<Authentication> TRANSPORT_AUTHENTICATION = new ChannelProperty<Authentication>(Authentication.class,"transportAuthentication");
378
379    private static final ThreadLocal<CLICommand> CURRENT_COMMAND = new ThreadLocal<CLICommand>();
380
381    /*package*/ static CLICommand setCurrent(CLICommand cmd) {
382        CLICommand old = getCurrent();
383        CURRENT_COMMAND.set(cmd);
384        return old;
385    }
386
387    /**
388     * If the calling thread is in the middle of executing a CLI command, return it. Otherwise null.
389     */
390    public static CLICommand getCurrent() {
391        return CURRENT_COMMAND.get();
392    }
393}