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