Ruby on Rails 1.2.6 + GetText でActionMailerを使ってフォームから入力された内容をメールで送信するような実装をしていたところ、「〜」を入力すると文字化けするとの連絡が。
調べたところ、Windowsで「〜」を入力すると、fullwidth tilde(U+FF5E, EFBD9E)のコードが渡されるんですが、GetTextから呼ばれているrubyのNKFモジュールはこれをJIS補助漢字の(2237)に変換するため、補助漢字を表示できないメールクライアントでは文字化けするようでした。より文字化けしにくいと思われるJIS基本漢字の(2141)の方はwave dash(U+301C, E3809C)に割り当てられているようです。 さらに探るとどろどろした歴史が垣間見えてきたのでこれはこういうもんだと納得することに。
かといって、全Windowsユーザーがらみで発生する問題を無視するわけにもいかないので、fullwidth tildeをwave dashに変換してしまうことに。
具体的には、
if defined? ActionMailer module ActionMailer #:nodoc: class Base #:nodoc: def create!(*arg) #:nodoc: create_without_gettext!(*arg) if Locale.get.language == "ja" require 'nkf' @mail.subject = base64(@mail.subject) part = @mail.parts.empty? ? @mail : @mail.parts.first if part.content_type == 'text/plain' part.charset = 'iso-2022-jp' if NKF.guess(part.body) == NKF::UTF8 part.body = part.body.gsub("\xef\xbd\x9e", "\xe3\x80\x9c") end part.body = NKF.nkf('-j', part.body) end end @mail end end end end
require 'lib/gettext_ext.rb'
ところで、
part.body = part.body.gsub("\xef\xbd\x9e", "\xe3\x80\x9c")
の行が
part.body.gsub!("\xef\xbd\x9e", "\xe3\x80\x9c")
では駄目だったんですけど、なぜ??
前回の続き。
前回の対処後、他に文字化けしないか検証していたところ、SubjectにJIS補助漢字が入ると文字化けすることを確認。
単純に、前回のソースの
@mail.subject = base64(@mail.subject)
の部分を
if NKF.guess(@mail.subject) == NKF::UTF8 @mail.subject = @mail.subject.gsub("\xef\xbd\x9e", "\xe3\x80\x9c") end @mail.subject = base64(@mail.subject)
のように書き換えると、「〜」に関してはうまくいくようになるが、コピーライトマークのような他のJIS補助漢字を試すと、Thunderbirdなどの、本文の時には文字化けが発生していなかったクライアントでも化けてしまう。
具体例を挙げると、コピーライトマーク一文字のSubjectは、「=?iso-2022-jp?B?GyRCRCJtGyhC?=」となり、これをデコードすると 「ESC $ B D " ( B ESC ( B \n」という完全に壊れた文字列になる。
で、ソース探索の旅に出て、ようやくたどり着いたのがTMailのEncoderクラスの中のscanaddとextract_Jの両メソッド。これらがJIS補助漢字のことを考慮に入れていないため、文字列を完全に壊してしまうらしい。
ということで、サブジェクトの文字化けにも対応した版はこちら。前回のgettext_ext.rbを置き換えて下さい。
if defined? ActionMailer module ActionMailer #:nodoc: class Base #:nodoc: def create!(*arg) #:nodoc: create_without_gettext!(*arg) if Locale.get.language == "ja" require 'nkf' if NKF.guess(@mail.subject) == NKF::UTF8 @mail.subject = @mail.subject.gsub("\xef\xbd\x9e", "\xe3\x80\x9c") end @mail.subject = NKF.nkf('-j -m0', @mail.subject) part = @mail.parts.empty? ? @mail : @mail.parts.first if part.content_type == 'text/plain' part.charset = 'iso-2022-jp' if NKF.guess(part.body) == NKF::UTF8 part.body = part.body.gsub("\xef\xbd\x9e", "\xe3\x80\x9c") end part.body = NKF.nkf('-j', part.body) end end @mail end end end module TMail class Encoder def scanadd( str, force = false ) types = '' strs = [] ki = false if str.respond_to?(:encoding) enc = str.encoding str.force_encoding(Encoding::ASCII_8BIT) end until str.empty? if m = /\A[^\e\t\r\n ]+/.match(str) types << (force ? 'j' : 'a') if str.respond_to?(:encoding) strs.push m[0].force_encoding(enc) else strs.push m[0] end elsif m = /\A[\t\r\n ]+/.match(str) types << 's' if str.respond_to?(:encoding) strs.push m[0].force_encoding(enc) else strs.push m[0] end elsif m = /\A\e(\x24(?:[B@]|\([BD@])[^\e]+)/.match(str) types << 'j' ki = true str = m.post_match if str.respond_to?(:encoding) strs.push m[1].force_encoding(enc) else strs.push m[1] end elsif m = /\A\e\(B/.match(str) str = m.post_match if ki ki = false else types << 'a' if str.respond_to?(:encoding) strs.push m[0].force_encoding(enc) else strs.push m[0] end end else raise 'TMail FATAL: encoder scan fail' end (str = m.post_match) unless m.nil? end do_encode types, strs end def extract_J( chunksize, str ) size = max_bytes(chunksize, str.size) - 6 size = (size % 2 == 0) ? (size) : (size - 1) return nil if size <= 0 "\e#{str.slice!(0, size)}\e(B" end end end end
ちなみに、前回
@mail.subject = base64(@mail.subject)
だったところが、
@mail.subject = NKF.nkf('-j -m0', @mail.subject)
に書き換わってますが、これは追跡の途中、ここでbase64エンコードしておいてもまったく意味が無いことが分かったので、JISに変換するだけにしました。
余談ですが、今回、grep頼りのソース探査の限界をひしひしと感じました。IDEとか使うとこういう作業は楽になるんですかね?
Express5800で今度はFreeBSDのインストールに挑戦。SATAのHDDや9650SEを使ったRAIDディスクへのインストールは普通にできるので、オンボードRAIDについてのみ書きます。
バージョンごとの状況は以下のようになりました
というわけで、7.2をインストール。 インストーラを起動してディスク選択に進むと、一覧ではHDDがadとして、RAIDがarとして見えます。^J .orangered.arで使用しているディスクがadとしても見える状態になりますが、RAIDとして使う場合はad側を使ってはいけません。^J あとはarを選択して通常のディスクと同じように使用領域の設定をし、インストールするだけです。
root権限でatacontrolコマンドを実行すると状態を確認できます。
■ RAID0の場合 # atacontrol status ar0 ar0: ATA RAID0 stripesize=128 status: READY subdisks: 0 ad6 ONLINE 1 ad8 ONLINE ■ RAID1の場合 # atacontrol status ar0 ar0: ATA RAID1 status: READY subdisks: 0 ad6 ONLINE 1 ad8 ONLINE
root権限が必要なので、Nagiosなどで監視する場合sudoの類と組み合わせる必要があります。
nagios ALL=NOPASSWD:/sbin/atacontrol
コマンドの定義:
define command{ command_name check_raid_ar command_line $USER1$/check_raid_ar -a $ARG1$ }
サービスの定義:
define service{ use generic-service # (割愛) host_name <ホスト名> service_description System Disk RAID check_command check_raid_ar!ar0 }
設定が正しく出来ると、以下のような感じで現在の状態を監視できるようになります(画像はarを使っている他のサーバの物です)。
前回、インストールに成功して喜んでいたオンボードRAIDですが、試しにディスクをわざと抜いて交換し、リビルドをかけようとするとカーネルがパニックしました。いちおうBIOSメニューからリビルドはできるのですが、その間サーバとしては停止してしまうのでよろしくありません。
というわけで、BIOS RAIDはやめてソフトウェアRAIDの設定を試すことにしました。BIOS RAIDの方もmetadata formatがベンダー依存ってだけで、実態はソフトウェアRAIDですが。
一連の作業の前に、RAIDの表示が起動時に出るのは煩わしいので、ケースを開けてジャンパを戻し、RAID機能は切っておきます。
FreeBSDのソフトウェアRAID実装としては、最近はgmirrorが主流のようです。ataraid(ar)はインストーラが対応してて、rootパーティションのミラーをする場合Fixitとか面倒な手順を踏まなくていいんですが、どうも上記パニックとか、以前send-prしたMatrixRAIDの不具合が放置されていたりと、あまり活発にメンテナンスされていない印象を受けます。ccdやgvinumというのも一応ありますが、ccdはルートパーティションのミラーが出来ない、gvinumはGEOM対応で不安定になったまま放置(たぶん)という状態だったと思います。 よって今回はgmirrorを使うことに。ちなみに、私が調べた範囲でのarとgmirrotの比較表を以下に示します。
.border.
デバイス | ar | gmirror |
---|---|---|
インストーラ対応 | ○ | × |
ホットスワップ | ○ | ○ |
オートリビルド | × | × |
リビルド中の再起動 | × | ○ |
最新版(7.2)ではgmirrorでのインストールに対応してないかなぁ、と淡い期待を寄せてarでミラーを組むときの手順を試してみました。参考までに、arでの手順も併記します。
インストーラが起動したら、インストール作業に入る前にまずFixitに入ってRAIDを構築します。
(((.half clear.
# chroot /dist # mount -t devfs devfs /dev # kldload geom_mirror.ko # gmirror label gmir0 ad6 ad8
)))
.half. (((
# chroot /dist # mount -t devfs devfs /dev # atacontrol create RAID1 ad6 ad8
)))
.clear. ここでいったん再起動します。
gmirrorの場合、FreeBSDのブート画面が表示されたら「6」を押してloader promptに入り、geom_mirrorモジュールをロードします。arの場合はここでは何もせずとも認識されます。
(((.half.
OK load geom_mirror : OK boot
)))
.clear. これでインストールに進みSelect Drive(s)の画面で「gmir0」が選択肢に挙がれば成功!なのですが、残念ながら表示されませんでした。インストーラのCDにもgeom_mirrorモジュールは入っていたので、ディスク検知の処理を書き換えればうまくいくんでしょうか?
ともあれ、現状ではインストール後にFixitでミラー構築するしかないようなので、そちらの方向で行くことに。
ミラーの設定をする前に、OSを通常の手順でマスターにするHDDへとインストールします。今後の作業もあるので、livefsのイメージを焼いてインストールするのが良いと思います。^J .orangered.以下の作業は一発勝負です。ミスすると最悪ディスクの内容が壊れて再インストールとなります。
インストールが完了したらいったんリブートします。インストーラが起動したらFixitを選択して、以下の作業を行います。
livefs環境の利用準備
Fixit# chroot /dist Fixit# mount -t devfs devfs /dev
gmirrorカーネルモジュールのロード
Fixit# gmirror load -v Module available. Done.
ミラーの設定
Fixit# gmirror label gm0 ad6 Fixit# gmirror insert gm0 ad10
これでディスクのコピーが始まるので、以下のコマンドで完了するまで待つ。
Fixit# gmirror status gm0 Name Status Components mirror/gm0 DEGRADED ad6 ad8(xx%)
完了するとstatusが次のようになる
Fixit# gmirror status gm0 Name Status Components mirror/gm0 COMPLETE ad6 ad8
コピーが完了したら、構築したRAIDから起動するように設定を変更します。
/をマウント
Fixit# mount /dev/mirror/gm0s1a /mnt
/mnt/etc/fstabを修正して、"ad6"→"mirror/gm0"に変更する。
Fixit# sed 's#ad6#mirror/gm0#' < /mnt/etc/fstab > /mnt/etc/fstab.tmp Fixit# cp /mnt/etc/fstab /mnt/etc/fstab.bak Fixit# mv /mnt/etc/fstab.tmp /mnt/etc/fstab Fixit# cat /mnt/etc/fstab # Device Mountpoint FStype Options Dump Pass# /dev/mirror/gm0s1b none swap sw 0 0 /dev/mirror/gm0s1a / ufs rw 1 1 :
/mnt/boot/loader.confにgeom_mirrorカーネルモジュールの読み込み指定を追加する
# echo 'geom_mirror_load="YES"' >> /mnt/boot/loader.conf
以上で設定は完了です。シェルを抜けてインストーラを終了し、CDを取り出して再起動をかけます。設定に問題なければ、gm0のディスクをマウントした状態でOSが起動します。
障害のエミュレーションとして、atacontrol detach、HDDを片方いきなり抜く。シャットダウンしてHDDを抜いて再起動、と試してみましたが、パニックなど起こすこともなくDEGRADED状態に移行しました。
あと、最初は日本語マニュアルだけ読んでオートリビルドができるのかと勘違いしてたんですが、英語のマニュアルには、
The component with the biggest priority is used by the prefer balance algorithm and is also used as a master component when resynchronization is needed, e.g. after a power failure when the device was open for writing.
と説明があるのでどうも出来ないようです。
リビルドの手順は以下の通りになります。 障害の発生したHDDがad10の場合、ata5のデバイスを切り離します。
# atacontrol detach ata5
この状態でHDDを交換し、デバイスを(再)接続して認識させます。
# atacontrol attach ata5
この状態ではまだad10の情報がgm0に残っているので、消します。
# gmirror forget gm0
最後に取り付けたHDDをgm0に追加します。
# gmirror insert gm0 ad10
これでリビルドが自動的に始まります。
ちなみに、リビルド中にリブートすると途中から再開しました。
最後に余談です。
gmirrorやarはHDDの後ろの方にmetadata(RAIDの管理情報)を書き込んで管理しています。で、atacontrolコマンドでいったん書きこまれたmetadataはごみデータが残りやすく、かなり厄介です。 特にテストのために一度抜いたHDDを戻したりすると、整合性が取れていないはずなのにReady状態に戻ってしまう(リビルドする方法が無い)ため、非常に危険です。
こういうどうしようもなくなったディスクからmetadataを削除する場合、ディスクの後の方の領域をクリアします。以下の例ではかなり適当に後の方をゴソッと消してますが、実稼働中のディスクでやる場合はちゃんと計算した方が良いと思います。
例えばad8のmetadataを消す場合、まずディスクの容量を調べます。
# diskinfo ad8 ad8 512 164696555520 321672960 319120 16 63
2番目の数字がセクタサイズで4番目の数字が総セクタ数です。これらの数字を参考に、最後の方を適当に消してしまいます。
# dd if=/dev/zero of=/dev/ad8 bs=512 seek=321650000
これで最後の約10MB(512×22960 byte)が消えます。seek=を省略すればディスク全体をクリアしますが、その場合はbsの値を大きめにすると早く終了します。
ちなみに、RAIDの情報が書き込まれる位置は/usr/src/sys/dev/ata/ata-raid.hに'*_LBA'という名前で定義されていますので、BIOS RAIDを使っている場合はRAIDコントローラのメーカを調べてそこから計算します。あるいは、起動画面で「5. Boot FreeBSD with verbose logging」を選択して起動し、dmesgをMetadataで検索すると、どのタイプのmetadataがディスクに記録されているか調べることができます。
前のエントリでも使っている、オリジナルのfootnote.rbを改造したものを公開しました。ダウンロードや説明は、こちらのページからどうぞ。
例えば、こんな感じのソースが、
* markごとに連{{fn '*の1個目'}}番{{fn '*の2個目'}}を{{fn '+の1個目', '+'}}付{{fn '*の3個目'}}加{{fn '+の2個目', '+'}} * 指定したmarkの脚注だけを任意の場所に表示{{fn 'こんな風に"-"の脚注だけここに表示し、残りの脚注はエントリの末尾に表示するといったことができます', '-'}} * 自動括弧閉じ&参考文献的表示{{cite '文献名'}} {{fn_put '-'}}
こんな風に表示されます。
末尾に表示されないものをfootnoteと呼ぶのはどうかと言われそうですが、どうせWebと印刷物ではページの概念が違うし、印刷物だって見方によっては本文をぶち切って脚注が表示されてるって解釈も出来るじゃんと開き直ることにしました。と、穴埋めが出来たところで、この下に表示されるのが従来どおりのfootnoteです。