FELIX-1212: Possible infinite loop when a remote ssh connection is closed

git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@783254 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/karaf/gshell/gshell-core/src/main/java/org/apache/geronimo/gshell/commands/ssh/ShellFactoryImpl.java b/karaf/gshell/gshell-core/src/main/java/org/apache/geronimo/gshell/commands/ssh/ShellFactoryImpl.java
index 62c9bbc..d173632 100644
--- a/karaf/gshell/gshell-core/src/main/java/org/apache/geronimo/gshell/commands/ssh/ShellFactoryImpl.java
+++ b/karaf/gshell/gshell-core/src/main/java/org/apache/geronimo/gshell/commands/ssh/ShellFactoryImpl.java
@@ -196,9 +196,11 @@
         }
 
         public void close() {
-            closed = true;
-            Closer.close(in, out, err);
-            callback.onExit(0);
+            if (!closed) {
+                closed = true;
+                Closer.close(in, out, err);
+                callback.onExit(0);
+            }
         }
 
         public boolean isInteractive() {
diff --git a/karaf/gshell/gshell-core/src/main/java/org/apache/geronimo/gshell/console/Console.java b/karaf/gshell/gshell-core/src/main/java/org/apache/geronimo/gshell/console/Console.java
new file mode 100644
index 0000000..518b019
--- /dev/null
+++ b/karaf/gshell/gshell-core/src/main/java/org/apache/geronimo/gshell/console/Console.java
@@ -0,0 +1,243 @@
+/*
+ * 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.geronimo.gshell.console;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+
+/**
+ * Provides an abstraction of a console.
+ *
+ * @version $Rev: 706594 $ $Date: 2008-10-21 14:22:21 +0200 (Tue, 21 Oct 2008) $
+ */
+public abstract class Console
+    implements Runnable
+{
+    protected final Logger log = LoggerFactory.getLogger(getClass());
+
+    protected boolean running = false;
+
+    protected boolean breakOnNull = true;
+
+    protected boolean autoTrim = true;
+
+    protected boolean ignoreEmpty = true;
+
+    protected Prompter prompter = new Prompter() {
+        public String prompt() {
+            return "> ";
+        }
+    };
+
+    protected Executor executor;
+
+    protected ErrorHandler errorHandler = new ErrorHandler() {
+        public Result handleError(Throwable error) {
+            return Result.STOP;
+        }
+    };
+
+    public Console(final Executor executor) {
+        assert executor != null;
+
+        this.executor = executor;
+    }
+
+    public boolean isRunning() {
+        return running;
+    }
+
+    public void setRunning(final boolean running) {
+        this.running = running;
+    }
+
+    public boolean isBreakOnNull() {
+        return breakOnNull;
+    }
+
+    public void setBreakOnNull(final boolean breakOnNull) {
+        this.breakOnNull = breakOnNull;
+    }
+
+    public boolean isAutoTrim() {
+        return autoTrim;
+    }
+
+    public void setAutoTrim(final boolean autoTrim) {
+        this.autoTrim = autoTrim;
+    }
+
+    public boolean isIgnoreEmpty() {
+        return ignoreEmpty;
+    }
+
+    public void setIgnoreEmpty(final boolean ignoreEmpty) {
+        this.ignoreEmpty = ignoreEmpty;
+    }
+
+    public ErrorHandler getErrorHandler() {
+        return errorHandler;
+    }
+
+    public void setErrorHandler(final ErrorHandler errorHandler) {
+        this.errorHandler = errorHandler;
+    }
+
+    public Prompter getPrompter() {
+        return prompter;
+    }
+
+    public void setPrompter(final Prompter prompter) {
+        this.prompter = prompter;
+    }
+
+    public Executor getExecutor() {
+        return executor;
+    }
+
+    public void setExecutor(final Executor executor) {
+        this.executor = executor;
+    }
+
+    public void run() {
+        log.debug("Running");
+
+        running = true;
+
+        while (running) {
+            try {
+                running = work();
+            }
+            catch (Throwable t) {
+                // Don't use {} here so we get the throwable detail in the log stream
+                log.debug("Work failed: " + t, t);
+
+                if (errorHandler != null) {
+                    ErrorHandler.Result result = errorHandler.handleError(t);
+
+                    // Allow the error handler to request that the loop stop
+                    if (result == ErrorHandler.Result.STOP) {
+                        log.debug("Error handler requested STOP");
+                        running = false;
+                    }
+                }
+            }
+        }
+
+        log.debug("Finished");
+    }
+
+    protected boolean work() throws Exception {
+        String line;
+
+        try {
+            line = readLine(prompter.prompt());
+        } catch (Throwable t) {
+            log.debug("Closing shell due to an exception while reading input: " + t, t);
+            return false;
+        }
+
+        // Stop on null (maybe, else ignore)
+        if (line == null) {
+            return !breakOnNull;
+        }
+
+        log.debug("Read line: {}", line);
+
+        // Log the line as HEX if trace is enabled
+        if (log.isTraceEnabled()) {
+            StringBuilder idx = new StringBuilder();
+            StringBuilder hex = new StringBuilder();
+
+            byte[] bytes = line.getBytes();
+            for (byte b : bytes) {
+                String h = Integer.toHexString(b);
+
+                hex.append("x").append(h).append(" ");
+                idx.append(" ").append((char)b).append("  ");
+            }
+
+            log.trace("HEX: {}", hex);
+            log.trace("     {}", idx);
+        }
+
+        // Auto trim the line (maybe)
+        if (autoTrim) {
+            line = line.trim();
+        }
+
+        // Ingore empty lines (maybe)
+        if (ignoreEmpty && line.length() == 0) {
+            return true;
+        }
+
+        // Execute the line
+        Executor.Result result = executor.execute(line);
+
+        // Allow executor to request that the loop stop
+        if (result == Executor.Result.STOP) {
+            log.debug("Executor requested STOP");
+            return false;
+        }
+
+        return true;
+    }
+
+    protected abstract String readLine(String prompt) throws IOException;
+
+    //
+    // Prompter
+    //
+
+    public static interface Prompter
+    {
+        String prompt();
+    }
+
+    //
+    // Executor
+    //
+
+    public static interface Executor
+    {
+        enum Result {
+            CONTINUE,
+            STOP
+        }
+
+        Result execute(String line) throws Exception;
+    }
+
+    //
+    // ErrorHandler
+    //
+
+    public static interface ErrorHandler
+    {
+        enum Result {
+            CONTINUE,
+            STOP
+        }
+
+        Result handleError(Throwable error);
+    }
+}