fix: detect in-place node updates (not just install/remove)

The node-set fingerprint only hashed NODE_CLASS_MAPPINGS keys, so updating an
existing node (new feature, same class name) didn't change it -> the stale
object_info kept being served across restarts and the update never appeared
(removing Tenaciousload 'fixed' it because the cache was gone).

Now the fingerprint also hashes every custom-node .py path+mtime, so an in-place
update (git pull/edit) changes it and the cache auto-invalidates on the next
restart -- which is when node code reloads anyway, so the update just shows up.
__pycache__/.git/node_modules are skipped (no false positives). Measured 0.27s
for 4268 .py files; runs once per startup. Unit-tested.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-05 11:56:09 +02:00
parent 04f6271867
commit fcd7c49da5
+38 -3
View File
@@ -315,9 +315,39 @@ def register_files(folder_name, rel_paths):
_sig_checked = False _sig_checked = False
def _custom_nodes_code_hash():
"""Hash of every custom-node .py path + mtime. Changes when a node's code is
updated in place (git pull / edit) even if its class names stay the same, so
feature updates to an existing node are detected. (custom_nodes is local, so
this walk is fast; __pycache__ is skipped so it has no false positives.)"""
try:
bases = folder_paths.get_folder_paths("custom_nodes")
except Exception:
bases = [os.path.join(getattr(folder_paths, "base_path", "."), "custom_nodes")]
skip = {"__pycache__", ".git", "node_modules", ".venv", "venv"}
items = []
for base in sorted(set(bases)):
if not os.path.isdir(base):
continue
for dirpath, dirnames, filenames in os.walk(base):
dirnames[:] = [d for d in dirnames if d not in skip]
for fn in filenames:
if fn.endswith(".py"):
p = os.path.join(dirpath, fn)
try:
items.append((p, os.path.getmtime(p)))
except OSError:
pass
h = hashlib.sha1()
for p, mt in sorted(items):
h.update(p.encode("utf-8", "replace"))
h.update(f":{mt}\x00".encode())
return h.hexdigest()
def _current_node_signature(): def _current_node_signature():
"""A cheap fingerprint of the available node set. Changes when a node is """Fingerprint of the available nodes AND their code. Changes when a node is
installed, removed, enabled or disabled.""" installed, removed, enabled, disabled, or updated in place."""
try: try:
import nodes import nodes
keys = sorted(nodes.NODE_CLASS_MAPPINGS.keys()) keys = sorted(nodes.NODE_CLASS_MAPPINGS.keys())
@@ -327,7 +357,12 @@ def _current_node_signature():
for k in keys: for k in keys:
h.update(k.encode("utf-8", "replace")) h.update(k.encode("utf-8", "replace"))
h.update(b"\x00") h.update(b"\x00")
return f"{len(keys)}:{h.hexdigest()}" try:
code = _custom_nodes_code_hash()
except Exception as e: # pragma: no cover
log.warning("Tenaciousload: custom_nodes code hash failed: %s", e)
code = ""
return f"{len(keys)}:{h.hexdigest()}:{code}"
def _check_node_signature(): def _check_node_signature():