Source

thg / tortoisehg / hgqt / serve.py

# serve.py - TortoiseHg dialog to start web server
#
# Copyright 2010 Yuya Nishihara <yuya@tcha.org>
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2, incorporated herein by reference.

import sys, os, httplib, socket, tempfile
from PyQt4.QtCore import *
from PyQt4.QtGui import *
from mercurial import extensions, hgweb, util, error, subrepo
from mercurial.hgweb import server  # workaround for demandimport
from tortoisehg.util import paths, wconfig, hglib
from tortoisehg.hgqt import cmdui, qtlib, thgrepo
from tortoisehg.hgqt.i18n import _
from tortoisehg.hgqt.serve_ui import Ui_ServeDialog
from tortoisehg.hgqt.webconf import WebconfForm

class ServeDialog(QDialog):
    """Dialog for serving repositories via web"""
    def __init__(self, webconf, parent=None):
        super(ServeDialog, self).__init__(parent)
        self.setWindowFlags((self.windowFlags() | Qt.WindowMinimizeButtonHint)
                            & ~Qt.WindowContextHelpButtonHint)
        # TODO: choose appropriate icon
        self.setWindowIcon(qtlib.geticon('proxy'))

        self._qui = Ui_ServeDialog()
        self._qui.setupUi(self)

        self._initwebconf(webconf)
        self._initcmd()
        self._initactions()
        self._updateform()

    def _initcmd(self):
        self._cmd = cmdui.Widget(True, False, self)
        # TODO: forget old logs?
        self._log_edit = self._cmd.core.outputLog
        self._qui.details_tabs.addTab(self._log_edit, _('Log'))
        self._cmd.hide()
        self._cmd.commandStarted.connect(self._updateform)
        self._cmd.commandFinished.connect(self._updateform)

    def _initwebconf(self, webconf):
        self._webconf_form = WebconfForm(webconf=webconf, parent=self)
        self._qui.details_tabs.addTab(self._webconf_form, _('Repositories'))

    def _initactions(self):
        self._qui.start_button.clicked.connect(self.start)
        self._qui.stop_button.clicked.connect(self.stop)

    @pyqtSlot()
    def _updateform(self):
        """update form availability and status text"""
        self._updatestatus()
        self._qui.start_button.setEnabled(not self.isstarted())
        self._qui.stop_button.setEnabled(self.isstarted())
        self._qui.settings_button.setEnabled(not self.isstarted())
        self._qui.port_edit.setEnabled(not self.isstarted())
        self._webconf_form.setEnabled(not self.isstarted())

    def _updatestatus(self):
        def statustext():
            if self.isstarted():
                # TODO: escape special chars
                link = '<a href="%s">%s</a>' % (self.rooturl, self.rooturl)
                return _('Running at %s') % link
            else:
                return _('Stopped')

        self._qui.status_edit.setText(statustext())

    @pyqtSlot()
    def start(self):
        """Start web server"""
        if self.isstarted():
            return

        _setupwrapper()
        self._cmd.run(self._cmdargs())

    def _cmdargs(self):
        """Build command args to run server"""
        a = ['serve', '--port', str(self.port), '--debug']
        if self._singlerepo:
            a += ['-R', self._singlerepo]
        else:
            a += ['--web-conf', self._tempwebconf()]
        return a

    def _tempwebconf(self):
        """Save current webconf to temporary file; return its path"""
        if not hasattr(self._webconf, 'write'):
            return self._webconf.path

        fd, fname = tempfile.mkstemp(prefix='webconf_', dir=qtlib.gettempdir())
        f = os.fdopen(fd, 'w')
        try:
            self._webconf.write(f)
            return fname
        finally:
            f.close()

    @property
    def _webconf(self):
        """Selected webconf object"""
        return self._webconf_form.webconf

    @property
    def _singlerepo(self):
        """Return repository path if serving single repository"""
        # NOTE: we cannot use web-conf to serve single repository at '/' path
        if len(self._webconf['paths']) != 1:
            return
        path = self._webconf.get('paths', '/')
        if path and '*' not in path:  # exactly a single repo (no wildcard)
            return path

    @pyqtSlot()
    def stop(self):
        """Stop web server"""
        if not self.isstarted():
            return

        self._cmd.cancel()
        self._fake_request()

    def _fake_request(self):
        """Send fake request for server to run python code"""
        TIMEOUT = 0.5  # [sec]
        conn = httplib.HTTPConnection('localhost:%d' % self.port)
        origtimeout = socket.getdefaulttimeout()
        socket.setdefaulttimeout(TIMEOUT)
        try:
            try:
                conn.request('GET', '/')
                res = conn.getresponse()
                res.read()
            except (socket.error, httplib.HTTPException):
                pass
        finally:
            socket.setdefaulttimeout(origtimeout)
            conn.close()

    def reject(self):
        self.stop()
        super(ServeDialog, self).reject()

    def isstarted(self):
        """Is the web server running?"""
        return self._cmd.core.running()

    @property
    def rooturl(self):
        """Returns the root URL of the web server"""
        # TODO: scheme, hostname ?
        return 'http://localhost:%d' % self.port

    @property
    def port(self):
        """Port number of the web server"""
        return int(self._qui.port_edit.value())

    def setport(self, port):
        self._qui.port_edit.setValue(port)

    def keyPressEvent(self, event):
        if self.isstarted() and event.key() == Qt.Key_Escape:
            self.stop()
            return

        return super(ServeDialog, self).keyPressEvent(event)

    def closeEvent(self, event):
        if self.isstarted():
            self._minimizetotray()
            event.ignore()
            return

        return super(ServeDialog, self).closeEvent(event)

    @util.propertycache
    def _trayicon(self):
        icon = QSystemTrayIcon(self.windowIcon(), parent=self)
        icon.activated.connect(self._restorefromtray)
        icon.setToolTip(self.windowTitle())
        # TODO: context menu
        return icon

    # TODO: minimize to tray by minimize button

    @pyqtSlot()
    def _minimizetotray(self):
        self._trayicon.show()
        self._trayicon.showMessage(_('TortoiseHg Web Server'),
                                   _('Running at %s') % self.rooturl)
        self.hide()

    @pyqtSlot()
    def _restorefromtray(self):
        self._trayicon.hide()
        self.show()

    @pyqtSlot()
    def on_settings_button_clicked(self):
        from tortoisehg.hgqt import settings
        settings.SettingsDialog(parent=self, focus='web.name').exec_()

