UECジャーナル

開眼☆シェルスクリプト CGIスクリプトを作る(3)―Ajaxで動的に画面を更新

Apache HTTP Serverを準備

動作にはApache HTTP Serverが動くUNIX系の環境を用意する。ここでは「CGIスクリプトを作る(1)―Webサーバへのデータは標準出力で渡す」でセットアップしたMac OS Xを使用する。FreeBSDやLinuxで実行する場合には適宜読み替えて準備してほしい。

セットアップしたMac OS X環境では/Library/WebServer/CGI-Executables/にCGIスクリプトを置くと、http://localhost/cgi-bin/hoge.cgiとURLを指定してCGIスクリプトを起動できる。/Library/WebServer/CGI-Executables/にいちいち移動するのは面倒なので、次のように作業アカウントのホームディレクトリにcgi-binという名前でシンボリックリンクを張って、そこにスクリプトを置くようにしている。

$ cd
$ ln -s /Library/WebServer/CGI-Executables/ ./cgi-bin
$ cd cgi-bin
$ sudo chown ueda:wheel .
$ 

さらに今回はHTMLファイルを置く場所についてもhtmlという名前でシンボリックリンクを張っておく。

リスト1: HTMLファイルの置き場所にリンクを張って所有権を変更

$ ln -s /Library/WebServer/Documents/ html
$ sudo chown ueda:staff html
$ 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 HTTP Serverを起動する。

$ sudo apachectl start
$ 

最初のサンプル

AjaxはXMLHttpRequestが提供する非同期通信機能を使ってWebページ遷移を伴うことなく動的にWebコンテンツの内容を更新する処理およびそれを実現するための関連技術の総称として使われている。その仕掛けのミニマムな構成がリスト2のHTMLファイルになる(注意: 厳密にはこのサンプルは非同期通信を行っていないのでAjaxではないのだが、まずもっとも簡単な例として次の表記を取り上げておきたい。非同期通信の例に関しては以後で取り上げる)。

リスト2: ~/html/ajax1.html

<!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()"を書く事によって、WebブラウザにこのHTMLの内容が表示されたときに6行目のfunction〜で定義した関数を実行させている。8〜11行目でCGIスクリプトを呼び出して、12行目でCGIスクリプトが送ってきた文字列を受け取っている。受け取った文字列は12行目の前半でdocument.body.innerHTML=とあるように、bodyの内側に相当する部分に代入される。Webブラウザにはこの代入がすぐに反映されるので、画面には代入したものが表示される。

8行目はPOSTメソッドを使い/cgi-bin/show.cgiにデータを送ると宣言している。9、10行目はshow.cgiを呼び出すときに使うHTTPヘッダを作っている。show.cgiを呼び出しているのは11行目で、show.cgiに向かってdummy=<乱数>という文字列を送っている。

このHTMLからアクセスさせるshow.cgiを作る。何か文字列を送ればWebブラウザに表示されるが、ここはリスト3のように書いてdateコマンドの出力でも送ってみる。

リスト3: ~/cgi-bin/show.cgi

#!/bin/sh 

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

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

show.cgiはHTTPヘッダを出力した後HTMLを出力する。

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

非同期通信のサンプル

最初のサンプルを応用すると動的にWebブラウザに表示されるコンテンツを書き換えるといったことができる。典型的にはリスト4のように書く。Webブラウザから閲覧するとajax1.htmlと同じように時刻が表示される。先ほどと異なるのは、通信が同期ではなく非同期に実施されているという点にある。

リスト4: ajax1.html を非同期処理に書き換えた ajax2.html

<!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>

openの第3引数がfalseではなくtrueになっているが、この指定が非同期通信を実施せよという指定になっている。

この書き方だとCGIスクリプトとの通信は非同期で実施されるようになるため、Webブラウザ側で待ちが発生しているように見えることはない。Ajaxではこのように非同期通信を使用する。

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

応用例として管理している複数のUNIXサーバの負荷をモニタするツールを作成する。Ajaxで呼び出されるシェルスクリプトをリスト6に示す。これはIPアドレスとsshのポート番号をPOSTされたら、そのIPの持ち主のロードアベレージを取得し、SVG(Scalable Vector Graphics)でグラフを描くシェルスクリプトとなっている。

リスト6: Ajaxで呼び出される ldavg.cgi

#!/bin/sh -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

このスクリプトには説明すべきポイントがいくつかある。まず、ターミナルから手動でシェルスクリプトを実行する場合は環境変数PATHが引き継がれた状態になっているが、HTTPサーバ経由で実行されるCGIスクリプトやcron(8)で呼ばれるスクリプトの場合は明示的に指定しないとパスが通っていない状態になっていることがある。このため先頭でPATHを設定している。

7、8行目はPOSTされたデータを読み込む処理になっている。GETリクエストではQUERY_STRINGという環境変数にデータがセットされているが、POSTリクエストではCGIスクリプトの標準入力にデータが送られてくるので、それをdd(1)コマンドで吸い出している。

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

リスト7: cgi-name の動作

$ echo 'host=ueda@www.usptomo.com&port=12345' | cgi-name 
host ueda@www.usptomo.com
port 12345
$ 

10、11行目は、変数hostおよびportにそれぞれホスト名およびポート番号を代入している。namereadOpen usp Tukubaiのコマンドで、ファイルから指定したキーの値を取る処理を実施する。hostやpostに不正データが代入される可能性があるので、sshのオプションに指定するときは必ずクオートしておく。

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

リスト8: sar(8)の出力

$ 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 に溜まるデータ

40 14時00分 12 0.12 27 39
60 13時50分 0 0.00 47 59
80 13時40分 3 0.03 67 79
...

sar(8)コマンドはSYSV系の商用UNIXやLinuxに特有のコマンドでBSD系のオペレーティングシステムには用意されていないことが多い。BSD系オペレーティングシステムではシステムの情報を得るコマンドとしてsystat(1)やvmstat(8)、iostat(8)、netstat(1)、top(1)などが使われることが多い。sar -qと同じ出力を得るのであればsystat(1)やvmstat(8)、またはtop(1)などをcron(8)経由で定期的に実行してロードアベレージをファイルに保存するなどして、そのデータを出力するようにスクリプトを組んでおけばよい。

ロードアベレージデータを得た後はSVGを作ってHTTPヘッダをつけて標準出力に出力すればよい。Open usp Tukubaimojihameコマンドで$tmp-svgにリスト6のデータを繰り返しはめ込んでいきグラフのSVGを作る。なおssh(1)は公開鍵認証でログインできるように設定しておく。

HTML側では複数のホストに対してldavg.cgiを実行しグラフを描くようにする。リスト11にコードを示す。これで複数のサーバの状態を一目で監視するWeb画面となる。

リスト11: ldavg.html

<!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,"graph2");
      }

      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="graph2" style="height:600px;width:350px;float:left"></div>
  </body>
</html>
$ 

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

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

図2: 完成した画面

おわりに

シェルスクリプトでAjaxを活用する方法を説明した。仕組みはとてもシンプルなものなので、基本となる部分を丁寧に記述するだけで簡単にこれら技術を活用できる。シェルスクリプトベースのCGIスクリプトおよびHTML、JavaScript、CSSの組み合わせで現在のWebページに要求される機能をすべて実現できる。

 

Software Design 2013年9月号 上田隆一著、「テキストデータならお手のもの 開眼シェルスクリプト 【21】CGIスクリプトを作る(3)―Ajaxで動的に画面を更新」より加筆修正後転載

Last modified: 2014-01-13 00:00:00