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