ClientCore.java
/*
** Module : ClientCore.java
** Abstract : core client loop which handles init and connects to server
**
** Copyright (c) 2011-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- ----------------------------------Description---------------------------------
** 001 GES 20110928 Moved core logic here from ClientDriver so that the same
** code could be used from multiple callers.
** 002 GES 20111028 Added support for startup of a client session within the
** same process as the serve.
** 003 GES 20111111 Pass single mode flag to thin client.
** 004 CA 20130529 If this is authenticated as an appserver, register the needed server exports.
** 005 CA 20130704 Removed H004, as the appserver launching was reworked.
** 006 CA 20131004 Expose client-side parameters to the server-side application code.
** 007 EVL 20131104 Batch mode support.
** 008 MAG 20131129 Import server KeyStore.
** 009 EVL 20140117 Moved here the code to load native P2J library.
** 010 MAG 20140128 Fix CTRL-C process for web clients.
** 011 MAG 20140131 Temporary accounts should be used only once to import server key store.
** 012 MAG 20140206 On remote application exit notify web client by sending a referrer address
** used to redirect the client.
** 013 CA 20140206 Refactored the temporary client connection to make it unaware of the type of
** P2J client being started.
** 014 MAG 20140217 Refactoring web client redirection flaw.
** 015 EVL 20140327 Changing the call to initErrorWriter() in OutputManager to take into account
** the server batch mode flag setting.
** 016 CA 20140805 Changes to isolate ChUI-specific code from common code: added generics,
** classes not related to ChUI behavior import individual ChUI package classes.
** 017 OM 20140909 Emulate the '-ininame' command-line parameter.
** 018 CA 20141123 Renamed TC.killDependentThreads to TC.terminate.
** 019 GES 20141228 Rework to eliminate application arguments and to extend the client parameters
** to more use cases.
** 020 GES 20150109 Added native methods to query pid and user name from the OS. Extended the
** client parameters with these values and also support for a user-specific
** "terminal name" that will be used as the equivalent of the UNIX device name.
** Also added a chui/gui flag to the parms so that the server doesn't need to
** call down to the client to figure that out.
** 021 CA 20150131 Clean the security caches when client restarts.
** 022 HC 20151124 Improved loading of boolean environment variables from Progress INI files and
** Windows Registry.
** 023 IAS 20160328 Client log support.
** 024 OM 20160927 Added batchMode to ClientParameters serialization.
** 025 CA 20170203 Notify the driver that the client is ready (useful for embedded mode, to
** notify the embedded app the client can handle messages).
** 025 SBI 20170628 Changed to shutdown the driver and then the current session, added to register
** the web client session on the server and to release the web client data on
** the server if the client is about to be closed.
** 026 SBI 20190129 Changed start(BootstrapConfig, boolean, boolean) in order to innitialize
** the ease access manager.
** 027 HC 20190306 Implemented limited Windows API emulation for GetSystemMetrics function.
** 028 EVL 20190629 Set up current user name for current client session.
** 029 HC 20191023 Improved emulation of getSystemMetrics Win32 function.
** 030 CA 20200527 Allow the project token to be overridden via a client:project:token setting.
** CA 20200608 The project token must be sent before ThinClient initializes.
** IAS 20210325 Added NIO configuration via BootstrapConfig
** HC 20210725 Added support for v6colon.
** IAS 20210827 Added SSL package seqNo tracking configuration via BootstrapConfig
** GES 20210827 Added diagnostics output.
** CA 20220208 Added -errorstack command-line option (client:cmd-line-option:error-stack).
** SVL 20220620 "ininame" parameter handling moved earlier to TC.initializePost.
** CA 20220918 Added socket trust store filename and password to the ClientParameters.
** 031 GBB 20221209 Path to debug log file to be read from client bootstrap configs.
** Passing arg to spawner for path to stderr log file for background processes
** 032 GBB 20230127 Fixing pid in outputToFile name by replacing it in Java code.
** 033 GBB 20230316 Set IS_OUTPUT_REDIRECTED flag to true on System.out redirect.
** 034 GBB 20230316 IS_OUTPUT_REDIRECTED flag imported from its new location in FileChecker.
** 035 VVT 20230318 Native methods processInit and getPid made public to be accessible from
** FWD test engine. See #6237.
** 036 VVT 20230318 Redesigned to support FWD test engine. See #6237.
** 037 GBB 20230322 Setting bootstrap configs related to logging to ClientParameters.
** 038 GBB 20230322 Removing initialization of clientlog in ThinClient. Replaced by a new impl.
** 039 HC 20230427 Implemented server-side file-system resource. File operations performed on
** client can be optionally performed directly on server without the expensive
** remote call.
** 040 SBI 20230411 Added shutdown hook to free webclient resources if there is a chance for it.
** 041 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 042 HC 20230513 Fixed a regression in ThinClient initialization. ThinClient must be
** instantiated yet before the custom authentication hook is invoked.
** 043 GBB 20230602 Handle outputToFile value /dev/null.
** 044 TT 20230621 Initialize the StartupParameters from the BootstrapConfig.
** TT 20230626 Added a new cmd-line-option, the temp-directory that is read from client.xml.
** Set the temporary directory from the client parameters. It can be null.
** 045 TT 20230808 Removed cmd-line-options.
** 046 GBB 20230825 Adding new client config "sso:subject:id" to be copied.
** 047 GBB 20231121 Handling params web and embedded in processClientParams.
** web:referrer:url cleanup. ClientUuid added to standardEntry().
** Adding driverClass, webPort, browserWebSocket client params.
** 048 GBB 20231124 uuid renamed to spawnedClientUuid.
** 049 TT 20230915 Use PostInitErrorManager to properly show errors after initialization.
** 050 TT 20231129 Added the propath client parameter.
** 051 GBB 20231130 ClientParameters.driverClass replaced by driverName.
** 052 GBB 20240214 Add OS device id to client params.
** 053 CA 20240311 Allow server:clientConfig:outputToFile to be set for the FWD unit test client, too.
** 054 GBB 20240314 Add browser-derived values to client params and screen details for ui clients.
** 055 GBB 20240404 Method logProcessError moved to Utils to be reused.
** 056 HC 20240513 Moved SPREADSHEET backend (Keikai web application and the related FWD
** integration) from client to server.
** 057 GBB 20240709 Hard-coded config names replaced by ConfigItem constants.
** Reflect changes in TemporaryClient. Diagnostics printed before init.
** 058 GBB 20240729 Initialize CentralLogger after SecurityManager is created.
** 059 ICP 20240722 Modified shutdown hook registration.
** 060 SBI 20240821 Set webRoot for clientParameters if driver type is web
** 061 GBB 20240826 Removing single client. storageId added to ClientParameters.
** 062 CA 20240910 'net:socket:nio' is enabled by default only for Java 8.
** 063 GBB 20250403 Sharing the timezone retrieval method by introducing getTimezone.
** 064 AL2 20250523 'net:socket:nio' is disabled by default.
** 065 LS 20250523 Added native method getFWDVersion().
** Added severe-level logging when the versions between native code and java code differ.
*/
/*
** 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.awt.*;
import java.awt.Dimension;
import java.io.*;
import java.util.*;
import java.util.List;
import java.util.function.*;
import java.util.logging.*;
import com.goldencode.p2j.Version;
import com.goldencode.p2j.cfg.*;
import com.goldencode.p2j.directory.*;
import com.goldencode.p2j.net.*;
import com.goldencode.p2j.net.Session;
import com.goldencode.p2j.security.SecurityManager;
import com.goldencode.p2j.ui.chui.*;
import com.goldencode.p2j.ui.client.*;
import com.goldencode.p2j.ui.client.driver.*;
import com.goldencode.p2j.ui.client.driver.web.*;
import com.goldencode.p2j.util.*;
import com.goldencode.p2j.util.logging.*;
import com.goldencode.util.*;
/**
* Core looping logic that performs initialization of a client UI and
* establishes a session with the server.
* <p>
* This code provides supports the startup of a client session within the
* same process as the server. This can be used to duplicate the traditional
* Progress 4GL fat client approach (a "single process" mode). It can also be
* used to implement multiple client sessions within a true server process
* which would be used to support web-based clients.
*/
public class ClientCore
{
/** Logger. */
private static final CentralLogger LOG = CentralLogger.get(ClientCore.class);
/** Flag to prevent multiple native library loading.*/
private static boolean nativeLayerInitialized = false;
/**
* Returns a flag to indicate if native library has loaded.
*
* @return A flag to indicate if native library has loaded.
*/
public static boolean isNativeLayerInitialized()
{
return nativeLayerInitialized;
}
/**
* Structure used for passing several values to and from
* {@link ClientCore#initialize(InitStruct, BootstrapConfig, String)}
* method.
*/
public static class InitStruct
{
/**
* Output driver
*/
public ScreenDriver<?> driver;
/**
* Security context for this thread
*/
public Object context;
/**
* Client session
*/
public Session session;
/**
* Main program entry
*/
public MainEntry main;
/**
* Security manager instance
*/
public SecurityManager sm;
/**
* Shutdown hook
*/
public Thread shutdownHook;
/**
* To free web client resources
*/
public Runnable freeWebClientResources;
};
/**
* Initialize client.
*
* @param is
* initialization structure in/out.
* @param config
* The configuration to use for setup of the client and for the
* server connection.
* @param uuid
* server:spawner:uuid configuration value
*/
public static void initialize(InitStruct is, BootstrapConfig config, String uuid)
{
try
{
if (is.shutdownHook != null)
{
OrderedShutdownHooks.deregisterShutdownHook(1, is.shutdownHook);
}
final SessionManager sessMgr;
// instantiate security manager
is.sm = SecurityManager.createInstance(config);
// instantiate network session manager
sessMgr = SessionManagerFactory.createLeafNode(config);
// after SecurityManager is created
CentralLogger.setBootstrapConfig(config);
// initialize the driver
OutputManager.init(config, is.driver);
// initialize core UI services
ThinClient.initializePrep(config);
// at this point it is safe to query the driver and save it off
// in case we need to re-initialize (the same driver must be
// re-used even though we create new instances of pretty much all
// other UI classes)
if (is.driver == null)
{
is.driver = OutputManager.getDriver();
}
// establish or re-establish a session with the P2J server
// (this handles authentication too)
is.session = sessMgr.connectDirect(new InterruptedExceptionHandler(), null);
// some client code must read configuration from the directory
DirectoryManager.initRemote(is.session);
if (is.driver.isWeb())
{
final WebClientRegistrar registrar =
(WebClientRegistrar) RemoteObject.obtainNetworkInstance(
WebClientRegistrar.class, is.session);
registrar.registerWebClientSession(uuid, is.session.getNodeAddress());
is.freeWebClientResources = () -> {
try
{
registrar.freeWebClientResources(uuid);
}
catch(Throwable t)
{
// there is no active session
}
};
is.shutdownHook = new Thread(is.freeWebClientResources);
OrderedShutdownHooks.registerShutdownHook(1, is.shutdownHook);
}
}
catch (Exception exc)
{
// STOP conditions don't need to be reported, they are an expected
// mechanism to terminate
if (!isCauseStop(exc))
{
LOG.severe("The client stopped working throwing an unexpected exception:", exc);
}
return;
}
// must be done after user session is established
ThinClient.getInstance().initOSResources();
// obtain the entry point
is.main = (MainEntry) RemoteObject.obtainNetworkInstance(MainEntry.class, is.session);
String projectToken = config.getString(ConfigItem.PROJECT_TOKEN, null);
// this will set the token from either client:project:token or directory's project_token.
is.main.setProjectToken(projectToken);
StartupParameters sp = new StartupParameters();
sp.initFromBootstrap(config);
sp = is.main.initializeStartupParameters(sp);
Utils.setTemporaryDirectory(sp.getTemporaryDirectory());
if (is.session != null)
{
CentralLogService logService =
(CentralLogService) RemoteObject.obtainNetworkInstance(CentralLogService.class, is.session);
CentralLoggerClient.setLogService(logService);
}
// determine any client-side parameters
ClientParameters params = processClientParams(is.driver, config);
// finish initializing the thin client
ThinClient.initializePost(config, is.session, is.context, sp, sp.getInitializationFile());
Directory remoteDir = DirectoryManager.getInstance();
if (remoteDir != null)
{
// initialize the Ease Access Manager
EaseAccessManager.init(remoteDir);
}
ThinClient.loadEnvironment(config);
// After full initialization of the client code we can set up the ErrorWriter for
// client side.
if (is.session != null)
{
OutputManager.initErrorWriter(is.session);
}
// Propagate client-side parameters to the server
is.main.setClientParams(params);
ThinClient.setClientParameters(params);
// initialize initial tooltip settings
ThinClient.setSessionTooltips();
// above this point, any CTRL-C processing that causes a STOP
// condition to be thrown will cause an exit (this is very timing
// sensitive but if you hold down the CTRL-C keys AND the server
// environment is configured to force a relogin (the return from
// standardEntry() is true) then at some point you will see such
// an exception; we can protect against this, BUT this same behavior
// has been seen in the 4GL, so it has been left here on purpose
// Activates batch mode if exists. If batch mode operation is requested the real work
// should be postponed until all subsystems to be initialized properly using usual
// console mode. At this point this preliminary work should be done and we can
// "activate" the special processing for batch mode.
if (OutputManager.inBatchMode())
{
OutputManager.activateBatch(true);
}
is.driver.clientReady();
ThinClient.getInstance().updateClientMetrics();
// set up current user name for client session
ThinClient.getInstance().setUserName(params.osUserName);
PostInitErrorManager.checkForErrors();
}
/**
* Start the client loop. This will not return until the user is exiting
* the client. The core loop initializes logging, the security manager,
* the UI and then tries to connect to the server. During the connection
* attempt the server will send down a login screen or if guest access is
* enabled, the login will be bypassed. Once login has occurred (or been
* bypassed), the session is established and the directory manager and the
* UI client finish initialization. Finally the main application entry
* point is called on the server. The server will not return from that call
* until the user asks to quit, a CTRL-C occurs (stop condition) or some
* abnormal end occurs. Any of these cases cause the loop to terminate and
* this method will then return.
*
* @param config
* The configuration to use for setup of the client and for the
* server connection.
* @param diag
* Details about the driver state that can help diagnose server startup issues.
* @throws Exception
*/
public static void start(BootstrapConfig config, Supplier<String> diag)
throws Exception
{
// Load native library
loadNativeLibrary();
CentralLoggerClient.setPid(getPid());
CentralLoggerClient.setUserOs(getUserName());
setOutputToFile(config);
String spawnedClientUuid = config.getString(ConfigItem.SPAWNER_UUID, null);
boolean running = true;
// save off the UI driver so the same instance can be used across all
// iterations of the core loop
final ScreenDriver<?> driver = (spawnedClientUuid == null) ?
null :
processTemporaryClient(spawnedClientUuid, config, diag);
if (driver == null)
{
// call as early as possible, for spawned clients it's called in TemporaryClient.doWork
outputDiagnostics(config, diag);
}
while (running)
{
final InitStruct is = new InitStruct();
is.driver = driver;
initialize(is, config, spawnedClientUuid);
// call the application
running = is.main.standardEntry(spawnedClientUuid);
if (!running)
{
if (is.driver.isWeb() && is.freeWebClientResources != null)
{
is.freeWebClientResources.run();
}
}
// dependent threads need to be shutdown, even if the client restarts on CTRL-C
ThinClient.terminate();
if (!running)
{
is.driver.shutdown();
}
// shut down session
CentralLoggerClient.finish();
is.session.terminate();
// clear after me
SecurityManager.clearInstance();
}
}
/**
* Inspect the chain of exceptions and report if any of them are STOP
* conditions.
*
* @param chained
* The top of the chain of exceptions.
*
* @return <code>true</code> if any of the exceptions in the chain are
* STOP conditions.
*/
public static boolean isCauseStop(Throwable chained)
{
boolean isStop = false;
while (chained != null)
{
if (chained instanceof StopConditionException)
{
isStop = true;
break;
}
chained = chained.getCause();
}
return isStop;
}
/**
* Reads server:clientConfig:outputToFile config from BootstrapConfig,
* replaces the placeholder %pid% with the current process id
* and redirects stdout to the file path defined as outputToFile.
*
* @param config
* Reference to the configuration reader.
*/
public static void setOutputToFile(BootstrapConfig config)
{
String outputToFile = config.getString(ConfigItem.OUTPUT_TO_FILE, null);
if (outputToFile == null)
{
return;
}
if (outputToFile.equals("/dev/null"))
{
System.setOut(new PrintStream(new OutputStream()
{
@Override
public void write(int b)
{
}
}));
FileChecker.IS_OUTPUT_REDIRECTED.set(true);
return;
}
try
{
outputToFile = outputToFile.replace("%pid%", String.valueOf(getPid()));
System.setOut(new PrintStream(new FileOutputStream(outputToFile), true));
FileChecker.IS_OUTPUT_REDIRECTED.set(true);
}
catch (Throwable t)
{
LOG.severe("Unable to redirect stdout to outputToFile " + outputToFile);
}
}
/**
* Loads native library and performs required initial steps for P2J library to working.
*/
public static synchronized void loadNativeLibrary()
{
if (!nativeLayerInitialized)
{
System.loadLibrary("p2j");
processInit();
String nativeFWDVersion = getFWDVersion();
String unformattedFWDVersion = Version.getFWDVersionUnformatted();
if (!unformattedFWDVersion.equals(nativeFWDVersion))
{
LOG.severe(String.format(
"Version mismatch detected: Java FWD version '%s' does not match native version '%s'",
unformattedFWDVersion,
nativeFWDVersion));
}
nativeLayerInitialized = true;
}
}
/**
* Native call to set up and redefine signal processing handlers.
*/
public static native void processInit();
/**
* Native call to read the operating system pid for the current JVM process.
*
* @return see above
*/
public static native long getPid();
/**
* Native call to read the operating system user name for the current JVM process.
*
* @return see above
*/
public static native String getUserName();
/**
* Native call to read the FWD version embedded during native code compilation.
*
* @return see above
*/
public static native String getFWDVersion();
/**
* Render the diagnostics as text and output this to the log.
*
* @param config
* The bootstrap configuration for this client.
* @param diagnostics
* Details about the client process.
*/
static void outputDiagnostics(BootstrapConfig config, Supplier<String> diagnostics)
{
if (LOG.isLoggable(Level.INFO))
{
LOG.log(Level.INFO, diagnostics.get());
LOG.log(Level.INFO, "BootstrapConfig " + config.toString());
}
}
/**
* Build a separate BootstrapConfig for temporary clients.
*
* @param config
* Client bootstrap configuration.
*
* @return A new BootstrapConfig configured for temporary client connections.
*/
private static BootstrapConfig buildTemporaryClientConfig(BootstrapConfig config)
{
try
{
// Create web client bootstrap configuration.
BootstrapConfig bc = new BootstrapConfig();
bc.setServer(false);
// Get user credentials from environment
String subject = System.getenv(TemporaryAccount.P2J_SUBJECT);
String password = System.getenv(TemporaryAccount.P2J_PASSWORD);
// Add web client configuration.
bc.setConfigItem("security", "trust_mgr", "disable", "true");
bc.setConfigItem("net", "queue", "conversation", "true");
bc.setConfigItem("net", "queue", "start_thread", "false");
bc.setConfigItem("security", "certificate", "validate", "false");
bc.setConfigItem("security", "authentication", "type", "program");
bc.setConfigItem("access", "subject", "id", TemporaryAccount.fromHex(subject));
bc.setConfigItem("access", "password", "user", TemporaryAccount.fromHex(password));
bc.setConfigItem(ConfigItem.SERVER_SOCKET_NIO,
String.valueOf(config.getBoolean(ConfigItem.SERVER_SOCKET_NIO, false)));
bc.setConfigItem(ConfigItem.SERVER_SSL_TRACKSEQNO,
String.valueOf(config.getBoolean(ConfigItem.SERVER_SSL_TRACKSEQNO, false)));
String[] copy =
{
ConfigItem.SECURE.fullOption(),
ConfigItem.SERVER_HOST.fullOption(),
ConfigItem.SERVER_SECURE_PORT.fullOption(),
ConfigItem.SERVER_INSECURE_PORT.fullOption(),
ConfigItem.SERVER_PORT.fullOption(),
ConfigItem.SPAWNER_UUID.fullOption(),
ConfigItem.SSO_SUBJECT_ID.fullOption(),
ConfigItem.WEB_SOCKET_TIMEOUT.fullOption(),
ConfigItem.WATCHDOG_TIMEOUT.fullOption(),
ConfigItem.PORT.fullOption(),
ConfigItem.HOST.fullOption(),
};
bc.copyConfigItems(copy, config);
return bc;
}
catch (ConfigurationException e)
{
throw new RuntimeException(e);
}
}
/**
* Pull the client parameters from the driver and bootstrap configuration and initialize
* a parameters object with the resulting data.
*
* @param driver
* The primary UI driver for this client session.
* @param config
* The bootstrap configuration for this session (this must have been previously
* merged with any command line overrides).
*
* @return The configured parameters object.
*/
private static ClientParameters processClientParams(ScreenDriver<?> driver, BootstrapConfig config)
{
ClientParameters cp = new ClientParameters();
String win = driver.getWindowSystem();
cp.windowSystem = config.getString("client", "driver", "window-system", win);
cp.osUserName = getUserName();
cp.terminalName = config.getString("client", "tty", "name", null);
cp.pid = getPid();
cp.driverName = driver.getName();
cp.osDeviceId = getOsDeviceId();
cp.storageId = config.getString(ConfigItem.SSO_STORAGE_ID, null);
cp.webPort = config.getInt(ConfigItem.PORT, 0);
cp.chui = driver.isChui();
cp.web = driver.isWeb();
cp.embedded = driver.isEmbeddedClient();
cp.batchMode = config.getBoolean("client", "mode", "batch", false) ? 2 : 0;
cp.isHeadless = GraphicsEnvironment.isHeadless();
cp.socketTrustStoreFilename = config.getString(ConfigItem.SOCKET_TRUSTSTORE_FILE, "trusted-cert.store");
cp.socketTrustStorePassword = config.getString(ConfigItem.SOCKET_TRUSTSTORE_PASS, "");
cp.propath = config.getString("client", "cmd-line-option", "propath", null);
cp.clientTimezone = getTimezone();
if (driver.isWeb())
{
BrowserDetails browserDetails = ((ScreenWebDriver) driver).getBrowserDetails();
cp.browserWebSocket = browserDetails.browserWebSocket;
cp.browserTimezone = browserDetails.browserTimezoneOffset;
cp.ipCaptureTimestamp = browserDetails.ipCaptureTimestamp;
cp.screenColorDepth = browserDetails.screenColorDepth;
cp.screenHeight = browserDetails.screenHeight;
cp.screenScalingFactor = browserDetails.screenScalingFactor;
cp.screenWidth = browserDetails.screenWidth;
cp.userAgent = browserDetails.userAgent;
cp.serverBaseUrl = config.getString(ConfigItem.SERVER_BASE_URL, null);
cp.webRoot = config.getString(ConfigItem.WEB_ROOT, "");
}
else if (!cp.isHeadless)
{
Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
cp.screenHeight = (int) screenSize.getHeight();
cp.screenWidth = (int) screenSize.getWidth();
// same as Toolkit.getDefaultToolkit().getScreenResolution() / 96f
// doesn't reflect display scale in Linux
cp.screenScalingFactor = (float) GraphicsEnvironment.getLocalGraphicsEnvironment()
.getDefaultScreenDevice()
.getDefaultConfiguration()
.getDefaultTransform()
.getScaleX();
cp.screenColorDepth = !PlatformHelper.isUnderWindowsFamily() ?
getLinuxScreenColorDepth() :
// returns DisplayMode.BIT_DEPTH_MULTI (-1) for linux
GraphicsEnvironment.getLocalGraphicsEnvironment()
.getDefaultScreenDevice()
.getDefaultConfiguration()
.getDevice()
.getDisplayMode()
.getBitDepth();
}
return cp;
}
/**
* Establish a direct connection using the temporary credentials and perform custom work,
* depending on the {@link TemporaryClient} implementation returned by
* {@link Spawner#getTemporaryClient}.
*
* @param uuid
* Remote client identifier uuid.
* @param config
* The configuration to use for setup of the client and for the server connection.
* @param diag
* Details about the driver state that can help diagnose server startup issues.
*
* @return The screen driver returned by {@link TemporaryClient#doWork}.
*
* @throws Exception
* If something goes wrong.
*/
public static ScreenDriver<?> processTemporaryClient(String uuid,
BootstrapConfig config,
Supplier<String> diag)
throws Exception
{
ScreenDriver<?> driver = null;
// Create a temporary session
BootstrapConfig bc = buildTemporaryClientConfig(config);
@SuppressWarnings("unused")
SecurityManager sm = SecurityManager.createInstance(bc);
SessionManager sessMgr = SessionManagerFactory.createLeafNode(bc);
Session session = sessMgr.connectDirect(new InterruptedExceptionHandler(), null);
DirectoryManager.initRemote(session);
try
{
// Obtain Spawner remote access interface
Spawner spawner = (Spawner) RemoteObject.obtainNetworkInstance(Spawner.class, session);
if (spawner != null)
{
String projectToken = config.getString(ConfigItem.PROJECT_TOKEN, null);
TemporaryClient tempClient = spawner.getTemporaryClient(uuid, projectToken);
driver = tempClient.doWork(spawner, config, diag);
}
else
{
throw new RuntimeException(String.format("Network server %s not found!",
Spawner.class.getName()));
}
}
finally
{
// Terminate temporary session.
session.terminate();
}
return driver;
}
/**
* Returns the OS specific device ID.
*
* @return The found device ID.
*/
public static String getOsDeviceId()
{
String deviceId = null;
try
{
if (!PlatformHelper.isUnderWindowsFamily())
{
// in the format 9f5c9c65b34d4055a746f69bd86a4b8b
String linuxCmd = "head /etc/machine-id";
Process process = Runtime.getRuntime().exec(linuxCmd);
Utils.logProcessError(LOG, process, linuxCmd);
Scanner resultScanner = new Scanner(new InputStreamReader(process.getInputStream()));
deviceId = resultScanner.hasNext() ? resultScanner.nextLine() : null;
}
else
{
// in the format 18B1FBB5-2728-701A-A89A-D8BBC19F009C
String winCmd = "wmic csproduct get UUID";
Process process = Runtime.getRuntime().exec(winCmd);
Utils.logProcessError(LOG, process, winCmd);
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
deviceId = reader.lines()
.map(line -> line.replaceAll("\\s", "").replace("UUID", ""))
.reduce("", String::concat);
}
}
catch (Exception e)
{
LOG.warning("Unable to find device ID.", e);
}
return deviceId;
}
/**
* Returns the the offset in minutes from UTC for the client timezone.
*
* @return See above.
*/
public static Integer getTimezone()
{
Calendar now = Calendar.getInstance();
return (now.get(Calendar.ZONE_OFFSET) + now.get(Calendar.DST_OFFSET)) / (60 * 1000);
}
/**
* Returns the found screen color depth in Linux.
*
* @return See above.
*/
private static int getLinuxScreenColorDepth()
{
try
{
String linuxCmd = "xwininfo -root";
Process process = Runtime.getRuntime().exec(linuxCmd);
Utils.logProcessError(LOG, process, linuxCmd);
Scanner scanner = new Scanner(new InputStreamReader(process.getInputStream()));
while (scanner.hasNext())
{
String line = scanner.nextLine();
if (line.contains("Depth:"))
{
String colorDepthString = line.replace("Depth:", "").trim();
return Integer.parseInt(colorDepthString);
}
}
}
catch (Exception e)
{
LOG.warning("Unable to find screen color depth in Linux.", e);
}
return -1;
}
}