2
$\begingroup$

How can I ensure an info message is displayed before a subprocess call in Blender? When executing a script from the 3D Viewport using F3 and running the operator, the "Processing... Please wait" message does not appear until after the subprocess completes. Instead, only the "Process complete!" message is shown at the end. It seems that any info or print text before a timer or subprocess routine does not display until after the operation finishes even though it was called before the subprocess call.

enter image description here

import bpy
import subprocess

class SimpleOperator(bpy.types.Operator):
    bl_idname = "object.simple_operator"
    bl_label = "Simple Operator"

    @classmethod
    def poll(cls, context):
        return True

    def execute(self, context):
        self.report({'INFO'}, "Processing... Please wait") # Does not get displayed
        command = ["powershell", "-Command", "Start-Sleep -Seconds 2; Write-Output 'Completed long-running task'"]
        result = subprocess.run(command, capture_output=True, text=True, shell=True, check=True)
        output = result.stdout
        self.report({'INFO'}, "Process complete!")

        return {'FINISHED'}

def register():
    bpy.utils.register_class(SimpleOperator)

def unregister():
    bpy.utils.unregister_class(SimpleOperator)

if __name__ == "__main__":
    register()

I need to implement this because my operator performs background installation tasks. To keep the user informed, I want to display a message indicating that the installation is in progress and that they should wait. I plan to achieve this using a straightforward message with self.report({'INFO'}, "info") to keep it as simple as possible.

Workaround using a timer:

I tried a workaround using a popup using bpy.app.timers.register but this will only work for simple operator examples like this one:

import bpy
import subprocess

class SimpleOperator(bpy.types.Operator):
    bl_idname = "object.simple_operator"
    bl_label = "Simple Operator"
    
    def execute(self, context):
        context.window_manager.popup_menu(lambda self, context: self.layout.label(text="Installation in progress... Please wait."), title="Info", icon='INFO')
        bpy.app.timers.register(self.execute_command, first_interval=0.1)
        return {'FINISHED'}

    def execute_command(self):
        command = ["powershell", "-Command", "Start-Sleep -Seconds 5; Write-Output 'Completed long-running task'"]
        result = subprocess.run(command, capture_output=True, text=True, check=True)
        print("Command output:", result.stdout)
        print("Command error:", result.stderr)

def register():
    bpy.utils.register_class(SimpleOperator)

def unregister():
    bpy.utils.unregister_class(SimpleOperator)

if __name__ == "__main__":
    register()

enter image description here

But it will not work for more complex operator examples like:

class OperatorInstallBase(bpy.types.Operator):
    bl_options = {'INTERNAL'}

    def get_script_path(self):
        raise NotImplementedError("Subclasses must implement this method")

    def run_script(self, script_path, *args):
        command = ["powershell", "-File", script_path]
        result = subprocess.run(command, capture_output=True, text=True, check=True)
        return result.stdout

    def execute_script(self):
        script_path = self.get_script_path()
        output = self.run_script(script_path)
        # process output

    def execute(self, context):
        context.window_manager.popup_menu(lambda self, context: self.layout.label(text="Installation in progress... Please wait."), title="Info", icon='INFO')
        context.view_layer.update()
        bpy.app.timers.register(self.execute_script, first_interval=0.1)
        return {'FINISHED'}

where you get errors like:

Traceback (most recent call last):
  File "C:\Users\harry\AppData\Roaming\Blender Foundation\Blender\4.0\scripts\addons\blender_web_pro\operators\installation\operator_install_base.py", line 39, in execute_script
    script_path = self.get_script_path()
  File "c:\Program Files\Blender Foundation\Blender 4.0\4.0\scripts\modules\bpy_types.py", line 898, in __getattribute__
    properties = StructRNA.path_resolve(self, "properties")
ReferenceError: StructRNA of type WEB_OT_OperatorInstallX has been removed
$\endgroup$

1 Answer 1

1
$\begingroup$

Since Blocking in bl_options is invalid in some cases, see:
How to lock the always on top subinterface when run a operator

suggest do something to prevent the user from controlling the child windows.

It is not recommended to put the execute_command function as a self function in this class. Otherwise, you will receive a ReferenceError when the operator finish. Blender will remove the operator instance when the operator is not running, and become unable to access.

