仙石浩明の日記

2010年9月21日

地デジ MPEG-2 TS の PCR/PTS/DTS ラップアラウンド (PCR Wrap-around) 問題を回避して ffmpeg で PS 変換できるようにしてみた

(地上)デジタルTV 放送を Linux サーバで予約録画しまくるようになった今日このごろ、 たまに失敗するのが気になっていた。 失敗といっても、 録画した MPEG-2 TS (Transport Stream) ファイルを VLC で再生する分には問題がないが、 ffmpeg を使って MPEG-2 PS (Program Stream) フォーマットへ変換しようとすると途中で映像が止まってしまう (TS 読み込みに失敗しているので他のフォーマットへの変換もおそらく無理)。

おそらく TS のデータに何らかの誤りがあって ffmpeg が映像データを読むのをそこで止めてしまうためだろうと思っていた。 そりゃ放送なんだからノイズが混じることもあるだろうと思い込んでいたのだが、 よほど電波状況が悪くない限りエラー補正が効くだろうから、 ノイズが原因というのはありそうもない話。

なぜ PS へ変換したいかといえば、 TS のままだとファイルサイズが大きすぎる (2時間の映画番組とかだと 12GB を超える) ので、 PS へ変換し (CM カットなどの編集を行なうときは PS の方が便利)、 さらに MPEG-4 などへ変換してサイズを小さくしたいから (もちろん保存する必要がない番組は見たらすぐ消す)。 ところが PS への変換に失敗した番組は TS のまま保存せざるを得ず、 そういうファイルが増えてきて、 2TB のハードディスクもだんだん手狭になってきた。

そこで、 何が原因で TS の読み込みに失敗しているのか、 まずは調べてみようと思ったのだが、 いかんせんファイルがデカすぎる。 12GB もあるとファイルの内容をダンプするのもままならない。 そこで変換が失敗する近辺のデータだけ抜き出して詳しく調べてみようと考えた。 MPEG-2 TS の仕様を調べてみると、 PCR (Program Clock Reference) なるタイムスタンプが 100msec 以下の間隔で MPEG-2 TS には埋め込まれているらしい。 問題の TS ファイルは、 2時間の番組中、 先頭から 1時間35分41秒ほど経過したあたりで読み込みに失敗するので、 PCR の値を手がかりにその近辺のデータを抜き出そうと考えた次第。

まずは PCR の値を表示する perl スクリプト tsdump.pl を作ってみた (後述する tsrenum.pl に -v オプションを与えることによって同じ出力が得られる)。

% ./tsdump.pl -v < 133000_GR23.ts
0008c4a PCR 24:55:00.570 8073051319+138
001a12d PTS 24:55:01.111 8073100071
001a132 DTS 24:55:01.011 8073091062
001d841 PTS 24:55:00.748 8073067367
0022556 PCR 24:55:00.628 8073056524+157
00226d5 PTS 24:55:01.045 8073094065
0026a65 PTS 24:55:00.769 8073069287
0029dcd PTS 24:55:01.078 8073097068
00300f1 PTS 24:55:00.791 8073071207
0033225 PTS 24:55:01.212 8073109080
003322a DTS 24:55:01.111 8073100071
0038f69 PTS 24:55:00.812 8073073127
003bcea PCR 24:55:00.685 8073061729+175
...

「133000_GR23.ts」 が問題の TS ファイル (23チャンネルなので TV東京 ^^;)。 次の 「0008c4a PCR 24:55:00.570 8073051319+138」 という行は、 ファイル先頭から 0008c4a バイト目に PCR があって、 そのタイムスタンプが 「24:55:00.570」 であることを示す。

行末尾の 「8073051319+138」 は生の PCR の値。 つまり、 PCR は 33bit の 「PCR base」 と 9bit の 「PCR extension」 から構成されるが、 それぞれ 8073051319 と 138 であることを示す。 PCR base は 90kHz の解像度、 PCR extension は 27MHz の解像度。 とりあえずここでは後者は無視して、 前者 8073051319 を 90000 (= 90kHz) で割ると 89700.570 秒で、 時分秒に直すと 24:55:00.570 になる。

PCR の他に、 PTS (Presentation Time Stamp) と DTS (Decode Time Stamp) というタイムスタンプも MPEG-2 TS には埋め込まれている。 これらは PCR と違って 90kHz の解像度の 33bit のデータのみ。 MPEG-2 TS では、 PCR の時刻を基準にして、 復号する時刻 (DTS) と再生する時刻 (PTS) を定めている (音声と映像の同期のためにも使われる)。 前述したプログラムの出力において、 2カラム目に PTS, DTS と出力している行は、 それぞれ PTS, DTS の値であることを示す。

で、 PCR の値を表示させてみていきなり気付いてしまったのだが、 この問題の TS ファイルは PCR の値が 33bit の上限値ギリギリ。 33bit の上限は 0x1FFFFFFFF = 8589934591 だから、 PCR は 26:30:43.717 (26時間30分43秒余) までしかカウントできない。 このファイルは冒頭のタイムスタンプが 24:55:00.570 だから、 冒頭から 01:35:43.147 ほど経過したあたりで PCR の値が桁あふれ (オーバーフロー) を起こして 00:00:00.000 へ戻ってしまう (ラップアラウンド)。

これは冒頭から 1時間35分41秒ほど経過したあたりで ffmpeg が止まってしまう症状にピッタリ符合する (2秒ほど差があるのは MPEG-2 復号に要する時間?) ので、 もう原因は PCR のラップアラウンド (PCR Wrap-around) で間違いないと推測。

「VLC で再生する分には問題がない」 と書いたが、 この問題の TS ファイルを VLC で再生して 1時間35分41秒あたり (VLC の場合 TS 再生時は時刻表示を行なわないのでシーンで探さざるを得ず大変) をよく見てみると、 再生が一瞬 (1秒ほど?) 止まっていた。 26時間半に一度、 必ず起こるラップアラウンドなのに、 VLC でも完全な対策は (まだ) 行なわれていないようだ。

原因調査の準備のために作ったスクリプトが、 いきなり原因究明の役に立つとは (^^;) と思いつつ、 tsdump.pl にタイムスタンプの値を付け替える (すなわち 24:55:00.570 から始まるタイムスタンプを 00:00:00.000 始まりにリナンバーする) 処理を付け加えたスクリプト tsrenum.pl を書いてみた (後述する pcr_write, ts_write 関数を追加しただけ)。

% ./tsrenum.pl < 133000_GR23.ts > 133000_GR23_fixed.ts

tsrenum.pl を使ってタイムスタンプを付け替えた 133000_GR23_fixed.ts は、 ffmpeg を使って PS 変換ができるようになった。 原因調査の準備のために作ったスクリプトが、 ほとんどそのまま問題解決の役にも立ってしまった (^^;)^2。

tsrenum.pl は、 標準入力から読み込んだ MPEG-2 TS において最初に現れた PCR/PTS/DTS タイムスタンプを基準 (下記スクリプト中の $pcrOrg) にして、 タイムスタンプを付け替えて標準出力へ書き出す:

#!/usr/bin/perl
use strict;
use warnings;
use POSIX;
use Getopt::Std;
our ($opt_v);
getopts('v') || &help;
my $PacketSize = 188;
my $offset = 0;
my $pcrOrg;
for (;;) {
    my $buf;
    my $len = read(STDIN, $buf, $PacketSize);
    last unless $len > 0;
    my @buf = unpack("C*", $buf);
    die if $buf[0] != 0x47;	# sync byte
    my $ind = (($buf[1] & 0xE0) >> 5);
    my $adp = (($buf[3] & 0x30) >> 4);
    my $pos = 4;
    if ($adp & 0x2) {	# Adaptation field exist
	$pos++;
	my $af = $buf[$pos++];
	if ($af & 0x10) {
	    my ($pcr33, $pcr9) = &pcr(\@buf, $pos);
	    printf(STDERR "%07x PCR %s %d+%d\n",
		   $offset+$pos, &stime($pcr33), $pcr33, $pcr9) if $opt_v;
	    &pcr_write(\@buf, $pos, $pcr33);
	    $pos += 6;
	}
	if ($af & 0x08) {
	    $pos += 6;
	}
	if ($af & 0x04) {
	    $pos++;
	}
    }
    if ($ind & 0x02) {
	if ($buf[$pos] == 0x00 && $buf[$pos+1] == 0x00
	    && $buf[$pos+2] == 0x01 && ($buf[$pos+6] & 0xC0) == 0x80) {
	    my $flag = $buf[$pos+7];
	    $pos += 9;
	    if ($flag & 0x80) { # PTS
		my $ts = &ts(\@buf, $pos);
		printf(STDERR "%07x PTS %s %d\n",
		       $offset+$pos, &stime($ts), $ts) if $opt_v;
		&ts_write(\@buf, $pos, $ts);
		$pos += 5;
		if ($flag & 0x40) { # DTS
		    my $ts = &ts(\@buf, $pos);
		    printf(STDERR "%07x DTS %s %d\n",
			   $offset+$pos, &stime($ts), $ts) if $opt_v;
		    &ts_write(\@buf, $pos, $ts);
		    $pos += 5;
		}
	    }
	}
    }
    syswrite(STDOUT, pack("C*", @buf), $len);
    $offset += $len;
}

sub stime {
    my ($ts) = @_;
    my $sec = floor($ts / 90000);	# 90kHz
    $ts -= $sec * 90000;
    my $min = floor($sec / 60);
    $sec -= $min * 60;
    my $hour = floor($min / 60);
    $min -= $hour * 60;
    return sprintf("%02d:%02d:%02d.%03d", $hour, $min, $sec, $ts/90);
}

sub help {
    print STDERR <<EOF;
Usage: tsrenum.pl <opt>
opt:   -v            ; verbose
EOF
    exit 1;
}

MPEG-2 TS を標準入力から 1パケットずつ @buf に読み込んで、 PCR を見つけたら
pcr(\@buf, $pos) 関数を呼び出して PCR base の値 $pcr33 を取得し、
pcr_write(\@buf, $pos, $pcr33) 関数で PCR base の値を変更して標準出力へ書き出す。 PTS, DTS についても同様に、 ts(\@buf, $pos) 関数を呼び出して値を取得し、 ts_write(\@buf, $pos, $ts) 関数で変更する。

ISO/IEC 13818-1 の 2.4.3.4節 Adaptation field (20ページ) の Table 2-6 - Transport Stream adaptation field を見ると、 adaptation field の 2バイト目から 33bit が PCR base の値、 続いて 6bit の予約ビット、 その次の 9bit が PCR extension の値であることが分かる。 したがって pcr(\@buf, $pos) 関数は以下のように書ける:

sub pcr {
    my ($br, $pos) = @_;
    my $pcr33 = (($br->[$pos] << 25) | ($br->[$pos+1] << 17)
		 | ($br->[$pos+2] << 9) | ($br->[$pos+3] << 1)
		 | (($br->[$pos+4] & 0x80) != 0));
    my $pcr9 = ((($br->[$pos+4] & 0x01) << 8) | $br->[$pos+5]);
    return ($pcr33, $pcr9);
}

pcr_write(\@buf, $pos, $pcr33) 関数は PCR base の値 $pcr33 から $pcrOrg を減算してから pcr(\@buf, $pos) 関数の逆を行なうだけ:

