Jasmin Script Tutorial

Version 1.1
September 1999
Jürgen Quittek, NEC Europe Ltd.

Contents


1 Introduction

This tutorial should assist the reader in understanding and writing Jasmin scripts, which can be installed at an SNMP agent supporting the Script MIB and which can be executed by the Jasmin runtime engine.

A Jasmin script is a Java program consisting of a set of class files which are stored in a single file using the Java archive (JAR) format. When we talk about a Jasmin script as an entity, usually this kind of jar file is being addressed. Section 2 of this tutorial explains in general how to write the Java code. It focusses on the few differences between writing Jasmin scripts and writing plain Java programs. A library supporting communication between Jasmin scripts and the SNMP agent is introduced in Section 3. How to store a Java program consisting of several class files into a jar file is explained in Section 4. The jar file is also the format in which a script is handled by the SNMP agent. Section 5 lists the packages of the JDK which can be used in Jasmin scripts.

A more detailed discussion of programming constraints for Jasmin scripts concerning runtime security and multi-threaded scripts can be found in Section 6 (runtime security) and Section 7 (multi-threading). Section 8 lists the possible exit codes of terminating scripts and the conditions under which they are generated. Issues concerning multiple scripts running concurrently at the same network element are discussed in Section 9

Section 10 might be the most insteresting one for many readers. It lists and explains several test scripts which demonstrate how to use features of the Jasmin runtime environment for writing scripts. Each of these scripts concentrates on a single feature.

2 Writing a Jasmin Script

This section provides a basic introduction to writing Jasmin scripts. It excludes specific issues such as runtime security and multi-threading, which are addressed in Section 6 (runtime security) and Section 7 (multi-threading).

Jasmin scripts can use the full functionality of the standard JDK except for opening windows, which really does not make much sense on a most network elements. The following is a very simple Jasmin script:

public class Main {
    public static String main() {
        return "Hello Jasmin.";
    }
}
The static method main of class Main returns a greeting string. It wouldn't make much sense to print "Hello Jasmin" on the command line, since the command line is on a remote network element and you probably would not see the output of your script. Instead the string is returned by method main as the result of this script. You can read the script result after the script has terminated by a get request on the Script MIB's corresponding smRunResult object.

2.1 Main Class and Main Method

For each system running Java programs the entry point of the program has to satisfy some requirements. For Sun's JDK for example, the main method must have the signature public static void main(String[]). Jasmin scripts relax these requirements a bit.

The parameter list of the main method may be empty or contain String[] only. The return type may be void, String, or byte[]. If a string or a byte array is returned, it is converted to an OCTET STRING, the type of script results in the MIB. The name of the method has to be main and it has to be declared as public and static. The following list contains all possible signatures of the main method:

public static void main();
public static void main(String[]);
public static String main();
public static String main(String[]);
public static byte[] main();
public static byte[] main(String[]);

Arguments are passed by setting the corresponding smLaunchArgument. If this argument contains no ASCII control characters, It is treated as an argument list and parsed for single arguments separated by whitespaces. Then the list of arguments is passed as a string array with one element per argument. If there are control characters contained in the argument, the string array passed to the script contains the whole argument as a single string.

While the name of the main method is fixed, the name of the class containing this method is not. Although based on JDK1.1.5, Jasmin already supports the Main-Class: entry in the jar file manifest (see Storing the Main Class in the Manifest), which is introduced by JDK1.2. This entry indicates the main class. If there is no such entry in the manifest, the main class must be called Main as in the example above.

3 Jasmin Script Library

For communication between a running script and the SNMP agent we provide a class handling intermediate results and a special exception class.

3.1 Intermediate Results

So far, we have discussed only one way of producing script output: returning a result from the main method. In general, a script can use more sophisticated ways, e.g. by establishing a network connetion to a management application (presuming a security profile allowing network connections). But there are two more basic ways to communicate via the SNMP agent.

A script can produce an intermediate result which is observable for the manager when getting the corresponding smRunResult even while the script is still running. In addition to the intermediate result, a smScriptResult notification can be requested.

Sending intermediate results to the agent is supported by class JasminResult which offers four static methods:

public class JasminResult {
    public static void deliver(String str) { ... }
    public static void deliver(byte[] bytes) { ... }
    public static void notify(String str) { ... }
    public static void notify(byte[] bytes) { ... }
}
The methods called deliver take a string or a byte array, convert it to an OCTET STRING, and deliver it to the SNMP agent. A very simple script producing intermediate results is
/**
 * Send 8 intermediate results given by the first
 * argument. Wait 10 seconds between two results.
 **/
public class Intermediate {
  public static void main() {
    for ( int i = 8; i > 0; i--) {
      // Send the intermediate result as a string.
      JasminResult.deliver(i+"0 seconds left");
      // Sleep 10 seconds.
      try { Thread.sleep(10000); }
      catch(InterruptedException e) { }
    }
  }
}
This script delivers an intermediate result every 10 seconds. The delivered string indicates the remaining lifetime of the script. Thread.sleep() may throw an InterruptedException which has to be caught. If we replace in the example above method deliver by method notify, then the SNMP agent will not only keep the passed intermediate result, but also generate an SNMP smScriptResult notificaton.

The class file JasminResult.class must be in your class path when compiling a script. The easiest way is to get it here or at the Jasmin home page and just put it in the same directory as your script sources.

3.2 Invalid Argument Exception

If a script throws an uncaught exception, the Jasmin runtime system terminates the script and sets the exit code to the value defined for runtimeError in the Script MIB. You can change this behaviour by throwing a JasminInvalidArgumentException. When receiving this exception, the runtime system will set the script's exit code to invalidArgument. So, this exception should only be used when the script arguments are checked. It is subtype of java.io.RuntimeException and does not have to be declared by the signature of the throwing method.

public class JasminInvalidArgumentException
                   extends java.lang.RuntimeException {
    JasminInvalidArgumentException() { ... }
    JasminInvalidArgumentException(String message)
    { ... }
}
This exception should not be confounded with java.lang.IllegalArgumentException which is thrown when the list of formal arguments of a method does not match a passed argument list.

You can get the class file for JasminInvalidArgumentException here or at the Jasmin home page.

4 Creating a Jar File

The Script MIB handles scripts as single units which e.g. can be downloaded from an HTTP server. Since a general Java program consists of a set of class files, Jasmin supports the jar file format for scripts. All class files of a script have to be stored in a single jar file. The jar tool which is part of Sun's JDK has a syntax similar to the tar program on Unix systems.

You can create a sript from Java class files by typing

    jar cf <script file name> <list of class files>
If we take our first example defining class Main and store it in a file called Main.java, we create script file called myFirstScript by typing
    javac Main.java
    jar cf myFirstScript.jar Main.class
Now, we can install and run myFirstScript.jar as a script at a Jasmin SNMP agent.

4.1 Storing the Main Class in the Manifest

For our second exaple some more effort is necessary. Since the name of the main class is not Main, an entry in the jar file manifest is required.

Starting with version 1.2, the JDK supports executable jar files which indicate the main class name in the jar file manifest. The manifest keeps meta-information about the jar file which is mainly used for security. But it also may contain a line such as

    Main-Class: Intermediate
indicating that Intermediate is the name of the main class in this jar file. Although Jasmin is based on JDK1.1.5, it already supports the Main-Class: entry. In order to add this line to the manifest you have to create a file containing this line only. If you call this file mainclass, you create a Jasmin script from the second code example created by
    javac Intermediate.java
    jar cfm mySecondScript mainclass \
        Intermediate.class JasminResult.class
Please note that file JasminResult.class has to be included in the jar file, because class Intermediate makes use of class JasminResult.

5 Supported JDK Packages

The runtime environment for Jasmin scripts offers the JDK1.1.5 packages. Hence, Jasmin scripts should be compiled with a JDK1.1.X, preferably JDK1.1.5 or higher. The following table shows which packages of JDK1.1.5 are available for scripts.

Packagesupported
java.appletno
java.awtno
java.awt.datatransferno
java.awt.eventno
java.awt.imageno
java.awt.peerno
java.beans*
java.ioyes
java.langyes
java.lang.reflectyes
java.mathyes
java.netyes
Packagesupported
java.rmiyes
java.rmi.dgcyes
java.rmi.registryyes
java.rmi.serveryes
java.securityyes
java.security.aclyes
java.security.interfacesyes
java.textyes
java.text.resourcesyes
java.utilyes
java.util.zipyes
* not clear yet

Although all supported packages are available, some of the classes in the packages might not be usable for a script depending on its runtime security profile (see the following section).

6 Jasmin Script Security

In Java, each access to crucial system resources, e.g. files or network connections, is mediated by the Java virtual machine and is checked in advance by the security manager or the class loader. A Jasmin security manager and a special Jasmin class loader restrict access of scripts to resources of the executing host.

Each running script is associated with a runtime security profile specifying the access rigths of that script. Security manager and class loader interpret the runtime security profile when deciding on whether access to a resource is granted to a script or not.

This implies that a script may run well with a specific runtime security profile while it may fail when running with another, more restricted profile. Currently, only two profiles have been implemented: "master" allows almost everything except manipulating other concurrently running scripts, and "untrusted" forbidding access to almost all resources.

When a script tries to violate its runtime security profile, a JasminSecurityException is thrown. It is a subtype of java.lang.SecurityException and therefore does not have to be declared by methods potentially throwing it. However, it can be caught as a java.lang.SecurityException by a script trying to access a resource (see example script CaughtSecurityViolation.jar).

7 Multi-Threaded Scripts

Like all other language features, also Java threads are available for Jasmin scripts. For each Jasmin script an own tread group is created in which the script is started as a thread, the script's main thread. The main thread executes the main method. It can create new threads and thread groups within its thread group. To all threads of a script the same runtime security policy is applied. However, there are a few differences in thread handling which are discussed in this section.

7.1 Script Termination

We defined the result of a script to be the return value of the main method. So, further threads created by the main thread can produce results either by communicating them to the main thread or by sending intermediate results.

Since it would be a quiet confusing situation if a thread produced intermediate results after the main method has returned the script result, we do not allow threads to continue after the main thread has been terminated. All other threads running at this point of time are terminated by the Jasmin runtime engine.

7.2 Thread Safety

However, there is no problem for different threads to use intermediate results concurrently. The library class JasminResult is thread save and all sent intermediate results are delivered to the SNMP agent.

7.3 Exceptions from Threads

If one of the script's threads throws an uncaught exception, the Jasmin runtime engine terminates not only this thread, but the whole script and sets the scripts exit code according to the thrown exception (see Section 8). If the exception has not been thrown by the main thread of the script, the message in the script result will explicitely state this. (See Section 10.4 for examples.)

There is one exception, java.lang.ThreadDeath, for which this is not true. The JDK provides this exception for "silently" terminating a thread without any notification of the user. In Jasmin scripts this exception is used to "silently" terminate a thread without terminating the whole script. This might be quiet desirable in many situations, e.g. if a thread detects that it failed to complete its task and should be terminated. You can compare such a situation to a single-threaded Java program calling System.exit()). However, if the main thread throws this exception, the whole script will be terminated. (To be precise: java.lang.ThreadDeath is in the Java terminology not really an exception but an error.)

8 Exit Codes

Terminating a script by calling System.exit() is not desirable for Jasmin scripts. For plain Java programs, this call not only terminates the program, but also terminates the Java virtual machine. This is not acceptable for Java scripts, since the JVM might be executing more than one script and all of these scripts would be terminated, if one of them called System.exit(). In order to prevent this, a security exception is thrown, if a script tries to calls this method. So, the call really terminates the script, but does not produce the desired exit code. Anyway, the possible exit codes of a script are predefined by the Script MIB.

For a script terminating regularly, the exit code noError is generated (corresponding to the definition of exit codes in the Script MIB). If a script is halted by setting the corresponding smRunControl object to abort, its exit code will be halted. If it is aborted, because the life time has expired, the exit code will be lifeTimeExpired. Exit code languageError is generated, if no main class can be found in the jar file or if the main class does not contain a method main wit a valid signature.

All other exit codes are triggered by exceptions thrown by the script. The following table lists all possible exit codes and the condition which produced them.

Exit Code Condition Remark
noError (1) terminated regularly  
halted (2) explicitely aborted by SNMP client  
lifeTimeExpired (3) life time has expired  
noResourcesLeft (4) script has thrown exception OutOfMemoryError or StackOverflowError see examples OutOfMemory.jar and StackOverflow.jar
languageError (5) main class not found or no valid method main see Section 2.1
runtimeError (6) script has thrown any exception not explicitely mentioned in this table  
invalidArgument (7) an argument has been passed to a script that does not take any argument, or the script has thrown a JasminInvalidArgumentException see Section 3.2 and example ReturnFirstArgument.jar
securityViolation (8) script has thrown a security violation exception see Section 6 and examples in section Section 10.1

9 Concurrent Scripts

If you are running several scripts concurrently, they have to share the resources of the network element. So, if you write a very time-consuming script, it might delay the execution of other scripts. Since the Jasmin runtime engine may execute several scripts within the same Java virtual machine, there is only a weak notion of fairness concerning the distribution of resources among different scripts. E.g. a script that wakes up every second in order to sample some performance values, is not guaranteed to start execution within a second, when other scripts are producing a heavy load. Two examples for such nasty scripts are Endless.jar and Busy.jar.

10 Test Scripts

This section lists and explains the permanent scripts installed at the Script MIB demo web page These are very simple scripts each demonstrating a basic action. They are intended to support studying the interactions between SNMP managers, SNMP agents, and scripts. They demonstrate returning results, passing arguments, producing intermediate results, and multi-threading in scripts.

10.1 Returning Results

This subsection introduces basic scripts demonstrating the return of results by a script. Results can be returned regularly or thrown as an exception. For results returned from multi-threaded scripts see subsection Multi-Threading, for intermediate results of scripts see subsection Intermediate Results.

EmptyScript.jar

This is the most simple script. It does nothing but starting and terminating. No argument is passed to it and no result is returned.
public class EmptyScript {
    public static void main() { return; }
}
After termination, the smRunResult is empty and the smExitCode of the script is noError.

StringResult.jar

This script just returns a String as result.
public class StringResult {
    public static String main() {
        return "This is the script result.";
    }
}
Now, the value of smRunResult after termination is "This is the script result.". As above, the smExitCode of the script is noError.

ByteArrayResult.jar

Instead of a string this script returns an array of four byte.
public class ByteArrayResult {
    public static byte[] main() {
        return new byte[] { (byte)0x01, (byte)0x23,
                             (byte)0xCD, (byte)0xEF };
    }
}
After termination of this script, its smRunResult contains the four bytes of the byte array created by the script. Again, the smExitCode of the script is noError.

SystemException.jar

Another kind of results of a script is exceptions. If a script throws an uncaught exception, the script is terminated and the script result is a string containing (1) a message from the Jasmin runtime system, (2) the type of the exception, and (3) the optional exception message. The following script violates an array boundary.
public class SystemException {
    public static void main() {
        int[] a = new int[2];;
        a[7] = 11;
    }
}
It produces the smRunResult: "Script exception: java.lang.ArrayIndexOutOfBoundsException: 7". The smExitCode of the script is runtimeError.

UserException.jar

The result is similar for an exception thrown by the user. Script UserException.jar
public class UserException {
    public static void main() {
        throw new RuntimeException(
            "my exception message");
    }
}
produces the output "Script exception: java.lang.RuntimeException: my exception message" with exit code runtimeError. There are only a few exceptions producing different exit codes. These are listed in Section 8.

DeclaredException.jar

A RuntimeException does not have to be declared when thrown by a method. This is not the case for many of the common exceptions in Java. The following script throws a declared exception.
public class DeclaredException {
    public static void main() throws Exception {
        throw new Exception(
            "my exception message");
    }
}
We see that the main method may additionally to the possible signatures listed in Section 2.1 can declare a list of exceptions that may be thrown.

SecurityViolation.jar

A JasminSecurityException is thrown when a script tries to access a resource which it is forbidden to. There are several resources for which access depends on the runtime security profile, but in order to be sure to get a JasminSecurityException, we try to do something in this example, that no script is allowed to do.

Calling System.exit() would terminate the Java virtual machine and thereby the entire Java runtime engine including all concurrently running scripts. Therefore, this call is forbidden to all scripts.

public class SecurityViolation {
    public static void main() {
        System.exit(0);
    }
}
The output of this script is: Script exception: security violation: Thread[5,4,5] tried to exit runtime engine. and the exit code is securityViolation.

