CruiseControl.NET でビルドログ作成エラー回避

CruiseControl.NET 1.0 でソース管理に Subversion を使っているとビルド自体は成功するが、ビルドログ作成時に例外が発生することがある。
Build Status には Failure でなくて Exception と表示されるので見分けはつくが、あまり気分のいいものではない。なんとか回避策を見つけたのでメモ。

なぜ例外が発生するか?

発生する例外はこんな感じ。

 ---> System.Xml.XmlException: 行 '11' の 'msg' 開始タグは 'logentry' の終了タグと一致しません。 行 12、位置 3 です。
   at System.Xml.XmlTextReader.ParseTag()
   at System.Xml.XmlTextReader.ParseBeginTagExpandCharEntities()
   at System.Xml.XmlTextReader.Read()
   at System.Xml.XmlValidatingReader.ReadNoCollectTextToken()
   at System.Xml.XmlValidatingReader.Read()
   at System.Xml.XmlLoader.LoadChildren(XmlNode parent)
   at System.Xml.XmlLoader.LoadElementNode()
   at System.Xml.XmlLoader.LoadCurrentNode()
   at System.Xml.XmlLoader.LoadCurrentNode()
   at System.Xml.XmlLoader.LoadChildren(XmlNode parent)
   at System.Xml.XmlLoader.LoadElementNode()
   at System.Xml.XmlLoader.LoadCurrentNode()
   at System.Xml.XmlLoader.LoadCurrentNode()
   at System.Xml.XmlLoader.LoadChildren(XmlNode parent)
   at System.Xml.XmlLoader.LoadElementNode()
   at System.Xml.XmlLoader.LoadCurrentNode()
   at System.Xml.XmlLoader.LoadCurrentNode()
   at System.Xml.XmlLoader.LoadDocSequence(XmlDocument parentDoc)
   at System.Xml.XmlLoader.Load(XmlDocument doc, XmlReader reader, Boolean preserveWhitespace)
   at System.Xml.XmlDocument.Load(XmlReader reader)
   at System.Xml.XmlDocument.LoadXml(String xml)
   at ThoughtWorks.CruiseControl.Core.Sourcecontrol.SvnHistoryParser.ReadSvnLogIntoXmlNode(TextReader svnLog)
   --- 内部例外スタック トレースの終わり ---
   at ThoughtWorks.CruiseControl.Core.Sourcecontrol.SvnHistoryParser.ReadSvnLogIntoXmlNode(TextReader svnLog)
   at ThoughtWorks.CruiseControl.Core.Sourcecontrol.SvnHistoryParser.Parse(TextReader svnLog, DateTime from, DateTime to)
   at ThoughtWorks.CruiseControl.Core.Sourcecontrol.ProcessSourceControl.ParseModifications(TextReader reader, DateTime from, DateTime to)
   at ThoughtWorks.CruiseControl.Core.Sourcecontrol.ProcessSourceControl.ParseModifications(ProcessResult result, DateTime from, DateTime to)
   at ThoughtWorks.CruiseControl.Core.Sourcecontrol.Svn.GetModifications(IIntegrationResult from, IIntegrationResult to)
   at ThoughtWorks.CruiseControl.Core.Sourcecontrol.QuietPeriod.GetModifications(ISourceControl sourceControl, IIntegrationResult lastBuild, IIntegrationResult thisBuild)
   at ThoughtWorks.CruiseControl.Core.IntegrationRunner.RunIntegration(BuildCondition buildCondition)

スタックトレースを見ると XML が不正らしい。で、これが原因の XML

<?xml version="1.0" encoding="utf-8"?>
<log>
<logentry
   revision="14">
<author>user</author>
<date>2006-01-28T06:43:06.703125Z</date>
<paths>
<path
   action="A">/trunk/UITest</path>
</paths>
<msg>譁・ュ怜喧縺代ユ繧ケ繝医・險ュ螳・/msg>
</logentry>
</log>

タグの部分が文字化けしていてタグが閉じていない。ちなみにログメッセージは「文字化けテストの設定」。

CruiseControl.NET は Subversion のログ取得時に System.Diagnostics.Process クラスを使用していて、それが Subversion の出力するログ(UTF-8)を Shift-JIS で取得しているため文字化けが発生しているようだ。

回避策

System.Diagnostics.Process クラスの StandardOutput プロパティからデータを取り出すときに正しくエンコードしてやるしかないようだけど、ThoughtWorks.CruiseControl.Core.Util.ProcessReader クラスを修正すると正しいエンコーディングを判定するのが面倒だし、影響範囲が多そうだったのでやめておいた。その代わりに Subversion を呼び出して出力を UTF-8 に変換するプログラム SvnHook.exe を作ってそれを呼び出すようにした。( svn.exe をリネームして SvnHook.exe を svn.exe にしようかと思ったが、trac にも影響が及びそうなので没)

1. ThoughtWorks.CruiseControl.Core.Sourcecontrol.Svn クラスを修正
public const string DefaultExecutable = "svn.exe";

上記の部分を以下のように変更

public const string DefaultExecutable = "SvnHook.exe";

ソースに同梱されている b.bat でビルドし、できたアセンブリ ThoughtWorks.CruiseControl.Core.dll で既存のファイルを上書きする。
.NET Framework 1.1 SDK がないとビルドに失敗するようだ。
※ ソリューションファイルを VS2005 用に変換してもビルドに失敗する。

2. SvnHook.exe の作成

コンソールプロジェクトを作成して以下のような Program クラスを作成し、SvnHook.exe へパスを通しておく。

namespace SvnHook {
    class Program {
        static void Main(string[] args) {
            ProcessStartInfo info = new ProcessStartInfo();
            info.FileName = @"svn.exe";
            if (args != null) {
                info.Arguments = string.Join(" ", args);
            }
            info.RedirectStandardOutput = true;
            info.UseShellExecute = false;
            info.CreateNoWindow = true;
            Process p = new Process();
            p.StartInfo = info;
            p.Start();
            string output;
            using (StreamReader r = new StreamReader(p.StandardOutput.BaseStream, Encoding.UTF8)) {
                output = r.ReadToEnd();
            }
            p.WaitForExit();

            System.Console.Out.WriteLine(output);
        }
    }
}


UTF8 固定なのでちょっと心配だが、とりあえず上記の方法で例外は発生しないようになった。