Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

pystray Icon in a thread will not end the thread with Icon.stop() #94

Closed
TJ-59 opened this issue Jun 12, 2021 · 3 comments
Closed

pystray Icon in a thread will not end the thread with Icon.stop() #94

TJ-59 opened this issue Jun 12, 2021 · 3 comments

Comments

@TJ-59
Copy link

TJ-59 commented Jun 12, 2021

So, I'm trying to use pystray to add a systray icon to some code using tkinter as GUI, and both of them have their own "mainloop", meaning they cannot run concurrently on the same thread.
Now, tkinter has this little quirk which forces the main thread to be the one giving it its commands (else you got the "RuntimeError: Calling Tcl from different appartment" error message), which means the pystray must be the one threaded.
After a bit of tinkering, currently on windows, and reading of the old issues (#16) and sources ( https://github.com/moses-palmer/pystray/blob/ccc699d32298ac7198664bd9a7afde7bbbccb577/lib/pystray/_win32.py )to get a better feeling of it,
what I'm left with is :
-you have to be using a class inheriting from threading.Thread, NOT just using it's run() member in a "Thread(target=my_pystray_icon.run)"
-keeping a reference to the various parts in a global object really does help
-using the icon's .stop() method turn its "_running" flag False, and calls the platform-dependent ._stop(), also the "del"of the icon makes sure to stop the running and to try the thread joining just as the stop() does (pretty nice to think of lazy/forgetful coders out there) BUT the _running being True, I couldn't catch (always appeared False, see the code at bottom)

The problem now is that when you stop() the icon, it does not disappear from the systray, and is not yet a "ghost icon" which disappear once you pass the mouse pointer over it, yet its message loop is down and clicks aren't processed, and menu doesn't pop up anymore.
This means the thread hasn't finished, which is easily verifiable by its .is_alive() method, which is False from the creation of the thread, then goes True while the thread is started (.start() -> which in turn use the .run(), which then use the Icon's .run() method ) until the .run is over, that is, no more code has to be executed by that thread, at which point the .is_alive() SHOULD go False and the thread should be dead (but here, .is_alive() is still True )
(TL;DR : thread isn't dead because something in the icon's .run() is still rolling)

Now, that thread class can be set as a daemon, by adding a self.setDaemon(True) in the __init__(), which means once the main thread is done (usually when you get to the sys.exit()) the daemon threads are killed without a care... which may be a problem for the setup part of the icon's .run() (if someone is using it with open files or whatever, it's quite a brutal ending)
So, with it as a daemon thread, the program exits correctly, but I bet this is not the proper way to do it.

Since the Icon is still "doing something", I looked a bit in the source (for windows) to see what could be the thing "still running" ...

    def _mainloop(self):
        """The body of the main loop thread.
        This method retrieves all events from *Windows* and makes sure to
        dispatch clicks.
        """
        # Pump messages
        try:
            msg = wintypes.MSG()
            lpmsg = ctypes.byref(msg)
            while True:
                r = win32.GetMessage(lpmsg, None, 0, 0)
                if not r:
                    break
                elif r == -1:
                    break
                else:
                    win32.TranslateMessage(lpmsg)
                    win32.DispatchMessage(lpmsg)

Up to there, my understanding is that the message loop is only active when "explorer" (windows, NT, whatever is the actual part managing this) has actually something to give them, which means the loop ALWAYS has something to "Get" from it's mailbox, since in-between, the system forces it to be inactive.
r being the value of that message, the if not r uses the falsy status of the 0 to detect a WM_QUIT message (value 0), then the elif r == -1 checks for an error, and both break the loop if it happens.
then the else: check for keyboard related values, and the dispatch allows it to reach all the handles belonging to the thread

        except:
            self._log.error(
                'An error occurred in the main loop', exc_info=True)

a bit of logging

        finally:
            try:
                self._hide()
                del self._HWND_TO_ICON[self._hwnd]
            except:
                # Ignore
                pass

            win32.DestroyWindow(self._hwnd)
            win32.DestroyWindow(self._menu_hwnd)
            if self._menu_handle:
                hmenu, callbacks = self._menu_handle
                win32.DestroyMenu(hmenu)
            self._unregister_class(self._atom)

that part is executed when the mainloop breaks,
It tries to hide the systray icon, then delete the handle of the systray icon which is currently stored in a dict, using the current Icon top-level window handle as an index, which result in the "self" of the Icon class... a bit weird, probably just some cleaning (?) and ignoring errors, destroying the top-level and then destroying the menu window (Shouldn't the menu be killed 1st, then the menu window, then the parent ? I have no idea if this can be making "orphans" and/or block the execution of the code that follows it)
Then if self._menu_handle, which split a tuple before using the menu handle to destroy said menu, then the unregister_class .

Could any of those be causing a hang which result in the Thread being still "alive" ?

Here is a bit of code to test this behaviour :

import sys
import pystray
import threading
import time
import tkinter as tk
from PIL import Image, ImageDraw
obj = dict()


def create_image(width,height,color1,color2):
	image = Image.new('RGB',(width, height),color1)
	dc = ImageDraw.Draw(image)
	dc.rectangle((width//2,0,width,height//2),fill=color2)
	dc.rectangle((0,height//2,width//2,height),fill=color2)
	return image

def switchroot():
	if obj["root"].winfo_ismapped() :
		obj["root"].withdraw()
	else :
		obj["root"].wm_deiconify()

		
def switchicon():
	global obj
	obj["threadsti"].sti.visible = not obj["threadsti"].sti.visible

def clean_close():
	print("simulating doing all sorts of cleanups before quitting...")
	time.sleep(3)
	print("clean_close : about to sti.stop()...")
	obj["threadsti"].sti.stop()
	#print("clean_close : sti is running ? : " + str(obj["threadsti"].sti._running))
	print("clean_close : threadsti is alive ? : " + str(obj["threadsti"].is_alive()))
	print("clean_close : about to root.destroy()...")
	obj["root"].destroy()       #would .quit() be preferable?

def make_root():
	global obj
	root = tk.Tk()
	obj["root"] = root
	#root.iconbitmap(bitmap="./icon.ico")
	root.title("testing tk and pystray")
	root.geometry("400x300")
	root.resizable(0,0)
	btn1 = tk.Button(root,text="Hide/show the systray icon",command=switchicon)
	btn1.grid(column=10,row=10)
	root.protocol("WM_DELETE_WINDOW",clean_close)

def make_stithread():
	global obj
	th = ThreadSTI()
	obj["threadsti"] = th


class ThreadSTI(threading.Thread):
	def __init__(self) :
		super().__init__()
		print("threadsti.__init__ : creating sti, the pystray.Icon...")
		self.sti = pystray.Icon("tk and pystray icon",
			icon=create_image(16,16, "yellow","orange"),
			title="hover icon tooltip",
			menu=pystray.Menu(
				pystray.MenuItem("show/hide",switchroot,default=True),
				pystray.Menu.SEPARATOR,
				pystray.MenuItem("stop",clean_close)
				)
			)
		#self.setDaemon(True)    #uncomment this for Daemon thread behaviour
		print("threadsti.__init__ : threadsti is daemon ? : " + str(self.daemon))
		print("threadsti.__init__ : threadsti is alive ? : " + str(self.is_alive()))
		#print("threadsti.__init__ : sti is running ? : " + str(self.sti._running))
		global obj
		obj["threadsti"] = self

	def run(self) :
		print("threadsti : threadsti.run()...\n")
		self.sti.run()
		#self.sti.run(fakesetup)      #only use one of those 
		# the fakesetup was to try to get the sti._running flag to change 
		# for the various "sti is running ?" prints (currently commented out)

def fakesetup(iconself):
	global obj
	obj["threadsti"].sti.visible = True
	print("fakesetup : sti just made visible")
	
def main():
	print("main : creating tk root...")
	make_root()
	print("main : creating sti thread...")
	make_stithread()
	print("main : starting threadsti...")
	obj["threadsti"].start()   #this in turn calls sti.run()
	print("main : threadsti is alive ? : " + str(obj["threadsti"].is_alive()))
	#print("main : sti is running ? : " + str(obj["threadsti"].sti._running))
	print("main : about to start the tk mainloop...")
	obj["root"].mainloop()
	print("main : this is after tk mainloop")


if __name__ == '__main__':
	print("module being run as main")
	status = main()
	print("about to get out, sys.exit()-ing soon... \
		\nIs the icon still there and frozen while the program is not finished ?")
	sys.exit(status)
	print("some random text just in case")

Notice the commented out setDaemon on line 69, which indeed force the thread to die, and allows the icon to become a "ghost", disappearing as soon as you interact with it.

So, what could be the cause, am I doing it wrong, or what could be a potential "fix" (like "clean enough", since Threads don't have a terminate() like multiprocess) ?
As a side note, I haven't been able to test it on other OSes, so I don't know if it is windows-related only; and yes, this bit of code should not work on a mac, since the pystray.Icon is not on the main thread, but then again, it seems some macs don't offer anything but a default action when clicking the icon anyway.

@moses-palmer
Copy link
Owner

Thank you very much for your detailed report, and apologies for my late reply.

Allowing pystray to interact with other main-loop hijacking libraries in a platform-independent way is unfortunately rather difficult, and I am quite sure it would require a modified API. For your case---running on Windows---it is a lot easier though, since the requirement to run pystray from the main thread comes from macOS alone. Simply spawn a new thread for pystray.Icon.run and make sure to join it before exiting, and then have Tkinter run in your main thread.

@TJ-59
Copy link
Author

TJ-59 commented Jun 28, 2021

Thanks for your time and answer,
I'll probably do differently on Macs, meaning "no systray icon since it doesn't help much anyway", with an os detection at startup, and bypass it completely if I compile the thing for Macs. But the problem remains for windows (and linux ?).
The problem being that .join() doesn't happen, be it in a threading.Thread inheriting class, or just the .run() going through the threading.Thread(target=sti.run) one-liner (sti being the name of the Icon object -as in Sys Tray Icon- in my example below).
As I mentioned, a thread that has finished its job should become "dead", as in its .is_alive() function should return False, ONLY THEN can the .join() succeed. Which is not happening with pystray for some reason.

Hereunder is what a classic threaded execution looks like, relative to the is_alive() result, in IDLE :

>>> import threading
>>> import time
>>> def task_one():
	i = 0
	while i < 5 :
		print("Doing task one, i = " + str(i))
		i += 1
		time.sleep(2)

		
>>> def task_two():
	j = 0
	while j < 5 :
		print("Doing task two, j = " + str(j))
		j += 1
		time.sleep(2)

		
>>> o = threading.Thread(target=task_one)
>>> t = threading.Thread(target=task_two)
>>> threading.current_thread()

<_MainThread(MainThread, started 1560)>

>>> threading.enumerate()

[<_MainThread(MainThread, started 1560)>, <Thread(SockThread, started daemon 11312)>]

>>> threading.active_count()

2

>>> def letsgo():
	print(str(threading.current_thread()))
	print("Letsgo start, Active thread(s) : " + str(threading.active_count()) + " -- : " + str(threading.enumerate()))
	k = 0
	while k < 18:
		if k == 2:
			o.start()
			t.start()
		print("k = " + str(k))
		print(" o is alive : " + str(o.is_alive()))
		print(" t is alive : " + str(t.is_alive()))
		print("Active thread(s) : " + str(threading.active_count()) + " -- : " + str(threading.enumerate()))
		k += 1
		time.sleep(1)

		
>>> letsgo()

<_MainThread(MainThread, started 1560)>
Letsgo start, Active thread(s) : 2 -- : [<_MainThread(MainThread, started 1560)>, <Thread(SockThread, started daemon 11312)>]
k = 0
o is alive : False
t is alive : False
Active thread(s) : 2 -- : [<_MainThread(MainThread, started 1560)>, <Thread(SockThread, started daemon 11312)>]
k = 1
o is alive : False
t is alive : False
Active thread(s) : 2 -- : [<_MainThread(MainThread, started 1560)>, <Thread(SockThread, started daemon 11312)>]
Doing task one, i = 0k = 2Doing task two, j = 0

In the above line, the "synchronicity" made it print a single line where it should be 3, the "k = 2" being in the middle; this happens sometimes (at k=5 too, the task one/i and task two/j prints are sort of concatenated)

o is alive : True
t is alive : True
Active thread(s) : 4 -- : [<_MainThread(MainThread, started 1560)>, <Thread(SockThread, started daemon 11312)>, <Thread(Thread-1, started 12224)>, <Thread(Thread-2, started 10080)>]
k = 3
o is alive : True
t is alive : True
Active thread(s) : 4 -- : [<_MainThread(MainThread, started 1560)>, <Thread(SockThread, started daemon 11312)>, <Thread(Thread-1, started 12224)>, <Thread(Thread-2, started 10080)>]
Doing task one, i = 1
Doing task two, j = 1
k = 4
o is alive : True
t is alive : True
Active thread(s) : 4 -- : [<_MainThread(MainThread, started 1560)>, <Thread(SockThread, started daemon 11312)>, <Thread(Thread-1, started 12224)>, <Thread(Thread-2, started 10080)>]
k = 5
o is alive : True
t is alive : True
Active thread(s) : 4 -- : [<_MainThread(MainThread, started 1560)>, <Thread(SockThread, started daemon 11312)>, <Thread(Thread-1, started 12224)>, <Thread(Thread-2, started 10080)>]
Doing task one, i = 2Doing task two, j = 2
k = 6
o is alive : True
t is alive : True
Active thread(s) : 4 -- : [<_MainThread(MainThread, started 1560)>, <Thread(SockThread, started daemon 11312)>, <Thread(Thread-1, started 12224)>, <Thread(Thread-2, started 10080)>]
k = 7
o is alive : True
t is alive : True
Active thread(s) : 4 -- : [<_MainThread(MainThread, started 1560)>, <Thread(SockThread, started daemon 11312)>, <Thread(Thread-1, started 12224)>, <Thread(Thread-2, started 10080)>]
Doing task one, i = 3
Doing task two, j = 3
k = 8
o is alive : True
t is alive : True
Active thread(s) : 4 -- : [<_MainThread(MainThread, started 1560)>, <Thread(SockThread, started daemon 11312)>, <Thread(Thread-1, started 12224)>, <Thread(Thread-2, started 10080)>]
k = 9
o is alive : True
t is alive : True
Active thread(s) : 4 -- : [<_MainThread(MainThread, started 1560)>, <Thread(SockThread, started daemon 11312)>, <Thread(Thread-1, started 12224)>, <Thread(Thread-2, started 10080)>]
Doing task one, i = 4
Doing task two, j = 4
k = 10
o is alive : True
t is alive : True
Active thread(s) : 4 -- : [<_MainThread(MainThread, started 1560)>, <Thread(SockThread, started daemon 11312)>, <Thread(Thread-1, started 12224)>, <Thread(Thread-2, started 10080)>]
k = 11
o is alive : True
t is alive : True
Active thread(s) : 4 -- : [<_MainThread(MainThread, started 1560)>, <Thread(SockThread, started daemon 11312)>, <Thread(Thread-1, started 12224)>, <Thread(Thread-2, started 10080)>]
k = 12
o is alive : False
t is alive : False
Active thread(s) : 2 -- : [<_MainThread(MainThread, started 1560)>, <Thread(SockThread, started daemon 11312)>]
k = 13
o is alive : False
t is alive : False
Active thread(s) : 2 -- : [<_MainThread(MainThread, started 1560)>, <Thread(SockThread, started daemon 11312)>]
k = 14
o is alive : False
t is alive : False
Active thread(s) : 2 -- : [<_MainThread(MainThread, started 1560)>, <Thread(SockThread, started daemon 11312)>]
k = 15
o is alive : False
t is alive : False
Active thread(s) : 2 -- : [<_MainThread(MainThread, started 1560)>, <Thread(SockThread, started daemon 11312)>]
k = 16
o is alive : False
t is alive : False
Active thread(s) : 2 -- : [<_MainThread(MainThread, started 1560)>, <Thread(SockThread, started daemon 11312)>]
k = 17
o is alive : False
t is alive : False
Active thread(s) : 2 -- : [<_MainThread(MainThread, started 1560)>, <Thread(SockThread, started daemon 11312)>]

With a few modifications, we can highlight the fact that the join is just a waiting point for the "spawner thread" (here the mainthread) at which point is stops its own execution until the sub-thread terminates itself :

>>> del o,t
>>> o = threading.Thread(target=task_one)
>>> t = threading.Thread(target=task_two)
>>> def letsgo():
	print(str(threading.current_thread()))
	print("Letsgo start, Active thread(s) : " + str(threading.active_count()) + " -- : " + str(threading.enumerate()))
	k = 0
	while k < 18:
		if k == 2:
			o.start()
			t.start()
		if k == 5:
			o.join()
		print("k = " + str(k))
		print(" o is alive : " + str(o.is_alive()))
		print(" t is alive : " + str(t.is_alive()))
		print("Active thread(s) : " + str(threading.active_count()) + " -- : " + str(threading.enumerate()))
		k += 1
		time.sleep(1)

		
>>> letsgo()

<_MainThread(MainThread, started 1560)>
Letsgo start, Active thread(s) : 2 -- : [<_MainThread(MainThread, started 1560)>, <Thread(SockThread, started daemon 11312)>]
k = 0
o is alive : False
t is alive : False
Active thread(s) : 2 -- : [<_MainThread(MainThread, started 1560)>, <Thread(SockThread, started daemon 11312)>]
k = 1
o is alive : False
t is alive : False
Active thread(s) : 2 -- : [<_MainThread(MainThread, started 1560)>, <Thread(SockThread, started daemon 11312)>]
Doing task one, i = 0Doing task two, j = 0k = 2
o is alive : True
t is alive : True
Active thread(s) : 4 -- : [<_MainThread(MainThread, started 1560)>, <Thread(SockThread, started daemon 11312)>, <Thread(Thread-5, started 10120)>, <Thread(Thread-6, started 11380)>]
k = 3
o is alive : True
t is alive : True
Active thread(s) : 4 -- : [<_MainThread(MainThread, started 1560)>, <Thread(SockThread, started daemon 11312)>, <Thread(Thread-5, started 10120)>, <Thread(Thread-6, started 11380)>]
Doing task one, i = 1Doing task two, j = 1
k = 4
o is alive : True
t is alive : True
Active thread(s) : 4 -- : [<_MainThread(MainThread, started 1560)>, <Thread(SockThread, started daemon 11312)>, <Thread(Thread-5, started 10120)>, <Thread(Thread-6, started 11380)>]
Doing task one, i = 2
Doing task two, j = 2

Here it stops the main thread, waiting for o to join, but o and t are still running, and when o finally joins, the main continues from "k= 5", after a few seconds of pause, but the tasks have already finished, hence the o is alive : False and t is alive : False results.

Doing task one, i = 3Doing task two, j = 3
Doing task one, i = 4
Doing task two, j = 4
k = 5
o is alive : False
t is alive : False
Active thread(s) : 2 -- : [<_MainThread(MainThread, started 1560)>, <Thread(SockThread, started daemon 11312)>]
k = 6
o is alive : False
t is alive : False
Active thread(s) : 2 -- : [<_MainThread(MainThread, started 1560)>, <Thread(SockThread, started daemon 11312)>]
k = 7
o is alive : False
t is alive : False
Active thread(s) : 2 -- : [<_MainThread(MainThread, started 1560)>, <Thread(SockThread, started daemon 11312)>]
k = 8
o is alive : False
t is alive : False
Active thread(s) : 2 -- : [<_MainThread(MainThread, started 1560)>, <Thread(SockThread, started daemon 11312)>]
k = 9
o is alive : False
t is alive : False
Active thread(s) : 2 -- : [<_MainThread(MainThread, started 1560)>, <Thread(SockThread, started daemon 11312)>]
k = 10
o is alive : False
t is alive : False
Active thread(s) : 2 -- : [<_MainThread(MainThread, started 1560)>, <Thread(SockThread, started daemon 11312)>]
k = 11
o is alive : False
t is alive : False
Active thread(s) : 2 -- : [<_MainThread(MainThread, started 1560)>, <Thread(SockThread, started daemon 11312)>]
k = 12
o is alive : False
t is alive : False
Active thread(s) : 2 -- : [<_MainThread(MainThread, started 1560)>, <Thread(SockThread, started daemon 11312)>]
k = 13
o is alive : False
t is alive : False
Active thread(s) : 2 -- : [<_MainThread(MainThread, started 1560)>, <Thread(SockThread, started daemon 11312)>]
k = 14
o is alive : False
t is alive : False
Active thread(s) : 2 -- : [<_MainThread(MainThread, started 1560)>, <Thread(SockThread, started daemon 11312)>]
k = 15
o is alive : False
t is alive : False
Active thread(s) : 2 -- : [<_MainThread(MainThread, started 1560)>, <Thread(SockThread, started daemon 11312)>]
k = 16
o is alive : False
t is alive : False
Active thread(s) : 2 -- : [<_MainThread(MainThread, started 1560)>, <Thread(SockThread, started daemon 11312)>]
k = 17
o is alive : False
t is alive : False
Active thread(s) : 2 -- : [<_MainThread(MainThread, started 1560)>, <Thread(SockThread, started daemon 11312)>]

Which means that the basic assumption here would be : "something" is still running in the pystray.Icon 's "run" thread.

Here is a V2 of the previous tinkering, this one is not using a class inheriting from threading.Thread, but the "one-liner" :

import sys
import pystray
import threading
import time
import tkinter as tk
from PIL import Image, ImageDraw
obj = dict()


def create_image(width,height,color1,color2):
	image = Image.new('RGB',(width, height),color1)
	dc = ImageDraw.Draw(image)
	dc.rectangle((width//2,0,width,height//2),fill=color2)
	dc.rectangle((0,height//2,width//2,height),fill=color2)
	return image

def switchroot():
	if obj["root"].winfo_ismapped() :
		obj["root"].withdraw()
	else :
		obj["root"].wm_deiconify()

		
def switchicon():
	global obj
	#obj["threadsti"].sti.visible = not obj["threadsti"].sti.visible
	obj["sti"].visible = not obj["sti"].visible

def clean_close():
	print("simulating doing all sorts of cleanups before quitting...")
	time.sleep(3)
	print("clean_close : about to sti.stop()...")
	#obj["threadsti"].sti.stop()
	obj["sti"].stop()
	print("clean_close : threadsti is alive ? : " + str(obj["threadsti"].is_alive()))
	print("clean_close : about to root.destroy()...")
	obj["root"].destroy()       #would .quit() be preferable?

def make_root():
	global obj
	root = tk.Tk()
	obj["root"] = root
	#root.iconbitmap(bitmap="./icon.ico")
	root.title("testing tk and pystray")
	root.geometry("400x300")
	root.resizable(0,0)
	btn1 = tk.Button(root,text="Hide/show the systray icon",command=switchicon)
	btn1.grid(column=10,row=10)
	root.protocol("WM_DELETE_WINDOW",clean_close)

def make_stithread():
	global obj
	sti = pystray.Icon("tk and pystray icon",
		icon=create_image(16,16, "purple","orange"),
		title="hover icon tooltip",
		menu=pystray.Menu(
			pystray.MenuItem("show/hide",switchroot,default=True),
			pystray.Menu.SEPARATOR,
			pystray.MenuItem("stop",clean_close)
			)
		)
	obj["sti"] = sti

	# th = ThreadSTI()
	th = threading.Thread(target=sti.run)
	obj["threadsti"] = th
	

# class ThreadSTI(threading.Thread):
# 	def __init__(self) :
# 		super().__init__()
# 		print("threadsti.__init__ : creating sti, the pystray.Icon...")
# 		self.sti = pystray.Icon("tk and pystray icon",
# 			icon=create_image(16,16, "yellow","orange"),
# 			title="hover icon tooltip",
# 			menu=pystray.Menu(
# 				pystray.MenuItem("show/hide",switchroot,default=True),
# 				pystray.Menu.SEPARATOR,
# 				pystray.MenuItem("stop",clean_close)
# 				)
# 			)
# 		#self.setDaemon(True)    #uncomment this for Daemon thread behaviour
# 		print("threadsti.__init__ : threadsti is daemon ? : " + str(self.daemon))
# 		print("threadsti.__init__ : threadsti is alive ? : " + str(self.is_alive()))
# 		#print("threadsti.__init__ : sti is running ? : " + str(self.sti._running))
# 		global obj
# 		obj["threadsti"] = self

# 	def run(self) :
# 		print("threadsti : threadsti.run()...\n")
# 		self.sti.run()
# 		#self.sti.run(fakesetup)      #only use one of those 
# 		# the fakesetup was to try to get the sti._running flag to change 
# 		# for the various "sti is running ?" prints (currently commented out)

def fakesetup(iconself):
	global obj
	obj["threadsti"].sti.visible = True
	print("fakesetup : sti just made visible")
	
def main():
	print("main : creating tk root...")
	make_root()
	print("main : creating sti thread...")
	make_stithread()
	print("main : starting threadsti...")
	obj["threadsti"].start()   #this in turn calls sti.run()
	print("main : threadsti is alive ? : " + str(obj["threadsti"].is_alive()))
	#print("main : sti is running ? : " + str(obj["threadsti"].sti._running))
	print("main : about to start the tk mainloop...")
	obj["root"].mainloop()
	print("main : this is after tk mainloop")
	t = 0
	while t < 10:
		print("main : trying to join threadsti...")
		obj["threadsti"].join(2)
		t += 1

if __name__ == '__main__':
	print("module being run as main")
	status = main()
	obj["threadsti"].join(2)  #Even this one does not join...
	print("about to get out, sys.exit()-ing soon... \
		\nIs the icon still there and frozen while the program is not finished ?")
	sys.exit(status)
	print("some random text that should never print")

module being run as main
main : creating tk root...
main : creating sti thread...
main : starting threadsti...
main : threadsti is alive ? : True
main : about to start the tk mainloop...
simulating doing all sorts of cleanups before quitting...
clean_close : about to sti.stop()...
clean_close : threadsti is alive ? : True
clean_close : about to root.destroy()...
main : this is after tk mainloop
main : trying to join threadsti...
main : trying to join threadsti...
main : trying to join threadsti...
main : trying to join threadsti...
main : trying to join threadsti...
main : trying to join threadsti...
main : trying to join threadsti...
main : trying to join threadsti...
main : trying to join threadsti...
main : trying to join threadsti...
about to get out, sys.exit()-ing soon...
Is the icon still there and frozen while the program is not finished ?
[Cancelled]

so, the TL;DR here : the .run() thread is still alive after .stop(), it should not, and I have no idea why.

@moses-palmer
Copy link
Owner

I apologise for this very late reply, with the hope that you managed to solve your issue by yourself.

I have looked at this from a few different angles. First, a minimal example with an icon running from a thread:

import threading

from PIL import Image, ImageDraw
from pystray import Icon, Menu, MenuItem


def create_image(color1, color2, width=64, height=64):
    image = Image.new('RGB', (width, height), color1)
    dc = ImageDraw.Draw(image)

    dc.rectangle((width // 2, 0, width, height // 2), fill=color2)
    dc.rectangle((0, height // 2, width // 2, height), fill=color2)

    return image


thread = threading.Thread(daemon=True, target=lambda: Icon(
    'test',
    create_image('black', 'white'),
    menu=Menu(
        MenuItem(
            'Exit',
            lambda icon, item: icon.stop()))).run())
thread.start()
thread.join()

I cannot reproduce your issue with this simple example.

Next, I ran your example using Tkinter, and observed that it indeed exhibited the behaviour you described. There is one difference however: you create the icon instance in one thread, and then call run from another.

The event loop is driven by calls to GetMessage as you noted in your initial report. Its second argument is the HWND for which to retrieve messages, but pystray passes NULL (well, None), which retrieves all messages for windows created by the current thread.

I verified this by adding logging to the message loop: the call to win32.GetMessage would block indefinitely.

So, to make this short, your issue should go away if you ensure to create the icon in the same thread that you run it from. I will close this issue now, but if my findings were inaccurate, please reopen.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants