仙石浩明の日記

プログラミングと開発環境

このエントリーを含むブックマーク 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: プログラミングと開発環境 — 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: プログラミングと開発環境 — 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

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: プログラミングと開発環境 — 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: プログラミングと開発環境 — hiroaki_sengoku @ 16:01
このエントリーを含むブックマーク 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月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
このエントリーを含むブックマーク 2009年12月4日

apache httpd が 「(22)Invalid argument: alloc_listener: failed to get a socket for (null)」 エラーを出して立ち上がらない理由

私は普段持ち歩いているノートPC (レッツノート CF-R7) で coLinux を常用している。 Windows マシンで Linux を使いたい場合、 QEMU や VMware などの PC エミュレータ (完全仮想化) を用いる方法もあるが、 coLinux などの準仮想化の方がパフォーマンス的に有利なので、 日常的に使用する (私の場合、Windows マシンを使っていながらほとんどの作業は Linux の中で完結させている) なら coLinux の方が便利だと思う。

もちろん、 準仮想化であるから標準的なカーネルをそのまま走らせることはできず、 パッチをあてる必要がある。 残念ながら最新カーネル用のパッチはまだ作られていないようで、 現時点では Linux kernel 2.6.26.8 用のパッチが最新。

私は自分で管理している Linux マシン (自宅と職場合わせて 10台以上ある) は、 この coLinux なマシンも含めてハードディスク (正確に言えば Linux のパーティション) の内容を同一にしている。 すなわち、 マスタマシン (senri.gcd.org) の内容を定期/不定期的に rsync を使って各マシンへ同期させている。 マスタマシンのカーネルは Linux 2.6.31.6 なので、 マスタでビルドしたソフトウェアの中には、 Linux 2.6.26.8 ベースである coLinux 環境で動かないものも当然でてくる。

先日 apache httpd 2.2.14 を (マスタマシンで) ビルドしたら、 coLinux 環境で動かなかった:

# uname -a
Linux ikeda.gcd.org 2.6.26.8-co-0.8.0 #1 PREEMPT Sat Nov 14 19:23:55 JST 2009 i686 GNU/Linux
# /usr/apache2/bin/httpd -t
[Sun Nov 29 17:13:41 2009] [crit] (22)Invalid argument: alloc_listener: failed to get a socket for (null)
Syntax error on line 32 of /usr/apache2/conf/httpd.conf:
Listen setup failed

「Syntax error on line 32」 ということだが httpd.conf の 32行目は、

Listen 80

となっているので、 少なくとも 「Syntax error」 ではないことは明らか。 「Listen 80」 の代りに 「Listen localhost:80」 などと書けば、 「failed to get a socket for (null)」 というエラーメッセージが 「failed to get a socket for localhost」 に変わる。

このエラーメッセージを手がかりに apache httpd 2.2.14 のソースを探すと、 エラーを出しているのは server/listen.c の以下の部分:

(続きを読む...)
Filed under: プログラミングと開発環境 — hiroaki_sengoku @ 07:24
このエントリーを含むブックマーク 2009年10月8日

__sync_bool_compare_and_swap_4 とは何か? ~ glibc をビルドする場合は、 gcc の –with-arch=i686 configure オプションを使ってはいけない

glibc-2.10.1 をビルドしようとしたら、 「__sync_bool_compare_and_swap_4 が定義されていない」 というエラーが出た:

senri:/usr/local/src/glibc-2.10.1.i386 % ../glibc-2.10.1/configure
	...
senri:/usr/local/src/glibc-2.10.1.i386 % make
	...
/usr/local/src/glibc-2.10.1.i386/libc_pic.os: In function `__libc_fork':
/usr/local/src/glibc-2.10.1/posix/../nptl/sysdeps/unix/sysv/linux/i386/../fork.c:79: undefined reference to `__sync_bool_compare_and_swap_4'
/usr/local/src/glibc-2.10.1.i386/libc_pic.os: In function `__nscd_drop_map_ref':
/usr/local/src/glibc-2.10.1/nscd/nscd-client.h:320: undefined reference to `__sync_fetch_and_add_4'
	...
/usr/local/src/glibc-2.10.1.i386/libc_pic.os: In function `*__GI___libc_freeres':
/usr/local/src/glibc-2.10.1/malloc/set-freeres.c:39: undefined reference to `__sync_bool_compare_and_swap_4'
collect2: ld returned 1 exit status
make[1]: *** [/usr/local/src/glibc-2.10.1.i386/libc.so] Error 1
make[1]: Leaving directory `/usr/local/src/glibc-2.10.1'
make: *** [all] Error 2

__sync_bool_compare_and_swap_4 は gcc の組み込み関数なので、 関数が未定義であることを示す 「undefined reference to ...」 というエラーメッセージは、 誤解を招く不親切なメッセージだと思う。

__sync_bool_compare_and_swap_4(mem, oldval, newval) は、 mem が指し示すメモリの値 (4バイト分) が oldval であれば newval に変更する、 という操作をアトミックに行なう組み込み関数。 アトミック (不可分) 操作とは、 操作の途中が存在してはいけない操作のことで、 この例なら比較 (メモリの値が oldval か?) と代入 (newval に変更) が必ず 「いっぺん」 に行なわれ、 「比較だけ行なったけどまだ代入は行なわれていない」 という状態が存在しないことを意味する。

アトミックに行なうためには、 当然ながら CPU でその操作をサポートしている必要がある (複数個の命令の列で実現しようとすると、 命令列の半ばを実行中の状態が必ず存在してしまう) わけだが、 残念ながら Intel 386 プロセッサでは、 この compare_and_swap (CMPXCHG 命令) をサポートしておらず、 サポートするのは Intel 486 以降の CPU である。 テストプログラムを書いて試してみる:

#include <stdio.h>

int main() {
    int mem[1], oldval, newval;
    oldval=0;
    newval=1;
    mem[0] = 0;
    __sync_bool_compare_and_swap(mem, oldval, newval);
    printf("mem[0]=%d\n", mem[0]);
    return 0;
}

見ての通り、 mem[0] の値を oldval の値 (0) と比較し、 一致していたら newval の値 (1) を代入し、 mem[0] の値を表示するだけのプログラムである。

関数名が 「__sync_bool_compare_and_swap」 であって、 後ろに 「_4」 がついていないことに注意。 gcc が引数の型 (この例では int) を見て、 その型のビット長を後ろにつけてくれる (この例では int 型は 4 バイトなので 「_4」 をつけてくれる)。

gcc では 「-march=タイプ」 オプションを指定することによって CPU タイプを指定できる。 -march オプションを指定しなかったり (この場合は全 CPU でサポートされる組み込み関数のみ利用できる)、 あるいは -march=i386 を指定したりすると、 コンパイル時にエラーになる:

% gcc -Wall test.c
/tmp/cc4eNX6L.o: In function `main':
test.c:(.text+0x3b): undefined reference to `__sync_bool_compare_and_swap_4'
collect2: ld returned 1 exit status
% gcc -Wall -march=i386 test.c
/tmp/cc6chtFj.o: In function `main':
test.c:(.text+0x36): undefined reference to `__sync_bool_compare_and_swap_4'
collect2: ld returned 1 exit status
% gcc -Wall -march=i486 test.c
% ./a.out
mem[0]=1

いまさら i486 というのもアレなので、 今なら i686 を指定するのがよさげ。 私の手元にはいまだ PentiumIII マシンがあるものの、 PentiumIII より古いマシンはない (昨年 ML115 と SC440 を買ったとき PentiumII マシンを引退させた) ので、 pentium3 を指定すれば SSE (Streaming SIMD Extensions) が利用できるようになるが、 glibc をビルドするときに必要かというと、 たぶん必要ない。

というわけでエラーの原因は分かったが、 では glibc をビルドするときは、 どうすればいいだろうか?

とりあえず google で検索してみたら、 gcc の configure オプションに 「--with-arch=i686」 を指定して gcc をビルドする必要がある、 と書いてあるページが見つかった。

--with-arch オプションは、 -march のデフォルトを設定するための configure オプションである。 つまり 「--with-arch=i686」 を指定して gcc を再インストールすると、 gcc に -march オプションをつけなくてもデフォルトが i686 になる。 なるほど確かにそうすれば、 glibc 側で何も変更せずに __sync_bool_compare_and_swap_4 関数が使えるようになりそうである。

いまどき i686 以前の CPU 用のコードが必要になりそうなケースは滅多にないだろうから、 -march オプションのデフォルトを i686 にするのも悪い選択ではないように思えた。 gcc をビルドし直すのは面倒だなーと思いつつも、 ついでに gcc のバージョンを上げておこうと gcc-4.3.4 をダウンロードしてきて 「--with-arch=i686」 付でビルドしてみた。

ところが!

(続きを読む...)
Filed under: システム構築・運用,プログラミングと開発環境 — hiroaki_sengoku @ 09:39
このエントリーを含むブックマーク 2008年12月9日

freeRADIUS 2.1.3 のバグ: ログを stdout/stderr へ出力できない

無線LAN の脆弱性について警告が飛び交う昨今、 WPA2 (Wi-Fi Protected Access) といえど、 パーソナル (PSK, Pre-Shared Key) モードだとパスワード破りの可能性が 無いわけでも無いので、 エンタープライズ (EAP, Extensible Authentication Protocol) モードに乗り換えてみた。

EAP (社員支援プログラムではなくて、 拡張認証プロトコル) の認証方式には EAP-MD5, EAP-FAST, EAP-SKE, EAP-SRP, MS-CHAP, EAP-GTC, EAP-GTC, Cisco LEAP, EAP-TLS, EAP-TTLS, PEAP, EAP-MAKE, EAP-SIM などがあるが、 対応機器/ソフトウェアが多そうな EAP-TLS を使ってみることにした。 EAP-TLS とは、 TLS (Transport Layer Security) すなわち SSL (Secure Sockets Layer) のサーバ認証とクライアント認証を行なって、 RADIUS サーバと無線LAN 端末が相互に認証を行なう仕掛けである。

RADIUS サーバとしては、 free RADIUS 2.1.3 を使用した。 自前の認証局 でサーバ証明書とクライアント証明書を発行し、 それぞれ RADIUS サーバと無線LAN 端末 (Windows マシン) へインストールする (自分で発行する証明書だが、 認証する側が自分の管理下なので、 いわゆる「オレオレ証明書」ではない)。

まず radiusd をデバッグ モードで走らせてみる:

# radiusd -X
FreeRADIUS Version 2.1.3, for host i686-pc-linux-gnu, built on Dec  6 2008 at 17:50:58
Copyright (C) 1999-2008 The FreeRADIUS server project and contributors. 
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A 
PARTICULAR PURPOSE. 
You may redistribute copies of FreeRADIUS under the terms of the 
GNU General Public License v2. 
Starting - reading configuration files ...
including configuration file /usr/local/etc/raddb/radiusd.conf
including configuration file /usr/local/etc/raddb/clients.conf
including configuration file /usr/local/etc/raddb/eap.conf
group = radius
user = radius
including dictionary file /usr/local/etc/raddb/dictionary
	...(中略)...
Listening on authentication address * port 1812
Ready to process requests.

とりあえず動いているようだ。 Windows マシンからアクセスポイントへ接続してみると、 アクセスポイントを介して Windows マシンと RADIUS サーバ間で、 TLS サーバ/クライアント認証が行なわれ、 無事 WPA2 エンタープライズ モードで接続が完了した。

では、radiusd を daemontools 配下で動かそうと、 次のような /service/radius/run スクリプトを書いて動かしてみる:

#!/bin/sh
export PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin
exec 2>&1
exec radiusd -fxxx

-x を指定して詳細なデバッグ情報を出力させるようにする。 daemontools 配下で動かす場合、 multilog プログラムがログをどんどんローテートしてくれるので、 通常運用でもデバッグ情報を出力させておける。 ログを標準出力 (stdout) へ出力させるため、 設定ファイル radiusd.conf において、 次のように指定しておく。

log {
	destination = stdout
}

これでログが /service/radius/log/main/current に書き出されるはず、 と思ったら何も出力されない... 何故に...?

radiusd(8) によれば、-X オプションは 「-sfxx -l stdout」 と等価らしい。 -s オプションは、 RADIUS サーバを単一スレッド/プロセスで走らせるための指定。 個人で使う分には単一スレッドでも構わないといえば構わないので、 ログを stdout に出力する目的で -X オプションを使ってしまっても構わないのだが、 せっかくだからともうちょっと追ってみることにした。

まず上記 run スクリプトにおいて 「-l stdout」 を指定してみる。 すると、/service/radius ディレクトリに stdout というファイルができて、 そこにログが出力された。 ダメだこりゃ。 -l オプションはマニュアルには記載されていないので、 -l に続く 「stdout」 をファイル名と見なすのも一つの「仕様」と言えなくもないが...

ソース radiusd.c を見てみると、 確かに -l オプションの処理では続く引数をファイル名としてしか扱っていない。 では設定ファイル radiusd.conf に指定した場合はどうかと、 mainconfig.c を見てみる。 「log { ... }」 の中で 「destination = stdout」 を指定すると、 mainconfig.radlog_dest に RADLOG_STDOUT が代入されるようだ。 ところが、mainconfig.radlog_fd を設定するコードがない。 これでは stdout にログが出力されるはずがない。

「-l stdout」 の件は百万歩譲って「仕様」でも構わないが、 mainconfig.radlog_dest に RADLOG_STDOUT を代入しておきながら mainconfig.radlog_fd に代入し忘れるのは、 仕様うんぬん以前にソースとして首尾一貫していないので、 明らかにバグである。

そこで以下のようなパッチをあてて、 mainconfig.radlog_dest が RADLOG_STDOUT あるいは RADLOG_STDERR のときは、 mainconfig.radlog_dest に STDOUT_FILENO あるいは STDERR_FILENO を それぞれ代入するようにしてみた。

--- src/main/mainconfig.c~	2008-12-06 01:37:56.000000000 +0900
+++ src/main/mainconfig.c	2008-12-06 16:16:27.455277946 +0900
@@ -738,6 +738,10 @@
 				cf_section_free(&cs);
 				return -1;
 			}
+		} else if (mainconfig.radlog_dest == RADLOG_STDOUT) {
+			mainconfig.radlog_fd = STDOUT_FILENO;
+		} else if (mainconfig.radlog_dest == RADLOG_STDERR) {
+			mainconfig.radlog_fd = STDERR_FILENO;
 		}
 	}
 

これで無事、 ログが stdout に出力され、 /service/radius/log/main/current に書き出されるようになった。

Filed under: プログラミングと開発環境 — hiroaki_sengoku @ 08:02
このエントリーを含むブックマーク 2008年7月3日

Western Digital RMA チームから届いた文字化けメールを解読してみた

故障した HDD WD10EACS を RMA (Return Merchandise Authorization, 返却承認) 手続きで交換してみた」で書いたように、 RMA 手続きを行なった上で Western Digital へ故障したハードディスク ドライブ (以下 HDD と略記) を送ったら、 激しく文字化けしたメールが送られてきた。

あとは HDD が送られてくるのを のんびり待つだけと思っていたら、 わずか一日後 6/26 18:44 に Western Digital からメールが来た。 しかし文字化けがひどくて読めない。 最初は何語で書いてあるかすら判然としなかったのだが、 どうやら Shift JIS で書かれた文面を quoted-printable エンコードする際に なにか問題があったようだ。 例えば 0x82 が「,」に、0x95 が「.」に置き換わってしまっている。 置換が規則的でないので、 暗号解読よろしく一文字一文字置き換え規則を推測していくしかない。

文面を再現するのに時間がかかりそうだなぁ~と思っている間に、 交換品の HDD が届いてしまったので、 「暗号」解読するモチベーションを失ってしまっていたのだが、

Posted by 通りすがり 2008年07月02日 00:36
結局、メールにはなんて書いてあったのでしょうか?

というコメントを頂いてしまったので、 暗号解読してみることにした。

以下、Western Digital からの文字化けメールを全文引用 (一部伏字) する:

From: "Western Digital RMA" <noreply@wdc.com>
To: <sengoku@gcd.org>
Date: Thu, 26 Jun 2008 02:44:25 -0700
MIME-Version: 1.0
Content-Type: text/plain;
	charset="iso-8859-1"
Content-Transfer-Encoding: quoted-printable
X-Mailer: Microsoft CDO for Windows 2000
Content-Class: urn:content-classes:message
X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2800.1896
X-OriginalArrivalTime: 26 Jun 2008 09:44:25.0728 (UTC) FILETIME=[3861F800:01C8D771]

HIROAKI SENGOKU -l,=D6=81A

^=C8?=BA,=C9.\Z=A6,=B3,=EA,=BD RMA =
,=CCfXfe=81[f^fX,=F0Sm"F,=B5,=C4,=AD,=BE,=B3,=A2=81B  RMA
,=C9S=D6,=B7,=E9,=A8-=E2,=A2=8D?,=ED,=B9,=CD,=B1,=CCf=81=81[f<,=C9.=D4=90=
M,=B5,=C4,=AD,=BE,=B3,=A2=81B
=8F=EE.=F1,=AA=90=B3,=B5,=A2=8F=EA=8D?=81A,=B1,=CC"dZqf=81=81[f<,=C9,=CD.=
=D4=90M,=B5,=C8,=A2,=C5,=AD,=BE,=B3,=A2=81B


RMA "=D4=8D?=81F 8083XXXX

--------------------------------------------------------------

O=F0S=B7fhf?fCfu,=F0 5=81`7 ?c<=C6"=FA'?,=C9"=AD'-,=B5,=DC,=B7=81B