def _create_server(orig, ui, app):
    """wrapper for hgweb.server.create_server to be interruptable"""
    server = orig(ui, app)
    server.accesslog = ui
    server.errorlog = ui  # TODO: ui.warn
    server._serving = False

    def serve_forever(orig):
        server._serving = True
        try:
            try:
                while server._serving:
                    server.handle_request()
            except KeyboardInterrupt:
                # raised outside try-block around process_request().
                # see SocketServer.BaseServer
                pass
        finally:
            server._serving = False
            server.server_close()

    def handle_error(orig, request, client_address):
        type, value, _traceback = sys.exc_info()
        if issubclass(type, KeyboardInterrupt):
            server._serving = False
        else:
            ui.write_err('%s\n' % value)

    extensions.wrapfunction(server, 'serve_forever', serve_forever)
    extensions.wrapfunction(server, 'handle_error', handle_error)
    return server

_setupwrapper_done = False
def _setupwrapper():
    """Wrap hgweb.server.create_server to get along with thg"""
    global _setupwrapper_done
    if not _setupwrapper_done:
        extensions.wrapfunction(hgweb.server, 'create_server',
                                _create_server)
        _setupwrapper_done = True

def run(ui, *pats, **opts):
    repopath = opts.get('root') or paths.find_root()
    webconfpath = opts.get('web_conf') or opts.get('webdir_conf')
    dlg = ServeDialog(webconf=_newwebconf(repopath, webconfpath))

    lui = ui.copy()
    if webconfpath:
        lui.readconfig(webconfpath)
    elif repopath:
        lui.readconfig(os.path.join(repopath, '.hg', 'hgrc'), repopath)
    try:
        dlg.setport(int(lui.config('web', 'port', '8000')))
    except ValueError:
        pass

    if repopath or webconfpath:
        dlg.start()
    return dlg

def recursiveRepoSearch(repo):
    yield repo.root
    try:
        wctx = repo[None]
        for s in wctx.substate:
            sub = wctx.sub(s)
            if isinstance(sub, subrepo.hgsubrepo):
                for root in recursiveRepoSearch(sub._repo):
                    yield root
    except (EnvironmentError, error.Abort, error.RepoError):
        pass

def _asconfigliststr(value):
    r"""
    >>> _asconfigliststr('foo')
    'foo'
    >>> _asconfigliststr('foo bar')
    '"foo bar"'
    >>> _asconfigliststr('foo,bar')
    '"foo,bar"'
    >>> _asconfigliststr('foo "bar"')
    '"foo \\"bar\\""'
    """
    # ui.configlist() uses isspace(), which is locale-dependent
    if util.any(c.isspace() or c == ',' for c in value):
        return '"' + value.replace('"', '\\"') + '"'
    else:
        return value

def _newwebconf(repopath, webconfpath):
    """create config obj for hgweb"""
    if webconfpath:
        # TODO: handle file not found
        c = wconfig.readfile(webconfpath)
        c.path = os.path.abspath(webconfpath)
        return c
    elif repopath:  # imitate webconf for single repo
        c = wconfig.config()
        try:
            repo = thgrepo.repository(None, repopath)
            roots = [root for root in recursiveRepoSearch(repo)]
            if len(roots) == 1:
                # no _asconfigliststr(repopath) for now, because ServeDialog
                # cannot parse it as a list in single-repo mode.
                c.set('paths', '/', repopath)
            else:
                # since hg 8cbb59124e67, path entry is parsed as a list
                base = hglib.fromunicode(repo.shortname)
                c.set('paths', base, _asconfigliststr(repopath))
                for root in roots[1:]:
                    c.set('paths', base + root[len(repopath):],
                          _asconfigliststr(root))
        except (EnvironmentError, error.Abort, error.RepoError):
            c.set('paths', '/', repopath)
        return c
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.