Closed TJ-59 closed 3 years ago
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.
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)>]
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.
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.
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 (https://github.com/moses-palmer/pystray/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 theelse:
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.