^=C8?=BA,=CCfhf?fCfu,=F0 Western Digital =
,=CDZ=F3-=CC,=B5,=DC,=B5,=BD=81F

     fVfSfAf<"=D4=8D?     =90=BB.i"=D4=8D?             =
Z=F3-=CC"=FA=81iGMT=81j
     ------------     ---------------      -------------
     WCASJxxxxxxx     WD10EACS-00ZJB0      6/25/2008

--------------------------------------------------------------

^=C8?=BA,=C9.\Z=A6,=B3,=EA,=BD RMA =
"=AD'-=8F=F3<=B5,=F0Sm"F,=B5,=C4,=AD,=BE,=B3,=A2=81B

-A'-<=C6Z=D2,=CCfVfXfef?,=CC=8DX=90V,=C91?c<=C6"=FA,=AA,=A9,=A9,=E8=81A,=BB=
,=CCO=E3"=AD'-'=C7=90=D5"=D4=8D?,=AA-LO=F8,=C9,=C8,=E8,=DC,=B7,=CC,=C5=81=
A,=B2-=B9=8F=B3,=AD,=BE,=B3,=A2=81B

O=F0S=B7fhf?fCfu,=CC'-.t=90=E6=81F

     HIROAKI SENGOKU
     XXXXXXXXXXXXXXXXXXXXXXXXX TAKATSU
     KAWASAKI, Japan 213-XXXX
     JAPAN

