#!/bin/bash # Copyright (C) 2022 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 # the Free Software Foundation, either version 3 of the License, or (at # your option) any later version. # # This program 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 this program. If not, see . # emacsclient(1) wrapper to handle updating the environment of existing # daemons and choosing between installed and in-tree builds of Emacs. min_arg=0 may_start=true want_update=false want_installed=false want_eval=false want_version=false devel_running=false installed_running=false pass_to_gdbmacs=false status= daemon_name= gdbmacs= args= xdg="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}" socket_dir="$xdg/emacs/" locks_dir="$xdg/spw_emacsclient/" get_listener () { local socket="$1" local listener=$(ss -Hplx src "$socket" \ | perl -wne'/pid=(\d+)/ and print $1') if [ -z "$listener" -a -e "$socket" ]; then # Nothing is listening: remove dangling socket. rm "$socket" else echo "$listener" fi } maybe_notify () { echo >&2 "$1" [ -n "$XDG_RUNTIME_DIR" ] \ && notify-send --urgency=low --expire-time=10000 "$1" } spw_flock () { mkdir -p "$locks_dir" eval "exec $1<>${locks_dir}$2" if ! flock --wait 15 "$1"; then maybe_notify "couldn't lock starting Emacs daemon named $2" exit 1 fi } die_wedged () { maybe_notify "in-tree Emacs appears to be wedged" # Only exit non-zero if we were expected to start the daemon. ! $may_start; exit } egrep_negate () { echo -n "^([^${1:0:1}]" for (( i=1; i < ${#1}; i++ )); do echo -n "|(${1:0:$i}([^${1:$i:1}]|$))" done echo -n "|($1.+))" } for arg do shift case "$arg" in '--spw/installed') want_installed=true ;; '--spw/no-start') may_start=false ;; '--spw/update-environment') want_update=true ;; '-V'|'--version') want_version=true ;;& '-e'|'--eval') want_eval=true ;;& '-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 devel_emacs="$HOME/src/emacs/src/emacs" devel_emacsclient="$HOME/src/emacs/lib-src/emacsclient" installed_emacs=$(command -v emacs) installed_emacsclient=$(PATH=$(echo "$PATH" \ | sed -e "s#$HOME/src/dotfiles/bin:##") \ command -v emacsclient) command -v ss >/dev/null || exec "$installed_emacsclient" -a "" "$@" $want_version || spw_flock 3 "${daemon_name:-server}" socket="$socket_dir${daemon_name:-server}" listener="$(get_listener $socket)" if [ -n "$listener" ]; then case "$(ps h -o exe $listener | sed -e 's/ (deleted)$//')" in "$devel_emacs") devel_running=true ;; "$(realpath $installed_emacs)") installed_running=true ;; *) echo >&2 "Unknown Emacs listening on $socket"; exit 1 ;; esac fi # ---- Special case for primary session that's always under gdb if [ -z "$daemon_name" ] && $devel_running && $want_installed; then inotifywait -qq -e delete "$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)}" ] \ && "$installed_emacsclient" -sgdbmacs \ --eval '(gdb-wait-for-pending (lambda () (gud-basic-call "detach") (gdb-wait-for-pending (lambda () (gud-basic-call "quit")))))' wait $inotifywait devel_running=false fi # ---- End special case for primary session that's always under gdb [ -z "$listener" ] && ! $may_start && exit 0 # We check whether there is something which looks like the installed Emacs # running because we do not want the in-tree emacsclient to try to connect to # that, and we check whether there is a build in progress and no running # daemon, as we don't want to start a new daemon until the build is finished. # The idea is that I can always hit C-i e without worrying about builds. if ! $want_installed && ! $installed_running \ && [ -e "$HOME/src/emacs/admin/flagfile.melete" \ -a -x "$HOME/src/emacs/lib-src/emacsclient" \ -a -d "/usr/share/emacs-snapshot/site-lisp/elpa" ] \ && ! ( ! $devel_running && ps h -o pid -C make \ | xargs pwdx | grep -q "$HOME/src/emacs" ); then emacs="$devel_emacs" emacsclient="$devel_emacsclient" # ---- Two special cases for primary session that's always under gdb if [ -z "$daemon_name" ] && ! $want_version; then spw_flock 4 gdbmacs # If devel Emacs is stopped or gdbmacs can't start it, ask gdbmacs to # handle the request: it's probs. C-i e or editing a file via EDITOR. [ -n "${gdbmacs:=$(get_listener ${socket_dir}gdbmacs)}" ] \ && status=$("$installed_emacsclient" \ -sgdbmacs \ --eval \ '(and (boundp '"'"'gud-comint-buffer) (get-buffer-process gud-comint-buffer) (string= "signal-received" gdb-inferior-status))') if [ "$status" = t ]; then if $want_eval || $want_update; then die_wedged else pass_to_gdbmacs=true fi elif [ -z "$listener" ]; then # inotifywait(1) in Debian "bullseye" doesn't have --include. inotifywait -qq -e create "${socket_dir}" \ --exclude "$(egrep_negate ${socket_dir}server)" & inotifywait=$! if "$installed_emacsclient" \ -a '' -sgdbmacs \ --eval '(spw/gdbmacs-attach)' 3>&- 4>&-; then wait $inotifywait listener=true elif $want_eval || $want_update; then kill $inotifywait die_wedged else kill $inotifywait pass_to_gdbmacs=true fi fi if $pass_to_gdbmacs; then emacs="$installed_emacs" emacsclient="$installed_emacsclient" socket="${socket_dir}gdbmacs" listener="$gdbmacs" set -- "$@" -sgdbmacs fi exec 4>&- fi # ---- End two special cases for primary session that's always under gdb else emacs="$installed_emacs" emacsclient="$installed_emacsclient" fi # Update Emacs daemon environment vars for a new interactive session. This # allows us to have the Emacs daemon survive restarts of the graphical desktop # environment, and to update Emacs on a remote host when there is a new # forwarded SSH agent socket. See 'upenv' in .bash_defns. if $want_update && [ -n "$listener" ]; then for var in DISPLAY WAYLAND_DISPLAY \ XAUTHORITY WINDOWID \ XDG_SESSION_TYPE XDG_CURRENT_DESKTOP \ SWAYSOCK I3SOCK \ SSH_ASKPASS SSH_CONNECTION \ SSH_AUTH_SOCK SSH_AGENT_PID; do eval isset=\${$var+x} if [ "$isset" = "x" ]; then eval val=\$$var args="$args"' "'$var'" "'$val'"' fi done [ -n "$args" ] \ && "$emacsclient" -s"${daemon_name:-server}" \ --eval "(spw/update-environment$args))" fi maybe_hold_lock () { # Hold on to the lock until daemon has started up. $want_version || [ -n "$listener" ] \ || inotifywait -qq -e create "${socket_dir}" \ --exclude "$(egrep_negate $socket)" & } # emacsclient(1) requires an argument. As a special case, if there are no # arguments except -s/--socket-name, and no daemon is running, just start one. # And if there were no arguments but we were requested just to update env # vars, exit peacefully. if [ "$#" -eq "$min_arg" -a -z "$listener" ]; then maybe_hold_lock exec "$emacs" --daemon"${daemon_name:+=$daemon_name}" 3>&- elif ! ( [ "$#" -eq "$min_arg" ] && $want_update ); then maybe_hold_lock exec "$emacsclient" -a "" "$@" 3>&- fi