Search code examples
pythonpython-3.xpython-multithreadinggtk3

Gtk.StatusIcon freezing in multi-threaded app


I have a problem with an application on CentOs / RH to show download statuses from the API. Everything works fine using the PyCharm IDE, but after compiling with PyInstaller (one folder) the application is very unstable and I can't find an error. I run the thread and download API statuses every 10 seconds and if there is a change, I update the icon and send a notification. After left-clicking on the icon, the statuses are displayed in Gtk.ApplicationWindow.

  • Sometimes Gtk.StatusIcon can not change status or be inactive - left / right clicking doesn't work (but notifications of status changes come)

  • the application may end unexpectedly

I suspect the problem is in threads, but I can't find a proper solution.

Code (main.py)

class ExampleSystemTrayInit(Gtk.StatusIcon):

def __init__(self):
    super().__init__()
    app.tray = Gtk.StatusIcon()
    app.tray.set_from_file(ico_start)
    app.tray.connect('popup-menu', self.on_right_click)
    app.tray.connect('activate', self.on_left_click, app)

def on_right_click(self, icon, event_button, event_time):
    self.menu = Gtk.Menu() 
    quit = Gtk.MenuItem("Quit")
    quit.connect('activate', self.quitApp)
    self.menu.append(quit)
    self.menu.show_all()
    self.menu.popup(None, None, Gtk.StatusIcon.position_menu, app.tray, event_button, event_time)

def on_left_click(self, icon, app):
    try:
        self.app = app
        if app.all_status:
            data = self.app.all_status
            Controller.show_window(self, data, self.app, refresh=False)
            #call to class MainWindow(Gtk.ApplicationWindow) and show some data
        else:
            pass;
    except Exception as e:
        print(e)
        app.tray.set_from_file(ico_disconnect)

def quitApp(self, par):
    app.quit()

class ExampleSystemTray(Gtk.Application):

def __init__(self, *args, **kwargs):
    super().__init__(
        application_id="example-system-tray2.app"
    )
    self.tray = None
    self.mainWindow = None


def do_activate(self):
    if not hasattr(self, "my_app"):
        self.hold()
        self.my_app_settings = "Primary application instance."
        self.systray = ExampleSystemTrayInit()
        TrayController(app)

    else:
        print("Already running!")

def do_startup(self):
    Gdk.threads_init()
    Gdk.threads_enter()
    Gtk.Application.do_startup(self)
    Gdk.threads_leave()


if __name__ == "__main__":
   GObject.threads_init()
   app = ExampleSystemTray()
   app.run()

TrayController (traycontroller.py)

class TrayController(threading.Thread):

def __init__(self, app):
    self.app = app
    self.cache = None
    threading.Thread.__init__(self, name='TrayController', )
    self.interval = 10
    self.finished = threading.Event()
    self.daemon = True
    self.start()

def run(self):
    while True:
        try:
            self.finished.wait(self.interval)
            if not self.finished.is_set():
                self.connect(self.app)
        except Exception as e:           
            print(e)

def cancel(self):
    """Stop the timer if it hasn't finished yet."""
    self.finished.set()

def connect(self, app):
    try:
        self.result = GetJson.get_json(self, api_view)
        if self.result == None:
            self.cache = None
            self.show_status(2)
            app.all_status = None
        elif (self.cache != self.result):
            self.cache = self.result
            app.all_status = AllStatusInstance(self.result)
            self.show_status(app.all_status.status)
            Controller.show_main_window(self, app.all_staus, app, refresh=True) 
    #call to class  MainWindow(Gtk.ApplicationWindow) if window is visble, then refresh 
             

    except Exception as e:
        print(e)
        self.show_status(2)
        app.all_status = None

def show_status(self, status):
    self.status = status
    if self.status == 0:
        return self.change_tray_icon(ico_red, notify_red, tag_red, widget=Gtk.StatusIcon)
    elif self.status == 1:
        return self.change_tray_icon(ico_gray, notify_gray, tag_gray, widget=Gtk.StatusIcon)
    elif self.status == 2:
        return self.change_tray_icon(ico_disconnect, notify_disconnect, tag_disconnect, 
               widget=Gtk.StatusIcon)

def change_tray_icon(self, icon, notification, tag, widget):
    if self.app.tray.get_title() != tag:
        self.app.tray.set_from_file(icon)
        self.app.tray.set_title(tag)
        self.notify("Notification:", notification, icon)

def notify(self, title, body, link):
    notify2.init("Alert", mainloop=None)
    icon = GdkPixbuf.Pixbuf.new_from_file(link)
    n = notify2.Notification(title, body)
    n.set_icon_from_pixbuf(icon)
    n.set_urgency(notify2.URGENCY_CRITICAL)
    n.show()


class AllStatusInstance(object):
__instance = None

def __new__(cls, val):
    if AllStatusInstance.__instance is None:
        AllStatusInstance.__instance = object.__new__(cls)
    AllStatusInstance.__instance.val = val
    return AllStatusInstance.__instance

Solution

  • Indeed, the problem is threads, in the sense that you cannot use GTK API from multiple threads, ever, as the documentation clearly states.

    You should use Gio.Task if you have a blocking, synchronous operation and you wish to update some UI state at the end of it; or, if you're using a thread object in Python, always use GLib.MainContext.invoke_full() to invoke a function within the same thread that is running the GTK main loop.

    What you should not do, is this:

    def do_startup(self):
        Gdk.threads_init()
        Gdk.threads_enter()
        Gtk.Application.do_startup(self)
        Gdk.threads_leave()
    

    and then call GTK API from a separate thread than the one that is running GTK's event loop; that is entirely undefined, non-portable behaviour.