From ac8ac7d171c21793e6192b2424ed374ba86c175a Mon Sep 17 00:00:00 2001 From: Sean Whitton Date: Fri, 6 Jan 2023 11:15:09 -0700 Subject: emacsclient wrapper: introduce more uses of timeouts Also improve handling of exit codes of intermediate gdbmacs requests. Where these use gdb-wait-for-pending, if they don't exit zero, it means we couldn't even submit the request, not just that it couldn't be fulfilled. Also fix inotifywait(1) event type delete => delete_self. --- .config/i3/config | 8 ++-- .config/sway/config | 8 ++-- bin/emacsclient | 110 +++++++++++++++++++++++++++++++++++----------------- 3 files changed, 83 insertions(+), 43 deletions(-) diff --git a/.config/i3/config b/.config/i3/config index b4f68d2f..6f42f06e 100644 --- a/.config/i3/config +++ b/.config/i3/config @@ -59,17 +59,17 @@ mode "C-i-" { # make a fresh Emacs frame -- typically this gets invoked only on # empty workspaces, as otherwise I use commands under `C-x 5` to get # more Emacs frames - bindsym e exec emacsclient -nc, mode "default" + bindsym e exec emacsclient -nc -w8, mode "default" # override the script's usual logic to start a fresh, non-debug daemon - bindsym Shift+e exec emacsclient -nc --spw/installed, mode "default" + bindsym Shift+e exec emacsclient -nc -w8 --spw/installed, mode "default" # special daemon instance for debugging primary instance & Gnus - bindsym Mod1+e exec emacsclient -nc -sgdbmacs, mode "default" + bindsym Mod1+e exec emacsclient -nc -w8 -sgdbmacs, mode "default" # Fresh Emacs frame and get a shell in HOME. Without this it's # C-i e M-& which is too many keys esp. compared to C-i DEL - bindsym Return exec emacsclient -nce \ + bindsym Return exec emacsclient -nc -w8 -e \ "(let ((default-directory (expand-file-name "'"'~/'"'")) \ (display-buffer-overriding-action \ '(display-buffer-same-window \ diff --git a/.config/sway/config b/.config/sway/config index fecfc63a..cde42aed 100644 --- a/.config/sway/config +++ b/.config/sway/config @@ -38,17 +38,17 @@ mode "C-i-" { # make a fresh Emacs frame -- typically this gets invoked only on # empty workspaces, as otherwise I use commands under `C-x 5` to get # more Emacs frames - bindsym e exec emacsclient -nc, mode "default" + bindsym e exec emacsclient -nc -w8, mode "default" # override the script's usual logic to start a fresh, non-debug daemon - bindsym Shift+e exec emacsclient -nc --spw/installed, mode "default" + bindsym Shift+e exec emacsclient -nc -w8 --spw/installed, mode "default" # special daemon instance for debugging primary instance & Gnus - bindsym Mod1+e exec emacsclient -nc -sgdbmacs, mode "default" + bindsym Mod1+e exec emacsclient -nc -w8 -sgdbmacs, mode "default" # Fresh Emacs frame and get a shell in HOME. Without this it's # C-i e M-& which is too many keys esp. compared to C-i DEL - bindsym Return exec emacsclient -nce \ + bindsym Return exec emacsclient -nc -w8 -e \ "(let ((default-directory (expand-file-name "'"'~/'"'")) \ (display-buffer-overriding-action \ '(display-buffer-same-window \ diff --git a/bin/emacsclient b/bin/emacsclient index 16aca4ae..8cb4b75b 100755 --- a/bin/emacsclient +++ b/bin/emacsclient @@ -1,6 +1,6 @@ #!/bin/sh -# Copyright (C) 2022 Sean Whitton +# Copyright (C) 2022-2023 Sean Whitton # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -26,6 +26,7 @@ want_update=false want_installed=false want_eval=false want_version=false +timeout= devel_running=false installed_running=false make= @@ -71,16 +72,23 @@ fail () { maybe_notify "$1"; exit 1 } +wait_inotifywait () { + wait $inotifywait + case $? in + 0|2) return $? ;; + *) fail "inotifywait(1) exited with code $?" ;; + esac +} + spw_flock () { ( umask 077; mkdir -p "$locks_dir" ) eval "exec $1<>${locks_dir}$2" - flock --wait 15 "$1" \ + flock $([ -n "$timeout" ] && printf -- "--wait %s" "$timeout") "$1" \ || fail "couldn't lock starting Emacs daemon named $2" } pass_to_gdbmacs () { if $want_eval || $want_update; then - maybe_notify "in-tree Emacs appears to be wedged" # Only exit non-zero if we were expected to start the daemon. ! $may_start; exit else @@ -104,9 +112,24 @@ for arg do '--spw/update-environment') want_update=true ;; '-V'|'--version') want_version=true ;;& '-e'|'--eval') want_eval=true ;;& + + # We'll use the same timeout for some things we do in this script, + # if the user has supplied a value. + # Otherwise, we wait forever, just like emacsclient(1), for flock(1). + # + # For inotifywait(1) and our own --eval requests, we always supply + # timeouts. This is because these requests can fail for reasons this + # script can't detect, and we don't want to wait forever while holding + # locks on starting up daemons, else no other invocation of this + # script will be able to do anything without manual intervention. + '-w'|'--timeout') timeout="$1" ;;& + '-w'?*) timeout="${arg:2}" ;;& + '--timeout='?*) timeout="${arg:10}" ;;& + '-s'|'--socket-name') min_arg=2; daemon_name="$1" ;;& '-s'?*) min_arg=1; daemon_name="${arg:2}" ;;& '--socket-name='?*) min_arg=1; daemon_name="${arg:14}" ;;& + *) set -- "$@" "$arg" ;; esac done @@ -140,22 +163,25 @@ fi # Make it possible, with primary session, to quickly replace in-tree Emacs # with installed Emacs. See 'C-i E' Sway/i3 binding. if [ -z "$daemon_name" ] && $devel_running && $want_installed; then - inotifywait -qq -e delete "$socket" & + inotifywait -qq --timeout "${timeout:-15}" -e delete_self "$socket" & inotifywait=$! kill "$listener" # Detach gdb so that Emacs can handle the SIGTERM. We also have to quit # gdb so that `spw/gdbmacs-attach' doesn't look at `gdb-inferior-status'. - [ -n "${gdbmacs:=$(get_listener ${socket_dir}gdbmacs)}" ] \ - && ( exec -a emacsclient "$installed_emacsclient" -sgdbmacs \ - --eval '(gdb-wait-for-pending - (lambda () - (gud-basic-call "detach") - (gdb-wait-for-pending - (lambda () - (gud-basic-call "quit")))))' ) + if [ -n "${gdbmacs:=$(get_listener ${socket_dir}gdbmacs)}" ]; then \ + ( exec -a emacsclient "$installed_emacsclient" -sgdbmacs -w2 \ + --eval '(gdb-wait-for-pending + (lambda () + (gud-basic-call "detach") + (gdb-wait-for-pending + (lambda () + (gud-basic-call "quit")))))' ) \ + || fail "gdbmacs detach request failed" + fi - wait $inotifywait + wait_inotifywait \ + || fail "inotifywait(1) timed out waiting for socket deletion" listener= devel_running=false fi @@ -178,14 +204,29 @@ if ! $want_installed && ! $installed_running && [ -x "$devel_emacsclient" ] \ emacsclient=("$devel_emacsclient") if [ -z "$daemon_name" ] && ! $want_version; then spw_flock 4 gdbmacs - [ -n "${gdbmacs:=$(get_listener ${socket_dir}gdbmacs)}" ] \ - && gud_status=$(exec -a emacsclient "$installed_emacsclient" \ - -sgdbmacs \ - --eval \ - '(and (boundp '"'"'gud-comint-buffer) - (get-buffer-process gud-comint-buffer) - (string= "signal-received" - gdb-inferior-status))') + if [ -n "${gdbmacs:=$(get_listener ${socket_dir}gdbmacs)}" ]; then + # If the primary session is known to be stopped, ask gdbmacs to + # handle the request: it's probably C-i e or editing via EDITOR. + # + # Ignore a failure to obtain any information here, because one + # likely cause is that the request times out because gdbmacs is + # busy starting up Gnus. In that case we'd rather just try + # sending the request to the primary session anyway. + # + # Ideally we could know that emacsclient exited non-zero only + # because of a timeout, but currently that can be done only by + # parsing emacsclient's output: see Emacs bug#60592. + gud_status="$(exec -a emacsclient "$installed_emacsclient" \ + -sgdbmacs -w2 --eval \ + '(and (boundp '"'"'gud-comint-buffer) + (get-buffer-process gud-comint-buffer) + (string= "signal-received" + gdb-inferior-status))')" + if [ $? -eq 0 -a "$gud_status" = t ]; then + maybe_notify "in-tree Emacs appears to be wedged" + pass_to_gdbmacs "$@" + fi + fi fi else emacs=(-a emacs "$installed_emacs") @@ -199,23 +240,22 @@ fi # patch the in-tree emacsclient.c to only execute the in-tree Emacs, instead # of handling it here.) -if [ "$gud_status" = t ]; then - # Primary session is stopped. Ask gdbmacs to handle the request: it's - # probably C-i e or editing a file via EDITOR. - pass_to_gdbmacs "$@" -elif [ -z "$listener" ] && ! $want_version; then +if [ -z "$listener" ] && ! $want_version; then if [ -z "$daemon_name" -a "${emacs[0]}" = "$devel_emacs" ]; then # inotifywait(1) in Debian "bullseye" doesn't have --include. - inotifywait -qq -e create "${socket_dir}" \ + inotifywait -qq --timeout "${timeout:-15}" \ + -e create "${socket_dir}" \ --exclude "$(egrep_negate ${socket_dir}server)" & inotifywait=$! - # If gdbmacs fails to start main session, have gdbmacs handle the - # request; again, probably C-i e or editing via EDITOR. - if ( exec -a emacsclient "$installed_emacsclient" -a '' -sgdbmacs \ - --eval '(spw/gdbmacs-attach)' 3>&- 4>&- ); then - wait $inotifywait - else - kill $inotifywait + # If gdbmacs fails to start main session, but we were able to + # successfully submit the start request, have gdbmacs handle the + # actual request too; again, probably C-i e or editing via EDITOR. + ( exec -a emacsclient "$installed_emacsclient" -a '' \ + -sgdbmacs -w2 --eval '(spw/gdbmacs-attach)' 3>&- 4>&- ) \ + || fail "gdbmacs start request failed" + if ! wait_inotifywait; then + maybe_notify \ + "timed out waiting for gdbmacs to start in-tree Emacs" pass_to_gdbmacs "$@" fi else @@ -242,7 +282,7 @@ elif $want_update && ! $want_version; then fi done [ -n "$args" ] \ - && ( exec "${emacsclient[@]}" -s"${daemon_name:-server}" \ + && ( exec "${emacsclient[@]}" -s"${daemon_name:-server}" -w2 \ --eval "(spw/update-environment$args))" ) fi -- cgit v1.2.3