SSHの制御が戻らない現象

SSHでコマンド実行した時にリモート先でコマンド終了後も制御が戻らないことがあった。現象を確認した後に対処方法を二つあげてみる。

今回SSHの制御が戻らなかった時のコマンドはservice jetty start。jettyを起動したかった。

SSHでリモートにログインした後で直接実行すると、問題なくコマンドは終了する。

$ remote=10.100.68.239
$ ssh $remote
$ sudo service jetty start
Starting Jetty: StartLog to /disk1/jetty/logs/start.log
2017-04-21 14:23:23.310:INFO::main: Logging initialized @807ms
2017-04-21 14:23:24.432:INFO::main: Redirecting stderr/stdout to /disk1/logs/jetty/logs/2017_04_21.stderrout.log
$

SSHでコマンド実行までやると、制御が戻らずSSHがフリーズしているかのような状態に陥る。

$ ssh $remote "sudo service jetty start"
Starting Jetty: StartLog to /disk1/jetty/logs/start.log
2017-04-21 14:28:24.041:INFO::main: Logging initialized @106ms
2017-04-21 14:28:24.223:INFO::main: Redirecting stderr/stdout to /disk1/logs/jetty/logs/2017_04_21.stderrout.log

別にリモートにログインしてみると、jettyの起動は完了しており、SSHの接続のみが残っている状況だった。

対処方法その1

Using SSH to remotely start a processによると、標準出力、エラー出力を/dev/nullもしくはファイルに吐くと正常にSSHが終了するようだ。

SSH connects stdin, stdout and stderr of the remote shell to your local terminal, so you can interact with the command that's running on the remote side.

As a side effect, it will keep running until these connections have been closed, which happens only when the remote command and all its children (!) have terminated (because the children, which is what "&" starts, inherit std* from their parent process and keep it open).

So you need to use something like

ssh user@host "/script/to/run < /dev/null > /tmp/mylogfile 2>&1 &"

上記の引用部分のうち、特に以下の部分が今回のjettyのservice起動で問題を起こした原因となる。

the remote command and all its children (!) have terminated (because the children, which is what "&" starts, inherit std* from their parent process and keep it open).

/etc/init.d/jettyのjetty起動部分のスクリプトは以下の通り。

su - "$JETTY_USER" $SU_SHELL -c "
  exec ${RUN_CMD[*]} start-log-file="$JETTY_LOGS/start.log" &
  disown \$!
  echo \$! > '$JETTY_PID'"

jetty起動部分がバックグラウンド実行されていて、親プロセスから標準入出力と標準エラー出力を受け継いでいる。さらにそこからdisownしているので、このバックグランド実行されたプロセスはずっと残り続け(service起動なのでずっとjettyプロセスが残り続ける仕様なのは当たり前といえば当たり前)、入出力もずっと開かれ続けることになる。

元々標準入力がjetty起動で開き続けられることはないので、とりあえず出力を/dev/nullに捨ててみるとうまく制御が戻った。

$ ssh $remote "sudo service jetty start >/dev/null 2>&1"
$

ただ、今まで見れていた出力がみれなくなってしまう。
jettyの場合、標準エラー出力だけであれば正常にSSHが終了するようなので、標準出力を標準エラー出力に統合して実行する。

$ ssh $remote "sudo service jetty start 1>&2"
Starting Jetty: StartLog to /disk1/jetty/logs/start.log
2017-04-21 14:59:07.415:INFO::main: Logging initialized @108ms
2017-04-21 14:59:07.589:INFO::main: Redirecting stderr/stdout to /disk1/logs/jetty/logs/2017_04_21.stderrout.log
$

対処方法その2

実際にremoteサーバにログインしてservice jetty startをするとコマンドが戻ってくるので、-tオプションをつけて擬似端末を割り当てて、実際にremoteサーバにログインした状態でコマンドを実行しているのに近い状態を作った。

man ssh

    -t    Force pseudo-tty allocation. This can be used to execute arbitrary screen-based programs on a remote machine, which can be very useful, e.g. when implementing menu services. Multiple -t options force tty allocation, even if ssh has no local tty.

コマンド

$ ssh -t $remote "sudo service jetty start"
Starting Jetty: StartLog to /disk1/jetty/logs/start.log
2017-04-21 15:56:07.389:INFO::main: Logging initialized @108ms
2017-04-21 15:56:07.548:INFO::main: Redirecting stderr/stdout to /disk1/logs/jetty/logs/2017_04_21.stderrout.log
Connection to 10.100.68.239 closed.
$

通常時と異なり、Connection closedと出力がされたが、うまくSSHが終了した。
擬似端末を割り当てる方が、SSHに握られ続けているのが標準出力でも標準エラー出力でもどちらでも対応できるし、余計なログファイルを作ったりする必要もないため、jettyの起動に関して言うと、こちらがbetterな対応方法だと思う。

今回のjettyの場合は、起動がバックグランド実行された後disownも実行されることでログアウトしても実行が継続されるようになっているため、疑似端末を割り当てたSSHがConnection closedしても問題なく起動し続ける。ただ、nohupやdisownが実行されない場合はSSH接続が切れたと同時にリモートでバックグランド実行したプロセスも終了してしまう。

簡単なスクリプトでテストをしてみる。
3秒待ってtestというファイルを作成するスクリプトをバックグランド実行させたとき、リダイレクト等を入れなければ3秒間SSHが終了せず、リダイレクト等を入れれば即座に終了する。
しかし、リダイレクトの場合は3秒後にtestファイルを確認すると存在するものの、疑似端末の場合はtestファイルが存在せず、SSHの接続断とともにtest.shのプロセス自体が終了してしまっていることがわかる。

$ ssh $remote 'echo "sleep 3 && echo done > test" > test.sh'

$ ssh $remote 'sh test.sh &'
$ ssh $remote 'cat test && rm test'
done

$ ssh $remote 'sh test.sh >/dev/null &'
$ ssh $remote 'cat test && rm test'
done

$ ssh -t $remote 'sh test.sh &'
Connection to 10.100.68.239 closed.
$ ssh $remote 'cat test && rm test'
cat: test: No such file or directory

対象方法1と2のどちらが本当にbetterなのかは言い切れないが、どちらの方法も難しいものではないので、二つとも使えるようにしておきたい。