Trigger event when CLI run has completed

As in the title I would like to be able to trigger some code after a CLI run has completed.

I know I can use the parameter wait_for_completion=True to pause the code execution, however if I do this the GUI does not update and the qCLIProgressBar does not run making it look as though the program has hung.

I have tried setting a connect condition to be triggered by the progress bar…

self.bar3.connect(‘valueChanged(int)’, self.onBar3)

but the qSlicerCLIProgressBar does not seem to give out the same signals as the qProgressBar.

I also tried using a While / Sleep loop to check the status of the cliNode (cliNode.GetStatus()) but this freezes the GUI just like the wait_for_completion parameter.

Is there any way to wait for the CLI to complete without freezing the GUI or (which would seem neater to me) to trigger a new routine once it has completed.

Thanks

Yes, you can put an observer on the command line module node and get events when the state changes. See “Running CLI in the background” here:

https://www.slicer.org/wiki/Documentation/Nightly/Developers/Python_scripting#Running_a_CLI_from_Python

2 Likes

Hi @pieper,
I am facing an issue when trying to react to a cli node’s execution completion. I saw the example you have linked below.

I attempted to do the same in cpp. The problem I am facing is that, until I open the python console and click enter, or open the data module, the status of my CLI node is not updating. For example, I send a request for a CLI module’s execution and opened the CLI module’s widget. The widget’s timer keeps updating till the CLI module runs. For example if the module’s execution took 20s, the widget reaches 20s and stops counting. Once the data module is opened after the 20s it needed for execution, the callbacks I registered are getting called. But these callbacks had to run when the vtkMRMLCommandLineModuleNode::StatusModifiedEvent fired as soon as the 20s where over, not when I am opening the data module or clicking enter into the python console. It seems that the event - vtkMRMLCommandLineModuleNode::StatusModifiedEvent is not getting fired even after the CLI node has finished execution.

This is how I am registering the callbacks inside my qSlicerExampleModuleWidget :-

vtkSmartPointer<vtkCallbackCommand> statusCallback = vtkSmartPointer<vtkCallbackCommand>::New();
statusCallback->SetCallback([](vtkObject* caller, unsigned long eventId, void* clientData, void* callData) {
  auto* moduleWidget = static_cast<qSlicerExampleModuleWidget*>(clientData);
  if (!moduleWidget)
  {
    qCritical() << Q_FUNC_INFO << ": failed to cast clientData to qSlicerExampleModuleWidget";
    return;
  }

  auto* cliNode = vtkMRMLCommandLineModuleNode::SafeDownCast(caller);
  if (!cliNode)
  {
    qCritical() << Q_FUNC_INFO << ": failed to cast caller to vtkMRMLCommandLineModuleNode";
    return;
  }

  moduleWidget->onCliStatusChanged(caller, (void*)cliNode->GetID(), eventId, callData);
});
statusCallback->SetClientData(this);
cliNode->AddObserver(vtkMRMLCommandLineModuleNode::StatusModifiedEvent, statusCallback);

cliNode->SetParameterAsString("inputPathA", std::string(inputPathA));
cliNode->SetParameterAsString("inputPathB", std::string(inputPathB));

cliLogic->Apply(cliNode, false);

qInfo() << Q_FUNC_INFO << ": CLI request sent successfully";

Whatever is inside moduleWidget->onCliStatusChangedis only getting called when I move to the data module or click enter in the python console. Any suggestions on why this could be happening? I tried using the vtkCommand::ModifiedEvent as well but got the same results. Any help would be greatly appreciated.

Thanks!

Hi all,
When I am running the CLI module synchronously, the callbacks are executing as soon as the CLI execution completes. But it is in the asynchronous CLI execution I am facing problems on the CLI node’s status updating reliably.

Working code :-

**everything else the same**

 cliLogic->ApplyAndWait(cliNode, false);

Hi,
I am providing some more context to this problem I am facing, and any help in debugging this issue would be greatly appreciated.
This is the pseudo code for what my CLI module is doing :-

import time


def main(mode: str):

     if mode == "sleep":

        time.sleep(30)  # vtkMRMLCommandLineModuleNode::StatusModifiedEvent arrives on time
     else:
        # vtkMRMLCommandLineModuleNode::StatusModifiedEvent only fires after UI interaction
        run_long_pipeline()  # torch + cv2 + long processing chain


def run_long_pipeline():

     # many steps...

     # process exits normally and outputs are valid

     pass


if __name__ == "__main__":

     main(mode="pipeline")

The reason I have written it this way is to demonstrate that, if I use a time.sleep(t) on the process running the CLI module, my custom widget is able to react precisely on time when the CLI module finishes execution. But when a custom pipeline I have written is called, the status of the CLI module’s node is not updating correctly.

