233 lines
8.1 KiB
Plaintext
233 lines
8.1 KiB
Plaintext
---
|
|
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.
|