メールでJenkinsにコマンドを送りたい(4) 〜Trigger編〜

コマンドメールの送信、および結果のメール受信までたどり着きました。
実際に使用してみると、これはこれで問題なく動作するのですが、ずっと動かしているとビルド履歴が大変なことに。。。


SCMのポーリング機能を思い出しました。
SCMのポーリング機能は、SCMに変更があるかどうかをまず確認し、変更がなければビルドを実行しない仕様になっています。
つまり、余計なビルド履歴が残りません。この仕様(実装も)を流用できないかと考えました。
基本仕様はSCMポーリングそのままに、CLIメールの到着だけ確認して到着してなければMailCommanderを実行しないようにします。
また、ポーリングのログも見たいと思いましたので、Actionも拡張することにします。
参考にするソースやリソースは、hudson.triggers.SCMTriggerです。



以下がconfig.jellyとなります。SCMTriggerで使用しているconfigと同じにしました。

<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
  <f:entry title="Schedule" field="spec"  help="/descriptor/hudson.triggers.TimerTrigger/help/spec">
    <f:textarea />
  </f:entry>
</j:jelly>

以下がhelp_ja.htmlです。

<div>
  CLIコマンドの書かれた電子メールの到着を監視します。
</div>

上の2つを用意すると以下のようなイメージとなります。


以下がindex.jellyとなります。プロジェクトのトップページを表示したときに、左側のメニューに表示させるための設定となります。

<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
  <l:layout>
    <st:include it="${it.owner}" page="sidepanel.jelly" />
    <l:main-panel>
      <h1>${%title(it.displayName)}</h1>
      <j:set var="log" value="${it.log}" />
      <j:choose>
        <j:when test="${empty(log)}">
          ${%Polling has not run yet.}
        </j:when>
        <j:otherwise>
          <pre>
            <st:getOutput var="output" />
            <j:whitespace>${it.writeLogTo(output)}</j:whitespace>
          </pre>
        </j:otherwise>
      </j:choose>
    </l:main-panel>
  </l:layout>
</j:jelly>

上のリソースが加わって、以下の図のように、左側のメニューの設定の下に表示されます。


以下が実装になります。ちょっと長いのですが、MailCommanderTriggerのrunメソッド以外は、ほとんどSCMTriggerの内容と同じです。
MailCommandTriggerのrunメソッドでやっていることは、主に3つです。
1)MailCommandBuilderの設定画面で入力されたPOP3サーバの設定をとってきています。
  方法が分からなかったので、設定ファイルを直読みしています(←多分よくないだろうな。。。)
2)取得した設定で、メールサーバに問い合わせします。
  CLIの書かれたメールがあれば、

if (isCommandExist)
    job.scheduleBuild(0, new MailCommandTriggerCause());

 のように、ビルドを即時にスケジュールします。
3)最後に、ポーリングログを吐き出しています。

public class MailCommandTrigger extends Trigger<SCMedItem> {

    private static final Logger LOGGER = Logger.getLogger(MailCommandTrigger.class.getName());

    @DataBoundConstructor
    public MailCommandTrigger(String spec) throws ANTLRException {
        super(spec);
    }

    @Override
    public Collection<? extends Action> getProjectActions() {
        return Collections.singleton(new MailCommandAction());
    }

    public File getLogFile() {
        return new File(job.getRootDir(), "mailcommander-polling.log");
    }

