mirror of
https://github.com/frappe/gunicorn.git
synced 2026-07-01 10:11:30 +08:00
feat(companion): Apply cwd and env in spawned companion child
Child runs _apply_environment before the target: os.chdir(cwd) then os.environ.update(env). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
5639d467f3
commit
ea2748a209
@ -672,7 +672,7 @@ No per-companion logic in Arbiter.
|
||||
- [x] Add status description helpers.
|
||||
- [x] Add `CompanionManager` skeleton.
|
||||
- [x] Spawn one companion process from the manager.
|
||||
- [ ] Apply `cwd` and `env` before target.
|
||||
- [x] Apply `cwd` and `env` before target.
|
||||
- [ ] Redirect `stdout` and `stderr`.
|
||||
- [ ] Reap exited companion processes.
|
||||
- [ ] Implement `STARTING -> RUNNING` using `startsecs`.
|
||||
|
||||
@ -3,12 +3,18 @@
|
||||
# See the NOTICE for more information.
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import os
|
||||
import time
|
||||
from typing import TYPE_CHECKING, Callable, Iterable, Union
|
||||
|
||||
from gunicorn.companion.process import CompanionProcess, State
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from gunicorn.companion.process import CompanionConfig
|
||||
|
||||
|
||||
class CompanionManager:
|
||||
"""Forks and supervises companion processes.
|
||||
@ -19,12 +25,12 @@ class CompanionManager:
|
||||
socket, and the run loop arrive in later tasks.
|
||||
"""
|
||||
|
||||
def __init__(self, configs, log):
|
||||
def __init__(self, configs: Iterable[CompanionConfig], log):
|
||||
self.log = log
|
||||
self.pid = os.getpid()
|
||||
self.processes = {c.name: CompanionProcess(c) for c in configs}
|
||||
|
||||
def spawn_process(self, proc):
|
||||
def spawn_process(self, proc: CompanionProcess) -> int:
|
||||
"""Fork one companion.
|
||||
|
||||
Parent records the pid and moves the companion to STARTING. Child
|
||||
@ -41,6 +47,7 @@ class CompanionManager:
|
||||
return pid
|
||||
|
||||
try:
|
||||
self._apply_environment(proc.config)
|
||||
target = self._resolve_target(proc.config.target)
|
||||
target()
|
||||
except SystemExit:
|
||||
@ -51,7 +58,20 @@ class CompanionManager:
|
||||
os._exit(0)
|
||||
|
||||
@staticmethod
|
||||
def _resolve_target(target):
|
||||
def _apply_environment(config: CompanionConfig) -> None:
|
||||
"""Apply ``cwd`` and ``env`` in the child before running the target.
|
||||
|
||||
cwd is changed first so a relative path in env (or the target itself)
|
||||
resolves against it. env is merged onto the inherited environment, not
|
||||
replaced, so the companion keeps the manager's variables.
|
||||
"""
|
||||
if config.cwd:
|
||||
os.chdir(config.cwd)
|
||||
if config.env:
|
||||
os.environ.update(config.env)
|
||||
|
||||
@staticmethod
|
||||
def _resolve_target(target: Union[Callable, str]) -> Callable:
|
||||
"""Return the zero-arg callable for a companion target.
|
||||
|
||||
Accepts an already-callable target or a ``"module:attr"`` import
|
||||
|
||||
@ -36,6 +36,24 @@ def test_resolve_target_rejects_bad_string():
|
||||
CompanionManager._resolve_target("no_colon")
|
||||
|
||||
|
||||
def test_apply_environment_sets_cwd_and_env():
|
||||
config = CompanionConfig(name="rq", target=lambda: None,
|
||||
cwd="/tmp", env={"COMPANION_X": "1"})
|
||||
with mock.patch("os.chdir") as chdir, \
|
||||
mock.patch.dict("os.environ", {}, clear=False):
|
||||
CompanionManager._apply_environment(config)
|
||||
chdir.assert_called_once_with("/tmp")
|
||||
import os
|
||||
assert os.environ["COMPANION_X"] == "1"
|
||||
|
||||
|
||||
def test_apply_environment_noop_without_cwd_env():
|
||||
config = CompanionConfig(name="rq", target=lambda: None)
|
||||
with mock.patch("os.chdir") as chdir:
|
||||
CompanionManager._apply_environment(config)
|
||||
chdir.assert_not_called()
|
||||
|
||||
|
||||
def test_spawn_parent_records_pid_and_starting():
|
||||
mgr = make_manager("rq")
|
||||
proc = mgr.processes["rq"]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user