gdbを使っていい感じにプロセスツリーを手元の端末に繋ぎ変えるシェルスクリプト

次のエントリにあるように、gdbを使うとプロセスの標準入出力を別の端末(tty)に繋ぎ変えることができる。

しかし実際にはあるプロセスからサブプロセスが複数呼ばれている状況もあり、そのような場合にはそれぞれのプロセスについてgdbでアタッチして端末を繋ぎ変える必要がある。 そこで、この操作を楽に行うためのシェルスクリプトを書いた。

#!/bin/bash
# grabtty.sh

rootpid="$1"
newtty=$(tty)

if [[ $(whoami) != "root" ]]; then
    echo "must be root."
    exit 1
fi

if [[ -z "$rootpid" ]]; then
    echo "Usage: $0 PID"
    exit 1
fi

grabpid() {
    local pid="$1"
    local children=$(ps --ppid=$pid -o pid --no-headings)
    for child in $children; do
        grabpid "$child"
    done

    gdb -q -p "$pid" &>/dev/null <<__EOF__
p close(0)
p open("$newtty", 0)
p close(1)
p open("$newtty", 1)
p close(2)
p open("$newtty", 1)
__EOF__
}

grabpid "$rootpid"

tpgid="ps --pid=$rootpid -o tpgid --no-headings"
if [[ $($tpgid) -eq -1 ]]; then
    trap "kill -SIGTERM $rootpid" SIGINT
else
    trap "kill -SIGINT -\$((\$($tpgid)))" SIGINT
fi

while ps --pid=$rootpid >/dev/null; do
    sleep 1
done

上のスクリプトでは、引数に指定したPIDを持つプロセス以下の子プロセスを再帰的にたどり、それらすべてを手元の端末に繋ぎ変えた後、指定したプロセス(ルートプロセス)が終了するまで待つ。 また、Ctrl+Cによる端末へのシグナル送信についても、元の端末を操作している感覚で扱えるようにtrapする。 具体的には、ルートプロセスが端末に繋がっている場合にはフォアグラウンドプロセスグループにそのままSIGINTを送信し、繋がっていない場合にはルートプロセスに直接SIGTERMを送り終了を試みる。

上のスクリプトを実際に使ってみる。 まず、1台のLinuxマシンに二つの端末からSSHログインし、片方の端末で次のようにプロセスツリーを作る。

$ tty
/dev/pts/0

$ python
Python 2.7.6 (default, Jun 22 2015, 17:58:13)
[GCC 4.8.2] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import os
>>> os.system("irb")
irb(main):001:0> system("sh")
$ ps auxf
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
(snip)
root       921  0.0  0.3  61368  3060 ?        Ss   22:29   0:00 /usr/sbin/sshd -D
root      1850  0.0  0.4 103560  4140 ?        Ss   22:54   0:00  \_ sshd: user [priv]
user      1898  0.1  0.1 103560  1936 ?        S    22:54   0:00  |   \_ sshd: user@pts/0
user      1899  0.1  0.6  26544  6644 pts/0    Ss   22:54   0:00  |       \_ -bash
user      1976  0.0  0.5  31856  5440 pts/0    S    22:54   0:00  |           \_ python
user      1977  0.0  0.0   4440   644 pts/0    S    22:54   0:00  |               \_ sh -c irb
user      1978  0.4  0.8  41484  8756 pts/0    Sl   22:54   0:00  |                   \_ irb
user      1980  0.0  0.0   4440   652 pts/0    S    22:54   0:00  |                       \_ sh
user      1982  0.0  0.1  19556  1376 pts/0    R+   22:54   0:00  |                           \_ ps auxf
root      1913  0.0  0.4 103560  4144 ?        Ss   22:54   0:00  \_ sshd: user [priv]
user      1961  0.0  0.1 103560  1952 ?        S    22:54   0:00      \_ sshd: user@pts/1
user      1962  0.1  0.6  26544  6644 pts/1    Ss+  22:54   0:00          \_ -bash
(snip)

そして、もう片方の端末で別の端末に繋がっているbash以下のプロセスツリーを手元に繋ぎ変えて操作してみる。

$ tty
/dev/pts/1

$ sudo ./grabtty.sh 1899
[ENTER]
$ ^C
$ exit
=> true
irb(main):002:0> ^C
irb(main):002:0> exit
2
>>>
KeyboardInterrupt
>>> exit()
$ exit
logout

それぞれのプロセスにSIGINTが適切に送られつつ、操作できていることがわかる。 また、最後にルートプロセスであるbashを終了すると、もう片方の端末のSSH接続が切断されることが確認できる。

関連リンク