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