Source

thg / hggtk / serve.py

# serve.py - TortoiseHg dialog to start web server
#
# Copyright 2007 Steve Borho <steve@borho.org>
# Copyright 2007 TK Soh <teekaysoh@gmail.com>
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2, incorporated herein by reference.

import gtk
import gobject
import httplib
import os
import pango
import Queue
import socket
import sys
import threading
import time

from mercurial import hg, ui, commands, cmdutil, util
from mercurial.hgweb import server

from thgutil.i18n import _
from thgutil import hglib, paths

from hggtk import dialog, gdialog, gtklib, thgconfig

gservice = None
class ServeDialog(gtk.Window):
    """ Dialog to run web server"""
    def __init__(self, webdir_conf):
        """ Initialize the Dialog """
        gtk.Window.__init__(self, gtk.WINDOW_TOPLEVEL)
        gtklib.set_tortoise_icon(self, 'proxy.ico')
        gtklib.set_tortoise_keys(self)

        self.connect('delete-event', self._delete)

        # Pipe stderr, stdout to self.write
        self._queue = Queue.Queue()
        sys.stdout = self
        sys.stderr = self

        # Override mercurial.commands.serve() with our own version
        # that supports being stopped
        commands.table.update(thg_serve_cmd)

        self._url = None
        self._root = paths.find_root()
        self._webdirconf = webdir_conf
        self._get_config()
        self.set_default_size(500, 300)

        # toolbar
        self.tbar = gtk.Toolbar()
        self.tooltips = gtk.Tooltips()
        self._button_start = self._toolbutton(gtk.STOCK_MEDIA_PLAY,
                                              _('Start'),
                                              self._on_start_clicked,
                                              tip=_('Start server'))
        self._button_stop  = self._toolbutton(gtk.STOCK_MEDIA_STOP,
                                              _('Stop'),
                                              self._on_stop_clicked,
                                              tip=_('Stop server'))
        self._button_browse = self._toolbutton(gtk.STOCK_HOME,
                                              _('Browse'),
                                              self._on_browse_clicked,
                                              tip=_('Launch browser to view repository'))
        self._button_conf = self._toolbutton(gtk.STOCK_PREFERENCES,
                                              _('Configure'),
                                              self._on_conf_clicked,
                                              tip=_('Configure web settings'))

        tbuttons = [
                self._button_start,
                self._button_stop,
                gtk.SeparatorToolItem(),
                self._button_browse,
                gtk.SeparatorToolItem(),
                self._button_conf,
            ]
        for btn in tbuttons:
            self.tbar.insert(btn, -1)

        vbox = gtk.VBox()
        self.add(vbox)
        vbox.pack_start(self.tbar, False, False, 2)

        # revision input
        revbox = gtk.HBox()
        lbl = gtk.Label(_('HTTP Port:'))
        lbl.set_property('width-chars', 16)
        lbl.set_alignment(0, 0.5)
        self._port_input = gtk.Entry()
        self._port_input.set_text(self.defport)
        revbox.pack_start(lbl, False, False)
        revbox.pack_start(self._port_input, False, False)
        vbox.pack_start(revbox, False, False, 2)

        scrolledwindow = gtk.ScrolledWindow()
        scrolledwindow.set_shadow_type(gtk.SHADOW_ETCHED_IN)
        scrolledwindow.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
        self.textview = gtk.TextView(buffer=None)
        self.textview.set_editable(False)
        self.textview.modify_font(pango.FontDescription('Monospace'))
        scrolledwindow.add(self.textview)
        self.textbuffer = self.textview.get_buffer()
        vbox.pack_start(scrolledwindow, True, True)
        self._set_button_states()

    def _get_config(self):
        try:
            repo = hg.repository(ui.ui(), path=self._root)
        except hglib.RepoError:
            self.destroy()
        self.defport = repo.ui.config('web', 'port') or '8000'
        self.webname = repo.ui.config('web', 'name') or \
                os.path.basename(self._root)
        if self._webdirconf:
            self.set_title(_('Serve %s - %s') %
                    (hglib.toutf(self._webdirconf), hglib.toutf(self.webname)))
        else:
            self.set_title(_('Serve - ') + hglib.toutf(self.webname))

    def _toolbutton(self, stock, label, handler, menu=None,
            userdata=None, tip=None):
        if menu:
            tbutton = gtk.MenuToolButton(stock)
            tbutton.set_menu(menu)
        else:
            tbutton = gtk.ToolButton(stock)

        if tip:
            tbutton.set_tooltip(self.tooltips, tip)

        tbutton.set_label(label)
        tbutton.connect('clicked', handler, userdata)
        return tbutton

    def _delete(self, widget, event):
        if not self.should_live():
            self.destroy()
        else:
            return True

    def should_live(self):
        return not self._server_stopped()

    def _server_stopped(self):
        '''
        check if server is running, or to terminate if running
        '''
        if gservice and not gservice.stopped:
            ret = gdialog.Confirm(_('Confirm Really Exit?'), [], self,
                    _('Server process is still running\n'
                      'Exiting will stop the server.')).run()
            if ret == gtk.RESPONSE_YES:
                self._stop_server()
                return True
            else:
                return False
        else:
            return True

    def _set_button_states(self):
        if gservice and not gservice.stopped:
            self._button_start.set_sensitive(False)
            self._button_stop.set_sensitive(True)
            self._button_browse.set_sensitive(True)
            self._button_conf.set_sensitive(False)
        else:
            self._button_start.set_sensitive(True)
            self._button_stop.set_sensitive(False)
            self._button_browse.set_sensitive(False)
            self._button_conf.set_sensitive(True)

    def _on_start_clicked(self, *args):
        self._start_server()
        self._set_button_states()

    def _on_stop_clicked(self, *args):
        self._stop_server()

    def _on_browse_clicked(self, *args):
        ''' launch default browser to view repo '''
        if self._url:
            def start_browser():
                if os.name == 'nt':
                    try:
                        import win32api, win32con
                        win32api.ShellExecute(0, "open", self._url, None, "",
                            win32con.SW_SHOW)
                    except:
                        # Firefox likes to create exceptions at launch,
                        # the user doesn't need to be bothered by them
                        pass
                else:
                    import gconf
                    client = gconf.client_get_default()
                    browser = client.get_string(
                            '/desktop/gnome/url-handlers/http/command') + '&'
                    os.system(browser % self._url)
            threading.Thread(target=start_browser).start()

    def _on_conf_clicked(self, *args):
        dlg = thgconfig.ConfigDialog(True)
        dlg.show_all()
        dlg.focus_field('web.name')
        dlg.run()
        dlg.hide()
        self._get_config()

    def _start_server(self):
        def threadfunc(path, q, *args):
            try:
                hglib.hgcmd_toq(path, q, *args)
            except util.Abort, e:
                self._write(_('Abort: %s\n') % str(e))

        # gather input data
        try:
            port = int(self._port_input.get_text())
        except:
            try: port = int(self.defport)
            except: port = 8000
            dialog.error_dialog(self, _('Invalid port 2048..65535'),
                    _('Defaulting to ') + self.defport)

        global gservice
        gservice = None

        args = [self._root, self._queue, 'serve', '--port', str(port)]
        if self._webdirconf:
            args.append('--webdir-conf=' + self._webdirconf)
        else:
            args.append('--name')
            args.append(self.webname)

        thread = threading.Thread(target=threadfunc, args=args)
        thread.start()

        while True:
            time.sleep(0.1)
            if gservice and hasattr(gservice, 'httpd'):
                break
            if not thread.isAlive():
                return

        # gservice.httpd.fqaddr turned out to be unreliable, so use
        # loopback addr directly
        self._url = 'http://127.0.0.1:%d/' % (port)
        gobject.timeout_add(10, self.process_queue)

    def _stop_server(self):
        if gservice and not gservice.stopped:
            gservice.stop()

    def flush(self, *args):
        pass

    def write(self, msg):
        self._queue.put(msg)

    def _write(self, msg, append=True):
        msg = hglib.toutf(msg)
        if append:
            enditer = self.textbuffer.get_end_iter()
            self.textbuffer.insert(enditer, msg)
        else:
            self.textbuffer.set_text(msg)

    def process_queue(self):
        """
        Handle all the messages currently in the queue (if any).
        """
        while self._queue.qsize():
            try:
                msg = self._queue.get(0)
                self._write(msg)
            except Queue.Empty:
                pass

        if gservice and gservice.stopped:
            self._set_button_states()
            return False # Stop polling this function
        else:
            return True

