Hudsonを利用してファイルI/Fを実現したい(2)

昨日(id:sikakura:20110223)の続きです。
HudsonのリモートアクセスAPIを実行するプログラムとしてはJavaで実装したいと思います。
理由は、

  • Hudsonの自動インストール機能のおかげでjavaのインストールの工数がかからない。
  • リモートアクセスAPIのサンプルがJavaで提供されている(Apache.commonsのHttpclientもサンプルが提供されている)

といったところでしょうか。
個人的にはgroovyも魅力的だったのですが、動くものを作成した場合のことを考えると、他の人に引き継げるだけのスキルが…。


リモートアクセスAPIについては、ここhttp://wiki.hudson-ci.org/display/JA/Remote+access+APIに解説は譲りますが、
簡単に説明すると、Hudsonサイトhttp://192.168.0.1/hudson/に定義されているジョブTestBuildTaskを実行するには、


http://192.168.0.1/hudson/job/TestBuildTask/build


というURLにアクセスすれば良いというものです。
そして実行されたジョブ(ビルド番号が35の時)を確認するには、


http://192.168.0.1/hudson/job/TestBuildTask/35/api/xml


というURLにアクセスすると、以下のようなXMLが返却されてきます。

<freeStyleBuild>
  <action>
    <cause>
      <shortDescription>ユーザーanonymousが実行</shortDescription>
      <userName>anonymous</userName>
    </cause>
  </action>
  <building>false</building>
  <duration>46125</duration>
  <fullDisplayName>TestBuildTask #35</fullDisplayName>
  <id>2011-02-24_16-54-07</id>
  <keepLog>false</keepLog>
  <number>35</number>
  <result>SUCCESS</result>
  <timestamp>1288252447000</timestamp>
  <url>http://192.168.0.1/hudson/job/TestBuildTask/35/</url>
  <builtOn>TEST</builtOn>
  <changeSet/>
</freeStyleBuild>

さて、私の手元で運用しているHudson環境は、TracLightningと一緒に使用しているので、
Hudsonサイトにアクセスすると、Tracのユーザ認証が行われます。
この認証はDigest認証になっていますので、HttpClientもDigest認証に対応させなければなりません。
リモートアクセスAPIのサンプルだとBasic認証のものしかありませんでしたので、ここはApache Commons HttpClientに頼ります。
Apache Commons HttpClient http://hc.apache.org/downloads.cgiから、
httpcomponents-client-4.0.3-bin-with-dependencies.zipをダウンロードしてきました。
ダウンロードしたファイルを解凍すると、Digest認証のサンプルがあります。
同じものがApacheのサイトhttp://hc.apache.org/httpcomponents-client-ga/examples.htmlにあります。
このClientPreemptiveDigestAuthentication.javaのmainメソッドの真ん中あたりに、

HttpGet httpget = new HttpGet("/");

という文を

HttpGet httpget = new HttpGet("http://192.168.0.1/hudson/job/TestBuildTask/35/api/xml");

と変更して、あとは適宜ユーザIDやパスワードを設定すると、あっさり動きました。
が。。。動いたのは一回だけ。。。
繰り返して動かしていると、

Exception in thread "main" java.lang.IllegalStateException: Invalid use of SingleClientConnManager: connection still allocated.
Make sure to release the connection before allocating another one.
        at org.apache.http.impl.conn.SingleClientConnManager.getConnection(SingleClientConnManager.java:199)
        at org.apache.http.impl.conn.SingleClientConnManager$1.getConnection(SingleClientConnManager.java:173)
        at org.apache.http.impl.client.DefaultRequestDirector.execute(DefaultRequestDirector.java:390)
        at org.apache.http.impl.client.AbstractHttpClient.execute(AbstractHttpClient.java:641)

というエラーがでて、何度実行してもエラーになってしまう状態が続きました。
コネクションは毎回シャットダウンしているように見えるのですが。。。
Tutorialに、上のエラーで表示されているSingleClientConnManagerについて書かれている部分を発見しました。
http://hc.apache.org/httpcomponents-client-ga/tutorial/html/connmgmt.html#d4e605
ここで書かれているサンプルの通りにソースを書き直しても、同じエラーが出ます。
そもそもSingleClientConnectionというのが駄目な気がしてきました。
そのままTutorialを読み続けていると、、、
Simple connection manager http://hc.apache.org/httpcomponents-client-ga/tutorial/html/connmgmt.html#d4e630
Pooling connection manager http://hc.apache.org/httpcomponents-client-ga/tutorial/html/connmgmt.html#d4e638
とあり、どうやらThreadSafeClientConnManagerを使用すれば良いみたいです。


以下に、ThreadSafeClientConnManagerを使用するようにサンプルを書き換えたもの(mainメソッドだけ)を載せておきます。

    public static void main(String[] args) throws Exception {

        HttpParams params = new BasicHttpParams();
        SchemeRegistry schemeRegistry = new SchemeRegistry();
        schemeRegistry.register(new Scheme("http",PlainSocketFactory.getSocketFactory(),80));
        ClientConnectionManager connMrg = new ThreadSafeClientConnManager(params,schemeRegistry);
        DefaultHttpClient httpclient = new DefaultHttpClient(connMrg,params);

        httpclient.getCredentialsProvider().setCredentials(
                new AuthScope("192.168.0.1", 80), 
                new UsernamePasswordCredentials("username", "password"));

        BasicHttpContext localcontext = new BasicHttpContext();
        // Generate DIGEST scheme object, initialize it and stick it to 
        // the local execution context
        DigestScheme digestAuth = new DigestScheme();
        // Suppose we already know the realm name
        digestAuth.overrideParamter("realm", "some realm");        
        // Suppose we already know the expected nonce value 
        digestAuth.overrideParamter("nonce", "whatever");        
        localcontext.setAttribute("preemptive-auth", digestAuth);
        
        // Add as the first request interceptor
        httpclient.addRequestInterceptor(new PreemptiveAuth(), 0);
        // Add as the last response interceptor
        httpclient.addResponseInterceptor(new PersistentDigest());
        
        HttpHost targetHost = new HttpHost("192.168.0.1", 80, "http"); 

        HttpGet httpget = new HttpGet("http://192.168.0.1/hudson/job/TestBuildTask/35/api/xml");

        System.out.println("executing request: " + httpget.getRequestLine());
        System.out.println("to target: " + targetHost);
        
        for (int i = 0; i < 3; i++) {
            HttpResponse response = httpclient.execute(targetHost, httpget, localcontext);
            HttpEntity entity = response.getEntity();

            System.out.println("----------------------------------------");
            System.out.println(response.getStatusLine());
            if (entity != null) {
                System.out.println("Response content length: " + entity.getContentLength());
                entity.consumeContent();
            }
        }
        // When HttpClient instance is no longer needed, 
        // shut down the connection manager to ensure
        // immediate deallocation of all system resources
        httpclient.getConnectionManager().shutdown();        
    }

上のサンプルを流用することで、リモートアクセスAPI経由で

  1. Hudson上のジョブを起動する(POSTするだけでOK)
  2. ジョブの実行状態を確認する(レスポンスのXMLを解析すればOK)
  3. 指定されたURLのファイルをダウンロードして保存する(レスポンスをストリームで受け取ってファイルに保存する)

といったことが可能な筈です。

今日はここまで。