Refuse to write a bundle whose parens we unbalanced
Commit47cdf72shipped a pattern that matched one closing paren and emitted one, netting an extra `)` in the Angular bundle: c(2,"filterByProviders",["STORJ_IX","S3","B2"]))("required",!0) ^^ syntax error The TrueNAS web UI went blank. Worse, MARKER was now present in the file, so every later run reported "already patched" and skipped -- the patch could not heal itself, and the bundle had to be hand-restored from the .pre-truecloud-patch backup. patch_ui.py now compares the parenthesis balance before and after substitution and refuses to write if it changed. A bundle we cannot patch correctly is left exactly as it was: an unpatched UI is a missing dropdown entry, a corrupted one is a dead web UI. Tests cover the real regression (verbatim47cdf72pattern) end to end: it still matches, the balance still shifts, main() refuses, and the file on disk is byte-for-byte unchanged. README documents the manual recovery for anyone who already hit it. 93 tests, ruff and shellcheck clean.
This commit is contained in:
@@ -252,6 +252,7 @@ mount | grep truecloud-nested # expect no output
|
|||||||
| Backup fails: `dataset '…' has no snapshot '…'; refusing to back up an incomplete tree` | Working as designed — a descendant dataset was not covered by the snapshot. The backup is refused rather than silently omitting that data. |
|
| Backup fails: `dataset '…' has no snapshot '…'; refusing to back up an incomplete tree` | Working as designed — a descendant dataset was not covered by the snapshot. The backup is refused rather than silently omitting that data. |
|
||||||
| Backup fails: `snapshot '…' cannot be read (Permission denied)` | The snapshot exists but is unreadable. Middleware runs as root, so this indicates a real permissions problem, not a missing snapshot. |
|
| Backup fails: `snapshot '…' cannot be read (Permission denied)` | The snapshot exists but is unreadable. Middleware runs as root, so this indicates a real permissions problem, not a missing snapshot. |
|
||||||
| `cloud_backup-*` snapshots accumulating | The sweep is not running. Check `apply.log` for the nested patch applying, and confirm `sync.py` carries the `TRUECLOUD_PATCH` block. |
|
| `cloud_backup-*` snapshots accumulating | The sweep is not running. Check `apply.log` for the nested patch applying, and confirm `sync.py` carries the `TRUECLOUD_PATCH` block. |
|
||||||
|
| Web UI blank after a patch | A bad pattern unbalanced the bundle. `apply.sh` now refuses to write in that case, but if you hit it on an older version: restore `chunk-*.js.pre-truecloud-patch` over the live chunk, then re-run `install.sh`. (`MARKER` makes an already-patched file skip, so the patch cannot heal a corrupted bundle by itself.) |
|
||||||
| Stale mounts under `/run/truecloud-nested` | A crashed run. The next backup tears them down. To clear them now: `python3 patch/truecloud_nested.py cleanup` (also run by `uninstall.sh` and `recover.sh`). It names any ZFS snapshot an interrupted run left pinned. |
|
| Stale mounts under `/run/truecloud-nested` | A crashed run. The next backup tears them down. To clear them now: `python3 patch/truecloud_nested.py cleanup` (also run by `uninstall.sh` and `recover.sh`). It names any ZFS snapshot an interrupted run left pinned. |
|
||||||
|
|
||||||
## Supported providers after patching
|
## Supported providers after patching
|
||||||
|
|||||||
@@ -65,6 +65,11 @@ def _match_pattern(content):
|
|||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
|
def _paren_delta(s):
|
||||||
|
"""Net parenthesis balance. Patching must not change it — see main()."""
|
||||||
|
return s.count("(") - s.count(")")
|
||||||
|
|
||||||
|
|
||||||
def find_bundle():
|
def find_bundle():
|
||||||
"""
|
"""
|
||||||
Search WEBUI_CANDIDATES for the JS chunk containing the filterByProviders
|
Search WEBUI_CANDIDATES for the JS chunk containing the filterByProviders
|
||||||
@@ -135,6 +140,24 @@ def main():
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Never write JS whose parentheses we have unbalanced. A pattern that eats one
|
||||||
|
# paren too many is a syntax error in the bundle and the entire TrueNAS web UI
|
||||||
|
# goes blank -- and because MARKER is then present, every later run reports
|
||||||
|
# "already patched" and skips, so the patch cannot heal itself. Recovery means
|
||||||
|
# hand-restoring the .pre-truecloud-patch backup.
|
||||||
|
#
|
||||||
|
# This is not hypothetical: it shipped once. Refuse instead.
|
||||||
|
if _paren_delta(patched) != _paren_delta(content):
|
||||||
|
print(
|
||||||
|
"[truecloud-patch] ERROR: the replacement would unbalance the bundle's "
|
||||||
|
"parentheses — refusing to write.\n"
|
||||||
|
"[truecloud-patch] The UI is UNCHANGED and still works. This means the "
|
||||||
|
"pattern no longer fits this TrueNAS build.\n"
|
||||||
|
"[truecloud-patch] File an issue at "
|
||||||
|
"https://github.com/sudolulo/truenas-truecloud-patch"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
tmp = path + ".tmp"
|
tmp = path + ".tmp"
|
||||||
try:
|
try:
|
||||||
with open(tmp, "w", encoding="utf-8") as fh:
|
with open(tmp, "w", encoding="utf-8") as fh:
|
||||||
|
|||||||
@@ -111,3 +111,61 @@ def test_patterns_compile_and_replacements_reference_group_one():
|
|||||||
for find, replace in _PATTERNS:
|
for find, replace in _PATTERNS:
|
||||||
assert isinstance(find, re.Pattern)
|
assert isinstance(find, re.Pattern)
|
||||||
assert r"\1" in replace, "replacement must preserve the binding name"
|
assert r"\1" in replace, "replacement must preserve the binding name"
|
||||||
|
|
||||||
|
|
||||||
|
class TestCorruptionGuard:
|
||||||
|
"""A bad pattern must never reach the bundle.
|
||||||
|
|
||||||
|
This is not hypothetical. Commit 47cdf72 shipped a pattern that consumed one
|
||||||
|
closing paren and emitted one, netting an extra `)`:
|
||||||
|
|
||||||
|
c(2,"filterByProviders",["STORJ_IX","S3","B2"]))("required",!0)
|
||||||
|
^^ syntax error
|
||||||
|
|
||||||
|
The web UI went blank. And because MARKER was then present in the file, every
|
||||||
|
subsequent run reported "already patched" and skipped — so the patch could not
|
||||||
|
heal itself, and the bundle had to be hand-restored from the backup.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Verbatim from 47cdf72.
|
||||||
|
BROKEN = (
|
||||||
|
re.compile(r'("filterByProviders",)\w+\(\d+,\w+,\w+\.CloudSyncProviderName\.Storj\)'),
|
||||||
|
r'\1["STORJ_IX","S3","B2"])',
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_the_regression_that_blanked_the_ui_is_detectable(self):
|
||||||
|
find, replace = self.BROKEN
|
||||||
|
patched, count = find.subn(replace, REAL_25X)
|
||||||
|
assert count == 1, "it did match — that is why it got written"
|
||||||
|
assert paren_delta(patched) != paren_delta(REAL_25X), (
|
||||||
|
"the paren balance changes; this is the signal main() now refuses on"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_main_refuses_to_write_an_unbalanced_bundle(self, monkeypatch, tmp_path, capsys):
|
||||||
|
import patch_ui
|
||||||
|
|
||||||
|
bundle = tmp_path / "chunk-TEST.js"
|
||||||
|
bundle.write_text(REAL_25X, encoding="utf-8")
|
||||||
|
|
||||||
|
monkeypatch.setattr(patch_ui, "WEBUI_CANDIDATES", [str(tmp_path)])
|
||||||
|
monkeypatch.setattr(patch_ui, "_PATTERNS", [self.BROKEN])
|
||||||
|
|
||||||
|
patch_ui.main()
|
||||||
|
|
||||||
|
out = capsys.readouterr().out
|
||||||
|
assert "refusing to write" in out
|
||||||
|
# The bundle must be byte-for-byte untouched — a broken UI is far worse
|
||||||
|
# than an unpatched one.
|
||||||
|
assert bundle.read_text(encoding="utf-8") == REAL_25X
|
||||||
|
|
||||||
|
def test_a_good_pattern_still_writes(self, monkeypatch, tmp_path):
|
||||||
|
import patch_ui
|
||||||
|
|
||||||
|
bundle = tmp_path / "chunk-TEST.js"
|
||||||
|
bundle.write_text(REAL_25X, encoding="utf-8")
|
||||||
|
monkeypatch.setattr(patch_ui, "WEBUI_CANDIDATES", [str(tmp_path)])
|
||||||
|
|
||||||
|
patch_ui.main()
|
||||||
|
|
||||||
|
assert MARKER in bundle.read_text(encoding="utf-8")
|
||||||
|
assert (tmp_path / "chunk-TEST.js.pre-truecloud-patch").exists()
|
||||||
|
|||||||
Reference in New Issue
Block a user