import bpy, weakref, subprocess

class BlockUI(bpy.types.Operator):
    __slots__ = '__weakref__'

    bl_idname = "wm.blocking"
    bl_label = "Modal for blocking"
    bl_options = {"INTERNAL"}

    fin_request = False
    modals = weakref.WeakValueDictionary() # items will automatically remove when modal finished

    def execute(self, context):
        context.window_manager.modal_handler_add(self)
        BlockUI.modals[context.window] = self
        return {'RUNNING_MODAL'}

    def modal(self, context, event):
        if BlockUI.fin_request is True:
            return {'CANCELLED'}
        return {'RUNNING_MODAL'}

    @classmethod
    def lock_UI(cls):
        cls.fin_request = False

        for w in bpy.context.window_manager.windows:
            if w in cls.modals:
                continue

            with bpy.context.temp_override(window=w):
                bpy.ops.wm.blocking()

    @classmethod
    def unlock_UI(cls):
        cls.fin_request = True
    

class SimpleOperator(bpy.types.Operator):
    bl_idname = "object.simple_operator"
    bl_label = "Simple Operator"
    
    xy: bpy.props.IntVectorProperty(size=2)

    def execute(self, context):
        context.window_manager.popup_menu(lambda self, context: self.layout.label(text="Installation in progress... Please wait. Interface will lock."), title="Info", icon='INFO')
        
        init_data = {"xy": self.xy[:]}
        bpy.app.timers.register(
            lambda: execute_command(init_data),
            first_interval=0.1)
        return {'FINISHED'}

def execute_command(init_data):
    print("start....")
    print(init_data)
    BlockUI.lock_UI()
    command = ["powershell", "-Command", "Start-Sleep -Seconds 5; Write-Output 'Completed long-running task'"]
    result = subprocess.run(command, capture_output=True, text=True, check=True)
    print("Command output:", result.stdout)
    print("Command error:", result.stderr)
    BlockUI.unlock_UI()
    bpy.context.window_manager.popup_menu(lambda self, context: self.layout.label(text="Done."), title="Info", icon='INFO')

classes = (
    BlockUI,
    SimpleOperator,
)

def register():
    for cls in classes:
        bpy.utils.register_class(cls)

if __name__ == "__main__":
    register()

without using timer

import bpy, weakref, subprocess

class BlockUI(bpy.types.Operator):
    __slots__ = '__weakref__'

    bl_idname = "wm.blocking"
    bl_label = "Modal for blocking"
    bl_options = {"INTERNAL"}

    fin_request = False
    modals = weakref.WeakValueDictionary() # items will automatically remove when modal finished

    def execute(self, context):
        context.window_manager.modal_handler_add(self)
        BlockUI.modals[context.window] = self
        return {'RUNNING_MODAL'}

    def modal(self, context, event):
        if BlockUI.fin_request is True:
            return {'CANCELLED'}
        return {'RUNNING_MODAL'}

    @classmethod
    def lock_UI(cls):
        cls.fin_request = False

        for w in bpy.context.window_manager.windows:
            if w in cls.modals:
                continue

            with bpy.context.temp_override(window=w):
                bpy.ops.wm.blocking()

    @classmethod
    def unlock_UI(cls):
        cls.fin_request = True
    
class NullOps(bpy.types.Operator):
    bl_idname = "wm.nullops"
    bl_label = "Null"

    fn = None

    def __del__(self):
        if self.__class__.fn:
            self.__class__.fn()

    def invoke(self, context, event):
        context.window_manager.modal_handler_add(self)
        return {'RUNNING_MODAL'}
    def modal(self, context, event):
        return {'FINISHED'}

class SimpleOperator(bpy.types.Operator):
    bl_idname = "object.simple_operator"
    bl_label = "Simple Operator"
    
    xy: bpy.props.IntVectorProperty(size=2)

    def execute(self, context):
        context.window_manager.popup_menu(lambda self, context: self.layout.label(text="Installation in progress... Please wait. Interface will lock."), title="Info", icon='INFO')
        
        init_data = {"xy": self.xy[:]}
        NullOps.fn = lambda: execute_command(init_data)
        bpy.ops.wm.nullops("INVOKE_DEFAULT")
        return {'FINISHED'}

