Patchwork hg serve: an option to use the first unused port above a given number

login
register
mail settings
Submitter Roman Neuhauser
Date Sept. 17, 2013, 1:26 a.m.
Message ID <20130917012656.GC71308@isis.sigpipe.cz>
Download mbox | patch
Permalink /patch/2504/
State Superseded
Headers show

Comments

Roman Neuhauser - Sept. 17, 2013, 1:26 a.m.
# kbullock+mercurial@ringworld.org / 2013-09-16 13:20:59 -0500:
> On 16 Sep 2013, at 11:18 AM, Roman Neuhauser wrote:
> 
> > hello,
> > 
> > i need to run hg-serve in tests for an extension i'm writing.
> > it looks like hg-serve can only listen on a given port (defaults
> > to 8000).  this makes the tests somewhat brittle: if there are
> > N concurrent runs, N-1 of them will fail.
> > 
> > i'd like to have hg-serve optionally bind to the first available
> > port at-or-above a given number.  looking at commands.serve and
> > hgweb.server.create_server, it shouldn't even be that hard.
> 
> We do concurrent tests of hgweb in Mercurial's own test suite.
> Check test-hgweb-*.t.
> 
> Do you have a strategy for avoiding race conditions in your proposed
> approach? If so then it might allow some cleanup of our own test
> suite, but we'd have to see a patch on -devel.

this is for hg-stable, for no particular reason.
it's not complete (no doc update), i want early feedback.

questions: how can i integrate the test into the mercurial suite?
is there anything besides mercurial/help/config.txt for docs i need
to update?

# HG changeset patch
# User Roman Neuhauser <neuhauser@sigpipe.cz>
# Date 1379378142 -7200
#      Tue Sep 17 02:35:42 2013 +0200
# Branch stable
# Node ID 003f4cd01fe4bb109208d447b262ec5bb06ea4e0
# Parent  064f7d697852ad6de03b7b2cbf452b69f5de6bef
`hg serve` can use one of multiple ports

A new configuration option, `web.ports` defines the length
of the port range `hg serve` should try.
web.port=8000, web.ports=2 means "try to bind() to 8000 first,
and to 8001 if that fails".  Default is 1.

setup:

  $ export HGRCPATH="$PWD/hgrc"
  $ cat > $HGRCPATH <<EOF
  > [ui]
  > interactive = False
  > [web]
  > port = 18000
  > ports = 4
  > EOF
  $ hg init

web.ports = 4 means the fifth will fail:

  $ hg serve -d --pid-file 18000.pid
  listening at http://stick.suse.cz:18000/ (bound to *:18000)
  $ hg serve -d --pid-file 18001.pid
  listening at http://stick.suse.cz:18001/ (bound to *:18001)
  $ hg serve -d --pid-file 18002.pid
  listening at http://stick.suse.cz:18002/ (bound to *:18002)
  $ hg serve -d --pid-file 18003.pid
  listening at http://stick.suse.cz:18003/ (bound to *:18003)
  $ hg serve -d --pid-file 1DEAD.pid
  abort: cannot start server at ':18003': Address already in use
  abort: child process failed to start
  [255]

teardown:

  $ kill $(cat *.pid)

Patch

diff --git a/mercurial/hgweb/server.py b/mercurial/hgweb/server.py
--- a/mercurial/hgweb/server.py
+++ b/mercurial/hgweb/server.py
@@ -322,9 +322,18 @@  def create_server(ui, app):
     import mimetypes; mimetypes.init()
 
     address = ui.config('web', 'address', '')
-    port = util.getport(ui.config('web', 'port', 8000))
-    try:
-        return cls(ui, app, (address, port), handler)
-    except socket.error, inst:
-        raise util.Abort(_("cannot start server at '%s:%d': %s")
-                         % (address, port, inst.args[1]))
+    base = util.getport(ui.config('web', 'port', 8000))
+    ports = ui.configint('web', 'ports', 1)
+    last = base + ports
+    if ports <= 0:
+        raise util.Abort(_("web.ports must be positive"))
+
+    error = 'unknown error'
+    for port in range(base, last):
+        try:
+            return cls(ui, app, (address, port), handler)
+        except socket.error, inst:
+            error = inst.args[1]
+
+    raise util.Abort(_("cannot start server at '%s:%d': %s")
+                     % (address, port, error))