def thg_serve(ui, repo, **opts):
    class service:
        def init(self):
            self.stopped = True
            util.set_signal_handler()
            try:
                baseui = (getattr(repo, 'baseui', None) or
                          getattr(ui, 'parentui', None) or ui)
                repoui = repo and repo.ui != baseui and repo.ui or None
                optlist = ("name templates style address port prefix ipv6"
                           " accesslog errorlog webdir_conf certificate")
                for o in optlist.split():
                    if opts[o]:
                        baseui.setconfig("web", o, str(opts[o]))
                        if repoui:
                            repoui.setconfig("web", o, str(opts[o]))
                self.httpd = server.create_server(ui, repo)
            except socket.error, inst:
                raise util.Abort(_('cannot start server: ') + inst.args[1])

            if self.httpd.prefix:
                prefix = self.httpd.prefix.strip('/') + '/'
            else:
                prefix = ''

            port = ':%d' % self.httpd.port
            if port == ':80':
                port = ''

            ui.status(_('listening at http://%s%s/%s (%s:%d)\n') %
                      (self.httpd.fqaddr, port, prefix, self.httpd.addr, self.httpd.port))

        def stop(self):
            self.stopped = True
            # issue request to trigger handle_request() and quit
            addr = '%s:%d' % (self.httpd.fqaddr, self.httpd.port)
            conn = httplib.HTTPConnection(addr)
            conn.request("GET", "/")
            res = conn.getresponse()
            res.read()
            conn.close()

        def run(self):
            self.stopped = False
            while not self.stopped:
                self.httpd.handle_request()
            self.httpd.server_close() # release port

    global gservice
    gservice = service()
    cmdutil.service(opts, initfn=gservice.init, runfn=gservice.run)

thg_serve_cmd =  {"^serve":
        (thg_serve,
         [('A', 'accesslog', '', _('name of access log file to write to')),
          ('d', 'daemon', None, _('run server in background')),
          ('', 'daemon-pipefds', '', _('used internally by daemon mode')),
          ('E', 'errorlog', '', _('name of error log file to write to')),
          ('p', 'port', 0, _('port to use (default: 8000)')),
          ('a', 'address', '', _('address to use')),
          ('', 'prefix', '', _('prefix path to serve from (default: server root)')),
          ('n', 'name', '',
           _('name to show in web pages (default: working dir)')),
          ('', 'webdir-conf', '', _('name of the webdir config file'
                                    ' (serve more than one repo)')),
          ('', 'pid-file', '', _('name of file to write process ID to')),
          ('', 'stdio', None, _('for remote clients')),
          ('t', 'templates', '', _('web templates to use')),
          ('', 'style', '', _('template style to use')),
          ('6', 'ipv6', None, _('use IPv6 in addition to IPv4')),
          ('', 'certificate', '', _('SSL certificate file'))],
         _('hg serve [OPTION]...'))}

def run(ui, *pats, **opts):
    return ServeDialog(opts.get('webdir_conf'))