内容


使用 Sun SPOT 作为构建监视器

使用 Small Programmable Object Technology 监视持续构建过程的健康状况

Comments

早期地下采煤的时候,矿井中的金丝雀常常拥有短暂而有价值的一生。因为它们对于甲烷和一氧化碳等致命毒气非常敏感,从栖木上掉下来的金丝雀是一个明显的信号,告诉矿工是时候离开了。过一段时间之后,如果新换上的金丝雀安然无恙,矿工们又可以安全地重返矿井。

您的软件项目也可以有自己的矿井金丝雀。如果您在使用 CruiseControl 之类的持续集成工具,那么很可能熟悉当构建失败时发送给团队的电子邮件消息。这是一个信号,说明项目代码中有些地方需要马上改正。但是,当收件箱中还有很多其他邮件时,这些消息有时候会被忽略。然后,在知道消息之前,我已经从有问题的储存库中更新了本地副本,或者直接回家了,让团队中的其他人直抱怨。

这时需要某种高度可视的东西,就像金丝雀一样,快速一瞥后就能发现持续构建过程的状态。我的金丝雀是一种新的来自 Sun Microsystems 的开源技术,它的名称是 Sun Small Programmable Object Technology(SPOT)。本文介绍 SPOT,并展示如何建立用于监视 CruiseControl 的构建监视器。

什么是 SPOT?

SPOT(见 图 1)是运行 Java™ 程序的小型无线设备。SPOT 载有很多传感器,用于监视它的环境,还有一组彩色 LED 用于与外部通信,以及两个用于提供基本反馈的按钮。我使用 LED 来显示构建的状态。可以通过一条 USB 线将一些 SPOT 连接起来,作为一个基站,其他 SPOT 可以通过这个基站访问工作站上的资源,例如数据库或 Web 应用程序。

图 1. Sun SPOT
Sun SPOT
Sun SPOT

SPOT 由以下硬件元件组成:

  • 主处理器是一个 180MHz Atmel AT91RM9200 系统芯片(system-on-chip)。
  • 每个 SPOT 有 4MB Flash RAM 和 512K 伪静态 RAM。
  • 电力由内部充电电池(圆柱形锂电池)、外接电源或 USB 主机提供。
  • 电池充电后可无间断使用大约 3 小时。当无事发生时,它进行休眠,从而延长使用寿命。
  • 演示子板包含温度和光传感器、一个三轴加速计、8 个三色 LED 以及两个按钮开关。必要时,还可以通过 5 个通用 I/O 插脚和 4 个高电流输出插脚增加更多的子板。
  • 无线通信通过一个遵从 IEEE 802.15.4 的收发器完成,该收发器采用 2.4GHz-to-2.4835GHz 免授权频段。

在这样的硬件上,SPOT 运行一个名为 Squawk 的小型 JVM,这个 JVM 几乎完全是用 Java 语言编写的。Squawk 遵从 Connected Limited Device Configuration(CLDC)1.1 Java Micro Edition(Java ME)配置。它无需底层操作系统便可运行 — 也就是所谓的 “在裸机上运行”(见 参考资料)。

用于 SPOT 的应用程序是根据 Java ME MIDlet 编程模型编写的。这意味着每个 SPOT 上的 JVM 以类似于 Java EE 下管理 servlet 和 Enterprise JavaBeans(EJBs)的方式来管理 MIDlet 的生命周期。但是,由于 MIDlet 运行环境的限制,CLDC 以 JDK 1.3.1 作为开始的基础,剥离所有不必要的部分。因此,SPOT 程序不能访问文件流;没有反射,没有串行化,没有本地方法,没有正则表达式,没有 Swing,只有有限的数据类型。唯一可用的集合数据结构是向量栈、枚举和 hash 表。有些特定于 CLDC 的连接类被添加到这个子集中,但是编程过程中仍然要受很多限制。

为 SPOT 编写、构建和部署代码

