# ProcessClientSpawner.java

    /*
    ** Module   : ProcessClientSpawner.java
    ** Abstract : define the process-based client spawner
    **
    ** Copyright (c) 2014-2025, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- --------------------------------Description-----------------------------------
    ** 001 CA  20140206 First version.
    ** 002 MAG 20140224 Use ProcessBuilderParameters for processes.
    ** 003 MAG 20140310 On Windows OS we must use also a password.
    ** 004 OM  20150108 Passed null map as additional environment to ProcessClientSpawner.spawn().
    ** 005 GES 20150505 Made the inner class private.
    ** 006 ECF 20150828 Enable Java 8.
    ** 007 GES 20160301 Added some javadoc.
    ** 008 SBI 20160628 Added new methods to allocate and to release the client's system resources.
    ** 009 SBI 20171019 Changed to allocate new system resources in order to run new web client on
    **                  the target host.
    ** 010 CA  20191211 When spawning a Java process programmatically, allow the caller to pass
    **                  environment options specific to that call (beside any jvmArgs configured in
    **                  the directory).
    ** 011 SBI 20230411 Removed unused code.
    ** 012 GBB 20240709 Fixing typization for ProcessBuilderOptions, ProcessClientBuilder and TemporaryClientTask.
    **                  Print diagnostics as part of TemporaryClient.doWork.
    ** 013 GBB 20240719 Logging cleanup.
    ** 014 GBB 20241010 Always pass true for security:certificate:validate.
    ** 015 GBB 20250106 Adding static inner class Params and contructor accepting param of type Params.
    ** 016 AP  20250310 Added parameter for default timeout.
    */
    /*
    ** 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.util.*;
    import java.util.function.*;

    import com.goldencode.p2j.cfg.*;
    import com.goldencode.p2j.directory.Base64;
    import com.goldencode.p2j.security.SecurityManager;
    import com.goldencode.p2j.ui.client.chui.driver.web.*;
    import com.goldencode.p2j.ui.client.driver.*;
    import com.goldencode.p2j.util.*;

    /**
     * Class used to spawn a new P2J client which will authenticate as a P2J process.
     * <p>
     * This class is tricky to understand because the main part of it runs on the server while the
     * <code>TemporaryClientTask</code> inner class is serialized and sent down to execute on the
     * client side.  That client side code then calls methods which are implemented as a remote
     * object with the {@link Spawner} interface, which the server portions of this class implement.
     * <p>
     * The spawner process will connect to the server using a secure socket and temporary credentials
     * to establish a secure session.  If that works, it will lookup the credentials to use for
     * spawning the client based on a unique UUID for this spawning session.  Using those credentials
     * it will start the client and notify the server about success or failure.
     * <p>
     * This notification must not be treated as the client process authenticated successfully; it
     * needs to be interpreted as a signal that the temporary connection has finished successfully.
     */
    public class ProcessClientSpawner
    extends ClientSpawner
    {
       /** Wrapper class for constructor params. */
       public static class Params
       {
          /** The P2J process ID. */
          private final String processName;

          /** The OS system user to spawn the client under. */
          private final String osUser;

          /** The OS system user password. */
          private final String osPass;

          /** Map of additional environment properties, in the <code>key=value</code> form. */
          private final Map<String, String> envMap;

          /** Default timeout for spawner launch, expressed in seconds. If this is {@code null} the fallback -
           *  {@code ClientSpawner.DEFAULT_TIMEOUT} is used. */
          private final Integer defaultTimeout;

          /**
           * Public constructor.
           *
           * @param    processName
           *           The P2J process ID.
           * @param    osUser
           *           The OS system user to spawn the client under.
           * @param    osPass
           *           The OS system user password.
           * @param    envMap
           *           Map of additional environment properties, in the <code>key=value</code> form.
           * @param    defaultTimeout
           *           Default timeout for spawner launch, expressed in seconds.
           */
          public Params(String processName, String osUser, String osPass,
                        Map<String, String> envMap, Integer defaultTimeout)
          {
             this.processName = processName;
             this.osUser = osUser;
             this.osPass = osPass;
             this.envMap = envMap;
             this.defaultTimeout = defaultTimeout;
          }

          /**
           * Getter for {@link #processName}.
           *
           * @return See above.
           */
          public String getProcessName()
          {
             return processName;
          }

          /**
           * Getter for {@link #osUser}.
           *
           * @return See above.
           */
          public String getOsUser()
          {
             return osUser;
          }

          /**
           * Getter for {@link #osPass}.
           *
           * @return See above.
           */
          public String getOsPass()
          {
             return osPass;
          }

          /**
           * Getter for {@link #envMap}.
           *
           * @return See above.
           */
          public Map<String, String> getEnvMap()
          {
             return envMap;
          }

          /**
           * Get the default timeout for spawner launch.
           *
           * @return   See above.
           */
          public Integer getDefaultTimeout()
          {
             return defaultTimeout;
          }
       }
       /** Default timeout for spawner launch, expressed in seconds */
       private final Integer defaultTimeout;

       /** The {@link ClientBuilder} used to spawn the P2J client process. */
       private final ProcessClientBuilder cb;

       /** The P2J process ID being spawned by this instance.*/
       private String process;

       /**
        * Create a new spawner for P2J processes.
        *
        * @param    params
        *           Wrapper object for constructor params.
        */
       public ProcessClientSpawner(Params params)
       {
          this(params.getProcessName(), params.getOsUser(), params.getOsPass(), params.getEnvMap(),
               params.getDefaultTimeout());
       }

       /**
        * Create a new spawner for P2J processes.
        *
        * @param    process
        *           The P2J process ID.
        * @param    osUser
        *           The OS system user to spawn the client under.
        * @param    osPass
        *           The OS system user password.
        * @param    env
        *           Map of additional environment properties, in the <code>key=value</code> form.
        * @param    defaultTimeout
        *           Default timeout for spawner launch, expressed in seconds.
        */
       public ProcessClientSpawner(String process, String osUser, String osPass, Map<String, String> env,
                                   Integer defaultTimeout)
       {
          ProcessBuilderParameters buildParams = new ProcessBuilderParameters(osUser,
                                                                              osPass,
                                                                              new ProcessBuilderOptions(process, env));
          this.cb = new ProcessClientBuilder(buildParams);
          this.process = process;
          this.defaultTimeout = defaultTimeout;
       }

       /**
        * Create a new spawner for P2J processes.
        *
        * @param    process
        *           The P2J process ID.
        * @param    osUser
        *           The OS system user to spawn the client under.
        * @param    osPass
        *           The OS system user password.
        * @param    env
        *           Map of additional environment properties, in the <code>key=value</code> form.
        */
       public ProcessClientSpawner(String process, String osUser, String osPass, Map<String, String> env)
       {
          this(process, osUser, osPass, env, null);
       }

       /**
        * Get {@link ProcessBuilderParameters} used to spawn P2J clients.
        *
        * @return   See above.
        */
       @Override
       public ProcessBuilderParameters getBuildParams()
       {
          return cb.getParams();
       }

       /**
        * Launch the specified batch process.
        * <p>
        * If the P2J process is linked with an appserver, it will delegate the launching to
        * {@link AppServerLauncher#launch(String)}.  Else, it will use the {@link ProcessClientSpawner
        * spawner} infrastructure to start a P2J client.
        *
        * @param    batchProcess
        *           The name of the batch process to be launched.
        */
       public static void launch(String batchProcess)
       {
          launch(batchProcess, null);
       }

       /**
        * Launch the specified batch process.
        * <p>
        * If the P2J process is linked with an appserver, it will delegate the launching to
        * {@link AppServerLauncher#launch(String)}.  Else, it will use the {@link ProcessClientSpawner
        * spawner} infrastructure to start a P2J client.
        *
        * @param    batchProcess
        *           The name of the batch process to be launched.
        * @param    env
        *           Map of additional environment properties, in the <code>key=value</code> form.
        */
       public static void launch(String batchProcess, Map<String, String> env)
       {
          SecurityManager sm = SecurityManager.getInstance();

          String appserver = sm.getAppserverForProcess(batchProcess);
          if (appserver != null)
          {
             // appservers are special, so treat them now
             AppServerLauncher.launch(appserver);
          }
          else
          {
             // normal process, launch it
             ProcessClientSpawner spawner = new ProcessClientSpawner(batchProcess,
                                                                     sm.getSystemUser(batchProcess),
                                                                     sm.getSystemPassword(batchProcess),
                                                                     env);

             // get the user name from the directory
             spawner.spawn();
          }
       }

       /**
        * Get a map containing the P2J process secure credentials: the serialized process keystore and
        * trust store and other details, like target aliases in the keystore and truststore.
        *
        * @return   Map of security configs key : value.
        */
       private Map<String, Object> getServerData()
       {
          // return a map with the process defaults
          Map<String, Object> defaults = new HashMap<>();

          ProcessBuilderOptions opts = getBuildParams().getDirOptions();

          if (opts.getProcessAlias() != null)
             defaults.put("security:keystore:processalias", opts.getProcessAlias());
          if (opts.getTruststoreAlias() != null)
             defaults.put("security:truststore:alias", opts.getTruststoreAlias());
          if (opts.getKeyentryPassword() != null)
             defaults.put("access:password:keystore", opts.getKeystorePassword());
          if (opts.getKeyentryPassword() != null)
             defaults.put("access:password:keyentry", opts.getKeyentryPassword());
          if (opts.getTruststorePassword() != null)
             defaults.put("access:password:truststore", opts.getTruststorePassword());
          if (opts.getProcessKeystore() != null)
             defaults.put("security:keystore:bytes", opts.getProcessKeystore());
          if (opts.getTruststore() != null)
             defaults.put("security:truststore:bytes", opts.getTruststore());

          defaults.put("security:certificate:validate", true);

          return defaults;
       }

       /**
        * Get the default timeout for spawner launch.
        *
        * @return   See above.
        */
       @Override
       public int getDefaultTimeout()
       {
          return this.defaultTimeout != null && this.defaultTimeout != 0 ? this.defaultTimeout :
             super.getDefaultTimeout();
       }

       /**
        * Get a {@link ProcessClientBuilder} used to spawn P2J clients for the target {@link #process}.
        *
        * @return   See above.
        */
       @Override
       protected ProcessClientBuilder getBuilder()
       {
          return cb;
       }

       /**
        * Get a {@link TemporaryClient} worker which will do the work after authenticating on the P2J
        * server using the temporary credentials.
        *
        * @param    uuid
        *           The client identifier uuid.
        * @param    projectToken
        *           The project token to be set for the temporary session.
        *
        * @return   See above.
        */
       @Override
       public TemporaryClient getTemporaryClient(String uuid, String projectToken)
       {
          return new TemporaryClientTask(uuid, getServerData());
       }

       /**
        * On P2J client side, it will do custom work after authenticating using the temporary
        * credentials.
        * <p>
        * For process clients case, it will retrieve from the P2J server the secure credentials
        * and save them in the {@link BootstrapConfig configuration} used to start the P2J process.
        */
       private static class TemporaryClientTask
       implements TemporaryClient
       {
          /** the process key store and trust stores from the server */
          private final Map<String, Object> securityConfigs;

          /** The unique id for the spawned client running the task. */
          private final String uuid;

          /**
           * Constructor.
           *
           * @param    uuid
           *           The client identifier uuid.
           * @param    securityConfigs
           *           A map of the security configs needed for setting up the client.
           */
          TemporaryClientTask(String uuid, Map<String, Object> securityConfigs)
          {
             this.uuid = uuid;
             this.securityConfigs = securityConfigs;
          }

          /**
           * On P2J client side, it will do custom work after authenticating using the temporary
           * credentials.
           * <p>
           * For web chui clients case, it will create a {@link ChuiWebDriver} and start the web
           * embedded server which will serve this client's requests.
           * <p>
           * Any custom configuration already existing in the {@code config} parameter will not
           * be overwritten.
           *
           * @param    spawner
           *           The spawner remote access interface.
           * @param    config
           *           The configuration to use for setup of the client and for the server connection.
           * @param    diagnostics
           *           Details about the client process.
           *
           * @return   Always {@code null}.
           *
           * @throws   RuntimeException
           *           If there are problems accessing the secure credentials.
           */
          @Override
          public ScreenDriver doWork(Spawner spawner, BootstrapConfig config, Supplier<String> diagnostics)
          {
             Map<String, Object> defaults = securityConfigs;

             String  alias       = (String)  defaults.get("security:keystore:processalias");
             String  tsAlias     = (String)  defaults.get("security:truststore:alias");
             String  ksPass      = (String)  defaults.get("access:password:keystore");
             String  kePass      = (String)  defaults.get("access:password:keyentry");
             String  tsPass      = (String)  defaults.get("access:password:truststore");
             byte[]  keystore    = (byte[])  defaults.get("security:keystore:bytes");
             byte[]  truststore  = (byte[])  defaults.get("security:truststore:bytes");

             try
             {
                // save them at the bootstrap config. any client-side config has priority
                if (alias != null &&
                    config.getConfigItem("security", "keystore", "processalias") == null)
                {
                   config.setConfigItem("security", "keystore", "processalias", alias);
                }

                if (tsAlias != null &&
                    config.getConfigItem("security", "truststore", "alias") == null)
                {
                   config.setConfigItem("security", "truststore", "alias", tsAlias);
                }

                if (ksPass != null &&
                    config.getConfigItem("access", "password", "keystore") == null)
                {
                   config.setConfigItem("access", "password", "keystore", ksPass);
                }

                if (kePass != null &&
                    config.getConfigItem("access", "password", "keyentry") == null)
                {
                   config.setConfigItem("access", "password", "keyentry", kePass);
                }

                if (tsPass != null &&
                   config.getConfigItem("access", "password", "truststore") == null)
                {
                   config.setConfigItem("access", "password", "truststore", tsPass);
                }

                config.setConfigItem("security", "certificate", "validate", Boolean.TRUE.toString());

                if (keystore != null &&
                    config.getConfigItem("security", "keystore", "filename") == null)
                {
                   config.setConfigItem("security", "keystore", "bytes",
                                        Base64.byteArrayToBase64(keystore));
                }

                if (truststore != null &&
                   config.getConfigItem("security", "truststore", "filename") == null)
                {
                   config.setConfigItem("security", "truststore", "bytes",
                                        Base64.byteArrayToBase64(truststore));
                }

                // invalidate the security transport...
                config.setConfigItem("security", "transport", "refresh", "true");

                ClientCore.outputDiagnostics(config, diagnostics);

                spawner.clientIsReady(uuid, null);
             }
             catch (ConfigurationException e)
             {
                throw new RuntimeException(e);
             }

             return null;
          }
       }

       @Override
       public void releaseClient()
       {
       }
    }