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();
   }
   
}