PHPでWake-on-LANしてみたり、wol代替コマンドにしてみたり。2009年07月23日 05時48分39秒

WoL事情を調べてみた。なんとなく。

社内でちょっとした必要があって、Web経由でWake-on-LANによるPCの起動をするツールでも作ろうかと思って調べてみた。

Vineなんかはapt経由でwolコマンド(どうやらこちらのコマンドがオリジナルみたい)をさくっとインストールできたり、CentOSやDebianにもコマンドがあるらしいことがわかった。

これならPHP経由でもコマンド叩くだけで必要な機能は実装できそう(といいつつ、CentOSのethtoolはroot権限必要だけど)なので楽かなーとか思っていたが、Windowsでデバッグするのにちょっと面倒くさそう。

じゃ、PHPだけでやってみるか

PHPだけでなんとかできんものかと思って調べたら、すでにやっておられる方もいるので、自分でもやってみることにした。

WakeOnLan.php

こんな感じでやってみた。仮に「WakeOnLan.php」とでもしておく。

<?php
class WakeOnLan {
    const BROADCAST_MAC_ADDR = 'FF:FF:FF:FF:FF:FF';
    
    const DEFAULT_BROADCAST_IP = '255.255.255.255';
    
    const DEFAULT_PORT = 2304;
    
    public static function macAddrToBytes($mac) {
        $mac = (string)$mac;
        if(! self::isValidMacAddr($mac)) {
            throw new Exception('invalid MAC address');
        }
        
        $buf = array();
        foreach(preg_split('/[:\-]/', $mac) as $one_octet) {
            $buf[] = chr(intval($one_octet, 16));
        }
        return join('', $buf);
    }
    
    public static function isValidMacAddr($mac) {
        return preg_match('/^[\da-zA-Z]{2}([:\-][\da-zA-Z]{2}){5}$/', $mac);
    }
    
    protected $_broadcastIp;
    
    protected $_port;
    
    public function __construct($broadcastIp = null, $port = null) {
        $this->setBroadcastIp($broadcastIp)->setPort($port);
    }
    
    public function getBroadcastIp() {
        return $this->_broadcastIp;
    }
    public function setBroadcastIp($ip) {
        $ip = (string)$ip;
        if(empty($ip)) $ip = self::DEFAULT_BROADCAST_IP;
        $this->_broadcastIp = $ip;
        return $this;
    }
    public function getBroadcastUrl() {
        return 'udp://' . $this->getBroadcastIp();
    }
    
    public function getPort() {
        return $this->_port;
    }
    public function setPort($port) {
        if($port == null) $port = -1;
        $port = (int)$port;
        if($port < 0) $port = self::DEFAULT_PORT;
        $this->_port = $port;
        return $this;
    }
    
    public function sendTo($mac) {
        $data = self::macAddrToBytes(self::BROADCAST_MAC_ADDR);
        $mac_data = self::macAddrToBytes($mac);
        for($i = 0; $i < 16; $i++) $data .= $mac_data;
        
        $fp = @fsockopen($this->getBroadcastUrl(), $this->getPort(), $errno, $errstr);
        if( $fp === false ) {
            throw new Exception($errstr . '(' . $errno . ')');
        }
        fwrite($fp, $data);
        @fclose($fp);
        return $this;
    }
}

ソース中に特にコメント入れてないけど、結構コンパクトなコードなので読むのはそんなに難しくないかと。WoL自体はWikipedia@itの記事を見ればだいたいわかるし。

んで、使い方はかなり単純で、

  • コンストラクタ(またはsetter)でブロードキャストアドレスとポートを設定し、
  • sendToメソッドにターゲットのMACアドレスを渡す
だけ。

たとえばMACアドレスが「00-0D-59-B5-31-08」なんてPCがあって、起動すると192.168.0.0/24なアドレスが割り振られるネットワークにいたとすると、これをWoLで起動する場合は

<php
require_once 'WakeOnLan.php';

$wol = new WakeOnLan('192.168.0.255', 2304);
$wol->sendTo('00-0D-59-B5-31-08');

てな感じで使う。あ、MACアドレスはハイフン区切りでもコロン区切りでも同じ動作をします。

ほんとはもうちょっと簡単

先の例ではご丁寧にブロードキャストアドレス/ポートとも指定したが、WoL発行元と同一セグメント上のPCを起こすなら「255.255.255.255」(=リミテッド・ブロードキャスト・アドレスっていうらしい)で問題ないし、ポートについても気休め程度のものらしいので、コンストラクタ引数を省略して

$wol = new WakeOnlan();
だけでもよい。

で、バッチで包む

例であげたような起動用PHPを作成してもいいんだけど、引数をそのまま流し込めばいいくらいなので、こんなバッチファイルで済ませられる。

@echo off
php -r "require_once 'WakeOnLan.php'; $wol=new WakeOnLan('%2'); $wol->sendTo('%1');"

これを「wol.bat」とか「wol.cmd」とかって名前でパスが通った場所に保存して(WakeOnLan.phpはinclude_pathが設定されているところにおいておけばいいでしょ)、
wol 00:0D:59:B5:31:08
とか、
wol 00:0D:59:B5:31:08 192.168.0.255
なんて感じで叩いたり、1行 - 1MACアドレスなテキストファイル作って、
for %m in ('type macaddr.txt') do @wol %m
みたいな感じで一斉起動とか。

