CommonDriver.java
/*
** Module : CommonDriver.java
** Abstract : common services for a command line driver
**
** Copyright (c) 2006-2024, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- ----------------------------------Description-----------------------------------
** 001 GES 20060815 @28611 First version which centralizes common services for client and server drivers.
** 002 GES 20070111 @31774 Match interface changes. Simplified the usage as a subclass. Now the subclass
** has all knowledge of the application that must be started, there is no generic
** class load and init as before. This simplifies the downstream processing in the
** net package.
** 003 GES 20070112 @31812 Removed initial debug level (it wasn't being used downstream) and added
** application args.
** 004 GES 20080118 @36863 Output any abend info to STDERR not STDOUT. For servers, STDERR is the main log
** file and for clients STDOUT is interactive and can't be the destination. If
** STDERR on the client is redirected to file, then a record of any abend will be
** available.
** 005 GES 20090722 @43330 Flush STDERR just before exit to ensure that the output is written to disk.
** 006 GES 20090723 @43357 Avoid logging STOP conditions.
** 007 GES 20110928 Moved isCauseStop into a common class so it can be easily shared.
** 008 GES 20141228 Rework to eliminate application arguments and to extend the client parameters to
** more use cases.
** 009 EVL 20160225 Javadoc fixes to make compatible with Oracle Java 8 for Solaris 10.
** 010 GES 20210827 Expose command line state for debugging/logging purposes.
** 011 HC 20220926 Added FWD version to printDriverDetails.
** 012 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 013 GBB 20230519 Resolve logger mode after server params are parsed and server mode is determined.
** 014 RFB 20230607 Enhanced the user config so that the tilde '~/' can be the start of the configFile.
** This allows the user_client.xml to be stored in the homespace. Ref. #4938.
** 015 GBB 20230914 Adding the method for setting default exception handler.
** 016 EVL 20240119 Fix for issue when -L and -O options are considering as config file.
** 017 GBB 20240709 Method processOverrides moved to BootstrapConfig to be reused. Log improvements.
** 018 GBB 20240729 Initialize CentralLogger after SecurityManager is created.
*/
/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
** GNU Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
**
** Additional terms under GNU Affero GPL version 3 section 7:
**
** Under Section 7 of the GNU Affero GPL version 3, the following additional
** terms apply to the works covered under the License. These additional terms
** are non-permissive additional terms allowed under Section 7 of the GNU
** Affero GPL version 3 and may not be removed by you.
**
** 0. Attribution Requirement.
**
** You must preserve all legal notices or author attributions in the covered
** work or Appropriate Legal Notices displayed by works containing the covered
** work. You may not remove from the covered work any author or developer
** credit already included within the covered work.
**
** 1. No License To Use Trademarks.
**
** This license does not grant any license or rights to use the trademarks
** Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
** of Golden Code Development Corporation. You are not authorized to use the
** name Golden Code, FWD, or the names of any author or contributor, for
** publicity purposes without written authorization.
**
** 2. No Misrepresentation of Affiliation.
**
** You may not represent yourself as Golden Code Development Corporation or FWD.
**
** You may not represent yourself for publicity purposes as associated with
** Golden Code Development Corporation, FWD, or any author or contributor to
** the covered work, without written authorization.
**
** 3. No Misrepresentation of Source or Origin.
**
** You may not represent the covered work as solely your work. All modified
** versions of the covered work must be marked in a reasonable way to make it
** clear that the modified work is not originating from Golden Code Development
** Corporation or FWD. All modified versions must contain the notices of
** attribution required in this license.
*/
package com.goldencode.p2j.main;
import com.goldencode.p2j.*;
import com.goldencode.p2j.cfg.*;
import com.goldencode.p2j.net.*;
import com.goldencode.p2j.util.*;
import com.goldencode.p2j.util.logging.*;
import java.io.*;
import java.util.*;
import java.util.stream.*;
/**
* Common services used by command line drivers.
*/
abstract class CommonDriver
{
/**
* The main text of the syntax display, with each element being displayed
* on a separate line.
*/
protected String[] helptxt = null;
/** Default configuration filename. */
protected String defCfg = null;
/**
* The maximum number of configuration files (and corresponding passwords)
* to honor.
*/
protected int maxCfg = 1;
/** Driver classname for logging purposes. */
protected String driverName = null;
/** Logger. Needs to be a class field to be instantiated after the process mode is set for the logger. */
private final CentralLogger LOG = CentralLogger.get(CommonDriver.class);
/** Command line arguments stored for debugging/logging. */
private String[] args = null;
/** Tracks the number of times a given parameter was specified. */
private Map<String, Integer> instances = new HashMap<>();
/** Configuration file(s) to process. */
private String[] cfg = new String[] { null, null };
/** Passwords used to decrypt corresponding configuration files. */
private char[][] pwd = new char[][] { null, null };
/** Number of configuration files actually specified. */
private int files = 0;
/** Number of passwords actually specified. */
private int pws = 0;
/** List of overrides. */
private List<String> overrd = new LinkedList<>();
/**
* Sets a default exception handler for the JVM that registers any uncaught exception to the
* CentralLogger logs before being thrown.
*
* @param logger
* An instance of the server/client driver logger.
*/
protected static void setDefaultExceptionHandler(CentralLogger logger)
{
Thread.UncaughtExceptionHandler defaultUncaughtExceptionHandler =
Thread.getDefaultUncaughtExceptionHandler();
Thread.setDefaultUncaughtExceptionHandler(
(t, e) ->
{
logger.severe("Unhandled exception thrown:", e);
if ("main".equals(t.getName()))
{
CentralLogger.handleShutDown();
}
defaultUncaughtExceptionHandler.uncaughtException(t, e);
});
}
/**
* Print syntax on <code>STDOUT</code> for interactive clients and use logger for server processes.
* <p>
* <b>This method does NOT return! It calls <code>System.exit</code>
* with the given exit code.</b>
*
* @param err
* An optional message that is displayed first or
* <code>null</code>.
* @param rc
* The exit code to use with <code>System.exit</code>.
*/
protected void syntax(String err, int rc)
{
if (err != null)
{
LOG.severe(err);
System.out.println(err);
System.out.println();
}
if (helptxt != null)
{
for (int i = 0; i < helptxt.length; i++)
{
LOG.severe(helptxt[i]);
System.out.println(helptxt[i]);
}
}
System.exit(rc);
}
/**
* Check that the number of parameters of a given type are not specified
* more than the maximum number of times and that any required following
* argument does exist.
* <p>
* On failure of any test, this method displays the syntax message and
* exits the process (it does not return to the caller in this case).
*
* @param args
* The command line arguments.
* @param current
* The current element being processed.
* @param max
* The maximum number instances of the parameter that are
* allowed in a single command line.
* @param follow
* <code>true</code> to force testing for the existence of a
* single following argument.
* @param name
* The parameter name to use in error messages.
*/
protected void testFollowing(String[] args, int current, int max, boolean follow, String name)
{
String parm = args[current];
Integer num = instances.get(parm);
int i = 0;
if (num != null)
{
i = num.intValue();
}
// incrment the number of instances
i++;
if (i > max)
{
syntax("Too many '" + parm + "' options.", -1);
}
if (args.length <= current + 1)
{
syntax(parm + " should be followed by " + name + " argument.", -2);
}
// save off the number of instances
instances.put(parm, i);
}
/**
* Process arguments for any data or options that are specific to the
* subclass. This method is called in a loop from {@link #process}.
*
* @param args
* The command line arguments.
* @param i
* The current element being processed.
*
* @return The number of array elements that have been processed by
* this method and which MUST be skipped (no processing in
* the base class).
*/
protected int processArguments(String[] args, int i)
{
int skip = 0;
// process password
if (args[i].equals("-p"))
{
testFollowing(args, i, maxCfg, true, "password");
pwd[pws] = args[i + 1].toCharArray();
pws++;
skip = 2;
}
// process overrides (they must always have >= 1 '=' characters
// and 0 through 2 ':' characters to the left of the first '='
// character
if (args[i].indexOf('=') != -1)
{
overrd.add(args[i]);
skip = 1;
}
// file redirections for spawner
if (skip == 0)
{
if (args[i].equals("-L"))
{
skip = 1;
}
else if(args[i].equals("-O"))
{
skip = 2;
}
}
// process config file
if (skip == 0)
{
if (files == maxCfg)
syntax("Too many configuration files.", -3);
cfg[files] = args[i];
files++;
skip = 1;
}
return skip;
}
/**
* Provide interactive password prompt if requested.
*/
protected void processPassword()
{
// don't need unrelated passwords
if (files < pws)
syntax("Unrelated password specified.", -11);
// check and get the password interactively
for (int i = 0; i < pws; i++)
{
if (pwd[i].length == 1 && pwd[i][0] == '-')
{
pwd[i] = null;
}
else if (pwd[i].length == 1 && pwd[i][0] == '?')
{
try
{
pwd[i] = Utils.prompt("Enter password for '" + cfg[i] + "':");
}
catch (IOException ex)
{
System.exit(-5);
}
}
}
}
/**
* Allows a subclass to check if the given configuration matches any
* required values. Edits may be made if needed.
*
* @param bc
* The configuration to be checked.
*/
protected void checkConfig(BootstrapConfig bc)
throws ConfigurationException
{
}
/**
* Runs the primary application using the preprocessed configuration and
* target object.
*
* @param bc
* The configuration to use.
*/
abstract void start(BootstrapConfig bc)
throws Exception;
/**
* Output diagnostic details for this process.
*
* @return The details as text.
*/
public String printDriverDetails()
{
// I didn't use 'FWD Version' label here, as getFWDVersion method returns 'FWD xyz'
String fwdVersion = "Product Version " + Version.getFWDVersion();
// JVM version
// TODO: in JDK9 we can use the Version class (accessed via Runtime.version())
String version = "JVM Version " + System.getProperty("java.version");
// JVM memory and CPU stats
Runtime runtime = Runtime.getRuntime();
String mem = String.format("JVM Memory: free = %d; total = %d; max = %d;",
runtime.freeMemory(),
runtime.totalMemory(),
runtime.maxMemory());
String cpus = String.format("JVM Available CPUs: %d", runtime.availableProcessors());
// JVM properties
Properties props = System.getProperties();
Set<String> propNames = props.stringPropertyNames();
String propsList = propNames.stream()
.map((key) -> " " + key + " = '" + props.getProperty(key) + "'")
.collect(Collectors.joining(",\n", "\n {\n", "\n }"));
// JVM environment
Map<String, String> env = System.getenv();
String envList = env.keySet()
.stream()
.map((key) -> " " + key + " = '" + env.get(key) + "'")
.collect(Collectors.joining(",\n", "\n {\n", "\n }"));
// render the arguments list
String arglist = "";
if (args != null)
{
for (int i = 0; i < args.length; i++)
{
if (i > 0)
{
arglist += ",\n";
}
arglist += " '" + args[i] + "'";
}
}
// put it all together
return String.format("%s: %d arguments%s\n %s\n %s\n %s\n %s\n" +
" JVM Properties =%s\n JVM Environment =%s",
driverName,
(args != null) ? args.length : 0,
(args != null) ? " =\n [\n" + arglist + "\n ]" : "",
fwdVersion,
version,
mem,
cpus,
propsList,
envList);
}
/**
* The command line entry point.
*
* @param args
* The array of command-line parameters.
*/
public void process(String[] args)
{
// honor help request
if (args.length == 1 && args[0].equals("-?"))
{
syntax(null, 0);
}
// save these for debugging/logging
this.args = args;
// process arguments
for (int i = 0; i < args.length; i++)
{
// allow the subclass to implement its own arguments processing
// (this superclass' processArguments() call may be called by the
// subclass as needed)
int skip = processArguments(args, i);
// if the subclass processed some number of arguments, then skip
// these and iterate
if (skip > 1)
{
// we will be skipping "the current one" automatically by
// iterating so we only need to manually skip one less than
// the skip value
i += --skip;
}
}
// error checking and interactive pw prompt if requested
processPassword();
// try standard configuration file if none is given
if (cfg[0] == null)
{
File std = new File(defCfg);
if (std.exists())
{
cfg[0] = defCfg;
}
}
else
{
// See if the tilde is in use, and set to the user's homespace
// for downstream processing.
if (cfg[0].startsWith("~/"))
{
String userHome = System.getProperty("user.home");
if (userHome.length() > 0)
{
cfg[0] = userHome + cfg[0].substring(1);
}
}
}
BootstrapConfig bc = null;
try
{
// instantiate bootstrap configuration
bc = new BootstrapConfig(cfg[0], pwd[0], cfg[1], pwd[1]);
checkConfig(bc);
if (!bc.processOverrides(overrd))
{
syntax("Config overrides are not successfully processed.", -9);
}
}
catch (ConfigurationException ce)
{
LOG.severe("Instantiate bootstrap configuration exception:", ce);
System.exit(-12);
}
// run the application
try
{
start(bc);
}
catch (Throwable thr)
{
// STOP conditions don't need to be reported, they are an expected
// mechanism to terminate
if (ClientCore.isCauseStop(thr) || (thr.getCause() != null && thr.getCause().getClass().equals(ApplicationRequestedStop.class)))
{
CentralLogger.handleShutDown();
System.exit(0);
}
LOG.severe("System exits with code -13 caused by:", thr);
CentralLogger.handleShutDown();
System.exit(-13);
}
CentralLogger.handleShutDown();
}
}