開眼シェルスクリプト2013年9月号

Pocket
LINEで送る

出典: 技術評論社SoftwareDesign

21. 開眼シェルスクリプト 第21回 シェルスクリプトでCGIスクリプト3

 ここ二回、シェルスクリプトでCGIやっちまえ!という企画で進めてきましたが、
今回はその最終回です。最終回らしく最もシェルスクリプトと縁遠そうな
Ajaxをやってみます。

 Ajaxというのは Asynchronous JavaScript + XML の略なのですが、
これほどよく分からない言葉もありません。
また、jQueryなどの直接Ajaxとは関係ないものと抱き合わせで覚える人も多いので、
なんとなく敷居の高いもののように感じている人もいると思います。

 今回は、JavaScriptとシェルスクリプトだけでAjaxを実現することで、
Ajaxの正体が案外単純なものであることをお見せします。
今回の内容を理解するには、JavaScriptの知識が少し必要です。
しかし、JSONもXMLもjQueryもprototype.jsも出てきません。
そいつらは本質的に無関係です。

言葉や属性こそ、物事の本質に一致すべきであり、
逆に本質を言葉に従わせるべきではない。
というのは、最初に物事が存在し、言葉はそのあとに従うものだからだ。

—ガリレオ・ガリレイ

21.1. 環境

 前回、前々回に引き続き、
筆者はMacでapacheを動作させてコードの動作確認をしています。
今回は、CGIスクリプトだけでなく、
静的なHTMLファイルもブラウザで閲覧したいのですが、
筆者のMacでは、デフォルトで /Library/WebServer/Documents/
というディレクトリにHTMLファイルを置くということになっているみたいです。
前々回、 ~/cgi-bin/ というシンボリックリンクを作って
CGIスクリプト置き場にリンクを張りましたが、
今回も同様にシンボリックリンクを張ります。

 手順をリスト1に示します。
万が一、前々回、前回を読んでいなくても、
リスト1の ls の出力のように設定できれば大丈夫です。

  • リスト1: HTMLファイルの置き場所にリンクを張って所有権を変更
1
2
3
4
5
uedamac:~ ueda$ ln -s /Library/WebServer/Documents/ html
uedamac:~ ueda$ sudo chown ueda:staff html
uedamac:~ ueda$ ls -l ~/cgi-bin ~/html
lrwxr-xr-x  1 ueda  staff  35  4 22 23:52 /Users/ueda/cgi-bin -> /Library/WebServer/CGI-Executables/
lrwxr-xr-x  1 ueda  staff  29  6 16 11:37 /Users/ueda/html -> /Library/WebServer/Documents/

 準備ができたら、apacheを立ち上げましょう。

uedamac:~ ueda$ sudo apachectl start

 また、今回はMac上で作ったCGIスクリプトがLinuxサーバと通信を行います。
通信先のLinuxサーバには sar コマンド(sysstat)
がインストールされていることを前提としています。

21.2. 最初の例

 まず、一番簡単な例から示します。
Ajaxというのは、結局のところ、
ブラウザに表示されたウェブページの裏でJavaScriptがCGIスクリプトを呼び出し、
結果をもらってウェブページの一部を書き換える方法です。
ということは、htmlの中に、その仕掛けを書いてやればよいということになります。

 その仕掛けのミニマムな構成が、リスト2のhtmlファイルです。
HTML5で書いていますが、別にHTML 4.01でもXHTMLでもかまいません。

  • リスト2: ~/html/ajax1.html
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <script>
      function callCgi(){
        var h = new XMLHttpRequest();
        h.open("POST","/cgi-bin/show.cgi",false);
        h.setRequestHeader("Content-Type",
            "application/x-www-form-urlencoded");
        h.send( "dummy=" + Math.random() );
        document.body.innerHTML = h.responseText;
      }
    </script>
  </head>
  <body onload="callCgi()">
  </body>
</html>

 シェルスクリプトの話でないのであまり細かく話したくありませんが、
最低限知っておくべきことを書きます。
16行目の onload="callCgi()" を書く事によって、
ブラウザにこのHTMLの内容が表示されたときに6行目の
function〜 で定義した関数が起動します。
8〜11行目でCGIスクリプトを呼び出して、
12行目でCGIスクリプトが送ってきた文字列を受け取っています。
それで、受け取った文字列は12行目の前半で document.body.innerHTML=
とあるように、bodyの内側に相当する部分に代入しています。
ブラウザにはこの代入がすぐに反映されるので、
画面には代入したものが表示されます。

 もうちょっとCGIスクリプトを呼び出す部分を説明しなければなりません。
まず、8行目はPOSTメソッドを使い、 /cgi-bin/show.cgi
にデータを送るぞと宣言しています。
先月号でGETメソッドを使ってCGIスクリプトに文字列を投げましたが、
POSTもCGIスクリプトにデータを送るもう一つの方法です。
もう一個の引数 false は、今は無視で。
9, 10行目は、 show.cgi を呼び出すときに使うHTTPヘッダを作っています。
実際に show.cgi を呼び出しているのは11行目で、
show.cgi に向かって dummy=<乱数> という文字列を送っています。
毎回同じ文字列をPOSTしようとすると、
怠けてCGIスクリプトを呼ばないブラウザがあるので、それを防いでいます。
ところで、この部分のJavaScriptの書き方は、
元来単純なHTTPを複雑にラッパーしていて、
正直ぎこちない感じがします。皆さんはどう感じるでしょうか?

 では、このHTMLから呼ばれる show.cgi を作りましょう。
とにかく何か文字列を送ればブラウザに表示されるのですが、
ここはリスト3のように書いて
date コマンドの出力でも送ってみましょう。

  • リスト3: ~/cgi-bin/show.cgi
1
2
3
4
5
6
7
#!/bin/bash

echo 'Content-type: text/html'
echo
echo '<strong style="font-size:24px">'
date
echo '</strong>'

このようにHTTPヘッダを出力した後に
date を実行します。
ただ時刻を送っても面白くないので、
strong で囲ってCSSでスタイルも指定しています。
show.cgi のパーミッションをいじって実行可能にしたら、
ajax1.html をブラウザで見てみましょう。
図1のように大きな太字で時刻が表示されたら成功です。

  show.cgi の方は、普通のCGIスクリプトのようにHTTPヘッダを出力した後、
HTMLの破片を出力します。
ajax1.html に比べて単純極まりないですが、
そういうものです。
これもまた、JSONで送った方がきれいとかいろいろ議論はありますが、
ここではスルーしておきましょう。
簡単にできることを無理に複雑にすることはないでしょう。

  • 図1: ajax1.html から show.cgi を呼び出した後

21.3. 非同期通信

 今の例を応用すると、
動的にブラウザに写るものを書き換え放題になるわけですが、
頻繁にCGIスクリプトを呼び出す場合には一つ問題があります。
上の書き方では、CGIスクリプトが返事をよこさないと、
ブラウザは待っている間、固まってしまいます。

 実はAjaxにはブラウザを固めないもう一つの書き方があります。
リスト4のように書きます。
ブラウザから閲覧すると、 ajax1.html
と同じように時刻が表示されると思います。

  • リスト4: ajax1.html を非同期処理に書き換えた ajax2.html
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <script>
      function callCgi(){
        var h = new XMLHttpRequest();
        h.onreadystatechange = function(){
          if(h.readyState != 4 || h.status != 200)
            return;

          document.body.innerHTML = h.responseText;
        }

        h.open("POST","/cgi-bin/show.cgi",true);
        h.setRequestHeader("Content-Type",
            "application/x-www-form-urlencoded");
        h.send( "dummy=" + Math.random() );
      }
    </script>
  </head>
  <body onload="callCgi()">
  </body>
</html>

 これもJavaScriptの話なのであまり詳しく説明したくないのですが、
