EmacsTalk

Tramp 使用指南

  tags: tramp project

文章目录

Tramp 是 Emacs 中用来编辑远端文件的模块,全称为『Transparent Remote (file) Access, Multiple Protocol』,类似于 VSCode 的 Remote Development,只不过比它年长 20 岁而已😄。 这篇文章就来介绍下 tramp 的使用方式与注意事项。

使用方式

在使用 find-file 打开文件时,使用下面的语法,即可打开远端的文件:

/method:user@host#port:path/to/file

比如, /ssh:[email protected]:/etc/hosts 即可通过 SSH 协议以 vagrant 用户登录 192.168.31.92 机器,并且打开 /etc/hosts 文件。

/ssh:: 会连接到 localhost,一般用来测试 tramp 的功能。在 Windows 系统中,PuTTY 是一个常用的 SSH 客户端,需要用 plink 协议: /plink:user@host:/path/to/file

借助于 SSH 的功能,可以通过下面的配置来简化(免密码)tramp 的使用:

# ~/.ssh/config
Host devhost
  HostName 192.168.31.92
  User vagrant
  IdentityFile ~/.ssh/vagrant-pk

这样只需输入 /ssh:devhost:/etc/hosts 即可。通过配置 tramp 默认协议为 SSH,可以进一步简化为: /-:devhost:/etc/hosts

(setq tramp-default-method "ssh")

另外,也可以通过配置 directory-abbrev-alist,达到简化目的:

