diff --git a/CHANGES.txt b/CHANGES.txt index 6646630ec..ea4795865 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -43,6 +43,7 @@ Features via an optional ``conf.py`` method ``TEMPLATE_ENGINE_FACTORY``. * Switch to pyproject.toml * Add path handler ``slug_source`` linking to source of post. +* Better error message in ``nikola serve`` when port is already in use Bugfixes -------- diff --git a/nikola/plugins/command/serve.py b/nikola/plugins/command/serve.py index 0b2cbac56..65676cef5 100644 --- a/nikola/plugins/command/serve.py +++ b/nikola/plugins/command/serve.py @@ -26,6 +26,7 @@ """Start test server.""" import atexit +import errno import os import sys import re @@ -144,7 +145,16 @@ def _execute(self, options, args): handler_factory = OurHTTPRequestHandler else: handler_factory = _create_RequestHandler_removing_basepath(base_path) - self.httpd = OurHTTP((options['address'], options['port']), handler_factory) + + try: + self.httpd = OurHTTP((options['address'], options['port']), handler_factory) + except OSError as e: + if e.errno == errno.EADDRINUSE: + self.logger.error(f"Port address {options['port']} already in use, please use " + "the `-p ` option to select a different one.") + return 3 + else: + raise sa = self.httpd.socket.getsockname() if ipv6: diff --git a/tests/helper.py b/tests/helper.py index 8fbebdb45..2bc995d0b 100644 --- a/tests/helper.py +++ b/tests/helper.py @@ -31,6 +31,7 @@ def __init__(self): self.template_system = self self.invariant = False self.debug = True + self.show_tracebacks = True self.config = { "DISABLED_PLUGINS": [], "EXTRA_PLUGINS": [], diff --git a/tests/integration/test_dev_server_serve.py b/tests/integration/test_dev_server_serve.py index a91362b64..085e11e7f 100644 --- a/tests/integration/test_dev_server_serve.py +++ b/tests/integration/test_dev_server_serve.py @@ -1,3 +1,8 @@ +import logging +import re +import socket +import sys +from io import StringIO from time import sleep import requests import pytest @@ -8,11 +13,68 @@ from .dev_server_test_helper import MyFakeSite, SERVER_ADDRESS, find_unused_port, LOGGER, OUTPUT_FOLDER +def test_server_on_used_port(site_and_base_path: tuple[MyFakeSite, str]) -> None: + """Check error if port for nikola serve is already being used. + + `nikola serve` uses a default port and if that port is already in use it should print out a nice + error message that tells the user what happend and how to fix this. + + To test the case where the port is already in use, we open a socket on the same port that we use + for `nikola serve` before starting the server. + + The program should exit with a return code of 3 in this case and print out a message to the user. + """ + + site, base_path = site_and_base_path + site.show_tracebacks = False + command_serve = serve.CommandServe() + command_serve.set_site(site) + command_serve.serve_pidfile = "there is no file with this name we hope" + command_serve.logger = logging.getLogger("dev_server_test") + catch_log = StringIO() + catch_log_handler = logging.StreamHandler(catch_log) + logging.getLogger().addHandler(catch_log_handler) + try: + s = socket.socket() + try: + ANY_PORT = 0 + s.bind((SERVER_ADDRESS, ANY_PORT)) + address, port = s.getsockname() + with ThreadPoolExecutor(max_workers=2) as executor: + options = { + "address": SERVER_ADDRESS, + "port": port, + "browser": False, + "detach": False, + "ipv6": False, + } + future_to_run_web_server = executor.submit(lambda: command_serve.execute(options=options)) + command_serve.shutdown() + result = future_to_run_web_server.result() + assert 3 == result + + # TODO: check if this works on windows + # for now we skip this assert on windows platforms. + if not sys.platform == 'win32': + assert re.match( + r"Port address \d+ already in use, " + r"please use the `\-p \` option to select a different one\.", + catch_log.getvalue() + ) + assert "OSError" not in catch_log.getvalue() + finally: + s.close() + finally: + logging.getLogger().removeHandler(catch_log_handler) + + def test_serves_root_dir( site_and_base_path: tuple[MyFakeSite, str], expected_text: str ) -> None: site, base_path = site_and_base_path command_serve = serve.CommandServe() + command_serve.serve_pidfile = "there is no file with this name we hope" + command_serve.logger = logging.getLogger("dev_server_test") command_serve.set_site(site) options = { "address": SERVER_ADDRESS,