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