自動ビルド失敗時にマヌケな音を鳴らしたい

少数精鋭のプロジェクトを運用する機会に恵まれたので、これ幸いとずっとやりたかった継続的インテグレーションを実施しようと思っている。使用するのはThoughtWorksCruiseControl.NET

ただ、俺には野望がある。継続的なビルドに失敗があった場合、これをWebサイトなりメールなりで通知してくれるのはよいのだが、物足りない。もっとこう、なんというか、ビルドを失敗させたやつがガックリと落ち込んで自らの行いを悔い改めるきっかけになるような、残念感あふれる演出がほしい。つまるところ、マヌケな音を鳴らしたいのだ!
さて、現在の環境で野望を実現するためには、次の課題をクリアしなければならない。

  • どうやって音を鳴らす?
  • どうやってCruiseControl.NETに組み込む?
  • どうやってビルドの失敗を拾う?

一つずつ解決していこうか。

どうやって音を鳴らす?

幸いなことに、.NET Framework 2.0からは簡単に音を再生するライブラリが追加されている。

// ファイルパスを渡して、PlaySyncするだけ。
System.Media.SoundPlayer player = new System.Media.SoundPlayer(soundFilePath);
player.PlaySync();
player.Dispose();

これだけでOK。

どうやってCruiseControl.NETに組み込む?

これは、音を再生するだけのCustom Builder Plug-inを作るしかあるまい。

Visual Studioで適当な名前のクラスライブラリプロジェクトを作成して、参照先にCruiseControl.NETのインストール先\serverにある二つのdllを追加する。

このとき、参照のプロパティでこいつらのローカルコピーを作らないよう設定しておくことをお勧めする。いや、作ってもいいんだけど、無駄なんで。プラグインを入れる先にCruiseControl.NETのdllが無いわけがない。

次に、メニューバーのプロジェクトのプロパティで、アセンブリ名をccnet.hogehoge.pluginにしておく。hogehogeには、プラグインの名前を渡す。今回はccnet.soundtask.pluginにしておいた。

そして、次のようなコードを書く。

using System;
using ThoughtWorks.CruiseControl.Core;
using Exortech.NetReflector;

namespace Idesaku.CruiseControl.Builder
{
    [ReflectorType("soundTask")]
    public class SoundTask : ITask
    {
        private string soundFilePath;

        public void Run(IIntegrationResult result)
        {
            System.Media.SoundPlayer player = new System.Media.SoundPlayer(soundFilePath);
            player.PlaySync();
            player.Dispose();
        }

        [ReflectorProperty("soundFile", Required=true)]
        public string SoundFile
        {
            get
            {
                return soundFilePath;
            }

            set
            {
                this.soundFilePath = value;
            }
        }
    }
}

Runメソッドの中に、実施したい処理を書く。

ReflectorTypeには、ccnet.configの中でタスク名として使う文字列を渡す。

ReflectorPropertyでは、このプロパティをsoundFileというタスクパラメータとして使うことを宣言している。Require=trueにしておけば、このパラメータが設定されていない場合にエラーになる。これで再生するサウンドファイルを設定ファイル上で定義できるようになるのである*1CruiseControl.NET自身のソースコードを読むと、public変数にこの属性を定義していたが、それはちょっと乱暴な気がしたのでプロパティを使っている。気にしすぎ?

あとはビルドして、生成されたbin\Debug\ccnet.soundtask.plugin.dll*2CruiseControl.NETのインストールディレクト\serverに配置しておく。

あとはこんな感じで設定ファイル(ccnet.config)のプロジェクト設定箇所に組み込むだけである。

<tasks>
    <soundTask>
        <soundFile>c:\path\to\wavefile.wav</soundFile>
    </soundTask>
</tasks>

タスク名とタスクパラメータ名が、それぞれソースコード中のReflectorTypeとReflectorPropertyで定義した文字列と一致しているのがわかるであろうか。

どうやってビルドの失敗を拾う?

とりあえず、もうビルド環境は作ってあるものとする。

さて、ビルドの失敗をどうやって拾うか、である。ビルドが失敗するとその時点でタスクの実行が止まるので、ビルドタスクの後ろにsoundTaskを仕込んでおけば、ビルドに成功したときだけ音を鳴らす、という挙動を実現できる。まぁ成功を祝ってファンファーレを鳴らすのも悪くないが、今やりたいことはその真逆だ。失敗したときだけ音を鳴らして、ビルドを壊した犯人を羞恥と失意のどん底に突き落としたいのである。このやり方では、我が野望を実現できぬ。

中からではダメなら外から監視するか、ということで、今回はプロジェクトのビルド状態を監視するためだけのプロジェクトをもう一つ作ることにした。

ビルド定義を書いてあるccnet.configに、新しいプロジェクトを定義する。

<project name="SoundNotifier">
    <triggers>
        <projectTrigger project="TargetProject">
            <triggerStatus>Failure</triggerStatus>
            <innerTrigger type="intervalTrigger" seconds="5" buildCondition="ForceBuild"/>
        </projectTrigger>
    </triggers>
    <tasks>
        <soundTask>
            <soundFile>c:\temp\mario_dead.wav</soundFile>
        </soundTask>
    </tasks>
</project>

projectTriggerで、監視対象プロジェクト(ここではTargetProject)を指定している。この設定では対象プロジェクトを5秒周期で監視し、ビルド結果がFailureになったのを検出したときに限り、このプロジェクトのタスクが実行される。

使用するサウンドファイルは・・・まぁ、なんだ、ファイル名から察することができると思うが、そういう音を鳴らしたいのである。他には、虚弱体質な探検家が転んでくたばった音とか、ティウンティウンとか、某有名RPGで呪われた・・・いや、セーブデータが消えた時というべきか・・・音もいいかもしれない。

これで、TargetProjectのビルド結果がFailureになった時に限り、soundTaskが実行される、という設定ができあがった。

ためしにccnet.exeを実行し、リポジトリをわざとコンパイルエラーになる状態にしてからDashboardから強制ビルドを実行すると・・・「ぷるんっ!」期待通り実に残念な音が鳴った。なんという脱力感、なんというこっぱずかしさ。おお、かくして裁きのとき来たれり。罪深きビルド破壊者共よ、屈辱にまみれたく無くば今のうちに悔い改め精進しておくがよい。

まとめ

動きはするのだが・・・。

  • Dashboardに音鳴らすだけのプロジェクトが映るのがキモい
  • NAntMSBuildでやったほうが簡単だったんじゃあ

などなどツッコミどころが残っていて困る。とにかく、もっといいやり方があるだろう、おまえ!と言わざるを得ないのだが、Custom Build Plug-inの作り方が少しわかったから、それでいいじゃん?と自分を慰めることにする。

*1:音を固定してよいのであれば、リソースとして組み込んでしまうのも手

*2:もちろんbin\Releaseでもよい