# Copyright (c) 2007, 2015, Oracle and/or its affiliates. All rights reserved.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; version 2 of the
# License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
# 02110-1301  USA

 # import platform before paramiko to avoid random import error in osx 10.6
import platform
try:
    platform.system()
except IOError, err:
    if err.errno == 4:
        print "platform.system() exception detected, trying workaround..."
        # random bizarre platform.system() bug detected, try again after 3s
        import time
        time.sleep(3)
        platform.system()

import os
import errno
import stat
import mforms
import time
import traceback

from wb_common import PermissionDeniedError, InvalidPasswordError, OperationCancelledError, Users
from workbench.utils import server_version_str2tuple
from wb_common import CmdOptions, CmdOutput, SSHFingerprintNewError, format_bad_host_exception

from workbench.log import log_info, log_warning, log_error, log_debug, log_debug2, log_debug3

try:
    import paramiko
    import socket
    
    class StoreIfConfirmedPolicy(paramiko.MissingHostKeyPolicy):
        def missing_host_key(self, client, hostname, key):
            raise SSHFingerprintNewError("Key mismatched", client, hostname, key)
        
    # paramiko 1.6 didn't have this class
    if hasattr(paramiko, "WarningPolicy"):
        WarningPolicy = paramiko.WarningPolicy
    else:
        class WarningPolicy(paramiko.MissingHostKeyPolicy):
            def missing_host_key(self, client, hostname, key):
                import binascii
                log_warning('WARNING: Unknown %s host key for %s: %s\n' % (key.get_name(), hostname, binascii.hexlify(key.get_fingerprint())))

    
except:
    traceback.print_exc()
    # temporary workaround
    paramiko = None
    socket = None


def normalize_windows_path_for_ftp(path):
    idx = -1
    if platform.system().lower() != 'windows':
        idx = path.find(':')

    # Gets rid of the unit from the path
    if idx != -1:
        path = path[idx + 1 :]

    # Replaces the \\ to //
    path = path.replace('\\', '/')

    return path


if paramiko and server_version_str2tuple(paramiko.__version__) >= (1, 7, 4):
    from paramiko.message import Message
    from paramiko.common import MSG_CHANNEL_OPEN
    from paramiko.channel import Channel
    from paramiko.ssh_exception import SSHException 
    import threading
    OPEN_CHANNEL_TIMEOUT = 15

    def wba_open_channel(self, kind, dest_addr=None, src_addr=None, timeout = None, window_size=None, max_packet_size=None):
        chan = None
        if not self.active:
            # don't bother trying to allocate a channel
            return None
        self.lock.acquire()
        try:
            
            chanid = self._next_channel()
            m = Message()
            m.add_byte(chr(MSG_CHANNEL_OPEN))
            m.add_string(kind)
            m.add_int(chanid)
            local_window_size = None
            local_max_packet_size = None
            if (server_version_str2tuple(paramiko.__version__) < (1, 15, 0)):
                local_window_size = self.window_size
                local_max_packet_size = self.max_packet_size
            else:
                if window_size is not None:
                    local_window_size = self._sanitize_window_size(window_size)
                else:
                    local_window_size = self.default_window_size
                
                if max_packet_size is not None:
                    local_max_packet_size = self._sanitize_packet_size(max_packet_size)
                else:
                    local_max_packet_size = self.default_max_packet_size

            m.add_int(local_window_size)
            m.add_int(local_max_packet_size)
            if (kind == 'forwarded-tcpip') or (kind == 'direct-tcpip'):
                m.add_string(dest_addr[0])
                m.add_int(dest_addr[1])
                m.add_string(src_addr[0])
                m.add_int(src_addr[1])
            elif kind == 'x11':
                m.add_string(src_addr[0])
                m.add_int(src_addr[1])
            chan = Channel(chanid)
            self._channels.put(chanid, chan)
            self.channel_events[chanid] = event = threading.Event()
            self.channels_seen[chanid] = True
            chan._set_transport(self)
            chan._set_window(local_window_size, local_max_packet_size)
        finally:
            self.lock.release()
        self._send_user_message(m)
        ts = time.time() + OPEN_CHANNEL_TIMEOUT if (timeout is None) else timeout
        while True:
            event.wait(0.1);
            if not self.active:
                e = self.get_exception()
                if e is None:
                    e = SSHException('Unable to open SSH channel.')
                raise e
            if event.isSet():
                break
            if time.time() > ts:
                chan.close()
                chan = None  # TODO: Check if clean up from self._channels is needed
                event.clear()
                self._channels.delete(chanid)
                if chanid in self.channel_events:
                    del self.channel_events[chanid]
                raise IOError("open SSH channel timeout")
        chan = self._channels.get(chanid)
        if chan is not None:
            return chan
        e = self.get_exception()
        if e is None:
            e = SSHException('Unable to open SSH channel.')
        raise e

    paramiko.Transport.open_channel = wba_open_channel
