New article: Editing remote files with Emacs, comfortably.
This commit is contained in:
parent
ff69b060f2
commit
63b05f18ee
|
@ -0,0 +1,215 @@
|
|||
---
|
||||
title: "Editing remote files with Emacs, comfortably"
|
||||
description: "Edit remote files with Emacs using SSH and a wrapper for emacsclient."
|
||||
date: "2019-05-05T23:22:04+02:00"
|
||||
draft: false
|
||||
tags: ["emacs", "ssh"]
|
||||
---
|
||||
|
||||
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. 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.
|
||||
|
||||
----
|
||||
{{< highlight 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)))))
|
||||
{{< / highlight >}}
|
||||
----
|
||||
|
||||
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`.
|
||||
|
||||
----
|
||||
{{< highlight 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? *")))
|
||||
{{< / highlight >}}
|
||||
----
|
||||
|
||||
== 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.
|
||||
|
||||
----
|
||||
{{< highlight 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
|
||||
{{< / highlight >}}
|
||||
----
|
||||
|
||||
== 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]
|
||||
----
|
||||
{{< highlight bash >}}
|
||||
#!/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
|
||||
# This only works if the user can access the file.
|
||||
sudo=1
|
||||
elif [[ "${p}" == "--local" ]]; then
|
||||
# Use local server, for use with --sudo.
|
||||
local=1
|
||||
else
|
||||
if [[ $(id -u) -eq 0 || ${sudo} -eq 1 ]]; then
|
||||
if [[ ${local} -eq 0 ]]; then
|
||||
params+=( "/ssh:$(hostname -f)|sudo:$(hostname -f):"$(readlink -f $p) )
|
||||
else
|
||||
params+=( "/sudo:localhost:"$(readlink -f $p) )
|
||||
fi
|
||||
else
|
||||
params+=( "/ssh:$(hostname -f):"$(readlink -f $p) )
|
||||
fi
|
||||
fi
|
||||
done
|
||||
emacsclient "${params[@]}"
|
||||
{{< / highlight >}}
|
||||
----
|
||||
|
||||
== 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`.
|
||||
|
||||
NOTE: I wrote the following code for Zsh, but it should also work for Bash.
|
||||
|
||||
----
|
||||
{{< highlight zsh >}}
|
||||
# Set preferred editor.
|
||||
if which emacsclient > /dev/null; then
|
||||
VISUAL="$(which emacsclient) -a emacs"
|
||||
if [[ -n "${SSH_CONNECTION}" ]]; then # Logged in via SSH.
|
||||
if which emacsremote > /dev/null; then
|
||||
VISUAL="$(which emacsremote)"
|
||||
fi
|
||||
elif [[ $(id -u) -eq 0 ]] && which emacsremote > /dev/null; then
|
||||
# Edit files as root in the Emacs instance run by the current user.
|
||||
VISUAL="$(which emacsremote) --sudo --local"
|
||||
fi
|
||||
elif which emacs > /dev/null; then
|
||||
VISUAL="$(which emacs)"
|
||||
elif which vim > /dev/null; then
|
||||
VISUAL="$(which vim)"
|
||||
elif which nano > /dev/null; then
|
||||
VISUAL="$(which nano)"
|
||||
fi
|
||||
export VISUAL
|
||||
export EDITOR="${VISUAL}"
|
||||
{{< / highlight >}}
|
||||
----
|
||||
|
||||
----
|
||||
{{< highlight zsh >}}
|
||||
# If ${VISUAL} contains emacs{client,remote}, return immediately to terminal.
|
||||
if [[ "${VISUAL}" =~ "emacs(client|remote)" ]]; then
|
||||
alias e="${VISUAL} -n"
|
||||
if [[ "${VISUAL}" =~ "emacsremote$" ]]; then
|
||||
alias se="${VISUAL} -n --sudo"
|
||||
elif which emacsremote >/dev/null && [[ -z "${SSH_CONNECTION}" ]]; then
|
||||
# Edit files as root in the Emacs instance run by the current user.
|
||||
alias se="$(which emacsremote) -n --sudo --local"
|
||||
fi
|
||||
else
|
||||
alias e="${VISUAL}"
|
||||
alias se="sudo ${VISUAL}"
|
||||
fi
|
||||
{{< / highlight >}}
|
||||
----
|
||||
|
||||
To detect SSH connections after using `sudo -i`, we have to tell sudo to
|
||||
preserve the environment variable `SSH_CONNECTION`.
|
||||
|
||||
----
|
||||
{{< highlight zsh >}}
|
||||
echo 'Defaults env_keep += "SSH_CONNECTION"' >> /etc/sudoers.d/ssh_vars
|
||||
{{< / highlight >}}
|
||||
----
|
Loading…
Reference in New Issue