Package x2go :: Module utils
[frames] | no frames]

Source Code for Module x2go.utils

  1  # -*- coding: utf-8 -*- 
  2   
  3  # Copyright (C) 2010-2012 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 'sshproxytunnel': 'sshproxy_tunnel', 209 'sessiontitle': 'session_title', 210 'setsessiontitle': 'set_session_title', 211 'published': 'published_applications', 212 'autostart': 'auto_start_or_resume', 213 'autoconnect': 'auto_connect', 214 'forwardsshagent': 'forward_sshagent', 215 'autologin': 'look_for_keys', 216 'sshproxyautologin': 'sshproxy_look_for_keys', 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 # currently known but ignored in Python X2go 332 _ignored_options = [ 333 'startsoundsystem', 334 'soundtunnel', 335 'defsndport', 336 'icon', 337 'xinerama', 338 'multidisp', 339 'krblogin', 340 'directrdp', 341 'directrdpsettings', 342 'rdpclient', 343 'rdpport', 344 'sshproxytype', 345 ] 346 for i in _ignored_options: 347 del _params[i] 348 349 return _params
350 351
352 -def session_names_by_timestamp(session_infos):
353 """\ 354 Sorts session profile names by their timestamp (as used in the file format's section name). 355 356 @param session_infos: a dictionary of session infos as reported by L{X2goClient.list_sessions()} 357 @type session_infos: C{dict} 358 359 @return: a timestamp-sorted list of session names found in C{session_infos} 360 @rtype: C{list} 361 362 """ 363 session_names = session_infos.keys() 364 sortable_session_names = [ '%s|%s' % (session_name.split('-')[-1].split('_')[0], session_name) for session_name in session_names ] 365 sortable_session_names.sort() 366 return [ session_name.split('|')[1] for session_name in sortable_session_names ]
367 368
369 -def touch_file(filename, mode='a'):
370 """\ 371 Imitates the behaviour of the GNU/touch command. 372 373 @param filename: name of the file to touch 374 @type filename: C{str} 375 @param mode: the file mode (as used for Python file objects) 376 @type mode: C{str} 377 378 """ 379 if not os.path.isdir(os.path.dirname(filename)): 380 os.makedirs(os.path.dirname(filename), mode=00700) 381 f = open(filename, mode=mode) 382 f.close()
383 384
385 -def unique(seq):
386 """\ 387 Imitates the behaviour of the GNU/uniq command. 388 389 @param seq: a list/sequence containing consecutive duplicates. 390 @type seq: C{list} 391 392 @return: list that has been clean up from the consecutive duplicates 393 @rtype: C{list} 394 395 """ 396 # order preserving 397 noDupes = [] 398 [noDupes.append(i) for i in seq if not noDupes.count(i)] 399 return noDupes
400 401
402 -def known_encodings():
403 """\ 404 Render a list of all-known-to-Python character encodings (including 405 all known aliases) 406 407 """ 408 from encodings.aliases import aliases 409 _raw_encname_list = [] 410 _raw_encname_list.extend(aliases.keys()) 411 _raw_encname_list.extend(aliases.values()) 412 _raw_encname_list.sort() 413 _encname_list = [] 414 for _raw_encname in _raw_encname_list: 415 _encname = _raw_encname.upper() 416 _encname = _encname.replace('_', '-') 417 _encname_list.append(_encname) 418 _encname_list.sort() 419 _encname_list = unique(_encname_list) 420 return _encname_list
421 422
423 -def patiently_remove_file(dirname, filename):
424 """\ 425 Try to remove a file, wait for unlocking, remove it once removing is possible... 426 427 @param dirname: directory name the file is in 428 @type dirname: C{str} 429 @param filename: name of the file to be removed 430 @type filename: C{str} 431 432 """ 433 _not_removed = True 434 while _not_removed: 435 try: 436 os.remove(os.path.join(dirname, filename)) 437 _not_removed = False 438 except: 439 # file is probably locked 440 gevent.sleep(5)
441 442
443 -def detect_unused_port(bind_address='127.0.0.1', preferred_port=None):
444 """\ 445 Detect an unused IP socket. 446 447 @param bind_address: IP address to bind to 448 @type bind_address: C{str} 449 @param preferred_port: IP socket port that shall be tried first for availability 450 @type preferred_port: C{str} 451 452 @return: free local IP socket port that can be used for binding 453 @rtype: C{str} 454 455 """ 456 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) 457 try: 458 if preferred_port: 459 sock.bind((bind_address, preferred_port)) 460 ipaddr, port = sock.getsockname() 461 else: 462 raise 463 except: 464 sock.bind(('', 0)) 465 ipaddr, port = sock.getsockname() 466 return port
467 468
469 -def get_encoding():
470 """\ 471 Detect systems default character encoding. 472 473 @return: The system's local character encoding. 474 @rtype: C{str} 475 476 """ 477 try: 478 encoding = locale.getdefaultlocale()[1] 479 if encoding is None: 480 raise BaseException 481 except: 482 try: 483 encoding = sys.getdefaultencoding() 484 except: 485 encoding = 'ascii' 486 return encoding
487 488
489 -def is_abs_path(path):
490 """\ 491 Test if a given path is an absolute path name. 492 493 @param path: test this path for absolutism... 494 @type path: C{str} 495 496 @return: Returns C{True} if path is an absolute path name 497 @rtype: C{bool} 498 499 """ 500 return bool((path.startswith('/') or re.match('^[%s]\:\\\\' % string.ascii_letters, path)))
501 502
503 -def xkb_rules_names():
504 """\ 505 Wrapper for: xprop -root _XKB_RULES_NAMES 506 507 @return: A Python dictionary that contains the current X11 keyboard rules. 508 @rtype: C{dict} 509 510 """ 511 p = subprocess.Popen(['xprop', '-root', '_XKB_RULES_NAMES',], stdout=subprocess.PIPE, ) 512 _rn_list = p.stdout.read().split('"') 513 _rn_dict = { 514 'rules': _rn_list[1], 515 'model': _rn_list[3], 516 'layout': _rn_list[5], 517 'variant': _rn_list[7], 518 'options': _rn_list[9], 519 } 520 return _rn_dict
521
522 -def local_color_depth():
523 """\ 524 Detect the current local screen's color depth. 525 526 @return: the local color depth in bits 527 @rtype: C{int} 528 529 """ 530 if _X2GOCLIENT_OS != 'Windows': 531 try: 532 p = subprocess.Popen(['xwininfo', '-root',], stdout=subprocess.PIPE, ) 533 _depth_line = [ _info.strip() for _info in p.stdout.read().split('\n') if 'Depth:' in _info ][0] 534 _depth = _depth_line.split(' ')[1] 535 return int(_depth) 536 except IndexError: 537 # a sensible default value 538 return 24 539 except OSError: 540 # for building your package... 541 return 24 542 543 else: 544 return win32api.GetSystemMetrics(2)
545 546
547 -def is_color_depth_ok(depth_session, depth_local):
548 """\ 549 Test if color depth of this session is compatible with the 550 local screen's color depth. 551 552 @param depth_session: color depth of the session 553 @type depth_session: C{int} 554 @param depth_local: color depth of local screen 555 @type depth_local: C{int} 556 557 @return: Does the session color depth work with the local display? 558 @rtype: C{bool} 559 560 """ 561 if depth_session == 0: 562 return True 563 if depth_session == depth_local: 564 return True 565 if ( ( depth_session == 24 or depth_session == 32 ) and ( depth_local == 24 or depth_local == 32 ) ): 566 return True; 567 if ( ( depth_session == 16 or depth_session == 17 ) and ( depth_local == 16 or depth_local == 17 ) ): 568 return True; 569 return False
570 571
572 -def find_session_window(session_name):
573 """\ 574 Find a session window by its X2GO session ID. 575 576 @param session_name: session name/ID of an X2Go session window 577 @type session_name: C{str} 578 579 @return: the window object (or ID) of the searched for session window 580 @rtype: C{obj} on Unix, C{int} on Windows 581 582 """ 583 if _X2GOCLIENT_OS != 'Windows': 584 # establish connection to the win API in use... 585 display = _X_DISPLAY 586 root = display.screen().root 587 588 success = False 589 windowIDs_obj = root.get_full_property(display.intern_atom('_NET_CLIENT_LIST'), Xlib.X.AnyPropertyType) 590 591 if windowIDs_obj is not None: 592 windowIDs = windowIDs_obj.value 593 594 for windowID in windowIDs: 595 window = display.create_resource_object('window', windowID) 596 name = window.get_wm_name() 597 if name is not None and session_name in name: 598 success = True 599 break 600 601 if success: 602 return window 603 604 else: 605 606 windows = [] 607 window = None 608 609 def _callback(hwnd, extra): 610 if win32gui.GetWindowText(hwnd) == "X2GO-%s" % session_name: 611 windows.append(hwnd)
612 613 win32gui.EnumWindows(_callback, None) 614 if len(windows): window = windows[0] 615 616 return window 617 618
619 -def get_desktop_geometry():
620 """\ 621 Get the geometry of the current screen's desktop by 622 wrapping around:: 623 624 xprop -root '_NET_DESKTOP_GEOMETRY' 625 626 @return: a (<width>, <height>) tuple will be returned 627 @rtype: C{tuple} 628 629 """ 630 if _X2GOCLIENT_OS != 'Windows': 631 p = subprocess.Popen(['xprop', '-root', '_NET_DESKTOP_GEOMETRY',], stdout=subprocess.PIPE, ) 632 _paramval = p.stdout.read().split("=") 633 if len(_paramval) == 2: 634 _list = _paramval[1].rstrip('\n').split(',') 635 if len(_list) == 2: 636 return (_list[0].strip(), _list[1].strip()) 637 638 return None
639
640 -def get_workarea_geometry():
641 """\ 642 Get the geometry of the current screen's work area by 643 wrapping around:: 644 645 xprop -root '_NET_WORKAREA' 646 647 @return: a (<width>, <height>) tuple will be returned 648 @rtype: C{tuple} 649 650 """ 651 if _X2GOCLIENT_OS != 'Windows': 652 p = subprocess.Popen(['xprop', '-root', '_NET_WORKAREA',], stdout=subprocess.PIPE, ) 653 _list = p.stdout.read().rstrip('\n').split(',') 654 if len(_list) == 4: 655 return (_list[2].strip(), _list[3].strip()) 656 else: 657 return None 658 else: 659 660 return None
661 662
663 -def set_session_window_title(session_window, session_title):
664 """\ 665 Set title of session window. 666 667 @param session_window: session window instance 668 @type session_window: C{obj} 669 @param session_title: session title to be set for C{session_window} 670 @type session_title: C{str} 671 672 """ 673 if _X2GOCLIENT_OS != 'Windows': 674 try: 675 session_window.set_wm_name(str(session_title)) 676 session_window.set_wm_icon_name(str(session_title)) 677 _X_DISPLAY.sync() 678 except Xlib.error.BadWindow: 679 pass 680 681 else: 682 win32gui.SetWindowText(session_window, session_title)
683 684
685 -def raise_session_window(session_window):
686 """\ 687 Raise session window. Not functional for Unix-like operating systems. 688 689 @param session_window: session window instance 690 @type session_window: C{obj} 691 692 """ 693 if _X2GOCLIENT_OS != 'Windows': 694 pass 695 else: 696 if session_window is not None: 697 win32gui.SetForegroundWindow(session_window)
698 699
700 -def merge_ordered_lists(l1, l2):
701 """\ 702 Merge sort two sorted lists 703 704 @param l1: first sorted list 705 @type l1: C{list} 706 @param l2: second sorted list 707 @type l2: C{list} 708 709 @return: the merge result of both sorted lists 710 @rtype: C{list} 711 712 """ 713 ordered_list = [] 714 715 # Copy both the args to make sure the original lists are not 716 # modified 717 l1 = l1[:] 718 l2 = l2[:] 719 720 while (l1 and l2): 721 if l1[0] not in l2: 722 item = l1.pop(0) 723 elif l2[0] not in l1: 724 item = l2.pop(0) 725 elif l1[0] in l2: 726 item = l1.pop(0) 727 l2.remove(item) 728 if item not in ordered_list: 729 ordered_list.append(item) 730 731 # Add the remaining of the lists 732 ordered_list.extend(l1 if l1 else l2) 733 734 return ordered_list
735
736 -def compare_versions(version_a, op, version_b):
737 """\ 738 Compare <version_a> with <version_b> using operator <op>. 739 In the background C{distutils.version.LooseVersion} is 740 used for the comparison operation. 741 742 @param version_a: a version string 743 @type version_a: C{str} 744 @param op: an operator provide as string (e.g. '<', '>', '==', '>=' etc.) 745 @type op: C{str} 746 @param version_b: another version string that is to be compared with <version_a> 747 @type version_b: C{str} 748 749 """ 750 751 ### FIXME: this comparison is not reliable with beta et al. version strings 752 753 ver_a = distutils.version.LooseVersion(version_a) 754 ver_b = distutils.version.LooseVersion(version_b) 755 756 return eval("ver_a %s ver_b" % op)
757
758 -class ProgressStatus(object):
759 """\ 760 A simple progress status iterator class. 761 762 """
763 - def __init__(self, progress_event, progress_func=range(0, 100, 10)):
764 """\ 765 @param progress_event: a threading.Event() object that gets notified on progress 766 @type progress_event: C{obj} 767 @param progress_func: a function that delivers a value between 0 and 100 (progress percentage value) 768 @type progress_func: C{func} 769 770 """ 771 self.ev = progress_event 772 self.progress_func = progress_func
773
774 - def __iter__(self):
775 """\ 776 Intialize the L{ProgresStatus} iterator object. 777 778 """ 779 self.status = self.progress_func() 780 return self
781
782 - def next(self):
783 """\ 784 On each iteration wait for the progress event to get triggered from an outside 785 part of the application. 786 787 Once the event fires read the progress status from the progress retrieval function 788 and clear the event afterwards (so we wait for the next firing of the event). 789 790 """ 791 if self.status < 100 and self.status != -1: 792 self.ev.wait() 793 self.status = self.progress_func() 794 self.ev.clear() 795 return self.status 796 else: 797 raise StopIteration
798