sub pcr_write {
    my ($br, $pos, $pcr33) = @_;
    $pcrOrg = $pcr33 unless defined $pcrOrg;
    $pcr33 -= $pcrOrg;
    $br->[$pos] =   (($pcr33 >> 25) & 0xFF);
    $br->[$pos+1] = (($pcr33 >> 17) & 0xFF);
    $br->[$pos+2] = (($pcr33 >>  9) & 0xFF);
    $br->[$pos+3] = (($pcr33 >>  1) & 0xFF);
    if ($pcr33 & 1) {
	$br->[$pos+4] |= 0x80;
    } else {
	$br->[$pos+4] &= 0x7F;
    }
}

ISO/IEC 13818-1 の 2.4.3.7節 Semantic definition of fields in PES packet (31ページ) の Table 2-17 - PES packet を見ると、 PES (Packetized Elementary Stream) の 9バイト目から marker bit を挟みつつ 33bit の PTS と DTS が格納されていることが分かる。

したがって ts(\@buf, $pos) 関数と ts_write(\@buf, $pos, $ts) 関数は以下のように書ける:

sub ts {
    my ($br, $pos) = @_;
    return ((($br->[$pos] & 0x0E) << 29)
	    | ($br->[$pos+1] << 22) | (($br->[$pos+2] & 0xFE) << 14)
	    | ($br->[$pos+3] << 7)  | (($br->[$pos+4] & 0xFE) >> 1));
}

sub ts_write {
    my ($br, $pos, $ts) = @_;
    $pcrOrg = $ts unless defined $pcrOrg;
    $ts -= $pcrOrg;
    $br->[$pos] =   ((($ts >> 29) & 0x0E) | ($br->[$pos] & 0xF1));
    $br->[$pos+1] =  (($ts >> 22) & 0xFF);
    $br->[$pos+2] = ((($ts >> 14) & 0xFE) | ($br->[$pos+2] & 0x01));
    $br->[$pos+3] =  (($ts >>  7) & 0xFF);
    $br->[$pos+4] = ((($ts <<  1) & 0xFE) | ($br->[$pos+4] & 0x01));
}

すべて解決した後で気付いたのだが (^^;)、 ラップアラウンドする TS ファイルを読み込んだときは ffmpeg が的確なエラーメッセージを出力していた:

...
frame= 1328 fps= 21 q=2.0 size=   31898kB time=44.00 bitrate=5938.8kbits/s dup=24 drop=0    
frame= 1341 fps= 21 q=2.0 size=   32002kB time=44.45 bitrate=5898.1kbits/s dup=24 drop=0    
[mpegts @ 0x6253c0]Invalid timestamps stream=0, pts=4078, dts=8589929661, size=53459
*** drop!
*** 1 dup!
*** drop!
*** drop!
*** drop!
*** drop!
*** drop!
adding -2147483648 audio samples of silence
adding -2147483648 audio samples of silence
*** drop!
...

「Invalid timestamps ... pts=4078, dts=8589929661」 すなわち PTS が 00:00:00.045 なのに、 DTS が 26:30:43.663 なので無効なタイムスタンプである、 というエラー出力。 タイムスタンプがラップアラウンドしたときは、 「Invalid timestamps」 とは言えないと思うのだが...

念のため ffmpeg の最新版を 「git clone git://git.ffmpeg.org/ffmpeg/」 で取得してコンパイルしてみたのだが、 「FFmpeg version git-735bbae」 でも同様に 「Invalid timestamps」 エラーが出力された。

Filed under: プログラミングと開発環境 — hiroaki_sengoku @ 08:13
2010年9月4日

中国でプリペイド SIMカード 神州行と如意通を買ってみた

週末に休みを追加して北京へ行ってきた。 今回は全日程終日自由行動のパックツアーだったので、 昨年 「連れ回しパックツアー」 で北京へ行ったときには見せてもらえなかったところ (終日バスで連れ回されたので街の日常の風景を見ることができなかった) が見えてきて大変興味深かった。 が、 その内容をここで書いてしまうと、 このブログが中国から読めなくなってしまいそうなので自粛。

実質 3日間の短期滞在だったので、 日本で使ってるソフトバンクのケータイをそのまま持ち込んで 「海外パケットし放題」 でもよかったのだが、 せっかく訪れたのだからと北京でも SIMカードを買ってみた。

China unicom at BCIA

北京首都国際空港 (簡体字だと 「北京首都国际机场」) に着いてすぐ 「电话卡」 (直訳すると 「電話カード」) と書いてある SIMカード自販機が目を引く (写真は出発ロビーの自販機だが、到着ロビーにもある) が、 通話料なしで 100元 (約 1300円) もする。 つまり SIMカードを買っただけではダメで、 通話料をチャージ (中国語では 「充値」) しなければ使えない。 街中なら 50元の通話料込の SIMカードが 100元以下で売っている (つまり SIMカード単体の価格で比較すれば半額以下) ので、 空港での購入は見送った。 SIMカードに限らず、 中国では場所によって値段が大幅に変わってくるので注意が必要。

私は中関村 (簡体字だと 「中关村」) のケータイショップで買ったのだが、 中関村でなくても、 というかむしろ西単とかの繁華街の方が簡単に買える。 「我要SIM」 (正しくは儲値卡) などと紙に書いて店員に見せたら (四声をちゃんと発声しないと通じないので紙の方が早いし正確, もちろん英語は通じない) 電話番号ごとの値段リストを見せてくれた。

(続きを読む...)
Filed under: SIM — hiroaki_sengoku @ 00:34
2010年8月4日

HYBRID W-ZERO3 の解約に続き、ウィルコムADSL も解約した。さよならウィルコム!

事業破綻の報道等で 「顧客 (サービス利用者) の保護」 という言葉をみかけることが多いが、 言語明瞭意味不明ワードの一つだと思う:

PHS の解約が予想以上に進み、 2月に 417万人だった契約者数は 6月末には 388万人に減った。 7月下旬に予定していた裁判所への再建計画提出も、 「環境が変わった」として 10月に延期していた。
管財人らは、顧客を守るためにも、再建には通信会社の協力が必要と判断。 XGPを引き受けるソフトバンクに、PHS事業への支援に加わるよう求めていた。

「顧客を守る」 などと書くと聞こえはよいが、 PHS サービス継続を望む顧客がどれだけいるかなんて、 「PHS の解約が予想以上に進」んでいることから明らかなはず。 現在 388万人(も) 契約者数が残っていると言ったって、 その大半は 2年縛りのせいで辞めるに辞められない人たちであって (私も 6月末時点だと契約者なのでこの 388万人に含まれている)、 現顧客にとって望ましいのはサービス継続じゃなくて 2年縛りを免除してあげることだと思う。

「顧客」 というと 「債権者」 というイメージが強いし、 この報道のように 「顧客を守る」 と書くとますます 「債権者たる顧客を守れ!」 というイメージが強調されるけど、 実状は 「辞めたいけど契約の縛りで払い続けざるを得ない債務者たる顧客」 と 「辞めていく顧客から少しでも多くの資金を回収したい債権者」 という (報道から受けるイメージとは真逆の) 構図であるわけで、 報道がいかに実状をゆがめたイメージを撒き散らしているかの一例だと思う。

私は丸 4年 (なのでちょうど解約可能なタイミングだった)、 ウィルコムの PHS サービスを利用してきた。 また自宅のバックアップ回線として 4年間近くウィルコムADSL を利用してきた。 なぜウィルコムだったかと言えば、 2006年当時まだ珍しかった 「スマートフォン」 W-ZERO3[es] を使いたかったから。 4年前ドコモショップで、 スマートフォンが使いたいから解約すると伝えたとき、 ドコモも近いうちにスマートフォンを出す予定 (hTc Z) と担当者が言っていたのを思い出す。

その後、 W-ZERO3[es], Advanced W-ZERO3[es], HYBRID W-ZERO3 と 3代続けて W-ZERO3 シリーズを使い続けてきたが、 HYBRID W-ZERO3 のあまりのデキの悪さ (そもそも電話の着信音 / バイブレーションが小さすぎて着信に気付けないのだから電話として失格) に幻滅し、 乗り換えを決意した次第。 輝かしい W-ZERO3 の歴史の最後の最後で欠陥品を出してしまうとは (私は使ったことがないが一つ前の WILLCOM 03 も失敗作だった?)、 いったいシャープに何が起こったのか?

iPhone を使いたいがためにソフトバンクに乗り換える人が圧倒的である昨今、 いまさら言うまでもないことだが、 (2006年ごろから?) 通信サービスは端末のオマケに成り下がったとつくづく思う。 いや、 端末ではなくコンテンツのほうがもっと大事だと言う人がいるかもしれないが、 そうなるのはもう少し先 (少なくともあと 3年くらい先)、 端末がコモディティ化した後の話だと思う。 少なくとも現時点では、 どんなに立派で魅力的なコンテンツでも、 それをユーザに届ける窓口である 「端末」 がヘタレであれば何の意味もない。

一般に解約するのは骨が折れる。 多くの場合、解約は電話のみの受付だったり、 解約通知書を郵送しなければいけなかったりと、いろいろ手間がかかる。 Web だけで契約が済んでしまう入会の時とは対照的。 しかも解約の方法は Web を丹念に見ていかないと見つけられなかったりする。 退会率を下げたいという事業者側の気持ちも (職業がら) もちろん分かるのだが、 解約方法を分かりにくくすればするほどサポートコストは増えるしユーザ満足度も下がるわけで、 結局事業者自身の首を絞めることにしかなってないと思う。

特に、 ウィルコムADSL の解約は大変だったので以下にメモ:

(続きを読む...)
Filed under: その他 — hiroaki_sengoku @ 08:07
2010年7月26日

Nexus One で Android 2.2 froyo のマルチタッチを試してみる

Android は 2.1-update1 以降でマルチタッチ (Multi-touch) をサポートしている。 ところがマルチタッチといっても、 ピンチイン/ピンチアウトなどのジェスチャをサポートしているだけのアプリが大半で、 複数のタッチを独立に扱えるアプリはいまだほとんどなく、 iPhone と比べるとその差が際立っている。

どうして Android にはマルチタッチを活用したアプリケーションが無いのだろう? と思ったので、 マルチタッチを試すテストアプリ MultiTouch.java (apk) を書いてみた:

MultiTouch

タッチした位置にタッチの強さに応じた大きさの円を表示するだけの単純なアプリ。 指を移動すれば円も追随する。 Android ではタッチID が順に割り振られるので、 ID が 0 のタッチを赤色の円で、 ID が 1 のタッチを緑色の円で描いている。

プログラム上は ID が 2 のタッチを青色の円で描くことになっているが、 残念ながら現行の Android で同時に扱えるタッチは 2箇所のみ (追記: Samsung Galaxy S は 5箇所のマルチタッチが可能らしい) なので、 3箇所にタッチしても三つ目の円が描かれることはない。 だから例えば iPhone のアプリにあるような鍵盤楽器アプリを作ろうと思っても、 三つ以上の音を同時に鳴らすことはできない。

とはいえ、 2箇所のタッチを独立に扱えれば、 いろいろ応用が効くだろうにと思いつつ、 このテストアプリをいじっていると...

