Meshroom/meshroom/core/desc/node.py
Candice Bentéjac 23901c502b [desc] Remove internalFolder in desc.Node
This is fully replaced with `{nodeCacheFolder}`.
2025-03-05 13:36:32 +00:00

284 lines
9.7 KiB
Python

from inspect import getfile
from pathlib import Path
import os
import psutil
import shlex
from .computation import Level, StaticNodeSize
from .attribute import StringParam, ColorParam
from meshroom.core import cgroup
class Node(object):
"""
"""
cpu = Level.NORMAL
gpu = Level.NONE
ram = Level.NORMAL
packageName = ''
packageVersion = ''
internalInputs = [
StringParam(
name="invalidation",
label="Invalidation Message",
description="A message that will invalidate the node's output folder.\n"
"This is useful for development, we can invalidate the output of the node when we modify the code.\n"
"It is displayed in bold font in the invalidation/comment messages tooltip.",
value="",
semantic="multiline",
advanced=True,
uidIgnoreValue="", # If the invalidation string is empty, it does not participate to the node's UID
),
StringParam(
name="comment",
label="Comments",
description="User comments describing this specific node instance.\n"
"It is displayed in regular font in the invalidation/comment messages tooltip.",
value="",
semantic="multiline",
invalidate=False,
),
StringParam(
name="label",
label="Node's Label",
description="Customize the default label (to replace the technical name of the node instance).",
value="",
invalidate=False,
),
ColorParam(
name="color",
label="Color",
description="Custom color for the node (SVG name or hexadecimal code).",
value="",
invalidate=False,
)
]
inputs = []
outputs = []
size = StaticNodeSize(1)
parallelization = None
documentation = ''
category = 'Other'
def __init__(self):
super(Node, self).__init__()
self.hasDynamicOutputAttribute = any(output.isDynamicValue for output in self.outputs)
self.sourceCodeFolder = Path(getfile(self.__class__)).parent.resolve().as_posix()
def upgradeAttributeValues(self, attrValues, fromVersion):
return attrValues
@classmethod
def onNodeCreated(cls, node):
"""Called after a node instance had been created from this node descriptor and added to a Graph."""
pass
@classmethod
def update(cls, node):
""" Method call before node's internal update on invalidation.
Args:
node: the BaseNode instance being updated
See Also:
BaseNode.updateInternals
"""
pass
@classmethod
def postUpdate(cls, node):
""" Method call after node's internal update on invalidation.
Args:
node: the BaseNode instance being updated
See Also:
NodeBase.updateInternals
"""
pass
def preprocess(self, node):
""" Gets invoked just before the processChunk method for the node.
Args:
node: The BaseNode instance about to be processed.
"""
pass
def postprocess(self, node):
""" Gets invoked after the processChunk method for the node.
Args:
node: The BaseNode instance which is processed.
"""
pass
def stopProcess(self, chunk):
raise NotImplementedError('No stopProcess implementation on node: {}'.format(chunk.node.name))
def processChunk(self, chunk):
raise NotImplementedError('No processChunk implementation on node: "{}"'.format(chunk.node.name))
class InputNode(Node):
"""
Node that does not need to be processed, it is just a placeholder for inputs.
"""
def __init__(self):
super(InputNode, self).__init__()
def processChunk(self, chunk):
pass
class CommandLineNode(Node):
"""
"""
commandLine = '' # need to be defined on the node
parallelization = None
commandLineRange = ''
def __init__(self):
super(CommandLineNode, self).__init__()
def buildCommandLine(self, chunk):
cmdPrefix = ''
# If rez available in env, we use it
if "REZ_ENV" in os.environ and chunk.node.packageVersion:
# If the node package is already in the environment, we don't need a new dedicated rez environment
alreadyInEnv = os.environ.get("REZ_{}_VERSION".format(chunk.node.packageName.upper()),
"").startswith(chunk.node.packageVersion)
if not alreadyInEnv:
cmdPrefix = '{rez} {packageFullName} -- '.format(rez=os.environ.get("REZ_ENV"),
packageFullName=chunk.node.packageFullName)
cmdSuffix = ''
if chunk.node.isParallelized and chunk.node.size > 1:
cmdSuffix = ' ' + self.commandLineRange.format(**chunk.range.toDict())
return cmdPrefix + chunk.node.nodeDesc.commandLine.format(**chunk.node._cmdVars) + cmdSuffix
def stopProcess(self, chunk):
# The same node could exists several times in the graph and
# only one would have the running subprocess; ignore all others
if not hasattr(chunk, "subprocess"):
return
if chunk.subprocess:
# Kill process tree
processes = chunk.subprocess.children(recursive=True) + [chunk.subprocess]
try:
for process in processes:
process.terminate()
except psutil.NoSuchProcess:
pass
def processChunk(self, chunk):
try:
with open(chunk.logFile, 'w') as logF:
cmd = self.buildCommandLine(chunk)
chunk.status.commandLine = cmd
chunk.saveStatusFile()
print(' - commandLine: {}'.format(cmd))
print(' - logFile: {}'.format(chunk.logFile))
chunk.subprocess = psutil.Popen(shlex.split(cmd), stdout=logF, stderr=logF, cwd=chunk.node.internalFolder)
# Store process static info into the status file
# chunk.status.env = node.proc.environ()
# chunk.status.createTime = node.proc.create_time()
chunk.statThread.proc = chunk.subprocess
stdout, stderr = chunk.subprocess.communicate()
chunk.subprocess.wait()
chunk.status.returnCode = chunk.subprocess.returncode
if chunk.subprocess.returncode != 0:
with open(chunk.logFile, 'r') as logF:
logContent = ''.join(logF.readlines())
raise RuntimeError('Error on node "{}":\nLog:\n{}'.format(chunk.name, logContent))
except Exception:
raise
finally:
chunk.subprocess = None
# Specific command line node for AliceVision apps
class AVCommandLineNode(CommandLineNode):
cgroupParsed = False
cmdMem = ''
cmdCore = ''
def __init__(self):
super(AVCommandLineNode, self).__init__()
if AVCommandLineNode.cgroupParsed is False:
AVCommandLineNode.cmdMem = ''
memSize = cgroup.getCgroupMemorySize()
if memSize > 0:
AVCommandLineNode.cmdMem = ' --maxMemory={memSize}'.format(memSize=memSize)
AVCommandLineNode.cmdCore = ''
coresCount = cgroup.getCgroupCpuCount()
if coresCount > 0:
AVCommandLineNode.cmdCore = ' --maxCores={coresCount}'.format(coresCount=coresCount)
AVCommandLineNode.cgroupParsed = True
def buildCommandLine(self, chunk):
commandLineString = super(AVCommandLineNode, self).buildCommandLine(chunk)
return commandLineString + AVCommandLineNode.cmdMem + AVCommandLineNode.cmdCore
class InitNode(object):
def __init__(self):
super(InitNode, self).__init__()
def initialize(self, node, inputs, recursiveInputs):
"""
Initialize the attributes that are needed for a node to start running.
Args:
node (Node): the node whose attributes must be initialized
inputs (list): the user-provided list of input files/directories
recursiveInputs (list): the user-provided list of input directories to search recursively for images
"""
pass
def resetAttributes(self, node, attributeNames):
"""
Reset the values of the provided attributes for a node.
Args:
node (Node): the node whose attributes are to be reset
attributeNames (list): the list containing the names of the attributes to reset
"""
for attrName in attributeNames:
if node.hasAttribute(attrName):
node.attribute(attrName).resetToDefaultValue()
def extendAttributes(self, node, attributesDict):
"""
Extend the values of the provided attributes for a node.
Args:
node (Node): the node whose attributes are to be extended
attributesDict (dict): the dictionary containing the attributes' names (as keys) and the values to extend with
"""
for attr in attributesDict.keys():
if node.hasAttribute(attr):
node.attribute(attr).extend(attributesDict[attr])
def setAttributes(self, node, attributesDict):
"""
Set the values of the provided attributes for a node.
Args:
node (Node): the node whose attributes are to be extended
attributesDict (dict): the dictionary containing the attributes' names (as keys) and the values to set
"""
for attr in attributesDict:
if node.hasAttribute(attr):
node.attribute(attr).value = attributesDict[attr]