Ansible をゆるふわに使う

先日、社内勉強会のようななにかで Ansible のことを話しました。

当初は、role を使わずにプレイブックからプレイブックを include する方が簡単なのでよほど大きな構成にならない限りは role は使わないほうが良いよー

というコンセプトだったのですが、業務で Ansible を使っているうちに role の方がいろいろ捗るような気がしてきたので途中で方向性を変えました。

プレイブック1枚だけで完結する程度であれば role は不要かもしれませんが、分割したくなったらとりあえず role にしておけばいいと思います。


最後の方に書いていますが、あんまり複雑なことはしないほうが良いです。じゃないと、

  • 手段と目的が逆転する
    • Ansible を使うことそのものが目的になってる
  • メリットが感じられなくなってモチベーションがダダ下がり
    • プレイブックのメンテにかけるトータルの時間より手作業の方が短くね?
    • そんな頻繁にサーバを再構築しないですしおすし

となり、やる気がゴリゴリ削がれていきます。

shell モジュール(とか command とか)はどうしても使わざるをえないことはありますが、ファイルの中身を弄くる系のモジュールはめんどくさいのでなるべく使わないほうが良いです。

また、今年のはじめの頃の Jenkins カンファレンスに参加したときに誰かが言っていましたが、

自動化であまり複雑なことをするとむしろ属人性を高める

と思います。

ssh で接続して vim である設定ファイルをちょっと変更して service XXX restart、ぐらいなら誰にでもできますが、ansible-playbook -i prod.ini all.yml と入力して Enter するのはなかなか誰にでもできる作業ではありません(心理的な意味で)。


スライドにはありませんが、role の中の files/ について。

例えば php.ini を置く場合、当初はサーバでのパスと同じようにディレクトリを掘っていました。

roles/
  php/
    files/
      etc/
        php.ini

が、ディレクトリ階層が無駄に深すぎじゃない? と思ったので、次のようにフラットにしました。

roles/
  php/
    files/
        php.ini

問題は、このディレクトリ以外のファイルも置きたい場合です。例えば php のエラーログをローテートするための logrotate の設定を置きたい場合、最初のサーバのパスそのままの構成であれば次のようになりますが、

roles/
  php/
    files/
      etc/
        php.ini
        logrotate.d/
          php

フラットにすると次のようになり、わけがわからなくなります。

roles/
  php/
    files/
        php.ini
        php

ローカルのファイル名とサーバのファイル名が一致している必要はないので、次のようにしてみました。

roles/
  php/
    files/
        php.ini
        logrotate

さらに /etc/php.d/ にもファイルをコピーしたい場合、流石にこいつらまでフラットに置くのはつらいので、次のようにしています。

roles/
  php/
    files/
        php.ini
        logrotate
        php.d/
          00-aaa.ini
          00-bbb.ini
          00-ccc.ini

この構成なら php.d/ に with_fileglob も使えますし(ただ、with_fileglob は滅多に使いません)。


前述の通り、with_fileglob は滅多に使いません。なぜなら・・・

ローカルのファイルを削除しただけでサーバのファイルも削除されることを期待してしまう

からです。

synchronize を使う案も考えましたが・・・

php.d/ には自分で管理しないファイル(yum install で配置されたものを放置)もあり、あとで php 拡張モジュールを追加したときに synchronize で消されてしまう事故があったので、使うのをやめました。

with_fileglob の対象となっているディレクトリからファイルを削除した場合、

なんらかの方法でサーバからそのファイルを削除しなければならない

ので、それなら with_items で1つ1つ指定しておいて、ファイルを削除したときには task の方も修正しなければならないようにしておいた方が事故りにくいです(task を修正しなければプレイブックの実行時にコケるので)。

PhpStorm の PHPSTORM_META でサービスロケーターとかを入力補完

先日 Twitter を眺めていたら、PhpStorm Advanced Metadata というものを知ったので使ってみました。

下記はいわゆる社内勉強会的ななにかで話したときのスライドです。

スライドの通りですが、.phpstorm.meta.php という名前で次のようなファイルを用意しておくと、メソッドの引数に指定した文字列リテラルを元に戻り値の型が認識されるようになります。

<?php
namespace PHPSTORM_META {
    /** @noinspection PhpUnusedLocalVariableInspection */
    /** @noinspection PhpIllegalArrayKeyTypeInspection */
    /** @noinspection PhpDynamicAsStaticMethodCallInspection */
    $STATIC_METHOD_TYPES = array(
        \SL::get('') => array(
            'ore' instanceof \Ore,
            'are' instanceof \Are,
        ),
    );
}

