17.7. The SimpleSensor: Integration with HackyInstaller

While our SimpleSensor is becoming increasingly full-featured, there are some important usability issues associated with the implementation thus far. First, how do users download the sensor from a server, or even know whether it is available? Second, how do users know when the server has been updated with a new release, which indicates the potential for a newly enhanced version of the SimpleSensor? Third, since sensors can sometimes be installed on a site-wide basis, how can a sensor designer provide individual users with control over whether or not the sensor collects data about their development?

To simplify the answers to the above questions, the Hackystat Framework provides an extensible client-side tool called HackyInstaller. Users download HackyInstaller from the server, then run it to find out what sensors are available for download from the server and whether their local sensors are out of date with respect to those on the server. They can also use HackyInstaller to "enable" or "disable" an installed sensor.

In this section, we will extend our SimpleSensor with the facilities required for its integration with HackyInstaller. This involves the creation of an XML file that tells HackyInstaller about the sensor and its characteristics, a Java class that implements certain installation commands in a sensor-specific manner, and a small addition to the local.build.xml file to integrate this new code into the build system. We will look at each of these enhancements in the next several subsections. (For a general overview of HackyInstaller development, see Chapter 21, HackyInstaller Developer Guide.)

17.7.1. Create the installer/ subdirectory

The first step in implementing an installer is to create a directory to hold all of the installer code. By convention, this directory is called "installer" and located in a subdirectory of your sensor implementation package directory. In the case of SimpleSensor, the installer directory is:

hackyDoc_SimpleSensor/src/org/hackystat/doc/simplesensor/installer

All of the files associated with the installer component of SimpleSensor will be located in this directory.

17.7.2. The hackyDoc_SimpleSensor.simplesensor.installer.def.xml file

The first step in implementing an installer is to define the properties associated with the sensor. Example 17.19, “The hackyDoc_SimpleSensor.simplesensor.installer.def.xml file” shows this definition file for SimpleSensor.

Example 17.19. The hackyDoc_SimpleSensor.simplesensor.installer.def.xml file

 
<sensor>
  <name value="SimpleSensor" />
  <os windows="true" mac="true" linux-unix="true" />
  <package value="org.hackystat.doc.simplesensor.installer" />
  <class value="SimpleSensorInstaller" />
  <description value="The Simple Sensor sends DevEvents to the Server." />
  <!-- Paths needed by this sensor -->
  <path id="SIMPLESENSOR_HOME" description="SimpleSensor home directory" example="C:\java\simplesensor (Windows), /usr/home/johnson/java/simplesensor (Unix)" />
  <!-- Sensor specfic properties for sensor.properties -->
  <property name="ENABLE_SIMPLESENSOR_SENSOR" value="false" readonly="false" validationclass="" description="Enable simple sensor." />
</sensor>

This installer definition file provides much of the information required by HackyInstaller to make this sensor available through its interface. The definition indicates the name of the sensor, what operating systems it can be used under, the package and class name containing the installer code, a short documentation string about the sensor that will appear in the HackyInstaller interface, one or more file paths that the user must provide and which the SimpleSensorInstaller code can use to properly install the sensor, and one or more properties that will be maintained in the sensor.properties file. In the case of Simple Sensor, the definition is quite simple. SIMPLESENSOR_HOME is the user-specified path where the sensor.simplesensor.jar file will be located. The ENABLE_SIMPLESENSOR_SENSOR property can be used by the user to disable or enable the sensor without having to install or reinstall the code. (We will update our SimpleSensor.java class to check this property below.)

For more details on the installer XML file, see Section 21.2, “Writing the Installer Definition File”.

17.7.3. The SimpleSensorInstaller.java file

After defining the installer XML file, the next step is to implement a Java class that extends the HackyInstaller SensorInstaller class. The SensorInstaller class contains five abstract methods that must be implemented by your subclass, and which together provide the sensor-specific functionality needed for HackyInstaller to manage your sensor. These methods are install(), update(), isInstalled(), remove(), and isEnabled().

Example 17.20, “The SimpleSensorInstaller.java file” shows the SimpleSensorInstaller.java file, with JavaDocs deleted to save space.

Example 17.20. The SimpleSensorInstaller.java file

 
package org.hackystat.doc.simplesensor.installer;

import java.io.File;
import java.io.IOException;
import org.hackystat.core.installer.model.InstallerException;
import org.hackystat.core.installer.model.ModelException;
import org.hackystat.core.installer.model.path.ConfigPersistor;
import org.hackystat.core.installer.model.path.Path;
import org.hackystat.core.installer.model.property.Property;
import org.hackystat.core.installer.model.property.PropertyManager;
import org.hackystat.core.installer.model.sensor.SensorDescriber;
import org.hackystat.core.installer.model.sensor.SensorInstaller;
import org.hackystat.core.installer.util.Directory;
import org.hackystat.core.installer.util.Version;
import org.xml.sax.SAXException;

public class SimpleSensorInstaller extends SensorInstaller {

  public SimpleSensorInstaller(SensorDescriber sensor) throws ModelException {
    super(sensor);
  }

  public void install () throws InstallerException, ModelException {
    try {
      // make sure paths are correct before installing sensor to the directory
      super.validateAllSensorPaths();
      Path path = super.pathManager.getPath("SIMPLESENSOR_HOME");

      // download sensor and set the new version number
      super.downloadToTempDir();
      super.removeOldSensor(path.getLocation());
      super.moveSensorFile(this.getSensorJarfileName(), path.getLocation());
      super.sensor.setVersion(Version.getServerVersion());

      // Write new information to hackyinstaller.xml.
      ConfigPersistor.getInstance().write(); 
    }
    catch (IOException e) {
      throw new InstallerException("Error contacting server for sensor files.", e);
    }
    catch (SAXException e) {
      throw new InstallerException("Could not retrieve version of sensor downloaded.", e);
    }
    catch (Exception e) {
      throw new InstallerException(e.getMessage(), e);
    }
    finally {
      Directory.deleteDir(this.sensorTempDir); // cleans up temporary directory
    }
  }
  
  public void update () throws ModelException, InstallerException {
    super.validateAllSensorPaths();
    if (!super.hasLatestVersion()) {
      this.install();
    }
  }  
  
  public void remove () throws ModelException, InstallerException {
    Path path = super.pathManager.getPath("SIMPLESENSOR_HOME");
    File simpleSensor = new File(path.getLocation(), super.getSensorJarfileName());

    // Check if sensor exists, throw exception if we can't find it. 
    if (!simpleSensor.exists()) {
      throw new InstallerException("Simple Sensor does not appear to be installed.");
    }
    // Delete sensor, throw exception if it doesn't get deleted.
    if (!simpleSensor.delete()) {
      throw new InstallerException("Error removing Simple Sensor.  File may be in use.");
    }
    // Update hackyinstaller.xml.
    super.sensor.setVersion("");
    ConfigPersistor.getInstance().write();
  }

  public boolean isEnabled () {
    PropertyManager properties = super.sensor.getPropertyManager();
    Property property = properties.getProperty("ENABLE_SIMPLESENSOR_SENSOR");
    return Boolean.valueOf(property.getValue()).booleanValue();
  }

  public boolean isInstalled () throws ModelException {
    Path path = super.pathManager.getPath("SIMPLESENSOR_HOME");
    File buildSensor = new File(path.getLocation(), super.getSensorJarfileName());
    return buildSensor.exists();
  }  
}

This code is pretty straightforward, and we'll go through each of the methods in turn.

17.7.3.1. SimpleSensorInstaller()

All SensorInstaller subclasses should provide a constructor that accepts a SensorDescriber as an argument and calls the superclass constructor. This enables HackyInstaller to initialize itself correctly. This code is quite simple and has the following form in the case of SimpleSensor:

 public SimpleSensorInstaller(SensorDescriber sensor) throws ModelException {
    super(sensor);
  }

