Paul Martin
2016-04-16 eecaad8b8e2c447429c31a01d49260ddd6b4ee03
src/main/java/com/gitblit/transport/ssh/commands/BaseCommand.java
@@ -1,23 +1,28 @@
// Copyright (C) 2009 The Android Open Source Project
//
// Licensed 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.
/*
 * Copyright (C) 2009 The Android Open Source Project
 * Copyright 2014 gitblit.com.
 *
 * Licensed 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 com.gitblit.transport.ssh.commands;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicReference;
@@ -26,405 +31,538 @@
import org.apache.sshd.server.Command;
import org.apache.sshd.server.Environment;
import org.apache.sshd.server.ExitCallback;
import org.apache.sshd.server.SessionAware;
import org.apache.sshd.server.session.ServerSession;
import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.CmdLineException;
import org.kohsuke.args4j.Option;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.gitblit.transport.ssh.AbstractSshCommand;
import com.gitblit.utils.IdGenerator;
import com.gitblit.Keys;
import com.gitblit.utils.StringUtils;
import com.gitblit.utils.WorkQueue;
import com.gitblit.utils.WorkQueue.CancelableRunnable;
import com.gitblit.utils.cli.CmdLineParser;
import com.google.common.base.Charsets;
import com.google.common.util.concurrent.Atomics;
public abstract class BaseCommand extends AbstractSshCommand {
  private static final Logger log = LoggerFactory
      .getLogger(BaseCommand.class);
public abstract class BaseCommand implements Command, SessionAware {
  /** Text of the command line which lead up to invoking this instance. */
  private String commandName = "";
   private static final Logger log = LoggerFactory.getLogger(BaseCommand.class);
  /** Unparsed command line options. */
  private String[] argv;
   private static final int PRIVATE_STATUS = 1 << 30;
  /** The task, as scheduled on a worker thread. */
  private final AtomicReference<Future<?>> task;
   public final static int STATUS_CANCEL = PRIVATE_STATUS | 1;
  private final WorkQueue.Executor executor;
   public final static int STATUS_NOT_FOUND = PRIVATE_STATUS | 2;
  public BaseCommand() {
    task = Atomics.newReference();
    IdGenerator gen = new IdGenerator();
    WorkQueue w = new WorkQueue(gen);
    this.executor = w.getDefaultQueue();
  }
   public final static int STATUS_NOT_ADMIN = PRIVATE_STATUS | 3;
  public void setInputStream(final InputStream in) {
    this.in = in;
  }
   protected InputStream in;
  public void setOutputStream(final OutputStream out) {
    this.out = out;
  }
   protected OutputStream out;
  public void setErrorStream(final OutputStream err) {
    this.err = err;
  }
   protected OutputStream err;
  public void setExitCallback(final ExitCallback callback) {
    this.exit = callback;
  }
   protected ExitCallback exit;
  protected void provideStateTo(final Command cmd) {
    cmd.setInputStream(in);
    cmd.setOutputStream(out);
    cmd.setErrorStream(err);
    cmd.setExitCallback(exit);
  }
   protected ServerSession session;
  protected String getName() {
    return commandName;
  }
   /** Ssh command context */
   private SshCommandContext ctx;
  void setName(final String prefix) {
    this.commandName = prefix;
  }
   /** Text of the command line which lead up to invoking this instance. */
   private String commandName = "";
  public String[] getArguments() {
    return argv;
  }
   /** Unparsed command line options. */
   private String[] argv;
  public void setArguments(final String[] argv) {
    this.argv = argv;
  }
   /** The task, as scheduled on a worker thread. */
   private final AtomicReference<Future<?>> task;
  /**
   * Parses the command line argument, injecting parsed values into fields.
   * <p>
   * This method must be explicitly invoked to cause a parse.
   *
   * @throws UnloggedFailure if the command line arguments were invalid.
   * @see Option
   * @see Argument
   */
  protected void parseCommandLine() throws UnloggedFailure {
    parseCommandLine(this);
  }
   private WorkQueue workQueue;
  /**
   * Parses the command line argument, injecting parsed values into fields.
   * <p>
   * This method must be explicitly invoked to cause a parse.
   *
   * @param options object whose fields declare Option and Argument annotations
   *        to describe the parameters of the command. Usually {@code this}.
   * @throws UnloggedFailure if the command line arguments were invalid.
   * @see Option
   * @see Argument
   */
  protected void parseCommandLine(Object options) throws UnloggedFailure {
    final CmdLineParser clp = newCmdLineParser(options);
    try {
      clp.parseArgument(argv);
    } catch (IllegalArgumentException err) {
      if (!clp.wasHelpRequestedByOption()) {
        throw new UnloggedFailure(1, "fatal: " + err.getMessage());
      }
    } catch (CmdLineException err) {
      if (!clp.wasHelpRequestedByOption()) {
        throw new UnloggedFailure(1, "fatal: " + err.getMessage());
      }
    }
   public BaseCommand() {
      task = Atomics.newReference();
   }
    if (clp.wasHelpRequestedByOption()) {
      StringWriter msg = new StringWriter();
      clp.printDetailedUsage(commandName, msg);
      msg.write(usage());
      throw new UnloggedFailure(1, msg.toString());
    }
  }
   @Override
   public void setSession(final ServerSession session) {
      this.session = session;
   }
  /** Construct a new parser for this command's received command line. */
  protected CmdLineParser newCmdLineParser(Object options) {
    return new CmdLineParser(options);
  }
   @Override
   public void destroy() {
      log.debug("destroying " + getClass().getName());
      Future<?> future = task.getAndSet(null);
      if (future != null && !future.isDone()) {
         future.cancel(true);
      }
      session = null;
      ctx = null;
   }
  protected String usage() {
    return "";
  }
   protected static PrintWriter toPrintWriter(final OutputStream o) {
      return new PrintWriter(new BufferedWriter(new OutputStreamWriter(o, Charsets.UTF_8)));
   }
  private final class TaskThunk implements com.gitblit.utils.WorkQueue.CancelableRunnable {
    private final CommandRunnable thunk;
    private final String taskName;
   @Override
   public abstract void start(Environment env) throws IOException;
    private TaskThunk(final CommandRunnable thunk) {
      this.thunk = thunk;
   protected void provideStateTo(final BaseCommand cmd) {
      cmd.setContext(ctx);
      cmd.setWorkQueue(workQueue);
      cmd.setInputStream(in);
      cmd.setOutputStream(out);
      cmd.setErrorStream(err);
      cmd.setExitCallback(exit);
   }
      // TODO
//      StringBuilder m = new StringBuilder("foo");
//      m.append(context.getCommandLine());
//      if (userProvider.get().isIdentifiedUser()) {
//        IdentifiedUser u = (IdentifiedUser) userProvider.get();
//        m.append(" (").append(u.getAccount().getUserName()).append(")");
//      }
      this.taskName = "foo";//m.toString();
    }
   public WorkQueue getWorkQueue() {
      return workQueue;
   }
    @Override
    public void cancel() {
      synchronized (this) {
        //final Context old = sshScope.set(context);
        try {
          //onExit(/*STATUS_CANCEL*/);
        } finally {
          //sshScope.set(old);
        }
      }
    }
   public void setWorkQueue(WorkQueue workQueue) {
      this.workQueue = workQueue;
   }
    @Override
    public void run() {
      synchronized (this) {
        final Thread thisThread = Thread.currentThread();
        final String thisName = thisThread.getName();
        int rc = 0;
        //final Context old = sshScope.set(context);
        try {
          //context.started = TimeUtil.nowMs();
          thisThread.setName("SSH " + taskName);
   public void setContext(SshCommandContext ctx) {
      this.ctx = ctx;
   }
          thunk.run();
   public SshCommandContext getContext() {
      return ctx;
   }
          out.flush();
          err.flush();
        } catch (Throwable e) {
          try {
            out.flush();
          } catch (Throwable e2) {
          }
          try {
            err.flush();
          } catch (Throwable e2) {
          }
          rc = handleError(e);
        } finally {
          try {
            onExit(rc);
          } finally {
            thisThread.setName(thisName);
          }
        }
      }
    }
   @Override
   public void setInputStream(final InputStream in) {
      this.in = in;
   }
    @Override
    public String toString() {
      return taskName;
    }
  }
   @Override
   public void setOutputStream(final OutputStream out) {
      this.out = out;
   }
  /** Runnable function which can throw an exception. */
  public static interface CommandRunnable {
    public void run() throws Exception;
  }
   @Override
   public void setErrorStream(final OutputStream err) {
      this.err = err;
   }
   @Override
   public void setExitCallback(final ExitCallback callback) {
      this.exit = callback;
   }
  /**
   * Spawn a function into its own thread.
   * <p>
   * Typically this should be invoked within {@link Command#start(Environment)},
   * such as:
   *
   * <pre>
   * startThread(new Runnable() {
   *   public void run() {
   *     runImp();
   *   }
   * });
   * </pre>
   *
   * @param thunk the runnable to execute on the thread, performing the
   *        command's logic.
   */
  protected void startThread(final Runnable thunk) {
    startThread(new CommandRunnable() {
      @Override
      public void run() throws Exception {
        thunk.run();
      }
    });
  }
   protected String getName() {
      return commandName;
   }
  /**
   * Terminate this command and return a result code to the remote client.
   * <p>
   * Commands should invoke this at most once. Once invoked, the command may
   * lose access to request based resources as any callbacks previously
   * registered with {@link RequestCleanup} will fire.
   *
   * @param rc exit code for the remote client.
   */
  protected void onExit(final int rc) {
    exit.onExit(rc);
//    if (cleanup != null) {
//      cleanup.run();
//    }
  }
   void setName(final String prefix) {
      this.commandName = prefix;
   }
  private int handleError(final Throwable e) {
    if ((e.getClass() == IOException.class
         && "Pipe closed".equals(e.getMessage()))
        || //
        (e.getClass() == SshException.class
         && "Already closed".equals(e.getMessage()))
        || //
        e.getClass() == InterruptedIOException.class) {
      // This is sshd telling us the client just dropped off while
      // we were waiting for a read or a write to complete. Either
      // way its not really a fatal error. Don't log it.
      //
      return 127;
    }
   public String[] getArguments() {
      return argv;
   }
    if (e instanceof UnloggedFailure) {
    } else {
      final StringBuilder m = new StringBuilder();
      m.append("Internal server error");
//      if (userProvider.get().isIdentifiedUser()) {
//        final IdentifiedUser u = (IdentifiedUser) userProvider.get();
//        m.append(" (user ");
//        m.append(u.getAccount().getUserName());
//        m.append(" account ");
//        m.append(u.getAccountId());
//        m.append(")");
//      }
//      m.append(" during ");
//      m.append(contextProvider.get().getCommandLine());
      log.error(m.toString(), e);
    }
   public void setArguments(final String[] argv) {
      this.argv = argv;
   }
    if (e instanceof Failure) {
      final Failure f = (Failure) e;
      try {
        err.write((f.getMessage() + "\n").getBytes(Charsets.UTF_8));
        err.flush();
      } catch (IOException e2) {
      } catch (Throwable e2) {
        log.warn("Cannot send failure message to client", e2);
      }
      return f.exitCode;
   /**
    * Parses the command line argument, injecting parsed values into fields.
    * <p>
    * This method must be explicitly invoked to cause a parse.
    *
    * @throws UnloggedFailure
    *             if the command line arguments were invalid.
    * @see Option
    * @see Argument
    */
   protected void parseCommandLine() throws UnloggedFailure {
      parseCommandLine(this);
   }
    } else {
      try {
        err.write("fatal: internal server error\n".getBytes(Charsets.UTF_8));
        err.flush();
      } catch (IOException e2) {
      } catch (Throwable e2) {
        log.warn("Cannot send internal server error message to client", e2);
      }
      return 128;
    }
  }
   /**
    * Parses the command line argument, injecting parsed values into fields.
    * <p>
    * This method must be explicitly invoked to cause a parse.
    *
    * @param options
    *            object whose fields declare Option and Argument annotations to
    *            describe the parameters of the command. Usually {@code this}.
    * @throws UnloggedFailure
    *             if the command line arguments were invalid.
    * @see Option
    * @see Argument
    */
   protected void parseCommandLine(Object options) throws UnloggedFailure {
      final CmdLineParser clp = newCmdLineParser(options);
      try {
         clp.parseArgument(argv);
      } catch (IllegalArgumentException err) {
         if (!clp.wasHelpRequestedByOption()) {
            throw new UnloggedFailure(1, "fatal: " + err.getMessage());
         }
      } catch (CmdLineException err) {
         if (!clp.wasHelpRequestedByOption()) {
            throw new UnloggedFailure(1, "fatal: " + err.getMessage());
         }
      }
  /**
   * Spawn a function into its own thread.
   * <p>
   * Typically this should be invoked within {@link Command#start(Environment)},
   * such as:
   *
   * <pre>
   * startThread(new CommandRunnable() {
   *   public void run() throws Exception {
   *     runImp();
   *   }
   * });
   * </pre>
   * <p>
   * If the function throws an exception, it is translated to a simple message
   * for the client, a non-zero exit code, and the stack trace is logged.
   *
   * @param thunk the runnable to execute on the thread, performing the
   *        command's logic.
   */
  protected void startThread(final CommandRunnable thunk) {
    final TaskThunk tt = new TaskThunk(thunk);
      task.set(executor.submit(tt));
  }
      if (clp.wasHelpRequestedByOption()) {
         CommandMetaData meta = getClass().getAnnotation(CommandMetaData.class);
         String title = meta.name().toUpperCase() + ": " + meta.description();
         String b = com.gitblit.utils.StringUtils.leftPad("", title.length() + 2, '═');
         StringWriter msg = new StringWriter();
         msg.write('\n');
         msg.write(b);
         msg.write('\n');
         msg.write(' ');
         msg.write(title);
         msg.write('\n');
         msg.write(b);
         msg.write("\n\n");
         msg.write("USAGE\n");
         msg.write("─────\n");
         msg.write(' ');
         msg.write(commandName);
         msg.write('\n');
         msg.write("  ");
         clp.printSingleLineUsage(msg, null);
         msg.write("\n\n");
         String txt = getUsageText();
         if (!StringUtils.isEmpty(txt)) {
            msg.write(txt);
            msg.write("\n\n");
         }
         msg.write("ARGUMENTS & OPTIONS\n");
         msg.write("───────────────────\n");
         clp.printUsage(msg, null);
         msg.write('\n');
         String examples = usage().trim();
         if (!StringUtils.isEmpty(examples)) {
            msg.write('\n');
            msg.write("EXAMPLES\n");
            msg.write("────────\n");
            msg.write(examples);
            msg.write('\n');
         }
  /** Thrown from {@link CommandRunnable#run()} with client message and code. */
  public static class Failure extends Exception {
    private static final long serialVersionUID = 1L;
         throw new UnloggedFailure(1, msg.toString());
      }
   }
    final int exitCode;
   /** Construct a new parser for this command's received command line. */
   protected CmdLineParser newCmdLineParser(Object options) {
      return new CmdLineParser(options);
   }
    /**
     * Create a new failure.
     *
     * @param exitCode exit code to return the client, which indicates the
     *        failure status of this command. Should be between 1 and 255,
     *        inclusive.
     * @param msg message to also send to the client's stderr.
     */
    public Failure(final int exitCode, final String msg) {
      this(exitCode, msg, null);
    }
   public String usage() {
      Class<? extends BaseCommand> clazz = getClass();
      if (clazz.isAnnotationPresent(UsageExamples.class)) {
         return examples(clazz.getAnnotation(UsageExamples.class).examples());
      } else if (clazz.isAnnotationPresent(UsageExample.class)) {
         return examples(clazz.getAnnotation(UsageExample.class));
      }
      return "";
   }
    /**
     * Create a new failure.
     *
     * @param exitCode exit code to return the client, which indicates the
     *        failure status of this command. Should be between 1 and 255,
     *        inclusive.
     * @param msg message to also send to the client's stderr.
     * @param why stack trace to include in the server's log, but is not sent to
     *        the client's stderr.
     */
    public Failure(final int exitCode, final String msg, final Throwable why) {
      super(msg, why);
      this.exitCode = exitCode;
    }
  }
   protected String getUsageText() {
      return "";
   }
  /** Thrown from {@link CommandRunnable#run()} with client message and code. */
  public static class UnloggedFailure extends Failure {
    private static final long serialVersionUID = 1L;
   protected String examples(UsageExample... examples) {
      int sshPort = getContext().getGitblit().getSettings().getInteger(Keys.git.sshPort, 29418);
      String username = getContext().getClient().getUsername();
      String hostname = "localhost";
      String ssh = String.format("ssh -l %s -p %d %s", username, sshPort, hostname);
    /**
     * Create a new failure.
     *
     * @param msg message to also send to the client's stderr.
     */
    public UnloggedFailure(final String msg) {
      this(1, msg);
    }
      StringBuilder sb = new StringBuilder();
      for (UsageExample example : examples) {
         sb.append(example.description()).append("\n\n");
         String syntax = example.syntax();
         syntax = syntax.replace("${ssh}", ssh);
         syntax = syntax.replace("${username}", username);
         syntax = syntax.replace("${cmd}", commandName);
         sb.append("   ").append(syntax).append("\n\n");
      }
      return sb.toString();
   }
    /**
     * Create a new failure.
     *
     * @param exitCode exit code to return the client, which indicates the
     *        failure status of this command. Should be between 1 and 255,
     *        inclusive.
     * @param msg message to also send to the client's stderr.
     */
    public UnloggedFailure(final int exitCode, final String msg) {
      this(exitCode, msg, null);
    }
   protected void showHelp() throws UnloggedFailure {
      argv = new String [] { "--help" };
      parseCommandLine();
   }
    /**
     * Create a new failure.
     *
     * @param exitCode exit code to return the client, which indicates the
     *        failure status of this command. Should be between 1 and 255,
     *        inclusive.
     * @param msg message to also send to the client's stderr.
     * @param why stack trace to include in the server's log, but is not sent to
     *        the client's stderr.
     */
    public UnloggedFailure(final int exitCode, final String msg,
        final Throwable why) {
      super(exitCode, msg, why);
    }
  }
   private final class TaskThunk implements CancelableRunnable {
      private final CommandRunnable thunk;
      private final String taskName;
      private TaskThunk(final CommandRunnable thunk) {
         this.thunk = thunk;
         StringBuilder m = new StringBuilder();
         m.append(ctx.getCommandLine());
         this.taskName = m.toString();
      }
      @Override
      public void cancel() {
         synchronized (this) {
            try {
               onExit(STATUS_CANCEL);
            } finally {
               ctx = null;
            }
         }
      }
      @Override
      public void run() {
         synchronized (this) {
            final Thread thisThread = Thread.currentThread();
            final String thisName = thisThread.getName();
            int rc = 0;
            try {
               thisThread.setName("SSH " + taskName);
               thunk.run();
               out.flush();
               err.flush();
            } catch (Throwable e) {
               try {
                  out.flush();
               } catch (Throwable e2) {
               }
               try {
                  err.flush();
               } catch (Throwable e2) {
               }
               rc = handleError(e);
            } finally {
               try {
                  onExit(rc);
               } finally {
                  thisThread.setName(thisName);
               }
            }
         }
      }
      @Override
      public String toString() {
         return taskName;
      }
   }
   /** Runnable function which can throw an exception. */
   public interface CommandRunnable {
      void run() throws Exception;
   }
   /** Runnable function which can retrieve a project name related to the task */
   public interface RepositoryCommandRunnable extends CommandRunnable {
      String getRepository();
   }
   /**
    * Spawn a function into its own thread.
    * <p>
    * Typically this should be invoked within
    * {@link Command#start(Environment)}, such as:
    *
    * <pre>
    * startThread(new Runnable() {
    *    public void run() {
    *       runImp();
    *    }
    * });
    * </pre>
    *
    * @param thunk
    *            the runnable to execute on the thread, performing the
    *            command's logic.
    */
   protected void startThread(final Runnable thunk) {
      startThread(new CommandRunnable() {
         @Override
         public void run() throws Exception {
            thunk.run();
         }
      });
   }
   /**
    * Terminate this command and return a result code to the remote client.
    * <p>
    * Commands should invoke this at most once.
    *
    * @param rc exit code for the remote client.
    */
   protected void onExit(final int rc) {
      exit.onExit(rc);
   }
   private int handleError(final Throwable e) {
      if ((e.getClass() == IOException.class && "Pipe closed".equals(e.getMessage())) ||
            (e.getClass() == SshException.class && "Already closed".equals(e.getMessage())) ||
            e.getClass() == InterruptedIOException.class) {
         // This is sshd telling us the client just dropped off while
         // we were waiting for a read or a write to complete. Either
         // way its not really a fatal error. Don't log it.
         //
         return 127;
      }
      if (e instanceof UnloggedFailure) {
      } else {
         final StringBuilder m = new StringBuilder();
         m.append("Internal server error");
         String user = ctx.getClient().getUsername();
         if (user != null) {
            m.append(" (user ");
            m.append(user);
            m.append(")");
         }
         m.append(" during ");
         m.append(ctx.getCommandLine());
         log.error(m.toString(), e);
      }
      if (e instanceof Failure) {
         final Failure f = (Failure) e;
         try {
            err.write((f.getMessage() + "\n").getBytes(Charsets.UTF_8));
            err.flush();
         } catch (IOException e2) {
         } catch (Throwable e2) {
            log.warn("Cannot send failure message to client", e2);
         }
         return f.exitCode;
      } else {
         try {
            err.write("fatal: internal server error\n".getBytes(Charsets.UTF_8));
            err.flush();
         } catch (IOException e2) {
         } catch (Throwable e2) {
            log.warn("Cannot send internal server error message to client", e2);
         }
         return 128;
      }
   }
   /**
    * Spawn a function into its own thread.
    * <p>
    * Typically this should be invoked within
    * {@link Command#start(Environment)}, such as:
    *
    * <pre>
    * startThread(new CommandRunnable() {
    *    public void run() throws Exception {
    *       runImp();
    *    }
    * });
    * </pre>
    * <p>
    * If the function throws an exception, it is translated to a simple message
    * for the client, a non-zero exit code, and the stack trace is logged.
    *
    * @param thunk
    *            the runnable to execute on the thread, performing the
    *            command's logic.
    */
   protected void startThread(final CommandRunnable thunk) {
      final TaskThunk tt = new TaskThunk(thunk);
      task.set(workQueue.getDefaultQueue().submit(tt));
   }
   /** Thrown from {@link CommandRunnable#run()} with client message and code. */
   public static class Failure extends Exception {
      private static final long serialVersionUID = 1L;
      final int exitCode;
      /**
       * Create a new failure.
       *
       * @param exitCode
       *            exit code to return the client, which indicates the
       *            failure status of this command. Should be between 1 and
       *            255, inclusive.
       * @param msg
       *            message to also send to the client's stderr.
       */
      public Failure(final int exitCode, final String msg) {
         this(exitCode, msg, null);
      }
      /**
       * Create a new failure.
       *
       * @param exitCode
       *            exit code to return the client, which indicates the
       *            failure status of this command. Should be between 1 and
       *            255, inclusive.
       * @param msg
       *            message to also send to the client's stderr.
       * @param why
       *            stack trace to include in the server's log, but is not
       *            sent to the client's stderr.
       */
      public Failure(final int exitCode, final String msg, final Throwable why) {
         super(msg, why);
         this.exitCode = exitCode;
      }
   }
   /** Thrown from {@link CommandRunnable#run()} with client message and code. */
   public static class UnloggedFailure extends Failure {
      private static final long serialVersionUID = 1L;
      /**
       * Create a new failure.
       *
       * @param msg
       *            message to also send to the client's stderr.
       */
      public UnloggedFailure(final String msg) {
         this(1, msg);
      }
      /**
       * Create a new failure.
       *
       * @param exitCode
       *            exit code to return the client, which indicates the
       *            failure status of this command. Should be between 1 and
       *            255, inclusive.
       * @param msg
       *            message to also send to the client's stderr.
       */
      public UnloggedFailure(final int exitCode, final String msg) {
         this(exitCode, msg, null);
      }
      /**
       * Create a new failure.
       *
       * @param exitCode
       *            exit code to return the client, which indicates the
       *            failure status of this command. Should be between 1 and
       *            255, inclusive.
       * @param msg
       *            message to also send to the client's stderr.
       * @param why
       *            stack trace to include in the server's log, but is not
       *            sent to the client's stderr.
       */
      public UnloggedFailure(final int exitCode, final String msg, final Throwable why) {
         super(exitCode, msg, why);
      }
   }
}