James Moger
2014-03-14 503a853acad49ac6da7f520c26b3b27942dbfec5
Merge AbstractSshCommand and BaseCommand into a single class
1 files renamed
1 files deleted
5 files modified
893 ■■■■ changed files
src/main/java/com/gitblit/transport/ssh/AbstractSshCommand.java 84 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/transport/ssh/commands/AbstractGitCommand.java 23 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/transport/ssh/commands/BaseCommand.java 743 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java 1 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/transport/ssh/commands/Receive.java 1 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/transport/ssh/commands/SshCommand.java 40 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/transport/ssh/commands/Upload.java 1 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/transport/ssh/AbstractSshCommand.java
File was deleted
src/main/java/com/gitblit/transport/ssh/commands/AbstractGitCommand.java
File was renamed from src/main/java/com/gitblit/transport/ssh/AbstractGitCommand.java
@@ -13,7 +13,7 @@
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.gitblit.transport.ssh;
package com.gitblit.transport.ssh.commands;
import java.io.IOException;
@@ -26,14 +26,14 @@
import com.gitblit.git.GitblitReceivePackFactory;
import com.gitblit.git.GitblitUploadPackFactory;
import com.gitblit.git.RepositoryResolver;
import com.gitblit.transport.ssh.commands.BaseCommand;
import com.gitblit.transport.ssh.SshDaemonClient;
/**
 * @author Eric Myhre
 *
 *
 */
public abstract class AbstractGitCommand extends BaseCommand {
    @Argument(index = 0, metaVar = "PROJECT.git", required = true, usage = "project name")
    @Argument(index = 0, metaVar = "REPOSITORY", required = true, usage = "repository name")
    protected String repository;
    protected RepositoryResolver<SshDaemonClient> repositoryResolver;
@@ -79,30 +79,25 @@
        // ssh://git@thishost/path should always be name="/path" here
        //
        if (!repository.startsWith("/")) {
            throw new Failure(1, "fatal: '" + repository
                    + "': not starts with / character");
            throw new Failure(1, "fatal: '" + repository + "': not starts with / character");
        }
        repository = repository.substring(1);
        try {
            return repositoryResolver.open(ctx.getClient(), repository);
        } catch (Exception e) {
            throw new Failure(1, "fatal: '" + repository
                    + "': not a git archive", e);
            throw new Failure(1, "fatal: '" + repository + "': not a git archive", e);
        }
    }
    public void setRepositoryResolver(
            RepositoryResolver<SshDaemonClient> repositoryResolver) {
    public void setRepositoryResolver(RepositoryResolver<SshDaemonClient> repositoryResolver) {
        this.repositoryResolver = repositoryResolver;
    }
    public void setReceivePackFactory(
            GitblitReceivePackFactory<SshDaemonClient> receivePackFactory) {
    public void setReceivePackFactory(GitblitReceivePackFactory<SshDaemonClient> receivePackFactory) {
        this.receivePackFactory = receivePackFactory;
    }
    public void setUploadPackFactory(
            GitblitUploadPackFactory<SshDaemonClient> uploadPackFactory) {
    public void setUploadPackFactory(GitblitUploadPackFactory<SshDaemonClient> uploadPackFactory) {
        this.uploadPackFactory = uploadPackFactory;
    }
}
src/main/java/com/gitblit/transport/ssh/commands/BaseCommand.java
@@ -14,10 +14,13 @@
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,13 +29,14 @@
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.transport.ssh.SshCommandContext;
import com.gitblit.utils.IdGenerator;
import com.gitblit.utils.WorkQueue;
@@ -41,398 +45,439 @@
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;
    /** Ssh context */
    protected SshCommandContext ctx;
  /** Ssh context */
  protected SshCommandContext ctx;
    protected InputStream in;
  /** The task, as scheduled on a worker thread. */
  private final AtomicReference<Future<?>> task;
    protected OutputStream out;
  private final WorkQueue.Executor executor;
    protected OutputStream err;
  public BaseCommand() {
    task = Atomics.newReference();
    IdGenerator gen = new IdGenerator();
    WorkQueue w = new WorkQueue(gen);
    this.executor = w.getDefaultQueue();
  }
    protected ExitCallback exit;
  public void setContext(SshCommandContext ctx) {
    this.ctx = ctx;
  }
    protected ServerSession session;
  public void setInputStream(final InputStream in) {
    this.in = in;
  }
    /** Text of the command line which lead up to invoking this instance. */
    private String commandName = "";
  public void setOutputStream(final OutputStream out) {
    this.out = out;
  }
    /** Unparsed command line options. */
    private String[] argv;
  public void setErrorStream(final OutputStream err) {
    this.err = err;
  }
    /** The task, as scheduled on a worker thread. */
    private final AtomicReference<Future<?>> task;
  public void setExitCallback(final ExitCallback callback) {
    this.exit = callback;
  }
    private final WorkQueue.Executor executor;
  protected void provideBaseStateTo(final Command cmd) {
    if (cmd instanceof BaseCommand) {
      ((BaseCommand)cmd).setContext(ctx);
    }
    cmd.setInputStream(in);
    cmd.setOutputStream(out);
    cmd.setErrorStream(err);
    cmd.setExitCallback(exit);
  }
    public BaseCommand() {
        task = Atomics.newReference();
        IdGenerator gen = new IdGenerator();
        WorkQueue w = new WorkQueue(gen);
        this.executor = w.getDefaultQueue();
    }
  protected String getName() {
    return commandName;
  }
    @Override
    public void setSession(final ServerSession session) {
        this.session = session;
    }
  void setName(final String prefix) {
    this.commandName = prefix;
  }
    @Override
    public void destroy() {
    }
  public String[] getArguments() {
    return argv;
  }
    protected static PrintWriter toPrintWriter(final OutputStream o) {
        return new PrintWriter(new BufferedWriter(new OutputStreamWriter(o, Charsets.UTF_8)));
    }
  public void setArguments(final String[] argv) {
    this.argv = argv;
  }
    @Override
    public abstract void start(Environment env) throws IOException;
  /**
   * 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);
  }
    public void setContext(SshCommandContext ctx) {
        this.ctx = ctx;
    }
  /**
   * 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());
      }
    }
    @Override
    public void setInputStream(final InputStream in) {
        this.in = in;
    }
    if (clp.wasHelpRequestedByOption()) {
      StringWriter msg = new StringWriter();
      clp.printDetailedUsage(commandName, msg);
      msg.write(usage());
      throw new UnloggedFailure(1, msg.toString());
    }
  }
    @Override
    public void setOutputStream(final OutputStream out) {
        this.out = out;
    }
  /** Construct a new parser for this command's received command line. */
  protected CmdLineParser newCmdLineParser(Object options) {
    return new CmdLineParser(options);
  }
    @Override
    public void setErrorStream(final OutputStream err) {
        this.err = err;
    }
  protected String usage() {
    return "";
  }
    @Override
    public void setExitCallback(final ExitCallback callback) {
        this.exit = callback;
    }
  private final class TaskThunk implements CancelableRunnable {
    private final CommandRunnable thunk;
    private final String taskName;
    protected void provideBaseStateTo(final Command cmd) {
        if (cmd instanceof BaseCommand) {
            ((BaseCommand) cmd).setContext(ctx);
        }
        cmd.setInputStream(in);
        cmd.setOutputStream(out);
        cmd.setErrorStream(err);
        cmd.setExitCallback(exit);
    }
    private TaskThunk(final CommandRunnable thunk) {
      this.thunk = thunk;
    protected String getName() {
        return commandName;
    }
      StringBuilder m = new StringBuilder();
      m.append(ctx.getCommandLine());
      this.taskName = m.toString();
    }
    void setName(final String prefix) {
        this.commandName = prefix;
    }
    @Override
    public void cancel() {
      synchronized (this) {
        try {
          //onExit(/*STATUS_CANCEL*/);
        } finally {
          ctx = null;
        }
      }
    }
    public String[] getArguments() {
        return argv;
    }
    @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();
    public void setArguments(final String[] argv) {
        this.argv = argv;
    }
          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);
          }
        }
      }
    }
    /**
     * 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);
    }
    @Override
    public String toString() {
      return taskName;
    }
  }
    /**
     * 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());
            }
        }
  /** Runnable function which can throw an exception. */
  public static interface CommandRunnable {
    public void run() throws Exception;
  }
        if (clp.wasHelpRequestedByOption()) {
            StringWriter msg = new StringWriter();
            clp.printDetailedUsage(commandName, msg);
            msg.write(usage());
            throw new UnloggedFailure(1, msg.toString());
        }
    }
    /** Construct a new parser for this command's received command line. */
    protected CmdLineParser newCmdLineParser(Object options) {
        return new CmdLineParser(options);
    }
  /** Runnable function which can retrieve a project name related to the task */
  public static interface RepositoryCommandRunnable extends CommandRunnable {
    public String getRepository();
  }
    protected String usage() {
        return "";
    }
  /**
   * 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();
      }
    });
  }
    private final class TaskThunk implements CancelableRunnable {
        private final CommandRunnable thunk;
        private final String taskName;
  /**
   * 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();
//    }
  }
        private TaskThunk(final CommandRunnable thunk) {
            this.thunk = thunk;
  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;
    }
            StringBuilder m = new StringBuilder();
            m.append(ctx.getCommandLine());
            this.taskName = m.toString();
        }
    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);
    }
        @Override
        public void cancel() {
            synchronized (this) {
                try {
                    // onExit(/*STATUS_CANCEL*/);
                } finally {
                    ctx = null;
                }
            }
        }
    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;
        @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();
    } 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;
    }
  }
                    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);
                    }
                }
            }
        }
  /**
   * 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));
  }
        @Override
        public String toString() {
            return taskName;
        }
    }
  /** Thrown from {@link CommandRunnable#run()} with client message and code. */
  public static class Failure extends Exception {
    private static final long serialVersionUID = 1L;
    /** Runnable function which can throw an exception. */
    public static interface CommandRunnable {
        public void run() throws Exception;
    }
    final int exitCode;
    /** Runnable function which can retrieve a project name related to the task */
    public static interface RepositoryCommandRunnable extends CommandRunnable {
        public String getRepository();
    }
    /**
     * 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);
    }
    /**
     * 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();
            }
        });
    }
    /**
     * 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;
    }
  }
    /**
     * 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();
        // }
    }
  /** Thrown from {@link CommandRunnable#run()} with client message and code. */
  public static class UnloggedFailure extends Failure {
    private static final long serialVersionUID = 1L;
    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;
        }
    /**
     * Create a new failure.
     *
     * @param msg message to also send to the client's stderr.
     */
    public UnloggedFailure(final String msg) {
      this(1, msg);
    }
        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);
        }
    /**
     * 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);
    }
        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;
    /**
     * 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);
    }
  }
        } 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(executor.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);
        }
    }
}
src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java
@@ -30,7 +30,6 @@
import com.gitblit.git.GitblitReceivePackFactory;
import com.gitblit.git.GitblitUploadPackFactory;
import com.gitblit.git.RepositoryResolver;
import com.gitblit.transport.ssh.AbstractGitCommand;
import com.gitblit.transport.ssh.CommandMetaData;
import com.gitblit.transport.ssh.PublicKeyAuthenticator;
import com.gitblit.transport.ssh.SshDaemonClient;
src/main/java/com/gitblit/transport/ssh/commands/Receive.java
@@ -17,7 +17,6 @@
import org.eclipse.jgit.transport.ReceivePack;
import com.gitblit.transport.ssh.AbstractGitCommand;
import com.gitblit.transport.ssh.CommandMetaData;
@CommandMetaData(name = "git-receive-pack", description = "Receive pack")
src/main/java/com/gitblit/transport/ssh/commands/SshCommand.java
@@ -20,26 +20,26 @@
import org.apache.sshd.server.Environment;
public abstract class SshCommand extends BaseCommand {
  protected PrintWriter stdout;
  protected PrintWriter stderr;
    protected PrintWriter stdout;
    protected PrintWriter stderr;
  @Override
  public void start(Environment env) throws IOException {
    startThread(new CommandRunnable() {
      @Override
      public void run() throws Exception {
        parseCommandLine();
        stdout = toPrintWriter(out);
        stderr = toPrintWriter(err);
        try {
          SshCommand.this.run();
        } finally {
          stdout.flush();
          stderr.flush();
        }
      }
    });
  }
    @Override
    public void start(Environment env) throws IOException {
        startThread(new CommandRunnable() {
            @Override
            public void run() throws Exception {
                parseCommandLine();
                stdout = toPrintWriter(out);
                stderr = toPrintWriter(err);
                try {
                    SshCommand.this.run();
                } finally {
                    stdout.flush();
                    stderr.flush();
                }
            }
        });
    }
  protected abstract void run() throws UnloggedFailure, Failure, Exception;
    protected abstract void run() throws UnloggedFailure, Failure, Exception;
}
src/main/java/com/gitblit/transport/ssh/commands/Upload.java
@@ -17,7 +17,6 @@
import org.eclipse.jgit.transport.UploadPack;
import com.gitblit.transport.ssh.AbstractGitCommand;
import com.gitblit.transport.ssh.CommandMetaData;
@CommandMetaData(name = "git-upload-pack", description = "Upload pack")