読者です 読者をやめる 読者になる 読者になる

Emacsでsudoを介して開いたファイルでflymakeの動作がおかしくなる

Emacs tramp flymake

Emacsでは、以下のように編集権限がないファイルをsudoを介して編集することが可能です。

が、あるファイルを開いたところ、開くまでにとても時間がかかり、やっと開かれて編集しだしたら、
バッファが突然書き換えられ、頻繁にrootのパスワードプロンプトが表示されて、
Emacsが固まってしまうという事象に出くわしました。

私の emacs-version は以下です。
GNU Emacs 24.3.1 (i686-pc-linux-gnu, GTK+ Version 3.4.2) of 2014-02-22 on chindi10, modified by Debian

原因

調査の結果、 flymake-mode が有効なバッファの場合に起きていることがわかりました。
sudoを介してファイルを開くには、 /sudo::/path/to/file の形式で find-file をする必要がありますが、
その場合、 flymake-start-syntax-check も同じパスから一時ファイルを作成したりするため、
Trampのプロセスが編集中バッファで生成され、挙動がおかしくなっていました。

対処

以下の設定を加えることで正常に動作するようになりました。

(defadvice flymake-start-syntax-check-process (before avoid-sudo-tramp-access activate)
  (let ((tramp-vec (when (and (not (ad-get-arg 2))
                              (tramp-tramp-file-p default-directory))
                     (tramp-dissect-file-name default-directory))))
    (when (and tramp-vec
               (string= (tramp-file-name-method tramp-vec) "sudo"))
      (ad-set-arg 2 (tramp-file-name-localname tramp-vec)))))

(defadvice flymake-create-temp-inplace (around avoid-tramp-access activate)
  (if (not (tramp-tramp-file-p (ad-get-arg 0)))
      ad-do-it
    (setq ad-return-value
          (funcall 'flymake-create-temp-with-folder-structure 
                   (tramp-file-name-localname (tramp-dissect-file-name (ad-get-arg 0)))
                   (ad-get-arg 1)))))

対処内容について

flymakeのプロセスは、 flymake-start-syntax-check-process の中で、 start-file-process によって生成されます。
で、 start-file-process という関数では、 file-name-handler-alist という変数によって
プロセス生成に使用する関数が変わるようになっていました。
今回の場合では、flymakeが実行されるバッファに紐づいたパスが /sudo::/path/to/file なため、
tramp-file-name-handler という関数にプロセス生成が委譲され、
Trampのプロセスとしてflymakeのプロセスが生成されてしまっていました。

そのため、 flymake-start-syntax-check-process にadviceをかまし、
対象のバッファのパスが /sudo::/path/to/file な場合には、 /path/to/file に変更するようにしました。

また、flymakeは処理対象のバッファに対応した一時ファイルを作成します。
その作成方法は、flymakeを使う各モード毎の設定で指定するようになっています。
例えば、デフォルトで定義されている flymake-xml-init では以下のようになっており、

(defun flymake-xml-init ()
  (list "xmlstarlet" (list "val" (flymake-init-create-temp-buffer-copy 'flymake-create-temp-inplace))))

巷に出回っている設定でも同じように、 flymake-create-temp-inplace という関数が使われているのではないかと思います。
で、この関数は、対象のファイルパスに"_flymake"を付加したパスを返すようになっているため、
/sudo::/path/to/file なファイルを作成しようとして、Trampのプロセスが生成されてしまいます。

そのため、 flymake-create-temp-inplace にadviceをかまし、
/sudo::/path/to/file なファイルの場合には、一時ファイルのパスを別の方法で生成するようにしました。
詳しくは、以下を。

その他

調査の中で、同じ原因と思われる以下のissueも見つかりました。

どうやら、本家のflymake.elからフォークされたプロジェクトのようで、
こちらを使えば、上記の対処はしなくても、正常に動作するかも知れません。

また、 flymake-create-temp-inplace に対する対処は行わなくても、
エラーを回避する観点では必要ないかも知れません。
私は、一時ファイルのためにTrampの処理が走るのが嫌だったので、上記の対処をするようにしました。