perl で exec 後に file descriptor を渡す

通常は 0, 1, 2 しか渡されない。

open my $fh, ">", "file.txt";
exec "ls", "-al", "/proc/$$/fd";
> perl test.pl
total 0
dr-x------ 2 skaji skaji  0 Mar 29 03:04 .
dr-xr-xr-x 9 skaji skaji  0 Mar 29 03:04 ..
lrwx------ 1 skaji skaji 64 Mar 29 03:04 0 -> /dev/pts/5
lrwx------ 1 skaji skaji 64 Mar 29 03:04 1 -> /dev/pts/5
lrwx------ 1 skaji skaji 64 Mar 29 03:04 2 -> /dev/pts/5
lr-x------ 1 skaji skaji 64 Mar 29 03:04 3 -> /proc/20500/fd

(注:3 -> /proc/20500/fd は ls 自身が開いたもの)

方法1 : FD_CLOEXEC を 0 にする

use Fcntl;
open my $fh, ">", "file.txt";
fcntl $fh, F_SETFD, 0; # FD_CLOEXEC を 0 に
exec "ls", "-al", "/proc/$$/fd";
> perl test.pl
total 0
dr-x------ 2 skaji skaji  0 Mar 29 03:07 .
dr-xr-xr-x 9 skaji skaji  0 Mar 29 03:07 ..
lrwx------ 1 skaji skaji 64 Mar 29 03:07 0 -> /dev/pts/5
lrwx------ 1 skaji skaji 64 Mar 29 03:07 1 -> /dev/pts/5
lrwx------ 1 skaji skaji 64 Mar 29 03:07 2 -> /dev/pts/5
l-wx------ 1 skaji skaji 64 Mar 29 03:07 3 -> /home/skaji/file.txt
lr-x------ 1 skaji skaji 64 Mar 29 03:07 4 -> /proc/20651/fd

方法2 : $^F をいじる

perldoc -v '$^F' 曰く、$^F 以下の file descriptor のみ exec に渡すらしい。よってこれを大きくすれば渡せる。

my $fh;
{
    local $^F = 10;
    open $fh, ">", "file.txt";
}
exec "ls", "-al", "/proc/$$/fd";

SEE ALSO

依存している distribution をかき集める

ある module を指定して、その module が依存している distribution をかき集めるやつを書いた。

Carton::Snapshot などがこれをやっており、そこからその機能だけを抽出した格好である。 名前長すぎなので変えたい。

インストールすると prereq-dist-dump.pl という便利スクリプトが入るのでこれの例。

例えば Data::Section::Simple が依存している distribution 全部は、こんな感じ。

f:id:ks0608:20150316051239p:plain

ちなみに、実行した local 環境での依存を表している (はずな) ので、例えば Encode を cpan から install していない場合は core の方に入るはず。

カレントディレクトリに cpanfile があれば、それの依存 distribution 一覧がでるので

> cat cpanfile
requires 'Amon2';
requires 'Teng';

> prereq-dist-dump.pl
prereq dists
 * Amon2-6.11
 * Apache-LogFormat-Compiler-0.32
 * B-Hooks-EndOfScope-0.14
 * CGI-4.13
 * CPAN-Meta-2.143240
 * CPAN-Meta-Requirements-2.133
 * Class-Accessor-0.34
 * Class-Accessor-Chained-0.01
 * Class-Accessor-Lite-0.06
...

のようになる。

入力は可能な限り早く decode すべき

最近、perl の文字の扱いではまることはなかったが久々にはまった。 単語リストが書かれた word.txt を読み込んで、もにょもにょやろうとしている下記プログラムのどこがダメか。

# test.pl
use 5.14.0;
use warnings;
use Encode 'decode_utf8';

open my $fh, "<", "word.txt" or die "word.txt: $!";
my %word;
while (my $line = <$fh>) {
    $line =~ s/\s+$//; # 行末の空白や改行を除去
    $word{ decode_utf8 $line }++;
}

# do something with %word
use Data::Dumper;
print Dumper \%word;

word.txt が

> cat word.txt
駅
原因

のような場合、

> perl test.pl
$VAR1 = {
          "\x{539f}\x{fffd}\x{fffd}" => 1,
          "\x{fffd}\x{fffd}" => 1
        };

となる。ここで "\x{fffd}" は壊れた unicode 文字の置き換えとして使われる文字である。

なぜこうなってしまったかというと 「駅」の utf8 byte 列は "\xe9\xa7\x85" であり、 $line =~ s/\s$// で "\x85" (NEXT LINE) が除去されてしまったから。

ちなみにこの挙動は perl のバージョンによって変わり、

  • perl 5.12 以下では起こらない。
  • perl 5.14 以上では use feature 'unicode_strings' もしくは use 5.12.0 以上を指定したとき。

のようだった。

