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