(続きを読む...)
Filed under: Android,プログラミングと開発環境 — hiroaki_sengoku @ 08:57
2010年7月17日

Nexus One の近接センサ/環境光センサは、どこにあるのか?調べてみた

Nexus One など最近のスマートフォンには、 加速度 (Accelerometer)、 環境光 (照度, Ambient Light)、 磁場 (磁界, Magnetic Field)、 方位 (電子コンパス, Orientation)、 近接 (Proximity) など、 様々なセンサがついている。 いろいろ応用できそうで夢がふくらむが、 携帯電話本来の使い方 (つまり通話すること) において、 使い勝手に直接影響する重要なセンサが近接センサ。

Nexus One や iPhone など全面タッチパネルの携帯電話だと、 (受話器として使うために) 耳に近づけたときタッチパネルが反応しては困る。 そこで近接センサを使って顔が接近してくることを感知し、 タッチパネルを無効にする (ついでにディスプレイをオフにして消費電力を抑える)。

私は Proximity なんて聞くと、 Proximity Warning System (接近警報システム) を思い浮かべてしまうくらいで、 携帯電話用の近接センサがどういうしくみか全く知らなかった。 今年1月の Nexus One の発表の時に近接センサのことを初めて知り、 その時はタッチパネル全体への接近を感知する (静電容量の変化を検知して?) のかと想像したが、 後述するように Nexus One の近接センサはタッチパネルの左上にしかなく、 タッチパネルの下方への接近は感知できないことが分かった。

Nexus One のどこに近接センサが搭載されているか、 センサの感応範囲がどれくらいなのか、 私には見当もつかなかったし、 google で検索してもその手の情報は見つからなかったので、 近接センサが感知した値を表示するだけの簡単なプログラムを書いてみた。

実は、 私にとって初めての android アプリ (^^;)。 しかも、 ここ数年 Java から遠ざかっていたので、 久々に書く Java プログラムだったりする。

お膳立ては Android SDK が全てやってくれるので、 わずか 74行のプログラム。 まず SensorManager#getSensorList メソッドで、 PROXIMITY タイプのセンサを取得し (sensor)、

	sensorManager = (SensorManager)getSystemService(SENSOR_SERVICE);
	List<Sensor> sensors
	    = sensorManager.getSensorList(Sensor.TYPE_PROXIMITY);
	Sensor sensor = sensors.get(0);

この sensor の値が変化したときなどにセンサの値を受け取るリスナ (SensorEventListener) を、 SensorManager#registerListener メソッドで登録するだけ。

public class Proximity implements SensorEventListener {
	...
	sensorManager.registerListener(this, sensor,
				       SensorManager.SENSOR_DELAY_NORMAL);
	...
    @Override
    public void onSensorChanged(SensorEvent event) {
	view.update(event.values);
    }

    @Override
    public void onAccuracyChanged(Sensor sensor, int accuracy) {
    }
}

近接センサの値が変化するとリスナの onSensorChanged メソッドが呼び出されるので、 新しいセンサ値を描画する (view.update):

	void update(float[] values) {
	    Canvas canvas = getHolder().lockCanvas();
	    if (canvas == null) return;
	    canvas.drawColor(Color.WHITE);
	    Paint paint = new Paint();
	    paint.setColor(Color.BLACK);
	    paint.setTextSize(40);
	    float height = paint.getTextSize();
	    for (int i = 0; i < values.length; i++) {
		canvas.drawText(" "+values[i], 0, height * (i + 1), paint);
	    }
	    getHolder().unlockCanvasAndPost(canvas);
	}

Nexus One のタッチパネル左上隅近く (黒枠部分) にセンサがあるらしく、 パネルまで 2cm ほどの距離に物体を近づけると反応する (センサの値が 9.0 から 0.0 へ変化する)。 また、 パネルと並行に物体を動かす場合、 センサの真上から 1cm ほど外れると反応しなくなる。

Proximity Sensor  ←  鈴を近づけたことにより、
近接センサが反応して、
値が 0.0 になっている

赤外線型の近接センサ (赤外線を照射し、近接する物体からの反射光を測定するセンサ) なので、 凸面の物体など赤外線があさっての方向へ反射してしまって受光素子に正しく届かない場合や、 あるいは黒色の物体などあまり反射しない場合などでは、 より近づけないと反応しない。

例えば、 黒く細い丸棒などだと 1cm 以下に近づけないと反応しない。 逆に白い紙 (凹面〜平面) など、 効果的に赤外線を反射し、かつ受光素子に反射光が効率的に届くケースだと、 2cm より遠くても (8cm くらいでも) 反応する。

ちなみに、 Nexus One に搭載されている近接センサは、 Capella MicrosystemsCM3602 という、 環境光センサ付短距離近接センサ (Short Distance Proximity Sensor with Ambient Light Sensor) であるようだ。 名前の通り環境光も測定できる。 おそらく近接センサの受光素子をそのまま使って照度を測定しているのだろう。

前述したプログラムにおいて、 「Sensor.TYPE_PROXIMITY」 を 「Sensor.TYPE_LIGHT」 に置き換えれば、 環境光センサの値を読み取ることができる。

(続きを読む...)
Filed under: Android,プログラミングと開発環境 — hiroaki_sengoku @ 08:55
2010年7月8日

Google カレンダーの過去の予定を自動的に削除する方法

ここ 2ヶ月ほど、 Willcom の HYBRID W-ZERO3 から Nexus One への移行を徐々に進めてきた。 Windows Mobile (HYBRID W-ZERO3) は PC とのデータ同期が基本なので、 PC にスケジュールや電話帳など丸ごと入れておけば済むが、 Android (Nexus One) の場合は 「クラウド」 との同期が基本なので一筋縄にはいかない。 つまり 「クラウド」 に全てのスケジュールを置いていいのか? という問題。 私の場合、 「スケジュール」 といいつつ会議の議事メモから個人的な日記まで、 プライベートな情報を全て集積しているので問題がより深刻になる。

ちなみに私は、 1999年に WorkPad 30J を使い始めて以来、 プライベートなデータを PDA / スマートフォンに集積してきた。 今まで使ってきた PDA / スマートフォンをまとめてみる:

購入月機種OS
1999-04 WorkPad 30J PalmOS 3.1J
2000-03 TRGpro PalmOS 3.5
2000-08 Palm m100 PalmOS 3.5.1
2002-09 Zaurus MI-E1 ZaurusOS
2002-12 Linux Zaurus SL-C700 Linux 2.4 Embedix
2003-06 Linux Zaurus SL-C750 Linux 2.4 Embedix
2006-07 W-ZERO3[es] Windows Mobile 5.0
2007-07 Advanced W-ZERO3[es] Windows Mobile 6 Classic
2008-12 HTC P3600 Windows Mobile 5.0
2010-01 HYBRID W-ZERO3 Windows Mobile 6.5 Professional
2010-04 Nexus One Android 2.2 froyo
2010-06 iPhone4 iOS 4
2010-11 IS01 Android 1.6 donut
2010-12 Galaxy S Android 2.2 froyo
2010-12 IDEOS U8150-B Android 2.2 froyo

WorkPad 30J から HYBRID W-ZERO3 に至るまで、 全て PC とのデータ同期が基本だったし、 それぞれデータ移行ツールが用意されていたので移行は容易だった。 ところが Nexus One で同じような同期を行なうには、 データを PC ではなく Google Calendar へ置かなければならない。

もちろん、 Google Calendar は共有設定さえ行なわなければ他人に読まれることはないだろうし、 「don't be evil」 と言ってるくらいだから、 Google が勝手にユーザのデータを活用する可能性も無い (と信じたい)。

だからといって、 個人的なデータや会社の超機密事項 (議事メモにはそういった情報も含まれる) も洗いざらい Google に預けてしまう、 なんてことは小心な私にはとてもできない。 よく知られているように Google Calendar は 「限定公開 URL」 が漏れるだけで一巻の終わりであるわけで、 漏れることを前提でリスク評価すべき。

というわけで、 議事メモや日記を Google Calendar に置くことはハナからあきらめて、 Google Calendar には直近の予定だけを置くことにした。 万一漏れても、向こう一週間くらいの予定だけであれば、 致命的というほどでもない。

ところが驚いたことに Google Calendar には過去の予定を一括削除する機能がない。 手作業でいちいち消していかない限り、 データは残り続けるようだ。 当たり障りのない 「予定」 でも積み重なればいろいろ見えてくることがあるわけで、 長年にわたって溜った予定データは脅威となりうる。

どうしてこんな超基本機能が無いのだろうと思いつつ google で検索してみると、 見つかるのは 「どうやったら過去のデータを (一括) 消去できるのか?」 という質問のページばかり。 過去データを削除したい、 というニーズは確実にありそうなのに、 なぜ Google は実装しようとしないのか? そういえば gmail も過去のメールを溜め続けるのが基本だし、 Google Calendar に自動的に削除する機能がないのは意図的なのかもしれない。

無い機能は作ってしまえと、 Google Calendar API のドキュメントを眺めてみる。 API を叩くためのクライアントライブラリが用意されているようだ。 が、.NET とか Java とか Python とか PHP とか、 あまり気の進まない (^^;) 言語ばかりが並んでいる。 Perl は無いのかっと思って CPAN を検索したら、 Net::Google::Calendar があっさり見つかった。

マニュアル片手にテストプログラムを書いてみる:

use Net::Google::Calendar;
use DateTime;
my $cal = Net::Google::Calendar->new;
$cal->login('sengoku@gmail.com', 'xxxxxxxx');
for my $event ($cal->get_events('start-max' => DateTime->now
				- DateTime::Duration->new(days => 7))) {
    $cal->delete_entry($event);
}

たったこれだけ。 最初に login($user, $pass) して、 get_events(%opts) で「予定」データを取り出して、 delete_entry($event) で削除。 get_events の引数で、 開始時刻が一週間以上過去の予定のみ取り出すよう指定している。

ちょっと書き足して、 ユーザID やパスワードを設定ファイルで設定できるようにしたスクリプト gcal_remove を作ってみた。

% gcal_remove -c /path/to/config.yaml -d 7

などと実行すると、 7日間以上前の予定を削除する。

設定ファイル config.yaml は以下のような感じ:

google_user: sengoku@gmail.com
google_passwd: xxxxxxxx
Filed under: Android,プログラミングと開発環境 — hiroaki_sengoku @ 09:44
2010年6月17日

Linode から prgmr.com へ乗り換えてみた

今年 2月から使っている Linode (VPS, 仮想サーバ) は、 月額 $19.95 の最小プラン Linode 360 だと RAM 360MB なので WordPress を動かす (Apache+PHP+MySQL) には若干足らない (稀に スラッシング 状態に陥って無反応になる)。 もちろん、 一つ上のプラン Linode 540 (RAM 540MB) にすればいいのであるが、 これだと月額 $29.95 でちょっと高い (私の感覚だと月額 $20 あたりに心理的な壁がある)。