I am certain that the cli node’s status is not updating correctly because I wrote a polling function using QTimer inside my custom widget “qSlicerExampleModuleWidget” to verify that. It’s status is updating only on specific interaction with the UI after it completes.

The UI interaction which does that is - if I change the current selected module after the cli module finished execution, the cli node’s status seems to update immediately, and my widget is able to react to it.

Another UI interaction that does the same is what I had mentioned above - which is clicking the enter button inside the python console. Would anyone have any idea what could cause such behaviour?

I’ve been working on a slicer-skill to see if AI agents can help with slicer development. I put your question in and I got the answer below. I don’t know if it’ll fix your problem but it sounds about right.

<ai>

Root Cause Analysis: CLI StatusModifiedEvent Not Firing Until UI Interaction

This is a well-understood architectural issue in Slicer. Here’s exactly what’s happening and why.

The Status Update Chain

When a CLI module runs asynchronously via Apply(), status updates follow this path:

Background Thread (ApplyTask):
  node->SetStatus(Completing, false)   // flags InvokeStatusModifiedEvent=true, but NO Modified() call
  appLogic->RequestModified(node)      // pushes node onto a thread-safe queue
          |
          v
Main Thread Queue (InternalModifiedQueue):
  [node sits waiting in queue...]
          |
          v
QTimer::singleShot chain:             ← REQUIRES Qt EVENT LOOP TO BE SPINNING
  processAppLogicModified()
    → ProcessModified()
      → node->Modified()
        → StatusModifiedEvent          ← YOUR OBSERVER FIRES HERE

The critical link is step 3: ProcessModified() is triggered by a self-rescheduling QTimer::singleShot chain managed in qSlicerCoreApplication. These timers can only fire when Qt’s event loop is actively processing events.

The Key Question: Is This a Scripted CLI or Executable CLI?

If your CLI module is a scripted CLI module (Python-based, *.py with the CLI XML descriptor), this is almost certainly the cause:

  • Scripted CLI modules run in Slicer’s main thread due to Python’s GIL (Global Interpreter Lock). Even though Slicer tries to run them in a background thread, the GIL forces Python execution back onto the main thread.
  • While your torch + cv2 pipeline is running, it monopolizes the main thread. The Qt event loop cannot spin, so QTimer::singleShot callbacks never fire, and ProcessModified() is never called.
  • time.sleep() works because it releases the GIL, allowing the Qt event loop to process pending events during the sleep.

This is confirmed by Steve Pieper in a related discourse thread: “CLI modules are run in independent threads, while python code runs in the main thread, so that may be the difference.”

If your CLI module is an executable CLI (separate process), the computation itself shouldn’t block the main thread, but something else in your setup might be (e.g., synchronous I/O waiting, a blocking call in the module widget, etc.).

Why UI Interaction “Fixes” It

When you click in the Python console, switch modules, or perform any UI interaction, Qt processes pending events as part of handling that interaction. This finally allows the queued QTimer::singleShot to fire → ProcessModified() runs → Modified() is called → StatusModifiedEvent fires.

Solutions

Option 1: Convert to an executable CLI module (recommended for heavy computation)

Executable CLI modules run as a completely separate process, so they don’t block Slicer’s main thread or the Qt event loop. This is the cleanest solution for torch/cv2 pipelines.

Option 2: If you must use a scripted CLI, call slicer.app.processEvents() periodically

Insert calls to process the event loop during your pipeline:

import slicer

def run_long_pipeline():
    step1_result = step1()
    slicer.app.processEvents()  # let Qt process pending events
    
    step2_result = step2(step1_result)
    slicer.app.processEvents()
    
    # ... etc

This is a workaround, not ideal for production, but it unblocks the event loop during execution.

Option 3: Use slicer.cli.run() with wait_for_completion=False and an observer

If you’re launching the CLI from Python, the standard pattern is:

cliNode = slicer.cli.run(module, parameters, wait_for_completion=False)
cliNode.AddObserver('ModifiedEvent', onCLIStatusChanged)

But this still requires the event loop to be spinning in the main thread.

Option 4: Use QProcess or subprocess to run the heavy pipeline externally

Wrap your torch/cv2 pipeline as a standalone script that runs in a separate process via QProcess or Python’s subprocess, keeping the main thread free.

Relevant Discourse Threads

TL;DR

Your heavy Python pipeline (torch + cv2) is blocking Slicer’s main thread via the GIL, which prevents the Qt event loop from processing the QTimer::singleShot callbacks that deliver StatusModifiedEvent. time.sleep() works because it releases the GIL. The fix is to either convert to an executable CLI module or ensure the main thread’s event loop can spin during execution.
</ai>

1 Like

Hi @pieper,
Thank you for the quick and detailed reply. I will go through this and get back to you.