A Day In The Boy's Life -96ページ目

A Day In The Boy's Life

とあるエンジニアのとある1日のつぶやき。

PEARパッケージのSystem_Daemon は、作ったPHPプログラムをデーモンとして常駐して動かしてくれます。


なんで、LL言語のPHPをデーモンとして動かす必要があるのか、またはcron使えばいいじゃんってところはあるのですが、作者のページ にも書かれているように、開発が容易で高速に開発できるPHPの特性を活かしてデーモンとして動かしたい場合や、cronで動作させる場合は単体で処理が繰り返されるために前後の処理の状況をチェックするのが面倒、または不具合の原因にもなるからってことで、このPEARパッケージを開発したようです。


LAMP主体のサービスを構築する企業なんかは、PHPで作られた豊富なライブラリを持ってたりもするでしょうから、PHPの資産を活かしつつデーモンとして動かしたいプログラムを開発するって場合はいいかもしれませんね。



System_Daemonのインストール


2010年04月29日現在、System_Daemonのバージョンは0.10.2でベータ版として提供されているため、pearコマンドでインストールする場合は、パッケージ名の最後に「-beta」を付けてインストールする必要があります。


# pear install System_Daemon-beta
WARNING: channel "pear.php.net" has updated its protocols, use "channel-update pear.php.net" to update
Did not download optional dependencies: pear/Log, use --alldeps to download automatically
pear/System_Daemon can optionally use package "pear/Log" (version >= 1.0)
downloading System_Daemon-0.10.2.tgz ...
Starting to download System_Daemon-0.10.2.tgz (31,154 bytes)
.........done: 31,154 bytes
install ok: channel://pear.php.net/System_Daemon-0.10.2


メッセージの途中で出ているPEARのLogパッケージは特に無くても動作します。

その場合、独自実装のログ出力になる模様。



System_Daemonを利用してPHPプログラムをデーモン化してみる


では、早速PHPのデーモンプログラムを書いてみます。


#!/usr/bin/php
<?php

require_once 'System/Daemon.php';

// デーモンの名前
$app_name = "date_deamon";

$options = array("appName"        => $app_name, // デーモンの名前
                 "authorEmail"    => "foo@example.com", // 作者のE-Mail
                 "appDir"         => dirname(__FILE__), // アプリを動かすディレクトリ
                 "logLocation"    => dirname(__FILE__) . "/" . $app_name . ".log", // アプリのログのパス(デフォルトだと/var/log/{アプリ名}.log)
                 "appPidLocation" => dirname(__FILE__) . "/" . $app_name . "/" . $app_name . ".pid", // プロセスIDのパス(デフォルトだと/var/run/{アプリ名}/{アプリ名}.pid)
                 "appRunAsUID"    => 500,
                 "appRunAsGID"    => 500);

// 上記オプションをまとめてセット
System_Daemon::setOptions($options);

// デーモンの起動
System_Daemon::start();

// これ以降の処理がデーモンとして動作させたい処理
// デーモンが生きてるかチェックしながらループ
while (!System_Daemon::isDying()) {
// アプリのログに日付をはき続ける
    System_Daemon::log(System_Daemon::LOG_INFO, date('Y/m/d H:i:s'));
// SLEEP処理(5秒待機)
    System_Daemon::iterate(5);
}

System_Daemon::stop();


デーモンとして動かすほどのプログラムでもないのですが、これを起動させてみてプロセスを表示してみると常駐して動作していることがわかります。


$ php date_deamon.php
[Apr 30 01:51:39]   notice: Starting date_deamon daemon, output in: '/home/work/workspace/date_deamon.log'

$ ps aux | grep date_deamon.php
work     21997  0.0  0.7  22172  3908 pts/0    S    01:51   0:00 php date_deamon.php


実行結果は、「System_Daemon::setOptions」の"logLocation"で指定したログファイルの中に日付が吐き出され続けます。


[Apr 30 01:55:34]     info: 2010/04/30 01:55:34
[Apr 30 01:55:39]     info: 2010/04/30 01:55:39
[Apr 30 01:55:44]     info: 2010/04/30 01:55:44
[Apr 30 01:55:49]     info: 2010/04/30 01:55:49

停止したい場合は、killコマンドを使い、その後にプロセス番号が書かれているpidファイルを削除しておきます。


$ kill -9 21997
$ rm date_deamon/date_deamon.pid


今回の例では、uidとgidが500のユーザーでデーモンを起動しています。

それぞれ、「System_Daemon::setOptions」の"appRunAsUID"および"appRunAsGID"で指定するわけですが、これを指定しなかった場合、System_Daemonははrootユーザーでデーモンを実行させようとします

この時、プロセスIDを格納するためのpidファイルの所有者およびグループをrootユーザーにしようとするため、エラーが出てデーモンが起動できません。


[Apr 30 00:42:46]      err: Unable to change group of file '/home/work/workspace/date_deamon' to 0 [l:1427]


rootユーザー以外でデーモンを起動したい場合は、必ず"appRunAsUID"および"appRunAsGID"オプションを指定しておく必要があります。
また、ログファイルやpidファイルのデフォルトの保存場所も「/var/log」や「/var/run」となるため、root以外のユーザーで実行する場合は、適宜権限のあるパスを指定しておきます。


ループ処理の中で、System_Daemon::iterateで5秒間待機させていますが、これはPHPのsleep関数で代替できそうですが、実際にはclearstatcache を呼び出すなど余分なキャッシュなどの掃除をしてくれるみたいです。


参考: 常駐プロセス(デーモン)として動くアプリを自作する(PHPとPEAR:: System_Daemon編) @ Web屋のネタ帳



ディスク容量をチェックするデーモン


もう少し実用的なものにするため、指定のディレクトリの容量をチェックするデーモンを作ってみます。


#!/usr/bin/php
<?php

require_once 'System/Daemon.php';

// 監視対象ディレクトリ
$check_dir = "/home/work/workspace/";

// 閾値(MB)
$limit = 100;

System_Daemon::setOption("appName", "diskcheck");
System_Daemon::setOption("authorEmail", "foo@example.com");
// デーモンの説明
System_Daemon::setOption("appDescription", "Disk Check Daemon");
// 作者の名前
System_Daemon::setOption("authorName", "itboy");

System_Daemon::start();

System_Daemon::log(System_Daemon::LOG_INFO, "Disk-Check-Daemon started");

while (!System_Daemon::isDying()) {
    $disk_size = getDirSize($check_dir);

    if ($disk_size > ($limit * 1024 * 1024)) {
        System_Daemon::log(System_Daemon::LOG_WARNING, $check_dir . " over " . $limit . "MB !!" . " / size : " . number_format($disk_size) . "Byte");
    } else {
        System_Daemon::log(System_Daemon::LOG_INFO, $check_dir . " size : " . number_format($disk_size) . "Byte");
    }
    System_Daemon::iterate(5);
}

System_Daemon::stop();

// ディレクトリの容量をチェックする関数(参照: http://itpro.nikkeibp.co.jp/article/COLUMN/20070827/280408/
function getDirSize($path) {
    $total_size = 0;
    if (is_file($path)) {
        return filesize($path);
    } elseif (is_dir($path)) {
        $basename = basename($path);
        if ($basename == '.' || $basename == '..') {
            return 0;
        }
        $file_list = scandir($path);
        foreach ($file_list as $file) {
            $total_size += getDirSize($path .'/'. $file);
        }
        return $total_size;
    } else {
        return 0;
    }
}


今回は、rootユーザーで実行する前提にしたので、オプションの数が少なくなっています。


※ "appDescription"と"authorName"のオプションを追加してますが、自動起動スクリプトの作成(後述)のためです。


オプションは配列でまとめて指定も出来ますが、今回のように「System_Daemon::setOption」で一つずつオプションを指定することも可能です。

ディレクトリ内のファイル容量をチェックする関数は、「54. 再帰関数を使ってみよう @ ITpro 」のものを使わせていただきました。


動作原理もそれほど複雑ではなく、getDirSize関数で指定のディレクトリ内のファイルの容量をチェックし、閾値に設定している10MBを超えた場合は、LOG_WARNINGでアラートをログに出すようにしています。

実際にはメールを飛ばすなんかの手段の方がよいかもしれませんね。


# php disk_check.php
[May 01 01:02:27]   notice: Starting diskcheck daemon, output in: '/var/log/diskcheck.log'

# ps aux | grep disk_check.php
root     25752  0.0  0.7  22172  4020 pts/0    S    01:02   0:00 php disk_check.php


この時に、容量の大きいファイルを作ってみて閾値を超える状態を作り出してみます。


$ dd if=/dev/zero of=largefile.dmp bs=1M count=100
100+0 records in
100+0 records out
104857600 bytes (105 MB) copied, 0.197439 seconds, 531 MB/s

この一連の流れをログで確認してみると・・・


[May 01 01:06:38]     info: /home/work/workspace/ size : 55,772Byte
[May 01 01:06:43]     info: /home/work/workspace/ size : 55,772Byte
[May 01 01:06:48]     info: /home/work/workspace/ size : 55,772Byte
[May 01 01:06:53]  warning: /home/work/workspace/ over 100MB !! / size : 104,913,372Byte [l:23]
[May 01 01:06:58]  warning: /home/work/workspace/ over 100MB !! / size : 104,913,372Byte [l:23]
[May 01 01:07:03]     info: /home/work/workspace/ size : 55,772Byte
[May 01 01:07:08]     info: /home/work/workspace/ size : 55,772Byte


途中で閾値の100MBの容量を超えたため、warningがログに吐き出され、容量の大きいファイルを削除した後は単なる情報としてディスクのサイズをログに書き出すようになっています。



作成したSystem_DaemonのPHPプログラムを自動起動スクリプトに登録する


System_Daemonの面白い機能の一つに、自動起動スクリプト(/etc/init.d)へ登録してくれるものがあります。

自動起動スクリプトに登録するためには、幾つかの準備が必要です。


1. オプションに"appName"、"authorEmail"、"appDescription"、"authorName"を指定する


2. System_Daemon::start();の後に、System_Daemon::writeAutoRun();の記述を追加する


System_Daemon::start();
$path = System_Daemon::writeAutoRun();


3. 実行するPHPスクリプトに実行権限を付与しておく


# chmod 755 disk_check.php


これで、disk_check.phpを一度実行させれば「/etc/init.d」ディレクトリ以下に自動起動スクリプトが作成されます。

ここから、起動させたければ


# /etc/init.d/diskcheck start
[May 01 01:41:09]   notice: Starting diskcheck daemon, output in: '/var/log/diskcheck.log'
Starting diskcheck:                                        [  OK  ]


とすれば、起動してくれます。

こちらの方がプロセス番号を見てkillしたり、pidファイルを掃除する必要も無いので楽でしょうね。


サーバー起動時に自動起動させたければ、


# chkconfig --add diskcheck
# chkconfig --level 35 diskcheck on
# chkconfig --list | grep diskcheck
diskcheck       0:off   1:off   2:off   3:on    4:off   5:on    6:off


としておけば、幸せかもしれません。





デジカメなんかで撮った写真をそのままブログに載せてもいいですが、少し違った加工をすると見え方も違ってきて面白いかもしれません。

そんな、画像に一味違う加工を施してくれるフリーのWebサービスを紹介します。


ちなみに今回のWebサービスを利用した元画像は、下記のものです。

(工場萌えではありません)


A Day In The Boy&#39;s Life-夜の工場



スライドバーで手軽に写真の色合いをpicnik


picnik.com
A Day In The Boy&#39;s Life-picnik


有名なWebサービスですが、picnikは日本語のメニューも使え、スライドバー一つで画像に様々な編集を加えてくれます。

例えば、元画像を編集してを「色温度」と「コントラスト」を強くしてみると・・・。

燃え上がるような写真に。


A Day In The Boy&#39;s Life-picnik加工例


画像ソフトがPCにインストールされて無くても、色合いや露出を気軽に変更できます。

また、夜景で人物を取るとよく陥る赤目補正なんかの機能もあります。



素材となる画像も豊富に掲載しているFutta.NET


futta.NET
A Day In The Boy&#39;s Life-futta.net


こちらも日本語で扱えるWebサービス。

画像編集以外に、素材となる写真も豊富です。


画像の加工は、左メニュー一番下にある「写真加工」「写真リサイズ」メニューから行えます。

写真の加工は、「ポラロイド」や「トイカメラ」や「アニメ」など予め決まった形式に写真を加工してくれます。

細かな調整をすることもでき、例えばトイカメラ風に写真を加工してみると・・・。


A Day In The Boy&#39;s Life-futta.net加工例


この通り。

いつもの写真に違う味わいを加えてくれます。



オリジナルのモザイクやポラロイド風の写真に加工してくれるPixisnap


pxisnap.com
A Day In The Boy&#39;s Life-pixisnap


こちらは、元画像にオリジナルのモザイクを入れたり、ポラロイド写真を幾つか組み合わせたような画像に編集してくれるWebサービスです。

英語のサイトにはなりますが、使い方はそれほど難しくありません。


- モザイクを入れたい場合

1. 画像のサイズを選ぶ

2. 元となる写真データをアップロードする

3. モザイク用に重ねる画像を幾つかアップロードする(これが元画像の上にモザイクとして重なります)

4. 「DESKTOP WALLPAPER」からダウンロード


オリジナルのモザイク入れて加工してみたらこんな感じに。


A Day In The Boy&#39;s Life-pixisnap加工例1


- ポラロイド写真に加工したい場合

1. 画像のサイズを選ぶ

2. 元となる写真データをアップロードする

3. 重ね合わせるポラロイド写真の形体を選択

4. 「DESKTOP WALLPAPER」からダウンロード


ポラロイド写真を幾つか重ね合わせたように加工してみたらこんな感じに。


A Day In The Boy&#39;s Life-pixisnap加工例2


大きくこの2種類への加工になりますが、オンラインでオリジナルの写真を作ってみることが出来ます。



シンプルなポラロイド写真を作りたいならInstantizer


instantizer.com
A Day In The Boy&#39;s Life-instantizer.com


シンプルにポラロイド風写真へ加工したい場合は、Instantizer.comが便利かもしれません。

手順はシンプルで、元となる写真データを指定し、ポラロイドの枠に入れたい文字(英字のみ可能)、写真の傾斜角度を指定してアップロードすればその通りに加工してくれます。


A Day In The Boy&#39;s Life-instantizer加工例


枠を付けただけかもしれませんが、加工の仕方によって随分と見え方も違ってきますね。



写真をファンキーに?befunky


befunky.com
A Day In The Boy&#39;s Life-befunky.com


なんと表現したら言いかわかりませんが、劇画タッチのものから古めかしい写真にいたるまで、写真に様々な効果を加えてくれるWebサービスです。

itboyの自画像を公開します 」の中でも紹介したのですが、その時に比べて劇的にサービスがバージョンアップされています。


一番効果が出るのが人物を撮った写真なのかもしれませんが、風景画でもこの通り。


A Day In The Boy&#39;s Life-befunky加工例


手書きの絵画のように写真を加工してくれたり。

写真をアップロードして左メニューから加工したいテーマを選択するだけのシンプルな操作だけでここまで写真を加工してくれます。



難しい画像ソフトを扱わなくても簡単にしかもオンラインで画像を編集してくれるWebサービス。

ブログにアップロードする際なんかに一手間加えてみるのもよいかもしれませんね。




curlというコマンドを使えば、Linuxなどのサーバー上でコマンドラインから簡単にHTTPやFTPなどのリクエストを処理することができます。

PHPのcURL関数 は、そのcurlコマンドと同等の機能を関数レベルで提供してくれます。

このcURL関数を使って、HTTPクライアントをプログラム上に実装してみたいと思います。


ちなみに、プログラムをHTTPクライアントとして動作させたい場合は、PEARのHTTP_Client を利用した方が直感的でわかりやすいかもしれません。

PEARのHTTP_Clientの利用法は、「Twitterのタイムラインを取得するPHPスクリプト 」で簡単に触れていますので、そちらも合わせてどうぞ。



