ちーぶーうな

コードを書いたり写真を撮ったりします。

Vagrant + Chef でハマったところメモ

Vagrant で作る環境に、 anyenv を入れるのでちょっとハマったのでメモ。 使っている Vagrant は 1.7.2 で、 Vagrantfile には、

  config.vm.provision "chef_zero" do |chef|
    chef.add_recipe "anyenv::ndenv"
  end

というようなことを書いている。

anyenv install でコケる

bash "install_ndenv" do
  user  node['anyenv']['user']
  group node['anyenv']['gname']
  code <<-COMMAND
    export PATH="#{node['anyenv']['destination']}/bin:$PATH"
    eval "$(anyenv init -)"
    anyenv install ndenv
  COMMAND
  creates "#{node['anyenv']['destination']}/envs/ndenv"
end

こんな感じに anyenv でなんとか env を入れようとすると、

anyenv-install: definition not found: ndenv

といったエラーでコケる。 anyenv の方のソースを見ると、

BUILTIN_DEFINITION_PATH="${ANYENV_ROOT}/share/anyenv-install/${DEFINITION}"
if [ -e "$BUILTIN_DEFINITION_PATH" ]; then
  DEFINITION_PATH="$BUILTIN_DEFINITION_PATH"
else
  echo "anyenv-install: definition not found: ${DEFINITION}"
  exit 2
fi

ということが書かれていたので、どうやら ANYENV_ROOT という環境変数が無いっぽい。

Bash の provisioning のドキュメントを見た感じ、 environment というのを使うと環境変数を設定できるようなので、

bash "install_ndenv" do
  user  node['anyenv']['user']
  group node['anyenv']['gname']
  environment 'ANYENV_ROOT' => node['anyenv']['destination'], 'HOME' => node['anyenv']['home']
  code <<-COMMAND
    export PATH="#{node['anyenv']['destination']}/bin:$PATH"
    eval "$(anyenv init -)"
    anyenv install ndenv
  COMMAND
  creates "#{node['anyenv']['destination']}/envs/ndenv"
end

vagrant ユーザで chef-solo 使うと HOME は /home/vagrant になるところが、 Vagrantfile からプロビジョニングすると、 root ユーザで su vagrant したような感じになって、 HOME が /root になるため。

vagrant provision がちゃんと動かない

Shared folders that Chef requires are missing on the virtual machine.
This is usually due to configuration changing after already booting the
machine. The fix is to run a `vagrant reload` so that the proper shared
folders will be prepared and mounted on the VM.

こんなエラーが出て chef が動かなくなったりした。ググったら、

http://stackoverflow.com/questions/27975541/vagrant-chef-error-in-provision-shared-folders-that-chef-requires-are-missin

rm .vagrant/machines/default/virtualbox/synced_folders
vagrant reload --provision

これをすると直るというので、やったら直った。

Mojolicious のテストがコケた時に CLI にエラーを出す

Mojolicious アプリのテストをやっていると、 500 エラーになるようなコケ方をしたときに、 CLI にエラーが出てこなくて原因を調べるのがちょっとめんどくさいですが、こんな風にテストを動かすといい感じになります。

Mojolicious 5.39 以前

ちょっと前の Mojolicious では、コントローラでコケた際には Mojolicious::Controller::render_exception が呼ばれるようになっていたので、そのメソッドに monkey patch する。

use common::sense;
use Mojolicious::Lite;
use Test::Mojo;
use Test::More;
use Test::Pretty;

is $Mojolicious::VERSION, '5.39';

get '/' => sub { die 'Hoge'; };

{
    no strict 'refs';
    no warnings 'redefine';

    *Mojolicious::Controller::render_exception = sub {
        my ($c, $e) = @_;
        print "\n\n", "*** Exception thrown:\n";
        warn $e;
        print "\n\n";
        $c->rendered(500);
        $c;
    };
};

my $t = Test::Mojo->new;

$t->get_ok('/')->status_is(200);

done_testing;

結果:

% carton exec -- perl -Ilib test.pl
✓    L7: is $Mojolicious::VERSION, '5.39';


*** Exception thrown:
Hoge at test.pl line 9.


✓  GET /
✖  200 OK
#   Failed test '200 OK'
#   at test.pl line 27.
#          got: '500'
#     expected: '200'

Mojolicious 5.40 以降

新しい方の Mojolicious では、 Mojolicious::Plugin::DefaultHelpers から追加される、 'reply.exception' というヘルパーが呼び出されるようになっていたので、テストでは上書きする。

use common::sense;
use Mojolicious::Lite;
use Test::Mojo;
use Test::More;
use Test::Pretty;

is $Mojolicious::VERSION, '5.72';

get '/' => sub { die 'Hoge'; };

my $t = Test::Mojo->new;

$t->app->helper(
    'reply.exception' => sub {
        my ($c, $e) = @_;
        print "\n\n", "*** Exception thrown:\n";
        warn $e;
        print "\n\n";
        $c->rendered(500);
        $c;
    }
);

$t->get_ok('/')->status_is(200);

done_testing;

結果:

% perl test.pl
✓    L7: is $Mojolicious::VERSION, '5.72';


*** Exception thrown:
Hoge at test.pl line 9.


✓  GET /
✖  200 OK
#   Failed test '200 OK'
#   at test.pl line 24.
#          got: '500'
#     expected: '200'

Term::ANSIColor なんかを使って、いい色にするとなおわかりやすいと思います。

Mojolicious で Streaming

Mojolicious で、レスポンスをストリーミングで返すには、 Mojolicious::Controller::write_chunk を使います。

使い方は簡単で、 chunk ごとにレスポンスを書いていく要領です。

例: 1024 byte ずつ返す

use strict;
use warnings;
use Mojolicious::Lite;

get '/streaming' => sub {
    my $c = shift;
    open my $fh, '< my-list.txt';

    my $cb;
    $cb = sub {
        my $c = shift;
        my $length = sysread($fh, my $buffer, 1024);
        if (!$length) {
            close $fh;
            undef $cb;
            return $c->finish('');
        }
        $c->write_chunk($buffer, $cb);
    };

    $c->res->headers->content_type('text/plain');
    $c->$cb;
};

app->start;

例: 1行ずつ返す

下のコードではファイルの中身を1行ずつ書いてますが、 どんなイテレーションでもそのまま書き出せるとメモリ的に捗ります。

use strict;
use warnings;
use Mojolicious::Lite;

get '/line-by-line' => sub {
    my $c = shift;
    open my $fh, '< my-list.txt';

    my $cb;
    $cb = sub {
        my $c = shift;
        my $buffer = <$fh>;
        if (!$buffer) {
            close $fh;
            undef $cb;
            return $c->finish('');
        }
        $c->write_chunk($buffer, $cb);
    };

    $c->res->headers->content_type('text/plain');
    $c->$cb;
};

app->start;

OS X の "path_helper" と anyenv

anyenv を使うと、いろいろな言語の環境を揃えたり使い分けたりするのが捗りますが、 Mac OS XGNU Screen を使うときには、環境変数PATH まわりでちょっとうまくいかないです。

Mac OS X では、 /etc/profile にあるシステムワイドな設定で、

eval `/usr/libexec/path_helper -s`

が動いて、 PATH を整理するみたいです。 その結果、

/Users/yowcow/.anyenv/envs/rbenv/bin
/Users/yowcow/.anyenv/envs/plenv/bin
/Users/yowcow/.anyenv/envs/ndenv/bin
/Users/yowcow/.anyenv/envs/rbenv/shims
/Users/yowcow/.anyenv/envs/plenv/shims
/Users/yowcow/.anyenv/envs/ndenv/shims
/Users/yowcow/.anyenv/bin
/usr/local/bin
/usr/bin
/bin
/usr/sbin
/sbin

という順に通っていてほしいところが、 GNU Screen を起動した後は、

/Users/yowcow/.anyenv/envs/rbenv/bin
/Users/yowcow/.anyenv/envs/plenv/bin
/Users/yowcow/.anyenv/envs/ndenv/bin
/Users/yowcow/.anyenv/envs/rbenv/shims
/Users/yowcow/.anyenv/bin
/usr/local/bin
/usr/bin
/bin
/usr/sbin
/sbin
/Users/yowcow/.anyenv/envs/rbenv/bin
/Users/yowcow/.anyenv/envs/plenv/bin
/Users/yowcow/.anyenv/envs/ndenv/bin
/Users/yowcow/.anyenv/envs/rbenv/shims
/Users/yowcow/.anyenv/envs/plenv/shims
/Users/yowcow/.anyenv/envs/ndenv/shims
/Users/yowcow/.anyenv/bin

こんな風に変わってしまいます。

rbenv 以外は .anyenv/envs/{env}/shims が後ろに行ってしまい、各 env で指定しているバージョンのものが使えなくなっています。

バッドノウハウ的ですが、ひとまず .zshenv に、

for env in $(find ~/.anyenv/envs -type d -d 1 -exec basename {} \;); do
    export PATH=~/.anyenv/envs/$env/shims:$PATH;
done;

と設定して動くように設定しました。

MojoX::Session::Simple をリリースしました

Mojolicious を使ったことがあればわかると思いますが、 Mojolicious のセッション機構 Mojolicious::Sessions は、 Plack::Middleware で扱うセッションとは互換性がありません。

Plack::Builder で複数のアプリケーションをサーバにマウントする場合、セッションを簡単に共有できると便利と思っていた際に、 Plack::Middleware::Session::Simple という大変便利なモジュールがあったので、 Mojolicious と組み合わせて使いたいなあと思い、 MojoX::Session::Simple というモジュールを作ってみました。

このモジュールを使うと、従来の Mojolicious::Controller を継承した実装のまま、 セッション部分は Plack::Middleware::Session::Simple を通して保存したり復元したりすることができるようになります。

AnyEvent のお勉強

AnyEvent を使ってイベントループからコールバックを呼び出すようにすると、 非同期でコードを動かせるようになるというのは前から知っていたけど、 AnyEvent のドキュメントやコードを見てもちょっと難しくてピンとこないので、 実際に動くものを作ってみました。

IO::Socket::INET を使った簡単なサーバ

TCP とか Socket とか、全然わかっていないのですが、

use common::sense;
use AnyEvent;
use AnyEvent::Handle;
use Data::Dumper;
use IO::Socket::INET;
   
my $sock = IO::Socket::INET->new(
    LocalPort => 5001,
    Listen => 5,
    Proto => 'tcp',
    ReuseAddr => 1,
) or die $!;
            
printf "Socket created: %s\n", ref $sock;
    
while (1) {
    local $| = 1;
        
    my $socket = $sock->accept;
    printf "Connected From %s:%s\n", $socket->peerhost, $socket->peerport;

    while (my $msg = $socket->getline) {
        $msg =~ s/\R//g;
        print "-> Got: ${msg}\n";
        $socket->send("Got: ${msg}\015\012");
            
        last if $msg =~ /^bye$/i;
    }   
        
    $socket->close;
    print "Disconnected\n";
}   
        
1; 

コネクションを一つだけ受け付けるブロッキングなサーバは、こんな風に作れました。 これだと、複数の接続をさばけないので、あまり楽しくないです。

AnyEvent::Socket と AnyEvent::Handle を使ったイベント駆動サーバ

AnyEvent::Intro を読みながら、 Twiggyの実装例A simple chat server in AnyEvent の真似をしてみました。

use common::sense;
use AnyEvent;
use AnyEvent::Socket;
use AnyEvent::Handle;
 
my $cv = AnyEvent->condvar;
my %clients;
my %names; 
    
tcp_server undef, 5001, sub {
    my ( $fh, $host, $port ) = @_;
    
    print "Connection established: ${host}:${port}\n";
 
    my $handle = AnyEvent::Handle->new( fh => $fh );
 
    my $broadcast_cb = sub {
        my ( $fileno, $line ) = @_;
        $clients{$_}->{handle}->push_write($line)
          for grep { $_ ne $fileno } keys %clients;
    };
 
    my $handle_disconnect_cb = sub {
        my $h      = shift;
        my $fileno = fileno( $h->fh );
        if ( my $client = delete $clients{ fileno( $h->fh ) } ) {
            my $name = $client->{name};
            $broadcast_cb->( $fileno, "-> ${name} has left.\n", );
        }
        $h->destroy;
    };
    
    my $handle_connect_cb = sub {
        my $h      = shift;
        my $fileno = fileno( $h->fh );
        my $name   = $clients{$fileno}->{name};
        $h->push_write("-> Welcome, ${name}!\n");
        $broadcast_cb->( $fileno, "-> ${name} has joined.\n" );
    };
 
    my $handle_read_cb = sub {
        my ( $h, $line ) = @_;
        my $fileno = fileno( $h->fh );
        my $client = $clients{$fileno};
 
        if ( !defined $client->{name} ) {
            if ( defined $names{$line} ) {
                $h->push_write("-> ${line} is taken, try again: ");
            }
            else {
                $names{$line} = 1;
                $client->{name} = $line;
                $handle_connect_cb->( $h, $client->{name} );
            }
        }
        else {
            $broadcast_cb->( $fileno, $client->{name} . ": ${line}\n" );
        }
    };
 
    $handle->on_error($handle_disconnect_cb);
    $handle->on_eof($handle_disconnect_cb);
    $handle->on_read( sub { $_[0]->push_read( line => $handle_read_cb ) } );
    #$handle->on_drain(sub { });
 
    $handle->push_write("-> Your name is?: ");
 
    $clients{ fileno($fh) } = {
        name   => undef,
        handle => $handle,
    };
};
 
$cv->recv;

こんな風に、 AnyEvent::Socket と AnyEvent::Handle を使ってできました。

どちらのモジュールについてもまだ全然わかっていないので、もっと触ってみようと思います。