Windows Vista への coLinux インストールメモ

coLinux を出来合いのディスクイメージ(Ubuntu 7.10)で Windows Vista にインストールしたときのメモ。あまり Windows XP と変わるところはない。
最初、CentOS 5.2のイメージを作ろうとしたけどQEMUインストーラを動かすとKernel Panicになるので断念。

coLinuxのインストール
  1. http://www.colinux.org/ から最新のstableバージョンをダウンロード(現在は0.7.3)
  2. インストール先はどこでもいい(ここではデフォルトの C:\Program Files\coLinux とする)
  3. "Root FileSystem image Download" はチェックを外す。あとはデフォルト
ネットワークの設定(NATタイプ)

coLinuxをインストールするとネットワーク接続の一覧に「ローカル エリア接続 2」のような新しい接続が "TAP-Win32 Adapter V8 (coLinux)" のような説明で追加されている。

元からある「ローカル エリア接続」のほうのプロパティを開き、

  1. 「共有」タブを選択。
  2. 「ネットワークのほかのユーザーに、このコンピュータのインターネット接続をとおしての接続を許可する」にチェックを入れ、「ホーム ネットワーク接続」で「ローカル エリア接続 2」を選択。
  3. OK→OKで終了。

この後、「ローカル エリア接続 2」のほうのプロパティを開き、お好みでIPアドレスを変更する。デフォルトの 192.168.0.0/24 は普通に使われてるケースも多いので別のに変更したほうがいいかも。とりあえずここではIPアドレスを 192.168.31.1 とした。

Windowsファイアウォールが動いている場合、192.168.31.0/24 ネットワークを開けておく。

ディスクイメージの配置
  1. http://sourceforge.net/project/showfiles.php?group_id=98788 から Images 2.6.x Ubuntu をダウンロード、展開
  2. Ubuntu-7.10.ext3.2gb.fs と swap128.fs を coLinux インストールフォルダに移動
  3. UACが有効ならエクスプローラを管理者権限で実行して移動
coLinuxの設定ファイル作成

coLinux インストールフォルダの example.conf を適当な名前でコピー(ここでは同じフォルダの ubuntu.conf)して編集。

cobd0, cobd1 をさっきダウンロードしたUbuntuのディスクイメージに変更する。

# File contains the root file system.
# Download and extract preconfigured file from SF "Images for 2.6".
cobd0="C:\Program Files\coLinux\Ubuntu-7.10.ext3.2gb.fs"

# Swap device, should be an empty file with 128..512MB.
cobd1="C:\Program Files\coLinux\swap128.fs"

あとTAPを使うので以下のようにtuntap部分のコメントを外す

# Tuntap as private network between guest and host on second linux device
eth1=tuntap
初回起動

スタートメニューの「すべてのプログラム」「アクセサリ」からコマンドプロンプトを右クリックし「管理者として実行」。その後以下のように起動。

C:\Windows\System32> cd "\Program Files\coLinux"
C:\Program Files\coLinux> colinux-daemon.exe @ubuntu.conf

正しくUbuntuが動いてrootログインのプロンプトが出ることを確認。とりあえず root/root でログインしてネットワークの設定だけ行う。

# vi /etc/network/interfaces

で、iface eth0 のところを、Windowsのほうのネットワークとあわせた固定アドレスに変更。

#iface eth0 inet dhcp
iface eth0 inet static
  address 192.168.31.2
  gateway 192.168.31.1
  netmask 255.255.255.0

その後

# /etc/init.d/networking restart
 * Reconfiguring network interfaces...                                   [ OK ]

あと /etc/resolv.conf も必要なら 192.168.31.1 (ホストのIPアドレス)に変更。できたら、どこかのドメインpingを打って疎通確認。

あとは shutdown -h now で停止するとcoLinuxのプロセスも停止する。

Windowsサービスとして起動する準備を行う

毎回コマンドラインから起動するのは面倒なのでサービスとして自動起動されるようにする。

まず coLinux Driver をインストール。一応一回アンインストールしたほうがいいらしい?

C:\Program Files\coLinux> colinux-daemon.exe --remove-driver
C:\Program Files\coLinux> colinux-daemon.exe --install-driver

以下のように出る場合は失敗。先にサービスをインストールしてしまったとかが原因。
その場合はサービスを削除してからもう一回行う。

driver already installed
daemon: driver installed

以下のように出ればOK

loading C:\Program Files\coLinux\linux.sys
daemon: driver installed