余談

WakeOnLan.phpではMagicPacketを送出するのにfsockopen() → fwrite()だけで済ませてるけど、参考にさせてもらったこちらのコードではsocket_set_option()でブロードキャスト許可設定をしている。fsockopen()の場合にはいらないんだろうか。それともデフォルトでブロードキャスト許可されてるんだろうか。

ローカル(?)php.ini2009年05月22日 03時46分28秒

PHPで作ったアプリケーションと一緒にアプリに必要な設定のみを記述したphp.iniを配置して、

foreach(parse_ini_file('./php.ini') as $key => $value) {
    ini_set($key, $value);
}
なんてコードをindex.phpの先頭に差し込んで設定上書きしてみる、というのを思いついてみたのだが。まぁファイル名は別に「php.ini」じゃなくてもいいんだけど。

上記コードで確かに設定上書きは可能なんだけども、

  • 所詮ini_set()での設定なので、ini_set()で反映されない項目には使えない
  • リクエストで直接呼び出されるすべてのスクリプトに記述する必要がある
ってな具合に制約があるので、思ったほど便利じゃないような

まぁ、ZFのMVCアプリケーションなんかで、mbstring関連なんかの設定を切り出しておいたりするのには使えるか。

参照代入で列挙中要素を置き換える2009年05月14日 00時53分09秒

なんというか、今まで知らなかったというか気にしたこともなかったというか。

foreachループ処理で

$array = array( 'a' => 'a', 'b' => 'b', 'c' => 'c' );
foreach( $array as &$item ) {
    $item = strtoupper( $item );
}
print_r( $array );
なんてやると、
Array
(
    [a] => A
    [b] => B
    [c] => C
)
なんて結果が得られるのね。

当然foreach内の「as &$item」を「as $item」にすると、print_r()の結果は小文字のまま。 なるほど、明示的な参照代入って、こういう風に使うんだぁ。

ちなみにこれはZend_Db_Adapterかなんかのソース見てて発見した。

コントローラ関連のシーケンス図など2008年06月03日 02時56分26秒

久々のZFネタっす。といっても自分でなんか書いたりするわけじゃなく、よそ様で紹介されていた資料の紹介。すまぬ。

すばらしい図面が紹介されてた

PHPSPOTさんで紹介されてたZF-users.jpというZendFrameworkユーザ向けのハブサイトさんで、Zend_Controller/Zend_View関連の図面が紹介されてました。