prgmr.com ならば、 RAM 512MB, ディスク 12GB が月額 $12 (年一括払いなら $115.2) で済むので乗り換えてみた。 Linode のようなユーザフレンドリーな Web ユーザインタフェースは無いし、 時々新規申込みの受付を中止したりする (今も RAM 1024MB 以上のプランは受付けていない) が、 逆に言えば Web インタフェースが不要な人 (含む私) にとっては prgmr.com のシンプルさがむしろ望ましく、 レスキュー用のブートイメージなど必要最低限のものは揃っているので、 不便と感じることはない。 また、 いったん申込みが完了してしまえば受付の一時停止は関係ないわけで、 むしろサーバリソースに見合ったユーザ数しか受付けない姿勢は好感が持てる。

Web から Signup すると、 以下のようなメールが届く。

you ordered a xen vps, 512MiB ram, 12GiB Disk 80 GiB transfer Xen VPS,  $12/month username sengoku 

Before I can set you up, I need to know what distro you would like and an OpenSSH format public key (on a *NIX, run ssh-keygen  and send me either the id_dsa.pub or id_rsa.pub file in an attachment)    

on putty/windows, see

http://www.unixwiz.net/techtips/putty-openssh.html#keypair

and scroll down to the screen shot where it says 'openssh format public key for pasting into authorized_keys' - that is what I need.

thanks. 

OpenSSH の公開鍵くらい Web から登録させればいいのにと思いつつ、 「I need to know what distro you would like」 などと聞いてくるということは多少の融通は聞くのかと思い、 パーティションの切り方をついでにリクエストしてみた (6/4 15:46)。 つまり、 自分で作った Linux distro (配布していないから 「distribution」 と呼ぶのは正確性に欠けるのだが、 それなら何て呼んだらいいのだろう?) をインストールしたいから、 最小のパーティション (例えば 2GB) に ubuntu をインストールしておいてもらえるか? などとリクエストしてみた:

Hi,

At Fri, 04 Jun 2010 05:48:05 +0000,
support@prgmr.com wrote:

> Before I can set you up, I need to know what distro you would like and
> an OpenSSH format public key (on a *NIX, run ssh-keygen and send me
> either the id_dsa.pub or id_rsa.pub file in an attachment)

I'd like to use my own linux-based image that is running on
fremont.gcd.org (linode) now.  Is it possible to make two partitions
(plus swap partition) ?  One is for a normal ubuntu, and the other is
for my own image.  I'd like to switch these two OSs by pv-grub.

Please minimize the ubuntu partition in order to maximize my own image.
For example, 2GiB for ubuntu, 1GiB for swap, and 9GiB for me.

I've attached my public key id_dsa.pub at the end of this mail.


#9860.
http://www.gcd.org/sengoku/		Hiroaki Sengoku <sengoku@gcd.org>

すると、 4日後の 6/8 09:03 に返事が来た。 リクエストはあっさり無視されて、 ディスク一杯に ubuntu をインストールした状態の VPS が提供されただけ。 個別対応しないのであれば、 自動化したほうがいいような。

Your vps is setup now, your host server is whetstone.prgmr.com.
Login with your username and ssh key and follow the instructions
at http://book.xen.prgmr.com/mediawiki/index.php/Quickstart
For more information about repartitioning, see
http://book.xen.prgmr.com/mediawiki/index.php/Repartition
The default root password for the vps is password, and you
should receive a bill soon by email. If you have any questions
please email support@prgmr.com. Thanks very much.
Nick Schmalenberger

言われた通り ssh でログインしてみる:

senri:/home/sengoku % ssh whetstone.prgmr.com
The authenticity of host 'whetstone.prgmr.com (65.49.73.105)' can't be established.
RSA key fingerprint is 97:52:07:d4:52:8d:c0:cc:13:c7:fb:5d:5d:1b:ab:b4.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added 'whetstone.prgmr.com,65.49.73.105' (RSA) to the list
of known hosts.
Name                                        ID   Mem VCPUs      State   Time(s)
sengoku                                    xxx   512     1     -b----     11.5

Options for sengoku
1. console
2. create/start
3. shutdown
4. destroy/hard shutdown
5. reboot
6. swap i386/amd64 bootloaders (pvgrub)
7. exit
press the number> 

「6. swap i386/amd64 bootloaders (pvgrub)」 が目を引くが、 PV-GRUB は DomainU 側で実行されるので、 PV-GRUB にて 32bit/64bit の両方のカーネルを立ち上げることができず、 PV-GRUB を実行する前にどちらか一方に決めておく必要がある、 ということのようだ。 デフォルトは (残念なことに) 32bit になっていて、 セットアップしてもらった ubuntu も 32bit 版だった。

「1」 を入力して VPS のコンソールを表示させてみる:

press the number> 1

Ubuntu 10.04 LTS sengoku.xen.prgmr.com hvc0

sengoku.xen.prgmr.com login: root
Password:
Last login: Mon May 31 23:51:42 UTC 2010 on hvc0
Linux sengoku.xen.prgmr.com 2.6.32-22-generic-pae #33-Ubuntu SMP Wed Apr 28 14:57:29 UTC 2010 i686 GNU/Linux
Ubuntu 10.04 LTS

Welcome to Ubuntu!
 * Documentation:  https://help.ubuntu.com/
root@sengoku:~# df
Filesystem           1K-blocks      Used Available Use% Mounted on
/dev/xvda1            11812024   1068124  10143876  10% /
none                    249488       136    249352   1% /dev
none                    253744         0    253744   0% /dev/shm
none                    253744        28    253716   1% /var/run
none                    253744         0    253744   0% /var/lock
none                    253744         0    253744   0% /lib/init/rw
none                  11812024   1068124  10143876  10% /var/lib/ureadahead/debugfs
root@sengoku:~# 

前掲のメールに書いてあった通り、 ユーザ 「root」 パスワード 「password」 でログインできた。 初めてログインしたのに 「Last login: Mon May 31 23:51:42 UTC 2010 on hvc0」 などと出ているが、 この ubuntu イメージを作ったとき試しに root でログインしたのが残ってしまったのだと思われる。 新規ユーザのセットアップのたびに、 5/31 に作ったイメージを使いまわしているのだろう。

ディスク 12GB のプランのはずなのに 1GB ほど足りないのは swap パーティションがあるから?と思い、 fdisk で確認してみる:

root@sengoku:~# fdisk -l /dev/xvda

Disk /dev/xvda: 12.8 GB, 12884901888 bytes
255 heads, 63 sectors/track, 1566 cylinders
Units = cylinders of 16065 * 512 = 8225280 bytes

    Device Boot      Start         End      Blocks   Id  System
/dev/xvda1               1        1494    12000523+  83  Linux
root@sengoku:~# 

シリンダの最大値は 1566 なので、 72 (=1566-1494) シリンダが使われずに余っている。 試しにパーティションを割当ててみたら普通に使えた。

自前 distro 10GB, swap 1GB, ubuntu 1GB といった切り分けかたにするため、 レスキュー用のカーネル 「CentOS 5.5 rescue」 をブートしてみる:

    GNU GRUB  version 0.97  (65536K lower / 0K upper memory)

 +-------------------------------------------------------------------------+
 | user bootloader configuration                                           |  
 | CentOS 5.5 rescue (2.6.18-194.3.1.el5xen)                               |
 | CentOS 5.5 installer                                                    |
 | NetBSD 5.0.2 installer                                                  |
 | Ubuntu 10.04 LTS installer                                              |
 |                                                                         |
 |                                                                         |
 |                                                                         |
 |                                                                         |
 |                                                                         |
 |                                                                         |
 |                                                                         |  
 +-------------------------------------------------------------------------+
    Use the ^ and v keys to select which entry is highlighted.
    Press enter to boot the selected OS, 'e' to edit the
    commands before booting, or 'c' for a command-line.

二行目の 「CentOS 5.5 rescue (2.6.18-194.3.1.el5xen)」 を選択してブート。 root でログイン (パスワードは無し) して fdisk を使ってパーティションを切り直した。 せっかくセットアップしてもらった Ubuntu も、 3分と使わずにお別れ。

CentOS release 5.5 (Final)
Kernel 2.6.18-194.3.1.el5xen on an x86_64

sengoku.xen.prgmr.com login: root
[root@sengoku ~]# 
	...
[root@sengoku ~]# fdisk -l /dev/xvda

Disk /dev/xvda: 12.8 GB, 12884901888 bytes
255 heads, 63 sectors/track, 1566 cylinders
Units = cylinders of 16065 * 512 = 8225280 bytes

    Device Boot      Start         End      Blocks   Id  System
/dev/xvda1               1        1306    10490413+  83  Linux
/dev/xvda2            1307        1438     1060290   82  Linux swap / Solaris
/dev/xvda3            1439        1566     1028160   83  Linux
[root@sengoku ~]# mount /dev/xvda1 /mnt
[root@sengoku ~]# 

私は個人で管理している Linux マシン (自宅と職場と VPS 合わせて十数台, もちろん商用サービスを行なっているサーバではない) のディスクの内容を、 コマンド一発で同一に保てるようにしている (つまりマスタで変更/追加/削除したファイルを各マシンへコピー/各マシンで削除)。

もちろん、 各マシンの性能や 32bit/64bit などの CPU 種別、 ディスクの容量に応じて違う部分もあるわけで、 この同期コマンドは同期先の各マシンに合わせてコピーしないファイルのリストを生成した上で rsync を実行している。 なので、 新しくマシンを増やすときもマスタ上で同期コマンドを実行するだけで、 インストールが完了するはずだが...

(続きを読む...)
Filed under: システム構築・運用 — hiroaki_sengoku @ 09:44
2010年5月26日

サイボウズ オフィス8 のカレンダーを iCalendar 形式に変換するスクリプトを書いてみた

私の職場ではグループウェアとしてサイボウズを利用している。 自分のスケジュールを iCalendar 形式で取得してみたくなった (Nexus One を買うとそういう気分になる ^^;) ので、 google で検索してみたところ次の二つのスクリプトが見つかった:

あいにく職場のサイボウズはオフィス8 なので、 オフィス8 に未対応の前者は使えない。 後者はオフィス8 用だが、 「暫定版」 と書いてある通りいろいろバグがある。 さくっと修正してみて一応それっぽく動かすことはできたのだが、 使い続けていくとなると一から書き直したほうがいいような気がしてきた (わたし的にはこの手のものを PHP スクリプトでは書きたくない) ので、 perl で書き直してみた

この perl スクリプト cybozu8_ical を、

% cybozu8_ical --conf /path/to/config.yaml

などと実行すると、 オフィス8 の 「月予定」 (月間スケジュール) ページをアクセスして、 iCalendar 形式に変換する。 向こう一週間以内の予定については 「予定の詳細」 ページもアクセスして、 「メモ」 および 「設備」 も取得する。 「月予定」 の全ての予定について 「予定の詳細」をアクセスすると、 オフィス8 サーバに負荷をかけすぎる懸念があったので、 このような仕様にしてみた。

設定ファイル config.yaml は以下のような感じ:

cybozu_url: http://intra.klab.org/cgi-bin/klcb/ag.cgi
calname: cybozu
userid: 73
password: xxxxxxxx
time_zone: Asia/Tokyo
input_encoding: shiftjis
output_file: /home/sengoku/tmp/cybozu.ics

