Emacs remote file editing over SSHFS

Posted by Michał ‘mina86’ Nazarewicz on 14th of February 2021

Previous article described how to use emacsclient inside of an SSH session. While the solution mentioned there relied on TRAMP, I’ve confessed that it isn’t what I’m actually using. From my experience, TRAMP doesn’t cache as much information as it could and as a result some operations are needlessly slow. For example, delay of find-file prompt completion is noticeable when working over connections with latency in the range of tens of milliseconds or more. Because for a long while I’d been working on a workstation ‘in the cloud’ in a data centre in another country, I’ve built my setup based on SSHFS instead.

It is important to note that TRAMP has myriad of features which won’t be available with this alternative approach. Most notably, it transparently routes shell commands executed from Emacs through SSH which often results in much faster execution than trying to do the same thing over SSHFS. grep command in particular will avoid copying entire files over network when done through TRAMP.

Depending on one’s workflow, either TRAMP-based or SSHFS-based solution may be preferred. If you are happy with TRAMP’s performance or rely on some of its feature, there’s no reason to switch. Otherwise, you might want to try an alternative approach described below.

This article assumes remote editing over TRAMP is already configured, i.e. the previous article is a prerequisite to this one.

Testing SSHFS

SSHFS is a remote file system which transfers data over SSH. It allows directories from hosts one can ssh into to be mounted and accessed locally. No special configuration on remote side is necessary (so long as SFTP is supported) and no super user privileges are needed on local side (so long as sshfs is installed). The usage is simple and boils down to creating a user-owned directory and mounting remote file system there:

$ mkdir -p ~/.phobos
$ sshfs -o idmap=user,transform_symlinks phobos.mars.uac:/ ~/.phobos

The above will make the entire Phobos’ file system accessible under ~/.phobos directory. Mounting everything may seem unnecessary but it simplifies emacsclient usage. Having a path to a file on remote machine it’s enough to add a fixed prefix to know how to access it locally. If only part of the remote file system was mounted, it would be more complicated to figure out local equivalent of a remote path.

The command specifies two mount options. The first one, idmap=user, maps remote user and group ids to local ids in case they are different. idmap may also be set to file for more detailed control of the translation. In the latter case further uidfile=path-to-uidfile and gidfile=path-to-gifdile are needed where each file contains name:id mappings.

The second option, transform_symlinks changes absolute symbolic links to relative ones. This is desired since absolute symlinks on remote machine assume that they are anchored to that host’s root and will often break if the root directory is changed.

Another option that could be useful is reconnect. It won’t work with password authentication though or even with an SSH key if it’s password-protected and agent is not used. Similarly, delay_connect may also be of use but once again requires an automatic authentication method.

Remote file systems in home directory

One note of caution: having a remote file system mounted in one’s home directory may lead to some commands surprisingly taking a long time. For example, normally calling du -hd1 ~ should be a relatively fast operation. Sure, it might take a while if one’s home directory has many files, but operating on local disk the command should finish in acceptable time. Similarly, find ~ -name '*some-name*' shouldn’t take too long.

However, once SSHFS mounts remote machine’s file system in user’s home directory, those operations start to involve traversing the entirety of the remote host. There’s no way to address this issue other than to remember about the mount points and act accordingly. In case of du the -x option does the trick; find has -xdev switch which will address the problem.

Automating SSHFS connection

It may be tempting to use the phobos SSH alias that has been established previously. Sadly, SSHFS doesn’t play nicely with control master or port forwarding. This can be worked around but a cleaner solution is to have a separate entry for the remote file system which furthermore disables control master. Something like the following added to ~/.ssh/config:

Host phobos-sshfs
    HostName phobos.mars.uac
    ControlMaster no

Still, typing the entire sshfs invocation each time doesn’t sound fun. Instead it’s a good idea to create a script which hides all the gory details. For example, an executable file named phobos somewhere in $PATH with the following content:

#!/bin/sh
dir=~/.phobos
test -d "$dir" || mkdir -p "$dir"
test -e "$dir/home" ||
	sshfs -o idmap=user,transform_symlinks \
	      phobos-sshfs:/ "$dir"
exec ssh phobos "$@"

With this, rather than using ssh phobos to open a new SSH session, phobos command can be used. It will automatically mount the remote file system if necessary and establish remote shell.

Finishing up with emacsclient

Last step is to adjust how emacsclient is used on remote machine. In last article a -T switch was used to specify a TRAMP prefix. The full name of the switch is --tramp indicating that’s exactly what the option is meant for. In reality the name is a bit misleading since the option can be used without TRAMP just as easily:

alias e="emacsclient -s earth -T '/home/$(id -un)/.phobos'"

The above assumes user names on both machines are the same. If that’s not the case (or if Phobos’ file system is mounted some place other than ~/.phobos), the value for -T switch needs to be adjusted accordingly.

