Skip to content

Commit

Permalink
Merge pull request #973 from olivergondza/cli-help
Browse files Browse the repository at this point in the history
[JENKINS-20023] Make CLI interface help more accesible
  • Loading branch information
kohsuke committed Oct 26, 2013
2 parents 705b013 + ae74404 commit 9ee9684
Show file tree
Hide file tree
Showing 26 changed files with 489 additions and 80 deletions.
123 changes: 123 additions & 0 deletions core/src/main/java/hudson/cli/CLIAction.java
@@ -0,0 +1,123 @@
/*
* The MIT License
*
* Copyright (c) 2013 Red Hat, 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;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletResponse;

import jenkins.model.Jenkins;

import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;

import hudson.Extension;
import hudson.model.FullDuplexHttpChannel;
import hudson.model.RootAction;
import hudson.remoting.Channel;

/**
* @author ogondza
*/
@Extension
@Restricted(NoExternalUse.class)
public class CLIAction implements RootAction {

private transient final Map<UUID,FullDuplexHttpChannel> duplexChannels = new HashMap<UUID, FullDuplexHttpChannel>();

public String getIconFileName() {
return null;
}

public String getDisplayName() {

return "Jenkins CLI";
}

public String getUrlName() {

return "/cli";
}

public void doCommand(StaplerRequest req, StaplerResponse rsp) throws ServletException, IOException {
final Jenkins jenkins = Jenkins.getInstance();
jenkins.checkPermission(Jenkins.READ);

// Strip trailing slash
final String commandName = req.getRestOfPath().substring(1);
CLICommand command = CLICommand.clone(commandName);
if (command == null) {
rsp.sendError(HttpServletResponse.SC_NOT_FOUND, "No such command " + commandName);
return;
}

req.setAttribute("command", command);
req.getView(this, "command.jelly").forward(req, rsp);
}

/**
* Handles HTTP requests for duplex channels for CLI.
*/
public void doIndex(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException, InterruptedException {
final Jenkins jenkins = Jenkins.getInstance();
if (!"POST".equals(req.getMethod())) {
// for GET request, serve _cli.jelly, assuming this is a browser
jenkins.checkPermission(Jenkins.READ);
req.setAttribute("command", CLICommand.clone("help"));
req.getView(this,"index.jelly").forward(req,rsp);
return;
}

// do not require any permission to establish a CLI connection
// the actual authentication for the connecting Channel is done by CLICommand

UUID uuid = UUID.fromString(req.getHeader("Session"));
rsp.setHeader("Hudson-Duplex",""); // set the header so that the client would know

FullDuplexHttpChannel server;
if(req.getHeader("Side").equals("download")) {
duplexChannels.put(uuid,server=new FullDuplexHttpChannel(uuid, !jenkins.hasPermission(Jenkins.ADMINISTER)) {
@Override
protected void main(Channel channel) throws IOException, InterruptedException {
// capture the identity given by the transport, since this can be useful for SecurityRealm.createCliAuthenticator()
channel.setProperty(CLICommand.TRANSPORT_AUTHENTICATION, Jenkins.getAuthentication());
channel.setProperty(CliEntryPoint.class.getName(),new CliManagerImpl(channel));
}
});
try {
server.download(req,rsp);
} finally {
duplexChannels.remove(uuid);
}
} else {
duplexChannels.get(uuid).upload(req,rsp);
}
}
}
52 changes: 50 additions & 2 deletions core/src/main/java/hudson/cli/CLICommand.java
Expand Up @@ -46,12 +46,15 @@
import org.apache.commons.discovery.resource.names.DiscoverServiceNames;
import org.jvnet.hudson.annotation_indexer.Index;
import org.jvnet.tiger_types.Types;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.args4j.ClassParser;
import org.kohsuke.args4j.CmdLineException;
import org.kohsuke.args4j.CmdLineParser;
import org.kohsuke.args4j.spi.OptionHandler;

import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
Expand Down Expand Up @@ -208,7 +211,7 @@ public int main(List<String> args, Locale locale, InputStream stdin, PrintStream
this.stderr = stderr;
this.locale = locale;
registerOptionHandlers();
CmdLineParser p = new CmdLineParser(this);
CmdLineParser p = getCmdLineParser();

// add options from the authenticator
SecurityContext sc = SecurityContextHolder.getContext();
Expand Down Expand Up @@ -242,6 +245,16 @@ public int main(List<String> args, Locale locale, InputStream stdin, PrintStream
sc.setAuthentication(old); // restore
}
}

/**
* Get parser for this command.
*
* Exposed to be overridden by {@link CLIRegisterer}.
* @since TODO
*/
protected CmdLineParser getCmdLineParser() {
return new CmdLineParser(this);
}

public Channel checkChannel() throws AbortException {
if (channel==null)
Expand Down Expand Up @@ -329,11 +342,46 @@ public void setTransportAuth(Authentication transportAuth) {
protected abstract int run() throws Exception;

protected void printUsage(PrintStream stderr, CmdLineParser p) {
stderr.println("java -jar jenkins-cli.jar "+getName()+" args...");
stderr.print("java -jar jenkins-cli.jar " + getName());
p.printSingleLineUsage(stderr);
stderr.println();
printUsageSummary(stderr);
p.printUsage(stderr);
}

/**
* Get single line summary as a string.
*/
@Restricted(NoExternalUse.class)
public final String getSingleLineSummary() {
ByteArrayOutputStream out = new ByteArrayOutputStream();
getCmdLineParser().printSingleLineUsage(out);
return out.toString();
}

/**
* Get usage as a string.
*/
@Restricted(NoExternalUse.class)
public final String getUsage() {
ByteArrayOutputStream out = new ByteArrayOutputStream();
getCmdLineParser().printUsage(out);
return out.toString();
}

/**
* Get long description as a string.
*/
@Restricted(NoExternalUse.class)
public final String getLongDescription() {
ByteArrayOutputStream out = new ByteArrayOutputStream();
PrintStream ps = new PrintStream(out);

printUsageSummary(ps);
ps.close();
return out.toString();
}

/**
* Called while producing usage. This is a good method to override
* to render the general description of the command that goes beyond
Expand Down
2 changes: 1 addition & 1 deletion core/src/main/java/hudson/cli/GroovyCommand.java
Expand Up @@ -66,7 +66,7 @@ public String getShortDescription() {
/**
* Remaining arguments.
*/
@Argument(index=1)
@Argument(metaVar="ARGUMENTS", index=1, usage="Command line arguments to pass into script.")
public List<String> remaining = new ArrayList<String>();

protected int run() throws Exception {
Expand Down
31 changes: 30 additions & 1 deletion core/src/main/java/hudson/cli/HelpCommand.java
Expand Up @@ -29,25 +29,41 @@
import java.util.Map;
import java.util.TreeMap;

import org.kohsuke.args4j.Argument;

/**
* Show the list of all commands.
*
* @author Kohsuke Kawaguchi
*/
@Extension
public class HelpCommand extends CLICommand {

@Argument(metaVar="COMMAND", usage="Name of the command")
public String command;

@Override
public String getShortDescription() {
return Messages.HelpCommand_ShortDescription();
}

@Override
protected int run() {
if (!Jenkins.getInstance().hasPermission(Jenkins.READ)) {
stderr.println("You must authenticate to access this Jenkins.\n"
+ "Use --username/--password/--password-file parameters or login command.");
return 0;
return -1;
}

if (command != null)
return showCommandDetails();

showAllCommands();

return 0;
}

private int showAllCommands() {
Map<String,CLICommand> commands = new TreeMap<String,CLICommand>();
for (CLICommand c : CLICommand.all())
commands.put(c.getName(),c);
Expand All @@ -56,6 +72,19 @@ protected int run() {
stderr.println(" "+c.getName());
stderr.println(" "+c.getShortDescription());
}

return 0;
}

private int showCommandDetails() {
CLICommand command = CLICommand.clone(this.command);
if (command == null) {
stderr.format("No such command %s. Awailable commands are: ", this.command);
showAllCommands();
return -1;
}

command.printUsage(stderr, command.getCmdLineParser());

return 0;
}
Expand Down
4 changes: 2 additions & 2 deletions core/src/main/java/hudson/cli/SetBuildParameterCommand.java
Expand Up @@ -18,10 +18,10 @@
*/
@Extension
public class SetBuildParameterCommand extends CommandDuringBuild {
@Argument(index=0, required=true, usage="Name of the build variable")
@Argument(index=0, metaVar="NAME", required=true, usage="Name of the build variable")
public String name;

@Argument(index=1,required=true, usage="Value of the build variable")
@Argument(index=1, metaVar="VALUE", required=true, usage="Value of the build variable")
public String value;

@Override
Expand Down
67 changes: 42 additions & 25 deletions core/src/main/java/hudson/cli/declarative/CLIRegisterer.java
Expand Up @@ -110,48 +110,65 @@ 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));

new ClassParser().parse(Jenkins.getInstance().getSecurityRealm().createCliAuthenticator(this), parser);

return parser;
}

@Override
public int main(List<String> args, Locale locale, InputStream stdin, PrintStream stdout, PrintStream stderr) {
this.stdout = stdout;
this.stderr = stderr;
this.locale = locale;

registerOptionHandlers();
CmdLineParser parser = new CmdLineParser(null);
List<MethodBinder> binders = new ArrayList<MethodBinder>();

CmdLineParser parser = bindMethod(binders);
try {
SecurityContext sc = SecurityContextHolder.getContext();
Authentication old = sc.getAuthentication();
try {
// 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();
method = findResolver(type);
if (method==null) {
stderr.println("Unable to find the resolver method annotated with @CLIResolver for "+type);
return 1;
}
}

List<MethodBinder> binders = new ArrayList<MethodBinder>();

while (!chains.isEmpty())
binders.add(new MethodBinder(chains.pop(),this,parser));

// authentication
CliAuthenticator authenticator = Jenkins.getInstance().getSecurityRealm().createCliAuthenticator(this);
new ClassParser().parse(authenticator,parser);

// fill up all the binders
parser.parseArgument(args);
Expand Down

0 comments on commit 9ee9684

Please sign in to comment.