このページで「シーケンス図 (http://www.kitpages.fr/zf_helper_plugin.php)」と「Zend_Controller と Zend_View 図でまとめ」という2つの図面が紹介されていて、どちらも非常に有用なのですが、特に「シーケンス図」がお勧め。個人的に。

ソース追っかけるときのお供に

この図、ブラウザからリクエストが発生して、Zend Frameworkのコントローラ群が処理を行ってブラウザにレスポンスを返すまでの流れをあらわしていて、これを片手にZend/Controller/Front.phpから流れを追っかけてくとコントローラ周辺の処理がよく理解できると思います。

ソース読めばまぁわかるんだけどもちと迷いそうなところだけ補足しておきます。

プラグインブローカー

まずカーキ色のボックス「Plugins」を呼び出しているところは、実際は Zend_Controller_Plugin_Broker (フロントコントローラ内の「_plugins」)に対する呼び出しです。ブローカーはこれらのメソッドを呼ばれると、自身に登録されている各プラグインクラスの同名メソッドを順次呼び出す仕組みになっています。この機構を利用すれば、例えばdispatchLoopStartupをフックして、特定条件下では必ずログインフォームへナビゲートさせるような認証プラグインなんかを実装することができます(フックメソッド中にリクエストオブジェクトのコントローラ名/アクション名を上書きしたりもできるので)。

ディスパッチループ

「12: preDispatch」~「28: postDispatch」まで(背景がカーキになっている部分)は「ディスパッチループ」で、特定の条件下、例えばアクションコントローラで _forward() した場合は再びpreDispatchへ処理が移るような流れになっています。

具体的な処理についてはソースを見てみてください。Zend_Controller_Action::_forward でリクエストオブジェクト(Zend_Controller_Request_Abstract)に対して setActrionNameメソッドで次に実行するアクションを登録し、 setDispatchedメソッドにfalseを渡しているのがわかると思います。この「setDispatched(false)」が、ディスパッチループを繰り返すという決定を下している部分になります。

その他の部分

その他の部分は特に解説はいらないと思います。「Helpers」(=アクションヘルパー)の部分は考え方としてはプラグインブローカー/プラグインと同様に捉えられると思いますので。

まとめ

なんて具合に人様のコンテンツの尻馬に乗ってお茶を濁しましたが、本当はいずれこういうシーケンス図を書くつもりだったんですよ、いや、まじで。(つか、リファレンスマニュアルにこういうの載せてくれればいいのに)

今回紹介されていた2つの図面を見るとコントローラ(とビュー)の基本クラスの関連はぐっと把握しやすくなり、ソースを追っかけた場合の混乱も軽減されると思うので、ぜひ印刷するなりで手元においておいて、さらにZFのソースを読んでみることをお勧めします。

PHPのバージョンでZend_Jsonの動作が違っていた件2008年04月06日 04時05分27秒

Zend_Json::decode()って便利!と思っていたが

Zend_Json::decode()が割と使える。UNICODEエスケープされた文字(\uXXXX形式ね)をデコードできるからだ。

<?php
require_once 'Zend/Json.php';

$s = '"\u65e5\u672c\u8a9e"';
echo Zend_Json::decode( $s );

とかってすると、「日本語」と出力が得られる(内部エンコードがutf-8じゃないとあかんみたいだが)。

だもんで、クライアント側でJSでescape()したマルチバイト文字を、preg_replace_callbackを絡めて'%uXXXX'を'\uXXXX'に変換した上でダブルクォートで囲ってZend_Json::decode()に渡すようにして復元したりしていた。

が、Zend_Jsonの動きをたいして気にしていなかったため、ちみっとハマった。

PHPのバージョンの違いで、なんかヘン。

ある環境では上記のようなデコード処理がまったく問題なく動いていたのだが、他の環境で動かしたとたんに'\uなんて不正なエスケープだ!'とエラーがでるようになった。Zend Frameworkのバージョンはどちらも「1.0.0」を使っているのに。

文字コードの関連も、実行環境に依存しないように必ずdefault_charsetとmbstring.internal_encoding、mbstring.http_outputをコード中で指定してutf-8にあわせてあるし、違いといえばPHPのバージョン。

ためしに、

<?php
require_once 'Zend/Json.php';

$s = '日本語';
echo Zend_Json::encode( $s );

なんてのをやってみたところ、正常に動作する環境は
"\u65e5\u672c\u8a9e"
とUNICODEエスケープで出力されたが、うまく動かない環境のほうでは
"日本語"
と、まんまで出力されている。はて。

ソースを覗いてみたら

エンコード部分で動作に違いがでたので、Zend/Json/Encoder.php(Zend_Json_Encoder)のソースを見てみた。該当するのは _encodeString プロテクトメソッドか。

/**
 * JSON encode a string value by escaping characters as necessary
 *
 * @param $value string
 * @return string
 */
protected function _encodeString(&$string)
{
    // Escape these characters with a backslash:
    // " \ / \n \r \t \b \f
    $search  = array('\\', "\n", "\t", "\r", "\b", "\f", '"');
    $replace = array('\\\\', '\\n', '\\t', '\\r', '\\b', '\\f', '\"');
    $string  = str_replace($search, $replace, $string);

    // Escape certain ASCII characters:
    // 0x08 => \b
    // 0x0c => \f
    $string = str_replace(array(chr(0x08), chr(0x0C)), array('\b', '\f'), $string);

    return '"' . $string . '"';
}

(Zend/Json/Encoder.php より抜粋)
はて、マルチバイト文字の扱いとか特にやってる風ではないな。どないなっとんねん。

じゃあ、実際にコードから叩いているZend_Jsonのほうを見てみるか。

/**
 * Encode the mixed $valueToEncode into the JSON format
 *
 * Encodes using ext/json's json_encode() if available.
 *
 * NOTE: Object should not contain cycles; the JSON format
 * does not allow object reference.
 *
 * NOTE: Only public variables will be encoded
 *
 * @param mixed $valueToEncode
 * @param boolean $cycleCheck Optional; whether or not to check for object recursion; off by default
 * @return string JSON encoded object
 */
public static function encode($valueToEncode, $cycleCheck = false)
{
    if (function_exists('json_encode') && self::$useBuiltinEncoderDecoder !== true) {
        return json_encode($valueToEncode);
    }

    require_once 'Zend/Json/Encoder.php';
    return Zend_Json_Encoder::encode($valueToEncode, $cycleCheck);
}

(Zend/Json.php より抜粋)
ほあ!?json_encode()??

はぁ、PHP5.2.0からだったのねん

同様にZend_Json::decode()部分もjson_decode()が存在していたらそっちを利用するようになっていた。調べてみるとこの2つの関数は、JSON関数として、PHP5.2.0からは標準でインストールされるようになったPECL拡張モジュールで提供されている関数だったと。

先ほどの2つの環境、うまく動かないほうは5.1.6、正常なほうは5.2.5だもんで、なるほどこの通りになるのか。

先ほどのjson拡張モジュール自体はPHP4.3.0以降に適合するので、それをインストールすれば同様の動作になるけど、Zend_Json関連を使うときは一応PHPのバージョンを気にしておいたほうがよいかも。

オマケ

前半部分でescape()した文字のデコード目的で使用、ってな話を書いたけど、「escape()って必ずUNICODEエスケープなのか?」ってのに自信がなくなったので調べてみたら、こんな一覧表が見つかった。

なるほど現在普通に使われるようなブラウザならたいていUNICODEエスケープとみて間違いないかな(MacユーザでiCab使ってる人いたらごめんなさい)。