diff options
Diffstat (limited to 'terminal.py')
-rw-r--r-- | terminal.py | 393 |
1 files changed, 393 insertions, 0 deletions
diff --git a/terminal.py b/terminal.py new file mode 100644 index 0000000..2e15c1e --- /dev/null +++ b/terminal.py @@ -0,0 +1,393 @@ +# Copyright (C) 2007, Eduardo Silva <edsiper@gmail.com>. +# Copyright (C) 2008, One Laptop Per Child +# Copyright (C) 2009, Ben Schwartz <bens@alum.mit.edu> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import os + +import logging +from gettext import gettext as _ + +import gtk +import dbus + +from sugar.activity import activity +from sugar import env +from sugar.graphics.toolbutton import ToolButton +from sugar.graphics.palette import Palette +import ConfigParser +import os.path + +import vte +import pango + +import telepathy +import shutil +import subprocess +import stat + +import random +import re +import signal + +SERVICE = 'org.sugarlabs.ShareTerm' +IFACE = SERVICE +PATH = '/org/sugarlabs/ShareTerm' + +class SharedTerminalActivity(activity.Activity): + + def __init__(self, handle): + activity.Activity.__init__(self, handle) + + self._logger = logging.getLogger('shareterm-activity') + self._logger.debug('Starting the ShareTerm activity') + + self.set_title(_('ShareTerm Activity')) + self.connect('key-press-event', self.__key_press_cb) + + toolbox = activity.ActivityToolbox(self) + + self._edit_toolbar = activity.EditToolbar() + toolbox.add_toolbar(_('Edit'), self._edit_toolbar) + self._edit_toolbar.show() + self._edit_toolbar.undo.props.visible = False + self._edit_toolbar.redo.props.visible = False + self._edit_toolbar.separator.props.visible = False + self._edit_toolbar.copy.connect('clicked', self._copy_cb) + self._edit_toolbar.paste.connect('clicked', self._paste_cb) + + activity_toolbar = toolbox.get_activity_toolbar() + activity_toolbar.keep.props.visible = False + + self.set_toolbox(toolbox) + toolbox.show() + + box = gtk.HBox(False, 4) + + self._vte = VTE() + self._vte.set_scroll_on_keystroke(True) + self._vte.connect("child-exited", lambda term: self.close()) + + self._screenpid = None + self._sshdaemon = None + self._otherscreenpid = None + + if not self._shared_activity: #I am the initiator + suffix = "ShareTerm%d" % random.getrandbits(32) + os.putenv('SCREENDIR',os.path.join(os.getenv('HOME'),'.screen')) + + #I tried to use envv with fork_command here, but it causes the + #command to die without explanation. It seems to work without it, though. + self._screenpid = self._vte.fork_command(command='screen', + argv=['screen','-S',suffix, + '-c',os.path.join(activity.get_bundle_path(),'screenrc')]) + + #Don't show the screen until Screen is active + self._vte.show() + + #self._screenname = "%d.%s" % (self._screenpid, suffix) + #The above line would be the logical way to determine the screenname. + #Unfortunately, screen seems to fork before determining the pid to use here, + #so the name computed this way has the wrong pid. Therefore, we must instead + #search the list of existing screens for one matching our unique random + #suffix. + #Note that there will only be more than one screen for the current user + #if Rainbow is not running. + + L = subprocess.Popen(['screen','-list'],stdout=subprocess.PIPE) + r = re.compile(r"\s*((\d+)\.%s)" % suffix) + for line in L.stdout: + m = r.match(line) + if m is not None: + self._screenname = m.group(1) + self._otherscreenpid = int(m.group(2)) + + self._connected = True + self.connect('shared', self._shared_cb) + else: # I am joining + self._connected = False + if self.get_shared(): #Already joined for some reason + self._joined_cb() + else: + self.connect('joined', self._joined_cb) + + scrollbar = gtk.VScrollbar(self._vte.get_adjustment()) + scrollbar.show() + + box.pack_start(self._vte) + box.pack_start(scrollbar, False, False, 0) + + self.set_canvas(box) + box.show() + + self._vte.grab_focus() + + def _copy_cb(self, button): + if self._vte.get_has_selection(): + self._vte.copy_clipboard() + + def _paste_cb(self, button): + self._vte.paste_clipboard() + + def __key_press_cb(self, window, event): + if event.state & gtk.gdk.CONTROL_MASK and event.state & gtk.gdk.SHIFT_MASK: + + if gtk.gdk.keyval_name(event.keyval) == "C": + if self._vte.get_has_selection(): + self._vte.copy_clipboard() + return True + elif gtk.gdk.keyval_name(event.keyval) == "V": + self._vte.paste_clipboard() + return True + + return False + + def _sharing_setup(self): + # This section creates the authorized_keys file, containing the public key in + # question. This code must agree with sshd_config's AuthorizedKeysFile. + home = os.getenv('HOME') + self._sshdir = os.path.join(home,'.ShareTerm') + os.mkdir(self._sshdir) + os.chmod(self._sshdir,stat.S_IRWXU) + bundle_path = activity.get_bundle_path() + authkeys = os.path.join(self._sshdir,'authorized_keys') + shutil.copyfile(os.path.join(bundle_path,'userkey.pub'), authkeys) + os.chmod(authkeys,stat.S_IRUSR|stat.S_IWUSR) + + + params = {} + params['screenname'] = self._screenname + params['username'] = os.getenv('USER') + + port = 5000 + listening = False + while not listening: + #FIXME: assumes sshd is in /usr/bin/sshd. (Full path invocation is required + #by sshd when running as non-root.) + self._sshdaemon = subprocess.Popen(['/usr/sbin/sshd','-h',os.path.join(bundle_path,'hostkey'), + '-f',os.path.join(bundle_path,'sshd_config'), + '-p',str(port),'-D','-e'], stderr=subprocess.PIPE) + x = self._sshdaemon.stderr.readline() + #FIXME: The following is an ugly hack, in attempt to determine + #whether sshd successfully launched by observing its logging output. + # It would be better if sshd used a return code to indicate whether it + # had successfully daemonized or failed, but this does not seem to be + # the case. + success = 'Server listening' + if x[:len(success)] == success: + listening = True + else: #failure; the daemon will stop itself + self._sshdaemon.wait() + port += 1 + if port == 65536: + break #FIXME: What should happen here? + self._logger.debug('started sshd on port %d' % port) + + return (params, port) + + def _shared_cb(self, activity): + self._logger.debug('My activity was shared') + self.initiating = True + (params, port) = self._sharing_setup() + + self._logger.debug('This is my activity: making a tube...') + + address = ('127.0.0.1', dbus.UInt16(port)) + + tubes_chan = self._shared_activity.telepathy_tubes_chan + id = tubes_chan[telepathy.CHANNEL_TYPE_TUBES].OfferStreamTube( + SERVICE, params, telepathy.SOCKET_ADDRESS_TYPE_IPV4, address, + telepathy.SOCKET_ACCESS_CONTROL_LOCALHOST, 0) + + def _joined_cb(self, also_self): + tubes_chan = self._shared_activity.telepathy_tubes_chan + + tubes_chan[telepathy.CHANNEL_TYPE_TUBES].connect_to_signal('NewTube', + self._new_tube_cb) + tubes_chan[telepathy.CHANNEL_TYPE_TUBES].ListTubes( + reply_handler=self._list_tubes_reply_cb, + error_handler=self._list_tubes_error_cb) + + def _new_tube_cb(self, tube_id, initiator, tube_type, service, params, state): + self._logger.debug('New Tube') + if ((tube_type == telepathy.TUBE_TYPE_STREAM) and + (service == SERVICE) and (not self._connected)): + tubes_chan = self._shared_activity.telepathy_tubes_chan + iface = tubes_chan[telepathy.CHANNEL_TYPE_TUBES] + addr = iface.AcceptStreamTube(tube_id, + telepathy.SOCKET_ADDRESS_TYPE_IPV4, + telepathy.SOCKET_ACCESS_CONTROL_LOCALHOST, 0) + + port = int(addr[1]) + username = str(params['username']) + screenname = str(params['screenname']) + bundle_path = activity.get_bundle_path() + + #self._vte.fork_command('ssh', '-l %s -F %s -p %d -i %s localhost' + # % (username, + # os.path.join(bundle_path,'ssh_config'), + # port, + # os.path.join(bundle_path,'userkey'))) + # I would prefer to use fork_command, but it doesn't seem to work here + # and I don't know why. + self._vte.feed_child("ssh -l %s -F %s -p %d -i %s localhost\n" + % (username, + os.path.join(bundle_path,'ssh_config'), + port, + os.path.join(bundle_path,'userkey'))) + # "SCREENDIR=" is necessary here because otherwise screen attempts to write + # to /var/run/screen, which is not permitted by Rainbow + self._vte.feed_child("SCREENDIR=$HOME/.screen screen -x %s\n" % screenname) + self._connected = True + # Now that we are connected and screen is up, we can show the screen + self._vte.show() + + def _list_tubes_reply_cb(self, tubes): + for tube_info in tubes: + self._new_tube_cb(*tube_info) + + def _list_tubes_error_cb(self, e): + self._logger.error('ListTubes() failed: %s' % e) + + def can_close(self): + #self._sshdaemon.terminate() #This requires 2.6, so we can't use it yet. + try: + os.kill(self._sshdaemon.pid,signal.SIGTERM) + except: + pass + try: + os.kill(self._screenpid,signal.SIGTERM) + except: + pass + try: + os.kill(self._otherscreenpid,signal.SIGTERM) + except: + pass + # Removing sshdir is only necessary in the absence of Rainbow, but in that case + # it has the potential to break if multiple ShareTerm sessions + # are in use simultaneously, and sshdir is deleted while still in use. + # The FIXME would be to use a randomly generated per-session sshdir, if we + # really care. + #try: + # shutil.rmtree(self._sshdir,ignore_errors=True) + #except: + # pass + return True + + +class VTE(vte.Terminal): + def __init__(self): + vte.Terminal.__init__(self) + self._configure_vte() + + os.chdir(os.environ["HOME"]) + self.fork_command() + + def _configure_vte(self): + conf = ConfigParser.ConfigParser() + conf_file = os.path.join(env.get_profile_path(), 'terminalrc') + + if os.path.isfile(conf_file): + f = open(conf_file, 'r') + conf.readfp(f) + f.close() + else: + conf.add_section('terminal') + + if conf.has_option('terminal', 'font'): + font = conf.get('terminal', 'font') + else: + font = 'Monospace 8' + conf.set('terminal', 'font', font) + self.set_font(pango.FontDescription(font)) + + if conf.has_option('terminal', 'fg_color'): + fg_color = conf.get('terminal', 'fg_color') + else: + fg_color = '#000000' + conf.set('terminal', 'fg_color', fg_color) + if conf.has_option('terminal', 'bg_color'): + bg_color = conf.get('terminal', 'bg_color') + else: + bg_color = '#FFFFFF' + conf.set('terminal', 'bg_color', bg_color) + self.set_colors(gtk.gdk.color_parse (fg_color), + gtk.gdk.color_parse (bg_color), + []) + + if conf.has_option('terminal', 'cursor_blink'): + blink = conf.getboolean('terminal', 'cursor_blink') + else: + blink = False + conf.set('terminal', 'cursor_blink', blink) + + self.set_cursor_blinks(blink) + + if conf.has_option('terminal', 'bell'): + bell = conf.getboolean('terminal', 'bell') + else: + bell = False + conf.set('terminal', 'bell', bell) + self.set_audible_bell(bell) + + if conf.has_option('terminal', 'scrollback_lines'): + scrollback_lines = conf.getint('terminal', 'scrollback_lines') + else: + scrollback_lines = 1000 + conf.set('terminal', 'scrollback_lines', scrollback_lines) + + self.set_scrollback_lines(scrollback_lines) + self.set_allow_bold(True) + + if conf.has_option('terminal', 'scroll_on_keystroke'): + scroll_key = conf.getboolean('terminal', 'scroll_on_keystroke') + else: + scroll_key = False + conf.set('terminal', 'scroll_on_keystroke', scroll_key) + self.set_scroll_on_keystroke(scroll_key) + + if conf.has_option('terminal', 'scroll_on_output'): + scroll_output = conf.getboolean('terminal', 'scroll_on_output') + else: + scroll_output = False + conf.set('terminal', 'scroll_on_output', scroll_output) + self.set_scroll_on_output(scroll_output) + + if conf.has_option('terminal', 'emulation'): + emulation = conf.get('terminal', 'emulation') + else: + emulation = 'xterm' + conf.set('terminal', 'emulation', emulation) + self.set_emulation(emulation) + + if conf.has_option('terminal', 'visible_bell'): + visible_bell = conf.getboolean('terminal', 'visible_bell') + else: + visible_bell = False + conf.set('terminal', 'visible_bell', visible_bell) + self.set_visible_bell(visible_bell) + conf.write(open(conf_file, 'w')) + + def on_gconf_notification(self, client, cnxn_id, entry, what): + self.reconfigure_vte() + + def on_vte_button_press(self, term, event): + if event.button == 3: + self.do_popup(event) + return True + + def on_vte_popup_menu(self, term): + pass |