Meshroom/meshroom/core/desc/node.py
Fabien Castan 008d6c75ee Automatically save the project when computing or submitting to renderfarm
If the project is not saved at all, it will suggest to save it manually
or to define a project in a temporary folder using date/time for the
project name.
2025-04-13 13:09:24 +02:00

375 lines
13 KiB
Python

import enum
from inspect import getfile
from pathlib import Path
import logging
import os
import psutil
import shlex
import shutil
import sys
from .computation import Level, StaticNodeSize
from .attribute import StringParam, ColorParam
import meshroom
from meshroom.core import cgroup
_MESHROOM_ROOT = Path(meshroom.__file__).parent.parent
_MESHROOM_COMPUTE = _MESHROOM_ROOT / "bin" / "meshroom_compute"
class MrNodeType(enum.Enum):
NONE = enum.auto()
BASENODE = enum.auto()
NODE = enum.auto()
COMMANDLINE = enum.auto()
INPUT = enum.auto()
class BaseNode(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(BaseNode, self).__init__()
self.hasDynamicOutputAttribute = any(output.isDynamicValue for output in self.outputs)
self.sourceCodeFolder = Path(getfile(self.__class__)).parent.resolve().as_posix()
def getMrNodeType(self):
return MrNodeType.BASENODE
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 processChunk(self, chunk):
raise NotImplementedError(f'No processChunk implementation on node: "{chunk.node.name}"')
def executeChunkCommandLine(self, chunk, cmd, env=None):
try:
with open(chunk.logFile, 'w') as logF:
chunk.status.commandLine = cmd
chunk.saveStatusFile()
cmdList = shlex.split(cmd)
# Resolve executable to full path
prog = shutil.which(cmdList[0], path=env.get('PATH') if env else None)
print(f"Starting Process for '{chunk.node.name}'")
print(f' - commandLine: {cmd}')
print(f' - logFile: {chunk.logFile}')
if prog:
cmdList[0] = prog
print(f' - command full path: {prog}')
# Change the process group to avoid Meshroom main process being killed if the subprocess
# gets terminated by the user or an Out Of Memory (OOM kill).
if sys.platform == "win32":
platformArgs = {"creationflags": psutil.CREATE_NEW_PROCESS_GROUP}
# Note: DETACHED_PROCESS means fully detached process.
# We don't want a fully detached process to ensure that if Meshroom is killed,
# the subprocesses are killed too.
else:
platformArgs = {"start_new_session": True}
# Note: "preexec_fn"=os.setsid is the old way before python-3.2
chunk.subprocess = psutil.Popen(
cmdList,
stdout=logF,
stderr=logF,
cwd=chunk.node.internalFolder,
env=env,
**platformArgs,
)
if hasattr(chunk, "statThread"):
# We only have a statThread if the node is running in the current process
# and not in a dedicated environment/process.
chunk.statThread.proc = chunk.subprocess
stdout, stderr = chunk.subprocess.communicate()
chunk.status.returnCode = chunk.subprocess.returncode
if chunk.subprocess.returncode and chunk.subprocess.returncode < 0:
signal_num = -chunk.subprocess.returncode
logF.write(f"Process was killed by signal: {signal_num}")
try:
status = chunk.subprocess.status()
logF.write(f"Process status: {status}")
except:
pass
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))
finally:
chunk.subprocess = None
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 chunk.subprocess:
logging.warning(f"[{chunk.node.name}] stopProcess: no subprocess")
return
# Retrieve process tree
processes = chunk.subprocess.children(recursive=True) + [chunk.subprocess]
logging.debug(f"[{chunk.node.name}] Processes to stop: {len(processes)}")
for process in processes:
try:
# With terminate, the process has a chance to handle cleanup
process.terminate()
except psutil.NoSuchProcess:
pass
# If it is still running, force kill it
for process in processes:
try:
# Use is_running() instead of poll() as we use a psutil.Process object
if process.is_running(): # Check if process is still alive
process.kill() # Forcefully kill it
except psutil.NoSuchProcess:
logging.info(f"[{chunk.node.name}] Process already terminated.")
except psutil.AccessDenied:
logging.info(f"[{chunk.node.name}] Permission denied to kill the process.")
class InputNode(BaseNode):
"""
Node that does not need to be processed, it is just a placeholder for inputs.
"""
def __init__(self):
super(InputNode, self).__init__()
def getMrNodeType(self):
return MrNodeType.INPUT
def processChunk(self, chunk):
pass
class Node(BaseNode):
def __init__(self):
super(Node, self).__init__()
def getMrNodeType(self):
return MrNodeType.NODE
def processChunkInEnvironment(self, chunk):
meshroomComputeCmd = f"python {_MESHROOM_COMPUTE} {chunk.node.graph.filepath} --node {chunk.node.name} --extern --inCurrentEnv"
if len(chunk.node.getChunks()) > 1:
meshroomComputeCmd += f" --iteration {chunk.range.iteration}"
runtimeEnv = None
self.executeChunkCommandLine(chunk, meshroomComputeCmd, env=runtimeEnv)
class CommandLineNode(BaseNode):
"""
"""
commandLine = '' # need to be defined on the node
parallelization = None
commandLineRange = ''
def __init__(self):
super(CommandLineNode, self).__init__()
def getMrNodeType(self):
return MrNodeType.COMMANDLINE
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 processChunk(self, chunk):
cmd = self.buildCommandLine(chunk)
# TODO: Setup runtime env
self.executeChunkCommandLine(chunk, cmd)
# 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]