# ClientSpawner.java

    /*
    ** Module   : ClientSpawner.java
    ** Abstract : define a generic client Spawner.
    **
    ** Copyright (c) 2013-2025, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- -------------------------------Description------------------------------------
    ** 001 MAG 20131119 First version.
    ** 002 CA  20140206 Refactored to make it unaware of the type of the P2J client being spawned.
    ** 003 MAG 20140710 Implements remote launcher.
    ** 004 OM  20150108 Added optional appserver specific environment settings to spawned process.
    ** 005 GES 20150211 Added P2J_HOME as a standard part of the environment of a spawned child.
    ** 006 GES 20160219 Added logging of non-zero exit codes.
    ** 007 SBI 20160628 Added new methods to allocate and to release the client's system resources.
    ** 008 SBI 20171019 Changed to spawn remote clients on dedicated hosts.
    ** 009 SBI 20230505 Added new spawn method to allocate network port before the client process is
    **                  spawned, updated java debugger and jmx agent options before the client has been
    **                  started locally, removed unused code.
    ** 010 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
    ** 011 GBB 20230517 Fix logSpawnerStream statement place.
    ** 012 SBI 20230615 Fixed the incorrect error code that hide the spawner error code.
    ** 013 GBB 20230626 Expose TIME_OUT field to be reused by spawner logger Thread.
    ** 014 CA  20230704 In case of an abend during spawn, force an error code, so the allocated config is freed.
    ** 015 GBB 20230825 Fixes of the timeout for releasing the web resources and the spawner process.
    ** 016 CA  20240210 'clientConfig/properties' contains a map of operating system variables to be passed to the
    **                  spawned process.
    ** 017 GBB 20240709 Letting concrete implementations to provide ClientBuilder. Reflecting changes in the
    **                  immutability in params and options.
    **                  Moved method updateWebClientOptions to WebClientBuilderParameters.updateOptions.
    ** 018 GBB 20240718 Moving base client builder configs to a field in the respective options class.
    ** 019 RNC 20250127 Check if session launching is enabled before spawning a new client.
    ** 020 CA  20250221 Added constants for the spawner modes.
    ** 021 AP  20250303 Added timeout for spawner launching.
    ** 022 GBB 20250403 Lower log level for invalid OS credentials.
    */
    /*
    ** 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.util.*;
    import java.util.concurrent.*;
    import java.util.concurrent.atomic.*;
    import java.util.logging.*;

    import com.goldencode.p2j.cfg.*;
    import com.goldencode.p2j.net.SessionManager;
    import com.goldencode.p2j.net.RouterSessionManager;
    import com.goldencode.p2j.util.logging.*;

    /**
     * This defines a way of spawning new P2J client processes.  Temporary credentials are sent to
     * the P2J client side, which, if used, will establish a P2J session with the P2J server and do
     * some custom work (depending on the implementation).
     * <p>
     * Actual implementations of this class will need to provide the following info:
     * <ul>
     *    <li>A {@link ClientBuilder} instance, to spawn the P2J client process.</li>
     *    <li>A {@link ClientBuilderParameters} instance, to configure the P2J client.</li>
     *    <li>Implementation for the {@link SpawnerListener#getTemporaryClient(String, String)}, with the work
     *        needed to be done after authenticating with the temporary credentials.</li>
     * </ul>
    */
    public abstract class ClientSpawner
    implements SpawnerListener
    {
       /** Default timeout for spawner launch, expressed in seconds */
       public static final int DEFAULT_TIMEOUT = 30;

       /** Mode to authenticate via the FWD server certificate. */
       public static final String MODE_AUTH_SERVER = "0";

       /** Mode to authenticate via OS username and password. */
       public static final String MODE_AUTH_PASSWORD = "1";

       /** Mode to check if a given pair of username and password is a valid OS credential. */
       public static final String MODE_CREDENTIALS_CHECK = "2";

       /** Timeout */
       public static final int CLIENT_STARTUP_TIMEOUT_SEC = 30;

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

       /** Executor service for simple tasks that don't throw exceptions.  */
       private static final ExecutorService GENERIC_EXECUTOR = Executors.newCachedThreadPool();

       /** Synchronize */
       private CountDownLatch clientStartupCountDown;

       /** Flag indicating the P2J client has been started. */
       private volatile boolean ready = false;

       /**
        * Get {@link ClientBuilderParameters} used to spawn P2J clients.
        *
        * @return   See above.
        */
       public abstract ClientBuilderParameters getBuildParams();

       /**
        * Get a {@link ClientBuilder} used to spawn P2J clients.
        *
        * @return   See above.
        */
       protected abstract ClientBuilder getBuilder();

       /**
        * Get the default timeout for spawner launch.
        *
        * @return   See above.
        */
       public int getDefaultTimeout()
       {
          return DEFAULT_TIMEOUT;
       }

       /**
        * Launch a P2J client local or remote. Get a list of brokers available for this user.
        * If no brokers are registered for this user the client is spawn local.
        * If at least one broker is registered the client is spawn remote.
        */
       public void spawn()
       {
          spawn(null, null);
       }

       /**
        * Launch a P2J client local or remote. Get a list of brokers available for this user.
        * If no brokers are registered for this user the client is spawn local.
        * If at least one broker is registered the client is spawn remote.
        *
        * @param    allocator
        *           The web client allocator
        * @param    requestParameters
        *           The given requests parameters
        *
        * @return   The exit code.
        */
       public int spawn(WebClientAllocator allocator, String[] requestParameters)
       {
          ClientBuilderParameters params = getBuildParams();
          if (params.getOsUser() == null)
          {
             throw new NullPointerException("The OS username must be provided!");
          }

          // Get a temporary account from pool.
          TemporaryAccount tmpAcc = TemporaryAccount.open();

          if (tmpAcc == null)
          {
             RuntimeException exc =
                new RuntimeException("Temporary Account Pool is empty! Please call your system administrator.");
             LOG.warning("Spawn exception:", exc);
             throw exc;
          }

          // Get a list of brokers for this user
          Brokers brokers = BrokerManager.getBrokers(params.getOsUser());

          // register listener
          SpawnerImpl spawner = SpawnerImpl.getInstance();
          spawner.addListener(params.getUuid(), this);

          clientStartupCountDown = new CountDownLatch(1);

          // initial environment map contains user and password
          Map<String, String> environmentMap = tmpAcc.getEnvironmentMap();

          BaseClientBuilderOptions baseOptions = params.getDirOptions().getBaseOptions();
          environmentMap.putAll(baseOptions.getEnvProperties());

          // add P2J_HOME so that all P2J client sessions spawned below this one can find their way home
          environmentMap.put(Configuration.PROP_HOME_DIR, baseOptions.getLibPath());

          if (requestParameters != null && requestParameters.length > 2)
          {
             environmentMap.put(BrokerManager.CLIENT_IP, requestParameters[2]);
          }

          // used by CentralLogger to stop listening for the streamed msgs.
          AtomicBoolean isSpawnerRunning = new AtomicBoolean(false);

          ready = false;

          SessionManager sessionManager = RouterSessionManager.get();

          int exitCode = SpawnError.GENERIC.getCode();
          try
          {
             if (sessionManager != null && sessionManager.getSessionLock())
             {
                exitCode = SpawnError.SESSION_LOCKED.getCode();
             }
             else
             {
                int spawnerLaunchTimeout = baseOptions.getSpawnerLaunchTimeout() != null ?
                   baseOptions.getSpawnerLaunchTimeout() : this.getDefaultTimeout();

                exitCode = brokers.getBrokers().isEmpty() ?
                   spawnLocal(environmentMap, allocator, requestParameters, isSpawnerRunning,
                              spawnerLaunchTimeout) :
                   spawnRemote(brokers, environmentMap, allocator, requestParameters);
             }

             if (exitCode != 0)
             {
                LOG.log(SpawnError.isInvalidOsCredentials(exitCode) ?
                           Level.FINER :
                           Level.WARNING,
                        SpawnError.getSpawnMsgOrDefault(exitCode));
             }
          }
          catch (Throwable t)
          {
             LOG.severe(SpawnError.GENERIC.getMsg(), t);
          }
          finally
          {
             String clientId = params.getUuid();
             spawner.removeListener(clientId);
             isSpawnerRunning.set(false);
             tmpAcc.close();

             // cleanup the command
             ClientBuilder.removeCommand(clientId);

             if (exitCode != 0 && allocator != null)
             {
                allocator.freeClient(clientId);
             }
          }
          return exitCode;
       }

       /**
        * Launch a new local P2J client.
        *
        * @param    environmentMap
        *           Environment variables.
        * @param    allocator
        *           The web client allocator
        * @param    requestParameters
        *           The given requests parameters
        * @param    isSpawnerRunning
        *           The thread-safe boolean flag to notify the logger to stop listening for spawner msgs.
        * @param    spawnerLaunchTimeout
        *           Launch timeout for the spawner, expressed in seconds.
        *
        * @return   The spawner exit code or {@code SpawnError.EXCEEDED_TIMEOUT} if the timeout is exceeded.
        */
       private int spawnLocal(Map<String, String> environmentMap,
                              WebClientAllocator allocator,
                              String[] requestParameters,
                              AtomicBoolean isSpawnerRunning,
                              int spawnerLaunchTimeout)
       throws IOException
       {
          if (allocator != null)
          {
             ClientBuilderParameters buildParams = getBuildParams();
             WebAllocatedResources allocatedResources = allocator.allocateClient(requestParameters,
                                                                                 "localhost",
                                                                                 buildParams.getUuid(),
                                                                                 buildParams.getOsUser());
             if (allocatedResources == null)
             {
                return SpawnError.NO_AVAILABLE_PORTS_ERR_CODE.getCode();
             }

             ((WebClientBuilder) getBuilder()).getParams().updateOptions(allocatedResources);
          }

          Process shell = getBuilder().localStart(environmentMap);

          isSpawnerRunning.set(true);
          CentralLoggerServer.logSpawnerStream(shell.getErrorStream(), isSpawnerRunning);

          Future<Integer> spawnerResultFuture =
             GENERIC_EXECUTOR.submit(() ->
                                     {
                                        int exitCode = SpawnError.GENERIC.getCode();
                                        try
                                        {
                                           if (!shell.waitFor(spawnerLaunchTimeout, TimeUnit.SECONDS))
                                           {
                                              LOG.log(Level.SEVERE, "Spawner launch procedure exceeded " +
                                                      "timeout of %d seconds!", spawnerLaunchTimeout);
                                              exitCode = SpawnError.EXCEEDED_TIMEOUT.getCode();
                                           }
                                           else
                                           {
                                              exitCode = shell.exitValue();
                                           }
                                        }
                                        catch (InterruptedException ignored)
                                        {
                                        }
                                        if (exitCode != 0)
                                        {
                                           // let the latch stop waiting, it's not a success
                                           clientStartupCountDown.countDown();
                                        }
                                        return exitCode;
                                     });

          Future waitClientFuture = GENERIC_EXECUTOR.submit(() ->
                                                            {
                                                               // wait for notification on successful client start
                                                               try
                                                               {
                                                                  clientStartupCountDown
                                                                     .await(CLIENT_STARTUP_TIMEOUT_SEC, TimeUnit.SECONDS);
                                                               }
                                                               catch (InterruptedException ignored)
                                                               {
                                                               }
                                                            });

          Integer exitCode = null;
          try
          {
             exitCode = spawnerResultFuture.get();
             waitClientFuture.get();
          }
          catch (InterruptedException | ExecutionException e)
          {
             LOG.log(Level.FINE, "Waiting for the exit code of the local spawn was unsuccessful.", e);
          }

          if (!ready)
          {
             shell.destroy();
          }

          if (exitCode == null)
          {
             exitCode = SpawnError.GENERIC.getCode();
          }
          return exitCode;
       }

       /**
        * Launch a new remote P2J client.
        *
        * @param    brokers
        *           Wrapper for list of available brokers.
        * @param    environmentMap
        *           Environment variables.
        * @param    allocator
        *           The web client allocator
        * @param    requestParameters
        *           The given requests parameters
        *
        * @return   The spawner exit code.
        */
       private int spawnRemote(Brokers brokers,
                               Map<String, String> environmentMap,
                               WebClientAllocator allocator,
                               String[] requestParameters)
       {
          BrokerSpawnResult result = getBuilder().remoteStart(getBuildParams().getOsUser(),
                                                              brokers,
                                                              environmentMap,
                                                              allocator,
                                                              requestParameters);
          int exitCode = result.getExitCode();
          if (result.getException() != null)
          {
             LOG.severe("spawnRemote exception:", result.getException());
          }
          else if (!ready)
          {
             // wait for notification on successful client start
             try
             {
                clientStartupCountDown.await(CLIENT_STARTUP_TIMEOUT_SEC, TimeUnit.SECONDS);
             }
             catch (InterruptedException ignored)
             {
             }
          }
          return exitCode;
       }

       /**
        * Notify when the client has started.
        *
        * @param    data
        *           Custom data sent by the P2J client back to the server.
        */
       @Override
       public void clientIsReady(String data)
       {
          this.ready = true;
          clientStartupCountDown.countDown();
       }
    }