New notion of local isolated computation for python nodes using meshroom_compute

Reoganization
- BaseNode: is the base class for all nodes
- Node: is now dedicated to python nodes, with the implentation directly
in the process function
- CommandLineNode: dedicated to generate and run external command lines
This commit is contained in:
Fabien Castan 2025-03-24 00:03:45 +01:00
parent faece7efca
commit 727a4d129b
6 changed files with 288 additions and 152 deletions

View file

@ -16,7 +16,7 @@ meshroom.setupEnvironment()
import meshroom.core
import meshroom.core.graph
from meshroom.core.node import Status
from meshroom.core.node import Status, ExecMode
parser = argparse.ArgumentParser(description='Execute a Graph of processes.')
@ -26,6 +26,8 @@ parser.add_argument('--node', metavar='NODE_NAME', type=str,
help='Process the node. It will generate an error if the dependencies are not already computed.')
parser.add_argument('--toNode', metavar='NODE_NAME', type=str,
help='Process the node with its dependencies.')
parser.add_argument('--inCurrentEnv', help='Execute process in current env without creating a dedicated runtime environment.',
action='store_true')
parser.add_argument('--forceStatus', help='Force computation if status is RUNNING or SUBMITTED.',
action='store_true')
parser.add_argument('--forceCompute', help='Compute in all cases even if already computed.',
@ -81,7 +83,11 @@ if args.node:
chunks = node.chunks
for chunk in chunks:
if chunk.status.status in submittedStatuses:
print('Warning: Node is already submitted with status "{}". See file: "{}"'.format(chunk.status.status.name, chunk.statusFile))
# Particular case for the LOCAL_ISOLATED, the node status is set to RUNNING by the submitter directly.
# We ensure that no other instance has started to compute, by checking that the sessionUid is empty.
if chunk.status.execMode == ExecMode.LOCAL_ISOLATED and not chunk.status.sessionUid and chunk.status.submitterSessionUid:
continue
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}')
# sys.exit(-1)
if args.extern:
@ -91,9 +97,9 @@ if args.node:
node.preprocess()
if args.iteration != -1:
chunk = node.chunks[args.iteration]
chunk.process(args.forceCompute)
chunk.process(args.forceCompute, args.inCurrentEnv)
else:
node.process(args.forceCompute)
node.process(args.forceCompute, args.inCurrentEnv)
node.postprocess()
else:
if args.iteration != -1:

View file

@ -21,36 +21,9 @@ from .computation import (
)
from .node import (
AVCommandLineNode,
BaseNode,
CommandLineNode,
InitNode,
InputNode,
Node,
)
__all__ = [
# attribute
"Attribute",
"BoolParam",
"ChoiceParam",
"ColorParam",
"File",
"FloatParam",
"GroupAttribute",
"IntParam",
"ListAttribute",
"PushButtonParam",
"StringParam",
# computation
"DynamicNodeSize",
"Level",
"MultiDynamicNodeSize",
"Parallelization",
"Range",
"StaticNodeSize",
# node
"AVCommandLineNode",
"CommandLineNode",
"InitNode",
"InputNode",
"Node",
]

View file

@ -1,16 +1,37 @@
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
class Node(object):
_MESHROOM_ROOT = Path(meshroom.__file__).parent.parent
_MESHROOM_COMPUTE = _MESHROOM_ROOT / "bin" / "meshroom_compute"
def isNodeSaved(node):
"""Returns whether a node is identical to its serialized counterpart in the current graph file."""
filepath = node.graph.filepath
if not filepath:
return False
from meshroom.core.graph import loadGraph
graphSaved = loadGraph(filepath)
nodeSaved = graphSaved.node(node.name)
if nodeSaved is None:
return False
return nodeSaved._uid == node._uid
class BaseNode(object):
"""
"""
cpu = Level.NORMAL
@ -62,7 +83,7 @@ class Node(object):
category = 'Other'
def __init__(self):
super(Node, self).__init__()
super(BaseNode, self).__init__()
self.hasDynamicOutputAttribute = any(output.isDynamicValue for output in self.outputs)
self.sourceCodeFolder = Path(getfile(self.__class__)).parent.resolve().as_posix()
@ -113,13 +134,102 @@ class Node(object):
pass
def stopProcess(self, chunk):
raise NotImplementedError('No stopProcess implementation on node: {}'.format(chunk.node.name))
logging.warning(f'No stopProcess implementation on node: {chunk.node.name}')
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)
class InputNode(Node):
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:
print(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.
"""
@ -130,7 +240,24 @@ class InputNode(Node):
pass
class CommandLineNode(Node):
class Node(BaseNode):
def __init__(self):
super(Node, self).__init__()
def processChunkInEnvironment(self, chunk):
if not isNodeSaved(chunk.node):
raise RuntimeError("File must be saved before computing in isolated environment.")
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
@ -143,14 +270,14 @@ class CommandLineNode(Node):
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)
# # 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:
@ -158,48 +285,10 @@ class CommandLineNode(Node):
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
# TODO: Setup runtime env
self.executeChunkCommandLine(chunk, cmd)
# Specific command line node for AliceVision apps
@ -282,3 +371,4 @@ class InitNode(object):
for attr in attributesDict:
if node.hasAttribute(attr):
node.attribute(attr).value = attributesDict[attr]

View file

@ -57,6 +57,7 @@ class Status(Enum):
class ExecMode(Enum):
NONE = auto()
LOCAL = auto()
LOCAL_ISOLATED = auto()
EXTERN = auto()
@ -67,20 +68,13 @@ class StatusData(BaseObject):
def __init__(self, nodeName='', nodeType='', packageName='', packageVersion='', parent: BaseObject = None):
super(StatusData, self).__init__(parent)
self.status = Status.NONE
self.execMode = ExecMode.NONE
self.nodeName = nodeName
self.nodeType = nodeType
self.packageName = packageName
self.packageVersion = packageVersion
self.graph = ''
self.commandLine = None
self.env = None
self.startDateTime = ""
self.endDateTime = ""
self.elapsedTime = 0
self.hostname = ""
self.sessionUid = meshroom.core.sessionUid
self.reset()
self.nodeName: str = nodeName
self.nodeType: str = nodeType
self.packageName: str = packageName
self.packageVersion: str = packageVersion
self.sessionUid: Optional[str] = meshroom.core.sessionUid
self.submitterSessionUid: Optional[str] = None
def merge(self, other):
self.startDateTime = min(self.startDateTime, other.startDateTime)
@ -88,27 +82,44 @@ class StatusData(BaseObject):
self.elapsedTime += other.elapsedTime
def reset(self):
self.status = Status.NONE
self.execMode = ExecMode.NONE
self.graph = ''
self.commandLine = None
self.env = None
self.startDateTime = ""
self.endDateTime = ""
self.elapsedTime = 0
self.hostname = ""
self.sessionUid = meshroom.core.sessionUid
self.nodeName: str = ""
self.nodeType: str = ""
self.packageName: str = ""
self.packageVersion: str = ""
self.resetDynamicValues()
def resetDynamicValues(self):
self.status: Status = Status.NONE
self.execMode: ExecMode = ExecMode.NONE
self.graph = ""
self.commandLine: str = ""
self.env: str = ""
self._startTime: Optional[datetime.datetime] = None
self.startDateTime: str = ""
self.endDateTime: str = ""
self.elapsedTime: float = 0.0
self.hostname: str = ""
def initStartCompute(self):
import platform
self.sessionUid = meshroom.core.sessionUid
self.hostname = platform.node()
self._startTime = time.time()
self.startDateTime = datetime.datetime.now().strftime(self.dateTimeFormatting)
# to get datetime obj: datetime.datetime.strptime(obj, self.dateTimeFormatting)
def initSubmit(self):
''' When submitting a node, we reset the status information to ensure that we do not keep outdated information.
'''
self.resetDynamicValues()
self.sessionUid = None
self.submitterSessionUid = meshroom.core.sessionUid
def initEndCompute(self):
self.sessionUid = meshroom.core.sessionUid
self.endDateTime = datetime.datetime.now().strftime(self.dateTimeFormatting)
if self._startTime != None:
self.elapsedTime = time.time() - self._startTime
@property
def elapsedTimeStr(self):
@ -118,9 +129,12 @@ class StatusData(BaseObject):
d = self.__dict__.copy()
d["elapsedTimeStr"] = self.elapsedTimeStr
# Skip non data attributes from BaseObject
# Skip some attributes (some are from BaseObject)
d.pop("destroyed", None)
d.pop("objectNameChanged", None)
d.pop("_parent", None)
d.pop("_startTime", None)
return d
def fromDict(self, d):
@ -142,6 +156,7 @@ class StatusData(BaseObject):
self.elapsedTime = d.get('elapsedTime', 0)
self.hostname = d.get('hostname', '')
self.sessionUid = d.get('sessionUid', '')
self.submitterSessionUid = d.get('submitterSessionUid', '')
class LogManager:
@ -251,9 +266,9 @@ class NodeChunk(BaseObject):
super(NodeChunk, self).__init__(parent)
self.node = node
self.range = range
self.logManager = LogManager(self)
self._status = StatusData(node.name, node.nodeType, node.packageName, node.packageVersion)
self.statistics = stats.Statistics()
self.logManager: LogManager = LogManager(self)
self._status: StatusData = StatusData(node.name, node.nodeType, node.packageName, node.packageVersion)
self.statistics: stats.Statistics = stats.Statistics()
self.statusFileLastModTime = -1
self.subprocess = None
# Notify update in filepaths when node's internal folder changes
@ -298,6 +313,7 @@ class NodeChunk(BaseObject):
try:
with open(statusFile, 'r') as jsonFile:
statusData = json.load(jsonFile)
# logging.debug(f"updateStatusFromCache({self.node.name}): From status {self.status.status} to {statusData['status']}")
self.status.fromDict(statusData)
self.statusFileLastModTime = os.path.getmtime(statusFile)
except Exception:
@ -343,12 +359,9 @@ class NodeChunk(BaseObject):
renameWritingToFinalPath(statusFilepathWriting, statusFilepath)
def upgradeStatusTo(self, newStatus, execMode=None):
if newStatus.value <= self._status.status.value:
logging.warning("Downgrade status on node '{}' from {} to {}".
format(self.name, self._status.status, newStatus))
if newStatus.value < self._status.status.value:
logging.warning(f"Downgrade status on node '{self.name}' from {self._status.status} to {newStatus}")
if newStatus == Status.SUBMITTED:
self._status = StatusData(self.node.name, self.node.nodeType, self.node.packageName, self.node.packageVersion)
if execMode is not None:
self._status.execMode = execMode
self.execModeNameChanged.emit()
@ -397,15 +410,22 @@ class NodeChunk(BaseObject):
def isFinished(self):
return self._status.status == Status.SUCCESS
def process(self, forceCompute=False):
def process(self, forceCompute=False, inCurrentEnv=False):
if not forceCompute and self._status.status == Status.SUCCESS:
logging.info("Node chunk already computed: {}".format(self.name))
return
# Start the process environment for nodes running in isolation.
# This only happens once, when the node has the SUBMITTED status.
# The sub-process will go through this method again, but the node status will have been set to RUNNING.
if not inCurrentEnv and isinstance(self.node.nodeDesc, desc.Node):
self._processInIsolatedEnvironment()
return
global runningProcesses
runningProcesses[self.name] = self
self._status.initStartCompute()
exceptionStatus = None
startTime = time.time()
executionStatus = None
self.upgradeStatusTo(Status.RUNNING)
self.statThread = stats.StatisticsThread(self)
self.statThread.start()
@ -413,18 +433,18 @@ class NodeChunk(BaseObject):
self.node.nodeDesc.processChunk(self)
# NOTE: this assumes saving the output attributes for each chunk
self.node.saveOutputAttr()
executionStatus = Status.SUCCESS
except Exception:
if self._status.status != Status.STOPPED:
exceptionStatus = Status.ERROR
executionStatus = Status.ERROR
raise
except (KeyboardInterrupt, SystemError, GeneratorExit):
exceptionStatus = Status.STOPPED
executionStatus = Status.STOPPED
raise
finally:
self._status.initEndCompute()
self._status.elapsedTime = time.time() - startTime
if exceptionStatus is not None:
self.upgradeStatusTo(exceptionStatus)
if executionStatus:
self.upgradeStatusTo(executionStatus)
logging.info(" - elapsed time: {}".format(self._status.elapsedTimeStr))
# Ask and wait for the stats thread to stop
self.statThread.stopRequest()
@ -432,19 +452,43 @@ class NodeChunk(BaseObject):
self.statistics = stats.Statistics()
del runningProcesses[self.name]
self.upgradeStatusTo(Status.SUCCESS)
def _processInIsolatedEnvironment(self):
"""Process this node chunk in the isolated environment defined in the environment configuration."""
try:
self._status.initSubmit()
self.upgradeStatusTo(Status.RUNNING, execMode=ExecMode.LOCAL_ISOLATED)
self.node.nodeDesc.processChunkInEnvironment(self)
except:
# status should be already updated by meshroom_compute
self.updateStatusFromCache()
if self._status.status != Status.ERROR:
# If meshroom_compute has crashed or been killed, the status may have not been set to ERROR.
# In this particular case, we enforce it from here.
self.upgradeStatusTo(Status.ERROR)
raise
# Update the chunk status.
self.updateStatusFromCache()
# Update the output attributes, as any chunk may have modified them.
self.node.updateOutputAttr()
def stopProcess(self):
if not self.isExtern():
if self._status.status == Status.RUNNING:
if self.isExtern():
return
if self._status.status != Status.RUNNING:
return
self.upgradeStatusTo(Status.STOPPED)
elif self._status.status == Status.SUBMITTED:
self.upgradeStatusTo(Status.NONE)
self.node.nodeDesc.stopProcess(self)
def isExtern(self):
return self._status.execMode == ExecMode.EXTERN or (
self._status.execMode == ExecMode.LOCAL and self._status.sessionUid != meshroom.core.sessionUid)
""" The computation is managed externally by another instance of Meshroom, or by meshroom_compute on renderfarm).
In the ambiguous case of an isolated environment, it is considered as local as we can stop it.
"""
if self._status.execMode == ExecMode.LOCAL_ISOLATED:
# It is a local isolated node, check if it is submitted by our current session.
return self._status.submitterSessionUid != meshroom.core.sessionUid
return self._status.sessionUid != meshroom.core.sessionUid
statusChanged = Signal()
status = Property(Variant, lambda self: self._status, notify=statusChanged)
@ -845,7 +889,13 @@ class BaseNode(BaseObject):
Status will be reset to Status.NONE
"""
if self.internalFolder and os.path.exists(self.internalFolder):
try:
shutil.rmtree(self.internalFolder)
except Exception as e:
# We could get some "Device or resource busy" on .nfs file while removing the folder on linux network.
# On windows, some output files may be open for visualization and the removal will fail.
# On both cases, we can ignore it.
logging.warning(f"Failed to remove internal folder: '{self.internalFolder}'. Error: {e}.")
self.updateStatusFromCache()
@Slot(result=str)
@ -1063,6 +1113,7 @@ class BaseNode(BaseObject):
def submit(self, forceCompute=False):
for chunk in self._chunks:
if forceCompute or chunk.status.status != Status.SUCCESS:
chunk._status.initSubmit()
chunk.upgradeStatusTo(Status.SUBMITTED, ExecMode.EXTERN)
def beginSequence(self, forceCompute=False):
@ -1077,9 +1128,9 @@ class BaseNode(BaseObject):
# Invoke the Node Description's pre-process for the Client Node to prepare its processing
self.nodeDesc.preprocess(self)
def process(self, forceCompute=False):
def process(self, forceCompute=False, inCurrentEnv=False):
for chunk in self._chunks:
chunk.process(forceCompute)
chunk.process(forceCompute, inCurrentEnv)
def postprocess(self):
# Invoke the post process on Client Node to execute after the processing on the node is completed
@ -1090,8 +1141,8 @@ class BaseNode(BaseObject):
return
if not self.nodeDesc.hasDynamicOutputAttribute:
return
# logging.warning("updateOutputAttr: {}, status: {}".format(self.name, self.globalStatus))
if self.getGlobalStatus() == Status.SUCCESS:
# logging.warning(f"updateOutputAttr: {self.name}, status: {self.globalStatus}")
if Status.SUCCESS in [c._status.status for c in self.getChunks()]:
self.loadOutputAttr()
else:
self.resetOutputAttr()
@ -1339,19 +1390,33 @@ class BaseNode(BaseObject):
return False
return True
def submitterStatusInThisSession(self):
if not self._chunks:
return False
for chunk in self._chunks:
if chunk.status.submitterSessionUid != meshroom.core.sessionUid:
return False
return True
@Slot(result=bool)
def canBeStopped(self):
# Only locked nodes running in local with the same
# sessionUid as the Meshroom instance can be stopped
return (self.locked and self.getGlobalStatus() == Status.RUNNING and
self.globalExecMode == "LOCAL" and self.statusInThisSession())
# logging.warning(f"[{self.name}] canBeStopped: globalExecMode={self.globalExecMode} globalStatus={self.getGlobalStatus()} statusInThisSession={self.statusInThisSession()}, submitterStatusInThisSession={self.submitterStatusInThisSession()}")
return (self.getGlobalStatus() == Status.RUNNING and
((self.globalExecMode == ExecMode.LOCAL.name and self.statusInThisSession()) or
(self.globalExecMode == ExecMode.LOCAL_ISOLATED.name and self.submitterStatusInThisSession())
))
@Slot(result=bool)
def canBeCanceled(self):
# Only locked nodes submitted in local with the same
# sessionUid as the Meshroom instance can be canceled
return (self.locked and self.getGlobalStatus() == Status.SUBMITTED and
self.globalExecMode == "LOCAL" and self.statusInThisSession())
# logging.warning(f"[{self.name}] canBeCanceled: globalExecMode={self.globalExecMode} globalStatus={self.getGlobalStatus()} statusInThisSession={self.statusInThisSession()}, submitterStatusInThisSession={self.submitterStatusInThisSession()}")
return (self.getGlobalStatus() == Status.SUBMITTED and
((self.globalExecMode == ExecMode.LOCAL.name and self.statusInThisSession()) or
(self.globalExecMode == ExecMode.LOCAL_ISOLATED.name and self.submitterStatusInThisSession())
))
def hasImageOutputAttribute(self):
"""

View file

@ -168,6 +168,7 @@ class _NodeCreator:
def _createNode(self) -> Node:
logging.info(f"Creating node '{self.name}'")
# TODO: user inputs/outputs may conflicts with internal names (like position, uid)
return Node(
self.nodeType,
position=self.position,

View file

@ -183,10 +183,11 @@ class ChunksMonitor(QObject):
return self.statusFiles, self.monitorableChunks
elif self.filePollerRefresh is PollerRefreshStatus.MINIMAL_ENABLED.value:
for c in self.monitorableChunks:
# Only chunks that are run externally should be monitored; when run locally, status changes are already notified
if c.isExtern():
# Only chunks that are run externally or local_isolated should be monitored,
# when run locally, status changes are already notified.
# Chunks with an ERROR status may be re-submitted externally and should thus still be monitored
if c._status.status in {Status.SUBMITTED, Status.RUNNING, Status.ERROR}:
if (c.isExtern() and c._status.status in (Status.SUBMITTED, Status.RUNNING, Status.ERROR)) or (
(c._status.execMode is ExecMode.LOCAL_ISOLATED) and (c._status.status in (Status.SUBMITTED, Status.RUNNING))):
files.append(c.statusFile)
chunks.append(c)
return files, chunks
@ -582,8 +583,8 @@ class UIGraph(QObject):
def updateGraphComputingStatus(self):
# update graph computing status
computingLocally = any([
(ch.status.execMode == ExecMode.LOCAL and
ch.status.sessionUid == sessionUid and
(((ch.status.execMode == ExecMode.LOCAL and ch.status.sessionUid == sessionUid) or
ch.status.execMode == ExecMode.LOCAL_ISOLATED) and
ch.status.status in (Status.RUNNING, Status.SUBMITTED))
for ch in self._sortedDFSChunks])
submitted = any([ch.status.status == Status.SUBMITTED for ch in self._sortedDFSChunks])