source: epoptes/trunk/fuentes/epoptes/ui/gui.py @ 295

Last change on this file since 295 was 295, checked in by mabarracus, 3 years ago

copy trusty epoptes code

File size: 41.9 KB
Line 
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3
4###########################################################################
5# GUI.
6#
7# Copyright (C) 2010 Fotis Tsamis <ftsamis@gmail.com>
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FINESS FOR A PARTICULAR PURPOSE.  See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program.  If not, see <http://www.gnu.org/licenses/>.
21#
22# On Debian GNU/Linux systems, the complete text of the GNU General
23# Public License can be found in `/usr/share/common-licenses/GPL".
24###########################################################################
25
26import pygtk
27pygtk.require('2.0')
28
29
30import gtk
31import gobject
32import os
33import re
34import dbus
35import logging
36import sys
37import shlex
38import pipes
39from gobject import TYPE_PYOBJECT as gPyobject
40from twisted.internet import reactor
41from twisted.python import log
42
43from epoptes import __version__
44from epoptes import ui
45from notifications import *
46from execcommand import *
47from sendmessage import *
48from about_dialog import About
49from client_information import ClientInformation
50from epoptes.daemon import uiconnection
51from epoptes.core.lib_users import *
52from epoptes.common import ltsconf
53from epoptes.common import config
54from epoptes.common.constants import *
55from epoptes.core import wol
56from epoptes.core import structs
57
58
59class EpoptesGui(object):
60   
61    def __init__(self):
62        self.shownCompatibilityWarning = False
63        self.vncserver = None
64        self.vncviewer = None
65        self.scrWidth = 100
66        self.scrHeight = 75
67        self.showRealNames = False
68        self.currentScreenshots = dict()
69        self.current_macs = subprocess.Popen(['sh', '-c',
70            """ip -oneline -family inet link show | sed -n '/.*ether[[:space:]]*\\([[:xdigit:]:]*\).*/{s//\\1/;y/abcdef-/ABCDEF:/;p;}'
71            echo $LTSP_CLIENT_MAC"""],
72            stdout=subprocess.PIPE).communicate()[0].split()
73        self.uid = os.getuid()
74        if not os.path.isdir(os.path.expanduser('~/.config/epoptes/')):
75             os.system("mkdir -p "+os.path.expanduser('~/.config/epoptes/'))
76        if 'thumbnails_width' in config.user:
77            self.scrWidth = config.user['thumbnails_width']
78        self.offline = gtk.gdk.pixbuf_new_from_file('images/offline.svg')
79        self.thin = gtk.gdk.pixbuf_new_from_file('images/thin.svg')
80        self.fat = gtk.gdk.pixbuf_new_from_file('images/fat.svg')
81        self.standalone = gtk.gdk.pixbuf_new_from_file('images/standalone.svg')
82        self.imagetypes = {'thin' : self.thin, 'fat' : self.fat,
83            'standalone' : self.standalone, 'offline' : self.offline}
84       
85        self.wTree = gtk.Builder()
86        self.wTree.add_from_file('epoptes.ui')
87       
88        self.get = lambda obj: self.wTree.get_object(obj)
89       
90        # Connect glade handlers with the callback functions
91        self.wTree.connect_signals(self)
92       
93       
94        # Hide the remote assistance menuitem if epoptes-client is not installed
95        if not os.path.isfile('/usr/share/epoptes-client/remote-assistance'):
96            self.get('mi_remote_assistance').set_property('visible', False)
97            self.get('remote_assistance_separator').set_property('visible', False)
98       
99        self.gstore = gtk.ListStore(str, object, bool)
100       
101        self.gtree = self.get("groups_tree")
102        self.gtree.set_model(self.gstore)
103        self.gtree.get_selection().connect("changed", self.on_group_selection_changed)
104       
105        self.mainwin = self.get('mainwindow')
106       
107        self.cstore = gtk.ListStore(str, gtk.gdk.Pixbuf, object, str)
108        self.cview = self.get('clientsview')
109        self.cView_order = (1, 0)
110        self.set_cView(*self.cView_order)
111       
112        self.cview.set_model(self.cstore)
113        self.cview.set_pixbuf_column(C_PIXBUF)
114        self.cview.set_text_column(C_LABEL)
115       
116        self.cstore.set_sort_column_id(C_LABEL, gtk.SORT_ASCENDING)
117        self.on_clients_selection_changed()
118       
119        self.cview.enable_model_drag_source(gtk.gdk.BUTTON1_MASK, [("add", gtk.TARGET_SAME_APP, 0)], gtk.gdk.ACTION_COPY)
120        self.gtree.enable_model_drag_dest([("add", gtk.TARGET_SAME_APP, 0)], gtk.gdk.ACTION_COPY)
121       
122        self.default_group = structs.Group('<b>'+_('Detected clients')+'</b>')
123        default_iter = self.gstore.append([self.default_group.name, self.default_group, False])
124        self.default_group.ref = gtk.TreeRowReference(self.gstore, self.gstore.get_path(default_iter))
125        self.gtree.get_selection().select_path(self.default_group.ref.get_path())
126       
127        self.get('iconsSizeAdjustment').set_value(self.scrWidth)
128        self.reload_imagetypes()
129       
130        saved_clients, groups = config.read_groups(os.path.expanduser('~/.config/epoptes/groups.json'))
131        for grp in groups:
132            self.gstore.append([grp.name, grp, True])
133       
134        self.fillIconView(self.getSelectedGroup()[1])
135        if config.settings.has_option('GUI', 'selected_group'):
136            path = config.settings.getint('GUI', 'selected_group')
137            self.gtree.get_selection().select_path(path)
138        if config.settings.has_option('GUI', 'label'):
139            try:
140                self.get(config.settings.get('GUI', 'label')).set_active(True)
141            except:
142                pass
143        if config.settings.has_option('GUI', 'showRealNames'):
144            self.get('mcShowRealNames').set_active(config.settings.getboolean('GUI', 'showRealNames'))
145       
146
147    #################################################################
148    #                       Callback functions                      #
149    #################################################################
150    def on_gtree_drag_motion(self, widget, context, x, y, etime):
151        drag_info = widget.get_dest_row_at_pos(x, y)
152        # Don't allow dropping in the empty space of the treeview,
153        # or inside the 'Detected' group, or inside the currently selected group
154        selected_path = self.gstore.get_path(self.getSelectedGroup()[0])
155        if (not drag_info or drag_info[0] == self.default_group.ref.get_path() or
156            drag_info[0] == selected_path):
157            widget.set_drag_dest_row(None, gtk.TREE_VIEW_DROP_AFTER)
158        else:
159            path, pos = drag_info
160            # Don't allow dropping between the groups treeview rows
161            if pos == gtk.TREE_VIEW_DROP_BEFORE:
162                widget.set_drag_dest_row(path, gtk.TREE_VIEW_DROP_INTO_OR_BEFORE)
163            elif pos == gtk.TREE_VIEW_DROP_AFTER:
164                widget.set_drag_dest_row(path, gtk.TREE_VIEW_DROP_INTO_OR_AFTER)
165       
166        context.drag_status(context.suggested_action, etime)
167        return True
168   
169    def on_gtree_drag_drop(self, wid, context, x, y, time):
170        dest = self.gtree.get_dest_row_at_pos(x, y)
171        if dest is not None:
172            path, pos = dest
173            group = self.gstore[path][G_INSTANCE]
174            if not group is self.default_group:
175                for cln in self.getSelectedClients():
176                    cln = cln[C_INSTANCE]
177                    if not group.has_client(cln):
178                        group.add_client(cln)
179   
180        context.finish(True, False, time)
181        return True
182
183    def on_mainwin_destroy(self, widget):
184        """
185        Quit clicked
186
187        Close main window
188        """
189        try:
190          sel_group = self.gstore.get_path(self.getSelectedGroup()[0])[0]
191          self.gstore.remove(self.gstore.get_iter(self.default_group.ref.get_path()))
192          config.save_groups(os.path.expanduser('~/.config/epoptes/groups.json'), self.gstore)
193          settings = config.settings
194          if not settings.has_section('GUI'):
195              settings.add_section('GUI')       
196       
197          settings.set('GUI', 'selected_group', sel_group)
198          settings.set('GUI', 'showRealNames', self.showRealNames)
199          settings.set('GUI', 'thumbnails_width', self.scrWidth)
200        except Exception as e:
201          print(str(e))
202          pass
203        try:
204            f = open(os.path.expanduser('~/.config/epoptes/settings'), 'w')
205            settings.write(f)
206            f.close()
207        except:
208            pass
209        if not self.vncserver is None:
210            self.vncserver.kill()
211        if not self.vncviewer is None:
212            self.vncviewer.kill()
213        reactor.stop()
214           
215    def toggleRealNames(self, widget=None):
216        """Show/hide the real names of the users instead of the usernames"""
217        self.showRealNames = widget.get_active()
218        for row in self.cstore:           
219            self.setLabel(row)
220
221    def wake_on_lan(self, widget):
222        """Boot the selected computers with WOL"""
223        for client in self.getSelectedClients():
224            # Make sure that only offline computers will be sent to wol
225            client = client[C_INSTANCE]
226            if client.is_offline():
227                wol.wake_on_lan(client.mac)
228
229    def poweroff(self, widget):
230        """Shut down the selected clients."""
231        self.execOnSelectedClients(["shutdown"],
232            warn=_('Are you sure you want to shutdown all the computers?'))
233
234    def reboot(self, widget):
235        """Reboot the selected clients."""
236        self.execOnSelectedClients(["reboot"],
237            warn=_('Are you sure you want to reboot all the computers?'))
238
239
240    def logout(self, widget):
241        """Log off the users of the selected clients."""
242        self.execOnSelectedClients(['logout'], mode=EM_SESSION,
243            warn=_('Are you sure you want to log off all the users?'))
244
245
246    def reverseConnection(self, widget, path, view_column, cmd, *args):
247        # Open vncviewer in listen mode
248        if self.vncviewer is None:
249            self.vncviewerport = self.findUnusedPort()
250            # If the user installed ssvnc, prefer it over xvnc4viewer
251            if os.path.isfile('/usr/bin/ssvncviewer'):
252                self.vncviewer = subprocess.Popen(['ssvncviewer',
253                    '-multilisten', str(self.vncviewerport-5500)])
254            elif os.path.isfile('/usr/bin/xvnc4viewer'):
255                self.vncviewer = subprocess.Popen(['xvnc4viewer',
256                    '-listen', str(self.vncviewerport)])
257            else:
258                self.vncviewer = subprocess.Popen(['vncviewer',
259                    '-listen', str(self.vncviewerport-5500)])
260
261        # And, tell the clients to connect to the server
262        # Adding some workarount to solve the "teacher-non-in-server problem"
263        # getting the ip from current machine and use it in xvnc
264        #
265        class_ip = self.getClassroomIp()
266        if class_ip == None:
267            class_ip = "server"
268
269        self.execOnSelectedClients([cmd, str(class_ip)+":"+str(self.vncviewerport)] + list(args))
270
271
272    def assistUser(self, widget, path=None, view_column=None):
273        if config.settings.has_option('GUI', 'grabkbdptr'):
274            grab = config.settings.getboolean('GUI', 'grabkbdptr')
275        if grab:
276            self.reverseConnection(widget, path, view_column, 'get_assisted', 'True')
277        else:
278            self.reverseConnection(widget, path, view_column, 'get_assisted')
279
280
281    def monitorUser(self, widget, path=None, view_column=None):
282        self.reverseConnection(widget, path, view_column, 'get_monitored')
283
284
285    def findUnusedPort(self):
286        """Find an unused port."""
287        import socket
288
289        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
290        s.bind(('', 0))
291        s.listen(1)
292        port = s.getsockname()[1]
293        s.close()
294        return port
295
296
297    def _broadcastScreen(self, fullscreen=''):
298        if self.vncserver is None:
299            import random
300            import string
301
302            pwdfile=os.path.expanduser('~/.config/epoptes/vncpasswd')
303            pwd=''.join(random.sample(string.letters + string.digits, 8))
304            subprocess.call(['x11vnc', '-storepasswd', pwd, pwdfile])
305            f=open(pwdfile)
306            pwd=f.read()
307            f.close()
308            self.pwd=''.join('\\%o' % ord(c) for c in pwd)
309            self.vncserverport = self.findUnusedPort()
310            class_ip = self.getClassroomIp()
311            if class_ip == None:
312                class_ip = "server"
313
314            self.vncserver = subprocess.Popen(['x11vnc', '-noshm', '-nopw',
315                '-quiet', '-viewonly', '-shared', '-forever', '-nolookup',
316                '-24to32', '-threads', '-rfbport', str(self.vncserverport),
317                '-rfbauth', pwdfile])
318        self.execOnSelectedClients(['stop_screensaver'],
319            mode=EM_SYSTEM_AND_SESSION)
320        self.execOnSelectedClients(["receive_broadcast", str(class_ip)+":"+str(self.vncserverport), 
321            self.pwd, fullscreen], mode=EM_SYSTEM_OR_SESSION)
322
323
324    def broadcastScreen(self, widget):
325        self._broadcastScreen('true')
326
327
328    def broadcastScreenWindowed(self, widget):
329        self._broadcastScreen('')
330
331
332    def stopTransmissions(self, widget):
333        self.execOnClients(['stop_receptions'], self.cstore,
334            mode=EM_SYSTEM_AND_SESSION)
335        if not self.vncserver is None:
336            self.vncserver.kill()
337            self.vncserver = None
338
339
340    ## FIXME FIXME: Should we allow for running arbitrary commands in clients?
341    def execDialog(self, widget):
342        cmd = startExecuteCmdDlg()
343        # If Cancel or Close were clicked
344        if cmd == '':
345            return
346        em = EM_SESSION
347        if cmd[:5] == 'sudo ':
348            em = EM_SYSTEM
349            cmd = cmd[4:]
350        self.execOnSelectedClients(['execute', cmd], mode=em)
351
352
353    def sendMessageDialog(self, widget):
354        msg = startSendMessageDlg()
355        if msg:
356            cmd = list(msg)
357            cmd.insert(0, 'message')
358            self.execOnSelectedClients(cmd)
359
360
361    ## FIXME / FIXUS: Should we allow it?
362    def openTerminal(self, em):
363        clients = self.getSelectedClients()
364       
365        # If there is no client selected, send the command to all
366        if len(clients) == 0:
367            clients = self.cstore
368
369        for client in clients:
370            inst = client[C_INSTANCE]
371            if inst.type == 'offline':
372                continue
373        # Adding some workarount to solve the "teacher-non-in-server problem"
374        # getting the ip from current machine and use it in xvnc
375        class_ip = self.getClassroomIp()
376        if class_ip == None:
377           class_ip = "server"
378
379        port = self.findUnusedPort()
380
381
382        subprocess.Popen(['xterm', '-e', 'socat',
383           'tcp-listen:%d,keepalive=1' % port, 'stdio,raw,echo=0'])
384
385        port = str(class_ip)+":"+str(port)
386        self.execOnClients(['remote_term', port], [client], mode=em)
387
388
389    def openUserTerminal(self, widget):
390        self.openTerminal(EM_SESSION)
391
392
393    def openRootTerminal(self, widget):
394        self.openTerminal(EM_SYSTEM)
395
396
397    def remoteRootTerminal(self, widget):
398        self.execOnSelectedClients(['root_term'], mode=EM_SYSTEM)
399    ## END_FIXUS
400
401
402    def lockScreen(self, widget):
403        """
404        Lock screen for all the selected clients, displaying a message
405        """
406        msg = _("The screen is locked by a system administrator.")
407        self.execOnSelectedClients(['lock_screen', 0, msg])
408
409
410    def unlockScreen(self, widget):
411        """
412        Unlock screen for all clients selected
413        """
414        self.execOnSelectedClients(['unlock_screen'], mode=EM_SESSION_AND_SYSTEM)
415
416
417    def blockInternet(self,widget):
418       
419
420        clients = self.getSelectedClients()
421        fat_clients=["standalone","fat"]
422        for client in clients:
423
424                if client[C_INSTANCE].type in fat_clients:
425                       
426                        ip=client[C_INSTANCE].users.keys()[0].split(":")[0]
427                        print
428                        print "* Blocking standalone client [%s] ..."%ip
429                       
430                        try:
431                                import xmlrpclib as x
432                                c=x.ServerProxy("https://server:9779")
433                                c.block((self.n4d_user,self.n4d_password),"IptablesManager","",ip)
434                        except Exception as e:
435                                print e
436                       
437                if client[C_INSTANCE].type=="thin":
438                       
439                        key=client[C_INSTANCE].users.keys()[0]
440                        user=client[C_INSTANCE].users[key]["uname"]
441                        print
442                        print "* Blocking thin client [%s] ..."%user
443                       
444                        try:
445                                import xmlrpclib as x
446                                c=x.ServerProxy("https://server:9779")
447                                c.block((self.n4d_user,self.n4d_password),"IptablesManager",user,"127.0.0.1")
448                        except Exception as e:
449                                print e
450                       
451                       
452
453
454    def unblockInternet(self,widget):
455
456        clients = self.getSelectedClients()
457        fat_clients=["standalone","fat"]
458        for client in clients:
459
460                if client[C_INSTANCE].type in fat_clients:
461                       
462                        ip=client[C_INSTANCE].users.keys()[0].split(":")[0]
463                        print
464                        print "* Unblocking standalone client [%s] ..."%ip
465                       
466                        try:
467                                import xmlrpclib as x
468                                c=x.ServerProxy("https://localhost:9779")
469                                c.unblock((self.n4d_user,self.n4d_password),"IptablesManager","",ip)
470                        except Exception as e:
471                                print e
472                       
473                if client[C_INSTANCE].type=="thin":
474                       
475                        key=client[C_INSTANCE].users.keys()[0]
476                        user=client[C_INSTANCE].users[key]["uname"]
477                       
478                        print "* Unblocking thin client [%s] ..."%user
479                       
480                        try:
481                                import xmlrpclib as x
482                                c=x.ServerProxy("https://localhost:9779")
483                                c.unblock((self.n4d_user,self.n4d_password),"IptablesManager",user,"127.0.0.1")
484                        except Exception as e:
485                                print e
486       
487
488    def soundOff(self, widget):
489        """
490        Disable sound usage for clients selected
491        """
492        self.execOnSelectedClients(['mute_sound', 0], mode=EM_SYSTEM_OR_SESSION)
493
494
495    def soundOn(self, widget):
496        """
497        Enable sound usage for clients selected
498        """
499        self.execOnSelectedClients(['unmute_sound'], mode=EM_SYSTEM_AND_SESSION)
500
501
502    def on_remove_from_group_clicked(self, widget):
503        clients = self.getSelectedClients()
504        group = self.getSelectedGroup()[1]
505       
506        if self.warnDlgPopup(_('Are you sure you want to remove the selected client(s) from group "%s"?') %group.name):
507            for client in clients:
508                group.remove_client(client[C_INSTANCE])
509            self.fillIconView(self.getSelectedGroup()[1], True)
510
511
512    def set_move_group_sensitivity(self):
513        selected = self.getSelectedGroup()
514        selected_path = self.gstore.get_path(selected[0])[0]
515        blocker = not selected[1] is self.default_group
516        self.get('move_group_up').set_sensitive(blocker and selected_path > 1)
517        self.get('move_group_down').set_sensitive(blocker and selected_path < len(self.gstore)-1)
518
519
520    def on_move_group_down_clicked(self, widget):
521        selected_group_iter = self.getSelectedGroup()[0]
522        self.gstore.swap(selected_group_iter, self.gstore.iter_next(selected_group_iter))
523        self.set_move_group_sensitivity()
524
525
526    def on_move_group_up_clicked(self, widget):
527        selected_group_iter = self.getSelectedGroup()[0]
528        previous_iter = self.gstore.get_iter(self.gstore.get_path(selected_group_iter)[0]-1)
529        self.gstore.swap(selected_group_iter, previous_iter)
530        self.set_move_group_sensitivity()
531
532
533    def on_remove_group_clicked(self, widget):
534        group_iter = self.getSelectedGroup()[0]
535        group = self.gstore[group_iter][G_INSTANCE]
536       
537        if self.warnDlgPopup(_('Are you sure you want to remove group "%s"?') % group.name):
538            self.gstore.remove(group_iter)
539
540
541    def on_add_group_clicked(self, widget):
542        new_group = structs.Group()
543        iter = self.gstore.append([new_group.name, new_group, True])
544        # Edit the name of the newly created group
545        self.gtree.set_cursor(self.gstore.get_path(iter), self.get('group_name_column'), True)
546
547
548    def on_group_renamed(self, widget, path, new_name):
549        self.gstore[path][G_LABEL] = new_name
550        self.gstore[path][G_INSTANCE].name = new_name
551
552
553    #FIXME: Remove the second parameter, find a better way
554    def on_tb_client_properties_clicked(self, widget=None):
555        dlg = ClientInformation(self.getSelectedClients(), self.daemon.command)
556        if self.isDefaultGroupSelected():
557            dlg.edit_button.set_sensitive(False)
558        dlg.run()
559        self.setLabel(self.getSelectedClients()[0])
560
561
562    def on_mi_remote_assistance_activate(self, widget=None):
563        path = '/usr/share/epoptes-client'
564        subprocess.Popen('%s/remote-assistance' %path, shell=True, cwd=path)
565
566
567    def on_mi_about_activate(self, widget=None):
568        About().run()
569
570
571    def on_cViewHU_toggled(self, mitem):
572        self.set_cView(1, 0)
573        config.settings.set('GUI', 'label', 'miClientsViewHostUser')
574
575
576    def on_cViewUH_toggled(self, mitem):
577        self.set_cView(0, 1)
578        config.settings.set('GUI', 'label', 'miClientsViewUserHost')
579
580
581    def on_cViewH_toggled(self, mitem):
582        self.set_cView(-1, 0)
583        config.settings.set('GUI', 'label', 'miClientsViewHost')
584
585
586    def on_cViewU_toggled(self, mitem):
587        self.set_cView(0, -1)
588        config.settings.set('GUI', 'label', 'miClientsViewUser')
589
590
591    def set_cView(self, user_pos= -1, name_pos=0):
592        # Save the order so all new clients get the selected format
593        self.cView_order = (user_pos, name_pos)
594        for row in self.cstore:           
595            self.setLabel(row)
596
597
598    def connected(self, daemon):
599        self.daemon = daemon
600        daemon.enumerateClients().addCallback(lambda h: self.amp_gotClients(h))
601
602
603    # AMP callbacks
604    def amp_clientConnected(self, handle):
605        print "New connection from", handle
606        d = self.daemon.command(handle, u'info')
607        d.addCallback(lambda r: self.addClient(handle, r))
608        d.addErrback(lambda err: self.printErrors("when connecting client %s: %s" %(handle, err)))
609
610
611    def amp_clientDisconnected(self, handle):
612        print "Disconnect from", handle
613       
614        def determine_offline(client):
615            if client.hsystem == '' and client.users == {}:
616                client.set_offline()
617        client = None
618        for client in structs.clients:
619            if client.hsystem == handle:
620                if self.getSelectedGroup()[1].has_client(client) or self.isDefaultGroupSelected():
621                    shutdownNotify(client.get_name())
622                client.hsystem = ''
623                determine_offline(client)
624                break
625           
626            elif handle in client.users:
627                if self.getSelectedGroup()[1].has_client(client) or self.isDefaultGroupSelected():
628                    logoutNotify(client.users[handle]['uname'], client.get_name())
629                del client.users[handle]
630                determine_offline(client)
631                break
632            else:
633                client = None
634
635        if not client is None:
636            for row in self.cstore:
637                if row[C_INSTANCE] is client: 
638                    self.fillIconView(self.getSelectedGroup()[1], True)
639                    break
640
641
642    def amp_gotClients(self, handles):
643        print "Got clients:", ', '.join(handles) or 'None'
644        for handle in handles:
645            d = self.daemon.command(handle, u'info')
646            d.addCallback(lambda r, h=handle: self.addClient(h, r, True))
647            d.addErrback(lambda err: self.printErrors("when enumerating client %s: %s" %(handle, err)))
648
649
650    def on_button_close_clicked(self, widget):
651        self.get('warningDialog').hide()
652
653
654    def on_group_selection_changed(self, treeselection):
655        self.cstore.clear()
656        selected = self.getSelectedGroup()
657               
658        if selected is not None:
659            self.fillIconView(selected[1])
660        else:
661            if not self.default_group.ref.valid():
662                return
663            self.gtree.get_selection().select_path(self.default_group.ref.get_path())
664        self.get('remove_group').set_sensitive(not self.isDefaultGroupSelected())
665        self.set_move_group_sensitivity()
666
667
668    def addToIconView(self, client):
669        """Properly add a Client class instance to the clients iconview."""
670        # If there are one or more users on client, add a new iconview entry
671        # for each one of them.
672       
673        label = 'uname'
674        if self.showRealNames:
675                label = 'rname'
676        if client.users:
677            for hsession, user in client.users.iteritems():
678                self.cstore.append([self.calculateLabel(client, user[label]), self.imagetypes[client.type], client, hsession])
679                self.askScreenshot(hsession, True)
680        else:
681            self.cstore.append([self.calculateLabel(client), self.imagetypes[client.type], client, ''])
682
683
684    def fillIconView(self, group, keep_selection=False):
685        """Fill the clients iconview from a Group class instance."""
686        if keep_selection:
687            selection = [row[C_INSTANCE] for row in self.getSelectedClients()]
688        self.cstore.clear()
689        if self.isDefaultGroupSelected():
690            clients_list = [client for client in structs.clients if client.type != 'offline']
691        else:
692            clients_list = group.get_members()
693        # Add the new clients to the iconview
694        for client in clients_list:
695            self.addToIconView(client)
696        if keep_selection:
697            for row in self.cstore:
698                if row[C_INSTANCE] in selection:
699                    self.cview.select_path(row.path)
700                    selection.remove(row[C_INSTANCE])
701
702
703    def isDefaultGroupSelected(self):
704        """Return True if the default group is selected"""
705        return self.getSelectedGroup()[1] is self.default_group
706
707
708    def getSelectedGroup(self):
709        """Return a 2-tuple containing the iter and the instance
710        for the currently selected group."""
711        iter = self.gtree.get_selection().get_selected()[1]
712        if iter:
713            return (iter, self.gstore[iter][G_INSTANCE])
714        else:
715            return None
716
717
718    def addClient(self, handle, r, already=False):
719        # already is True if the client was started before epoptes
720        print "---\n**addClient's been called for", handle
721        try:
722            info = {}
723            for line in r.strip().split('\n'):
724                key, value = line.split('=', 1)
725                info[key.strip()] = value.strip()
726            user, host, ip, mac, type, uid, version, name = \
727                info['user'], info['hostname'], info['ip'], info['mac'], \
728                info['type'], int(info['uid']), info['version'], info['name']
729        except:
730            print "Can't extract client information, won't add this client"
731            return
732       
733        # Check if the incoming client is the same with the computer in which
734        # epoptes is running, so we don't add it to the list.
735        # FIXME FiXME: Both ifs don't work for root clients that run in the same
736        # computer as epoptes
737        if (mac in self.current_macs) and (uid == self.uid):
738            print "* Won't add this client to my lists"
739            return False
740       
741        print '  Continuing inside addClient...'
742       
743        # Compatibility check
744        if [int(x) for x in re.split('[^0-9]*', version)] < COMPATIBILITY_VERSION:
745            if not self.shownCompatibilityWarning:
746                self.shownCompatibilityWarning = True
747                dlg = self.get('warningDialog')
748                # Show different messages for LTSP clients and standalones.
749                msg = _("A connection attempt was made by a client with version %s, \
750which is incompatible with the current epoptes version.\
751\n\nYou need to update your clients to the latest epoptes-client version.") % version
752                dlg.set_property('text', msg)
753                dlg.set_transient_for(self.mainwin)
754                dlg.show()
755            self.daemon.command(handle, u"exit")
756            return False
757        sel_group = self.getSelectedGroup()[1]
758        client = None
759        for inst in structs.clients:
760            # Find if the new handle is a known client
761            if mac == inst.mac:
762                client = inst
763                print '* This is an existing client'
764                break
765        if client is None:
766            print '* This client is a new one, creating an instance'
767            client = structs.Client(mac=mac)
768           
769        # Update/fill the client information
770        client.type, client.hostname = type, host
771        if uid == 0:
772            # This is a root epoptes-client
773            print '* I am a root client'
774            client.hsystem = handle
775        else:
776            # This is a user epoptes-client
777            print '* I am a user client, will add', user, 'in my list'
778            client.add_user(user, name, handle)
779            if not already and (sel_group.has_client(client) or self.isDefaultGroupSelected()):
780                loginNotify(user, host)
781       
782        if sel_group.has_client(client) or self.isDefaultGroupSelected():
783            self.fillIconView(sel_group, True)
784
785
786    def setLabel(self, row):
787        inst = row[C_INSTANCE]
788        if row[C_SESSION_HANDLE]:           
789            label = 'uname'
790            if self.showRealNames:
791                label = 'rname'
792            user = row[C_INSTANCE].users[row[C_SESSION_HANDLE]][label]
793        else:
794            user = ''
795        row[C_LABEL] = self.calculateLabel(inst, user)
796
797
798    def calculateLabel(self, client, username=''):
799        """Return the iconview label from a hostname/alias
800        and a username, according to the user options.
801        """
802        user_pos, name_pos = self.cView_order
803       
804        alias = client.get_name()
805        if username == '' or user_pos == -1:
806            return alias
807        else:
808            if user_pos == 0:
809                label = username
810                if name_pos == 1:
811                    label += " (%s)" % alias
812            elif name_pos == 0:
813                label = alias
814                if user_pos == 1:
815                    label += " (%s)" % username
816            return label
817
818
819    def getShowRealNames(self):
820        return self.get('')
821   
822   
823   
824   
825    def screenshotTimeout(self, handle):
826        print "Screenshot for client %s timed out. Requesting a new one..." % handle
827        self.askScreenshot(handle)
828        return False
829
830
831    def askScreenshot(self, handle, firstTime=False):
832        # Should always return False to prevent glib from calling us again
833        if firstTime:
834            if not handle in self.currentScreenshots:
835                # Mark that we started asking for screenshots, but didn't yet get one
836                self.currentScreenshots[handle] = None
837            else:
838                # We're already asking the client for screenshots, reuse the existing one
839                if not self.currentScreenshots[handle] is None:
840                    for i in self.cstore:
841                        if handle == i[C_SESSION_HANDLE]:
842                            self.cstore[i.path][C_PIXBUF] = self.currentScreenshots[handle]
843                            break
844                return False
845        # TODO: Implement this using gtk.TreeRowReferences instead
846        # of searching the whole model (Need to modify execOnClients)
847        for client in self.cstore:
848            if handle == client[C_SESSION_HANDLE]:
849                timeoutID = gobject.timeout_add(10000, lambda h=handle: self.screenshotTimeout(h))
850                self.execOnClients(['screenshot', self.scrWidth, self.scrHeight], handles=[handle], 
851                                   reply=self.gotScreenshot, params=[timeoutID])
852                return False
853        # That handle is no longer in the cstore, remove it
854        try: del self.currentScreenshots[handle]
855        except: pass
856        return False
857
858
859    def gotScreenshot(self, handle, reply, timeoutID):
860        # Cancel the timeout event. If it already happened, exit.
861        if not gobject.source_remove(timeoutID):
862            return
863        for i in self.cstore:
864            if handle == i[C_SESSION_HANDLE]:
865                # We want to ask for thumbnails every 5 sec after the last one.
866                # So if the client is too stressed and needs 7 secs to
867                # send a thumbnail, we'll ask for one every 12 secs.
868                gobject.timeout_add(5000, self.askScreenshot, handle)
869#                print "I got a screenshot from %s." % handle
870                if not reply:
871                    return
872                try:
873                    rowstride, size, pixels = reply.split('\n', 2)
874                except:
875                    return
876                rowstride = int(rowstride)
877                width, height = size.split('x')
878                pxb = gtk.gdk.pixbuf_new_from_data(pixels,
879                    gtk.gdk.COLORSPACE_RGB, False, 8, int(width), int(height),
880                    rowstride)
881                self.currentScreenshots[handle] = pxb
882                self.cstore[i.path][C_PIXBUF] = pxb
883                return
884        # That handle is no longer in the cstore, remove it
885        try: del self.currentScreenshots[handle]
886        except: pass
887
888
889    def getSelectedClients(self):
890        selected = self.cview.get_selected_items()
891        items = []
892        for i in selected:
893            items.append(self.cstore[i])
894        return items
895
896
897    def changeHostname(self, mac, new_name):
898        pass #FIXME: Implement this (virtual hostname)
899
900
901    def openLink(self, link):
902        subprocess.Popen(["xdg-open", link])
903
904
905    def openHelpLink(self, widget):
906        self.openLink("http://www.epoptes.org")
907
908
909    def openBugsLink(self, widget):
910        self.openLink("https://bugs.launchpad.net/epoptes")
911
912
913    def openQuestionsLink(self, widget):
914        self.openLink("https://answers.launchpad.net/epoptes")
915
916
917    def openTranslationsLink(self, widget):
918        self.openLink("http://www.epoptes.org/translations")
919
920
921    def openIRCLink(self, widget):
922        user = os.getenv("USER")
923        if user is None:
924            user = "epoptes_user." # The dot is converted to a random digit
925        self.openLink("http://webchat.freenode.net/?nick=" + user + 
926            "&channels=ltsp&prompt=1")
927   
928   
929    def iconsSizeScale_button_event(self, widget, event):
930        """A hack to make the left click work like middle click
931        in the scale (for jump-to functionality). Right click resets
932        the thumbnail size.
933        """
934        if event.button == 1:
935            event.button = 2
936        elif event.button == 3:
937            self.iconsSizeScaleChanged(None, 120) # Reset the thumbnail size
938        return False
939   
940   
941    def reload_imagetypes(self):
942        """Improve the quality of previously resized svg icons,
943        by reloading them.
944        """
945        old_pixbufs = self.imagetypes.values()
946        loadSVG = lambda path: gtk.gdk.pixbuf_new_from_file_at_size(path, 
947                                                  self.scrWidth, self.scrHeight)
948        self.imagetypes = {
949            'offline' : loadSVG('images/offline.svg'),
950            'thin' : loadSVG('images/thin.svg'),
951            'fat' : loadSVG('images/fat.svg'),
952            'standalone' : loadSVG('images/standalone.svg')
953        }
954       
955        rows = [row for row in self.cstore if row[C_PIXBUF] in old_pixbufs]
956        for row in rows:
957            row[C_PIXBUF] = self.imagetypes[row[C_INSTANCE].type]
958   
959   
960    def on_iconsSizeScale_button_release_event(self, widget=None, event=None):
961        # Here we want to resize the SVG icons from imagetypes at a better
962        # quality than this of the quick pixbuf scale, since we assume that
963        # the user has decided the desired zoom level.
964        self.reload_imagetypes()
965   
966   
967    def iconsSizeScaleChanged(self, widget=None, width=None):
968        adj = self.get('iconsSizeAdjustment')
969        if width:
970            adj.set_value(width)
971        else:
972            width = adj.get_value()
973        self.scrWidth = int(width)
974        self.scrHeight = int(3 * self.scrWidth / 4) # Κeep the 4:3 aspect ratio
975       
976        # Fast scale all the thumbnails to make the change quickly visible
977        old_pixbufs = self.imagetypes.values()
978        for row in self.cstore:
979            if row[C_PIXBUF] in old_pixbufs:
980                ctype = row[C_INSTANCE].type
981                cur_w = self.imagetypes[ctype].get_width()
982                cur_h = self.imagetypes[ctype].get_height()
983                if not (cur_w == self.scrWidth and cur_h == self.scrHeight):
984                    new_thumb = row[C_PIXBUF].scale_simple(self.scrWidth, self.scrHeight, gtk.gdk.INTERP_NEAREST)
985                    self.imagetypes[ctype] = new_thumb
986                row[C_PIXBUF] = self.imagetypes[ctype]
987            else:
988                new_thumb = row[C_PIXBUF].scale_simple(self.scrWidth, self.scrHeight, gtk.gdk.INTERP_NEAREST)
989                row[C_PIXBUF] = new_thumb
990       
991        # Hack to remove the extra padding that remains after a 'zoom out'
992        self.cview.set_resize_mode(gtk.RESIZE_IMMEDIATE)
993        self.cview.get_cells()[1].set_fixed_size(-1, -1)
994        self.cview.check_resize()
995   
996   
997    def scrIncreaseSize(self, widget):
998        # Increase the size of screenshots by 2 pixels in width
999        adj = self.get('iconsSizeAdjustment')
1000        adj.set_value(adj.get_value() + 15)
1001        self.reload_imagetypes()
1002
1003
1004    def scrDecreaseSize(self, widget):
1005        # Decrease the size of screenshots by 2 pixels in width
1006        adj = self.get('iconsSizeAdjustment')
1007        adj.set_value(adj.get_value() - 15)
1008        self.reload_imagetypes()
1009   
1010   
1011    def contextMenuPopup(self, widget, event):
1012        clicked = widget.get_path_at_pos(int(event.x), int(event.y))
1013
1014        if event.button == 3:
1015            if widget is self.cview:
1016                selection = widget
1017                selected = widget.get_selected_items()
1018            else:
1019                selection = widget.get_selection()
1020                selected = selection.get_selected_rows()[1]
1021                if clicked:
1022                    clicked = clicked[0]
1023
1024            if clicked:
1025                if not clicked in selected:
1026                    selection.unselect_all()
1027                    selection.select_path(clicked)
1028            else:
1029                selection.unselect_all()
1030
1031            if widget is self.cview:
1032                menu = self.get('clients').get_submenu()
1033           
1034            menu.popup(None, None, None, event.button, event.time)
1035            menu.show()
1036            return True
1037
1038
1039    def on_clients_selection_changed(self, widget=None):
1040        selected = self.getSelectedClients()
1041        sensitive = False
1042        if len(selected) == 1:
1043            sensitive = True
1044        self.get('miClientProperties').set_sensitive(sensitive)
1045        self.get('tb_client_properties').set_sensitive(sensitive)
1046       
1047        if len(selected) > 0 and not self.isDefaultGroupSelected():
1048            self.get('miRemoveFromGroup').set_sensitive(True)
1049        else:
1050            self.get('miRemoveFromGroup').set_sensitive(False)
1051       
1052        if len(selected) > 1:
1053            self.get('statusbar_label').set_text(_('%d clients selected' % len(selected)))
1054        else:
1055            self.get('statusbar_label').set_text('')
1056
1057
1058    def execOnClients(self, command, clients=[], reply=None,
1059            mode=EM_SESSION_OR_SYSTEM, handles=[], warning='', params=None):
1060        '''reply should be a method in which the result will be sent'''
1061        if params is None:
1062            params = []
1063        if len(self.cstore) == 0:
1064            # print 'No clients'
1065            return False
1066        if isinstance(command, list) and len(command) > 0:
1067            command = '%s %s' %(command[0], ' '.join([pipes.quote(str(x)) for x in command[1:]]))
1068        if (clients != [] or handles != []) and warning != '':
1069            if self.warnDlgPopup(warning) == False:
1070                return
1071        if clients == [] and handles != []:
1072            for handle in handles:
1073                cmd = self.daemon.command(handle, unicode(command))
1074                if reply:
1075                    cmd.addCallback(lambda re, h=handle, p=params: reply(h, re, *p))
1076                    cmd.addErrback(lambda err: self.printErrors("when executing command %s on client %s: %s" %(command,handle, err)))
1077        for client in clients:
1078            sent = False
1079            for em in mode:
1080                if em == EM_SESSION_ONLY:
1081                    handle = client[C_SESSION_HANDLE]
1082                elif em == EM_SYSTEM_ONLY:
1083                    try:
1084                       handle = client[C_INSTANCE].hsystem
1085                       print(handle)
1086                    except Exception as e:
1087                       print(str(e))
1088                else: # em == EM_EXIT_IF_SENT
1089                    if sent:
1090                        break
1091                    else:
1092                        continue
1093                if handle == '':
1094                    continue
1095                sent = True
1096                cmd = self.daemon.command(handle, unicode(command))
1097                if reply:
1098                    cmd.addCallback(lambda re, h=handle, p=params: reply(h, re, *p))
1099                    cmd.addErrback(lambda err: self.printErrors("when executing command %s on client %s: %s" %(command,handle, err)))
1100
1101
1102    def execOnSelectedClients(self, command, reply=None,
1103            mode=EM_SESSION_OR_SYSTEM, warn=''):
1104        clients = self.getSelectedClients()
1105        if len(clients) == 0: # No client selected, send the command to all
1106            clients = self.cstore
1107        else: # Show the warning only when no clients are selected
1108            warn = ''
1109        self.execOnClients(command, clients, reply, mode, warning=warn)
1110
1111
1112    def warnDlgPopup(self, warning):
1113        dlg = self.get('executionwarning')
1114        dlg.set_property('text', warning)
1115        resp = dlg.run()
1116        dlg.set_property('text', '')
1117        dlg.hide()
1118        # -8 is the response id of the "Yes" button (-9 for the "No")
1119        if resp == -8:
1120            return True
1121        else:
1122            return False
1123
1124
1125    def printErrors(self, error):
1126        print '  **Twisted error:', error
1127        return
1128
1129    def getClassroomIp(self):
1130        '''
1131        Adding some workarount to solve the "teacher-non-in-server problem"
1132        getting the ip from current machine and use it in xvnc
1133        ''' 
1134        class_ip = None
1135        try:
1136            import socket
1137            s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1138            s.connect(("server",80))
1139            class_ip = s.getsockname()[0]
1140            s.close()
1141            if "127.0.0.1" == class_ip:
1142                return None
1143        except Exception as e:
1144            print(str(e))
1145        return class_ip
Note: See TracBrowser for help on using the repository browser.