diff --git a/supervisor/medusa/asyncore_25.py b/supervisor/medusa/asyncore_25.py index 1c578310a..d3efdf7a5 100644 --- a/supervisor/medusa/asyncore_25.py +++ b/supervisor/medusa/asyncore_25.py @@ -358,8 +358,16 @@ def recv(self, buffer_size): def close(self): self.del_channel() - self.socket.shutdown(socket.SHUT_RDWR) - self.socket.close() + + try: + self.socket.shutdown(socket.SHUT_RDWR) + except socket.error: + # must swallow exception from already-closed socket + # (at least with Python 3.11.7 on macOS 14.2.1) + pass + + # does not raise if called on already-closed socket + self.socket.close() # cheap inheritance, used to pass all other attribute # references to the underlying socket object. diff --git a/supervisor/tests/fixtures/issue-1596.conf b/supervisor/tests/fixtures/issue-1596.conf new file mode 100644 index 000000000..750214bbe --- /dev/null +++ b/supervisor/tests/fixtures/issue-1596.conf @@ -0,0 +1,12 @@ +[supervisord] +loglevel=info ; log level; default info; others: debug,warn,trace +logfile=/tmp/issue-1596.log ; main log file; default $CWD/supervisord.log +pidfile=/tmp/issue-1596.pid ; supervisord pidfile; default supervisord.pid +nodaemon=true ; start in foreground if true; default false +identifier=from_config_file + +[rpcinterface:supervisor] +supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface + +[unix_http_server] +file=/tmp/issue-1596.sock ; the path to the socket file diff --git a/supervisor/tests/test_end_to_end.py b/supervisor/tests/test_end_to_end.py index bc7c47421..25b71d2a2 100644 --- a/supervisor/tests/test_end_to_end.py +++ b/supervisor/tests/test_end_to_end.py @@ -426,3 +426,32 @@ def test_pull_request_1578_echo_supervisord_conf(self): echo_supervisord_conf = pexpect.spawn(sys.executable, args, encoding='utf-8') self.addCleanup(echo_supervisord_conf.kill, signal.SIGKILL) echo_supervisord_conf.expect_exact('Sample supervisor config file') + + def test_issue_1596_asyncore_close_does_not_crash(self): + """If the socket is already closed when socket.shutdown(socket.SHUT_RDWR) + is called in the close() method of an asyncore dispatcher, an exception + will be raised (at least with Python 3.11.7 on macOS 14.2.1). If it is + not caught in that method, supervisord will crash.""" + filename = resource_filename(__package__, 'fixtures/issue-1596.conf') + args = ['-m', 'supervisor.supervisord', '-c', filename] + supervisord = pexpect.spawn(sys.executable, args, encoding='utf-8') + self.addCleanup(supervisord.kill, signal.SIGINT) + supervisord.expect_exact('supervisord started with pid') + + from supervisor.compat import xmlrpclib + from supervisor.xmlrpc import SupervisorTransport + + socket_url = 'unix:///tmp/issue-1596.sock' + dummy_url = 'http://transport.ignores.host/RPC2' + + # supervisord will crash after close() if it has the bug + t1 = SupervisorTransport('', '', socket_url) + s1 = xmlrpclib.ServerProxy(dummy_url, t1) + s1.system.listMethods() + t1.close() + + # this call will only succeed if supervisord did not crash + t2 = SupervisorTransport('', '', socket_url) + s2 = xmlrpclib.ServerProxy(dummy_url, t2) + s2.system.listMethods() + t2.close()