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