Repository: gevent/gevent

Issue:
BackdoorServer doesn't work anymore with AF_UNIX listeners
* gevent version: 20.12.1 from pypi
* Python version: 2.7.18 from the official Fedora repos.
* Operating System: Fedora 5.8.10-200.fc32.x86_64

### Description:

`gevent.backdoor.BackdoorServer()`  doesn't work when given an `AF_UNIX` listener. It assumes the listener is `AF_INET` and tries to do a `conn.setsockopt(socket.SOL_TCP, socket.TCP_NODELAY, True)` in `BackdoorServer.handle()`.

Previously, on an older gevent version unix sockets worked fine. In some cases they're preferable over TCP on localhost, because basic authentication can be done with the socket file owner/permissions.

We can probably check the socket type in `handle()` with `getsockopt(SO_DOMAIN)` or something. Thoughts?

Hints from issue comments:
@jamadden: This changed in [1.5a2](https://github.com/gevent/gevent/commit/9d27d269ed01a7e752966caa7a6f85d773780a1a#diff-5c1fac58666e8444f93264107cb96915fe63966d46459770fe5bc06585d50a35) to deal with timeouts apparently caused by buffering. 

Tests would need to be added to use that type of socket (being sure not to run them when that socket type isn't supported, of course). Subclassing `gevent.tests.test__backdoor:Test` and overriding `_create_connection` should be sufficient for both.

It's probably sufficient and easier to just catch the exception ("EAFP" vs "LBYL").@danmilon: Oh a PR has been submitted about it!

Solution patch:
diff --git a/docs/changes/1769.bugfix.rst b/docs/changes/1769.bugfix.rst
new file mode 100644
index 00000000..b0de912d
--- /dev/null
+++ b/docs/changes/1769.bugfix.rst
@@ -0,0 +1 @@
+Fix using gevent's BackdoorServer with Unix sockets.
diff --git a/src/gevent/backdoor.py b/src/gevent/backdoor.py
index f4f47cf1..61a82caa 100644
--- a/src/gevent/backdoor.py
+++ b/src/gevent/backdoor.py
@@ -164,7 +164,11 @@ class BackdoorServer(StreamServer):
             ``locals`` dictionary. Previously they were shared in a
             potentially unsafe manner.
         """
-        conn.setsockopt(socket.SOL_TCP, socket.TCP_NODELAY, True) # pylint:disable=no-member
+        try:
+            conn.setsockopt(socket.SOL_TCP, socket.TCP_NODELAY, True)
+        except OSError:
+            pass
+
         raw_file = conn.makefile(mode="r")
         getcurrent().stdin = _StdIn(conn, raw_file)
         getcurrent().stdout = _StdErr(conn, raw_file)


Test patch:
diff --git a/src/gevent/tests/test__backdoor.py b/src/gevent/tests/test__backdoor.py
index 232e05d9..3dccf985 100644
--- a/src/gevent/tests/test__backdoor.py
+++ b/src/gevent/tests/test__backdoor.py
@@ -1,14 +1,14 @@
-from __future__ import print_function
-from __future__ import absolute_import
+import os
+import tempfile
 
 import gevent
-from gevent import socket
-from gevent import backdoor
-
 import gevent.testing as greentest
+from gevent import backdoor
+from gevent import socket
 from gevent.testing.params import DEFAULT_BIND_ADDR_TUPLE
 from gevent.testing.params import DEFAULT_CONNECT
 
+
 def read_until(conn, postfix):
     read = b''
     assert isinstance(postfix, bytes)
@@ -60,15 +60,25 @@ class Test(greentest.TestCase):
         gevent.sleep() # let spawned greenlets die
         super(Test, self).tearDown()
 
+    def _server_listen_argument(self):
+        return DEFAULT_BIND_ADDR_TUPLE
+
     def _make_and_start_server(self, *args, **kwargs):
-        server = backdoor.BackdoorServer(DEFAULT_BIND_ADDR_TUPLE, *args, **kwargs)
+        server = backdoor.BackdoorServer(self._server_listen_argument(), *args, **kwargs)
         server.start()
         return server
 
+    def _connection_arguments(self, server):
+        return ((DEFAULT_CONNECT, server.server_port),)
+
+    def _socket_arguments(self):
+        return ()
+
     def _create_connection(self, server):
-        conn = SocketWithBanner()
-        conn.connect((DEFAULT_CONNECT, server.server_port)) # pylint:disable=not-callable
+        conn = SocketWithBanner(*self._socket_arguments())
+
         try:
+            conn.connect(*self._connection_arguments(server)) # pylint:disable=not-callable
             banner = self._wait_for_prompt(conn)
         except:
             conn.close()
@@ -139,7 +149,8 @@ class Test(greentest.TestCase):
             msg="locals() unusable: %s..." % response)
 
     def test_switch_exc(self):
-        from gevent.queue import Queue, Empty
+        from gevent.queue import Empty
+        from gevent.queue import Queue
 
         def bad():
             q = Queue()
@@ -163,6 +174,38 @@ class Test(greentest.TestCase):
             'switching out, then throwing in\nGot Empty\nswitching out\nswitched in\n>>> ',
             response)
 
+@greentest.skipUnless(
+    hasattr(socket, 'AF_UNIX'),
+    "Needs AF_UNIX support"
+)
+class TestUnixSocket(Test):
+    def setUp(self):
+        super().setUp()
+        # Create a temporary file for the Unix socket
+        self.unix_socket_path = tempfile.mktemp()
+
+    def tearDown(self):
+        # Clean up the Unix socket file
+        if os.path.exists(self.unix_socket_path):
+            os.unlink(self.unix_socket_path)
+        super().tearDown()
+
+    def _server_listen_argument(self):
+        # Use the Unix socket for the BackdoorServer
+        listener = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+        try:
+            listener.bind(self.unix_socket_path)
+            listener.listen(socket.SOMAXCONN)
+        except Exception as ex:
+            listener.close()
+            raise RuntimeError(f"Failed to bind Unix socket at {self.unix_socket_path}: {ex}")
+        return listener
+
+    def _connection_arguments(self, server):
+        return (self.unix_socket_path,)
+
+    def _socket_arguments(self):
+        return (socket.AF_UNIX, socket.SOCK_STREAM)
 
 if __name__ == '__main__':
     greentest.main() # pragma: testrunner-no-combine