unofficial mirror of bug-gnu-emacs@gnu.org 
 help / color / mirror / code / Atom feed
From: "J.P." <jp@neverwas.me>
To: Thibaut Meyer <thibaut@thibaut.dev>
Cc: emacs-erc@gnu.org, 70840@debbugs.gnu.org
Subject: bug#70840: 29.3; ERC 5.5.0.29.1: erc-kill-buffer-on-part kills server buffer
Date: Wed, 15 May 2024 06:22:54 -0700	[thread overview]
Message-ID: <87seyj6wgh.fsf__27960.7857389905$1715779469$gmane$org@neverwas.me> (raw)
In-Reply-To: <8734ql9ppd.fsf@neverwas.me> (J. P.'s message of "Mon, 13 May 2024 17:55:58 -0700")

[-- Attachment #1: Type: text/plain, Size: 1699 bytes --]

"J.P." <jp@neverwas.me> writes:

> Such an addition would require us checking how `erc-kill-channel-hook'
> is used in erc-log.el and erc-status-sidebar.el, etc., and confirming
> that running `erc-part-hook' in this fashion (within another hook) won't
> cause any problems.

I've checked `erc-log', and it indeed requires a tweak in order to
prevent a double save. The use case in `erc-status-sidebar' is that the
module wants `erc-status-sidebar-refresh' to run when parting. Luckily,
it doesn't care about the current buffer, so I'm somewhat confident that
it's safe to run whenever the user kills a live channel buffer.

> It's probably also worth doing the same with popular third-party
> packages and configs.

I've found a couple packages that operate in the affected areas. The
first one, sauron, uses `erc-server-PART-functions' instead of
`erc-part-hook', so that should be safe. The second, ZNC.el, adds advice
around `erc-kill-channel', inhibiting it when its own option is non-nil.
This too should be fine, since it also conditionally suppresses our
running `erc-part-hook'.

> There may well be other issues here I've not yet considered, so if you
> can think of any, please share. Also, if you're up for taking this on
> (not necessarily in the way I've described), please feel free to send a
> patch. The same goes for anyone else out there. I can definitely help
> with the leg work and test coverage if that makes things easier.
> Otherwise, I may not be able to get to this in time for the next release
> (not that there's any rush).

I've gone ahead with a provisional patch (attached) that incorporates
the points raised so far. Anyone interested, please take a look.

Thanks.


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: 0001-5.6-Don-t-kill-server-buffer-with-erc-kill-buffer-on.patch --]
[-- Type: text/x-patch, Size: 13569 bytes --]

From 98d16ec15baeb4803014033de6b1c1d3a488ad87 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Tue, 14 May 2024 21:09:55 -0700
Subject: [PATCH] [5.6] Don't kill server buffer with erc-kill-buffer-on-part

* etc/ERC-NEWS: Mention running `erc-part-hook' on
`erc-kill-channel-hook'.
* lisp/erc/erc-backend.el (erc-server-PART): When buffer doesn't
exist, only run hook when `erc--run-part-hook-with-nil-buffer-p' is
non-nil.  Bind that same variable to t when killing the buffer.
* lisp/erc/erc-log.el (erc-conditional-save-buffer): Don't save logs
when killing the channel buffer because this module already subscribes
to `erc-kill-channel-hook'.
* lisp/erc/erc.el (erc-part-hook): Redo doc string.
(erc--run-part-hook-with-nil-buffer-p): New variable, an escape hatch
to regain pre ERC 5.6 behavior regarding nil buffer arguments passed
to `erc-part-hook' members.
(erc-kill-channel-hook): Use new name for `erc-kill-channel'.
(erc--inhibit-part-channel-on-kill-p): New variable.
(erc-kill-channel, erc-part-channel-on-kill): Rename former to latter.
Run `erc-part-hook' when compat switch is nil.
* test/lisp/erc/erc-scenarios-base-kill-on-part.el: New file.
---
 etc/ERC-NEWS                                  |  10 ++
 lisp/erc/erc-backend.el                       |  13 +-
 lisp/erc/erc-log.el                           |   5 +-
 lisp/erc/erc.el                               |  36 +++--
 .../erc/erc-scenarios-base-kill-on-part.el    | 132 ++++++++++++++++++
 5 files changed, 183 insertions(+), 13 deletions(-)
 create mode 100644 test/lisp/erc/erc-scenarios-base-kill-on-part.el

diff --git a/etc/ERC-NEWS b/etc/ERC-NEWS
index b66ea6a7a02..9d5a26595be 100644
--- a/etc/ERC-NEWS
+++ b/etc/ERC-NEWS
@@ -648,6 +648,16 @@ release lacks a similar solution for detecting "joinedness" directly,
 but users can turn to 'xor'-ing 'erc-default-target' and 'erc-target'
 as a makeshift kludge.
 
+*** The function 'erc-kill-channel' may run the hook 'erc-part-hook'.
+Previously, when a user killed a channel buffer, 'erc-part-hook' would
+run in the response handler upon receipt of the corresponding 'PART'
+response, meaning hook members would receive a nil channel-buffer
+argument.  ERC now runs this hook immediately when a user kills a
+channel, just before sending the 'PART' command to the server.  Those
+disrupted by this change should see the definition of 'erc-part-hook'
+for a temporary escape hatch.  Also, for clarity, the function
+'erc-kill-channel' has been renamed 'erc-part-channel-on-kill'.
+
 *** Channel-mode handling has become stricter and more predictable.
 ERC has always processed channel modes using "standardized" letters
 and popular status prefixes.  Starting with this release, ERC will
diff --git a/lisp/erc/erc-backend.el b/lisp/erc/erc-backend.el
index ab419d2b018..7e8e597da46 100644
--- a/lisp/erc/erc-backend.el
+++ b/lisp/erc/erc-backend.el
@@ -1883,12 +1883,17 @@ erc--server-determine-join-display-context
          (buffer (erc-get-buffer chnl proc)))
     (pcase-let ((`(,nick ,login ,host)
                  (erc-parse-user (erc-response.sender parsed))))
+      ;; When `buffer' is nil, `erc-remove-channel-member',
+      ;; `erc-remove-channel-users', and `erc-part-hook', don't run or
+      ;; do nothing, and the message is printed in the server buffer.
       (erc-remove-channel-member buffer nick)
       (erc-display-message parsed 'notice buffer
                            'PART ?n nick ?u login
                            ?h host ?c chnl ?r (or reason ""))
       (when (string= nick (erc-current-nick))
-        (run-hook-with-args 'erc-part-hook buffer)
+        (defvar erc--run-part-hook-with-nil-buffer-p)
+        (when (or buffer erc--run-part-hook-with-nil-buffer-p)
+          (run-hook-with-args 'erc-part-hook buffer))
         (erc-with-buffer
             (buffer)
           (erc-remove-channel-users))
@@ -1896,8 +1901,10 @@ erc--server-determine-join-display-context
           (erc-delete-default-channel chnl buffer))
         (erc-update-mode-line buffer)
         (defvar erc-kill-buffer-on-part)
-        (when erc-kill-buffer-on-part
-          (kill-buffer buffer))))))
+        (when (and erc-kill-buffer-on-part buffer)
+          (defvar erc--inhibit-part-channel-on-kill-p)
+          (let ((erc--inhibit-part-channel-on-kill-p t))
+            (kill-buffer buffer)))))))
 
 (define-erc-response-handler (PING)
   "Handle ping messages." nil
diff --git a/lisp/erc/erc-log.el b/lisp/erc/erc-log.el
index d5c56bcc2b3..f0bd1b45e7c 100644
--- a/lisp/erc/erc-log.el
+++ b/lisp/erc/erc-log.el
@@ -290,7 +290,10 @@ erc-save-query-buffers
 
 (defun erc-conditional-save-buffer (buffer)
   "Save Query BUFFER if `erc-save-queries-on-quit' is t."
-  (when erc-save-buffer-on-part
+  (when (and erc-save-buffer-on-part
+             ;; Don't run this from `erc-part-channel-on-kill' because
+             ;; `erc-save-buffer-in-logs' aready has it covered.
+             (not erc--inhibit-part-channel-on-kill-p))
     (erc-save-buffer-in-logs buffer)))
 
 (defun erc-conditional-save-queries (process)
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index c92fd42322a..a1c94288e8d 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -470,13 +470,23 @@ erc-quit-hook
 
 (defcustom erc-part-hook nil
   "Hook run when processing a PART message directed at our nick.
-
-The hook receives one argument, the current BUFFER.
-See also `erc-server-QUIT-functions', `erc-quit-hook' and
-`erc-disconnected-hook'."
+Called in the server buffer with one argument: the BUFFER being parted.
+Also runs via `erc-part-channel-on-kill' on `erc-kill-channel-hook' if
+killing BUFFER initiated the \"PART\".  Note that beginning with ERC
+5.6, this hook doesn't run upon receiving a \"PART\" from the server if
+BUFFER has already been killed.  See code near this option's definition
+for a possible escape hatch."
   :group 'erc-hooks
   :type 'hook)
 
+(defvar erc--run-part-hook-with-nil-buffer-p nil
+  "When non-nil, allow running `erc-part-hook' with a nil buffer argument.
+Moreover, forgo running the hook before sending a \"PART\" when the user
+kills a channel buffer.  This compat-oriented escape hatch exists for
+third-party code that expects to run unconditionally in the default
+response handler `erc-server-PART' when ERC receives a self-directed
+\"PART\" from the server.")
+
 (defcustom erc-kick-hook nil
   "Hook run when processing a KICK message directed at our nick.
 
@@ -9598,7 +9608,7 @@ erc-kill-server-hook
   :type 'hook)
 
 (defcustom erc-kill-channel-hook
-  '(erc-kill-channel
+  '(erc-part-channel-on-kill
     erc-networks-shrink-ids-and-buffer-names
     erc-networks-rename-surviving-target-buffer)
   "Invoked whenever a channel-buffer is killed via `kill-buffer'."
@@ -9659,10 +9669,18 @@ erc-kill-server
     (setq erc-server-quitting t)
     (erc-server-send (format "QUIT :%s" (funcall erc-quit-reason nil)))))
 
-(defun erc-kill-channel ()
-  "Sends a PART command to the server when the channel buffer is killed.
-This function should be on `erc-kill-channel-hook'."
-  (when (erc-server-process-alive)
+(defvar erc--inhibit-part-channel-on-kill-p nil)
+(define-obsolete-function-alias 'erc-kill-channel #'erc-part-channel-on-kill
+  "30.1")
+(defun erc-part-channel-on-kill ()
+  "Run `erc-part-hook' and send a \"PART\" when killing a channel buffer."
+  (when (and (erc-server-process-alive)
+             (not erc--inhibit-part-channel-on-kill-p))
+    (unless erc--run-part-hook-with-nil-buffer-p
+      (let ((buffer (current-buffer))
+            (erc--inhibit-part-channel-on-kill-p t))
+        (erc-with-server-buffer
+          (run-hook-with-args 'erc-part-hook buffer))))
     (let ((tgt (erc-default-target)))
       (if tgt
          (erc-server-send (format "PART %s :%s" tgt
diff --git a/test/lisp/erc/erc-scenarios-base-kill-on-part.el b/test/lisp/erc/erc-scenarios-base-kill-on-part.el
new file mode 100644
index 00000000000..109cf27b01a
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-base-kill-on-part.el
@@ -0,0 +1,132 @@
+;;; erc-scenarios-base-kill-on-part.el --- killing buffers on part -*- lexical-binding: t -*-
+
+;; Copyright (C) 2024 Free Software Foundation, Inc.
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; GNU Emacs is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Code:
+
+(require 'ert-x)
+(eval-and-compile
+  (let ((load-path (cons (ert-resource-directory) load-path)))
+    (require 'erc-scenarios-common)))
+
+;; Assert channel buffer is killed when `erc-kill-buffer-on-part' is
+;; enabled and a user issues a /part.
+(ert-deftest erc-scenarios-base-kill-on-part--enabled ()
+  :tags '(:expensive-test)
+  (should-not erc-kill-buffer-on-part)
+
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/reuse-buffers/channel")
+       (erc-server-flood-penalty 0.1)
+       (dumb-server (erc-d-run "localhost" t 'foonet))
+       (port (process-contact dumb-server :service))
+       (erc-kill-buffer-on-part t)
+       (calls nil)
+       (erc-part-hook (lambda (b) (push (buffer-name b) calls)))
+       (expect (erc-d-t-make-expecter)))
+
+    (ert-info ("Connect to foonet")
+      (with-current-buffer (erc :server "127.0.0.1"
+                                :port port
+                                :nick "tester"
+                                :password "foonet:changeme"
+                                :full-name "tester")
+        (funcall expect 10 "This server is in debug mode")))
+
+    (with-current-buffer (erc-d-t-wait-for 20 (get-buffer "#chan"))
+      (funcall expect 10 "<alice> bob: Whilst I can shake")
+      (erc-scenarios-common-say "/part"))
+
+    (erc-d-t-wait-for 20 (null (get-buffer "#chan")))
+    (should (equal calls '("#chan")))))
+
+;; When `erc-kill-buffer-on-part' is non-nil, and the parted buffer has
+;; already been killed, don't kill the server buffer.  Bug#70840
+(ert-deftest erc-scenarios-base-kill-on-part--enabled/killed ()
+  :tags '(:expensive-test)
+  (should-not erc-kill-buffer-on-part)
+
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/reuse-buffers/channel")
+       (erc-server-flood-penalty 0.1)
+       (dumb-server (erc-d-run "localhost" t 'foonet))
+       (port (process-contact dumb-server :service))
+       (erc-kill-buffer-on-part t)
+       (killingp nil)
+       (calls nil)
+       (erc-part-hook (lambda (b) (push (cons (buffer-name b) killingp)
+                                        calls)))
+       (expect (erc-d-t-make-expecter)))
+
+    (ert-info ("Connect to foonet")
+      (with-current-buffer (erc :server "127.0.0.1"
+                                :port port
+                                :nick "tester"
+                                :password "foonet:changeme"
+                                :full-name "tester")
+        (funcall expect 10 "This server is in debug mode")))
+
+    (with-current-buffer (erc-d-t-wait-for 20 (get-buffer "#chan"))
+      (funcall expect 10 "<alice> bob: Whilst I can shake")
+      ;; There's a race here because the response handler may run
+      ;; before `killingp' is nil, but at least false negatives aren't
+      ;; possible.  Shadowing `erc-server-send-queue' temporarily
+      ;; would fix it, but that's too leaky for this style of test.
+      (setq killingp 'kill)
+      (kill-buffer)
+      (setq killingp nil))
+
+    (erc-d-t-wait-for 20 (null (get-buffer "#chan")))
+    (should (equal calls '(("#chan" . kill))))
+    (erc-d-t-ensure-for 0.1 (get-buffer "foonet"))))
+
+(ert-deftest erc-scenarios-base-kill-on-part--enabled/killed/compat ()
+  :tags '(:expensive-test)
+  (should-not erc-kill-buffer-on-part)
+
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/reuse-buffers/channel")
+       (erc-server-flood-penalty 0.1)
+       (dumb-server (erc-d-run "localhost" t 'foonet))
+       (port (process-contact dumb-server :service))
+       (erc-kill-buffer-on-part t)
+       (erc--run-part-hook-with-nil-buffer-p t) ; option under test
+       (killingp nil)
+       (calls nil)
+       (erc-part-hook (lambda (b) (push (cons b killingp) calls)))
+       (expect (erc-d-t-make-expecter)))
+
+    (ert-info ("Connect to foonet")
+      (with-current-buffer (erc :server "127.0.0.1"
+                                :port port
+                                :nick "tester"
+                                :password "foonet:changeme"
+                                :full-name "tester")
+        (funcall expect 10 "This server is in debug mode")))
+
+    (with-current-buffer (erc-d-t-wait-for 20 (get-buffer "#chan"))
+      (funcall expect 10 "<alice> bob: Whilst I can shake")
+      (setq killingp 'kill)
+      (kill-buffer)
+      (setq killingp nil))
+
+    (erc-d-t-wait-for 20 (null (get-buffer "#chan")))
+    (erc-d-t-wait-for 20 (equal calls '((nil . nil))))
+    (should (get-buffer "foonet"))))
+
+;;; erc-scenarios-base-kill-on-part.el ends here
-- 
2.44.0


  parent reply	other threads:[~2024-05-15 13:22 UTC|newest]

Thread overview: 5+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2024-05-08 20:37 bug#70840: 29.3; ERC 5.5.0.29.1: erc-kill-buffer-on-part kills server buffer Thibaut Meyer via Bug reports for GNU Emacs, the Swiss army knife of text editors
2024-05-14  0:55 ` J.P.
     [not found] ` <8734ql9ppd.fsf@neverwas.me>
2024-05-15 13:22   ` J.P. [this message]
     [not found]   ` <87seyj6wgh.fsf@neverwas.me>
2024-05-17  3:57     ` J.P.
     [not found]     ` <87wmnt5bun.fsf@neverwas.me>
2024-05-28 13:36       ` J.P.

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

  List information: https://www.gnu.org/software/emacs/

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to='87seyj6wgh.fsf__27960.7857389905$1715779469$gmane$org@neverwas.me' \
    --to=jp@neverwas.me \
    --cc=70840@debbugs.gnu.org \
    --cc=emacs-erc@gnu.org \
    --cc=thibaut@thibaut.dev \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
Code repositories for project(s) associated with this public inbox

	https://git.savannah.gnu.org/cgit/emacs.git

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).