CaughtSecurityViolation.jar

A JasminSecurityException is a subclass of java.lang.SecurityException which in turn is a subclass of java.lang.RuntimeException. Therefore, it does not have to be declared by a method potentially throwing it. However, it can be caugth by a script testing its access rights. Since the class JasminSecurityException is not directly accessible to scripts, the exception must be caught as a java.lang.SecurityException.

public class CaughtSecurityViolation {
    public static void main() {
        try { System.exit(0); }
        catch(java.lang.SecurityException e) {
            // Handle the situation.
        }
    }
}
This script returns regularly without any message. In this way e.g. accessibility of files can be tested without terminating the script on a JasminSecurityException.

Endless.jar

At the end of the subsection on returning arguments we have a lok on a script which never returns anything, because it contains an endless loop. This script can only be terminated by an explicit command.
public class Endless {
    public static void main() {
        while (true) ;
    }
}

10.2 Passing Arguments

After returning results, the next important act of communication is passing arguments. While just passing arguments to a script is quiet simple, we will see that checking arguments in the script may require some effort.

ReturnFirstArgument0.jar

This script returns the first passed argument as a String.
public class ReturnFirstArgument0 {
    public static String main(String[] args) {
       if (args.length >= 1) { return args[0]; }
       return "Error: no argument passed";
    }
}
An argument check required here is the lenght of the number of passed arguments. If it is 1 or greater, the first argument is returned as the script result. Otherwise, a String containing an error statement is returned. This does not seem to be the most proper way of reporting the error, because the exit code of the script is noError in each case.

ReturnFirstArgument.jar

This improved version of the script has the same behaviour, except that it throws a special exception indicating the invalid argument list.
public class ReturnFirstArgument {
    public static String main(String[] args) {
       if (args.length > 0) { return args[0]; }
       throw new JasminInvalidArgumentException(
                            "no argument passed");
    }
}
Now, the exit code of the script is invalidArgument if you invoked the script without any argument. Because the script has been terminated by an exception, the smRunResult contains the following: "Script exception: invalid arguments: no argument passed".

Timer.jar

The next script shows how to convert integer arguments from their String representation to an integer. Script Timer.jar does nothing but being inactive for a passed number of seconds.
public class Timer {
  public static String main(String[] args) {
    if (args.length > 0) {
      try {
        int seconds = Integer.parseInt(args[0]);
        try { Thread.sleep(1000 * seconds); }
        catch (InterruptedException e) { }
        return ("Script Timer slept " +
                seconds + " seconds.");
      }
      catch (NumberFormatException e) {
        throw new JasminInvalidArgumentException(
            "script requires integer argument");
      }
    }
    throw new JasminInvalidArgumentException(
                          "no argument passed");
  }
}
The first argument is converted to an integer. If convertion fails, e.g. if there are no digits in the argument, a NumberFormatException is thrown by the convertion method Integer.parseInt(). This exception is caught by the script and converted to a JasminInvalidArgumentException. So, if you run the program with argument "hello", the result will be "Script exception: invalid arguments: script requires integer argument".

Busy.jar

The antipode to Timer.jar is Busy.jar. It also runs for a certain amount of time which is given by the script argument, but instead of sleeping quietly as Timer.jar does, it creates high computing load by continuously polling the system timer. By this, it might delay the execution of concurrent scripts.
public class Busy {
  public static void main(String [] args) {
    if (args.length != 1) {
      throw new JasminInvalidArgumentException(
                          "no argument passed");
    }
    try {
      int seconds = Integer.parseInt(args[0]);
      long time = new java.util.Date().getTime();
      time += 1000 * seconds;
      while (new java.util.Date().getTime() < time);
    }
    catch (NumberFormatException e) {
      throw new JasminInvalidArgumentException(
            "script requires integer argument");
    }
  }
}

10.3 Intermediate Results

Intermediate results have already been discussed in section 3.1. There are two basic scripts using intermediate results. They are listed here for completeness of the documentation of all installed basic scripts.

Intermediate.jar