为 SPOT 编写、构建和部署代码非常简单,可以使用自己选择的 IDE。例如,若要在 Eclipse 中进行开发:

  1. 创建一个标准的 Java 项目,删除默认的 JRE。
  2. 将 SPOT SDK 的 lib 文件夹中的以下 JAR 添加到 classpath:
    • transducer_device.jar
    • multihop_common.jar
    • spotlib_device.jar
    • spotlib_common.jar
    • squawk_device.jar
  3. 在 resources/META-INF 目录中创建一个 MANIFEST.MF 文件。该文件包含 Squawk VM 用于运行应用程序的信息。例如,清单 1 是我的构建监视器的 manifest 文件:
    清单 1. resources/META-INF/MANIFEST.MF 文件的内容
    MIDlet-Name: BuildCanary
    MIDlet-Version: 1.0.0
    MIDlet-Vendor: Craig Caulfield
    MIDlet-1: Build Canary, , speck.buildmonitor.BuildCanary
    MicroEdition-Profile: IMP-1.0
    MicroEdition-Configuration: CLDC-1.1
    BaseStationAddress: 0014.4F01.0000.3A3C
    PortNumber: 100

    清单 1 中最重要的一行是:

    MIDlet-1: Build Canary, , speck.buildmonitor.BuildCanary

    第一个参数是应用程序的名称,第三个参数是应用程序主类的完全限定类名。

    可以在该文件中添加自己的属性,并在运行时读取这些属性,例如:

    String baseStationAddress = getAppProperty("BaseStationAddress");
  4. 创建一个扩展 javax.microedition.midlet.MIDlet 的类,然后开始开发应用程序。
  5. 当准备好部署代码时,将代码打包到一个 JAR 中,通过无线的方式将它发送到 SPOT:
    1. 使用 USB 线将一个基站 SPOT 连接到工作站。
    2. 执行 SPOT SDK 安装目录中的 ant startbasestation,启动基站。
    3. 执行以下命令部署 JAR:
      ant -DremoteId=0014.4F01.0000.3A19 deploy

下载 中提供了 build-canary 应用程序的 Eclipse 项目,可以以此为基础。

应用程序概述

图 2 中的部署图展示我如何设置构建监视器,以监视 CruiseControl 构建。(我使用 CruiseControl,因为它易于安装和配置,并且有很多插件可用于构造适合几乎任何需求的构建过程。请参阅 参考资料 获得详细信息)。

图 2. 构建监视器的部署图
构建部署图
构建部署图

CruiseControl 循环构建在一个构建服务器上运行,该构建服务器有一个通过 USB 线连接的 SPOT 基站。每当构建的当前状态(SUCCESSFAILEDRUNNING)改变时,构建服务器上都会调用一个简单的 Java SE 应用程序 — CanaryHandler 。然后,通过基站 SPOT 将一条无线消息发送到 BuildCanary— 远程 SPOT 上运行的一个 MIDlet — 以更新该 SPOT 的 LED,从而反映构建的新状态。

CanaryHandler 代码

为了让 CanaryHandler 程序有一个良好的开端,我使用 Apache Commons CLI 解析命令行参数(见 参考资料)。CLI 负责收集和验证参数,并提供方便的帮助功能。例如,如果输入 java CanaryHandler --help,可以看到清单 2 中的输出:

清单 2. CanaryHandler 的帮助文本
usage: java CanaryHandler (--running | --failed | --success) --spot
            "0014.4F01.xxxx.yyyy" --port 100 --serial COM4
This program connects with a remote Sun SPOT to set a colour to denote the
current state of the Continuous Integration build process.
 -a,--spot <spot>    The IEEE wireless address (like
                     0014.4F01.0000.30E0) of the SPOT (enclose in double quotes).
 -c,--serial <serial>The serial port (e.g. COM4) to which the SPOT base
                     station is attached.
 -f,--failed         The build has failed.
 -h,--help           Print this usage information.
 -p,--port <port>    The port address (range 32 to 255) to be used for
                        communicating with the SPOT.
 -r,--running        The building is running
 -s,--success        The building was successful.
For more instructions see the Javadoc in the docs/api directory.

清单 2 中可以看到,CanaryHandler 接受 4 个参数:

  • 构建的当前状态,可能的值有:
    • Running(SPOT 的 LED 中显示蓝色流光)。
    • Failed(SPOT 的 LED 闪烁红色)。
    • Successful(SPOT 的 LED 显示为不动的绿条)。
  • 一个远程 SPOT 的地址。每个 SPOT 由一个 64 位的 IEEE 无线地址标识,该地址以 0014.4F01 开头,后面再加上两个四位字节,从而惟一地标识 SPOT。
  • 一个端口号,惟一地标识基站与远程 SPOT 之间的连接。
  • 一个串行端口,标识构建服务器上与基站连接的串行端口。

解析命令行参数后,CanaryHandler 打开与远程 SPOT 的 radiostream 连接,如清单 3 所示:

清单 3. CanaryHandler 中的 main() 方法
/**
 * Respond to the state a continuous build process by setting the LEDs on a
 * remote SPOT accordingly. This is done by writing a simple message to an
 * output stream, on the end of which is a SPOT waiting to read.
 *
 * @param args the command line arguments. See the printUsage
 * method further down for a full description of the parameters.
 */
public static void main(String[] args) throws IOException {

    createCommandLineParser();
    StreamConnection connection = null;
    DataOutputStream dos = null;
    DataInputStream dis = null;

    try {

        CommandLine commandLine = parser.parse(options, args);
        String spotAddress = commandLine.getOptionValue("spot");
        String port = commandLine.getOptionValue("port");
        String spotConnection = "radiostream://" + spotAddress + ':' + port;

        System.setProperty("SERIAL_PORT", commandLine.getOptionValue("serial"));

        log.info("Setting address to " + spotConnection);

        connection = (StreamConnection) Connector.open(spotConnection);
        dos = connection.openDataOutputStream();
        dis = connection.openDataInputStream();

        if (commandLine.hasOption("running")) {

            log.info("Setting build state to RUN.");
            dos.writeUTF("RUN");
            dos.flush();
            log.info("SPOT responded with: " + dis.readUTF());

        } else if (commandLine.hasOption("failed")) {

            log.info("Setting build state to FAIL.");
            dos.writeUTF("FAIL");
            dos.flush();
            log.info("SPOT responded with " + dis.readUTF());

        } else if (commandLine.hasOption("success")) {

            log.info("Setting build state to SUCCESS.");
            dos.writeUTF("SUCCESS");
            dos.flush();
            log.info("SPOT responded with " + dis.readUTF());

        } else {

            printUsage(options);
            System.exit(1);

        }

    } catch (ParseException e) {

        // This will be thrown if the command line arguments are malformed.
        printUsage(options);
        System.exit(1);

    } catch (NoRouteException nre) {

        log.severe("Couldn't get a connection to the remote SPOT.");
        nre.printStackTrace();
        System.exit(1);

    } finally {

        if (connection != null) {
            connection.close();
        }

        System.exit(0);
    }
}

radiostream 协议类似于点对点套接字连接,它为基站与远程 SPOT 之间提供可靠、有缓冲的基于流的 I/O。另外,还打开数据输入和输出流。然后,一条关于构建的简单的消息被写到输出流,远程 SPOT 读取该消息,然后返回一条简短的确认消息。接着 CanaryHandler 结束,等待下一次构建状态改变时再次被调用。

BuildCanary 代码

CanaryHandler 打开一个 radiostream 连接并发送一条关于构建的简单消息时,连接的另一端是 BuildCanary,它是部署在远程 SPOT 上的一个 Java ME MIDlet。MIDlet 与 servlet 和 EJB 类似,也是实现一个专门的接口,运行时环境负责在它的生命周期中的某些时候调用某些方法。

例如,MIDlet 的典型的入口点是 startApp() 方法。在 BuildCanary 中,startApp() 委托给另一个方法,后者又产生一个线程,以侦听来自 CanaryHandler 的消息。清单 4 显示 BuildCanary 的入口点的代码:

清单 4. BuildCanary 的入口点
/**
 * MIDlet call to start our application.
 */
protected void startApp() throws MIDletStateChangeException {
    run();
}

/**
 * Main application run loop which spawns a thread to listen for updates
 * about the build status from the host.
 */
private void run() {

    // Spawn a thread to listen for messages from the host.
    new Thread() {

        public void run() {
            try {

                updateBuildStatus();

            } catch (IOException ex) {

                try {
                    if (connection != null) {
                        connection.close();
                    }

                    if (dos != null) {
                        dos.close();
                    }

                    if (dis != null) {
                        dis.close();
                    }

                    ex.printStackTrace();
                } catch (IOException ex1) {
                    // Can't do much now.
                    ex1.printStackTrace();
                }
            }
        }
    }.start();
}

updateBuildStatus() 方法处理 MIDlet 的主要工作,如清单 5 所示:

清单 5. 侦听 CanaryHandler 发送的消息的方法
/**
 * Reflect the status of the continuous build process taking place on the
 * host PC by setting the colours of the SPOT's LEDs appropriately.
 */
