Fix infinite recursion in import hook and stdout contamination in apply.sh

sitecustomize.py: exec_module was calling importlib.util.find_spec(fullname)
after Python had already added the module to sys.modules. find_spec
short-circuits to sys.modules[name].__spec__, which is our own wrapper spec,
so real_spec.loader.exec_module(module) immediately called exec_module again
— infinite recursion. Fix: resolve the real file spec in find_spec (before
sys.modules is populated) and store it on the Loader. exec_module now uses
the stored spec directly with no find_spec call.

Also move _mark_done into the finally block so a module that fails to load
doesn't trigger infinite retry loops on subsequent import attempts.

apply.sh: warn() writes to stdout. Inside $(find_mw_python), stdout is
captured by the command substitution, so any warn() call folded its text
into $PYTHON — causing every subsequent "$PYTHON" invocation to fail. Fix:
emit the fallback warning to stderr (>&2) so it goes to the log without
being captured.
This commit is contained in:
2026-06-15 02:25:51 +00:00
parent 01bf5f8603
commit 2d32c81485
2 changed files with 32 additions and 17 deletions
+2 -1
View File
@@ -53,8 +53,9 @@ find_mw_python() {
fi
# Verify the chosen interpreter can actually import middlewared.
# Use >&2 so this message goes to stderr, not captured by $(...) substitution.
if ! "$py" -c "import middlewared" 2>/dev/null; then
warn "Detected Python '$py' cannot import middlewared; falling back to python3"
echo "WARNING: '$py' cannot import middlewared; falling back to python3" >&2
py="python3"
fi
+30 -16
View File
@@ -42,12 +42,32 @@ class _Finder:
def find_spec(self, fullname, path, target=None): # noqa: ARG002
import importlib.machinery
import importlib.util
if (
fullname in self._targets
and fullname not in self._done
and fullname not in self._loading
):
return importlib.machinery.ModuleSpec(fullname, _Loader(self, fullname))
# Find the real file spec HERE, before Python adds the module to
# sys.modules. If we deferred this to exec_module, find_spec would
# short-circuit via sys.modules[fullname].__spec__ (our own spec) and
# exec_module would call itself recursively forever.
self._loading.add(fullname)
try:
real_spec = importlib.util.find_spec(fullname)
finally:
self._loading.discard(fullname)
if real_spec is None:
return None # module doesn't exist; don't intercept
return importlib.machinery.ModuleSpec(
fullname,
_Loader(self, fullname, real_spec),
origin=real_spec.origin,
is_package=real_spec.submodule_search_locations is not None,
)
return None
def _mark_done(self, fullname):
@@ -60,37 +80,31 @@ class _Finder:
class _Loader:
def __init__(self, finder, fullname):
def __init__(self, finder, fullname, real_spec):
self._finder = finder
self._fullname = fullname
self._real_spec = real_spec
def create_module(self, spec): # noqa: ARG002
return None # use Python's default module creation
def exec_module(self, module):
import importlib.util
fullname = self._fullname
self._finder._loading.add(fullname)
real_spec = self._real_spec
try:
# find_spec for the real file — our finder returns None while
# fullname is in _loading, so the normal finders handle this.
real_spec = importlib.util.find_spec(fullname)
if real_spec is None:
raise ImportError(f"No module named {fullname!r}")
real_spec.loader.exec_module(module)
# Fix module metadata so it looks like a normal import.
module.__spec__ = real_spec
module.__loader__ = real_spec.loader
if getattr(real_spec, "origin", None):
if real_spec.origin:
module.__file__ = real_spec.origin
finally:
self._finder._loading.discard(fullname)
self._finder._mark_done(fullname)
# Mark done even on failure so a broken module doesn't cause
# infinite retry loops on subsequent import attempts.
self._finder._mark_done(fullname)
# exec_module succeeded — apply our patch.
try:
_PATCHES[fullname](module)
except Exception as exc: