GNU screenのattach時に環境変数を自動的に引き継ぐ

GNU screenを使っていれば, 作業の途中でログアウトするときにdetachしておいて, 作業を再開するときにattachすれば, 作業途中の端末の状態がそのまま維持されるのでとてもべんり. たとえば, 最初作業していたのとは別のホストからsshでログインしてattachなんてこともできる.

ただし, 最初にscreenを立ち上げたのとは別の端末でattachすると, screenは最初に立ち上げた端末の環境変数のままになっていて, 別のホストから接続していることを認識できず面倒な思いをすることがある. 今回はこれをなんとかしたという話.

問題の詳細

話を簡単にするために, DISPLAY環境変数を例にとって説明しよう. DISPLAY環境変数は, 基本的には現在ログイン中の環境で使うべきXサーバを指し示している*1.

最初はホストAに直接ログインしているとする. DISPLAY環境変数は':0.0'になっていて, ホストA自身がXサーバでもある.

ここで一旦screenをdetachして, 後でホストBからホストAにsshでログインして, screenをattachした.

すると, screenはホストAで直接ログインしたときの環境変数のままなので, DISPLAY変数はホストAのXサーバを指したままになっている. screenをdetachしたときにホストAのX環境から抜けてしまっていれば, GUIアプリを起動しようにもディスプレイに接続できずにエラーとなってしまう. 仮にX環境に入ったままだったとしても, ホストAのディスプレイ上にGUIアプリが起動するだけで, ホストBからは操作できない.

ホストBからsshで接続したときのシェルのDISPLAY環境変数には'localhost:10.0'という値が入っていて, これはホストBのXサーバにつながる. なので, screenをattachするときに, screenの内側のDISPLAY環境変数を'localhost:10.0'に設定できれば問題ない.

つまり, 異なる環境でattachするときには, その環境の環境変数がscreenとその内側で動くプロセスに反映されるようにしたい. できれば自動的に.

解決のためのアイディア

環境変数を反映する対象のプロセス

attachされるscreenそのものの環境変数を更新するだけでは, screenの内側のシェルや, シェルから起動されたプロセスには反映されない. 今回はひとまず

ということを考える. screenが入れ子になっていたり, シェル上でプロセスを実行している場合のことは考えない.

能動的か受動的か

プロセスの環境変数を更新するにあたっては, そのプロセスが外部の情報を読み取って自身の環境変数を更新する方法(能動的)と, 外部からそのプロセスに対して更新命令を送る方法(受動的)の2種類が考えられる. 今回は

  • screenは受動的に更新
  • zshは能動的に更新

という方法にした.

screenが新しい環境変数の情報を知るためには, 自身をattachしたシェルの環境変数を参照しなければならない. けれど, このためには何らかの方法でattachしたシェルのプロセスIDをscreenに知らせる必要があり, 結局screenは受動的に情報を受け取る必要がある. screen -Xを使えば, 外部からscreenのコマンドを実行できるので, プロセスIDを受け渡すのも不可能ではない. ただ, それならそもそもsetenvコマンドを外部から実行してやればいい. つまり, screenをattachするときには, attachを実行しつつ, 実行したシェルが責任を持って引き継ぐべき環境変数をscreenに渡すようにすればいい.

screenの内側で動くシェルは, 自分がどのscreenの中にいるかに関しては環境変数STYで容易に知ることができる. 逆に, シェルの環境変数を外部から設定する簡単な方法はありそうにない. そこで, zshのpreexec_functionsを使って, シェル内で何かコマンドを実行する度にscreenの環境変数を参照(screen -Q echo)して, 自身の環境変数を更新することにした. もちろん, これを愚直にやるとオーバーヘッドが大きいので, 環境変数のうち「この変数の値が変わっていたら, 別環境にattachされたと見なす」というものを決めておき, 毎回チェックするのはその変数のみで, 別環境だと分かったときにだけ残りの変数も設定するようにした.

実際のやり方

必要なもの

screen-alias.zshはscreen_hoge argsというコマンドをsc hoge argsで実行できるようにしたい場合のみ必要.

インストール

~/.zshrcなどに

typeset -ga preexec_functions
source ~/.zsh/screen.zsh
source ~/.zsh/screen-attach.zsh
source ~/.zsh/screen-alias.zsh

と書く. この行より後で, 「値が変わっていたら, 別環境にattachされたと見なす」変数, attach時に引き継ぎたい変数それぞれの名前を

SCREEN_CRITICAL_ENV+=(HOGE)
SCREEN_EXPORT_ENV+=(HOGE FOO BAR)

のようにして追加する. デフォルト値はscreen-attach.zshの先頭を参照.

使い方

screenのattachを

sc attach [sty]

というコマンドで行なうようにすれば, 自動的に環境変数が引き継がれる. styを省略した場合は, 起動中のscreenセッションのうち先頭のものが自動選択される(detachされているもの優先).

通常はscreen内のzshはコマンドを実行する度に自動的にscreenの環境変数と同期を取るので, これを(そのシェルでだけ)やめたい場合は

sc auto-env off

とする. onにするとまた同期を取るようになる. 同期をやめても既に同期してしまった環境変数の値が元に戻るわけではないので注意.

補遺: Emacsデーモンを使用する場合

Emacsをターミナル上で実行(コマンドラインオプション-nw)している場合にも, screenのattachに伴って環境変数を引き継いでほしい場合がある. とくに, Emacsデーモンを使っている場合, Emacsクライアントを起動し直せばDISPLAY環境変数はクライアントを起動したシェルのものに設定されるものの, screenをattachする度にEmacsクライアントを再起動するのは煩わしい. しかも, 環境変数STYはデーモンを起動したシェルのものしか参照できないので, Emacsクライアントを起動した後でscreenの内部に開いているかどうか判定するのは事実上不可能になる.

そこで, Emacsクライアントを起動するときに, screenの内部で起動されたことを記録しておき, screenをattachする際には, クライアントがscreen内部にある可能性のあるEmacsデーモンに対し, 環境変数を再設定させるようにした. Emacsクライアントを複数のscreen内で使う場合には一番最後にattachしたscreenの環境変数が反映されるという仕様.

必要なもの
インストール

.emacs等に

(load "20_screen")

と書く.

Emacsデーモンを使う場合は, .zshrc等に

source ~/.zsh/emacs.zsh
EMACS_CLIENT_CMD=emacsclient
EMACS_STANDALONE_CMD=emacs

と書く(emacsclient, emacsは環境にあわせて適宜変更する). ちなみに, これを使うとemacsコマンドが「デーモンが起動していなければ起動して, クライアントを端末内に起動」になるので注意.

使い方

Emacsクライアントを使うときに, emacsclient -nwの代わりに単にemacsとするだけ. screenのattach時に自動的にEmacsデーモンに環境変数更新命令が飛ぶようになる.

Emacsデーモンを使っていない場合は, screenの環境変数Emacsに反映させたいときにM-x screen-sync-envを実行する(あるいはこのコマンドが自動的に実行されるようにする).

どちらの場合も, デフォルトでは, screenの環境変数のうち, DISPLAY環境変数のみが同期される.

*1:実際にはデスクトップマネージャなどが認証管理等しているのでもう少し複雑かもしれない