「cybozu_url」 はオフィス8 の URL を指定する (もちろん intra.klab.org は KLab 社内LAN からしかアクセスできない)。 「userid」 と 「password」 はスケジュールを取得したいユーザの ID とパスワード。 「output_file」 に指定したファイルへ iCalendar 形式のスケジュールを出力する。 「output_file」 を省略すると標準出力へ書き出す。

なお 「--conf /path/to/config.yaml」 オプションを省略すると、 cybozu8_ical と同じディレクトリにある config.yaml が読み込まれる。

cybozu8_ical で生成した iCalendar 形式のファイルは、 とりあえず Google カレンダー および Thunderbird + Lightning で読み込めることは確認したが、 なにぶんまだ RFC 2445 を真面目に読んでいないので、 不具合などあったらご指摘頂けると有難い。

私はオフィス8 の API を知らないので、 「月予定」 「予定の詳細」 のページを取得してきて scrape しているだけ。 「繰り返し予定」 は個々の予定として扱っている。 iCalendar の UID (各予定固有のID) が同じままだと 2個目以降の予定が無視されてしまうので、 UID の末尾に通し番号をつけて互いに区別できるようにしている。

サイボウズのスケジュールは 「場所」 を登録できない。 その代わり 「施設」 として会議室を登録する。 ところが私の職場の場合 「施設」 には会議室だけでなく 「ビデオ会議システム」 などもあったりするので、 「施設」 に登録されているデータをそのまま iCalendar の LOCATION として使うわけにもいかない。 そこで 「施設」 に会議室の名称が登録されている場合のみ、 その会議室名を LOCATION として出力するようにしている。

iCalendar の LOCATION として出力すべき会議室名の一覧を、 cybozu8_ical スクリプトの始めの方で、

my @facility = (
	'20F 大会議室1', '20F 大会議室2', '20F 大会議室3',
	'20F 中会議室1', '20F 中会議室2',
	'20F 小会議室1', '20F 小会議室2',
	'20F 和室', '22F 社長室前MTGスペ-ス'
);

などと定義している。 ここに列挙されていない会議室等は 「施設」 に登録されていても単に無視される。

Filed under: Android,プログラミングと開発環境 — hiroaki_sengoku @ 16:01
2010年5月3日

米国のプリペイド SIM カードで Nexus One を使ってみた

私は米国ではプリペイド SIM カードを購入して携帯電話を使っている。 AT&T の Pay As You Go $1 Mobile to Mobile Plan は、 携帯電話同士の通話が 1日 1ドルで使い放題になるので便利。 一方データ通信は、 米国だとホテルをはじめ多くの場所で無線LAN が使えるので、 今までは必要性をあまり感じなかったのだが、 今回のハワイ滞在で Nexus One を入手したら、 携帯電話でもデータ通信をしてみたくなってしまった。

日本でも HTC Desire が発売された今となっては 「いまさら」 感もあるが、 SIM ロック フリーな Nexus One を入手する意味はあると思い、 渡米前に Web サイトで Nexus One を購入し (日本からは、 このサイトへアクセスできないと書いてあるページを見かけるが、 トップページ以外は普通にアクセスできるような...?)、 配送先として滞在する予定のホテルを指定しておいた (配送先に日本の住所は指定できない)。 ただし米国のホテルはテロを警戒して、 非滞在者宛の荷物の受け取りを (事前連絡しても) 拒否するそうなので、 購入手続きは出発直前に行なった。
日本を発つ日 (フライトは晩の 20:50) の早朝 4:57 JST (12:57 PDT) に購入手続きを行なったら、 その翌日 0:02 JST (8:02 PDT) に発送され (FedEx overnight)、 ハワイ到着日の翌日昼間 13:30 HAST にホテルに配達された (日本時間でいうと購入日の 2日後の朝 8:30 JST)。 出発日の前日に購入手続きを行なっていれば到着日に受け取れたかもしれないが、 ホテルにチェックイン (12:00 HAST) する前に配達されるとマズイかも?

ホテルのフロントで Nexus One の小包を受け取って、 SIM を入れて電源 ON したら、 勝手に 3G (W-CDMA) でデータ通信を始めてしまった (当たり前 ^^;)。 Pay As You Go (AT&T のプリペイドSIM) は、 Data Package を追加購入せずにデータ通信を行なうと、 1KB あたり 1セントも課金されるので一瞬で (例えば、わずか 10MB で約1万円) パケ死してしまう。 慌てて無線LAN に切り替えたが既に 87セント課金されていた。 さらに、 位置情報を取得するために 1セント (基地局ID の取得?)、 無線LAN 経由だと google アカウントの同期が行なえなかった (ホテルの LAN の問題?) ので 3G でつなぎ直したら 29セントかかった。 この時点で残度数 $22.73 (SIM カード利用開始時は $25)。

Pay As You Go Online で、 「Login」 ボタンの上にある 「Forgot your password?」 をクリックすると 「Request Your Password」 ページに遷移するので、 AT&T プリペイド SIM の携帯電話番号を入力して 「Send Password」 ボタンを押す。 すると、

1111509064: S: Your account status has changed.
ATT Free Msg: Your password has been changed to 8181.

などといった SMS (ショートメッセージサービス) が送られてくる。 このメッセージ中に書いてある password を使ってログインし、 任意の password に設定し直せばよい。 いったんログインできるようになれば、 米国内外を問わず Web 上で度数の追加 (refill) や追加パッケージ (feature packages) の購入が可能。

追加パッケージには、 Unlimited Talk & Text ($60 で通話と SMS が 30日間使い放題), Messaging Packages ($19.99 で SMS 無制限), Data Packages (データ通信), Night and Weekend Minutes Package ($19.99 で通話と SMS が夜間・週末無制限) があって、 Data Package は、 1MB ($4.99) と 100MB ($19.99) のいずれかを選べる。

Nexus One をいじっていたらすぐ 100MB くらいはいくだろうと思い 100MB を購入した。 実際、 google map をナビ代わりに使う (海外旅行ではすごく便利!) と、 20MB くらいはすぐ使ってしまう。 また google アカウントとの自動同期を行なうと、 何もしなくても一日 1MB くらいは使ってしまうようだ。 あっというまに 100MB を使い切りそうなので、 Data Package 100MB を追加購入することにした。

追加パッケージは、 Credit/Debit カードで支払うこともできるが、 SIM カードの残度数で払うこともできる。 つまり、 いったん度数を購入 (refill) してから、 その度数で追加パッケージを購入するということができる。 なんでそんな回りくどいことを?と思うかも知れないが、 refill だと SIM カードの有効期限を延長できるという違いがある。 例えば $100 refill すると有効期限が 1年後になる ($25〜$75 だと 90日後、$15 だと 30日後)。

有効期限は追加できないので、 例えば有効期限が 3ヶ月後の場合 (新規に SIM カードを購入した場合など) に、 $100 refill したからといって有効期限が 15ヶ月後 (=3ヶ月+1年) になるわけではなく、 refill した時点の 1年後が有効期限になる。 有効期限はもちろん短くはならない。 例えば $15 refill だと有効期限は 30日後だが、 refill 以前の有効期限が 3ヶ月後なら、 3ヶ月後のままで変わらない。
一方、 refill 以前の度数も新しい期限まで有効期限が延長される。 つまり有効期限に達する前に refill し続ければ、 度数を失効させずに済む。 ただし一枚の SIM カードに溜められる度数は $500 が上限。

というわけで、 最初に購入した Data Package 100MB の残りがわずかになった段階で、 $100 refill し、 その度数から $19.99 を使って Data Package 100MB を追加購入した。

Filed under: Android,Hawaii,SIM — hiroaki_sengoku @ 18:04
2010年4月20日

Linux サーバ同士をシリアルケーブルでつなぐには寡黙な getty が必要

私の自宅 LAN (GCD) の対外ルータである 2台の Linux サーバ (senriasao) は、 双方のシリアルポート (つまり DSUB9ピンの RS-232C) 同士がつながっている。 いまさら RS-232C ? と思う人が大半だと思うが、 最後の命綱としての安心感は測り知れない。

つまりサーバをリモートから (ネットワーク経由で ssh ログインして) いじっていると、 ちょっとしたミスで操作不能に陥ってしまうことがある。 例えば NIC を down してしまった、 iptables で DROP する設定にしてしまった、 sshd を殺してしまった、 等々。 あるいは、 リモートから再起動を行なう場合、 起動スクリプトのほんの些細な設定ミスで、 リモートからは操作不能になってしまう。

こんなとき、 2台のサーバのシリアルポート同士を RS-232C クロスケーブル (シリアルケーブル) で接続しておけば、 生きている方のサーバからシリアルケーブル経由でログインできて、 リモートからは操作不能に陥ったサーバのネットワーク設定を修復したり、 sshd を起動し直したり、 あるいは再起動の場合であれば、 GRUB を操作することができる (ML115 などであれば BIOS 設定までいじれる)。

職場のサーバ群は、 もちろん IPMI を利用していて大抵の操作は IPMI でできてしまうのでシリアルケーブルの出番はあまりないのだが、 それでもやはり最後の命綱がある安心感は何ものにも代えがたい。 しかも数百円のケーブル一本で済むのだから、 掛けておくべき保険だろう。

シリアルケーブル経由でサーバにログインするには、 そのサーバの /dev/ttyS0 (シリアルポート COM1 の場合) で getty を動かしておく必要がある。 例えば GCD の場合 mgetty を使っているので、 /etc/inittab に

# Dialin lines
s0:25:respawn:/usr/sbin/mgetty ttyS0

などと設定している。 シリアルケーブルでつながった 2台の Linux サーバ senri と asao において、 asao から senri へシリアルケーブル経由でアクセスすると、 senri のシリアルコンソールが表示される:

asao:/home/sengoku % cu ttyS0
Connected.

Welcome to Linux 2.6.31.13-x86_64.


senri.gcd.org!login: root
otp-md5 299 se5047 ext
Response or Password:
Last login: (null) on /dev/ttyS0

senri.gcd.org:/root # tty
/dev/ttyS0
senri.gcd.org:/root # 

たとえ senri のネットワーク機能が全滅していたとしても、 mgetty は /sbin/init から直接起動されるので影響を受けない。 シリアルコンソールから root でログインして復旧作業を行なうことができる。 逆に、 asao のネットワーク機能が全滅したときは、 senri へ ssh ログインした上で asao へシリアルケーブル経由でアクセスすればよい。 ネットワーク機能が麻痺する可能性があるような危険な作業を senri と asao の両方で同時に行なったりしないようにすれば、 何が起きてもどちらかは生きていることが保証できる。

さて、 両サーバで mgetty が動いているときは以上で問題無いのだが、 どちらかのサーバを再起動するときなど、 mgetty 以外のものが動くときは状況が変わってくる。

(続きを読む...)
Filed under: システム構築・運用 — hiroaki_sengoku @ 07:48
2010年4月2日

「無知の無知」への 3ステップ

KLab(株)は、 2007年から新卒採用を行っています。
今年も例年通り、4月1日に入社式を行いました。

- o -

みなさん、入社おめでとうございます。 社長のお話が終わり、みなさんは挨拶が終わって、 緊張もほぐれてきたのではないかと思います。 ここからは各役員の挨拶なのですが、 なぜか私だけは挨拶だけでなく (長くならない範囲で ^^;) 話もしろということになったらしいので、 話を捻り出してみます。

