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 2015/11/17 08:10:58 UTC

[6/6] mina-sshd git commit: [SSHD-587] Take into account reported PTY modes when creating command input/output streams for the shell

[SSHD-587] Take into account reported PTY modes when creating command input/output streams for the shell


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

Branch: refs/heads/master
Commit: 97b73947d5dbdfc777123a0f511e9966efb14556
Parents: f61edfc
Author: Lyor Goldstein <lg...@vmware.com>
Authored: Tue Nov 17 09:10:39 2015 +0200
Committer: Lyor Goldstein <lg...@vmware.com>
Committed: Tue Nov 17 09:10:39 2015 +0200

----------------------------------------------------------------------
 .../org/apache/sshd/common/channel/PtyMode.java | 383 +++++++++++++++++--
 .../org/apache/sshd/common/util/OsUtils.java    |  30 +-
 .../common/util/buffer/ByteArrayBuffer.java     |   5 +-
 .../sshd/common/util/io/NullInputStream.java    |  88 +++++
 .../sshd/common/util/io/NullOutputStream.java   |  70 ++++
 .../java/org/apache/sshd/server/Command.java    |  27 +-
 .../apache/sshd/server/CommandLifecycle.java    |  48 +++
 .../java/org/apache/sshd/server/SshServer.java  |   4 +-
 .../sshd/server/channel/ChannelSession.java     | 186 +++------
 .../shell/InteractiveProcessShellFactory.java   |  31 +-
 .../apache/sshd/server/shell/InvertedShell.java |  23 +-
 .../sshd/server/shell/InvertedShellWrapper.java |  35 +-
 .../apache/sshd/server/shell/ProcessShell.java  |  68 +++-
 .../sshd/server/shell/ProcessShellFactory.java  |  52 ++-
 .../sshd/server/shell/TtyFilterInputStream.java | 121 ++++--
 .../server/shell/TtyFilterOutputStream.java     |  88 ++++-
 .../apache/sshd/server/shell/TtyOptions.java    |  74 ----
 .../apache/sshd/common/channel/PtyModeTest.java |  63 +++
 .../sshd/server/channel/ChannelSessionTest.java |  22 --
 .../server/shell/InvertedShellWrapperTest.java  |   6 +-
 .../server/shell/TtyFilterInputStreamTest.java  | 153 ++++----
 .../server/shell/TtyFilterOutputStreamTest.java | 153 ++++----
 .../sshd/util/test/BogusInvertedShell.java      |   9 +-
 23 files changed, 1142 insertions(+), 597 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/97b73947/sshd-core/src/main/java/org/apache/sshd/common/channel/PtyMode.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/channel/PtyMode.java b/sshd-core/src/main/java/org/apache/sshd/common/channel/PtyMode.java
index 83daeaa..f83cf9c 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/channel/PtyMode.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/channel/PtyMode.java
@@ -18,8 +18,15 @@
  */
 package org.apache.sshd.common.channel;
 
+import java.util.Collection;
 import java.util.Collections;
+import java.util.EnumMap;
+import java.util.EnumSet;
+import java.util.HashMap;
 import java.util.Map;
+import java.util.Set;
+
+import org.apache.sshd.common.util.GenericUtils;
 
 /**
  * A enum describing the tty modes according to
@@ -29,38 +36,268 @@ import java.util.Map;
  */
 public enum PtyMode {
 
-    // Chars
-    VINTR(1), VQUIT(2), VERASE(3), VKILL(4), VEOF(5), VEOL(6), VEOL2(7), VSTART(8), VSTOP(9),
-    VSUSP(10), VDSUSP(11), VREPRINT(12), VWERASE(13), VLNEXT(14), VFLUSH(15), VSWTCH(16), VSTATUS(17), VDISCARD(18),
+    /////////////////////////////// Chars ////////////////////////////////////
+
+    /**
+     * Interrupt character; 255 if none. Similarly for the other characters.
+     * Not all of these characters are supported on all systems.
+     */
+    VINTR(1),
+    /**
+     * The quit character (sends SIGQUIT signal on POSIX systems).
+     */
+    VQUIT(2),
+    /**
+     * Erase the character to left of the cursor.
+     */
+    VERASE(3),
+    /**
+     * Kill the current input line.
+     */
+    VKILL(4),
+    /**
+     * End-of-file character (sends EOF from the terminal).
+     */
+    VEOF(5),
+    /**
+     * End-of-line character in addition to carriage return and/or line-feed.
+     */
+    VEOL(6),
+    /**
+     * Additional end-of-line character.
+     */
+    VEOL2(7),
+    /**
+     * Continues paused output (normally control-Q).
+     */
+    VSTART(8),
+    /**
+     * Pauses output (normally control-S).
+     */
+    VSTOP(9),
+    /**
+     * Suspends the current program.
+     */
+    VSUSP(10),
+    /**
+     * Another suspend character.
+     */
+    VDSUSP(11),
+    /**
+     * Reprints the current input line.
+     */
+    VREPRINT(12),
+    /**
+     * Erases a word left of cursor.
+     */
+    VWERASE(13),
+    /**
+     * Enter the next character typed literally, even if it is a special character
+     */
+    VLNEXT(14),
+    /**
+     * Character to flush output.
+     */
+    VFLUSH(15),
+    /**
+     * Switch to a different shell layer.
+     */
+    VSWTCH(16),
+    /**
+     * Prints system status line (load, command, pid, etc).
+     */
+    VSTATUS(17),
+    /**
+     * Toggles the flushing of terminal output.
+     */
+    VDISCARD(18),
+
+    ///////////////////////////////// I-flags ////////////////////////////////
+
+    /**
+     * The ignore parity flag.  The parameter SHOULD be 0 if this flag
+     * is FALSE, and 1 if it is TRUE.
+     */
+    IGNPAR(30),
+    /**
+     * Mark parity and framing errors.
+     */
+    PARMRK(31),
+    /**
+     * Enable checking of parity errors.
+     */
+    INPCK(32),
+    /**
+     * Strip 8th bit off characters.
+     */
+    ISTRIP(33),
+    /**
+     * Map NL into CR on input.
+     */
+    INLCR(34),
+    /**
+     * Ignore CR on input.
+     */
+    IGNCR(35),
+    /**
+     * Map CR to NL on input.
+     */
+    ICRNL(36),
+    /**
+     * Translate uppercase characters to lowercase.
+     */
+    IUCLC(37),
+    /**
+     * Enable output flow control.
+     */
+    IXON(38),
+    /**
+     * Any char will restart after stop.
+     */
+    IXANY(39),
+    /**
+     * Enable input flow control.
+     */
+    IXOFF(40),
+    /**
+     * Ring bell on input queue full.
+     */
+    IMAXBEL(41),
+
+    /////////////////////////////// L-flags //////////////////////////////////
 
-    // I flags
-    IGNPAR(30), PARMRK(31), INPCK(32), ISTRIP(33), INLCR(34), IGNCR(35), ICRNL(36), IUCLC(37), IXON(38), IXANY(39),
-    IXOFF(40), IMAXBEL(41),
+    /**
+     * Enable signals INTR, QUIT, [D]SUSP.
+     */
+    ISIG(50),
+    /**
+     * Canonicalize input lines.
+     */
+    ICANON(51),
+    /**
+     * Enable input and output of uppercase characters by preceding their
+     * lowercase equivalents with &quot;\&quot;.
+     */
+    XCASE(52),
+    /**
+     * Enable echoing.
+     */
+    ECHO(53),
+    /**
+     * Visually erase chars.
+     */
+    ECHOE(54),
+    /**
+     * Kill character discards current line.
+     */
+    ECHOK(55),
+    /**
+     * Echo NL even if ECHO is off.
+     */
+    ECHONL(56),
+    /**
+     * Don't flush after interrupt.
+     */
+    NOFLSH(57),
+    /**
+     * Stop background jobs from output.
+     */
+    TOSTOP(58),
+    /**
+     * Enable extensions.
+     */
+    IEXTEN(59),
+    /**
+     * Echo control characters as ^(Char).
+     */
+    ECHOCTL(60),
+    /**
+     * Visual erase for line kill.
+     */
+    ECHOKE(61),
+    /**
+     * Retype pending input.
+     */
+    PENDIN(62),
 
-    // L flags
-    ISIG(50), ICANON(51), XCASE(52), ECHO(53), ECHOE(54), ECHOK(55), ECHONL(56), NOFLSH(57), TOSTOP(58), IEXTEN(59),
-    ECHOCTL(60), ECHOKE(61), PENDIN(62),
+    /////////////////////////// O-flags //////////////////////////////////////
 
-    // O flags
-    OPOST(70), OLCUC(71), ONLCR(72), OCRNL(73), ONOCR(74), ONLRET(75),
+    /**
+     * Enable output processing.
+     */
+    OPOST(70),
+    /**
+     * Convert lowercase to uppercase.
+     */
+    OLCUC(71),
+    /**
+     * Map NL to CR-NL.
+     */
+    ONLCR(72),
+    /**
+     * Translate carriage return to newline (output).
+     */
+    OCRNL(73),
+    /**
+     * Translate newline to carriage return-newline (output).
+     */
+    ONOCR(74),
+    /**
+     * Newline performs a carriage return (output).
+     */
+    ONLRET(75),
 
-    // C flags
-    CS7(90), CS8(91), PARENB(92), PARODD(93),
+    //////////////////////////////// C-flags /////////////////////////////////
 
-    // Speeed
-    TTY_OP_ISPEED(128), TTY_OP_OSPEED(129);
+    /**
+     * 7 bit mode.
+     */
+    CS7(90),
+    /**
+     * 8 bit mode.
+     */
+    CS8(91),
+    /**
+     * Parity enable.
+     */
+    PARENB(92),
+    /**
+     * Odd parity, else even.
+     */
+    PARODD(93),
+
+    /////////////////////////// Speed(s) /////////////////////////////////////
+
+    /**
+     * Specifies the input baud rate in bits per second.
+     */
+    TTY_OP_ISPEED(128),
+    /**
+     * Specifies the output baud rate in bits per second.
+     */
+    TTY_OP_OSPEED(129);
 
     public static final byte TTY_OP_END = 0x00;
 
-    private static final Map<Integer, PtyMode> COMMANDS;
+    // objects that can be used to set {@link PtyMode}s as {@code true} or {@code false}
+    public static final Integer FALSE_SETTING = Integer.valueOf(0);
+    public static final Integer TRUE_SETTING = Integer.valueOf(1);
 
-    static {
-        Map<Integer, PtyMode> commands = new java.util.HashMap<>();
-        for (PtyMode c : PtyMode.values()) {
-            commands.put(c.toInt(), c);
-        }
-        COMMANDS = Collections.unmodifiableMap(commands);
-    }
+    /**
+     * An un-modifiable {@link Set} of all defined {@link PtyMode}s
+     */
+    public static final Set<PtyMode> MODES = Collections.unmodifiableSet(EnumSet.allOf(PtyMode.class));
+
+    private static final Map<Integer, PtyMode> COMMANDS =
+            Collections.unmodifiableMap(new HashMap<Integer, PtyMode>(MODES.size()) {
+                private static final long serialVersionUID = 1L;    // we're not serializing it
+
+                {
+                    for (PtyMode c : PtyMode.values()) {
+                        put(c.toInt(), c);
+                    }
+                }
+            });
 
     private int v;
 
@@ -72,7 +309,105 @@ public enum PtyMode {
         return v;
     }
 
+    /**
+     * @param b The numeric value of the option
+     * @return The matching {@link PtyMode} or {@code null} if no match found
+     * @see #toInt()
+     */
     public static PtyMode fromInt(int b) {
-        return COMMANDS.get(0x00FF & (b + 0x100));
+        return COMMANDS.get(0x00FF & b);
+    }
+
+    /**
+     * @param options The options to enable - ignored if {@code null}/empty
+     * @return A {@link Map} where all the specified {@link PtyMode}s have {@link #TRUE_SETTING}
+     */
+    public static Map<PtyMode, Integer> createEnabledOptions(PtyMode ... options) {
+        return createEnabledOptions(GenericUtils.of(options));
+    }
+
+    /**
+     * @param options The options to enable - ignored if {@code null}/empty
+     * @return A {@link Map} where all the specified {@link PtyMode}s have {@link #TRUE_SETTING}
+     */
+    public static Map<PtyMode, Integer> createEnabledOptions(Collection<PtyMode> options) {
+        if (GenericUtils.isEmpty(options)) {
+            return Collections.emptyMap();
+        }
+
+        Map<PtyMode, Integer> modes = new EnumMap<>(PtyMode.class);
+        for (PtyMode m : options) {
+            modes.put(m, TRUE_SETTING);
+        }
+
+        return modes;
+    }
+
+    public static Set<PtyMode> resolveEnabledOptions(Map<PtyMode, ?> modes, PtyMode ... options) {
+        return resolveEnabledOptions(modes, GenericUtils.of(options));
+    }
+
+    /**
+     * @param modes The PTY settings - ignored if {@code null}/empty
+     * @param options The options that should be enabled
+     * @return A {@link Set} of all the {@link PtyMode}s that appeared
+     * in the settings and were enabled
+     * @see #getBooleanSettingValue(Map, PtyMode)
+     */
+    public static Set<PtyMode> resolveEnabledOptions(Map<PtyMode, ?> modes, Collection<PtyMode> options) {
+        if (GenericUtils.isEmpty(modes) || GenericUtils.isEmpty(options)) {
+            return Collections.emptySet();
+        }
+
+        Set<PtyMode> enabled = EnumSet.noneOf(PtyMode.class);
+        for (PtyMode m : options) {
+            if (getBooleanSettingValue(modes, m)) {
+                enabled.add(m);
+            }
+        }
+
+        return enabled;
+    }
+
+    /**
+     * @param modes The current modes {@link Map}-ing
+     * @param m The required {@link PtyMode}
+     * @return {@code true} if <U>all</U> of these conditions hold:</BR>
+     * <UL>
+     *      <LI>Modes map is not {@code null}/empty</LI>
+     *      <LI>Required mode setting is not {@code null}</LI>
+     *      <LI>The setting has a mapped value</LI>
+     *      <LI>The mapped value is a {@link Number}</LI>
+     *      <LI>The {@link Number#intValue()} is non-zero</LI>
+     * </UL>
+     * @see #getBooleanSettingValue(Object)
+     */
+    public static boolean getBooleanSettingValue(Map<PtyMode, ?> modes, PtyMode m) {
+        if ((m == null) || GenericUtils.isEmpty(modes)) {
+            return false;
+        } else {
+            return getBooleanSettingValue(modes.get(m));
+        }
+    }
+
+    /**
+     * @param v The value to be tested
+     * @return {@code true} if <U>all</U> of these conditions hold:</BR>
+     * <UL>
+     *      <LI>The mapped value is a {@link Number}</LI>
+     *      <LI>The {@link Number#intValue()} is non-zero</LI>
+     * </UL>
+     * @see #getBooleanSettingValue(int)
+     */
+    public static boolean getBooleanSettingValue(Object v) {
+        return (v instanceof Number) && getBooleanSettingValue(((Number) v).intValue());
+    }
+
+    /**
+     * @param v The setting value
+     * @return {@code true} if value is non-zero
+     */
+    public static boolean getBooleanSettingValue(int v) {
+        return v != 0;
     }
 }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/97b73947/sshd-core/src/main/java/org/apache/sshd/common/util/OsUtils.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/util/OsUtils.java b/sshd-core/src/main/java/org/apache/sshd/common/util/OsUtils.java
