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>