You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@mina.apache.org by gn...@apache.org on 2013/07/26 10:37:26 UTC

[1/3] git commit: [SSHD-243] The sshd client always send to channel 0

Updated Branches:
  refs/heads/master e05ade90c -> f549a71bc


[SSHD-243] The sshd client always send to channel 0

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

Branch: refs/heads/master
Commit: f31ac81b98e2b7b7468c2a39fcd4eceed8c533c1
Parents: e05ade9
Author: Guillaume Nodet <gn...@apache.org>
Authored: Thu Jul 25 21:32:43 2013 +0200
Committer: Guillaume Nodet <gn...@apache.org>
Committed: Thu Jul 25 21:32:43 2013 +0200

----------------------------------------------------------------------
 .../org/apache/sshd/client/channel/ChannelSession.java    | 10 +++++-----
 .../org/apache/sshd/common/session/AbstractSession.java   |  6 ++----
 sshd-core/src/test/java/org/apache/sshd/LoadTest.java     |  2 +-
 3 files changed, 8 insertions(+), 10 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f31ac81b/sshd-core/src/main/java/org/apache/sshd/client/channel/ChannelSession.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/channel/ChannelSession.java b/sshd-core/src/main/java/org/apache/sshd/client/channel/ChannelSession.java
index 3166666..d6c9124 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/channel/ChannelSession.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/channel/ChannelSession.java
@@ -42,6 +42,11 @@ public class ChannelSession extends AbstractClientChannel {
     }
 
     public OpenFuture open() throws IOException {
+        return internalOpen();
+    }
+
+    @Override
+    protected void doOpen() throws IOException {
         invertedIn = new ChannelOutputStream(this, remoteWindow, log, SshConstants.Message.SSH_MSG_CHANNEL_DATA);
         if (out == null) {
             ChannelPipedInputStream pis = new ChannelPipedInputStream(localWindow);
@@ -55,11 +60,6 @@ public class ChannelSession extends AbstractClientChannel {
             err = pos;
             invertedErr = pis;
         }
-        return internalOpen();
-    }
-
-    @Override
-    protected void doOpen() throws IOException {
         if (in != null) {
             streamPumper = new Thread("ClientInputStreamPump") {
                 @Override

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f31ac81b/sshd-core/src/main/java/org/apache/sshd/common/session/AbstractSession.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/session/AbstractSession.java b/sshd-core/src/main/java/org/apache/sshd/common/session/AbstractSession.java
index ad83261..f86e787 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/session/AbstractSession.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/session/AbstractSession.java
@@ -100,7 +100,7 @@ public abstract class AbstractSession implements Session {
     /** Map of channels keyed by the identifier */
     protected final Map<Integer, Channel> channels = new ConcurrentHashMap<Integer, Channel>();
     /** Next channel identifier */
-    protected int nextChannelId;
+    protected static AtomicInteger nextChannelId = new AtomicInteger(100);
 
     /** Session listener */
     protected final List<SessionListener> listeners = new ArrayList<SessionListener>();
@@ -983,9 +983,7 @@ public abstract class AbstractSession implements Session {
 
 
     protected int getNextChannelId() {
-        synchronized (channels) {
-            return nextChannelId++;
-        }
+        return nextChannelId.incrementAndGet();
     }
 
     public int registerChannel(Channel channel) throws IOException {

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f31ac81b/sshd-core/src/test/java/org/apache/sshd/LoadTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/LoadTest.java b/sshd-core/src/test/java/org/apache/sshd/LoadTest.java
index 299b8e3..2fd30a7 100644
--- a/sshd-core/src/test/java/org/apache/sshd/LoadTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/LoadTest.java
@@ -123,7 +123,7 @@ public class LoadTest {
         ByteArrayOutputStream err = new ByteArrayOutputStream();
         channel.setOut(out);
         channel.setErr(err);
-        channel.open();
+        channel.open().await();
         OutputStream pipedIn = channel.getInvertedIn();
 
         msg += "\nexit\n";


[2/3] git commit: [SSHD-90] Fix some problems with SCP client Support for attributes preservation on copy

Posted by gn...@apache.org.
[SSHD-90] Fix some problems with SCP client
Support for attributes preservation on copy

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

Branch: refs/heads/master
Commit: 725bcd9f2fc7c0027eaec7c30e439b375f088254
Parents: f31ac81
Author: Guillaume Nodet <gn...@apache.org>
Authored: Thu Jul 25 23:49:52 2013 +0200
Committer: Guillaume Nodet <gn...@apache.org>
Committed: Thu Jul 25 23:49:52 2013 +0200

----------------------------------------------------------------------
 .../java/org/apache/sshd/client/ScpClient.java  |  18 +-
 .../client/channel/AbstractClientChannel.java   |   4 +
 .../sshd/client/scp/DefaultScpClient.java       | 105 +++++---
 .../common/channel/ChannelPipedInputStream.java |   2 +-
 .../org/apache/sshd/common/scp/ScpHelper.java   | 189 +++++++++++--
 .../apache/sshd/server/command/ScpCommand.java  |  25 +-
 .../sshd/server/command/ScpCommandFactory.java  |  32 +--
 .../src/test/java/org/apache/sshd/ScpTest.java  | 263 +++++++++++++++++++
 8 files changed, 518 insertions(+), 120 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/725bcd9f/sshd-core/src/main/java/org/apache/sshd/client/ScpClient.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/ScpClient.java b/sshd-core/src/main/java/org/apache/sshd/client/ScpClient.java
index d2bdaa8..381a210 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/ScpClient.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/ScpClient.java
@@ -24,18 +24,18 @@ import java.io.IOException;
  */
 public interface ScpClient {
 
-    void download(String remote, String local) throws IOException;
+    enum Option {
+        Recursive,
+        PreserveAttributes,
+        TargetIsDirectory
+    }
 
-    void download(String remote, String local, boolean recursive) throws IOException;
+    void download(String remote, String local, Option... options) throws IOException;
 
-    void download(String[] remote, String local) throws Exception;
+    void download(String[] remote, String local, Option... options) throws Exception;
 
-    void download(String[] remote, String local, boolean recursive) throws Exception;
+    void upload(String local, String remote, Option... options) throws IOException;
 
-    void upload(String remote, String local) throws IOException;
-
-    void upload(String remote, String local, boolean recursive) throws IOException;
-
-    void upload(String[] local, String remote, boolean recursive) throws IOException;
+    void upload(String[] local, String remote, Option... options) throws IOException;
 
 }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/725bcd9f/sshd-core/src/main/java/org/apache/sshd/client/channel/AbstractClientChannel.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/channel/AbstractClientChannel.java b/sshd-core/src/main/java/org/apache/sshd/client/channel/AbstractClientChannel.java
index 8d04dba..b786c39 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/channel/AbstractClientChannel.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/channel/AbstractClientChannel.java
@@ -227,6 +227,10 @@ public abstract class AbstractClientChannel extends AbstractChannel implements C
     }
 
     protected void doWriteData(byte[] data, int off, int len) throws IOException {
+        // If we're already closing, ignore incoming data
+        if (closing.get()) {
+            return;
+        }
         if (out != null) {
             out.write(data, off, len);
             out.flush();

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/725bcd9f/sshd-core/src/main/java/org/apache/sshd/client/scp/DefaultScpClient.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/scp/DefaultScpClient.java b/sshd-core/src/main/java/org/apache/sshd/client/scp/DefaultScpClient.java
index 1b590c2..911eb5c 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/scp/DefaultScpClient.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/scp/DefaultScpClient.java
@@ -20,7 +20,11 @@ package org.apache.sshd.client.scp;
 
 import java.io.IOException;
 import java.io.InterruptedIOException;
+import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
 
 import org.apache.sshd.ClientSession;
 import org.apache.sshd.client.ScpClient;
@@ -42,39 +46,44 @@ public class DefaultScpClient implements ScpClient {
         this.clientSession = clientSession;
     }
 
-    public void download(String remote, String local) throws IOException {
-        download(new String[] { remote }, local, false, false);
-    }
-
-    public void download(String remote, String local, boolean recursive) throws IOException {
-        download(new String[] { remote }, local, recursive, false);
-    }
-
-    public void download(String[] remote, String local) throws IOException {
-        download(remote, local, false, true);
+    public void download(String remote, String local, Option... options) throws IOException {
+        local = checkNotNullAndNotEmpty(local, "Invalid argument local: {}");
+        remote = checkNotNullAndNotEmpty(remote, "Invalid argument remote: {}");
+        download(remote, local, Arrays.asList(options));
     }
 
-    public void download(String[] remote, String local, boolean recursive) throws IOException {
-        download(remote, local, recursive, true);
+    public void download(String[] remote, String local, Option... options) throws IOException {
+        local = checkNotNullAndNotEmpty(local, "Invalid argument local: {}");
+        remote = checkNotNullAndNotEmpty(remote, "Invalid argument remote: {}");
+        List<Option> opts = options(options);
+        if (remote.length > 1) {
+            opts.add(Option.TargetIsDirectory);
+        }
+        for (String r : remote) {
+            download(r, local, opts);
+        }
     }
 
-    private void download(String[] remote, String local, boolean recursive, boolean shouldBeDir) throws IOException {
+    protected void download(String remote, String local, Collection<Option> options) throws IOException {
         local = checkNotNullAndNotEmpty(local, "Invalid argument local: {}");
         remote = checkNotNullAndNotEmpty(remote, "Invalid argument remote: {}");
+
         StringBuilder sb = new StringBuilder("scp");
-        if (recursive) {
+        if (options.contains(Option.Recursive)) {
             sb.append(" -r");
         }
-        sb.append(" -f");
-        for (String r : remote) {
-            r = checkNotNullAndNotEmpty(r, "Invalid argument remote: {}");
-            sb.append(" ").append(r);
+        if (options.contains(Option.PreserveAttributes)) {
+            sb.append(" -p");
         }
+        sb.append(" -f");
+        sb.append(" --");
+        sb.append(" ");
+        sb.append(remote);
 
         FileSystemFactory factory = clientSession.getFactoryManager().getFileSystemFactory();
         FileSystemView fs = factory.createFileSystemView(clientSession);
         SshFile target = fs.getFile(local);
-        if (shouldBeDir) {
+        if (options.contains(Option.TargetIsDirectory)) {
             if (!target.doesExist()) {
                 throw new SshException("Target directory " + target.toString() + " does not exists");
             }
@@ -92,42 +101,47 @@ public class DefaultScpClient implements ScpClient {
 
         ScpHelper helper = new ScpHelper(channel.getInvertedOut(), channel.getInvertedIn(), fs);
 
-        helper.receive(target, recursive, shouldBeDir);
+        helper.receive(target,
+                       options.contains(Option.Recursive),
+                       options.contains(Option.TargetIsDirectory),
+                       options.contains(Option.PreserveAttributes));
 
         channel.close(false);
     }
 
-    public void upload(String remote, String local) throws IOException {
-        upload(new String[] { remote }, local, false, false);
-    }
-
-    public void upload(String remote, String local, boolean recursive) throws IOException {
-        upload(new String[] { remote }, local, recursive, false);
-    }
-
-    public void upload(String[] local, String remote) throws IOException {
-        upload(local, remote, false, true);
+    public void upload(String local, String remote, Option... options) throws IOException {
+        local = checkNotNullAndNotEmpty(local, "Invalid argument local: {}");
+        remote = checkNotNullAndNotEmpty(remote, "Invalid argument remote: {}");
+        upload(new String[] { local }, remote, options(options));
     }
 
-    public void upload(String[] local, String remote, boolean recursive) throws IOException {
-        upload(local, remote, false, true);
+    public void upload(String[] local, String remote, Option... options) throws IOException {
+        local = checkNotNullAndNotEmpty(local, "Invalid argument local: {}");
+        remote = checkNotNullAndNotEmpty(remote, "Invalid argument remote: {}");
+        List<Option> opts = options(options);
+        if (local.length > 1) {
+            opts.add(Option.TargetIsDirectory);
+        }
+        upload(local, remote, opts);
     }
 
-    private void upload(String[] local, String remote, boolean recursive, boolean shouldBeDir) throws IOException {
+    protected void upload(String[] local, String remote, Collection<Option> options) throws IOException {
         local = checkNotNullAndNotEmpty(local, "Invalid argument local: {}");
         remote = checkNotNullAndNotEmpty(remote, "Invalid argument remote: {}");
         StringBuilder sb = new StringBuilder("scp");
-        if (recursive) {
+        if (options.contains(Option.Recursive)) {
             sb.append(" -r");
         }
-        if (shouldBeDir) {
+        if (options.contains(Option.TargetIsDirectory)) {
             sb.append(" -d");
         }
-        sb.append(" -t");
-        for (String r : local) {
-            r = checkNotNullAndNotEmpty(r, "Invalid argument remote: {}");
-            sb.append(" ").append(r);
+        if (options.contains(Option.PreserveAttributes)) {
+            sb.append(" -p");
         }
+        sb.append(" -t");
+        sb.append(" --");
+        sb.append(" ");
+        sb.append(remote);
         ChannelExec channel = clientSession.createExecChannel(sb.toString());
         try {
             channel.open().await();
@@ -138,13 +152,22 @@ public class DefaultScpClient implements ScpClient {
         FileSystemFactory factory = clientSession.getFactoryManager().getFileSystemFactory();
         FileSystemView fs = factory.createFileSystemView(clientSession);
         ScpHelper helper = new ScpHelper(channel.getInvertedOut(), channel.getInvertedIn(), fs);
-        SshFile target = fs.getFile(remote);
 
-        helper.send(Arrays.asList(local), recursive);
+        helper.send(Arrays.asList(local),
+                    options.contains(Option.Recursive),
+                    options.contains(Option.PreserveAttributes));
 
         channel.close(false);
     }
 
+    private List<Option> options(Option... options) {
+        List<Option> opts = new ArrayList<Option>();
+        if (options != null) {
+            opts.addAll(Arrays.asList(options));
+        }
+        return opts;
+    }
+
     private <T> T checkNotNull(T t, String message) {
         if (t == null) {
             throw new IllegalStateException(String.format(message, t));

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/725bcd9f/sshd-core/src/main/java/org/apache/sshd/common/channel/ChannelPipedInputStream.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/channel/ChannelPipedInputStream.java b/sshd-core/src/main/java/org/apache/sshd/common/channel/ChannelPipedInputStream.java
index e68d074..1d7b489 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/channel/ChannelPipedInputStream.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/channel/ChannelPipedInputStream.java
@@ -96,7 +96,7 @@ public class ChannelPipedInputStream extends InputStream {
         lock.lock();
         try {
             for (;;) {
-                if (closed) {
+                if (closed && !writerClosed) {
                     throw new IOException("Pipe closed");
                 }
                 if (buffer.available() > 0) {

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/725bcd9f/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpHelper.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpHelper.java b/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpHelper.java
index 17b55c1..b9198f3 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpHelper.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpHelper.java
@@ -23,7 +23,10 @@ import java.io.EOFException;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.util.EnumSet;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 import org.apache.sshd.common.SshException;
 import org.apache.sshd.common.file.FileSystemView;
@@ -43,6 +46,16 @@ public class ScpHelper {
     public static final int WARNING = 1;
     public static final int ERROR = 2;
 
+    public static final int S_IRUSR =  0000400;
+    public static final int S_IWUSR =  0000200;
+    public static final int S_IXUSR =  0000100;
+    public static final int S_IRGRP =  0000040;
+    public static final int S_IWGRP =  0000020;
+    public static final int S_IXGRP =  0000010;
+    public static final int S_IROTH =  0000004;
+    public static final int S_IWOTH =  0000002;
+    public static final int S_IXOTH =  0000001;
+
     protected final FileSystemView root;
     protected final InputStream in;
     protected final OutputStream out;
@@ -53,7 +66,7 @@ public class ScpHelper {
         this.root = root;
     }
 
-    public void receive(SshFile path, boolean recursive, boolean shouldBeDir) throws IOException {
+    public void receive(SshFile path, boolean recursive, boolean shouldBeDir, boolean preserve) throws IOException {
         if (shouldBeDir) {
             if (!path.doesExist()) {
                 throw new SshException("Target directory " + path.toString() + " does not exists");
@@ -63,6 +76,7 @@ public class ScpHelper {
             }
         }
         ack();
+        long[] time = null;
         for (;;)
         {
             String line;
@@ -76,13 +90,17 @@ public class ScpHelper {
                     isDir = true;
                 case 'C':
                     line = ((char) c) + readLine();
+                    log.debug("Received header: " + line);
                     break;
                 case 'T':
-                    readLine();
+                    line = ((char) c) + readLine();
+                    log.debug("Received header: " + line);
+                    time = parseTime(line);
                     ack();
                     continue;
                 case 'E':
-                    readLine();
+                    line = ((char) c) + readLine();
+                    log.debug("Received header: " + line);
                     return;
                 default:
                     //a real ack that has been acted upon already
@@ -91,19 +109,21 @@ public class ScpHelper {
 
             if (recursive && isDir)
             {
-                receiveDir(line, path);
+                receiveDir(line, path, time, preserve);
+                time = null;
             }
             else
             {
-                receiveFile(line, path);
+                receiveFile(line, path, time, preserve);
+                time = null;
             }
         }
     }
 
 
-    public void receiveDir(String header, SshFile path) throws IOException {
+    public void receiveDir(String header, SshFile path, long[] time, boolean preserve) throws IOException {
         if (log.isDebugEnabled()) {
-            log.debug("Writing dir {}", path);
+            log.debug("Receiving directory {}", path);
         }
         if (!header.startsWith("D")) {
             throw new IOException("Expected a D message but got '" + header + "'");
@@ -128,20 +148,34 @@ public class ScpHelper {
             throw new IOException("Could not create directory " + file);
         }
 
+        if (preserve) {
+            Map<SshFile.Attribute, Object> attrs = new HashMap<SshFile.Attribute, Object>();
+            attrs.put(SshFile.Attribute.Permissions, fromOctalPerms(perms));
+            if (time != null) {
+                attrs.put(SshFile.Attribute.LastModifiedTime, time[0]);
+                attrs.put(SshFile.Attribute.LastAccessTime, time[1]);
+            }
+            file.setAttributes(attrs);
+        }
+
         ack();
 
+        time = null;
         for (;;) {
             header = readLine();
+            log.debug("Received header: " + header);
             if (header.startsWith("C")) {
-                receiveFile(header, file);
+                receiveFile(header, file, time, preserve);
+                time = null;
             } else if (header.startsWith("D")) {
-                receiveDir(header, file);
+                receiveDir(header, file, time, preserve);
+                time = null;
             } else if (header.equals("E")) {
                 ack();
                 break;
-            } else if (header.equals("T")) {
+            } else if (header.startsWith("T")) {
+                time = parseTime(header);
                 ack();
-                break;
             } else {
                 throw new IOException("Unexpected message: '" + header + "'");
             }
@@ -149,9 +183,9 @@ public class ScpHelper {
 
     }
 
-    public void receiveFile(String header, SshFile path) throws IOException {
+    public void receiveFile(String header, SshFile path, long[] time, boolean preserve) throws IOException {
         if (log.isDebugEnabled()) {
-            log.debug("Writing file {}", path);
+            log.debug("Receiving file {}", path);
         }
         if (!header.startsWith("C")) {
             throw new IOException("Expected a C message but got '" + header + "'");
@@ -194,6 +228,16 @@ public class ScpHelper {
             os.close();
         }
 
+        if (preserve) {
+            Map<SshFile.Attribute, Object> attrs = new HashMap<SshFile.Attribute, Object>();
+            attrs.put(SshFile.Attribute.Permissions, fromOctalPerms(perms));
+            if (time != null) {
+                attrs.put(SshFile.Attribute.LastModifiedTime, time[0]);
+                attrs.put(SshFile.Attribute.LastAccessTime, time[1]);
+            }
+            file.setAttributes(attrs);
+        }
+
         ack();
         readAck(false);
     }
@@ -219,7 +263,7 @@ public class ScpHelper {
         }
     }
 
-    public void send(List<String> paths, boolean recursive) throws IOException {
+    public void send(List<String> paths, boolean recursive, boolean preserve) throws IOException {
         for (String pattern : paths) {
             int idx = pattern.indexOf('*');
             if (idx >= 0) {
@@ -233,13 +277,13 @@ public class ScpHelper {
                 for (String path : included) {
                     SshFile file = root.getFile(basedir + "/" + path);
                     if (file.isFile()) {
-                        sendFile(file);
+                        sendFile(file, preserve);
                     } else if (file.isDirectory()) {
                         if (!recursive) {
                             out.write(ScpHelper.WARNING);
                             out.write((path + " not a regular file\n").getBytes());
                         } else {
-                            sendDir(file);
+                            sendDir(file, preserve);
                         }
                     } else {
                         out.write(ScpHelper.WARNING);
@@ -258,12 +302,12 @@ public class ScpHelper {
                     throw new IOException(file + ": no such file or directory");
                 }
                 if (file.isFile()) {
-                    sendFile(file);
+                    sendFile(file, preserve);
                 } else if (file.isDirectory()) {
                     if (!recursive) {
                         throw new IOException(file + " not a regular file");
                     } else {
-                        sendDir(file);
+                        sendDir(file, preserve);
                     }
                 } else {
                     throw new IOException(file + ": unknown file type");
@@ -272,15 +316,33 @@ public class ScpHelper {
         }
     }
 
-    public void sendFile(SshFile path) throws IOException {
+    public void sendFile(SshFile path, boolean preserve) throws IOException {
         if (log.isDebugEnabled()) {
-            log.debug("Reading file {}", path);
+            log.debug("Sending file {}", path);
+        }
+
+        Map<SshFile.Attribute,Object> attrs =  path.getAttributes(true);
+        if (preserve) {
+            StringBuffer buf = new StringBuffer();
+            buf.append("T");
+            buf.append(attrs.get(SshFile.Attribute.LastModifiedTime));
+            buf.append(" ");
+            buf.append("0");
+            buf.append(" ");
+            buf.append(attrs.get(SshFile.Attribute.LastAccessTime));
+            buf.append(" ");
+            buf.append("0");
+            buf.append("\n");
+            out.write(buf.toString().getBytes());
+            out.flush();
+            readAck(false);
         }
+
         StringBuffer buf = new StringBuffer();
         buf.append("C");
-        buf.append("0644"); // TODO: what about perms
+        buf.append(preserve ? toOctalPerms((EnumSet<SshFile.Permission>) attrs.get(SshFile.Attribute.Permissions)) : "0644");
         buf.append(" ");
-        buf.append(path.getSize()); // length
+        buf.append(attrs.get(SshFile.Attribute.Size)); // length
         buf.append(" ");
         buf.append(path.getName());
         buf.append("\n");
@@ -305,13 +367,30 @@ public class ScpHelper {
         readAck(false);
     }
 
-    public void sendDir(SshFile path) throws IOException {
+    public void sendDir(SshFile path, boolean preserve) throws IOException {
         if (log.isDebugEnabled()) {
-            log.debug("Reading directory {}", path);
+            log.debug("Sending directory {}", path);
+        }
+        Map<SshFile.Attribute,Object> attrs =  path.getAttributes(true);
+        if (preserve) {
+            StringBuffer buf = new StringBuffer();
+            buf.append("T");
+            buf.append(attrs.get(SshFile.Attribute.LastModifiedTime));
+            buf.append(" ");
+            buf.append("0");
+            buf.append(" ");
+            buf.append(attrs.get(SshFile.Attribute.LastAccessTime));
+            buf.append(" ");
+            buf.append("0");
+            buf.append("\n");
+            out.write(buf.toString().getBytes());
+            out.flush();
+            readAck(false);
         }
+
         StringBuffer buf = new StringBuffer();
         buf.append("D");
-        buf.append("0755"); // what about perms
+        buf.append(preserve ? toOctalPerms((EnumSet<SshFile.Permission>) attrs.get(SshFile.Attribute.Permissions)) : "0755");
         buf.append(" ");
         buf.append("0"); // length
         buf.append(" ");
@@ -323,9 +402,9 @@ public class ScpHelper {
 
         for (SshFile child : path.listSshFiles()) {
             if (child.isFile()) {
-                sendFile(child);
+                sendFile(child, preserve);
             } else if (child.isDirectory()) {
-                sendDir(child);
+                sendDir(child, preserve);
             }
         }
 
@@ -334,6 +413,62 @@ public class ScpHelper {
         readAck(false);
     }
 
+    private long[] parseTime(String line) {
+        String[] numbers = line.substring(1).split(" ");
+        return new long[] { Long.parseLong(numbers[0]), Long.parseLong(numbers[2]) };
+    }
+
+    public static String toOctalPerms(EnumSet<SshFile.Permission> perms) {
+        int pf = 0;
+        for (SshFile.Permission p : perms) {
+            switch (p) {
+                case UserRead:      pf |= S_IRUSR; break;
+                case UserWrite:     pf |= S_IWUSR; break;
+                case UserExecute:   pf |= S_IXUSR; break;
+                case GroupRead:     pf |= S_IRGRP; break;
+                case GroupWrite:    pf |= S_IWGRP; break;
+                case GroupExecute:  pf |= S_IXGRP; break;
+                case OthersRead:    pf |= S_IROTH; break;
+                case OthersWrite:   pf |= S_IWOTH; break;
+                case OthersExecute: pf |= S_IXOTH; break;
+            }
+        }
+        return String.format("%04o", pf);
+    }
+
+    public static EnumSet<SshFile.Permission> fromOctalPerms(String str) {
+        int perms = Integer.parseInt(str, 8);
+        EnumSet<SshFile.Permission> p = EnumSet.noneOf(SshFile.Permission.class);
+        if ((perms & S_IRUSR) != 0) {
+            p.add(SshFile.Permission.UserRead);
+        }
+        if ((perms & S_IWUSR) != 0) {
+            p.add(SshFile.Permission.UserWrite);
+        }
+        if ((perms & S_IXUSR) != 0) {
+            p.add(SshFile.Permission.UserExecute);
+        }
+        if ((perms & S_IRGRP) != 0) {
+            p.add(SshFile.Permission.GroupRead);
+        }
+        if ((perms & S_IWGRP) != 0) {
+            p.add(SshFile.Permission.GroupWrite);
+        }
+        if ((perms & S_IXGRP) != 0) {
+            p.add(SshFile.Permission.GroupExecute);
+        }
+        if ((perms & S_IROTH) != 0) {
+            p.add(SshFile.Permission.OthersRead);
+        }
+        if ((perms & S_IWOTH) != 0) {
+            p.add(SshFile.Permission.OthersWrite);
+        }
+        if ((perms & S_IXOTH) != 0) {
+            p.add(SshFile.Permission.OthersExecute);
+        }
+        return p;
+    }
+
     public void ack() throws IOException {
         out.write(0);
         out.flush();

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/725bcd9f/sshd-core/src/main/java/org/apache/sshd/server/command/ScpCommand.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/command/ScpCommand.java b/sshd-core/src/main/java/org/apache/sshd/server/command/ScpCommand.java
index 8ecd7cd..d67715c 100644
--- a/sshd-core/src/main/java/org/apache/sshd/server/command/ScpCommand.java
+++ b/sshd-core/src/main/java/org/apache/sshd/server/command/ScpCommand.java
@@ -23,6 +23,7 @@ import java.io.InputStream;
 import java.io.OutputStream;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.List;
 
 import org.apache.sshd.common.file.FileSystemAware;
@@ -52,19 +53,17 @@ public class ScpCommand implements Command, Runnable, FileSystemAware {
     protected boolean optD;
     protected boolean optP; // TODO: handle modification times
     protected FileSystemView root;
-    protected List<String> paths;
+    protected String path;
     protected InputStream in;
     protected OutputStream out;
     protected OutputStream err;
     protected ExitCallback callback;
     protected IOException error;
 
-    public ScpCommand(String[] args) {
-        name = Arrays.asList(args).toString();
-        if (log.isDebugEnabled()) {
-            log.debug("Executing command {}", name);
-        }
-        paths = new ArrayList<String>();
+    public ScpCommand(String command) {
+        this.name = command;
+        log.debug("Executing command {}", command);
+        String[] args = command.split(" ");
         for (int i = 1; i < args.length; i++) {
             if (args[i].charAt(0) == '-') {
                 for (int j = 1; j < args[i].length(); j++) {
@@ -89,16 +88,14 @@ public class ScpCommand implements Command, Runnable, FileSystemAware {
 //                            return;
                     }
                 }
-            } else if (i == args.length - 1) {
-                paths.add(args[args.length - 1]);
+            } else {
+                path = command.substring(command.indexOf(args[i-1]) + args[i-1].length() + 1);
+                break;
             }
         }
         if (!optF && !optT) {
             error = new IOException("Either -f or -t option should be set");
         }
-        if (optT && paths.size() != 1) {
-            error = new IOException("One and only one path must be given with -t option");
-        }
     }
 
     public void setInputStream(InputStream in) {
@@ -137,9 +134,9 @@ public class ScpCommand implements Command, Runnable, FileSystemAware {
         ScpHelper helper = new ScpHelper(in, out, root);
         try {
             if (optT) {
-                helper.receive(root.getFile(paths.get(0)), optR, optD);
+                helper.receive(root.getFile(path), optR, optD, optP);
             } else if (optF) {
-                helper.send(paths, optR);
+                helper.send(Collections.singletonList(path), optR, optP);
             } else {
                 throw new IOException("Unsupported mode");
             }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/725bcd9f/sshd-core/src/main/java/org/apache/sshd/server/command/ScpCommandFactory.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/command/ScpCommandFactory.java b/sshd-core/src/main/java/org/apache/sshd/server/command/ScpCommandFactory.java
index 61bded0..51eee6c 100644
--- a/sshd-core/src/main/java/org/apache/sshd/server/command/ScpCommandFactory.java
+++ b/sshd-core/src/main/java/org/apache/sshd/server/command/ScpCommandFactory.java
@@ -55,7 +55,10 @@ public class ScpCommandFactory implements CommandFactory {
      */
     public Command createCommand(String command) {
         try {
-            return new ScpCommand(splitCommandString(command));
+            if (!command.startsWith("scp")) {
+                throw new IllegalArgumentException("Unknown command, does not begin with 'scp'");
+            }
+            return new ScpCommand(command);
         } catch (IllegalArgumentException iae) {
             if (delegate != null) {
                 return delegate.createCommand(command);
@@ -64,31 +67,4 @@ public class ScpCommandFactory implements CommandFactory {
         }
     }
 
-    private String[] splitCommandString(String command) {
-        if (!command.trim().startsWith("scp")) {
-            throw new IllegalArgumentException("Unknown command, does not begin with 'scp'");
-        }
-
-        String[] args = command.split(" ");
-        List<String> parts = new ArrayList<String>();
-        parts.add(args[0]);
-        for (int i = 1; i < args.length; i++) {
-            if (!args[i].trim().startsWith("-")) {
-                parts.add(concatenateWithSpace(args, i));
-                break;
-            } else {
-                parts.add(args[i]);
-            }
-        }
-        return parts.toArray(new String[parts.size()]);
-    }
-
-    private String concatenateWithSpace(String[] args, int from) {
-        StringBuilder sb = new StringBuilder();
-
-        for (int i = from; i < args.length; i++) {
-            sb.append(args[i] + " ");
-        }
-        return sb.toString().trim();
-    }
 }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/725bcd9f/sshd-core/src/test/java/org/apache/sshd/ScpTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/ScpTest.java b/sshd-core/src/test/java/org/apache/sshd/ScpTest.java
index 835b931..19572b6 100644
--- a/sshd-core/src/test/java/org/apache/sshd/ScpTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/ScpTest.java
@@ -20,11 +20,13 @@ package org.apache.sshd;
 
 import java.io.ByteArrayOutputStream;
 import java.io.File;
+import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.net.ServerSocket;
 import java.util.Properties;
+import java.util.concurrent.TimeUnit;
 
 import ch.ethz.ssh2.Connection;
 import ch.ethz.ssh2.SCPClient;
@@ -33,6 +35,7 @@ import com.jcraft.jsch.JSch;
 import com.jcraft.jsch.JSchException;
 import com.jcraft.jsch.Logger;
 import com.jcraft.jsch.UserInfo;
+import org.apache.sshd.client.ScpClient;
 import org.apache.sshd.server.command.ScpCommandFactory;
 import org.apache.sshd.util.BogusPasswordAuthenticator;
 import org.apache.sshd.util.EchoShellFactory;
@@ -45,6 +48,7 @@ import org.junit.Test;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
 /**
  * Test for SCP support.
@@ -62,6 +66,7 @@ public class ScpTest {
         ServerSocket s = new ServerSocket(0);
         port = s.getLocalPort();
         s.close();
+//        port = 8102;
 
         sshd = SshServer.setUpDefaultServer();
         sshd.setPort(port);
@@ -123,6 +128,264 @@ public class ScpTest {
     }
 
     @Test
+    public void testScpNativeOnSingleFile() throws Exception {
+        SshClient client = SshClient.setUpDefaultClient();
+        client.start();
+        ClientSession session = client.connect("localhost", port).await().getSession();
+        session.authPassword("test", "test").await();
+
+        ScpClient scp = session.createScpClient();
+
+        String data = "0123456789\n";
+
+        File root = new File("target/scp");
+        Utils.deleteRecursive(root);
+        root.mkdirs();
+        new File(root, "local").mkdirs();
+        assertTrue(root.exists());
+
+
+        writeFile(new File("target/scp/local/out.txt"), data);
+        try {
+            scp.upload("target/scp/local/out.txt", "target/scp/remote/out.txt");
+            fail("Expected IOException");
+        } catch (IOException e) {
+            // ok
+        }
+        new File(root, "remote").mkdirs();
+        scp.upload("target/scp/local/out.txt", "target/scp/remote/out.txt");
+        assertFileLength(new File("target/scp/remote/out.txt"), data.length(), 5000);
+
+        scp.download("target/scp/remote/out.txt", "target/scp/local/out2.txt");
+        assertFileLength(new File("target/scp/local/out2.txt"), data.length(), 5000);
+
+        session.close(false);
+        client.stop();
+    }
+
+    @Test
+    public void testScpNativeOnMultipleFiles() throws Exception {
+        SshClient client = SshClient.setUpDefaultClient();
+        client.start();
+        ClientSession session = client.connect("localhost", port).await().getSession();
+        session.authPassword("test", "test").await();
+
+        ScpClient scp = session.createScpClient();
+
+        String data = "0123456789\n";
+
+        File root = new File("target/scp");
+        Utils.deleteRecursive(root);
+        root.mkdirs();
+        new File(root, "local").mkdirs();
+        new File(root, "remote").mkdirs();
+        assertTrue(root.exists());
+
+
+        writeFile(new File("target/scp/local/out1.txt"), data);
+        writeFile(new File("target/scp/local/out2.txt"), data);
+        try {
+            scp.upload(new String[] { "target/scp/local/out1.txt", "target/scp/local/out2.txt" }, "target/scp/remote/out.txt");
+            fail("Expected IOException");
+        } catch (IOException e) {
+            // Ok
+        }
+        writeFile(new File("target/scp/remote/out.txt"), data);
+        try {
+            scp.upload(new String[] { "target/scp/local/out1.txt", "target/scp/local/out2.txt" }, "target/scp/remote/out.txt");
+            fail("Expected IOException");
+        } catch (IOException e) {
+            // Ok
+        }
+        new File(root, "remote/dir").mkdirs();
+        scp.upload(new String[] { "target/scp/local/out1.txt", "target/scp/local/out2.txt" }, "target/scp/remote/dir");
+        assertFileLength(new File("target/scp/remote/dir/out1.txt"), data.length(), 5000);
+        assertFileLength(new File("target/scp/remote/dir/out2.txt"), data.length(), 5000);
+
+        try {
+            scp.download(new String[] { "target/scp/remote/dir/out1.txt", "target/scp/remote/dir/out2.txt" }, "target/scp/local/out1.txt");
+            fail("Expected IOException");
+        } catch (IOException e) {
+            // Ok
+        }
+        try {
+            scp.download(new String[] { "target/scp/remote/dir/out1.txt", "target/scp/remote/dir/out2.txt" }, "target/scp/local/dir");
+            fail("Expected IOException");
+        } catch (IOException e) {
+            // Ok
+        }
+        new File(root, "local/dir").mkdirs();
+        scp.download(new String[] { "target/scp/remote/dir/out1.txt", "target/scp/remote/dir/out2.txt" }, "target/scp/local/dir");
+        assertFileLength(new File("target/scp/local/dir/out1.txt"), data.length(), 5000);
+        assertFileLength(new File("target/scp/local/dir/out2.txt"), data.length(), 5000);
+
+        session.close(false);
+        client.stop();
+    }
+
+    @Test
+    public void testScpNativeOnRecursiveDirs() throws Exception {
+        SshClient client = SshClient.setUpDefaultClient();
+        client.start();
+        ClientSession session = client.connect("localhost", port).await().getSession();
+        session.authPassword("test", "test").await();
+
+        ScpClient scp = session.createScpClient();
+
+        String data = "0123456789\n";
+
+        File root = new File("target/scp");
+        Utils.deleteRecursive(root);
+        root.mkdirs();
+        new File(root, "local").mkdirs();
+        new File(root, "remote").mkdirs();
+        assertTrue(root.exists());
+
+        new File("target/scp/local/dir").mkdirs();
+        writeFile(new File("target/scp/local/dir/out1.txt"), data);
+        writeFile(new File("target/scp/local/dir/out2.txt"), data);
+        scp.upload("target/scp/local/dir", "target/scp/remote/", ScpClient.Option.Recursive);
+        assertFileLength(new File("target/scp/remote/dir/out1.txt"), data.length(), 5000);
+        assertFileLength(new File("target/scp/remote/dir/out2.txt"), data.length(), 5000);
+
+        Utils.deleteRecursive(new File("target/scp/local/dir"));
+        scp.download("target/scp/remote/dir", "target/scp/local", ScpClient.Option.Recursive);
+        assertFileLength(new File("target/scp/local/dir/out1.txt"), data.length(), 5000);
+        assertFileLength(new File("target/scp/local/dir/out2.txt"), data.length(), 5000);
+
+        session.close(false);
+        client.stop();
+    }
+
+    @Test
+    public void testScpNativeOnDirWithPattern() throws Exception {
+        SshClient client = SshClient.setUpDefaultClient();
+        client.start();
+        ClientSession session = client.connect("localhost", port).await().getSession();
+        session.authPassword("test", "test").await();
+
+        ScpClient scp = session.createScpClient();
+
+        String data = "0123456789\n";
+
+        File root = new File("target/scp");
+        Utils.deleteRecursive(root);
+        root.mkdirs();
+        new File(root, "local").mkdirs();
+        new File(root, "remote").mkdirs();
+        assertTrue(root.exists());
+
+        writeFile(new File("target/scp/local/out1.txt"), data);
+        writeFile(new File("target/scp/local/out2.txt"), data);
+        scp.upload("target/scp/local/*", "target/scp/remote/");
+        assertFileLength(new File("target/scp/remote/out1.txt"), data.length(), 5000);
+        assertFileLength(new File("target/scp/remote/out2.txt"), data.length(), 5000);
+
+        new File("target/scp/local/out1.txt").delete();
+        new File("target/scp/local/out2.txt").delete();
+        scp.download("target/scp/remote/*", "target/scp/local");
+        assertFileLength(new File("target/scp/local/out1.txt"), data.length(), 5000);
+        assertFileLength(new File("target/scp/local/out2.txt"), data.length(), 5000);
+
+        session.close(false);
+        client.stop();
+    }
+
+    @Test
+    public void testScpNativeOnMixedDirAndFiles() throws Exception {
+        SshClient client = SshClient.setUpDefaultClient();
+        client.start();
+        ClientSession session = client.connect("localhost", port).await().getSession();
+        session.authPassword("test", "test").await();
+
+        ScpClient scp = session.createScpClient();
+
+        String data = "0123456789\n";
+
+        File root = new File("target/scp");
+        Utils.deleteRecursive(root);
+        root.mkdirs();
+        new File(root, "local").mkdirs();
+        new File(root, "remote").mkdirs();
+        assertTrue(root.exists());
+
+        new File("target/scp/local/dir").mkdirs();
+        writeFile(new File("target/scp/local/out1.txt"), data);
+        writeFile(new File("target/scp/local/dir/out2.txt"), data);
+        scp.upload("target/scp/local/*", "target/scp/remote/", ScpClient.Option.Recursive);
+        assertFileLength(new File("target/scp/remote/out1.txt"), data.length(), 5000);
+        assertFileLength(new File("target/scp/remote/dir/out2.txt"), data.length(), 5000);
+
+        Utils.deleteRecursive(new File("target/scp/local/out1.txt"));
+        Utils.deleteRecursive(new File("target/scp/local/dir"));
+        scp.download("target/scp/remote/*", "target/scp/local");
+        assertFileLength(new File("target/scp/local/out1.txt"), data.length(), 5000);
+        assertFalse(new File("target/scp/local/dir/out2.txt").exists());
+
+        Utils.deleteRecursive(new File("target/scp/local/out1.txt"));
+        scp.download("target/scp/remote/*", "target/scp/local", ScpClient.Option.Recursive);
+        assertFileLength(new File("target/scp/local/out1.txt"), data.length(), 5000);
+        assertFileLength(new File("target/scp/local/dir/out2.txt"), data.length(), 5000);
+
+        session.close(false);
+        client.stop();
+    }
+
+    @Test
+    public void testScpNativePreserveAttributes() throws Exception {
+        SshClient client = SshClient.setUpDefaultClient();
+        client.start();
+        ClientSession session = client.connect("localhost", port).await().getSession();
+        session.authPassword("test", "test").await();
+
+        ScpClient scp = session.createScpClient();
+
+        String data = "0123456789\n";
+
+        File root = new File("target/scp");
+        Utils.deleteRecursive(root);
+        root.mkdirs();
+        new File(root, "local").mkdirs();
+        new File(root, "remote").mkdirs();
+        assertTrue(root.exists());
+
+        new File("target/scp/local/dir").mkdirs();
+        long lastMod = new File("target/scp/local/dir").lastModified() - TimeUnit.DAYS.toMillis(1);
+
+        writeFile(new File("target/scp/local/out1.txt"), data);
+        writeFile(new File("target/scp/local/dir/out2.txt"), data);
+        new File("target/scp/local/out1.txt").setLastModified(lastMod);
+        new File("target/scp/local/out1.txt").setExecutable(true, true);
+        new File("target/scp/local/out1.txt").setWritable(false, false);
+        new File("target/scp/local/dir/out2.txt").setLastModified(lastMod);
+        scp.upload("target/scp/local/*", "target/scp/remote/", ScpClient.Option.Recursive, ScpClient.Option.PreserveAttributes);
+        assertFileLength(new File("target/scp/remote/out1.txt"), data.length(), 5000);
+        assertEquals(lastMod, new File("target/scp/remote/out1.txt").lastModified());
+        assertFileLength(new File("target/scp/remote/dir/out2.txt"), data.length(), 5000);
+        assertEquals(lastMod, new File("target/scp/remote/dir/out2.txt").lastModified());
+
+        Utils.deleteRecursive(new File("target/scp/local"));
+        new File("target/scp/local").mkdirs();
+        scp.download("target/scp/remote/*", "target/scp/local", ScpClient.Option.Recursive, ScpClient.Option.PreserveAttributes);
+        assertFileLength(new File("target/scp/local/out1.txt"), data.length(), 5000);
+        assertEquals(lastMod, new File("target/scp/local/out1.txt").lastModified());
+        assertFileLength(new File("target/scp/local/dir/out2.txt"), data.length(), 5000);
+        assertEquals(lastMod, new File("target/scp/local/dir/out2.txt").lastModified());
+
+        session.close(false);
+        client.stop();
+    }
+
+    private void writeFile(File file, String data) throws IOException {
+        FileOutputStream fos = new FileOutputStream(file);
+        try {
+            fos.write(data.getBytes());
+        } finally {
+            fos.close();
+        }
+    }
+
+    @Test
     public void testScp() throws Exception {
         session = getJschSession();
 


[3/3] git commit: [SSHD-246] Let commands finish stream consumption and cleanly exit

Posted by gn...@apache.org.
[SSHD-246] Let commands finish stream consumption and cleanly exit

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

Branch: refs/heads/master
Commit: f549a71bc1559df036b2149db0f2a405595b329b
Parents: 725bcd9
Author: Guillaume Nodet <gn...@apache.org>
Authored: Fri Jul 26 10:37:12 2013 +0200
Committer: Guillaume Nodet <gn...@apache.org>
Committed: Fri Jul 26 10:37:12 2013 +0200

----------------------------------------------------------------------
 .../sshd/agent/unix/AgentForwardedChannel.java  |  4 +-
 .../client/channel/AbstractClientChannel.java   |  8 +-
 .../sshd/client/channel/ChannelSession.java     |  5 +-
 .../sshd/common/channel/AbstractChannel.java    | 77 ++++++++++++--------
 .../common/channel/ChannelPipedInputStream.java |  4 +-
 .../sshd/common/forward/TcpipClientChannel.java | 14 +++-
 .../sshd/server/ServerFactoryManager.java       |  7 ++
 .../server/channel/AbstractServerChannel.java   |  1 +
 .../sshd/server/channel/ChannelSession.java     | 45 +++++++++++-
 .../sshd/server/x11/X11ForwardSupport.java      | 14 +++-
 10 files changed, 131 insertions(+), 48 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f549a71b/sshd-core/src/main/java/org/apache/sshd/agent/unix/AgentForwardedChannel.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/agent/unix/AgentForwardedChannel.java b/sshd-core/src/main/java/org/apache/sshd/agent/unix/AgentForwardedChannel.java
index 3207074..4f23584 100644
--- a/sshd-core/src/main/java/org/apache/sshd/agent/unix/AgentForwardedChannel.java
+++ b/sshd-core/src/main/java/org/apache/sshd/agent/unix/AgentForwardedChannel.java
@@ -66,9 +66,9 @@ public class AgentForwardedChannel extends AbstractClientChannel implements Runn
     }
 
     @Override
-    protected synchronized void doClose() {
+    protected synchronized void postClose() {
         Socket.close(socket);
-        super.doClose();
+        super.postClose();
     }
 
     protected synchronized void doWriteData(byte[] data, int off, int len) throws IOException {

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f549a71b/sshd-core/src/main/java/org/apache/sshd/client/channel/AbstractClientChannel.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/channel/AbstractClientChannel.java b/sshd-core/src/main/java/org/apache/sshd/client/channel/AbstractClientChannel.java
index b786c39..6a332d4 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/channel/AbstractClientChannel.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/channel/AbstractClientChannel.java
@@ -123,9 +123,9 @@ public abstract class AbstractClientChannel extends AbstractChannel implements C
     }
 
     @Override
-    protected void doClose() {
-        super.doClose();
+    protected void postClose() {
         IoUtils.closeQuietly(invertedIn, invertedOut, invertedErr, in, out, err);
+        super.postClose();
     }
 
     public int waitFor(int mask, long timeout) {
@@ -207,7 +207,7 @@ public abstract class AbstractClientChannel extends AbstractChannel implements C
         } catch (Exception e) {
             this.openFuture.setException(e);
             this.closeFuture.setClosed();
-            this.doClose();
+            this.postClose();
         } finally {
             notifyStateChanged();
         }
@@ -222,7 +222,7 @@ public abstract class AbstractClientChannel extends AbstractChannel implements C
         this.openFailureMsg = msg;
         this.openFuture.setException(new SshException(msg));
         this.closeFuture.setClosed();
-        this.doClose();
+        this.postClose();
         notifyStateChanged();
     }
 

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f549a71b/sshd-core/src/main/java/org/apache/sshd/client/channel/ChannelSession.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/channel/ChannelSession.java b/sshd-core/src/main/java/org/apache/sshd/client/channel/ChannelSession.java
index d6c9124..e18378a 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/channel/ChannelSession.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/channel/ChannelSession.java
@@ -26,6 +26,7 @@ import org.apache.sshd.common.SshConstants;
 import org.apache.sshd.common.channel.ChannelOutputStream;
 import org.apache.sshd.common.channel.ChannelPipedInputStream;
 import org.apache.sshd.common.channel.ChannelPipedOutputStream;
+import org.apache.sshd.common.future.CloseFuture;
 import org.apache.sshd.common.util.Buffer;
 
 /**
@@ -76,12 +77,12 @@ public class ChannelSession extends AbstractClientChannel {
     }
 
     @Override
-    protected void doClose() {
-        super.doClose();
+    protected void postClose() {
         if (streamPumper != null) {
             streamPumper.interrupt();
             streamPumper = null;
         }
+        super.postClose();
     }
 
     protected void pumpInputStream() {

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f549a71b/sshd-core/src/main/java/org/apache/sshd/common/channel/AbstractChannel.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/channel/AbstractChannel.java b/sshd-core/src/main/java/org/apache/sshd/common/channel/AbstractChannel.java
index 71d9fa4..a0caaf8 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/channel/AbstractChannel.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/channel/AbstractChannel.java
@@ -29,6 +29,9 @@ import org.apache.sshd.common.Session;
 import org.apache.sshd.common.SshConstants;
 import org.apache.sshd.common.future.CloseFuture;
 import org.apache.sshd.common.future.DefaultCloseFuture;
+import org.apache.sshd.common.future.DefaultSshFuture;
+import org.apache.sshd.common.future.SshFuture;
+import org.apache.sshd.common.future.SshFutureListener;
 import org.apache.sshd.common.util.Buffer;
 import org.apache.sshd.common.util.BufferUtils;
 import org.slf4j.Logger;
@@ -93,36 +96,43 @@ public abstract class AbstractChannel implements Channel {
     }
 
     public CloseFuture close(boolean immediately) {
-        if (closeFuture.isClosed()) {
-            return closeFuture;
-        }
         if (closing.compareAndSet(false, true)) {
-            try {
-                if (immediately) {
-                    log.debug("Closing channel {} immediately", id);
-                    doClose();
-                    closeFuture.setClosed();
-                    notifyStateChanged();
-                    session.unregisterChannel(this);
-                } else {
-                    log.debug("Closing channel {} gracefully", id);
-                    doClose();
-                    log.debug("Send SSH_MSG_CHANNEL_CLOSE on channel {}", id);
-                    Buffer buffer = session.createBuffer(SshConstants.Message.SSH_MSG_CHANNEL_CLOSE, 0);
-                    buffer.putInt(recipient);
-                    session.writePacket(buffer).addListener(new IoFutureListener<WriteFuture>() {
-                        public void operationComplete(WriteFuture future) {
-                            if (closedByOtherSide) {
-                                log.debug("Message SSH_MSG_CHANNEL_CLOSE written on channel {}", id);
-                                closeFuture.setClosed();
-                                notifyStateChanged();
-                            }
+            if (immediately) {
+                log.debug("Closing channel {} immediately", id);
+                preClose(immediately).addListener(new SshFutureListener<CloseFuture>() {
+                    public void operationComplete(CloseFuture future) {
+                        postClose();
+                        closeFuture.setClosed();
+                        notifyStateChanged();
+                        session.unregisterChannel(AbstractChannel.this);
+                    }
+                });
+            } else {
+                log.debug("Closing channel {} gracefully", id);
+                preClose(immediately).addListener(new SshFutureListener<CloseFuture>() {
+                    public void operationComplete(CloseFuture future) {
+                        log.debug("Send SSH_MSG_CHANNEL_CLOSE on channel {}", id);
+                        Buffer buffer = session.createBuffer(SshConstants.Message.SSH_MSG_CHANNEL_CLOSE, 0);
+                        buffer.putInt(recipient);
+                        try {
+                            session.writePacket(buffer).addListener(new IoFutureListener<WriteFuture>() {
+                                public void operationComplete(WriteFuture future) {
+                                    if (closedByOtherSide) {
+                                        log.debug("Message SSH_MSG_CHANNEL_CLOSE written on channel {}", id);
+                                        postClose();
+                                        closeFuture.setClosed();
+                                        notifyStateChanged();
+                                    }
+                                }
+                            });
+                        } catch (IOException e) {
+                            log.debug("Exception caught while writing SSH_MSG_CHANNEL_CLOSE packet on channel " + id, e);
+                            postClose();
+                            closeFuture.setClosed();
+                            notifyStateChanged();
                         }
-                    });
-                }
-            } catch (IOException e) {
-                session.exceptionCaught(e);
-                closeFuture.setClosed();
+                    }
+                });
             }
         }
         return closeFuture;
@@ -134,12 +144,19 @@ public abstract class AbstractChannel implements Channel {
         if (closedByOtherSide) {
             close(false);
         } else {
-            close(false).setClosed();
+            postClose();
+            closeFuture.setClosed();
             notifyStateChanged();
         }
     }
 
-    protected void doClose() {
+    protected CloseFuture preClose(boolean immediately) {
+        CloseFuture future = new DefaultCloseFuture(lock);
+        future.setClosed();
+        return future;
+    }
+
+    protected void postClose() {
     }
 
     protected void writePacket(Buffer buffer) throws IOException {

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f549a71b/sshd-core/src/main/java/org/apache/sshd/common/channel/ChannelPipedInputStream.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/channel/ChannelPipedInputStream.java b/sshd-core/src/main/java/org/apache/sshd/common/channel/ChannelPipedInputStream.java
index 1d7b489..2257cd9 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/channel/ChannelPipedInputStream.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/channel/ChannelPipedInputStream.java
@@ -40,6 +40,7 @@ public class ChannelPipedInputStream extends InputStream {
     private final Buffer buffer = new Buffer();
     private final byte[] b = new byte[1];
     private boolean closed;
+    private boolean eofSent;
 
     private final Lock lock = new ReentrantLock();
     private final Condition dataAvailable = lock.newCondition();
@@ -96,13 +97,14 @@ public class ChannelPipedInputStream extends InputStream {
         lock.lock();
         try {
             for (;;) {
-                if (closed && !writerClosed) {
+                if (closed && writerClosed && eofSent || closed && !writerClosed) {
                     throw new IOException("Pipe closed");
                 }
                 if (buffer.available() > 0) {
                     break;
                 }
                 if (writerClosed) {
+                    eofSent = true;
                     return -1; // no more data to read
                 }
                 try {

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f549a71b/sshd-core/src/main/java/org/apache/sshd/common/forward/TcpipClientChannel.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/forward/TcpipClientChannel.java b/sshd-core/src/main/java/org/apache/sshd/common/forward/TcpipClientChannel.java
index d0c4de8..ccd1f77 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/forward/TcpipClientChannel.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/forward/TcpipClientChannel.java
@@ -22,6 +22,7 @@ import java.io.IOException;
 import java.net.InetSocketAddress;
 
 import org.apache.mina.core.buffer.IoBuffer;
+import org.apache.mina.core.future.IoFutureListener;
 import org.apache.mina.core.session.IoSession;
 import org.apache.sshd.client.channel.AbstractClientChannel;
 import org.apache.sshd.client.future.DefaultOpenFuture;
@@ -30,6 +31,8 @@ import org.apache.sshd.common.SshConstants;
 import org.apache.sshd.common.SshException;
 import org.apache.sshd.common.SshdSocketAddress;
 import org.apache.sshd.common.channel.ChannelOutputStream;
+import org.apache.sshd.common.future.CloseFuture;
+import org.apache.sshd.common.future.DefaultCloseFuture;
 import org.apache.sshd.common.util.Buffer;
 
 /**
@@ -96,9 +99,14 @@ public class TcpipClientChannel extends AbstractClientChannel {
     }
 
     @Override
-    protected synchronized void doClose() {
-        serverSession.close(false);
-        super.doClose();
+    protected synchronized CloseFuture preClose(boolean immediately) {
+        final CloseFuture future = new DefaultCloseFuture(null);
+        serverSession.close(immediately).addListener(new IoFutureListener<org.apache.mina.core.future.CloseFuture>() {
+            public void operationComplete(org.apache.mina.core.future.CloseFuture f) {
+                future.setClosed();
+            }
+        });
+        return future;
     }
 
     protected synchronized void doWriteData(byte[] data, int off, int len) throws IOException {

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f549a71b/sshd-core/src/main/java/org/apache/sshd/server/ServerFactoryManager.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/ServerFactoryManager.java b/sshd-core/src/main/java/org/apache/sshd/server/ServerFactoryManager.java
index 81a437d..d47e10c 100644
--- a/sshd-core/src/main/java/org/apache/sshd/server/ServerFactoryManager.java
+++ b/sshd-core/src/main/java/org/apache/sshd/server/ServerFactoryManager.java
@@ -83,6 +83,13 @@ public interface ServerFactoryManager extends FactoryManager {
     public static final String AUTH_METHODS = "auth-methods";
 
     /**
+     * Key used to configure the timeout used when receiving a close request
+     * on a channel to wait until the command cleanly exits after setting
+     * an EOF on the input stream. In milliseconds.
+     */
+    public static final String COMMAND_EXIT_TIMEOUT = "command-exit-timeout";
+
+    /**
      * Retrieve the list of named factories for <code>UserAuth<code> objects.
      *
      * @return a list of named <code>UserAuth</code> factories, never <code>null</code>

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f549a71b/sshd-core/src/main/java/org/apache/sshd/server/channel/AbstractServerChannel.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/channel/AbstractServerChannel.java b/sshd-core/src/main/java/org/apache/sshd/server/channel/AbstractServerChannel.java
index 4ab0412..e96f483 100644
--- a/sshd-core/src/main/java/org/apache/sshd/server/channel/AbstractServerChannel.java
+++ b/sshd-core/src/main/java/org/apache/sshd/server/channel/AbstractServerChannel.java
@@ -66,6 +66,7 @@ public abstract class AbstractServerChannel extends AbstractChannel {
             buffer.putByte((byte) 0);
             buffer.putInt(v);
             writePacket(buffer);
+            notifyStateChanged();
         }
     }
 

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f549a71b/sshd-core/src/main/java/org/apache/sshd/server/channel/ChannelSession.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/channel/ChannelSession.java b/sshd-core/src/main/java/org/apache/sshd/server/channel/ChannelSession.java
index 5388d8a..1fcf6d0 100644
--- a/sshd-core/src/main/java/org/apache/sshd/server/channel/ChannelSession.java
+++ b/sshd-core/src/main/java/org/apache/sshd/server/channel/ChannelSession.java
@@ -25,8 +25,10 @@ import java.util.EnumSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.TimerTask;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.CopyOnWriteArraySet;
+import java.util.concurrent.TimeUnit;
 
 import org.apache.sshd.agent.SshAgent;
 import org.apache.sshd.agent.SshAgentFactory;
@@ -37,6 +39,7 @@ import org.apache.sshd.common.PtyMode;
 import org.apache.sshd.common.SshConstants;
 import org.apache.sshd.common.channel.ChannelOutputStream;
 import org.apache.sshd.common.future.CloseFuture;
+import org.apache.sshd.common.future.DefaultCloseFuture;
 import org.apache.sshd.common.future.SshFuture;
 import org.apache.sshd.common.future.SshFutureListener;
 import org.apache.sshd.common.util.Buffer;
@@ -48,6 +51,7 @@ import org.apache.sshd.server.Environment;
 import org.apache.sshd.server.ExitCallback;
 import org.apache.sshd.common.file.FileSystemAware;
 import org.apache.sshd.common.file.FileSystemFactory;
+import org.apache.sshd.server.ServerFactoryManager;
 import org.apache.sshd.server.SessionAware;
 import org.apache.sshd.server.Signal;
 import org.apache.sshd.server.SignalListener;
@@ -61,6 +65,8 @@ import org.apache.sshd.server.x11.X11ForwardSupport;
  */
 public class ChannelSession extends AbstractServerChannel {
 
+    public static final long DEFAULT_COMMAND_EXIT_TIMEOUT = 5000;
+
     public static class Factory implements NamedFactory<Channel> {
 
         public String getName() {
@@ -171,19 +177,52 @@ public class ChannelSession extends AbstractServerChannel {
     protected ChannelDataReceiver receiver;
     protected StandardEnvironment env = new StandardEnvironment();
     protected Buffer tempBuffer;
+    protected final CloseFuture commandExitFuture = new DefaultCloseFuture(lock);
 
     public ChannelSession() {
     }
 
     @Override
-    protected void doClose() {
+    protected CloseFuture preClose(boolean immediately) {
+        if (immediately) {
+            commandExitFuture.setClosed();
+        } else if (!commandExitFuture.isClosed()) {
+            log.debug("Wait 5s for shell to exit cleanly");
+            IoUtils.closeQuietly(receiver);
+            final TimerTask task = new TimerTask() {
+                @Override
+                public void run() {
+                    commandExitFuture.setClosed();
+                }
+            };
+            long timeout = DEFAULT_COMMAND_EXIT_TIMEOUT;
+            String val = getSession().getFactoryManager().getProperties().get(ServerFactoryManager.COMMAND_EXIT_TIMEOUT);
+            if (val != null) {
+                try {
+                   timeout = Long.parseLong(val);
+                } catch (NumberFormatException e) {
+                    // Ignore
+                }
+            }
+            getSession().getFactoryManager().getScheduledExecutorService().schedule(task, timeout, TimeUnit.MILLISECONDS);
+            commandExitFuture.addListener(new SshFutureListener<CloseFuture>() {
+                public void operationComplete(CloseFuture future) {
+                    task.cancel();
+                }
+            });
+        }
+        return commandExitFuture;
+    }
+
+    @Override
+    protected void postClose() {
         if (command != null) {
             command.destroy();
             command = null;
         }
         remoteWindow.notifyClosed();
         IoUtils.closeQuietly(out, err, receiver);
-        super.doClose();
+        super.postClose();
     }
 
     @Override
@@ -568,9 +607,9 @@ public class ChannelSession extends AbstractServerChannel {
         if (!closing.get()) {
             sendEof();
             sendExitStatus(exitValue);
-            // TODO: We should wait for all streams to be consumed before closing the channel
             close(false);
         }
+        commandExitFuture.setClosed();
     }
 
 }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f549a71b/sshd-core/src/main/java/org/apache/sshd/server/x11/X11ForwardSupport.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/x11/X11ForwardSupport.java b/sshd-core/src/main/java/org/apache/sshd/server/x11/X11ForwardSupport.java
index 37ec331..84d8e9a 100644
--- a/sshd-core/src/main/java/org/apache/sshd/server/x11/X11ForwardSupport.java
+++ b/sshd-core/src/main/java/org/apache/sshd/server/x11/X11ForwardSupport.java
@@ -24,6 +24,7 @@ import java.net.InetSocketAddress;
 import java.util.EnumSet;
 
 import org.apache.mina.core.buffer.IoBuffer;
+import org.apache.mina.core.future.IoFutureListener;
 import org.apache.mina.core.service.IoAcceptor;
 import org.apache.mina.core.service.IoHandlerAdapter;
 import org.apache.mina.core.session.IoEventType;
@@ -36,6 +37,8 @@ import org.apache.sshd.client.future.OpenFuture;
 import org.apache.sshd.common.SshConstants;
 import org.apache.sshd.common.SshException;
 import org.apache.sshd.common.channel.ChannelOutputStream;
+import org.apache.sshd.common.future.CloseFuture;
+import org.apache.sshd.common.future.DefaultCloseFuture;
 import org.apache.sshd.common.util.Buffer;
 import org.apache.sshd.server.session.ServerSession;
 import org.slf4j.Logger;
@@ -207,9 +210,14 @@ public class X11ForwardSupport extends IoHandlerAdapter {
         }
 
         @Override
-        protected synchronized void doClose() {
-            serverSession.close(false);
-            super.doClose();
+        protected synchronized CloseFuture preClose(boolean immediately) {
+            final CloseFuture future = new DefaultCloseFuture(null);
+            serverSession.close(immediately).addListener(new IoFutureListener<org.apache.mina.core.future.CloseFuture>() {
+                public void operationComplete(org.apache.mina.core.future.CloseFuture f) {
+                    future.setClosed();
+                }
+            });
+            return future;
         }
 
         protected synchronized void doWriteData(byte[] data, int off, int len) throws IOException {