/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.sun.jini.tool.envcheck;
import com.sun.jini.resource.Service;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.lang.reflect.Modifier;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.rmi.RMISecurityManager;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Properties;
import java.util.ResourceBundle;
import net.jini.config.Configuration;
import net.jini.config.ConfigurationException;
import net.jini.config.ConfigurationProvider;
import net.jini.config.EmptyConfiguration;
import net.jini.config.NoSuchEntryException;
import com.sun.jini.start.ServiceDescriptor;
import com.sun.jini.start.NonActivatableServiceDescriptor;
import com.sun.jini.start.SharedActivatableServiceDescriptor;
import com.sun.jini.start.SharedActivationGroupDescriptor;
import com.sun.jini.tool.envcheck.Reporter.Message;
/**
* Tool used to perform validity checks on the run-time environment of a client
* or service. The output of this tool is a report; command-line options
* control the verbosity and severity level at which report entries are
* generated. A simple plugin architecture is implemented; a set of plugins
* implementing a variety of checks is bundled with the tool, and support is
* provided to allow additional plugins to be supplied by the user.
* <p>
* The following items are discussed below:
* <ul>
* <li><a href="#running">Running the Tool</a>
* <li><a href="#processing">Processing Options</a>
* <li><a href="#examples">Examples</a>
* <li><a href="#plugins">Bundled Plugins</a>
* </ul>
*
* <a name="running"></a>
* <h3>Running the Tool</h3>
*
* This tool primarily validates the system properties and configuration
* files used when starting the target client or service. This is accomplished
* by providing the unmodified command line for launching the component as
* arguments to the tool. Thus, for a service designed to be run by the
* service starter having the hypothetical original command line:
* <blockquote><pre>
* java -Djava.security.policy=mypolicy
* -jar /jini/lib/start.jar mystart.config
* </pre></blockquote>
* the simplest invocation of the tool would be:
* <blockquote><pre>
* java -jar /jini/lib/envcheck.jar
* java -Djava.security.policy=mypolicy
* -jar /jini/lib/start.jar mystart.config
* </pre></blockquote>
* Note that the entire command line, including the <code>java</code> command,
* is supplied as arguments to the tool. The <code>java</code> command used to
* run the tool may be different than the <code>java</code> command invoked by
* the command line under test. The first token in the command line being
* analyzed must not begin with a '-' and must end with the string "java".
*
* <a name="processing"></a>
* <h3>Processing Options</h3>
* <p>
* <dl>
* <dt><b><code>-traces</code></b>
* <dd>The implementation of a validity check may detect success or failure by
* handling an expected exception. By default, an error message will be
* generated in these cases, but the stack trace will be inhibited. This
* option is a hint that stack traces are desired. It is the responsibility
* of the individual plugin implementation to honor this option.
* </dd>
* <p>
* <dt><b><code>-explain</code></b>
* <dd>By default, the output of a validity check will be a short message with
* enough detail to allow a knowledgeable user to interpret it; however, it
* may not be understandable to a novice user. The <code>-explain</code>
* option is a hint that may result in the generation of additional output
* describing the purpose and context of the check. An explanation is output
* the first time its associated message is output, and is not repeated. It
* is the responsibility of the individual plugin implementation to honor
* this option.
* </dd>
* <p>
* <dt><b><code>-level</code> <var>info|warning|error</var></b>
* <dd>The tool supports three severity levels for message generation.
* <p>
* <dl>
* <dt><var>info</var>
* <dd>'success' messages or other non-failure oriented configuration data
* <dt><var>warning</var>
* <dd>a condition or value has been detected that is 'legal', but which
* could result in unintended behavior. An example is the use of
* <code>localhost</code> in a codebase annotation.
* <dt><var>error</var>
* <dd>a condition has been detected that is likely due to an error
* on the command line or in a configuration file. An example is
* assigning the value of a non-existant file name to the
* <code>java.util.logging.config.file</code> system property.
* </dl>
* <p>
* This option is used to set the level at which message records are
* generated. The default value is <var>warning</var>.
* </dd>
* <p>
* <dt><b><code>-plugin</code> <var>file</var></b>
* <dd>Identifies a JAR file containing user supplied plugins that will be run
* after the standard plugins are run. All of the necessary support classes
* and resources required to support the plugins must be included in this
* file. The file must also include a resource named
* <code>META-INF/services/com.sun.jini.tool.envcheck.Plugin</code>, which
* contains the class names of the plugins to run listed one per line.
* Every class listed must implement the
* <code>com.sun.jini.tool.envcheck.Plugin</code> interface. This option
* may be supplied zero or more times.
* </dd>
* <p>
* <dt><b><code>-security</code></b>
* <dd>A plugin specific option that is recognized by one of the bundled
* plugins. Specifying this option will activate a number of JAAS and JSSE
* checks. User supplied plugins wishing to recognize this option must
* implement the <code>isPlugOption</code> method of the
* <code>Plugin</code> interface.
* </dd>
* </dl>
* <p>
* <a name="examples"></a>
* <h3>Examples</h3>
* The following example will analyze a command line used to start a user
* service. The command being analyzed defines a classpath and two system
* properties, and names a class containing a <code>main</code> method:
* <p>
* <blockquote><pre>
* java -jar /jini/lib/envcheck.jar -level info -explain -traces
* /usr/bin/java -cp mylib/myservice.jar:/jini/lib/jsk-platform.jar
* -Djava.security.policy=mypolicy
* -Djava.server.rmi.codebase=http://myhost/myservice-dl.jar
* myservice.MyServiceImpl
* </blockquote></pre>
* In this case, the tool is limited to performing validity checks on the
* classpath, policy, and codebase values identified by the system properties
* and options provided on the service command line. The <code>-level</code>,
* <code>-explain</code>, and <code>-traces</code> options supplied will result
* in the most verbose output possible from the tool.
* <p>
* The following example will analyze a command line used to start reggie using
* the service starter. The command being analyzed uses a policy and service
* starter configuration located in the working directory:
* <p>
* <blockquote><pre>
* java -jar /jini/lib/envcheck.jar -level error
* /usr/bin/java -Djava.security.policy=mystarterpolicy
* -jar /jini/lib/start.jar reggie.config
* </blockquote></pre>
* The tool can perform many more checks in this case because the
* bundled plugins include built-in knowledge about the service starter
* and its public configuration entries. The tool options used will minimize
* the output produced by the tool.
* <p>
* <a name="plugins"></a>
* <h3>Bundled Plugins</h3>
* A set of plugins are loaded automatically by the tool to perform some
* basic analysis.
* <p>
* If the command line being analyzed invokes the service starter, the tool will
* create a <code>Configuration</code> from the arguments of the command line
* being analyzed. Failure to create the <code>Configuration</code> will result
* in termination of the tool, otherwise the
* <code>com.sun.jini.start.ServiceDescriptor</code>s provided by the
* <code>Configuration</code> are examined. The following checks are done for
* each <code>ServiceDescriptor</code>:
* <ul>
* <li>Verify that the <code>getPolicy</code> method returns a reference to a
* policy file that is valid and accessible
* <li>Check whether that policy grants <code>AllPermissions</code> to all
* protection domains
* </ul>
* The following checks are done for each
* <code>NonActivatableServiceDescriptor</code> and each
* <code>SharedActivatableServiceDescriptor</code>
* <ul>
* <li>Verify that calling <code>getServerConfigArgs</code> does not return
* <code>null</code> or an empty array
* <li>Verify that a <code>Configuration</code> can be constructed from those
* args
* <li>Verify that any entry in that <code>Configuration</code> named
* <code>initialLookupGroups</code> does not have a value of
* <code>ALL_GROUPS</code>
* <li>Verify that the export codebase is defined. For each component in the
* export codebase:
* <ul>
* <li>Verify that the URL is not malformed
* <li>If it is an HTTPMD URL, verify that the necessary protocol handler
* is installed
* <li>Check that domain names are fully qualified
* <li>Warn if an md5 HTTPMD URL is being used (md5 has a security hole)
* <li>Verify that the host name in the URL can be resolved
* <li>Verify that the host name does not resolve to a loopback address
* <li>Verify that it's possible to open a connection using the URL
* </ul>
* <li>If the <code>-security</code> option was specified:
* <ul>
* <li>Verify that <code>javax.net.ssl.trustStore</code> is defined and
* that the trust store it references is accessible
* <li>Check whether <code>com.sun.jini.discovery.x500.trustStore</code>
* is defined, and if so that the trust store it references is
* accessible
* <li>Check whether <code>javax.net.ssl.keyStore</code> is defined, and
* if so that the key store it references is accessible
* <li>Verify that a login configuration is defined and that it is
* accessible and syntactically correct
* </ul>
* </ul>
* The following checks are done for each
* <code>SharedActivatableServiceDescriptor</code>:
* <ul>
* <li>Verify that any entry in that <code>Configuration</code> named
* <code>persistenceDirectory</code> refers to either an empty directory
* or a non-existant directory
* </ul>
* The following checks are done for each
* <code>SharedActivationGroupDescriptor</code>:
* <ul>
* <li>Verify that the activation system is running
* <li>Verify that the virtual machine (VM) created by that command is at
* least version 1.4
* <li>Verify that <code>jsk-policy.jar</code> is loaded from the extensions
* directory
*
* <li>Verify that <code>jsk-platform.jar</code> is in the classpath
*
*
* <li>If <code>java.util.logging.config.file</code> is defined in the
* properties returned by calling <code>getServerProperties</code> then
* verify that the file it references is accessible
* </ul>
* A subset of these checks are performed on the command line being analyzed:
* <ul>
* <li>Codebase/URL checks based on the value of
* <code>java.rmi.server.codebase</code>
* <li>Policy file checks based on the value of
* <code>java.security.policy</code>
* <li>Check for <code>jsk-policy</code> being loaded by the extension
* class loader
* <li>Check for <code>jsk-platform</code> in the classpath
* <li>Security checks if <code>-security</code> was specified
* <li>The logging config file check
* </ul>
* In all cases, check that the local host name does not resolve to the
* loopback address.
*/
public class EnvCheck {
/** the list of plugins instances to run */
private ArrayList pluginList = new ArrayList();
/** flag controlling the display of stack traces in the output */
private boolean printStackTraces = false;
/** the command line arguments of the command being analyzed */
private String[] args;
/** the <code>ServiceDescriptor</code>s obtained from the starter config */
private ServiceDescriptor[] descriptors = new ServiceDescriptor[0];
/** the localization resource bundle */
private static ResourceBundle bundle;
/** the java command on the command line being checked */
private String javaCmd;
/** the options on the command line being checked (never null) */
String[] options = new String[0];
/** the properties on the command line being checked (never null) */
Properties properties = new Properties();
/** the classpath on the command line being checked */
String classpath = null;
/** the main class on the command line being checked */
String mainClass = null;
/** the executable JAR file on the command line being checked */
String jarToRun = null;
/** the list of plugins supplied via the -plugin option */
static ArrayList pluginJarList = new ArrayList();
/** the class loader for loading plugins */
ClassLoader pluginLoader = EnvCheck.class.getClassLoader();
/** the classpath of the tool, including the plugins (updated in run) */
static String combinedClasspath = System.getProperty("java.class.path");
/**
* The entry point for the tool. The localization resource bundle is
* located, the plugins are loaded, and the checks are performed. The system
* property <code>java.protocol.handler.pkgs</code> for the tool VM is set
* to <code>net.jini.url</code> to ensure that the tool can manipulate
* HTTPMD URLs.
*
* @param args the command line arguments
*/
public static void main(String[] args) {
System.setProperty("java.protocol.handler.pkgs", "net.jini.url");
bundle = Util.getResourceBundle(EnvCheck.class);
if (args.length == 0) {
usage();
System.exit(1);
}
findPlugins(args);
new EnvCheck().run(args);
}
/**
* Output the usage message.
*/
private static void usage(){
System.err.println(Util.getString("envcheck.usage", bundle));
}
/**
* Helper to print a localized string
*
* @param key the resource key
*/
private String getString(String key) {
return Util.getString(key, bundle);
}
/**
* Helper to print a localized string
*
* @param key the resource key
* @param val the value parameter expected by the message
*/
private String getString(String key, String val) {
return Util.getString(key, bundle, val);
}
/**
* Search the command line for user supplied plugin definitions
* and place them in the internal plugin list.
*
* @param cmdline the original command line args
*/
private static void findPlugins(String[] cmdLine) {
int index = 0;
String arg;
while ((arg = cmdLine[index++]).startsWith("-")) {
if (arg.equals("-plugin")) {
String pluginName = cmdLine[index++];
if (!(new File(pluginName).exists())) {
System.err.println(Util.getString("envcheck.noplugin",
bundle,
pluginName));
System.exit(1);
}
combinedClasspath += File.pathSeparator + pluginName;
try {
pluginJarList.add(new URL("file:" + pluginName));
} catch (MalformedURLException e) { // should never happen
e.printStackTrace();
System.exit(1);
}
} else if (arg.equals("-level")) {
index++;
}
}
}
/**
* Parse the command line, identifying options for the tool and parsing
* the VM, system properties, options, JAR/main class and arguments
* for the command line being tested. The <code>-plugin</code> option
* is ignored by this parser since they must have been processed
* earlier.
*
* @param cmdLine the original command line arguments
*/
private void parseArgs(String[] cmdLine) {
int index = 0;
try {
while (true) {
String arg = cmdLine[index++];
if (arg.equals("-traces")) {
printStackTraces = true;
Reporter.setPrintTraces(true);
} else if (arg.equals("-explain")) {
Reporter.setExplanation(true);
} else if (arg.equals("-level")) {
String val = cmdLine[index++];
if (val.equals("info")) {
Reporter.setLevel(Reporter.INFO);
} else if (val.equals("warning")) {
Reporter.setLevel(Reporter.WARNING);
} else if (val.equals("error")) {
Reporter.setLevel(Reporter.ERROR);
} else {
System.err.println(getString("envcheck.badlevel"));
usage();
System.exit(1);
}
} else if (arg.equals("-plugin")) {
index++; // already processed the plugin
} else if (isPluginOption(arg)) { //no additional work to do
} else if (arg.startsWith("-")) {
System.err.println(getString("envcheck.illegalopt", arg));
usage();
System.exit(1);
} else if (arg.endsWith("java")) {
javaCmd = arg;
break;
} else {
System.err.println(getString("envcheck.nojavacmd"));
usage();
System.exit(1);
}
}
} catch (ArrayIndexOutOfBoundsException e) {
System.err.println(getString("envcheck.nocmdargs"));
usage();
System.exit(1);
}
try {
ArrayList optList = new ArrayList();
while (true) {
String opt = cmdLine[index++];
if (!opt.startsWith("-")) {
mainClass = opt;
break;
} else if (opt.startsWith("-D")) {
int valueIndex = opt.indexOf("=");
String key = opt.substring(2, valueIndex);
String value = opt.substring(valueIndex + 1);
properties.put(key, value);
} else if (opt.equals("-cp") || opt.equals("-classpath")) {
classpath = cmdLine[index++];
} else if (opt.equals("-jar")) {
jarToRun = cmdLine[index++];
break;
} else {
optList.add(opt);
}
}
options =
(String[]) optList.toArray(new String[optList.size()]);
} catch (ArrayIndexOutOfBoundsException e) {
System.err.println(getString("noexecutable"));
usage();
System.exit(1);
}
ArrayList argList = new ArrayList();
while (index < cmdLine.length) {
argList.add(cmdLine[index++]);
}
args = (String[]) argList.toArray(new String[argList.size()]);
}
/**
* Return the <code>ServiceDescriptor</code>s contained in the service
* starter configuration. If the command being analyzed does not invoke the
* service starter, a zero-length array will be returned.
*
* @return the descriptors in the starter configuration or an empty array
*/
public ServiceDescriptor[] getDescriptors() {
return descriptors;
}
/**
* Return the <code>SharedActivationGroupDescriptor</code> contained in the
* service starter configuration. Returns <code>null</code> if there is no
* such descriptor, or if the command being analyzed does not invoke the
* service starter.
*
* @return the <code>SharedActivationGroupDescriptor</code> or
* <code>null</code>
*/
public SharedActivationGroupDescriptor getGroupDescriptor() {
for (int i = 0; i < descriptors.length; i++) {
if (descriptors[i] instanceof SharedActivationGroupDescriptor) {
return (SharedActivationGroupDescriptor) descriptors[i];
}
}
return null;
}
/**
* Perform the runtime checks. If any user plugins were supplied, construct
* a class loader capable of loading them and modify
* <code>combinedClasspath</code> to make the classes available to subtasks.
* Load the plugins and parse the command line. The plugins must be
* available to the parser so that plugin specific options can be checked
* for. Load the service starter configuration if the service starter is
* being invoked. Execute all of the plugin classes in the order loaded.
*
* @param cmdLine the original command line arguments
*/
private void run(String[] cmdLine) {
if (pluginJarList.size() > 0) {
URL[] urls =
(URL[]) pluginJarList.toArray(new URL[pluginJarList.size()]);
pluginLoader = new URLClassLoader(urls, pluginLoader);
}
loadPlugins();
parseArgs(cmdLine);
if (jarToRun == null && classpath == null) {
System.err.println(getString("envcheck.missingclasspath"));
System.exit(1);
}
loadConfiguration();
Iterator plugins = pluginList.iterator();
while (plugins.hasNext()) {
Plugin plugin = (Plugin) plugins.next();
try {
plugin.run(this);
} catch (SecurityException e) { // limp on if permission check fails
e.printStackTrace();
}
}
int warningCount = Reporter.getWarningCount();
int errorCount = Reporter.getErrorCount();
if ((warningCount + errorCount) > 0) {
System.out.println();
System.out.println();
System.out.println(getString("envcheck.summaryseparator"));
}
if (warningCount > 0) {
System.out.println(getString("envcheck.warningheader")
+ warningCount);
}
if (errorCount > 0) {
System.out.println(getString("envcheck.errorheader") + errorCount);
}
}
/**
* Check whether <code>arg</code> is a plugin specific option. The
* <code>isPluginOption</code> method of every plugin is called in
* case an option is shared among multiple plugins.
*
* @param arg the argument to check
* @return <code>true</code> if any plugin's <code>isPluginOption</code>
* method returns <code>true</code>
*/
private boolean isPluginOption(String arg) {
boolean gotOne = false;
Iterator plugins = pluginList.iterator();
while (plugins.hasNext()) {
Plugin plugin = (Plugin) plugins.next();
if (plugin.isPluginOption(arg)) {
gotOne = true;
}
}
return gotOne;
}
/**
* Instantiate the service starter configuration if the command line being
* analyzed runs the service starter. This is assumed to be true if the
* command line specified an executable JAR file named <code>start.jar</code> and
* if that file resides in the same directory as a file named
* <code>jsk-platform.jar</code>. Extract and save all of the service
* descriptors and service destructors from the configuration. If a
* <code>ConfigurationException</code> is thrown the tool will exit. If
* there are no <code>ServiceDescriptor</code> or
* <code>ServiceDestructor</code> entries supplied by the configuration a
* error is generated and a normal return is done. The instantiation of
* the configuration is done in a child process using the VM, properties,
* options and arguments supplied in the command line under test.
*/
private void loadConfiguration() {
if (jarToRun != null) {
if (!jarToRun.endsWith("start.jar")) {
return;
}
//XXX do existence check in parser
File starterFile = new File(jarToRun);
File jskLibDir = starterFile.getParentFile();
File jskplatform = new File(jskLibDir, "jsk-platform.jar");
if (!jskplatform.exists()) {
return; // presumably a user file coincidentally named start.jar
}
} else {
if (!mainClass.equals("com.sun.jini.start.ServiceStarter")) {
return;
}
}
//XXX need to check validity of javaCmd and arg list length
Object lobj =
launch("com.sun.jini.tool.envcheck.EnvCheck$GetDescriptors", args);
if (lobj instanceof ServiceDescriptor[]) {
descriptors = (ServiceDescriptor[]) lobj;
if (descriptors.length == 0) {
Reporter.print(new Message(Reporter.ERROR,
getString("envcheck.emptyconfig"),
null));
}
} else if (lobj instanceof ConfigurationException) {
System.err.println(getString("envcheck.ssdescfailed"));
((Throwable) lobj).printStackTrace();
System.exit(1);
} else {
System.err.println(getString("envcheck.subtaskex", lobj.toString()));
((Throwable) lobj).printStackTrace();
System.exit(1);
}
}
/**
* Load the plugin classes. All of the resources named
* <code>META-INF/services/com.sun.jini.tool.envcheck.Plugin</code> are
* read; each such resource is expected to contain a list of plugin class
* names, one per line (white space is trimmed). The corresponding classes
* are loaded and saved. Any other exceptions thrown while processing
* the plugin JAR files will cause the tool to exit.
*/
private void loadPlugins() {
Package pkg = getClass().getPackage();
String pkgName = "";
if (pkg != null) {
pkgName = pkg.getName();
}
pkgName = pkgName.replace('.', '/');
if (pkgName.length() > 0) {
pkgName += "/";
}
Iterator plugins =
Service.providers(com.sun.jini.tool.envcheck.Plugin.class,
pluginLoader);
while (plugins.hasNext()) {
pluginList.add(plugins.next());
}
}
/**
* Get the command line arguments of the command being analyzed.
*
* @return the args
*/
// unused, but supplied to support user developed plugins
public String[] getArgs() {
return args;
}
/**
* Return the flag indicating whether to output stack traces that
* result from a check.
*/
public boolean printStacks() {
return printStackTraces;
}
/**
* Launch a child VM using the <code>java</code> command, properties, and
* options supplied on the command line being analyzed. If an executable JAR
* file was specified, the classpath of the child VM consists of the JAR
* file name augmented with the classpath of the tool and plugins. If a main
* class was specified, the classpath of the child VM consists of the
* <code>-cp/-classpath</code> option value of the command line being
* analyzed augmented with the classpath of the tool and plugins.
*
* @param task the class name of the task to launch, which must implement
* the <code>SubVMTask</code> interface
* @param args the arguments to pass to the main method of the task
* @return the result or exception returned by the subtask supplied as a
* serialized object written on the subtask's
* <code>System.out</code> stream.
*/
public Object launch(String task, String[] args) {
String cp;
if (jarToRun != null) {
cp = jarToRun + File.pathSeparator;
} else {
cp = classpath + File.pathSeparator;
}
cp += combinedClasspath;
String[] opts = new String[options.length + 2];
opts[0] = "-cp";
opts[1] = cp;
System.arraycopy(options, 0, opts, 2, options.length);
if (args == null) {
args = new String[0];
}
String[] subvmArgs = new String[args.length + 1];
subvmArgs[0] = task;
System.arraycopy(args, 0, subvmArgs, 1, args.length);
return launch(javaCmd, properties, opts, subvmArgs);
}
/**
* Launch a child VM using the <code>java</code> command, properties, and
* options supplied on the command line. If an executable JAR file was
* specified, the classpath of the child VM consists of the JAR file name
* augmented with the classpath of the tool and plugins. If a main class was
* specified, the classpath of the child VM consists of the
* <code>-cp/-classpath</code> option value of the command line being
* analyzed augmented with the classpath of the tool and plugins.
*
* @param task the class name of the task to launch, which must implement
* the <code>SubVMTask</code> interface
* @return the result or exception returned by the subtask supplied as a
* serialized object written on the subtask's
* <code>System.out</code> stream.
*/
public Object launch(String task) {
return launch(task, null);
}
/**
* Return a property value that was specified on the command line being
* analyzed. Only properties explicitly defined on the command line will
* resolve to a value.
*
* @param key the name of the property
* @return the property value, or <code>null</code> if undefined
*/
public String getProperty(String key) {
return properties.getProperty(key);
}
/**
* Return a copy of the properties that were specified on the
* command line being analyzed. The caller may modify the returned
* properties object.
*
* @return the properties, which may be empty
*/
public Properties getProperties() {
return (Properties) properties.clone();
}
/**
* Launch a subtask VM using the <code>java</code> command given by
* <code>javaCmd</code>. If <code>javaCmd</code> is <code>null</code>, the
* command to run is derived from the value of the <code>java.home</code>
* property of the tool VM. The first value in <code>args</code> must name
* a class that implements the <code>SubVMTask</code> interface. The
* <code>props</code> and <code>opts</code> arrays must contain fully
* formatted command line values for properties and options
* (i.e. "-Dfoo=bar" or "-opt"). <code>opts</code> must include a
* <code>-cp</code> or <code>-classpath</code> option and its value must
* completely specify the classpath required to run the subtask.
*
* @param javaCmd the <code>java</code> command to execute, or
* <code>null</code> to create another instance of the
* tool VM
* @param props properties to define, which may be <code>null</code> or
* empty
* @param opts options to define, which must include a classpath
* definition
* @param args arguments to pass to the child VM
* @return the result or exception returned by the subtask supplied
* as a serialized object written on the subtask's
* <code>System.out</code> stream.
*
* @throws IllegalArgumentException if <code>opts</code> does not
* include a classpath definition, or if <code>args[0]</code>
* does not contain the name of a class that implements
* <code>SubVMTask</code>
*/
public Object launch(String javaCmd,
Properties props,
String[] opts,
String[] args)
{
if (args == null || args.length == 0) {
throw new IllegalArgumentException("No class name in args[0]");
}
// make sure subtask is valid to avoid obscure failures at exec time
String taskName = args[0];
try {
Class taskClass = Class.forName(taskName, true, pluginLoader);
if (!SubVMTask.class.isAssignableFrom(taskClass)) {
throw new IllegalArgumentException(taskName
+ " does not implement "
+ "SubVMTask");
}
// make sure inner classes are static
if (taskClass.getDeclaringClass() != null) {
if ((taskClass.getModifiers() & Modifier.STATIC) == 0) {
throw new IllegalArgumentException(taskName
+ " must be a static class");
}
}
} catch (ClassNotFoundException e) {
throw new IllegalArgumentException("Class not found: " + taskName);
}
if (javaCmd == null) {
javaCmd = System.getProperty("java.home");
javaCmd += File.separator + "bin" + File.separator + "java";
}
ArrayList cmdList = new ArrayList();
cmdList.add(javaCmd);
boolean gotClasspath = false;
if (opts != null) {
for (int i = 0; i < opts.length; i++) {
if (opts[i].equals("-cp") || opts[i].equals("-classpath"))
if (i + 1 == opts.length) {
throw new IllegalArgumentException("classpath option "
+ "does not have "
+ "a value");
}
gotClasspath = true;
cmdList.add(opts[i]);
}
}
if (!gotClasspath) {
throw new IllegalArgumentException("no classpath defined");
}
if (props != null) {
Enumeration en = props.propertyNames();
while (en.hasMoreElements()) {
String key = (String) en.nextElement();
String value = props.getProperty(key);
cmdList.add("-D" + key + "=" + value);
}
}
cmdList.add("com.sun.jini.tool.envcheck.SubVM");
for (int i = 0; i < args.length; i++) {
cmdList.add(args[i]);
}
try {
String[] argList =
(String[]) cmdList.toArray(new String[cmdList.size()]);
// for (int i = 0; i < argList.length; i++) {
// System.out.print(argList[i] + " ");
// }
// System.out.println();
Process p = Runtime.getRuntime().exec(argList);
Pipe pipe = new Pipe("errorPipe",
p.getErrorStream(),
System.err,
"Child VM: ");
ObjectInputStream s = new ObjectInputStream(p.getInputStream());
Object o = s.readObject();
pipe.waitTillEmpty(5000);
return o;
} catch (Exception e) {
return e;
}
}
/**
* Launch a subtask using the environment defined by the given service
* descriptors. Calling this method is equivalent to calling
* <code>launch(d, g, taskName, null)</code>.
*
* @param d the services descriptor, which may be <code>null</code>
* @param gd the group descriptor, which may be <code>null</code
* @param taskName the name of the subtask to run
* @return the result or exception returned by the subtask supplied as a
* serialized object written on the subtask's
* <code>System.out</code> stream.
*/
public Object launch(NonActivatableServiceDescriptor d,
SharedActivationGroupDescriptor gd,
String taskName)
{
return launch(d, gd, taskName, null);
}
/**
* Launch a subtask using the environment defined by the given service
* descriptors.
* <p>
* If <code>d</code> and <code>gd</code> are both <code>null</code>, then
* calling this method is equivalent to calling
* <code>launch(taskName, args)</code>.
* <p>
* If <code>d</code> is <code>null</code> and <code>gd</code> is
* non-<code>null</code> then the properties are taken from
* <code>gd.getServerProperties()</code> and the
* <code>java.security.policy</code> property is added or replaced with the
* value of <code>gd.getPolicy()</code>. The options are taken from
* <code>gd.getServerOptions()</code>, but any <code>-cp/-classpath</code>
* option is discarded; a <code>-cp</code> option is added that is the value
* of <code>gd.getClasspath()</code> augmented with the classpath of the
* tool and plugins. If <code>gd.getServerCommand()</code> is
* non-<code>null</code>, its value is used to invoke the child VM;
* otherwise the <code>java</code> command of the command line being
* analyzed is used. The arguments passed to the child VM consist of an
* array whose first element is <code>taskName</code> and whose remaining
* elements are taken from <code>args</code>.
* <p>
* If <code>d</code> is not <code>null</code>, but <code>gd</code> is
* <code>null</code>, then if <code>d</code> is an instance of
* <code>SharedActivatableServiceDescriptor</code> an
* <code>IllegalArgumentException</code> is thrown. Otherwise the properties
* and options are taken from the command line being analyzed. The
* <code>java.security.policy</code> property is added or replaced using the
* value of <code>d.getPolicy()</code>. The <code>-cp/-classpath</code>
* option is replaced with the value of <code>d.getImportCodebase()</code>
* augmented with the classpath of the tool and plugins. The arguments
* passed to the child VM consist of <code>taskName</code> followed by
* <code>args</code> if <code>args</code> is non-<code>null</code>, or
* followed by <code>d.getServerConfigArgs()</code> otherwise. The VM is
* invoked using the <code>java</code> command of the command line being
* analyzed.
* <p>
* if <code>d</code> and <code>gd</code> are both non-<code>null</code> then
* if <code>d</code> is an instance of
* <code>SharedActivatableServiceDescriptor</code> then the properties,
* options, and <code>java</code> command are taken from
* <code>gd.getServerProperties()</code>,
* <code>gd.getServerOptions()</code>, and
* <code>gd.getServerCommand()</code>; however, if the value of
* <code>gd.getServerCommand()</code> is <code>null</code>, the
* <code>java</code> command is taken from the command line being
* analysed. If <code>d</code> is not an instance of
* <code>SharedActivatableServiceDescriptor</code> then the properties,
* options, and <code>java</code> command are taken from the command line
* being analyzed. In all cases the <code>java.security.policy</code>
* property is added or replaced using the value of
* <code>d.getPolicy()</code>. The <code>-cp/-classpath</code> option is
* added or replaced with the value of <code>d.getImportCodebase()</code>
* augmented with the value of the classpath of the tool and plugins. The
* arguments passed to the child VM consist of <code>taskName</code>
* followed by <code>args</code> if <code>args</code> is
* non-<code>null</code>, or followed by
* <code>d.getServerConfigArgs()</code> otherwise.
* <p>
*
* @param d the service descriptor, which may be <code>null</code>
* @param gd the group descriptor, which may be <code>null</code
* @param taskName the name of the subtask to run
* @param args the arguments to pass to the child VM, which may be
* <code>null</code>
* @return the result or exception returned by the subtask supplied as a
* serialized object written on the subtask's
* <code>System.out</code> stream.
*/
public Object launch(NonActivatableServiceDescriptor d,
SharedActivationGroupDescriptor gd,
String taskName,
String[] args)
{
if (d == null && gd == null) {
return launch(taskName, args);
}
//build taskArgs array
if (args == null) {
if (d != null) {
args = d.getServerConfigArgs();
if (args == null || args.length == 0) {
return new IllegalArgumentException("No configuration args "
+ "in descriptor");
}
} else {
args = new String[0];
}
}
String[] taskArgs = new String[args.length + 1];
taskArgs[0] = taskName;
System.arraycopy(args, 0, taskArgs, 1, args.length);
if (d == null && gd != null) {
Properties props = gd.getServerProperties();
props.put("java.security.policy", gd.getPolicy());
ArrayList l = new ArrayList();
String[] opts = gd.getServerOptions();
if (opts != null) {
for (int i = 0; i < opts.length; i++ ) {
if (opts[i].equals("-cp")) {
i++; // bump past value
} else {
l.add(opts[i]);
}
}
}
l.add("-cp");
l.add(gd.getClasspath() + File.pathSeparator + combinedClasspath);
opts = (String[]) l.toArray(new String[l.size()]);
String cmd = gd.getServerCommand();
if (cmd == null) {
cmd = javaCmd;
}
return launch(cmd, props, opts, taskArgs);
} else if (d != null && gd == null) {
if (d instanceof SharedActivatableServiceDescriptor) {
throw new IllegalArgumentException("no group for service");
}
Properties props = getProperties();
props.put("java.security.policy", d.getPolicy());
ArrayList l = new ArrayList();
for (int i = 0; i < options.length; i++ ) {
if (options[i].equals("-cp")
|| options[i].equals("-classpath")) {
i++; // bump past value
} else {
l.add(options[i]);
}
}
l.add("-cp");
l.add(d.getImportCodebase()
+ File.pathSeparator
+ combinedClasspath);
String[] opts = (String[]) l.toArray(new String[l.size()]);
return launch(javaCmd, props, opts, taskArgs);
} else if (d != null && gd != null) {
Properties props = getProperties();
String[] opts = options;
String vm = null;
if (d instanceof SharedActivatableServiceDescriptor) {
props = gd.getServerProperties();
if (props == null) {
props = new Properties();
}
opts = gd.getServerOptions();
vm = gd.getServerCommand();
}
if (vm == null) {
vm = javaCmd;
}
props.put("java.security.policy", d.getPolicy());
ArrayList l = new ArrayList();
if (opts != null) {
for (int i = 0; i < opts.length; i++ ) {
if (opts[i].equals("-cp")
|| opts[i].equals("-classpath")) {
i++; // bump past value
} else {
l.add(opts[i]);
}
}
}
l.add("-cp");
l.add(d.getImportCodebase()
+ File.pathSeparator
+ combinedClasspath);
opts = (String[]) l.toArray(new String[l.size()]);
return launch(vm, props, opts, taskArgs);
} else {
throw new IllegalStateException("Should never get here");
}
}
/**
* Return the <code>java</code> command for the command line being analyzed.
*
* @return the <code>java</code> command
*/
public String getJavaCmd() {
return javaCmd;
}
/**
* Check for the existence of a file identified by a property
* supplied on the command line being analyzed.
*
* @param prop the name of the property
* @param desc a brief description of the file
* @return the property value, or <code>null</code> if undefined
*/
public String checkFile(String prop, String desc) {
String name = getProperty(prop);
if (name == null) {
return getString("util.undef", desc);
}
return Util.checkFileName(name, desc);
}
/**
* Return the name of the executable JAR file supplied on the
* command line being analyzed.
*
* @return the JAR file name, or <code>null</code> if the command line
* did not specify one.
*/
public String getJarToRun() {
return jarToRun;
}
/**
* Get the classpath provided by the command line being analyzed. If
* <code>getJarToRun()</code> returns a non-<code>null</code> value then
* its value is returned. Otherwise the value supplied by the command
* line <code>-cp/-classpath</code> option is returned.
*
* @return the classpath supplied on the command line being analyzed.
*/
public String getClasspath() {
if (jarToRun != null) {
return jarToRun;
}
return classpath;
}
/**
* A subtask which returns the service descriptors and service
* destructors supplied by the service starter configuration constructed
* from <code>args</code>.
*/
static class GetDescriptors implements SubVMTask {
public Object run(String[] args) {
Configuration starterConfig = null;
try {
starterConfig = ConfigurationProvider.getInstance(args);
} catch (ConfigurationException e) {
return e;
}
try {
ServiceDescriptor[] d1 = (ServiceDescriptor[])
starterConfig.getEntry("com.sun.jini.start",
"serviceDescriptors",
ServiceDescriptor[].class,
new ServiceDescriptor[0]);
ServiceDescriptor[] d2 = (ServiceDescriptor[])
starterConfig.getEntry("com.sun.jini.start",
"serviceDestructors",
ServiceDescriptor[].class,
new ServiceDescriptor[0]);
ServiceDescriptor[] descriptors =
new ServiceDescriptor[d1.length + d2.length];
System.arraycopy(d1, 0, descriptors, 0, d1.length);
System.arraycopy(d2, 0, descriptors, d1.length, d2.length);
return descriptors;
} catch (ConfigurationException e) {
return e;
}
}
}
/**
* An I/O redirection pipe. A daemon thread copies data from an input
* stream to an output stream. An optional annotation may be provided
* which will prefix each line of the copied data with a label which
* can be used to identify the source.
*/
private class Pipe implements Runnable {
/** the line separator character */
private final static byte SEPARATOR = (byte) '\n';
/** output line buffer */
private ByteArrayOutputStream bufOut = new ByteArrayOutputStream();
/** the input stream */
private InputStream in;
/** the output PrintStream */
private PrintStream stream;
/** the output stream annotation */
private String annotation;
/** the thread to process the data */
private Thread outThread;
/**
* Create a new Pipe object and start the thread to handle the data.
*
* @param name the name to assign to the thread
* @param in input stream from which pipe input flows
* @param stream the stream to which output will be sent
* @param a the annotation for prepending text to logged lines
*/
Pipe(String name,
InputStream in,
PrintStream stream,
String a)
{
this.in = in;
this.stream = stream;
this.annotation = a;
outThread = new Thread(this, name);
outThread.setDaemon(true);
outThread.start();
}
/**
* Wait until the run method terminates due to reading EOF on input
*
* @param timeout max time to wait for the thread to terminate
*/
void waitTillEmpty(int timeout) {
try {
outThread.join(timeout);
} catch (InterruptedException ignore) {
}
}
/**
* Read and write data until EOF is detected. Flush any remaining data to
* the output steam and return, terminating the thread.
*/
public void run() {
byte[] buf = new byte[256];
int count;
try {
/* read bytes till there are no more. */
while ((count = in.read(buf)) != -1) {
write(buf, count);
}
/* If annotating, flush internal buffer... may not have ended on a
* line separator, we also need a last annotation if
* something was left.
*/
String lastInBuffer = bufOut.toString();
bufOut.reset();
if (lastInBuffer.length() > 0) {
if (annotation != null) {
stream.print(annotation);
}
stream.println(lastInBuffer);
}
} catch (IOException e) {
}
}
/**
* Write each byte in the give byte array.
*
* @param b the array of input bytes
* @param len the number data bytes in the array
*/
private void write(byte b[], int len) throws IOException {
if (len < 0) {
throw new ArrayIndexOutOfBoundsException(len);
}
for (int i = 0; i < len; i++) {
write(b[i]);
}
}
/**
* If not annotated, write the byte to the stream immediately. Otherwise,
* write a byte of data to the internal buffer. If we have matched a line
* separator, then the currently buffered line is sent to the output writer
* with a prepended annotation string.
*/
private void write(byte b) throws IOException {
bufOut.write(b);
// write buffered line if line separator detected
if (b == SEPARATOR) {
String s = bufOut.toString();
bufOut.reset();
if (annotation != null) {
stream.print(annotation);
}
stream.print(s);
}
}
}
}