# BrokerCore.java

    /*
    ** Module   : BrokerCore.java
    ** Abstract : client side broker implementation.
    **
    ** Copyright (c) 2014-2025, Golden Code Development Corporation.

    **
    ** -#- -I- --Date-- ---------------------------------Description----------------------------------
    ** 001 MAG 20140707 Implements remote launcher (broker).
    ** 002 GES 20141228 Modified start() signature to match changes elsewhere.
    ** 003 EVL 20160223 Javadoc fixes to make compatible with Oracle Java 8 for Solaris 10.
    ** 004 SBI 20171023 Added getDedicatedHost() and new client parameters: remote:java:args,
    **                  remote:java:classpath, remote:agent:host, remote:agent:user and
    **                  remote:agent:dedicatedMode.
    ** 005 SBI 20230411 Added severe warning if "localhost" is given instead of client:web:host that
    **                  should be network host or IP address accessible from the server network.
    ** 006 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
    ** 007 GBB 20240709 Hard-coded config names replaced by ConfigItem constants.
    ** 008 GBB 20240729 Initialize CentralLogger after SecurityManager is created.
    ** 009 AP  20250303 Added timeout for spawner launching.
    */
    /*
    ** 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 java.io.*;
    import java.lang.management.*;
    import java.net.*;
    import java.util.*;
    import java.util.concurrent.*;
    import java.util.logging.*;

    import com.goldencode.p2j.cfg.*;
    import com.goldencode.p2j.net.*;
    import com.goldencode.p2j.security.SecurityManager;
    import com.goldencode.p2j.util.*;
    import com.goldencode.p2j.util.logging.*;

    /**
     * Remote launcher client side implementation.
     */
    public class BrokerCore
    {
       /** Host placeholder */
       public static final String PARAM_HOST = "${remote.host}";

       /** Secure port placeholder */
       public static final String PARAM_PORT = "${remote.secure.port}";

       /** Spawner placeholder */
       public static final String PARAM_SPAWNER = "${remote.spawner}";

       /** JVM arguments placeholder */
       public static final String PARAM_JVM = "${remote.jvm.args}";

       /** CLASSPATH placeholder */
       public static final String PARAM_CP = "${remote.broker.classpath}";

       /** Broker P2J server address net:server:host */
       public static final String BROKER_HOST = "BROKER_HOST";

       /** Broker P2J secured port net:server:secyred_port */
       public static final String BROKER_PORT = "BROKER_PORT";

       /** Broker runtime JVM arguments */
       public static final String BROKER_JVMARGS = "BROKER_JVMARGS";

       /** Broker runtime JVM arguments */
       public static final String BROKER_JVMARGS_SEPARATOR = "^";

       /** Broker runtime class path */
       public static final String BROKER_CLASSPATH = "BROKER_CLASSPATH";

       /** Logger. */
       private static final CentralLogger LOG = CentralLogger.get(BrokerCore.class.getName());

       /** JVM arguments and CLASSPATH */
       private static List<String> jvmArgs = new LinkedList<>();

       /** Export BrokerRemote API */
       private static Class<?>[] ifaces = new Class[] { BrokerClientServices.class };

       /** Session latch used to notify when terminate */
       private static CountDownLatch sessionLatch = new CountDownLatch(1);

       /** Assigned UUID */
       private static String uuid;

       /** P2J Server address */
       private static String host;

       /** P2J Server secure port */
       private static String port;

       /** JVM args */
       private static String jvmargs;

       /** classpath */
       private static String classpath;

       /** Spawner */
       private static String spawner;

       /** Launch timeout for the spawner, expressed in seconds */
       private static int spawnerLaunchTimeout;

       /** Dedicated web host */
       private static String dedicatedHost;

       /** Associated user */
       private static String associatedUser;

       /** Dedicated mode */
       private static boolean dedicatedMode;

       /** Number of retry */
       private static int count = 10;

       /** Number of seconds to wait until retry */
       private static int seconds = 10;

       /**
        * Start the broker. A number of retry attempts will be made in order to connect to the
        * P2J server. Also whenever the connection with server is lost the broker will try
        * to automatically reconnect to P2J server.
        * <p>
        * The number of retries and the interval between retries could be specified in
        * configuration (client.xml) file or could be override in command line.
        * If missing the default values are used
        * Example:
        *    remote:retry:count=10
        *    remote:retry:minutes=1
        * <p>
        * Broker name parameter MUST be override in command line because this parameter
        * is used to identify broker clients and to set the name of the logger file.
        * Example:
        *    remote:broker:name=&lt;broker name&gt;
        *
        * @param   cfg
        *          Bootstrap configuration.
        */
       public static void start(final BootstrapConfig cfg)
       {
          try
          {
             // get JVM arguments and classpath
             initialize(cfg);

             @SuppressWarnings("unused")
             final SecurityManager secMgr = SecurityManager.createInstance(cfg);
             final SessionManager sessMgr = SessionManagerFactory.createLeafNode(cfg);

             CentralLogger.setBootstrapConfig(cfg); // after SecurityManager is created

             while (count-- > 0)
             {
                // retry log
                LOG.logp(Level.INFO,
                         "BrokerCore.start()",
                         "",
                         CentralLogger.generate("Connecting (%d) ...", count));

                connect(sessMgr, cfg);
                asleep(seconds);
             }

             LOG.logp(Level.INFO,
                      "BrokerCore.start()",
                      "",
                      "Exit");
          }
          catch (Throwable t)
          {
             LOG.logp(Level.SEVERE,
                      "BrokerCore.start()",
                      "",
                      "Configuration Exception.",
                      t);
          }
       }

       /**
        * This is an exported service used to process remote launch.
        *
        * @param   args
        *          Remote launch parameters.
        *
        * @return  A <p>BrokerSpawnResult</p> object containing the remote launch result.
        */
       public static synchronized BrokerSpawnResult spawn(BrokerSpawnParameters args)
       {
          // The ProcessBuilder instance
          final ProcessBuilder pb = new ProcessBuilder();

          // Remote spawn response
          final BrokerSpawnResult result = new BrokerSpawnResult();

          // Process
          Process shell = null;

          // Set environment variables.
          if (args.getEnvironment() == null)
          {
             args.setEnvironment(new HashMap<String, String>());
          }

          // Exports as environment variables
          args.getEnvironment().put(BROKER_HOST, host);
          args.getEnvironment().put(BROKER_PORT, port);
          args.getEnvironment().put(BROKER_JVMARGS, jvmargs);
          args.getEnvironment().put(BROKER_CLASSPATH, classpath);

          pb.environment().putAll(args.getEnvironment());

          // Set directory
          File spawnerDir = new File(spawner);
          pb.directory(spawnerDir.getAbsoluteFile().getParentFile());

          // Command line arguments
          List<String> command = new LinkedList<String>();

          // Replace place holders
          for (String cmd : args.getCommand())
          {
             // replace placeholders
             if (cmd.indexOf(BrokerCore.PARAM_HOST) > -1)
             {
                command.add(cmd.replace(BrokerCore.PARAM_HOST, host));
             }
             else if (cmd.indexOf(BrokerCore.PARAM_PORT) > -1)
             {
                command.add(cmd.replace(BrokerCore.PARAM_PORT, port));
             }
             else if (BrokerCore.PARAM_SPAWNER.equals(cmd))
             {
                command.add(spawner);
             }
             else if (BrokerCore.PARAM_JVM.equals(cmd))
             {
                command.addAll(jvmArgs);
             }
             else if (BrokerCore.PARAM_CP.equals(cmd))
             {
                command.add("-classpath");
                command.add(classpath);
             }
             else
             {
                command.add(cmd);
             }
          }

          // add client:web:host
          command.add("client:web:host=" + dedicatedHost);
          command.add("client:web:dedicatedMode=" + String.valueOf(dedicatedMode));

          LOG.logp(Level.INFO,
                   "BrokerCore.spawn()",
                   "",
                   CentralLogger.generate("Command line arguments: %s", command.toString()));

          pb.command(command);
          pb.redirectError(ProcessBuilder.Redirect.INHERIT);

          try
          {
             shell = pb.start();

             if (args.isPasswordAuthentication())
             {
                // write password
                OutputStreamWriter os = new OutputStreamWriter(shell.getOutputStream());
                os.write(args.getPassword());
                os.close();
             }

             if (!shell.waitFor(spawnerLaunchTimeout, TimeUnit.SECONDS))
             {
                LOG.log(Level.SEVERE, "Spawner launch procedure exceeded timeout of %d seconds!",
                        spawnerLaunchTimeout);
                result.setExitCode(SpawnError.EXCEEDED_TIMEOUT.getCode());
             }
             else
             {
                result.setExitCode(shell.exitValue());
             }

             LOG.logp(Level.INFO,
                      "BrokerCore.spawn()",
                      "",
                      CentralLogger.generate("Spawn exitValue=%d", result.getExitCode()));
          }
          catch (Exception e)
          {
             result.setException(e);
             LOG.logp(Level.SEVERE,
                      "BrokerCore.spawn()",
                      "",
                      "Spawning exception.",
                      e);
          }
          finally
          {
             if (shell != null)
             {
                shell.destroy();
             }
          }

          return result;
       }

       /**
        * This method is used for tests only.
        *
        * @param   uuid
        *          Assigned UUID
        *
        * @return  UUID parameter.
        */
       public static synchronized String ping(String uuid)
       {
          LOG.logp(Level.INFO,
                   "BrokerCore.ping()",
                   "",
                   CentralLogger.generate("Ping uuid=%s", uuid));

          return BrokerCore.uuid;
       }

       /**
        * Returns the dedicated host on which a spawned client will be run.
        *
        * @return   The dedicated host
        */
       public static synchronized String getDedicatedHost()
       {
          return BrokerCore.dedicatedHost;
       }

       /**
        * Get OS loading. Not available on Windows OS -1.00 is always returned.
        *
        * @return  the system load average; or a negative value if not available.
        */
       public static synchronized double getSystemLoading()
       {
          // TODO implements a native solution which works on both Linux and Windows
          return ManagementFactory.getOperatingSystemMXBean().getSystemLoadAverage();
       }

       /**
        * Get broker JVM arguments and CLASSPATH which is inherited by spawned clients.
        * Get other parameters from configuration file.
        *
        * @param   cfg
        *          Bootstrap configuration.
        */
       private static void initialize(BootstrapConfig cfg)
       {
          cfg.setServer(false);

          // UUID
          uuid = null;
          // get spawner name
          host = cfg.getString(ConfigItem.SERVER_HOST, "localhost");
          // secured port as string
          port = String.valueOf(cfg.getInt(ConfigItem.SERVER_SECURE_PORT, -1));
          // number of retry
          count = cfg.getInt("remote", "retry", "count", 10);
          // retry time in seconds
          seconds = cfg.getInt("remote", "retry", "seconds", 10);
          // get spawner name
          spawner = cfg.getString("remote", "spawner", "file", "./spawn");
          // get launch timeout for the spawner
          spawnerLaunchTimeout = cfg.getInt(ConfigItem.SPAWNER_LAUNCH_TIMEOUT, ClientSpawner.DEFAULT_TIMEOUT);

          // get the dedicated web host
          String localHost;
          try
          {
             InetAddress inetAddress = InetAddress.getLocalHost();
             localHost = inetAddress.getHostName();
          }
          catch (UnknownHostException e)
          {
             localHost = "localhost"; // this localhost should not work for
          }
          dedicatedHost = cfg.getString("remote", "agent", "host", localHost);

          if (Utils.isValidHostNameOrIpAddress(dedicatedHost))
          {
             LOG.logp(Level.SEVERE,
                      BrokerCore.class.getName(),
                      "initialize",
                      "remote:agent:host should be configured as a network host name or IP address");
          }
          // get the associated user account name
          associatedUser = cfg.getString("remote", "agent", "user", "");
          // get the dedicated broker mode
          dedicatedMode = cfg.getBoolean("remote", "agent", "dedicatedMode", false);

          // JVM arguments
          String jargs = cfg.getString("remote", "java", "args", "");

          if (jargs.isEmpty())
          {
             jvmArgs = ManagementFactory.getRuntimeMXBean().getInputArguments();
          }
          else
          {
             jvmArgs = Arrays.asList(jargs.split(" "));
          }

          StringBuilder sb = new StringBuilder();
          for (String arg : jvmArgs)
          {
             if (sb.length() != 0)
             {
                sb.append(BROKER_JVMARGS_SEPARATOR);
             }
             sb.append(arg);
          }

          // store as string
          jvmargs = sb.toString();

          // classpath
          String cp = cfg.getString("remote", "java", "classpath", "");

          if (cp.isEmpty())
          {
             classpath = ManagementFactory.getRuntimeMXBean().getClassPath();
          }
          else
          {
             classpath = cp;
          }
       }

       /**
        * Sleep for a while.
        *
        * @param   sleepSeconds
        *          Sleep amount time in seconds.
        */
       private static void asleep(long sleepSeconds)
       {
          try
          {
             new CountDownLatch(1).await(sleepSeconds, TimeUnit.SECONDS);
          }
          catch (InterruptedException e)
          {
             // nothings
          }
       }

       /**
        * Establish a secure connection to the P2J server running on the specified port.
        * Use the certificate targeted by the specified alias for authentication.
        * <p>
        * remote:broker:name=&lt;broker name&gt;
        *
        * @param   cfg
        *          Bootstrap configuration.
        */
       private static void connect(SessionManager sessMgr, BootstrapConfig cfg)
       {
          Session session = null;
          try
          {
             // establish a secure connection with the remote P2J server
             session = sessMgr.connectDirect(cfg, null, createSessionListener());

             RemoteObject.registerStaticNetworkServer(ifaces, BrokerCore.class);

             // Get server exported API
             BrokerServerServices remote = (BrokerServerServices) RemoteObject.obtainNetworkInstance(
                            BrokerServerServices.class,
                            session);

             // register broker
             uuid = remote.registerBroker(dedicatedHost, associatedUser, dedicatedMode);

             // log a message
             LOG.logp(Level.INFO,
                      "BrokerCore.connect()",
                      "",
                      CentralLogger.generate("Broker has been registered uid=%s", uuid));

             // after successful register reinitialize retry count.
             count = cfg.getInt("remote", "retry", "count", 10);

             // wait until session is closed
             do
             {
                remote.start(uuid);
             } while (sessionLatch.getCount() > 0);
          }
          catch (Throwable t)
          {
             LOG.logp(Level.SEVERE,
                      "BrokerCore.connect()",
                      "",
                      "Connection Exception.",
                      t);
          }
          finally
          {
             cleanup(session);
          }
       }

       /**
        * Create a <p>SessionListener</p> object.
        *
        * @return  A <p>SessionListener</p> instance.
        */
       private static SessionListener createSessionListener()
       {
          // session listener
          SessionListener sessionListener = new SessionListener()
          {
             @Override
             public void initialize(Session session)
             {
                sessionLatch = new CountDownLatch(1);
             }

             @Override
             public void terminate(Session session)
             {
                if (sessionLatch != null && sessionLatch.getCount() == 1)
                {
                   sessionLatch.countDown();
                }
             }
          };

          return sessionListener;
       }

       /**
        * Cleanup on exit or error
        */
       private static void cleanup(Session session)
       {
          // close session
          if (session != null)
          {
             session.terminate();
          }

          // unregister services
          String[] list = { ifaces[0].getName() };
          RemoteObject.deregisterStaticNetworkServer(list);
       }
    }