index ad023e7..24f2fdd 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/util/OsUtils.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/util/OsUtils.java
@@ -18,24 +18,30 @@
  */
 package org.apache.sshd.common.util;
 
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
 /**
  * Operating system dependent utility methods.
  *
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
  */
 public final class OsUtils {
+    public static final String WINDOWS_SHELL_COMMAND_NAME = "cmd.exe";
+    public static final String LINUX_SHELL_COMMAND_NAME = "/bin/sh";
+
+    public static final List<String> LINUX_COMMAND =
+            Collections.unmodifiableList(Arrays.asList(LINUX_SHELL_COMMAND_NAME, "-i", "-l"));
+    public static final List<String> WINDOWS_COMMAND =
+            Collections.unmodifiableList(Collections.singletonList(WINDOWS_SHELL_COMMAND_NAME));
 
-    private static final boolean WIN32;
+    private static final boolean WIN32 = GenericUtils.trimToEmpty(System.getProperty("os.name")).toLowerCase().contains("windows");
 
     private OsUtils() {
         throw new UnsupportedOperationException("No instance allowed");
     }
 
-    static {
-        String os = System.getProperty("os.name").toLowerCase();
-        WIN32 = os.contains("windows");
-    }
-
     /**
      * @return true if the host is a UNIX system (and not Windows).
      */
@@ -50,4 +56,16 @@ public final class OsUtils {
         return WIN32;
     }
 
+    public static List<String> resolveDefaultInteractiveCommand() {
+        return resolveInteractiveCommand(isWin32());
+    }
+
+    public static List<String> resolveInteractiveCommand(boolean isWin32) {
+        if (isWin32) {
+            return WINDOWS_COMMAND;
+        } else {
+            return LINUX_COMMAND;
+        }
+    }
+
 }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/97b73947/sshd-core/src/main/java/org/apache/sshd/common/util/buffer/ByteArrayBuffer.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/util/buffer/ByteArrayBuffer.java b/sshd-core/src/main/java/org/apache/sshd/common/util/buffer/ByteArrayBuffer.java