else:
    print "Warning! Can't use connect with timeout in paramiko", paramiko and paramiko.__version__
    if paramiko:
        log_warning('Cannot use connect with timeout in paramiko version %s\n' % paramiko.__version__)
    else:
        log_warning('Paramiko unavailable.\n')

#===============================================================================
class ConnectionError(Exception):
    pass


#===============================================================================
class SSHDownException(Exception):
    pass

#===============================================================================
#
#===============================================================================
class WbAdminSFTP(object):
    def __init__(self, sftp):
        self.sftp = sftp

    #-----------------------------------------------------------------------------
    def pwd(self):
        ret = None
        if self.sftp:
            try:
                ret = self.sftp.getcwd()

                if ret is None:
                    self.sftp.chdir(".")
                    ret = self.sftp.getcwd()

                if ret is None:
                    ret = ''
            except IOError, e:
                print e

        return ret

    #-----------------------------------------------------------------------------
    def ls(self, path, include_size=False):
        path = normalize_windows_path_for_ftp(path)
        ret = ((),())
        if self.sftp:
            fnames = ()
            fattrs = ()
            try:
                fnames = self.sftp.listdir(path)
                fattrs = self.sftp.listdir_attr(path)
            except IOError, e:
                if e.errno == errno.ENOENT and path.strip(" \r\t\n") == ".":
                    raise
                ret = (('Failed to read directory content',),())

            if len(fnames) > 0 and len(fnames) == len(fattrs):
                dirs = []
                rest = []
                for i in range(0, len(fnames)):
                    attr = fattrs[i]
                    if stat.S_ISDIR(attr.st_mode):
                        dirs.append((attr.filename, 0) if include_size else attr.filename)
                    elif stat.S_ISREG(attr.st_mode):
                        rest.append((attr.filename, attr.st_size) if include_size else attr.filename)
                    else:
                        rest.append((attr.filename, attr.st_size) if include_size else attr.filename)
                if include_size:
                    dirs.sort(lambda a,b: cmp(a[0], b[0]))
                    rest.sort(lambda a,b: cmp(a[0], b[0]))
                else:
                    dirs.sort()
                    rest.sort()
                ret = (tuple(dirs), tuple(rest))
        return ret

    #-----------------------------------------------------------------------------
    def cd(self, path):
        path = normalize_windows_path_for_ftp(path)
        ret = False
        if self.sftp:
            try:
                self.sftp.chdir(path)
                ret = True
            except IOError:
                ret = False

        return ret

    #-----------------------------------------------------------------------------
    def close(self):
        if self.sftp:
            self.sftp.close()

