mirror of
https://github.com/frappe/gunicorn.git
synced 2026-07-02 18:51:31 +08:00
Use user-writable default path for control socket (#3551)
The previous default /run/gunicorn.ctl requires root permissions. Now uses $XDG_RUNTIME_DIR/gunicorn.ctl if available, otherwise $HOME/.gunicorn/gunicorn.ctl. This works on Linux, FreeBSD, OpenBSD, and macOS without requiring elevated privileges. - Add _get_default_control_socket() helper in config.py - Create parent directory automatically with 0o700 permissions - Update gunicornc CLI to use the same default path - Add unit tests for path selection and directory creation
This commit is contained in:
parent
3667a10478
commit
0ad47db800
@ -54,7 +54,9 @@ A WSGI application path in pattern ``$(MODULE_NAME):$(VARIABLE_NAME)``.
|
|||||||
|
|
||||||
**Command line:** `--control-socket PATH`
|
**Command line:** `--control-socket PATH`
|
||||||
|
|
||||||
**Default:** `'/run/gunicorn.ctl'`
|
**Default:**
|
||||||
|
|
||||||
|
$XDG_RUNTIME_DIR/gunicorn.ctl or $HOME/.gunicorn/gunicorn.ctl
|
||||||
|
|
||||||
Unix socket path for control interface.
|
Unix socket path for control interface.
|
||||||
|
|
||||||
@ -62,9 +64,9 @@ The control socket allows runtime management of Gunicorn via the
|
|||||||
``gunicornc`` command-line tool. Commands include viewing worker
|
``gunicornc`` command-line tool. Commands include viewing worker
|
||||||
status, adjusting worker count, and graceful reload/shutdown.
|
status, adjusting worker count, and graceful reload/shutdown.
|
||||||
|
|
||||||
By default, creates ``/run/gunicorn.ctl`` (requires write access to
|
Default: ``$XDG_RUNTIME_DIR/gunicorn.ctl`` if XDG_RUNTIME_DIR is set,
|
||||||
``/run``). For user-level deployments, specify a different path such
|
otherwise ``$HOME/.gunicorn/gunicorn.ctl``. The parent directory is
|
||||||
as ``/tmp/gunicorn.ctl`` or ``~/.gunicorn.ctl``.
|
created automatically if needed.
|
||||||
|
|
||||||
Use ``--no-control-socket`` to disable.
|
Use ``--no-control-socket`` to disable.
|
||||||
|
|
||||||
|
|||||||
@ -3128,13 +3128,32 @@ class DirtyWorkerExit(Setting):
|
|||||||
|
|
||||||
# Control Socket Settings
|
# Control Socket Settings
|
||||||
|
|
||||||
|
|
||||||
|
def _get_default_control_socket():
|
||||||
|
"""Get default control socket path based on available directories.
|
||||||
|
|
||||||
|
Prefers XDG_RUNTIME_DIR if available (standard on Linux, sometimes BSD),
|
||||||
|
falls back to $HOME/.gunicorn/ directory.
|
||||||
|
"""
|
||||||
|
# Prefer XDG_RUNTIME_DIR if available
|
||||||
|
xdg_runtime = os.environ.get('XDG_RUNTIME_DIR')
|
||||||
|
if xdg_runtime and os.path.isdir(xdg_runtime):
|
||||||
|
return os.path.join(xdg_runtime, 'gunicorn.ctl')
|
||||||
|
|
||||||
|
# Fall back to $HOME/.gunicorn/
|
||||||
|
home = os.path.expanduser('~')
|
||||||
|
gunicorn_dir = os.path.join(home, '.gunicorn')
|
||||||
|
return os.path.join(gunicorn_dir, 'gunicorn.ctl')
|
||||||
|
|
||||||
|
|
||||||
class ControlSocket(Setting):
|
class ControlSocket(Setting):
|
||||||
name = "control_socket"
|
name = "control_socket"
|
||||||
section = "Control"
|
section = "Control"
|
||||||
cli = ["--control-socket"]
|
cli = ["--control-socket"]
|
||||||
meta = "PATH"
|
meta = "PATH"
|
||||||
validator = validate_string
|
validator = validate_string
|
||||||
default = "/run/gunicorn.ctl"
|
default = _get_default_control_socket()
|
||||||
|
default_doc = "$XDG_RUNTIME_DIR/gunicorn.ctl or $HOME/.gunicorn/gunicorn.ctl"
|
||||||
desc = """\
|
desc = """\
|
||||||
Unix socket path for control interface.
|
Unix socket path for control interface.
|
||||||
|
|
||||||
@ -3142,9 +3161,9 @@ class ControlSocket(Setting):
|
|||||||
``gunicornc`` command-line tool. Commands include viewing worker
|
``gunicornc`` command-line tool. Commands include viewing worker
|
||||||
status, adjusting worker count, and graceful reload/shutdown.
|
status, adjusting worker count, and graceful reload/shutdown.
|
||||||
|
|
||||||
By default, creates ``/run/gunicorn.ctl`` (requires write access to
|
Default: ``$XDG_RUNTIME_DIR/gunicorn.ctl`` if XDG_RUNTIME_DIR is set,
|
||||||
``/run``). For user-level deployments, specify a different path such
|
otherwise ``$HOME/.gunicorn/gunicorn.ctl``. The parent directory is
|
||||||
as ``/tmp/gunicorn.ctl`` or ``~/.gunicorn.ctl``.
|
created automatically if needed.
|
||||||
|
|
||||||
Use ``--no-control-socket`` to disable.
|
Use ``--no-control-socket`` to disable.
|
||||||
|
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import json
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
from gunicorn.config import _get_default_control_socket
|
||||||
from gunicorn.ctl.client import ControlClient, ControlClientError, parse_command
|
from gunicorn.ctl.client import ControlClient, ControlClientError, parse_command
|
||||||
|
|
||||||
|
|
||||||
@ -405,8 +406,8 @@ Examples:
|
|||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'-s', '--socket',
|
'-s', '--socket',
|
||||||
default='gunicorn.ctl',
|
default=_get_default_control_socket(),
|
||||||
help='Control socket path (default: gunicorn.ctl in current directory)'
|
help='Control socket path (default: auto-detected based on XDG_RUNTIME_DIR or ~/.gunicorn/)'
|
||||||
)
|
)
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
|
|||||||
@ -187,6 +187,11 @@ class ControlSocketServer:
|
|||||||
"""Main async server loop."""
|
"""Main async server loop."""
|
||||||
self._loop = asyncio.get_running_loop()
|
self._loop = asyncio.get_running_loop()
|
||||||
|
|
||||||
|
# Create parent directory if needed (for ~/.gunicorn/)
|
||||||
|
socket_dir = os.path.dirname(self.socket_path)
|
||||||
|
if socket_dir and not os.path.exists(socket_dir):
|
||||||
|
os.makedirs(socket_dir, mode=0o700)
|
||||||
|
|
||||||
# Remove socket if it exists
|
# Remove socket if it exists
|
||||||
if os.path.exists(self.socket_path):
|
if os.path.exists(self.socket_path):
|
||||||
os.unlink(self.socket_path)
|
os.unlink(self.socket_path)
|
||||||
|
|||||||
@ -361,3 +361,59 @@ class TestControlSocketServerPermissions:
|
|||||||
assert mode == 0o660
|
assert mode == 0o660
|
||||||
finally:
|
finally:
|
||||||
server.stop()
|
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()
|
||||||
|
|||||||
@ -5,10 +5,12 @@
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
import tempfile
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from gunicorn import config
|
from gunicorn import config
|
||||||
|
from gunicorn.config import _get_default_control_socket
|
||||||
from gunicorn.app.base import Application
|
from gunicorn.app.base import Application
|
||||||
from gunicorn.app.wsgiapp import WSGIApplication
|
from gunicorn.app.wsgiapp import WSGIApplication
|
||||||
from gunicorn.errors import ConfigError
|
from gunicorn.errors import ConfigError
|
||||||
@ -551,3 +553,39 @@ def test_str():
|
|||||||
assert False, 'missing expected setting lines? {}'.format(
|
assert False, 'missing expected setting lines? {}'.format(
|
||||||
OUTPUT_MATCH.keys()
|
OUTPUT_MATCH.keys()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Tests for _get_default_control_socket
|
||||||
|
|
||||||
|
class TestGetDefaultControlSocket:
|
||||||
|
"""Tests for the _get_default_control_socket function."""
|
||||||
|
|
||||||
|
def test_uses_xdg_runtime_dir_when_set_and_exists(self, monkeypatch):
|
||||||
|
"""When XDG_RUNTIME_DIR is set and exists, use it."""
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
monkeypatch.setenv('XDG_RUNTIME_DIR', tmpdir)
|
||||||
|
result = _get_default_control_socket()
|
||||||
|
assert result == os.path.join(tmpdir, 'gunicorn.ctl')
|
||||||
|
|
||||||
|
def test_falls_back_when_xdg_runtime_dir_not_exists(self, monkeypatch):
|
||||||
|
"""When XDG_RUNTIME_DIR is set but doesn't exist, fall back to home."""
|
||||||
|
monkeypatch.setenv('XDG_RUNTIME_DIR', '/nonexistent/path/that/does/not/exist')
|
||||||
|
monkeypatch.setenv('HOME', '/home/testuser')
|
||||||
|
result = _get_default_control_socket()
|
||||||
|
assert result == '/home/testuser/.gunicorn/gunicorn.ctl'
|
||||||
|
|
||||||
|
def test_falls_back_when_xdg_runtime_dir_not_set(self, monkeypatch):
|
||||||
|
"""When XDG_RUNTIME_DIR is not set, use home directory."""
|
||||||
|
monkeypatch.delenv('XDG_RUNTIME_DIR', raising=False)
|
||||||
|
monkeypatch.setenv('HOME', '/home/testuser')
|
||||||
|
result = _get_default_control_socket()
|
||||||
|
assert result == '/home/testuser/.gunicorn/gunicorn.ctl'
|
||||||
|
|
||||||
|
def test_uses_home_directory_structure(self, monkeypatch):
|
||||||
|
"""Verify the path structure uses .gunicorn subdirectory."""
|
||||||
|
monkeypatch.delenv('XDG_RUNTIME_DIR', raising=False)
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
monkeypatch.setenv('HOME', tmpdir)
|
||||||
|
result = _get_default_control_socket()
|
||||||
|
assert result == os.path.join(tmpdir, '.gunicorn', 'gunicorn.ctl')
|
||||||
|
assert result.endswith('.gunicorn/gunicorn.ctl')
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user