def call_setup(self): """ Call this method to prepare the window: * makes sure it still exists (by querying its geometry which may raise an XError) * setup composite redirection * calls setup The difficulty comes from X11 errors and synchronization: we want to catch errors and undo what we've done. The mix of GTK and pure-X11 calls is not helping. """ try: with xsync: geom = X11Window.geometry_with_border(self.xid) if geom is None: raise Unmanageable("window %#x disappeared already" % self.xid) self._internal_set_property("geometry", geom[:4]) self._read_initial_X11_properties() except XError as e: log("failed to manage %#x", self.xid, exc_info=True) raise Unmanageable(e) from e add_event_receiver(self.client_window, self) # Keith Packard says that composite state is undefined following a # reparent, so I'm not sure doing this here in the superclass, # before we reparent, actually works... let's wait and see. try: self._composite = CompositeHelper(self.client_window) with xsync: self._composite.setup() if X11Window.displayHasXShape(): X11Window.XShapeSelectInput(self.xid) except Exception as e: remove_event_receiver(self.client_window, self) log("%s %#x does not support compositing: %s", self._MODELTYPE, self.xid, e) with xswallow: self._composite.destroy() self._composite = None if isinstance(e, Unmanageable): raise raise Unmanageable(e) from e #compositing is now enabled, #from now on we must call setup_failed to clean things up self._managed = True try: with xsync: self.setup() except XError as e: log("failed to setup %#x", self.xid, exc_info=True) try: with xsync: self.setup_failed(e) except Exception as ex: log.error("error in cleanup handler: %s", ex) raise Unmanageable(e) from None self._setup_done = True
class CoreX11WindowModel(WindowModelStub): """ The utility superclass for all GTK2 / X11 window models, it wraps an X11 window (the "client-window"). Defines the common properties and signals, sets up the composite helper so we get the damage events. The x11_property_handlers sync X11 window properties into Python objects, the py_property_handlers do it in the other direction. """ __common_properties__ = { #the actual X11 client window "client-window": (gobject.TYPE_PYOBJECT, "gtk.gdk.Window representing the client toplevel", "", PARAM_READABLE), #the X11 window id "xid": (gobject.TYPE_INT, "X11 window id", "", -1, 65535, -1, PARAM_READABLE), #FIXME: this is an ugly virtual property "geometry": (gobject.TYPE_PYOBJECT, "current coordinates (x, y, w, h, border) for the window", "", PARAM_READABLE), #bits per pixel "depth": (gobject.TYPE_INT, "window bit depth", "", -1, 64, -1, PARAM_READABLE), #if the window depth is 32 bit "has-alpha": (gobject.TYPE_BOOLEAN, "Does the window use transparency", "", False, PARAM_READABLE), #from WM_CLIENT_MACHINE "client-machine": (gobject.TYPE_PYOBJECT, "Host where client process is running", "", PARAM_READABLE), #from _NET_WM_PID "pid": (gobject.TYPE_INT, "PID of owning process", "", -1, 65535, -1, PARAM_READABLE), #from _NET_WM_NAME or WM_NAME "title": (gobject.TYPE_PYOBJECT, "Window title (unicode or None)", "", PARAM_READABLE), #from WM_WINDOW_ROLE "role" : (gobject.TYPE_PYOBJECT, "The window's role (ICCCM session management)", "", PARAM_READABLE), #from WM_PROTOCOLS via XGetWMProtocols "protocols": (gobject.TYPE_PYOBJECT, "Supported WM protocols", "", PARAM_READABLE), #from WM_COMMAND "command": (gobject.TYPE_PYOBJECT, "Command used to start or restart the client", "", PARAM_READABLE), #from WM_CLASS via getClassHint "class-instance": (gobject.TYPE_PYOBJECT, "Classic X 'class' and 'instance'", "", PARAM_READABLE), #ShapeNotify events will populate this using XShapeQueryExtents "shape": (gobject.TYPE_PYOBJECT, "Window XShape data", "", PARAM_READABLE), #synced to "_NET_FRAME_EXTENTS" "frame": (gobject.TYPE_PYOBJECT, "Size of the window frame, as per _NET_FRAME_EXTENTS", "", PARAM_READWRITE), #synced to "_NET_WM_ALLOWED_ACTIONS" "allowed-actions": (gobject.TYPE_PYOBJECT, "Supported WM actions", "", PARAM_READWRITE), } __common_signals__ = { #signals we emit: "unmanaged" : one_arg_signal, "raised" : one_arg_signal, "initiate-moveresize" : one_arg_signal, "grab" : one_arg_signal, "ungrab" : one_arg_signal, "bell" : one_arg_signal, "client-contents-changed" : one_arg_signal, "motion" : one_arg_signal, #x11 events we catch (and often re-emit as something else): "xpra-property-notify-event" : one_arg_signal, "xpra-xkb-event" : one_arg_signal, "xpra-shape-event" : one_arg_signal, "xpra-configure-event" : one_arg_signal, "xpra-unmap-event" : one_arg_signal, "xpra-client-message-event" : one_arg_signal, "xpra-focus-in-event" : one_arg_signal, "xpra-focus-out-event" : one_arg_signal, "xpra-motion-event" : one_arg_signal, } #things that we expose: _property_names = ["xid", "depth", "has-alpha", "client-machine", "pid", "title", "role", "command", "shape", "class-instance", "protocols"] #exposed and changing (should be watched for notify signals): _dynamic_property_names = ["title", "command", "shape", "class-instance", "protocols"] #should not be exported to the clients: _internal_property_names = ["frame", "allowed-actions"] _initial_x11_properties = ["_NET_WM_PID", "WM_CLIENT_MACHINE", "WM_NAME", "_NET_WM_NAME", #_NET_WM_NAME is redundant, as it calls the same handler as "WM_NAME" "WM_PROTOCOLS", "WM_CLASS", "WM_WINDOW_ROLE"] _DEFAULT_NET_WM_ALLOWED_ACTIONS = [] _MODELTYPE = "Core" _scrub_x11_properties = [ "WM_STATE", #"_NET_WM_STATE", # "..it should leave the property in place when it is shutting down" "_NET_FRAME_EXTENTS", "_NET_WM_ALLOWED_ACTIONS"] def __init__(self, client_window): super(CoreX11WindowModel, self).__init__() self.xid = get_xwindow(client_window) log("new window %#x", self.xid) self.client_window = client_window self.client_window_saved_events = self.client_window.get_events() self._composite = None self._damage_forward_handle = None self._setup_done = False self._kill_count = 0 self._internal_set_property("client-window", client_window) ######################################### # Setup and teardown ######################################### def call_setup(self): """ Call this method to prepare the window: * makes sure it still exists (by querying its geometry which may raise an XError) * setup composite redirection * calls setup The difficulty comes from X11 errors and synchronization: we want to catch errors and undo what we've done. The mix of GTK and pure-X11 calls is not helping. """ try: with xsync: geom = X11Window.geometry_with_border(self.xid) if geom is None: raise Unmanageable("window %#x disappeared already" % self.xid) self._internal_set_property("geometry", geom[:4]) self._read_initial_X11_properties() except XError as e: raise Unmanageable(e) add_event_receiver(self.client_window, self) # Keith Packard says that composite state is undefined following a # reparent, so I'm not sure doing this here in the superclass, # before we reparent, actually works... let's wait and see. try: self._composite = CompositeHelper(self.client_window) with xsync: self._composite.setup() if X11Window.displayHasXShape(): X11Window.XShapeSelectInput(self.xid) except Exception as e: remove_event_receiver(self.client_window, self) log("%s %#x does not support compositing: %s", self._MODELTYPE, self.xid, e) with xswallow: self._composite.destroy() self._composite = None if isinstance(e, Unmanageable): raise raise Unmanageable(e) #compositing is now enabled, #from now on we must call setup_failed to clean things up self._managed = True try: with xsync: self.setup() except XError as e: try: with xsync: self.setup_failed(e) except Exception as ex: log.error("error in cleanup handler: %s", ex) raise Unmanageable(e) self._setup_done = True def setup_failed(self, e): log("cannot manage %s %#x: %s", self._MODELTYPE, self.xid, e) self.do_unmanaged(False) def setup(self): # Start listening for important events. self.client_window.set_events(self.client_window_saved_events | ADDMASK) self._damage_forward_handle = self._composite.connect("contents-changed", self._forward_contents_changed) self._setup_property_sync() def unmanage(self, exiting=False): if self._managed: self.emit("unmanaged", exiting) def do_unmanaged(self, wm_exiting): if not self._managed: return self._managed = False log("%s.do_unmanaged(%s) damage_forward_handle=%s, composite=%s", self._MODELTYPE, wm_exiting, self._damage_forward_handle, self._composite) remove_event_receiver(self.client_window, self) glib.idle_add(self.managed_disconnect) if self._composite: if self._damage_forward_handle: self._composite.disconnect(self._damage_forward_handle) self._damage_forward_handle = None self._composite.destroy() self._composite = None self._scrub_x11() ######################################### # Damage / Composite ######################################### def acknowledge_changes(self): c = self._composite assert c, "composite window destroyed outside the UI thread?" c.acknowledge_changes() def _forward_contents_changed(self, _obj, event): if self._managed: self.emit("client-contents-changed", event) def uses_XShm(self): c = self._composite return c and c.has_xshm() def get_image(self, x, y, width, height): return self._composite.get_image(x, y, width, height) def _setup_property_sync(self): metalog("setup_property_sync()") #python properties which trigger an X11 property to be updated: for prop, cb in self._py_property_handlers.items(): self.connect("notify::%s" % prop, cb) #initial sync: for cb in self._py_property_handlers.values(): cb(self) #this one is special, and overriden in BaseWindow too: self.managed_connect("notify::protocols", self._update_can_focus) def _update_can_focus(self, *_args): can_focus = "WM_TAKE_FOCUS" in self.get_property("protocols") self._updateprop("can-focus", can_focus) def _read_initial_X11_properties(self): """ This is called within an XSync context, so that X11 calls can raise XErrors, pure GTK calls are not allowed. (they would trap the X11 error and crash!) Calling _updateprop is safe, because setup has not completed yet, so the property update will not fire notify() """ metalog("read_initial_X11_properties() core") #immutable ones: depth = X11Window.get_depth(self.xid) metalog("initial X11 properties: xid=%#x, depth=%i", self.xid, depth) self._updateprop("depth", depth) self._updateprop("xid", self.xid) self._updateprop("has-alpha", depth==32) self._updateprop("allowed-actions", self._DEFAULT_NET_WM_ALLOWED_ACTIONS) self._updateprop("shape", self._read_xshape()) #note: some of those are technically mutable, #but we don't export them as "dynamic" properties, so this won't be propagated #maybe we want to catch errors parsing _NET_WM_ICON ? metalog("initial X11_properties: querying %s", self._initial_x11_properties) #to make sure we don't call the same handler twice which is pointless #(the same handler may handle more than one X11 property) handlers = set() for mutable in self._initial_x11_properties: handler = self._x11_property_handlers.get(mutable) if not handler: log.error("BUG: unknown initial X11 property: %s", mutable) elif handler not in handlers: handlers.add(handler) try: handler(self) except Exception as e: log.error("_read_initial_X11_properties()", exc_info=True) log.error("Error parsing initial property '%s':", mutable) log.error(" %s", e) def _scrub_x11(self): metalog("scrub_x11() x11 properties=%s", self._scrub_x11_properties) if not self._scrub_x11_properties: return with xswallow: for prop in self._scrub_x11_properties: X11Window.XDeleteProperty(self.xid, prop) ######################################### # XShape ######################################### def _read_xshape(self, x=0, y=0): if not X11Window.displayHasXShape() or not XSHAPE: return {} extents = X11Window.XShapeQueryExtents(self.xid) if not extents: shapelog("read_shape for window %#x: no extents", self.xid) return {} #w,h = X11Window.getGeometry(xid)[2:4] shapelog("read_shape for window %#x: extents=%s", self.xid, extents) bextents = extents[0] cextents = extents[1] if bextents[0]==0 and cextents[0]==0: shapelog("read_shape for window %#x: none enabled", self.xid) return {} v = { "x" : x, "y" : y, "Bounding.extents" : bextents, "Clip.extents" : cextents, } for kind in SHAPE_KIND.keys(): kind_name = SHAPE_KIND[kind] rectangles = X11Window.XShapeGetRectangles(self.xid, kind) v[kind_name+".rectangles"] = rectangles shapelog("_read_shape()=%s", v) return v ################################ # Property reading ################################ def get_dimensions(self): #just extracts the size from the geometry: return self.get_property("geometry")[2:4] def get_geometry(self): return self.get_property("geometry")[:4] ######################################### # Python objects synced to X11 properties ######################################### def prop_set(self, key, ptype, value): prop_set(self.client_window, key, ptype, value) def _sync_allowed_actions(self, *_args): actions = self.get_property("allowed-actions") or [] metalog("sync_allowed_actions: setting _NET_WM_ALLOWED_ACTIONS=%s on %#x", actions, self.xid) with xswallow: prop_set(self.client_window, "_NET_WM_ALLOWED_ACTIONS", ["atom"], actions) def _handle_frame_changed(self, *_args): #legacy name for _sync_frame() called from Wm self._sync_frame() def _sync_frame(self, *_args): if not FRAME_EXTENTS: return v = self.get_property("frame") framelog("sync_frame: frame(%#x)=%s", self.xid, v) if not v and (not self.is_OR() and not self.is_tray()): root = self.client_window.get_screen().get_root_window() v = prop_get(root, "DEFAULT_NET_FRAME_EXTENTS", ["u32"], ignore_errors=True) if not v: #default for OR, or if we don't have any other value: v = (0, 0, 0, 0) framelog("sync_frame: setting _NET_FRAME_EXTENTS=%s on %#x", v, self.xid) with xswallow: prop_set(self.client_window, "_NET_FRAME_EXTENTS", ["u32"], v) _py_property_handlers = { "allowed-actions" : _sync_allowed_actions, "frame" : _sync_frame, } ######################################### # X11 properties synced to Python objects ######################################### def prop_get(self, key, ptype, ignore_errors=None, raise_xerrors=False): """ Get an X11 property from the client window, using the automatic type conversion code from prop.py Ignores property errors during setup_client. """ if ignore_errors is None and (not self._setup_done or not self._managed): ignore_errors = True return prop_get(self.client_window, key, ptype, ignore_errors=bool(ignore_errors), raise_xerrors=raise_xerrors) def do_xpra_property_notify_event(self, event): #X11: PropertyNotify assert event.window is self.client_window self._handle_property_change(str(event.atom)) def _handle_property_change(self, name): #ie: _handle_property_change("_NET_WM_NAME") metalog("Property changed on %#x: %s", self.xid, name) x11proptype = X11_PROPERTIES_DEBUG.get(name) if x11proptype is not None: metalog.info("%s=%s", name, self.prop_get(name, x11proptype, True, False)) if name in PROPERTIES_IGNORED: return handler = self._x11_property_handlers.get(name) if handler: try: with xsync: handler(self) except XError as e: log("_handle_property_change", exc_info=True) log.error("Error processing property change for '%s'", name) log.error(" on window %#x", self.xid) log.error(" %s", e) #specific properties: def _handle_pid_change(self): pid = self.prop_get("_NET_WM_PID", "u32") or -1 metalog("_NET_WM_PID=%s", pid) self._updateprop("pid", pid) def _handle_client_machine_change(self): client_machine = self.prop_get("WM_CLIENT_MACHINE", "latin1") metalog("WM_CLIENT_MACHINE=%s", client_machine) self._updateprop("client-machine", client_machine) def _handle_wm_name_change(self): name = self.prop_get("_NET_WM_NAME", "utf8", True) metalog("_NET_WM_NAME=%s", name) if name is None: name = self.prop_get("WM_NAME", "latin1", True) metalog("WM_NAME=%s", name) if self._updateprop("title", sanestr(name)): metalog("wm_name changed") def _handle_role_change(self): role = self.prop_get("WM_WINDOW_ROLE", "latin1") metalog("WM_WINDOW_ROLE=%s", role) self._updateprop("role", role) def _handle_protocols_change(self): with xsync: protocols = X11Window.XGetWMProtocols(self.xid) metalog("WM_PROTOCOLS=%s", protocols) self._updateprop("protocols", protocols) def _handle_command_change(self): command = self.prop_get("WM_COMMAND", "latin1") metalog("WM_COMMAND=%s", command) if command: command = command.strip("\0") self._updateprop("command", command) def _handle_class_change(self): class_instance = X11Window.getClassHint(self.xid) if class_instance: class_instance = tuple(v.decode("latin1") for v in class_instance) metalog("WM_CLASS=%s", class_instance) self._updateprop("class-instance", class_instance) #these handlers must not generate X11 errors (must use XSync) _x11_property_handlers = { "_NET_WM_PID" : _handle_pid_change, "WM_CLIENT_MACHINE" : _handle_client_machine_change, "WM_NAME" : _handle_wm_name_change, "_NET_WM_NAME" : _handle_wm_name_change, "WM_WINDOW_ROLE" : _handle_role_change, "WM_PROTOCOLS" : _handle_protocols_change, "WM_COMMAND" : _handle_command_change, "WM_CLASS" : _handle_class_change, } ######################################### # X11 Events ######################################### def do_xpra_unmap_event(self, _event): self.unmanage() def do_xpra_destroy_event(self, event): if event.delivered_to is self.client_window: # This is somewhat redundant with the unmap signal, because if you # destroy a mapped window, then a UnmapNotify is always generated. # However, this allows us to catch the destruction of unmapped # ("iconified") windows, and also catch any mistakes we might have # made with unmap heuristics. I love the smell of XDestroyWindow in # the morning. It makes for simple code: self.unmanage() def process_client_message_event(self, event): # FIXME # Need to listen for: # _NET_CURRENT_DESKTOP # _NET_WM_PING responses # and maybe: # _NET_RESTACK_WINDOW # _NET_WM_STATE (more fully) if event.message_type=="_NET_CLOSE_WINDOW": log.info("_NET_CLOSE_WINDOW received by %s", self) self.request_close() return True elif event.message_type=="_NET_REQUEST_FRAME_EXTENTS": framelog("_NET_REQUEST_FRAME_EXTENTS") self._handle_frame_changed() return True elif event.message_type=="_NET_MOVERESIZE_WINDOW": #this is overriden in WindowModel, skipped everywhere else: geomlog("_NET_MOVERESIZE_WINDOW skipped on %s (data=%s)", self, event.data) return True #not handled: return False def do_xpra_configure_event(self, event): if self.client_window is None or not self._managed: return #shouldn't the border width always be 0? geom = (event.x, event.y, event.width, event.height) geomlog("CoreX11WindowModel.do_xpra_configure_event(%s) client_window=%#x, new geometry=%s", event, self.xid, geom) self._updateprop("geometry", geom) def do_xpra_shape_event(self, event): shapelog("shape event: %s, kind=%s", event, SHAPE_KIND.get(event.kind, event.kind)) cur_shape = self.get_property("shape") if cur_shape and cur_shape.get("serial", 0)>=event.serial: shapelog("same or older xshape serial no: %#x (current=%#x)", event.serial, cur_shape.get("serial", 0)) return #remove serial before comparing dicts: try: cur_shape["serial"] except: pass #read new xshape: with xswallow: #should we pass the x and y offsets here? #v = self._read_xshape(event.x, event.y) if event.shaped: v = self._read_xshape() else: v = {} if cur_shape==v: shapelog("xshape unchanged") return v["serial"] = int(event.serial) shapelog("xshape updated with serial %#x", event.serial) self._internal_set_property("shape", v) def do_xpra_xkb_event(self, event): #X11: XKBNotify log("WindowModel.do_xpra_xkb_event(%r)" % event) if event.subtype!="bell": log.error("WindowModel.do_xpra_xkb_event(%r) unknown event type: %s" % (event, event.type)) return event.window_model = self self.emit("bell", event) def do_xpra_client_message_event(self, event): #X11: ClientMessage log("do_xpra_client_message_event(%s)", event) if not event.data or len(event.data)!=5: log.warn("invalid event data: %s", event.data) return if not self.process_client_message_event(event): log.warn("do_xpra_client_message_event(%s) not handled", event) def do_xpra_focus_in_event(self, event): #X11: FocusIn grablog("focus_in_event(%s) mode=%s, detail=%s", event, GRAB_CONSTANTS.get(event.mode), DETAIL_CONSTANTS.get(event.detail, event.detail)) if event.mode==NotifyNormal and event.detail==NotifyNonlinearVirtual: self.emit("raised", event) else: self.may_emit_grab(event) def do_xpra_focus_out_event(self, event): #X11: FocusOut grablog("focus_out_event(%s) mode=%s, detail=%s", event, GRAB_CONSTANTS.get(event.mode), DETAIL_CONSTANTS.get(event.detail, event.detail)) self.may_emit_grab(event) def may_emit_grab(self, event): if event.mode==NotifyGrab: grablog("emitting grab on %s", self) self.emit("grab", event) if event.mode==NotifyUngrab: grablog("emitting ungrab on %s", self) self.emit("ungrab", event) def do_xpra_motion_event(self, event): self.emit("motion", event) ################################ # Actions ################################ def raise_window(self): X11Window.XRaiseWindow(get_xwindow(self.client_window)) def set_active(self): root = self.client_window.get_screen().get_root_window() prop_set(root, "_NET_ACTIVE_WINDOW", "u32", self.xid) ################################ # Killing clients: ################################ def request_close(self): if "WM_DELETE_WINDOW" in self.get_property("protocols"): with xswallow: send_wm_delete_window(self.client_window) else: title = self.get_property("title") xid = self.get_property("xid") if FORCE_QUIT: log.warn("window %#x ('%s') does not support WM_DELETE_WINDOW... using force_quit", xid, title) # You don't wanna play ball? Then no more Mr. Nice Guy! self.force_quit() else: log.warn("window %#x ('%s') cannot be closed,", xid, title) log.warn(" it does not support WM_DELETE_WINDOW") log.warn(" and FORCE_QUIT is disabled") def force_quit(self): pid = self.get_property("pid") machine = self.get_property("client-machine") from socket import gethostname localhost = gethostname() log("force_quit() pid=%s, machine=%s, localhost=%s", pid, machine, localhost) def XKill(): with xswallow: X11Window.XKillClient(self.xid) if pid > 0 and machine is not None and machine == localhost: if pid==os.getpid(): log.warn("force_quit() refusing to kill ourselves!") return if self._kill_count==0: #first time around: just send a SIGINT and hope for the best try: os.kill(pid, signal.SIGINT) except OSError: log.warn("failed to kill(SIGINT) client with pid %s", pid) else: #the more brutal way: SIGKILL + XKill try: os.kill(pid, signal.SIGKILL) except OSError: log.warn("failed to kill(SIGKILL) client with pid %s", pid) XKill() self._kill_count += 1 return XKill()