Repository: gradio-app/gradio

Issue:
[Suggestions] Don't autofind a port when a port is user specified
**Is your feature request related to a problem? Please describe.**

When using `iface = gr.Interface(.., server_port=5000)` if port `5000` is already used it will use port `5001` instead.

**Describe the solution you'd like**

If it could crash instead that would be better saying the port is already used.

**Additional context**
This was an issue when reloading regularly the gradio app and reloading pages on the browser



Discussion:
Makes sense, we'll fix it!

Resolving patch:
diff --git a/gradio/interface.py b/gradio/interface.py
--- a/gradio/interface.py
+++ b/gradio/interface.py
@@ -30,7 +30,7 @@ class Interface:
     Interfaces are created with Gradio by constructing a `gradio.Interface()` object or by calling `gradio.Interface.load()`.
     """
 
-    instances = weakref.WeakSet()  # stores all currently existing Interface instances
+    instances = weakref.WeakSet()  # stores references to all currently existing Interface instances
 
     @classmethod
     def get_instances(cls):
@@ -76,9 +76,8 @@ def __init__(self, fn, inputs=None, outputs=None, verbose=None, examples=None,
                  examples_per_page=10, live=False, layout="unaligned", show_input=True, show_output=True,
                  capture_session=None, interpretation=None, num_shap=2.0, theme=None, repeat_outputs_per_model=True,
                  title=None, description=None, article=None, thumbnail=None,
-                 css=None, server_port=None, server_name=None, height=500, width=900,
-                 allow_screenshot=True, allow_flagging=None, flagging_options=None, encrypt=False,
-                 show_tips=None, flagging_dir="flagged", analytics_enabled=None, enable_queue=None, api_mode=None):
+                 css=None, height=500, width=900, allow_screenshot=True, allow_flagging=None, flagging_options=None, 
+                 encrypt=False, show_tips=None, flagging_dir="flagged", analytics_enabled=None, enable_queue=None, api_mode=None):
         """
         Parameters:
         fn (Callable): the function to wrap an interface around.
@@ -175,12 +174,7 @@ def __init__(self, fn, inputs=None, outputs=None, verbose=None, examples=None,
                 "Examples argument must either be a directory or a nested list, where each sublist represents a set of inputs.")
         self.num_shap = num_shap
         self.examples_per_page = examples_per_page
-        
-        self.server_name = server_name
-        self.server_port = server_port
-        if server_name is not None or server_port is not None:
-            warnings.warn("The server_name and server_port parameters in the `Interface` class will be deprecated. Please provide them in the `launch()` method instead.")
-        
+                
         self.simple_server = None
         self.allow_screenshot = allow_screenshot
         # For allow_flagging and analytics_enabled: (1) first check for parameter, (2) check for environment variable, (3) default to True
@@ -586,7 +580,6 @@ def launch(self, inline=None, inbrowser=None, share=False, debug=False,
         path_to_local_server (str): Locally accessible link
         share_url (str): Publicly accessible link (if share=True)
         """
-
         # Set up local flask server
         config = self.get_config_file()
         self.config = config
@@ -595,6 +588,7 @@ def launch(self, inline=None, inbrowser=None, share=False, debug=False,
         self.auth = auth
         self.auth_message = auth_message
         self.show_tips = show_tips
+        self.show_error = show_error
 
         # Request key for encryption
         if self.encrypt:
@@ -602,19 +596,18 @@ def launch(self, inline=None, inbrowser=None, share=False, debug=False,
                 getpass.getpass("Enter key for encryption: "))
 
         # Store parameters
-        server_name = server_name or self.server_name or networking.LOCALHOST_NAME
-        server_port = server_port or self.server_port or networking.INITIAL_PORT_VALUE
         if self.enable_queue is None:
             self.enable_queue = enable_queue
 
         # Launch local flask server
-        server_port, path_to_local_server, app, thread = networking.start_server(
+        server_port, path_to_local_server, app, thread, server = networking.start_server(
             self, server_name, server_port, self.auth)
         self.local_url = path_to_local_server
         self.server_port = server_port
         self.status = "RUNNING"
-        self.server = app
-        self.show_error = show_error
+        self.server = server
+        self.server_app = app
+        self.server_thread = thread
 
         # Count number of launches
         utils.launch_counter()
@@ -709,21 +702,18 @@ def launch(self, inline=None, inbrowser=None, share=False, debug=False,
 
         return app, path_to_local_server, share_url
 
-    def close(self):
+    def close(self, verbose=True):
         """
         Closes the Interface that was launched. This will close the server and free the port.
         """
         try:
-            if self.share_url:
-                requests.get("{}/shutdown".format(self.share_url))
-                print("Closing Gradio server on port {}...".format(self.server_port))
-            elif self.local_url:
-                requests.get("{}shutdown".format(self.local_url))
-                print("Closing Gradio server on port {}...".format(self.server_port))
-            else:
-                pass # server not running
-        except (requests.ConnectionError, ConnectionResetError):
-            pass  # server is already closed
+            self.server.shutdown()
+            self.server_thread.join()
+            print("Closing server running on port: {}".format(self.server_port))
+        except AttributeError: # can't close if not running
+            pass
+        except OSError: # sometimes OSError is thrown when shutting down
+            pass
 
     def integrate(self, comet_ml=None, wandb=None, mlflow=None):
         """
@@ -764,11 +754,13 @@ def integrate(self, comet_ml=None, wandb=None, mlflow=None):
                 utils.integration_analytics(data)
 
 
-def close_all():
+def close_all(verbose=True):
+    # Tries to close all running interfaces, but method is a little flaky.
     for io in Interface.get_instances():
-        io.close()
+        io.close(verbose)
 
 
 def reset_all():
-    warnings.warn("The `reset_all()` method has been renamed to `close_all()`. Please use `close_all()` instead.")
+    warnings.warn("The `reset_all()` method has been renamed to `close_all()` " 
+    "and will be deprecated. Please use `close_all()` instead.")
     close_all()
diff --git a/gradio/networking.py b/gradio/networking.py
--- a/gradio/networking.py
+++ b/gradio/networking.py
@@ -2,45 +2,44 @@
 Defines helper methods useful for setting up ports, launching servers, and handling `ngrok`
 """
 
-import os
-import socket
-import threading
+import csv
+import datetime
 from flask import Flask, request, session, jsonify, abort, send_file, render_template, redirect
 from flask_cachebuster import CacheBuster
 from flask_login import LoginManager, login_user, current_user, login_required
 from flask_cors import CORS
-import threading
-import pkg_resources
-import datetime
-import time
+from functools import wraps
+import inspect
+import io
 import json
-import urllib.request
-from shutil import copyfile
+import logging
+import os
+import pkg_resources
 import requests
+import socket
 import sys
-import csv
-import logging
-from gradio.tunneling import create_tunnel
-from gradio import encryptor
-from gradio import queue
-from functools import wraps
-import io
-import inspect
+import threading
+import time
 import traceback
+import urllib.request
 from werkzeug.security import safe_join
+from werkzeug.serving import make_server
+from gradio import encryptor
+from gradio import queue
+from gradio.tunneling import create_tunnel
 
-INITIAL_PORT_VALUE = int(os.getenv(
-    'GRADIO_SERVER_PORT', "7860"))  # The http server will try to open on port 7860. If not available, 7861, 7862, etc.
-TRY_NUM_PORTS = int(os.getenv(
-    'GRADIO_NUM_PORTS', "100"))  # Number of ports to try before giving up and throwing an exception.
-LOCALHOST_NAME = os.getenv(
-    'GRADIO_SERVER_NAME', "127.0.0.1")
+# By default, the http server will try to open on port 7860. If not available, 7861, 7862, etc.
+INITIAL_PORT_VALUE = int(os.getenv('GRADIO_SERVER_PORT', "7860"))  
+# Number of ports to try before giving up and throwing an exception.
+TRY_NUM_PORTS = int(os.getenv('GRADIO_NUM_PORTS', "100"))  
+LOCALHOST_NAME = os.getenv('GRADIO_SERVER_NAME', "127.0.0.1")
 GRADIO_API_SERVER = "https://api.gradio.app/v1/tunnel-request"
 GRADIO_FEATURE_ANALYTICS_URL = "https://api.gradio.app/gradio-feature-analytics/"
 
 STATIC_TEMPLATE_LIB = pkg_resources.resource_filename("gradio", "templates/")
 STATIC_PATH_LIB = pkg_resources.resource_filename("gradio", "templates/frontend/static")
 VERSION_FILE = pkg_resources.resource_filename("gradio", "version.txt")
+
 with open(VERSION_FILE) as version_file:
     GRADIO_STATIC_ROOT = "https://gradio.s3-us-west-2.amazonaws.com/" + \
         version_file.read().strip() + "/static/"
@@ -426,10 +425,22 @@ def queue_thread(path_to_local_server, test_mode=False):
             break
 
 
-def start_server(interface, server_name, server_port, auth=None, ssl=None):
-    port = get_first_available_port(
-        server_port, server_port + TRY_NUM_PORTS
-    )
+def start_server(interface, server_name=None, server_port=None, auth=None, ssl=None):
+    if server_name is None:
+        server_name = LOCALHOST_NAME
+    if server_port is None:  # if port is not specified, start at 7860 and search for first available port        
+        port = get_first_available_port(
+            INITIAL_PORT_VALUE, INITIAL_PORT_VALUE + TRY_NUM_PORTS
+        )
+    else:
+        try:
+            s = socket.socket()  # create a socket object
+            s.bind((LOCALHOST_NAME, server_port))  # Bind to the port to see if it's available (otherwise, raise OSError)
+            s.close()
+        except OSError:
+            raise OSError("Port {} is in use. If a gradio.Interface is running on the port, you can close() it or gradio.close_all().".format(server_port))
+        port = server_port
+
     url_host_name = "localhost" if server_name == "0.0.0.0" else server_name
     path_to_local_server = "http://{}:{}/".format(url_host_name, port)
     if auth is not None:
@@ -451,15 +462,13 @@ def start_server(interface, server_name, server_port, auth=None, ssl=None):
         app.queue_thread.start()
     if interface.save_to is not None:
         interface.save_to["port"] = port
-    app_kwargs = {"port": port, "host": server_name}
+    app_kwargs = {"app": app, "port": port, "host": server_name}
     if ssl:
         app_kwargs["ssl_context"] = ssl
-    thread = threading.Thread(target=app.run,
-                              kwargs=app_kwargs,
-                              daemon=True)
+    server = make_server(**app_kwargs)
+    thread = threading.Thread(target=server.serve_forever, daemon=True)
     thread.start()
-
-    return port, path_to_local_server, app, thread
+    return port, path_to_local_server, app, thread, server
 
 def get_state():
     return session.get("state")


Test patch:
diff --git a/test/test_interfaces.py b/test/test_interfaces.py
--- a/test/test_interfaces.py
+++ b/test/test_interfaces.py
@@ -10,6 +10,7 @@
 from comet_ml import Experiment
 import mlflow
 import wandb
+import socket
 
 os.environ["GRADIO_ANALYTICS_ENABLED"] = "False"
 
@@ -24,10 +25,19 @@ def captured_output():
         sys.stdout, sys.stderr = old_out, old_err
 
 class TestInterface(unittest.TestCase):
-    def test_reset_all(self):
+    def test_close(self):
+        io = Interface(lambda input: None, "textbox", "label")
+        _, local_url, _ = io.launch(prevent_thread_lock=True)
+        response = requests.get(local_url)
+        self.assertEqual(response.status_code, 200)
+        io.close()
+        with self.assertRaises(Exception):
+            response = requests.get(local_url)
+        
+    def test_close_all(self):
         interface = Interface(lambda input: None, "textbox", "label")
         interface.close = mock.MagicMock()
-        reset_all()
+        close_all()
         interface.close.assert_called()
         
     def test_examples_invalid_input(self):
diff --git a/test/test_networking.py b/test/test_networking.py
--- a/test/test_networking.py
+++ b/test/test_networking.py
@@ -199,7 +199,6 @@ def test_state_initialization(self):
 
     def test_state_value(self):
         io = gr.Interface(lambda x: len(x), "text", "label")
-        io.launch(prevent_thread_lock=True)
         app, _, _ = io.launch(prevent_thread_lock=True)
         with app.test_request_context():
             networking.set_state("test")
diff --git a/test/test_utils.py b/test/test_utils.py
--- a/test/test_utils.py
+++ b/test/test_utils.py
@@ -101,20 +101,8 @@ def test_readme_to_html_doesnt_crash_on_connection_error(self, mock_get):
         readme_to_html("placeholder")
         
     def test_readme_to_html_correct_parse(self):
-        readme_to_html("https://github.com/gradio-app/gradio/blob/master/README.md")
-
-    def test_launch_counter(self):
-        with tempfile.NamedTemporaryFile() as tmp:
-            with mock.patch('gradio.utils.JSON_PATH', tmp.name):
-                interface = gradio.Interface(lambda x: x, "textbox", "label")
-                os.remove(tmp.name)
-                interface.launch(prevent_thread_lock=True)
-                with open(tmp.name) as j:
-                    self.assertEqual(json.load(j)['launches'], 1)
-                interface.launch(prevent_thread_lock=True)
-                with open(tmp.name) as j:
-                    self.assertEqual(json.load(j)['launches'], 2)
-    
+        readme_to_html("https://github.com/gradio-app/gradio/blob/master/README.md")    
+
 
 if __name__ == '__main__':
     unittest.main()