Repository: certbot/certbot

Issue:
Race condition in recent Travis builds
#726, #752 and #754 are affected by annoying race condition that causes Travis build to fail randomly (see https://travis-ci.org/letsencrypt/letsencrypt/builds/77715204, https://travis-ci.org/letsencrypt/letsencrypt/builds/78978888, https://travis-ci.org/letsencrypt/letsencrypt/builds/78990354, resp.).

It seems that manual authenticator doesn't manage to bootstrap on time before we proceed to `simple_verify`.



Discussion:
See:
- https://travis-ci.org/letsencrypt/letsencrypt/jobs/77715206#L1928
- https://travis-ci.org/letsencrypt/letsencrypt/jobs/78978893#L1860
- https://travis-ci.org/letsencrypt/letsencrypt/jobs/78990359#L1856

for offending lines. In general:

```
2015-08-28 17:08:44,297:ERROR:acme.challenges:Unable to reach https://le.wtf:5001/.well-known/acme-challenge/CpQe6qbvF-u0qcbhlBNWN-6fYVxIV_wL7g1jV3CY4_k: ('Connection aborted.', error(111, 'Connection refused'))
```


Resolving patch:
diff --git a/letsencrypt/plugins/manual.py b/letsencrypt/plugins/manual.py
--- a/letsencrypt/plugins/manual.py
+++ b/letsencrypt/plugins/manual.py
@@ -4,6 +4,7 @@
 import pipes
 import shutil
 import signal
+import socket
 import subprocess
 import sys
 import tempfile
@@ -122,6 +123,20 @@ def perform(self, achalls):  # pylint: disable=missing-docstring
             responses.append(self._perform_single(achall))
         return responses
 
+    @classmethod
+    def _test_mode_busy_wait(cls, port):
+        while True:
+            time.sleep(1)
+            sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+            try:
+                sock.connect(("localhost", port))
+            except socket.error:  # pragma: no cover
+                pass
+            else:
+                break
+            finally:
+                sock.close()
+
     def _perform_single(self, achall):
         # same path for each challenge response would be easier for
         # users, but will not work if multiple domains point at the
@@ -129,13 +144,13 @@ def _perform_single(self, achall):
         response, validation = achall.gen_response_and_validation(
             tls=(not self.config.no_simple_http_tls))
 
+        port = (response.port if self.config.simple_http_port is None
+                else int(self.config.simple_http_port))
         command = self.template.format(
             root=self._root, achall=achall, response=response,
             validation=pipes.quote(validation.json_dumps()),
             encoded_token=achall.chall.encode("token"),
-            ct=response.CONTENT_TYPE, port=(
-                response.port if self.config.simple_http_port is None
-                else self.config.simple_http_port))
+            ct=response.CONTENT_TYPE, port=port)
         if self.conf("test-mode"):
             logger.debug("Test mode. Executing the manual command: %s", command)
             try:
@@ -153,7 +168,7 @@ def _perform_single(self, achall):
             logger.debug("Manual command running as PID %s.", self._httpd.pid)
             # give it some time to bootstrap, before we try to verify
             # (cert generation in case of simpleHttpS might take time)
-            time.sleep(4)  # XXX
+            self._test_mode_busy_wait(port)
             if self._httpd.poll() is not None:
                 raise errors.Error("Couldn't execute manual command")
         else:


Test patch:
diff --git a/letsencrypt/plugins/manual_test.py b/letsencrypt/plugins/manual_test.py
--- a/letsencrypt/plugins/manual_test.py
+++ b/letsencrypt/plugins/manual_test.py
@@ -71,25 +71,29 @@ def test_perform_test_command_oserror(self, mock_popen):
         mock_popen.side_effect = OSError
         self.assertEqual([False], self.auth_test_mode.perform(self.achalls))
 
+    @mock.patch("letsencrypt.plugins.manual.socket.socket", autospec=True)
     @mock.patch("letsencrypt.plugins.manual.time.sleep", autospec=True)
     @mock.patch("letsencrypt.plugins.manual.subprocess.Popen", autospec=True)
     def test_perform_test_command_run_failure(
-            self, mock_popen, unused_mock_sleep):
+            self, mock_popen, unused_mock_sleep, unused_mock_socket):
         mock_popen.poll.return_value = 10
         mock_popen.return_value.pid = 1234
         self.assertRaises(
             errors.Error, self.auth_test_mode.perform, self.achalls)
 
+    @mock.patch("letsencrypt.plugins.manual.socket.socket", autospec=True)
     @mock.patch("letsencrypt.plugins.manual.time.sleep", autospec=True)
     @mock.patch("acme.challenges.SimpleHTTPResponse.simple_verify",
                 autospec=True)
     @mock.patch("letsencrypt.plugins.manual.subprocess.Popen", autospec=True)
-    def test_perform_test_mode(self, mock_popen, mock_verify, mock_sleep):
+    def test_perform_test_mode(self, mock_popen, mock_verify, mock_sleep,
+                               mock_socket):
         mock_popen.return_value.poll.side_effect = [None, 10]
         mock_popen.return_value.pid = 1234
         mock_verify.return_value = False
         self.assertEqual([False], self.auth_test_mode.perform(self.achalls))
         self.assertEqual(1, mock_sleep.call_count)
+        self.assertEqual(1, mock_socket.call_count)
 
     def test_cleanup_test_mode_already_terminated(self):
         # pylint: disable=protected-access