あるサイトへアクセスしてHTMLを取得する


まずは、PHPのプログラムからとあるサイトへアクセスし、ブラウザ同様にHTMLを取得すると言うことをやってみます。


<?php

$url = 'http://www.example.com/';

$ch = curl_init($url);

// HTTPヘッダを出力しない
curl_setopt($ch, CURLOPT_HEADER, FALSE);
// 返り値を文字列として受け取る
curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
// HTTPステータスコード400の以上の場合も何も処理しない
curl_setopt($ch, CURLOPT_FAILONERROR, TRUE);

// サイトへアクセス
$result = curl_exec($ch);

// HTTPステータスコードをチェックしエラーならエラー内容を出力
if(curl_errno($ch)) {
    echo 'Curl error: ' . curl_error($ch);
}

// セッションをクローズ
curl_close($ch);

// リクエストの内容を出力
var_dump($result);


これで、URIが正しければきちんとそのHTMLソースを受信することができます。

cURL関数は、curl_setopt関数 で細かなリクエストの処理を変えることが出来ます。

例えば、上記の場合はHTMLのみを受信しますが、HTTPヘッダも合わせて取得したければ、


// HTTPヘッダも出力
curl_setopt($ch, CURLOPT_HEADER, TRUE);


とすることで、下記のようにHTTPヘッダを受け取ることも出来ます。


string(282) "HTTP/1.1 200 OK
Date: Thu, 22 Apr 2010 15:12:41 GMT
Server: Apache/2.2.3 (CentOS)
Last-Modified: Thu, 22 Apr 2010 14:39:46 GMT
ETag: "1e7c03a-22-4831d080"
Accept-Ranges: bytes
Content-Length: 34
Connection: close
Content-Type: text/html

また、「CURLOPT_FAILONERROR」オプションをTRUEに設定しておかないと、400以上のHTTPステータスコードが返ってきた場合でも、そのままエラーページのHTMLを表示します

例えば、存在しないファイルへアクセスした場合、HTTPステータスコード404を返しますが、その404のエラーページのHTMLを受け取ってしまいます。



データをPOSTしてそのリクエストを受け取る


次は、とあるページにデータをPOSTし、そのリクエストを取得してみます。

POSTする先のプログラムは、下記のようにしておきます。


<?php

if ($_POST['var'] !== "") {
    echo "hello";
}


上記のプログラムへデータをPOSTするプログラムを書きます。


<?php

$url = 'http://www.examle.com/post.php';

// POSTするデータを連想配列で格納
$post_data['var'] = "hoge";

$ch = curl_init($url);
// HTTPヘッダを出力しない
curl_setopt($ch, CURLOPT_HEADER, FALSE);
// 返り値を文字列として受け取る
curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
// HTTPステータスコード400の以上の場合も何も処理しない
curl_setopt($ch, CURLOPT_FAILONERROR, TRUE);
// POSTするデータをセット
curl_setopt($ch,CURLOPT_POSTFIELDS, $post_data);

// サイトへアクセス
$result = curl_exec($ch);

// HTTPステータスコードをチェックしエラーならエラー内容を出力
if(curl_errno($ch)) {
    echo 'Curl error: ' . curl_error($ch);
}

// セッションをクローズ
curl_close($ch);

// リクエストの内容を出力
var_dump($result);


結果は、下記のようにプログラムからの出力結果を受け取ることができます。


string(5) "hello"


POSTするデータは、連想配列にセットしておき「CURLOPT_POSTFIELDS」オプションでまとめて渡します。



Basic認証をクリアしてその先のページへアクセスする


次は、SSL環境下でBasic認証がかかっているHTMLファイルへアクセスしてみます。

SSLアクセスと、Basic認証を実行する場合のオプションがそれぞれ加わります。


<?php

$url = 'https://www.example.com';
$basic_user     = "foo";
$basic_password = "password";

