#!/bin/sh # 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 make= branch= gud_status= daemon_name= gdbmacs= args= 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) for cmd in bash flock inotifywait perl pwdx ss; do command -v $cmd >/dev/null || exec "$installed_emacsclient" -a "" "$@" done [ -n "$BASH_VERSION" ] || exec bash "$0" "$@" 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" $was_set && notify-send --urgency=low --expire-time=10000 "$1" } spw_flock () { ( umask 077; 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 } 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 exec -a emacsclient "$installed_emacsclient" -sgdbmacs "$@" 3>&- 4>&- fi } 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 # -sgdbmacs implies --spw/installed, as a special case. [ "$daemon_name" = gdbmacs ] && want_installed=true # We set and export XDG_RUNTIME_DIR such that Emacs doesn't choose # "/tmp/emacs$(id -u)" as the socket dir. if [ -n "$XDG_RUNTIME_DIR" ]; then was_set=true else was_set=false export XDG_RUNTIME_DIR=/run/user/$(id -u) fi socket_dir="$XDG_RUNTIME_DIR/emacs/" locks_dir="$XDG_RUNTIME_DIR/spw_emacsclient/" $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 # 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=$! 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")))))' ) wait $inotifywait listener= devel_running=false fi [ -z "$listener" ] && ! $may_start && ! $want_version && exit 0 # We start up an in-tree Emacs daemon only when a branch named after this host # is checked out, packages compiled against my emacs-snapshot Debian packages # are available, and there isn't a build still in progress -- avoids having to # consider whether a build is in progress when hitting 'C-i e'. # # The purpose of a branch for localhost is to keep track of what upstream # commit we want to be running on this host (e.g. emacs-29 vs. master), and # for WIP commits. It doesn't usually need to be pushed anywhere. # # We also examine $installed_running here to avoid having the in-tree # emacsclient(1) connect to the installed Emacs. if ! $want_installed && ! $installed_running \ && [ -x "$HOME/src/emacs/lib-src/emacsclient" \ -a -d "/usr/share/emacs-snapshot/site-lisp/elpa" ]; then make="$(ps h -o pid -C make)" branch="$(git -C $HOME/src/emacs rev-parse --abbrev-ref HEAD)" fi if [ "$branch" = "$(hostname -s)" ] \ && ! ( ! $devel_running && [ -n "$make" ] \ && echo "$make" | xargs pwdx \ | grep -q "$HOME/src/emacs" ); then emacs=("$devel_emacs") 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))') fi else emacs=(-a emacs "$installed_emacs") emacsclient=(-a emacsclient "$installed_emacsclient") fi # Except in the case above where ss(1) is not available, this wrapper script # is responsible for starting up the daemon if it's not running. This is to # ensure that only the in-tree emacsclient(1) talks to the in-tree Emacs, and # only the installed emacsclient(1) talks to the installed Emacs. (We used to # 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 "$daemon_name" -a "${emacs[0]}" = "$devel_emacs" ]; then # inotifywait(1) in Debian "bullseye" doesn't have --include. inotifywait -qq -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 pass_to_gdbmacs "$@" fi else ( exec "${emacs[@]}" --daemon"${daemon_name:+=$daemon_name}" 3>&- ) fi elif $want_update && ! $want_version; then # Facility to update a (running) daemon's env for a new login 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. # # --spw/update-environment may be combined with a regular request or be # the only thing asked of this script. 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" ] \ && ( exec "${emacsclient[@]}" -s"${daemon_name:-server}" \ --eval "(spw/update-environment$args))" ) fi # emacsclient(1) requires argument(s) beyond just -s/--socket-name. # However, in the absence of any such arguments, if the daemon wasn't already # running, or if --spw/update-environment, this wrapper starts the daemon if # it wasn't already running, and exits zero. # # Note that if the daemon *was* already running and the only argument was # -s/--socket-name, we do exec emacsclient(1) and it'll immediately exit. [ "$#" -eq "$min_arg" ] && ( $want_update || [ -z "$listener" ] ) \ || exec "${emacsclient[@]}" "$@" 3>&- 4>&-