( 5.12 はおそらくバグってる。cf: https://gist.github.com/shoichikaji/70b714c7a79b59897bd6 )

つまるところ、入力はできる限り早く decode するようにすれば、このようなことは起こらない。 今回の場合は open layer に指定するのが一番。

# test.pl
use 5.14.0;
use warnings;

open my $fh, "<:encoding(UTF-8)", "word.txt" or die "word.txt: $!";
my %word;
while (my $line = <$fh>) {
    $line =~ s/\s+$//;
    $word{ $line }++;
}

SEE ALSO

( perl のバージョンをいろいろ変えてみるべし)

Yet Anthor Carton

OrePAN2 で carton の代わりとなるものが結構簡単に作れそうだと思ったので作ってみた。

特徴

  • version 固定に02packages.details.txtを直接使う。
  • よって deploy 時には cpanm だけあればいい。
  • cpanfile の git / dist option をサポート。

git / dist option というのは

requires 'My::Module', git => 'git://ghe.example.com/you/My-Module.git', ref => '0.01';
requires 'Anthor::Module', dist => 'http://darkpan.example.com/Anothor-Module-0.01.tar.gz';

みたいなやつのことである。

いくつも TODO はあるが、いける気がする!

github にある module を require したい

github (or github enterprise) にある module を require したい。

かなり前の dev 版 carton には github にあるモジュールを require する機能があったが、消えてしまった。

モジュールは cpan にあげるべきだという意見はもっともだが、会社で書いているモジュールは社内の c++ コードに依存しているのでちょっと無理である。

また、cpan mirror server を立てるべきというのももっともだが、もっとコンパクトにやりたい。

ということでできるようにしてみた。

これは cpanm or carton を実行する直前に OrePAN2 によって local repository を作ってから、それらを実行するラッパーである。

cpanfile が以下のようなものであるとする。

# Test::PackageName というのは github にしかない
requires 'Test::PackageName', git => 'git://github.com/shoichikaji/Test-PackageName.git@master';

で、cpan-zero をかまして cpanm or carton を実行する。

> cpan-zero carton install
Installing modules using /Users/skaji/cpanfile
Successfully installed ExtUtils-Config-0.008
Successfully installed ExtUtils-InstallPaths-0.011
Successfully installed ExtUtils-Helpers-0.022
Successfully installed Module-Build-Tiny-0.039
Successfully installed Test-PackageName-0.01
5 distributions installed
Complete! Modules were installed into /Users/skaji/local

random thoughts

  • carton の git をはじめとするいろいろな dependencies support を期待している。
  • local repository をまず作る、というのは module 探索手段が統一され悪くないかもしれない。
  • 最初に local mirror から module を探すので、 ~/.cpanm/build.log に Download failedが出まくる。

手軽に親子プロセスで hash を共有する

手軽に親子プロセスで hash みたいに使えるデータを共有するモジュール書いてみた。

ファイルにデータを書いて親子で共有している (よって実際は親子である必要もない)。

使い方

use Shared::Hash;

my $hash = Shared::Hash->new;

my $pid = fork // die;
if ($pid == 0) {
    # child
    $hash->set(message => "from child!");
    exit;
}

sleep 1;
print $hash->get("message"); # from child!

さらにアトミック操作が簡単に書ける!

$hash->lock(sub {
    # この中で書いた $hash に対する get, set はアトミック
    # 例えば以下はちゃんと $i をインクリメントして set し直せる
    my $i = $hash->get("foo");
    $i++;
    $hash->set(foo => $i);
});

またどのファイルにデータを保存するか明示的に指定すれば、 永続的にそのデータを共有できる。

$ perl -MShared::Hash -e 'Shared::Hash->new(path => "foo.data")->set(foo => "bar")'

$ perl -MShared::Hash -e 'print(Shared::Hash->new(path => "foo.data")->get("foo"))'
bar

余談

  • 最初 tcp 通信で書いてたら TIME_WAIT が 28000 をすぐに超え死んだw
  • 次に unix domain socket で書いてたみたが汎用性にかけた。
  • $hash->wait(sub {}) なるメソッドで hash に変化があるときまで wait するやつを書きたいが、なかなかいいのが書けない。 Linux::Inotify2 の検知してる変化がよくわからず、Mac::FSEvents は ディレクトリしか指定できなかった。

追記

完全にかぶったw https://metacpan.org/release/Test-SharedObject cpan に上がってるのでこちらを使いましょう。

Distribution::Metadata というのを書いてみた

Distribution::Metadata というのを書いてみた。

これは local にある Module::Name からそれの属する distribution を特定し、

  • main module
  • .packlist ファイル
  • .meta ディレクトリ
  • install.json ファイル
  • MYMETA.json ファイル

を取得するものである。

例えば LWP::UserAgent に対して以下のようなことができる。

use Distribution::Metadata;
my $info = Distribution::Metadata->new_from_module("LWP::UserAgent");
print $info->meta_directory, "\n";
print $_, "\n" for @{ $info->files }
__END__
/Users/skaji/env/plenv/versions/5.20.1/lib/perl5/site_perl/5.20.1/darwin-2level/.meta/libwww-perl-6.08
/Users/skaji/env/plenv/versions/5.20.1/bin/lwp-download
...
/Users/skaji/env/plenv/versions/5.20.1/lib/perl5/site_perl/5.20.1/LWP.pm
/Users/skaji/env/plenv/versions/5.20.1/lib/perl5/site_perl/5.20.1/LWP/Authen/Basic.pm
...

この機能はすでに cpanm などが持ち合わせているが、cpanm をライブラリとして使うべきでないと miyagawa さんに指摘していただいたので別モジュールとしてまとめてみた。

TODO

  • 同じ機能の cpan module って既にある?
  • cpanm 以外の cpan クライアントでインストールした distribution もこのモジュールで特定できるか調べるべき。
  • windows でどう動くか確認すべき。
  • 何のインターフェイスを提供すべきかよくわからない。meta ファイルの path だけ提供できればあとは使う人が適当にやってくれそうである。
  • cpan にあげたい!