(setq directory-abbrev-alist '(("^/dev" . "/-:dev:/etc")))

输入 /dev 后,按 TAB,即会自动打开 /-:dev:/etc

SSH ControlMaster

ControlMaster 是 SSH 进行多路复用的机制,这样用户只需要在第一次登录时需要输入密码信息,后续 SSH 登录同一主机时,会复用之前的 TCP 连接。

ControlMaster 的主要缺点是第一次建立连接的 SSH 会话必须一直保留着,如果 logout 这个主会话,其他的 SSH 会话则会“卡住”。也因为这个原因,tramp 使用 SSH 时,默认用 tramp-ssh-controlmaster-options 覆盖掉 SSH config 中 ControlMaster 的行为,默认值为:

"-o ControlMaster=auto -o ControlPath='tramp.%%C' -o ControlPersist=no"

没有进行持久化。如果想要使用 SSH config 中的配置,则需配置:

(setq tramp-use-ssh-controlmaster-options nil)

与其他模块结合

在 Emacs 中,shell.el、eshell.el、compile.el、gud.el(gdb)这几个内置模块都与 tramp 做了完美整合,执行相应命令时会通过相应协议在远端执行。

多级跳跃 multiple hops

出于安全考虑,公司会禁止开发同学直接登录生产机器,需要通过一跳板机来登录生产机器 ,这时就需要多次跳跃。tramp 支持通过下面的语法,级联登录多个机器

C-x C-f /ssh:bird@bastion|ssh:admin@production:/path RET

上面命令会先用 bird 用户登录堡垒机 bastion,之后再在堡垒机上以 admin 用户登录 production 打开 /path

如果要在多级跳跃时使用 ControlMaster,中转的机器需要配置如下:

# ~/.ssh/config
Host *
     ControlMaster      auto
     ControlPath        tramp.%C
     ControlPersist     no

以 sudo 方式打开文件

一般来说,登录远端机器时都是非 root 用户,有时会需要用 sudo 来打开某些文件,tramp 通过下面的语法支持这类操作:

C-x C-f /ssh:you@remotehost|sudo::/path RET

sudo:: 的方式在 Emacs 27 上运行没有问题,其他低版本可能需要输入完整的命令:

C-x C-f /ssh:you@remotehost|sudo:remotehost:/path RET

注意事项

Tramp 打开的远端文件和本地的文件没什么区别,会被记录在 backup、autosave、recentf 等中。在今后重启 Emacs 时,如果这时无法连接远端机器,Emacs 可能会卡住,这是因为 tramp 会对之前打开的文件进行检查,可以通过下面的一些配置来绕过 tramp,让 backup 等机制不对 tramp 打开的文件起作用:

(setq recentf-exclude `(,tramp-file-name-regexp
                        "COMMIT_EDITMSG")
      tramp-auto-save-directory temporary-file-directory
      backup-directory-alist (list (cons tramp-file-name-regexp nil)))

如果用了 emacs-dashboard 来展示 project.el 中的项目,Emacs 启动时会检查这些项目,因此也需要跳过那些远端项目,不要持久化保存:

(defun my/project-remember-advice (fn pr &optional no-write)
  (let* ((remote? (file-remote-p (project-root pr)))
         (no-write (if remote? t no-write)))
    (funcall fn pr no-write)))

(advice-add 'project-remember-project :around
            'my/project-remember-advice)

添加上面的配置后,还需要检查下之前是否已经有 tramp 的文件被记录,如有手动删除即可。

如果打开 Emacs 还是有卡顿的问题,可以通过调整 tramp-verbose 来进行调试:

(setq tramp-verbose 10); 默认是 3

设置之后再重启时,会在 *debug-tramp* 内打印出详细日志。下图堆栈为笔者调试因 project.el 卡住时的截图:

https://img.alicdn.com/imgextra/i2/581166664/O1CN011TDwOt1z6A7zQEy7u_!!581166664.png
debug-tramp 示意图

下面的堆栈是在调试 docker-tramp 导致的卡住问题时,通过开启 (toggle-debug-on-error) 后得到的:

Debugger entered--Lisp error: (file-error "Couldn't find command to check if file exists")
  signal(file-error ("Couldn't find command to check if file exists"))
  tramp-error((tramp-file-name "docker" nil nil "helix" nil "/vagrant/" nil) file-error "Couldn't find command to check if file exists")
  tramp-find-file-exists-command((tramp-file-name "docker" nil nil "helix" nil "/vagrant/" nil))
  tramp-get-file-exists-command((tramp-file-name "docker" nil nil "helix" nil "/vagrant/" nil))
  tramp-sh-handle-file-exists-p("/docker:helix:/vagrant/")
  apply(tramp-sh-handle-file-exists-p "/docker:helix:/vagrant/")
  tramp-sh-file-name-handler(file-exists-p "/docker:helix:/vagrant/")
  apply(tramp-sh-file-name-handler file-exists-p "/docker:helix:/vagrant/")
  tramp-file-name-handler(file-exists-p "/docker:helix:/vagrant/")
  file-exists-p("/docker:helix:/vagrant/")
  project-forget-zombie-projects()
  dashboard-funcall-fboundp(project-forget-zombie-projects)
  dashboard-projects-backend-load-projects()
  dashboard-insert-projects(8)
  #f(compiled-function (els) #<bytecode 0x1573fa35edad7240>)((projects . 8))
  mapc(#f(compiled-function (els) #<bytecode 0x1573fa35edad7240>) ((agenda . 5) (recents . 10) (projects . 8) (bookmarks . 5)))
  dashboard-insert-startupify-lists()
  #f(compiled-function () #<bytecode 0x1f414d8ede95>)()
  run-hooks(after-init-hook delayed-warnings-hook)
  command-line()
  normal-top-level()

file-truename

file-truename 作用在 tramp 打开的 buffer 上时,也会进行远程连接。笔者之前配置时,曾出现过下面这个错误信息:

Tramp: Opening connection nil for ${HOSTNAME} using ssh...failed

主要原因就是在配置中使用了 file-truename

Docker/Vagrant

SSH 是 tramp 中常用的协议,除此之外,tramp 还支持非常多的协议,比如:ftp、smb、adb(连接 Android 手机)等,具体可参考文档:TRAMP Inline methods。社区内也有一些插件支持 DockerVagrant。use-package 配置如下:

(use-package docker-tramp
  :defer t
  :custom ((docker-tramp-use-names t)))

(use-package vagrant-tramp
  :ensure nil
  :load-path "/path/to/vagrant-tramp"
  :defer t)

vagrant-tramp 原作者貌似已经不维护了,有些小问题,笔者已经提交了 Pull Request,在作者合并前,读者可使用 fork 的版本。不仅单实例模式正常工作,在 Multi-Machine 模式下也没有问题。

Update:2022-06-27 上述 PR 已经合并

收听方式

反馈