January 2008 Archives

In developing Ledgerscape, one thing that has become really important is to provide end users with the most Windows-like experience as possible when it comes to installation, starting, and stopping the server. This has long been a painful topic for Java developers. (Don't even get me started about Java WebStart... :) )

Ledgerscape runs inside Tomcat, and I should note also that Tomcat does include a native Windows launcher that may be sufficient for many purposes. In my case, however, I don't want my users to know that they are running Tomcat. I want to have complete control over the startup environment and the outward appearance of the app. I also don't want my users to have access to the configuration options that come with the Tomcat system tray icon.

Fortunately, there are some reasonable options out there, and the best I have found is Launch4j. I'm going to describe the broad strokes of how to create a Launch4j exe that starts up Tomcat for you. Over all, I've been very pleased with the results, and would recommend this to anyone who is deploying primarily to Windows users and wants to deploy their app in a professional-feeling package.

I should also note that this entry by Roshan Shrestha from several years back provided a very helpful starting point. I've updated and simplified what he did there, and I also opted to use Launch4j instead of JSmooth as it seems to be more up to date.

Overview

In short, my requirements were:

  • Provide a native .EXE that launches Tomcat
  • Appear in the taskbar like a regular .EXE
  • Take the user to a download page if they don't have a JRE installed
  • Open the user's browser after the server is launched from the EXE
  • Allow me to provide a custom icon and splash screen
  • In general, just 'feel' as much like a native Windows app as possible

There are 5 basic steps you need to do to get this going:

  1. Create a main class to start Tomcat
  2. Ensure that the Catalina environment gets initialized
  3. Create a Launch4j Configuration
  4. Create an icon and a splash screen
  5. Set up an Ant script to put it all together

I describe these steps in more detail below.

Create a main class to start Tomcat

At the end of the day, Launch4j just invokes a main method and then dies. In our main method, we want to start Tomcat and then start the user's browser pointed at the new Tomcat instance.

The listing of TomcatLauncher.java below shows what you need to do. Note the two key dependencies: on Tomcat's Bootstrap class for starting tomcat, and on ejalber's very useful BrowserLauncher for starting the user's browser. (You'll need to download the BrowserLauncher source and integrate it into your build).

Ensure that the Catalina environment gets initialized

The default Tomcat startup scripts do some mucking around to set various system properties when Tomcat is actually launched. We need to ensure that our launcher emulates this mucking around. For the most part, the two main things you need to worry about are setting catalina.home and logging system properties.

I've found the easiest and most flexible way to set the required properties is to use Launch4j's ini file feature, which lets you specify arguments to the launched JVM in an .ini file. This file is read every time the launcher is executed (NOT when it is built), so it also allows you to tweak VM settings (such as heap size) in the wild. The file has to be in the same directory as the lancher .exe file and have the same name but with an extension of .l4j.ini rather than .exe.

The .ini file can use a number of useful variables such as EXEDIR that we can use to ensure that the right directories get found. See the mylauncher.l4j.ini listing below for an example.

Create a Launch4j Configuration

Launch4j needs an XML file that describes how to create the launcher at buildtime. There isn't a whole lot to this - it basically tells it where to find jars and icons and the name of the class to run. Launch4j comes with a lot of documentation and even a fancy GUI for setting this up, so I won't belabor this part of it. See the appended launcher.xml for an example and a few notes on potential gotchas.

Create an icon and a splash screen

The splash screen is completely native and for this reason (apparently), it *has* to be a bloated .BMP. The .BMP

The icon file has to be a Windows .ico file. There are lots of ways to make this. On MacOS, I found a very handy one called IcoMaker.

Set up an Ant script to put it all together

Launch4j includes a nice ant task that lets you automate the building of your .exe (even on MacOS!). Again, I won't belabor the details here, but you need to:

  • Compile your classes and jar them up.
  • Run the Launch4jTask, pointing it at the launch4j binaries, splash BMP, and ICO files
  • Copy your jar, the ini file, and whatever else into your deployment directory

That's pretty much all there is to it. As I say, so far I have been very pleased with they way this has worked. Users don't even know they are running a Java application, and I retain a lot of control over the environment that they're running in.

Another thing that I've done that completes the illusion is to create a windows tray icon using the com.jeans.trayicon package - I'll add an entry about this later.


Code Listings


launcher.xml

<launch4jConfig>

  <!-- Maybe just me, but I like keeping the jar outside -->
  <dontWrapJar>true</dontWrapJar>
  <headerType>gui</headerType>
  
  <outfile>mylauncher.exe</outfile>
  <errTitle>An error has occurred</errTitle>
  <chdir>.</chdir>
  <priority>normal</priority>
  <downloadUrl>http://java.com/download</downloadUrl>
  <supportUrl>http://www.mycompany.com/support</supportUrl>

  <!-- This is resolved at BUILDTIME -->
  
  <icon>images/desktop_icon.ico</icon>
  <classPath>
    <mainClass>com.mycompany.TomcatLauncher</mainClass>
    
    <!-- You need to use backslashes here -->    
    <!-- These are resolved at RUNTIME -->
    
    <cp>%EXEDIR%\lib\mylauncher.jar</cp>
    <cp>%EXEDIR%\tomcat\bin\bootstrap.jar</cp>
  </classPath>
  <jre>
    <path>jre</path>
    <minVersion>1.5.0</minVersion>
    <maxVersion></maxVersion>
    <dontUsePrivateJres>true</dontUsePrivateJres>
  </jre>
  <splash>
      <!-- This is resolved at BUILDTIME -->

    <file>images/splash.bmp</file>
    <waitForWindow>true</waitForWindow>
    <timeout>60</timeout>
    <timeoutErr>false</timeoutErr>
  </splash>  
</launch4jConfig>

build.xml

...
  <target name="exe">

    <taskdef name="launch4j"
      classname="net.sf.launch4j.ant.Launch4jTask"
      classpath="${launch4j.dir}/launch4j.jar:${launch4j.dir}/lib/xstream.jar" />
      
    <launch4j configFile="launcher.xml" 
              outfile='${build.dir}/mylauncher.exe' 
              bindir='${launch4j.dir}/bin'/>
  </target>

mylauncher.l4j.ini

-Dcatalina.home="%EXEDIR%/tomcat"
-Djava.util.logging.config.file="%EXEDIR%/tomcat/conf/logging.properties"
-Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager
-Xms512m
-Xmx512m

TomcatLauncher.java

package com.mycompany;

import org.apache.catalina.startup.Bootstrap;
import edu.stanford.ejalbert.BrowserLauncher;
import javax.swing.JOptionPane;

public class TomcatLauncher implements Runnable {


  // =========================================================================
  // Constants

  // url that the launcher will open in a browser.  you might need to make
  // this more flexible (e.g., passed in via args or configuration).
  private static final String START_URL = "http://localhost:8080";

  // =========================================================================
  // main() - start Tomcat in a separate thread so launcher thread can die

  public static void main(String[] args) {
    TomcatLauncher tl = new TomcatLauncher();
    Thread t = new Thread(tl);
    t.start();
  }


  // =========================================================================
  // Runnable imlementation - launch Tomcat


  public void run() {
    registerShutdownHook();
    startTomcat();
    showStartPage();
  }

  // =========================================================================
  // Private methods that do the real work

  /**
   * In case Ctrl-C or other unexpected shutdown...
   */
  private void registerShutdownHook() {
    this.shutdownThread = new Thread("shutdownHook") {
      public void run() {
        if (readyToExit == false) {// abnormal exit
        readyToExit = true;
        try {
          if (bootStrap != null) bootStrap.stop();
        } catch (Exception e) {
          e.printStackTrace();
        }
        // do any other cleanup work, e.g. closing log files
      } catch (Exception e) {
        e.printStackTrace();
      }
    }
    Runtime.getRuntime().addShutdownHook(this.shutdownThread);
  }

  /**
   * Start Tomcat
   */
  private void startTomcat() {
    try {
      log("Starting Tomcat...");
      bootStrap = new Bootstrap();
      bootStrap.init();
      bootStrap.start();
      log("...Tomcat started.");
    } catch (Throwable t) {
      t.printStackTrace();
      JOptionPane.showMessageDialog(this,
          "Error starting Ledgerscape server.", title,
          JOptionPane.ERROR_MESSAGE);
    }
  }

  /**
   * Open the browser
   */
  private void showStartPage() throws Exception {
    try {
      BrowserLauncher.openURL(START_URL);
    } catch (Exception e) {
      e.printStackTrace();
      JOptionPane.showMessageDialog(this, "Error starting browser.", title,
          JOptionPane.ERROR_MESSAGE);
    }
  }
  
  private void log(String msg) {
    // FIXME you will probably want to make this write to a file
    System.out.println(msg);
  }
}