Non-blocking GUI while serving blocking external i/o

Pravin Kumar Sinha
7 min readJan 8, 2024

--

Introduction — GUI application works in callback or asynchronous call flow which is event driven in architecture. An event loop is maintained and all registered/scheduled callback functions gets executed one by one. An event, needs to get processed, must register with event loop manager. A callback function is associated with an event. Events in event loop get executed through execution of associated callback function.

GUI callbacks are called against GUI events. Making an blocking i/o from an event callback would delay the processing of GUI events leading to freezing GUI. This article discuss about serving an blocking external i/o in a GUI framework in a manner where non freezing GUI is possible.

Problem — How to making blocking external i/o in GUI related application?

Scenario — GUI application work in event callback program flow rather than sequential program flow, i.e ‘C programming’. An event, with associated callback function, needs to get registered with event loop manager. Once event triggers, event loop dispatcher/manager calls the associated callback function. Making a blocking i/o call from any of the event callback function would do freeze of the GUI, as execution of program would not return to the event loop. Barring event loop manager to dispatch any GUI event from the event loop.

Solution — Two things in this arena.

  1. I/O happens through a file descriptor, i.e socket.
  2. Along with all other event types, there is an timer event which gets triggered upon timeout expiry.

Solution is proposed to have i/o operation in ‘Timer’ event callback where ‘select’, i/o descriptor multiplexer, api is used to check for read or write activity on set/list of file descriptors in non-blocking manner. I/O would happen once ‘select’ api return without a timeout. Timeout would be zero making timer event callback completely non blocking.

External Reference — ‘select’ api is blocking when timeout argument is not provided, other wise non blocking with timeout. ‘select’ api device driver implementation can be found in book Linux Device Drivers 3rd edition. Published under Oreilly publication.

under subsection ‘Poll and Select’ in chapter ‘Advanced Char driver Operation’. Where as python implementation documentation can be found at docs.python.org

GUI programming’ event mechanism can be found in Xlib Programming Manual under chapter ‘Events’

In Python, ‘Event Loop’ is a subsection in topic ‘Async i/o’.

Design

                                           (1)
+------------> Check i/o
+----------+ / status using 'select'
+----> | callback | +--------------> Perform task
| +----------+ | (2) if i/o is green lighted
| |
| |
Timer | |
Callback | +-----+
| |
| | (3) Return on zero
| +----------+ | timeout or post processing
| / Events \ |
| |------------| |
| | ......... | |
+----- | ......... | |
| ......... |<----+
| ......... |
| ......... |
+------------+
Event Loop Manager

Design is made to have a timer event registered with Event Loop manager. Timer event callback function would do ‘select’ on i/o file descriptor in non blocking timeout mode. i/o would happen if descriptor is set for i/o. Timer callback function would return to event loop processing or zero timeout.

Experimental Output

We have scenario when Blender, a 3D GUI modeling tool, need to run python program that is external to the GUI tool and execute as standalone process. By default Blender supports “in application” python interpreter. Follow the image below.

Blender in process python IDE support.
Blender In Process/Application Python IDE

We need Blender python support as out process/application Python application. Something like

 -------------------    ipc      -------------  <<running>>
| Python Client | ----------> | Blender | o-----> Python
| Process | ------------- Server Process
-------------------
o
| <<Running>>
|
v
Python Program

Python client connects to Blender running python server code through socket connection.

                                        <<manages event loop>>
|
v
------------------- <<socket>> ------------- <<running>>
| Python Client | -----------> | Blender | o-----> Python
| Process | ------------- Server Process
------------------- ^ ^ ^ |
| <<Running>> | | | |
| | | | |
v | | + Timer Event <---+
Python Program1, Python Program2... | | Registration
| |
------------------- <<socket>> | |
| Python Client | ---------------------+ |
| Process | |
------------------- |
... |
|
------------------- <<socket>> |
| Python Client | -------------------------+
| Process |
-------------------

Python server code registers Timer event with event loop manager. In timer event callback, Python server checks for i/o through ‘select’ api call. If timeout happens or descriptor is set (data arrives), timer event callback would return after processing the request. Client and Server python code.

$cat blender_client2.py
#!/usr/bin/env python
#blender_client.py script1.py script2.py
#developed at Minh, Inc. https://youtube.com/@minhinc
import sys,time,select

PORT = 8081
HOST = "localhost"

def main():
import sys
import socket

if len(sys.argv)<2:
print(f'---usage---\npython3 blender_client.py blenderpythonscript.py blenderpythonscript2.py ..')
sys.exit(-1)

clientsocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
clientsocket.connect((HOST, PORT))
except Exception as exc:
print(f'E Error in connection with server, probably try after sometime/minutes... Exception type -> {type(exc)=}')
sys.exit(-1)
else:
print(f'I blender_client connection to server successful')

filestosend=' '.join(sys.argv[1:])
print(f'sending file(s) -> {filestosend} to the server')
clientsocket.sendall(filestosend.encode("utf-8") + b'\x00')
print(f'I blender_client message sent successfully, waiting for response..')
while True:
messagerecved=clientsocket.recv(4096)
if not messagerecved:
print(f'Empty message received, sleeping for 10 secs...')
time.sleep(10)
else:
print(f'Message received {messagerecved=}, exiting...')
clientsocket.close()
break
if __name__ == "__main__":
main()

$cat blender_server2.py
#blender --python blender_server.py
#developed at Minh, Inc. https://youtube.com/@minhinc
import socket,time,select,re,datetime
import bpy

PORT = 8081
HOST = "localhost"
PATH_MAX = 4096

