Changeset 45b4be294429…
Parent c44381498274…
by
Changes to 3 files · Browse files at 45b4be294429 Showing diff from parent c44381498274 Diff from another changeset...
@@ -413,6 +413,11 @@ from tortoisehg.hgqt.hgignore import run
qtrun(run, ui, *pats, **opts)
+def serve(ui, *pats, **opts):
+ """start stand-alone webserver"""
+ from tortoisehg.hgqt.serve import run
+ qtrun(run, ui, *pats, **opts)
+
def shellconfig(ui, *pats, **opts):
"""explorer extension configuration editor"""
from tortoisehg.hgqt.shellconf import run
@@ -723,6 +728,7 @@ "remove|rm": (remove, [], _('thg remove [FILE]...')),
"revert": (revert, [], _('thg revert [FILE]...')),
"forget": (forget, [], _('thg forget [FILE]...')),
+ "^serve": (serve, [], _('thg serve [OPTION]')),
"^status": (status,
[('c', 'clean', False, _('show files without changes')),
('i', 'ignored', False, _('show ignored files'))],
|
|
|
@@ -0,0 +1,174 @@ + # 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
+from PyQt4.QtCore import Qt, pyqtSlot
+from PyQt4.QtGui import QDialog
+from mercurial import extensions, hgweb
+from tortoisehg.hgqt import cmdui, qtlib
+from tortoisehg.hgqt.i18n import _
+
+try:
+ from tortoisehg.hgqt.ui_serve import Ui_ServeDialog
+except ImportError:
+ from PyQt4 import uic
+ Ui_ServeDialog = uic.loadUiType(os.path.join(os.path.dirname(__file__),
+ 'serve.ui'))[0]
+
+class ServeDialog(QDialog):
+ """Dialog for serving repositories via web"""
+ def __init__(self, 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._initcmd()
+ self._initactions()
+ # TODO: webconf tab (multi-repo support)
+ self._updateform()
+
+ def _initcmd(self):
+ self._cmd = cmdui.Widget()
+ # TODO: forget old logs?
+ self._log_edit = self._cmd.core.output_text
+ 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 _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())
+
+ 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(['serve', '--port', str(self.port)])
+
+ @pyqtSlot()
+ def stop(self):
+ """Stop web server"""
+ if not self.isstarted():
+ return
+
+ self._cmd.cancel()
+ self._fake_request()
+ # TODO: sometimes it doesn't release the port
+
+ 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:
+ 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.is_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 keyPressEvent(self, event):
+ if self.isstarted() and event.key() == Qt.Key_Escape:
+ self.stop()
+ return
+
+ return super(ServeDialog, self).keyPressEvent(event)
+
+ @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
+ while server._serving:
+ server.handle_request()
+
+ def handle_error(orig, request, client_address):
+ type = sys.exc_info()[0]
+ if issubclass(type, KeyboardInterrupt):
+ server._serving = False
+ else:
+ orig(request, client_address)
+
+ 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):
+ # TODO: handle --web-conf
+ dlg = ServeDialog()
+ dlg.start()
+ return dlg
|
|
|
@@ -0,0 +1,142 @@ + <?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ServeDialog</class>
+ <widget class="QDialog" name="ServeDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>500</width>
+ <height>400</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Serve</string>
+ </property>
+ <layout class="QVBoxLayout" name="dialog_layout" stretch="0,1">
+ <item>
+ <layout class="QHBoxLayout" name="top_layout" stretch="1,0">
+ <item>
+ <layout class="QFormLayout" name="opts_layout">
+ <property name="fieldGrowthPolicy">
+ <enum>QFormLayout::FieldsStayAtSizeHint</enum>
+ </property>
+ <item row="0" column="0">
+ <widget class="QLabel" name="port_label">
+ <property name="text">
+ <string>Port:</string>
+ </property>
+ <property name="buddy">
+ <cstring>port_edit</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QSpinBox" name="port_edit">
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ <property name="minimum">
+ <number>1</number>
+ </property>
+ <property name="maximum">
+ <number>65535</number>
+ </property>
+ <property name="value">
+ <number>8000</number>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLabel" name="status_label">
+ <property name="text">
+ <string>Status:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QLabel" name="status_edit">
+ <property name="text">
+ <string/>
+ </property>
+ <property name="textFormat">
+ <enum>Qt::RichText</enum>
+ </property>
+ <property name="openExternalLinks">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QVBoxLayout" name="actions_layout">
+ <item>
+ <widget class="QPushButton" name="start_button">
+ <property name="text">
+ <string>Start</string>
+ </property>
+ <property name="default">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="stop_button">
+ <property name="text">
+ <string>Stop</string>
+ </property>
+ <property name="autoDefault">
+ <bool>false</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="actions_spacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Expanding</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>0</width>
+ <height>5</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QPushButton" name="settings_button">
+ <property name="text">
+ <string>Settings</string>
+ </property>
+ <property name="autoDefault">
+ <bool>false</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QTabWidget" name="details_tabs">
+ <property name="currentIndex">
+ <number>-1</number>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <tabstops>
+ <tabstop>port_edit</tabstop>
+ <tabstop>start_button</tabstop>
+ <tabstop>stop_button</tabstop>
+ <tabstop>settings_button</tabstop>
+ <tabstop>details_tabs</tabstop>
+ </tabstops>
+ <resources/>
+ <connections/>
+</ui>
|
Loading...