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