Neovim 0.13の:detach! SSHが切れても作業が消えない
SSH越しにサーバのファイルを編集している最中に回線が切れると、向こう側のNeovimはそこで終わります。開いていたバッファも、まだ保存していない編集も一緒に消えます。これを避けるために「まずtmuxを起動してからNeovimを立ち上げる」という順番を体に覚えさせている人は多いはずです。Neovim 0.13の:detach!は、この備えをNeovim自身に持たせるコマンドです。
検証環境:nightlyビルドのNVIM v0.13.0-dev-1389+gd3b4f562a6と安定版のNVIM v0.12.4(macOS)を使い、tmuxで作った端末の中でNeovimを起動して、端末ごと落とす操作を繰り返しました。掲載しているプロセス数・エラーメッセージ・serverlist()の出力はすべて実行結果です。長いパスとユーザー名だけ読みやすさのために伏せてあります。0.13は開発中なので、リリースまでに細部が変わる可能性があります。
目次(9項目)
| 操作 | これだけ覚える |
|---|---|
:detach! | 印を付けるだけ。端末が落ちても本体が残る |
:detach | いますぐ切り離す(0.12.4にもある) |
:%detach | 自分以外のUIを切り離す |
:connect {アドレス} | 残っている本体に乗り移る |
nvim --server {アドレス} --remote-ui | 端末からつなぎ直す |
serverlist(#{info: v:true}) | 残っている本体を探す |
端末が落ちるとNeovimも道連れになる
まず何が起きているかを測りました。tmuxで端末を1つ作り、その中でNeovimを起動して1行目にWIP-と書き足します。保存はしません。そのうえでtmuxのセッションごと落とします。回線が切れて端末が消えるのと同じ状況です。数えているのはNeovimのプロセス数です。
端末を落とす前 端末を落とした後
0.13 :detach! を打たない 2 個 0 個
0.13 :detach! を打つ 2 個 1 個
0.12.4(:detach! は使えない) 2 個 0 個
打たなかった場合は2つとも消えました。未保存のWIP-もろとも失われます。:detach!を打っておいた場合だけ1つ残ります。0.12.4で同じコマンドを打つと、その場で断られました。
E477: No ! allowed
事故に備えて印を付けるだけのコマンド
紛らわしいのですが、:detachそのものは0.12.4にもあります。ただしこちらは打った瞬間にUIを切り離すコマンドで、試すとNeovimの画面が閉じてシェルに戻り、本体だけが残りました。SSHが切れるのは予期しないから事故なのであって、切れる前に:detachを打てるなら困っていません。0.12.4のヘルプはui-lifecycleの使いどころとして「予期しないSSH切断のあともセッションを生かす」を挙げていますが、その用途を素の:detachだけで満たすことはできませんでした。
0.13の:detach!は、いま切り離すのではなく「あとでUIが不意に消えても終了するな」という印を付けます。打つと次のメッセージが出て、画面はそのまま編集を続けられます。
Nvim will continue running if the UI disconnects
印を付けるだけなので、起動して最初に1回打っておけば足ります。毎回打つのが面倒なら設定ファイルに書けます。手元ではinit.luaにこの1行だけを置いた状態で起動し、端末を落としても本体が残ることを確認しました。
-- init.lua に書く場合
vim.cmd('silent detach!')
" init.vim に書く場合
silent detach!
なぜ端末と一緒に死ぬのか
仕組みを見ると腑に落ちます。NeovimをTUIとして起動すると、プロセスは1つではなく2つ立ちます。psでPIDと親PIDを並べたのが次の出力です。
6005 6003 nvim --embed -u NONE -i NONE -n --listen /tmp/nvp.sock p.txt
6003 10259 nvim -u NONE -i NONE -n --listen /tmp/nvp.sock p.txt
左がPID、その右が親のPIDです。下の6003が端末につながっているUIで、上の6005はその子として動いている本体です。バッファも未保存の変更も持っているのは子のほうで、UIは画面を描いてキーを送るだけの薄い層になっています。端末が消えるとUIが死に、UIが死ぬと本体も後を追います。:detach!が印を付ける相手は本体の側です。
この見方をすると、印は1つのNeovimにつき1回でよいことも説明が付きます。:detach!を打ってから端末を落とし、別の端末からつなぎ直し、そのつなぎ直したほうを今度は:detach!を打たずに落としてみましたが、本体は残ったままでした。あとからソケット越しにつないだUIは本体の子ではないので、道連れにする関係がそもそも無いということです。
落ちたあとにつなぎ直す
残っている本体には住所があります。v:servernameがそれで、--listenを付けずに起動しても自動で1つ割り当てられます。
/var/folders/…/nvim.<ユーザー名>/12xxuk/nvim.1242.0
控えてあれば、別の端末からこれを指してつなぎ直せます。
# 端末からつなぎ直す
nvim --server /var/folders/…/nvim.1242.0 --remote-ui
問題は、事故のときに限って住所を控えていないことです。0.13のserverlist()はinfoを付けると、自分が開いたサーバだけでなく同じマシンで動いている他のNeovimも返すようになりました。適当にNeovimを起動して次を打つと、残骸が見つかります。
:echo serverlist(#{info: v:true})
[{'active': 1787463621942746880, 'pid': 1518,
'addr': '/var/folders/…/FGupQ5/nvim.1518.0', 'own': v:true},
{'active': 1787463613678382080, 'pid': 1242,
'addr': '/var/folders/…/12xxuk/nvim.1242.0', 'own': v:false}]
ownがv:falseのほうが、端末を失って残っている本体です。addrをそのまま:connectに渡すと、いま開いているUIがそちらへ乗り移ります。手元では、消えたはずのWIP-付きのバッファがそのまま出てきました。
:connect /var/folders/…/12xxuk/nvim.1242.0
つなぎ直すときの端末は、元と同じ大きさでなくても構いません。100桁24行で作業していたものに60桁18行の端末からつないだところ、画面は新しい大きさに合わせて描き直されました。ひとつだけ癖があって、つないだ直後は画面が真っ白のままのことがあります。何かキーを押すと描画されるので、失敗したと思って閉じないほうがよいです。
:terminalの中のシェルも生きている
残るのはバッファだけではありません。:terminalで開いたシェルごと残ります。Neovimの中でシェルを開き、sleep 600 &をバックグラウンドに投げてから端末を落としてみました。sleepのプロセスは生きたままで、つなぎ直したあとの画面には落とす前のやりとりがそのまま残り、同じシェルが続けて入力を受け付けました。
katsushi@host ~ % sleep 600 & echo MARKER-ALIVE
[1] 3821
MARKER-ALIVE
katsushi@host ~ % echo RECONNECTED
RECONNECTED
上の3行が端末を落とす前、下の2行がつなぎ直したあとのやりとりです。落とす前の出力とジョブ番号がそのまま画面に残っていて、sleepのプロセスも生きたままでした。シェルが起動し直されたわけではありません。ここは:restartによる再起動との分かれ目で、あちらはウィンドウ配置とバッファを復元しますが、ターミナルバッファで動いていたプロセスは戻りません。:detach!は本体を落とさないので、動いていたものが動いたままになります。
tmuxとの違いと使い分け
やっていることが似ているので並べます。守れる範囲が違います。
| 項目 | tmux | :detach! |
|---|---|---|
| 守れる範囲 | 端末の中で動いているものすべて | そのNeovim1つ(中のシェルを含む) |
| 事前の手当て | 先にtmuxを起動しておく | Neovimの中で1回打つ |
| つなぎ直し方 | tmux attach | --remote-uiか:connect |
| Neovim以外 | 守れる | 守れない |
ビルドやテストを別のペインで流しているなら、その面倒はtmuxしか見られません。逆にssh host nvim fileのようにNeovimだけを直接起動する使い方だと、tmuxを挟むために手順が1つ増えていたところが:detach!で済みます。VPSにSSHで入って編集する構成では、どちらを選んでもよく、両方かけておいても損はありません。tmuxを使っていても、そのtmuxサーバごと落ちる事故には:detach!のほうが効きます。
気をつけること
いちばん引っかかるのは、つなぎ直さないと本体が残り続けることです。事故のあとに新しくNeovimを立ち上げて作業を再開すると、古い本体が未保存の変更を抱えたままメモリに残ります。serverlist()で見つけて:connectし、要らなければ:qaで閉じます。乗り移ると同時に元のほうを止める:connect!もあります。
UIが複数ぶら下がっている状態も作れます。同じ本体に2つの端末からつないだ状態で:%detachを打つと、自分以外が切り離されました。実測ではnvim_list_uis()の数が2から1に減り、もう一方の端末は閉じています。誰かに画面を共有していたのを片付けるときの操作です。
:detachと:detach!は名前が1文字違うだけで役割が逆に近いので、そこだけ混ぜないようにします。前者はいま切り離す、後者は将来に備えて印を付ける、です。そして0.13はまだ開発中です。手元で確かめたのは開発版なので、リリースまでに動きが変わる可能性は残ります。
最初は「tmuxがあるのだから要らないのでは」と思っていました。考えが変わったのは、印を付けたあとの本体が:terminalの中のシェルごと残ると分かったときです。手元の検証ではsleepを投げたシェルがジョブ番号もそのままで戻ってきました。もうひとつ、住所を控えていない状態からserverlist()で残骸を見つけられたのが大きく、これが無いと「生きているのは分かるがつなぎ方が分からない」で詰みます。ひとつ焦ったのは、つなぎ直した直後に画面が真っ白だったことで、失敗したと思って一度閉じかけました。キーを1つ押せば出てきます。
まとめ
Neovim 0.13の:detach!は、端末が不意に消えてもNeovimの本体を終了させないための印です。実測では、打たずに端末を落とすとプロセスは2つとも消え、打っておくと1つ残りました。0.12.4には:detachしか無く、こちらは打った瞬間に切り離すコマンドなので、予期しない切断には間に合いません。残った本体にはv:servernameの住所があり、控えていなくてもserverlist(#{info: v:true})でownがv:falseのものを探せます。あとは--remote-uiか:connectでつなぎ直すと、未保存の変更も:terminalのシェルもそのまま戻ってきます。tmuxが端末全体を守るのに対してこちらはNeovim1つぶんですが、起動して1回打つだけで済むので、SSHの先で編集する日には入れておく価値があります。