Intermediate.jar produces a passed number of intermediate results waiting 10 seconds between two of them.
public class Intermediate {
  public static String main(String [] args) {
    if (args.length > 0) {
      try {
        int loops = Integer.parseInt(args[0]);
        for (int i = loops; i > 0; i --) {
          JasminResult.deliver(i+"0 seconds left");
          try { Thread.sleep( 10000); }
          catch (InterruptedException e) { }
        }
        return ("Script Intermediate sent " +
                loops + " intermediate results");
      }
      catch (NumberFormatException e) {
        throw new JasminInvalidArgumentException(
                "script requires integer argument");
      }
    }
    throw new JasminInvalidArgumentException(
                        "no argument passed");
  }
}

Trigger.jar

The only difference between Trigger.jar and Intermediate.jar is the method JasminResult.notify() replacing JasminResult.deliver().
public class Trigger {
  public static String main(String [] args) {
    if (args.length > 0) {
      try {
        int loops = Integer.parseInt(args[0]);
        for (int i = loops; i > 0; i --) {
          JasminResult.notify(i+"0 seconds left");
          try { Thread.sleep(10000); }
          catch (InterruptedException e) { }
        }
        return ("Script Intermediate sent " +
                loops + " intermediate results");
      }
      catch (NumberFormatException e) {
        throw new JasminInvalidArgumentException(
                "script requires integer argument");
      }
    }
    throw new JasminInvalidArgumentException(
                        "no argument passed");
  }
}

ByteArrayTrigger.jar

Instead of sending a notification string as in the script before, this script sends a byte array notification. The byte array contains only one byte giving the number of notifications left to send.
public class ByteArrayTrigger {
  public static String main(String [] args) {
    if (args.length > 0) {
      try {
        int loops = Integer.parseInt(args[0]);
        for (int i = loops; i > 0; i--) {
          int byteValue = i;
          byteValue = byteValue > 255 ?
                                 255 : byteValue;
          byteValue = byteValue > 127 ?
                     byteValue - 256 : byteValue;
          JasminResult.notify(
                  new byte[] { (byte)byteValue });
          try { Thread.sleep(10000); }
          catch (InterruptedException e) { }
        }
        return ("Script Trigger sent "
                + loops + " notifications.");
      }
      catch (NumberFormatException e) {
          throw new JasminInvalidArgumentException(
                "script requires integer argument");
      }
    }
    return ("Script Trigger returned immediately.");
  }
}

10.4 Multi-Threading

Multi-threaded scripts require some special treatment by the Jasmin runtime engine. This is demonstrated by the basic scripts in this subsection.

TwoThreads.jar

This is just a simple script using two threads. The main thread creates and starts another one, which is realized by an anonymous inner class.

public class TwoThreads {
    private static String result = null;
    public static String main()
                throws InterruptedException {
        Thread otherThread = new Thread() {
            public void run() {
                result = "thread result";
            }
        };
        otherThread.start();
        try { otherThread.join(); }
        catch (InterruptedException e) { throw e; }
        return result;
    }
}
The created thread produces a result which it writes to a static variable. After it has terminated, the result is returned by the main thread. While waiting for the created thread to terminate using method join(), the main thread may be interrupted by an InterruptedException. This exception is thrown again in order to report the interruption.

Please note that when creating a jar file for a script with an anonymous inner class as the one above, not only the file TwoThreads.class has to be included in the jar file, but also the class file TwoThreads$1.class which contains the inner class.

ExceptionInThread.jar

If an exception is thrown by one of the threads, the whole script is terminated.
public class ExceptionInThread {
    public static void main() {
        Thread throwIt = new Thread() {
            public void run() {
                throw new RuntimeException();
            }
        };
        throwIt.start();
        try { throwIt.join(); }
        catch (InterruptedException e) { }
    }
}
This script produces the result: "Script exception in subthread: java.lang.RuntimeException". The message states that the exception was not thrown by the main thread.

ThreadDeathScript.jar

The only exception that terminates the throwing thread but not the whole script is ThreadDeath:
public class ThreadDeathScript {
    public static void main() {
        Thread throwIt = new Thread() {
            public void run() {
                throw new ThreadDeath();
            }
        };
        throwIt.start();
        try { throwIt.join(); }
        catch (InterruptedException e) { }
    }
}
So, this script terminates regularly with an empty result, because ThreadDeath terminated only the created thread.