「考えることが大事」ということは社長のお話にもありましたし、 みなさんも既によく知っていることだと思います。 ただ、 知っていることと実際に実行できることとの間には越えがたい壁があることも事実で、 分かっちゃいるけどついつい考えなくなってしまう、 あるいは考えているつもりが、 いつのまにか深く考えることがなくなってしまうものなのでしょう。 そもそも自分がどのくらい考えているかなんて、 なかなか意識するのは難しいですよね。

入社時にはみんな等しく、 これからは「考える習慣」を身につけよう、 と決意を新たにしたはずなのに、 3年で大きな差がついてしまうのが現実です。 考える習慣を身につけた人がどんどん成長するのに対し、 考えることを習慣化し損なった人は停滞したままなので、 その後も差は広がる一方。 10年 20年もたつととんでもない差になってしまいます。

なぜこんなにも差がついてしまうのでしょうか? ただやみくもに 「考えよう」 としても、 具体的に何をすればいいのかよく分かりませんよね? なので逆に何をしてはいけないかについて今日はお話ししたいと思います。 今日からみっちり 2ヶ月間、 新人研修でいろいろなことを学んでいくみなさんにとって、 「何をしてはいけないか」 を押えておくことはきっと役に立つと思います。

私は仕事柄、 面接をする機会が多いのですが、 お会いする人の中には考える習慣が全くない人もいます。 ご本人にはそういう自覚が全く無く、 人並みには考えていると思っているようなのですが、 私から見ると正に 「人生オワタ」 状態で、 なぜそうなってしまったのだろうかと考えているうちに、 ついついその人の身の上を根掘り葉掘り聞いて人生相談モードへ入ってしまうので、 そういうことを繰り返すうちに、 だんだん原因が見えてきたように感じています。

考える習慣を持たない人に共通する問題点として 「無知の知」 ならぬ 「無知の無知」 があります。 自分が分かってないことを分かってない、 自分の浅慮では到底及ばない領域があることが全く想像できない、 いわば 「井の中の蛙」 状態ですね。 「無知の無知」 状態になってしまうと、 今の自分の考え方で満足してしまい、 今の自分の枠を越えて考えてみようという発想がでてきません。 当然、 考える習慣もなくなってしまうわけです。 なぜこうなってしまうのか? 次の 3つのステップを経て 「無知の無知」 状態に至るケースが多いように思います:

  1. 後で調べればいいや
  2. 何が分からないのか分からない
  3. 分かったつもり

「質問する前に考えろ」 「それくらい Google 検索などを使って調べろ」 「ググれカス」 などと言われるようになったのはいつのころからでしょうか? 確かに何でもかんでも 「教えて君」 では困りますが、 「すぐ質問すること」 = 「悪いこと」 とする風潮はいかがなものかと思います。 こういう風潮が、 「聞くのは恥ずかしい」 「後で調べればいいや」 という意識を生んでいないでしょうか。

ちなみに私は全く正反対で、 「分からないと言える」 = 「カッコイイ」 と中学生の頃から思い込んでるので、 今でも堂々と 「分からない」 を連発しています。 もちろん、 「CTO がこんなことも知らないのか?」 と思われる (^^;) リスクも無いわけではないので、 TPO を考慮せざるを得ないこともあるのがちょっと残念です。

確かに本当に後で調べるのならまだいいのですが大抵は忘れてしまいます。 仮に忘れずに後で調べて言葉の意味を知ったとしても時間は遡れません。 その場で意味を聞いていれば相手と有益な議論を展開できたのかもしれないのに、 その機会は永久に失われてしまうわけです。

そして何より怖いのは、 知らない言葉が出てきても何とも思わなくなってしまうことです。 ふつう、 相手の話の中に意味が取りにくい言葉があれば気持ち悪く感じ、 なんとかその気持ち悪さを解消しようと思うものだと思うのですが、 「後で調べればいいや」 と思ってるとだんだんこの 「気持ち悪さ」 が無くなってきます。 この 「不感症」 が最初のステップです。

そして一つの話の中に知らない言葉がいくつも出てきても、 何とも思わない状態になると次のステップが見えてきます。 知らない言葉が複数出てくれば、 当然話全体が分からなくなるし、 そもそも話自体に興味が持てなくなります。 こうなってしまうと、 「何が分からないか分からない」 状態に陥ります。 いわゆる 「質問したくても何を質問したらいいのか分からない」 状態ですね。

質問しようとしない人に、 「もっと質問しようよ」 と言うと返ってくる言葉が、この 「何を質問すればいいのか分からない」 です。 話を興味を持って聞くということが無くなってしまっているために、 相手が話していない部分を聞き出そうという意欲が失われてしまっています。 もちろん、 人の興味は十人十色ですから、 ある話題に興味が持てないというのは普通でしょう。 問題なのは、 どんな話であっても興味が持てないことにあります。 しかも本人的にはそれが普通な状態なので、 他人の話を興味を持って聞くということがどんな体験だったのか、 もう覚えていなかったりします。 この 「無関心・無気力」 が第2 のステップとなります。

この第2 のステップの怖いところは自覚症状があまり無いことにあります。 自分は 「無気力」 ではない、 と思う人が大半だとは思いますが、 どんな話を聞いても、 質問したいことが思い浮かばないようだと 「第2 のステップ」 がかなり進行中であるわけで要注意です。 なんとか気付いて危機感を持って欲しいですし、 私はことある毎に 「質問しろ〜」 と口を酸っぱくして言ってるのですが、 長年染み付いた習慣というのはなかなか克服できないものなのかも知れません。

第2 のステップは 「分からない」 ということ自体は自覚できているのでまだ救いがあるのですが、 この症状が進むと第3 のステップである 「分かったつもり」 に至ります。 「分からない」 という意識さえあれば何かのきっかけで 「分かろうとする意欲」 が芽生えるかもしれないのですが、 分かっていないこと自体が分からなくなると、 その可能性すら無くなってしまうので末期的です。

何を聞いても 「分かったつもり」 になってしまって、 表面的な理解だけで満足するようになってしまうと、 自分の考え方と相手の考え方が異なっていてもそれに気付かない、 ということが起り得ます。 つまり相手の話を自分の思考の枠内だけで理解してしまうわけです。 まさに 「無知の無知」 です。

と、 いうわけで今日からの研修では決して疑問点を残さないよう、 積極的に質問してください。 また、 「分かった」 と思っても本当に理解しているのか、 今一度自分を疑ってみてください。

例えば、 半年前に内定式で渋滞の話をしました。 忘れてしまった人は私のブログを参照してください。 社会人経験がまだ無いみなさんには、 あの話を完全に理解することはなかなか難しいのではないかと思いますが、 「分からない」 という感覚を持っていますか? そして分からないとすればどこが分からないのか考え、 何を質問すれば自分の理解を深められるか是非考えてみてください。

まだまだ自分は 「分かっていない」 と自分を疑うことこそが、 考えを深めていくために必要なのだと思います。 KLab への入社が、 みなさんの人生において飛躍のきっかけとなることを期待します。

Filed under: 元CTO の日記,自己啓発 — hiroaki_sengoku @ 13:19
2010年3月8日

tinydns のゾーンを MyDNS へ反映させるスクリプトを書いてみた

MyDNS というのは MySQL (あるいは PostgreSQL) のレコードを、 そのままゾーンレコードとして扱えるネームサーバ。 私は MyDNS を使って実験的にダイナミックDNSサービスを (もちろん無償で) 提供している。 実験と言いつつ、 サービス開始以来 2年以上安定的に継続できているし、 先月から海外レンタルサーバを使って地域分散したので、 仮に私の自宅の回線が切れてもネームサーバが見えなくなることはない。

gcd.jp のマスタネームサーバである ns.gcd.jp と、 スレーヴネームサーバである ns2.gcd.jp (海外サーバ fremont.gcd.org の別名。 名前の通りカルフォルニア州フリーモントにある) とは、 MySQL のレプリケーションによってゾーンデータを同期させている。 したがってマスタ側での変更が即座にスレーヴ側に伝わる。

ちなみに、 ネームサーバ間の同期でよく利用されるゾーン転送 (AXFR) は、 ゾーンデータを丸ごと転送するので (ゾーンが大きくなってくると) 同期頻度を高くすることが難しく、 ダイナミックDNSサービスにはあまり向いていない。 MySQL のレプリケーションでネームサーバ間の同期が行なえてしまう MyDNS は、 ダイナミックDNSサービス向きと言える。

ところが gcd.org では (ダイナミックではない) 普通のネームサーバ ns1.gcd.org も運用していて、 これは tinydns を利用している。 せっかく海外にサーバ fremont.gcd.org を借りたのだから、 tinydns で管理しているゾーンも fremont.gcd.org で引けるようにしたいが、 fremont に複数 IP アドレスを付与するのはお金がかかる (月額 $1 追加) ので、 fremont で MyDNS と tinydns の両方を走らせるわけにもいかない。

私が借りてるレンタルサーバ (正確に言うと VPS) Linode は、 もともと DNS サービス (マスタ/スレーヴどちらでも、ドメインいくつでも) を無料で利用できるので、 DNS のためだけに 1IP 追加する気にはちょっとなれない。

もちろん、 tinydns を止めてしまって全てのゾーンを MyDNS へ移行してしまうという解決策も無くはないのだが、 MyDNS に全面的に依存してしまうのは恐い気もする。 できれば tinydns 側はいじらずに、 tinydns のゾーンデータを MyDNS 側へ反映させる仕掛けを作りたい。

というわけで、 tinydns のゾーンデータを読み込み、 MyDNS のゾーンデータの形式で MySQL へレコードを書込む perl スクリプト tinydns2mydns を書いてみた (CVSリポジトリ)。

なお MyDNS には、 mydnsimport という外部からゾーンデータを取り込むツールが付属していて、 tinydns のゾーンデータもインポートできるのだが、 以下の欠陥があってゾーン転送目的には使えない:

  1. ゾーンの全レコードをいったん削除した上でインポートを行なう実装になっている。
  2. SRV レコードをインポートできない。
(続きを読む...)
Filed under: プログラミングと開発環境 — hiroaki_sengoku @ 09:25
2010年3月1日

x86_64 Linux などの 64bit 環境で MD5 を使うときの注意点

MD5 (Message Digest Algorithm 5) は、 RFC 1321 でアルゴリズムが紹介されていて、 Appendix (付録) として C によるリファレンス実装が付属しているが、 その global.h に

/* UINT4 defines a four byte word */
typedef unsigned long int UINT4;

と書いてある。 すなわち 32bit 整数として UINT4 型を定義している。 x86_64 Linux を始め多くの 64bit Unix は LP64 すなわち long int (とポインタ) が 64bit な整数データモデルを採用している。 したがって UINT4 型の定義が 「unsigned long int」 のままで、 この MD5 リファレンス実装を使ってしまうと、 32bit であるべき UINT4 型が 64bit になってしまい、 間違ったハッシュ値を算出してしまう。

