Package x2go :: Module utils

[frames] | no frames]

# Source Code for Module x2go.utils

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