I'm working on an add-on that has multithreading. I've created a stripped down version that has very little functionality in order to demonstrate the issue: http://simplecarnival.com/forum/simple_carnival_thread_test.py
I'm running this on Blender 2.78 on Windows. This add-on will write a test file at c:\junk.txt that demonstrates the various functions that the add-on reaches.
First, I'll describe the problem. Install the add-on. Go to the 3D View. Open the T panel. Open the Misc tab. You'll see a control there that says Thread Test, and a button that says "Enable". Click Enable. The add-on will start running a thread. If you click the same button (which now says "Disable"), the add-on will stop running the thread. At this point, if you quit out of Blender, everything works as it should. No background thread is being run.
On the other hand, if you click the button when it says "Enable" (the text above the button will then say "* * * ENABLED * * *") and then quit out of Blender, Blender will still be running in the background. You will need to go into Task Manager and manually kill the Blender process.
If you open the c:\junk.txt file that this add-on creates, you'll see the various points of the add-on that have written to this file.
What should happen -- because I'm importing atexit and setting up atexit.register(cleanup) -- is that, when Blender quits, it should call that cleanup() function. At least, that's how I understand atexit is supposed to work (though maybe it doesn't work that way with Blender). If you look at the cleanup() function, it should theoretically write some text to the c:\junk.txt file. However, that text is never written, as cleanup() apparently is never called.
Any idea what is wrong with this code? Am I taking the wrong approach by using atexit and there's another way I can run some code from an add-on before Blender exits?
Here's the code:
bl_info = {
'name': "Simple Carnival Thread Test",
'author': "simplecarnival",
'version': (0, 0, 3),
'blender': (2, 7, 8),
'api': 44136,
'location': "View3D > Left panel",
'description': "Test for trying to get threads to clean up when shutting down Blender without disabling the add-on",
'warning': "",
'wiki_url': "",
'tracker_url': "",
'category': "Object"}
import sys
import time
import bpy
import threading
import socket
from bpy.props import *
from time import sleep
import re
import numpy
import math
import atexit
threadTest_running = False
thread_created = False
threadSocket = 0
socketServer = 0
receivedSocket = "none"
listening = False
socketMessages = []
shutDown = False
receivedData = ''
def cleanup():
with open("c:\junk.txt", "a") as text_file:
text_file.write("Cleanup was automatically called when shutting down...but this never gets called!\n")
global threadTest_running, threadSocket, listening, socketServer
threadTest_running = False
listening = False
socketServer.settimeout(0.1)
threadSocket.join()
threadSocket.daemon = True
socketServer.close()
del socketServer
thread_created = False
shutDown = False
atexit.register(cleanup)
def create_thread():
with open("c:\junk.txt", "a") as text_file:
text_file.write("thread is about to be created\n")
global threadSocket,listening
threadSocket = threading.Thread(name='threadSocket', target= socket_listen)
listening = True
create_socket_connection()
threadSocket.start()
def socket_listen():
global receivedSocket,listening, receivedData,socketServer, socketMessages
socketServer.listen(5)
while listening:
(receivedSocket , adreess) = socketServer.accept()
receivedData = (receivedSocket.recv(1024)).decode("utf-8")[:-2]
socketMessages.append(receivedData)
sleep(0.03)
receivedSocket.close()
def create_socket_connection():
global socketServer
socketServer = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
socketServer.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
socketServer.bind(('127.0.0.1',4000))
class open_threadtest(bpy.types.Operator):
bl_idname = "threadtest_button.modal"
bl_label = "Enable"
_timer = None
def modal(self, context, event):
global threadTest_running, thread_created, listening, socketServer, socketMessages, shutDown
result = {'PASS_THROUGH'}
if context.area.type == 'VIEW_3D' and threadTest_running and event.type == 'TIMER' :
if shutDown:
with open("c:\junk.txt", "a") as text_file:
text_file.write("We manually disabled the add-on\n")
threadTest_running = False
listening = False
socketServer.settimeout(0.01)
#socketServer.shutdown(socket.SHUT_RDWR)
socketServer.close()
threadSocket.join()
del socketServer
context.window_manager.event_timer_remove(self._timer)
thread_created = False
shutDown = False
result = {'CANCELLED'}
self.report({'WARNING'}, "Thread Test has been disabled")
return result
def invoke(self, context, event):
global threadTest_running,thread_created
if context.area.type == 'VIEW_3D' and threadTest_running == False :
self.cursor_on_handle = 'None'
self._timer = context.window_manager.event_timer_add(0.01,context.window)
threadTest_running = True
context.window_manager.modal_handler_add(self)
create_thread()
thread_created = True
return {'RUNNING_MODAL'}
else:
global shutDown
shutDown = True
return {'FINISHED'}
class AddBox(bpy.types.Operator):
bl_idname = "threadtest.close"
bl_label = "Disable"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
global shutDown
shutDown = True
return {'FINISHED'}
class threadtest_panel(bpy.types.Panel):
bl_label = "Thread Test"
bl_space_type = "VIEW_3D"
bl_region_type = "TOOLS"
def draw(self, context):
global receivedSocket,listening
sce = context.scene
layout = self.layout
box = layout.box()
if listening:
box.label(text="*** ENABLED ***")
box.operator("threadtest.close")
else:
box.label(text="Disabled")
box.operator("threadtest_button.modal")
def register():
bpy.utils.register_module(__name__)
def unregister():
bpy.utils.unregister_module(__name__)
if __name__ == "__main__":
register()