何をやっているかというと、 h.onreadystatechange
というのが、CGIスクリプトから返事が来たら実行される関数の名前で、
そこに = function(){... で関数の中身を結びつけています。
8行目から13行目は、単に関数を名前に代入しているだけなので、
実際に実行されるのはCGIスクリプトから返事が来たときです。

 ということは、8〜13行目はすっ飛ばされて、
open 以下の、CGIスクリプトにちょっかいを出す処理が行われた後に、
この関数は終わります。
open の第三引数が false から true に変わっていますが、
これは「非同期にするよ」という意味です。

 関数が終わった後(いや、反応がものすごい早い場合は終わる前かもしれませんが)
CGIスクリプトから返事が来ます。
そこで、8~13行目で設定した関数の中身が走ります。
まず、9行目で

  • CGIスクリプトから受信完了( h.readyState が4 )
  • CGIスクリプトからのステータスコードがOK( h.status が200 )(脚注: 404 not found とか 403 forbidden とかのアレです。)

であることを確認し、その下に書いてある処理を実行します。

 この書き方だと、CGIスクリプトからの受信を受け取る処理が後ろに回るので、
ブラウザ側で待ちが発生しているように見えることはありません。
Ajaxの際は、普通はこのように非同期を使い、
画面の内容に齟齬が出ないようにしたいときは同期を使います。

21.4. 複数のサーバの監視画面を作る

 このままだとまるでJavaScript講座になってしまうので、
シェルスクリプトを組み合わせて作り物をしてみましょう。
管理している複数のLinuxサーバの負荷をモニタするツールを作ってみます。

 まず、Ajaxで呼び出されるシェルスクリプトを書きます。
リスト6に示すのは、IPアドレスとsshのポート番号をPOSTされたら、
そのIPの持ち主のロードアベレージを取得し、
SVG(Scalable Vector Graphics)でグラフを描くシェルスクリプトです。

  • リスト6: Ajaxで呼び出される ldavg.cgi
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#!/bin/bash -xv
exec 2> /tmp/log

PATH=/usr/local/bin:$PATH
tmp=/tmp/$$

dd bs=${CONTENT_LENGTH} |
cgi-name -i_ -d_        > $tmp-name

host=$(nameread host $tmp-name)
port=$(nameread port $tmp-name)

ssh "$host" -p "$port" 'LANG=C sar -q'  |
grep "^..:..:.."                        |
sed 's/^\(..\):\(..\):../\1時\2分/'       |
grep -v ldavg                           |
tail -r                                 |
awk '{print NR*20+20,$1,int($4*100),$4,\
     NR*20+7,NR*20+19}' > $tmp-sar
#1:文字y位置 2:時刻 3:棒グラフ幅 4:ldavg
#5:棒グラフy位置 6:ldavg文字y位置

cat << FIN > $tmp-svg
<svg style="width:300px;height:600px">
  <text x="0" y="20" font-size="20">$host</text>
<!-- RECORDS -->
  <text x="0" y="%1" font-size="14">%2</text>
  <rect x="68" y="%5" width="%3" height="15"
    fill="navy" stroke="black" />
  <text x="70" y="%6" font-size="10" fill="white">%4</text>
<!-- RECORDS -->
</svg>
FIN

echo "Content-Type: text/html"
echo
mojihame -lRECORDS $tmp-svg $tmp-sar

rm -f $tmp-*
exit 0

 このスクリプトは説明すべき点がいくつもあります。
まず、4行目の PATH の設定は、
標準的でないコマンド
(脚注:この場合はOpen usp Tukubai。https://uec.usp-lab.com を参考のこと)
の場所を明示的に指定するためのものです。
端末から手でシェルスクリプトと実行する場合は、
立ち上がりの際に設定ファイルからコマンドのパスが読み込まれた状態になりますが、
CGIスクリプトやcronで呼ばれるスクリプトの場合は、
明示的に指定する必要があります。

 そして、7,8行目は、POSTされたデータを読み込む処理です。
POSTは、前回行ったGETメソッドと同じくクライアント
(ブラウザ)側からCGIスクリプトにデータを送り込む処理です。
GETの場合は QUERY_STRING という変数にデータがセットされますが、
POSTではapacheがCGIスクリプトの標準入力にデータを突っ込んでくるので、
それを dd コマンドで吸い出します。
dd は、HDDのイメージを吸い出したりするあの dd です。
標準入力なのでもっと簡単な方法もありそうですが、
筆者がUSP研究所に入社したときはすでにこの方法が確立されていたので、
他を試していません。

  dd から出たデータは、これも弊社ではお約束ですが、Open usp Tukubaiの
cgi-name というコマンドに通してそのままファイルに出力します。
cgi-name の動きをリスト7に示します。
HTMLのフォームからPOSTされたデータは、
このリストの echo のオプションのような文字列でやって来るのですが、
それをコマンドなどでさばきやすいようにキーバリュー式のテキストに変換します。
エンコードされた日本語等も変換してくれます。

  • リスト7: cgi-name の動作
1
2
3
$ echo 'host=ueda@www.usptomo.com&port=12345' | cgi-name
host ueda@www.usptomo.com
port 12345

 10,11行目は、変数 host, port にそれぞれホスト、
ポート番号を代入する処理です。 nameread も Open usp Tukubai
のコマンドで、ファイルから、指定したキーの値を取るものです。
このとき、 host,post に変な(攻撃用の)データが代入されるかもしれません。
後ろの ssh のオプションに指定するときは、
必ずクオートしておきましょう。

 13〜19行目は、監視対象のLinuxホストからロードアベレージ
を取得して、SVGに埋め込む文字列を作っています。
sar -q の出力は、リスト8のようなものです。
この出力から余計なヘッダを除去し、
ldavg-1 というフィールドを取得して、リスト9のように、
グラフを描くために必要な縦軸、横軸、その他座標を出力します。
tail -r はファイルの上下を逆さにするコマンドで、
Linuxの tac と等価です。

  • リスト8: sar の出力
1
2
3
4
5
6
7
8
uedamac:~ ueda$ ssh www.usptomo.com -p 12345 'LANG=C sar -q' | head -n 7
Linux 2.6.32-279.19.1.el6.x86_64 ()

00:00:01      runq-sz  plist-sz   ldavg-1   ldavg-5  ldavg-15
00:10:01            1       136      1.26      1.10      0.58
00:20:01            0       132      0.02      0.32      0.45
00:30:01            0       133      0.08      0.06      0.23
00:40:01            0       131      0.00      0.00      0.10
  • リスト9: $tmp-sar に溜まるデータ
1
2
3
4
40 14時00分 12 0.12 27 39
60 13時50分 0 0.00 47 59
80 13時40分 3 0.03 67 79
...

 あとはSVGを作ってHTTPヘッダをつけて標準出力に出すだけです。
Open usp Tukubaiの mojihame コマンドで、
$tmp-svg にリスト6のデータを繰り返しはめ込んでいき、
グラフのSVGを作ります。これはずいぶん昔、
第4回で扱ったテーマなので繰り返し説明することはやめておきますが、
とにかく絵を描くためのHTML片を出力しているんだと納得し、
先にお進み下さい。

 次はHTML側・・・と行きたいのですが、
ssh で鍵認証を使うのでその設定をしなければなりません。
_www ユーザで ueda@www.usptomo.com
に接続したいのですが、
Macの場合は /Library/WebServer/.ssh/
下に鍵一式を置けばよいようです。
私は自分の鍵を流用するためにリスト10のような横着をしましたが、
まともにやるならrootになって鍵を作って接続先のサーバにセットしましょう。
所有者とパーミッションに注意。

  • リスト10: ueda アカウントの鍵を _www アカウントに移す
1
2
3
4
bash-3.2# cd /Library/WebServer/
bash-3.2# rsync -a /Users/ueda/.ssh/ .ssh/
bash-3.2# chown _www:_www .ssh/
bash-3.2# chown _www:_www .ssh/*

 これでHTML側の話に移れます。
HTML側では、複数のホストに対して ldavg.cgi
を実行し、グラフを描くようにコーディングします。
リスト11にコードを示します。
これで複数のサーバの状態を一目で監視するウェブ画面の出来上がりです。
Ajaxは面倒臭いですけど非同期で使います。

  • リスト11: ldavg.html
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <script>
      var hosts = ["host=ueda@www.usptomo.com&port=12345",
                   "host=ueda@araibo.is-a-geek.com&port=12345"];

      function check(){
        ldavg(0,"graph0");
        ldavg(1,"graph1");
      }

      function ldavg(hostno,target){
        var h = new XMLHttpRequest();
        h.onreadystatechange = function(){
          if(h.readyState != 4 || h.status != 200)
            return;

          document.getElementById(target).innerHTML = h.responseText;
        }

        h.open("POST","/cgi-bin/ldavg.cgi",true);
        h.setRequestHeader("Content-Type",
            "application/x-www-form-urlencoded");
        h.send( "d=" + Math.random() + "&" + hosts[hostno]);
      }

    </script>
  </head>
  <body onload="check();setInterval('check()',60000)">
    <div id="graph0" style="height:600px;width:350px;float:left"></div>
    <div id="graph1" style="height:600px;width:350px;float:left"></div>
  </body>
</html>

 このコードは、リスト3をもとにして作ったものです。
31行目の <body onload=... で、
ページが読み込まれたときに check
という関数を呼び出し、あとは60秒ごとに check
を繰り返し呼びます。
check 関数では、監視対象のホストを指定して
ldavg 関数を呼び出しています。

 これで ldavg.html をブラウザに表示すると図2
のようにグラフが表示され、
1分毎( sar のデータ自体は10分毎)に再描画されます。

  • 図2: 完成した画面

21.5. おわりに

 今回はCGIの最終回ということで、
シェルスクリプトでAjaxというお題に挑戦しました。
今回紹介した方法でできないことというのはそんなにないので、
きれいにウェブページをデザインすれば、
まさか後ろがシェルスクリプトだとは
思わないようなサイトが作れることでしょう。

・・・案外、そういうサイトは多いのかもしれませんよ。

 次回は、原稿やメモ書きなどの、
文章を扱うというお題を扱います。

Pocket
LINEで送る