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 '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
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
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
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
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
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
397 noDupes = []
398 [noDupes.append(i) for i in seq if not noDupes.count(i)]
399 return noDupes
400
401
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
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
440 gevent.sleep(5)
441
442
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
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
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
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
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
538 return 24
539 except OSError:
540
541 return 24
542
543 else:
544 return win32api.GetSystemMetrics(2)
545
546
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
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
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
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
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
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
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
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
716
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
732 ordered_list.extend(l1 if l1 else l2)
733
734 return ordered_list
735
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
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
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
775 """\
776 Intialize the L{ProgresStatus} iterator object.
777
778 """
779 self.status = self.progress_func()
780 return self
781
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