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