diff --git a/server/cli/launch.py b/server/cli/launch.py index 1c4f1005..5e8f2ceb 100644 --- a/server/cli/launch.py +++ b/server/cli/launch.py @@ -1,3 +1,4 @@ +import errno import logging from os import devnull from os.path import splitext, basename, getsize @@ -12,6 +13,7 @@ from server.app.app import Server from server.app.util.errors import ScanpyFileError from server.app.util.utils import custom_format_warning from server.utils.constants import MODES +from server.utils.utils import find_available_port # anything bigger than this will generate a special message @@ -55,7 +57,8 @@ BIG_FILE_SIZE_THRESHOLD = 100 * 2**20 # 100MB show_default=True, help="Open the web browser after launch.", ) -@click.option("--port", "-p", help="Port to run server on.", metavar="", default=5005, show_default=True) +@click.option("--port", "-p", help="Port to run server on, if not specified cellxgene will find an available port.", + metavar="", show_default=True) @click.option("--obs-names", default=None, metavar="", help="Name of annotation field to use for observations.") @click.option("--var-names", default=None, metavar="", help="Name of annotation to use for variables.") @click.option("--host", default="127.0.0.1", help="Host IP address") @@ -140,6 +143,9 @@ security risk by including the --scripts flag. Make sure you trust the scripts t file_parts = splitext(basename(data)) title = file_parts[0] + if not port: + port = find_available_port(host) + # Setup app cellxgene_url = f"http://{host}:{port}" @@ -197,4 +203,9 @@ security risk by including the --scripts flag. Make sure you trust the scripts t f = open(devnull, "w") sys.stdout = f - server.app.run(host=host, debug=debug, port=port, threaded=True) + try: + server.app.run(host=host, debug=debug, port=port, threaded=True) + except OSError as e: + if e.errno == errno.EADDRINUSE: + raise click.ClickException("Port is in use, please specify an open port using the --port flag.") from e + raise diff --git a/server/test/test_api.py b/server/test/test_api.py index 57f6fa45..adb06de8 100644 --- a/server/test/test_api.py +++ b/server/test/test_api.py @@ -19,7 +19,7 @@ class EndPoints(unittest.TestCase): @classmethod def setUpClass(cls): - cls.ps = Popen(["cellxgene", "launch", "example-dataset/pbmc3k.h5ad", "--debug"]) + cls.ps = Popen(["cellxgene", "launch", "example-dataset/pbmc3k.h5ad", "--debug", "--port", "5005"]) session = requests.Session() for i in range(90): try: diff --git a/server/test/test_nan_rest.py b/server/test/test_nan_rest.py index ccceab03..23b3db50 100644 --- a/server/test/test_nan_rest.py +++ b/server/test/test_nan_rest.py @@ -21,7 +21,7 @@ class WithNaNs(unittest.TestCase): @classmethod def setUpClass(cls): cls.ps = Popen( - ["cellxgene", "launch", "server/test/test_datasets/nan.h5ad", "--debug"] + ["cellxgene", "launch", "server/test/test_datasets/nan.h5ad", "--debug", "--port", "5005"] ) session = requests.Session() for i in range(90): diff --git a/server/utils/utils.py b/server/utils/utils.py new file mode 100644 index 00000000..37a984e4 --- /dev/null +++ b/server/utils/utils.py @@ -0,0 +1,19 @@ +import contextlib +import errno +import socket + + +def find_available_port(host, port=5005): + """ + Helper method to find open port on host. Tries 5000 ports incremented from the specified port + """ + # Takes approx 2 seconds to do a scan of 5000 ports on my laptop + num_ports_to_try = 5000 + for port_to_try in range(port, port + num_ports_to_try): + with contextlib.closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s: + try: + s.bind((host, port_to_try)) + return port_to_try + except socket.error: + pass + raise socket.error(errno.EADDRINUSE, f"No port in range {port} - {port + num_ports_to_try - 1} available.")