サービスを追加する。--install-service はサービス名。適当でいいはず。

C:\Program Files\coLinux> colinux-daemon.exe @ubuntu.conf --install-service "coLinux Service"

以下のように出れば問題なし

Cooperative Linux Daemon, 0.7.3
Daemon compiled on Sat May 24 22:36:07 2008

daemon: installing service 'coLinux Service'
daemon: service command line: "C:\Program Files\coLinux\colinux-daemon.exe" @ubuntu.conf --run-service "coLinux Service"
daemon: setting restart options
daemon: service installed.
サービスの確認と依存関係の追加

サービスマネージャ(services.msc) を見ると "coLinux Service" が追加されているはずなのでお好みで自動起動に設定。

最後に、レジストリエディタ(regedit)で
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\coLinux Service の DependsOnService
に tap0801co を追加。以下のように改行区切りで指定することに注意。

CoLinuxDriver
tap0801co

もう一回サービスマネージャで "coLinux Service" の詳細画面を開き「依存関係」を見ると「TAP-Win32 Adapter V8 (coLinux)」が追加されているはず。

完了

できたら、サービスを開始する。特に問題なくサービスが起動し、

C:\Program Files\coLinux> start colinux-console-nt.exe

で再度Ubuntuのコンソールが出ればOK。

設定ファイル類のバージョン管理

開発者ごとに変更したりする設定ファイルの類いをSubversionでバージョン管理するとき困るのが「ローカルの変更が誤ってコミットされる」という問題。たとえば 開発者ごとにDBユーザ割り当ててる場合の接続先設定とかそういうの。

Subversionの公式FAQを見ると

私のプロジェクトには各開発者が変更しなくてはいけないファイルがありますが、そのようなローカルでの変更をコミットされたくありません。"svn commit" にファイルを無視させるにはどうすればいいですか?

http://subversion.tigris.org/faq.html#ignore-commit

というドンピシャの質問があるんだけど、回答は「テンプレートファイルをバージョン管理して、それを開発者にコピー・編集させろ」とのこと。

う〜ん。でも、update はしたいんですよ。たまにその設定ファイル自体に項目が追加されたりすることがあったりするから。テンプレート形式だと、テンプレートが更新されたら開発者全員に「テンプレが更新されたんで設定ファイル作り直してくださ〜い」とかお知らせしないと気づきませんよね普通。設定ファイル自体がバージョン管理下にあれば、知らせるまでもなく update で更新を取り込んでもらえるのですが。

コミットミスが避けられないのと更新頻度を勘案して大体はテンプレ形式でやってるけど、svn:ignore-commit 属性があればいいのに…といつも思う。

長いファイル名を変更する

Windowsでは、パス文字列の長さが260文字を超えてしまうと、Explorerだろうがコマンドプロンプトだろうが「ファイル名または拡張子が長すぎます。」と言われて移動や削除ができなくなってしまうらしい(Vistaでも直ってない)。これの対処方法。

親フォルダの名前を短くする

Microsoftのページ にある解決方法。でも、そもそも C:\a\b\c\d\e\f\g\長いファイル名 のように切り詰められない場合は?

SUBSTコマンドを使う

フォルダに対してドライブ文字を割り当てるSUBSTコマンドを使って、極限まで短くする。たとえば上のような場合なら、コマンドプロンプトから

subst X: C:\a\b\c\d\e\f\g

とすると C:\a\b\c\d\e\f\g フォルダに X:\ でアクセスできるようになるので、エクスプローラでXドライブからアクセスしたり、コマンドプロンプトから

X:
ren 長いファイル名 短いファイル名

のようにすればファイル名を短く変更できる。(パス全体ではなく)ファイル名自体はどうやっても255文字くらいまでしか付けられないので、この方法で必ず対処できるはず。

Win32 APIを直接叩く

今はC#を使用中なのでC#で無理矢理やってみる。
たとえば、Visual Studio C# の Express 版をダウンロードして以下のようなコンソールプログラムを作成する。

using System;
using System.Runtime.InteropServices;

namespace MoveFile
{
    class Program
    {
        [DllImport("kernel32.dll", EntryPoint="MoveFileW", CharSet=CharSet.Unicode,
           SetLastError=true, ExactSpelling=true, CallingConvention=CallingConvention.StdCall)]
        private extern static bool MoveFile(String from, String to);

        static void Main(string[] args)
        {
            MoveFile(args[0], args[1]);
        }
    }
}

