Fix four quality findings from re-review

sitecustomize.py: exec_module now records a FAIL status when the underlying
module load raises, instead of leaving hook_status.json unwritten. cmd_verify
will now show a diagnostic FAIL rather than the misleading "no status file".

sitecustomize.py: URL-fix loop covers --repository and --repository= in
addition to --repo/--repo=/-r; these are documented restic synonyms.

sitecustomize.py: _replace fallback now catches AttributeError in addition
to TypeError so an unrecognised ResticConfig return type silently falls back
to returning the unmodified result rather than crashing the backup job.

patch_ui.py: warns when FIND.subn produces a count other than 1, making
unexpected multi-replacement visible in the apply log.

uninstall.sh: find for JS bundle restore now includes /usr/share/truenas-ui,
matching all three entries in patch_ui.py's WEBUI_CANDIDATES.
This commit is contained in:
2026-06-15 03:58:06 +00:00
parent 91c607a707
commit 8829fbb9d4
3 changed files with 47 additions and 21 deletions
+6
View File
@@ -93,6 +93,12 @@ def main():
shutil.copy2(path, backup)
patched, count = FIND.subn(REPLACE, content)
if count != 1:
print(
f"[truecloud-patch] WARNING: {count} replacement(s) in {path}; "
f"expected exactly 1 — file an issue at "
f"https://github.com/sudolulo/truenas-truecloud-patch"
)
tmp = path + ".tmp"
try:
+38 -18
View File
@@ -99,9 +99,14 @@ class _Loader:
module.__loader__ = real_spec.loader
if real_spec.origin:
module.__file__ = real_spec.origin
finally:
# Mark done even on failure so a broken module doesn't cause
# infinite retry loops on subsequent import attempts.
except Exception as exc:
# Mark done to prevent infinite retry loops, then surface the failure
# in hook_status.json so cmd_verify shows a diagnostic FAIL rather
# than "no status file found".
self._finder._mark_done(fullname)
_record_status(fullname, ok=False,
detail=f"module load failed: {exc}")
raise
self._finder._mark_done(fullname)
# exec_module succeeded — apply our patch.
@@ -142,7 +147,8 @@ def _patch_b2(module):
cls.get_restic_config = staticmethod(_b2_restic_config)
cls.restic = True
sys.stderr.write("[truecloud-patch] B2 restic support enabled\n")
_record_status("middlewared.rclone.remote.b2", ok=True)
_record_status("middlewared.rclone.remote.b2", ok=True,
detail="method attached; credential fields verified at first backup")
def _patch_restic(module):
@@ -160,33 +166,38 @@ def _patch_restic(module):
# env construction, and everything else we don't own.
result = _orig(cloud_backup)
# Scan the built command for the repo argument and fix the URL if it
# has a stray leading slash: "b2:/bucket/path" → "b2:bucket/path".
# "scheme://path" is intentional (Storj) and must not be touched.
# Covers all three flag forms restic accepts:
# -r <url> (short, two-element)
# --repo <url> (long, two-element)
# --repo=<url> (long, single-element)
# Fix stray leading slash in repo URL: "b2:/bucket" → "b2:bucket".
# "scheme://path" (Storj) must not be touched.
# Covers all flag forms restic accepts (--repository is a documented synonym):
# -r <url> --repo <url> --repo=<url>
# --repository <url> --repository=<url>
cmd = list(result.cmd)
for i, part in enumerate(cmd):
if part.startswith("--repo="):
url = part[len("--repo="):]
if part.startswith("--repo=") or part.startswith("--repository="):
prefix, _, url = part.partition("=")
prefix += "="
scheme, sep, rest = url.partition(":")
if sep and rest.startswith("/") and not rest.startswith("//"):
cmd[i] = f"--repo={scheme}:{rest[1:]}"
cmd[i] = f"{prefix}{scheme}:{rest[1:]}"
try:
return dataclasses.replace(result, cmd=cmd)
except TypeError:
try:
return result._replace(cmd=cmd)
except AttributeError:
return result
break
if i and cmd[i - 1] in ("-r", "--repo"):
if i and cmd[i - 1] in ("-r", "--repo", "--repository"):
scheme, sep, rest = part.partition(":")
if sep and rest.startswith("/") and not rest.startswith("//"):
cmd[i] = f"{scheme}:{rest[1:]}"
try:
return dataclasses.replace(result, cmd=cmd)
except TypeError:
try:
return result._replace(cmd=cmd)
except AttributeError:
return result
break
return result
@@ -232,15 +243,21 @@ def _install():
return # kill switch
# Scope to the middlewared service process only — not midclt, debug scripts,
# or other tools that happen to share the same venv.
# Also covers python -m middlewared where argv[0] is the __main__.py path.
_argv0 = (sys.argv or [""])[0]
if os.path.basename(_argv0) != "middlewared":
_base = os.path.basename(_argv0)
if not (
_base == "middlewared"
or (_base == "__main__.py"
and os.path.basename(os.path.dirname(_argv0)) == "middlewared")
):
return
import importlib.util
if importlib.util.find_spec("middlewared") is None:
return
# If apply.sh displaced an existing sitecustomize.py, exec it first so any
# iX-provided startup code (path additions, codec registrations, etc.) still
# runs. We use a separate namespace so it cannot shadow our globals.
# vendor startup code (path additions, codec registrations, etc.) still runs.
# __file__ is absent in some embedded contexts; the empty fallback is safe.
_self = globals().get("__file__", "")
if _self:
_pre = _self + ".pre-truecloud-patch"
@@ -248,6 +265,9 @@ def _install():
try:
import builtins
with open(_pre, encoding="utf-8") as _fh:
# Separate globals dict prevents the exec'd code from
# shadowing our names; sys.path changes still take effect
# via the shared sys module object.
exec( # noqa: S102
compile(_fh.read(), _pre, "exec"),
{"__builtins__": builtins, "__file__": _pre,
+1 -1
View File
@@ -97,7 +97,7 @@ while IFS= read -r backup; do
mv "$backup" "$original"
echo " Restored: $original"
RESTORED=1
done < <(find /usr/share/truenas /var/www/truenas \
done < <(find /usr/share/truenas /usr/share/truenas-ui /var/www/truenas \
-name "*.js.pre-truecloud-patch" 2>/dev/null)
if [ "$RESTORED" -eq 0 ]; then