    @SuppressWarnings("unchecked")
    @Override
    public void run() {

        String address = null, port = null, username = null, password = null;
        boolean isCommandExist = false;
        try {
            FileInputStream fis = new FileInputStream(new File(job.getRootDir(), "config.xml"));
            Document dom = new SAXReader().read(fis);

            Element mailcommander =
                dom.getRootElement().element("builders").element(MailCommandBuilder.class.getName());
            address = mailcommander.elementText("address");
            port = mailcommander.elementText("port");
            username = mailcommander.elementText("username");
            password = mailcommander.elementText("password");
            fis.close();
        } catch (IOException ioe) {
            ioe.printStackTrace();

        } catch (DocumentException e) {
            e.printStackTrace();
        }

        Properties props = new Properties();
        Session sess = Session.getDefaultInstance(props);
        Store store;
        try {
            store = sess.getStore("pop3");
        } catch (NoSuchProviderException e) {
            e.printStackTrace();
            return;
        }

        StringBuffer commands = new StringBuffer();
        for (CLICommand c : CLICommand.all())
            commands.append(c.getName());

        try {
            store.connect(address, Integer.valueOf(port), username, password);

            Folder rootFolder = store.getDefaultFolder();
            Folder inbox = rootFolder.getFolder("INBOX");
            inbox.open(Folder.READ_ONLY);

            Message[] messages = null;
            if (inbox.getMessageCount() > 10)
                messages = inbox.getMessages(inbox.getMessageCount() - 10 + 1, inbox.getMessageCount());
            else
                messages = inbox.getMessages();
            FetchProfile fp = new FetchProfile();
            fp.add("Subject");
            inbox.fetch(messages, fp);
            for (Message message : messages) {
                String findStr = message.getSubject().split(" ")[0];
                if (commands.indexOf(findStr) != -1) {
                    isCommandExist = true;
                    break;
                }
            }
            inbox.close(true);
            store.close();
        } catch (MessagingException e) {
            e.printStackTrace();
            return;
        }

        if (isCommandExist)
            job.scheduleBuild(0, new MailCommandTriggerCause());

        try {
            StreamTaskListener listener = new StreamTaskListener(getLogFile());

            try {
                PrintStream logger = listener.getLogger();
                logger.println("Started on " + DateFormat.getDateTimeInstance().format(new Date()));
                if (isCommandExist)
                    logger.println("Changes found");
                else
                    logger.println("No changes");
            } catch (Error e) {
                e.printStackTrace(listener.error("Failed to record Mail Commander polling"));
                LOGGER.log(Level.SEVERE, "Failed to record SCM polling", e);
                throw e;
            } catch (RuntimeException e) {
                e.printStackTrace(listener.error("Failed to record Mail Commander polling"));
                LOGGER.log(Level.SEVERE, "Failed to record Mail Commander polling", e);
                throw e;
            } finally {
                listener.close();
            }
        } catch (IOException e) {
            LOGGER.log(Level.SEVERE, "Failed to record Mail Commander polling", e);
        }

    }

    @Extension
    public static class DescriptorImpl extends TriggerDescriptor {
        public boolean isApplicable(Item item) {
            return item instanceof BuildableItem;
        }
        public String getDisplayName() {
            return Messages.MailCommandTrigger_DisplayName();
        }
        public FormValidation doCheck(@QueryParameter String value) {
            return doCheckSpec(value);
        }
        public FormValidation doCheckSpec(@QueryParameter String value) {
            try {
                String msg = CronTabList.create(fixNull(value)).checkSanity();
                if (msg != null)
                    return FormValidation.warning(msg);
                return FormValidation.ok();
            } catch (ANTLRException e) {
                return FormValidation.error(e.getMessage());
            }
        }
    }
    public static class MailCommandTriggerCause extends Cause {
        @Override
        public String getShortDescription() {
            return "Mail Commander Trigger";
        }
        @Override
        public boolean equals(Object o) {
            return o instanceof MailCommandTriggerCause;
        }
        @Override
        public int hashCode() {
            return 5;
        }
    }
    public final class MailCommandAction implements Action {
        public AbstractProject<?, ?> getOwner() {
            return job.asProject();
        }
        public String getIconFileName() {
            return "clipboard.gif";
        }
        public String getDisplayName() {
            return "Mail Command Action";
        }
        public String getUrlName() {
            return "mailcommandPollLog";
        }
        public String getLog() throws IOException {
            return Util.loadFile(getLogFile());
        }
        public void writeLogTo(XMLOutput out) throws IOException {
            new AnnotatedLargeText<MailCommandAction>(getLogFile(), Charset.defaultCharset(), true, this).writeHtmlTo(0,out.asWriter());
        }
    }
}


と、ここまで作成してきて、ふと疑問。
今回の要件のような、BuilderやTrigger、Publisherのように単独ではあまり意味をなさないようなプラグインって
1つのクラスで実装して、TriggerやPublisherはBuilderの内部クラスにまとめてしまったほうが良いのでしょうか。


ともあれ、この実装により定期実行をずっとしていてもビルド履歴が無駄に溜まる心配がなくなりました。
最後に、Builderを少し改良してgroovyシェルを実行できるようにしたいと思います。