これをビルドしてできる MoveFile.exe を使って

> MoveFile.exe "\\?\C:\a\b\c\d\e\f\g\長いファイル名" "短いファイル名"

のように変更する。
MSDNMoveFileのドキュメントによると、長いパス名を扱うには先頭に「\\?\」のようなプレフィックスを付ける必要があるらしい。初めて知った。

このプレフィックスさえつければ普通に変更できるのかな? と思って ren コマンドの引数や、JScriptの MoveFile とかで試してみたけどダメだった。手軽な方法が無くて残念。

同値の判定とシリアル化

前の人から引き継いだクライアント・サーバアプリケーションで以下のようなコードを見た。

namespace MyService.IF
{
    [Serializable]
    public sealed class MyResult
    {
        public static readonly MyResult OK = new MyResult(1, "OK");
        public static readonly MyResult ERROR = new MyResult(2, "ERROR");

        private Int32 _code;
        private String _name;

        private MyResult(Int32 code, String name)
        {
            _code = code;
            _name = name;
        }

        public override String ToString()
        {
            return String.Format("{0}: {1}", _code, _name);
        }
    }
}

こういう擬似enumのようなものを定義して、

MyResult result = Hoge(); // 何かの処理
if (result == MyResult.OK)
{
  // 処理がOKなら……
}

と結果を判定するというもの。

MyResult のインスタンスは OK と ERROR しか存在しないんだから、== でも問題ないしこっちのほうが効率いいよねという発想なんだろうけど、

IMyService remote = (IMyService)Activator.GetObject(typeof(IMyService), "サービスURI");
MyResult result = remote.DoIt();
if (result == MyResult.OK)
{
  // 処理がOKなら……
}

たとえば上のように .Net Remoting の公開サービスを使った結果だとすると、リモートサービス側で MyResult.OK を返したとしても「処理がOKなら」の部分は実行されない。中身の値が同じだけで異なるインスタンスなんだから当然そうなる。

C# に限ったことではない当たり前の話なんだけど、既存のコードの中ではローカル環境限定で使われていたため問題なかったらしい。ただ、このやり方をコピペしてリモートサービスとのやりとりに使った途端問題が起きましたよ、という。

Equals や operator == をオーバーライドするなどの正攻法でなく、どうしてもこのままでやりたければ、デシリアライズする部分をカスタマイズする必要がある。シリアル化のカスタマイズ を参考にしてみると、こんな感じになるんだろうか?

using System.Runtime.Serialization;
using System.Security.Permissions;

namespace MyService.IF
{
    [Serializable]
    public sealed class MyResult : ISerializable
    {
        public static readonly MyResult OK = new MyResult(1, "OK");
        public static readonly MyResult ERROR = new MyResult(2, "ERROR");

        private Int32 _code;
        private String _name;

        private MyResult(Int32 code, String name)
        {
            _code = code;
            _name = name;
        }

        [SecurityPermissionAttribute(SecurityAction.LinkDemand,
        Flags = SecurityPermissionFlag.SerializationFormatter)]
        public void GetObjectData(SerializationInfo info, StreamingContext context)
        {
            info.SetType(typeof(MyResultSerializationHelper));
            info.AddValue("_code", _code);
        }

        internal static MyResult Parse(Int32 code)
        {
            // 手抜き
            if (code == OK._code) { return OK; }
            else if (code == ERROR._code) { return ERROR; }
            else { throw new ArgumentException("invalid code"); }
        }
    }

    [Serializable]
    [PermissionSet(SecurityAction.Demand, Name = "FullTrust")]
    [SecurityPermissionAttribute(SecurityAction.LinkDemand,
        Flags = SecurityPermissionFlag.SerializationFormatter)]
    internal sealed class MyResultSerializationHelper : IObjectReference
    {
        internal Int32 _code;

        public Object GetRealObject(StreamingContext context)
        {
            return MyResult.Parse(_code);            
        }
    }
}

セキュリティ関係の属性設定の部分の意味をいまいち理解しきれてない……。

NUnitからlog4netを使うライブラリを使う場合

常識なのか知らないけど、log4netでログを吐くアセンブリAを、NUnitテスト用アセンブリBから参照して使うような場合の注意。

B.config に log4net の設定を行うだけでは、その内容を読んでくれないらしく、ログが出力されない。

期待通りログが吐かれるようにするには、Bに

[SetUpFixture]
public class SetupClass
{
    [SetUp]
    public void RunBeforeAnyTests()
    {
        log4net.Config.XmlConfigurator.Configure();
    }
}

