xpra icon
Bug tracker and wiki

This bug tracker and wiki are being discontinued
please use https://github.com/Xpra-org/xpra instead.


Ticket #228: menus-refcount.patch

File menus-refcount.patch, 17.7 KB (added by Antoine Martin, 6 years ago)

more proper reference counting for shared dbus menu services

  • tests/xpra/x11/set_gtk_menu.py

     
    44# Xpra is released under the terms of the GNU GPL v2, or, at your option, any
    55# later version. See the file COPYING for details.
    66
     7import copy
    78import sys
    89from xpra.x11.gtk2.gdk_display_source import display    #@UnresolvedImport
     10assert display
    911from xpra.x11.gtk_x11.prop import prop_set
    10 from xpra.dbus.helper import DBusHelper
    1112from xpra.dbus.gtk_menuactions import Menus, Actions
    1213
    1314#beware: this import has side-effects:
     
    1516assert dbus.glib
    1617import gtk
    1718
    18 from xpra.dbus.common import loop_init
    1919
     20class WindowWithMenu(gtk.Window):
    2021
    21 def main(args):
    22     window = gtk.Window(gtk.WINDOW_TOPLEVEL)
    23     window.set_size_request(200, 200)
    24     window.connect("delete_event", gtk.mainquit)
    25     window.realize()
    26     w = window.get_window()
     22    def __init__(self, *args):
     23        gtk.Window.__init__(self, gtk.WINDOW_TOPLEVEL)
     24        self.set_size_request(200, 200)
     25        self.connect("delete_event", gtk.mainquit)
    2726
    28     dbus_helper = DBusHelper()
    29     loop_init()
    30     session_bus = dbus_helper.get_session_bus()
     27    def init_menu(self):
     28        from xpra.dbus.common import init_session_bus
     29        session_bus = init_session_bus()
     30        self.app_id = u"org.xpra.ExampleMenu"
     31        self.app_path = u"/org/xpra/ExampleMenu"
     32        self.menu_path = u"%s/menus/appmenu" % self.app_path
     33        self.window_path = u"%s/window/1" % self.app_path
     34        self.bus_name = session_bus.get_unique_name().decode()
    3135
    32     app_id = u"org.xpra.ExampleMenu"
    33     app_path = u"/org/xpra/ExampleMenu"
    34     menu_path = u"%s/menus/appmenu" % app_path
    35     window_path = u"%s/window/1" % app_path
    36     bus_name = session_bus.get_unique_name().decode()
    37 
    38     def action_cb(*args):
    39         print("action_cb%s" % str(args))
    40     window_actions={'reset'         : [True, 'b', [], action_cb],
    41                     'fullscreen'    : [True, '', [0], action_cb],
    42                     'about'         : [True, '', [], action_cb],
    43                     'preferences'   : [True, '', [], action_cb],
    44                     'switch-tab'    : [True, 'i', [], action_cb],
     36        self.window_actions = {
     37                    'reset'         : [True, 'b', [],   self.action_cb],
     38                    'fullscreen'    : [True, '', [0],   self.action_cb],
     39                    'about'         : [True, '', [],    self.action_cb],
     40                    'preferences'   : [True, '', [],    self.action_cb],
     41                    'switch-tab'    : [True, 'i', [],   self.action_cb],
    4542                    'detach-tab'    : [True, '', []],
    4643                    'save-contents' : [True, '', []],
    4744                    'edit-profile'  : [True, 's', []],
     
    5552                    'copy'          : [True, '', []],
    5653                    'paste'         : [True, 's', []],
    5754                    'find'          : [True, 's', []],
    58                     'help'          : [True, '', []]}
    59     Actions(app_id, window_path, session_bus, window_actions)
    60     app_actions = {
     55                    'help'          : [True, '', []]
     56                }
     57        self.app_actions = {
    6158                    'quit'          : [True, '', []],
    6259                    'about'         : [True, '', []],
    6360                    'activate-tab'  : [True, 's', []],
     
    6562                    'help'          : [True, '', []],
    6663                    'custom'        : [True, '', []],
    6764                  }
    68     Actions(app_id, app_path, session_bus, app_actions)
    69     menus = {0:
     65        self.menus = {0:
    7066             {
    71                 0: [{':section': (0, 1)}, {':section': (0, 2)}, {':section': (0, 3)}],
     67                0: [{'action': 'app.help', 'label': '_Help'}, {':section': (0, 1)}, {':section': (0, 2)}, {':section': (0, 3)}],
    7268                1: [{'action': 'win.new-terminal', 'label': '_New Terminal', 'target': ['default', 'default']}],
    7369                2: [{'action': 'app.preferences', 'label': '_Preferences'}],
    7470                3: [{'action': 'app.help', 'label': '_Help'},
     
    7672                    {'action': 'app.quit', 'label': '_Quit'}
    7773                   ]
    7874             },
    79              #not shown anywhere:
     75             #not shown anywhere when defined (group=1):
    8076             #1:
    8177             #{
    8278             #   0: [{':section': (0, 1)}],
     
    8379             #   1: [{'action': 'app.custom', 'label': '_Custom'}]
    8480             #},
    8581            }
    86     menus_service = Menus(app_id, menu_path, session_bus, menus)
     82        self.alt_menus = {}
     83        self.alt_menus = copy.deepcopy(self.menus)
     84        #remove about:
     85        help_about_quit = self.alt_menus[0][3]
     86        help_about_quit.remove(help_about_quit[1])
     87        self.current_menu = self.menus
    8788
    88     def pset(key, value):
    89         return prop_set(w, key, "utf8", value)
    90     pset("_GTK_APP_MENU_OBJECT_PATH",       menu_path)
    91     pset("_GTK_WINDOW_OBJECT_PATH",         window_path)
    92     pset("_GTK_APPLICATION_OBJECT_PATH",    app_path)
    93     pset("_GTK_UNIQUE_BUS_NAME",            bus_name)
    94     pset("_GTK_APPLICATION_ID",             app_id)
    95     print("gtk menu properties for window %#x on display %s" % (w.xid, display.get_name()))
    9689
    97     import copy
    98     new_menus = copy.deepcopy(menus)
    99     #remove about:
    100     help_about_quit = new_menus[0][3]
    101     help_about_quit.remove(help_about_quit[1])
     90    def publish_menu(self, *args):
     91        print("publish_menu()")
     92        self.init_menu()
     93        self.setup_dbus_services()
     94        self.set_X11_props()
    10295
    103     both_menus = [menus, new_menus]
    104     def toggle_menu(*args):
    105         menus_service.set_menus(both_menus[0])
    106         saved = list(both_menus)
    107         both_menus[0] = saved[1]
    108         both_menus[1] = saved[0]
     96
     97    def toggle_menu(self, *args):
     98        print("toggle_menu()")
     99        if self.current_menu == self.menus:
     100            m = self.alt_menus
     101        else:
     102            m = self.menus
     103        self.current_menu = m
     104        self.menus_service.set_menus(self.current_menu)
    109105        return True
     106
     107    def setup_dbus_services(self):
     108        from xpra.dbus.common import init_session_bus
     109        session_bus = init_session_bus()
     110        self.window_actions_service = Actions(self.app_id, self.window_path, session_bus, self.window_actions)
     111        self.app_actions_service = Actions(self.app_id, self.app_path, session_bus, self.app_actions)
     112        self.menus_service = Menus(self.app_id, self.menu_path, session_bus, self.menus)
     113
     114    def set_X11_props(self):
     115        w = self.get_window()
     116        def pset(key, value):
     117            return prop_set(w, key, "utf8", value)
     118        pset("_GTK_APP_MENU_OBJECT_PATH",       self.menu_path)
     119        pset("_GTK_WINDOW_OBJECT_PATH",         self.window_path)
     120        pset("_GTK_APPLICATION_OBJECT_PATH",    self.app_path)
     121        pset("_GTK_UNIQUE_BUS_NAME",            self.bus_name)
     122        pset("_GTK_APPLICATION_ID",             self.app_id)
     123
     124
     125    def action_cb(self, *args):
     126        print("action_cb%s" % str(args))
     127
     128
     129
     130def main(args):
     131    from xpra.dbus.common import loop_init
     132    loop_init()
     133    w = WindowWithMenu()
     134    w.show()
    110135    import gobject
    111     gobject.timeout_add(1000*5, toggle_menu)
    112     window.show()
     136    #show custom app menu after N seconds
     137    gobject.timeout_add(1000*10, w.publish_menu)
     138    #toggle it every M seconds:
     139    gobject.timeout_add(1000*30, w.toggle_menu)
    113140    gtk.main()
    114141
    115142
  • xpra/platform/xposix/gtkmenu_tray.py

     
    8383               }
    8484        from xpra.x11.gtk_x11 import menu
    8585        menu.fallback_menus = menus
    86         #menu.our_menu = "Xpra", menus
     86        #FIXME: we should trigger a refresh of the dbus menu...
     87        #(rather than wait for the next window event)
  • xpra/x11/gtk_x11/menu.py

     
    55
    66import weakref
    77from xpra.log import Logger
    8 menulog = Logger("menu")
     8log = Logger("menu")
    99
    10 from xpra.util import typedict, bytestostr
     10from xpra.util import typedict, bytestostr, AtomicInteger
    1111
    1212
    1313def has_gtk_menu_support(root_window):
     
    1616        from xpra.dbus.helper import DBusHelper
    1717        assert DBusHelper
    1818    except Exception as e:
    19         menulog("has_menu_support() no dbus: %s", e)
     19        log("has_menu_support() no dbus: %s", e)
    2020        return False
    2121    try:
    2222        from xpra.x11.gtk_x11.prop import prop_get
    2323    except Exception as e:
    24         menulog("has_menu_support() no X11 bindings: %s", e)
     24        log("has_menu_support() no X11 bindings: %s", e)
    2525        return False
    2626    v = prop_get(root_window, "_NET_SUPPORTED", ["atom"], ignore_errors=True, raise_xerrors=False)
    2727    if not v:
    28         menulog("has_menu_support() _NET_SUPPORTED is empty!?")
     28        log("has_menu_support() _NET_SUPPORTED is empty!?")
    2929        return False
    3030    show_window_menu = "_GTK_SHOW_WINDOW_MENU" in v
    31     menulog("has_menu_support() _GTK_SHOW_WINDOW_MENU in _NET_SUPPORTED: %s", show_window_menu)
     31    log("has_menu_support() _GTK_SHOW_WINDOW_MENU in _NET_SUPPORTED: %s", show_window_menu)
    3232    return show_window_menu
    3333
    3434
     35#for each window id, keep track of the current menu services we have created:
     36#wid -> (app_id, [app_actions_service, window_actions_service, window_menu_service])
    3537window_menus = {}
     38#keep track of all the services we have created, and the usage count for each service
     39#(the same service may be available under different paths):
     40#(service_class, name, path) -> (service, counter)
    3641window_menu_services = weakref.WeakValueDictionary()
    3742
    3843fallback_menus = {}
    3944
     45
     46def unref_service(service):
     47    v = service.reference_count
     48    count = v.decrease()
     49    log("unref service %s, new reference count=%i", service, count)
     50    if count<=0:
     51        service.remove_from_connection()
     52
     53def get_service(current_service, service_class, name, path, *args):
     54    """ find the service by name and path, or create one """
     55    global window_menu_services
     56    service = window_menu_services.get((service_class, name, path))
     57    if current_service:
     58        if current_service==service:
     59            log("keeping existing service: %s", current_service)
     60            #no change
     61            return current_service, True
     62        #drop the old service
     63        unref_service(current_service)
     64        current_service = None
     65    if service is None:
     66        from xpra.dbus.common import init_session_bus
     67        session_bus = init_session_bus()
     68        service = service_class(name, path, session_bus, *args)
     69        service.reference_count = AtomicInteger(0)
     70        window_menu_services[(service_class, name, path)] = service
     71        service.reference_count.increase()
     72    return service, False
     73
     74
    4075def setup_dbus_window_menu(add, wid, menus, application_action_callback=None, window_action_callback=None):
     76    global window_menus, fallback_menus
     77    cur_app_id, services  = window_menus.get(wid, (None, [None, None, None]))
     78    app_actions_service, window_actions_service, window_menu_service = services
     79
     80    def unref_all_services():
     81        #frees up all the dbus services if their refcount reaches 0:
     82        for x in (app_actions_service, window_actions_service, window_menu_service):
     83            if x:
     84                unref_service(x)
     85        try:
     86            del window_menus[wid]
     87        except:
     88            pass
    4189    def nomenu():
     90        unref_all_services()
    4291        #tell caller to clear all properties if they exist:
    4392        return {
    4493                "_GTK_APP_MENU_OBJECT_PATH"     : None,
     
    4796                "_GTK_UNIQUE_BUS_NAME"          : None,
    4897                "_GTK_APPLICATION_ID"           : None
    4998                }
    50     if add is False:
     99
     100    enabled = menus.get("enabled", False)
     101    if (len(menus)==0 or (enabled is False)) and fallback_menus:
     102        #use fallback menus if the application menus are empty or disabled
     103        log("using fallback menu")
     104        menus = fallback_menus
     105    enabled = menus.get("enabled", False)
     106    if len(menus)==0 or (add is False) or (not enabled):
     107        #remove everything
    51108        return nomenu()
    52     global window_menu_services, window_menus, fallback_menus
    53     if len(menus)==0 and fallback_menus:
    54         menus = fallback_menus
    55109    #ie: menu = {
    56110    #         'enabled': True,
    57111    #         'application-id':         'org.xpra.ExampleMenu',
     
    65119    #                }
    66120    #             }
    67121    #           }
    68     enabled = menus.get("enabled", False)
    69     app_actions_service, window_actions_service, window_menu_service = None, None, None
    70     def remove_services(*args):
    71         """ removes all the services if they are not longer used by any windows """
    72         for x in (app_actions_service, window_actions_service, window_menu_service):
    73             if x:
    74                 if x not in window_menu_services.values():
    75                     try:
    76                         x.remove_from_connection()
    77                     except Exception as e:
    78                         menulog.warn("Error removing %s: %s", x, e)
    79         try:
    80             del window_menus[wid]
    81         except:
    82             pass
    83     if enabled:
    84         m = typedict(menus)
    85         app_id          = bytestostr(m.strget("application-id", b"org.xpra.Window%i" % wid)).decode()
    86         app_actions     = m.dictget("application-actions")
    87         window_actions  = m.dictget("window-actions")
    88         window_menu     = m.dictget("window-menu")
    89     if wid in window_menus:
    90         #update, destroy or re-create the services:
    91         app_actions_service, window_actions_service, window_menu_service, cur_app_id = window_menus[wid]
    92         if not enabled or cur_app_id!=app_id:
    93             remove_services()   #falls through to re-create them if enabled is True
    94             app_actions_service, window_actions_service, window_menu_service = None, None, None
    95         else:
    96             #update them:
    97             app_actions_service.set_actions(app_actions)
    98             window_actions_service.set_actions(window_actions)
    99             window_menu_service.set_menus(window_menu)
    100             return
    101     if not enabled:
    102         #tell caller to clear all properties if they exist:
    103         return nomenu()
     122    m = typedict(menus)
     123    app_id          = bytestostr(m.strget("application-id", b"org.xpra.Window%i" % wid)).decode()
     124    app_actions     = m.dictget("application-actions")
     125    window_actions  = m.dictget("window-actions")
     126    window_menu     = m.dictget("window-menu")
     127
     128    if cur_app_id is not None and cur_app_id!=app_id:
     129        #we can't change the path of a service, so re-create them:
     130        log("app-id has changed from %s to %s, re-creating the services %s", cur_app_id, app_id, services)
     131        unref_all_services()    #falls through to re-create them
     132        app_actions_service, window_actions_service, window_menu_service = None, None, None
     133        services = [None, None, None]
     134    window_menus[wid] = app_id, services         
    104135    #make or re-use services:
    105136    try:
    106137        NAME_PREFIX = "org.xpra."
     
    114145            if name.startswith(strip):
    115146                name = name[len(strip):]
    116147        name = NAME_PREFIX + name
    117         menulog("normalized named(%s)=%s", app_id, name)
     148        log("normalized named(%s)=%s", app_id, name)
    118149
    119         def get_service(service_class, name, path, *args):
    120             """ find the service by name and path, or create one """
    121             service = window_menu_services.get((service_class, name, path))
    122             if service is None:
    123                 menulog
    124                 service = service_class(name, path, session_bus, *args)
    125                 window_menu_services[(service_class, name, path)] = service
    126             return service
    127 
    128150        app_path = strtobytes("/"+name.replace(".", "/")).decode()
    129         app_actions_service = get_service(Actions, name, app_path, app_actions, application_action_callback)
     151        app_actions_service, keep = get_service(app_actions_service, Actions, name, app_path, app_actions, application_action_callback)
     152        if keep:
     153            app_actions_service.set_actions(app_actions)
     154        else:
     155            services[0] = app_actions_service
    130156
    131157        #this one should be unique and therefore not re-used? (only one "window_action_callback"..)
    132158        window_path = u"%s/window/%s" % (app_path, wid)
    133         window_actions_service = get_service(Actions, name, window_path, window_actions, window_action_callback)
     159        window_actions_service, keep = get_service(window_actions_service, Actions, name, window_path, window_actions, window_action_callback)
     160        if keep:
     161            window_actions_service.set_actions(window_actions)
     162        else:
     163            services[1] = window_actions_service
    134164
    135165        menu_path = u"%s/menus/appmenu" % app_path
    136         window_menu_service = get_service(Menus, app_id, menu_path, window_menu)
    137         window_menus[wid] = app_actions_service, window_actions_service, window_menu_service, app_id
     166        window_menu_service, keep = get_service(window_menu_service, Menus, app_id, menu_path, window_menu)
     167        if keep:
     168            window_menu_service.set_menus(window_menu)
     169        else:
     170            services[2] = window_menu_service
    138171
    139172        return {
    140173                "_GTK_APP_MENU_OBJECT_PATH"     : ("utf8", menu_path),
     
    144177                "_GTK_APPLICATION_ID"           : ("utf8", app_id),
    145178               }
    146179    except Exception:
    147         menulog.error("Error: cannot parse or apply menu:", exc_info=True)
    148         remove_services()
     180        log.error("Error: cannot parse or apply menu:", exc_info=True)
    149181        return nomenu()