It is common to encounter an OSError: [Errno 9] Bad file descriptor on macOS when trying to shut down a server with server.close_all() from another thread. This was previously reported in #430 which was closed without comment by its originator.
I think this could be a race condition where one thread invokes ioloop.close() and closes one of the file descriptors that the server's ioloop.loop() is polling at that moment. _kqueue.control() immediately raises OSError(EBADF) which is not caught.
Consider a simple example:
import threading
import time
from pyftpdlib.handlers import FTPHandler
from pyftpdlib.servers import FTPServer
server = FTPServer(("127.0.0.1", 0), FTPHandler)
t = threading.Thread(target=server.serve_forever, name="pyftpdlib", daemon=True)
t.start()
time.sleep(10)
server.close_all()
t.join(timeout=2.0)
The output on macOS 15.6 and Python 3.9 is:
[I 2025-08-09 19:17:46] concurrency model: async
[I 2025-08-09 19:17:46] masquerade (NAT) address: None
[I 2025-08-09 19:17:46] passive ports: None
[I 2025-08-09 19:17:46] >>> starting FTP server on 127.0.0.1:61530, pid=86473 <<<
Exception in thread pyftpdlib:
Traceback (most recent call last):
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/threading.py", line 973, in _bootstrap_inner
self.run()
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/threading.py", line 910, in run
self._target(*self._args, **self._kwargs)
File "/private/tmp/testpyftpd/.venv/lib/python3.9/site-packages/pyftpdlib/servers.py", line 251, in serve_forever
self.ioloop.loop(timeout, blocking)
File "/private/tmp/testpyftpd/.venv/lib/python3.9/site-packages/pyftpdlib/ioloop.py", line 385, in loop
poll(soonest_timeout)
File "/private/tmp/testpyftpd/.venv/lib/python3.9/site-packages/pyftpdlib/ioloop.py", line 745, in poll
kevents = self._kqueue.control(
OSError: [Errno 9] Bad file descriptor
This affects both the Kqueue and Select IOLoop implementations but Poll seems to work OK.
A workaround with Kqueue might be to subclass it:
# Workaround for kqueue issue on macOS when closing the server.
# See: https://github.com/giampaolo/pyftpdlib/issues/662
if IOLoop.__name__ == "Kqueue":
class KqueueFix662(IOLoop):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._closing = False
def close(self):
self._closing = True
super().close()
def poll(self, *args, **kwargs):
try:
return super().poll(*args, **kwargs)
except OSError as e:
if self._closing and e.errno == 9:
pass
else:
raise
IOLoop = KqueueFix662
server = FTPServer(("127.0.0.1", 0), FTPHandler, ioloop=IOLoop())
It is common to encounter an
OSError: [Errno 9] Bad file descriptoron macOS when trying to shut down a server withserver.close_all()from another thread. This was previously reported in #430 which was closed without comment by its originator.I think this could be a race condition where one thread invokes
ioloop.close()and closes one of the file descriptors that the server'sioloop.loop()is polling at that moment._kqueue.control()immediately raisesOSError(EBADF)which is not caught.Consider a simple example:
The output on macOS 15.6 and Python 3.9 is:
This affects both the
KqueueandSelectIOLoop implementations butPollseems to work OK.A workaround with
Kqueuemight be to subclass it: