Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/terminal.py
diff options
context:
space:
mode:
Diffstat (limited to 'terminal.py')
-rw-r--r--terminal.py393
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