#===============================================================================
#
#===============================================================================
class WbAdminSSH(object):
    client = None
    def wrapped_connect(self, settings, password_delegate):
        assert hasattr(password_delegate, "get_password_for")
        assert hasattr(password_delegate, "reset_password_for")

        log_debug2("%s: starting connect\n" % self.__class__.__name__)
        self.client = None

        #loginInfo = settings.loginInfo
        #serverInfo = settings.serverInfo

        host = settings.ssh_hostname #loginInfo['ssh.hostName']
        usekey = settings.ssh_usekey #int(loginInfo['ssh.useKey'])
        pwd = None # It is ok to keep pwd set to None even if we have it in server settings
                   # it will be retrived later
        port = settings.ssh_port#loginInfo['ssh.port']
        self.keepalive = settings.ssh_keepalive
        self.ssh_timeout = settings.ssh_timeout
        if usekey == 1:
            # We need to check if keyfile needs password. For some reason paramiko does not always
            # throw exception to request password
            key_filename = settings.ssh_key #loginInfo['ssh.key']
            if key_filename.startswith('~'):
                key_filename = os.path.expanduser(key_filename)
            f = None
            try:
                f = open(key_filename, 'r')
            except IOError:
                f = None # set file handle to None indicating that open failed

            keycont = None # Will hold contents of the keyfile
            if f is not None:
                keycont = f.read()
                f.close()
            else:
                usekey = 0 # Reset usekey to 0 so paramiko will not use non-existent key file
                key_filename = None

            if usekey == 0:
                # We need password for password ssh auth as keyfile is missing. Retrieve password or ask for it
                pwd = password_delegate.get_password_for("ssh")
                #accepted, pwd = mforms.Utilities.find_or_ask_for_password("SSH key file missing. Remote SSH Login (%s)" % serverInfo['sys.system'], "ssh@%s:%s" % (host,port or 22), loginInfo['ssh.userName'], False)
            elif keycont is not None:
                if 'ENCRYPTED' in keycont:
                    # Retrieve password or ask for it
                    try:
                        pwd = password_delegate.get_password_for("sshkey")
                    except OperationCancelledError, exc:
                        # SSH key usage cancelled, just login normally
                        usekey = 0
                        pwd = password_delegate.get_password_for("ssh")
                    #accepted, pwd = mforms.Utilities.find_or_ask_for_password("Unlock SSH Private Key", "ssh_keyfile@%s"%key_filename, loginInfo['ssh.userName'], False)
                #else:
                #  accepted = True

            #if not accepted:
            #  raise OperationCancelledError("Cancelled key password input")

            try:
                self.connect(host, port, settings.ssh_username, pwd, usekey, key_filename)
            except InvalidPasswordError, exc:
                if usekey:
                    password_delegate.reset_password_for("sshkey")
                else:
                    password_delegate.reset_password_for("ssh")
                raise exc
            except paramiko.PasswordRequiredException:
                pwd = password_delegate.get_password_for("sshkey")
                self.connect(host, port, settings.ssh_username, pwd, usekey, key_filename)
                # Retry key pwd
        else:
            #if pwd is None:
            #  accepted, pwd = mforms.Utilities.find_or_ask_for_password("Remote SSH Login (%s)" % serverInfo['sys.system'], "ssh@%s:%s" % (host,port or 22), loginInfo['ssh.userName'], False)
            pwd = password_delegate.get_password_for("ssh")

            #if accepted:
            try:
                self.connect(host, port, settings.ssh_username, pwd, None, None)
            except InvalidPasswordError, exc:
                password_delegate.reset_password_for("ssh")
                raise exc
            #else:
            #  raise OperationCancelledError("Cancelled login")
        log_debug2("%s: Leave\n" % self.__class__.__name__)

    def _get_ssh_config_path(self):
        paths = []
        if platform.system().lower() == "windows":
            paths.append("%s\ssh\config" % mforms.App.get().get_user_data_folder())
            paths.append("%s\ssh\ssh_config" % mforms.App.get().get_user_data_folder())
        else:
            paths.append("~/.ssh/config")
            paths.append("~/.ssh/ssh_config")
            
        for path in paths:
            if os.path.isfile(os.path.expanduser(path)):
                return os.path.expanduser(path)
        else:
            log_debug3("ssh config file not found")
            return None

    def connect(self, host, port, user, pwd, usekey = 0, key = None):
        if port == None or port == 0:
            port = 22

        if not paramiko:
            raise Exception("One of the modules necessary for SSH base administration could not be imported.")

        if key and key.startswith("~"):
            key = os.path.expanduser(key)

        config = paramiko.config.SSHConfig()
        config_file_path = self._get_ssh_config_path()
        if config_file_path:
            with open(config_file_path) as f:
                config.parse(f)

        opts = config.lookup(host)
        

        client = paramiko.SSHClient()
        ssh_known_hosts_file = None
        if "userknownhostsfile" in opts:
            ssh_known_hosts_file = opts["userknownhostsfile"]
        else:
            client.get_host_keys().clear()
            ssh_known_hosts_file = '~/.ssh/known_hosts'
            
            if platform.system().lower() == "windows":
                ssh_known_hosts_file = '%s\ssh\known_hosts' % mforms.App.get().get_user_data_folder()
                
        try:
            client.load_host_keys(os.path.expanduser(ssh_known_hosts_file))
        except IOError, e:
            log_warning("IOError, probably caused by file %s not found, the message was: %s\n" % (ssh_known_hosts_file, e))
        
        if "stricthostkeychecking" in opts and opts["stricthostkeychecking"].lower() == "no":
            client.set_missing_host_key_policy(WarningPolicy())
        else:
            client.set_missing_host_key_policy(StoreIfConfirmedPolicy())

        # TODO: Check if file with
        #client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
        #client.set_missing_host_key_policy(paramiko.WarningPolicy()) # This is a temp workaround, later we need to have UI with buttons accept

           
        try:
            if 'timeout' in paramiko.SSHClient.connect.func_code.co_varnames:
                client.connect(hostname = host, port = int(port), username = user, password = pwd, #pkey = None,
                                    key_filename = key, timeout = 10, look_for_keys=bool(usekey), allow_agent=bool(usekey) )
            else:
                client.connect(hostname = host, port = int(port), username = user, password = pwd, #pkey = None,
                                    key_filename = key, look_for_keys=bool(usekey), allow_agent=bool(usekey) )
            log_info("%s: Connected via ssh to %s\n" % (self.__class__.__name__, host) )
            if self.keepalive != 0:
                client.get_transport().set_keepalive(self.keepalive)

            self.client = client
        except socket.error, exc:
            import traceback
            log_error("Error opening SSH connection to %s: %s\n" % (host, traceback.format_exc()))

            if exc.args[0] == 8: # [Errno 8] nodename nor servname provided, or not known
                raise ConnectionError("Unable to resolve host name '%s'" % host)
            else:
                raise SSHDownException(str(exc))
            #if exc.args[0] == 61: # connection refused
            #  raise ConnectionError("Could not establish SSH connection: %r.\nMake sure the SSH daemon is running and is accessible." % exc)
            #else:
            #  raise ConnectionError("Could not establish SSH connection: %r.\nMake sure the SSH daemon is running and is accessible." % exc)
        except paramiko.BadHostKeyException, exc:
            raise Exception(format_bad_host_exception(exc, '%s\ssh\known_hosts' % mforms.App.get().get_user_data_folder() if platform.system().lower() == "windows" else "~/.ssh/known_hosts file"))

        except paramiko.PasswordRequiredException, exc:
            if pwd is not None:
                raise ConnectionError("Could not unlock private keys. %s" % exc)
            else:
                raise exc
        except paramiko.BadAuthenticationType, exc:
            import traceback
            log_error("Error opening SSH connection: %s\n" % traceback.format_exc())
            if 'keyboard-interactive' in exc.allowed_types and pwd is not None:
                # wrong password
                raise InvalidPasswordError("Authentication failed for SSH user %s" % user)
            raise ConnectionError("Could not establish SSH connection: %s." % exc)
        except paramiko.AuthenticationException, exc:
            import traceback
            log_error("Error opening SSH connection: %s\n" % traceback.format_exc())
            if usekey:
                raise InvalidPasswordError("Invalid ssh key %s for SSH user %s" % (key ,user))
            else:
                raise InvalidPasswordError("Invalid password for SSH user %s" % user)
        except paramiko.SSHException, exc:
            import traceback
            log_error("Error opening SSH connection: %s\n" % traceback.format_exc())
            raise ConnectionError("Could not establish SSH connection: %s." % exc)
        except SSHFingerprintNewError, exc: # We need to handle this upper in the chain
            raise exc
        except Exception, exc:
            import traceback
            log_error("Error opening SSH connection: %s\n" % traceback.format_exc())
            raise ConnectionError("Could not establish SSH connection. %s." % exc)

    def is_connected(self):
        return self.client is not None

    def mkdir(self, path):
        path = normalize_windows_path_for_ftp(path)
        sftp = self.client.open_sftp()
        try:
            sftp.mkdir(path)
            sftp.close()
        except:
            import traceback
            log_error("Error creating remote dir via ssh: %s\n" % traceback.format_exc())
            sftp.close()
            raise

    def rmdir(self, path):
        sftp = self.client.open_sftp()
        try:
            sftp.rmdir(path)
            sftp.close()
        except:
            import traceback
            log_error("Error removing remote dir via ssh: %s\n" % traceback.format_exc())
            sftp.close()
            raise


    def remove(self, path):
        sftp = self.client.open_sftp()
        try:
            sftp.remove(path)
            sftp.close()
        except:
            import traceback
            log_error("Error removing remote file via ssh: %s\n" % traceback.format_exc())
            sftp.close()
            raise


    def file_exists(self, path):
        path = normalize_windows_path_for_ftp(path)
        ret = False
        if self.client is None:
            raise Exception("wb_admin_ssh: SSH client not connected. file_exists failed")

        sftp = self.client.open_sftp()
        try:
            sftp.stat(path)
            ret = True
            sftp.close()
        except IOError, e:
            sftp.close()
            if e.errno != errno.ENOENT:
              raise
        except:
            import traceback
            log_error("Error checking remote file via ssh: %s\n" % traceback.format_exc())
            sftp.close()
            raise

        return ret

    def stat(self, path):
        path = normalize_windows_path_for_ftp(path)
        ret = None
        sftp = self.client.open_sftp()
        try:
            ret = sftp.stat(path)
            sftp.close()
        except IOError:
            ret = None
            sftp.close()
        except:
            import traceback
            log_error("Error stating remote file via ssh: %s\n" % traceback.format_exc())
            ret = None
            sftp.close()

        return ret
        

    def get(self, source, dest):
        source = normalize_windows_path_for_ftp(source)
        if source is not None:
            source = source.strip("'\"")

        ret = False
        try:
            sftp = self.client.open_sftp()
            sftp.get(source, dest)
            ret = True
            sftp.close()
        except IOError, e:
            log_error('%s: Retrieval of file "%s" failed: %s\n' % (self.__class__.__name__, source, str(e)) )
            raise

        return ret

    def get_contents(self, path): # raises IOError
        path = normalize_windows_path_for_ftp(path)
        
        sftp = self.client.open_sftp()

        # test if sftp session is ok. I get a EACCES doing anything after sftp session is open with freesshd
        try:
            sftp.chdir(".")
        except IOError, exc:
            sftp.close()
            if exc.errno == errno.EACCES:
                raise PermissionDeniedError("Permission denied opening SFTP session. Please ensure the ssh account is correctly setup.")
            raise exc

        try:
            f = sftp.file(path, "r")
        except IOError, exc:
            sftp.close()
            if exc.errno == errno.EACCES:
                raise PermissionDeniedError("Permission denied opening remote file %s for reading: %s" % (path, exc))
            raise exc
        ret = f.read()
        f.close()
        sftp.close()
        return ret

    def set_contents(self, path, data, mode = "w"): # raises IOError
        path = normalize_windows_path_for_ftp(path)
        
        sftp = self.client.open_sftp()
        try:
            f = sftp.file(path, mode)
            f.chmod(stat.S_IREAD | stat.S_IWRITE)
        except IOError, exc:
            sftp.close()
            if exc.errno == errno.EACCES:
                raise PermissionDeniedError("Permission denied opening remote file %s for writing: %s" % (path, exc))
            raise exc
        ret = f.write(data)
        f.close()
        sftp.close()
        return ret

    def put(self, source, dest):
        ret = False
        try:
            sftp = self.client.open_sftp()
            sftp.put(source, dest)
            ret = True
            sftp.close()
        except Exception, e:
            log_error('%s: Sending of file "%s" to the server failed: %s\n' % (self.__class__.__name__, source, str(e)) )
            raise

        return ret
    
    def _read_streams(self, chan, stdout, stderr, read_size, end_text = None, timeout=0, spawn_process = False, wait_output = CmdOutput.WAIT_ALWAYS):
        """
          Will read data from the channel streams with different exit criteria:
          - end_text is specified and found on the read data.
          - timeout is specified and it has completed.
          - If the server indicated the command completed and there's nothing left to read
          
          Rturns a tuple containing:
          - Command return code if completed on the server else None
          - A boolean value indicating if the end_text was found
          - The data read from the stdout stream
          - The data read from the stderr stream
          
          Expected Output
          - Used to determine whether output should be read or not from the executed
            command, possible values include:
            - YES : Will wait for output all the time
            - NO  : Will not wait for output all the time
            - ON_OK : Will only wait for output if the command succeeds
            - ON_ERROR : Will only wait for output if the command fails
          """
        t = time.time()
        all_data = ''
        all_error= ''
        
        srt_timeout = 2
        status_ready_time = None
        
        
        
        # Initializes the exit criteria
        cmd_ret = None
        found_text = False
        read_done = False
        time_complete = False
        
        
        #while not found_text and cmd_ret is None and not time_complete:
        while not found_text and not read_done and not time_complete:
            data = ''
            error = ''
            
            # Reads from the stdout if available
            while chan.recv_ready() and len(data) < read_size:
                data += stdout.read(1)
            if data:
                log_debug2("ssh session stdout [%d]: plain>>>%s<<<   hex>>>%s<<<x\n" % (len(data), data, ' '.join(x.encode('hex') for x in data)))

            # Reads from the stderr if available
            while chan.recv_stderr_ready() and len(error) < read_size:
                error += stderr.read(1)
            if error:
                log_debug2("ssh session stderr [%d]: plain>>>%s<<<   hex>>>%s<<<x\n" % (len(error), error, ' '.join(x.encode('hex') for x in error)))

            # Appends any read data on stdout and stderr
            if data or error:
                all_data += data
                all_error += error
            
            # If nothing was read, checks if the command completed and
            # Reads the exist status
            elif chan.exit_status_ready():
                if cmd_ret is None:
                    cmd_ret = chan.recv_exit_status()
                
                # No need to read output...
                if ((all_data or all_error) and chan.closed) or\
                   (wait_output == CmdOutput.WAIT_NEVER) or\
                   (wait_output == CmdOutput.WAIT_IF_OK and cmd_ret != 0) or\
                   (wait_output == CmdOutput.WAIT_IF_FAIL and cmd_ret == 0):
                   read_done = True
                
                # Needs to read output : YES, ON_OK and RC is 0 and ON_ERROR and RC != 0
                elif status_ready_time:
                    new_time = time.time()
                    diff = new_time - status_ready_time
                    read_done = diff >= srt_timeout
                else:
                    status_ready_time = time.time()
                      

                
            # If end_text was specified, checks if it was found on the
            # read data to exit properly
            if end_text:
                if all_data and all_data.find(end_text) != -1:
                    found_text = True
                
                # Same check on the stderr data if needed
                if spawn_process and all_error and all_error.find(end_text) != -1:
                    found_text = True
            
            # If timeout was specified checks wether it has completed
            if timeout:
                time_complete = time.time() - t >= timeout
        
        return (cmd_ret, found_text, all_data, all_error)


    def _prepare_channel(self, transport):
          channel = None
          stdin = None
          stdout = None
          stderr = None

          channel = transport.open_session()

          if channel:
              channel.setblocking(True)
              channel.settimeout(10)
              channel.get_pty()
              
              bufsize = -1
              stdin = channel.makefile('wb', bufsize)
              stdout = channel.makefile('rb', bufsize)
              stderr = channel.makefile_stderr('rb', bufsize)

          return channel, stdin, stdout, stderr



    def exec_cmd(self, cmd, as_user = Users.CURRENT, user_password = None, output_handler = None, read_size = 128, get_channel_cb = None, options = None):
        out = ""
        error = ""
        ret_code = None
        

        # For some reason we have bool in pwd
        if type(user_password) is unicode:
            user_password = user_password.encode("utf8")

        if type(user_password) is not str:
            user_password = None

        expect_sudo_failure = False

        read_timeout = self.ssh_timeout

        if self.client is not None:
            transport = self.client.get_transport()
            try:
                spawn_process = False
                if 'nohup' in cmd:
                    spawn_process = True
                
                chan = None
                chan, stdin, stdout, stderr = self._prepare_channel(transport)
                wait_output = CmdOutput.WAIT_ALWAYS
                if options and options.has_key(CmdOptions.CMD_WAIT_OUTPUT):
                    wait_output = options[CmdOptions.CMD_WAIT_OUTPUT]

                if chan:
                    ret_code = None
                    initial_data = ""
                    error = ""
                    more_data = True

                    log_debug2("About to execute command through ssh session: %s\n" % cmd)
                    chan.exec_command(cmd)
                    pass_prompt_count = 0
                  
                    if (as_user != Users.CURRENT and user_password is not None):
                      
                        ret_code, prompted, initial_data, error = self._read_streams(chan, stdout, stderr, 1, 'EnterPasswordHere', read_timeout+5, spawn_process, wait_output)
                        log_debug2("%s.exec_cmd initial read for command [%s]:\nRetCode : [%s]\nPrompted : [%s]\nData : [%s]\nError : [%s]\n" % (self.__class__.__name__, cmd, ret_code, prompted, initial_data, error) )
                        
                        if prompted:
                            initial_data = ''
                            
                            # feed the password
                            log_debug2("exec_cmd: sending password to child...\n")
                            
                            try:
                                stdin.write(user_password+"\n")
                                stdin.flush()
                                stdin.close()
                            except socket.error, e:
                                es = str(e)
                                if not('ocket' in es and 'closed' in es):
                                    raise
                            
                            expect_sudo_failure = True # we could get a Sorry or the password prompt again
                        else:
                            if ret_code:
                                log_error("exec_cmd: error on sudo operation : %s\n" % error)
                                more_data = False
                            else:
                                log_debug("exec_cmd: was expecting sudo password prompt, but it never came\n")
            
                                if initial_data:
                                    wait_output = CmdOutput.WAIT_NEVER

                        # Outputs the initial read
                        if output_handler:
                            output_handler(initial_data)
                        else:
                            if prompted:
                                pass_prompt_count=1

                            out = initial_data


                    # TODO: Is this really needed????
                    #       The call with channel get_channel_cb is only done in two places:
                    #       - On the unit tests at the bottom of this file where it is only used to ensure the channel
                    #         is returned. (So the channel is not used)
                    #       - On wba_monitor_be.WinRemoteStats where it is only stored and not used (pretty much the same as the Test class)
                    if get_channel_cb is not None:
                        log_debug3("%s.exec_cmd: Getting channel via passed cb (%s)\n" % (self.__class__.__name__, get_channel_cb.__name__) )
                        get_channel_cb(chan)

                    # Now reads the command output and sends it to the handler if needed.
                    # Note that is is needed to enter this loop even if there's no output 
                    # handler to prevent ending the channel before the command is completed.
                    while more_data:
                        ret_code, prompted, chunk, error = self._read_streams(chan, stdout, stderr, read_size, None, read_timeout, False, wait_output)
                        
                        if chunk or error:
                            out += chunk
                            # Detects an additional password prompt which could come in 2 formats
                            # EnterPasswordHere is checked against pass_prompt_count as out could may or may not contain
                            # the initial password prompt, the Sorry.... 
                            if expect_sudo_failure and (out.count("EnterPasswordHere") > pass_prompt_count or out.count("Sorry, try again") > 0):
                                raise InvalidPasswordError("Incorrect password for sudo")
                            elif output_handler:
                                output_handler(chunk)
                                
                            if error:
                                log_warning("%s.exec_cmd error : %s\n" % (self.__class__.__name__, error))

                        # Quits reading when the exit code is read
                        if ret_code != None:
                            more_data = False

                    if ret_code is None:
                        log_warning("%s: Read from the peer is done, but status code is not available\n" % self.__class__.__name__)

            except paramiko.SSHException, e:
                if chan and chan.recv_ready():
                    out = chan.recv(128)
                log_error("SSHException in SSH: %s\n" % str(e))
                traceback.print_exc()
            except InvalidPasswordError, e:
                raise
            except Exception, e:
                if chan and chan.recv_ready():
                    out = chan.recv(128)
                # A "socket closed" exception happens sometimes when sending the password, for some reason.
                # Not sure, but it could be that the command was accepted without a password (maye sudo doing internal
                # password caching for some time). Until that is investigated, exceptions shouldn't bubble up
                log_error("Exception in SSH: %s\n%s\n" % (str(e), traceback.format_exc()))
            except:
                log_error("Unknown exception in ssh\n")

            if chan is not None:
                chan.close()

        # If the password was sent removes the first occurrence of it from the output
        # This is to prevent the sent password to be included but to not affect the
        # rest of the output
        if out is not None and user_password is not None and not initial_data:
            out = out.replace(user_password, "", 1)

        try:
            log_debug3("%s.exec_cmd(cmd=%s, output=%s. Retcode = %s)\n" % (self.__class__.__name__.encode('utf8'), cmd.encode('utf8'), out.decode('utf8'), ret_code) )
        except (UnicodeDecodeError, UnicodeEncodeError), e:
            log_error("Unexpected output received (byte-string <-> UTF-8 conversion failed).  Below is the sanitised version of output:\n")
            log_error("  %s.exec_cmd(cmd=%s, output=%s. Retcode = %s)\n" % (self.__class__.__name__.encode('utf8','replace'), cmd.encode('utf8','replace'), out.decode('utf8','replace'), ret_code))

        return (out, ret_code)

    def getftp(self):
        self.sftp = WbAdminSFTP(self.client.open_sftp())
        return self.sftp

    def close(self):
        if self.client is not None:
            self.client.close()





            


# === Unit tests ===
if __name__ == "__main__":
    #import threading

    class Settings:
        def __init__(self):
            self.loginInfo = {}
            self.serverInfo = {}

            self.loginInfo['ssh.hostName'] = ''
            self.loginInfo['ssh.useKey']   = 0
            self.loginInfo['ssh.userName'] = ''
            self.loginInfo['ssh.port'] = ''

    settings = Settings()

    class Delegate:
        def get_password_for(self, what):
            return ""

        def reset_password_for(self, what):
            return ""

    wbassh = WbAdminSSH()
    wbassh.wrapped_connect(settings, Delegate())
    ftp = wbassh.getftp()

    print ftp.pwd()
    print ftp.ls('.')
    ftp.cd('OpenVPN')
    print ftp.pwd()
    print ftp.ls('.')

    wbassh.close()
    quit()

    class Test:
        def __init__(self):
            self.chan = None
            self.running = [True]

        def save_channel(self, c):
            print "Saving channel", c
            self.chan = c

        def cpu(self, text):
            text = text.strip(" \r\t\n")
            value = None
            try:
                value = int(text)
            except ValueError:
                value = None
            if value is not None:
                print "CPU", value

        def mem(self, text):
            text = text.strip(" \r\t\n")
            value = None
            try:
                value = int(text)
            except ValueError:
                value = None
            if value is not None:
                print "Mem", value

        def reader(self, ssh_session):
            what = ""
            out = ""
            timeouts = 12
            while self.running[0]: # running is passed in list via "refernce"
                try:
                    ch = ssh_session.recv(1)
                    timeouts = 12
                    if ch == "C":
                        what = self.cpu
                    elif ch == "M":
                        what = self.mem
                    elif ch == "\r" or ch == "\n":
                        if what is not None:
                            what(out)
                        what = None
                        out = ""
                        pass
                    elif ch in "0123456789. ":
                        out += ch
                    else:
                        what = None
                        out = ""
                except socket.timeout:
                    timeouts -= 1
                    if timeouts <= 0:
                        ssh_session.close()
                        raise Exception("Can't read from remote Windows script")




    ts = Test()

    t = threading.Thread(target = wbassh.exec_cmd, args=("cmd /C cscript //NoLogo \"C:\Program Files\MySQL\MySQL Server 5.1\mysql_system_status.vbs\" /DoStdIn", Users.CURRENT, False, ts.reader, 1, ts.save_channel))
    t.setDaemon(True)
    t.start()

    time.sleep(10)

    t.join()
    wbassh.close()
