Hi everyone,

Whole last day I've tried to implement a managed SSH access for Git. The 
problem I faced was not quite trivial: whenever I try to `git clone` something 
over SSH transport I either get successfully cloned repository or `fatal: early 
EOF`. After hours of investigating I finally managed to identify a problem. It 
is in `InvertedShellWrapper.pumpStreams`:

           for (;;) {
               if (!shell.isAlive()) {
                   callback.onExit(shell.exitValue());
                   // risk of loosing process data
                   return;
               }
               if (pumpStream(in, shellIn, buffer)) {
                   continue;
               }
               if (pumpStream(shellOut, out, buffer)) {
                   continue;
               }
               if (pumpStream(shellErr, err, buffer)) {
                   continue;
               }
               Thread.sleep(1);
           }

In most Unix systems a process may exit as soon as it writes everything into a 
stream. If the streams are buffered, then there is a chance of dropping 
everything that is left in STDOUT and STDERR (`shellOut` and `shellErr`) after 
the `shell.isAlive` has reported that the underlying process has finished.

The solution is simple: you just pump streams one more time until no more data 
is left in STDOUT or STDERR. Here is what I came up with (sorry for Scala):

   class GitShellFactory(val verb: String, val path: String)
       extends ProcessShellFactory(Array[String](verb, path)) {
     override def create() = new SshShellWrapper(new ProcessShell())
   }

   class SshShellWrapper(shell: InvertedShell) extends SimpleCommand {
     var buffer = new Array[Byte](4096)
     var stdin: OutputStream = _
     var stdout: InputStream = _
     var stderr: InputStream = _
     def start(env: Environment): Unit = {
       shell.start(env.getEnv)
       stdin = shell.getInputStream
       stdout = shell.getOutputStream
       stderr = shell.getErrorStream

       new Thread() {
         override def run() = try {
           while (shell.isAlive) {
             copyStream(in, stdin)
             copyStream(stdout, out)
             copyStream(stderr, err)
             Thread.sleep(1)
           }
           copyStream(stdout, out)
           copyStream(stderr, err)
         } finally {
           shell.destroy()
           exit(shell.exitValue)
         }
       }.start()
     }
     def destroy = if (shell != null) shell.destroy
     def copyStream(in: InputStream, out: OutputStream): Unit =
       while (in.available > 0) {
         val len = in.read(buffer)
         if (len > 0) {
           out.write(buffer, 0, len)
           out.flush
         }
       }
   }

Then I simply use GitShellFactory instead of ProcessShellFactory and everything 
works perfectly.

Hope it will save some time for those who stuck with the same issue. It would 
be great to have this fixed in later releases, though.

Best regards,
Boris Okunskiy

Reply via email to