changeset 22994:840be5ca03e1

cmdserver: add service that listens on unix domain socket and forks process Typical use case of 'unix' mode is a background hg daemon. $ hg serve --cmdserver unix --cwd / -a /tmp/hg-`id -u`.sock Unlike 'pipe' mode in which parent process keeps stdio channel, 'unix' server can be detached. So clients can freely connect and disconnect from server, saving Python start-up time. It might be better to write "--cmdserver socket -a unix:/sockpath" instead of "--cmdserver unix -a /sockpath" in case hgweb gets the ability to listen on unix domain socket.
author Yuya Nishihara <yuya@tcha.org>
date Sat, 04 Oct 2014 16:46:50 +0900
parents 24c5fd2894f8
children 2587631c5f8a
files mercurial/commandserver.py tests/hghave.py tests/test-commandserver.t
diffstat 3 files changed, 117 insertions(+), 1 deletions(-) [+]
line wrap: on
line diff
--- a/mercurial/commandserver.py	Sat Sep 27 19:18:20 2014 +0900
+++ b/mercurial/commandserver.py	Sat Oct 04 16:46:50 2014 +0900
@@ -7,7 +7,7 @@
 
 from i18n import _
 import struct
-import sys, os
+import sys, os, errno, traceback, SocketServer
 import dispatch, encoding, util
 
 logfile = None
@@ -256,8 +256,59 @@
     def run(self):
         return self.server.serve()
 
+class _requesthandler(SocketServer.StreamRequestHandler):
+    def handle(self):
+        ui = self.server.ui
+        repo = self.server.repo
+        sv = server(ui, repo, self.rfile, self.wfile)
+        try:
+            try:
+                sv.serve()
+            # handle exceptions that may be raised by command server. most of
+            # known exceptions are caught by dispatch.
+            except util.Abort, inst:
+                ui.warn(_('abort: %s\n') % inst)
+            except IOError, inst:
+                if inst.errno != errno.EPIPE:
+                    raise
+            except KeyboardInterrupt:
+                pass
+        except: # re-raises
+            # also write traceback to error channel. otherwise client cannot
+            # see it because it is written to server's stderr by default.
+            traceback.print_exc(file=sv.cerr)
+            raise
+
+class unixservice(object):
+    """
+    Listens on unix domain socket and forks server per connection
+    """
+    def __init__(self, ui, repo, opts):
+        self.ui = ui
+        self.repo = repo
+        self.address = opts['address']
+        if not util.safehasattr(SocketServer, 'UnixStreamServer'):
+            raise util.Abort(_('unsupported platform'))
+        if not self.address:
+            raise util.Abort(_('no socket path specified with --address'))
+
+    def init(self):
+        class cls(SocketServer.ForkingMixIn, SocketServer.UnixStreamServer):
+            ui = self.ui
+            repo = self.repo
+        self.server = cls(self.address, _requesthandler)
+        self.ui.status(_('listening at %s\n') % self.address)
+        self.ui.flush()  # avoid buffering of status message
+
+    def run(self):
+        try:
+            self.server.serve_forever()
+        finally:
+            os.unlink(self.address)
+
 _servicemap = {
     'pipe': pipeservice,
+    'unix': unixservice,
     }
 
 def createservice(ui, repo, opts):
--- a/tests/hghave.py	Sat Sep 27 19:18:20 2014 +0900
+++ b/tests/hghave.py	Sat Oct 04 16:46:50 2014 +0900
@@ -1,5 +1,6 @@
 import os, stat
 import re
+import socket
 import sys
 import tempfile
 
@@ -258,6 +259,10 @@
     finally:
         os.rmdir(d)
 
+@check("unix-socket", "AF_UNIX socket family")
+def has_unix_socket():
+    return getattr(socket, 'AF_UNIX', None) is not None
+
 @check("root", "root permissions")
 def has_root():
     return getattr(os, 'geteuid', None) and os.geteuid() == 0
--- a/tests/test-commandserver.t	Sat Sep 27 19:18:20 2014 +0900
+++ b/tests/test-commandserver.t	Sat Oct 04 16:46:50 2014 +0900
@@ -545,3 +545,63 @@
   *** runcommand init repo2
   *** runcommand id -R repo2
   000000000000 tip
+
+
+unix domain socket:
+
+  $ cd repo
+  $ hg update -q
+
+#if unix-socket
+
+  >>> import cStringIO
+  >>> from hgclient import unixserver, readchannel, runcommand, check
+  >>> server = unixserver('.hg/server.sock', '.hg/server.log')
+  >>> def hellomessage(conn):
+  ...     ch, data = readchannel(conn)
+  ...     print '%c, %r' % (ch, data)
+  ...     runcommand(conn, ['id'])
+  >>> check(hellomessage, server.connect)
+  o, 'capabilities: getencoding runcommand\nencoding: *' (glob)
+  *** runcommand id
+  eff892de26ec tip bm1/bm2/bm3
+  >>> def unknowncommand(conn):
+  ...     readchannel(conn)
+  ...     conn.stdin.write('unknowncommand\n')
+  >>> check(unknowncommand, server.connect)  # error sent to server.log
+  >>> def serverinput(conn):
+  ...     readchannel(conn)
+  ...     patch = """
+  ... # HG changeset patch
+  ... # User test
+  ... # Date 0 0
+  ... 2
+  ... 
+  ... diff -r eff892de26ec -r 1ed24be7e7a0 a
+  ... --- a/a
+  ... +++ b/a
+  ... @@ -1,1 +1,2 @@
+  ...  1
+  ... +2
+  ... """
+  ...     runcommand(conn, ['import', '-'], input=cStringIO.StringIO(patch))
+  ...     runcommand(conn, ['log', '-rtip', '-q'])
+  >>> check(serverinput, server.connect)
+  *** runcommand import -
+  applying patch from stdin
+  *** runcommand log -rtip -q
+  2:1ed24be7e7a0
+  >>> server.shutdown()
+
+  $ cat .hg/server.log
+  listening at .hg/server.sock
+  abort: unknown command unknowncommand
+  killed!
+
+#else
+
+  $ hg serve --cmdserver unix -a .hg/server.sock
+  abort: unsupported platform
+  [255]
+
+#endif