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
Comments
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 |
Thanks for your time and answer, Hereunder is what a classic threaded execution looks like, relative to the is_alive() result, in IDLE :
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)
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 :
Here it stops the main thread, waiting for
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
so, the TL;DR here : the .run() thread is still alive after |
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 The event loop is driven by calls to I verified this by adding logging to the message loop: the call to 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. |
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" flagFalse
, 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 thestop()
does (pretty nice to think of lazy/forgetful coders out there) BUT the_running
beingTrue
, I couldn't catch (always appearedFalse
, 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 isFalse
from the creation of the thread, then goesTrue
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 goFalse
and the thread should be dead (but here,.is_alive()
is stillTrue
)(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 thesys.exit()
) the daemon threads are killed without a care... which may be a problem for thesetup
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" ...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, theif not r
uses the falsy status of the 0 to detect a WM_QUIT message (value 0), then theelif 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 threada bit of logging
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 theunregister_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 :
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.
The text was updated successfully, but these errors were encountered: