# StandardServer.java

    /*
    ** Module   : StandardServer.java
    ** Abstract : standard server application
    **
    ** Copyright (c) 2005-2025, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- --JPRM-- ---------------------------------Description-------------------------------------
    ** 001 NVS 20050831   @22451 Created the initial version.
    ** 002 NVS 20050915   @22735 Alternative path /server/default/exports is
    **                           tried if no /server/serverID/exports exists.
    **                           Standard entry point processing added, based
    **                           on directory entries under /server/serverID/
    **                           entry/accountID.
    ** 003 NVS 20051221   @23733 Saves the reference to the queue in the session
    **                           context so it can be accessed at any time.
    ** 004 GES 20060110   @23850 Implemented proper transaction manager
    **                           infrastructure and init such that converted
    **                           P2J applications can be run with this
    **                           class as their launch point.
    ** 005 ECF 20060114   @23904 Moved instantiation of target class to the
    **                           invoke() method. This is necessary to include
    **                           constructors/initializers within the first
    **                           TransactionManager scope created. Modified
    **                           various catch statements to not discard stack
    **                           traces of caught exceptions.
    ** 006 GES 20060130   @24143 Added quit state flag processing in invoke.
    ** 007 GES 20060202   @24229 Added new flag in pushScope() signature.
    ** 008 GES 20060203   @24244 Changed TransactionManager method name
    **                           makeBackup() to blockSetup().
    ** 009 GES 20060210   @24528 Shifted to a simpler approach for server
    **                           exports. First pass (more is coming later).
    ** 010 NVS 20060226   @24750 Made portions of invoke() reusable in Utils.
    **                           Calling reusable invoke() method from here.
    ** 011 GES 20060305   @24891 Cleaned up logging.
    ** 012 NVS 20060306   @24901 Added exports registration for ErrorManager.
    ** 013 GES 20060314   @25062 Fixed exception processing to properly retry on STOP.
    ** 014 NVS 20060417   @25578 Added support for the password aging. Before
    **                           the configured application entry point is
    **                           taken, the user account is checked for the
    **                           aged password, and, if so, the password
    **                           change method is called..
    ** 015 GES 20060608   @27054 Added logging of any abnormal end before it
    **                           is re-thrown.  ConditionExceptions and
    **                           RetryUnwindExceptions are not logged as these
    **                           are "normal" transaction manager flow of
    **                           control interactions.
    ** 016 NVS 20060614   @27196 Fixed a problem with NET package exports of
    **                           SecurityManager class.
    **                           Added a call to RemoteObject to do deferred
    **                           registrations.
    ** 017 GES 20060804   @28432 Honor new iterate() processing which only
    **                           happens at the top of the block body.
    ** 018 NVS 20060617   @30461 Implemented stop_disposition configuration
    **                           option reading and honoring when the business
    **                           logic encounters STOP condition.
    ** 019 GES 20061212   @31666 Force "quit" processing in any fatal abend
    **                           (something other than one of the "expected"
    **                           4GL conditions).  This avoids a call to
    **                           LogicalTerminal.pauseBeforeEnd() which will
    **                           fail in any case where we are exiting because
    **                           the client was forcefully disconnected.
    ** 020 GES 20061219   @31718 Removed dead code.
    ** 021 GES 20070111   @31782 Reworked notifiable interface as needed.
    ** 022 NVS 20070126   @32004 Application entry point is now the subject to
    **                           the standardized search in the directory.
    **                           The node is named "p2j-entry" to separate it
    **                           from the server level entry point.
    ** 023 NVS 20070221   @32381 Administration Server exports are registered.
    ** 024 EVL 20070609   @34005 Adding explicit import of the class
    **                           com.goldencode.p2j.net.Queue to eliminate
    **                           conflict with the same class from java.util
    **                           package to be able to compile for Java 6
    ** 025 GES 20071011   @35424 Eliminated compile warning.
    ** 026 ECF 20071112   @35892 Refactored to leverage net package changes.
    **                           Replaced initialize() with bootstrap(). All
    **                           non-network function that was previously in
    **                           ServerBootstrap is now in this class, while
    **                           the network-specific function is now handled
    **                           by the net package's SessionManager.
    ** 027 GES 20071214   @36410 Added code to translate unexpected exceptions
    **                           (but not errors) into a stop condition. This
    **                           will avoid the silent drop-out to the command
    **                           line unless the stop disposition is also
    **                           configured to silently exit (disposition 2).
    ** 028 GES 20080409   @37912 Export remote directory access when the network protocol is ready.
    ** 029 NVS 20090312   @41522 Added initialization of the AdminServerImpl class.
    ** 030 SIY 20090507   @42111 Added support for context hooks.
    ** 031 SIY 20090511   @42142 Added support for server hooks.
    ** 032 SIY 20090512   @42144 Reworked server hooks to support more than one server hook.
    ** 033 SIY 20090515   @42188 All services now are started as server hooks and hardcoded.
    ** 034 NVS 20090528   @42482 Custom account extension plugin is loaded at startup, if defined
    **                           in the directory
    ** 035 GES 20090722   @43331 Handle chained errors as errors not exceptions.
    ** 036 GES 20090723   @43359 Avoid logging STOP conditions.
    ** 037 NVS 20090817   @43684 Server hook load failures are logged but are not critical for the
    **                           server startup.
    ** 038 NVS 20090818   @43690 Excluded the exception stack trace from the error message in #037.
    ** 039 NVS 20090821   @43711 Admin Enabled flag is now searched correctly scoped to the per
    **                           server scope.
    ** 040 NVS 20090826   @43775 Passing server name to the LogHelper.initialize().
    ** 041 ECF 20090827   @43787 Fixed logging to file. SecureFileHandler is now used when
    **                           initializing the LogHelper, and is closed when the server is
    **                           finished shutting down.
    ** 042 CA  20090901   @43809 On bootstrap, SessionManager will be set a field which contains the
    **                           InterruptHandler implementation.
    ** 043 CA  20090917   @43927 Fix abnormal connection end. In invoke(), any SilentUnwindExceptions
    **                           will stop the Conversation thread.
    ** 044 NVS 20090918   @43950 Reworked invoke() method to take an instance of Isolatable interface
    **                           to allow the reuse of invoke for admin server purposes where some
    **                           methods have to run in a transaction manager environment to be able
    **                           to access the database properly, including modifications. Reworked
    **                           standardEntry() method to comply with the new requirements; added
    **                           the inner class MainInvoker that encapsulates the client entry
    **                           specific logic.
    ** 045 NVS 20090928   @44048 Server startup is protected against non-existent directory.
    ** 046 LMR 20101210          Edited bootstrap() and registerDefaultServices() to initialize the
    **                           AdminGate service only if net/connection/secure is set to true in
    **                           the bootstrap config (if not found it defaults to false).
    ** 047 GES 20111004          Reworked the web server startup to more generically support multiple
    **                           applets (not just admin alone).
    ** 048 GES 20111025          Signature changes in calling RemoteObject to export methods. Added
    **                           waitUntilReady().
    ** 049 CA  20111130          Use the system classloader to load hooks, as Class.forName caches
    **                           the loaded class in some native code from which it can never be
    **                           removed. Before initializing hooks and exports, create class loaders
    **                           for all the jars defined in the directory.
    ** 050 CA  20130529          Added appserver support.
    ** 051 CA  20130705          Register the appserver launcher.
    ** 052 SVL 20130624          Set runtime Configuration to be used.
    ** 053 HC  20130902          For #2164 added warning log when server persistence is inactive.
    ** 054 CA  20131004          Expose client-side parameter to server-side application code.
    ** 055 CA  20131023          At runtime, the in-memory registry plugin must be used.
    ** 056 MAG 20131101          Upgrade to jetty 9.1 embedded server.
    ** 057 MAG 20131114          Add Handler for Chui Web client.
    ** 058 OM  20131210          Made DatabaseTriggerManager Scopable context-local instantiation.
    ** 059 EVL 20131115          Adding UnstoppableExitException handling for unrecoverable errors in
    **                           batch mode. The serve side of the ErrorManages is also initialized
    **                           here.
    ** 060 MAG 20131223          Get Chui Web client target root from directory. Initialize temporary
    **                           accounts pool.
    ** 061 MAG 20140128          Change the main return type from boolean to Result. For web
    **                           clients provide temporary account credentials.
    ** 062 MAG 20140131          Reverse changes at 061 (2014-01-28).
    ** 063 CA  20140206          Added scheduler support.
    ** 064 MAG 20140217          Added terminate method to web server hooker.
    ** 065 EVL 20140327          Adding the support to get the server properties to client.
    ** 066 ECF 20140404          Initialize conversion pool during bootstrap.
    ** 067 ECF 20140430          Removed stack trace from conversion pool initialization warning.
    **                           Enhanced warning message and added debug-level logging to provide
    **                           more information and stack trace.
    ** 068 MAG 20140707          Implements remote launcher (broker). Exports broker API.
    ** 069 GES 20141229          Added support for user-specified entry points.
    ** 070 GES 20150310          Made web client support more generic (it isn't chui-specific now).
    ** 071 CA  20150622          Context-local client parameters (localParams field) must not be reset.
    **                           Added database statistics collection (ECF).
    ** 072 CA  20160205          Added ServerKeyStore.initialize() at the server startup hooks.
    ** 073 IAS 20160329          Provide client-side parameters in a separate call
    ** 074 GES 20160121          Added WebClientLauncher service registration.
    ** 075 OM  20160422          Added project token implementation for multiple project support.
    ** 076 OM  20160527          SourceNameMapper.convertName() changed to convertNameToClass().
    ** 077 OM  20160927          Activated BatchMode when expressly requested by client.
    ** 078 HC  20170118          FWD version is printed to the log output on server start.
    ** 079 CA  20170228          Added PUBLISH/SUBSCRIBE/UNSUBSCRIBE extensions for global support and
    **                           for external applications.
    ** 080 SBI 20170628          Added sessions listener to manage web client sessions.
    ** 081 HC  20170612          Changes related to implementation of new GWT-based Admin client.
    ** 082 ECF 20180615          Read list of jars containing application resources from the
    **                           directory during bootstrap, so resource searches can be limited to
    **                           these jars.
    ** 083 CA  20180724          Allow the embedded web app server to run from within FWD.
    ** 084 CA  20190611          Added support for legacy services (REST).
    ** 085 CA  20190703          Added support for WebHandler services.
    ** 086 ECF 20190521          Register a controller at server startup for method execution tracing.
    ** 087 CA  20190710          The legacy services need to be registered after their handlers have
    **                           been initialized.
    ** 088 GES 20200213          Bypass termination of embedded mode, REST services and the web
    **                           handler when if they were never initialized.
    ** 089 CA  20200514          Added support for SOAP web services.
    **     CA  20200527          Allow the project token to be overridden via a client:project:token setting.
    ** 090 CA  20200915          Upgraded to Jetty 9.4.22.
    ** 091 CA  20200930          Force ANTLR to use Class.forName instead of loadClass.
    ** 092 SBI 20210413          Added web content handlers to public static content.
    **     CA  20210511          The client's entry point (be it from directory's 'p2j-entry' or the client's
    **                           startup-procedure configuration) will be executed as a "RUN external-program"
    **                           statement, if the target is an external program which can be resolved.
    **     CA  20210512          Refactored CA/20210511 so that the legacy RUN emulation and the old 'execute'
    **                           method are separated.
    **     IAS 20210520          Added support for session-based database auto-connect
    **     HC  20210725          Added support for v6colon.
    **     GES 20210827          Added driver-level diagnostics information logged at server startup.
    **     SBI 20210923          Added FontTable initialization.
    **     OM  20190620          Added -profile command line option for specifying the configuration profile.
    **     CA  20210928          Set the temp directory for the FWD admin web app.
    **     CA  20211114          The InMemoryRegistryPlugin instance at the AstManager must be context-local, for
    **                           runtime conversion to work in concurrent mode.
    **     CA  20220405          Added authentication and authorization for web requests.  When this is enabled,
    **                           the target API call will be executed under the authenticated FWD context, and not
    **                           the agent's context.
    **     CA  20220901          Refactored scope notification support: ScopeableFactory was removed, and the
    **                           registration is now specific to each type of scopeable.  For each case, the block
    **                           will be registered for scope support (for that particular scopeable) only when
    **                           the scopeable is 'active' (i.e. unnamed streams or accumulators are used).  This
    **                           allows a lazy registration of scopeables, to avoid the unnecessary overhead of
    **                           processing all the scopeables for each and every block.
    **     CA  20220906          Moved the interactive client cleaner to LogicalTerminal.registerCleaner().
    **     TJD 20220504          Migration to Java 11 minor changes
    **     SBI 20221215          Changed log message for loadStartup to specify the failure cause.
    **     CA  20220501          Each profile has the same structure as the main 'global' config.  Allow multiple
    **                           profiles to be ran at once, with the conversion switching the state between
    **                           profiles, when a resource (like a file or namespace) is being processed.  Only
    **                           front phase is supported at this time.
    **     CA  20220520          Initialize SourceNameMapper at server startup.
    ** 093 VVT 20230318          UnitTestEngine static network server registration added. See #6237.
    ** 094 GBB 20230412          Initialize LegacyLogManager.
    ** 095 GBB 20230608          Added a flag to LegacyLogManagerConfigs for server-side filesystem in use.
    ** 096 CA  20230605          In FWD admin console, customer extension code can emulate 4GL-style code which
    **                           uses RecordBuffer definitions; in such cases, these need to be declared and
    **                           initialized as if the 4GL program is instantiated.  For this reason, a new
    **                           version of 'invoke' was added, which receives as arguments, beside the surrogate
    **                           program name and defining Java class, a Block instance, where the init() method
    **                           specifies the variable initialization code, and the body() method contains the
    **                           4GL-style compatible code to be ran (wrapped in an externalProcedure).
    ** 097 CA  20230620          Initialize the app_services configuration for remote appservers on server startup,
    **                           which will prevent the FWD server to start if there are configuration errors.
    ** 098 DDF 20230627          Initialized configurations at server bootstrap.
    **     DDF 20230627          Removed DatabaseStatistics.get() call.
    **     DDF 20230705          Initialize FileSystemOps and SecurityOps configurations at server bootstrap.
    ** 099 TT  20230621          Update the StartupParameters using the DirectoryService. Store them in the
    **                           SessionUtils server-side context-local.
    **     TT  20230626          Pass null as an extra parameter for getOrCreateTemporaryDirectory() to keep the
    **                           same logic for the server side.
    ** 100 TT  20230808          Read cmd-line-options from StartupParameters instead of ClientParameters.
    ** 101 GBB 20230825          Stricter conditions for registering embedded / virtual desktop handlers.
    ** 102 GBB 20231114          clientUuid added to standardEntry(). New method setupClient().
    ** 103 GBB 20231124          Session description and related session id resolved only for web.
    ** 104 TT  20231129          Set the propath when the ClientParameters are received from the client.
    ** 105 GBB 20231130          ClientParameters.driverClass replaced by driverName.
    ** 106 RAA 20231010          Removed DatabaseStatistics usage.
    **     RAA 20231122          Removed terminate() method from registerDefaultServices.
    ** 107 GBB 20240214          Setting SESSION:DEVICE-ID in setupClient (post-init). Support for multiple session
    **                           InitTermListener hooks. One Browser Policy validation.
    ** 108 CA  20240308          Extracted the client session initialization into a separate method, to be used by
    **                           the unit test engine.
    ** 109 GBB 20240314          setupClient() renamed to setupSession().
    **                           Set multiple client params to session. Set screen params for ui clients.
    ** 110 HC  20240513          Moved SPREADSHEET backend (Keikai web application and the related
    **                           FWD integration) from client to server.
    ** 111 GBB 20240515          Set login params to session.
    ** 112 GBB 20240613          Add check for expiring OS pass before starting 4GL execution.
    ** 113 GBB 20240618          Setting client type.
    ** 114 ICP 20240705          Fixed method of obtaining the class loader.
    ** 115 GBB 20240709          Hard-coded config names replaced by ConfigItem constants. Server hook for
    **                           WebClientBuilderOptions.initialize removed, instead options resolved in
    **                           WebDriverHandler.
    ** 116 GBB 20240729          Initialize CentralLogger after SecurityManager is created.
    ** 117 GBB 20240808          Calling SsoAuthenticator.readDirectory after DB is loaded.
    ** 118 RAA 20240604          Added support for TenantHandler.
    ** 119 AD  20240403          Added hook for server shutdown to call DynamicTablesHelper.createCache().
    ** 120 ICP 20240719          Modified server hook that call DynamicTablesHelper.createCache() to ensure that
    **                           logs are properly managed.
    ** 121 ICP 20240722          Modified shutdown hook registration.
    ** 122 GBB 20240826          Setting storageId to SessionUtils.
    ** 123 GBB 20240912          OSResourceManager method isServerSide to use an enum param.
    **                           Don't restart (stop_disposition=1) on procedure not found.
    ** 124 GBB 20241010          Initialize all appserver definitions on server start.
    ** 125 RNC 20250108          Added support for SessionHandler.
    ** 126 FER 20250127          Assign a default output for batch programs. Currently a NullStream
    **                           placeholder. We need a StdioStream class.
    ** 127 ICP 20250212          Added bootstrap initialization for DynamicTablesHelper.
    ** 128 ICP 20250212          Added server hook to initialize the CacheManager.
    ** 129 OM  20250211          Renamed TenantHandler to AdminHandler.
    **     OM  20250320          Big refactoring. AdminRestHandler is the only handler to be loaded. It will
    **                           automatically handle the initialization and execution of all REST handlers.
    ** 130 GBB 20250403          Initialize all appserver definitions before scheduled launch gets triggered.
    ** 131 ES  20250424          Handle StopConditionException through ErrorManager.
    */

    /*
    ** This program is free software: you can redistribute it and/or modify
    ** it under the terms of the GNU Affero General Public License as
    ** published by the Free Software Foundation, either version 3 of the
    ** License, or (at your option) any later version.
    **
    ** This program is distributed in the hope that it will be useful,
    ** but WITHOUT ANY WARRANTY; without even the implied warranty of
    ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    ** GNU Affero General Public License for more details.
    **
    ** You may find a copy of the GNU Affero GPL version 3 at the following
    ** location: https://www.gnu.org/licenses/agpl-3.0.en.html
    **
    ** Additional terms under GNU Affero GPL version 3 section 7:
    **
    **   Under Section 7 of the GNU Affero GPL version 3, the following additional
    **   terms apply to the works covered under the License.  These additional terms
    **   are non-permissive additional terms allowed under Section 7 of the GNU
    **   Affero GPL version 3 and may not be removed by you.
    **
    **   0. Attribution Requirement.
    **
    **     You must preserve all legal notices or author attributions in the covered
    **     work or Appropriate Legal Notices displayed by works containing the covered
    **     work.  You may not remove from the covered work any author or developer
    **     credit already included within the covered work.
    **
    **   1. No License To Use Trademarks.
    **
    **     This license does not grant any license or rights to use the trademarks
    **     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
    **     of Golden Code Development Corporation. You are not authorized to use the
    **     name Golden Code, FWD, or the names of any author or contributor, for
    **     publicity purposes without written authorization.
    **
    **   2. No Misrepresentation of Affiliation.
    **
    **     You may not represent yourself as Golden Code Development Corporation or FWD.
    **
    **     You may not represent yourself for publicity purposes as associated with
    **     Golden Code Development Corporation, FWD, or any author or contributor to
    **     the covered work, without written authorization.
    **
    **   3. No Misrepresentation of Source or Origin.
    **
    **     You may not represent the covered work as solely your work.  All modified
    **     versions of the covered work must be marked in a reasonable way to make it
    **     clear that the modified work is not originating from Golden Code Development
    **     Corporation or FWD.  All modified versions must contain the notices of
    **     attribution required in this license.
    */

    package com.goldencode.p2j.main;

    import java.io.*;
    import java.lang.reflect.*;
    import java.util.*;
    import java.util.concurrent.*;
    import java.util.function.*;
    import java.util.logging.*;
    import javax.servlet.*;
    import com.goldencode.p2j.translation.*;
    import com.goldencode.p2j.ui.client.driver.web.*;
    import com.goldencode.p2j.util.appserver.*;
    import com.goldencode.p2j.util.osresource.*;
    import com.goldencode.util.*;
    import org.eclipse.jetty.server.handler.*;
    import org.eclipse.jetty.util.resource.Resource;
    import org.eclipse.jetty.webapp.*;
    import com.goldencode.ast.*;
    import com.goldencode.p2j.*;
    import com.goldencode.p2j.admin.*;
    import com.goldencode.p2j.admin.server.*;
    import com.goldencode.p2j.aspects.ltw.*;
    import com.goldencode.p2j.cfg.*;
    import com.goldencode.p2j.cfg.Configuration;
    import com.goldencode.p2j.classloader.*;
    import com.goldencode.p2j.directory.*;
    import com.goldencode.p2j.net.*;
    import com.goldencode.p2j.persist.*;
    import com.goldencode.p2j.persist.trigger.*;
    import com.goldencode.p2j.reporting.*;
    import com.goldencode.p2j.rest.*;
    import com.goldencode.p2j.rest.serializers.JavaTypeSerializer;
    import com.goldencode.p2j.scheduler.*;
    import com.goldencode.p2j.schema.TableHints;
    import com.goldencode.p2j.security.*;
    import com.goldencode.p2j.security.SecurityManager;
    import com.goldencode.p2j.soap.*;
    import com.goldencode.p2j.testengine.*;
    import com.goldencode.p2j.ui.*;
    import com.goldencode.p2j.util.*;
    import com.goldencode.p2j.util.logging.*;
    import com.goldencode.p2j.util.ErrorManager;

    /**
     * Standard server application which is driven by the P2J directory contents.
     * <p>
     * In particular, exported entry points are read from directory objects
     * /server/serverID/exports/groupID/methodID where methodID is a named string
     * object. The name of this object is the well known exported method name.
     * The "value" attribute is a string encoding fully qualified class name,
     * method name and, possibly, the signature as in class.method(signature)
     * <p>
     * The directory also encodes the standard entry point of the P2J converted
     * application which is to be invoked.  This class provides the mechanism
     * to look up this entry point and invoke it (see {@link #standardEntry} and
     * {@link #invoke}).
     */
    public class StandardServer
    {
       /** Logger (this is JVM-wide rather than being context-local). */
       private static final CentralLogger LOG =
          CentralLogger.get(StandardServer.class.getName());

       /** File name extension for jar files */
       private static final String JAR_EXT = ".jar";

       /** Name of P2J jar */
       private static final String P2J_JAR = "p2j.jar";

       /** Context-local client-side parameters exposed to the server. */
       private static final ContextLocal<ClientParameters> localParams = new ContextLocal<ClientParameters>()
       {
          protected boolean isResetAllowed()
          {
             return false;
          }
       };

       /** Array of paths to jars in the classpath which hold application resources */
       private static List<String> resourceJarPaths;

       /** Lock object for shutdown synchronization. */
       private static final Object shutdownLock = new Object();

       /** Flag which is set to <code>true</code> when shutdown is done. */
       private static boolean shutdownDone = false;

       /** Server hooks. */
       private static final ArrayList<InitTermListener> serverHooks = new ArrayList<>();

       /** Global session listeners. */
       private static final List<InitTermListener> sessionListeners = new CopyOnWriteArrayList<>();

       /** Executor service for concurrent client calls.  */
       private static final ExecutorService oneBrowserChecksExecutor = Executors.newCachedThreadPool();

       /** Map that keeps instantiated classes by name. */
       private final Map<String, Object> inst = new HashMap<>();

       /** Signals when the non-network portions of the server are ready. */
       private final CountDownLatch ready = new CountDownLatch(1);

       /** Admin extension. */
       private AdminServerExtension adminServerExtension;

       /**
        * Set the client parameters.
        *
        * @param   params
        *          A container with any client-related parameters.
        *
        */
       public static void setClientParams(ClientParameters params)
       {
          localParams.set(params);
          EnvironmentOps.setSearchPath(params.propath);
       }

       /**
        * The method loads client startup parameters from the directory.
        *
        * @param startupParameters
        *    is the object passed from the client
        *
        * @return Object holding the client startup parameters.
        */
       public static StartupParameters initializeStartupParameters(StartupParameters startupParameters)
       {
          startupParameters.initFromDirectory();
          SessionUtils.setStartupParameters(startupParameters);

          return startupParameters;
       }

       /**
        * Set the project token to the server-side.
        *
        * @param   token
        *          the token that identifies the project.
        *
        * @see   Utils#setProjectToken(String)
        */
       public static void setProjectToken(String token)
       {
          // access the directory service
          DirectoryService ds = DirectoryService.getInstance();

          if (!ds.bind())
          {
             throw new RuntimeException("directory bind failed");
          }

          // set the token before we do directory lookup
          if (token == null || token.isEmpty())
          {
             // client's client:project:token overrides any directory setting.
             token = Utils.getDirectoryNodeString(ds, "project_token", null);
          }

          if (token != null && !token.isEmpty())
          {
             Utils.setProjectToken(token);
             // some settings could have been already set at a previous moment when project_token was
             // not available yet
             EnvironmentOps.setSearchPath(Utils.getDirectoryNodeString(ds, "searchpath", "."), false);
             // TODO: update other settings
          }

          ds.unbind();
       }

       /**
        * Initialize the state of this client session.  Appserver agents can not be initialized using this call.
        */
       public static void initializeClientSession()
       {
          initializeClientSession(false, null);
       }

       /**
        * Serves as a standard transaction entry point to be called from the client which reroutes control to a
        * specific entry point which is defined in the directory.
        * <p>
        * The entry point definition is the subject to a standardized directory lookup, which allows server or
        * account specific definitions as well as server-wide or system-wide defaults.
        * <p>
        * p2j-entry can refer to a public method of the class. It can be either a static or an instance method
        * taking no arguments. The class is instantiated before calling the named method. The returned object, if
        * any, is discarded.
        * <p>
        * If the resolved class name is mapped to a legacy external program, and the method's name is
        * <code>execute</code>, the {@link LegacyInvoker} will be used to execute the program.  Otherwise, the
        * {@link MainInvoker} will invoke the method, which will bypass the normal state processing for a legacy
        * external program.
        * <p>
        * The {@link #invoke} method is the worker that actually handles the method's invocation using reflection.
        * <p>
        * If directory contains definition for context hook, then it is instantiated. Hook definition directory
        * entry is searched using the same rules as application entry point (refer to
        * {@link Utils#findDirectoryNodePath(DirectoryService, String, String, boolean)} for more details),
        * appropriate variable has name <b>context-hook</b>.
        * Context hook class must implement {@link InitTermListener} interface.
        *
        * @param   spawnedClientUuid
        *          The UUID of the spawned client.
        *
        * @return  <code>true</code> if re-logon should be performed.
        */
       public static boolean standardEntry(String spawnedClientUuid)
       {
          if (!initializeClientSession(true, spawnedClientUuid))
          {
             return false;
          }

          // access the directory service
          DirectoryService ds = DirectoryService.getInstance();

          if (!ds.bind())
          {
             throw new RuntimeException("directory bind failed");
          }

          // get the entry point method definition and any configured session hooks; these values
          // need no security check since the directory is an inherently trusted source; anyone that
          // can control the directory can control everything about the server so there is no
          // security check that can make this any better here
          String entryPath = Utils.findDirectoryNodePath(ds, "p2j-entry", "string", true);
          String method    = Utils.getDirectoryNodeString(ds, "p2j-entry", "");

          // obtain the STOP disposition parameter:
          // 0 - retry without logging off
          // 1 - force logoff and then re-logon
          // 2 - force logoff
          int stopDisp = Utils.getDirectoryNodeInt(ds, "stop_disposition", 1);

          if (stopDisp < 0 || stopDisp > 2)
          {
             LOG.warning("stop_disposition configuration item is rejected: " + stopDisp);
             stopDisp = 1;
          }

          // last use of the directory, end our session with it
          ds.unbind();

          DatabaseManager.autoConnect();

          // these are used only for a 'com.foo.Bar.execute' p2j-entry case
          String className  = null;
          String methodName = null;

          Isolatable invoker = null;

          StartupParameters startupParameters = SessionUtils._startupParameters();
          String startupProcedure = startupParameters.getStartupProcedure();
          if (StringHelper.hasContent(startupProcedure))
          {
             SecurityManager sm = SecurityManager.getInstance();

             // this is emulated via a RUN always, and that will take care to resolve any errors if the program is
             // not found.  just do the ACL check here.
             EntryPointResource epr = (EntryPointResource) sm.getPluginInstance("entrypoint");

             // security check is needed here because this data is user-supplied (which is an untrusted source)
             if (epr == null || !epr.isAllowed(startupProcedure))
             {
                if (LOG.isLoggable(Level.WARNING))
                {
                   if (epr == null)
                   {
                      String spec1 = "User-specified startup program '%s' cannot be accessed " +
                                     "because the EntryPointResource is NOT enabled.";

                      LOG.log(Level.WARNING, spec1, startupProcedure);
                   }
                   else
                   {
                      String spec2 = "Access to user-specified startup program '%s' not " +
                                     "allowed for user %s.  Falling back to default.";

                      LOG.log(Level.WARNING, spec2, startupProcedure, sm.getUserId());
                   }
                }

                // disallow access
                return false;
             }
             else
             {
                invoker = new LegacyInvoker(startupProcedure);
             }
          }
          else
          {
             // check if there is an entry point to execute
             if (method.length() == 0)
             {
                throw new RuntimeException("no entry point definition found");
             }

             // resolve the p2j-entry - either a 'com.foo.Bar.execute' Java method or a legacy program name.

             // try to resolve the legacy program name first
             String javaClass = SourceNameMapper.convertNameToClass(method);
             if (javaClass != null)
             {
                // the program name exists, use an emulated RUN statement
                invoker = new LegacyInvoker(method);

                // TODO: always use a LegacyInvoker
             }
             else
             {
                // TODO: this code will become obsolete and needs to be removed.

                // fallback to a 'com.foo.Bar.Execute' Java method

                // parse the definition into class name and method name
                int l = method.lastIndexOf(".");

                if (l == -1)
                {
                   throw new RuntimeException("incorrect syntax for method definition " + method + " for " +
                                               entryPath);
                }
                else
                {
                   className = method.substring(0, l);
                   methodName = method.substring(l + 1);
                }

                // inspect the class
                Class<?> cl = null;
                try
                {
                   cl = Class.forName(className);
                }
                catch (Exception e)
                {
                   // something's wrong with the class
                   throw new RuntimeException("can't load " + className + " for " + entryPath, e);
                }

                // inspect the method
                Method entry = null;
                try
                {
                   entry = cl.getMethod(methodName, (Class[]) null);
                }
                catch (Exception ex)
                {
                   throw new RuntimeException(
                         "no method " + methodName + " defined in " + className + " for " + entryPath,
                         ex);
                }

                invoker = new MainInvoker(cl, entry, stopDisp);
             }
          }

          // instantiate the class (if necessary) and invoke the entry point method
          boolean mainResult = false;

          Throwable t = null;

          try
          {
             // should go after setupClient
             for (InitTermListener sessionListener : sessionListeners)
             {
                sessionListener.initialize();
             }

             invoke(stopDisp, () ->
             {
                if (LogicalTerminal.isInBatchMode())
                {
                   UnnamedStreams.assignOut(StreamFactory.openNullStream());
                }
             },
             invoker);

          }
          catch (InstantiationException ex)
          {
             t = ex;
             throw new RuntimeException("unable to instantiate " + className, ex);
          }
          catch (IllegalAccessException ex)
          {
             t = ex;
             throw new RuntimeException("access problem in " + methodName, ex);
          }
          catch (IllegalArgumentException ex)
          {
             t = ex;
             throw new RuntimeException("invalid argument to " + methodName, ex);
          }
          catch (ReflectiveOperationException ex)
          {
             t = ex;
             throw new RuntimeException("exception in " + methodName, ex);
          }
          catch (StopConditionException sce)
          {
             t = sce;

             if (stopDisp == 1)
             {
                boolean isProcedureNotFound = false;
                Throwable cause = sce.getCause();
                if (cause instanceof NumberedException)
                {
                   isProcedureNotFound =
                      ((NumberedException) cause).getNumber() == ControlFlowOps.EXIT_CODE_PROCEDURE_NOT_FOUND;
                }
                if (!isProcedureNotFound)
                {
                   // this value forces logoff and then re-logon (continued operation)
                   mainResult = true;
                }
             }
             else
             {
                // this value forces logoff (exit)
                mainResult = false;
             }
          }
          catch (UnstoppableExitException uee)
          {
             // this value forces logoff (exit)
             mainResult = false;
          }
          catch (RuntimeException re)
          {
             t = re;
             throw re;
          }

          finally
          {
             for (InitTermListener sessionListener : sessionListeners)
             {
                sessionListener.terminate(t);
             }
          }

          return mainResult;
       }

       /**
        * Setting up the session, before the 4GL procedure is invoked. For web clients sets
        * SESSION:AUTH-BLOB and checks if a new forked session runs in the same browser as the original
        * session. Enriches the session stored in the security context.
        *
        * @param   params
        *          The client parameters.
        * @param   spawnedClientUuid
        *          The UUID of the spawned client.
        */
       private static void setupSession(ClientParameters params, String spawnedClientUuid)
       {
          String relatedSessionId = null;
          String sessionDescription = null;

          if (params.web)
          {
             relatedSessionId = WebDriverHandler.CLIENT_UUID_RELATED_SESSION_ID_PAIRS.remove(spawnedClientUuid);

             SessionUtils.setAuthBlob(WebDriverHandler.CLIENT_UUID_AUTHBLOB_PAIRS.remove(spawnedClientUuid));

             String deviceId = WebDriverHandler.CLIENT_UUID_DEVICEID_PAIRS.remove(spawnedClientUuid);
             SessionUtils.setDeviceId(deviceId);

             ClientExports client = LogicalTerminal.getClient();

             String appIdentifier = Configuration.getParameter("pkgroot");
             client.registerBroadcastChannel(appIdentifier, SessionUtils.getSessionId().toStringMessage());

             if (relatedSessionId != null)
             {
                verifySessionFork(appIdentifier, relatedSessionId, spawnedClientUuid);
             }
             else if (WebDriverHandler.ONE_BROWSER_POLICY)
             {
                enforceOneBrowserPolicy(client, appIdentifier, deviceId, spawnedClientUuid);
             }

             sessionDescription = WebDriverHandler.CLIENT_UUID_SESS_DESCR_PAIRS.remove(spawnedClientUuid);

             String[] browserIpPort = params.browserWebSocket.split(":");
             SessionUtils.setBrowserExternalIp(browserIpPort[0].replaceAll("/", ""));
             SessionUtils.setBrowserPort(Integer.parseInt(browserIpPort[1]));
             SessionUtils.setBrowserTimezone(params.browserTimezone);
             SessionUtils.setIpCaptureTime(params.ipCaptureTimestamp);
             SessionUtils.setUserAgent(params.userAgent);
             SessionUtils.setStorageId(params.storageId);

             Map<String, String[]> validLoginParams = WebDriverHandler.filterOutValidLoginParams(
                WebDriverHandler.CLIENT_UUID_LOGIN_PARAMS.remove(spawnedClientUuid)
             );
             SessionUtils.setLoginParams(validLoginParams);

             boolean isOsPassChangeEnabled = ConfigItem.CHANGE_EXPIRING_OS_PASS.read(false);

             if (isOsPassChangeEnabled && !client.runOsPassExpirationCheck())
             {
                throw new StopConditionException(
                   "OS pass expiration check / OS pass change unsuccessful. Check client logs for more details.");
             }
          }
          else
          {
             SessionUtils.setDeviceId(params.osDeviceId);
          }

          if (!params.isHeadless || params.web)
          {
             SessionUtils.setScreenColorDepth(params.screenColorDepth);
             SessionUtils.setScreenHeight(params.screenHeight);
             SessionUtils.setScreenScalingFactor(params.screenScalingFactor);
             SessionUtils.setScreenWidth(params.screenWidth);
          }

          SessionUtils.setClientTimezone(params.clientTimezone);

          try
          {
             SecuritySession session = SecurityManager.getInstance().sessionSm
                .addSessionDetails(SessionUtils.getSessionId().toStringMessage(),
                                   relatedSessionId,
                                   params.osUserName,
                                   params.pid,
                                   params.driverName,
                                   params.webPort,
                                   sessionDescription,
                                   params.browserWebSocket);
             SessionInfo sessionInfo = SecurityManager.getInstance().sessionSm
                .getSessionDescriptor(session);
             String[] clientAddressPortPair = sessionInfo.remoteNet.split(":");
             SessionUtils.setClientExternalIp(clientAddressPortPair[0].replaceAll("/", ""));
             SessionUtils.setClientPort(Integer.parseInt(clientAddressPortPair[1]));
             SessionUtils.setClientIpCaptureTime(session.getTimeStamp());
          }
          catch (RestrictedUseException e)
          {
             LOG.warning("setupClient exception", e);
          }
       }

       /**
        * Add session hook which will be notified about client session initialization and termination.
        *
        * @param    sessionHook
        *           Session hook to add.
        */
       public static void registerSessionListener(InitTermListener sessionHook)
       {
          sessionListeners.add(sessionHook);
       }

       /**
        * Get the registered hook which is defined in the given jar, if any.
        *
        * @param   jarName
        *          The jar file name.
        *
        * @return  See above.
        */
       public static String getHookForJar(String jarName)
       {
          synchronized (shutdownLock)
          {
             for (InitTermListener listener : serverHooks)
             {
                if (MultiClassLoader.handledByJar(listener.getClass(), jarName))
                {
                   return listener.getClass().getName();
                }
             }
          }

          return null;
       }

       /**
        * Terminate and remove the given server hook.
        *
        * @param   hookClass
        *          The hook class name.
        */
       public static void removeHook(String hookClass)
       {
          synchronized (shutdownLock)
          {
             Iterator<InitTermListener> iter = serverHooks.iterator();
             while (iter.hasNext())
             {
                InitTermListener listener = iter.next();

                if (listener.getClass().getName().equals(hookClass))
                {
                   listener.terminate(null);
                   iter.remove();

                   break;
                }
             }
          }
       }

       /**
        * Add server hook which will be notified about server startup and shutdown.
        *
        * @param    hook
        *           Server hook to add.
        */
       public static void register(InitTermListener hook)
       {
          synchronized (shutdownLock)
          {
             serverHooks.add(hook);
          }
       }

       /**
        * Load {@link InitTermListener} hook using specified class and path.
        *
        * @param    hook
        *           Full class name.
        * @param    hookPath
        *           Hook path in directory.
        *
        * @return   Instance of loaded hook if any.
        */
       public static InitTermListener loadHook(String hook, String hookPath)
       {
          InitTermListener listener = null;

          if (hook != null && hook.length() > 0)
          {
             Class<?> hookCl = null;

             try
             {
                // can not use Class.forName, as it caches the loaded class in
                // some native code from which it can never be removed
                hookCl = MultiClassLoader.getClassLoader().loadClass(hook);
             }
             catch (Exception e)
             {
                // something's wrong with the hook class
                throw new RuntimeException("can't load " + hook + " for "
                                           + hookPath,
                                           e);
             }

             if (!InitTermListener.class.isAssignableFrom(hookCl))
                // something's wrong with the hook class
                throw new RuntimeException("class " + hook + " must implement "
                                           + InitTermListener.class.getSimpleName()
                                           + " interface.");

             try
             {
                listener = (InitTermListener) hookCl.getDeclaredConstructor().newInstance();
             }
             catch (InstantiationException e)
             {
                throw new RuntimeException("unable to instantiate " + hook, e);
             }
             catch (InvocationTargetException e)
             {
                throw new RuntimeException("wrapped exception in " + hook + " constructor", e.getTargetException());
             }
             catch (NoSuchMethodException e)
             {
                throw new RuntimeException("access problem in " + hook + " constructor", e);
             }
             catch (SecurityException e)
             {
                throw new RuntimeException("security problem in " + hook + " constructor", e);
             }
             catch (IllegalAccessException e)
             {
                throw new RuntimeException("access problem in " + hook + " constructor", e);
             }
             catch (IllegalArgumentException e)
             {
                throw new RuntimeException("argument problem in " + hook + " constructor", e);
             }
          }

          return listener;
       }

       /**
        * Get the parameters exposed by the client-side exposed, for this context.
        *
        * @return   A {@link ClientParameters} instance, with the context's parameters.
        */
       public static ClientParameters getClientParameters()
       {
          return localParams.get();
       }

       /**
        * Return the paths, relative to the server starting directory, of jars from which resources
        * are to be loaded. This will be a subset of the server process' classpath.
        *
        * @return  List of resource jar paths.
        */
       public static List<String> getResourceJarPaths()
       {
          return resourceJarPaths;
       }

       /**
        * Block the calling thread until the non-network portions of the server
        * are fully initialized.  At that point the server is ready to execute
        * real logic.
        */
       public void waitUntilReady()
       throws InterruptedException
       {
          ready.await();
       }

       /**
        * Bootstrap the server environment by initializing network services,
        * exporting all standard entry points for this server, loading all
        * application specific startup classes, and starting the server socket
        * listening loop.
        * <p>
        * The implementation looks up the directory under the path
        * <code>/server/serverID/exports</code> to figure out the subtree of
        * registrations.
        * <p>
        * If no such subtree exists, <code>/server/default/exports</code> is
        * checked next.
        * <p>
        * Entries have the format <code>export-group-ID/export-method-ID</code>
        * where the <code>export-group-ID</code> is a container naming a group and
        * <code>export-method-ID</code> is an object naming the export and
        * providing its implementation location through its string type "value"
        * attribute as <code>class-name.method-name(signature)</code>.
        * <p>
        * <code>method-name</code> should refer to a public method of the class.
        * It can be either a static or instance method. The class is instantiated
        * once for the first instance method reference.
        * <p>
        * Specifying a method without signature is allowed if the method is not
        * overloaded.
        * <p>
        * Once all the generic directory-driven exports are processed, then all
        * the following are registered using the simpler <code>RemoteObject</code>
        * mechanism:
        * <p>
        *
        * <pre>
        * Interface         Type        Target
        * ----------------- ----------- ----------------------------------
        * ServerExports     static      LogicalTerminal
        * RemoteErrorData   static      ErrorManager
        * MainEntry         static      StandardServer
        * </pre>
        * <p>
        * When basic initialization is finished, this method registers default
        * services and then loads user defined services if they are defined in
        * <b>startups</b> directory entry under either
        * <code>/server/serverID</code> or <code>/server/default</code> entry.
        * <p>
        * User defined services must implement {@link InitTermListener} interface
        * and its {@link InitTermListener#initialize()} will be invoked right
        * before server will start listen for incoming connections while its
        * {@link InitTermListener#terminate(Throwable)} method will be invoked
        * when server stopped accepting incoming connections and all active
        * sessions are terminated. If server is stopped in graceful way (via
        * remote request) then <code>null</code> is passed as a parameter into
        * {@link InitTermListener#terminate(Throwable)} method. If server is
        * stopped in abnormal way, for example, because user typed Ctrl-C, then
        * some non-null value is passed as a parameter. Passed value should not be
        * relied on to determine real cause of server stop.
        *
        * @param   config
        *          Bootstrap configuration information for this server.
        * @param   diagnostics
        *          Details about the driver state that can help diagnose server startup issues.
        * @param   cfgProfile
        *          The optional configuration profile.
        *
        * @throws  Exception
        *          Various exceptions generated by invalid configurations.
        */
       public void bootstrap(BootstrapConfig config, Supplier<String> diagnostics, String cfgProfile)
       throws Exception
       {
          config.setServer(true);

          // instantiate the directory service with must_exist option
          config.setConfigItem("directory", "xml", "must_exist", "true");

          // set a global property to for Antlr to use Class.forName
          System.setProperty("ANTLR_USE_DIRECT_CLASS_LOADING", "true");

          LOG.info(Version.getFWDVersion() + " Server starting initialization." );
          LOG.info(diagnostics.get());
          LOG.info("BootstrapConfig " + config);

          Configuration.setRuntimeConfig(true);

          DirectoryService ds = DirectoryService.createInstance(config);

          // bind to the directory
          ds.bind();

          // instantiate security manager
          final SecurityManager sm     = SecurityManager.createInstance(config);
          final SessionManager sessMgr = SessionManagerFactory.createRouterNode(config);

          // after SecurityManager is created
          CentralLoggerServer.initialize(CentralLoggerServer.Configs.fromDirectory(ds));

          // initialize the class loaders for the jars set in the directory
          initClassLoaders(ds);

          // enumerate exported groups
          String path = Utils.findDirectoryNodePath(ds, "exports", "container", false);
          String[] groups = ds.enumerateNodes(path);

          if (groups == null || groups.length == 0)
             LOG.info("No exported entry points defined in the P2J directory");

          // put instance references into the map for these special cases
          Class<?> cl = ds.getClass();
          inst.put(cl.getName(), ds);       // reference to DirectoryService
          cl = sm.getClass();
          inst.put(cl.getName(), sm);       // reference to SecurityManager
          cl = this.getClass();
          inst.put(cl.getName(), this);     // reference to StandardServer

          if (groups != null)
          {
             // process groups
             for (String group : groups)
             {
                // enumerate exports for this group
                String methodPath = path + "/" + group;
                String[] methods = ds.enumerateNodes(methodPath);

                if (methods == null)
                   continue;

                // process exports
                for (String s : methods)
                {
                   // read the method specification
                   String valuePath = methodPath + "/" + s;
                   String method = ds.getNodeString(valuePath, "value");

                   // register method
                   if (exportMethod(group, s, method))
                      LOG.finer(" exported " + group + ":" + s);
                   else
                      LOG.finer("export failed " + group + ":" + s);
                }
             }
          }

          // simple form (not directory based)
          Class<?>[] ifaces = new Class[] { ServerExports.class };
          RemoteObject.registerStaticNetworkServer(ifaces, LogicalTerminal.class);

          ifaces = new Class[] { RemoteErrorData.class };
          RemoteObject.registerStaticNetworkServer(ifaces, ErrorManager.class);

          ifaces = new Class[] { MainEntry.class };
          RemoteObject.registerStaticNetworkServer(ifaces, StandardServer.class);

          ifaces = new Class[] { AppServerEntry.class };
          RemoteObject.registerStaticNetworkServer(ifaces, AppServerManager.class);

          ifaces = new Class[] { AppServer.class };
          RemoteObject.registerStaticNetworkServer(ifaces, AppServerLauncher.class);

          ifaces = new Class[] { BatchProcess.class };
          RemoteObject.registerStaticNetworkServer(ifaces, ProcessClientSpawner.class);

          ifaces = new Class[] { RemoteSpawner.class };
          RemoteObject.registerStaticNetworkServer(ifaces, ClientBuilder.class);

          ifaces = new Class[] { BrokerServerServices.class };
          RemoteObject.registerStaticNetworkServer(ifaces, BrokerManager.class);

          ifaces = new Class[] { ServerPropertiesInspector.class };
          RemoteObject.registerStaticNetworkServer(ifaces, ServerPropertiesDaemon.class);

          ifaces = new Class[] { CentralLogService.class };
          RemoteObject.registerStaticNetworkServer(ifaces, CentralLogStaticService.class);

          RemoteObject.registerStaticNetworkServer(new Class[] { UnitTestEngine.class }, UnitTestServer.class);

          AdminServerImpl.initialize();

          DirectoryManager.initServer();

          // initialize all appserver definitions
          AppserverDefinition.initAppServerDefinitions();

          if (MultiSessionAppserverDefinition.count() > 0)
          {
             // register network interfaces on the server side for multi-session agent appserver
             OSResourceManager.getInstance();
          }

          AppServerManager.initRemoteAppServers();

          // set the optional configuration profile must be called after initialization of Configuration
          // and DirectoryManager
          if (cfgProfile != null)
          {
             Configuration.setDefaultProfile(cfgProfile);
          }

          registerDefaultServices(sm, config);

          AstManager.initialize(InMemoryRegistryPlugin::new);

          Throwable poolExc = null;
          try
          {
             ConversionPool.initialize();
          }
          catch (RuntimeException exc)
          {
             poolExc = exc;
          }
          catch (ConfigurationException exc)
          {
             poolExc = exc.getCause();
          }
          finally
          {
             if (poolExc != null)
             {
                // not all apps use runtime conversion services, so this is not a fatal error, but
                // log a warning
                if (LOG.isLoggable(Level.WARNING))
                {
                   LOG.log(Level.WARNING,
                           "Unable to initialize runtime conversion pool; if dynamic database " +
                           "features are not in use, this warning can be ignored safely (otherwise " +
                           "restart server with FINE logging for additional information)");
                   if (LOG.isLoggable(Level.FINE))
                   {
                      LOG.log(Level.FINE,
                              "Ensure configuration items for runtime conversion are stored in the " +
                              "application jar file",
                              poolExc);
                   }
                }
             }
          }

          // set the interrupt handler in the session manager.
          sessMgr.setInterruptHandler(new InterruptedExceptionHandler());

          // load the standard startup classes (must be after the dispatcher,
          // directory, security manager are all initialized)
          loadStartup(ds);

          // initialize the list of jar files from which to load resources
          initResourceJarPaths(ds);

          hookInitialize();

          String sessionHookPath = Utils.findDirectoryNodePath(ds, "context-hook", "string", true);
          String sessionHook = Utils.getDirectoryNodeString(ds, "context-hook", "");
          InitTermListener sessionListener = loadHook(sessionHook, sessionHookPath);
          if (sessionListener != null)
          {
             sessionListeners.add(sessionListener);
          }

          OrderedShutdownHooks.registerShutdownHook(1, new Thread(() -> {
             Throwable t = new Throwable();

             try
             {
                sm.contextSm.setInitialSecurityContext();
                sessMgr.waitForShutdown();
             }
             catch (Throwable e)
             {
                t = e;
             }

             hookTerminate(t);
          }));

          try
          {
             // wake up any waiting threads, the non-network portions of the
             // server are fully initialized now
             ready.countDown();

             boolean embeddedEnabled = ConfigItem.EMBEDDED.read(false);
             boolean virtualDesktopEnabled = ConfigItem.VIRTUAL_DESKTOP.read(false);

             // main listening loop;  service incoming socket connections
             sessMgr.listen(embeddedEnabled || virtualDesktopEnabled ?
                               WebClientsManager.getInstance() :
                               null);
          }
          finally
          {
             sessMgr.waitForShutdown();
             hookTerminate(null);
          }
       }

       /**
        * Initialize the state of this client session.  Appserver agents can be initialized only if the specified
        * <code>forAppserver</code> flag is set.
        *
        * @param    allowAppserver
        *           Flag indicating {@link AppServerManager#startAppServer} is allowed to be executed (in other
        *           words, appserver agents are allowed to be started).
        *
        * @return   <code>false</code> if an appserver agent was started and normal client session can not
        *           continue; <code>true</code> if the normal FWD client session continues.
        */
       private static boolean initializeClientSession(boolean allowAppserver, String spawnedClientUuid)
       {
          EnvironmentOps.setCurrentLanguage(SessionlessTranslationManager.CURRENT_LANGUAGES_CONFIG);
          SessionUtils.setClientTypes(ClientType._4GLCLIENT);

          SecurityManager sm = SecurityManager.getInstance();
          String userId = sm.getUserId();

          StartupParameters startupParameters = SessionUtils._startupParameters();
          ClientParameters params = localParams.get();
          Integer inputCharacters = startupParameters.getInputCharacters();
          boolean isServerSideFileSystem = OSResourceManager.getInstance().isServerSide(OsResourceType.FILESYSTEM);

          LegacyLogManagerConfigs legacyLogManagerConfigs =
             new LegacyLogManagerConfigs(startupParameters.getClientLog(),
                                         startupParameters.getLoggingLevel(),
                                         startupParameters.getLogEntryTypes(),
                                         startupParameters.getLogThreshold(),
                                         startupParameters.getNumLogFiles(),
                                         params.pid,
                                         inputCharacters == null ? 15_000 : inputCharacters,
                                         params.osUserName,
                                         userId,
                                         isServerSideFileSystem);

          // should go before sessionListener.initialize()
          setupSession(params, spawnedClientUuid);

          // this is an app server and needs its own management
          if (allowAppserver && AppServerManager.startAppServer(legacyLogManagerConfigs))
          {
             return false;
          }

          LegacyLogManager logManager = LegacyLogOps.logMgr();
          logManager.initialize(legacyLogManagerConfigs);

          if (params.batchMode > 0)
          {
             // run this connection in batch mode (as requested from client command-line)
             EnvironmentOps.setBatchMode(true);
             // LogicalTerminal.activateBatchMode(true); -- should already be set ?
          }

          return true;
       }

       /**
        * Initialize the class loaders for all the jars defined in the directory.
        * This can be done only if the <code>java.system.class.loader</code>
        * property is set to the {@link MultiClassLoader custom class loader}.
        *
        * @param   ds
        *          The directory service.
        */
       private static void initClassLoaders(DirectoryService ds)
       {
          // initialize the class loaders for the jars defined in the directory
          String path = Utils.findDirectoryNodePath(ds,
                                                    "jars",
                                                    "strings",
                                                    false);
          if (path == null)
          {
             return;
          }

          String[] jars = ds.getNodeStrings(path, "values");
          if (jars != null && jars.length > 0)
          {
             ClassLoader scl = MultiClassLoader.getClassLoader();
             if (scl instanceof MultiClassLoader)
             {
                try
                {
                   for (String jar : jars)
                   {
                      if (MultiClassLoader.addJarLoader(jar) != ErrorCode.SUCCESS)
                      {
                         // we need to stop the server, as it could not initialize
                         // properly
                         throw new RuntimeException(
                            "Could not initialize class loader for jar " +
                                  jar + " !");
                      }
                   }
                }
                catch (RestrictedUseException e)
                {
                   throw new IllegalStateException("Call of " +
                      "MultiClassLoader.addJarLoader from this function should" +
                      "be granted.");
                }
             }
             else
             {
                if (LOG.isLoggable(Level.SEVERE))
                {
                   String msg = "Jars are defined in the directory, but the " +
                              "java.system.class.loader is not an instance of " +
                              "com.goldencode.p2j.classloader.MultiClassLoader !";

                   LOG.logp(Level.SEVERE,
                            "StandardServer.initClassLoaders",
                            "",
                            msg);
                }
             }
          }
       }

       /**
        * Register default services:
        * <ol>
        * <li> {@link Persistence} </li>
        * <li> {@link WebServer} </li>
        * <li> custom account extension plugin </li>
        * </ol>
        *
        * @param    sm
        *           The initialized instance of <code>SecurityManager</code>.
        * @param    config
        *           The bootstrap configuration information for this server.
        */
       private void registerDefaultServices(final SecurityManager sm,
                                            final BootstrapConfig config)
       {
          serverHooks.add(new AbstractInitTermListener()
          {
             @Override
             public void initialize()
             {
                // initialize cache manager.
                // read cache configurations from directory.
                CacheManager.initialize();
             }
          });

          serverHooks.add(new AbstractInitTermListener()
          {
             @Override
             public void initialize()
             {
                // initialize broker manager.
                // read broker configurations from directory.
                BrokerManager.initialize();
             }

             @Override
             public void terminate(Throwable t)
             {
                // close brokers sessions
                BrokerManager.cleanup();
             }
          });

          serverHooks.add(new AbstractInitTermListener()
          {
             @Override
             public void initialize()
             {
                // schedule the jobs to be launched; this will wait for the server to open its
                // secure and insecure sockets (depending on configuration)
                Scheduler.scheduleJobs();
             }
          });

          serverHooks.add(new AbstractInitTermListener()
          {
             @Override
             public void initialize()
             {
                // after scheduler
                ProcedureManager.initialize();
             }
          });

          serverHooks.add(new AbstractInitTermListener()
          {
             @Override
             public void initialize()
             {
                ServerKeyStore.initialize();
             }
          });

          // On this step we can initialize the server side of the ErrorWriter inside the
          // ErrorManager. It is important to do this exactly after the LogicalTerminal setup is
          // complete. Do not move this cal agead.
          ErrorManager.initErrorWriter(new ErrorWriterServer());

          serverHooks.add(new AbstractInitTermListener()
          {
             @Override
             public void initialize()
             {
                if (!Utils.getDirectoryNodeBoolean(null, "persistence/active", true, false))
                {
                   LOG.warning("Server persistence is inactive. Running applications with " +
                               "persistence dependency may fail.");
                   return;
                }

                try
                {
                   Persistence.initialize();
                }
                catch (PersistenceException e)
                {
                   throw new RuntimeException(e);
                }
             }
          });

          // the SSO init hook should go after Persistence.initialize()
          serverHooks.add(new AbstractInitTermListener()
          {
             @Override
             public void initialize()
             {
                SsoAuthenticator ssoAuthenticator = sm.ssoSm.getSsoAuthenticator();
                if (ssoAuthenticator != null)
                {
                   ssoAuthenticator.initialize();
                }
             }

             @Override
             public void terminate(Throwable t)
             {
                SsoAuthenticator ssoAuthenticator = sm.ssoSm.getSsoAuthenticator();
                if (ssoAuthenticator != null)
                {
                   ssoAuthenticator.terminate();
                }
             }
          });

          serverHooks.add(new AbstractInitTermListener()
          {
             @Override
             public void initialize()
             {
                MetafileHelper.bootstrap();
                XprHelper.bootstrap();
             }
          });

          serverHooks.add(new AbstractInitTermListener()
          {
             @Override
             public void initialize()
             {
                JavaTypeSerializer.bootstrap();
             }
          });

          serverHooks.add(new AbstractInitTermListener()
          {
             @Override
             public void initialize()
             {
                TableHints.bootstrap();
             }
          });

          serverHooks.add(new AbstractInitTermListener()
          {
             @Override
             public void initialize()
             {
                EnvironmentOps.bootstrap();
                FileSystemOps.bootstrap();
                SecurityOps.bootstrap();
             }
          });

          serverHooks.add(new AbstractInitTermListener()
          {
             @Override
             public void initialize()
             {
                SourceNameMapper.initialize();
             }
          });

          // register controller for method execution tracing
          serverHooks.add(new AbstractInitTermListener()
          {
             @Override
             public void initialize()
             {
                Class<?>[] ifaces = new Class[] { MethodTraceController.class };
                RemoteObject.registerStaticNetworkServer(ifaces, MethodTraceController.Impl.class);
             }
          });

          // don't start the web service unless we're in a secure environment
          boolean isSecure = config.getBoolean("net", "connection", "secure", false);

          if (isSecure)
          {
             serverHooks.add(new AbstractInitTermListener()
             {
                @Override
                public void initialize()
                {
                   // Initialize pool
                   TemporaryAccountPool.getInstance();
                }

                @Override
                public void terminate(Throwable t)
                {
                   // Kill worker thread
                   TemporaryAccountPool.shutDown();
                }
             });

             // FontTable initialization
             serverHooks.add(new AbstractInitTermListener()
             {
                @Override
                public void initialize()
                {
                   FontTable.initialize();
                }
             });

             serverHooks.add(new AbstractInitTermListener()
             {
                /** Flag to determine if the REST services are active. */
                boolean rest = false;

                /** Flag to determine if the web handler is active. */
                boolean legacyWebHandler = false;

                /** Flag to determine if the soap handler is active. */
                boolean soap       = false;

                /** Flag to determine if the SPREADSHEET widget web application is active. */
                boolean spreadsheetHandlerEnabled;

                @Override
                public void initialize()
                {
                   int     num = 0;
                   boolean adm = false;
                   boolean virtualDesktop = false;
                   boolean ssoEnabled = SecurityManager.getInstance().ssoSm.isSsoEnabled();
                   boolean embedded = false;
                   AdminRestHandler adminHandle;

                   if (ssoEnabled && ConfigItem.EMBEDDED.read(false))
                   {
                      // embedded works only with SSO enabled
                      num++;
                      embedded = true;
                   }

                   if (Utils.getDirectoryNodeBoolean(null, "adminEnabled", false, false))
                   {
                      // admin
                      num++;
                      adm = true;
                   }

                   if (ConfigItem.VIRTUAL_DESKTOP.read(false))
                   {
                      num++;
                      virtualDesktop = true;
                   }

                   if (Utils.getDirectoryNodeBoolean(null, "rest/enabled", false, false))
                   {
                      num++;
                      rest = true;
                   }

                   if (Utils.getDirectoryNodeBoolean(null, "soap/enabled", false, false))
                   {
                      num++;
                      soap = true;
                   }

                   if (Utils.getDirectoryNodeBoolean(null, "webHandler/enabled", false, false))
                   {
                      num++;
                      legacyWebHandler = true;
                   }

                   adminHandle = AdminRestHandler.initialize();
                   num += (adminHandle != null) ? 1 : 0;

                   if (ConfigItem.SPREADSHEET_ENABLED.read(false, Utils.DirScope.BOTH))
                   {
                      num++;
                      spreadsheetHandlerEnabled = true;
                   }

                   ContextHandler[] webContentHandlers = null;
                   try
                   {
                      webContentHandlers = WebServer.getWebContentHandlers();
                      num += webContentHandlers.length;
                   }
                   catch (Throwable ex)
                   {
                      LOG.severe("", ex);
                   }

                   if (num == 0)
                      return;

                   final org.eclipse.jetty.server.Handler[] handlers =
                      new org.eclipse.jetty.server.Handler[num];

                   // admin
                   if (adm)
                   {
                      Resource resourceBase = Resource.newClassPathResource("com/goldencode/p2j/admin");

                      String path = "/adminapp";
                      Class servletExtClass = null;
                      String[] welcomeFile = new String[] {"AdminApp.html"};

                      if (adminServerExtension != null)
                      {
                         path = adminServerExtension.modulePath;
                         if (adminServerExtension.adminClientResourcePath != null)
                         {
                            Resource res = Resource.newClassPathResource(adminServerExtension.adminClientResourcePath);
                            resourceBase = new TwoURLsResource(res, resourceBase);
                         }

                         servletExtClass = adminServerExtension.adminExtensionServlet;
                         welcomeFile[0] = adminServerExtension.welcomeFile;
                      }

                      WebAppContext webApp = new WebAppContext("adminapp", "/admin");
                      webApp.getSessionHandler().setMaxInactiveInterval(20);
                      webApp.setBaseResource(resourceBase);
                      webApp.addServlet(AdminServiceImpl.class, path + "/AdminService");
                      webApp.addServlet(AuthServiceImpl.class, path + "/AuthService");
                      webApp.addServlet(ReportServlet.class, path + "/ReportService");

                      // prevent directory listing
                      webApp.setInitParameter("org.eclipse.jetty.servlet.Default.dirAllowed",
                                              "false");

                      // admin server extension
                      if (servletExtClass != null)
                      {
                         String pathSpec = path + adminServerExtension.servletPath;
                         webApp.addServlet(servletExtClass, pathSpec);

                         webApp.addFilter(SynchronizeFilter.class, pathSpec,
                                          EnumSet.allOf(DispatcherType.class));
                         webApp.addFilter(AuthFilter.class, pathSpec,
                                          EnumSet.allOf(DispatcherType.class));
                      }

                      webApp.addFilter(SynchronizeFilter.class,
                                       path + "/AdminService",
                                       EnumSet.allOf(DispatcherType.class));
                      webApp.addFilter(SynchronizeFilter.class,
                                       path + "/ReportService",
                                       EnumSet.allOf(DispatcherType.class));
                      webApp.addFilter(AuthFilter.class,
                                       path + "/AdminService",
                                       EnumSet.allOf(DispatcherType.class));
                      webApp.addFilter(AuthFilter.class,
                                       path + "/ReportService",
                                       EnumSet.allOf(DispatcherType.class));

                      webApp.setWelcomeFiles(welcomeFile);
                      webApp.setParentLoaderPriority(true);
                      String adminTmp =
                         Utils.getOrCreateTemporaryDirectory("fwdadmin" + new Random().nextLong());
                      webApp.setTempDirectory(new File(adminTmp));

                      HandlerList list = new HandlerList();
                      list.setHandlers(new org.eclipse.jetty.server.Handler[] {
                         webApp,
                         new DefaultHandler()
                      });

                      handlers[--num] = list;
                   }

                   if (embedded)
                   {
                      try
                      {
                         handlers[--num] = new EmbeddedWebHandler();
                      }
                      catch (Throwable t)
                      {
                         LOG.severe("Couldn't initialize embedded handlers.", t);
                      }
                   }

                   if (virtualDesktop)
                   {
                      handlers[--num] = new VirtualDesktopWebHandler();
                   }

                   if (rest)
                   {
                      handlers[--num] = RestHandler.initialize();
                   }

                   if (soap)
                   {
                      handlers[--num] = SoapHandler.initialize();
                   }

                   if (legacyWebHandler)
                   {
                      handlers[--num] = WebServiceHandler.initialize();
                   }

                   if (adminHandle != null)
                   {
                      handlers[--num] = adminHandle;
                   }

                   if (rest || legacyWebHandler || soap || adminHandle != null)
                   {
                      // force loading of the services...
                      SourceNameMapper.registerServices();

                      if (RestHandler.useWebServiceAuthentication()       ||
                          SoapHandler.useWebServiceAuthentication()       ||
                          WebServiceHandler.useWebServiceAuthentication() ||
                          adminHandle != null)
                      {
                         sm.legacyWebSm.startWebServiceContextThreads();
                      }
                   }

                   if (spreadsheetHandlerEnabled)
                   {
                      SpreadsheetWebApp spreadsheetWebApp = new SpreadsheetWebApp();
                      handlers[--num] = spreadsheetWebApp.init();
                   }

                   if (webContentHandlers != null)
                   {
                      System.arraycopy(webContentHandlers, 0, handlers, 0, webContentHandlers.length);
                   }

                   WebServer.initialize(handlers);

                   if (soap)
                   {
                      SoapHandler.initializePost();
                   }
                }

                @Override
                public void terminate(Throwable t)
                {
                   WebServer.terminate();

                   if (rest)
                   {
                      RestHandler.terminate();
                   }

                   if (soap)
                   {
                      SoapHandler.terminate();
                   }

                   if (legacyWebHandler)
                   {
                      WebServiceHandler.terminate();
                   }
                }
             });
          }

          serverHooks.add(new AbstractInitTermListener()
          {
             @Override
             public void initialize()
             {
                // force initialization to load the persistent cache map from disk at startup
                DynamicTablesHelper.bootstrap(config);
                DynamicTablesHelper.getInstance();
             }

             @Override
             public void terminate(Throwable t)
             {
                // save dynamic DMOs to a JAR to improve first iteration response times
                DynamicTablesHelper.getInstance().createCache();
             }
          });

          // load custom account extension server-side plugin
          String customExt = sm.getCustomServerExt();

          if (customExt == null)
          {
             return;
          }

          Class<?> cext = null;

          try
          {
             cext = Class.forName(customExt);
          }

          catch (ClassNotFoundException cnfe)
          {
             LOG.severe("server extension class not found", cnfe);
             return;
          }

          final Method minit;

          try
          {
             minit = cext.getMethod("initialize", (Class[])null);
          }

          catch (Exception e)
          {
             LOG.severe("server extension class has no initialize() method", e);
             return;
          }

          serverHooks.add(new AbstractInitTermListener()
          {
             @Override
             public void initialize()
             {
                try
                {
                   minit.invoke(null, (Object[])null);
                   LOG.fine("server extension plugin initialized");
                }
                catch (Exception e)
                {
                   LOG.severe("server extension class failed to initialize", e);
                }
             }
          });

          try
          {
             Method m = cext.getMethod("getAdminExtension", (Class[])null);
             adminServerExtension = (AdminServerExtension) m.invoke(null, (Object[]) null);
          }
          catch (NoSuchMethodException e)
          {
             // the method is optional, so ignore
          }
          catch (Exception e)
          {
             LOG.severe("getServletClass call failed on server extension class", e);
          }
       }

       /**
        * Emulate the invocation of the given {@link Block#body() code} as if it was ran from within the given
        * program (which can be a surrogate program name) and the defining program class.
        * <p>
        * Only the following methods from {@link Block} are being used:
        * <ul>
        *    <li> {@link Block#init()} for initializing vars which would belong to the external program.</li>
        *    <li> {@link Block#body()} for execute the actual external program code, wrapped in a
        *         {@link BlockManager#externalProcedure} call.</li>
        * </ul>
        *
        * @param    stopDisp
        *           STOP condition disposition.
        *           <ul>
        *             <li>0 - retry without logging off
        *             <li>1 - force logoff and then re-logon
        *             <li>2 - force logoff
        *           </ul>
        *
        * @param    programName
        *           The surrogate program name.
        * @param    programClass
        *           The surrogate class.
        * @param    block
        *           The code to execute.  Place in {@link Block#init()} any code which defines variables for
        *           the external program.
        *
        * @throws   NullPointerException
        * @throws   IllegalAccessException
        * @throws   IllegalArgumentException
        * @throws   InvocationTargetException
        * @throws   InstantiationException
        */
       public static void invoke(int stopDisp, String programName, Class<?> programClass, Block block)
       throws NullPointerException,
              IllegalAccessException,
              InvocationTargetException,
              InstantiationException
       {
          Runnable init = () ->
          {
             try
             {
                ProcedureManager.setInstantingExternalProgram(programName);
                ProcedureManager.setInstantingExternalProgramClass(programClass);
                block.init();
             }
             finally
             {
                ProcedureManager.setInstantingExternalProgram(null);
                ProcedureManager.setInstantingExternalProgramClass(null);
             }
          };

          invoke(stopDisp, init, () -> { block.body(); return null; });
       }

       /**
        * Initializes the <code>TransactionManager</code> environment and other
        * user-specific runtime initialization and then executes the given entry
        * point.
        *
        * @param    stopDisp
        *           STOP condition disposition.
        *           <ul>
        *             <li>0 - retry without logging off
        *             <li>1 - force logoff and then re-logon
        *             <li>2 - force logoff
        *           </ul>
        *
        * @param    entry
        *           instance of the Isolatable interface that provides the entry
        *           point to be executed
        *
        * @return   The result of the invocation.  If the signature of the method
        *           specifies a <code>void</code> return then this will return
        *           <code>null</code>.  Note that it is a limitation of the
        *           design of Java method signatures such that we do not
        *           distinguish between a method with a <code>void</code> return
        *           and a method that has a real return value which is set to
        *           <code>null</code>.
        *
        * @throws   NullPointerException
        * @throws   IllegalAccessException
        * @throws   IllegalArgumentException
        * @throws   InvocationTargetException
        * @throws   InstantiationException
        */
       public static Object invoke(int stopDisp, Isolatable entry)
       throws NullPointerException,
              IllegalAccessException,
              InvocationTargetException,
              InstantiationException
       {
          return invoke(stopDisp, null, entry);
       }

       /**
        * Initializes the <code>TransactionManager</code> environment and other
        * user-specific runtime initialization and then executes the given entry
        * point.
        *
        * @param    stopDisp
        *           STOP condition disposition.
        *           <ul>
        *             <li>0 - retry without logging off
        *             <li>1 - force logoff and then re-logon
        *             <li>2 - force logoff
        *           </ul>
        * @param    init
        *           Optional code to execute just before the {@link Isolatable#execute()} is ran.
        * @param    entry
        *           instance of the Isolatable interface that provides the entry
        *           point to be executed
        *
        * @return   The result of the invocation.  If the signature of the method
        *           specifies a <code>void</code> return then this will return
        *           <code>null</code>.  Note that it is a limitation of the
        *           design of Java method signatures such that we do not
        *           distinguish between a method with a <code>void</code> return
        *           and a method that has a real return value which is set to
        *           <code>null</code>.
        *
        * @throws   NullPointerException
        * @throws   IllegalAccessException
        * @throws   IllegalArgumentException
        * @throws   InvocationTargetException
        * @throws   InstantiationException
        */
       public static Object invoke(int stopDisp, Runnable init, Isolatable entry)
       throws NullPointerException,
              IllegalAccessException,
              InvocationTargetException,
              InstantiationException
       {
          // simple test for a quick out
          if (entry == null)
          {
             throw new NullPointerException("Specified entry point is null!");
          }

          // force DatabaseTriggerManager context-local instantiation before the new scope is pushed
          DatabaseTriggerManager.get();

          // add the topmost scope to the TransactionManager
          TransactionManager.pushScope("startup",
                                       TransactionManager.NO_TRANSACTION,
                                       true,
                                       true,
                                       false,
                                       false);

          // make sure that the global block gets a special notification - but only for non-appserver sessions.
          LogicalTerminal.registerCleaner();

          Object result = null;
          try
          {
             startup:
             {
                TransactionManager.blockSetup();

                do
                {
                   try
                   {
                      if (init != null)
                      {
                         init.run();
                      }

                      // calling the entry point
                      result = entry.execute();
                   }

                   catch (StopConditionException sce)
                   {
                      if (stopDisp != 0)
                      {
                         if (stopDisp == 1)
                            LogicalTerminal.setRelogin(true);

                         throw sce;
                      }

                      TransactionManager.disableLoopProtection(null);
                      TransactionManager.triggerRetry(null);
                   }

                   catch (ConditionException ce)
                   {
                      if (ce instanceof QuitConditionException)
                      {
                         TransactionManager.setProcessingQuit(true);
                      }
                      break startup;
                   }

                   catch (RetryUnwindException ru)
                   {
                      TransactionManager.honorRetry(ru);
                   }

                   catch (Throwable th)
                   {
                      Throwable chained = th;

                      boolean   isError = false;
                      boolean   isStop  = false;

                      // look through all causes to determine if there was an
                      // underlying error
                      while (chained != null)
                      {
                         if (chained instanceof Error)
                         {
                            isError = true;
                         }

                         if (chained instanceof StopConditionException)
                         {
                            isStop = true;
                         }

                         if (chained instanceof SilentUnwindException)
                         {
                            isStop = true;
                         }

                         if (chained instanceof UnstoppableExitException)
                         {
                            throw new UnstoppableExitException(chained);
                         }

                         chained = chained.getCause();
                      }

                      // stop conditions can be generated via many (sometimes
                      // unexpected) paths and may be wrapped in other exception
                      // types, in such a case we don't need to log it
                      if (!isStop)
                      {
                         // log the fact that we have an abnormal end occurring
                         LOG.logp(Level.SEVERE,
                                  "StandardServer",
                                  "invoke",
                                  "Abnormal end!",
                                  th);
                      }

                      // treat Exception (but not Error) types the same as if
                      // we got a STOP
                      if (!isError)
                      {
                         if (stopDisp != 0)
                         {
                            if (stopDisp == 1)
                               LogicalTerminal.setRelogin(true);

                            // in case of a SilentUnwindException, although we
                            // throw a STOP here, the TM is still aware of the
                            // abnormal connection end and will overwrite the STOP
                            // with another SilentUnwindException - which is OK
                            ErrorManager.handleStopException(new StopConditionException(th));
                         }

                         TransactionManager.disableLoopProtection(null);
                         TransactionManager.triggerRetry(null);
                      }
                      else
                      {
                         // the abend was an instance of Error - this is so
                         // severe that we can't let the user continue

                         // set the flag to allow direct exit since we have such
                         // a severe problem that we are going to exit to the
                         // client BUT we MUST do this without trying to access
                         // the client any further (via LogicalTerminal's
                         // pauseBeforeEnd())
                         TransactionManager.setProcessingQuit(true);

                         // rethrow to ensure that we exit
                         throw new RuntimeException(th);
                      }
                   }

                }
                while (TransactionManager.needsRetry());
             }
          }

          // let any other exception pass through to the caller!

          finally
          {
             // remove the topmost scope from the TransactionManager
             TransactionManager.popScope();
          }

          return result;
       }

       /**
        * Reads the portion of the directory where all preloaded application
        * classes are listed and instantiates them.
        *
        * @param    ds
        *           instance of <code>DirectoryService</code> to read from
        *
        * @throws   ConfigurationException
        *           when a class listed in the directory cannot be loaded
        */
       private static void loadStartup(DirectoryService ds)
       throws ConfigurationException
       {
          String nodeId = Utils.findDirectoryNodePath(ds, "server-hooks", "strings", false);

          // check the node for existence and proper type
          if (nodeId == null)
          {
             return;
          }

          // read all startup names
          String[] startups = ds.getNodeStrings(nodeId, "values");

          if (startups == null || startups.length == 0)
          {
             return;
          }

          // check that only a single hook is assigned per jar
          String[] loadedJars             = MultiClassLoader.listRegisteredJars();
          Map<String, String> jarsToHooks = new HashMap<>();
          if (loadedJars.length > 0)
          {
             for (String hookClass : startups)
             {
                for (String jar : loadedJars)
                {
                   if (MultiClassLoader.handledByJar(hookClass, jar))
                   {
                      if (jarsToHooks.containsKey(jar))
                      {
                        throw new ConfigurationException("Cannot load hook " +
                              hookClass + " for jar " + jar + " because the hook " +
                                  jarsToHooks.get(jar) + " is already assigned " +
                                  " for this jar!");
                      }

                      jarsToHooks.put(jar, hookClass);
                      break;
                   }
                }
             }
          }

          // initialize startups
          for (String hook : startups)
          {
             InitTermListener listener = null;

             try
             {
                listener = loadHook(hook, nodeId);
             }
             catch (RuntimeException rte)
             {
                LOG.logp(Level.SEVERE,
                         "StandardServer.loadStartup",
                         "",
                         "failed to load " + hook + " with " + rte);

                continue;
             }

             serverHooks.add(listener);
          }
       }

       /**
        * Initialize the list of jars from which to load application resources. This is read from
        * the directory. If nothing is listed in the directory, only the {@code p2j.jar} file will
        * be searched for resources. The {@code p2j.jar} file will be added to the end of this list,
        * if not explicitly listed in the directory.
        *
        * @param   ds
        *          Instance of {@code DirectoryService} from which to read configuration.
        */
       private static void initResourceJarPaths(DirectoryService ds)
       {
          String nodeId = Utils.findDirectoryNodePath(ds, "resource-jars", "strings", false);
          Set<String> jarSet = new LinkedHashSet<>();

          if (nodeId != null)
          {
             String[] jars = ds.getNodeStrings(nodeId, "values");
             if (jars != null)
             {
                for (String jar : jars)
                {
                   if (!jar.toLowerCase().endsWith(JAR_EXT))
                   {
                      jar = jar + JAR_EXT;
                   }
                   jarSet.add(jar);
                }
             }
          }

          // p2j.jar contains resources; ensure it is always the last resource jar, unless specified
          // explicitly in the directory
          jarSet.add(P2J_JAR);

          List<String> resPaths = new ArrayList<>();
          String cp = System.getProperty("java.class.path");
          String[] paths = cp.split(File.pathSeparator);

          // loop through the unqualified resource jar names
          for (String jar : jarSet)
          {
             // loop through the classpath parts to find the paths associated with the resource jars
             for (String path : paths)
             {
                if (path.endsWith(jar))
                {
                   resPaths.add(path);

                   break;
                }
             }
          }

          resourceJarPaths = Collections.unmodifiableList(resPaths);
       }

       /**
        * Registers an entry point as an export.
        * <p>
        * The entry point method can be specified either by name or by name and
        * signature. The short form is acceptable when the method is unique
        * (not overloaded). Overloaded methods require signature specification to
        * be fully resolved.
        *
        * @param    group
        *           export group name
        * @param    entry
        *           export entry name
        * @param    methodSpec
        *           class and method specification as in class.method(signature)
        *           where the (signature) is optional.
        *
        * @return <code>true</code> if succeeded
        */
       private boolean exportMethod(String group, String entry, String methodSpec)
       {
          LOG.finer("registering " + group + ":" + entry + " as " + methodSpec);

          // parse the method specification
          String className = null;
          String methodName = null;
          String signature = null;
          int signInd = methodSpec.indexOf('(');

          if (signInd == -1)
             methodName = methodSpec;
          else
          {
             methodName = methodSpec.substring(0, signInd);
             signature  = methodSpec.substring(signInd);
          }

          signInd = methodName.lastIndexOf('.');

          if (signInd == -1)         // fully qualified name always has dots
             return false;

          className = methodName.substring(0, signInd);
          methodName = methodName.substring(signInd + 1);

          // inspecting the class
          Class<?> appClass = null;

          try
          {
             // can not use Class.forName, as it caches the loaded class in
             // some native code from which it can never be removed
             appClass = MultiClassLoader.getClassLoader().loadClass(className);
          }
          catch (Exception e)
          {
             return false;
          }

          // looking up the method
          Method[] methods = appClass.getMethods();
          Method method = null;
          int mods = 0;
          int count = 0;

          for (int i = 0; i < methods.length; i ++)
          {
             mods = methods[i].getModifiers();
             if (!Modifier.isPublic(mods))
                continue;
             if (methods[i].getName().equals(methodName))
             {
                count ++;
                if (signature != null)
                {
                   if (getSignature(methods[i]).equals(signature))
                   {
                      method = methods[i];
                      break;
                   }
                }
                else
                   method = methods[i];
             }
          }

          if (method == null)                    // no such public method
             return false;
          if (signature == null && count > 1)    // ambiguous method name
             return false;

          // static or instance?
          mods = method.getModifiers();
          Object ref = null;
          if (!Modifier.isStatic(mods))
          {
             // instance method. check if already instantiated the class
             if (!inst.containsKey(className))
             {
                try
                {
                   ref = appClass.getDeclaredConstructor().newInstance();
                }
                catch (Exception e)
                {
                   return false;
                }
                inst.put(className, ref);
             }
             else
                ref = inst.get(className);
          }

          // finally, do the registration
          RoutingKey key = Dispatcher.generateRoutingKey(group, entry);

          if (key == null)
             return false;

          try
          {
             Dispatcher.addMethod(key, ref, method, null);
          }
          catch (Exception e)
          {
             return false;
          }

          return true;
       }

       /**
        * Returns method's signature as text.
        *
        * @param    method
        *           <code>Method</code> to get the signature for.
        *
        * @return   The method signature in parenthesis.
        */
       private String getSignature(Method method)
       {
          Class<?>[] pars = null;
          StringBuilder sb = new StringBuilder();
          int i = 0;

          pars = method.getParameterTypes();
          sb.append("(");
          for (i = 0; i < pars.length; i ++)
          {
             sb.append(pars[i].getName());
             sb.append(",");
          }
          if (i > 0)
             sb.setLength(sb.length() - 1);
          sb.append(")");

          return new String(sb);
       }

       /**
        * Notify server hook about application initialization.
        *
        * @throws  ConfigurationException
        *          If {@code initialize()} method any of {@code serverHooks} fails.
        */
       private void hookInitialize()
       throws ConfigurationException
       {
          for (InitTermListener hook : serverHooks)
          {
             try
             {
                hook.initialize();
             }
             catch (Throwable t)
             {
                throw new ConfigurationException(" Initialization failure", t);
             }
          }
       }

       /**
        * Notify server hook about application termination.
        *
        * @param    t
        *           Parameter which will be passed to
        *           {@link InitTermListener#terminate(Throwable)}.
        */
       private void hookTerminate(Throwable t)
       {
          synchronized(shutdownLock)
          {
             if (!shutdownDone)
             {
                ListIterator<InitTermListener> iterator =
                   serverHooks.listIterator(serverHooks.size());

                while (iterator.hasPrevious())
                {
                   try
                   {
                      iterator.previous().terminate(t);
                   }
                   catch (Throwable e)
                   {
                      LOG.logp(Level.SEVERE,
                               "StandardServer",
                               "hookTerminate",
                               "terminate() failed!",
                               e);
                   }
                }

                shutdownDone = true;
             }
          }
       }

       /**
        * Verifies if the newly forked session is in the same browser as the original client by sending a ping
        * message to the neighbour tabs over the BroadcastChannel of the app.
        *
        * @param    channelName
        *           The name of the BroadcastChannel.
        * @param    relatedSessionId
        *           The ID of the parent session.
        * @param    spawnedClientUuid
        *           The newly spawned client UUID.
        */
       private static void verifySessionFork(String channelName,
                                             String relatedSessionId,
                                             String spawnedClientUuid)
       {
          SessionUtils.setRelatedSessionId(relatedSessionId);
          boolean relatedSessionFound = LogicalTerminal.getClient()
                                                       .pingBroadcastChannel(channelName, relatedSessionId);
          if (!relatedSessionFound)
          {
             String error = "The forked session is not in the same browser as the original session. " +
                "Stopping the client with uuid " + spawnedClientUuid;
             LOG.warning(error);
             throw new StopConditionException(error);
          }
       }

       /**
        * Gets the list of all live sessions with the same device ID and checks if the currently spawned new
        * client can find them in the same browser (by sending a BroadcastChannel message). If there are
        * currently no live sessions with the same device ID or the newly spawned client receives a response
        * from a neighbour session on the same BroadcastChannel, then it's considered to be the correct browser
        * and One Browser Policy verification is completed successfully. Otherwise, the older sessions need to
        * be confirmed dead. Simultaneously and in parallel ping calls are sent to all of their browsers. If
        * any success response is received, then the current newly spawned session is considered in a
        * different browser and is terminated.
        *
        * @param    client
        *           The client network interface for remote calls.
        * @param    appIdentifier
        *           The app identifier to be used as BroadcastChannel name.
        * @param    deviceId
        *           The device ID.
        * @param    spawnedClientUuid
        *           The newly spawned client UUID.
        */
       private static void enforceOneBrowserPolicy(ClientExports client,
                                                   String appIdentifier,
                                                   String deviceId,
                                                   String spawnedClientUuid)
       {
          List<String> liveSessions = WebDriverHandler.getSessionsByDeviceId(deviceId);
          if (liveSessions == null   ||
              liveSessions.isEmpty() ||
              client.findAnyIdInChannel(appIdentifier, liveSessions))
          {
             // no other sessions for the same device id OR
             // one of the recorded sessions is found in the same browser
             // One Browser Policy successfully passes
             return;
          }

          // none of the recorded sessions can be found in the same browser
          List<Future<Boolean>> clientCallFutures = new ArrayList<>();

          for (String liveSessionId : liveSessions)
          {
             ClientExports liveSessionClient = WebDriverHandler.getSessionClientInterface(liveSessionId);
             if (liveSessionClient != null)
             {
                clientCallFutures.add(oneBrowserChecksExecutor.submit(liveSessionClient::pingOnWebSocket));
             }
          }

          int timeout = ConfigItem.BROADCAST_CHANNEL_PING_TIMEOUT.read(
             StorageMessagingWebSocket.DEFAULT_BROADCAST_CHANNEL_PING_TIMEOUT_SEC);

          boolean isAnySessionFoundInSameBrowser = clientCallFutures.parallelStream()
                                                       .map(future ->
                                                            {
                                                               try
                                                               {
                                                                  return future.get(timeout, TimeUnit.SECONDS);
                                                               }
                                                               catch (TimeoutException t)
                                                               {
                                                                  return false;
                                                               }
                                                               catch (Throwable t)
                                                               {
                                                                  LOG.info( "Can't get the WebSocket state of a" +
                                                                               " client.", t);
                                                               }
                                                               return null;
                                                            })
                                                       .filter(isWebSocketLiveResponse -> isWebSocketLiveResponse)
                                                       .findAny()
                                                       .orElse(false);

          if (!isAnySessionFoundInSameBrowser)
          {
             String error = "The current session is not in the same browser as the other live sessions with " +
                "the same device id and should be terminated according to One Browser Policy. Stopping the " +
                "client with uuid " + spawnedClientUuid;
             LOG.warning(error);
             throw new StopConditionException(error);
          }
          else
          {
             // terminate all recorded sessions as dead
             for (String deadSessionId : liveSessions)
             {
                Object sessionNetId = WebDriverHandler.getSessionNetId(deadSessionId);
                if (sessionNetId != null)
                {
                   try
                   {
                      SecurityManager.getInstance().sessionSm.killSession((int) sessionNetId);
                   }
                   catch (RestrictedUseException e)
                   {
                      LOG.warning("Couldn't kill the dead sessions for device id " + deviceId, e);
                   }
                }
             }
          }
       }

       /**
        * Abstract adapter for a basic init/term listener.
        */
       static abstract class AbstractInitTermListener
       implements InitTermListener
       {
          /**
           * Called when the server initializes.
           */
          @Override
          abstract public void initialize();

          /**
           * Called when the server terminates. This routine does nothing.
           *
           * @param    t
           *           If not <code>null</code>, this describes the reason for the server's exit.
           */
          @Override
          public void terminate(Throwable t)
          {
             // do nothing
          }
       }

       /**
        * Executes the exported entry point as a legacy external program.
        */
       private static class LegacyInvoker
       implements Isolatable
       {
          /** The external program name. */
          String programName;

          /**
           * Initialize the invoker to execute the given program name.
           */
          public LegacyInvoker(String programName)
          {
             this.programName = programName;
          }

          /**
           * Execute the {@link #programName external program}, by emulating a RUN statement.
           */
          @Override
          public Object execute()
          throws ReflectiveOperationException
          {
             // reset the user's session-specific elapsed millis counter
             date.elapsed(true);

             // password aging support
             SecurityManager sm = SecurityManager.getInstance();

             if (sm.isPasswordAged())
                sm.changePassword();

             InvokeConfig cfg = new InvokeConfig(programName);
             return ControlFlowOps.invoke(cfg);
          }
       }

       /**
        * Prepares the exported entry point to be called in the properly
        * initialized transactionable environment.
        */
       private static class MainInvoker
       implements Isolatable
       {
          /** Class for the main entry point */
          private final Class<?> cl;

          /** Entry point method */
          private final Method entry;

          /** stop disposition value */
          @SuppressWarnings("unused")
          int stopDisp = -1;

          /**
           * Constructor that simply remembers the parameters of the planned
           * entry point invocation.
           *
           * @param    cls
           *           The class where the called method is defined.
           * @param    method
           *           The method to call.
           * @param    stop
           *           The stop condition disposition value.
           */
          private MainInvoker(Class<?> cls, Method method, int stop)
          {
             cl = cls;
             entry = method;
             stopDisp = stop;
          }

          /**
           * Specifies the method that is executed in a transaction environment,
           *
           * @return the result of the execution or <code>null</code> if void.
           */
          public Object execute()
          throws ReflectiveOperationException
          {
             // reset the user's session-specific elapsed millis counter
             date.elapsed(true);

             // password aging support
             SecurityManager sm = SecurityManager.getInstance();

             if (sm.isPasswordAged())
             {
                sm.changePassword();
             }

             // calling the main entry
             return Utils.invoke(cl, entry, (Object[])null);
          }
       }
    }