のようなものを追加して、テスト前に明示的に B.config を読んでlog4net設定が行われるようにする必要があるらしい。

コンソールアプリケーションCからAを使うようなケースでは C.config に log4net の設定をするだけでいいので、何故NUnitではだめなのかわからず、微妙にハマった。

参考: Logging with log4net and NUnit

出力したログと全く同じ内容を文字列で取得

log4netを使っていて、「××がログファイルに出力されること」みたいなテスト項目があったとする。無理矢理ログファイルをオープンして読む以外に自動的に確認する方法はあるんだろうか。

ということで、ファイルに書き込むと同時にStringWriterにも同じ内容を書き込んで保持しておくAppenderを作って使ってみる。

using System;
using System.IO;

using log4net.Appender;
using log4net.Core;
using log4net.Util;

namespace MyTest
{
    class TapFileAppender : FileAppender
    {
        private TapQuietTextWriter _tapWriter = null;

        // FileAppenderが書き込みに使う QuietWriter プロパティを
        // セットする箇所を上書き
        override protected void SetQWForFiles(TextWriter writer)
        {
            _tapWriter = new TapQuietTextWriter(writer, ErrorHandler);
            base.SetQWForFiles(_tapWriter);
        }

        public String Text
        {
            get { return _tapWriter.Text; }
        }
    }

    class TapQuietTextWriter : QuietTextWriter
    {
        private StringWriter _tapWriter;

        public TapQuietTextWriter(TextWriter writer, IErrorHandler errorHandler)
            : base(writer, errorHandler)
        {
            _tapWriter = new StringWriter();
        }

        public override void Write(char value)
        {
            base.Write(value);
            _tapWriter.Write(value);
        }

        public override void Write(char[] buffer, int index, int count)
        {
            base.Write(buffer, index, count);
            _tapWriter.Write(buffer, index, count);
        }

        override public void Write(string value)
        {
            base.Write(value);
            _tapWriter.Write(value);
        }

        public String Text
        {
            get { return _tapWriter.ToString(); }
        }
    }
}

log4netの設定で、AppenderをTapFileAppenderに変更

	<log4net>
		<appender name="LogFileAppender"
				  type="MyTest.TapFileAppender" >
			<param name="File" value="..\log\test.log" />
			<param name="AppendToFile" value="true" />
			<layout type="log4net.Layout.PatternLayout">
				<param name="ConversionPattern"
					   value="%d [%t] %-5p %c - %m%n" />
			</layout>
		</appender>

		<root>
			<level value="ALL" />
			<appender-ref ref="LogFileAppender" />
		</root>
	</log4net>

Appenderのうまい取得方法がよく分からないが、とりあえず以下のように取れるらしい。

  log4net.Repository.Hierarchy.Hierarchy repo =
      (log4net.Repository.Hierarchy.Hierarchy)log4net.LogManager.GetRepository();
  TapFileAppender appender =
      (TapFileAppender)repo.Root.GetAppender("LogFileAppender");

appender.Text で書き込んだ全内容が取れる。

だめなところ

  • FileAppenderのAppendメソッドをオーバーライドしてRenderLoggingEventを呼ぶだけで同じことができるような気もする。とりあえず中身がよくわからないので「ファイルに出力したのと全く同じ内容」にするため、より低レベルなところ(ファイル書き込み寸前のところ)でキャプチャーするようにしてみた。
  • 中身はStringWriterなのでそのままではマルチスレッドでは悲惨になるっぽい。
  • 一回 Text プロパティを読んだらそこまでの内容は捨てるようにしたい。
  • log4netを使いはじめて2時間のド素人なので何が正しいのかよくわからない。

テーブルが更新されていないことを確認

HOGEテーブル(重複行がない前提で)がテストの前後で全く変更されていないことを確認したい場合、どうするか。

create table HOGE_PREV as select * from HOGE;

で準備しておいて、テストを実行。

/* 追加・変更された行を表示。Oracleならminus */
select * from HOGE except select * from HOGE_PREV;
/* 削除・変更された行を表示。Oracleならminus */
select * from HOGE_PREV except select * from HOGE;

で両方0件ならOK。

でいいのかな? おわったら HOGE_PREV は drop してしまえばいい。この程度の機械的な手順なら自動テストにも組み込める。

Excelにデータを貼付けてマクロで比較している人がいたので、もっとましな方法がないかと思った次第。