16bit CPU が主流だった大昔なら 「int が 16bit なデータモデルを採用している環境」 が多かったのかもしれないが、 RFC 1321 が出た 1992年ごろは既に 32bit CPU が主流だったわけで、 UINT4 型を 「int」 と定義しておいてくれてもよかったのにと思う。 そうすれば、 「long が 64bit なデータモデルを採用している環境」 が多くなる昨今でも (int は 32bit のままなので) 問題を起こさずに済んだだろうに。

試しにテストプログラムを書いてみる:

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include "global.h"
#include "md5.h"
#define DIGEST_LEN 16
#define BUFFER_LEN 256

int main(int argc, char *argv[]) {
    MD5_CTX context;
    unsigned char digest[DIGEST_LEN];
    unsigned char buf[BUFFER_LEN];
    int i;
    MD5Init(&context);
    while ((i=read(0, buf, BUFFER_LEN)) > 0) MD5Update(&context, buf, i);
    MD5Final(digest, &context);
    for (i=0; i < DIGEST_LEN; i++) printf("%02x", digest[i]);
    printf("\n");
    return 0;
}

32bit 環境 (i686 Linux) では正しく動く:

senri:/home/sengoku/src/md5 % uname -m
i686
senri:/home/sengoku/src/md5 % ls
global.h  main.c  md5.h  md5c.c
senri:/home/sengoku/src/md5 % cc -Wall main.c md5c.c
senri:/home/sengoku/src/md5 % file a.out
a.out: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), for GNU/Linux 2.0.0, dynamically linked (uses shared libs), not stripped
senri:/home/sengoku/src/md5 % echo "Hello, world" | ./a.out
a7966bf58e23583c9a5a4059383ff850
senri:/home/sengoku/src/md5 % echo "Hello, world" | openssl md5
a7966bf58e23583c9a5a4059383ff850

ところが、 64bit 環境 (x86_64 Linux) だと:

senri:/home/sengoku/src/md5 % uname -m
x86_64
senri:/home/sengoku/src/md5 % cc -Wall main.c md5c.c
senri:/home/sengoku/src/md5 % file a.out
a.out: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), for GNU/Linux 2.4.0, dynamically linked (uses shared libs), not stripped
senri:/home/sengoku/src/md5 % echo "Hello, world" | ./a.out
fd578222c6a471623ea1e3eb2b6e6f6b

などと、 誤った MD5 の値が出力されてしまう。

MD5 の値を求めること自体が目的であれば、 誤ったハッシュ値が出力されればすぐ気付くのでいいのだが、 値そのものが目的であることは (当然ながら) あまりなくて、 普通はアプリケーションの中で MD5 を利用するので、 32bit 環境で使っていたアプリケーションを 64bit 環境でコンパイルし直して使おうとするとハマる。

(続きを読む...)
Filed under: プログラミングと開発環境 — hiroaki_sengoku @ 09:34
2010年2月25日

本ブログのサーバを海外レンタルサーバ Linode を使って冗長化・地域分散してみた

前回前々回に書いたように、 このブログ 「仙石浩明の日記」 (および 「仙石浩明CTO の日記」) は、 私の自宅にある PC サーバで動かしている。 2台の PC からなる冗長構成になっていて、 10年以上の安定稼働実績がある (両サーバ共に停止したことは皆無) が、 回線が (家庭用の) フレッツ光ネクストなので、 インターネットとの接続が切れてしまう可能性は皆無ではない。 実際、 Bフレッツからフレッツ光ネクストへ切り替えたときは 40分間ほど切れてしまった。 一応バックアップ回線として ADSL 回線も契約しているが、 こちらは固定 IP アドレスを割当てていないので WWW サービス用としては使いにくい。

短時間とはいえブログが見えないようなことがあると、 たまたまそのタイミングで (検索エンジン等で見つけて) 訪れた人が、 ブログが既に閉鎖してしまったものと勘違いしてしまうかもしれない。 ここは一つ自宅以外の場所にもサーバを立てて、 回線断の影響を受けないようにしたいところ。 海外のサーバなら地域分散もできてなおよい。

道楽で運営している自宅サーバにそこまで可用性を求めるのは、 やりすぎの感もあるが、 近頃流行りの VPS (Virtual Private Server, 仮想専有サーバ) は、 月額 $20 (初期費用無し) 程度で利用できるらしい。 $20 なら試しに使ってみるのも悪くない。

サービス
プラン名
メモリ
ディスク
帯域
追加額/単位
月額
年額
データセンタの
場所
ServerAxis Xen
VS000.5G-0025.0GP
512MB
25GB
0GB
$.05/GB
$15
$180
Chicago, IL
Linode
Xen 360
360MB
16GB
200GB
$10/100GB
$19.95
$215.46
Fremont, CA
Dallas, TX ...
VPSLink
Xen VPS Link-3
256MB
10GB
300GB
$.50/GB
$19.95
$201.12
Seattle, WA
New York, NY
slicehost
Xen VPS 256 slice
256MB
10GB
150GB
$.30/GB
$20
$216
Dallas, TX
St. Louis, MO

VPSLink 以外は最安のプランで比較した。 VPSLink には、 メモリ 64MB で月額 $7.95 の Link-1 プラン、 メモリ 128MB で月額 $13.95 の Link-2 プランもあるが、 256MB 未満のメモリではできることも限られてしまうので比較対象から外した。

月額基本料だけを比較すると、 ServerAxis が格安に見えるが、 これは帯域課金が含まれていないためで、 例えば 100GB (送受信の合計バイト数) 使うと $5 加算されて $20 と他社並になる。

データセンタの場所 (というかネットワーク上での位置) によって遅延時間が変わってくるので、 どこのデータセンタを使っているかも重要。 例えば同じ Linode でもデータセンタによって、 日本からアクセスしたときの RTT (Round Trip Time, 往復にかかる時間, ミリ秒) が倍以上変わってくる。 それぞれ 10回ずつ ping を打って、 RTT の最小/平均/最大を比較してみる:

データセンタの場所ホスト名 最小平均最大
Fremont, Californiafremont1.linode.com 121.341132.299146.939
Dallas, Texasdallas1.linode.com 171.049173.152174.905
Atlanta, Georgiaatlanta1.linode.com 183.184185.938190.174
Newark, New Jerseynewark1.linode.com 200.629203.053204.755
London, Englandlondon1.linode.com 269.324273.499276.822

米国西海岸だと RTT は 130msec 程度で済むが、 東海岸は 200msec を超えてしまい、 英国はもっと遠い。 西海岸にデータセンタがあるのは Linode と VPSLink だが、 Linode がデータセンタの情報を詳細に公開しているのに対し、 VPSLink は、 Spry Hosting のシアトルのデータセンタを使用している、 ということ以上の情報を公開していない (しかもこのページは 2006年8月の内容のまま) のが少し気になる。

サポートしている Linux ディストリビューションは、 各社だいたい共通で、 CentOS, Debian, Gentoo, Ubuntu の各バージョンが利用できるが、 VPSLink Ubuntu Plan のページに Ubuntu 9.10 Karmic が書かれていない点も、 ちょっと気になった (ちゃんとポリシーを持って 9.10 をサポートしないのならいいのだが、 どうもそうではなく単に運用の手を抜いているだけのような気配がする)。 Web 界隈での評判も Linode のほうがよさげだったので、 Linode を使ってみることに決めた。

サポートしているディストリビューションのバージョンが古くてもアップグレードできるのだろうが、 帯域で課金されるので最初から新しいバージョンをインストールできる方が好ましい。

サービス CentOSDebianGentooUbuntu Fedoraその他
ServerAxis 5.45.02008.09.10- Mandriva SUSE
Linode 5.35.02008.09.1011 Arch SUSE Slack
VPSLink 55O9.0411 Arch Slack
slicehost 5.45.02008.09.1012 Arch RedHat

おそらく各社ともカーネルも入れ替えることができるのだと思うが、 Linode 以外は使っていないので未確認。 Linode の場合は pv_ops (paravirtualization) を有効にしておけば任意のカーネルを利用できる。 したがって (帯域課金さえ気にしなければ ;-) 任意のディストリビューションの利用 (あるいは完全なカスタマイズ) が可能 (私はまだそこまではやっていない)。

なお、VPS サービスは探すといろいろあるようだ。 前述した 4社と比べると公開している情報が不十分 (特に Virtuozzo/OpenVZ な VPS は実際のパフォーマンスを予想しにくく、 実地に使ってみないとなんとも...) なので私は比較検討対象から外したが、 ダメ元で使ってみるのも面白いかもしれない:

サービス
プラン名
メモリ
ディスク
帯域
追加額/単位
月額
年額
データセンタの
場所
SplitServ
Xen 1024
1024MB
15GB
200GBs
$8/400GB
$14.95
$149.50
Los Angeles, CA
Kansas City, MO
WebKeepers
Virtuozzo ライト
512MB
50GB
?1280円
11760円
?
PhotonVPS
Xen VPS WARP 1
512MB
35GB
500GB
?
$16.95
$203.40
Los Angeles, CA
KnownHost
Virtuozzo VS2
512MB
30GB
750GB
$40/100GB
$35
$400
Los Angeles, CA
Dallas, TX
BurstNet
VPS PACKAGE #1
512MB
20GB
1000GB
$25/200GB
$5.95
$71.40
Scranton, PA
VPS NOC
OpenVZ Bronze
512 MB
20 GB
400 GB
?
$12.95
$155.40
Kansas City, MO
CoreNetworks
CoreMR
512MB
12GB
500GB
?
$19.95
$239.40
?
prgmr.com
Xen VPS
512MB
12GB
80GB
?
$12
$115.20
San Jose, CA
HostCadet
VPS Micro
512MB
10GB
400GB
?
$9.99
$119.88
Colorado Springs, CO
NY NOC OpenVZ
Super VPS #1
512MB
10GB
1000GB
?
$10.00
$120
Chicago, IL
North Bergen, NJ
ARP Networks
KVM/QEMU
512MB
10GB
100GB
?
$15
$180
Los Angeles, CA
VirtuallyDedicated
Xen VX384
384MB
24GB
300GB
?
$10.99
$131.88
Chicago, IL
Scranton, PA
GrokThis.net
Xen VPS
320MB
16GB
160GB
?
$20
$200
Philadelphia, PA
DMEHosting
OpenVZ VPS1
256MB
25GB
1000GB
$10/200GB
$5.95
$71.40
Denver, CO
Chicago, IL
Quantact
OpenVZ VS1
256MB
15GB
300GB
$1/GB
$14.99
$179.88
Santa Rosa, CA
RackspaceCloud
Xen CloudServers
256MB
10GB
0GB
$.08-$.22/GB
$10.95
$131.40
Dallas, TX
XENnode
XEN 256
256MB
10GB
200GB
?
$14.99
$143.99
Dallas, TX
Datarealm Xen
PowerVPS
256MB
10GB
1Mbps
Unmetered
$19.95
$215.52
?
HostVirtual Xen
XV0 Server
256MB
10GB
250GB
?
$14.95
$179.40
San Jose, CA
RackUnlimited
Xen RVPS-Starter
256MB
10GB
100GB
?
$8
$96
Asheville, NC
QuickWeb
Xen Lite
256MB
5GB
240GB
?
$17.95
$215.40
San Jose, CA
QuickVPS
Xen プラン1
256MB
10GB
50GB
?
2300円
21600円
日本国内
QuillHost
OpenVZ Standard
256MB
8GB
150GB
?
$9.99
$119.88
Washington, DC
Charlotte, NC

と、いうわけで、 Linode 360 を契約してみた。

(続きを読む...)
Filed under: システム構築・運用 — hiroaki_sengoku @ 08:58
2010年2月10日

CTO日記も livedoorブログから WordPress へ引越しました (URL は変更なし)

「仙石浩明の日記」 に続いて、 「仙石浩明CTO の日記」 も先週末に livedoorブログから自宅サーバへ引っ越した (つまりネームサーバの設定を変更して切替。有料プランの解約はこれから)。 もともと両ブログは相互にリンクを張って密接に連係していたので、 引越を機会に両者を統合した。

統合といっても両ブログは微妙(?)に読者層が異なると思われるし、 何よりページの体裁が大きく変わってしまっては読者の方々を戸惑わせてしまうので、 CTO日記を 「仙石浩明の日記」 の一カテゴリという位置付けにして、 かつページの体裁は WordPress のテーマを切り替えることによって、 どちらのブログもあまり大きな変化がないようにしている。

「仙石浩明CTO の日記」 http://sengoku.blog.klab.org/ をアクセスすると、 次のような PHP スクリプトを走らせた上で、 WordPress を呼び出す (末尾の require 文):

<?php
$new = NULL;
if ($_SERVER['REQUEST_URI'] == "/") {
    $new = "/blog/category/cto/";
} elseif ($_SERVER['REQUEST_URI'] == "/feed/") {
    $new = "/blog/category/cto/feed/";
} elseif (preg_match('@^/\d+/\d+/\d+/@',
		     $_SERVER['REQUEST_URI'], $matches)) {
    $new = $_SERVER['REQUEST_URI'];
...(中略)...
}
if ($new) {
    ...(中略)...
    $ORIG_SERVER_NAME = $_SERVER['SERVER_NAME'];
    $host = "www.gcd.org";
    $_SERVER['SERVER_NAME'] = $host;
    $_SERVER['REQUEST_URI'] = $new;
    $_SERVER['SCRIPT_NAME'] = $new;
    $_SERVER['PHP_SELF'] = $new;
    $abspath = "/usr/local/www/wordpress/";
    $themepath = "${abspath}wp-content/themes/sengoku_cto/";
    define('WP_USE_THEMES', true);
    define('TEMPLATEPATH', $themepath);
    define('STYLESHEETPATH', $themepath);
    require("${abspath}wp-blog-header.php");
...(中略)...
}
?>

つまり http://sengoku.blog.klab.org/ へのアクセスは、 パス名に 「/category/cto/」 を追加することによって、 CTO日記カテゴリへのアクセスに変換する。

ページの体裁については、 「wp-content/themes/sengoku_cto/」 ディレクトリが、 CTO日記のテーマフォルダで、 二つの PHP 定数 TEMPLATEPATH と STYLESHEETPATH をこのディレクトリへ設定することによって、 テーマの切り替えを行なっている。

テーマフォルダの中にあるテーマ関数ファイル 「functions.php」 は、 WordPress の初期化中に読み込まれるので、 ここに PHP スクリプトを書いておくことによって WordPress の挙動を変更することができる。 例えばブログのタイトルを 「仙石浩明CTO の日記」 に変更するには、 以下のスクリプトを functions.php に追加しておけばよい:

function option_blogname_cto() {
    return '仙石浩明CTO の日記';
}
add_filter('pre_option_blogname', 'option_blogname_cto');

つまり、 pre_option_blogname フックに、 option_blogname_cto フィルタを登録する。

WordPress では、 ブログのタイトルなど各種オプションの設定値 (DB に格納している) を、 get_option($setting) 関数を呼び出すことで参照している。 例えばタイトルは get_option('blogname') を呼び出すことで得られ、 URL は get_option('home') で得られる。

get_option($setting) 関数は wp-includes/functions.php で定義されていて、 以下のようにフィルタフック pre_option_* が定義されている:

function get_option( $setting, $default = false ) {
    global $wpdb;

    // Allow plugins to short-circuit options.
    $pre = apply_filters('pre_option_' . $setting, false);
    if ( false !== $pre )
	return $pre;
    ...(中略)...
}

つまり、 「pre_option_設定名」 というフックに登録されたフィルタが値を返すなら、 get_option はオプションの設定値ではなくフィルタが返した値を返すようになる。 前述の例なら、 「pre_option_blogname」 フックに登録された 「option_blogname_cto」 フィルタが 「仙石浩明CTO の日記」 という値を返すので、 get_option('blogname') も 「仙石浩明CTO の日記」 という値を返すようになり、 結果としてブログのタイトルを変更できる、というわけ。

ただし、 前述したように CTO日記は 「仙石浩明の日記」 の一カテゴリという位置付けなので、 ブログのタイトルを変更しただけだと、 ブログ 「仙石浩明CTO の日記」 の 「仙石浩明CTO の日記」 カテゴリということで、 ページのタイトル等が 「仙石浩明CTO の日記 » 仙石浩明CTO の日記」 という冗長なものになってしまう。 そこで、 以下のようなスクリプトを 「functions.php」 に追加して、 タイトルとカテゴリ名が同じときはカテゴリ名が表示されないようにする:

function single_cat_title_cto($category_name) {
    $name = get_option('blogname');
    if ($category_name == $name) return "";
    return $category_name;
}
add_filter('single_cat_title', 'single_cat_title_cto');

single_cat_title フックは、 wp-includes/general-template.php で定義されていて、 ページのタイトルなどに表示されるカテゴリ名を変更することができる。

以上で、 「仙石浩明の日記」 の一カテゴリを 「仙石浩明CTO の日記」 の体裁で見せることができるようになる。 しかし、 元が 「仙石浩明の日記」 であるだけに、 リンク先が全て 「仙石浩明の日記」 のページになってしまう。 例えば、 「仙石浩明CTO の日記」 のトップページの一番下に、 「古い投稿 »」 というリンクがあるが、 このリンク先が http://www.gcd.org/blog/page/2/ になってしまい、 たどると 「仙石浩明の日記」 のトップページの 2ページ目へ遷移してしまう。

また、 本文中 (あるいはサイドバー) に現れるリンクも、 DB のデータは 「仙石浩明の日記」 のパーマリンクを用いているので、 たとえそれが 「仙石浩明CTO の日記」 カテゴリに含まれていても、 そのリンクをたどると 「仙石浩明の日記」 の記事として表示されてしまう。

そこで、 遷移先も 「仙石浩明CTO の日記」 として表示したいリンクを、 フィルタで書き換えることにした。 つまり DB のデータは 「仙石浩明の日記」 へのリンクのままで、 ブラウザに送信する前に都度書き換える。

対象となるリンクは、 記事本文中だけでなく、 前述したページナビ 「古い投稿」 「新しい投稿」 や、 サイドバー (「人気記事」 や 「最近の投稿」) にも現れる。 ページ丸ごと (つまり HTTP レスポンス丸ごと) HTML を書き換えられるフックがあるとよかったのだが、 残念ながらそういうフックは定義されていないようだ。 以下のフックそれぞれについてリンクを書き換えればよさげ:

フィルタフック フィルタが変更できる対象, 第2引数, ...
the_content 記事本文 HTML
the_category 記事の末尾に表示されるカテゴリーリストの HTML,
$separator, $parents
get_pagenum_link ページ末尾に表示されるページナビ 「古い投稿」 「新しい投稿」 の URL
post_link 記事の URL (パーマリンク), $post, $leavename
widget_text サイドバーに表示されるテキストウィジェットの HTML, $instance
wp_list_categories サイドバーに表示されるカテゴリーのリストの HTML
category_feed_link カテゴリーの RSSフィードの URL, $feed

書き換え対象のリンクを決めるために、 まず CTO日記カテゴリに属す記事の ID を取得する:

function setup_cto_id() {
    global $wpdb;
    global $is_cto_id;
    $result = $wpdb->get_results("SELECT object_id FROM wp_term_relationships WHERE term_taxonomy_id=17", ARRAY_N);
    foreach ($result as $row) {
	$is_cto_id[$row[0]] = 1;
    }
}

あるカテゴリに属す記事ID のデータを取得する関数など、 WordPress に含まれているんじゃないかと探してみたのだが、 見つからなかったので DB に問合わせて取得するようにしてみた。 毎回 DB アクセスが発生してしまうが、 キャッシュとかはアクセス数が増えてから考える (^^;)。 「term_taxonomy_id=17」 が CTO日記のカテゴリ (決め打ち ^^;)。 CTO日記カテゴリに属す記事は、 配列 $is_cto_id[記事ID] に 1 を代入しておく。

次に URL を書き換えるスクリプト:

function replace_URL_cto($matches) {
    global $is_cto_id;
    global $ORIG_SERVER_NAME;
    if (!is_array($is_cto_id)) {
	setup_cto_id();
    }
    if (is_null($matches[2])) {
	if ($matches[1] == "category/cto/") return "http://$ORIG_SERVER_NAME/";
	return "http://$ORIG_SERVER_NAME/$matches[1]";
    } elseif ($is_cto_id[$matches[2]]) {
	return "http://$ORIG_SERVER_NAME/$matches[1]";
    }
    return $matches[0];
}

function convert_URLs_cto($text) {
    $textarr = preg_split("/(<.*>)/U", $text, -1, PREG_SPLIT_DELIM_CAPTURE);
    $stop = count($textarr);
    for ($i = 0; $i < $stop; $i++) {
	$content = $textarr[$i];
	if (strlen($content) > 0) {
	    $content = preg_replace_callback(
		    '@http://www.gcd.org/blog/(\d+/\d+/(\d+)|category/cto/)@',
		    'replace_URL_cto', $content);
	}
	$output .= $content;
    }
    return $output;
}

顔文字を画像に変換して表示するフィルタ wptexturize (wp-includes/formatting.php で定義されている) を参考にさせてもらった。 preg_replace_callback() を使って書き換え対象リンクを探し、 replace_URL_cto で記事ID がCTO日記カテゴリに属す ($is_cto_id[$matches[2]] が TRUE) 場合のみ書き換える。

最後に、 この書き換えフィルタ replace_URL_cto を前述したフィルタフックに追加:

add_filter('the_content', 'convert_URLs_cto');
add_filter('the_category', 'convert_URLs_cto');
add_filter('get_pagenum_link', 'convert_URLs_cto');
add_filter('post_link', 'convert_URLs_cto');
add_filter('widget_text', 'convert_URLs_cto');
add_filter('wp_list_categories', 'convert_URLs_cto', 12000);
add_filter('category_feed_link', 'convert_URLs_cto');

wp_list_categories にフィルタを追加すると Category Order プラグインと衝突するので、 優先順位を下げて Category Order プラグインの後で実行されるようにしている。

また、 the_category, post_link, widget_text, category_feed_link 各フックは、 2つ以上の引数を持つが、 第1引数 (書き換え対象の HTML あるいは URL) のみ使用するので引数の数 (add_filter の第3引数) を省略している。

Filed under: システム構築・運用,プログラミングと開発環境 — hiroaki_sengoku @ 07:46
« Newer PostsOlder Posts »