Fix PREINIT 10-second timeout killing apply.sh before patches land

Registers the boot hook with timeout:120 so TrueNAS gives apply.sh
two minutes instead of the default ten seconds.  Also consolidates
apply.sh Python subprocess count from ~8 to 2, cutting startup
overhead from ~12-16s to ~2-4s.

Bumps all scripts to v0.0.3.
This commit is contained in:
2026-06-22 15:40:54 +00:00
parent c8de9971e7
commit ee190f558f
6 changed files with 120 additions and 123 deletions
+25
View File
@@ -1,5 +1,30 @@
# Changelog
## v0.0.3 — 2026-06-22
### Fixed
- **`patch/apply.sh` silently killed by the 10-second PREINIT timeout.**
TrueNAS PREINIT initshutdownscripts have a 10-second default timeout. The
previous apply.sh ran approximately 8 Python subprocesses (each ~1-2 s), so it
was routinely killed mid-run. Symptoms: patches not applied after reboot, but
re-running `bash apply.sh` manually (no timeout) always succeeded.
Fix: `install.sh` now registers the hook with `"timeout": 120`. Existing
installations are updated to the new timeout on the next `bash install.sh` run.
Additionally, `patch/apply.sh` consolidates its Python subprocess invocations
from ~8 down to 2, reducing startup overhead from ~12-16 s to ~2-4 s — well
within the new 120-second budget.
### Changed
- `find_mw_python` in `apply.sh` no longer spawns a separate Python process to
verify the interpreter can import `middlewared`. Verification is now implicit in
the combined path-discovery subprocess that follows.
---
## v0.0.2 — 2026-06-19
### Fixed
+4 -4
View File
@@ -16,7 +16,7 @@
set -euo pipefail
VERSION="0.0.2"
VERSION="0.0.3"
# The directory containing install.sh is the permanent install location.
PATCH_DIR="$(cd "$(dirname "$0")" && pwd)"
@@ -73,9 +73,9 @@ for s in json.load(sys.stdin):
" 2>/dev/null || true)
if [ -n "$EXISTING_ID" ]; then
echo "Already registered (id=$EXISTING_ID). Updating path and enabling ..."
echo "Already registered (id=$EXISTING_ID). Updating path, timeout, and enabling ..."
if ! _midclt_out=$(midclt call initshutdownscript.update "$EXISTING_ID" \
"{\"enabled\": true, \"script\": \"$PATCH_DIR/patch/apply.sh\"}" 2>&1); then
"{\"enabled\": true, \"script\": \"$PATCH_DIR/patch/apply.sh\", \"timeout\": 120}" 2>&1); then
echo "ERROR: Failed to update PREINIT hook (id=$EXISTING_ID)." >&2
[ -n "$_midclt_out" ] && echo " midclt: $_midclt_out" >&2
echo " To remove the stale entry and retry:" >&2
@@ -84,7 +84,7 @@ if [ -n "$EXISTING_ID" ]; then
fi
else
if ! _midclt_out=$(midclt call initshutdownscript.create \
"{\"type\":\"SCRIPT\",\"script\":\"$PATCH_DIR/patch/apply.sh\",\"when\":\"PREINIT\",\"enabled\":true,\"comment\":\"$_HOOK_COMMENT\"}" \
"{\"type\":\"SCRIPT\",\"script\":\"$PATCH_DIR/patch/apply.sh\",\"when\":\"PREINIT\",\"enabled\":true,\"timeout\":120,\"comment\":\"$_HOOK_COMMENT\"}" \
2>&1); then
echo "ERROR: Failed to register PREINIT hook." >&2
[ -n "$_midclt_out" ] && echo " midclt: $_midclt_out" >&2
+84 -112
View File
@@ -18,7 +18,7 @@
# Derive PATCH_DIR from this script's location (parent of the patch/ directory).
PATCH_DIR="$(cd "$(dirname "$0")/.." && pwd)"
LOG="$PATCH_DIR/apply.log"
VERSION="0.0.2"
VERSION="0.0.3"
# Rotate log at 512 KB to avoid unbounded growth on a system volume.
# Keep two prior generations (.1 and .2) so the last three boots are always available.
@@ -68,6 +68,8 @@ _ensure_writable() {
# Find the Python interpreter that middlewared actually uses.
# On TrueNAS SCALE, /usr/bin/middlewared is usually a Python entry-point script
# with a shebang pointing at the right interpreter (system or venv).
# The shebang is read with dd (no Python startup cost); import verification is
# deferred to the combined subprocess below which handles failure gracefully.
find_mw_python() {
local py="python3"
local shebang=""
@@ -83,47 +85,56 @@ find_mw_python() {
fi
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
echo "WARNING: '$py' cannot import middlewared; falling back to python3" >&2
py="python3"
if ! "$py" -c "import middlewared" 2>/dev/null; then
echo "WARNING: 'python3' also cannot import middlewared; backend patch will be skipped" >&2
fi
fi
echo "$py"
}
PYTHON=$(find_mw_python)
echo "Using Python: $PYTHON"
# ── Native support check ──────────────────────────────────────────────────────
# If TrueNAS has shipped native B2 restic support, this patch is no longer
# needed. Set the kill switch and instruct the user to uninstall cleanly.
# ── Discover paths + native support check (single Python subprocess) ──────────
# Combines what were previously four separate Python invocations into one to
# avoid repeated interpreter startup overhead under the PREINIT timeout budget.
_tc_info=$("$PYTHON" -c "
import inspect, os, sys
result = {'native': 'no', 'site_pkg': '', 'mw_dir': ''}
try:
import middlewared
mw_file = os.path.abspath(middlewared.__file__)
result['mw_dir'] = os.path.dirname(mw_file)
result['site_pkg'] = os.path.dirname(os.path.dirname(mw_file))
except ImportError:
try:
import site
result['site_pkg'] = site.getsitepackages()[0]
except Exception:
pass
_tc_native=$("$PYTHON" -c "
try:
import inspect
import middlewared.rclone.remote.b2 as _b2_mod
from middlewared.rclone.remote.b2 import B2RcloneRemote
if 'get_restic_config' not in B2RcloneRemote.__dict__:
print('no')
else:
if 'get_restic_config' in B2RcloneRemote.__dict__:
src = open(inspect.getfile(_b2_mod), encoding='utf-8', errors='replace').read()
if 'TRUECLOUD_PATCH' in src:
print('no')
else:
# Distinguish a real implementation from a stub that raises NotImplementedError.
if 'TRUECLOUD_PATCH' not in src:
try:
method_src = inspect.getsource(B2RcloneRemote.get_restic_config)
except (OSError, TypeError):
method_src = ''
print('no' if 'NotImplementedError' in method_src else 'yes')
if 'NotImplementedError' not in method_src:
result['native'] = 'yes'
except Exception:
print('no')
" 2>/dev/null || echo "no")
pass
print(result['native'])
print(result['site_pkg'])
print(result['mw_dir'])
" 2>/dev/null || printf 'no\n\n\n')
_tc_native=$(printf '%s' "$_tc_info" | sed -n '1p')
SITE_PKG=$(printf '%s' "$_tc_info" | sed -n '2p')
_MW_DIR=$(printf '%s' "$_tc_info" | sed -n '3p')
if [ "$_tc_native" = "yes" ]; then
echo "NOTICE: TrueNAS now provides native B2 restic support — truecloud-patch is no longer needed."
@@ -145,29 +156,6 @@ fi
echo "--- backend patch ---"
# Derive site-packages from where middlewared actually lives.
# getsitepackages()[0] may return the wrong directory; using middlewared.__file__
# ensures we patch files in the directory Python will actually read.
SITE_PKG=$("$PYTHON" -c "
import os
try:
import middlewared
print(os.path.dirname(os.path.dirname(os.path.abspath(middlewared.__file__))))
except ImportError:
import site
print(site.getsitepackages()[0])
" 2>/dev/null || true)
# MW_DIR is the middlewared package directory itself (one level below SITE_PKG).
_MW_DIR=$("$PYTHON" -c "
import os
try:
import middlewared
print(os.path.dirname(os.path.abspath(middlewared.__file__)))
except ImportError:
pass
" 2>/dev/null || true)
_b2_ok=0
_restic_ok=0
@@ -182,12 +170,13 @@ else
_B2_PY="$_MW_DIR/rclone/remote/b2.py"
_RESTIC_PY="$_MW_DIR/plugins/cloud_backup/restic.py"
# ── b2.py ─────────────────────────────────────────────────────────────
if [ -f "$_B2_PY" ]; then
if "$PYTHON" - "$_B2_PY" << 'PYEOF'
import sys
# ── patch b2.py + restic.py + hook_status.json (single subprocess) ──────
if "$PYTHON" - "$_B2_PY" "$_RESTIC_PY" "$PATCH_DIR/hook_status.json" << 'PYEOF'
import json, os, sys, time
BLOCK = """
b2_path, restic_path, status_path = sys.argv[1], sys.argv[2], sys.argv[3]
B2_BLOCK = """
# TRUECLOUD_PATCH — added by truenas-truecloud-patch/patch/apply.sh
def _tc_get_restic_config(task):
p = task["credentials"]["provider"]
@@ -197,34 +186,7 @@ B2RcloneRemote.get_restic_config = staticmethod(_tc_get_restic_config)
B2RcloneRemote.restic = True
"""
path = sys.argv[1]
with open(path, encoding="utf-8") as fh:
content = fh.read()
marker = "\n# TRUECLOUD_PATCH"
idx = content.find(marker)
base = content[:idx] if idx != -1 else content
patched = base.rstrip("\n") + "\n" + BLOCK
with open(path, "w", encoding="utf-8") as fh:
fh.write(patched)
PYEOF
then
echo "OK: Patched b2.py → $_B2_PY"
_b2_ok=1
else
echo "WARNING: Failed to patch b2.py"
fi
else
echo "WARNING: b2.py not found at $_B2_PY"
fi
# ── restic.py ─────────────────────────────────────────────────────────
if [ -f "$_RESTIC_PY" ]; then
if "$PYTHON" - "$_RESTIC_PY" << 'PYEOF'
import sys
BLOCK = """
RESTIC_BLOCK = """
# TRUECLOUD_PATCH — added by truenas-truecloud-patch/patch/apply.sh
try:
_tc_orig_get_restic_config = get_restic_config
@@ -267,56 +229,66 @@ else:
get_restic_config._truecloud_patched = True
"""
path = sys.argv[1]
def patch_file(path, block):
with open(path, encoding="utf-8") as fh:
content = fh.read()
marker = "\n# TRUECLOUD_PATCH"
idx = content.find(marker)
base = content[:idx] if idx != -1 else content
patched = base.rstrip("\n") + "\n" + BLOCK
with open(path, "w", encoding="utf-8") as fh:
fh.write(patched)
PYEOF
then
echo "OK: Patched restic.py → $_RESTIC_PY"
_restic_ok=1
else
echo "WARNING: Failed to patch restic.py"
fi
else
echo "WARNING: restic.py not found at $_RESTIC_PY"
fi
# Write hook_status.json so 'verify' reflects the current patch state.
"$PYTHON" -c "
import json, os, sys, time
b2_ok = sys.argv[1] == '1'
restic_ok = sys.argv[2] == '1'
fh.write(base.rstrip("\n") + "\n" + block)
b2_ok = restic_ok = False
if os.path.exists(b2_path):
try:
patch_file(b2_path, B2_BLOCK)
b2_ok = True
print(f"OK: Patched b2.py → {b2_path}")
except Exception as e:
print(f"WARNING: Failed to patch b2.py: {e}")
else:
print(f"WARNING: b2.py not found at {b2_path}")
if os.path.exists(restic_path):
try:
patch_file(restic_path, RESTIC_BLOCK)
restic_ok = True
print(f"OK: Patched restic.py → {restic_path}")
except Exception as e:
print(f"WARNING: Failed to patch restic.py: {e}")
else:
print(f"WARNING: restic.py not found at {restic_path}")
patches = {
'middlewared.rclone.remote.b2': {
'ok': b2_ok,
'detail': ('patched on disk in overlay at boot' if b2_ok
else 'b2.py not found or write failed'),
'detail': 'patched on disk in overlay at boot' if b2_ok else 'b2.py not found or write failed',
},
'middlewared.plugins.cloud_backup.restic': {
'ok': restic_ok,
'detail': ('patched on disk in overlay at boot' if restic_ok
else 'restic.py not found or write failed'),
'detail': 'patched on disk in overlay at boot' if restic_ok else 'restic.py not found or write failed',
},
}
payload = {'patched_at': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()),
'patches': patches}
sf = sys.argv[3]
tmp = sf + '.tmp'
payload = {'patched_at': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()), 'patches': patches}
tmp = status_path + '.tmp'
try:
with open(tmp, 'w') as f:
json.dump(payload, f, indent=2)
os.replace(tmp, sf)
os.replace(tmp, status_path)
print('OK: Wrote hook_status.json')
except OSError as e:
print(f'WARNING: Could not write hook_status.json: {e}')
" "$_b2_ok" "$_restic_ok" "$PATCH_DIR/hook_status.json" || true
sys.exit(0 if (b2_ok and restic_ok) else 1)
PYEOF
then
_b2_ok=1
_restic_ok=1
else
# Individual results already printed above; exit code 1 means at least one failed.
true
fi
fi
# ── Step 2: Angular bundle ────────────────────────────────────────────────────
+1 -1
View File
@@ -48,7 +48,7 @@ import sys
import urllib.error
import urllib.request
__version__ = "0.0.2"
__version__ = "0.0.3"
_PATCH_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
_STATUS_FILE = os.path.join(_PATCH_DIR, "hook_status.json")
+1 -1
View File
@@ -16,7 +16,7 @@
# rm /mnt/tank/truenas-truecloud-patch/disabled
# bash /mnt/tank/truenas-truecloud-patch/patch/apply.sh
VERSION="0.0.2"
VERSION="0.0.3"
PATCH_DIR="$(cd "$(dirname "$0")" && pwd)"
+1 -1
View File
@@ -3,7 +3,7 @@
set -euo pipefail
VERSION="0.0.2"
VERSION="0.0.3"
PATCH_DIR="$(cd "$(dirname "$0")" && pwd)"
_HOOK_COMMENT='TrueCloud provider patch (S3/B2)'