Skip to content

Commit 006288a

Browse files
authored
Merge pull request #2703 from alicevision/dev/isolatedProcessEnv
New local isolated computation for python nodes
2 parents 2fa380f + f8567fb commit 006288a

24 files changed

Lines changed: 1005 additions & 502 deletions

bin/meshroom_batch

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ if not args.input and not args.inputRecursive:
146146
print('Nothing to compute. You need to set --input or --inputRecursive.')
147147
sys.exit(1)
148148

149+
meshroom.core.initPlugins()
149150
meshroom.core.initNodes()
150151

151152
graph = meshroom.core.graph.Graph(name=args.pipeline)

bin/meshroom_compute

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,22 @@
11
#!/usr/bin/env python
22
import argparse
3+
import logging
4+
import os
35
import sys
46

5-
import meshroom
7+
try:
8+
import meshroom
9+
except Exception:
10+
# If meshroom module is not in the PYTHONPATH, add our root using the relative path
11+
import pathlib
12+
meshroomRootFolder = pathlib.Path(__file__).parent.parent.resolve()
13+
sys.path.append(meshroomRootFolder)
14+
import meshroom
615
meshroom.setupEnvironment()
716

17+
import meshroom.core
818
import meshroom.core.graph
9-
from meshroom.core.node import Status
19+
from meshroom.core.node import Status, ExecMode
1020

1121

