aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorF. Jason Park2025-04-08 23:17:21 -0700
committerF. Jason Park2025-04-16 17:30:13 -0700
commitc0cb59578b5aeb75b4856dda518d80cd015caa7d (patch)
tree8173689cf01e13547dfa0cf1c834a0cf2219d26c
parent8f18b398a57f2f1bbef28260740e139b0494927b (diff)
downloademacs-c0cb59578b5aeb75b4856dda518d80cd015caa7d.tar.gz
emacs-c0cb59578b5aeb75b4856dda518d80cd015caa7d.zip
Don't round-trip auto-reconnect probe in ERC
* lisp/erc/erc-backend.el (erc-server--reconnect-opened) (erc--server-reconnect-opened): Rename former to latter. Restore original buffer-local value of session connector for Emacs 29 and below. (erc--server-reconnect-timeout-check) (erc--server-reconnect-timeout-scale-function): Change from buffer-local to normal variables, which they should have been originally. (erc--recon-probe-reschedule): Ensure `erc-server-reconnect-timeout' is always non-nil to avoid seeing format specifier in admin message. Use current buffer when `proc' argument is nil. Perform cleanup when `proc' and `erc-server-process' differ. (erc-server-delayed-check-reconnect-reuse-process-p): New variable. (erc--recon-probe-sentinel): Run `erc--server-reconnect-opened' immediately because sending a speculative PING doesn't work on all servers and proxies, most crucially on ZNC, which replies with an error only after an extended timeout. (erc--recon-probe-filter): Remove unused function. (erc--recon-probe-check) Rework to not use fixed periodic timer, change second parameter to a Lisp time object. (erc-server-delayed-check-reconnect): Use realistic name when reusing process so that the session's process isn't "*erc-connectivity-check*". Set filter to `ignore'. Always run `erc--recon-probe-sentinel' when status is `open' or something other than `connect', but don't bother spawning a `erc--recon-probe-check' task as well because any problems creating the process should already be known. Handle quits during connect functions that perform blocking I/O, such as `socks-open-network-stream'. (erc-schedule-reconnect): Don't bother setting filter to nil. * test/lisp/erc/erc-scenarios-base-auto-recon.el (erc-scenarios-base-auto-recon-unavailable) (erc-scenarios-base-auto-recon-check/no-reuse): Rename former to latter. (erc-scenarios-base-auto-recon-no-proto) (erc-scenarios-base-auto-recon-check/reuse): Rename former to latter and rewrite not to expect a PING. * test/lisp/erc/resources/erc-d/erc-d.el (erc-d--forget-process): New function. (erc-d--process-sentinel): Stop serving when all dialogs have been exhausted. (Bug#62044)
-rw-r--r--lisp/erc/erc-backend.el153
-rw-r--r--test/lisp/erc/erc-scenarios-base-auto-recon.el19
-rw-r--r--test/lisp/erc/resources/erc-d/erc-d.el15
3 files changed, 90 insertions, 97 deletions
diff --git a/lisp/erc/erc-backend.el b/lisp/erc/erc-backend.el
index e9b39a6f3f4..81907ffa462 100644
--- a/lisp/erc/erc-backend.el
+++ b/lisp/erc/erc-backend.el
@@ -832,17 +832,23 @@ Make sure you are in an ERC buffer when running this."
832 (with-current-buffer buffer 832 (with-current-buffer buffer
833 (erc-server-reconnect)))) 833 (erc-server-reconnect))))
834 834
835(defun erc-server--reconnect-opened (buffer process) 835(defun erc--server-reconnect-opened (buffer process)
836 "Reconnect session for server BUFFER using open PROCESS." 836 "Reconnect session for server BUFFER using open PROCESS."
837 (when (buffer-live-p buffer) 837 (when (buffer-live-p buffer)
838 (with-current-buffer buffer 838 (with-current-buffer buffer
839 (let ((erc-session-connector (lambda (&rest _) process))) 839 (let* ((orig erc-session-connector)
840 (erc-session-connector
841 (lambda (&rest _)
842 (setq erc-session-connector orig)
843 process)))
840 (erc-server-reconnect))))) 844 (erc-server-reconnect)))))
841 845
842(defvar-local erc--server-reconnect-timeout nil) 846(defvar-local erc--server-reconnect-timeout nil)
843(defvar-local erc--server-reconnect-timeout-check 10) 847
844(defvar-local erc--server-reconnect-timeout-scale-function 848;; These variables exist for use in unit tests.
845 #'erc--server-reconnect-timeout-double) 849(defvar erc--server-reconnect-timeout-check 10)
850(defvar erc--server-reconnect-timeout-scale-function
851 #'erc--server-reconnect-timeout-double)
846 852
847(defun erc--server-reconnect-timeout-double (existing) 853(defun erc--server-reconnect-timeout-double (existing)
848 "Double EXISTING timeout, but cap it at 5 minutes." 854 "Double EXISTING timeout, but cap it at 5 minutes."
@@ -851,84 +857,57 @@ Make sure you are in an ERC buffer when running this."
851(defun erc--recon-probe-reschedule (proc) 857(defun erc--recon-probe-reschedule (proc)
852 "Print a message saying PROC's intended peer can't be reached. 858 "Print a message saying PROC's intended peer can't be reached.
853Then call `erc-schedule-reconnect'." 859Then call `erc-schedule-reconnect'."
854 (let ((buffer (process-buffer proc))) 860 (let ((buffer (or (and-let* ((proc)
855 (when (buffer-live-p buffer) 861 (buffer (process-buffer proc))
856 (with-current-buffer buffer 862 ((buffer-live-p buffer))
857 (let ((erc-server-reconnect-timeout erc--server-reconnect-timeout)) 863 (buffer)))
858 ;; FIXME either remove this deletion or explain why the one 864 (current-buffer))))
859 ;; performed by `erc-schedule-reconnect' is insufficient. 865 (with-current-buffer buffer
860 ;; Perhaps because `proc' may not equal `erc-server-process'? 866 (let ((erc-server-reconnect-timeout
861 (when proc ; conn refused w/o :nowait 867 (or erc--server-reconnect-timeout
862 (delete-process proc)) 868 erc-server-reconnect-timeout)))
863 (erc-display-message nil '(notice error) buffer 869 (when (and proc (not (eq proc erc-server-process)))
864 'recon-probe-nobody-home) 870 (set-process-sentinel proc #'ignore)
865 (erc-schedule-reconnect buffer 0)))))) 871 (delete-process proc))
872 (erc-display-message nil '(notice error) buffer
873 'recon-probe-nobody-home)
874 (erc-schedule-reconnect buffer 0)))))
875
876(defvar erc-server-delayed-check-reconnect-reuse-process-p t
877 "Whether to reuse a successful probe as the session process.")
866 878
867(defun erc--recon-probe-sentinel (proc event) 879(defun erc--recon-probe-sentinel (proc event)
868 "Send a \"PING\" to PROC's peer on an \"open\" EVENT. 880 "Send a \"PING\" to PROC's peer on an \"open\" EVENT.
869Otherwise, try connecting from scratch again after timeout." 881Otherwise, try connecting from scratch again after timeout."
870 (pcase event 882 (pcase event
871 ("open\n" 883 ("open\n"
872 (let ((cookie (time-convert nil 'integer))) 884 (set-process-sentinel proc #'ignore)
873 (process-put proc 'erc--reconnect-cookie cookie) 885 ;; This has been observed to possibly raise a `file-error'.
874 ;; FIXME account for possible `file-error' when sending. 886 (if erc-server-delayed-check-reconnect-reuse-process-p
875 (run-at-time nil nil #'process-send-string proc 887 (run-at-time nil nil #'erc--server-reconnect-opened
876 (format "PING %d\r\n" cookie)))) 888 (process-buffer proc) proc)
877 ((and "connection broken by remote peer\n" 889 (run-at-time nil nil #'delete-process proc)
878 (guard (process-get proc 'erc--reconnect-cookie)) 890 (run-at-time nil nil #'erc-server-delayed-reconnect
879 (let buffer (process-buffer proc)) 891 (process-buffer proc))))
880 (guard (buffer-live-p buffer)))
881 ;; This can run, for example, if the client dials a TLS-terminating
882 ;; endpoint with a non-TLS opener, like `erc-open-tls-stream', or
883 ;; if the server doesn't take kindly to an opening "PING" during
884 ;; connection registration.
885 (with-current-buffer buffer
886 (delete-process proc)
887 ;; Undo latest penalizing timeout increment.
888 (setq erc--server-reconnect-timeout
889 (max 1 (/ erc--server-reconnect-timeout 2)))
890 (erc-display-message nil '(notice error) buffer 'recon-probe-hung-up
891 ?t erc--server-reconnect-timeout)
892 (run-at-time erc--server-reconnect-timeout
893 nil #'erc-server-delayed-reconnect buffer)))
894 ((or "connection broken by remote peer\n" (rx bot "failed")) 892 ((or "connection broken by remote peer\n" (rx bot "failed"))
895 (run-at-time nil nil #'erc--recon-probe-reschedule proc)))) 893 (run-at-time nil nil #'erc--recon-probe-reschedule proc))))
896 894
897(defun erc--recon-probe-filter (proc string) 895(defun erc--recon-probe-check (proc expire)
898 "Reconnect, reusing PROC if STRING contains a \"PONG\"." 896 "Restart reconnect probe if PROC has failed or EXPIRE time has passed.
899 (when-let* ((buffer (process-buffer proc)) 897Otherwise, if PROC's buffer is live and its status is `connect', arrange
900 (buffer-live-p buffer)) 898for running again in 1 second."
901 (with-current-buffer buffer 899 (let* ((buffer (process-buffer proc))
902 (setq erc--server-reconnect-timeout nil)) 900 ;;
903 (if-let* ; reuse proc if string has complete message 901 status)
904 ((cookie (process-get proc 'erc--reconnect-cookie))
905 ;; Accommodate a leading ":<source> ".
906 ((string-suffix-p (format "PONG %d\r\n" cookie) string)))
907 (progn
908 (erc-log-irc-protocol string nil)
909 (set-process-sentinel proc #'ignore)
910 (set-process-filter proc nil)
911 (run-at-time nil nil #'erc-server--reconnect-opened buffer proc))
912 (delete-process proc)
913 (run-at-time nil nil #'erc-server-delayed-reconnect buffer))))
914
915(defun erc--recon-probe-check (proc tmrx)
916 "Restart auto-reconnect probe if PROC has failed or TIMER has EXPIRE'd.
917Expect TMRX to be a cons cell of (EXPIRE . TIMER)."
918 (let* ((status (process-status proc))
919 (expiredp (time-less-p (pop tmrx) (current-time)))
920 (buffer (process-buffer proc)))
921 (when (or expiredp
922 (not (eq 'connect status)) ; e.g., `closed'
923 (not (buffer-live-p buffer)))
924 (cancel-timer tmrx))
925 (cond ((not (buffer-live-p buffer))) 902 (cond ((not (buffer-live-p buffer)))
926 (expiredp 903 ((time-less-p expire (current-time))
904 ;; TODO convert into proper catalog message for i18n.
927 (erc-display-message nil 'error buffer "Timed out while dialing...") 905 (erc-display-message nil 'error buffer "Timed out while dialing...")
928 (delete-process proc)
929 (erc--recon-probe-reschedule proc)) 906 (erc--recon-probe-reschedule proc))
930 ((eq 'failed status) 907 ((eq (setq status (process-status proc)) 'failed)
931 (erc--recon-probe-reschedule proc))))) 908 (erc--recon-probe-reschedule proc))
909 ((eq status 'connect)
910 (run-at-time 1 nil #'erc--recon-probe-check proc expire)))))
932 911
933;; This probing strategy may appear to hang at various junctures. It's 912;; This probing strategy may appear to hang at various junctures. It's
934;; assumed that when *Messages* contains "Waiting for socket ..." or 913;; assumed that when *Messages* contains "Waiting for socket ..." or
@@ -951,26 +930,31 @@ this function as their reconnector."
951 erc-server-reconnect-timeout))) 930 erc-server-reconnect-timeout)))
952 (condition-case _ 931 (condition-case _
953 (let* ((cert erc-session-client-certificate) 932 (let* ((cert erc-session-client-certificate)
954 (tmrx (list (time-add erc--server-reconnect-timeout-check
955 (current-time))))
956 (server (if (string-match erc--server-connect-dumb-ipv6-regexp 933 (server (if (string-match erc--server-connect-dumb-ipv6-regexp
957 erc-session-server) 934 erc-session-server)
958 (match-string 1 erc-session-server) 935 (match-string 1 erc-session-server)
959 erc-session-server)) 936 erc-session-server))
960 (proc (apply erc-session-connector "*erc-connectivity-check*" 937 (name (if erc-server-delayed-check-reconnect-reuse-process-p
938 (format "erc-%s-%s" server erc-session-port)
939 "*erc-connectivity-check*"))
940 (proc (apply erc-session-connector name
961 nil server erc-session-port 941 nil server erc-session-port
962 (and cert (list :client-certificate cert))))) 942 (and cert (list :client-certificate cert))))
963 (setcdr tmrx (run-at-time 1 1 #'erc--recon-probe-check proc tmrx)) 943 (status (process-status proc)))
964 (set-process-filter proc #'erc--recon-probe-filter)
965 (set-process-sentinel proc #'erc--recon-probe-sentinel)
966 (set-process-buffer proc buffer) 944 (set-process-buffer proc buffer)
967 ;; Should `erc-server-process' also be set to `proc' here so 945 (set-process-filter proc #'ignore)
968 ;; that `erc-schedule-reconnect' can use it? 946 (if (not (eq status 'connect)) ; :nowait is nil
969 (cl-assert (processp proc)) 947 (erc--recon-probe-sentinel proc (if (eq status 'open)
970 (when (eq (process-status proc) 'open) ; :nowait is nil 948 "open\n"
971 (erc--recon-probe-sentinel proc "open\n"))) 949 "failed"))
950 (run-at-time 1 nil #'erc--recon-probe-check proc
951 (time-add erc--server-reconnect-timeout-check
952 (current-time)))
953 (set-process-sentinel proc #'erc--recon-probe-sentinel)))
972 ;; E.g., "make client process failed" "Connection refused". 954 ;; E.g., "make client process failed" "Connection refused".
973 (file-error (erc--recon-probe-reschedule nil)))))) 955 (file-error (erc--recon-probe-reschedule nil))
956 ;; C-g during blocking connect, like with the SOCKS connector.
957 (quit (erc--cancel-auto-reconnect-timer))))))
974 958
975(defun erc-server-prefer-check-reconnect (buffer) 959(defun erc-server-prefer-check-reconnect (buffer)
976 "Defer to another reconnector based on BUFFER's `erc-session-connector'. 960 "Defer to another reconnector based on BUFFER's `erc-session-connector'.
@@ -1085,7 +1069,6 @@ When `erc-server-reconnect-attempts' is a number, increment
1085 ?i (if count erc-server-reconnect-count "N") 1069 ?i (if count erc-server-reconnect-count "N")
1086 ?n (if count erc-server-reconnect-attempts "A")) 1070 ?n (if count erc-server-reconnect-attempts "A"))
1087 (set-process-sentinel proc #'ignore) 1071 (set-process-sentinel proc #'ignore)
1088 (set-process-filter proc nil)
1089 (delete-process proc) 1072 (delete-process proc)
1090 (erc-update-mode-line) 1073 (erc-update-mode-line)
1091 (setq erc-server-reconnecting nil 1074 (setq erc-server-reconnecting nil
diff --git a/test/lisp/erc/erc-scenarios-base-auto-recon.el b/test/lisp/erc/erc-scenarios-base-auto-recon.el
index e08a062ccab..744e2041bce 100644
--- a/test/lisp/erc/erc-scenarios-base-auto-recon.el
+++ b/test/lisp/erc/erc-scenarios-base-auto-recon.el
@@ -39,7 +39,7 @@
39;; This demos one possible flavor of intermittent service. 39;; This demos one possible flavor of intermittent service.
40;; It may end up needing to be marked :unstable. 40;; It may end up needing to be marked :unstable.
41 41
42(ert-deftest erc-scenarios-base-auto-recon-unavailable () 42(ert-deftest erc-scenarios-base-auto-recon-check/no-reuse ()
43 :tags '(:expensive-test) 43 :tags '(:expensive-test)
44 (erc-scenarios-common-with-cleanup 44 (erc-scenarios-common-with-cleanup
45 ((erc-server-flood-penalty 0.1) 45 ((erc-server-flood-penalty 0.1)
@@ -48,6 +48,7 @@
48 (erc-server-auto-reconnect t) 48 (erc-server-auto-reconnect t)
49 (expect (erc-d-t-make-expecter)) 49 (expect (erc-d-t-make-expecter))
50 (erc-scenarios-common-dialog "base/reconnect") 50 (erc-scenarios-common-dialog "base/reconnect")
51 (erc-server-delayed-check-reconnect-reuse-process-p nil)
51 (dumb-server nil)) 52 (dumb-server nil))
52 53
53 (ert-info ("Dialing fails: nobody home") 54 (ert-info ("Dialing fails: nobody home")
@@ -94,14 +95,12 @@
94 95
95;; Here, a listener accepts but doesn't respond to any messages. 96;; Here, a listener accepts but doesn't respond to any messages.
96 97
97(ert-deftest erc-scenarios-base-auto-recon-no-proto () 98(ert-deftest erc-scenarios-base-auto-recon-check/reuse ()
98 :tags '(:expensive-test) 99 :tags '(:expensive-test)
100 (should erc-server-delayed-check-reconnect-reuse-process-p)
99 (erc-scenarios-common-with-cleanup 101 (erc-scenarios-common-with-cleanup
100 ((erc-server-flood-penalty 0.1) 102 ((erc-server-flood-penalty 0.1)
101 (erc-scenarios-common-dialog "base/reconnect") 103 (erc-scenarios-common-dialog "base/reconnect")
102 (erc-d-auto-pong nil)
103 (erc-d-tmpl-vars
104 `((cookie . ,(lambda (a) (funcall a :set (funcall a :match 1))))))
105 (dumb-server (erc-d-run "localhost" t 'unexpected-disconnect)) 104 (dumb-server (erc-d-run "localhost" t 'unexpected-disconnect))
106 (port (process-contact dumb-server :service)) 105 (port (process-contact dumb-server :service))
107 (erc--server-reconnect-timeout-scale-function (lambda (_) 1)) 106 (erc--server-reconnect-timeout-scale-function (lambda (_) 1))
@@ -117,19 +116,19 @@
117 (funcall expect 10 "server is in debug mode") 116 (funcall expect 10 "server is in debug mode")
118 (should (equal (buffer-name) "FooNet")) 117 (should (equal (buffer-name) "FooNet"))
119 (erc-d-t-wait-for 10 erc--server-reconnect-timer) 118 (erc-d-t-wait-for 10 erc--server-reconnect-timer)
120 (delete-process dumb-server)
121 (funcall expect 10 "failed") 119 (funcall expect 10 "failed")
122 120
123 (ert-info ("Reconnect function freezes attempts at 1") 121 (ert-info ("Reconnect function freezes attempts at 1")
124 (funcall expect 10 '(: "reconnecting" (+ nonl) "attempt 1/2")) 122 (funcall expect 10 '(: "reconnecting" (+ nonl) "attempt 1/2"))
125 (funcall expect 10 "nobody home") 123 (funcall expect 10 "Timed out while dialing")
126 (funcall expect 10 "timed out while dialing") 124 (funcall expect 10 "Nobody home")
127 (funcall expect 10 '(: "reconnecting" (+ nonl) "attempt 1/2")) 125 (funcall expect 10 '(: "reconnecting" (+ nonl) "attempt 1/2"))
128 (funcall expect 10 "nobody home")))) 126 (funcall expect 10 "Timed out while dialing")
127 (funcall expect 10 "Nobody home"))))
129 128
130 (ert-info ("Service restored") 129 (ert-info ("Service restored")
130 (delete-process dumb-server)
131 (setq dumb-server (erc-d-run "localhost" port 131 (setq dumb-server (erc-d-run "localhost" port
132 'just-ping
133 'unexpected-disconnect)) 132 'unexpected-disconnect))
134 (with-current-buffer "FooNet" 133 (with-current-buffer "FooNet"
135 (funcall expect 30 "server is in debug mode"))) 134 (funcall expect 30 "server is in debug mode")))
diff --git a/test/lisp/erc/resources/erc-d/erc-d.el b/test/lisp/erc/resources/erc-d/erc-d.el
index 0d777806474..6cbd7525827 100644
--- a/test/lisp/erc/resources/erc-d/erc-d.el
+++ b/test/lisp/erc/resources/erc-d/erc-d.el
@@ -422,10 +422,19 @@ This will start the teardown for DIALOG."
422 (make-erc-d-i-message :command "eof" :unparsed erc-d--eof-sentinel)) 422 (make-erc-d-i-message :command "eof" :unparsed erc-d--eof-sentinel))
423 (run-at-time nil nil #'erc-d-command dialog 'eof)) 423 (run-at-time nil nil #'erc-d-command dialog 'eof))
424 424
425(defun erc-d--forget-process (process)
426 "Set sentinel and filter for PROCESS to `ignore'."
427 (let ((server (process-get process :server)))
428 (set-process-sentinel server #'ignore)
429 (set-process-sentinel process #'ignore)
430 (set-process-filter server #'ignore)
431 (set-process-filter process #'ignore)))
432
425(defun erc-d--process-sentinel (process event) 433(defun erc-d--process-sentinel (process event)
426 "Set up or tear down client-connection PROCESS depending on EVENT." 434 "Set up or tear down client-connection PROCESS depending on EVENT."
427 (erc-d--log-process-event process process event) 435 (erc-d--log-process-event process process event)
428 (if (eq 'open (process-status process)) 436 (if (and (eq 'open (process-status process))
437 (process-get process :dialog-dialogs))
429 (erc-d--initialize-client process) 438 (erc-d--initialize-client process)
430 (let* ((dialog (process-get process :dialog)) 439 (let* ((dialog (process-get process :dialog))
431 (exes (and dialog (erc-d-dialog-exchanges dialog)))) 440 (exes (and dialog (erc-d-dialog-exchanges dialog))))
@@ -435,7 +444,9 @@ This will start the teardown for DIALOG."
435 ;; Ignore disconnecting peer when pattern is DROP 444 ;; Ignore disconnecting peer when pattern is DROP
436 ((and (string-prefix-p "deleted" event) 445 ((and (string-prefix-p "deleted" event)
437 (erc-d--drop-p (ring-ref exes -1)))) 446 (erc-d--drop-p (ring-ref exes -1))))
438 (t (erc-d--teardown))) 447 (t (erc-d--forget-process process)
448 (erc-d--teardown)))
449 (erc-d--forget-process process)
439 (erc-d--teardown))))) 450 (erc-d--teardown)))))
440 451
441(defun erc-d--filter (process string) 452(defun erc-d--filter (process string)