private void updateBuildStatus() throws IOException {

    setColour(LEDColor.WHITE, "");

    String baseStationAddress = getAppProperty("BaseStationAddress");
    String portNumber = getAppProperty("PortNumber");
    String connectionString = "radiostream://" + baseStationAddress + ':' +
            portNumber;

    // Open a connection to the base station.
    connection = (RadiostreamConnection) Connector.open(connectionString);
    dis = connection.openDataInputStream();
    dos = connection.openDataOutputStream();

    while (true) {

        // dis will block here forever until it has something from the host
        // to read.
        String buildStatus = dis.readUTF();

        if (buildStatus.equals("RUN")) {

            setColour(LEDColor.BLUE, buildStatus);

        } else if (buildStatus.equals("SUCCESS")) {

            setColour(LEDColor.GREEN, buildStatus);

        } else if (buildStatus.equals("FAIL")) {

            setColour(LEDColor.RED, buildStatus);

        } else {
            throw new IllegalArgumentException("The build status of " +
                    buildStatus + " isn't recognised.");
        }

        dos.writeUTF("Build status updated to " + buildStatus);
        dos.flush();
    }
}

该方法首先将所有 LED 变成白色,表示 SPOT 就绪。然后,与 CanaryHandler 类似,它打开一个 radiostream 连接以及数据输入和输出流。但是,在这里,它首先尝试从输入流读一条消息,并阻塞直到收到消息。

如清单 6 所示,当收到一条消息时,setColour() 方法产生另一个线程,以更新 SPOT 上的 LED 的颜色。然后,BuildCanary 回到 while 循环的顶端,等待下一条消息。

清单 6. 产生一个设置颜色的新线程
/**
 * Set a colour for the LEDs to display.
 *
 * @param colour an LEDColor value.
 * @param buildStatus the current status if the build.
 */
private void setColour(final LEDColor colour, final String buildStatus) {

    if (colourThread != null) {

        colourThread.interrupt();

    }

    setColour = new SetColour(colour, buildStatus);
    colourThread = new Thread(setColour);
    colourThread.start();
}

与此同时,真正操作 LED 是在 SetColour 类中,如清单 7 所示:

清单 7. 操作 LED 的内部类
/**
 * An inner class to handle the colour and behaviour (flashing, solid,
 * running) of the LEDs of a SPOT.
 */
public class SetColour implements Runnable {

    /**
     * A reference to the SPOTs LEDs so they can be set according to the
     * state of the build.
     */
    private ITriColorLED[] leds = EDemoBoard.getInstance().getLEDs();

    /**
     * The RGB colour to set the LEDs to.
     */
    private LEDColor colour;

    /**
     * The current status of the build. This will be used to set the
     * behaviour of the LEDs (flashing, solid, running).
     */
    private String buildStatus;

    public SetColour(LEDColor colour, final String buildStatus) {

        this.colour = colour;
        this.buildStatus = buildStatus;
    }

    public void run() {

        try {

            if (buildStatus.equals("RUN")) {

          // Display running LEDs.
                while (true) {

                    for (int i = 0; i < 8; i++) {

                        leds[i].setColor(colour);
                        leds[i].setOn();
                        Thread.sleep(200);
                        leds[i].setOff();

                    }
                }

            } else if (buildStatus.equals("FAIL")) {

          // Flash the LEDs on and off.
                while (true) {

                    for (int i = 0; i < 8; i++ ) {
                        leds[i].setColor(colour);
                        leds[i].setOn();
                     }

                    Thread.sleep(250);

                     for (int i = 0; i < 8; i++ ) {
                        leds[i].setOff();

                     }

                    Thread.sleep(250);
                }

            } else {

          // Display the LEDs as a solid bar.
                for (int i = 0; i < 8; i++) {
                    leds[i].setColor(colour);
                    leds[i].setOn();
                }
            }

        } catch (InterruptedException ie) {
        // Do nothing. Just bail out so we can set the LEDs to another colour.
        }
    }
}

清单 7 为 SPOT 的 8 个 LED 分别设置颜色,并根据构建的状态打开或关闭这些 LED。

下一步是将构建监视器附加到持续构建过程。

CruiseControl 配置

当 CruiseControl 开始时,它在一个连续循环中运行,周期性地检查源代码库,例如 Subversion,然后从头开始构建和测试项目。然后,CruiseControl 可以将构建成功或失败的状态发布到网站上,供所有人查看,并发出各种不同的消息。

CruiseControl 循环的行为由一个 XML 配置文件 config.xml 表示,如清单 8 所示:

清单 8. CruiseControl 配置文件
<cruisecontrol>

   <property environment="env"/>
   <property name="local.directory" value="C:\data\skills\development\builds"/>
   <property name="repository" value="http://kimba/svn"/>
   <property name="javaExecutable" value="C:\applications\java\jdk1.6.0_14\bin\
                       java.exe"/>
   <property name="workingDirectory" value="C:\data\skills\development\java\micro\
                                  netbeans\sunspots\CanaryHandler\dist"/>
   <property name="libraryPath" value="-Djava.library.path=C:\applications\Sun\
                       SunSPOT\sdk-red-090706\lib"/>
   <property name="commonArguments" value="${libraryPath} -jar
                       ${workingDirectory}\CanaryHandler.jar
                       --spot "0014.4F01.0000.3A19"
                       --port 100
                       --serial COM4"/>

   <plugin name="log" dir="${logdir}"/>
   <plugin name="svn" classname="net.sourceforge.cruisecontrol.sourcecontrols.SVN"
                     username="cruise"
                     password="catbert"/>

   <project name="developer-ci-build" buildafterfailed="false">

<!-- Defines where CruiseControl looks for changes to decide whether to run the build. -->
      <modificationset quietperiod="30">
         <svn localWorkingCopy="${local.directory}/checkout/SunDeveloper"
                   repositoryLocation="${repository}/DEVELOPER/trunk/SunDeveloper"/>
      </modificationset>

<!-- Check for modifications every 60 seconds -->
      <schedule interval="60">
         <composite>
         <exec command="${javaExecutable}" args="${commonArguments}
                              --running"/>
         <maven2 mvnhome="${env.MAVEN_HOME}"
                           pomfile="${local.directory}/checkout/SunDeveloper/pom.xml"
                           goal="clean | scm:update | compile"/>
         </composite>
      </schedule>

      <listeners>
         <currentbuildstatuslistener
                      file="${local.directory}/logs/${project.name}/buildstatus.txt"/>
      </listeners>

<!-- The publishers are run when a build completes one way or another. -->
      <publishers>
         <onsuccess>
            <execute command="${javaExecutable} ${commonArguments}
                              --success"/>
         </onsuccess>
         <onfailure>
            <execute command="${javaExecutable} ${commonArguments}
                              --failed"/>
         </onfailure>
      </publishers>
   </project>

</cruisecontrol>

在我的循环中,有一个项目 developer-ci-build — 这是在本地 Subversion 库中注册的一个 Apache Maven 项目。CruiseControl 每分钟检查一次这个库,看是否有修改;如果有改动,它执行一些 Maven 目标,以构建项目。但是,首先它带一个 --running 参数调用 CanaryHandler,以显示构建正在进行。

构建要么成功,要么失败,导致要么调用 onsuccess 发布者,要么调用 onfailure 发布者。在这些发布者中有更多对 CanaryHandler 的调用,用于设置构建的状态。

运行构建

现在,所有的块已经就绪,接下来可以运行构建,看看远程 spot 有何反应。

图 3 显示 SPOT 的开始状态,所有 LED 被设为白色:

图 3. 构建服务器上的构建监视器的初始状态(LED 均为白色)
构建监视器的初始状态
构建监视器的初始状态

然后,故意将有错的代码提交到 Subversion 库,等待 CruiseControl 检测到变化并开始构建。当构建开始时,构建监视器 SPOT 上的 LED 从全白变为蓝色流光,如图 4 所示:

图 4. 构建监视器显示构建正在进行中(LED 显示蓝色流光)
构建正在进行时的构建监视器
构建正在进行时的构建监视器

不久后,构建如期失败,LED 变成紧急闪烁的红色,如图 5 所示:

图 5. 构建监视器显示构建失败(LED 闪烁红色)
构建监视器显示构建失败
构建监视器显示构建失败

最后,将修正错误后的代码提交到 Subversion。CruiseControl 再次开始构建,这一次构建成功,LED 变成不动的绿条,如图 6 所示:

图 6. 构建监视器显示构建成功
构建监视器显示构建成功
构建监视器显示构建成功

结束语

如果在持续集成系统上运行的构建失败,那么可以肯定失败的原因出在最近的更改中。越早知道失败可以越快作出反应。本文描述的 SPOT 的构建监视器用于提供高度可视的指示器,表明需要紧急修复项目代码。

虽然 SPOT 仍然是一项实验技术,但已经有一群积极的拥护者,并且他们以许多激动人心的、有想象力的方式在使用该技术 — 从安装在火箭上向高校学生演示物理概念,到安装在树上监视火情,甚至被用作游戏控制器。请访问 Sun SPOT World 上的论坛,观看 YouTube 上的演示,了解各种不同的 SPOT 应用(请参阅 参考资料)。


下载资源


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology, Open source
ArticleID=458000
ArticleTitle=使用 Sun SPOT 作为构建监视器
publish-date=12212009