17.7.3.2. install()

The install() method is called by HackyInstaller when the user requests that the sensor be installed. The SimpleSensor implementation of install() is:

  public void install () throws InstallerException, ModelException {
    try {
      // make sure paths are correct before installing sensor to the directory
      super.validateAllSensorPaths();
      Path path = super.pathManager.getPath("SIMPLESENSOR_HOME");

      // download sensor and set the new version number
      super.downloadToTempDir();
      super.removeOldSensor(path.getLocation());
      super.moveSensorFile(this.getSensorJarfileName(), path.getLocation());
      super.sensor.setVersion(Version.getServerVersion());

      // Write new information to hackyinstaller.xml.
      ConfigPersistor.getInstance().write(); 
    }
    catch (IOException e) {
      throw new InstallerException("Error contacting server for sensor files.", e);
    }
    catch (SAXException e) {
      throw new InstallerException("Could not retrieve version of sensor downloaded.", e);
    }
    catch (Exception e) {
      throw new InstallerException(e.getMessage(), e);
    }
    finally {
      Directory.deleteDir(this.sensorTempDir); // cleans up temporary directory
    }
  }

This code is also quite straightforward and uses a number of methods inherited from the super class. The validateAllSensorPaths() method ensures that any paths defined by the user are valid. Next, the downloadToTempDir() method finds the sensor.simplesensor.jar file on the Hackystat server and places it into a local temp directory. Next, any old versions of the SimpleSensor are removed, and the downloaded file is then placed into the directory. Finally, the Hackystat server version associated with this downloaded code is saved locally. This is used by HackyInstaller to detect whether an updated version exists at the server. The last step is to update the hackyinstaller.xml file with the updated sensor version information.

Note that in most cases, sensors can use this code almost unchanged, simply substituting you own HOME path variable for the one in this example.

17.7.3.3. update()

The update() method is called from HackyInstaller when the user requests that the system download and install a new version of the sensor, if and only if the Hackystat server contains a more recent release. The code quite straightforward and looks like the following:

  public void update () throws ModelException, InstallerException {
    super.validateAllSensorPaths();
    if (!super.hasLatestVersion()) {
      this.install();
    }
  }  

17.7.3.4. remove()

The remove() method is called when the user requests that the sensor be uninstalled. The code checks to be sure that the sensor is installed, then deletes it, then update the hackyInstaller.xml file to indicate the sensor is no longer installed. It throws appropriate error messages as necessary:

  public void remove () throws ModelException, InstallerException {
    Path path = super.pathManager.getPath("SIMPLESENSOR_HOME");
    File simpleSensor = new File(path.getLocation(), super.getSensorJarfileName());

    // Check if sensor exists, throw exception if we can't find it. 
    if (!simpleSensor.exists()) {
      throw new InstallerException("Simple Sensor does not appear to be installed.");
    }
    // Delete sensor, throw exception if it doesn't get deleted.
    if (!simpleSensor.delete()) {
      throw new InstallerException("Error removing Simple Sensor.  File may be in use.");
    }
    // Update hackyinstaller.xml.
    super.sensor.setVersion("");
    ConfigPersistor.getInstance().write();
  }

17.7.3.5. isEnabled()

The isEnabled() method is called by HackyInstaller when displaying the "summary" table in the opening window with the status of each sensor. By convention, each sensor defines a property called ENABLE_<SENSORNAME>_SENSOR that holds a value of true or false and indicates whether the installed sensor will actually run or not. This method simply returns the value of that property as a boolean.

  public boolean isEnabled () {
    PropertyManager properties = super.sensor.getPropertyManager();
    Property property = properties.getProperty("ENABLE_SIMPLESENSOR_SENSOR");
    return Boolean.valueOf(property.getValue()).booleanValue();
  }

17.7.3.6. isInstalled()

The isInstalled() method should return true if the sensor is installed. One way to implement this is by checking to see whether the sensor file has been downloaded or not:

  public boolean isInstalled () throws ModelException {
    Path path = super.pathManager.getPath("SIMPLESENSOR_HOME");
    File buildSensor = new File(path.getLocation(), super.getSensorJarfileName());
    return buildSensor.exists();
  } 

17.7.3.7. Summary

After implementing the Installer XML definition file and SensorInstaller subclass, the next step is to integrate this code into the build. This requires a minor addition to the local.build.xml file, as discussed next.

17.7.4. The local.build.xml file

In order for the SimpleSensor installer definition and code to be usable by HackyInstaller, it must be copied to an appropriate area during the build process at an appropriate time. Both of these constraints can be satisfied by defining a new "pre-sensorshell" build target and invoking the HackyCore_Installer.installXmlDefinitions macro, as illustrated in Example 17.21, “The hackyDoc_SimpleSensor.install.pre-sensorshell target”.

Example 17.21. The hackyDoc_SimpleSensor.install.pre-sensorshell target

 
  <target name="hackyDoc_SimpleSensor.install.pre-sensorshell" if="hackyDoc_SimpleSensor.available"
    description="Copy over installer files.">
    <hackyCore_Installer.installXmlDefinitions module.src.dir="${hackyDoc_SimpleSensor.src.dir}" />
  </target>

There's not much to say about this target: simply use it as a template for your own version of the pre-sensorshell target in your local.build.xml, making sure you change all references to hackyDoc_SimpleSensor to your own module name.

17.7.5. Reacting to the enable property in the sensor

An important capability provided by the HackyInstaller definition is the presence of a property called ENABLE_SIMPLESENSOR_SENSOR whose value is true if the sensor should actually run if invoked, and false otherwise. Example 17.22, “The SimpleSensor.java file with support for the enabled property” shows how the SimpleSensor class can be enhanced to react to the presence of this property. The JavaDocs have been removed, and the added lines are highlighted.

Example 17.22. The SimpleSensor.java file with support for the enabled property

 
package org.hackystat.doc.simplesensor;
import java.util.Date;

import org.hackystat.core.kernel.admin.SensorProperties;
import org.hackystat.core.kernel.shell.SensorShell;

public class SimpleSensor {
  
  private SensorShell shell; 
  private boolean isEnabled = false;

  public SimpleSensor() {
    SensorProperties sensorProperties = new SensorProperties("SimpleSensor");
    this.isEnabled = sensorProperties.isSensorEnabled();
    this.shell = new SensorShell(sensorProperties, false, "SimpleSensor");
  }
  
  public SimpleSensor(String host, String userKey) {
    SensorProperties sensorProperties = new SensorProperties(host, userKey);
    this.isEnabled = true; // always enable for testing purposes.
    this.shell = new SensorShell(sensorProperties, false, "SimpleSensor");
  }
  
  public void addDevEvent(Date tstamp, String type, String path) {
    if (this.isEnabled) {
      this.shell.doCommand(tstamp, "DevEvent", "add", "tool=SimpleSensor", 
          "type=" + type, "path=" + path);
    }
  }

  public boolean send() {
    return (this.isEnabled) ? this.shell.send() : false;
  }
  
  public static void main(String[] args) {
    String type = (args.length > 0) ? args[0] : "Invoked";
    SimpleSensor sensor = new SimpleSensor();
    sensor.addDevEvent(new Date(), type, System.getProperty("user.dir"));
    sensor.send();
  }
}

This example illustrates the canonical way to handle the enabled property. In the constructor, the sensor calls the isSensorEnabled() method to determine if the property ENABLE_<SENSOR>_SENSOR in the sensor.properties file is set to true. The result of this call is saved in an instance variable and the methods that manipulate the sensor instance (addDevEvent(), send()) are conditionalized to do nothing unless the sensor is "enabled".

As you can also see from this example, the "client" code (i.e. for example, the main() method) does not change at all. It should not be the sensor client's responsibility to decide whether or not the sensor is enabled or not. Instead, the client simply "uses" the sensor, and it is the sensor's responsibility to decide whether or not this "usage" has any effect or not.

