#!/usr/bin/env python3 """Checks unraid/lgtv-audiocap-loopback.plg without needing an Unraid box. Verifies the plugin is well-formed XML (a CDATA-free bash script anywhere in it means a stray "&" or "<" one edit away from breaking the DOCTYPE entity expansion Unraid's installer relies on), that entities substitute the way Unraid's installer would substitute them, that both embedded scripts are syntactically valid bash, and that the install/remove pair is idempotent and symmetric against a scratch go-file. """ import subprocess import sys import tempfile import os import xml.dom.minidom as minidom HERE = os.path.dirname(os.path.abspath(__file__)) PLG = os.path.join(HERE, os.pardir, "unraid", "lgtv-audiocap-loopback.plg") passed = 0 failed = 0 def check(condition, description): global passed, failed if condition: print(" ok %s" % description) passed += 1 else: print(" FAIL %s" % description) failed += 1 def bash_syntax_ok(script): result = subprocess.run(["bash", "-n"], input=script, text=True, capture_output=True) return result.returncode == 0, result.stderr def main(): doc = minidom.parse(PLG) plugin = doc.getElementsByTagName("PLUGIN") check(len(plugin) == 1, "exactly one PLUGIN element") attrs = dict(plugin[0].attributes.items()) if plugin else {} for key in ("name", "author", "version", "pluginURL", "min"): check(bool(attrs.get(key)), "PLUGIN has a non-empty %s attribute" % key) check(attrs.get("name") == "lgtv-audiocap-loopback", "name matches the filename's stem") check(attrs.get("pluginURL", "").endswith(attrs.get("name", "\0") + ".plg"), "pluginURL points at this same file's name") files = doc.getElementsByTagName("FILE") check(len(files) == 2, "exactly two FILE blocks (install + remove)") install_script = remove_script = None for f in files: inline = f.getElementsByTagName("INLINE") check(len(inline) == 1, "FILE (Method=%s) has one INLINE child" % (f.getAttribute("Method") or "install")) script = inline[0].firstChild.data if inline and inline[0].firstChild else "" ok, stderr = bash_syntax_ok(script) check(ok, "FILE (Method=%s) script is valid bash%s" % ( f.getAttribute("Method") or "install", "" if ok else ": " + stderr.strip())) if f.getAttribute("Method") == "remove": remove_script = script else: install_script = script check(install_script is not None, "found the install script") check(remove_script is not None, "found the remove script") check("lgtv-audiocap-loopback" in (install_script or ""), "&name; entity actually expanded inside the install script (not left literal)") # The plugin appends to /boot/config/go; redirect that at a scratch file # to exercise the real install/remove logic end to end, not just parse it. with tempfile.TemporaryDirectory() as tmp: go = os.path.join(tmp, "go") with open(go, "w") as fh: fh.write("#!/bin/bash\n/usr/local/sbin/emhttp\n") original = open(go).read() # modprobe isn't run for real here; the script already tolerates that # (it warns and continues), so there's nothing to stub out beyond # keeping its stderr out of /tmp. env_script = install_script.replace("GO=/boot/config/go", "GO=%s" % go) env_script = env_script.replace("/tmp/${NAME}.err", os.path.join(tmp, "err")) subprocess.run(["bash", "-c", env_script], check=True) after_install = open(go).read() check(after_install != original, "install actually appended something to go") check("modprobe snd-aloop" in after_install, "the modprobe line ended up in go") subprocess.run(["bash", "-c", env_script], check=True) after_second_install = open(go).read() check(after_second_install == after_install, "installing twice does not duplicate the block") env_remove = remove_script.replace("GO=/boot/config/go", "GO=%s" % go) env_remove = env_remove.replace("/sbin/rmmod snd_aloop 2>/dev/null || true", "true") subprocess.run(["bash", "-c", env_remove], check=True) after_remove = open(go).read() check(after_remove == original, "remove restores go to its original contents exactly") print() print("%d/%d checks passed" % (passed, passed + failed)) return 0 if failed == 0 else 1 if __name__ == "__main__": sys.exit(main())