# # This file is part of gunicorn released under the MIT license. # See the NOTICE for more information. """Tests for control socket server.""" import os import tempfile import time from unittest.mock import MagicMock import pytest from gunicorn.ctl.server import ControlSocketServer from gunicorn.ctl.client import ControlClient class MockWorker: """Mock worker for testing.""" def __init__(self, pid, age, booted=True, aborted=False): self.pid = pid self.age = age self.booted = booted self.aborted = aborted self.tmp = MagicMock() self.tmp.last_update.return_value = time.monotonic() class MockConfig: """Mock config for testing.""" def __init__(self): self.bind = ['127.0.0.1:8000'] self.workers = 4 self.worker_class = 'sync' self.threads = 1 self.timeout = 30 self.graceful_timeout = 30 self.keepalive = 2 self.max_requests = 0 self.max_requests_jitter = 0 self.worker_connections = 1000 self.preload_app = False self.daemon = False self.pidfile = None self.proc_name = 'test_app' self.reload = False self.dirty_workers = 0 self.dirty_apps = [] self.dirty_timeout = 30 self.control_socket = 'gunicorn.ctl' self.control_socket_disable = False class MockLog: """Mock logger for testing.""" def debug(self, msg, *args): pass def info(self, msg, *args): pass def warning(self, msg, *args): pass def error(self, msg, *args): pass def exception(self, msg, *args): pass class MockArbiter: """Mock arbiter for testing.""" def __init__(self): self.cfg = MockConfig() self.log = MockLog() self.pid = 12345 self.WORKERS = {} self.LISTENERS = [] self.dirty_arbiter_pid = 0 self.dirty_arbiter = None self.num_workers = 4 self._stats = { 'start_time': time.time() - 3600, 'workers_spawned': 10, 'workers_killed': 5, 'reloads': 2, } def wakeup(self): pass class TestControlSocketServerInit: """Tests for server initialization.""" def test_init(self): """Test server initialization.""" arbiter = MockArbiter() server = ControlSocketServer(arbiter, "/tmp/test.sock", 0o600) assert server.arbiter is arbiter assert server.socket_path == "/tmp/test.sock" assert server.socket_mode == 0o600 assert server._running is False class TestControlSocketServerLifecycle: """Tests for server start/stop.""" def test_start_stop(self): """Test starting and stopping the server.""" with tempfile.TemporaryDirectory() as tmpdir: socket_path = os.path.join(tmpdir, "test.sock") arbiter = MockArbiter() server = ControlSocketServer(arbiter, socket_path) server.start() # Wait for server to start for _ in range(50): if os.path.exists(socket_path): break time.sleep(0.1) time.sleep(0.2) # Extra wait for server to be fully ready assert os.path.exists(socket_path) server.stop() # Wait for cleanup time.sleep(0.2) # Socket should be cleaned up assert not os.path.exists(socket_path) def test_start_already_running(self): """Test that start is idempotent.""" with tempfile.TemporaryDirectory() as tmpdir: socket_path = os.path.join(tmpdir, "test.sock") arbiter = MockArbiter() server = ControlSocketServer(arbiter, socket_path) server.start() first_thread = server._thread server.start() assert server._thread is first_thread server.stop() def test_stop_not_running(self): """Test stopping a non-running server.""" arbiter = MockArbiter() server = ControlSocketServer(arbiter, "/tmp/test.sock") # Should not raise server.stop() class TestControlSocketServerIntegration: """Integration tests for server with client.""" def test_show_workers(self): """Test show workers command.""" with tempfile.TemporaryDirectory() as tmpdir: socket_path = os.path.join(tmpdir, "test.sock") arbiter = MockArbiter() arbiter.WORKERS = { 1001: MockWorker(1001, 1), 1002: MockWorker(1002, 2), } server = ControlSocketServer(arbiter, socket_path) server.start() # Wait for server to start for _ in range(50): if os.path.exists(socket_path): break time.sleep(0.1) time.sleep(0.2) # Extra wait for server to be fully ready try: with ControlClient(socket_path, timeout=5.0) as client: result = client.send_command("show workers") assert result["count"] == 2 assert len(result["workers"]) == 2 finally: server.stop() def test_show_stats(self): """Test show stats command.""" with tempfile.TemporaryDirectory() as tmpdir: socket_path = os.path.join(tmpdir, "test.sock") arbiter = MockArbiter() server = ControlSocketServer(arbiter, socket_path) server.start() for _ in range(50): if os.path.exists(socket_path): break time.sleep(0.1) time.sleep(0.2) # Extra wait for server to be fully ready try: with ControlClient(socket_path, timeout=5.0) as client: result = client.send_command("show stats") assert result["pid"] == 12345 assert result["workers_spawned"] == 10 finally: server.stop() def test_help_command(self): """Test help command.""" with tempfile.TemporaryDirectory() as tmpdir: socket_path = os.path.join(tmpdir, "test.sock") arbiter = MockArbiter() server = ControlSocketServer(arbiter, socket_path) server.start() for _ in range(50): if os.path.exists(socket_path): break time.sleep(0.1) time.sleep(0.2) # Extra wait for server to be fully ready try: with ControlClient(socket_path, timeout=5.0) as client: result = client.send_command("help") assert "commands" in result assert "show workers" in result["commands"] finally: server.stop() def test_worker_add(self): """Test worker add command.""" with tempfile.TemporaryDirectory() as tmpdir: socket_path = os.path.join(tmpdir, "test.sock") arbiter = MockArbiter() arbiter.wakeup = MagicMock() server = ControlSocketServer(arbiter, socket_path) server.start() for _ in range(50): if os.path.exists(socket_path): break time.sleep(0.1) time.sleep(0.2) # Extra wait for server to be fully ready try: with ControlClient(socket_path, timeout=5.0) as client: result = client.send_command("worker add 2") assert result["added"] == 2 assert result["total"] == 6 assert arbiter.num_workers == 6 arbiter.wakeup.assert_called() finally: server.stop() def test_invalid_command(self): """Test handling invalid command.""" with tempfile.TemporaryDirectory() as tmpdir: socket_path = os.path.join(tmpdir, "test.sock") arbiter = MockArbiter() server = ControlSocketServer(arbiter, socket_path) server.start() for _ in range(50): if os.path.exists(socket_path): break time.sleep(0.1) time.sleep(0.2) # Extra wait for server to be fully ready try: with ControlClient(socket_path, timeout=5.0) as client: with pytest.raises(Exception) as exc_info: client.send_command("invalid_command") assert "Unknown command" in str(exc_info.value) finally: server.stop() def test_multiple_commands(self): """Test sending multiple commands on same connection.""" with tempfile.TemporaryDirectory() as tmpdir: socket_path = os.path.join(tmpdir, "test.sock") arbiter = MockArbiter() arbiter.WORKERS = {1001: MockWorker(1001, 1)} server = ControlSocketServer(arbiter, socket_path) server.start() for _ in range(50): if os.path.exists(socket_path): break time.sleep(0.1) time.sleep(0.2) # Extra wait for server to be fully ready try: with ControlClient(socket_path, timeout=5.0) as client: result1 = client.send_command("show workers") result2 = client.send_command("show stats") result3 = client.send_command("help") assert result1["count"] == 1 assert result2["pid"] == 12345 assert "commands" in result3 finally: server.stop() class TestControlSocketServerPermissions: """Tests for socket permissions.""" @pytest.mark.skipif( os.uname().sysname == "FreeBSD", reason="FreeBSD socket permissions behavior differs" ) def test_socket_permissions(self): """Test that socket is created with correct permissions.""" with tempfile.TemporaryDirectory() as tmpdir: socket_path = os.path.join(tmpdir, "test.sock") arbiter = MockArbiter() server = ControlSocketServer(arbiter, socket_path, 0o660) server.start() # Wait for socket to exist for _ in range(50): if os.path.exists(socket_path): break time.sleep(0.1) # Extra wait for chmod to complete time.sleep(0.2) try: mode = os.stat(socket_path).st_mode & 0o777 assert mode == 0o660 finally: server.stop() class TestControlSocketServerDirectoryCreation: """Tests for automatic directory creation.""" def test_creates_parent_directory(self): """Test that server creates parent directory if it doesn't exist.""" with tempfile.TemporaryDirectory() as tmpdir: # Create a path with a non-existent subdirectory subdir = os.path.join(tmpdir, '.gunicorn') socket_path = os.path.join(subdir, 'gunicorn.ctl') assert not os.path.exists(subdir) arbiter = MockArbiter() server = ControlSocketServer(arbiter, socket_path) server.start() # Wait for socket to exist for _ in range(50): if os.path.exists(socket_path): break time.sleep(0.1) try: # Directory should have been created assert os.path.isdir(subdir) # Directory should have restricted permissions (0o700) mode = os.stat(subdir).st_mode & 0o777 assert mode == 0o700 # Socket should exist assert os.path.exists(socket_path) finally: server.stop() def test_works_with_existing_directory(self): """Test that server works when parent directory already exists.""" with tempfile.TemporaryDirectory() as tmpdir: socket_path = os.path.join(tmpdir, 'gunicorn.ctl') arbiter = MockArbiter() server = ControlSocketServer(arbiter, socket_path) server.start() # Wait for socket to exist for _ in range(50): if os.path.exists(socket_path): break time.sleep(0.1) try: assert os.path.exists(socket_path) finally: server.stop()