Package x2go :: Module utils

[frames] | no frames]

# Source Code for Module x2go.utils

      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  Python X2Go helper functions, constants etc.
     22
     23  """
     24  __NAME__ = 'x2goutils-pylib'
     25
     26  import sys
     27  import os
     28  import locale
     29  import re
     30  import types
     31  import copy
     32  import socket
     33  import gevent
     34  import string
     35  import subprocess
     36  import distutils.version
     37  import paramiko
     38
     39  # Python X2Go modules
     40  from defaults import X2GOCLIENT_OS as _X2GOCLIENT_OS
     41  from defaults import X2GO_SESSIONPROFILE_DEFAULTS as _X2GO_SESSIONPROFILE_DEFAULTS
     42  from defaults import X2GO_MIMEBOX_ACTIONS as _X2GO_MIMEBOX_ACTIONS
     43  from defaults import pack_methods_nx3
     44
     45  from defaults import BACKENDS as _BACKENDS
     46
     47  import x2go_exceptions
     48
     49  if _X2GOCLIENT_OS != 'Windows':
     50      import Xlib
     51      from defaults import X_DISPLAY as _X_DISPLAY
     52
     53  if _X2GOCLIENT_OS == 'Windows':
     54      import win32gui
     55      import win32print
     56      import win32con
     57
     58 -def is_in_nx3packmethods(method):
     59
     60      """\
     61      Test if a given compression method is valid for NX3 Proxy.
     62
     63      @return: C{True} if C{method} is in the hard-coded list of NX3 compression methods.
     64      @rtype: C{bool}
     65
     66      """
     67      return method in pack_methods_nx3
     68
     69
     70 -def find_session_line_in_x2golistsessions(session_name, stdout):
     71      """\
     72      Return the X2Go session meta information as returned by the
     73      C{x2golistsessions} server command for session C{session_name}.
     74
     75      @param session_name: name of a session
     76      @type session_name: C{str}
     77      @param stdout: raw output from the ,,x2golistsessions'' command, as list of strings
     78      @type stdout: C{list}
     79
     80      @return: the output line that contains C{<session_name>}
     81      @rtype: C{str} or C{None}
     82
     83      """
     84      sessions = stdout.read().split("\n")
     85      for line in sessions:
     86          # skip empty lines
     87          if not line:
     88              continue
     89          if session_name == line.split("|")[1]:
     90              return line
     91      return None
     92
     93
     94 -def slugify(value):
     95      """\
     96      Normalizes string, converts to lowercase, removes non-alpha characters,
     97      converts spaces to hyphens and replaces round brackets by pointed brackets.
     98
     99      @param value: a string that shall be sluggified
    100      @type value: C{str}
    101
    102      @return: the sluggified string
    103      @rtype: C{str}
    104
    105      """
    106      import unicodedata
    107      value = unicodedata.normalize('NFKD', unicode(value)).encode('ascii', 'ignore')
    108      value = re.sub('[^\w\s-]', '', value).strip().lower()
    109      value = re.sub('[(]', '<', value).strip().lower()
    110      value = re.sub('[)]', '>', value).strip().lower()
    111      return value
    112
    113 -def _genSessionProfileId():
    114      """\
    115      Generate a session profile ID as used in x2goclient's sessions config file.
    116
    117      @return: profile ID
    118      @rtype: C{str}
    119
    120      """
    121      import datetime
    122      return datetime.datetime.utcnow().strftime('%Y%m%d%H%m%S%f')
    123
    124
    125 -def _checkIniFileDefaults(data_structure):
    126      """\
    127      Check an ini file data structure passed on by a user app or class.
    128
    129      @param data_structure: an ini file date structure
    130      @type data_structure: C{dict} of C{dict}s
    131
    132      @return: C{True} if C{data_structure} matches that of an ini file data structure
    133      @rtype: C{bool}
    134
    135      """
    136      if data_structure is None:
    137          return False
    138      if type(data_structure) is not types.DictType:
    139          return False
    140      for sub_dict in data_structure.values():
    141          if type(sub_dict) is not types.DictType:
    142              return False
    143      return True
    144
    145
    146 -def _checkSessionProfileDefaults(data_structure):
    147      """\
    148      Check the data structure of a default session profile passed by a user app.
    149
    150      @param data_structure: an ini file date structure
    151      @type data_structure: C{dict} of C{dict}s
    152
    153      @return: C{True} if C{data_structure} matches that of an ini file data structure
    154      @rtype: C{bool}
    155
    156      """
    157      if data_structure is None:
    158          return False
    159      if type(data_structure) is not types.DictType:
    160          return False
    161      return True
    162
    163
    164 -def _convert_SessionProfileOptions_2_SessionParams(options):
    165      """\
    166      Convert session profile options as used in x2goclient's sessions file to
    167      Python X2Go session parameters.
    168
    169      @param options: a dictionary of options, parameter names as in the X2Go ,,sessions'' file
    170      @type options: C{dict}
    171
    172      @return: session options as used in C{X2GoSession} instances
    173      @rtype: C{dict}
    174
    175      """
    176      _params = copy.deepcopy(options)
    177
    178      # get rid of unknown session profile options
    179      _known_options = _X2GO_SESSIONPROFILE_DEFAULTS.keys()
    180      for p in _params.keys():
    181          if p not in _known_options:
    182              del _params[p]
    183
    184      _rename_dict = {
    185              'host': 'server',
    186              'user': 'username',
    187              'soundsystem': 'snd_system',
    188              'sndport': 'snd_port',
    189              'type': 'kbtype',
    190              'layout': 'kblayout',
    191              'variant': 'kbvariant',
    192              'speed': 'link',
    193              'sshport': 'port',
    194              'useexports': 'allow_share_local_folders',
    195              'restoreexports': 'restore_shared_local_folders',
    196              'usemimebox': 'allow_mimebox',
    197              'mimeboxextensions': 'mimebox_extensions',
    198              'mimeboxaction': 'mimebox_action',
    199              'print': 'printing',
    200              'name': 'profile_name',
    201              'key': 'key_filename',
    202              'command': 'cmd',
    203              'rdpserver': 'rdp_server',
    204              'rdpoptions': 'rdp_options',
    205              'xdmcpserver': 'xdmcp_server',
    206              'useiconv': 'convert_encoding',
    207              'iconvto': 'server_encoding',
    208              'iconvfrom': 'client_encoding',
    209              'usesshproxy': 'use_sshproxy',
    210              'sshproxyhost': 'sshproxy_host',
    211              'sshproxyport': 'sshproxy_port',
    212              'sshproxyuser': 'sshproxy_user',
    213              'sshproxykeyfile': 'sshproxy_key_filename',
    214              'sessiontitle': 'session_title',
    215              'setsessiontitle': 'set_session_title',
    216              'published': 'published_applications',
    217              'autostart': 'auto_start_or_resume',
    218              'autoconnect': 'auto_connect',
    219              'forwardsshagent': 'forward_sshagent',
    220              'autologin': 'look_for_keys',
    221              'sshproxyautologin': 'sshproxy_look_for_keys',
    222              'uniquehostkeyaliases': 'unique_hostkey_aliases',
    223      }
    224      _speed_dict = {
    225              '0': 'modem',
    226              '1': 'isdn',
    227              '2': 'adsl',
    228              '3': 'wan',
    229              '4': 'lan',
    230      }
    231
    232      for opt, val in options.iteritems():
    233
    234          # rename options if necessary
    235          if opt in _rename_dict.keys():
    236              del _params[opt]
    237              opt = _rename_dict[opt]
    238              if opt in _known_options:
    239                  _type = type(_known_options[opt])
    240                  _params[opt] = _type(val)
    241              else:
    242                  _params[opt] = val
    243
    244          # translate integer values for connection speed to readable strings
    245          if opt == 'link':
    246              val = str(val).lower()
    247              if val in _speed_dict.keys():
    248                  val = _speed_dict[val]
    249              val = val.lower()
    250              _params['link'] = val
    251
    252          # share_local_folders is a list
    253          if opt in ('share_local_folders', 'mimebox_extensions'):
    254              if type(val) is types.StringType:
    255                  if val:
    256                      _params[opt] = val.split(',')
    257                  else:
    258                      _params[opt] = []
    259
    260      if _params['cmd'] == "XFCE4": _params['cmd'] = "XFCE"
    261      if _params['look_for_keys']:
    262          _params['allow_agent'] = True
    263      if _params['sshproxy_look_for_keys']:
    264          _params['sshproxy_allow_agent'] = True
    265
    266      # append value for quality to value for pack method
    267      if _params['quality']:
    268          _params['pack'] = '%s-%s' % (_params['pack'], _params['quality'])
    269      # delete quality in any case...
    270      del _params['quality']
    271
    272      del _params['fstunnel']
    273
    274      if _params.has_key('export'):
    275
    276          _export = _params['export']
    277          del _params['export']
    278          if type(_export) is types.DictType:
    279
    280              # since Python X2Go 0.5.0.0
    281              _params['share_local_folders'] = _export.keys()
    282
    283          else:
    284
    285              # before Python X2Go 0.5.0.0 (analysing the INI file's format for the export field)
    286
    287              # fix for wrong export field usage in PyHoca-GUI/CLI and python-x2go before 20110923
    288              _export = _export.replace(",", ";")
    289
    290              _export = _export.strip().strip('"').strip().strip(';').strip()
    291              _export_list = [ f for f in _export.split(';') if f ]
    292
    293              _params['share_local_folders'] = []
    294              for _shared_folder in _export_list:
    295                  # fix for wrong export field usage in PyHoca-GUI/CLI and python-x2go before 20110923
    296                  if not ":" in _shared_folder: _shared_folder = "%s:1" % _shared_folder
    297                  if _shared_folder.split(":")[-1] == "1":
    298                      _params['share_local_folders'].append(":".join(_shared_folder.split(":")[:-1]))
    299
    300      if options['fullscreen']:
    301          _params['geometry'] = 'fullscreen'
    302      elif options['maxdim']:
    303          _params['geometry'] = 'maximize'
    304      else:
    305          _params['geometry'] = '%sx%s' % (options['width'], options['height'])
    306      del _params['width']
    307      del _params['height']
    308      del _params['fullscreen']
    309      del _params['maxdim']
    310
    311      if not options['sound']:
    312          _params['snd_system'] = 'none'
    313      del _params['sound']
    314
    315      if not options['rootless']:
    316          _params['session_type'] = 'desktop'
    317      else:
    318          _params['session_type'] = 'application'
    319      del _params['rootless']
    320
    321      if _params['mimebox_action'] not in _X2GO_MIMEBOX_ACTIONS.keys():
    322          _params['mimebox_action'] = 'OPEN'
    323
    324      if not options['usekbd']:
    325          _params['kbtype'] = 'null/null'
    326          _params['kblayout'] = 'null'
    327          _params['kbvariant'] = 'null'
    328      del _params['usekbd']
    329
    330      if not _params['kbtype'].strip(): _params['kbtype'] = 'null/null'
    331      if not _params['kblayout'].strip(): _params['kblayout'] = 'null'
    332      if not _params['kbvariant'].strip(): _params['kbvariant'] = 'null'
    333
    334      if not options['setdpi']:
    335          del _params['dpi']
    336      del _params['setdpi']
    337
    338      if options['sshproxysameuser']:
    339          _params['sshproxy_user'] = _params['username']
    340      del _params['sshproxysameuser']
    341      if options['sshproxysamepass']:
    342          _params['sshproxy_reuse_authinfo'] = True
    343          _params['sshproxy_key_filename'] = _params['key_filename']
    344      del _params['sshproxysamepass']
    345
    346      if _params['use_sshproxy']:
    347
    348          # compat code for Python X2Go 0.2.1.0 -> 0.2.2.0
    349          if options.has_key('sshproxytunnel'):
    350              if not options['sshproxytunnel'].startswith('DEPRECATED'):
    351                  _params['server'] = options['sshproxytunnel'].split(":")[-2]
    352                  _params['port'] = options['sshproxytunnel'].split(":")[-1]
    353              try: del _params['sshproxytunnel']
    354              except KeyError: pass
    355
    356
    357      # currently known but ignored in Python X2Go
    358      _ignored_options = [
    359              'startsoundsystem',
    360              'soundtunnel',
    361              'defsndport',
    362              'icon',
    363              'xinerama',
    364              'multidisp',
    365              'display',
    366              'krblogin',
    367              'directrdp',
    368              'directrdpsettings',
    369              'rdpclient',
    370              'rdpport',
    371              'sshproxytype',
    372      ]
    373      for i in _ignored_options:
    374          del _params[i]
    375
    376      return _params
    377
    378
    379 -def session_names_by_timestamp(session_infos):
    380      """\
    381      Sorts session profile names by their timestamp (as used in the file format's section name).
    382
    383      @param session_infos: a dictionary of session infos as reported by L{X2GoClient.list_sessions()}
    384      @type session_infos: C{dict}
    385
    386      @return: a timestamp-sorted list of session names found in C{session_infos}
    387      @rtype: C{list}
    388
    389      """
    390      session_names = session_infos.keys()
    391      sortable_session_names = [ '%s|%s' % (session_name.split('-')[-1].split('_')[0], session_name) for session_name in session_names ]
    392      sortable_session_names.sort()
    393      return [ session_name.split('|')[1] for session_name in sortable_session_names ]
    394
    395
    396 -def touch_file(filename, mode='a'):
    397      """\
    398      Imitates the behaviour of the GNU/touch command.
    399
    400      @param filename: name of the file to touch
    401      @type filename: C{str}
    402      @param mode: the file mode (as used for Python file objects)
    403      @type mode: C{str}
    404
    405      """
    406      if not os.path.isdir(os.path.dirname(filename)):
    407          os.makedirs(os.path.dirname(filename), mode=00700)
    408      f = open(filename, mode=mode)
    409      f.close()
    410
    411
    412 -def unique(seq):
    413      """\
    414      Imitates the behaviour of the GNU/uniq command.
    415
    416      @param seq: a list/sequence containing consecutive duplicates.
    417      @type seq: C{list}
    418
    419      @return: list that has been clean up from the consecutive duplicates
    420      @rtype: C{list}
    421
    422      """
    423      # order preserving
    424      noDupes = []
    425      [noDupes.append(i) for i in seq if not noDupes.count(i)]
    426      return noDupes
    427
    428
    429 -def known_encodings():
    430      """\
    431      Render a list of all-known-to-Python character encodings (including
    432      all known aliases)
    433
    434      """
    435      from encodings.aliases import aliases
    436      _raw_encname_list = []
    437      _raw_encname_list.extend(aliases.keys())
    438      _raw_encname_list.extend(aliases.values())
    439      _raw_encname_list.sort()
    440      _encname_list = []
    441      for _raw_encname in _raw_encname_list:
    442          _encname = _raw_encname.upper()
    443          _encname = _encname.replace('_', '-')
    444          _encname_list.append(_encname)
    445      _encname_list.sort()
    446      _encname_list = unique(_encname_list)
    447      return _encname_list
    448
    449
    450 -def patiently_remove_file(dirname, filename):
    451      """\
    452      Try to remove a file, wait for unlocking, remove it once removing is possible...
    453
    454      @param dirname: directory name the file is in
    455      @type dirname: C{str}
    456      @param filename: name of the file to be removed
    457      @type filename: C{str}
    458
    459      """
    460      _not_removed = True
    461      while _not_removed:
    462          try:
    463              os.remove(os.path.join(dirname, filename))
    464              _not_removed = False
    465          except:
    466              # file is probably locked
    467              gevent.sleep(5)
    468
    469
    470 -def detect_unused_port(bind_address='127.0.0.1', preferred_port=None):
    471      """\
    472      Detect an unused IP socket.
    473
    474      @param bind_address: IP address to bind to
    475      @type bind_address: C{str}
    476      @param preferred_port: IP socket port that shall be tried first for availability
    477      @type preferred_port: C{str}
    478
    479      @return: free local IP socket port that can be used for binding
    480      @rtype: C{str}
    481
    482      """
    483      sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
    484      try:
    485          if preferred_port:
    486              sock.bind((bind_address, preferred_port))
    487              ipaddr, port = sock.getsockname()
    488          else:
    489              raise
    490      except:
    491          sock.bind(('', 0))
    492          ipaddr, port = sock.getsockname()
    493      return port
    494
    495
    496 -def get_encoding():
    497      """\
    498      Detect systems default character encoding.
    499
    500      @return: The system's local character encoding.
    501      @rtype: C{str}
    502
    503      """
    504      try:
    505          encoding = locale.getdefaultlocale()[1]
    506          if encoding is None:
    507              raise BaseException
    508      except:
    509          try:
    510              encoding = sys.getdefaultencoding()
    511          except:
    512              encoding = 'ascii'
    513      return encoding
    514
    515
    516 -def is_abs_path(path):
    517      """\
    518      Test if a given path is an absolute path name.
    519
    520      @param path: test this path for absolutism...
    521      @type path: C{str}
    522
    523      @return: Returns C{True} if path is an absolute path name
    524      @rtype: C{bool}
    525
    526      """
    527      return bool((path.startswith('/') or re.match('^[%s]\:\\\\' % string.ascii_letters, path)))
    528
    529
    530 -def xkb_rules_names():
    531      """\
    532      Wrapper for: xprop -root _XKB_RULES_NAMES
    533
    534      @return: A Python dictionary that contains the current X11 keyboard rules.
    535      @rtype: C{dict}
    536
    537      """
    538      p = subprocess.Popen(['xprop', '-root', '_XKB_RULES_NAMES',], stdout=subprocess.PIPE, )
    539      _rn_list = p.stdout.read().split('"')
    540      _rn_dict = {
    541          'rules': _rn_list[1],
    542          'model': _rn_list[3],
    543          'layout': _rn_list[5],
    544          'variant': _rn_list[7],
    545          'options': _rn_list[9],
    546      }
    547      return _rn_dict
    548
    549 -def local_color_depth():
    550      """\
    551      Detect the current local screen's color depth.
    552
    553      @return: the local color depth in bits
    554      @rtype: C{int}
    555
    556      """
    557      if _X2GOCLIENT_OS != 'Windows':
    558          try:
    559              p = subprocess.Popen(['xwininfo', '-root',], stdout=subprocess.PIPE, )
    560              _depth_line = [ _info.strip() for _info in p.stdout.read().split('\n') if 'Depth:' in _info ][0]
    561              _depth = _depth_line.split(' ')[1]
    562              return int(_depth)
    563          except IndexError:
    564              # a sensible default value
    565              return 24
    566          except OSError:
    567              # for building your package...
    568              return 24
    569
    570      else:
    571          # This gets the color depth of the primary monitor. All monitors need not have the same color depth.
    572          dc = win32gui.GetDC(None)
    573          _depth = win32print.GetDeviceCaps(dc, win32con.BITSPIXEL) * win32print.GetDeviceCaps(dc, win32con.PLANES)
    574          win32gui.ReleaseDC(None, dc)
    575          return _depth
    576
    577 -def is_color_depth_ok(depth_session, depth_local):
    578      """\
    579      Test if color depth of this session is compatible with the
    580      local screen's color depth.
    581
    582      @param depth_session: color depth of the session
    583      @type depth_session: C{int}
    584      @param depth_local: color depth of local screen
    585      @type depth_local: C{int}
    586
    587      @return: Does the session color depth work with the local display?
    588      @rtype: C{bool}
    589
    590      """
    591      if depth_session == 0:
    592          return True
    593      if depth_session == depth_local:
    594          return True
    595      if ( ( depth_session == 24 or depth_session == 32 ) and ( depth_local == 24 or depth_local == 32 ) ):
    596          return True;
    597      if ( ( depth_session == 16 or depth_session == 17 ) and ( depth_local == 16 or depth_local == 17 ) ):
    598          return True;
    599      return False
    600
    601
    602 -def find_session_window(session_name):
    603      """\
    604      Find a session window by its X2GO session ID.
    605
    606      @param session_name: session name/ID of an X2Go session window
    607      @type session_name: C{str}
    608
    609      @return: the window object (or ID) of the searched for session window
    610      @rtype: C{obj} on Unix, C{int} on Windows
    611
    612      """
    613      if _X2GOCLIENT_OS != 'Windows':
    614          # establish connection to the win API in use...
    615          display = _X_DISPLAY
    616          if display:
    617              root = display.screen().root
    618
    619              success = False
    620              windowIDs_obj = root.get_full_property(display.intern_atom('_NET_CLIENT_LIST'), Xlib.X.AnyPropertyType)
    621
    622              if windowIDs_obj is None:
    623                  # works with i3 session manager...
    624                  windowIDs_obj = root.get_full_property(display.intern_atom('_NET_CLIENT_LIST_STACKING'), Xlib.X.AnyPropertyType)
    625
    626              if windowIDs_obj is not None:
    627                  windowIDs = windowIDs_obj.value
    628
    629                  for windowID in windowIDs:
    630                      window = display.create_resource_object('window', windowID)
    631                      try:
    632                          name = window.get_wm_name()
    633                      except Xlib.error.BadWindow:
    634                          continue
    635                      if name is not None and "X2GO-{session_name}".format(session_name=session_name) == name:
    636                          success = True
    637                          break
    638
    639              if success:
    640                  return window
    641
    642      else:
    643
    644          windows = []
    645          window = None
    646
    647          def _callback(hwnd, extra):
    648              if win32gui.GetWindowText(hwnd) == "X2GO-%s" % session_name:
    649                  windows.append(hwnd)
    650
    651          win32gui.EnumWindows(_callback, None)
    652          if len(windows): window = windows[0]
    653
    654          return window
    655
    656
    657 -def get_desktop_geometry():
    658      """\
    659      Get the geometry of the current screen's desktop.
    660
    661      @return: a (<width>, <height>) tuple will be returned
    662      @rtype: C{tuple}
    663
    664      """
    665      if _X2GOCLIENT_OS != 'Windows':
    666          display = _X_DISPLAY
    667          if display:
    668              root = display.screen().root
    669              return (root.get_geometry().width, root.get_geometry().height)
    670
    671      return None
    672
    673 -def get_workarea_geometry():
    674      """\
    675      Get the geometry of the current screen's work area by
    676      wrapping around::
    677
    678          xprop -root '_NET_WORKAREA'
    679
    680      @return: a (<width>, <height>) tuple will be returned
    681      @rtype: C{tuple}
    682
    683      """
    684      if _X2GOCLIENT_OS != 'Windows':
    685          p = subprocess.Popen(['xprop', '-root', '_NET_WORKAREA',], stdout=subprocess.PIPE, )
    686          _list = p.stdout.read().rstrip('\n').split(',')
    687          if len(_list) == 4:
    688              return (_list[2].strip(), _list[3].strip())
    689          else:
    690              return None
    691      else:
    692
    693          return None
    694
    695
    696 -def set_session_window_title(session_window, session_title):
    697      """\
    698      Set title of session window.
    699
    700      @param session_window: session window instance
    701      @type session_window: C{obj}
    702      @param session_title: session title to be set for C{session_window}
    703      @type session_title: C{str}
    704
    705      """
    706      if _X2GOCLIENT_OS != 'Windows':
    707          try:
    708              session_window.set_wm_name(str(session_title))
    709              session_window.set_wm_icon_name(str(session_title))
    710              _X_DISPLAY.sync()
    711          except Xlib.error.BadWindow:
    712              pass
    713
    714      else:
    715          win32gui.SetWindowText(session_window, session_title)
    716
    717
    718 -def raise_session_window(session_window):
    719      """\
    720      Raise session window. Not functional for Unix-like operating systems.
    721
    722      @param session_window: session window instance
    723      @type session_window: C{obj}
    724
    725      """
    726      if _X2GOCLIENT_OS != 'Windows':
    727          pass
    728      else:
    729          if session_window is not None:
    730              win32gui.SetForegroundWindow(session_window)
    731
    732
    733 -def merge_ordered_lists(l1, l2):
    734      """\
    735      Merge sort two sorted lists
    736
    737      @param l1: first sorted list
    738      @type l1: C{list}
    739      @param l2: second sorted list
    740      @type l2: C{list}
    741
    742      @return: the merge result of both sorted lists
    743      @rtype: C{list}
    744
    745      """
    746      ordered_list = []
    747
    748      # Copy both the args to make sure the original lists are not
    749      # modified
    750      l1 = l1[:]
    751      l2 = l2[:]
    752
    753      while (l1 and l2):
    754          if l1[0] not in l2:
    755              item = l1.pop(0)
    756          elif l2[0] not in l1:
    757              item = l2.pop(0)
    758          elif l1[0] in l2:
    759              item = l1.pop(0)
    760              l2.remove(item)
    761          if item not in ordered_list:
    762              ordered_list.append(item)
    763
    764      # Add the remaining of the lists
    765      ordered_list.extend(l1 if l1 else l2)
    766
    767      return ordered_list
    768
    769 -def compare_versions(version_a, op, version_b):
    770      """\
    771      Compare <version_a> with <version_b> using operator <op>.
    772      In the background C{distutils.version.LooseVersion} is
    773      used for the comparison operation.
    774
    775      @param version_a: a version string
    776      @type version_a: C{str}
    777      @param op: an operator provide as string (e.g. '<', '>', '==', '>=' etc.)
    778      @type op: C{str}
    779      @param version_b: another version string that is to be compared with <version_a>
    780      @type version_b: C{str}
    781
    782      """
    783
    784      ### FIXME: this comparison is not reliable with beta et al. version strings
    785
    786      ver_a = distutils.version.LooseVersion(version_a)
    787      ver_b = distutils.version.LooseVersion(version_b)
    788
    789      return eval("ver_a %s ver_b" % op)
    790
    791 -class ProgressStatus(object):
    792      """\
    793      A simple progress status iterator class.
    794
    795      """
    796 -    def __init__(self, progress_event, progress_func=range(0, 100, 10)):
    797          """\
    798          @param progress_event: a threading.Event() object that gets notified on progress
    799          @type progress_event: C{obj}
    800          @param progress_func: a function that delivers a value between 0 and 100 (progress percentage value)
    801          @type progress_func: C{func}
    802
    803          """
    804          self.ev = progress_event
    805          self.progress_func = progress_func
    806
    807 -    def __iter__(self):
    808          """\
    809          Intialize the L{ProgressStatus} iterator object.
    810
    811          """
    812          self.status = self.progress_func()
    813          return self
    814
    815 -    def next(self):
    816          """\
    817          On each iteration wait for the progress event to get triggered from an outside
    818          part of the application.
    819
    820          Once the event fires read the progress status from the progress retrieval function
    821          and clear the event afterwards (so we wait for the next firing of the event).
    822
    823          """
    824          if self.status < 100 and self.status != -1:
    825              self.ev.wait()
    826              self.status = self.progress_func()
    827              self.ev.clear()
    828              return self.status
    829          else:
    830              raise StopIteration
    831
    832 -def _get_backend_class(backend, class_name):
    833      # silence pyflakes, the _this_class var will be assigned in an exec() statement below...
    834      _this_class = None
    835      if type(backend) not in (types.StringType, types.UnicodeType): return backend
    836      backend = backend.upper()
    837      available_backends = [ k for k in _BACKENDS[class_name].keys() if k != 'default' ]
    838      # if for backend is given 'default' use the default backend module
    839      if backend == 'default': backend = _BACKENDS[class_name]['default']
    840      if backend in available_backends:
    841          exec("from {backend} import {class_name} as _this_class".format(backend=_BACKENDS[class_name][backend], class_name=class_name))
    842      else:
    843          raise x2go_exceptions.X2GoBackendException('unknown backend name %s for class %s' % (backend, class_name))
    844      return _this_class
    845
    846 -def genkeypair(local_username, client_address, key_type='RSA'):
    847      """\
    848      Generate an SSH pub/priv key pair without writing the private key to file.
    849
    850      @param local_username: the key is for this user
    851      @type local_username: C{unicode}
    852      @param client_address: the key is only valid for this client
    853      @type client_address: C{unicode}
    854      @param key_type: either of: RSA, DSA
    855      @type key_type: C{unicode}
    856
    857      """
    858      key = None
    859      pubkey = None
    860
    861      # generate key pair
    862      if unicode(key_type) == u'RSA':
    863          key = paramiko.RSAKey.generate(2048)
    864      elif unicode(key_type) == u'DSA':
    865          key = paramiko.DSSKey.generate(1024)
    866
    867      if key:
    868
    869          # assemble the public key
    870          if key_type == "RSA":
    871              pubkey_type = 'ssh-rsa'
    872          elif key_type == "DSA":
    873              pubkey_type = 'ssh-dss'
    874
    875          # FIXME: the from option does not work properly by some reason. Fix it later
    876          #pubkey = "from={client_address},no-X11-forwarding,no-pty,no-user-rc {pubkey_type} {pubkey} {local_username}@{client_address}".format(pubkey=key.get_base64(), pubkey_type=pubkey_type, local_username=local_username, client_address=client_address)
    877          pubkey = "no-X11-forwarding,no-pty,no-user-rc {pubkey_type} {pubkey} {local_username}@{client_address}".format(pubkey=key.get_base64(), pubkey_type=pubkey_type, local_username=local_username, client_address=client_address)
    878
    879      return (pubkey, key)
    880
    881
    882 -def which(basename):
    883      """\
    884      Python equivalent to the shell command "which".
    885
    886      @param basename: the basename of an application to be search for in $PATH
    887      @type basename: C{str}
    888
    889      @return: full path to the application
    890      @rtype: C{str}
    891
    892      """
    893      if _X2GOCLIENT_OS == 'Windows':
    894          delim = ";"
    895      else:
    896          delim = ":"
    897
    898      for path in os.environ["PATH"].split(delim):
    899          if os.path.exists(os.path.join(path, basename)):
    900                  return os.path.join(path, basename)
    901
    902      return None
    903