1222
parser = argparse.ArgumentParser(description='Execute a Graph of processes.')
@@ -16,6 +26,8 @@ parser.add_argument('--node', metavar='NODE_NAME', type=str,
1626
help='Process the node. It will generate an error if the dependencies are not already computed.')
1727
parser.add_argument('--toNode', metavar='NODE_NAME', type=str,
1828
help='Process the node with its dependencies.')
29+
parser.add_argument('--inCurrentEnv', help='Execute process in current env without creating a dedicated runtime environment.',
30+
action='store_true')
1931
parser.add_argument('--forceStatus', help='Force computation if status is RUNNING or SUBMITTED.',
2032
action='store_true')
2133
parser.add_argument('--forceCompute', help='Compute in all cases even if already computed.',
@@ -25,12 +37,31 @@ parser.add_argument('--extern', help='Use this option when you compute externall
2537
parser.add_argument('--cache', metavar='FOLDER', type=str,
2638
default=None,
2739
help='Override the cache folder')
40+
parser.add_argument('-v', '--verbose',
41+
help='Set the verbosity level for logging:\n'
42+
' - fatal: Show only critical errors.\n'
43+
' - error: Show errors only.\n'
44+
' - warning: Show warnings and errors.\n'
45+
' - info: Show standard informational messages.\n'
46+
' - debug: Show detailed debug information.\n'
47+
' - trace: Show all messages, including trace-level details.',
48+
default=os.environ.get('MESHROOM_VERBOSE', 'info'),
49+
choices=['fatal', 'error', 'warning', 'info', 'debug', 'trace'])
2850

2951
parser.add_argument('-i', '--iteration', type=int,
3052
default=-1, help='')
3153

3254
args = parser.parse_args()
3355

56+
# Setup the verbose level
57+
if args.extern:
58+
# For extern computation, we want to focus on the node computation log.
59+
# So, we avoid polluting the log with general warning about plugins, versions of nodes in file, etc.
60+
logging.getLogger().setLevel(level=logging.ERROR)
61+
else:
62+
logging.getLogger().setLevel(meshroom.logStringToPython[args.verbose])
63+
64+
meshroom.core.initPlugins()
3465
meshroom.core.initNodes()
3566

3667
graph = meshroom.core.graph.loadGraph(args.graphFile)
@@ -53,15 +84,23 @@ if args.node:
5384
chunks = node.chunks
5485
for chunk in chunks:
5586
if chunk.status.status in submittedStatuses:
56-
print('Warning: Node is already submitted with status "{}". See file: "{}"'.format(chunk.status.status.name, chunk.statusFile))
87+
# Particular case for the local isolated, the node status is set to RUNNING by the submitter directly.
88+
# We ensure that no other instance has started to compute, by checking that the sessionUid is empty.
89+
if chunk.node.getMrNodeType() == meshroom.core.MrNodeType.NODE and not chunk.status.sessionUid and chunk.status.submitterSessionUid:
90+
continue
91+
print(f'Warning: Node is already submitted with status "{chunk.status.status.name}". See file: "{chunk.statusFile}". ExecMode: {chunk.status.execMode.name}, SessionUid: {chunk.status.sessionUid}, submitterSessionUid: {chunk.status.submitterSessionUid}')
5792
# sys.exit(-1)
5893

94+
if args.extern:
95+
# Restore the log level
96+
logging.getLogger().setLevel(meshroom.logStringToPython[args.verbose])
97+
5998
node.preprocess()
6099
if args.iteration != -1:
61100
chunk = node.chunks[args.iteration]
62-
chunk.process(args.forceCompute)
101+
chunk.process(args.forceCompute, args.inCurrentEnv)
63102
else:
64-
node.process(args.forceCompute)
103+
node.process(args.forceCompute, args.inCurrentEnv)
65104
node.postprocess()
66105
else:
67106
if args.iteration != -1:

bin/meshroom_submit

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ parser.add_argument("--submitLabel",
2222

2323
args = parser.parse_args()
2424

25+
meshroom.core.initPlugins()
2526
meshroom.core.initNodes()
2627
meshroom.core.initSubmitters()
2728

meshroom/core/__init__.py

Lines changed: 114 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
1-
import hashlib
21
from contextlib import contextmanager
2+
import hashlib
33
import importlib
44
import inspect
5-
import os
6-
import tempfile
7-
import uuid
85
import logging
6+
import os
7+
from pathlib import Path
98
import pkgutil
10-
119
import sys
10+
import tempfile
11+
import traceback
12+
import uuid
1213

1314
try:
1415
# for cx_freeze
@@ -19,7 +20,9 @@
1920
pass
2021

2122
from meshroom.core.submitter import BaseSubmitter
23+
from meshroom.env import EnvVar, meshroomFolder
2224
from . import desc
25+
from .desc import MrNodeType
2326

2427
# Setup logging
2528
logging.basicConfig(format='[%(asctime)s][%(levelname)s] %(message)s', level=logging.INFO)
@@ -28,13 +31,12 @@
2831
sessionUid = str(uuid.uuid1())
2932

3033
cacheFolderName = 'MeshroomCache'
31-
defaultCacheFolder = os.environ.get('MESHROOM_CACHE', os.path.join(tempfile.gettempdir(), cacheFolderName))
32-
nodesDesc = {}
33-
submitters = {}
34-
pipelineTemplates = {}
34+
nodesDesc: dict[str, desc.BaseNode] = {}
35+
submitters: dict[str, BaseSubmitter] = {}
36+
pipelineTemplates: dict[str, str] = {}
3537

3638

37-
def hashValue(value):
39+
def hashValue(value) -> str:
3840
""" Hash 'value' using sha1. """
3941
hashObject = hashlib.sha1(str(value).encode('utf-8'))
4042
return hashObject.hexdigest()
@@ -52,19 +54,34 @@ def add_to_path(p):
5254
sys.path = old_path
5355

5456

55-
def loadPlugins(folder, packageName, classType):
57+
def loadClasses(folder, packageName, classType):
5658
"""
5759
"""
58-
59-
pluginTypes = []
60+
classes = []
6061
errors = []
6162

63+
resolvedFolder = str(Path(folder).resolve())
6264
# temporarily add folder to python path
63-
with add_to_path(folder):
65+
with add_to_path(resolvedFolder):
6466
# import node package
65-
package = importlib.import_module(packageName)
66-
packageName = package.packageName if hasattr(package, 'packageName') else package.__name__
67-
packageVersion = getattr(package, "__version__", None)
67+
68+
try:
69+
package = importlib.import_module(packageName)
70+
packageName = package.packageName if hasattr(package, 'packageName') else package.__name__
71+
packageVersion = getattr(package, "__version__", None)
72+
packagePath = os.path.dirname(package.__file__)
73+
except Exception as e:
74+
tb = traceback.extract_tb(e.__traceback__)
75+
last_call = tb[-1]
76+
logging.warning(f' * Failed to load package "{packageName}" from folder "{resolvedFolder}" ({type(e).__name__}): {str(e)}\n'
77+
# filename:lineNumber functionName
78+
f'{last_call.filename}:{last_call.lineno} {last_call.name}\n'
79+
# line of code with the error
80+
f'{last_call.line}'
81+
# Full traceback
82+
f'\n{traceback.format_exc()}\n\n'
83+
)
84+
return []
6885

6986
for importer, pluginName, ispkg in pkgutil.iter_modules(package.__path__):
7087
pluginModuleName = '.' + pluginName
@@ -75,7 +92,7 @@ def loadPlugins(folder, packageName, classType):
7592
if plugin.__module__ == '{}.{}'.format(package.__name__, pluginName)
7693
and issubclass(plugin, classType)]
7794
if not plugins:
78-
logging.warning("No class defined in plugin: {}".format(pluginModuleName))
95+
logging.warning(f"No class defined in plugin: {pluginModuleName}")
7996

8097
importPlugin = True
8198
for p in plugins:
@@ -88,16 +105,26 @@ def loadPlugins(folder, packageName, classType):
88105
break
89106
p.packageName = packageName
90107
p.packageVersion = packageVersion
108+
p.packagePath = packagePath
91109
if importPlugin:
92-
pluginTypes.extend(plugins)
110+
classes.extend(plugins)
93111
except Exception as e:
94-
errors.append(' * {}: {}'.format(pluginName, str(e)))
112+
tb = traceback.extract_tb(e.__traceback__)
113+
last_call = tb[-1]
114+
errors.append(f' * {pluginName} ({type(e).__name__}): {str(e)}\n'
115+
# filename:lineNumber functionName
116+
f'{last_call.filename}:{last_call.lineno} {last_call.name}\n'
117+
# line of code with the error
118+
f'{last_call.line}'
119+
# Full traceback
120+
f'\n{traceback.format_exc()}\n\n'
121+
)
95122

96123
if errors:
97-
logging.warning('== The following "{package}" plugins could not be loaded ==\n'
124+
logging.warning(' The following "{package}" plugins could not be loaded:\n'
98125
'{errorMsg}\n'
99126
.format(package=packageName, errorMsg='\n'.join(errors)))
100-
return pluginTypes
127+
return classes
101128

102129

103130
def validateNodeDesc(nodeDesc):
@@ -283,75 +310,114 @@ def registerNodeType(nodeType):
283310
284311
After registration, nodes of this type can be instantiated in a Graph.
285312
"""
286-
global nodesDesc
287313
if nodeType.__name__ in nodesDesc:
288-
logging.error("Node Desc {} is already registered.".format(nodeType.__name__))
314+
logging.error(f"Node Desc {nodeType.__name__} is already registered.")
289315
nodesDesc[nodeType.__name__] = nodeType
290316

291317

292318
def unregisterNodeType(nodeType):
293319
""" Remove 'nodeType' from the list of register node types. """
294-
global nodesDesc
295320
assert nodeType.__name__ in nodesDesc
296321
del nodesDesc[nodeType.__name__]
297322

298323

299324
def loadNodes(folder, packageName):
300-
return loadPlugins(folder, packageName, desc.Node)
325+
if not os.path.isdir(folder):
326+
logging.error(f"Node folder '{folder}' does not exist.")
327+
return
328+
329+
return loadClasses(folder, packageName, desc.BaseNode)
301330

302331

303332
def loadAllNodes(folder):
304-
global nodesDesc
305333
for importer, package, ispkg in pkgutil.walk_packages([folder]):
306334
if ispkg:
307335
nodeTypes = loadNodes(folder, package)
308336
for nodeType in nodeTypes:
309337
registerNodeType(nodeType)
310-
logging.debug('Nodes loaded [{}]: {}'.format(package, ', '.join([nodeType.__name__ for nodeType in nodeTypes])))
338+
nodesStr = ', '.join([nodeType.__name__ for nodeType in nodeTypes])
339+
logging.debug(f'Nodes loaded [{package}]: {nodesStr}')
340+
341+
342+
def loadPluginFolder(folder):
343+
if not os.path.isdir(folder):
344+
logging.info(f"Plugin folder '{folder}' does not exist.")
345+
return
346+
347+
mrFolder = Path(folder, 'meshroom')
348+
if not mrFolder.exists():
349+
logging.info(f"Plugin folder '{folder}' does not contain a 'meshroom' folder.")
350+
return
351+
352+
binFolders = [Path(folder, 'bin')]
353+
libFolders = [Path(folder, 'lib'), Path(folder, 'lib64')]
354+
pythonPathFolders = [Path(folder)] + binFolders
355+
356+
loadAllNodes(folder=mrFolder)
357+
loadPipelineTemplates(folder=mrFolder)
358+
359+
360+
def loadPluginsFolder(folder):
361+
if not os.path.isdir(folder):
362+
logging.debug(f"PluginSet folder '{folder}' does not exist.")
363+
return
364+
365+
for file in os.listdir(folder):
366+
if os.path.isdir(file):
367+
subFolder = os.path.join(folder, file)
368+
loadPluginFolder(subFolder)
311369

312370

313371
def registerSubmitter(s):
314-
global submitters
315372
if s.name in submitters:
316-
logging.error("Submitter {} is already registered.".format(s.name))
373+
logging.error(f"Submitter {s.name} is already registered.")
317374
submitters[s.name] = s
318375

319376

320377
def loadSubmitters(folder, packageName):
321-
return loadPlugins(folder, packageName, BaseSubmitter)
378+
if not os.path.isdir(folder):
379+
logging.error(f"Submitters folder '{folder}' does not exist.")
380+
return
381+
382+
return loadClasses(folder, packageName, BaseSubmitter)
322383

323384

324385
def loadPipelineTemplates(folder):
325-
global pipelineTemplates
386+
if not os.path.isdir(folder):
387+
logging.error(f"Pipeline templates folder '{folder}' does not exist.")
388+
return
326389
for file in os.listdir(folder):
327390
if file.endswith(".mg") and file not in pipelineTemplates:
328391
pipelineTemplates[os.path.splitext(file)[0]] = os.path.join(folder, file)
329392

330393

331394
def initNodes():
332-
meshroomFolder = os.path.dirname(os.path.dirname(__file__))
333-
additionalNodesPath = os.environ.get("MESHROOM_NODES_PATH", "").split(os.pathsep)
334-
# filter empty strings
335-
additionalNodesPath = [i for i in additionalNodesPath if i]
395+
additionalNodesPath = EnvVar.getList(EnvVar.MESHROOM_NODES_PATH)
336396
nodesFolders = [os.path.join(meshroomFolder, 'nodes')] + additionalNodesPath
337397
for f in nodesFolders:
338398
loadAllNodes(folder=f)
339399

340400

341401
def initSubmitters():
342-
meshroomFolder = os.path.dirname(os.path.dirname(__file__))
343-
subs = loadSubmitters(os.environ.get("MESHROOM_SUBMITTERS_PATH", meshroomFolder), 'submitters')
344-
for sub in subs:
345-
registerSubmitter(sub())
402+
additionalPaths = EnvVar.getList(EnvVar.MESHROOM_SUBMITTERS_PATH)
403+
allSubmittersFolders = [meshroomFolder] + additionalPaths
404+
for folder in allSubmittersFolders:
405+
subs = loadSubmitters(folder, 'submitters')
406+
for sub in subs:
407+
registerSubmitter(sub())
346408

347409

348410
def initPipelines():
349-
meshroomFolder = os.path.dirname(os.path.dirname(__file__))
350-
# Load pipeline templates: check in any folder the user might have added to the environment variable
351-
pipelinesPath = os.environ.get("MESHROOM_PIPELINE_TEMPLATES_PATH", "").split(os.pathsep)
352-
pipelineTemplatesFolders = [i for i in pipelinesPath if i]
411+
# Load pipeline templates: check in the default folder and any folder the user might have
412+
# added to the environment variable
413+
additionalPipelinesPath = EnvVar.getList(EnvVar.MESHROOM_PIPELINE_TEMPLATES_PATH)
414+
pipelineTemplatesFolders = [os.path.join(meshroomFolder, 'pipelines')] + additionalPipelinesPath
353415
for f in pipelineTemplatesFolders:
354-
if os.path.isdir(f):
355-
loadPipelineTemplates(f)
356-
else:
357-
logging.warning("Pipeline templates folder '{}' does not exist.".format(f))
416+
loadPipelineTemplates(f)
417+
418+
419+
def initPlugins():
420+
additionalpluginsPath = EnvVar.getList(EnvVar.MESHROOM_PLUGINS_PATH)
421+
nodesFolders = [os.path.join(meshroomFolder, 'plugins')] + additionalpluginsPath
422+
for f in nodesFolders:
423+
loadPluginFolder(folder=f)

0 commit comments

Comments
 (0)