17.7.6. Using HackyInstaller with SimpleSensor

Let's now create a build of Hackystat with a HackyInstaller that knows about SimpleSensor and see how it looks.

[Note]Warning: Back up your sensor.properties and hackyinstaller.xml files now!

When you get to this point in the development of your sensor, you may want to make a backup copy of your sensor.properties and hackyinstaller.xml files in your .hackystat directory. (I normally just rename them to sensor.properties.old and hackyinstaller.xml.old.) The reason for this is that when you build your new version of HackyInstaller and run it, it will overwrite the current values of sensor.properties and hackyinstaller.xml. You will almost always lose some settings that you want, but that is the price you pay for hacking HackyInstaller! The way around it is to make a backup of these two files, then copy them back once you are done testing your local version of HackyInstaller.

17.7.6.1. Run AutoConfig

Since we have made changes to the local.build.xml file, it is important to run AutoConfig so that the build system sees the addition of the pre-sensorshell target:

c:\svn\hackyCore_Build>ant -f autoconfig.build.xml
Buildfile: autoconfig.build.xml

run:
    [mkdir] Created dir: C:\svn\hackyCore_Build\build\autoconfig
     [echo] [AutoConfig] Generated modules.build.xml and sample.hackystat.build.properties.

autoconfig.build.default:

BUILD SUCCESSFUL
Total time: 1 second
c:\svn\hackyCore_Build>

17.7.6.2. Build and run unit tests

Next, build the system and run the tests using 'ant -q freshStart all.junit':
c:\svn\hackyCore_Build>ant -q freshStart all.junit
     [echo] (16:02:01) Completed hackyCore_Build.checkModuleAvailability
     [echo] (16:02:02) Completed hackyCore_Build.hotUndeployHackystat
[hackyCore_Build.javac] Note: Some input files use unchecked or unsafe operations.
[hackyCore_Build.javac] Note: Recompile with -Xlint:unchecked for details.
[hackyCore_Build.javac] Note: Some input files use unchecked or unsafe operations.
[hackyCore_Build.javac] Note: Recompile with -Xlint:unchecked for details.
[hackyCore_Build.javac] Note: Some input files use unchecked or unsafe operations.
[hackyCore_Build.javac] Note: Recompile with -Xlint:unchecked for details.
[hackyCore_Build.javac] Note: Some input files use unchecked or unsafe operations.
[hackyCore_Build.javac] Note: Recompile with -Xlint:unchecked for details.
[hackyCore_Build.javac] Note: Some input files use unchecked or unsafe operations.
[hackyCore_Build.javac] Note: Recompile with -Xlint:unchecked for details.
[hackyCore_Build.javac] Note: Some input files use unchecked or unsafe operations.
[hackyCore_Build.javac] Note: Recompile with -Xlint:unchecked for details.
[hackyCore_Build.javac] Note: Some input files use unchecked or unsafe operations.
[hackyCore_Build.javac] Note: Recompile with -Xlint:unchecked for details.
     [echo] (16:02:24) Completed all.compile
     [echo] (16:02:47) Completed all.checkstyle
     [echo] (16:02:54) Completed all.install.pre-sensorshell
     [echo] (16:03:05) Completed hackyCore_Build.unjarSensorShellFiles
     [echo] (16:03:08) Completed hackyCore_Build.installSensorShell
     [echo] (16:03:41) Completed all.install.post-sensorshell
     [echo] (16:03:41) Completed hackyCore_Build.deployTestData
     [echo] (16:03:49) Completed hackyCore_Build.hotDeployHackystat
     [echo] (16:04:02) Completed hackyCore_Kernel.junit.
     [echo] (16:04:09) Completed hackyCore_Installer.junit.
     [echo] (16:04:09) Completed hackyCore_Statistics.junit.
     [echo] (16:04:22) Completed hackyCore_Report.junit.
     [echo] (16:04:32) Completed hackyCore_Common.junit.
     [echo] (16:04:34) Completed hackyCore_Telemetry.junit.
     [echo] (16:04:47) Completed hackySdt_Activity.junit.
     [echo] (16:04:50) Completed hackySdt_FileMetric.junit.
     [echo] (16:04:54) Completed hackySdt_WorkspaceMap.junit.
     [echo] (16:04:56) Completed hackySdt_DevEvent.junit.
     [echo] (16:04:59) Completed hackyDoc_SimpleSensor.junit.
     [echo] (16:04:59) Completed all.junitReport
     [echo] (16:04:59) Completed all.junit

