branch: externals/exwm
commit 96a6b1bba221aee49db1151e2aba4163d0058e04
Author: Steven Allen <[email protected]>
Commit: Steven Allen <[email protected]>
Rework closing of "hung" windows
In X, there are 4 ways to "kill":
1. If supported, you can ASK the application to close the window with
WM_DELETE_WINDOW. This gives the application the chance to, e.g., ask
the user if they want to save their work first.
2. You can destroy the window directly and immediately.
3. You can kill the X connection associated with the client that owns
the window. This is what xkill does and generally results in the
application exiting and/or crashing.
4. You can directly lookup the X client's PID and kill it.
In terms of other window managers:
- i3 does 1 if supported, and 2 if not. It used to fallback on 3, but it
changed the default behavior at some point.
- BSPWM and DWM do 1 if supported, and 3 if not.
More complex window managers likely implement some kind of timeout
mechanism as EXWM does, but I'm not going to go digging through GNOME's
code.
EXWM, on the other hand does 1 if supported, 2 if 1 isn't supported,
then 4 if 1 is supported but the application isn't responding to pings,
finally falling back on 3 if it can't get the PID of the application
associated with the X window.
This commit changes this to behave more like other window managers:
1. We attempt 1 if supported, 2 if 1 is unsupported.
2. If 1 is supported and we see that the application isn't responded, we
fallback on 2.
Falling back on 3 in that second case would also be reasonable, but 2 is
the more conservative option.
NOBODY except EXWM does 4 because, as the comment in the removed code
states: programs (potentially running as other users) can simply lie
about their PID.
* exwm-core.el (exwm--ping): Add a variable to track the ping counter on
an X window
* exwm.el (exwm--on-wm-protocols): Update `exwm--ping' when a window
responds to a ping.
* exwm-manage.el (exwm-manage--ping-lock): Remove the ping lock because
it assumes we can only be pinging a single window at a time.
(exwm-manage--ping): Add a new function to ping a window, returning
either nil (when pinging is unsupported) or the current ping
count (before sending this ping).
(exwm-manage--kill-buffer-timeout-function): Add a function to check if
the X window is unresponsive. We use this instead of blocking on process
output.
(exwm-manage--kill-buffer-query-function): Implement the new X window
killing logic described above.
(exwm-manage--kill-client): Simplify `exwm-manage--kill-client' to only
send the KillClient message and never attempt to send a signal to the
underlying process.
---
exwm-core.el | 2 +
exwm-manage.el | 127 +++++++++++++++++++++++++++++----------------------------
exwm.el | 11 +++--
3 files changed, 74 insertions(+), 66 deletions(-)
diff --git a/exwm-core.el b/exwm-core.el
index 627db1468c..14b8bfa822 100644
--- a/exwm-core.el
+++ b/exwm-core.el
@@ -281,6 +281,8 @@ If CONN is non-nil, use it instead of the value of the
variable
One of `line-mode' or `char-mode'.")
(defvar-local exwm--input-mode 'line-mode
"Actual input mode, i.e. whether mouse and keyboard are grabbed.")
+(defvar-local exwm--ping 0
+ "The number of pings received from the window.")
;; Properties
(defvar-local exwm--desktop nil "_NET_WM_DESKTOP.")
(defvar-local exwm-window-type nil "_NET_WM_WINDOW_TYPE.")
diff --git a/exwm-manage.el b/exwm-manage.el
index 674fc4f5c3..9f06a2c352 100644
--- a/exwm-manage.el
+++ b/exwm-manage.el
@@ -148,9 +148,6 @@ want to match against EXWM internal variables such as
`exwm-title',
(defvar exwm-manage--frame-outer-id-list nil
"List of window-outer-id's of all frames.")
-(defvar exwm-manage--ping-lock nil
- "Non-nil indicates EXWM is pinging a window.")
-
(defvar exwm-input--skip-buffer-list-update)
(defvar exwm-input-prefix-keys)
(defvar exwm-workspace--current)
@@ -531,6 +528,39 @@ manager is shutting down."
(xcb:flush exwm--connection)
(exwm-manage--manage-window i)))))))
+(defun exwm-manage--ping ()
+ "Send a ping to the current EXWM window.
+On reply, `exwm--ping' will be incremented."
+ (cl-assert exwm--id)
+ (xcb:+request exwm--connection
+ (make-instance 'xcb:SendEvent
+ :propagate 0
+ :destination exwm--id
+ :event-mask xcb:EventMask:NoEvent
+ :event (xcb:marshal
+ (make-instance 'xcb:ewmh:_NET_WM_PING
+ :window exwm--id
+ :timestamp 0
+ :client-window exwm--id)
+ exwm--connection)))
+ (xcb:flush exwm--connection))
+
+(defun exwm-manage--kill-buffer-timeout-function (last-ping buffer)
+ "Called from a timer to potentially kill unresponsive windows.
+
+BUFFFER is the BUFFER to potentially kill.
+Unless the BUFFER's `exwm--ping' greater than LAST-PING, the X window
+is considered to be unresponsive."
+ (when (buffer-live-p buffer)
+ (with-current-buffer buffer
+ (unless (< last-ping exwm--ping)
+ (when (yes-or-no-p (format "'%s' is not responding. \
+Would you like to force close it? " (buffer-name)))
+ (setq exwm--protocols
+ (delq xcb:Atom:WM_DELETE_WINDOW
+ exwm--protocols))
+ (kill-buffer buffer))))))
+
(defun exwm-manage--kill-buffer-query-function ()
"Run in `kill-buffer-query-functions'."
(exwm--log "id=#x%x; buffer=%s" (or exwm--id 0) (current-buffer))
@@ -568,69 +598,42 @@ manager is shutting down."
(xcb:flush exwm--connection)
;; Wait for DestroyNotify event.
(throw 'return nil))
- (let ((id exwm--id))
- ;; Try to close the X window with WM_DELETE_WINDOW client message.
- (xcb:+request exwm--connection
- (make-instance 'xcb:icccm:SendEvent
- :destination id
- :event (xcb:marshal
- (make-instance 'xcb:icccm:WM_DELETE_WINDOW
- :window id)
- exwm--connection)))
- (xcb:flush exwm--connection)
- ;;
- (unless (memq xcb:Atom:_NET_WM_PING exwm--protocols)
- ;; For X windows without _NET_WM_PING support, we'd better just
- ;; wait for DestroyNotify events.
- (throw 'return nil))
- ;; Try to determine if the X window is dead with _NET_WM_PING.
- (setq exwm-manage--ping-lock t)
- (xcb:+request exwm--connection
- (make-instance 'xcb:SendEvent
- :propagate 0
- :destination id
- :event-mask xcb:EventMask:NoEvent
- :event (xcb:marshal
- (make-instance 'xcb:ewmh:_NET_WM_PING
- :window id
- :timestamp 0
- :client-window id)
- exwm--connection)))
- (xcb:flush exwm--connection)
- (with-timeout (exwm-manage-ping-timeout
- (if (y-or-n-p (format "'%s' is not responding. \
-Would you like to kill it? "
- (buffer-name)))
- (progn (exwm-manage--kill-client id)
- ;; Kill the unresponsive X window and
- ;; wait for DestroyNotify event.
- (throw 'return nil))
- ;; Give up.
- (throw 'return nil)))
- (while (and exwm-manage--ping-lock
- (exwm--id->buffer id)) ;may have been destroyed.
- (accept-process-output nil 0.1))
- ;; Give up.
- (throw 'return nil)))))
+ ;; Try to close the X window with WM_DELETE_WINDOW client message.
+ (xcb:+request exwm--connection
+ (make-instance 'xcb:icccm:SendEvent
+ :destination exwm--id
+ :event (xcb:marshal
+ (make-instance 'xcb:icccm:WM_DELETE_WINDOW
+ :window exwm--id)
+ exwm--connection)))
+ (xcb:flush exwm--connection)
+ ;; PING the window (if the PING protocol is supported) and
+ ;; schedule a future deletion in case the application doesn't
+ ;; respond. If the window doesn't support the PING protocol,
+ ;; there's nothing we can do here. The application may actually be
+ ;; responsive, it may just be refusing to close because it's
+ ;; asking the user to, e.g., save a document.
+ (when (memq xcb:Atom:_NET_WM_PING exwm--protocols)
+ (let ((last-ping exwm--ping))
+ (exwm-manage--ping)
+ (run-with-timer exwm-manage-ping-timeout nil
+ #'exwm-manage--kill-buffer-timeout-function
+ last-ping (current-buffer))))
+ ;; At this point, we don't close the buffer but instead wait
+ ;; for the window to close itself. Once that happens, we'll
+ ;; receive an event and kill the buffer.
+ nil))
(defun exwm-manage--kill-client (&optional id)
- "Kill X client ID.
-If ID is nil, kill X window corresponding to current buffer."
+ "Kill the X client associated with the window ID.
+If ID is nil, kill the X client associated with the current buffer.
+
+NOTE: This command is the equivalent of the xkill program. If you just
+want to close a window, delete the associated buffer."
(unless id (setq id (exwm--buffer->id (current-buffer))))
(exwm--log "id=#x%x" id)
- (let* ((response (xcb:+request-unchecked+reply exwm--connection
- (make-instance 'xcb:ewmh:get-_NET_WM_PID :window id)))
- (pid (and response (slot-value response 'value)))
- (request (make-instance 'xcb:KillClient :resource id)))
- (if (not pid)
- (xcb:+request exwm--connection request)
- ;; What if the PID is fake/wrong?
- (signal-process pid 'SIGKILL)
- ;; Ensure it's dead
- (run-with-timer exwm-manage-ping-timeout nil
- (lambda ()
- (xcb:+request exwm--connection request))))
- (xcb:flush exwm--connection)))
+ (xcb:+request exwm--connection (make-instance 'xcb:KillClient :resource id))
+ (xcb:flush exwm--connection))
(defun exwm-manage--add-frame (frame)
"Run in `after-make-frame-functions'.
diff --git a/exwm.el b/exwm.el
index 8eab306fee..2eb932add7 100644
--- a/exwm.el
+++ b/exwm.el
@@ -754,11 +754,14 @@ DATA contains unmarshalled PropertyNotify event data."
:window id :data (vconcat props-new)))
(xcb:flush exwm--connection)))))
-(defun exwm--on-wm-protocols (_id data)
- "Handle WM_PROTOCOLS message with DATA."
- (let ((type (elt data 0)))
+(defun exwm--on-wm-protocols (id data)
+ "Handle WM_PROTOCOLS message with DATA to window ID."
+ (let ((type (elt data 0))
+ (client (elt data 2)))
(cond ((= type xcb:Atom:_NET_WM_PING)
- (setq exwm-manage--ping-lock nil))
+ (when-let* (((eq id exwm--root))
+ (buf (exwm--id->buffer client)))
+ (cl-incf (buffer-local-value 'exwm--ping buf))))
(t (exwm--log "Unhandled WM_PROTOCOLS of type: %d" type)))))
(defun exwm--on-wm-change-state (id data)