"z'-<=C6Z=D2=81F     Fedex
"z'-'=C7=90=D5"=D4=8D?=81F XXXXXXXXXXXX

     fVfSfAf<"=D4=8D?     =90=BB.i"=D4=8D?             =
"=AD'-"=FA=81iGMT=81j
     ------------     ---------------      -------------
     WCASJXXXXXXX     WD10EACS-32ZJB0      6/26/2008

--------------------------------------------------------------

S=D6~AfSf"fN=81F
RMAZ=E8=8F?ZwZ=A6=8F=EE.=F1,=CC?{--/^=F3=8D=FC
  - =

http://websupport.wdc.com/rd.asp?t=3D102&l=3Djp&p=3Dm&r=3D8083XXXX&f=3De

"=AD'-,=C6=8D=AB.=EF,=CC=8F=EE.=F1
  - http://websupport.wdc.com/rd.asp?t=3D103&l=3Djp&p=3Drp

RMAfXfe=81[f^fX,=CC?{--
  - =

http://websupport.wdc.com/rd.asp?t=3D104&l=3Djp&p=3Dv&r=3D8083XXXX&z=3D21=

3-XXXX

Western Digital fTf|=81[fgfz=81[f?fy=81[fW
  - http://websupport.wdc.com/rd.asp?t=3D105&l=3Djp&p=3Dh

^=C8=8F=E3=81A
WD RMA f`=81[f?

http://websupport.wdc.com/rd.asp?t=3D105&l=3Djp&p=3Dh

ヘッダに「quoted-printable」と書いてあるとおり、 quoted-printable エンコーディングを行なったのだろうが、 のっけから「^=C8?=BA,=C9.\Z=A6,=B3,=EA,=BD」となっていて、 一体何語なんだ?と思わせる始まり方である。

ちなみに quoted-printable というのは 8bit データを、 「印字可能 (printable)」つまり 7bit の英数字・記号だけで表現するための方法 (エンコーディング) で、 印字可能でない 8bit データは 16進数で表わして前に「=」をつける (「=」自身は「=3D」で表現する)。 例えば「^=C8?=BA,=C9.\Z=A6,=B3,=EA,=BD」は、 16進数で書くと 「5E C8 3F BA 2C C9 2E 5C 5A A6 2C B3 2C EA 2C BD」 という 8bit データ列を意味する。

腕に覚えのあるかたは、解答を見ずに解読を試みてはいかがだろうか?

(続きを読む...)
このエントリーを含むブックマーク 2008年5月12日

x86_64 な Linux カーネルで i386 プログラムを実行するときの注意点 ── ivtv ドライバの ioctl インタフェース

64bit Linux (x86_64 別名 amd64) は、 CONFIG_IA32_EMULATION を有効にしておくことにより、 32bit プログラム (i386 別名 ia32) を走らせることができる。 したがって 64bit へ移行する際は、 全プログラムを一度に 64bit 化する必要はなく、 まずカーネルだけ 64bit 化しておいて、 各プログラムは (バージョンアップの機会などに) 徐々に 64bit 化していけばよい。 ただし 32bit プログラムがカーネルの機能を呼び出す場合は、 各機能それぞれが 32bit プログラムからの呼び出しに対応していることが前提となる。

32bit プログラムからの呼び出しに対応するといっても、 基本的には引数の型を変換するだけである。 x86_64 の整数データモデルは LP64、 つまり long int 型とポインタ型が 64bit で (引数の型として多用される) int型は 32bit のままなので、 変換が不要なケースも多い。

例えば ioctl システムコールはファイル・ディスクリプタ (file descriptor, 以下 fd と略記) ごとに カーネルが実行すべき機能は変わってくるわけで、 その実装は各デバイス・ドライバに委ねられることが多い。 したがって 32bit プログラムからの ioctl 呼び出しに応えられるか否かは、 各ドライバが 32bit 対応しているか否かに依存する。 不幸にしてドライバが対応していない場合は、

ioctl32(tv:11028): Unknown cmd fd(5) cmd(40045613){t:'V';sz:4} arg(081ec8b4) on /dev/video0

などといったカーネル・メッセージ (dmesg) が出力される。 このメッセージは、 カーネル・ソース中 fs/compat_ioctl.c の compat_ioctl_error が出力している:

static void compat_ioctl_error(struct file *filp, unsigned int fd,
    unsigned int cmd, unsigned long arg)
{
    ...
    compat_printk("ioctl32(%s:%d): Unknown cmd fd(%d) "
            "cmd(%08x){t:%s;sz:%u} arg(%08x) on %s\n",
            current->comm, current->pid,
            (int)fd, (unsigned int)cmd, buf,
            (cmd >> _IOC_SIZESHIFT) & _IOC_SIZEMASK,
            (unsigned int)arg, fn);
    ...
}

fs/compat_ioctl.c は 32bit 版 ioctl システムコールを実装していて、 32bit プログラムが ioctl システムコールを呼び出すと、 この中の compat_sys_ioctl ルーチンが呼ばれる:

asmlinkage long compat_sys_ioctl(unsigned int fd, unsigned int cmd,
                unsigned long arg)
{
    ...
        if (filp->f_op && filp->f_op->compat_ioctl) {
            error = filp->f_op->compat_ioctl(filp, cmd, arg);
            if (error != -ENOIOCTLCMD)
                goto out_fput;
        }
    ...
            compat_ioctl_error(filp, fd, cmd, arg);
    ...
 out_fput:
    fput_light(filp, fput_needed);
 out:
    return error;
}

つまりドライバ側で file 構造体の compat_ioctl 関数ポインタ (filp->f_op->compat_ioctl) が定義されていればそれが呼ばれ、 未定義ならば上記のような「Unknown cmd」エラーが出力される。

ちなみにこのエラーメッセージの「tv:11028」は、 ioctl を呼び出した 32bit プロセスの名前 (コマンド名) とプロセスID であり、 fd(5), cmd(40045613), arg(081ec8b4) は、 それぞれ ioctl システムコールの第一 (つまり fd 番号)、 第二 (ioctl リクエスト番号)、第三引数 (ioctl リクエストの引数) であり、 最後の on /dev/video0 は (第一引数の) fd 番号に対応するファイルのパス名である。

そして、この tv コマンドは 「ビデオキャプチャ・カード GV-MVP/RX2W を使って Linux 2.6.24.4 でテレビ録画」で紹介した perl スクリプト であり、 その名称から推測できるとおりテレビ録画を行なうためのスクリプトである。

このスクリプトでは Video::ivtv モジュールを利用していて、 このモジュールが /dev/video0 つまり TV キャプチャ・デバイスに対して、 ioctl システムコールを呼び出している。 上記エラーはスクリプト中 $IvTV->stopEncoding($TunerFD); を実行したときに発生した。

その名称から推測できる通り、 stopEncoding メソッドはキャプチャ・デバイスに対して エンコーディングの停止を指示するためのもので、 内部で ioctl(fd, VIDIOC_STREAMOFF) などと ioctl 呼び出しを行なっている。 VIDIOC_STREAMOFF は videodev2.h にて、

#define VIDIOC_STREAMOFF    _IOW  ('V', 19, int)

と定義されていて、このマクロを展開すると 40045613 (16進) となり、 上記カーネル・メッセージ「cmd(40045613)」と一致する。

というわけで、(少なくとも Linux 2.6.24.7 に含まれる) ivtv ドライバは、 残念ながら 32bit 対応していないことが分かった。 もちろん x86_64 なカーネルではなく、 i386 カーネルを使えば 32bit プログラムから ivtv ドライバを使うことができるが、 x86_64 なカーネルでは、32bit プログラムからの ioctl システムコールを 64bit カーネルが受付けられる形に変換できないということだ。

とはいうものの、 32bit だろうが 64bit だろうが ioctl のインタフェースに大した変わりはないはずだ。 どうして ivtv ドライバは 32bit 呼び出しをサポートしていないのだろう? と 思いながらドライバのソースを眺めていると... drivers/media/video/compat_ioctl32.c を見つけた。 名前からしていかにも 32bit 版 ioctl のように見える。

compat_ioctl32.c の中の v4l_compat_ioctl32 ルーチンは、 32bit な ioctl 呼び出しを受付けて 引数を 64bit へ変換し (といっても int 型はどちらも 32bit だが)、 本来の (64bit な) ioctl を呼び出し直す仕組みになっている。 なぜ ivtv ドライバは、このルーチンを利用していないのだろうか。

static int do_video_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
    ...
    /* First, convert the command. */
    switch(cmd) {
        ...
    case VIDIOC_STREAMOFF32: realcmd = cmd = VIDIOC_STREAMOFF; break;
    };

    switch(cmd) {
        ...
    case VIDIOC_STREAMOFF:
        err = get_user(karg.vx, (u32 __user *)up);
        compatible_arg = 1;
        break;
        ...
    };
    if(err)
        goto out;

    if(compatible_arg)
        err = native_ioctl(file, realcmd, (unsigned long)up);
    else {
        mm_segment_t old_fs = get_fs();

        set_fs(KERNEL_DS);
        err = native_ioctl(file, realcmd, (unsigned long) &karg);
        set_fs(old_fs);
    }
    ...
    return err;
}

long v4l_compat_ioctl32(struct file *file, unsigned int cmd, unsigned long arg)
{
    ...
        ret = do_video_ioctl(file, cmd, arg);
        break;
    ...
    return ret;
}

ざっと見た感じ、 ivtv ドライバからこの v4l_compat_ioctl32 ルーチンを呼んでも 特に問題は無いように思われる。

そこで、ivtv ドライバの file 構造体 (の中の file_operations 構造体) の compat_ioctl 関数ポインタに、 v4l_compat_ioctl32 を設定してみた。

--- linux-2.6.24.5.org/drivers/media/video/ivtv/ivtv-streams.c	2008-01-25 07:58:37.000000000 +0900
+++ linux-2.6.24.5/drivers/media/video/ivtv/ivtv-streams.c	2008-05-04 09:10:07.581416212 +0900
@@ -49,6 +49,7 @@
       .write = ivtv_v4l2_write,
       .open = ivtv_v4l2_open,
       .ioctl = ivtv_v4l2_ioctl,
+      .compat_ioctl = v4l_compat_ioctl32,
       .release = ivtv_v4l2_close,
       .poll = ivtv_v4l2_enc_poll,
 };
@@ -59,6 +60,7 @@
       .write = ivtv_v4l2_write,
       .open = ivtv_v4l2_open,
       .ioctl = ivtv_v4l2_ioctl,
+      .compat_ioctl = v4l_compat_ioctl32,
       .release = ivtv_v4l2_close,
       .poll = ivtv_v4l2_dec_poll,
 };

このパッチをあてることにより、 x86_64 なカーネル上で i386 な Video::ivtv モジュールを使って、 ビデオキャプチャ・カード GV-MVP/RX2W を コントロールすることができるようになった。 一週間ほど使ってみた (多数の TV 番組を予約録画した) が、 今のところ問題は起きていない。

このエントリーを含むブックマーク 2007年12月25日

2行でできる高精度ハードウェア自動認識 (initramfs の init を busybox だけで書く)

10行でできる高精度ハードウェア自動認識」にコメントを頂いた:

最近の modprobe は、 自分で勝手に modules.alias を探してくれるようになっているようです。 この機能を使うと、 より簡単かつ高速に自動認識が可能になります。

そうだったのか... orz

いままで、 modules.alias から modporbe すべきモジュールを検索するために、 以下のような感じで sh スクリプト (/tmp/dev2mod) を生成し、 それを読み込んで (. $tmp) いたのだが、

tmp=/tmp/dev2mod
echo 'dev2mod(){ while read dev; do case $dev in' > $tmp
sort -r /lib/modules/`uname -r`/modules.alias \
| sed -n 's/^alias  *\([^ ]*\)  *\(.*\)/\1)modprobe \2;;/p' >> $tmp
echo 'esac; done; }' >> $tmp
. $tmp
rm $tmp
cat /sys/bus/*/devices/*/modalias | dev2mod

modprobe が自分で modules.alias を探してくれるとなると、 sh スクリプトを動的生成する必要が無くなってしまい、 上記コードは次のように書けてしまう:

dev2mod(){ while read dev; do modprobe $dev; done }
cat /sys/bus/*/devices/*/modalias | dev2mod

わずかに 2行 (^^;)

/sys/bus/*/devices/*/modalias の内容を手当たり次第 modprobe するので、 modprobe が「failed to load module」というエラー・メッセージを出してしまうが、 特に問題は無さげである。

PCMCIA や USB につないだデバイスも、 以下のように dev2mod を二度呼び出すだけで、 自動認識してしまう。

dev2mod(){ while read dev; do modprobe $dev; done }
cat /sys/bus/*/devices/*/modalias | dev2mod
modprobe pcmcia
cat /sys/bus/*/devices/*/modalias | dev2mod

う~んすごい。

Filed under: プログラミングと開発環境 — hiroaki_sengoku @ 08:04
このエントリーを含むブックマーク 2007年11月22日

x86_64 Linux でメモリ・デバッグ・ツール Valgrind を使う場合の注意点

次のようなプログラム test.c について考える:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <string.h>

struct test {
    int32_t len;
    int8_t buf[16];
};

int main(int argc, char *argv[]) {
    struct test *p = malloc(sizeof(struct test));
    int8_t buf[16];
    p->len = sizeof(p->buf);
    bzero(p->buf, p->len);
    printf("0x%lX-0x%lX => 0x%lX\n",
	   (long)p->buf, (long)p->buf+p->len-1, (long)buf);
    bcopy(p->buf, buf, p->len);
    free(p);
    return 0;
}

malloc(3) で確保した領域のうち、 16 byte を bcopy(3) でコピーするだけの極めて単純なプログラムであり、 特に問題はないように見える。

ところが memory debugging tool Valgrind を使って検証してみると、 x86_64 Linux だと次のようなエラーが出てしまう。

sag16:/home/sengoku/tmp % cc -O -Wall test.c
sag16:/home/sengoku/tmp % valgrind ./a.out
==19008== Memcheck, a memory error detector.
==19008== Copyright (C) 2002-2006, and GNU GPL'd, by Julian Seward et al.
==19008== Using LibVEX rev 1658, a library for dynamic binary translation.
==19008== Copyright (C) 2004-2006, and GNU GPL'd, by OpenWorks LLP.
==19008== Using valgrind-3.2.1-Debian, a dynamic binary instrumentation framework.
==19008== Copyright (C) 2000-2006, and GNU GPL'd, by Julian Seward et al.
==19008== For more details, rerun with: -v
==19008==
0x4D5C034-0x4D5C043 => 0x7FF000750
==19008== Invalid read of size 8
==19008==    at 0x4B9326B: (within /lib/libc-2.3.6.so)
==19008==    by 0x4B92C06: bcopy (in /lib/libc-2.3.6.so)
==19008==    by 0x4005BD: main (in /home/sengoku/tmp/a.out)
==19008==  Address 0x4D5C040 is 16 bytes inside a block of size 20 alloc'd
==19008==    at 0x4A1B858: malloc (vg_replace_malloc.c:149)
==19008==    by 0x400574: main (in /home/sengoku/tmp/a.out)
==19008==
==19008== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 8 from 1)
==19008== malloc/free: in use at exit: 0 bytes in 0 blocks.
==19008== malloc/free: 1 allocs, 1 frees, 20 bytes allocated.
==19008== For counts of detected errors, rerun with: -v
==19008== All heap blocks were freed -- no leaks are possible.

「Invalid read of size 8」、 すなわちアクセスすべきではないメモリを、 64bit (8 byte) 読み込み命令で読んだというエラー。

test.c で読み込みを行なう可能性があるところと言えば、 「bcopy(p->buf, buf, p->len);」の部分だけであり、 その範囲は printf で表示しているように、 0x4D5C034 番地から 0x4D5C043 番地までの 16 byte である。

ところが、Valgrind 曰く:

Address 0x4D5C040 is 16 bytes inside a block of size 20 alloc'd

ちょっと英語の意味が取りにくい (私の英語力が低いだけ? ^^;) が、 つまり「malloc で確保した 20 byte の領域のうち、 先頭から数えて 16 byte 目 (先頭は 0 byte 目と数える) が 0x4D5C040 番地であり、 この番地に対してメモリ読み込みが行なわれた」 という意味である (「16 byte 目」なら 「16 bytes」でなくて「16th byte」のような...?)。

すなわち、 「20 byte の領域のうち 16 byte 目」というのは残り 4 byte であり、 あと 4 byte コピーすればいいのにもかかわらず、 64bit 読み込み命令を使って 8 byte いっぺんに読んでしまっているから、 malloc で確保した領域の外をアクセスしてしまう、というわけ。

結果として 4 byte 無駄に読んでしまっている (実はコピー開始位置も 4 byte 前から行なうので、計 8 byte 余計に読み込んでいる) わけだが、 CPU にとって一番高速にコピーできる単位が (64bit 境界に合わせた) 64bit 読み書きだから、 bcopy の実装がこのようになっているのだろう。

より正確に言えば、 bcopy は 16 byte 以上のコピーを行なう場合は コピー開始位置手前の 64bit 境界 (alignment) の番地から 64bit ずつコピーし、 16 byte 未満の場合は byte 単位でコピーする。 test.c では、 コピー開始位置 p->buf が (直前のメンバが int32_t なので) 64bit 境界に一致しておらず、 しかもコピーする byte 数 p->len が 16 byte (= 64bit の倍数) なので、 16 byte 以上のコピーかつコピー終了位置も 64bit 境界に一致していない、 というのがミソである。

したがって 32bit な x86 Linux の場合であれば 32bit 単位でコピーを行なうので、 test.c ではこのようなエラーは起きない。 もちろん、64bit な x86_64 Linux で Valgrind がエラーを出すからといって、 bcopy の x86_64 における実装に問題がある、というわけではない。 Valgrind は、 あくまでバグの「可能性」を指摘するだけであって、 malloc で確保した領域の外へのアクセスでも、 それが意図的なものであれば (メモリ保護違反などでない限り) 何の問題もない。

分かってみれば単純な話なのであるが、 Valgrind のメッセージ「16 bytes inside a block」の意味が把握できなかった私は、 glibc の bcopy のソースを読んで 64bit 単位でコピーを行なっていることを知り、 4 byte の領域外読み込みが行なわれることを理解して初めて、 Valgrind のメッセージの意味が分かったという、 本末転倒な体験をした (^^;)。

ちなみに、 もちろん最初から上記のようなテストプログラムを Valgrind で チェックしようと思ったわけではなく、 「struct test」構造体は実際には次のような SockAddr 構造体であり、 saDup 関数にて malloc した SockAddr 構造体を doconnect 関数で bcopy する処理になっていて、 元ネタは拙作 stone である。

typedef struct {
    socklen_t len;
    struct sockaddr addr;
} SockAddr;
#define SockAddrBaseSize	((int)&((SockAddr*)NULL)->addr)
...

SockAddr *saDup(struct sockaddr *sa, socklen_t salen) {
    SockAddr *ret = malloc(SockAddrBaseSize + salen);
...

int doconnect(Pair *p1, struct sockaddr *sa, socklen_t salen) {
    struct sockaddr_storage ss;
    struct sockaddr *dst = (struct sockaddr*)&ss;	/* destination */
...
    bcopy(sa, dst, salen);
...

stone ML にて、 Valgrind で検証したらエラーが出た、という報告を頂いて (_O_) 以上のような調査を行なった次第。 bcopy に与えた引数に問題はなく、 どうしてこれが 「Invalid read of size 8」 エラーを引き起こすのか謎だった。 結果的には stone には問題はなく、 修正の必要もないことが判明したわけであるが、 今まで使っていなかった Valgrind を使ってみるいいきっかけになった。 実を言うと 64bit Linux を (プログラミングのレベルで) 使ったのも、 今回が初めてだったりする (^^;)。

Filed under: stone 開発日記,プログラミングと開発環境 — hiroaki_sengoku @ 20:36
このエントリーを含むブックマーク 2007年9月29日

10行でできる高精度ハードウェア自動認識 (initramfs の init を busybox だけで書く)

これまでLinuxのハードウェア自動認識と言えば、 /sys/bus/pci/devices 以下と、 /lib/modules/`uname -r`/modules.pcimap を照らし合わせて 解析していくのが定石でした。 USBにも対応しようとすると、もう一つ大変です。
しかしこれからの常識は、 /sys/bus/*/devices/*/modalias と
/lib/modules/`uname -r`/modules.alias です。
古橋貞之の日記「20行できる高精度ハードウェア自動認識」から引用

すばらしい。 確かに modules.alias を使う方が、 簡単かつ確実に必要なモジュールを読み込むことができそう。 さっそくこの方法を使って initramfs の init スクリプトを書き直してみた。

tmp=/tmp/dev2mod
echo 'dev2mod(){ while read dev; do case $dev in' > $tmp
sort -r /lib/modules/`uname -r`/modules.alias \
| sed -n 's/^alias  *\([^ ]*\)  *\(.*\)/\1)modprobe \2;;/p' >> $tmp
echo 'esac; done; }' >> $tmp
. $tmp
rm $tmp
cat /sys/bus/*/devices/*/modalias | dev2mod

わずかに 8行 (^^)
(9/30追記: modules.alias を逆順ソートしておく必要があることが判明、sort -r を追加)。

シェルスクリプト版はRuby版と比べて40倍くらい遅いので注意。
同ページ(古橋貞之の日記)から続けて引用

sh スクリプトの名誉のために言っておくと、 私が書いた上記 sh スクリプトだと、 古橋さんの Ruby 版と比べて 4倍くらいの遅さで済んでいる。

% time ./dev2mod
ide_cd
intel_agp
intelfb
uhci_hcd
...(中略)...
libusual
usbcore
0.252u 0.012s 0:00.77 33.7%	0+0k 0+0io 0pf+0w
% time ./detect_kmod.rb
["ivtv", "snd_intel8x0", "intelfb", "libusual", "ftdi_sio", "usbhid", "uhci_hcd", "ehci_hcd", "usbcore", "via_velocity", "eepro100", "e100", "3c59x", "psmouse", "ide_cd", "i2c_i801", "hw_random", "intel_agp"]
0.072u 0.008s 0:00.18 38.8%	0+0k 0+0io 0pf+0w

ちなみに古橋さんのスクリプトは、 modules.alias の各行それぞれに対し、 マッチするデバイスが /sys/bus/*/devices/*/modalias に存在すれば、 そのモジュールを読み込む処理になっている。
しかしながら、これだと一つのデバイスに対し、 複数のモジュールが読み込まれてしまうことになるのではないだろうか?

古橋さんが同日追記されているように、 複数のモジュールが読み込まれること自体は簡単に修正可能で、 むしろモジュールの読み込み順が modules.alias に載っている順になることのほうが問題。 この問題点を解決するため、 /sys/bus/*/devices/*/modalias の各行それぞれに対し、 マッチするモジュールを modules.alias から見つける修正版が追記された。 さすが古橋さん、すばやい。
9/30追記

例えば古橋さんのスクリプトだと、 私の手元のマシンでは e100 と eepro100 の両方のモジュールが読み込まれてしまう。 つまり、

% cat /sys/bus/pci/devices/0000:01:08.0/modalias
pci:v00008086d00001050sv0000107Bsd00004043bc02sc00i00

が、modules.alias の次の二つの行にマッチするため、 このようなことが起こる。

alias pci:v00008086d00001050sv*sd*bc*sc*i* eepro100
alias pci:v00008086d00001050sv*sd*bc02sc00i* e100

modules.alias を検索する際は、 マッチする行が見つかった時点で以降の行はスキップしないと、 この例のように複数のモジュール読み込みが起きる恐れがある。 マッチした以降の行を読み飛ばすには、 私が書いた上記 sh スクリプトのように、 /sys/bus/*/devices/*/modalias の各行それぞれに対し、 マッチするモジュールを一つだけ modules.alias から見つけて読み込む処理のほうが、 簡単に書けるのではないかと思うがどうだろうか。

とはいえ、実際の NIC は Intel Pro 10/100 だったりする (^^;) ので、 読み込むべきモジュールは e100 であるような気もする。 もし e100 が正しいモジュールであるのなら、 modules.alias における eepro100 のパターンが適切ではないということになるのかも。
9/30追記
「*」を多く含むパターンは「後で」マッチさせたほうが、 より適切なモジュールを選択できると考えられるため、 modules.alias を逆順ソートしておくことにした。 これにより、eepro100 ではなく、e100 を読み込むようになった。
9/30さらに追記

参考までに initramfs の /init スクリプト全体を添付しておく:

(続きを読む...)
Older Posts »