Teaching Emacs the file is remote

There are several things that Emacs dose differently when editing remote and local files. For example, auto-save files are always stored on local machine and lock files are not created for remote files. Often this is to avoid network traffic so it is beneficial to teach Emacs that files accessed via SSHFS mount should be treated as remote as well.

In truth, those steps are optional — especially on low-latency connections — but may reduce network traffic and delays when editing.

Auto save

Where auto save files end up is governed by the auto-save-file-name-transforms variable. It contains rules which match file names and indicate how to construct an auto save file name for them. To make Emacs handle data accessed over SSHFS like other remote files the following needs to be added to one’s init.el file:

(defconst mpn-file-remote-mount-points
  (mapcar (lambda (d) (directory-file-name
                       (expand-file-name d)))
          '("~/.phobos"))
  "List of locations where remote file systems have been mounted.
Each directory listed must be an absolute expanded path and must
not end with a slash.")

(push (let ((re (regexp-opt mpn-file-remote-mount-points nil)))
        (list (concat "\\`" re "\\(?:/\\|\\'\\)")
              (concat temporary-file-directory "remote")
              t))
      auto-save-file-name-transforms)

This tells Emacs that if a file name starts with /home/user/.phobos, it’s auto save file should be put in /tmp directory. The mpn-file-remote-mount-points constant can be extended to include multiple paths in case there are several different locations to consider (e.g. ~/.phobos and ~/.deimos).

Lock files

To avoid conflicts when editing a file with multiple Emacs instances (or by multiple people), Emacs creates a lock file when buffer visiting a local file is modified. Files opened via TRAMP don’t get that treatment. To prevent file accessible through network file systems getting a lock file as well, the create-lockfiles variable needs to be set to nil. This time the code is a bit more complicated:

;;(defconst mpn-file-remote-mount-points
;;  … defined as above …)

(defun mpn-file-remote-mount-p (&optional file-name)
  "Return whether FILE-NAME is under a remote mount point.
Use ‘buffer-file-name’ if FILE-NAME is not given.  List of remote
mount points is defined in ‘mpn-file-remote-mount-points’
variable."
  (when-let ((name (or file-name buffer-file-name)))
    (let ((dirs mpn-file-remote-mount-points)
          (name-len (length name))
          dir dir-len matched)
      (while (and dirs (not matched))
        (setq dir (car dirs)
              dirs (cdr dirs)
              dir-len (length dir)
              matched (and (> name-len dir-len)
                           (eq ?/ (aref name dir-len))
                           (eq t (compare-strings name 0 dir-len
                                                  dir 0 dir-len)))))
      matched)))

(defun mpn-dont-lock-remote-files ()
  "Set ‘create-lockfiles’ to nil if buffer opens a remote file.
Use ‘mpn-file-remote-mount-p’ to determine whether opened file is
remote or not.  Do nothing if ‘create-lockfiles’ is already nil."
  (and create-lockfiles
       (mpn-file-remote-mount-p)
       (setq-local create-lockfiles nil)))

(add-hook 'find-file-hook #'mpn-dont-lock-remote-files)

Functions added to find-file-hook are executed after opening a file. The above code introduces mpn-dont-lock-remote-files which checks whether file visited in the buffer falls within a mount point of a remote file system (as before, configured in mpn-file-remote-mount-points list) and if so sets create-lockfiles to nil to prevent lock file from being created.

Auto revert

Another difference is auto reverting. Unless an auto-revert-remote-files variable is non-nil, (global) auto revert mode will not work on remote files. Since those modes are disabled by default, this section is not applicable unless the configuration has been changed. To disable auto reverting of files on remote file systems an approach similar to the one for lock files can be utilised:

;;(defconst mpn-file-remote-mount-points
;;  … defined as above …)

;; (defun mpn-file-remote-mount-p (&optional file-name)
;;  … defined as above …)

(defun mpn-dont-auto-revert-remote-files ()
  "Disable auto-revert if buffer opens a remote file.
Use ‘mpn-file-remote-mount-p’ to determine whether opened file is
remote or not."
  (when (mpn-file-remote-mount-p)
    (setq-local global-auto-revert-ignore-buffer t)
    (auto-revert-mode -1)))

(add-hook 'find-file-hook #'mpn-dont-auto-revert-remote-files t)

Other differences

There may be other situations where Emacs treats local and remote files differently. As already mentioned there’s the case of TRAMP routing shell commands through remote shell which would be tricky to achieve with SSHFS mounts. While one could try and identify all the places ultimately it might not be worth it. I’ve been using the SSHFS setup for years and haven’t run into any issues with excess network traffic. As a matter of fact, I’ve kept auto-revert on finding it beneficial even with a small delays every now and again.

With all that done, the setup is done.