--- title: "Editing remote files with Emacs, comfortably" description: "Edit remote files with Emacs using SSH and a wrapper for emacsclient." date: "2019-05-08T15:59:00+02:00" draft: false tags: ["emacs", "ssh"] comtodon: 9ibPpjSsYeNmHrbBDc --- :source-highlighter: pygments It took me a long time to collect all the bits and pieces I needed to make editing remote files with Emacs work the way I want, with a simple command that works via SSH. I hope I can save you some time by stitching it here together into a tutorial. I assume you use https://github.com/jwiegley/use-package[use-package] in my examples. == Emacs server & TRAMP We start with Emacs's good old inbuilt https://www.gnu.org/software/emacs/manual/html_node/emacs/Emacs-Server.html[server]. The default is to use an UNIX domain socket; We have to change that to TCP to be able to receive input from our remote hosts. The server will bind to 127.0.0.1. Pick a strong password that is exactly 64 characters long and a port https://www.w3.org/Daemon/User/Installation/PrivilegedPorts.html[above 1023]. I chose 51313 because if we substitute the digits for letters in the Latin alphabet, we get E M A C. The server will create the file `~/.emacs.d/server/server` with the IP, port and password in it. This file needs to be distributed to every host that should be able to access the server. [source,elisp] ---- ;; Run server if: ;; - Our EUID is not 0, ;; - We are not logged in via SSH, ;; - It is not already running. (unless (equal (user-real-uid) 0) (unless (getenv "SSH_CONNECTION") (use-package server :init (setq server-use-tcp t server-port 51313 server-auth-key ; 64 chars, saved in ~/.emacs.d/server/server. "looph8oow3Aph5ahje1eek1aish3Ohthu4Paengae0iketohGhaemi2iek5ae4ee") :config (unless (eq (server-running-p) t) ; Run server if not t. (server-start))))) ---- The server expects filenames as input, we can't just feed it the file. The package https://www.gnu.org/software/tramp/[TRAMP] allow us to use remote file paths with Emacs with the help of SSH. I have modified `tramp-password-prompt-regexp` to look for verification code prompts from the https://github.com/google/google-authenticator-libpam[Google Authenticator PAM module]. [NOTE] My modification overwrites the original value of `tramp-password-prompt-regexp`, which has a bunch of localized variants of “password” in it. You can view the original value with `C-h v tramp-password-prompt-regexp`. [source,elisp] ---- (use-package tramp :custom (tramp-use-ssh-controlmaster-options nil) ; Don't override SSH config. (tramp-default-method "ssh") ; ssh is faster than scp and supports ports. (tramp-password-prompt-regexp ; Add verification code support. (concat "^.*" (regexp-opt '("passphrase" "Passphrase" "password" "Password" "Verification code") t) ".*:\0? *"))) ---- == SSH In order to avoid having to enter our password again and again, we can edit our https://linux.die.net/man/5/ssh_config[SSH configuration] to reuse existing connections. The following configuration will create an UNIX domain socket per host and re-use that for all further connections to this host. It will also forward the Emacs server port, that we picked earlier, to every host we connect to. We will have to create `~/.ssh/sockets/` before we use the new configuration. [WARNING] These sockets allow for unauthenticated access to every host you are connected to. While this is very convenient, it is also a *security risk*. The sockets are only usable by your user and root (file mode 0600). [WARNING] Everyone on the remote host can connect to the port you forward. They will still need the password, but you might not want to do this if you don't trust the other users. [source,cfg] ---- Host fc??:* fd??:* 192.168.* server1.example.com server2.example.com # Reuse connections. ControlMaster auto # Close socket 600s after after last connection closes. ControlPersist 600 # Set path for sockets. ControlPath ~/.ssh/sockets/%r@%h-%p # Forward Emacs-server port. RemoteForward 127.0.0.1:51313 127.0.0.1:51313 ---- == Wrapper for emacsclient Using file paths in TRAMP notation gets annoying really quick. Thankfully Andy Skelton created a https://andy.wordpress.com/2013/01/03/automatic-emacsclient/[wrapper script]; I extended it with the ability to https://stackoverflow.com/a/16408592[become root using sudo] and an option to use it with local servers. This file needs to be distributed to every host that should be able to access the server. .https://dotfiles.tastytea.de/bin/emacsremote[emacsremote] [source,shell] ---- #!/bin/bash # Open file on a remote Emacs server. # https://andy.wordpress.com/2013/01/03/automatic-emacsclient/ with added sudo. params=() sudo=0 local=0 for p in "${@}"; do if [[ "${p}" == "-n" ]]; then params+=( "${p}" ) elif [[ "${p:0:1}" == "+" ]]; then params+=( "${p}" ) elif [[ "${p}" == "--sudo" ]]; then sudo=1 elif [[ "${p}" == "--local" ]]; then # Use local server, for use with --sudo. local=1 else # Setting field separator to newline so that filenames with spaces will # not be split up into 2 array elements. OLDIFS=${IFS} IFS=$'\n' if [[ $(id -u) -eq 0 || ${sudo} -eq 1 ]]; then if [[ ${local} -eq 0 ]]; then params+=( "/ssh:$(hostname -f)|sudo:$(hostname -f):"$(realpath -m "${p}") ) else params+=( "/sudo:localhost:"$(realpath -m "${p}") ) fi else params+=( "/ssh:$(hostname -f):"$(realpath "${p}") ) fi IFS=${OLDIFS} fi done emacsclient -f ~/.emacs.d/server/server "${params[@]}" ---- I had to add `[[ "${TERM}" = "dumb" ]] && unsetopt zle` to my Zsh configuration to prevent TRAMP connections from hanging all the time. Thanks to Darius for https://emacs.stackexchange.com/a/45810[their answer on StackExchange]. == Shell configuration Now we should set `VISUAL` and `EDITOR` to the wrapper and set up some nice, short aliases. In my examples I assume we called our wrapper `emacsremote`. The argument `-f` causes emacsclient to not try to use UNIX domain sockets (and print an error message). NOTE: I wrote the following code for Zsh, but it should also work for Bash. [source,shell] ---- # Set preferred editor. if command -v emacsclient > /dev/null; then VISUAL="$(command -v emacsclient) -f ~/.emacs.d/server/server -a emacs" if [[ -n "${SSH_CONNECTION}" ]]; then # Logged in via SSH. if command -v emacsremote > /dev/null; then VISUAL="$(command -v emacsremote)" fi elif [[ $(id -u) -eq 0 ]] && command -v emacsremote > /dev/null; then # Edit files as root in the Emacs instance run by the current user. VISUAL="$(command -v emacsremote) --sudo --local" fi elif command -v emacs > /dev/null; then VISUAL="$(command -v emacs)" elif command -v vim > /dev/null; then VISUAL="$(command -v vim)" elif command -v nano > /dev/null; then VISUAL="$(command -v nano)" fi export VISUAL export EDITOR="${VISUAL}" ---- [source,shell] ---- if [[ "${VISUAL}" =~ "emacs(client|remote)" ]]; then alias e="${VISUAL} -n" if [[ "${VISUAL}" =~ "emacsremote$" ]]; then # Don't block the terminal until the file is closed. alias se="${VISUAL} -n --sudo" elif command -v emacsremote >/dev/null && [[ -z "${SSH_CONNECTION}" ]]; then # Edit files as root in the Emacs instance run by the current user. alias se="$(command -v emacsremote) -n --sudo --local" fi else alias e="${VISUAL}" alias se="sudo ${VISUAL}" fi ---- To detect SSH connections after using `sudo -i`, we have to tell sudo to preserve the environment variable `SSH_CONNECTION`. [source,shell] ---- echo 'Defaults env_keep += "SSH_CONNECTION"' >> /etc/sudoers.d/ssh_vars ---- == Updates * Updated 2019-05-12: Add `-f` argument to emacsclient. * Updated 2019-10-06: Support files with spaces in emacsremote and allow to open files the user can't read (for use with emacsremote --sudo). * Updated 2019-10-17: Added Zsh-hack to prevent hanging TRAMP-connections.