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
37
38 from defaults import X2GOCLIENT_OS as _X2GOCLIENT_OS
39 from defaults import X2GO_SESSIONPROFILE_DEFAULTS as _X2GO_SESSIONPROFILE_DEFAULTS
40 from defaults import X2GO_MIMEBOX_ACTIONS as _X2GO_MIMEBOX_ACTIONS
41 from defaults import pack_methods_nx3
42
43 if _X2GOCLIENT_OS != 'Windows':
44 import Xlib
45 from defaults import X_DISPLAY as _X_DISPLAY
46
47 if _X2GOCLIENT_OS == 'Windows':
48 import win32api
49 import win32gui
50
52
53 """\
54 Test if a given compression method is valid for NX3 Proxy.
55
56 @return: C{True} if C{method} is in the hard-coded list of NX3 compression methods.
57 @rtype: C{bool}
58
59 """
60 return method in pack_methods_nx3
61
62
64 """\
65 Return the X2Go session meta information as returned by the
66 C{x2golistsessions} server command for session C{session_name}.
67
68 @param session_name: name of a session
69 @type session_name: C{str}
70 @param stdout: raw output from the ,,x2golistsessions'' command, as list of strings
71 @type stdout: C{list}
72
73 @return: the output line that contains C{<session_name>}
74 @rtype: C{str} or C{None}
75
76 """
77 sessions = stdout.read().split("\n")
78 for line in sessions:
79
80 if not line:
81 continue
82 if session_name == line.split("|")[1]:
83 return line
84 return None
85
86
88 """\
89 Normalizes string, converts to lowercase, removes non-alpha characters,
90 converts spaces to hyphens and replaces round brackets by pointed brackets.
91
92 @param value: a string that shall be sluggified
93 @type value: C{str}
94
95 @return: the sluggified string
96 @rtype: C{str}
97
98 """
99 import unicodedata
100 value = unicodedata.normalize('NFKD', unicode(value)).encode('ascii', 'ignore')
101 value = re.sub('[^\w\s-]', '', value).strip().lower()
102 value = re.sub('[(]', '<', value).strip().lower()
103 value = re.sub('[)]', '>', value).strip().lower()
104 return value
105
107 """\
108 Generate a session profile ID as used in x2goclient's sessions config file.
109
110 @return: profile ID
111 @rtype: C{str}
112
113 """
114 import datetime
115 return datetime.datetime.utcnow().strftime('%Y%m%d%H%m%S%f')
116
117
119 """\
120 Check an ini file data structure passed on by a user app or class.
121
122 @param data_structure: an ini file date structure
123 @type data_structure: C{dict} of C{dict}s
124
125 @return: C{True} if C{data_structure} matches that of an ini file data structure
126 @rtype: C{bool}
127
128 """
129 if data_structure is None:
130 return False
131 if type(data_structure) is not types.DictType:
132 return False
133 for sub_dict in data_structure.values():
134 if type(sub_dict) is not types.DictType:
135 return False
136 return True
137
138
140 """\
141 Check the data structure of a default session profile passed by a user app.
142
143 @param data_structure: an ini file date structure
144 @type data_structure: C{dict} of C{dict}s
145
146 @return: C{True} if C{data_structure} matches that of an ini file data structure
147 @rtype: C{bool}
148
149 """
150 if data_structure is None:
151 return False
152 if type(data_structure) is not types.DictType:
153 return False
154 return True
155
156
158 """\
159 Convert session profile options as used in x2goclient's sessions file to
160 Python X2Go session parameters.
161
162 @param options: a dictionary of options, parameter names as in the X2Go ,,sessions'' file
163 @type options: C{dict}
164
165 @return: session options as used in C{X2goSession} instances
166 @rtype: C{dict}
167
168 """
169 _params = copy.deepcopy(options)
170
171
172 _known_options = _X2GO_SESSIONPROFILE_DEFAULTS.keys()
173 for p in _params.keys():
174 if p not in _known_options:
175 del _params[p]
176
177 _rename_dict = {
178 'host': 'server',
179 'user': 'username',
180 'soundsystem': 'snd_system',
181 'sndport': 'snd_port',
182 'type': 'kbtype',
183 'layout': 'kblayout',
184 'variant': 'kbvariant',
185 'speed': 'link',
186 'sshport': 'port',
187 'useexports': 'allow_share_local_folders',
188 'usemimebox': 'allow_mimebox',
189 'mimeboxextensions': 'mimebox_extensions',
190 'mimeboxaction': 'mimebox_action',
191 'print': 'printing',
192 'name': 'profile_name',
193 'key': 'key_filename',
194 'command': 'cmd',
195 'rdpserver': 'rdp_server',
196 'rdpoptions': 'rdp_options',
197 'xdmcpserver': 'xdmcp_server',
198 'useiconv': 'convert_encoding',
199 'iconvto': 'server_encoding',
200 'iconvfrom': 'client_encoding',
201 'usesshproxy': 'use_sshproxy',
202 'sshproxyhost': 'sshproxy_host',
203 'sshproxyuser': 'sshproxy_user',
204 'sshproxykeyfile': 'sshproxy_key_filename',
205 'sshproxytunnel': 'sshproxy_tunnel',
206 'sessiontitle': 'session_title',
207 'setsessiontitle': 'set_session_title',
208 'published': 'published_applications',
209 'autostart': 'auto_start_or_resume',
210 'autologin': 'auto_connect',
211
212 }
213 _speed_dict = {
214 '0': 'modem',
215 '1': 'isdn',
216 '2': 'adsl',
217 '3': 'wan',
218 '4': 'lan',
219 }
220
221 for opt, val in options.iteritems():
222
223
224 if opt in _rename_dict.keys():
225 del _params[opt]
226 opt = _rename_dict[opt]
227 if opt in _known_options:
228 _type = type(_known_options[opt])
229 _params[opt] = _type(val)
230 else:
231 _params[opt] = val
232
233
234 if opt == 'link':
235 val = str(val).lower()
236 if val in _speed_dict.keys():
237 val = _speed_dict[val]
238 val = val.lower()
239 _params['link'] = val
240
241
242 if opt in ('share_local_folders', 'mimebox_extensions'):
243 if type(val) is types.StringType:
244 if val:
245 _params[opt] = val.split(',')
246 else:
247 _params[opt] = []
248
249
250 if _params['quality']:
251 _params['pack'] = '%s-%s' % (_params['pack'], _params['quality'])
252
253 del _params['quality']
254
255 del _params['fstunnel']
256
257 if _params.has_key('export'):
258
259 _export = _params['export']
260 del _params['export']
261
262 _export = _export.replace(",", ";")
263
264 _export = _export.strip().strip('"').strip().strip(';').strip()
265 _export_list = [ f for f in _export.split(';') if f ]
266
267 _params['share_local_folders'] = []
268 for _shared_folder in _export_list:
269
270 if not ":" in _shared_folder: _shared_folder = "%s:1" % _shared_folder
271 if _shared_folder.split(":")[-1] == "1":
272 _params['share_local_folders'].append(":".join(_shared_folder.split(":")[:-1]))
273
274 if not options['fullscreen']:
275 _params['geometry'] = '%sx%s' % (options['width'], options['height'])
276 else:
277 _params['geometry'] = 'fullscreen'
278 del _params['width']
279 del _params['height']
280 del _params['fullscreen']
281
282 if not options['sound']:
283 _params['snd_system'] = 'none'
284 del _params['sound']
285
286 if not options['rootless']:
287 _params['session_type'] = 'desktop'
288 else:
289 _params['session_type'] = 'application'
290 del _params['rootless']
291
292 if _params['mimebox_action'] not in _X2GO_MIMEBOX_ACTIONS.keys():
293 _params['mimebox_action'] = 'OPEN'
294
295 if not options['usekbd']:
296 _params['kbtype'] = 'null/null'
297 _params['kblayout'] = 'null'
298 _params['kbvariant'] = 'null'
299 del _params['usekbd']
300
301 if not _params['kbtype'].strip(): _params['kbtype'] = 'null/null'
302 if not _params['kblayout'].strip(): _params['kblayout'] = 'null'
303 if not _params['kbvariant'].strip(): _params['kbvariant'] = 'null'
304
305 if not options['setdpi']:
306 del _params['dpi']
307 del _params['setdpi']
308
309
310 _ignored_options = [
311 'startsoundsystem',
312 'soundtunnel',
313 'defsndport',
314 'icon',
315 'xinerama',
316 'multidisp',
317 'krblogin',
318 ]
319 for i in _ignored_options:
320 del _params[i]
321
322 return _params
323
324
326 """\
327 Sorts session profile names by their timestamp (as used in the file format's section name).
328
329 @param session_infos: a dictionary of session infos as reported by L{X2goClient.list_sessions()}
330 @type session_infos: C{dict}
331
332 @return: a timestamp-sorted list of session names found in C{session_infos}
333 @rtype: C{list}
334
335 """
336 session_names = session_infos.keys()
337 sortable_session_names = [ '%s|%s' % (session_name.split('-')[-1].split('_')[0], session_name) for session_name in session_names ]
338 sortable_session_names.sort()
339 return [ session_name.split('|')[1] for session_name in sortable_session_names ]
340
341
343 """\
344 Imitates the behaviour of the GNU/touch command.
345
346 @param filename: name of the file to touch
347 @type filename: C{str}
348 @param mode: the file mode (as used for Python file objects)
349 @type mode: C{str}
350
351 """
352 if not os.path.isdir(os.path.dirname(filename)):
353 os.makedirs(os.path.dirname(filename), mode=00700)
354 f = open(filename, mode=mode)
355 f.close()
356
357
359 """\
360 Imitates the behaviour of the GNU/uniq command.
361
362 @param seq: a list/sequence containing consecutive duplicates.
363 @type seq: C{list}
364
365 @return: list that has been clean up from the consecutive duplicates
366 @rtype: C{list}
367
368 """
369
370 noDupes = []
371 [noDupes.append(i) for i in seq if not noDupes.count(i)]
372 return noDupes
373
374
376 """\
377 Render a list of all-known-to-Python character encodings (including
378 all known aliases)
379
380 """
381 from encodings.aliases import aliases
382 _raw_encname_list = []
383 _raw_encname_list.extend(aliases.keys())
384 _raw_encname_list.extend(aliases.values())
385 _raw_encname_list.sort()
386 _encname_list = []
387 for _raw_encname in _raw_encname_list:
388 _encname = _raw_encname.upper()
389 _encname = _encname.replace('_', '-')
390 _encname_list.append(_encname)
391 _encname_list.sort()
392 _encname_list = unique(_encname_list)
393 return _encname_list
394
395
397 """\
398 Try to remove a file, wait for unlocking, remove it once removing is possible...
399
400 @param dirname: directory name the file is in
401 @type dirname: C{str}
402 @param filename: name of the file to be removed
403 @type filename: C{str}
404
405 """
406 _not_removed = True
407 while _not_removed:
408 try:
409 os.remove(os.path.join(dirname, filename))
410 _not_removed = False
411 except:
412
413 gevent.sleep(5)
414
415
417 """\
418 Detect an unused IP socket.
419
420 @param bind_address: IP address to bind to
421 @type bind_address: C{str}
422 @param preferred_port: IP socket port that shall be tried first for availability
423 @type preferred_port: C{str}
424
425 @return: free local IP socket port that can be used for binding
426 @rtype: C{str}
427
428 """
429 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
430 try:
431 if preferred_port:
432 sock.bind((bind_address, preferred_port))
433 ipaddr, port = sock.getsockname()
434 else:
435 raise
436 except:
437 sock.bind(('', 0))
438 ipaddr, port = sock.getsockname()
439 return port
440
441
443 """\
444 Detect systems default character encoding.
445
446 @return: The system's local character encoding.
447 @rtype: C{str}
448
449 """
450 try:
451 encoding = locale.getdefaultlocale()[1]
452 if encoding is None:
453 raise BaseException
454 except:
455 try:
456 encoding = sys.getdefaultencoding()
457 except:
458 encoding = 'ascii'
459 return encoding
460
461
463 """\
464 Test if a given path is an absolute path name.
465
466 @param path: test this path for absolutism...
467 @type path: C{str}
468
469 @return: Returns C{True} if path is an absolute path name
470 @rtype: C{bool}
471
472 """
473 return bool((path.startswith('/') or re.match('^[%s]\:\\\\' % string.ascii_letters, path)))
474
475
477 """\
478 Wrapper for: xprop -root _XKB_RULES_NAMES
479
480 @return: A Python dictionary that contains the current X11 keyboard rules.
481 @rtype: C{dict}
482
483 """
484 p = subprocess.Popen(['xprop', '-root', '_XKB_RULES_NAMES',], stdout=subprocess.PIPE, )
485 _rn_list = p.stdout.read().split('"')
486 _rn_dict = {
487 'rules': _rn_list[1],
488 'model': _rn_list[3],
489 'layout': _rn_list[5],
490 'variant': _rn_list[7],
491 'options': _rn_list[9],
492 }
493 return _rn_dict
494
495
497 """\
498 Detect the current local screen's color depth.
499
500 @return: the local color depth in bits
501 @rtype: C{int}
502
503 """
504 if _X2GOCLIENT_OS != 'Windows':
505 try:
506 p = subprocess.Popen(['xwininfo', '-root',], stdout=subprocess.PIPE, )
507 _depth_line = [ _info.strip() for _info in p.stdout.read().split('\n') if 'Depth:' in _info ][0]
508 _depth = _depth_line.split(' ')[1]
509 return int(_depth)
510 except IndexError:
511
512 return 24
513 except OSError:
514
515 return 24
516
517 else:
518 return win32api.GetSystemMetrics(2)
519
520
522 """\
523 Test if color depth of this session is compatible with the
524 local screen's color depth.
525
526 @param depth_session: color depth of the session
527 @type depth_session: C{int}
528 @param depth_local: color depth of local screen
529 @type depth_local: C{int}
530
531 @return: Does the session color depth work with the local display?
532 @rtype: C{bool}
533
534 """
535 if depth_session == 0:
536 return True
537 if depth_session == depth_local:
538 return True
539 if ( ( depth_session == 24 or depth_session == 32 ) and ( depth_local == 24 or depth_local == 32 ) ):
540 return True;
541 return False
542
543
545 """\
546 Find a session window by its X2GO session ID.
547
548 @param session_name: session name/ID of an X2Go session window
549 @type session_name: C{str}
550
551 @return: the window object (or ID) of the searched for session window
552 @rtype: C{obj} on Unix, C{int} on Windows
553
554 """
555 if _X2GOCLIENT_OS != 'Windows':
556
557 display = _X_DISPLAY
558 root = display.screen().root
559
560 success = False
561 windowIDs = root.get_full_property(display.intern_atom('_NET_CLIENT_LIST'), Xlib.X.AnyPropertyType).value
562 for windowID in windowIDs:
563 window = display.create_resource_object('window', windowID)
564 name = window.get_wm_name()
565 if name is not None and session_name in name:
566 success = True
567 break
568
569 if success:
570 return window
571
572 else:
573
574 windows = []
575 window = None
576
577 def _callback(hwnd, extra):
578 if win32gui.GetWindowText(hwnd) == "X2GO-%s" % session_name:
579 windows.append(hwnd)
580
581 win32gui.EnumWindows(_callback, None)
582 if len(windows): window = windows[0]
583
584 return window
585
586
588 """\
589 Set title of session window.
590
591 @param session_window: session window instance
592 @type session_window: C{obj}
593 @param session_title: session title to be set for C{session_window}
594 @type session_title: C{str}
595
596 """
597 if _X2GOCLIENT_OS != 'Windows':
598 try:
599 session_window.set_wm_name(str(session_title))
600 session_window.set_wm_icon_name(str(session_title))
601 _X_DISPLAY.sync()
602 except Xlib.error.BadWindow:
603 pass
604
605 else:
606 win32gui.SetWindowText(session_window, session_title)
607
608
610 """\
611 Raise session window. Not functional for Unix-like operating systems.
612
613 @param session_window: session window instance
614 @type session_window: C{obj}
615
616 """
617 if _X2GOCLIENT_OS != 'Windows':
618 pass
619 else:
620 if session_window is not None:
621 win32gui.SetForegroundWindow(session_window)
622
623
625 """\
626 Merge sort two sorted lists
627
628 @param l1: first sorted list
629 @type l1: C{list}
630 @param l2: second sorted list
631 @type l2: C{list}
632
633 @return: the merge result of both sorted lists
634 @rtype: C{list}
635
636 """
637 ordered_list = []
638
639
640
641 l1 = l1[:]
642 l2 = l2[:]
643
644 while (l1 and l2):
645 if l1[0] not in l2:
646 item = l1.pop(0)
647 elif l2[0] not in l1:
648 item = l2.pop(0)
649 elif l1[0] in l2:
650 item = l1.pop(0)
651 l2.remove(item)
652 if item not in ordered_list:
653 ordered_list.append(item)
654
655
656 ordered_list.extend(l1 if l1 else l2)
657
658 return ordered_list
659