index aaec9c6..932e3cb 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/util/buffer/ByteArrayBuffer.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/util/buffer/ByteArrayBuffer.java
@@ -104,8 +104,9 @@ public final class ByteArrayBuffer extends Buffer {
 
     @Override
     public void compact() {
-        if (available() > 0) {
-            System.arraycopy(data, rpos, data, 0, wpos - rpos);
+        int avail = available();
+        if (avail > 0) {
+            System.arraycopy(data, rpos, data, 0, avail);
         }
         wpos -= rpos;
         rpos = 0;

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/97b73947/sshd-core/src/main/java/org/apache/sshd/common/util/io/NullInputStream.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/util/io/NullInputStream.java b/sshd-core/src/main/java/org/apache/sshd/common/util/io/NullInputStream.java
new file mode 100644
index 0000000..ad7e2ed
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/common/util/io/NullInputStream.java
@@ -0,0 +1,88 @@
+/*
+ * 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.common.util.io;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.channels.Channel;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * A {@code /dev/null} input stream
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class NullInputStream extends InputStream implements Channel {
+    private final AtomicBoolean open = new AtomicBoolean(true);
+
+    public NullInputStream() {
+        super();
+    }
+
+    @Override
+    public boolean isOpen() {
+        return open.get();
+    }
+
+    @Override
+    public int read() throws IOException {
+        if (!isOpen()) {
+            throw new IOException("Stream is closed for reading one value");
+        }
+        return -1;
+    }
+
+    @Override
+    public int read(byte[] b, int off, int len) throws IOException {
+        if (!isOpen()) {
+            throw new IOException("Stream is closed for reading " + len + " bytes");
+        }
+        return -1;
+    }
+
+    @Override
+    public long skip(long n) throws IOException {
+        if (!isOpen()) {
+            throw new IOException("Stream is closed for skipping " + n + " bytes");
+        }
+        return 0L;
+    }
+
+    @Override
+    public int available() throws IOException {
+        if (!isOpen()) {
+            throw new IOException("Stream is closed for availability query");
+        }
+        return 0;
+    }
+
+    @Override
+    public synchronized void reset() throws IOException {
+        if (!isOpen()) {
+            throw new IOException("Stream is closed for reset");
+        }
+    }
+
+    @Override
+    public void close() throws IOException {
+        if (open.getAndSet(false)) {
+            return; // debug breakpoint
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/97b73947/sshd-core/src/main/java/org/apache/sshd/common/util/io/NullOutputStream.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/util/io/NullOutputStream.java b/sshd-core/src/main/java/org/apache/sshd/common/util/io/NullOutputStream.java
new file mode 100644
index 0000000..64d59b6
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/common/util/io/NullOutputStream.java
@@ -0,0 +1,70 @@
+/*
+ * 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.common.util.io;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.channels.Channel;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * A {code /dev/null} output stream
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class NullOutputStream extends OutputStream implements Channel {
+    private final AtomicBoolean open = new AtomicBoolean(true);
+
+    public NullOutputStream() {
+        super();
+    }
+
+    @Override
+    public boolean isOpen() {
+        return open.get();
+    }
+
+    @Override
+    public void write(int b) throws IOException {
+        if (!isOpen()) {
+            throw new IOException("Stream is closed for writing one byte");
+        }
+    }
+
+    @Override
+    public void write(byte[] b, int off, int len) throws IOException {
+        if (!isOpen()) {
+            throw new IOException("Stream is closed for writing " + len + " bytes");
+        }
+    }
+
+    @Override
+    public void flush() throws IOException {
+        if (!isOpen()) {
+            throw new IOException("Stream is closed for flushing");
+        }
+    }
+
+    @Override
+    public void close() throws IOException {
+        if (open.getAndSet(false)) {
+            return; // debug breakpoint
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/97b73947/sshd-core/src/main/java/org/apache/sshd/server/Command.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/Command.java b/sshd-core/src/main/java/org/apache/sshd/server/Command.java
index 380abe0..4043e48 100644
--- a/sshd-core/src/main/java/org/apache/sshd/server/Command.java
+++ b/sshd-core/src/main/java/org/apache/sshd/server/Command.java
@@ -18,7 +18,6 @@
  */
 package org.apache.sshd.server;
 
-import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 
@@ -34,7 +33,7 @@ import java.io.OutputStream;
  * </p>
  * see {@link org.apache.sshd.server.shell.InvertedShellWrapper}.
  */
-public interface Command {
+public interface Command extends CommandLifecycle {
 
     /**
      * Set the input stream that can be used by the shell to read input.
@@ -63,28 +62,4 @@ public interface Command {
      * @param callback The {@link ExitCallback} to call when shell is closed
      */
     void setExitCallback(ExitCallback callback);
-
-    /**
-     * Starts the shell.
-     * All streams must have been set before calling this method.
-     * The command should implement Runnable, and this method
-     * should spawn a new thread like:
-     * <pre>
-     * {@code
-     * Thread(this).start();
-     * }
-     * </pre>
-     *
-     * @param env The {@link Environment}
-     * @throws IOException If failed to start
-     */
-    void start(Environment env) throws IOException;
-
-    /**
-     * Destroy the shell.
-     * This method can be called by the SSH server to destroy the shell because
-     * the client has disconnected somehow.
-     */
-    void destroy();
-
 }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/97b73947/sshd-core/src/main/java/org/apache/sshd/server/CommandLifecycle.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/CommandLifecycle.java b/sshd-core/src/main/java/org/apache/sshd/server/CommandLifecycle.java
new file mode 100644
index 0000000..cbe3cf0
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/server/CommandLifecycle.java
@@ -0,0 +1,48 @@
+/*
+ * 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;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public interface CommandLifecycle {
+    /**
+     * Starts the command execution. All streams must have been set <U>before</U>
+     * calling this method. The command should implement {@link java.lang.Runnable},
+     * and this method should spawn a new thread like:
+     * <pre>
+     * {@code Thread(this).start(); }
+     * </pre>
+     *
+     * @param env The {@link Environment}
+     * @throws IOException If failed to start
+     */
+    void start(Environment env) throws IOException;
+
+    /**
+     * This method is called by the SSH server to destroy the command because
+     * the client has disconnected somehow.
+     *
+     * @throws Exception if failed to destroy
+     */
+    void destroy() throws Exception;
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/97b73947/sshd-core/src/main/java/org/apache/sshd/server/SshServer.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/SshServer.java b/sshd-core/src/main/java/org/apache/sshd/server/SshServer.java
index 55d5a5a..6a50428 100644
--- a/sshd-core/src/main/java/org/apache/sshd/server/SshServer.java
+++ b/sshd-core/src/main/java/org/apache/sshd/server/SshServer.java
@@ -475,11 +475,11 @@ public class SshServer extends AbstractFactoryManager implements ServerFactoryMa
             sshd.setKeyPairProvider(new SimpleGeneratorHostKeyProvider(new File("key.ser")));
         }
 
-        sshd.setShellFactory(new InteractiveProcessShellFactory());
+        sshd.setShellFactory(InteractiveProcessShellFactory.INSTANCE);
         sshd.setPasswordAuthenticator(new PasswordAuthenticator() {
             @Override
             public boolean authenticate(String username, String password, ServerSession session) {
-                return (username != null) && username.equals(password);
+                return (GenericUtils.length(username) > 0) && username.equals(password);
             }
         });
         sshd.setPublickeyAuthenticator(AcceptAllPublickeyAuthenticator.INSTANCE);

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/97b73947/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 31cd5c1..fd4ff86 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
@@ -20,14 +20,9 @@ package org.apache.sshd.server.channel;
 
 import java.io.IOException;
 import java.io.OutputStream;
-import java.util.Arrays;
-import java.util.Collection;
 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.ScheduledExecutorService;
 import java.util.concurrent.TimeUnit;
 
@@ -66,7 +61,7 @@ import org.apache.sshd.server.ExitCallback;
 import org.apache.sshd.server.ServerFactoryManager;
 import org.apache.sshd.server.SessionAware;
 import org.apache.sshd.server.Signal;
-import org.apache.sshd.server.SignalListener;
+import org.apache.sshd.server.StandardEnvironment;
 import org.apache.sshd.server.forward.ForwardingFilter;
 import org.apache.sshd.server.session.ServerSession;
 import org.apache.sshd.server.x11.X11ForwardSupport;
@@ -78,112 +73,6 @@ import org.apache.sshd.server.x11.X11ForwardSupport;
  */
 public class ChannelSession extends AbstractServerChannel {
 
-    protected static class StandardEnvironment implements Environment {
-
-        private final Map<Signal, Set<SignalListener>> listeners;
-        private final Map<String, String> env;
-        private final Map<PtyMode, Integer> ptyModes;
-
-        public StandardEnvironment() {
-            listeners = new ConcurrentHashMap<>(3);
-            env = new ConcurrentHashMap<>();
-            ptyModes = new ConcurrentHashMap<>();
-        }
-
-        @Override
-        public void addSignalListener(SignalListener listener, Signal... signals) {
-            if (signals == null) {
-                throw new IllegalArgumentException("signals may not be null");
-            }
-
-            addSignalListener(listener, Arrays.asList(signals));
-        }
-
-        @Override
-        public void addSignalListener(SignalListener listener) {
-            addSignalListener(listener, Signal.SIGNALS);
-        }
-
-        /*
-         * NOTE: we don't care if the collection is a Set or not - after all,
-         * we hold the listeners inside a Set, so even if we add several times
-         * the same listener to the same signal set, it is harmless
-         */
-        @Override
-        public void addSignalListener(SignalListener listener, Collection<Signal> signals) {
-            if (listener == null) {
-                throw new IllegalArgumentException("listener may not be null");
-            }
-
-            if (signals == null) {
-                throw new IllegalArgumentException("signals may not be null");
-            }
-
-            for (Signal s : signals) {
-                getSignalListeners(s, true).add(listener);
-            }
-        }
-
-        @Override
-        public Map<String, String> getEnv() {
-            return env;
-        }
-
-        @Override
-        public Map<PtyMode, Integer> getPtyModes() {
-            return ptyModes;
-        }
-
-        @Override
-        public void removeSignalListener(SignalListener listener) {
-            if (listener == null) {
-                throw new IllegalArgumentException("listener may not be null");
-            }
-            for (Signal s : Signal.SIGNALS) {
-                final Set<SignalListener> ls = getSignalListeners(s, false);
-                if (ls != null) {
-                    ls.remove(listener);
-                }
-            }
-        }
-
-        public void signal(Signal signal) {
-            final Set<SignalListener> ls = getSignalListeners(signal, false);
-            if (ls != null) {
-                for (SignalListener l : ls) {
-                    l.signal(signal);
-                }
-            }
-        }
-
-        /**
-         * adds a variable to the environment. This method is called <code>set</code>
-         * according to the name of the appropriate posix command <code>set</code>
-         *
-         * @param key   environment variable name
-         * @param value environment variable value
-         */
-        public void set(String key, String value) {
-            // TODO: listening for property changes would be nice too.
-            getEnv().put(key, value);
-        }
-
-        protected Set<SignalListener> getSignalListeners(Signal signal, boolean create) {
-            Set<SignalListener> ls = listeners.get(signal);
-            if (ls == null && create) {
-                synchronized (listeners) {
-                    ls = listeners.get(signal);
-                    if (ls == null) {
-                        ls = new CopyOnWriteArraySet<>();
-                        listeners.put(signal, ls);
-                    }
-                }
-            }
-            // may be null in case create=false
-            return ls;
-        }
-    }
-
     protected String type;
     protected ChannelAsyncOutputStream asyncOut;
     protected ChannelAsyncOutputStream asyncErr;
@@ -191,8 +80,8 @@ public class ChannelSession extends AbstractServerChannel {
     protected OutputStream err;
     protected Command command;
     protected ChannelDataReceiver receiver;
-    protected StandardEnvironment env = new StandardEnvironment();
     protected Buffer tempBuffer;
+    protected final StandardEnvironment env = new StandardEnvironment();
     protected final CloseFuture commandExitFuture = new DefaultCloseFuture(lock);
 
     public ChannelSession() {
@@ -232,7 +121,14 @@ public class ChannelSession extends AbstractServerChannel {
             if (immediately || command == null) {
                 commandExitFuture.setClosed();
             } else if (!commandExitFuture.isClosed()) {
-                IoUtils.closeQuietly(receiver);
+                IOException e = IoUtils.closeQuietly(receiver);
+                if (e != null) {
+                    if (log.isDebugEnabled()) {
+                        log.debug("close({})[immediately={}] failed ({}) to close receiver: {}",
+                                  this, immediately, e.getClass().getSimpleName(), e.getMessage());
+                    }
+                }
+
                 final TimerTask task = new TimerTask() {
                     @Override
                     public void run() {
@@ -265,17 +161,47 @@ public class ChannelSession extends AbstractServerChannel {
     @Override
     protected void doCloseImmediately() {
         if (command != null) {
-            command.destroy();
-            command = null;
+            try {
+                command.destroy();
+            } catch (Exception e) {
+                log.warn("doCloseImmediately({}) failed ({}) to destroy command: {}",
+                         this, e.getClass().getSimpleName(), e.getMessage());
+            } finally {
+                command = null;
+            }
         }
-        IoUtils.closeQuietly(remoteWindow, out, err, receiver);
+
+        IOException e = IoUtils.closeQuietly(remoteWindow, out, err, receiver);
+        if (e != null) {
+            if (log.isDebugEnabled()) {
+                log.debug("doCloseImmediately({}) failed ({}) to close resources: {}",
+                          this, e.getClass().getSimpleName(), e.getMessage());
+            }
+
+            if (log.isTraceEnabled()) {
+                Throwable[] suppressed = e.getSuppressed();
+                if (GenericUtils.length(suppressed) > 0) {
+                    for (Throwable t : suppressed) {
+                        log.trace("Suppressed " + t.getClass().getSimpleName() + ") while close immediately resource(s) of " + this + ": " + t.getMessage());
+                    }
+                }
+            }
+        }
+
         super.doCloseImmediately();
     }
 
     @Override
     public void handleEof() throws IOException {
         super.handleEof();
-        IoUtils.closeQuietly(receiver);
+
+        IOException e = IoUtils.closeQuietly(receiver);
+        if (e != null) {
+            if (log.isDebugEnabled()) {
+                log.debug("handleEof({}) failed ({}) to close receiver: {}",
+                          this, e.getClass().getSimpleName(), e.getMessage());
+            }
+        }
     }
 
     @Override
@@ -371,7 +297,8 @@ public class ChannelSession extends AbstractServerChannel {
         byte[] modes = buffer.getBytes();
         Environment environment = getEnvironment();
         Map<PtyMode, Integer> ptyModes = environment.getPtyModes();
-        for (int i = 0; i < modes.length && modes[i] != 0;) {
+
+        for (int i = 0; i < modes.length && (modes[i] != PtyMode.TTY_OP_END);) {
             int opcode = modes[i++] & 0x00FF;
             PtyMode mode = PtyMode.fromInt(opcode);
             /**
@@ -379,7 +306,7 @@ public class ChannelSession extends AbstractServerChannel {
              * "Opcodes 160 to 255 are not yet defined, and cause parsing to stop"
              */
             if (mode == null) {
-                log.warn("Unknown pty opcode value: " + opcode);
+                log.warn("handlePtyReq(" + this + ") unknown pty opcode value: " + opcode);
                 break;
             }
             int val = ((modes[i++] << 24) & 0xff000000)
@@ -388,10 +315,10 @@ public class ChannelSession extends AbstractServerChannel {
                     | ((modes[i++]) & 0x000000ff);
             ptyModes.put(mode, val);
         }
+
         if (log.isDebugEnabled()) {
-            log.debug("pty for channel {}: term={}, size=({} - {}), pixels=({}, {}), modes=[{}]",
-                    id, term, tColumns, tRows,
-                    tWidth, tHeight, ptyModes);
+            log.debug("handlePtyReq({}): term={}, size=({} - {}), pixels=({}, {}), modes=[{}]",
+                      this, term, tColumns, tRows, tWidth, tHeight, ptyModes);
         }
 
         addEnvVariable(Environment.ENV_TERM, term);
@@ -406,12 +333,11 @@ public class ChannelSession extends AbstractServerChannel {
         int tWidth = buffer.getInt();
         int tHeight = buffer.getInt();
         if (log.isDebugEnabled()) {
-            log.debug("window-change for channel {}: ({} - {}), ({}, {})",
-                    id, tColumns, tRows,
-                    tWidth, tHeight);
+            log.debug("handleWindowChange({}): ({} - {}), ({}, {})",
+                      this, tColumns, tRows, tWidth, tHeight);
         }
 
-        final StandardEnvironment e = getEnvironment();
+        StandardEnvironment e = getEnvironment();
         e.set(Environment.ENV_COLUMNS, Integer.toString(tColumns));
         e.set(Environment.ENV_LINES, Integer.toString(tRows));
         e.signal(Signal.WINCH);
@@ -421,14 +347,14 @@ public class ChannelSession extends AbstractServerChannel {
     protected boolean handleSignal(Buffer buffer) throws IOException {
         String name = buffer.getString();
         if (log.isDebugEnabled()) {
-            log.debug("Signal received on channel {}: {}", id, name);
+            log.debug("handleSignal({}): {}", this, name);
         }
 
-        final Signal signal = Signal.get(name);
+        Signal signal = Signal.get(name);
         if (signal != null) {
             getEnvironment().signal(signal);
         } else {
-            log.warn("Unknown signal received: " + name);
+            log.warn("handleSignal(" + this + ") unknown signal received: " + name);
         }
         return true;
     }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/97b73947/sshd-core/src/main/java/org/apache/sshd/server/shell/InteractiveProcessShellFactory.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/shell/InteractiveProcessShellFactory.java b/sshd-core/src/main/java/org/apache/sshd/server/shell/InteractiveProcessShellFactory.java
index 7b4d7e1..a0ebee2 100644
--- a/sshd-core/src/main/java/org/apache/sshd/server/shell/InteractiveProcessShellFactory.java
+++ b/sshd-core/src/main/java/org/apache/sshd/server/shell/InteractiveProcessShellFactory.java
@@ -19,6 +19,8 @@
 
 package org.apache.sshd.server.shell;
 
+import java.util.List;
+
 import org.apache.sshd.common.util.OsUtils;
 
 /**
@@ -27,33 +29,14 @@ import org.apache.sshd.common.util.OsUtils;
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
  */
 public class InteractiveProcessShellFactory extends ProcessShellFactory {
-
-    public static final InteractiveProcessShellFactory INSTANCE;
-
-    private static final String[] LINUX_COMMAND;
-    private static final String[] WINDOWS_COMMAND;
-
-    static {
-        LINUX_COMMAND = new String[] {"/bin/sh", "-i", "-l"};
-        WINDOWS_COMMAND = new String[] {"cmd.exe"};
-        INSTANCE = new InteractiveProcessShellFactory();
-    }
+    public static final InteractiveProcessShellFactory INSTANCE = new InteractiveProcessShellFactory();
 
     public InteractiveProcessShellFactory() {
-        super(resolveDefaultInteractiveCommand(), TtyOptions.resolveDefaultTtyOptions());
-    }
-
-    public static String[] resolveDefaultInteractiveCommand() {
-        return resolveInteractiveCommand(OsUtils.isWin32());
+        super(OsUtils.resolveDefaultInteractiveCommand());
     }
 
-    public static String[] resolveInteractiveCommand(boolean isWin32) {
-        // return clone(s) to avoid inadvertent modification
-        if (isWin32) {
-            return WINDOWS_COMMAND.clone();
-        } else {
-            return LINUX_COMMAND.clone();
-        }
+    @Override
+    protected List<String> resolveEffectiveCommand(List<String> original) {
+        return original;
     }
-
 }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/97b73947/sshd-core/src/main/java/org/apache/sshd/server/shell/InvertedShell.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/shell/InvertedShell.java b/sshd-core/src/main/java/org/apache/sshd/server/shell/InvertedShell.java
index aef00c1..ac3e8a7 100644
--- a/sshd-core/src/main/java/org/apache/sshd/server/shell/InvertedShell.java
+++ b/sshd-core/src/main/java/org/apache/sshd/server/shell/InvertedShell.java
@@ -18,10 +18,10 @@
  */
 package org.apache.sshd.server.shell;
 
-import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
-import java.util.Map;
+
+import org.apache.sshd.server.CommandLifecycle;
 
 /**
  * This shell have inverted streams, such as the one obtained when launching a
@@ -31,17 +31,7 @@ import java.util.Map;
  *
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
  */
-public interface InvertedShell {
-
-    /**
-     * Starts the shell and will make the streams available for
-     * the ssh server to retrieve and use.
-     *
-     * @param env The environment
-     * @throws IOException if failed to start
-     */
-    void start(Map<String, String> env) throws IOException;
-
+public interface InvertedShell extends CommandLifecycle {
     /**
      * Returns the output stream used to feed the shell.
      * This method is called after the shell has been started.
@@ -74,11 +64,4 @@ public interface InvertedShell {
      * @return the exit value of the shell
      */
     int exitValue();
-
-    /**
-     * Destroy the shell.
-     * This method can be called by the SSH server to destroy the shell because
-     * the client has disconnected somehow.
-     */
-    void destroy();
 }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/97b73947/sshd-core/src/main/java/org/apache/sshd/server/shell/InvertedShellWrapper.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/shell/InvertedShellWrapper.java b/sshd-core/src/main/java/org/apache/sshd/server/shell/InvertedShellWrapper.java
index 565ff95..6c55025 100644
--- a/sshd-core/src/main/java/org/apache/sshd/server/shell/InvertedShellWrapper.java
+++ b/sshd-core/src/main/java/org/apache/sshd/server/shell/InvertedShellWrapper.java
@@ -25,6 +25,7 @@ import java.util.concurrent.Executor;
 import java.util.concurrent.ExecutorService;
 
 import org.apache.sshd.common.PropertyResolverUtils;
+import org.apache.sshd.common.util.GenericUtils;
 import org.apache.sshd.common.util.ValidateUtils;
 import org.apache.sshd.common.util.io.IoUtils;
 import org.apache.sshd.common.util.logging.AbstractLoggingBean;
@@ -147,7 +148,7 @@ public class InvertedShellWrapper extends AbstractLoggingBean implements Command
     @Override
     public synchronized void start(Environment env) throws IOException {
         // TODO propagate the Environment itself and support signal sending.
-        shell.start(env.getEnv());
+        shell.start(env);
         shellIn = shell.getInputStream();
         shellOut = shell.getOutputStream();
         shellErr = shell.getErrorStream();
@@ -160,10 +161,28 @@ public class InvertedShellWrapper extends AbstractLoggingBean implements Command
     }
 
     @Override
-    public synchronized void destroy() {
-        shell.destroy();
+    public synchronized void destroy() throws Exception {
+        Exception err = null;
+        try {
+            shell.destroy();
+        } catch (Exception e) {
+            log.warn("destroy({}) failed ({}) to destroy shell: {}",
+                     this, e.getClass().getSimpleName(), e.getMessage());
+            err = GenericUtils.accumulateException(err, e);
+        }
+
         if (shutdownExecutor && (executor instanceof ExecutorService)) {
-            ((ExecutorService) executor).shutdown();
+            try {
+                ((ExecutorService) executor).shutdown();
+            } catch (Exception e) {
+                log.warn("destroy({}) failed ({}) to shut down executor: {}",
+                         this, e.getClass().getSimpleName(), e.getMessage());
+                err = GenericUtils.accumulateException(err, e);
+            }
+        }
+
+        if (err != null) {
+            throw err;
         }
     }
 
@@ -198,7 +217,13 @@ public class InvertedShellWrapper extends AbstractLoggingBean implements Command
                 Thread.sleep(pumpSleepTime);
             }
         } catch (Exception e) {
-            shell.destroy();
+            try {
+                shell.destroy();
+            } catch (Exception err) {
+                log.warn("pumpStreams({}) failed ({}) to destroy shell: {}",
+                         this, e.getClass().getSimpleName(), e.getMessage());
+                err = GenericUtils.accumulateException(err, e);
+            }
 
             int exitValue = shell.exitValue();
             if (log.isDebugEnabled()) {

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/97b73947/sshd-core/src/main/java/org/apache/sshd/server/shell/ProcessShell.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/shell/ProcessShell.java b/sshd-core/src/main/java/org/apache/sshd/server/shell/ProcessShell.java
index b964506..fa69429 100644
--- a/sshd-core/src/main/java/org/apache/sshd/server/shell/ProcessShell.java
+++ b/sshd-core/src/main/java/org/apache/sshd/server/shell/ProcessShell.java
@@ -21,14 +21,19 @@ package org.apache.sshd.server.shell;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
 import java.util.Map;
-import java.util.Set;
 
+import org.apache.sshd.common.channel.PtyMode;
 import org.apache.sshd.common.util.GenericUtils;
 import org.apache.sshd.common.util.ValidateUtils;
 import org.apache.sshd.common.util.io.IoUtils;
 import org.apache.sshd.common.util.logging.AbstractLoggingBean;
+import org.apache.sshd.server.Environment;
 
 /**
  * Bridges the I/O streams between the SSH command and the process that executes it
@@ -36,38 +41,43 @@ import org.apache.sshd.common.util.logging.AbstractLoggingBean;
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
  */
 public class ProcessShell extends AbstractLoggingBean implements InvertedShell {
-    private final Set<TtyOptions> ttyOptions;
-    private final String[] command;
+    private final List<String> command;
     private String cmdValue;
     private Process process;
     private TtyFilterOutputStream in;
     private TtyFilterInputStream out;
     private TtyFilterInputStream err;
 
-    public ProcessShell(Collection<TtyOptions> ttyOptions, String ... command) {
-        // we create a copy of the options so as to avoid concurrent modifications
-        this.ttyOptions = GenericUtils.of(ttyOptions);
-        // we clone the original array so as not to change it
-        this.command = ValidateUtils.checkNotNullAndNotEmpty(command, "No process shell command(s)").clone();
+    /**
+     * @param command    The command components which when joined (with space separator)
+     *                   create the full command to be executed by the shell
+     */
+    public ProcessShell(String ... command) {
+        this(GenericUtils.isEmpty(command) ? Collections.<String>emptyList() : Arrays.asList(command));
+    }
+    public ProcessShell(Collection<String> command) {
+        // we copy the original list so as not to change it
+        this.command = new ArrayList<String>(ValidateUtils.checkNotNullAndNotEmpty(command, "No process shell command(s)"));
         this.cmdValue = GenericUtils.join(command, ' ');
     }
 
     @Override
-    public void start(Map<String, String> env) throws IOException {
-        for (int i = 0; i < command.length; i++) {
-            String cmd = command[i];
+    public void start(Environment env) throws IOException {
+        Map<String, String> varsMap = resolveShellEnvironment(env.getEnv());
+        for (int i = 0; i < command.size(); i++) {
+            String cmd = command.get(i);
             if ("$USER".equals(cmd)) {
-                cmd = env.get("USER");
-                command[i] = cmd;
+                cmd = varsMap.get("USER");
+                command.set(i, cmd);
                 cmdValue = GenericUtils.join(command, ' ');
             }
         }
 
         ProcessBuilder builder = new ProcessBuilder(command);
-        if (GenericUtils.size(env) > 0) {
+        if (GenericUtils.size(varsMap) > 0) {
             try {
                 Map<String, String> procEnv = builder.environment();
-                procEnv.putAll(env);
+                procEnv.putAll(varsMap);
             } catch (Exception e) {
                 log.warn("Could not set environment for command=" + cmdValue, e);
             }
@@ -78,9 +88,20 @@ public class ProcessShell extends AbstractLoggingBean implements InvertedShell {
         }
 
         process = builder.start();
-        out = new TtyFilterInputStream(process.getInputStream(), ttyOptions);
-        err = new TtyFilterInputStream(process.getErrorStream(), ttyOptions);
-        in = new TtyFilterOutputStream(process.getOutputStream(), err, ttyOptions);
+
+        Map<PtyMode, ?> modes = resolveShellTtyOptions(env.getPtyModes());
+        out = new TtyFilterInputStream(process.getInputStream(), modes);
+        err = new TtyFilterInputStream(process.getErrorStream(), modes);
+        in = new TtyFilterOutputStream(process.getOutputStream(), err, modes);
+    }
+
+    protected Map<String, String> resolveShellEnvironment(Map<String, String> env) {
+        return env;
+    }
+
+    // for some reason these modes provide best results BOTH with Linux SSH client and PUTTY
+    protected Map<PtyMode, Integer> resolveShellTtyOptions(Map<PtyMode, Integer> modes) {
+        return modes;
     }
 
     @Override
@@ -134,7 +155,16 @@ public class ProcessShell extends AbstractLoggingBean implements InvertedShell {
         IOException e = IoUtils.closeQuietly(getInputStream(), getOutputStream(), getErrorStream());
         if (e != null) {
             if (log.isDebugEnabled()) {
-                log.debug(e.getClass().getSimpleName() + " while destroy streams of '" + cmdValue + "': " + e.getMessage());
+                log.debug(e.getClass().getSimpleName() + " while destroy streams of '" + this + "': " + e.getMessage());
+            }
+
+            if (log.isTraceEnabled()) {
+                Throwable[] suppressed = e.getSuppressed();
+                if (GenericUtils.length(suppressed) > 0) {
+                    for (Throwable t : suppressed) {
+                        log.trace("Suppressed " + t.getClass().getSimpleName() + ") while destroy streams of '" + this + "': " + t.getMessage());
+                    }
+                }
             }
         }
     }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/97b73947/sshd-core/src/main/java/org/apache/sshd/server/shell/ProcessShellFactory.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/shell/ProcessShellFactory.java b/sshd-core/src/main/java/org/apache/sshd/server/shell/ProcessShellFactory.java
index b29c46b..cfadb50 100644
--- a/sshd-core/src/main/java/org/apache/sshd/server/shell/ProcessShellFactory.java
+++ b/sshd-core/src/main/java/org/apache/sshd/server/shell/ProcessShellFactory.java
@@ -18,10 +18,13 @@
  */
 package org.apache.sshd.server.shell;
 
-import java.util.Collection;
-import java.util.Set;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
 import org.apache.sshd.common.Factory;
 import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.OsUtils;
+import org.apache.sshd.common.util.ValidateUtils;
 import org.apache.sshd.common.util.logging.AbstractLoggingBean;
 import org.apache.sshd.server.Command;
 
@@ -32,28 +35,30 @@ import org.apache.sshd.server.Command;
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
  */
 public class ProcessShellFactory extends AbstractLoggingBean implements Factory<Command> {
-    private String[] command;
-    private final Set<TtyOptions> ttyOptions;
+    private List<String> command;
 
     public ProcessShellFactory() {
-        this(GenericUtils.EMPTY_STRING_ARRAY);
+        this(Collections.<String>emptyList());
     }
 
-    public ProcessShellFactory(String[] command) {
-        this(command, TtyOptions.resolveDefaultTtyOptions());
+    public ProcessShellFactory(String ... command) {
+        this(GenericUtils.isEmpty(command) ? Collections.<String>emptyList() : Arrays.asList(command));
     }
 
-    public ProcessShellFactory(String[] command, Collection<TtyOptions> ttyOptions) {
-        this.command = command;
-        this.ttyOptions = GenericUtils.of(ttyOptions);
+    public ProcessShellFactory(List<String> command) {
+        this.command = ValidateUtils.checkNotNullAndNotEmpty(command, "No command");
     }
 
-    public String[] getCommand() {
+    public List<String> getCommand() {
         return command;
     }
 
-    public void setCommand(String[] command) {
-        this.command = command;
+    public void setCommand(String ... command) {
+        setCommand(GenericUtils.isEmpty(command) ? Collections.<String>emptyList() : Arrays.asList(command));
+    }
+
+    public void setCommand(List<String> command) {
+        this.command = ValidateUtils.checkNotNullAndNotEmpty(command, "No command");
     }
 
     @Override
@@ -62,6 +67,25 @@ public class ProcessShellFactory extends AbstractLoggingBean implements Factory<
     }
 
     protected InvertedShell createInvertedShell() {
-        return new ProcessShell(ttyOptions, getCommand());
+        return new ProcessShell(resolveEffectiveCommand(getCommand()));
+    }
+
+    protected List<String> resolveEffectiveCommand(List<String> original) {
+        if (!OsUtils.isWin32()) {
+            return original;
+        }
+
+        // Turns out that running a command with no arguments works just fine
+        if (GenericUtils.size(original) <= 1) {
+            return original;
+        }
+
+        // For windows create a "cmd.exe /C "..."" string
+        String cmdName = original.get(0);
+        if (OsUtils.WINDOWS_SHELL_COMMAND_NAME.equalsIgnoreCase(cmdName)) {
+            return original;    // assume callers knows what they're doing
+        }
+
+        return Arrays.asList(OsUtils.WINDOWS_SHELL_COMMAND_NAME, "/C", GenericUtils.join(original, ' '));
     }
 }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/97b73947/sshd-core/src/main/java/org/apache/sshd/server/shell/TtyFilterInputStream.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/shell/TtyFilterInputStream.java b/sshd-core/src/main/java/org/apache/sshd/server/shell/TtyFilterInputStream.java
index 3059c8c..840eca6 100644
--- a/sshd-core/src/main/java/org/apache/sshd/server/shell/TtyFilterInputStream.java
+++ b/sshd-core/src/main/java/org/apache/sshd/server/shell/TtyFilterInputStream.java
@@ -18,14 +18,16 @@
  */
 package org.apache.sshd.server.shell;
 
-import java.io.EOFException;
 import java.io.FilterInputStream;
 import java.io.IOException;
 import java.io.InputStream;
-import java.io.StreamCorruptedException;
 import java.util.Collection;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.Map;
 import java.util.Set;
 
+import org.apache.sshd.common.channel.PtyMode;
 import org.apache.sshd.common.util.GenericUtils;
 import org.apache.sshd.common.util.buffer.Buffer;
 import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
@@ -37,18 +39,21 @@ import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
  */
 public class TtyFilterInputStream extends FilterInputStream {
-    private final Set<TtyOptions> ttyOptions;
+    public static final Set<PtyMode> INPUT_OPTIONS =
+            Collections.unmodifiableSet(EnumSet.of(PtyMode.ONLCR, PtyMode.OCRNL, PtyMode.ONLRET, PtyMode.ONOCR));
+
+    private final Set<PtyMode> ttyOptions;
     private Buffer buffer = new ByteArrayBuffer(32);
     private int lastChar = -1;
 
-    public TtyFilterInputStream(InputStream in, Collection<TtyOptions> ttyOptions) {
+    public TtyFilterInputStream(InputStream in, Map<PtyMode, ?> modes) {
+        this(in, PtyMode.resolveEnabledOptions(modes, INPUT_OPTIONS));
+    }
+
+    public TtyFilterInputStream(InputStream in, Collection<PtyMode> ttyOptions) {
         super(in);
         // we create a copy of the options so as to avoid concurrent modifications
-        this.ttyOptions = GenericUtils.of(ttyOptions);
-
-        if (this.ttyOptions.contains(TtyOptions.LfOnlyInput) && this.ttyOptions.contains(TtyOptions.CrLfInput)) {
-            throw new IllegalArgumentException("Ambiguous TTY options: " + ttyOptions);
-        }
+        this.ttyOptions = GenericUtils.of(ttyOptions);  // TODO validate non-conflicting options
     }
 
     public synchronized void write(int c) {
@@ -56,11 +61,15 @@ public class TtyFilterInputStream extends FilterInputStream {
     }
 
     public synchronized void write(byte[] buf, int off, int len) {
-        buffer.putBytes(buf, off, len);
+        if (len == 1) {
+            write(buf[off] & 0xFF);
+        } else {
+            buffer.putBytes(buf, off, len);
+        }
     }
 
     @Override
-    public int available() throws IOException {
+    public synchronized int available() throws IOException {
         return super.available() + buffer.available();
     }
 
@@ -71,55 +80,87 @@ public class TtyFilterInputStream extends FilterInputStream {
             return c;
         }
 
-        if ((c == '\r') && ttyOptions.contains(TtyOptions.LfOnlyInput)) {
-            c = readRawInput();
-            if (c == -1) {
-                throw new EOFException("Premature EOF while waiting for LF after CR");
-            }
-
-            if (c != '\n') {
-                throw new StreamCorruptedException("CR not followed by LF");
-            }
-        }
-
-        if ((c == '\n') && ttyOptions.contains(TtyOptions.CrLfInput)) {
-            if (lastChar != '\r') {
-                c = '\r';
-                Buffer buf = new ByteArrayBuffer();
-                buf.putByte((byte) '\n');
-                buf.putBuffer(buffer);
-                buffer = buf;
-            }
+        if (c == '\r') {
+            c = handleCR();
+        } else if (c == '\n') {
+            c = handleLF();
         }
 
         lastChar = c;
         return c;
     }
 
+    protected int handleCR() throws IOException {
+        if (ttyOptions.contains(PtyMode.OCRNL)) {
+            return '\n';    // Translate carriage return to newline
+        } else {
+            return '\r';
+        }
+    }
+
+    protected int handleLF() throws IOException {
+        // Map NL to CR-NL.
+        if ((ttyOptions.contains(PtyMode.ONLCR) || ttyOptions.contains(PtyMode.ONOCR)) && (lastChar != '\r')) {
+            buffer = insertCharacter(buffer, '\n');
+            return '\r';
+        } else if (ttyOptions.contains(PtyMode.ONLRET)) {   // Newline performs a carriage return
+            return '\r';
+        } else {
+            return '\n';
+        }
+    }
+
+    // TODO add 'insertXXX' methods to the Buffer class
+    protected Buffer insertCharacter(Buffer org, int c) {
+        int remaining = org.capacity();
+        int readPos = org.rpos();
+        // see if can accommodate the character in the original buffer
+        if ((remaining > 0) && (readPos > 0)) {
+            int writePos = org.wpos();
+            org.wpos(readPos - 1);
+            org.putByte((byte) c);
+            org.wpos(writePos);
+            org.rpos(readPos - 1);
+            return org;
+        } else {
+            Buffer buf = new ByteArrayBuffer(org.available() + 1);
+            buf.putByte((byte) c);
+            buf.putBuffer(org);
+            return buf;
+        }
+
+    }
     protected int readRawInput() throws IOException {
-        // see if have any pending data
         if (buffer.available() > 0) {
-            int c = buffer.getUByte();
-            buffer.compact();
-            return c;
+            return buffer.getUByte();
+        } else {
+            return this.in.read();
         }
-
-        return super.read();
     }
 
     @Override
     public synchronized int read(byte[] b, int off, int len) throws IOException {
+        if (len == 1) {
+            int c = read();
+            if (c == -1) {
+                return -1;
+            }
+
+            b[off] = (byte) c;
+            return 1;
+        }
+
         if (buffer.available() == 0) {
-            int nb = super.read(b, off, len);
+            int nb = this.in.read(b, off, len);
             if (nb == -1) {
-                return -1;
+                return nb;
             }
             buffer.putRawBytes(b, off, nb);
         }
 
         int nb = 0;
-        while ((nb < len) && (buffer.available() > 0)) {
-            b[off + nb++] = (byte) read();
+        for (int curPos = off; (nb < len) && (buffer.available() > 0); nb++, curPos++) {
+            b[curPos] = (byte) read();
         }
 
         return nb;

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/97b73947/sshd-core/src/main/java/org/apache/sshd/server/shell/TtyFilterOutputStream.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/shell/TtyFilterOutputStream.java b/sshd-core/src/main/java/org/apache/sshd/server/shell/TtyFilterOutputStream.java
index 010eb28..f4c4988 100644
--- a/sshd-core/src/main/java/org/apache/sshd/server/shell/TtyFilterOutputStream.java
+++ b/sshd-core/src/main/java/org/apache/sshd/server/shell/TtyFilterOutputStream.java
@@ -22,59 +22,105 @@ import java.io.FilterOutputStream;
 import java.io.IOException;
 import java.io.OutputStream;
 import java.util.Collection;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.Map;
 import java.util.Set;
 
+import org.apache.sshd.common.channel.PtyMode;
 import org.apache.sshd.common.util.GenericUtils;
 import org.apache.sshd.common.util.ValidateUtils;
 
 /**
  * Handles the output stream while taking care of the {@link TtyOptions} for CR / LF
- * and ECHO settings. <B>Note:</B> does not close the echo stream when filter stream is closed
+ * and ECHO settings
  *
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
  */
 public class TtyFilterOutputStream extends FilterOutputStream {
-    private final Set<TtyOptions> ttyOptions;
-    private TtyFilterInputStream echo;
-    private int lastChar = -1;
+    public static final Set<PtyMode> OUTPUT_OPTIONS =
+            Collections.unmodifiableSet(EnumSet.of(PtyMode.ECHO, PtyMode.INLCR, PtyMode.ICRNL, PtyMode.IGNCR));
 
-    public TtyFilterOutputStream(OutputStream out, TtyFilterInputStream echo, Collection<TtyOptions> ttyOptions) {
+    private final Set<PtyMode> ttyOptions;
+    private final TtyFilterInputStream echo;
+
+    public TtyFilterOutputStream(OutputStream out, TtyFilterInputStream echo, Map<PtyMode, ?> modes) {
+        this(out, echo, PtyMode.resolveEnabledOptions(modes, OUTPUT_OPTIONS));
+    }
+
+    public TtyFilterOutputStream(OutputStream out, TtyFilterInputStream echo, Collection<PtyMode> ttyOptions) {
         super(out);
         // we create a copy of the options so as to avoid concurrent modifications
-        this.ttyOptions = GenericUtils.of(ttyOptions);
-        if (this.ttyOptions.contains(TtyOptions.LfOnlyOutput) && this.ttyOptions.contains(TtyOptions.CrLfOutput)) {
-            throw new IllegalArgumentException("Ambiguous TTY options: " + this.ttyOptions);
-        }
-
-        this.echo = this.ttyOptions.contains(TtyOptions.Echo) ? ValidateUtils.checkNotNull(echo, "No echo stream") : echo;
+        this.ttyOptions = GenericUtils.of(ttyOptions);    // TODO validate non-conflicting options
+        this.echo = this.ttyOptions.contains(PtyMode.ECHO) ? ValidateUtils.checkNotNull(echo, "No echo stream") : echo;
     }
 
     @Override
     public void write(int c) throws IOException {
-        if ((c == '\r') && ttyOptions.contains(TtyOptions.LfOnlyOutput)) {
-            lastChar = c;
-            return;
+        if (c == '\r') {
+            handleCR();
+        } else if (c == '\n') {
+            handleLF();
+        } else {
+            writeRawOutput(c);
         }
+    }
 
-        if ((c == '\n') && ttyOptions.contains(TtyOptions.CrLfOutput) && (lastChar != '\r')) {
+    protected void handleCR() throws IOException {
+        if (ttyOptions.contains(PtyMode.ICRNL)) {
+            writeRawOutput('\n');   // Map CR to NL on input
+        } else if (ttyOptions.contains(PtyMode.IGNCR)) {
+            return;    // Ignore CR on input
+        } else {
             writeRawOutput('\r');
         }
+    }
 
-        writeRawOutput(c);
+    protected void handleLF() throws IOException {
+        if (ttyOptions.contains(PtyMode.INLCR)) {
+            writeRawOutput('\r');   // Map NL into CR on input
+        } else {
+            writeRawOutput('\n');
+        }
     }
 
     protected void writeRawOutput(int c) throws IOException {
-        lastChar = c;
-        super.write(c);
-        if (ttyOptions.contains(TtyOptions.Echo)) {
+        this.out.write(c);
+        if (ttyOptions.contains(PtyMode.ECHO)) {
             echo.write(c);
         }
     }
 
     @Override
     public void write(byte[] b, int off, int len) throws IOException {
-        for (int curPos = off, l = 0; l < len; curPos++, l++) {
-            write(b[curPos]);
+        if (len == 1) {
+            write(b[off] & 0xFF);
+            return;
+        }
+
+        int lastPos = 0;
+        int maxPos = off + len;
+        for (int curPos = off; curPos < maxPos; curPos++) {
+            int c = b[curPos] & 0xFF;
+            if ((c == '\r') || (c == '\n')) {
+                if (lastPos < curPos) { // No CR or LF in this segment
+                    writeRawOutput(b, lastPos, curPos - lastPos);
+                }
+
+                lastPos = curPos + 1;   // prepare for next character
+                write(c);
+            }
+        }
+
+        if (lastPos < maxPos) { // No CR or LF in this segment
+            writeRawOutput(b, lastPos, maxPos - lastPos);
+        }
+    }
+
+    protected void writeRawOutput(byte[] b, int off, int len) throws IOException {
+        this.out.write(b, off, len);
+        if (ttyOptions.contains(PtyMode.ECHO)) {
+            echo.write(b, off, len);
         }
     }
 }
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/97b73947/sshd-core/src/main/java/org/apache/sshd/server/shell/TtyOptions.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/shell/TtyOptions.java b/sshd-core/src/main/java/org/apache/sshd/server/shell/TtyOptions.java
deleted file mode 100644
index d0192e8..0000000
--- a/sshd-core/src/main/java/org/apache/sshd/server/shell/TtyOptions.java
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * 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.shell;
-
-import java.util.Collections;
-import java.util.EnumSet;
-import java.util.Set;
-
-import org.apache.sshd.common.util.OsUtils;
-
-/**
- * Options controlling the I/O streams behavior regarding CR/LF and echoing
- * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
- */
-public enum TtyOptions {
-    /**
-     * Echo the output
-     */
-    Echo,
-    /**
-     * Convert CRLF to LF if not already such when writing to output stream
-     */
-    LfOnlyOutput,
-    /**
-     * Convert LF to CRLF if not already such when writing to output stream
-     */
-    CrLfOutput,
-    /**
-     * Convert LF to CRLF if not already such when reading from input stream
-     */
-    CrLfInput,
-    /**
-     * Convert CRLF to LF if not already such when reading from input stream
-     */
-    LfOnlyInput;
-
-    public static final Set<TtyOptions> LINUX_OPTIONS =
-            Collections.unmodifiableSet(EnumSet.of(TtyOptions.LfOnlyOutput, TtyOptions.LfOnlyInput));
-
-    /*
-     * NOTE !!! the assumption is that a Windows server is writing to a Linux
-     * client (the most likely) thus it expects CRLF on input, but outputs LF only
-     */
-    public static final Set<TtyOptions> WINDOWS_OPTIONS =
-            Collections.unmodifiableSet(EnumSet.of(TtyOptions.Echo, TtyOptions.LfOnlyOutput, TtyOptions.CrLfInput));
-
-    public static Set<TtyOptions> resolveDefaultTtyOptions() {
-        return resolveTtyOptions(OsUtils.isWin32());
-    }
-
-    public static Set<TtyOptions> resolveTtyOptions(boolean isWin32) {
-        if (isWin32) {
-            return WINDOWS_OPTIONS;
-        } else {
-            return LINUX_OPTIONS;
-        }
-    }
-}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/97b73947/sshd-core/src/test/java/org/apache/sshd/common/channel/PtyModeTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/common/channel/PtyModeTest.java b/sshd-core/src/test/java/org/apache/sshd/common/channel/PtyModeTest.java
new file mode 100644
index 0000000..a61f924
--- /dev/null
+++ b/sshd-core/src/test/java/org/apache/sshd/common/channel/PtyModeTest.java
@@ -0,0 +1,63 @@
+/*
+ * 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.common.channel;
+
+import java.util.EnumSet;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.util.test.BaseTestSupport;
+import org.junit.FixMethodOrder;
+import org.junit.Test;
+import org.junit.runners.MethodSorters;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+public class PtyModeTest extends BaseTestSupport {
+    public PtyModeTest() {
+        super();
+    }
+
+    @Test
+    public void testFromInt() {
+        for (PtyMode expected : PtyMode.MODES) {
+            int num = expected.toInt();
+            PtyMode actual = PtyMode.fromInt(num);
+            assertSame("Mismatched result for value=" + num, expected, actual);
+        }
+    }
+
+    @Test
+    public void testEnabledOptions() {
+        Set<PtyMode> expected = EnumSet.of(PtyMode.ECHO, PtyMode.CS8, PtyMode.ICANON);
+        Map<PtyMode, Integer> modes = PtyMode.createEnabledOptions(expected);
+        assertEquals("Mismatched modes size", expected.size(), GenericUtils.size(modes));
+
+        for (PtyMode m : expected) {
+            assertSame("Mismatched setting for " + m, PtyMode.TRUE_SETTING, modes.get(m));
+        }
+
+        Set<PtyMode> actual = PtyMode.resolveEnabledOptions(modes, expected);
+        assertEquals("Mismatched enabled options", expected, actual);
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/97b73947/sshd-core/src/test/java/org/apache/sshd/server/channel/ChannelSessionTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/server/channel/ChannelSessionTest.java b/sshd-core/src/test/java/org/apache/sshd/server/channel/ChannelSessionTest.java
index 3c86a32..97c5cc3 100644
--- a/sshd-core/src/test/java/org/apache/sshd/server/channel/ChannelSessionTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/server/channel/ChannelSessionTest.java
@@ -19,7 +19,6 @@
 package org.apache.sshd.server.channel;
 
 import java.io.IOException;
-import java.util.Collection;
 import java.util.Collections;
 import java.util.concurrent.atomic.AtomicBoolean;
 
@@ -27,8 +26,6 @@ import org.apache.sshd.common.PropertyResolverUtils;
 import org.apache.sshd.common.channel.ChannelAsyncOutputStream;
 import org.apache.sshd.common.util.buffer.Buffer;
 import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
-import org.apache.sshd.server.Signal;
-import org.apache.sshd.server.SignalListener;
 import org.apache.sshd.util.test.BaseTestSupport;
 import org.apache.sshd.util.test.BogusChannel;
 import org.junit.FixMethodOrder;
@@ -66,23 +63,4 @@ public class ChannelSessionTest extends BaseTestSupport {
             assertTrue("Expanded ?", expanded.get());
         }
     }
-
-    @Test
-    public void testAddSignalListenerOnDuplicateSignals() {
-        ChannelSession.StandardEnvironment environ = new ChannelSession.StandardEnvironment();
-        SignalListener listener = new SignalListener() {
-            @Override
-            public void signal(Signal signal) {
-                // ignored
-            }
-        };
-
-        for (Signal s : Signal.SIGNALS) {
-            environ.addSignalListener(listener, s, s, s, s, s, s);
-
-            Collection<SignalListener> ls = environ.getSignalListeners(s, false);
-            int numListeners = (ls == null) ? 0 : ls.size();
-            assertEquals("Mismatched registered listeners count for signal=" + s, 1, numListeners);
-        }
-    }
 }
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/97b73947/sshd-core/src/test/java/org/apache/sshd/server/shell/InvertedShellWrapperTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/server/shell/InvertedShellWrapperTest.java b/sshd-core/src/test/java/org/apache/sshd/server/shell/InvertedShellWrapperTest.java
index e74cb08..6632965 100644
--- a/sshd-core/src/test/java/org/apache/sshd/server/shell/InvertedShellWrapperTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/server/shell/InvertedShellWrapperTest.java
@@ -25,8 +25,8 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.nio.charset.StandardCharsets;
-import java.util.Map;
 
+import org.apache.sshd.server.Environment;
 import org.apache.sshd.util.test.BaseTestSupport;
 import org.apache.sshd.util.test.BogusEnvironment;
 import org.apache.sshd.util.test.BogusExitCallback;
@@ -80,7 +80,7 @@ public class InvertedShellWrapperTest extends BaseTestSupport {
             private boolean destroyed;
 
             @Override
-            public void start(Map<String, String> env) throws IOException {
+            public void start(Environment env) throws IOException {
                 bogusShell.start(env);
             }
 
@@ -172,7 +172,7 @@ public class InvertedShellWrapperTest extends BaseTestSupport {
 
             InvertedShell shell = new InvertedShell() {
                 @Override
-                public void start(Map<String, String> env) throws IOException {
+                public void start(Environment env) throws IOException {
                     // ignored
                 }
 

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/97b73947/sshd-core/src/test/java/org/apache/sshd/server/shell/TtyFilterInputStreamTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/server/shell/TtyFilterInputStreamTest.java b/sshd-core/src/test/java/org/apache/sshd/server/shell/TtyFilterInputStreamTest.java
index e3b1e56..b819984 100644
--- a/sshd-core/src/test/java/org/apache/sshd/server/shell/TtyFilterInputStreamTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/server/shell/TtyFilterInputStreamTest.java
@@ -19,105 +19,114 @@
 
 package org.apache.sshd.server.shell;
 
-import java.io.BufferedReader;
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
-import java.io.InputStreamReader;
+import java.io.OutputStream;
 import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.Date;
+import java.util.EnumSet;
 import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.sshd.common.channel.PtyMode;
 import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.ValidateUtils;
+import org.apache.sshd.common.util.io.IoUtils;
 import org.apache.sshd.util.test.BaseTestSupport;
 import org.junit.FixMethodOrder;
 import org.junit.Test;
+import org.junit.runner.RunWith;
 import org.junit.runners.MethodSorters;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
 
 /**
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
  */
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
+@RunWith(Parameterized.class)   // see https://github.com/junit-team/junit/wiki/Parameterized-tests
 public class TtyFilterInputStreamTest extends BaseTestSupport {
-    public TtyFilterInputStreamTest() {
-        super();
-    }
+    private static final List<PtyMode> MODES =
+            Collections.unmodifiableList(new ArrayList<PtyMode>(TtyFilterInputStream.INPUT_OPTIONS.size() + 2) {
+                private static final long serialVersionUID = 1L;    // we-re not serializing it
 
-    @Test
-    public void testLfOnlyStream() throws IOException {
-        List<String> expected = createTestLines();
-        List<String> actual = new ArrayList<>(expected.size());
-        byte[] data = GenericUtils.join(expected, "\r\n").getBytes(StandardCharsets.UTF_8);
-        try (ByteArrayInputStream bais = new ByteArrayInputStream(data);
-             TtyFilterInputStream tty = new TtyFilterInputStream(bais, Collections.singleton(TtyOptions.LfOnlyInput)) {
-                 private long offset;
-
-                 @Override
-                 public synchronized int read() throws IOException {
-                     int c = super.read();
-                     if (c == -1) {
-                         return -1;
-                     }
-
-                     offset++;
-                     if (c == '\n') {
-                         offset++;  // compensate for CR filtering
-                     }
-                     assertFalse("Unexpected CR at offset=" + offset, c == '\r');
-                     return c;
-                 }
-             };
-             BufferedReader rdr = new BufferedReader(new InputStreamReader(tty, StandardCharsets.UTF_8))) {
-
-            for (String line = rdr.readLine(); line != null; line = rdr.readLine()) {
-                actual.add(line);
-            }
-        }
+                {
+                    add(PtyMode.ECHO);  // NO-OP
+
+                    for (PtyMode m : TtyFilterInputStream.INPUT_OPTIONS) {
+                        add(m);
+                    }
+                }
+            });
 
-        assertListEquals("Mismatched lines", expected, actual);
+    private final PtyMode mode;
+
+    public TtyFilterInputStreamTest(PtyMode mode) {
+        this.mode = ValidateUtils.checkNotNull(mode, "No test modes");
+    }
+
+    @Parameters(name = "mode={0}")
+    public static Collection<Object[]> parameters() {
+        return parameterize(MODES);
     }
 
     @Test
-    public void testCrLfStream() throws IOException {
-        List<String> expected = createTestLines();
-        List<String> actual = new ArrayList<>(expected.size());
-        byte[] data = GenericUtils.join(expected, '\n').getBytes(StandardCharsets.UTF_8);
-        try (ByteArrayInputStream bais = new ByteArrayInputStream(data);
-             TtyFilterInputStream tty = new TtyFilterInputStream(bais, Collections.singleton(TtyOptions.CrLfInput)) {
-                 private long offset;
-                 private int lastChar = -1;
-
-                 @Override
-                 public synchronized int read() throws IOException {
-                     int c = super.read();
-                     if (c == -1) {
-                         return -1;
-                     }
-
-                     if (c != '\r') {
-                         offset++;
-                     }
-
-                     if (c == '\n') {
-                         assertEquals("LF not preceded by CR at offset=" + offset, '\r', lastChar);
-                     }
-
-                     lastChar = c;
-                     return c;
-                 }
-             };
-             BufferedReader rdr = new BufferedReader(new InputStreamReader(tty, StandardCharsets.UTF_8))) {
-
-            for (String line = rdr.readLine(); line != null; line = rdr.readLine()) {
-                actual.add(line);
+    public void testCRLFHandling() throws IOException {
+        List<String> lines = Arrays.asList(getClass().getPackage().getName(),
+                getClass().getSimpleName(), getCurrentTestName(),
+                "(" + mode + ")", new Date(System.currentTimeMillis()).toString());
+        String content = GenericUtils.join(lines, "\r\n");
+        try (ByteArrayInputStream bais = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8));
+             TtyFilterInputStream tty = new TtyFilterInputStream(bais, EnumSet.of(mode))) {
+            final AtomicInteger crCount = new AtomicInteger(0);
+            final AtomicInteger lfCount = new AtomicInteger(0);
+
+            try (OutputStream output = new OutputStream() {
+                @Override
+                public void write(int b) throws IOException {
+                    if (b == '\r') {
+                        crCount.incrementAndGet();
+                    } else if (b == '\n') {
+                        lfCount.incrementAndGet();
+                    }
+                }
+            }) {
+                long copySize = IoUtils.copy(tty, output);
+                assertTrue("Copy size (" + copySize + ") above total length (" + content.length() + ")", copySize <= content.length());
             }
-        }
 
-        assertListEquals("Mismatched lines", expected, actual);
+            assertCRLFCounts(mode, lines.size() - 1 /* last line has no NL */, crCount.get(), lfCount.get());
+        }
     }
 
-    private List<String> createTestLines() {
-        return Arrays.asList(getClass().getPackage().getName(), getClass().getSimpleName(), getCurrentTestName(), new Date(System.currentTimeMillis()).toString());
+    private static void assertCRLFCounts(PtyMode mode, int numLines, int crCount, int lfCount) {
+        switch(mode) {
+            case ECHO:
+            case ONLCR:
+            case ONOCR:
+                // No modifications
+                assertEquals("Mismatched CR count", numLines, crCount);
+                assertEquals("Mismatched LF count", numLines, lfCount);
+                break;
+
+            case OCRNL:
+                // Translate carriage return to newline
+                assertEquals("Mismatched CR count", 0, crCount);
+                assertEquals("Mismatched LF count", 2 * numLines, lfCount);
+                break;
+
+            case ONLRET:
+                // Newline performs a carriage return
+                assertEquals("Mismatched CR count", 2 * numLines, crCount);
+                assertEquals("Mismatched LF count", 0, lfCount);
+                break;
+
+            default:
+                fail("Unsupported mode: " + mode);
+        }
     }
 }