def execute_command(init_data):
    print("start....")
    print(init_data)
    BlockUI.lock_UI()
    command = ["powershell", "-Command", "Start-Sleep -Seconds 5; Write-Output 'Completed long-running task'"]
    result = subprocess.run(command, capture_output=True, text=True, check=True)
    print("Command output:", result.stdout)
    print("Command error:", result.stderr)
    BlockUI.unlock_UI()
    bpy.context.window_manager.popup_menu(lambda self, context: self.layout.label(text="Done."), title="Info", icon='INFO')

classes = (
    BlockUI,
    NullOps,
    SimpleOperator,
)

def register():
    for cls in classes:
        bpy.utils.register_class(cls)

if __name__ == "__main__":
    register()

Some tips

import bpy
import subprocess

class BlockUI(bpy.types.Operator):
    bl_idname = "wm.blocking"
    bl_label = "Blocking Modal"
    bl_options = {"INTERNAL"}

    _finish_request = False
    _active_modals = {}

    def execute(self, context):
        BlockUI._finish_request = False
        BlockUI._active_modals.clear()

        context.window_manager.modal_handler_add(self)
        BlockUI._active_modals[context.window] = self
        return {'RUNNING_MODAL'}

    def modal(self, context, event):
        if BlockUI._finish_request:
            BlockUI._active_modals.pop(context.window, None)
            return {'CANCELLED'}
        return {'RUNNING_MODAL'}

    @classmethod
    def lock_UI(cls, except_window=None):
        cls._finish_request = False

        for window in bpy.context.window_manager.windows:
            if window is except_window:
                continue
            if window not in cls._active_modals:
                with bpy.context.temp_override(window=window):
                    bpy.ops.wm.blocking()

    @classmethod
    def unlock_UI(cls):
        cls._finish_request = True
        cls._active_modals.clear()

class SimpleOperator(bpy.types.Operator):
    bl_idname = "object.simple_operator"
    bl_label = "Simple Operator"

    process = None

    def execute(self, context):
        context.window_manager.popup_menu(
            lambda self, context: self.layout.label(text="Installation in progress... Please wait. Interface will lock."),
            title="Info",
            icon='INFO'
        )
        self.is_multi_window = len(context.window_manager.windows) != 1
        if self.is_multi_window:
            BlockUI.lock_UI(except_window=context.window)

        command = ["powershell", "-Command", "Start-Sleep -Seconds 5; Write-Output 'Completed long-running task'"]
        self.process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
        context.window_manager.modal_handler_add(self)

        return {'RUNNING_MODAL'}

    def modal(self, context, event):
        # need catch here because when your code raise error,
        # operartor will dead and unlock_UI will never run, and the sub-window will lock forever.
        
        # Another problem is: you need add a modal timer to keep the modal running,
        # Otherwise, the modal will not run when the mouse is not moving or no keyboard events.
        try:
            if self.process.poll() is not None:
                output, error = self.process.communicate()
                print("Command output:", output)
                print("Command error:", error)
                BlockUI.unlock_UI()
                return {'FINISHED'}
        except:
            BlockUI.unlock_UI()
            return {'FINISHED'}

        return {'RUNNING_MODAL'}

classes = (
    BlockUI,
    SimpleOperator,
)

def register():
    for cls in classes:
        bpy.utils.register_class(cls)

if __name__ == "__main__":
    register()

$\endgroup$
3
  • $\begingroup$ perfect! wow thank you so much! I wish I could upvote it more times :) $\endgroup$
    – Harry McKenzie
    Commented Aug 21 at 3:46
  • $\begingroup$ I have tried to make it simpler by removing the NullOps and without the global function execute_command. Everthing is now member funcs. This version pastecode.io/s/zhsta9io but I'm not sure how robust it is. For now it works but there was 1 time it hang. Not sure if it's because of this code. What do you think? Maybe you can see the problem in it? Or should it work perfectly? $\endgroup$
    – Harry McKenzie
    Commented Aug 21 at 5:15
  • $\begingroup$ It looks ok, I change the main modal to {"RUNNING_MODAL"}. See above. $\endgroup$
    – X Y
    Commented Aug 21 at 7:53

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .