Package x2go :: Package backends :: Package control :: Module plain

[frames] | no frames]

# Source Code for Module x2go.backends.control.plain

       1  # -*- coding: utf-8 -*-
       2
       3  # Copyright (C) 2010-2016 by Mike Gabriel <mike.gabriel@das-netzwerkteam.de>
       4  #
       5  # Python X2Go is free software; you can redistribute it and/or modify
       6  # it under the terms of the GNU Affero General Public License as published by
       7  # the Free Software Foundation; either version 3 of the License, or
       8  # (at your option) any later version.
       9  #
      10  # Python X2Go is distributed in the hope that it will be useful,
      11  # but WITHOUT ANY WARRANTY; without even the implied warranty of
      12  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      13  # GNU Affero General Public License for more details.
      14  #
      15  # You should have received a copy of the GNU Affero General Public License
      16  # along with this program; if not, write to the
      17  # Free Software Foundation, Inc.,
      18  # 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
      19
      20  """\
      21  L{X2GoControlSession} class - core functions for handling your individual X2Go sessions.
      22
      23  This backend handles X2Go server implementations that respond via server-side PLAIN text output.
      24
      25  """
      26  __NAME__ = 'x2gocontrolsession-pylib'
      27
      28  # modules
      29  import os
      30  import types
      31  import paramiko
      32  import gevent
      33  import copy
      34  import string
      35  import random
      36  import re
      37  import locale
      38  import threading
      39  import cStringIO
      40  import base64
      41  import uuid
      42
      43  from gevent import socket
      44
      45  # Python X2Go modules
      46  import x2go.sshproxy as sshproxy
      47  import x2go.log as log
      48  import x2go.utils as utils
      49  import x2go.x2go_exceptions as x2go_exceptions
      50  import x2go.defaults as defaults
      51  import x2go.checkhosts as checkhosts
      52
      53  from x2go.defaults import BACKENDS as _BACKENDS
      54
      55  import x2go._paramiko
      56  x2go._paramiko.monkey_patch_paramiko()
      57
      58 -def _rerewrite_blanks(cmd):
      59      """\
      60      In command strings X2Go server scripts expect blanks being rewritten to ,,X2GO_SPACE_CHAR''.
      61      Commands get rewritten in the terminal sessions. This re-rewrite function helps
      62      displaying command string in log output.
      63
      64      @param cmd: command that has to be rewritten for log output
      65      @type cmd: C{str}
      66
      67      @return: the command with ,,X2GO_SPACE_CHAR'' re-replaced by blanks
      68      @rtype: C{str}
      69
      70      """
      71      # X2Go run command replace X2GO_SPACE_CHAR string with blanks
      72      if cmd:
      73          cmd = cmd.replace("X2GO_SPACE_CHAR", " ")
      74      return cmd
      75
      76 -def _rewrite_password(cmd, user=None, password=None):
      77      """\
      78      In command strings Python X2Go replaces some macros with actual values:
      79
      80        - X2GO_USER -> the user name under which the user is authenticated via SSH
      81        - X2GO_PASSWORD -> the password being used for SSH authentication
      82
      83      Both macros can be used to on-the-fly authenticate via RDP.
      84
      85      @param cmd: command that is to be sent to an X2Go server script
      86      @type cmd: C{str}
      87      @param user: the SSH authenticated user name
      88      @type password: the password being used for SSH authentication
      89
      90      @return: the command with macros replaced
      91      @rtype: C{str}
      92
      93      """
      94      # if there is a ,,-u X2GO_USER'' parameter in RDP options then we will replace
      95      # it by our X2Go session password
      96      if cmd and user:
      97          cmd = cmd.replace('X2GO_USER', user)
      98      # if there is a ,,-p X2GO_PASSWORD'' parameter in RDP options then we will replace
      99      # it by our X2Go session password
     100      if cmd and password:
     101          cmd = cmd.replace('X2GO_PASSWORD', password)
     102      return cmd
     103
     104
     105 -class X2GoControlSession(paramiko.SSHClient):
     106      """\
     107      In the Python X2Go concept, X2Go sessions fall into two parts: a control session and one to many terminal sessions.
     108
     109      The control session handles the SSH based communication between server and client. It is mainly derived from
     110      C{paramiko.SSHClient} and adds on X2Go related functionality.
     111
     112      """
     113 -    def __init__(self,
     114                   profile_name='UNKNOWN',
     115                   add_to_known_hosts=False,
     116                   known_hosts=None,
     117                   forward_sshagent=False,
     118                   unique_hostkey_aliases=False,
     119                   terminal_backend=_BACKENDS['X2GoTerminalSession']['default'],
     120                   info_backend=_BACKENDS['X2GoServerSessionInfo']['default'],
     121                   list_backend=_BACKENDS['X2GoServerSessionList']['default'],
     122                   proxy_backend=_BACKENDS['X2GoProxy']['default'],
     123                   client_rootdir=os.path.join(defaults.LOCAL_HOME, defaults.X2GO_CLIENT_ROOTDIR),
     124                   sessions_rootdir=os.path.join(defaults.LOCAL_HOME, defaults.X2GO_SESSIONS_ROOTDIR),
     125                   ssh_rootdir=os.path.join(defaults.LOCAL_HOME, defaults.X2GO_SSH_ROOTDIR),
     126                   logger=None, loglevel=log.loglevel_DEFAULT,
     127                   published_applications_no_submenus=0,
     128                   low_latency=False,
     129                   **kwargs):
     130          """\
     131          Initialize an X2Go control session. For each connected session profile there will be one SSH-based
     132          control session and one to many terminal sessions that all server-client-communicate via this one common control
     133          session.
     134
     135          A control session normally gets set up by an L{X2GoSession} instance. Do not use it directly!!!
     136
     137          @param profile_name: the profile name of the session profile this control session works for
     138          @type profile_name: C{str}
     139          @param add_to_known_hosts: Auto-accept server host validity?
     140          @type add_to_known_hosts: C{bool}
     141          @param known_hosts: the underlying Paramiko/SSH systems C{known_hosts} file
     142          @type known_hosts: C{str}
     143          @param forward_sshagent: forward SSH agent authentication requests to the X2Go client-side
     144          @type forward_sshagent: C{bool}
     145          @param unique_hostkey_aliases: instead of storing [<hostname>]:<port> in known_hosts file, use the
     146              (unique-by-design) profile ID
     147          @type unique_hostkey_aliases: C{bool}
     148          @param terminal_backend: X2Go terminal session backend to use
     149          @type terminal_backend: C{str}
     150          @param info_backend: backend for handling storage of server session information
     151          @type info_backend: C{X2GoServerSessionInfo*} instance
     152          @param list_backend: backend for handling storage of session list information
     153          @type list_backend: C{X2GoServerSessionList*} instance
     154          @param proxy_backend: backend for handling the X-proxy connections
     155          @type proxy_backend: C{X2GoProxy*} instance
     156          @param client_rootdir: client base dir (default: ~/.x2goclient)
     157          @type client_rootdir: C{str}
     158          @param sessions_rootdir: sessions base dir (default: ~/.x2go)
     159          @type sessions_rootdir: C{str}
     160          @param ssh_rootdir: ssh base dir (default: ~/.ssh)
     161          @type ssh_rootdir: C{str}
     162          @param published_applications_no_submenus: published applications menus with less items than C{published_applications_no_submenus}
     163              are rendered without submenus
     164          @type published_applications_no_submenus: C{int}
     165          @param logger: you can pass an L{X2GoLogger} object to the
     166              L{X2GoControlSession} constructor
     167          @type logger: L{X2GoLogger} instance
     168          @param loglevel: if no L{X2GoLogger} object has been supplied a new one will be
     169              constructed with the given loglevel
     170          @type loglevel: C{int}
     171          @param low_latency: set this boolean switch for weak connections, it will double all timeout values.
     172          @type low_latency: C{bool}
     173          @param kwargs: catch any non-defined parameters in C{kwargs}
     174          @type kwargs: C{dict}
     175
     176          """
     177          self.associated_terminals = {}
     178          self.terminated_terminals = []
     179
     180          self.profile_name = profile_name
     181          self.add_to_known_hosts = add_to_known_hosts
     182          self.known_hosts = known_hosts
     183          self.forward_sshagent = forward_sshagent
     184          self.unique_hostkey_aliases = unique_hostkey_aliases
     185
     186          self.hostname = None
     187          self.port = None
     188
     189          self.sshproxy_session = None
     190
     191          self._session_auth_rsakey = None
     192          self._remote_home = None
     193          self._remote_group = {}
     194          self._remote_username = None
     195          self._remote_peername = None
     196
     197          self._server_versions = None
     198          self._server_features = None
     199
     200          if logger is None:
     201              self.logger = log.X2GoLogger(loglevel=loglevel)
     202          else:
     203              self.logger = copy.deepcopy(logger)
     204          self.logger.tag = __NAME__
     205
     206          self._terminal_backend = terminal_backend
     207          self._info_backend = info_backend
     208          self._list_backend = list_backend
     209          self._proxy_backend = proxy_backend
     210
     211          self.client_rootdir = client_rootdir
     212          self.sessions_rootdir = sessions_rootdir
     213          self.ssh_rootdir = ssh_rootdir
     214
     215          self._published_applications_menu = {}
     216
     217          self.agent_chan = None
     218          self.agent_handler = None
     219
     220          paramiko.SSHClient.__init__(self)
     221          if self.add_to_known_hosts:
     222              self.set_missing_host_key_policy(paramiko.AutoAddPolicy())
     223
     224          self.session_died = False
     225
     226          self.low_latency = low_latency
     227
     228          self.published_applications_no_submenus = published_applications_no_submenus
     229          self._already_querying_published_applications = threading.Lock()
     230
     231          self._transport_lock = threading.Lock()
     232
     233 -    def get_hostname(self):
     234          """\
     235          Get the hostname as stored in the properties of this control session.
     236
     237          @return: the hostname of the connected X2Go server
     238          @rtype: C{str}
     239
     240          """
     241          return self.hostname
     242
     243 -    def get_port(self):
     244          """\
     245          Get the port number of the SSH connection as stored in the properties of this control session.
     246
     247          @return: the server-side port number of the control session's SSH connection
     248          @rtype: C{str}
     249
     250          """
     251          return self.port
     252
     253 -    def load_session_host_keys(self):
     254          """\
     255          Load known SSH host keys from the C{known_hosts} file.
     256
     257          If the file does not exist, create it first.
     258
     259          """
     260          if self.known_hosts is not None:
     261              utils.touch_file(self.known_hosts)
     262              self.load_host_keys(self.known_hosts)
     263
     264 -    def __del__(self):
     265          """\
     266          On instance descruction, do a proper session disconnect from the server.
     267
     268          """
     269          self.disconnect()
     270
     271 -    def test_sftpclient(self):
     272          ssh_transport = self.get_transport()
     273          try:
     274              self.sftp_client = paramiko.SFTPClient.from_transport(ssh_transport)
     275          except (AttributeError, paramiko.SFTPError):
     276              raise x2go_exceptions.X2GoSFTPClientException('failed to initialize SFTP channel')
     277
     278 -    def _x2go_sftp_put(self, local_path, remote_path, timeout=20):
     279          """
     280          Put a local file on the remote server via sFTP.
     281
     282          During sFTP operations, remote command execution gets blocked.
     283
     284          @param local_path: full local path name of the file to be put on the server
     285          @type local_path: C{str}
     286          @param remote_path: full remote path name of the server-side target location, path names have to be Unix-compliant
     287          @type remote_path: C{str}
     288          @param timeout: this SFTP put action should not take longer then the given value
     289          @type timeout: C{int}
     290
     291          @raise X2GoControlSessionException: if the SSH connection dropped out
     292
     293          """
     294          ssh_transport = self.get_transport()
     295          self._transport_lock.acquire()
     296          if ssh_transport and ssh_transport.is_authenticated():
     297              self.logger('sFTP-put: %s -> %s:%s' % (os.path.normpath(local_path), self.remote_peername(), remote_path), loglevel=log.loglevel_DEBUG)
     298
     299              if self.low_latency: timeout = timeout * 2
     300              timer = gevent.Timeout(timeout)
     301              timer.start()
     302
     303              try:
     304                  try:
     305                      self.sftp_client = paramiko.SFTPClient.from_transport(ssh_transport)
     306                  except paramiko.SFTPError:
     307                      self._transport_lock.release()
     308                      raise x2go_exceptions.X2GoSFTPClientException('failed to initialize SFTP channel')
     309                  try:
     310                      self.sftp_client.put(os.path.normpath(local_path), remote_path)
     311                  except (x2go_exceptions.SSHException, socket.error, IOError):
     312                      # react to connection dropped error for SSH connections
     313                      self.session_died = True
     314                      self._transport_lock.release()
     315                      raise x2go_exceptions.X2GoControlSessionException('The SSH connection was dropped during an sFTP put action.')
     316
     317              except gevent.timeout.Timeout:
     318                  self.session_died = True
     319                  self._transport_lock.release()
     320                  if self.sshproxy_session:
     321                      self.sshproxy_session.stop_thread()
     322                  raise x2go_exceptions.X2GoControlSessionException('the X2Go control session timed out during an SFTP write command')
     323              finally:
     324                  timer.cancel()
     325
     326              self.sftp_client = None
     327          if self._transport_lock.locked():
     328              self._transport_lock.release()
     329
     330 -    def _x2go_sftp_write(self, remote_path, content, timeout=20):
     331          """
     332          Create a text file on the remote server via sFTP.
     333
     334          During sFTP operations, remote command execution gets blocked.
     335
     336          @param remote_path: full remote path name of the server-side target location, path names have to be Unix-compliant
     337          @type remote_path: C{str}
     338          @param content: a text file, multi-line files use Unix-link EOL style
     339          @type content: C{str}
     340          @param timeout: this SFTP write action should not take longer then the given value
     341          @type timeout: C{int}
     342
     343          @raise X2GoControlSessionException: if the SSH connection dropped out
     344
     345          """
     346          ssh_transport = self.get_transport()
     347          self._transport_lock.acquire()
     348          if ssh_transport and ssh_transport.is_authenticated():
     349              self.logger('sFTP-write: opening remote file %s on host %s for writing' % (remote_path, self.remote_peername()), loglevel=log.loglevel_DEBUG)
     350
     351              if self.low_latency: timeout = timeout * 2
     352              timer = gevent.Timeout(timeout)
     353              timer.start()
     354
     355              try:
     356                  try:
     357                      self.sftp_client = paramiko.SFTPClient.from_transport(ssh_transport)
     358                  except paramiko.SFTPError:
     359                      self._transport_lock.release()
     360                      raise x2go_exceptions.X2GoSFTPClientException('failed to initialize SFTP channel')
     361                  try:
     362                      remote_fileobj = self.sftp_client.open(remote_path, 'w')
     363                      self.logger('sFTP-write: writing content: %s' % content, loglevel=log.loglevel_DEBUG_SFTPXFER)
     364                      remote_fileobj.write(content)
     365                      remote_fileobj.close()
     366                  except (x2go_exceptions.SSHException, socket.error, IOError):
     367                      self.session_died = True
     368                      self._transport_lock.release()
     369                      self.logger('sFTP-write: opening remote file %s on host %s failed' % (remote_path, self.remote_peername()), loglevel=log.loglevel_WARN)
     370                      if self.sshproxy_session:
     371                          self.sshproxy_session.stop_thread()
     372                      raise x2go_exceptions.X2GoControlSessionException('The SSH connection was dropped during an sFTP write action.')
     373
     374              except gevent.timeout.Timeout:
     375                  self.session_died = True
     376                  self._transport_lock.release()
     377                  if self.sshproxy_session:
     378                      self.sshproxy_session.stop_thread()
     379                  raise x2go_exceptions.X2GoControlSessionException('the X2Go control session timed out during an SFTP write command')
     380              finally:
     381                  timer.cancel()
     382
     383              self.sftp_client = None
     384          if self._transport_lock.locked():
     385              self._transport_lock.release()
     386
     387 -    def _x2go_sftp_remove(self, remote_path, timeout=20):
     388          """
     389          Remote a remote file from the server via sFTP.
     390
     391          During sFTP operations, remote command execution gets blocked.
     392
     393          @param remote_path: full remote path name of the server-side file to be removed, path names have to be Unix-compliant
     394          @type remote_path: C{str}
     395          @param timeout: this SFTP remove action should not take longer then the given value
     396          @type timeout: C{int}
     397
     398          @raise X2GoControlSessionException: if the SSH connection dropped out
     399
     400          """
     401          ssh_transport = self.get_transport()
     402          self._transport_lock.acquire()
     403          if ssh_transport and ssh_transport.is_authenticated():
     404              self.logger('sFTP-write: removing remote file %s on host %s' % (remote_path, self.remote_peername()), loglevel=log.loglevel_DEBUG)
     405
     406              if self.low_latency: timeout = timeout * 2
     407              timer = gevent.Timeout(timeout)
     408              timer.start()
     409
     410              try:
     411                  try:
     412                      self.sftp_client = paramiko.SFTPClient.from_transport(ssh_transport)
     413                  except paramiko.SFTPError:
     414                      self._transport_lock.release()
     415                      raise x2go_exceptions.X2GoSFTPClientException('failed to initialize SFTP channel')
     416                  try:
     417                      self.sftp_client.remove(remote_path)
     418                  except (x2go_exceptions.SSHException, socket.error, IOError):
     419                      self.session_died = True
     420                      self._transport_lock.release()
     421                      self.logger('sFTP-write: removing remote file %s on host %s failed' % (remote_path, self.remote_peername()), loglevel=log.loglevel_WARN)
     422                      if self.sshproxy_session:
     423                          self.sshproxy_session.stop_thread()
     424                      raise x2go_exceptions.X2GoControlSessionException('The SSH connection was dropped during an sFTP remove action.')
     425
     426              except gevent.timeout.Timeout:
     427                  self.session_died = True
     428                  self._transport_lock.release()
     429                  if self.sshproxy_session:
     430                      self.sshproxy_session.stop_thread()
     431                  raise x2go_exceptions.X2GoControlSessionException('the X2Go control session timed out during an SFTP write command')
     432              finally:
     433                  timer.cancel()
     434
     435              self.sftp_client = None
     436          if self._transport_lock.locked():
     437              self._transport_lock.release()
     438
     439 -    def _x2go_exec_command(self, cmd_line, loglevel=log.loglevel_INFO, timeout=20, **kwargs):
     440          """
     441          Execute an X2Go server-side command via SSH.
     442
     443          During SSH command executions, sFTP operations get blocked.
     444
     445          @param cmd_line: the command to be executed on the remote server
     446          @type cmd_line: C{str} or C{list}
     447          @param loglevel: use this loglevel for reporting about remote command execution
     448          @type loglevel: C{int}
     449          @param timeout: if commands take longer than C{<timeout>} to be executed, consider the control session connection
     450              to have died.
     451          @type timeout: C{int}
     452          @param kwargs: parameters that get passed through to the C{paramiko.SSHClient.exec_command()} method.
     453          @type kwargs: C{dict}
     454
     455          @return: C{True} if the command could be successfully executed on the remote X2Go server
     456          @rtype: C{bool}
     457
     458          @raise X2GoControlSessionException: if the command execution failed (due to a lost connection)
     459
     460          """
     461          if type(cmd_line) == types.ListType:
     462              cmd = " ".join(cmd_line)
     463          else:
     464              cmd = cmd_line
     465
     466          cmd_uuid = str(uuid.uuid1())
     467          cmd = 'echo X2GODATABEGIN:%s; PATH=/usr/local/bin:/usr/bin:/bin sh -c \"%s\"; echo X2GODATAEND:%s' % (cmd_uuid, cmd, cmd_uuid)
     468
     469          if self.session_died:
     470              self.logger("control session seams to be dead, not executing command ,,%s'' on X2Go server %s" % (_rerewrite_blanks(cmd), self.profile_name,), loglevel=loglevel)
     471              return (cStringIO.StringIO(), cStringIO.StringIO(), cStringIO.StringIO('failed to execute command'))
     472
     473          self._transport_lock.acquire()
     474
     475          _retval = None
     476          _password = None
     477
     478          ssh_transport = self.get_transport()
     479          if ssh_transport and ssh_transport.is_authenticated():
     480
     481              if self.low_latency: timeout = timeout * 2
     482              timer = gevent.Timeout(timeout)
     483              timer.start()
     484              try:
     485                  self.logger("executing command on X2Go server ,,%s'': %s" % (self.profile_name, _rerewrite_blanks(cmd)), loglevel=loglevel)
     486                  if self._session_password:
     487                      _password = base64.b64decode(self._session_password)
     488                  _retval = self.exec_command(_rewrite_password(cmd, user=self.get_transport().get_username(), password=_password), **kwargs)
     489              except AttributeError:
     490                  self.session_died = True
     491                  self._transport_lock.release()
     492                  if self.sshproxy_session:
     493                      self.sshproxy_session.stop_thread()
     494                  raise x2go_exceptions.X2GoControlSessionException('the X2Go control session has died unexpectedly')
     495              except EOFError:
     496                  self.session_died = True
     497                  self._transport_lock.release()
     498                  if self.sshproxy_session:
     499                      self.sshproxy_session.stop_thread()
     500                  raise x2go_exceptions.X2GoControlSessionException('the X2Go control session has died unexpectedly')
     501              except x2go_exceptions.SSHException:
     502                  self.session_died = True
     503                  self._transport_lock.release()
     504                  if self.sshproxy_session:
     505                      self.sshproxy_session.stop_thread()
     506                  raise x2go_exceptions.X2GoControlSessionException('the X2Go control session has died unexpectedly')
     507              except gevent.timeout.Timeout:
     508                  self.session_died = True
     509                  self._transport_lock.release()
     510                  if self.sshproxy_session:
     511                      self.sshproxy_session.stop_thread()
     512                  raise x2go_exceptions.X2GoControlSessionException('the X2Go control session command timed out')
     513              except socket.error:
     514                  self.session_died = True
     515                  self._transport_lock.release()
     516                  if self.sshproxy_session:
     517                      self.sshproxy_session.stop_thread()
     518                  raise x2go_exceptions.X2GoControlSessionException('the X2Go control session has died unexpectedly')
     519              finally:
     520                  timer.cancel()
     521
     522          else:
     523              self._transport_lock.release()
     524              raise x2go_exceptions.X2GoControlSessionException('the X2Go control session is not connected (while issuing SSH command=%s)' % cmd)
     525
     526          if self._transport_lock.locked():
     527              self._transport_lock.release()
     528
     529          # sanitized X2Go relevant data, protect against data injection via .bashrc files
     530          (_stdin, _stdout, _stderr) = _retval
     531          raw_stdout = _stdout.read()
     532
     533          sanitized_stdout = ''
     534          is_x2go_data = False
     535          for line in raw_stdout.split('\n'):
     536              if line.startswith('X2GODATABEGIN:'+cmd_uuid):
     537                  is_x2go_data = True
     538                  continue
     539              if not is_x2go_data: continue
     540              if line.startswith('X2GODATAEND:'+cmd_uuid): break
     541              sanitized_stdout += line + "\n"
     542
     543          _stdout_new = cStringIO.StringIO(sanitized_stdout)
     544
     545          _retval = (_stdin, _stdout_new, _stderr)
     546          return _retval
     547
     548      @property
     549 -    def _x2go_server_versions(self):
     550          """\
     551          Render a dictionary of server-side X2Go components and their versions. Results get cached
     552          once there has been one successful query.
     553
     554          """
     555          if self._server_versions is None:
     556              self._server_versions = {}
     557              (stdin, stdout, stderr) = self._x2go_exec_command('which x2goversion >/dev/null && x2goversion')
     558              _lines = stdout.read().split('\n')
     559              for _line in _lines:
     560                  if ':' not in _line: continue
     561                  comp = _line.split(':')[0].strip()
     562                  version = _line.split(':')[1].strip()
     563                  self._server_versions.update({comp: version})
     564              self.logger('server-side X2Go components and their versions are: %s' % self._server_versions, loglevel=log.loglevel_DEBUG)
     565          return self._server_versions
     566
     567 -    def query_server_versions(self, force=False):
     568          """\
     569          Do a query for the server-side list of X2Go components and their versions.
     570
     571          @param force: do not use the cached component list, really ask the server (again)
     572          @type force: C{bool}
     573
     574          @return: dictionary of X2Go components (as keys) and their versions (as values)
     575          @rtype: C{list}
     576
     577          """
     578          if force:
     579              self._server_versions = None
     580          return self._x2go_server_versions
     581      get_server_versions = query_server_versions
     582
     583      @property
     584 -    def _x2go_server_features(self):
     585          """\
     586          Render a list of server-side X2Go features. Results get cached once there has been one successful query.
     587
     588          """
     589          if self._server_features is None:
     590              (stdin, stdout, stderr) = self._x2go_exec_command('which x2gofeaturelist >/dev/null && x2gofeaturelist')
     591              self._server_features = stdout.read().split('\n')
     592              self._server_features = [ f for f in self._server_features if f ]
     593              self._server_features.sort()
     594              self.logger('server-side X2Go features are: %s' % self._server_features, loglevel=log.loglevel_DEBUG)
     595          return self._server_features
     596
     597 -    def query_server_features(self, force=False):
     598          """\
     599          Do a query for the server-side list of X2Go features.
     600
     601          @param force: do not use the cached feature list, really ask the server (again)
     602          @type force: C{bool}
     603
     604          @return: list of X2Go feature names
     605          @rtype: C{list}
     606
     607          """
     608          if force:
     609              self._server_features = None
     610          return self._x2go_server_features
     611      get_server_features = query_server_features
     612
     613      @property
     614 -    def _x2go_remote_home(self):
     615          """\
     616          Retrieve and cache the remote home directory location.
     617
     618          """
     619          if self._remote_home is None:
     620              (stdin, stdout, stderr) = self._x2go_exec_command('echo $HOME')
     621              stdout_r = stdout.read()
     622              if stdout_r:
     623                  self._remote_home = stdout_r.split()[0]
     624                  self.logger('remote user\' home directory: %s' % self._remote_home, loglevel=log.loglevel_DEBUG)
     625              return self._remote_home
     626          else:
     627              return self._remote_home
     628
     629 -    def _x2go_remote_group(self, group):
     630          """\
     631          Retrieve and cache the members of a server-side POSIX group.
     632
     633          @param group: remote POSIX group name
     634          @type group: C{str}
     635
     636          @return: list of POSIX group members
     637          @rtype: C{list}
     638
     639          """
     640          if not self._remote_group.has_key(group):
     641              (stdin, stdout, stderr) = self._x2go_exec_command('getent group %s | cut -d":" -f4' % group)
     642              self._remote_group[group] = stdout.read().split('\n')[0].split(',')
     643              self.logger('remote %s group: %s' % (group, self._remote_group[group]), loglevel=log.loglevel_DEBUG)
     644              return self._remote_group[group]
     645          else:
     646              return self._remote_group[group]
     647
     648 -    def is_x2gouser(self, username):
     649          """\
     650          Is the remote user allowed to launch X2Go sessions?
     651
     652          FIXME: this method is currently non-functional.
     653
     654          @param username: remote user name
     655          @type username: C{str}
     656
     657          @return: C{True} if the remote user is allowed to launch X2Go sessions
     658          @rtype: C{bool}
     659
     660          """
     661          ###
     662          ### FIXME:
     663          ###
     664          # discussion about server-side access restriction based on posix group membership or similar currently
     665          # in process (as of 20110517, mg)
     666          #return username in self._x2go_remote_group('x2gousers')
     667          return True
     668
     669 -    def is_sshfs_available(self):
     670          """\
     671          Check if the remote user is allowed to use SSHFS mounts.
     672
     673          @return: C{True} if the user is allowed to connect client-side shares to the X2Go session
     674          @rtype: C{bool}
     675
     676          """
     677          (stdin, stdout, stderr) = self._x2go_exec_command('which fusermount')
     678
     679          # if which returns the full path of fusermount, the current use is allowed to execute it
     680          return bool(stdout.read())
     681
     682 -    def remote_username(self):
     683          """\
     684          Returns (and caches) the control session's remote username.
     685
     686          @return: SSH transport's user name
     687          @rtype: C{str}
     688
     689          @raise X2GoControlSessionException: on SSH connection loss
     690
     691          """
     692          if self._remote_username is None:
     693              if self.get_transport() is not None:
     694                  try:
     695                      self._remote_username = self.get_transport().get_username()
     696                  except:
     697                      self.session_died = True
     698                      raise x2go_exceptions.X2GoControlSessionException('Lost connection to X2Go server')
     699          return self._remote_username
     700
     701 -    def remote_peername(self):
     702          """\
     703          Returns (and caches) the control session's remote host (name or ip).
     704
     705          @return: SSH transport's peer name
     706          @rtype: C{tuple}
     707
     708          @raise X2GoControlSessionException: on SSH connection loss
     709
     710          """
     711          if self._remote_peername is None:
     712              if self.get_transport() is not None:
     713                  try:
     714                      self._remote_peername = self.get_transport().getpeername()
     715                  except:
     716                      self.session_died = True
     717                      raise x2go_exceptions.X2GoControlSessionException('Lost connection to X2Go server')
     718          return self._remote_peername
     719
     720      @property
     721 -    def _x2go_session_auth_rsakey(self):
     722          """\
     723          Generate (and cache) a temporary RSA host key for the lifetime of this control session.
     724
     725          """
     726          if self._session_auth_rsakey is None:
     727              self._session_auth_rsakey = paramiko.RSAKey.generate(defaults.RSAKEY_STRENGTH)
     728          return self._session_auth_rsakey
     729
     730 -    def set_profile_name(self, profile_name):
     731          """\
     732          Manipulate the control session's profile name.
     733
     734          @param profile_name: new profile name for this control session
     735          @type profile_name: C{str}
     736
     737          """
     738          self.profile_name = profile_name
     739
     740 -    def check_host(self, hostname, port=22):
     741          """\
     742          Wraps around a Paramiko/SSH host key check.
     743
     744          @param hostname: the remote X2Go server's hostname
     745          @type hostname: C{str}
     746          @param port: the SSH port of the remote X2Go server
     747          @type port: C{int}
     748
     749          @return: C{True} if the host key check succeeded, C{False} otherwise
     750          @rtype: C{bool}
     751
     752          """
     753          # trailing whitespace tolerance
     754          hostname = hostname.strip()
     755
     756          # force into IPv4 for localhost connections
     757          if hostname in ('localhost', 'localhost.localdomain'):
     758              hostname = '127.0.0.1'
     759
     760          return checkhosts.check_ssh_host_key(self, hostname, port=port)
     761
     762 -    def connect(self, hostname, port=22, username=None, password=None, passphrase=None, pkey=None,
     763                  key_filename=None, timeout=None, allow_agent=False, look_for_keys=False,
     764                  use_sshproxy=False, sshproxy_host=None, sshproxy_port=22, sshproxy_user=None, sshproxy_password=None, sshproxy_force_password_auth=False,
     765                  sshproxy_key_filename=None, sshproxy_pkey=None, sshproxy_look_for_keys=False, sshproxy_passphrase='', sshproxy_allow_agent=False,
     766                  sshproxy_tunnel=None,
     767                  add_to_known_hosts=None,
     768                  forward_sshagent=None,
     769                  unique_hostkey_aliases=None,
     770                  force_password_auth=False,
     771                  session_instance=None,
     772          ):
     773          """\
     774          Connect to an X2Go server and authenticate to it. This method is directly
     775          inherited from the C{paramiko.SSHClient} class. The features of the Paramiko
     776          SSH client connect method are recited here. The parameters C{add_to_known_hosts},
     777          C{force_password_auth}, C{session_instance} and all SSH proxy related parameters
     778          have been added as X2Go specific parameters
     779
     780          The server's host key is checked against the system host keys
     781          (see C{load_system_host_keys}) and any local host keys (C{load_host_keys}).
     782          If the server's hostname is not found in either set of host keys, the missing host
     783          key policy is used (see C{set_missing_host_key_policy}).  The default policy is
     784          to reject the key and raise an C{SSHException}.
     785
     786          Authentication is attempted in the following order of priority:
     787
     788              - The C{pkey} or C{key_filename} passed in (if any)
     789              - Any key we can find through an SSH agent
     790              - Any "id_rsa" or "id_dsa" key discoverable in C{~/.ssh/}
     791              - Plain username/password auth, if a password was given
     792
     793          If a private key requires a password to unlock it, and a password is
     794          passed in, that password will be used to attempt to unlock the key.
     795
     796          @param hostname: the server to connect to
     797          @type hostname: C{str}
     798          @param port: the server port to connect to
     799          @type port: C{int}
     800          @param username: the username to authenticate as (defaults to the
     801              current local username)
     802          @type username: C{str}
     803          @param password: a password to use for authentication or for unlocking
     804              a private key
     805          @type password: C{str}
     806          @param passphrase: a passphrase to use for unlocking
     807              a private key in case the password is already needed for two-factor
     808              authentication
     809          @type passphrase: C{str}
     810          @param key_filename: the filename, or list of filenames, of optional
     811              private key(s) to try for authentication
     812          @type key_filename: C{str} or list(str)
     813          @param pkey: an optional private key to use for authentication
     814          @type pkey: C{PKey}
     815          @param forward_sshagent: forward SSH agent authentication requests to the X2Go client-side
     816              (will update the class property of the same name)
     817          @type forward_sshagent: C{bool}
     818          @param unique_hostkey_aliases: update the unique_hostkey_aliases class property
     819          @type unique_hostkey_aliases: C{bool}
     820          @param timeout: an optional timeout (in seconds) for the TCP connect
     821          @type timeout: float
     822          @param look_for_keys: set to C{True} to enable searching for discoverable
     823              private key files in C{~/.ssh/}
     824          @type look_for_keys: C{bool}
     825          @param allow_agent: set to C{True} to enable connecting to a local SSH agent
     826              for acquiring authentication information
     827          @type allow_agent: C{bool}
     828          @param add_to_known_hosts: non-paramiko option, if C{True} paramiko.AutoAddPolicy()
     829              is used as missing-host-key-policy. If set to C{False} paramiko.RejectPolicy()
     830              is used
     831          @type add_to_known_hosts: C{bool}
     832          @param force_password_auth: non-paramiko option, disable pub/priv key authentication
     833              completely, even if the C{pkey} or the C{key_filename} parameter is given
     834          @type force_password_auth: C{bool}
     835          @param session_instance: an instance L{X2GoSession} using this L{X2GoControlSession}
     836              instance.
     837          @type session_instance: C{obj}
     838          @param use_sshproxy: connect through an SSH proxy
     839          @type use_sshproxy: C{True} if an SSH proxy is to be used for tunneling the connection
     840          @param sshproxy_host: hostname of the SSH proxy server
     841          @type sshproxy_host: C{str}
     842          @param sshproxy_port: port of the SSH proxy server
     843          @type sshproxy_port: C{int}
     844          @param sshproxy_user: username that we use for authenticating against C{<sshproxy_host>}
     845          @type sshproxy_user: C{str}
     846          @param sshproxy_password: a password to use for SSH proxy authentication or for unlocking
     847              a private key
     848          @type sshproxy_password: C{str}
     849          @param sshproxy_passphrase: a passphrase to use for unlocking
     850              a private key needed for the SSH proxy host in case the sshproxy_password is already needed for
     851              two-factor authentication
     852          @type sshproxy_passphrase: C{str}
     853          @param sshproxy_force_password_auth: enforce using a given C{sshproxy_password} even if a key(file) is given
     854          @type sshproxy_force_password_auth: C{bool}
     855          @param sshproxy_key_filename: local file location of the private key file
     856          @type sshproxy_key_filename: C{str}
     857          @param sshproxy_pkey: an optional private key to use for SSH proxy authentication
     858          @type sshproxy_pkey: C{PKey}
     859          @param sshproxy_look_for_keys: set to C{True} to enable connecting to a local SSH agent
     860              for acquiring authentication information (for SSH proxy authentication)
     861          @type sshproxy_look_for_keys: C{bool}
     862          @param sshproxy_allow_agent: set to C{True} to enable connecting to a local SSH agent
     863              for acquiring authentication information (for SSH proxy authentication)
     864          @type sshproxy_allow_agent: C{bool}
     865          @param sshproxy_tunnel: the SSH proxy tunneling parameters, format is: <local-address>:<local-port>:<remote-address>:<remote-port>
     866          @type sshproxy_tunnel: C{str}
     867
     868          @return: C{True} if an authenticated SSH transport could be retrieved by this method
     869          @rtype: C{bool}
     870
     871          @raise BadHostKeyException: if the server's host key could not be
     872              verified
     873          @raise AuthenticationException: if authentication failed
     874          @raise SSHException: if there was any other error connecting or
     875              establishing an SSH session
     876          @raise socket.error: if a socket error occurred while connecting
     877          @raise X2GoSSHProxyException: any SSH proxy exception is passed through while establishing the SSH proxy connection and tunneling setup
     878          @raise X2GoSSHAuthenticationException: any SSH proxy authentication exception is passed through while establishing the SSH proxy connection and tunneling setup
     879          @raise X2GoRemoteHomeException: if the remote home directory does not exist or is not accessible
     880          @raise X2GoControlSessionException: if the remote peer has died unexpectedly
     881
     882          """
     883          _fake_hostname = None
     884
     885          if hostname and type(hostname) not in (types.UnicodeType, types.StringType):
     886              hostname = [hostname]
     887          if hostname and type(hostname) is types.ListType:
     888              hostname = random.choice(hostname)
     889
     890          if not username:
     891              self.logger('no username specified, cannot connect without username', loglevel=log.loglevel_ERROR)
     892              raise paramiko.AuthenticationException('no username specified, cannot connect without username')
     893
     894          if type(password) not in (types.StringType, types.UnicodeType):
     895              password = ''
     896          if type(sshproxy_password) not in (types.StringType, types.UnicodeType):
     897              sshproxy_password = ''
     898
     899          if unique_hostkey_aliases is None:
     900              unique_hostkey_aliases = self.unique_hostkey_aliases
     901          # prep the fake hostname with the real hostname, so we trigger the corresponding code path in
     902          # x2go.checkhosts and either of its missing host key policies
     903          if unique_hostkey_aliases:
     904              if port != 22: _fake_hostname = "[%s]:%s" % (hostname, port)
     905              else: _fake_hostname = hostname
     906
     907          if add_to_known_hosts is None:
     908              add_to_known_hosts = self.add_to_known_hosts
     909
     910          if forward_sshagent is None:
     911              forward_sshagent = self.forward_sshagent
     912
     913          if look_for_keys:
     914              key_filename = None
     915              pkey = None
     916
     917          _twofactorauth = False
     918          if password and (passphrase is None) and not force_password_auth: passphrase = password
     919
     920          if use_sshproxy and sshproxy_host and sshproxy_user:
     921              try:
     922                  if not sshproxy_tunnel:
     923                      sshproxy_tunnel = "localhost:44444:%s:%s" % (hostname, port)
     924                  self.sshproxy_session = sshproxy.X2GoSSHProxy(known_hosts=self.known_hosts,
     925                                                                add_to_known_hosts=add_to_known_hosts,
     926                                                                sshproxy_host=sshproxy_host,
     927                                                                sshproxy_port=sshproxy_port,
     928                                                                sshproxy_user=sshproxy_user,
     929                                                                sshproxy_password=sshproxy_password,
     930                                                                sshproxy_passphrase=sshproxy_passphrase,
     931                                                                sshproxy_force_password_auth=sshproxy_force_password_auth,
     932                                                                sshproxy_key_filename=sshproxy_key_filename,
     933                                                                sshproxy_pkey=sshproxy_pkey,
     934                                                                sshproxy_look_for_keys=sshproxy_look_for_keys,
     935                                                                sshproxy_allow_agent=sshproxy_allow_agent,
     936                                                                sshproxy_tunnel=sshproxy_tunnel,
     937                                                                session_instance=session_instance,
     938                                                                logger=self.logger,
     939                                                               )
     940                  hostname = self.sshproxy_session.get_local_proxy_host()
     941                  port = self.sshproxy_session.get_local_proxy_port()
     942                  _fake_hostname = self.sshproxy_session.get_remote_host()
     943                  _fake_port = self.sshproxy_session.get_remote_port()
     944                  if _fake_port != 22:
     945                      _fake_hostname = "[%s]:%s" % (_fake_hostname, _fake_port)
     946
     947              except:
     948                  if self.sshproxy_session:
     949                      self.sshproxy_session.stop_thread()
     950                  self.sshproxy_session = None
     951                  raise
     952
     953              if self.sshproxy_session is not None:
     954                  self.sshproxy_session.start()
     955
     956                  # divert port to sshproxy_session's local forwarding port (it might have changed due to
     957                  # SSH connection errors
     958                  gevent.sleep(.1)
     959                  port = self.sshproxy_session.get_local_proxy_port()
     960
     961          if not add_to_known_hosts and session_instance:
     962              self.set_missing_host_key_policy(checkhosts.X2GoInteractiveAddPolicy(caller=self, session_instance=session_instance, fake_hostname=_fake_hostname))
     963
     964          if add_to_known_hosts:
     965              self.set_missing_host_key_policy(checkhosts.X2GoAutoAddPolicy(caller=self, session_instance=session_instance, fake_hostname=_fake_hostname))
     966
     967          # trailing whitespace tolerance in hostname
     968          hostname = hostname.strip()
     969
     970          self.logger('connecting to [%s]:%s' % (hostname, port), loglevel=log.loglevel_NOTICE)
     971
     972          self.load_session_host_keys()
     973
     974          _hostname = hostname
     975          # enforce IPv4 for localhost address
     976          if _hostname in ('localhost', 'localhost.localdomain'):
     977              _hostname = '127.0.0.1'
     978
     979          # update self.forward_sshagent via connect method parameter
     980          if forward_sshagent is not None:
     981              self.forward_sshagent = forward_sshagent
     982
     983          if timeout and self.low_latency:
     984              timeout = timeout * 2
     985
     986          if key_filename and "~" in key_filename:
     987              key_filename = os.path.expanduser(key_filename)
     988
     989          if key_filename or pkey or look_for_keys or allow_agent or (password and force_password_auth):
     990              try:
     991                  if password and force_password_auth:
     992                      self.logger('trying password based SSH authentication with server', loglevel=log.loglevel_DEBUG)
     993                      paramiko.SSHClient.connect(self, _hostname, port=port, username=username, password=password, pkey=None,
     994                                                 key_filename=None, timeout=timeout, allow_agent=False,
     995                                                 look_for_keys=False)
     996                  elif (key_filename and os.path.exists(os.path.normpath(key_filename))) or pkey:
     997                      self.logger('trying SSH pub/priv key authentication with server', loglevel=log.loglevel_DEBUG)
     998                      paramiko.SSHClient.connect(self, _hostname, port=port, username=username, pkey=pkey, password=passphrase,
     999                                                 key_filename=key_filename, timeout=timeout, allow_agent=False,
    1000                                                 look_for_keys=False)
    1001                  else:
    1002                      self.logger('trying SSH key discovery or agent authentication with server', loglevel=log.loglevel_DEBUG)
    1003                      paramiko.SSHClient.connect(self, _hostname, port=port, username=username, pkey=None, password=passphrase,
    1004                                                 key_filename=None, timeout=timeout, allow_agent=allow_agent,
    1005                                                 look_for_keys=look_for_keys)
    1006
    1007              except (paramiko.PasswordRequiredException, paramiko.SSHException), e:
    1008                  self.close()
    1009                  if type(e) == paramiko.SSHException and str(e).startswith('Two-factor authentication requires a password'):
    1010                      self.logger('X2Go Server requests two-factor authentication', loglevel=log.loglevel_NOTICE)
    1011                      _twofactorauth = True
    1012                  if passphrase is not None:
    1013                      self.logger('unlock SSH private key file with provided password', loglevel=log.loglevel_INFO)
    1014                      try:
    1015                          if not password: password = None
    1016                          if (key_filename and os.path.exists(os.path.normpath(key_filename))) or pkey:
    1017                              self.logger('re-trying SSH pub/priv key authentication with server', loglevel=log.loglevel_DEBUG)
    1018                              try:
    1019                                  paramiko.SSHClient.connect(self, _hostname, port=port, username=username, password=password, passphrase=passphrase, pkey=pkey,
    1020                                                             key_filename=key_filename, timeout=timeout, allow_agent=False,
    1021                                                             look_for_keys=False)
    1022                              except TypeError:
    1023                                  if _twofactorauth and password and passphrase and password != passphrase:
    1024                                      self.logger('your version of Paramiko/SSH does not support authentication workflows which require SSH key decryption in combination with two-factor authentication', loglevel=log.loglevel_WARN)
    1025                                  paramiko.SSHClient.connect(self, _hostname, port=port, username=username, password=password, pkey=pkey,
    1026                                                             key_filename=key_filename, timeout=timeout, allow_agent=False,
    1027                                                             look_for_keys=False)
    1028                          else:
    1029                              self.logger('re-trying SSH key discovery now with passphrase for unlocking the key(s)', loglevel=log.loglevel_DEBUG)
    1030                              try:
    1031                                  paramiko.SSHClient.connect(self, _hostname, port=port, username=username, password=password, passphrase=passphrase, pkey=None,
    1032                                                             key_filename=None, timeout=timeout, allow_agent=allow_agent,
    1033                                                             look_for_keys=look_for_keys)
    1034                              except TypeError:
    1035                                  if _twofactorauth and password and passphrase and password != passphrase:
    1036                                      self.logger('your version of Paramiko/SSH does not support authentication workflows which require SSH key decryption in combination with two-factor authentication', loglevel=log.loglevel_WARN)
    1037                                  paramiko.SSHClient.connect(self, _hostname, port=port, username=username, password=password, pkey=None,
    1038                                                             key_filename=None, timeout=timeout, allow_agent=allow_agent,
    1039                                                             look_for_keys=look_for_keys)
    1040
    1041                      except paramiko.AuthenticationException, auth_e:
    1042                          # the provided password cannot be used to unlock any private SSH key file (i.e. wrong password)
    1043                          raise paramiko.AuthenticationException(str(auth_e))
    1044
    1045                      except paramiko.SSHException, auth_e:
    1046                          if str(auth_e) == 'No authentication methods available':
    1047                              raise paramiko.AuthenticationException('Interactive password authentication required!')
    1048                          else:
    1049                              self.close()
    1050                              if self.sshproxy_session:
    1051                                  self.sshproxy_session.stop_thread()
    1052                              raise auth_e
    1053
    1054                  else:
    1055                      self.close()
    1056                      if self.sshproxy_session:
    1057                          self.sshproxy_session.stop_thread()
    1058                      raise e
    1059
    1060              except paramiko.AuthenticationException, e:
    1061                  self.close()
    1062                  if password:
    1063                      self.logger('next auth mechanism we\'ll try is password authentication', loglevel=log.loglevel_DEBUG)
    1064                      try:
    1065                          paramiko.SSHClient.connect(self, _hostname, port=port, username=username, password=password,
    1066                                                     key_filename=None, pkey=None, timeout=timeout, allow_agent=False, look_for_keys=False)
    1067                      except:
    1068                          self.close()
    1069                          if self.sshproxy_session:
    1070                              self.sshproxy_session.stop_thread()
    1071                          raise
    1072                  else:
    1073                      self.close()
    1074                      if self.sshproxy_session:
    1075                          self.sshproxy_session.stop_thread()
    1076                      raise e
    1077
    1078              except paramiko.SSHException, e:
    1079                  if str(e) == 'No authentication methods available':
    1080                      raise paramiko.AuthenticationException('Interactive password authentication required!')
    1081                  else:
    1082                      self.close()
    1083                      if self.sshproxy_session:
    1084                          self.sshproxy_session.stop_thread()
    1085                      raise e
    1086
    1087              except:
    1088                  self.close()
    1089                  if self.sshproxy_session:
    1090                      self.sshproxy_session.stop_thread()
    1091                  raise
    1092
    1093          # if there is no private key (and no agent auth), we will use the given password, if any
    1094          else:
    1095              # create a random password if password is empty to trigger host key validity check
    1096              if not password:
    1097                  password = "".join([random.choice(string.letters+string.digits) for x in range(1, 20)])
    1098              self.logger('performing SSH password authentication with server', loglevel=log.loglevel_DEBUG)
    1099              try:
    1100                  paramiko.SSHClient.connect(self, _hostname, port=port, username=username, password=password,
    1101                                             timeout=timeout, allow_agent=False, look_for_keys=False)
    1102              except:
    1103                  self.close()
    1104                  if self.sshproxy_session:
    1105                      self.sshproxy_session.stop_thread()
    1106                  raise
    1107
    1108          self.set_missing_host_key_policy(paramiko.RejectPolicy())
    1109
    1110          self.hostname = hostname
    1111          self.port = port
    1112
    1113          # preparing reverse tunnels
    1114          ssh_transport = self.get_transport()
    1115          ssh_transport.reverse_tunnels = {}
    1116
    1117          # mark Paramiko/SSH transport as X2GoControlSession
    1118          ssh_transport._x2go_session_marker = True
    1119          try:
    1120              self._session_password = base64.b64encode(password)
    1121          except TypeError:
    1122              self._session_password = None
    1123
    1124          if ssh_transport is not None:
    1125
    1126              # since Paramiko 1.7.7.1 there is compression available, let's use it if present...
    1127              if x2go._paramiko.PARAMIKO_FEATURE['use-compression']:
    1128                  ssh_transport.use_compression(compress=False)
    1129              # enable keep alive callbacks
    1130              ssh_transport.set_keepalive(5)
    1131
    1132              self.session_died = False
    1133              self.query_server_features(force=True)
    1134              if self.forward_sshagent:
    1135                  if x2go._paramiko.PARAMIKO_FEATURE['forward-ssh-agent']:
    1136                      try:
    1137                          self.agent_chan = ssh_transport.open_session()
    1138                          self.agent_handler = paramiko.agent.AgentRequestHandler(self.agent_chan)
    1139                          self.logger('Requesting SSH agent forwarding for control session of connected session profile %s' % self.profile_name, loglevel=log.loglevel_INFO)
    1140                      except EOFError, e:
    1141                          # if we come across an EOFError here, we must assume the session is dead...
    1142                          self.session_died = True
    1143                          raise x2go_exceptions.X2GoControlSessionException('The SSH connection was dropped while setting up SSH agent forwarding socket.')
    1144                  else:
    1145                      self.logger('SSH agent forwarding is not available in the Paramiko version used with this instance of Python X2Go', loglevel=log.loglevel_WARN)
    1146
    1147          else:
    1148              self.close()
    1149              if self.sshproxy_session:
    1150                  self.sshproxy_session.stop_thread()
    1151
    1152          self._remote_home = None
    1153          if not self.home_exists():
    1154              self.close()
    1155              if self.sshproxy_session:
    1156                  self.sshproxy_session.stop_thread()
    1157              raise x2go_exceptions.X2GoRemoteHomeException('remote home directory does not exist')
    1158
    1159          return (self.get_transport() is not None)
    1160
    1161 -    def dissociate(self, terminal_session):
    1162          """\
    1163          Drop an associated terminal session.
    1164
    1165          @param terminal_session: the terminal session object to remove from the list of associated terminals
    1166          @type terminal_session: C{X2GoTerminalSession*}
    1167
    1168          """
    1169          for t_name in self.associated_terminals.keys():
    1170              if self.associated_terminals[t_name] == terminal_session:
    1171                  del self.associated_terminals[t_name]
    1172                  if self.terminated_terminals.has_key(t_name):
    1173                      del self.terminated_terminals[t_name]
    1174
    1175 -    def disconnect(self):
    1176          """\
    1177          Disconnect this control session from the remote server.
    1178
    1179          @return: report success or failure after having disconnected
    1180          @rtype: C{bool}
    1181
    1182          """
    1183          if self.associated_terminals:
    1184              t_names = self.associated_terminals.keys()
    1185              for t_obj in self.associated_terminals.values():
    1186                  try:
    1187                      if not self.session_died:
    1188                          t_obj.suspend()
    1189                  except x2go_exceptions.X2GoTerminalSessionException:
    1190                      pass
    1191                  except x2go_exceptions.X2GoControlSessionException:
    1192                      self.session_died
    1193                  t_obj.__del__()
    1194              for t_name in t_names:
    1195                  try:
    1196                      del self.associated_terminals[t_name]
    1197                  except KeyError:
    1198                      pass
    1199
    1200          self._remote_home = None
    1201          self._remote_group = {}
    1202
    1203          self._session_auth_rsakey = None
    1204
    1205          # in any case, release out internal transport lock
    1206          if self._transport_lock.locked():
    1207              self._transport_lock.release()
    1208
    1209          # close SSH agent auth forwarding objects
    1210          if self.agent_handler is not None:
    1211              self.agent_handler.close()
    1212
    1213          if self.agent_chan is not None:
    1214              try:
    1215                  self.agent_chan.close()
    1216              except EOFError:
    1217                  pass
    1218
    1219          retval = False
    1220          try:
    1221              if self.get_transport() is not None:
    1222                  retval = self.get_transport().is_active()
    1223                  try:
    1224                      self.close()
    1225                  except IOError:
    1226                      pass
    1227          except AttributeError:
    1228              # if the Paramiko _transport object has not yet been initialized, ignore it
    1229              # but state that this method call did not close the SSH client, but was already closed
    1230              pass
    1231
    1232          # take down sshproxy_session no matter what happened to the control session itself
    1233          if self.sshproxy_session is not None:
    1234              self.sshproxy_session.stop_thread()
    1235
    1236          return retval
    1237
    1238 -    def home_exists(self):
    1239          """\
    1240          Test if the remote home directory exists.
    1241
    1242          @return: C{True} if the home directory exists, C{False} otherwise
    1243          @rtype: C{bool}
    1244
    1245          """
    1246          (_stdin, _stdout, _stderr) = self._x2go_exec_command('stat -tL "%s"' % self._x2go_remote_home, loglevel=log.loglevel_DEBUG)
    1247          if _stdout.read():
    1248              return True
    1249          return False
    1250
    1251
    1252 -    def is_alive(self):
    1253          """\
    1254          Test if the connection to the remote X2Go server is still alive.
    1255
    1256          @return: C{True} if the connection is still alive, C{False} otherwise
    1257          @rtype: C{bool}
    1258
    1259          """
    1260          try:
    1261              if self._x2go_exec_command('echo', loglevel=log.loglevel_DEBUG):
    1262                  return True
    1263          except x2go_exceptions.X2GoControlSessionException:
    1264              self.session_died = True
    1265              self.disconnect()
    1266          return False
    1267
    1268 -    def has_session_died(self):
    1269          """\
    1270          Test if the connection to the remote X2Go server died on the way.
    1271
    1272          @return: C{True} if the connection has died, C{False} otherwise
    1273          @rtype: C{bool}
    1274
    1275          """
    1276          return self.session_died
    1277
    1278 -    def get_published_applications(self, lang=None, refresh=False, raw=False, very_raw=False, max_no_submenus=defaults.PUBAPP_MAX_NO_SUBMENUS):
    1279          """\
    1280          Retrieve the menu tree of published applications from the remote X2Go server.
    1281
    1282          The C{raw} option lets this method return a C{list} of C{dict} elements. Each C{dict} elements has a
    1283          C{desktop} key containing a shortened version of the text output of a .desktop file and an C{icon} key
    1284          which contains the desktop base64-encoded icon data.
    1285
    1286          The {very_raw} lets this method return the output of the C{x2gogetapps} script as is.
    1287
    1288          @param lang: locale/language identifier
    1289          @type lang: C{str}
    1290          @param refresh: force reload of the menu tree from X2Go server
    1291          @type refresh: C{bool}
    1292          @param raw: retrieve a raw output of the server list of published applications
    1293          @type raw: C{bool}
    1294          @param very_raw: retrieve a very raw output of the server list of published applications
    1295          @type very_raw: C{bool}
    1296
    1297          @return: an i18n capable menu tree packed as a Python dictionary
    1298          @rtype: C{list}
    1299
    1300          """
    1301          self._already_querying_published_applications.acquire()
    1302
    1303          if defaults.X2GOCLIENT_OS != 'Windows' and lang is None:
    1304              lang = locale.getdefaultlocale()[0]
    1305          elif lang is None:
    1306              lang = 'en'
    1307
    1308          if 'X2GO_PUBLISHED_APPLICATIONS' in self.get_server_features():
    1309              if self._published_applications_menu is {} or \
    1310                 not self._published_applications_menu.has_key(lang) or \
    1311                 raw or very_raw or refresh or \
    1312                 (self.published_applications_no_submenus != max_no_submenus):
    1313
    1314                  self.published_applications_no_submenus = max_no_submenus
    1315
    1316                  ### STAGE 1: retrieve menu from server
    1317
    1318                  self.logger('querying server (%s) for list of published applications' % self.profile_name, loglevel=log.loglevel_NOTICE)
    1319                  (stdin, stdout, stderr) = self._x2go_exec_command('which x2gogetapps >/dev/null && x2gogetapps')
    1320                  _raw_output = stdout.read()
    1321
    1322                  if very_raw:
    1323                      self.logger('published applications query for %s finished, return very raw output' % self.profile_name, loglevel=log.loglevel_NOTICE)
    1324                      self._already_querying_published_applications.release()
    1325                      return _raw_output
    1326
    1327                  ### STAGE 2: dissect the text file retrieved from server, cut into single menu elements
    1328
    1329                  _raw_menu_items = _raw_output.split('</desktop>\n')
    1330                  _raw_menu_items = [ i.replace('<desktop>\n', '') for i in _raw_menu_items ]
    1331                  _menu = []
    1332                  for _raw_menu_item in _raw_menu_items:
    1333                      if '<icon>\n' in _raw_menu_item and '</icon>' in _raw_menu_item:
    1334                          _menu_item = _raw_menu_item.split('<icon>\n')[0] + _raw_menu_item.split('</icon>\n')[1]
    1335                          _icon_base64 = _raw_menu_item.split('<icon>\n')[1].split('</icon>\n')[0]
    1336                      else:
    1337                          _menu_item = _raw_menu_item
    1338                          _icon_base64 = None
    1339                      if _menu_item:
    1340                          _menu.append({ 'desktop': _menu_item, 'icon': _icon_base64, })
    1341                          _menu_item = None
    1342                          _icon_base64 = None
    1343
    1344                  if raw:
    1345                      self.logger('published applications query for %s finished, returning raw output' % self.profile_name, loglevel=log.loglevel_NOTICE)
    1346                      self._already_querying_published_applications.release()
    1347                      return _menu
    1348
    1349                  if len(_menu) > max_no_submenus >= 0:
    1350                      _render_submenus = True
    1351                  else:
    1352                      _render_submenus = False
    1353
    1354                  # STAGE 3: create menu structure in a Python dictionary
    1355
    1356                  _category_map = {
    1357                      lang: {
    1358                          'Multimedia': [],
    1359                          'Development': [],
    1360                          'Education': [],
    1361                          'Games': [],
    1362                          'Graphics': [],
    1363                          'Internet': [],
    1364                          'Office': [],
    1365                          'System': [],
    1366                          'Utilities': [],
    1367                          'Other Applications': [],
    1368                          'TOP': [],
    1369                      }
    1370                  }
    1371                  _empty_menus = _category_map[lang].keys()
    1372
    1373                  for item in _menu:
    1374
    1375                      _menu_entry_name = ''
    1376                      _menu_entry_fallback_name = ''
    1377                      _menu_entry_comment = ''
    1378                      _menu_entry_fallback_comment = ''
    1379                      _menu_entry_exec = ''
    1380                      _menu_entry_cat = ''
    1381                      _menu_entry_shell = False
    1382
    1383                      lang_regio = lang
    1384                      lang_only = lang_regio.split('_')[0]
    1385
    1386                      for line in item['desktop'].split('\n'):
    1387                          if re.match('^Name\[%s\]=.*' % lang_regio, line) or re.match('Name\[%s\]=.*' % lang_only, line):
    1388                              _menu_entry_name = line.split("=")[1].strip()
    1389                          elif re.match('^Name=.*', line):
    1390                              _menu_entry_fallback_name = line.split("=")[1].strip()
    1391                          elif re.match('^Comment\[%s\]=.*' % lang_regio, line) or re.match('Comment\[%s\]=.*' % lang_only, line):
    1392                              _menu_entry_comment = line.split("=")[1].strip()
    1393                          elif re.match('^Comment=.*', line):
    1394                              _menu_entry_fallback_comment = line.split("=")[1].strip()
    1395                          elif re.match('^Exec=.*', line):
    1396                              _menu_entry_exec = line.split("=")[1].strip()
    1397                          elif re.match('^Terminal=.*(t|T)(r|R)(u|U)(e|E).*', line):
    1398                              _menu_entry_shell = True
    1399                          elif re.match('^Categories=.*', line):
    1400                              if 'X2Go-Top' in line:
    1401                                  _menu_entry_cat = 'TOP'
    1402                              elif 'Audio' in line or 'Video' in line:
    1403                                  _menu_entry_cat = 'Multimedia'
    1404                              elif 'Development' in line:
    1405                                  _menu_entry_cat = 'Development'
    1406                              elif 'Education' in line:
    1407                                  _menu_entry_cat = 'Education'
    1408                              elif 'Game' in line:
    1409                                  _menu_entry_cat = 'Games'
    1410                              elif 'Graphics' in line:
    1411                                  _menu_entry_cat = 'Graphics'
    1412                              elif 'Network' in line:
    1413                                  _menu_entry_cat = 'Internet'
    1414                              elif 'Office' in line:
    1415                                  _menu_entry_cat = 'Office'
    1416                              elif 'Settings' in line:
    1417                                  continue
    1418                              elif 'System' in line:
    1419                                  _menu_entry_cat = 'System'
    1420                              elif 'Utility' in line:
    1421                                  _menu_entry_cat = 'Utilities'
    1422                              else:
    1423                                  _menu_entry_cat = 'Other Applications'
    1424
    1425                      if not _menu_entry_exec:
    1426                          continue
    1427                      else:
    1428                          # FIXME: strip off any noted options (%f, %F, %u, %U, ...), this can be more intelligent
    1429                          _menu_entry_exec = _menu_entry_exec.replace('%f', '').replace('%F','').replace('%u','').replace('%U','')
    1430                          if _menu_entry_shell:
    1431                              _menu_entry_exec = "x-terminal-emulator -e '%s'" % _menu_entry_exec
    1432
    1433                      if not _menu_entry_cat:
    1434                          _menu_entry_cat = 'Other Applications'
    1435
    1436                      if not _render_submenus:
    1437                          _menu_entry_cat = 'TOP'
    1438
    1439                      if _menu_entry_cat in _empty_menus:
    1440                          _empty_menus.remove(_menu_entry_cat)
    1441
    1442                      if not _menu_entry_name: _menu_entry_name = _menu_entry_fallback_name
    1443                      if not _menu_entry_comment: _menu_entry_comment = _menu_entry_fallback_comment
    1444                      if not _menu_entry_comment: _menu_entry_comment = _menu_entry_name
    1445
    1446                      _menu_entry_icon = item['icon']
    1447
    1448                      _category_map[lang][_menu_entry_cat].append(
    1449                          {
    1450                              'name': _menu_entry_name,
    1451                              'comment': _menu_entry_comment,
    1452                              'exec': _menu_entry_exec,
    1453                              'icon': _menu_entry_icon,
    1454                          }
    1455                      )
    1456
    1457                  for _cat in _empty_menus:
    1458                      del _category_map[lang][_cat]
    1459
    1460                  for _cat in _category_map[lang].keys():
    1461                      _sorted = sorted(_category_map[lang][_cat], key=lambda k: k['name'])
    1462                      _category_map[lang][_cat] = _sorted
    1463
    1464                  self._published_applications_menu.update(_category_map)
    1465                  self.logger('published applications query for %s finished, return menu tree' % self.profile_name, loglevel=log.loglevel_NOTICE)
    1466
    1467          else:
    1468              # FIXME: ignoring the absence of the published applications feature for now, handle it appropriately later
    1469              pass
    1470
    1471          self._already_querying_published_applications.release()
    1472          return self._published_applications_menu
    1473
    1474 -    def start(self, **kwargs):
    1475          """\
    1476          Start a new X2Go session.
    1477
    1478          The L{X2GoControlSession.start()} method accepts any parameter
    1479          that can be passed to any of the C{X2GoTerminalSession} backend class
    1480          constructors.
    1481
    1482          @param kwargs: parameters that get passed through to the control session's
    1483              L{resume()} method, only the C{session_name} parameter will get removed
    1484              before pass-through
    1485          @type kwargs: C{dict}
    1486
    1487          @return: return value of the cascaded L{resume()} method, denoting the success or failure
    1488              of the session startup
    1489          @rtype: C{bool}
    1490
    1491          """
    1492          if 'session_name' in kwargs.keys():
    1493              del kwargs['session_name']
    1494          return self.resume(**kwargs)
    1495
    1496 -    def resume(self, session_name=None, session_instance=None, session_list=None, **kwargs):
    1497          """\
    1498          Resume a running/suspended X2Go session.
    1499
    1500          The L{X2GoControlSession.resume()} method accepts any parameter
    1501          that can be passed to any of the C{X2GoTerminalSession*} backend class constructors.
    1502
    1503          @return: True if the session could be successfully resumed
    1504          @rtype: C{bool}
    1505
    1506          @raise X2GoUserException: if the remote user is not allowed to launch/resume X2Go sessions.
    1507
    1508          """
    1509          if self.get_transport() is not None:
    1510
    1511              if not self.is_x2gouser(self.get_transport().get_username()):
    1512                  raise x2go_exceptions.X2GoUserException('remote user %s is not allowed to run X2Go commands' % self.get_transport().get_username())
    1513
    1514              session_info = None
    1515              try:
    1516                  if session_name is not None:
    1517                      if session_list:
    1518                          session_info = session_list[session_name]
    1519                      else:
    1520                          session_info = self.list_sessions()[session_name]
    1521              except KeyError:
    1522                  _success = False
    1523
    1524              _terminal = self._terminal_backend(self,
    1525                                                 profile_name=self.profile_name,
    1526                                                 session_info=session_info,
    1527                                                 info_backend=self._info_backend,
    1528                                                 list_backend=self._list_backend,
    1529                                                 proxy_backend=self._proxy_backend,
    1530                                                 client_rootdir=self.client_rootdir,
    1531                                                 session_instance=session_instance,
    1532                                                 sessions_rootdir=self.sessions_rootdir,
    1533                                                 **kwargs)
    1534
    1535              _success = False
    1536              try:
    1537                  if session_name is not None:
    1538                      _success = _terminal.resume()
    1539                  else:
    1540                      _success = _terminal.start()
    1541              except x2go_exceptions.X2GoTerminalSessionException:
    1542                  _success = False
    1543
    1544              if _success:
    1545                  while not _terminal.ok():
    1546                      gevent.sleep(.2)
    1547
    1548                  if _terminal.ok():
    1549                      self.associated_terminals[_terminal.get_session_name()] = _terminal
    1550                      self.get_transport().reverse_tunnels[_terminal.get_session_name()] = {
    1551                          'sshfs': (0, None),
    1552                          'snd': (0, None),
    1553                      }
    1554
    1555                      return _terminal or None
    1556
    1557          return None
    1558
    1559 -    def share_desktop(self, desktop=None, user=None, display=None, share_mode=0, **kwargs):
    1560          """\
    1561          Share another already running desktop session. Desktop sharing can be run
    1562          in two different modes: view-only and full-access mode.
    1563
    1564          @param desktop: desktop ID of a sharable desktop in format C{<user>@<display>}
    1565          @type desktop: C{str}
    1566          @param user: user name and display number can be given separately, here give the
    1567              name of the user who wants to share a session with you
    1568          @type user: C{str}
    1569          @param display: user name and display number can be given separately, here give the
    1570              number of the display that a user allows you to be shared with
    1571          @type display: C{str}
    1572          @param share_mode: desktop sharing mode, 0 stands for VIEW-ONLY, 1 for  FULL-ACCESS mode
    1573          @type share_mode: C{int}
    1574
    1575          @return: True if the session could be successfully shared
    1576          @rtype: C{bool}
    1577
    1578          @raise X2GoDesktopSharingException: if C{username} and C{dislpay} do not relate to a
    1579              sharable desktop session
    1580
    1581          """
    1582          if desktop:
    1583              user = desktop.split('@')[0]
    1584              display = desktop.split('@')[1]
    1585          if not (user and display):
    1586              raise x2go_exceptions.X2GoDesktopSharingException('Need user name and display number of shared desktop.')
    1587
    1588          cmd = '%sXSHAD%sXSHAD%s' % (share_mode, user, display)
    1589
    1590          kwargs['cmd'] = cmd
    1591          kwargs['session_type'] = 'shared'
    1592
    1593          return self.start(**kwargs)
    1594
    1595 -    def list_desktops(self, raw=False, maxwait=20):
    1596          """\
    1597          List all desktop-like sessions of current user (or of users that have
    1598          granted desktop sharing) on the connected server.
    1599
    1600          @param raw: if C{True}, the raw output of the server-side X2Go command
    1601              C{x2golistdesktops} is returned.
    1602          @type raw: C{bool}
    1603
    1604          @return: a list of X2Go desktops available for sharing
    1605          @rtype: C{list}
    1606
    1607          @raise X2GoTimeOutException: on command execution timeouts, with the server-side C{x2golistdesktops}
    1608              command this can sometimes happen. Make sure you ignore these time-outs and to try again
    1609
    1610          """
    1611          if raw:
    1612              (stdin, stdout, stderr) = self._x2go_exec_command("export HOSTNAME && x2golistdesktops")
    1613              return stdout.read(), stderr.read()
    1614
    1615          else:
    1616
    1617              # this _success loop will catch errors in case the x2golistsessions output is corrupt
    1618              # this should not be needed and is a workaround for the current X2Go server implementation
    1619
    1620              if self.low_latency:
    1621                  maxwait = maxwait * 2
    1622
    1623              timeout = gevent.Timeout(maxwait)
    1624              timeout.start()
    1625              try:
    1626                  (stdin, stdout, stderr) = self._x2go_exec_command("export HOSTNAME && x2golistdesktops")
    1627                  _stdout_read = stdout.read()
    1628                  _listdesktops = _stdout_read.split('\n')
    1629              except gevent.timeout.Timeout:
    1630                  # if we do not get a reply here after <maxwait> seconds we will raise a time out, we have to
    1631                  # make sure that we catch this at places where we want to ignore timeouts (e.g. in the
    1632                  # desktop list cache)
    1633                  raise x2go_exceptions.X2GoTimeOutException('x2golistdesktop command timed out')
    1634              finally:
    1635                  timeout.cancel()
    1636
    1637              return _listdesktops
    1638
    1639 -    def list_mounts(self, session_name, raw=False, maxwait=20):
    1640          """\
    1641          List all mounts for a given session of the current user on the connected server.
    1642
    1643          @param session_name: name of a session to query a list of mounts for
    1644          @type session_name: C{str}
    1645          @param raw: if C{True}, the raw output of the server-side X2Go command
    1646              C{x2golistmounts} is returned.
    1647          @type raw: C{bool}
    1648          @param maxwait: stop processing C{x2golistmounts} after C{<maxwait>} seconds
    1649          @type maxwait: C{int}
    1650
    1651          @return: a list of client-side mounts for X2Go session C{<session_name>} on the server
    1652          @rtype: C{list}
    1653
    1654          @raise X2GoTimeOutException: on command execution timeouts, queries with the server-side
    1655              C{x2golistmounts} query should normally be processed quickly, a time-out may hint that the
    1656              control session has lost its connection to the X2Go server
    1657
    1658          """
    1659          if raw:
    1660              (stdin, stdout, stderr) = self._x2go_exec_command("export HOSTNAME && x2golistmounts %s" % session_name)
    1661              return stdout.read(), stderr.read()
    1662
    1663          else:
    1664
    1665              if self.low_latency:
    1666                  maxwait = maxwait * 2
    1667
    1668              # this _success loop will catch errors in case the x2golistmounts output is corrupt
    1669
    1670              timeout = gevent.Timeout(maxwait)
    1671              timeout.start()
    1672              try:
    1673                  (stdin, stdout, stderr) = self._x2go_exec_command("export HOSTNAME && x2golistmounts %s" % session_name)
    1674                  _stdout_read = stdout.read()
    1675                  _listmounts = {session_name: [ line for line in _stdout_read.split('\n') if line ] }
    1676              except gevent.timeout.Timeout:
    1677                  # if we do not get a reply here after <maxwait> seconds we will raise a time out, we have to
    1678                  # make sure that we catch this at places where we want to ignore timeouts
    1679                  raise x2go_exceptions.X2GoTimeOutException('x2golistmounts command timed out')
    1680              finally:
    1681                  timeout.cancel()
    1682
    1683              return _listmounts
    1684
    1685 -    def list_sessions(self, raw=False):
    1686          """\
    1687          List all sessions of current user on the connected server.
    1688
    1689          @param raw: if C{True}, the raw output of the server-side X2Go command
    1690              C{x2golistsessions} is returned.
    1691          @type raw: C{bool}
    1692
    1693          @return: normally an instance of a C{X2GoServerSessionList*} backend is returned. However,
    1694              if the raw argument is set, the plain text output of the server-side C{x2golistsessions}
    1695              command is returned
    1696          @rtype: C{X2GoServerSessionList} instance or str
    1697
    1698          @raise X2GoControlSessionException: on command execution timeouts, if this happens the control session will
    1699              be interpreted as disconnected due to connection loss
    1700          """
    1701          if raw:
    1702              if 'X2GO_LIST_SHADOWSESSIONS' in self._x2go_server_features:
    1703                  (stdin, stdout, stderr) = self._x2go_exec_command("export HOSTNAME && { x2golistsessions; x2golistshadowsessions; }")
    1704              else:
    1705                  (stdin, stdout, stderr) = self._x2go_exec_command("export HOSTNAME && x2golistsessions")
    1706              return stdout.read(), stderr.read()
    1707
    1708          else:
    1709
    1710              # this _success loop will catch errors in case the x2golistsessions output is corrupt
    1711              # this should not be needed and is a workaround for the current X2Go server implementation
    1712              _listsessions = {}
    1713              _success = False
    1714              _count = 0
    1715              _maxwait = 20
    1716
    1717              # we will try this 20 times before giving up... we might simply catch the x2golistsessions
    1718              # output in the middle of creating a session in the database...
    1719              while not _success and _count < _maxwait:
    1720                  _count += 1
    1721                  try:
    1722                      if 'X2GO_LIST_SHADOWSESSIONS' in self._x2go_server_features:
    1723                          (stdin, stdout, stderr) = self._x2go_exec_command("export HOSTNAME && { x2golistsessions; x2golistshadowsessions; }")
    1724                      else:
    1725                          (stdin, stdout, stderr) = self._x2go_exec_command("export HOSTNAME && x2golistsessions")
    1726                      _stdout_read = stdout.read()
    1727                      _listsessions = self._list_backend(_stdout_read, info_backend=self._info_backend).sessions
    1728                      _success = True
    1729                  except KeyError:
    1730                      gevent.sleep(1)
    1731                  except IndexError:
    1732                      gevent.sleep(1)
    1733                  except ValueError:
    1734                      gevent.sleep(1)
    1735
    1736              if _count >= _maxwait:
    1737                  self.session_died = True
    1738                  self.disconnect()
    1739                  raise x2go_exceptions.X2GoControlSessionException('x2golistsessions command failed after we have tried 20 times')
    1740
    1741              # update internal variables when list_sessions() is called
    1742              if _success and not self.session_died:
    1743                  for _session_name, _terminal in self.associated_terminals.items():
    1744                      if _session_name in _listsessions.keys():
    1745                          # update the whole session_info object within the terminal session
    1746                          if hasattr(self.associated_terminals[_session_name], 'session_info') and not self.associated_terminals[_session_name].is_session_info_protected():
    1747                              self.associated_terminals[_session_name].session_info.update(_listsessions[_session_name])
    1748                      else:
    1749                          self.associated_terminals[_session_name].__del__()
    1750                          try: del self.associated_terminals[_session_name]
    1751                          except KeyError: pass
    1752                          self.terminated_terminals.append(_session_name)
    1753                      if _terminal.is_suspended():
    1754                          self.associated_terminals[_session_name].__del__()
    1755                          try: del self.associated_terminals[_session_name]
    1756                          except KeyError: pass
    1757
    1758              return _listsessions
    1759
    1760 -    def clean_sessions(self, destroy_terminals=True, published_applications=False):
    1761          """\
    1762          Find X2Go terminals that have previously been started by the
    1763          connected user on the remote X2Go server and terminate them.
    1764
    1765          @param destroy_terminals: destroy the terminal session instances after cleanup
    1766          @type destroy_terminals: C{bool}
    1767          @param published_applications: also clean up published applications providing sessions
    1768          @type published_applications: C{bool}
    1769
    1770          """
    1771          session_list = self.list_sessions()
    1772          if published_applications:
    1773              session_names = session_list.keys()
    1774          else:
    1775              session_names = [ _sn for _sn in session_list.keys() if not session_list[_sn].is_published_applications_provider() ]
    1776          for session_name in session_names:
    1777              if self.associated_terminals.has_key(session_name):
    1778                  self.associated_terminals[session_name].terminate()
    1779                  if destroy_terminals:
    1780                      if self.associated_terminals[session_name] is not None:
    1781                          self.associated_terminals[session_name].__del__()
    1782                      try: del self.associated_terminals[session_name]
    1783                      except KeyError: pass
    1784              else:
    1785                  self.terminate(session_name=session_name)
    1786
    1787 -    def is_connected(self):
    1788          """\
    1789          Returns C{True} if this control session is connected to the remote server (that
    1790          is: if it has a valid Paramiko/SSH transport object).
    1791
    1792          @return: X2Go session connected?
    1793          @rtype: C{bool}
    1794
    1795          """
    1796          return self.get_transport() is not None and self.get_transport().is_authenticated()
    1797
    1798 -    def is_running(self, session_name):
    1799          """\
    1800          Returns C{True} if the given X2Go session is in running state,
    1801          C{False} else.
    1802
    1803          @param session_name: X2Go name of the session to be queried
    1804          @type session_name: C{str}
    1805
    1806          @return: X2Go session running? If C{<session_name>} is not listable by the L{list_sessions()} method then C{None} is returned
    1807          @rtype: C{bool} or C{None}
    1808
    1809          """
    1810          session_infos = self.list_sessions()
    1811          if session_name in session_infos.keys():
    1812              return session_infos[session_name].is_running()
    1813          return None
    1814
    1815 -    def is_suspended(self, session_name):
    1816          """\
    1817          Returns C{True} if the given X2Go session is in suspended state,
    1818          C{False} else.
    1819
    1820          @return: X2Go session suspended? If C{<session_name>} is not listable by the L{list_sessions()} method then C{None} is returned
    1821          @rtype: C{bool} or C{None}
    1822
    1823          """
    1824          session_infos = self.list_sessions()
    1825          if session_name in session_infos.keys():
    1826              return session_infos[session_name].is_suspended()
    1827          return None
    1828
    1829 -    def has_terminated(self, session_name):
    1830          """\
    1831          Returns C{True} if the X2Go session with name C{<session_name>} has been seen
    1832          by this control session and--in the meantime--has been terminated.
    1833
    1834          If C{<session_name>} has not been seen, yet, the method will return C{None}.
    1835
    1836          @return: X2Go session has terminated?
    1837          @rtype: C{bool} or C{None}
    1838
    1839          """
    1840          session_infos = self.list_sessions()
    1841          if session_name in self.terminated_terminals:
    1842              return True
    1843          if session_name not in session_infos.keys() and session_name in self.associated_terminals.keys():
    1844              # do a post-mortem tidy up
    1845              self.terminate(session_name)
    1846              return True
    1847          if self.is_suspended(session_name) or self.is_running(session_name):
    1848              return False
    1849
    1850          return None
    1851
    1852 -    def suspend(self, session_name):
    1853          """\
    1854          Suspend X2Go session with name C{<session_name>} on the connected
    1855          server.
    1856
    1857          @param session_name: X2Go name of the session to be suspended
    1858          @type session_name: C{str}
    1859
    1860          @return: C{True} if the session could be successfully suspended
    1861          @rtype: C{bool}
    1862
    1863          """
    1864          _ret = False
    1865          _session_names = [ t.get_session_name() for t in self.associated_terminals.values() ]
    1866          if session_name in _session_names:
    1867
    1868              self.logger('suspending associated terminal session: %s' % session_name, loglevel=log.loglevel_DEBUG)
    1869              (stdin, stdout, stderr) = self._x2go_exec_command("x2gosuspend-session %s" % session_name, loglevel=log.loglevel_DEBUG)
    1870              stdout.read()
    1871              stderr.read()
    1872              if self.associated_terminals.has_key(session_name):
    1873                  if self.associated_terminals[session_name] is not None:
    1874                      self.associated_terminals[session_name].__del__()
    1875                  try: del self.associated_terminals[session_name]
    1876                  except KeyError: pass
    1877              _ret = True
    1878
    1879          else:
    1880
    1881              self.logger('suspending non-associated terminal session: %s' % session_name, loglevel=log.loglevel_DEBUG)
    1882              (stdin, stdout, stderr) = self._x2go_exec_command("x2gosuspend-session %s" % session_name, loglevel=log.loglevel_DEBUG)
    1883              stdout.read()
    1884              stderr.read()
    1885              _ret = True
    1886
    1887          return _ret
    1888
    1889 -    def terminate(self, session_name, destroy_terminals=True):
    1890          """\
    1891          Terminate X2Go session with name C{<session_name>} on the connected
    1892          server.
    1893
    1894          @param session_name: X2Go name of the session to be terminated
    1895          @type session_name: C{str}
    1896
    1897          @return: C{True} if the session could be successfully terminated
    1898          @rtype: C{bool}
    1899
    1900          """
    1901
    1902          _ret = False
    1903          if session_name in self.associated_terminals.keys():
    1904
    1905              self.logger('terminating associated session: %s' % session_name, loglevel=log.loglevel_DEBUG)
    1906              (stdin, stdout, stderr) = self._x2go_exec_command("x2goterminate-session %s" % session_name, loglevel=log.loglevel_DEBUG)
    1907              stdout.read()
    1908              stderr.read()
    1909
    1910              if destroy_terminals:
    1911                  if self.associated_terminals[session_name] is not None:
    1912                      self.associated_terminals[session_name].__del__()
    1913                  try: del self.associated_terminals[session_name]
    1914                  except KeyError: pass
    1915
    1916              self.terminated_terminals.append(session_name)
    1917              _ret = True
    1918
    1919          else:
    1920
    1921              self.logger('terminating non-associated session: %s' % session_name, loglevel=log.loglevel_DEBUG)
    1922              (stdin, stdout, stderr) = self._x2go_exec_command("x2goterminate-session %s" % session_name, loglevel=log.loglevel_DEBUG)
    1923              stdout.read()
    1924              stderr.read()
    1925              _ret = True
    1926
    1927          return _ret
    1928