Compare commits

..
2 Commits
Author SHA1 Message Date
flan ee190f558f 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.
2026-06-22 15:40:54 +00:00
flan c8de9971e7 Print version in all scripts; add Updating section to README 2026-06-19 19:04:48 +00:00
7 changed files with 155 additions and 121 deletions
+25
View File
@@ -1,5 +1,30 @@
# Changelog # 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 ## v0.0.2 — 2026-06-19
### Fixed ### Fixed
+24
View File
@@ -120,6 +120,30 @@ call that path on every boot.
Refresh your browser. S3 and B2 credentials now appear in the Refresh your browser. S3 and B2 credentials now appear in the
**Data Protection → TrueCloud Backup → Add** credential dropdown. **Data Protection → TrueCloud Backup → Add** credential dropdown.
## Updating
To update to a new version of the patch:
```bash
cd /mnt/tank/truenas-truecloud-patch
# If install.sh was previously run as root, the .git directory may be owned
# by root. Fix it first, or just pull as root:
sudo git pull # easiest option
# — or —
sudo chown -R $(whoami) .git && git pull
bash install.sh
```
`install.sh` clears any stale kill switch, re-applies the updated patches,
and restarts middlewared. Run `python3 patch/create_task.py verify` afterwards
to confirm the patches loaded successfully.
Check [CHANGELOG.md](CHANGELOG.md) to see what changed between versions.
---
## Creating a task via CLI ## Creating a task via CLI
If the UI still shows only Storj after refreshing (e.g. the JS bundle pattern If the UI still shows only Storj after refreshing (e.g. the JS bundle pattern
+6 -4
View File
@@ -16,6 +16,8 @@
set -euo pipefail set -euo pipefail
VERSION="0.0.3"
# The directory containing install.sh is the permanent install location. # The directory containing install.sh is the permanent install location.
PATCH_DIR="$(cd "$(dirname "$0")" && pwd)" PATCH_DIR="$(cd "$(dirname "$0")" && pwd)"
_HOOK_COMMENT='TrueCloud provider patch (S3/B2)' _HOOK_COMMENT='TrueCloud provider patch (S3/B2)'
@@ -29,7 +31,7 @@ if [ ! -f "$PATCH_DIR/patch/apply.sh" ]; then
exit 1 exit 1
fi fi
echo "=== TrueNAS TrueCloud Provider Patch — Install ===" echo "=== TrueNAS TrueCloud Provider Patch v${VERSION} — Install ==="
echo "" echo ""
# ── Preflight ───────────────────────────────────────────────────────────────── # ── Preflight ─────────────────────────────────────────────────────────────────
@@ -71,9 +73,9 @@ for s in json.load(sys.stdin):
" 2>/dev/null || true) " 2>/dev/null || true)
if [ -n "$EXISTING_ID" ]; then 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" \ 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 echo "ERROR: Failed to update PREINIT hook (id=$EXISTING_ID)." >&2
[ -n "$_midclt_out" ] && echo " midclt: $_midclt_out" >&2 [ -n "$_midclt_out" ] && echo " midclt: $_midclt_out" >&2
echo " To remove the stale entry and retry:" >&2 echo " To remove the stale entry and retry:" >&2
@@ -82,7 +84,7 @@ if [ -n "$EXISTING_ID" ]; then
fi fi
else else
if ! _midclt_out=$(midclt call initshutdownscript.create \ 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 2>&1); then
echo "ERROR: Failed to register PREINIT hook." >&2 echo "ERROR: Failed to register PREINIT hook." >&2
[ -n "$_midclt_out" ] && echo " midclt: $_midclt_out" >&2 [ -n "$_midclt_out" ] && echo " midclt: $_midclt_out" >&2
+89 -116
View File
@@ -18,6 +18,7 @@
# Derive PATCH_DIR from this script's location (parent of the patch/ directory). # Derive PATCH_DIR from this script's location (parent of the patch/ directory).
PATCH_DIR="$(cd "$(dirname "$0")/.." && pwd)" PATCH_DIR="$(cd "$(dirname "$0")/.." && pwd)"
LOG="$PATCH_DIR/apply.log" LOG="$PATCH_DIR/apply.log"
VERSION="0.0.3"
# Rotate log at 512 KB to avoid unbounded growth on a system volume. # 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. # Keep two prior generations (.1 and .2) so the last three boots are always available.
@@ -27,7 +28,7 @@ if [ -f "$LOG" ] && [ "$(wc -c < "$LOG")" -gt 524288 ]; then
fi fi
exec >> "$LOG" 2>&1 exec >> "$LOG" 2>&1
echo "=== $(date -Iseconds) ===" echo "=== $(date -Iseconds) [v${VERSION}] ==="
# Kill switch: if this file exists, skip all patching and exit cleanly. # Kill switch: if this file exists, skip all patching and exit cleanly.
# Recovery: touch "$PATCH_DIR/disabled" (then reboot or restart middlewared). # Recovery: touch "$PATCH_DIR/disabled" (then reboot or restart middlewared).
@@ -67,6 +68,8 @@ _ensure_writable() {
# Find the Python interpreter that middlewared actually uses. # Find the Python interpreter that middlewared actually uses.
# On TrueNAS SCALE, /usr/bin/middlewared is usually a Python entry-point script # On TrueNAS SCALE, /usr/bin/middlewared is usually a Python entry-point script
# with a shebang pointing at the right interpreter (system or venv). # 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() { find_mw_python() {
local py="python3" local py="python3"
local shebang="" local shebang=""
@@ -82,47 +85,56 @@ find_mw_python() {
fi fi
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" echo "$py"
} }
PYTHON=$(find_mw_python) PYTHON=$(find_mw_python)
echo "Using Python: $PYTHON" echo "Using Python: $PYTHON"
# ── Native support check ────────────────────────────────────────────────────── # ── Discover paths + native support check (single Python subprocess) ──────────
# If TrueNAS has shipped native B2 restic support, this patch is no longer # Combines what were previously four separate Python invocations into one to
# needed. Set the kill switch and instruct the user to uninstall cleanly. # 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: try:
import inspect
import middlewared.rclone.remote.b2 as _b2_mod import middlewared.rclone.remote.b2 as _b2_mod
from middlewared.rclone.remote.b2 import B2RcloneRemote from middlewared.rclone.remote.b2 import B2RcloneRemote
if 'get_restic_config' not in B2RcloneRemote.__dict__: if 'get_restic_config' in B2RcloneRemote.__dict__:
print('no')
else:
src = open(inspect.getfile(_b2_mod), encoding='utf-8', errors='replace').read() src = open(inspect.getfile(_b2_mod), encoding='utf-8', errors='replace').read()
if 'TRUECLOUD_PATCH' in src: if 'TRUECLOUD_PATCH' not in src:
print('no')
else:
# Distinguish a real implementation from a stub that raises NotImplementedError.
try: try:
method_src = inspect.getsource(B2RcloneRemote.get_restic_config) method_src = inspect.getsource(B2RcloneRemote.get_restic_config)
except (OSError, TypeError): except (OSError, TypeError):
method_src = '' method_src = ''
print('no' if 'NotImplementedError' in method_src else 'yes') if 'NotImplementedError' not in method_src:
result['native'] = 'yes'
except Exception: except Exception:
print('no') pass
" 2>/dev/null || echo "no")
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 if [ "$_tc_native" = "yes" ]; then
echo "NOTICE: TrueNAS now provides native B2 restic support — truecloud-patch is no longer needed." echo "NOTICE: TrueNAS now provides native B2 restic support — truecloud-patch is no longer needed."
@@ -144,29 +156,6 @@ fi
echo "--- backend patch ---" 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 _b2_ok=0
_restic_ok=0 _restic_ok=0
@@ -181,12 +170,13 @@ else
_B2_PY="$_MW_DIR/rclone/remote/b2.py" _B2_PY="$_MW_DIR/rclone/remote/b2.py"
_RESTIC_PY="$_MW_DIR/plugins/cloud_backup/restic.py" _RESTIC_PY="$_MW_DIR/plugins/cloud_backup/restic.py"
# ── b2.py ───────────────────────────────────────────────────────────── # ── patch b2.py + restic.py + hook_status.json (single subprocess) ──────
if [ -f "$_B2_PY" ]; then if "$PYTHON" - "$_B2_PY" "$_RESTIC_PY" "$PATCH_DIR/hook_status.json" << 'PYEOF'
if "$PYTHON" - "$_B2_PY" << 'PYEOF' import json, os, sys, time
import sys
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 # TRUECLOUD_PATCH — added by truenas-truecloud-patch/patch/apply.sh
def _tc_get_restic_config(task): def _tc_get_restic_config(task):
p = task["credentials"]["provider"] p = task["credentials"]["provider"]
@@ -196,34 +186,7 @@ B2RcloneRemote.get_restic_config = staticmethod(_tc_get_restic_config)
B2RcloneRemote.restic = True B2RcloneRemote.restic = True
""" """
path = sys.argv[1] RESTIC_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 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 = """
# TRUECLOUD_PATCH — added by truenas-truecloud-patch/patch/apply.sh # TRUECLOUD_PATCH — added by truenas-truecloud-patch/patch/apply.sh
try: try:
_tc_orig_get_restic_config = get_restic_config _tc_orig_get_restic_config = get_restic_config
@@ -266,56 +229,66 @@ else:
get_restic_config._truecloud_patched = True get_restic_config._truecloud_patched = True
""" """
path = sys.argv[1] def patch_file(path, block):
with open(path, encoding="utf-8") as fh: with open(path, encoding="utf-8") as fh:
content = fh.read() content = fh.read()
marker = "\n# TRUECLOUD_PATCH"
idx = content.find(marker)
base = content[:idx] if idx != -1 else content
with open(path, "w", encoding="utf-8") as fh:
fh.write(base.rstrip("\n") + "\n" + block)
marker = "\n# TRUECLOUD_PATCH" b2_ok = restic_ok = False
idx = content.find(marker)
base = content[:idx] if idx != -1 else content if os.path.exists(b2_path):
patched = base.rstrip("\n") + "\n" + BLOCK 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}")
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'
patches = { patches = {
'middlewared.rclone.remote.b2': { 'middlewared.rclone.remote.b2': {
'ok': b2_ok, 'ok': b2_ok,
'detail': ('patched on disk in overlay at boot' if b2_ok 'detail': 'patched on disk in overlay at boot' if b2_ok else 'b2.py not found or write failed',
else 'b2.py not found or write failed'),
}, },
'middlewared.plugins.cloud_backup.restic': { 'middlewared.plugins.cloud_backup.restic': {
'ok': restic_ok, 'ok': restic_ok,
'detail': ('patched on disk in overlay at boot' if restic_ok 'detail': 'patched on disk in overlay at boot' if restic_ok else 'restic.py not found or write failed',
else 'restic.py not found or write failed'),
}, },
} }
payload = {'patched_at': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()), payload = {'patched_at': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()), 'patches': patches}
'patches': patches} tmp = status_path + '.tmp'
sf = sys.argv[3]
tmp = sf + '.tmp'
try: try:
with open(tmp, 'w') as f: with open(tmp, 'w') as f:
json.dump(payload, f, indent=2) json.dump(payload, f, indent=2)
os.replace(tmp, sf) os.replace(tmp, status_path)
print('OK: Wrote hook_status.json') print('OK: Wrote hook_status.json')
except OSError as e: except OSError as e:
print(f'WARNING: Could not write hook_status.json: {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 fi
# ── Step 2: Angular bundle ──────────────────────────────────────────────────── # ── Step 2: Angular bundle ────────────────────────────────────────────────────
+3
View File
@@ -48,6 +48,8 @@ import sys
import urllib.error import urllib.error
import urllib.request import urllib.request
__version__ = "0.0.3"
_PATCH_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) _PATCH_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
_STATUS_FILE = os.path.join(_PATCH_DIR, "hook_status.json") _STATUS_FILE = os.path.join(_PATCH_DIR, "hook_status.json")
@@ -194,6 +196,7 @@ def main():
formatter_class=argparse.RawDescriptionHelpFormatter, formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__.split("Examples")[1] if __doc__ and "Examples" in __doc__ else "", epilog=__doc__.split("Examples")[1] if __doc__ and "Examples" in __doc__ else "",
) )
p.add_argument("--version", "-V", action="version", version=f"truecloud-patch {__version__}")
p.add_argument("--host", default=None, metavar="HOST", p.add_argument("--host", default=None, metavar="HOST",
help="TrueNAS hostname or IP address (required except for verify)") help="TrueNAS hostname or IP address (required except for verify)")
p.add_argument("--api-key", default=None, metavar="KEY", p.add_argument("--api-key", default=None, metavar="KEY",
+5
View File
@@ -16,8 +16,13 @@
# rm /mnt/tank/truenas-truecloud-patch/disabled # rm /mnt/tank/truenas-truecloud-patch/disabled
# bash /mnt/tank/truenas-truecloud-patch/patch/apply.sh # bash /mnt/tank/truenas-truecloud-patch/patch/apply.sh
VERSION="0.0.3"
PATCH_DIR="$(cd "$(dirname "$0")" && pwd)" PATCH_DIR="$(cd "$(dirname "$0")" && pwd)"
echo "=== TrueNAS TrueCloud Provider Patch v${VERSION} — Recover ==="
echo ""
if [ "$(id -u)" -ne 0 ]; then if [ "$(id -u)" -ne 0 ]; then
echo "ERROR: must be run as root." >&2 echo "ERROR: must be run as root." >&2
exit 1 exit 1
+3 -1
View File
@@ -3,10 +3,12 @@
set -euo pipefail set -euo pipefail
VERSION="0.0.3"
PATCH_DIR="$(cd "$(dirname "$0")" && pwd)" PATCH_DIR="$(cd "$(dirname "$0")" && pwd)"
_HOOK_COMMENT='TrueCloud provider patch (S3/B2)' _HOOK_COMMENT='TrueCloud provider patch (S3/B2)'
echo "=== TrueNAS TrueCloud Provider Patch — Uninstall ===" echo "=== TrueNAS TrueCloud Provider Patch v${VERSION} — Uninstall ==="
echo "" echo ""
if [ "$(id -u)" -ne 0 ]; then if [ "$(id -u)" -ne 0 ]; then