BUILD SUCCESSFUL
Total time: 3 minutes 1 second
Sending build result to Hackystat server... Done!
c:\svn\hackyCore_Build>

17.7.6.3. Invoke the new version of HackyInstaller

To invoke the new HackyInstaller, change directories to hackyCore_Build/build/war/download. You should see a file called "hackyinstaller.jar" (as well as a file called "sensor.simplesensor.jar") in that directory. Either double-click the hackyinstaller.jar file (if your operating system supports that) or run "java -jar hackyinstaller.jar" from the command line. Figure 17.4, “ HackyInstaller main window ” illustrates the Main HackyInstaller window.

Figure 17.4.  HackyInstaller main window


HackyInstaller main window

This screen image shows the state of the HackyInstaller main window after we have reset the host to "http://localhost:8080/" (which is where the test server is running) and the key to "testdataset" (which is simply a test user account which we know to be defined after running junit tests). We have already clicked the "Verify Hackystat Host/Key" and the Status Log window indicates that HackyInstaller was able to contact the localhost and login with the user key.

The middle pane shows that SimpleSensor is available within HackyInstaller, but not yet installed locally. Let's fix that.

Figure 17.5, “ SimpleSensor configuration window (initial) ” shows the window that appears after we select SimpleSensor in the Sensor Settings pane and click "Configure Selected Sensor".

Figure 17.5.  SimpleSensor configuration window (initial)


SimpleSensor configuration window (initial)

This window shows us several things. First, it shows us the server version and the fact that we have no local version of the sensor (so an update is recommended). Second, we do not have the sensor enabled. Third, we must provide a home directory for the sensor code.

Figure 17.6, “ SimpleSensor configuration window (settings applied) ” illustrates the SimpleSensor configuration window after we have checked the "Enable simple sensor" checkbox, provided a home directory, and clicked on the "Apply" button to save the settings.

Figure 17.6.  SimpleSensor configuration window (settings applied)


SimpleSensor configuration window (settings applied)

Now we are finally ready to download and install the sensor.

Figure 17.7, “ SimpleSensor configuration window (sensor installed) ” illustrates the state of the SimpleSensor configuration window after pressing the "Update" button. You can see that the local version of the sensor now matches the server version.

Figure 17.7.  SimpleSensor configuration window (sensor installed)


SimpleSensor configuration window (sensor installed)

Figure 17.8, “ SimpleSensor home directory ” illustrates the contents of our SimpleSensor "home" directory after having installed the sensor. The file sensor.simplesensor.jar is now located in that directory and ready to be used to collect DevEvents.

Figure 17.8.  SimpleSensor home directory


SimpleSensor home directory

Note that although HackyInstaller was configured to obtain the SimpleSensor jar file from localhost, the SimpleSensor itself is independent from its "point of origin". In other words, if the user now went into HackyInstaller and changed the Hackystat Host property from "http://localhost:8080/" to "http://hackystat.ics.hawaii.edu", the next time SimpleSensor ran, it would retrieve this new host location from the sensor.properties file and send its DevEvent data to http://hackystat.ics.hawaii.edu instead.

[Note]Don't forget to revert your sensor.properties and hackyinstaller.xml files!

Just a friendly reminder that after playing around with your sensor definition and insuring that it works with HackyInstaller, you should not forget to revert your sensor.properties and hackyinstaller.xml files back to their previous states. Otherwise, you will be sending your development data to your local test server, which is almost certainly not what you want to do!