1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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
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
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
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
81 if not line:
82 continue
83 if session_name == line.split("|")[1]:
84 return line
85 return None
86
87
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
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
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
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
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
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
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
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
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
261 if _params['quality']:
262 _params['pack'] = '%s-%s' % (_params['pack'], _params['quality'])
263
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
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
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
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
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
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
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
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
411 noDupes = []
412 [noDupes.append(i) for i in seq if not noDupes.count(i)]
413 return noDupes
414
415
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
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
454 gevent.sleep(5)
455
456
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
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
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
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
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
552 return 24
553 except OSError:
554
555 return 24
556
557 else:
558 return win32api.GetSystemMetrics(2)
559
560
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
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
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
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
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
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
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
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
730
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
746 ordered_list.extend(l1 if l1 else l2)
747
748 return ordered_list
749
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
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
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
789 """\
790 Intialize the L{ProgressStatus} iterator object.
791
792 """
793 self.status = self.progress_func()
794 return self
795
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