def execfile(filepath):
import os
global_namespace = {
"__file__": filepath,
"__name__": "__main__",
}
with open(filepath, 'rb') as file:
exec(compile(file.read(), filepath, 'exec'), global_namespace)

def main():
global serversocket,read_list,file_list,connection,result_list
file_list=[]
result_list=[]
connection=None
serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
serversocket.bind((HOST, PORT))
serversocket.listen(5) #accept upto 6 connect and messages

print("Listening on %s:%s" % (HOST, PORT))
read_list=[serversocket]

def handle_data():
global file_list,connection,result_list
timeout=20
def send_data():
nonlocal timeout
print(f'I blender_server executing file {file_list[0]} full {file_list=} ')
try:
execfile(file_list[0])
print(f'executed successfully {file_list[0]=}')
result_list.append(f'{file_list[0]} - success')
except Exception as exc:
print(f'Error while executing {file_list[0]=} {exc=}')
result_list.append(f'{file_list[0]} - failed exception {exc}')
file_list[0:1]=[]
timeout=2
if file_list:
send_data()
else:
if connection:
connection.sendall('\n'.join(result_list).encode('utf-8') + b'\x00')
print("response ",'\n'.join(result_list)," sent to client")
connection.close()
connection=None
result_list=[]
readable,writable,errored=select.select(read_list,[],[],0.0)
print(f'E handle_data() {(readable,writable,errored)=} {read_list=} at time -> {datetime.datetime.now():%H:%M:%S}')
for s in readable:
if s in read_list:
connection, address = serversocket.accept()
print(f'I blender_server connection accepted {connection=} {address=}')
file_list = re.split(r'\s+',re.split(b'\x00',connection.recv(PATH_MAX))[0].decode())
print(f'I blender_server data received {file_list=}')
send_data()
print(f'handle_data, returning {timeout} second timeout..')
return timeout

if __name__ == "__main__":
main()
bpy.app.timers.register(handle_data)

Script can be executed in two terminals as
1. First terminal
blender — python blender_server.py
2. Second terminal
python3 blender_client.py <pythonprogram1> <pythonprogram2> <pythonprogram3>

Program description

Client program accepts multiple python program as command line argument. Program names are joined in a string and sent to the server. Server parses the request and process each python file one by one. Each time it return from the Timer Event Loop so that Event loop manager would get chance to process other evens.

Python program sent to Python server as concatenated string. Server process one by one. Making return to event loop after each fie processing.
Video on forking separate terminal for client and server. Firing python scripts from client to server.

Gif animation of cube addition and deletion.

Gif animation of cube addition and deletion through scripts fired by client application

Cube addition and Deletion code

$ cat cubeadd_y.py
import bpy
import bmesh
import mathutils


bm = bmesh.new()
bmesh.ops.create_cube(bm, size=4)

mesh = bpy.data.meshes.new('Basic_Cube')
bm.to_mesh(mesh)
mesh.update()
bm.free()

basic_cube = bpy.data.objects.new("Basic_Cube", mesh)
basic_cube.matrix_world.translation += basic_cube.matrix_world.to_3x3() @ mathutils.Vector((0.0,6.0,0.0))
bpy.context.collection.objects.link(basic_cube)

$ cat cubeadd_x.py
import bpy
import bmesh
import mathutils


bm = bmesh.new()
bmesh.ops.create_cube(bm, size=4)

mesh = bpy.data.meshes.new('Basic_Cube')
bm.to_mesh(mesh)
mesh.update()
bm.free()

basic_cube = bpy.data.objects.new("Basic_Cube", mesh)
basic_cube.matrix_world.translation += basic_cube.matrix_world.to_3x3() @ mathutils.Vector((-6.0,0.0,0.9))
bpy.context.collection.objects.link(basic_cube)

$ cat cubedelete.py
import bpy
import mathutils
try:
cube = bpy.data.objects['Cube']
bpy.data.objects.remove(cube, do_unlink=True)
except:
print("Object bpy.data.objects['Cube'] not found")

bpy.ops.outliner.orphans_purge()

How it works ? Two python programs client and server interacts with each other through ‘socket’ inter process communication. Socket can be used among inter computer communications also. IP address needs to be real IP address of the server. Blender starts in scripting mode with ‘ — python’ as command line argument. Blender starts python program in main thread. Python program rather than doing any task, it registers an ‘Timer Event’ to event loop through code,

Activity diagram for interaction flow

‘bpy.app.timers.register(handle_data)’ passing ‘handle_event’ as callback function. Event callback function ‘handle_data’ is called in main loop and it uses ‘select’ as i/o multiplexer to handle the i/o in non-blocking mode. Once connection arrives ‘read descriptor is set’, connection request is read and processed. In case of multiple files, Timer callback returns (to event main loop) with timeout of 0 seconds. Here 2 seconds is used to make explanation more visual. Returning to event loop between each python script file processing, gives event loop manager chance to execute other GUI event, making GUI appear interactive.

Further enhancements Client python scripts can be edited through IDE editor. Where editors will have GUI button option and context menu options to execute the script.

Proposed IDE editor with separate Tool Bar Button and context menu to execute the script

Data/string communications with serer would be displayed in ‘docket’ window.

Further Studies

  1. Advanced Programming in the UNIX Environment, W. Richard Stevens. Addison-Wesley Professional Computing Series
  2. TCP/IP Illustrated, W. Richard Stevens : The Protocols Volume 1, The implementation Volume 2, TCP for Transactions, HTTP, NNTP and the UNIX Domain Protocols Volume 3

--

--

Pravin Kumar Sinha

Minh Inc provide assistance in writing technology articles and publishing technology videos. Catch up at https://YouTube.com/@minhinc