You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@mina.apache.org by lg...@apache.org on 2018/02/18 18:20:11 UTC

[2/2] mina-sshd git commit: [SSHD-798] Provide capability to use a user-defined ExecutorService when servicing GIT commands

[SSHD-798] Provide capability to use a user-defined ExecutorService when servicing GIT commands


Project: http://git-wip-us.apache.org/repos/asf/mina-sshd/repo
Commit: http://git-wip-us.apache.org/repos/asf/mina-sshd/commit/e0d63503
Tree: http://git-wip-us.apache.org/repos/asf/mina-sshd/tree/e0d63503
Diff: http://git-wip-us.apache.org/repos/asf/mina-sshd/diff/e0d63503

Branch: refs/heads/master
Commit: e0d6350360f6e36c309020ae13d75683c9a500b2
Parents: 59a7fcf
Author: Goldstein Lyor <ly...@c-b4.com>
Authored: Sun Feb 18 15:09:27 2018 +0200
Committer: Lyor Goldstein <ly...@gmail.com>
Committed: Sun Feb 18 20:22:50 2018 +0200

----------------------------------------------------------------------
 README.md                                       |  13 ++
 .../sshd/server/AbstractCommandSupport.java     | 168 ++++++++++++++++++
 .../org/apache/sshd/server/scp/ScpCommand.java  |  36 ++--
 .../server/subsystem/sftp/SftpSubsystem.java    |  34 ++--
 .../java/org/apache/sshd/agent/AgentTest.java   |   6 +-
 .../sshd/client/channel/ChannelExecTest.java    |   2 +-
 .../sshd/client/session/ClientSessionTest.java  |   6 +-
 .../sshd/util/test/CommandExecutionHelper.java  |  90 +---------
 .../org/apache/sshd/util/test/EchoShell.java    |   2 +-
 .../org/apache/sshd/git/AbstractGitCommand.java | 133 ++++++++++++++
 .../sshd/git/AbstractGitCommandFactory.java     |  96 ++++++++++
 .../apache/sshd/git/pack/GitPackCommand.java    | 175 ++++---------------
 .../sshd/git/pack/GitPackCommandFactory.java    |  34 ++--
 .../sshd/git/pgm/EmbeddedCommandRunner.java     |   6 +-
 .../org/apache/sshd/git/pgm/GitPgmCommand.java  | 145 ++-------------
 .../sshd/git/pgm/GitPgmCommandFactory.java      |  35 ++--
 16 files changed, 558 insertions(+), 423 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e0d63503/README.md
----------------------------------------------------------------------
diff --git a/README.md b/README.md
index 82e92d3..283cc85 100644
--- a/README.md
+++ b/README.md
@@ -1380,6 +1380,19 @@ See `GitPackCommandFactory` and `GitPgmCommandFactory`. These command factories
     // or any other combination ...
 ```
 
+as with all other built-in commands, the factories allow the user to provide an `ExecutorService` in order to control the spwaned threads
+for servicing the commands. If none provided, an internal single-threaded "pool" is created ad-hoc and destroyed once the command execution
+is completed (regardless of whether successful or not):
+
+
+```java
+
+    sshd.setCommandFactory(new GitPackCommandFactory(rootDir, new MyCommandFactory())
+        .withExecutorService(myService)
+        .withShutdownOnExit(false));
+
+```
+
 ## LDAP adaptors
 
 The _sshd-ldap_ artifact contains an [LdapPasswordAuthenticator](https://issues.apache.org/jira/browse/SSHD-607) and an [LdapPublicKeyAuthenticator](https://issues.apache.org/jira/browse/SSHD-608) that have been written along the same lines as the [openssh-ldap-publickey](https://github.com/AndriiGrytsenko/openssh-ldap-publickey) project. The authenticators can be easily configured to match most LDAP schemes, or alternatively serve as base classes for code that extends them and adds proprietary logic.

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e0d63503/sshd-core/src/main/java/org/apache/sshd/server/AbstractCommandSupport.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/AbstractCommandSupport.java b/sshd-core/src/main/java/org/apache/sshd/server/AbstractCommandSupport.java
new file mode 100644
index 0000000..7533157
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/server/AbstractCommandSupport.java
@@ -0,0 +1,168 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.apache.sshd.server;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Collection;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.logging.AbstractLoggingBean;
+import org.apache.sshd.common.util.threads.ExecutorServiceCarrier;
+import org.apache.sshd.common.util.threads.ThreadUtils;
+
+/**
+ * Provides a basic useful skeleton for {@link Command} executions
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public abstract class AbstractCommandSupport
+        extends AbstractLoggingBean
+        implements Command, Runnable, ExitCallback, ExecutorServiceCarrier {
+    private final String command;
+    private InputStream in;
+    private OutputStream out;
+    private OutputStream err;
+    private ExitCallback callback;
+    private Environment environment;
+    private Future<?> cmdFuture;
+    private ExecutorService executorService;
+    private boolean shutdownOnExit;
+    private boolean cbCalled;
+
+    protected AbstractCommandSupport(String command, ExecutorService executorService, boolean shutdownOnExit) {
+        this.command = command;
+
+        if (executorService == null) {
+            String poolName = GenericUtils.isEmpty(command) ? getClass().getSimpleName() : command.replace(' ', '_').replace('/', ':');
+            this.executorService = ThreadUtils.newSingleThreadExecutor(poolName);
+            this.shutdownOnExit = true;    // we always close the ad-hoc executor service
+        } else {
+            this.executorService = executorService;
+            this.shutdownOnExit = shutdownOnExit;
+        }
+    }
+
+    public String getCommand() {
+        return command;
+    }
+
+    @Override
+    public ExecutorService getExecutorService() {
+        return executorService;
+    }
+
+    @Override
+    public boolean isShutdownOnExit() {
+        return shutdownOnExit;
+    }
+
+    public InputStream getInputStream() {
+        return in;
+    }
+
+    @Override
+    public void setInputStream(InputStream in) {
+        this.in = in;
+    }
+
+    public OutputStream getOutputStream() {
+        return out;
+    }
+
+    @Override
+    public void setOutputStream(OutputStream out) {
+        this.out = out;
+    }
+
+    public OutputStream getErrorStream() {
+        return err;
+    }
+
+    @Override
+    public void setErrorStream(OutputStream err) {
+        this.err = err;
+    }
+
+    public ExitCallback getExitCallback() {
+        return callback;
+    }
+
+    @Override
+    public void setExitCallback(ExitCallback callback) {
+        this.callback = callback;
+    }
+
+    public Environment getEnvironment() {
+        return environment;
+    }
+
+    protected Future<?> getStartedCommandFuture() {
+        return cmdFuture;
+    }
+
+    @Override
+    public void start(Environment env) throws IOException {
+        environment = env;
+        ExecutorService executors = getExecutorService();
+        cmdFuture = executors.submit(this);
+    }
+
+    @Override
+    public void destroy() {
+        ExecutorService executors = getExecutorService();
+        if ((executors != null) && (!executors.isShutdown()) && isShutdownOnExit()) {
+            Collection<Runnable> runners = executors.shutdownNow();
+            if (log.isDebugEnabled()) {
+                log.debug("destroy() - shutdown executor service - runners count=" + runners.size());
+            }
+        }
+        this.executorService = null;
+    }
+
+    @Override
+    public void onExit(int exitValue, String exitMessage) {
+        if (cbCalled) {
+            if (log.isTraceEnabled()) {
+                log.trace("onExit({}) ignore exitValue={}, message={} - already called",
+                        this, exitValue, exitMessage);
+            }
+            return;
+        }
+
+        ExitCallback cb = getExitCallback();
+        try {
+            if (log.isDebugEnabled()) {
+                log.debug("onExit({}) exiting - value={}, message={}", this, exitValue, exitMessage);
+            }
+            cb.onExit(exitValue, exitMessage);
+        } finally {
+            cbCalled = true;
+        }
+    }
+
+    @Override
+    public String toString() {
+        return getClass().getSimpleName() + "[" + getCommand() + "]";
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e0d63503/sshd-core/src/main/java/org/apache/sshd/server/scp/ScpCommand.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/scp/ScpCommand.java b/sshd-core/src/main/java/org/apache/sshd/server/scp/ScpCommand.java
index 8ff52c1..23d204b 100644
--- a/sshd-core/src/main/java/org/apache/sshd/server/scp/ScpCommand.java
+++ b/sshd-core/src/main/java/org/apache/sshd/server/scp/ScpCommand.java
@@ -37,6 +37,7 @@ import org.apache.sshd.common.session.Session;
 import org.apache.sshd.common.session.SessionHolder;
 import org.apache.sshd.common.util.GenericUtils;
 import org.apache.sshd.common.util.logging.AbstractLoggingBean;
+import org.apache.sshd.common.util.threads.ExecutorServiceCarrier;
 import org.apache.sshd.common.util.threads.ThreadUtils;
 import org.apache.sshd.server.Command;
 import org.apache.sshd.server.Environment;
@@ -55,7 +56,7 @@ import org.apache.sshd.server.session.ServerSessionHolder;
 public class ScpCommand
         extends AbstractLoggingBean
         implements Command, Runnable, FileSystemAware, SessionAware,
-                   SessionHolder<Session>, ServerSessionHolder {
+                   SessionHolder<Session>, ServerSessionHolder, ExecutorServiceCarrier {
 
     protected final String name;
     protected final int sendBufferSize;
@@ -73,12 +74,13 @@ public class ScpCommand
     protected OutputStream err;
     protected ExitCallback callback;
     protected IOException error;
-    protected ExecutorService executors;
-    protected boolean shutdownExecutor;
     protected Future<?> pendingFuture;
     protected ScpTransferEventListener listener;
     protected ServerSession serverSession;
 
+    private ExecutorService executorService;
+    private boolean shutdownOnExit;
+
     /**
      * @param command         The command to be executed
      * @param executorService An {@link ExecutorService} to be used when
@@ -103,11 +105,11 @@ public class ScpCommand
 
         if (executorService == null) {
             String poolName = command.replace(' ', '_').replace('/', ':');
-            executors = ThreadUtils.newSingleThreadExecutor(poolName);
-            shutdownExecutor = true;    // we always close the ad-hoc executor service
+            this.executorService = ThreadUtils.newSingleThreadExecutor(poolName);
+            this.shutdownOnExit = true;    // we always close the ad-hoc executor service
         } else {
-            executors = executorService;
-            shutdownExecutor = shutdownOnExit;
+            this.executorService = executorService;
+            this.shutdownOnExit = shutdownOnExit;
         }
 
         if (sendSize < ScpHelper.MIN_SEND_BUFFER_SIZE) {
@@ -174,12 +176,23 @@ public class ScpCommand
                 break;
             }
         }
-        if (!optF && !optT) {
+
+        if ((!optF) && (!optT)) {
             error = new IOException("Either -f or -t option should be set for " + command);
         }
     }
 
     @Override
+    public ExecutorService getExecutorService() {
+        return executorService;
+    }
+
+    @Override
+    public boolean isShutdownOnExit() {
+        return shutdownOnExit;
+    }
+
+    @Override
     public Session getSession() {
         return getServerSession();
     }
@@ -226,6 +239,7 @@ public class ScpCommand
         }
 
         try {
+            ExecutorService executors = getExecutorService();
             pendingFuture = executors.submit(this);
         } catch (RuntimeException e) {    // e.g., RejectedExecutionException
             log.error("Failed (" + e.getClass().getSimpleName() + ") to start command=" + name + ": " + e.getMessage(), e);
@@ -246,14 +260,14 @@ public class ScpCommand
 
         pendingFuture = null;
 
-        if ((executors != null) && (!executors.isShutdown()) && shutdownExecutor) {
+        ExecutorService executors = getExecutorService();
+        if ((executors != null) && (!executors.isShutdown()) && isShutdownOnExit()) {
             Collection<Runnable> runners = executors.shutdownNow();
             if (log.isDebugEnabled()) {
                 log.debug("destroy() - shutdown executor service - runners count=" + runners.size());
             }
         }
-
-        executors = null;
+        this.executorService = null;
 
         try {
             fileSystem.close();

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e0d63503/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/SftpSubsystem.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/SftpSubsystem.java b/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/SftpSubsystem.java
index 5d87d8e..e2cfa5e 100644
--- a/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/SftpSubsystem.java
+++ b/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/SftpSubsystem.java
@@ -59,6 +59,7 @@ import org.apache.sshd.common.util.buffer.Buffer;
 import org.apache.sshd.common.util.buffer.BufferUtils;
 import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
 import org.apache.sshd.common.util.io.IoUtils;
+import org.apache.sshd.common.util.threads.ExecutorServiceCarrier;
 import org.apache.sshd.common.util.threads.ThreadUtils;
 import org.apache.sshd.server.Command;
 import org.apache.sshd.server.Environment;
@@ -73,7 +74,7 @@ import org.apache.sshd.server.session.ServerSession;
  */
 public class SftpSubsystem
         extends AbstractSftpSubsystemHelper
-        implements Command, Runnable, SessionAware, FileSystemAware {
+        implements Command, Runnable, SessionAware, FileSystemAware, ExecutorServiceCarrier {
 
     /**
      * Properties key for the maximum of available open handles per session.
@@ -120,8 +121,6 @@ public class SftpSubsystem
     protected Random randomizer;
     protected int fileHandleSize = DEFAULT_FILE_HANDLE_SIZE;
     protected int maxFileHandleRounds = DEFAULT_FILE_HANDLE_ROUNDS;
-    protected ExecutorService executors;
-    protected boolean shutdownExecutor;
     protected Future<?> pendingFuture;
     protected byte[] workBuf = new byte[Math.max(DEFAULT_FILE_HANDLE_SIZE, Integer.BYTES)];
     protected FileSystem fileSystem = FileSystems.getDefault();
@@ -133,6 +132,8 @@ public class SftpSubsystem
 
     private ServerSession serverSession;
     private final AtomicBoolean closed = new AtomicBoolean(false);
+    private ExecutorService executorService;
+    private boolean shutdownOnExit;
 
     /**
      * @param executorService The {@link ExecutorService} to be used by
@@ -153,11 +154,11 @@ public class SftpSubsystem
         super(policy, accessor, errorStatusDataHandler);
 
         if (executorService == null) {
-            executors = ThreadUtils.newSingleThreadExecutor(getClass().getSimpleName());
-            shutdownExecutor = true;    // we always close the ad-hoc executor service
+            this.executorService = ThreadUtils.newSingleThreadExecutor(getClass().getSimpleName());
+            this.shutdownOnExit = true;    // we always close the ad-hoc executor service
         } else {
-            executors = executorService;
-            shutdownExecutor = shutdownOnExit;
+            this.executorService = executorService;
+            this.shutdownOnExit = shutdownOnExit;
         }
     }
 
@@ -172,6 +173,16 @@ public class SftpSubsystem
     }
 
     @Override
+    public ExecutorService getExecutorService() {
+        return executorService;
+    }
+
+    @Override
+    public boolean isShutdownOnExit() {
+        return shutdownOnExit;
+    }
+
+    @Override
     public void setSession(ServerSession session) {
         this.serverSession = Objects.requireNonNull(session, "No session");
 
@@ -233,7 +244,8 @@ public class SftpSubsystem
     public void start(Environment env) throws IOException {
         this.env = env;
         try {
-            pendingFuture = executors.submit(this);
+            ExecutorService executor = getExecutorService();
+            pendingFuture = executor.submit(this);
         } catch (RuntimeException e) {    // e.g., RejectedExecutionException
             log.error("Failed (" + e.getClass().getSimpleName() + ") to start command: " + e.toString(), e);
             throw new IOException(e);
@@ -1029,14 +1041,14 @@ public class SftpSubsystem
 
         pendingFuture = null;
 
-        if ((executors != null) && (!executors.isShutdown()) && shutdownExecutor) {
+        ExecutorService executors = getExecutorService();
+        if ((executors != null) && (!executors.isShutdown()) && isShutdownOnExit()) {
             Collection<Runnable> runners = executors.shutdownNow();
             if (log.isDebugEnabled()) {
                 log.debug("destroy(" + session + ") - shutdown executor service - runners count=" + runners.size());
             }
         }
-
-        executors = null;
+        this.executorService = null;
 
         try {
             fileSystem.close();

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e0d63503/sshd-core/src/test/java/org/apache/sshd/agent/AgentTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/agent/AgentTest.java b/sshd-core/src/test/java/org/apache/sshd/agent/AgentTest.java
index 113dc98..f252be7 100644
--- a/sshd-core/src/test/java/org/apache/sshd/agent/AgentTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/agent/AgentTest.java
@@ -159,9 +159,9 @@ public class AgentTest extends BaseTestSupport {
                                         session2.auth().verify(15L, TimeUnit.SECONDS);
 
                                         try (ChannelShell channel2 = session2.createShellChannel()) {
-                                            channel2.setIn(shellFactory.shell.getIn());
-                                            channel2.setOut(shellFactory.shell.getOut());
-                                            channel2.setErr(shellFactory.shell.getErr());
+                                            channel2.setIn(shellFactory.shell.getInputStream());
+                                            channel2.setOut(shellFactory.shell.getOutputStream());
+                                            channel2.setErr(shellFactory.shell.getErrorStream());
                                             channel2.setAgentForwarding(true);
                                             channel2.open().verify(9L, TimeUnit.SECONDS);
 

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e0d63503/sshd-core/src/test/java/org/apache/sshd/client/channel/ChannelExecTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/client/channel/ChannelExecTest.java b/sshd-core/src/test/java/org/apache/sshd/client/channel/ChannelExecTest.java
index 01cfed4..038ce55 100644
--- a/sshd-core/src/test/java/org/apache/sshd/client/channel/ChannelExecTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/client/channel/ChannelExecTest.java
@@ -54,7 +54,7 @@ public class ChannelExecTest extends BaseTestSupport {
         sshd.setCommandFactory(command -> new CommandExecutionHelper(command) {
             @Override
             protected boolean handleCommandLine(String command) throws Exception {
-                OutputStream stdout = getOut();
+                OutputStream stdout = getOutputStream();
                 stdout.write(command.getBytes(StandardCharsets.US_ASCII));
                 stdout.flush();
                 return false;

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e0d63503/sshd-core/src/test/java/org/apache/sshd/client/session/ClientSessionTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/client/session/ClientSessionTest.java b/sshd-core/src/test/java/org/apache/sshd/client/session/ClientSessionTest.java
index 2b14a4c..aeb5e3b 100644
--- a/sshd-core/src/test/java/org/apache/sshd/client/session/ClientSessionTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/client/session/ClientSessionTest.java
@@ -91,7 +91,7 @@ public class ClientSessionTest extends BaseTestSupport {
             protected boolean handleCommandLine(String command) throws Exception {
                 assertEquals("Mismatched incoming command", expectedCommand, command);
                 assertFalse("Duplicated command call", cmdProcessed);
-                OutputStream stdout = getOut();
+                OutputStream stdout = getOutputStream();
                 stdout.write(expectedResponse.getBytes(StandardCharsets.US_ASCII));
                 stdout.flush();
                 cmdProcessed = true;
@@ -120,7 +120,7 @@ public class ClientSessionTest extends BaseTestSupport {
             protected boolean handleCommandLine(String command) throws Exception {
                 assertEquals("Mismatched incoming command", expectedCommand, command);
                 assertFalse("Duplicated command call", cmdProcessed);
-                OutputStream stderr = getErr();
+                OutputStream stderr = getErrorStream();
                 stderr.write(expectedErrorMessage.getBytes(StandardCharsets.US_ASCII));
                 stderr.flush();
                 cmdProcessed = true;
@@ -171,7 +171,7 @@ public class ClientSessionTest extends BaseTestSupport {
                     protected boolean handleCommandLine(String command) throws Exception {
                         assertEquals("Mismatched incoming command", expectedCommand, command);
                         assertFalse("Duplicated command call", cmdProcessed);
-                        OutputStream stdout = getOut();
+                        OutputStream stdout = getOutputStream();
                         stdout.write(command.getBytes(StandardCharsets.US_ASCII));
                         stdout.flush();
                         cmdProcessed = true;

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e0d63503/sshd-core/src/test/java/org/apache/sshd/util/test/CommandExecutionHelper.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/util/test/CommandExecutionHelper.java b/sshd-core/src/test/java/org/apache/sshd/util/test/CommandExecutionHelper.java
index ce47a65..c44a2ad 100644
--- a/sshd-core/src/test/java/org/apache/sshd/util/test/CommandExecutionHelper.java
+++ b/sshd-core/src/test/java/org/apache/sshd/util/test/CommandExecutionHelper.java
@@ -21,97 +21,31 @@ package org.apache.sshd.util.test;
 
 import java.io.BufferedReader;
 import java.io.IOException;
-import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.io.InterruptedIOException;
 import java.io.OutputStream;
 import java.nio.charset.StandardCharsets;
 
-import org.apache.sshd.common.util.logging.AbstractLoggingBean;
-import org.apache.sshd.server.Command;
-import org.apache.sshd.server.Environment;
-import org.apache.sshd.server.ExitCallback;
+import org.apache.sshd.server.AbstractCommandSupport;
 
 /**
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
  */
-public abstract class CommandExecutionHelper extends AbstractLoggingBean implements Command, Runnable, ExitCallback {
-    private InputStream in;
-    private OutputStream out;
-    private OutputStream err;
-    private ExitCallback callback;
-    private Environment environment;
-    private Thread thread;
-    private boolean cbCalled;
-    // null/empty if shell
-    private String command;
-
+public abstract class CommandExecutionHelper extends AbstractCommandSupport {
     protected CommandExecutionHelper() {
         this(null);
     }
 
     protected CommandExecutionHelper(String command) {
-        this.command = command;
-    }
-
-    public InputStream getIn() {
-        return in;
-    }
-
-    public OutputStream getOut() {
-        return out;
-    }
-
-    public OutputStream getErr() {
-        return err;
-    }
-
-    public Environment getEnvironment() {
-        return environment;
-    }
-
-    public ExitCallback getExitCallback() {
-        return callback;
-    }
-
-    @Override
-    public void setInputStream(InputStream in) {
-        this.in = in;
-    }
-
-    @Override
-    public void setOutputStream(OutputStream out) {
-        this.out = out;
-    }
-
-    @Override
-    public void setErrorStream(OutputStream err) {
-        this.err = err;
-    }
-
-    @Override
-    public void setExitCallback(ExitCallback callback) {
-        this.callback = callback;
-    }
-
-    @Override
-    public void start(Environment env) throws IOException {
-        environment = env;
-        thread = new Thread(this, "CommandExecutionHelper");
-        thread.setDaemon(true);
-        thread.start();
-    }
-
-    @Override
-    public void destroy() {
-        thread.interrupt();
+        super(command, null, true);
     }
 
     @Override
     public void run() {
+        String command = getCommand();
         try {
             if (command == null) {
-                try (BufferedReader r = new BufferedReader(new InputStreamReader(getIn(), StandardCharsets.UTF_8))) {
+                try (BufferedReader r = new BufferedReader(new InputStreamReader(getInputStream(), StandardCharsets.UTF_8))) {
                     for (;;) {
                         command = r.readLine();
                         if (command == null) {
@@ -131,7 +65,7 @@ public abstract class CommandExecutionHelper extends AbstractLoggingBean impleme
         } catch (Exception e) {
             String message = "Failed (" + e.getClass().getSimpleName() + ") to handle '" + command + "': " + e.getMessage();
             try {
-                OutputStream stderr = getErr();
+                OutputStream stderr = getErrorStream();
                 stderr.write(message.getBytes(StandardCharsets.US_ASCII));
             } catch (IOException ioe) {
                 log.warn("Failed ({}) to write error message={}: {}",
@@ -144,18 +78,6 @@ public abstract class CommandExecutionHelper extends AbstractLoggingBean impleme
         }
     }
 
-    @Override
-    public void onExit(int exitValue, String exitMessage) {
-        if (!cbCalled) {
-            ExitCallback cb = getExitCallback();
-            try {
-                cb.onExit(exitValue, exitMessage);
-            } finally {
-                cbCalled = true;
-            }
-        }
-    }
-
     /**
      * @param command The command line
      * @return {@code true} if continue accepting command

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e0d63503/sshd-core/src/test/java/org/apache/sshd/util/test/EchoShell.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/util/test/EchoShell.java b/sshd-core/src/test/java/org/apache/sshd/util/test/EchoShell.java
index 76a8d5d..9ad8a14 100644
--- a/sshd-core/src/test/java/org/apache/sshd/util/test/EchoShell.java
+++ b/sshd-core/src/test/java/org/apache/sshd/util/test/EchoShell.java
@@ -31,7 +31,7 @@ public class EchoShell extends CommandExecutionHelper {
 
     @Override
     protected boolean handleCommandLine(String command) throws Exception {
-        OutputStream out = getOut();
+        OutputStream out = getOutputStream();
         out.write((command + "\n").getBytes(StandardCharsets.UTF_8));
         out.flush();
 

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e0d63503/sshd-git/src/main/java/org/apache/sshd/git/AbstractGitCommand.java
----------------------------------------------------------------------
diff --git a/sshd-git/src/main/java/org/apache/sshd/git/AbstractGitCommand.java b/sshd-git/src/main/java/org/apache/sshd/git/AbstractGitCommand.java
new file mode 100644
index 0000000..2ed85b0
--- /dev/null
+++ b/sshd-git/src/main/java/org/apache/sshd/git/AbstractGitCommand.java
@@ -0,0 +1,133 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.apache.sshd.git;
+
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+
+import org.apache.sshd.common.channel.ChannelOutputStream;
+import org.apache.sshd.server.AbstractCommandSupport;
+
+/**
+ * Provides basic support for GIT command implementations
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public abstract class AbstractGitCommand extends AbstractCommandSupport {
+    public static final int CHAR = 0x001;
+    public static final int DELIMITER = 0x002;
+    public static final int STARTQUOTE = 0x004;
+    public static final int ENDQUOTE = 0x008;
+
+    private String rootDir;
+
+    protected AbstractGitCommand(String rootDir, String command, ExecutorService executorService, boolean shutdownOnExit) {
+        super(command, executorService, shutdownOnExit);
+        this.rootDir = rootDir;
+    }
+
+    public String getRootDir() {
+        return rootDir;
+    }
+
+    @Override
+    public void setOutputStream(OutputStream out) {
+        super.setOutputStream(out);
+        if (out instanceof ChannelOutputStream) {
+            ((ChannelOutputStream) out).setNoDelay(true);
+        }
+    }
+
+    @Override
+    public void setErrorStream(OutputStream err) {
+        super.setErrorStream(err);
+        if (err instanceof ChannelOutputStream) {
+            ((ChannelOutputStream) err).setNoDelay(true);
+        }
+    }
+
+    /**
+     * Parses delimited string and returns an array containing the tokens. This
+     * parser obeys quotes, so the delimiter character will be ignored if it is
+     * inside of a quote. This method assumes that the quote character is not
+     * included in the set of delimiter characters.
+     *
+     * @param value the delimited string to parse.
+     * @param delim the characters delimiting the tokens.
+     * @param trim {@code true} if the strings are trimmed before being added to the list
+     * @return a list of string or an empty list if there are none.
+     */
+    public static List<String> parseDelimitedString(String value, String delim, boolean trim) {
+        if (value == null) {
+            value = "";
+        }
+
+        List<String> list = new ArrayList<>();
+        StringBuilder sb = new StringBuilder();
+        int expecting = CHAR | DELIMITER | STARTQUOTE;
+        boolean isEscaped = false;
+        for (int i = 0; i < value.length(); i++) {
+            char c = value.charAt(i);
+            boolean isDelimiter = delim.indexOf(c) >= 0;
+            if (!isEscaped && (c == '\\')) {
+                isEscaped = true;
+                continue;
+            }
+
+            if (isEscaped) {
+                sb.append(c);
+            } else if (isDelimiter && ((expecting & DELIMITER) != 0)) {
+                if (trim) {
+                    String str = sb.toString();
+                    list.add(str.trim());
+                } else {
+                    list.add(sb.toString());
+                }
+                sb.delete(0, sb.length());
+                expecting = CHAR | DELIMITER | STARTQUOTE;
+            } else if ((c == '"') && ((expecting & STARTQUOTE) != 0)) {
+                sb.append(c);
+                expecting = CHAR | ENDQUOTE;
+            } else if ((c == '"') && ((expecting & ENDQUOTE) != 0)) {
+                sb.append(c);
+                expecting = CHAR | STARTQUOTE | DELIMITER;
+            } else if ((expecting & CHAR) != 0) {
+                sb.append(c);
+            } else {
+                throw new IllegalArgumentException("Invalid delimited string: " + value);
+            }
+
+            isEscaped = false;
+        }
+
+        if (sb.length() > 0) {
+            if (trim) {
+                String str = sb.toString();
+                list.add(str.trim());
+            } else {
+                list.add(sb.toString());
+            }
+        }
+
+        return list;
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e0d63503/sshd-git/src/main/java/org/apache/sshd/git/AbstractGitCommandFactory.java
----------------------------------------------------------------------
diff --git a/sshd-git/src/main/java/org/apache/sshd/git/AbstractGitCommandFactory.java b/sshd-git/src/main/java/org/apache/sshd/git/AbstractGitCommandFactory.java
new file mode 100644
index 0000000..337643d
--- /dev/null
+++ b/sshd-git/src/main/java/org/apache/sshd/git/AbstractGitCommandFactory.java
@@ -0,0 +1,96 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.apache.sshd.git;
+
+import java.util.concurrent.ExecutorService;
+
+import org.apache.sshd.common.util.ValidateUtils;
+import org.apache.sshd.common.util.threads.ExecutorServiceCarrier;
+import org.apache.sshd.server.Command;
+import org.apache.sshd.server.CommandFactory;
+import org.apache.sshd.server.scp.UnknownCommand;
+
+/**
+ * TODO Add javadoc
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public abstract class AbstractGitCommandFactory implements CommandFactory, ExecutorServiceCarrier {
+    private final String rootDir;
+    private final CommandFactory delegate;
+    private final String cmdPrefix;
+    private ExecutorService executorService;
+    private boolean shutdownOnExit;
+
+    protected AbstractGitCommandFactory(String rootDir, CommandFactory delegate, String cmdPrefix) {
+        this.rootDir = rootDir;
+        this.delegate = delegate;
+        this.cmdPrefix = ValidateUtils.checkNotNullAndNotEmpty(cmdPrefix, "No command prefix provided");
+    }
+
+    public AbstractGitCommandFactory withExecutorService(ExecutorService executorService) {
+        this.executorService = executorService;
+        return this;
+    }
+
+    public String getRootDir() {
+        return rootDir;
+    }
+
+    public CommandFactory getDelegate() {
+        return delegate;
+    }
+
+    public String getCommandPrefix() {
+        return cmdPrefix;
+    }
+
+    public AbstractGitCommandFactory withShutdownOnExit(boolean shutdownOnExit) {
+        this.shutdownOnExit = shutdownOnExit;
+        return this;
+    }
+
+    @Override
+    public ExecutorService getExecutorService() {
+        return executorService;
+    }
+
+    @Override
+    public boolean isShutdownOnExit() {
+        return shutdownOnExit;
+    }
+
+    @Override
+    public Command createCommand(String command) {
+        String prefix = getCommandPrefix();
+        if (command.startsWith(prefix)) {
+            return createGitCommand(command);
+        }
+
+        CommandFactory delegate = getDelegate();
+        if (delegate != null) {
+            return delegate.createCommand(command);
+        } else {
+            return new UnknownCommand(command);
+        }
+    }
+
+    protected abstract AbstractGitCommand createGitCommand(String command);
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e0d63503/sshd-git/src/main/java/org/apache/sshd/git/pack/GitPackCommand.java
----------------------------------------------------------------------
diff --git a/sshd-git/src/main/java/org/apache/sshd/git/pack/GitPackCommand.java b/sshd-git/src/main/java/org/apache/sshd/git/pack/GitPackCommand.java
index 229cbfb..8aebac5 100644
--- a/sshd-git/src/main/java/org/apache/sshd/git/pack/GitPackCommand.java
+++ b/sshd-git/src/main/java/org/apache/sshd/git/pack/GitPackCommand.java
@@ -19,20 +19,15 @@
 package org.apache.sshd.git.pack;
 
 import java.io.File;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.util.ArrayList;
 import java.util.List;
+import java.util.concurrent.ExecutorService;
 
-import org.apache.sshd.common.channel.ChannelOutputStream;
-import org.apache.sshd.common.util.logging.AbstractLoggingBean;
-import org.apache.sshd.server.Command;
+import org.apache.sshd.git.AbstractGitCommand;
 import org.apache.sshd.server.Environment;
-import org.apache.sshd.server.ExitCallback;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.lib.RepositoryCache;
 import org.eclipse.jgit.transport.ReceivePack;
+import org.eclipse.jgit.transport.RemoteConfig;
 import org.eclipse.jgit.transport.UploadPack;
 import org.eclipse.jgit.util.FS;
 
@@ -41,166 +36,56 @@ import org.eclipse.jgit.util.FS;
  *
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
  */
-public class GitPackCommand extends AbstractLoggingBean implements Command, Runnable {
-
-    private static final int CHAR = 1;
-    private static final int DELIMITER = 2;
-    private static final int STARTQUOTE = 4;
-    private static final int ENDQUOTE = 8;
-
-    private String rootDir;
-    private String command;
-    private InputStream in;
-    private OutputStream out;
-    private OutputStream err;
-    private ExitCallback callback;
-
-    public GitPackCommand(String rootDir, String command) {
-        this.rootDir = rootDir;
-        this.command = command;
-    }
-
-    @Override
-    public void setInputStream(InputStream in) {
-        this.in = in;
-    }
-
-    @Override
-    public void setOutputStream(OutputStream out) {
-        this.out = out;
-        if (out instanceof ChannelOutputStream) {
-            ((ChannelOutputStream) out).setNoDelay(true);
-        }
-    }
-
-    @Override
-    public void setErrorStream(OutputStream err) {
-        this.err = err;
-        if (err instanceof ChannelOutputStream) {
-            ((ChannelOutputStream) err).setNoDelay(true);
-        }
-    }
-
-    @Override
-    public void setExitCallback(ExitCallback callback) {
-        this.callback = callback;
-    }
-
-    @Override
-    public void start(Environment env) throws IOException {
-        Thread thread = new Thread(this);
-        thread.setDaemon(true);
-        thread.start();
+public class GitPackCommand extends AbstractGitCommand {
+    /**
+     * @param rootDir Root directory for the command
+     * @param command Command to execute
+     * @param executorService An {@link ExecutorService} to be used when {@link #start(Environment)}-ing
+     * execution. If {@code null} an ad-hoc single-threaded service is created and used.
+     * @param shutdownOnExit  If {@code true} the {@link ExecutorService#shutdownNow()} will be called when
+     * command terminates - unless it is the ad-hoc service, which will be shutdown regardless
+     */
+    public GitPackCommand(String rootDir, String command, ExecutorService executorService, boolean shutdownOnExit) {
+        super(rootDir, command, executorService, shutdownOnExit);
     }
 
     @Override
     public void run() {
+        String command = getCommand();
         try {
             List<String> strs = parseDelimitedString(command, " ", true);
             String[] args = strs.toArray(new String[strs.size()]);
             for (int i = 0; i < args.length; i++) {
-                if (args[i].startsWith("'") && args[i].endsWith("'")) {
-                    args[i] = args[i].substring(1, args[i].length() - 1);
+                String argVal = args[i];
+                if (argVal.startsWith("'") && argVal.endsWith("'")) {
+                    args[i] = argVal.substring(1, argVal.length() - 1);
+                    argVal = args[i];
                 }
-                if (args[i].startsWith("\"") && args[i].endsWith("\"")) {
-                    args[i] = args[i].substring(1, args[i].length() - 1);
+                if (argVal.startsWith("\"") && argVal.endsWith("\"")) {
+                    args[i] = argVal.substring(1, argVal.length() - 1);
+                    argVal = args[i];
                 }
             }
 
             if (args.length != 2) {
                 throw new IllegalArgumentException("Invalid git command line: " + command);
             }
+
+            String rootDir = getRootDir();
             File srcGitdir = new File(rootDir, args[1]);
             RepositoryCache.FileKey key = RepositoryCache.FileKey.lenient(srcGitdir, FS.DETECTED);
             Repository db = key.open(true /* must exist */);
-            if ("git-upload-pack".equals(args[0])) {
-                new UploadPack(db).upload(in, out, err);
-            } else if ("git-receive-pack".equals(args[0])) {
-                new ReceivePack(db).receive(in, out, err);
+            if (RemoteConfig.DEFAULT_UPLOAD_PACK.equals(args[0])) {
+                new UploadPack(db).upload(getInputStream(), getOutputStream(), getErrorStream());
+            } else if (RemoteConfig.DEFAULT_RECEIVE_PACK.equals(args[0])) {
+                new ReceivePack(db).receive(getInputStream(), getOutputStream(), getErrorStream());
             } else {
                 throw new IllegalArgumentException("Unknown git command: " + command);
             }
 
-            if (callback != null) {
-                callback.onExit(0);
-            }
+            onExit(0);
         } catch (Throwable t) {
-            log.warn("Failed {} to execute command={}: {}",
-                     t.getClass().getSimpleName(), command, t.getMessage());
-            if (callback != null) {
-                callback.onExit(-1, t.getClass().getSimpleName());
-            }
-        }
-    }
-
-    @Override
-    public void destroy() {
-        //To change body of implemented methods use File | Settings | File Templates.
-    }
-
-    /**
-     * Parses delimited string and returns an array containing the tokens. This
-     * parser obeys quotes, so the delimiter character will be ignored if it is
-     * inside of a quote. This method assumes that the quote character is not
-     * included in the set of delimiter characters.
-     *
-     * @param value the delimited string to parse.
-     * @param delim the characters delimiting the tokens.
-     * @param trim {@code true} if the strings are trimmed before being added to the list
-     * @return a list of string or an empty list if there are none.
-     */
-    private static List<String> parseDelimitedString(String value, String delim, boolean trim) {
-        if (value == null) {
-            value = "";
+            onExit(-1, t.getClass().getSimpleName());
         }
-
-        List<String> list = new ArrayList<>();
-        StringBuilder sb = new StringBuilder();
-        int expecting = CHAR | DELIMITER | STARTQUOTE;
-        boolean isEscaped = false;
-        for (int i = 0; i < value.length(); i++) {
-            char c = value.charAt(i);
-            boolean isDelimiter = delim.indexOf(c) >= 0;
-            if (!isEscaped && (c == '\\')) {
-                isEscaped = true;
-                continue;
-            }
-
-            if (isEscaped) {
-                sb.append(c);
-            } else if (isDelimiter && ((expecting & DELIMITER) != 0)) {
-                if (trim) {
-                    String str = sb.toString();
-                    list.add(str.trim());
-                } else {
-                    list.add(sb.toString());
-                }
-                sb.delete(0, sb.length());
-                expecting = CHAR | DELIMITER | STARTQUOTE;
-            } else if ((c == '"') && ((expecting & STARTQUOTE) != 0)) {
-                sb.append(c);
-                expecting = CHAR | ENDQUOTE;
-            } else if ((c == '"') && ((expecting & ENDQUOTE) != 0)) {
-                sb.append(c);
-                expecting = CHAR | STARTQUOTE | DELIMITER;
-            } else if ((expecting & CHAR) != 0) {
-                sb.append(c);
-            } else {
-                throw new IllegalArgumentException("Invalid delimited string: " + value);
-            }
-
-            isEscaped = false;
-        }
-
-        if (sb.length() > 0) {
-            if (trim) {
-                String str = sb.toString();
-                list.add(str.trim());
-            } else {
-                list.add(sb.toString());
-            }
-        }
-
-        return list;
     }
 }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e0d63503/sshd-git/src/main/java/org/apache/sshd/git/pack/GitPackCommandFactory.java
----------------------------------------------------------------------
diff --git a/sshd-git/src/main/java/org/apache/sshd/git/pack/GitPackCommandFactory.java b/sshd-git/src/main/java/org/apache/sshd/git/pack/GitPackCommandFactory.java
index b5399c5..e12c228 100644
--- a/sshd-git/src/main/java/org/apache/sshd/git/pack/GitPackCommandFactory.java
+++ b/sshd-git/src/main/java/org/apache/sshd/git/pack/GitPackCommandFactory.java
@@ -18,37 +18,39 @@
  */
 package org.apache.sshd.git.pack;
 
-import org.apache.sshd.server.Command;
+import java.util.concurrent.ExecutorService;
+
+import org.apache.sshd.git.AbstractGitCommandFactory;
 import org.apache.sshd.server.CommandFactory;
-import org.apache.sshd.server.scp.UnknownCommand;
 
 /**
  * TODO Add javadoc
  *
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
  */
-public class GitPackCommandFactory implements CommandFactory {
-
-    private final String rootDir;
-    private final CommandFactory delegate;
+public class GitPackCommandFactory extends AbstractGitCommandFactory {
+    public static final String GIT_COMMAND_PREFIX = "git-";
 
     public GitPackCommandFactory(String rootDir) {
         this(rootDir,  null);
     }
 
     public GitPackCommandFactory(String rootDir, CommandFactory delegate) {
-        this.rootDir = rootDir;
-        this.delegate = delegate;
+        super(rootDir, delegate, GIT_COMMAND_PREFIX);
+    }
+
+    @Override
+    public GitPackCommandFactory withExecutorService(ExecutorService executorService) {
+        return (GitPackCommandFactory) super.withExecutorService(executorService);
+    }
+
+    @Override
+    public GitPackCommandFactory withShutdownOnExit(boolean shutdownOnExit) {
+        return (GitPackCommandFactory) super.withShutdownOnExit(shutdownOnExit);
     }
 
     @Override
-    public Command createCommand(String command) {
-        if (command.startsWith("git-")) {
-            return new GitPackCommand(rootDir, command);
-        } else if (delegate != null) {
-            return delegate.createCommand(command);
-        } else {
-            return new UnknownCommand(command);
-        }
+    public GitPackCommand createGitCommand(String command) {
+        return new GitPackCommand(getRootDir(), command, getExecutorService(), isShutdownOnExit());
     }
 }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e0d63503/sshd-git/src/main/java/org/apache/sshd/git/pgm/EmbeddedCommandRunner.java
----------------------------------------------------------------------
diff --git a/sshd-git/src/main/java/org/apache/sshd/git/pgm/EmbeddedCommandRunner.java b/sshd-git/src/main/java/org/apache/sshd/git/pgm/EmbeddedCommandRunner.java
index 8ccc510..204dc80 100644
--- a/sshd-git/src/main/java/org/apache/sshd/git/pgm/EmbeddedCommandRunner.java
+++ b/sshd-git/src/main/java/org/apache/sshd/git/pgm/EmbeddedCommandRunner.java
@@ -81,8 +81,8 @@ public class EmbeddedCommandRunner {
      * @param err the error stream, may be null in which case the system error stream will be used
      * @throws Exception if an error occurs
      */
-    public void execute(final String[] argv, InputStream in, OutputStream out, OutputStream err) throws Exception {
-        final CmdLineParser clp = new CmdLineParser(this);
+    public void execute(String[] argv, InputStream in, OutputStream out, OutputStream err) throws Exception {
+        CmdLineParser clp = new CmdLineParser(this);
         PrintWriter writer = new PrintWriter(err != null ? err : System.err);
         try {
             clp.parseArgument(argv);
@@ -95,7 +95,7 @@ public class EmbeddedCommandRunner {
         }
 
         if (argv.length == 0 || help) {
-            final String ex = clp.printExample(OptionHandlerFilter.ALL, CLIText.get().resourceBundle());
+            String ex = clp.printExample(OptionHandlerFilter.ALL, CLIText.get().resourceBundle());
             writer.println("jgit" + ex + " command [ARG ...]"); //$NON-NLS-1$
             if (help) {
                 writer.println();

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e0d63503/sshd-git/src/main/java/org/apache/sshd/git/pgm/GitPgmCommand.java
----------------------------------------------------------------------
diff --git a/sshd-git/src/main/java/org/apache/sshd/git/pgm/GitPgmCommand.java b/sshd-git/src/main/java/org/apache/sshd/git/pgm/GitPgmCommand.java
index 013f4e5..2b7a1ab 100644
--- a/sshd-git/src/main/java/org/apache/sshd/git/pgm/GitPgmCommand.java
+++ b/sshd-git/src/main/java/org/apache/sshd/git/pgm/GitPgmCommand.java
@@ -19,15 +19,12 @@
 package org.apache.sshd.git.pgm;
 
 import java.io.IOException;
-import java.io.InputStream;
 import java.io.OutputStream;
 import java.nio.charset.StandardCharsets;
-import java.util.ArrayList;
 import java.util.List;
+import java.util.concurrent.ExecutorService;
 
-import org.apache.sshd.common.channel.ChannelOutputStream;
-import org.apache.sshd.common.util.logging.AbstractLoggingBean;
-import org.apache.sshd.server.Command;
+import org.apache.sshd.git.AbstractGitCommand;
 import org.apache.sshd.server.Environment;
 import org.apache.sshd.server.ExitCallback;
 
@@ -36,60 +33,24 @@ import org.apache.sshd.server.ExitCallback;
  *
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
  */
-public class GitPgmCommand extends AbstractLoggingBean implements Command, Runnable {
-
-    private static final int CHAR = 1;
-    private static final int DELIMITER = 2;
-    private static final int STARTQUOTE = 4;
-    private static final int ENDQUOTE = 8;
-
-    private String rootDir;
-    private String command;
-    private InputStream in;
-    private OutputStream out;
-    private OutputStream err;
-    private ExitCallback callback;
-
-    public GitPgmCommand(String rootDir, String command) {
-        this.rootDir = rootDir;
-        this.command = command;
-    }
-
-    @Override
-    public void setInputStream(InputStream in) {
-        this.in = in;
-    }
-
-    @Override
-    public void setOutputStream(OutputStream out) {
-        this.out = out;
-        if (out instanceof ChannelOutputStream) {
-            ((ChannelOutputStream) out).setNoDelay(true);
-        }
-    }
-
-    @Override
-    public void setErrorStream(OutputStream err) {
-        this.err = err;
-        if (err instanceof ChannelOutputStream) {
-            ((ChannelOutputStream) err).setNoDelay(true);
-        }
-    }
-
-    @Override
-    public void setExitCallback(ExitCallback callback) {
-        this.callback = callback;
-    }
-
-    @Override
-    public void start(Environment env) throws IOException {
-        Thread thread = new Thread(this);
-        thread.setDaemon(true);
-        thread.start();
+public class GitPgmCommand extends AbstractGitCommand {
+    /**
+     * @param rootDir Root directory for the command
+     * @param command Command to execute
+     * @param executorService An {@link ExecutorService} to be used when {@link #start(Environment)}-ing
+     * execution. If {@code null} an ad-hoc single-threaded service is created and used.
+     * @param shutdownOnExit  If {@code true} the {@link ExecutorService#shutdownNow()} will be called when
+     * command terminates - unless it is the ad-hoc service, which will be shutdown regardless
+     */
+    public GitPgmCommand(String rootDir, String command, ExecutorService executorService, boolean shutdownOnExit) {
+        super(rootDir, command, executorService, shutdownOnExit);
     }
 
     @Override
     public void run() {
+        String command = getCommand();
+        ExitCallback callback = getExitCallback();
+        OutputStream err = getErrorStream();
         try {
             List<String> strs = parseDelimitedString(command, " ", true);
             String[] args = strs.toArray(new String[strs.size()]);
@@ -102,7 +63,7 @@ public class GitPgmCommand extends AbstractLoggingBean implements Command, Runna
                 }
             }
 
-            new EmbeddedCommandRunner(rootDir).execute(args, in, out, err);
+            new EmbeddedCommandRunner(getRootDir()).execute(args, getInputStream(), getOutputStream(), err);
             if (callback != null) {
                 callback.onExit(0);
             }
@@ -119,76 +80,4 @@ public class GitPgmCommand extends AbstractLoggingBean implements Command, Runna
             }
         }
     }
-
-    @Override
-    public void destroy() {
-        //To change body of implemented methods use File | Settings | File Templates.
-    }
-
-    /**
-     * Parses delimited string and returns an array containing the tokens. This
-     * parser obeys quotes, so the delimiter character will be ignored if it is
-     * inside of a quote. This method assumes that the quote character is not
-     * included in the set of delimiter characters.
-     *
-     * @param value the delimited string to parse.
-     * @param delim the characters delimiting the tokens.
-     * @param trim {@code true} if the strings are trimmed before being added to the list
-     * @return a list of string or an empty list if there are none.
-     */
-    private static List<String> parseDelimitedString(String value, String delim, boolean trim) {
-        if (value == null) {
-            value = "";
-        }
-
-        List<String> list = new ArrayList<>();
-        StringBuilder sb = new StringBuilder();
-        int expecting = CHAR | DELIMITER | STARTQUOTE;
-        boolean isEscaped = false;
-        for (int i = 0; i < value.length(); i++) {
-            char c = value.charAt(i);
-            boolean isDelimiter = delim.indexOf(c) >= 0;
-
-            if (!isEscaped && (c == '\\')) {
-                isEscaped = true;
-                continue;
-            }
-
-            if (isEscaped) {
-                sb.append(c);
-            } else if (isDelimiter && ((expecting & DELIMITER) != 0)) {
-                if (trim) {
-                    String str = sb.toString();
-                    list.add(str.trim());
-                } else {
-                    list.add(sb.toString());
-                }
-                sb.delete(0, sb.length());
-                expecting = CHAR | DELIMITER | STARTQUOTE;
-            } else if ((c == '"') && ((expecting & STARTQUOTE) != 0)) {
-                sb.append(c);
-                expecting = CHAR | ENDQUOTE;
-            } else if ((c == '"') && ((expecting & ENDQUOTE) != 0)) {
-                sb.append(c);
-                expecting = CHAR | STARTQUOTE | DELIMITER;
-            } else if ((expecting & CHAR) != 0) {
-                sb.append(c);
-            } else {
-                throw new IllegalArgumentException("Invalid delimited string: " + value);
-            }
-
-            isEscaped = false;
-        }
-
-        if (sb.length() > 0) {
-            if (trim) {
-                String str = sb.toString();
-                list.add(str.trim());
-            } else {
-                list.add(sb.toString());
-            }
-        }
-
-        return list;
-    }
 }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e0d63503/sshd-git/src/main/java/org/apache/sshd/git/pgm/GitPgmCommandFactory.java
----------------------------------------------------------------------
diff --git a/sshd-git/src/main/java/org/apache/sshd/git/pgm/GitPgmCommandFactory.java b/sshd-git/src/main/java/org/apache/sshd/git/pgm/GitPgmCommandFactory.java
index 8323528..cba6c4e 100644
--- a/sshd-git/src/main/java/org/apache/sshd/git/pgm/GitPgmCommandFactory.java
+++ b/sshd-git/src/main/java/org/apache/sshd/git/pgm/GitPgmCommandFactory.java
@@ -18,38 +18,39 @@
  */
 package org.apache.sshd.git.pgm;
 
-import org.apache.sshd.server.Command;
+import java.util.concurrent.ExecutorService;
+
+import org.apache.sshd.git.AbstractGitCommandFactory;
 import org.apache.sshd.server.CommandFactory;
-import org.apache.sshd.server.scp.UnknownCommand;
 
 /**
- * TODO Add javadoc
+ * Runs a GIT command locally using an embedded executor
  *
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
  */
-public class GitPgmCommandFactory implements CommandFactory {
+public class GitPgmCommandFactory extends AbstractGitCommandFactory {
     public static final String GIT_COMMAND_PREFIX = "git ";
 
-    private final String rootDir;
-    private final CommandFactory delegate;
-
     public GitPgmCommandFactory(String rootDir) {
         this(rootDir,  null);
     }
 
     public GitPgmCommandFactory(String rootDir, CommandFactory delegate) {
-        this.rootDir = rootDir;
-        this.delegate = delegate;
+        super(rootDir, delegate, GIT_COMMAND_PREFIX);
+    }
+
+    @Override
+    public GitPgmCommandFactory withExecutorService(ExecutorService executorService) {
+        return (GitPgmCommandFactory) super.withExecutorService(executorService);
+    }
+
+    @Override
+    public GitPgmCommandFactory withShutdownOnExit(boolean shutdownOnExit) {
+        return (GitPgmCommandFactory) super.withShutdownOnExit(shutdownOnExit);
     }
 
     @Override
-    public Command createCommand(String command) {
-        if (command.startsWith(GIT_COMMAND_PREFIX)) {
-            return new GitPgmCommand(rootDir, command.substring(GIT_COMMAND_PREFIX.length()));
-        } else if (delegate != null) {
-            return delegate.createCommand(command);
-        } else {
-            return new UnknownCommand(command);
-        }
+    public GitPgmCommand createGitCommand(String command) {
+        return new GitPgmCommand(getRootDir(), command.substring(GIT_COMMAND_PREFIX.length()), getExecutorService(), isShutdownOnExit());
     }
 }