Skip to content

Commit cca5cc5

Browse files
authored
Merge pull request #2947 from alicevision/fix/reloadPlugins
Correctly catch syntax errors in node descriptions when reloading plugins
2 parents c939290 + cb709b1 commit cca5cc5

File tree

3 files changed

+56
-9
lines changed

3 files changed

+56
-9
lines changed

meshroom/core/plugins.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -476,7 +476,12 @@ def reload(self) -> bool:
476476
f"at {self.path} has not been modified since the last load.")
477477
return False
478478

479-
updated = importlib.reload(sys.modules.get(self.nodeDescriptor.__module__))
479+
try:
480+
updated = importlib.reload(sys.modules.get(self.nodeDescriptor.__module__))
481+
except Exception as exc:
482+
logging.error(f"[Reload] {self.nodeDescriptor.__name__}: {exc} ({type(exc).__name__})")
483+
self.status = NodePluginStatus.DESC_ERROR
484+
return False
480485
descriptor = getattr(updated, self.nodeDescriptor.__name__)
481486

482487
if not descriptor:

meshroom/ui/reconstruction.py

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from meshroom.core.node import Node, CompatibilityNode, Status, Position, CompatibilityIssue
2020
from meshroom.core.taskManager import TaskManager
2121
from meshroom.core.evaluation import MathEvaluator
22+
from meshroom.core.plugins import NodePluginStatus
2223

2324
from meshroom.ui import commands
2425
from meshroom.ui.graph import UIGraph
@@ -445,17 +446,25 @@ def _reloadPlugins(self):
445446
The nodes in the graph will be updated to match the changes in the description, if
446447
there was any.
447448
"""
448-
nodeTypes: list[str] = []
449+
reloadedNodes: list[str] = []
450+
errorNodes: list[str] = []
449451
for plugin in meshroom.core.pluginManager.getPlugins().values():
450452
for node in plugin.nodes.values():
451453
if node.reload():
452-
nodeTypes.append(node.nodeDescriptor.__name__)
453-
self.pluginsReloaded.emit(nodeTypes)
454+
reloadedNodes.append(node.nodeDescriptor.__name__)
455+
else:
456+
if node.status == NodePluginStatus.DESC_ERROR or node.status == NodePluginStatus.ERROR:
457+
errorNodes.append(node.nodeDescriptor.__name__)
458+
459+
self.pluginsReloaded.emit(reloadedNodes, errorNodes)
454460

455461
@Slot(list)
456-
def _onPluginsReloaded(self, nodeTypes: list):
457-
self._graph.reloadNodePlugins(nodeTypes)
458-
self.parent().showMessage("Plugins reloaded!", "ok")
462+
def _onPluginsReloaded(self, reloadedNodes: list, errorNodes: list):
463+
self._graph.reloadNodePlugins(reloadedNodes)
464+
if len(errorNodes) > 0:
465+
self.parent().showMessage(f"Some plugins failed to reload: {', '.join(errorNodes)}", "error")
466+
else:
467+
self.parent().showMessage("Plugins reloaded!", "ok")
459468

460469
@Slot(result=bool)
461470
@Slot(str, result=bool)
@@ -957,7 +966,7 @@ def setBuildingIntrinsics(self, value):
957966
displayedAttrs3DChanged = Signal()
958967
displayedAttrs3D = Property(QObject, lambda self: self._displayedAttrs3D, notify=displayedAttrs3DChanged)
959968

960-
pluginsReloaded = Signal(list)
969+
pluginsReloaded = Signal(list, list)
961970

962971
@Slot(QObject)
963972
def setActiveNode(self, node, categories=True, inputs=True):

tests/test_plugins.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ def test_loadedPlugin(self):
158158
assert name == "sharedTemplate"
159159
assert plugin.templates[name] == os.path.join(str(plugin.path), "sharedTemplate.mg")
160160

161-
def test_reloadNodePlugin(self):
161+
def test_reloadNodePluginInvalidDescrpition(self):
162162
plugin = pluginManager.getPlugin("pluginB")
163163
assert plugin == self.plugin
164164
node = plugin.nodes["PluginBNodeB"]
@@ -217,6 +217,39 @@ def test_reloadNodePlugin(self):
217217
assert node.status == NodePluginStatus.DESC_ERROR # Not NOT_LOADED
218218
assert not pluginManager.isRegistered(nodeName)
219219

220+
def test_reloadNodePluginSyntaxError(self):
221+
plugin = pluginManager.getPlugin("pluginB")
222+
assert plugin == self.plugin
223+
node = plugin.nodes["PluginBNodeA"]
224+
nodeName = node.nodeDescriptor.__name__
225+
226+
# Check that the node has been registered
227+
assert node.status == NodePluginStatus.LOADED
228+
assert pluginManager.isRegistered(nodeName)
229+
230+
# Introduce a syntax error in the description
231+
originalFileContent = None
232+
with open(node.path, "r") as f:
233+
originalFileContent = f.read()
234+
235+
replaceFileContent = originalFileContent.replace('name="input",', 'name="input"')
236+
with open(node.path, "w") as f:
237+
f.write(replaceFileContent)
238+
239+
# Reload the node and assert it is invalid but still registered
240+
node.reload()
241+
assert node.status == NodePluginStatus.DESC_ERROR
242+
assert pluginManager.isRegistered(nodeName)
243+
244+
# Restore the node file to its original state (with a description error)
245+
with open(node.path, "w") as f:
246+
f.write(originalFileContent)
247+
248+
# Assert the status is correct and the node is still registered
249+
node.reload()
250+
assert node.status == NodePluginStatus.NOT_LOADED
251+
assert pluginManager.isRegistered(nodeName)
252+
220253

221254
class TestPluginsConfiguration:
222255
CONFIG_PATH = ("CONFIG_PATH", "sharedTemplate.mg", "config.json")

0 commit comments

Comments
 (0)