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