そのため、次のようなコードでも入力補完が効きます。

<?php
class SL
{
    /**
     * @param string $name
     * @return mixed
     */
    public function get($name) {}
}

class Ore
{
    public function this_is_ore() {}
}

class Are
{
    public function this_is_are() {}
}

$o = new SL();
$o->get('ore')->this_is_ore();
$o->get('are')->this_is_are();

サービスロケーターを使っていると、オブジェクトのインスタンスをサービスロケーターから取得するたびにいちいち /* @var $ore Ore */ のように書く必要がありましたが、.phpstorm.meta.php をうまいこと作成するようにしておけばそんな必要はなくなります。

EC2 で CentOS 6 の HVM の AMI をゼロから作る

最近 AWS Marketprice で CentOS 7 の HVM の AMI が使えるようになりましたが、CentOS 6 の HVM の AMI は AWS Marketprice にはまだありません。

CentOS 6 でも t2.micro インスタンスが使いたかったので、ゼロから AMI を作成してみました。

概ね参考記事の手順そのままですが、極力最小の手順にしています。

参考記事

作業用インスタンスとボリュームの作成

Management console で次の手順で作業用のインスタンスと EBS を作成します。

作業用インスタンスでディスクイメージの作成

作業用インスタンスssh でログインし、アタッチした EBS に CentOS 6.5 x64 のディスクイメージを作成します。

フォーマットとマウント

messages の最後の方にアタッチされた EBS のデバイス名があるので確認します。

tail /var/log/messages
Oct 25 12:11:57 ip-xx-xx-xx-xx kernel: blkfront: xvdj: barriers disabled
Oct 25 12:11:57 ip-xx-xx-xx-xx kernel: xvdj: unknown partition table

わからなければ fdisk -l で調べてください。

パーティションを作成します。parted は使い方を知らないので fdisk を使います。

fdisk /dev/xvdj

フォーマットします。

mkfs.ext4 /dev/xvdj1

パーティションにラベルを付けます。下記の手順では / というラベルを付けています。

e2label /dev/xvdj1 /

マウントします。

mount /dev/xvdj1 /mnt
cd /mnt

CentOS 6.5

ディスクのマウント先である /mnt に yumCentOS 6.5 をインストールします。

作業用のインスタンスCentOS 6.5 なので yum の設定ファイルを作る必要はありません(作業用インスタンスの設定がそのまま使えます)。

acpid を入れているのは、入れておかないと Management console から Stop や Reboot したときに正常なシャットダウンやリブートが行なわれないような気がするからです(KVM はそうだった気がする)。

yum -y --installroot=/mnt --releasever=6.5 install @core kernel grub acpid

grub

grub.conf を作成します。console=ttyS0 を追加しておかないと Management console の Get System Log でブートのログが表示されません。

cat <<EOS> boot/grub/grub.conf
default=0
timeout=0
hiddenmenu
title CentOS 6.5 x86_64
        root (hd0,0)
        kernel /boot/vmlinuz-$(rpm --root=/mnt -q --queryformat "%{version}-%{release}.%{arch}" kernel) ro root=LABEL=/ console=ttyS0
        initrd /boot/initramfs-$(rpm --root=/mnt -q --queryformat "%{version}-%{release}.%{arch}" kernel).img
EOS

menu.lst のシンボリックリンクを作成します。

ln -s grub.conf boot/grub/menu.lst

grub をインストールします。まずは grub の stage ファイルをコピーします。

cp -a usr/share/grub/x86_64-redhat/* boot/grub/

作業用インスタンスの /dev を /mnt/dev にマウントします。

mount --bind /dev /mnt/dev

/mnt に chroot して grub をインストールします。

cat <<EOS | chroot /mnt grub --batch
device (hd0) /dev/xvdj
root (hd0,0)
setup (hd0)
EOS

/mnt/dev をアンマウントします。

umount /mnt/dev

SELinux

SELinux を無効にします。

sed -i '/^SELINUX=/c SELINUX=disabled' etc/selinux/config

IPv6

IPv6 を無効にします。

cat <<EOS>> etc/sysctl.conf

# ipv6 disable
net.ipv6.conf.all.disable_ipv6 = 1
net.ipv6.conf.default.disable_ipv6 = 1
EOS

fstab

fstab を作成します。

cat <<EOS>> etc/fstab
LABEL=/     /           ext4    defaults        1 1
devpts      /dev/pts    devpts  gid=5,mode=620  0 0
tmpfs       /dev/shm    tmpfs   defaults        0 0
proc        /proc       proc    defaults        0 0
sysfs       /sys        sysfs   defaults        0 0
EOS

いろいろコピー

作業用のインスタンスからいろいろコピーします。

\cp -a /etc/sysconfig/network-scripts/ifcfg-eth0  etc/sysconfig/network-scripts/ifcfg-eth0
\cp -a /etc/sysconfig/network                     etc/sysconfig/network
\cp -a /etc/rc.d/rc.local                         etc/rc.d/rc.local
\cp -a /etc/ssh/sshd_config                       etc/ssh/sshd_config

アンマウント

ディスクイメージの作成は終わったので /mnt をアンマウントします。

cd
umount /mnt

AMI 作成

Management console で次の手順で AMI を作成します。

  • EBS ボリュームをインスタンスからデタッチする
  • EBS ボリュームからスナップショットを作成する
  • スナップショットから AMI を作成する
    • 以下の箇所に注意して作成します
      • Architecture: x86_64
      • Virtualization type: Hardware-assisted virtualization
      • Root device name: /dev/sda1
      • Kernel ID: Use default
      • RAM disk ID : Use default

AMI からインスタンスを作成

作成した AMI から t2.micro インスタンスを作成し、普通に起動すれば成功です。

なぞ?

AWS で実際に作業する前に、手順の確認のために同じような方法で VirtualBox で既存のゲストにアタッチしたディスクにブート可能な CentOS 6.5 のディスクイメージを作ってみたのですが、なぜか初回起動時に SELinux が自動的に enforcing になりました。

しかも、ssh はもとよりコンソールからもログインできません(ログインすると直後に切断される)。

シングルユーザーモードでブートして /etc/selinux/config を見てみると enforcing になっていました。もちろんディスクイメージの作成時には /etc/selinux/config を disabled に書き換えています。タイムスタンプを見てみると、ブート時の時刻になっています。

ディスクイメージに作成時に selinux-policy と selinux-policy-targeted を削除しておいたり、あるいは、カーネルパラメータで enforcing=0 と指定しておいたりすれば大丈夫ですが、VirtualBox で初回起動時に SELinux が必ず有効になるような機構でもあるのでしょうか。。。?

Eclipse PDT + MakeGood でも SSH 経由で PHPUnit を実行する

PhpStorm 8 が正式にリリースされましたね。

PHP Remote Interpreters というものが実装されたので PHPUnitSSH 経由で実行するのがとても簡単になりました。

が、少し触ってみたところちょっと微妙なことろもありました。下記の記事にも記載されていますが Test scope に Directory や Class や Method を指定するとローカルとリモートのパスの違いでエラーになります。

この記事ではシンボリックリンクでどうにかする方法が紹介されていますが、ローカルが Windows でリモートが Linux な環境で開発している私のようなキチガイにそれは難しいです。なんせ D:\path\to\hoge みたいなパスですから。

わりと Ctrl+Shift+F10 でクラスやメソッド指定でテストを実行することがあるので、この機能が使えないと困ります。なので少し前に書いた下記の方法をしばらくは使っていこうと思ってます。

なお、上記の記事に埋め込んでいる Gist のコードは少し古いです。コードは こっち に移しました。使い方とかは多分変わっていませんが、まぁ自分しか使わない俺得なものなので細かい説明はなくてもいいでしょう。

↓追記 2014/09/19

なわけないですね、キチンと動作します。

ローカルとリモートのパスのマッピングのための Deployment の設定が必要なのですが、私は cifs でローカル~リモートを共有しているので Deployment は設定していませんでした。 Deployment でマッピングを設定するとうまく動作しました。ただ cifs で共有している状態で本当に PhpStorm でデプロイしてしまうと、コピー元とコピー先が同じファイルであるがゆえにファイルの中身が消し飛んでしまったりするのでできれば設定したくなかったです。

↑追記 2014/09/19

.

.

.

それはそれとして、自社内では Eclipse PDT の方が主流っぽいので Eclipse PDT + MakeGood でも同じようなことをやってみました。

私自身もう Eclipse PDT は使っていないので本当に誰得ですが、せっかくなので記事にしました。

Eclipse PDT + MakeGood での設定

phpunit と stagehand-testrunner を composer でインストールします。

stagehand-testrunner は MakeGood から phpunit を実行するときのランチャーのようなもので MakeGood をインストールすると一緒に含まれていますが、リモートにも必要なので composer でインストールします。

忘れがちなのが stagehand-testrunner を composer でインストールした後は vendor/bin/testrunner compile する必要があります。

次に MakeGood をそれっぽく設定します。

まずは プリロードスクリプト に後述する remote-makegood.php を指定します。また、PHPUnit タブの XML設定ファイルphpunit.xml が指定できるようになっていますが、ここでは指定しないでください。phpunit.xml は後述の remote-makegood.local.php で指定します。その他は普通に設定してください。

次に remote-makegood.php と同じディレクトリに remote-makegood.local.php を作成します。

<?php
// SSH のログインユーザー
$remote_user = 'ore';

// リモートホスト名
$remote_host = 'ore-no-server';

// リモートのパス
$remote_dir = '/home/ore/work';

// ローカルのパス
$local_dir = dirname(__DIR__);

// プリロードスクリプトのパス
$preload_script = 'vendor/autoload.php';

// phpunit.xml のパス
$phpunit_config = 'phpunit.xml';

$remote_user$remote_host$remote_dir$local_dir は見ての通りのものです。

$preload_script は MakeGood の設定でプリロードスクリプトに指定するはずだったものです。プリロードスクリプトには remote-makegood.php を指定する必要があるので、本来指定するはずだったものをここで指定します。普通は vendor/autoload.php のような PHPUnit をオートロードするためのものが指定されると思います(後述の通りこの方法なら vendor/autoload.php をプリロードする必要はありませんが)。

$phpunit_config には phpunit.xml のパスを指定します。私の実装がクソいので 諸事情により MakeGood の設定で phpunit.xml を指定するとうまく動作しなくなります。その代わりに remote-makegood.local.phpphpunit.xml を指定してください。なお、キチンと実装すれば MakeGood の設定で指定した phpunit.xml をそのまま活かすこともできるはずです。

なお、$preload_script$phpunit_config も、$local_dir からの相対パスで指定します。

正しく設定されていれば MakeGood でテストを実行したときに ssh 経由でテストが実行されて結果が IDE 上に表示されます。

なお、デバッグ実行には対応していません。これは実装が手抜きだからであって、頑張れば不可能ではないと思います。

remote-makegood.php のざっくり解説

PhpStorm でやったときと比べるとかなり手抜きな実装になっています。私が常用するものではないので。。。

前述の通り MakeGood は phpunit を直接実行するのではなく stagehand-testrunner というランチャーを利用して phpunit を実行します。ちなみに次のような $argv が渡されます(設定とか実行時の状況によって引数は増えたり減ったりします)。

Array
(
    [0] => D:\app\eclipse-php-luna-R-win32\plugins\com.piece_framework.makegood.stagehandtestrunner_3.1.1.v201409021510\resources\php\bin\testrunner.php
    [1] => --no-ansi
    [2] => phpunit
    [3] => -p
    [4] => D:/ore/devel/phpunit-via-ssh-on-ide/tests/remote-makegood.php
    [5] => --log-junit=C:\Users\ore\AppData\Local\Temp\com.piece_framework.makegood.launch\MakeGood1410358831955.xml
    [6] => --log-junit-realtime
    [7] => -R
    [8] => --test-file-pattern=Test(?:Case)?\.php$
    [9] => D:/ore/devel/phpunit-via-ssh-on-ide/tests
)

テストコードのパス $argv[9] はローカルのパスからリモートのパスに書き換えるだけです。

プリロード $argv[4]vendor/autoload.php とかに書き換えます。stagehand-testrunner を composer からインストールしていれば vendor/autoload.php は勝手に読まれるので、実はプリロードで vendor/autoload.php を読む必要はありません。

$argv[5]--log-junit は少し厄介です。

MakeGood はこのファイルを監視していて、このファイルに出力された内容を元に IDE にテスト結果を表示しているようです。

PhpStorm で SSH 経由で PHPUnit を実行させたときはリモートの標準出力(正確には PHP の出力、いわゆる php://output)をローカルの標準出力に書き込むだけで良かったのですが、MakeGood の場合はリモートの標準出力と標準エラーと --log-junit の合計で3つのストリームをどうにかする必要があります。

ssh 経由なので標準出力と標準エラーで2本のストリームが使えます。なので、リモートの標準出力と標準エラーはローカルの標準出力へ、--log-junit はローカルの標準エラーを経由して本来の出力先にリダイレクトすることにします。

また、--log-junitIDEスタックトレースなどをローカルのパスで表示するためにパスの書き換えも行います。

まず、リモートの標準出力と標準エラーをローカルの標準出力に流す方法ですが、proc_open$desc で次のように指定しました。

$desc = array(
    0 => array('file', '/dev/null', 'r'),
    1 => array('file', 'php://stdout', 'w'),
    2 => array('file', 'php://stdout', 'w'),
);

$proc = proc_open($cmd, $desc, $pipes);

--log-junit への書き込みを標準エラーに流す方法は、簡単な方法が思いつかなかったので FIFO(名前付きパイプ)を使いました。

$tmp = tempnam(sys_get_temp_dir(), "makegood-junit-");
unlink($tmp);
posix_mkfifo($tmp, 0600);

try {
     :

    $fifo = fopen($tmp, 'r');
    $stderr = fopen('php://stderr', 'w');

    while (strlen($data = fread($fifo, 1024)) !== 0) {
        $data = str_replace($remote_dir, $local_dir, $data);
        fwrite($stderr, $data);
    }

     :
} finally {
    unlink($tmp);
}

remote-makegood.php

remote-makegood.php の全文は次のとおりです。

cmd.exe のコマンドラインの解釈と Windows と Linux のプロセス作成の違い

PHPescapeshellarg の実装が Windows だと謎すぎたので調べていたら下記の記事にたどり着きました。

わりと長いこと Windows を使っているつもりなのですが知らなかったこともあり、とても興味深いとともに、もう cmd.exe には関わりたくないと思いました。

環境変数の展開

フェーズ1の環境変数%APPDATA% みたいなの)の展開は、フェーズ2のコマンドラインの解釈やフェーズ3の特殊文字の処理よりも先にあったんですね、知りませんでした。

例えば %APPDATA% のような文字をコマンドに渡したい場合、次のように書いていましたが・・・

> php -r "echo $argv[1] . '[EOF]' . PHP_EOL;" -- ^%APPDATA^%
%APPDATA%[EOF]

これは %^(キャレット)でエスケープしているからではなかったんですね。

いや、エスケープしているといえばエスケープしているのですが、^%特殊文字としての効果を打ち消しているわけではなく、単に APPDATA^ などという環境変数は存在しないから展開されないのですね。

なので、2つあるキャレットのうち、先頭部分は意味がないです。次のように2番目のキャレットだけで環境変数の展開を抑止できます。

> php -r "echo $argv[1] . '[EOF]' . PHP_EOL;" -- %APPDATA^%
%APPDATA%[EOF]

逆に1番目のキャレットだけだと環境変数が展開されます。

> php -r "echo $argv[1] . '[EOF]' . PHP_EOL;" -- ^%APPDATA%
C:\Users\oreore\AppData\Roaming[EOF]

また、次のようにシェルの特殊文字環境変数に入れた場合、その特殊文字は有効です。

> set PIPE=^|
> echo hogehoge %PIPE% php -r "echo 'PHP: '; stream_copy_to_stream(STDIN, STDOUT);"
PHP: hogehoge

bash だとそうはならないです。

$ export PIPE="|"
$ echo hogehoge $PIPE php -r "echo 'PHP: '; stream_copy_to_stream(STDIN, STDOUT);"
hogehoge | php -r echo 'PHP: '; stream_copy_to_stream(STDIN, STDOUT);

ダブルクオートのエスケープ

ダブルクオートを文字として渡したい場合、ダブルクオートの中でダブルクオートを2つ繋げていました。

> php -r "echo $argv[1] . '[EOF]' . PHP_EOL;" -- "A""B""""C"
A"B""C[EOF]

が、バックスラッシュでもできたんですね。なぜか知りませんでした。

> php -r "echo $argv[1] . '[EOF]' . PHP_EOL;" -- A\"B\"\"C
A"B""C[EOF]

がしかし、バックスラッシュの次の動作はキモいです。

> php -r "echo $argv[1] . '[EOF]' . PHP_EOL;" -- \\\\\\\\\"
\\\\"[EOF]
> php -r "echo $argv[1] . '[EOF]' . PHP_EOL;" -- \\\\\\\\\
\\\\\\\\\[EOF]

バックスラッシュは " に前置で連続しているときだけ \ 自身をエスケープし、それ以外の \ はエスケープしません。

Windows は UNC で \\ComputerName\SharedFolder のような記述をするので、バックスラッシュ単独ではエスケープされないようにしたかったからでしょうかね、予想ですけど。

WindowsLinux のプロセス作成のコマンド引数の違い

どちらかといえば最初は Windows 畑で育ったわたしは元々知っていましたが、参考記事のコメントにも書かれている通り Windows ではプロセスを作成するときのコマンド引数の指定の方法が Linux とは全く違います。

Linux だとコマンド引数は呼び出し側(通常はシェル)がコマンド引数の文字列を配列にバラします。

system(3) だとコマンドラインの文字列そのものを指定しますが、これはシェルを経由するからであって、コマンド引数の文字列はシェルが配列にバラしています。

PHP で言うと pcntl_exec だけはコマンド引数を配列で渡すのに対して、その他の普通のプロセス実行系の関数はコマンドライン全体を文字列として渡します(shell_exec など)。これは、pcntl_exec 以外はすべてシェルを経由するからです。

node.js なら child_process.execchild_process.spawn にその違いが現れています。

他の言語でもたぶん同じです。シェルを経由するのであればコマンドライン全体を単一の文字列で指定し、シェルを経由しないのであればコマンドのパスと引数の配列を指定します。もし、シェルを経由しないにも関わらずコマンド引数を配列ではなく文字列で指定できるのであれば、それはホスト言語が自前で配列にバラしているはずです。

一方、Windows だとプロセスを作成するときには、実行するバイナリのパス、と、コマンド引数、の2つの文字列を指定します(文字列と文字列の配列、では無く)。

なので、パイプやリダイレクト、および、実行するバイナリのパスとその後の文字列の分割、までは cmd.exe の仕事ですが、コマンド引数の文字列を配列 argv[] にバラすのは呼び出された側の仕事です。

C言語なら CRT の仕事です。MSVC なら真のエントリポイントである mainCRTStartup が配列にバラして main 関数の argv[] にセットします。おそらく CommandLineToArgvW 関数 が使われているのではないでしょうか。参考記事によると ruby は独自に解釈しているっぽいし、gcc でビルドしたバイナリも解釈が異なるようです。

ソースを確認したわけではないですが、rubymain 関数の argv[] は無視して GetCommandLine 関数 かなにかでコマンドラインを取得して独自に解析しているのではないでしょうか? まさか ruby のための CRT がある、なんてことは無いと思いますので。

Linux でもシェルによってコマンド引数の文字列を配列にバラすルールは微妙に違うかもしれませんが、POSIX の sh で最低限の仕様は担保されているのだと思います(system(3) なんかはユーザーのデフォルトシェルに関わらず /bin/sh が呼ばれるんでしたっけ?)。

が、Windows だと実行するコマンドによってその動作はバラバラなので、汎用的な escapeshellarg 関数など作りようがありません。

コマンド引数に改行を含める

参考記事には記述がありませんでしたが、cmd.exe ではキャレットを改行の直前に記述するとその改行が除去されます。

> php -r "echo $argv[1] . '[EOF]' . PHP_EOL;" -- A^
More? Z
AZ[EOF]

Bash で言うところの改行の直前の \ と同じです。

More? はプロンプトの文字なので無視してください。バッチファイルの方が判りやすいですね。

a.bat

@php -r "echo $argv[1] . '[EOF]' . PHP_EOL;" -- A^
Z
> a.bat
AZ[EOF]

また、キャレットの後の改行の直後が改行だと、改行になります。つまり "^\n\n" が改行になります。

a.bat

@php -r "echo $argv[1] . '[EOF]' . PHP_EOL;" -- A^

Z
> a.bat
A
Z[EOF]

改行を複数入れたければ次のようにします。

@php -r "echo $argv[1] . '[EOF]' . PHP_EOL;" -- A^

^

^

Z
> a.bat
A


Z[EOF]

が、しかし、MSVC の system 関数だと改行以降の文字が無視されました。

a.c

#include <stdio.h>
#include <stdlib.h>

int main()
{
    system("php -r \"echo $argv[1] . '[EOF]' . PHP_EOL;\" -- A^\n\nZ");
    return 0;
}
> a.exe
A[EOF]

うーん、謎。

さいごに

とりあえずの結論としては、WindowsPHP では escapeshellarg は使わないほうが良いと思います。あるいはメタ文字っぽいものが無いことをあらかじめチェックした上で使った方が良いです。メタ文字を含む文字をコマンドに渡すために使う関数なのにメタ文字が渡せないなんて本末転倒もいいところですが。

あるいは proc_openbypass_shell オプション付きで使い、参考記事のフェーズ4のメタ文字だけどうにかすれば良いです。

プロセス実行系の関数に動的な文字を含めるようなことをしないのが一番良いのは間違いないですけどね。

PhpStorm 7 で SSH 経由で PHPUnit を実行する

下記の記事によると PhpStorm 8 では SSH 経由で PHPUnit が実行できるようです。

ホストが Windows で開発環境の VMLinux(CentOS) なわたしは PhpStorm から PHPUnit が実行できるようにするためだけに Windows でも Linux でも動作するコードを書くように心がけているのですが(主に int のサイズ)、PhpStorm 8 がリリースされればそのような苦行とはさよならできそうです、やったね。

.

.

.

それはそれとして、思いつきで PhpStorm 7 でも SSH 経由で PHPUnit が実行できるようにしてみました。例外処理とかしていないやっつけですが PHPUnit を実行するだけなのでまぁいいかなと。

前提

  • ローカルホストは Windows
  • リモートホストLinux
    • php にはパスが通っている
  • PhpStorm で Deployment の設定ができている
    • あるいはローカルとリモートでディレクトリが共有されている
  • PhpStorm で ローカルホストで PHPUnit が実行できるように設定済
    • composer で phpunit をインストール
    • [PHP -> PHPUnit] で必要な設定を行う
  • [PHP -> Servers]マッピングの設定名はリモートホストのホスト名の最初のセグメントと一致する
    • abc.example.net なら abc という設定名にする

方法

PhpStorm の メインメニューから [Run -> Edit Configurations] を選択します。

[PHPUnit] の下にある設定をすべて削除します。

img:PHPUnit の下にある設定をすべて削除

[Defaults -> PHPUnit] を選択して次のように設定します。

img:Defaults -> PHPUnit を選択

  • Interpreter options: -d include_path=. -d auto_prepend_file=remote.php
  • Custom working directory: プロジェクトのルートディレクトリ

要するに PhpStorm で PHPUnit を実行するときに auto_prepend_fileremote.php が実行されれば OK です。

remote.php を次の内容で作成します。先頭の3つの変数は適当に書き換えてください。あるいは同じディレクトリに remote.local.php を作って、そのファイルに3つの変数を書くのでも OK です。

  • $remote_hostリモートホストの IP アドレスやホスト名
  • $remote_dirリモートホストのデプロイ先ディレクトリ
  • $local_dir はローカルの PhpStorm のプロジェクトのディレクトリ

PhpStorm で PHPUnit を実行します(PHPUnit on Server じゃなくてただの PHPUnit)。PHPUnitSSH 経由で実行され、PhpStorm や Run や Debug のコンソールに結果が表示されます。

次のような Windows では通らないテストも・・・

グリーンです。

img:グリーンです

テストがコケたときのスタックトレースも、それっぽく書き換えているので PhpStorm でクリックすることができます。

img:スタックトレースもクリックできる

リモートデバッグをしたいときは [Start Listen for PHP Debug Connections] を On にしてデバッグ実行してください。

img:Start Listen for PHP Debug

[PHP -> Servers] でパスのマッピングを行っていれば、ブレークポイントを仕込んだところで止まります。

img:ブレークポイント


PhpStorm から PHPUnitデバッグ実行したとき、本当なら XDEBUG_CONFIG で idekey が渡されるのですが、その idekey で xdebug.remote のセッションを開始してもパスのマッピングが解決できない(PhpStorm は xdebug からローカルのパスで通知されると思っている)ため、PHPUnitデバッグセッションではなく PHP Remote Debug のデバッグセッションとしてアタッチする必要があります。

ストリームラッパーを使って素の PHP で自動エスケープ

素の PHP をテンプレートとして使うフレームワークは今でも結構あるようです。

素の PHP をテンプレートに使う、というのがどういうことかというと、簡単な例ですが次のようなものです。

index.php

<?php
$name = filter_input(INPUT_GET, 'name');
include __DIR__ . '/index.html.php';

index.html.php

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title></title>
</head>
<body>
Hello <?= $name ?>
</body>
</html>

php -S localhost:8888 でビルドインウェブサーバを起動して http://localhost:8888/?name=oreore にアクセスすると Hello oreore と表示されることでしょう。

しかし、↑のコードは $name がエスケープされておらずよろしくありません。 http://localhost:8888/?name=%3Cscript%3Ealert%28%27!!!%27%29%3C/script%3E などとアクセスすると !!! と alert されてしまいます。

これを防止するためには、テンプレートで変数を表示するときに htmlspecialchars をかます必要があります。

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title></title>
</head>
<body>
Hello <?= htmlspecialchars($name, ENT_QUOTES) ?>
</body>
</html>

さすがに毎度 <?= htmlspecialchars($name, ENT_QUOTES) ?> などと書くのはめんどくさいので、フレームワークによっては別の書き方が提供されています。

例えば・・・

  • Zend Framework<?= $this->escapeHtml($name) ?> とすればエスケープされます
    • あまり楽になっている気がしないです
  • CakePHP<?= h($name) ?> と書けば良いらしいです
    • こういうグローバル関数がたくさん定義されているようです
  • FuelPHP は変数をビューにアサインしたときに自動でエスケープされるようです
    • Security::htmlentities を見た感じオブジェクトをアサインするときに困りそうです

Twig も Smarty も自動的にエスケープする機能を持っており、かつ、FuelPHP のようなオブジェクトをアサインしたときの問題もありません。素の PHP でも同じようなことがやりたいです。。。

.

.

.

やってみました。

index.php

<?php
require __DIR__ . '/../vendor/autoload.php';

use PhpRenderer\Renderer;

$data = [
    'name' => "<script>alert('!')</script>",
    'html' => "<strong>safe string</strong>",
];

echo (new Renderer)->render(__DIR__ . '/index.html.php', $data);

index.html.php

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title></title>
</head>
<body>
Hello <?= $name ?>.
<br>
None Escape <?php echo $html ?>
</body>
</html>
  • <?= $name ?> のようなショートタグを使うと自動的にエスケープされます
  • <?php echo $html ?> のように普通の PHP タグを使うとエスケープされません

↑の例は次のような HTML になります。

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title></title>
</head>
<body>
Hello &lt;script&gt;alert('!')&lt;/script&gt;.
<br>
None Escape <strong>safe string</strong></body>
</html>

実装方法

PHP にはストリームラッパーという便利なものがあります。fopen などのファイルシステム関数に http:// とかを渡すことができるあれです。

実は require や include にもストリームラッパーが使えます(いくつかのストリームラッパーは allow_url_include を on にしなければ使えません)。

例えば compress.zlib ストリームラッパーで gzip 圧縮された PHP のコードを include してみます。

gzip.php

<?php
echo "this is gzip\n";

index.php

<?php
include 'compress.zlib://' . __DIR__ . '/gzip.php.gz';
$ gzip gzip.php
$ php index.php
this is gzip

そして、ストリームラッパーは独自のものを新たに登録することもできます。

なので、独自のストリームラッパーを作れば require や include するときに PHP のコードをプリプロセスすることができます。

実装コード

実装したコードは下記にあります。

StreamWrapper.php がストリームラッパーの実装クラスです。php.renderer:// のようなスキームで呼べるように登録します。

rewrite()PHP のコードを書き換えています。 正規表現でざっくり はさすがに怖い気がしたので token_get_all() の字句解析の結果を元に T_OPEN_TAG_WITH_ECHOT_CLOSE_TAG を書き換えています(<?=?> です)。

問題点

opcache

素の PHP ですが opcache は効きません。opcache は file:// と phar:// 以外のストリームラッパーはキャッシュしないようです。

なので、コードの書き換えは都度発生します。

ストリームラッパーを使わずに、書き換えたコードをどこかに保存してそのまま include すれば大丈夫ですが・・・当初の目論見ではストリームラッパーによって変換された後の opcode がキャッシュされるかなーと思ってたので残念です。

parse error

単純に

<?= $name ?>

<?=StreamWrapper::html( $name )?>

と書き換えているだけなので、

<?= $name; ?>

などと書かれると

<?=StreamWrapper::html( $name; )?>

となってパースエラーになります。

その他

普段まったく使わないのですっかり忘れていましたが asp_tags なんてものもありました。 asp_tags を有効にして <%= の形式のときだけ自動エスケープを行うのも良いかもしれません。

さいごに

社内で勉強会したときのスライド。若干ネタ成分あり。