$ch = curl_init($url);
// HTTPヘッダを出力しない
curl_setopt($ch, CURLOPT_HEADER, FALSE);
// SSLバージョン3を利用する
curl_setopt($ch, CURLOPT_SSLVERSION, 3);
// 返り値を文字列として受け取る
curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
// サーバー証明書の検証をスキップ
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE);
// HTTPステータスコード400の以上の場合も何も処理しない
curl_setopt($ch, CURLOPT_FAILONERROR, TRUE);
// Basic認証のユーザー名:パスワードをセット
curl_setopt($ch, CURLOPT_USERPWD, $basic_user . ":" . $basic_password);

// サイトへアクセス
$result = curl_exec($ch);

// HTTPステータスコードをチェックしエラーならエラー内容を出力
if(curl_errno($ch)) {
    echo 'Curl error: ' . curl_error($ch);
}

// セッションをクローズ
curl_close($ch);

// リクエストの内容を出力
var_dump($result);


Basic認証をクリアするために「CURLOPT_USERPWD」オプションと、SSL証明書の検証をスキップするための「CURLOPT_SSL_VERIFYPEER」「CURLOPT_SSL_VERIFYHOST」オプションをFALSEにセットしています。

curlは、対象ホストの情報とSSL証明書の情報が一致するか厳密にチェックするため、そのようなシステム構成になっていない場合は、処理をスキップさせるようにする必要があります。



おまけ: cURL関数を使ってTwitterへデータをPOSTする


Twitterへ投稿するPHPスクリプト 」では、PEARのHTTP_Clientを利用してプログラムを書いてみましたが、今回のcurlでの処理を応用して書き直して見ます。


<?php

$url = 'https://twitter.com/statuses/update.xml';
$twitter_user     = "your_twitter_id";
$twitter_password = "your_twitter_password";

// TwitterへPOSTする内容
$tweet['status'] = "hello";

$ch = curl_init($url);
// HTTPヘッダを出力しない
curl_setopt($ch, CURLOPT_HEADER, FALSE);
// SSLバージョン3を利用する
curl_setopt($ch, CURLOPT_SSLVERSION, 3);
// 返り値を文字列として受け取る
curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
// サーバー証明書の検証をスキップ
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE);
// HTTPステータスコード400の以上の場合も何も処理しない
curl_setopt($ch, CURLOPT_FAILONERROR, TRUE);
// TwitterAPIのBasic認証のユーザー名:パスワードをセット
curl_setopt($ch, CURLOPT_USERPWD, $twitter_user . ":" . $twitter_password);
// ツイートするデータをセット
curl_setopt($ch, CURLOPT_POSTFIELDS, $tweet);
// Expectヘッダを抑制
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Expect:'));

// サイトへアクセス
$result = curl_exec($ch);

// HTTPステータスコードをチェックしエラーならエラー内容を出力
if(curl_errno($ch)) {
    echo 'Curl error: ' . curl_error($ch);
}

// セッションをクローズ
curl_close($ch);

// リクエストの内容を出力(XMLで返ってくるので実際は解析処理が必要)
var_dump($result);


今回はHTTPSで通信するバージョンで書いてみました。

また、今までのプログラムと比べ、「CURLOPT_HTTPHEADER」というオプションを加えています。

これを加えなかった場合、TwitterのAPIより下記のようなメッセージが返ってきます。


<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>417 Expectation Failed</title>
</head><body>
<h1>Expectation Failed</h1>
<p>The expectation given in the Expect request-header
field could not be met by this server.</p>
<p>The client sent<pre>
    Expect: 100-continue
</pre>
but we only allow the 100-continue expectation.</p>
</body></html>


クライアント側から「Expect: 100-continue」リクエストを投げろと要求してるけど、Twitterのサーバーでは許可してないよって返ってくるので、「Expect:」ヘッダを加えています

で、この「100-continue」リクエストというのは、大きなデータを投げるときに、まずはヘッダ情報だけを投げ、「100 Continue」のレスポンスが返ってきたらボディ(データ本体)を投げるというHTTP1.1の仕様のようです。


参考: Twitter APIの417 Expectation Failed対策 @ エスカフラーチェブログ


やっぱり、HTTP_Clientを使うより若干手間がかかりますね。。。