# Agent.java

    /*
    ** Module   : Agent.java
    ** Abstract : Definition and implementation of an appserver agent.
    **
    ** Copyright (c) 2013-2025, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- ---------------------------------Description----------------------------------
    ** 001 CA  20130529 Created initial version.
    ** 002 CA  20130628 Fixed context-local data reset for State-reset operating mode.
    ** 003 CA  20130701 Although official docs mention that State-reset doesn't call the shutdown or
    **                  startup procedure, testing showed startup/shutdown procedures are called for
    **                  this operating mode too.
    **                  Reworked appserver launching - implemented by the AppServerLauncher class.
    ** 004 CA  20130823 Allow each request to be identified via an unique ID. Changes related to
    **                  support async requests.
    ** 005 OM  20131018 Updated to conform the new SessionListener API.
    ** 006 SVL 20131203 Changes caused by renaming of Nameable.get/setName to Nameable.name.
    ** 007 CA  20140218 Always use the full program name when interrogating SourceNameMapper.
    ** 008 CA  20140220 Argument postProcessing needs to be done before the procedure scope is popped.
    **                  The propath needs to be re-initialized after context reset.
    ** 009 CA  20140223 Startup and Connect (State-aware and State-reset) procedures need to be ran
    **                  persistent.
    ** 010 CA  20140326 The remote persistent procedures need to be deleted by the Agent which created
    **                  them.
    ** 011 CA  20140509 Fixed passing the parameter to the startup procedure.
    ** 012 CA  20140407 prepare() needs to force loading of the context-local DatabaseTriggerManager.
    ** 013 SVL 20140414 Implemented processing for TableParameters, added "appserver-agent" scope.
    ** 014 SVL 20140709 Added fall-through codes for table parameters errors.
    ** 015 CA  20150827 Release the memory for all memptr arguments, before returning to the caller.
    ** 016 CA  20151222 Any ERROR conditions raised during instantiation in obtainProxy are sent back
    **                  to the caller (also, the pushed scope must be removed).
    ** 017 CA  20160404 Enhanced the appserver invocations, to allow implicit or explicit termination
    **                  of requests being executing on a certain connection.
    ** 018 CA  20160422 When resetting the Agent's context, make a bogus call to the P2J client-side,
    **                  to clean up the frames (via the StateSynchronizer).
    **                  Fixed deadlock when the Agent's java process is killed while it has control.
    ** 019 CA  20160626 Let the Agent recover from an UnstoppableExitException.
    ** 020 GES 20171220 Added TODO comment.
    ** 021 GES 20180102 Clarified an error handling comment.
    ** 022 CA  20180503 Added SESSION:SERVER-CONNECTION-ID support.
    ** 023 CA  20190327 If the command received by waitForCommand is on the same thread as the Agent's
    **                  listening thread, execute immediately.
    ** 024 CA  20190612 Added APIs to create remote legacy objects and invoke methods on them.
    ** 025 CA  20190628 Proper SINGLETON and SINGLE-RUN implementation; misc improvements.
    **     CA  20190703 Added WebHandler service support.
    ** 026 CA  20190710 Hooked runtime for legacy WebHandler support.
    **     CA  20190714 Added DATASET support.
    **     CA  20190811 Fixed context reset.
    ** 027 CA  20191119 DATASET/TEMP-TABLE parameters register themselves as belonging to a bogus
    **                  class.
    ** 028 CA  20200427 Refactoring to allow support for CURRENT-REQUEST-INFO and CURRENT-RESPONSE-INFO
    **                  transfer from the SERVER:REQUEST-INFO and to SERVER:RESPONSE-INFO.
    **     CA  20200514 Fixed error handling for handleWebService.
    **     CA  20200528 Fixes for extent arguments.
    ** 029 IAS 20200722 Improved logging.
    **     CA  20200827 Reworked asynchronous invocations to perform all context-local work on the Conversation
    **                  thread, and let only the actual invocation be performed in an AssociatedThread.
    **     RFB 20200926 Removed use of localMode since the 4GL doesn't care about the signature at an IN SUPER
    **                  procedure decalaration. ControlFlowOps should validate the arguments.
    **     CA  20201027 The remote side can delete the IN HANDLE used by the requester at the RUN statement.
    **     SVL 20210127 Assign local RETURN-VALUE after an appserver call only if RETURN-VALUE of the remote
    **                  procedure was set during this call.
    **     SVL 20210202 Assign return value to an invocation result only if the value was set during this call.
    **     CA  20210405 Again fixed the Agent context re-initialization in STATE-RESET mode.
    **     IAS 20210520 Added support for session-based database auto-connect
    **     CA  20211112 Fixed error management in multi-session agent appserver.
    **     CA  20211214 For State-free appservers, the agents can be bound to remote persistent procedures; this
    **                  binding must be made using the agent's ID, as a client can invoke multiple remote
    **                  persistent procedures, and using the procedure's ID for this binding can lead to
    **                  collisions, as the pair (connection ID, procedure ID) is not guaranteed to be unique for
    **                  a connection.
    **     CA  20211216 Fixed the agent's task execution timeout (is specified in seconds).
    **     CA  20220104 Fixed Stateless appserver mode, which got broken in CA/20211214 - now the agent tracks any
    **                  persistent procedure ran over it,
    **     CA  20220111 Fixed SESSION:SERVER-CONNECTION-CONTEXT - this value must be shared to any agent running
    **                  requests for a connection.
    **     CA  20220211 An OUTPUT memptr parameter must not be set to unknown if the MemoryBuffer has no bytes.
    **     CA  20220310 Ensure the 'appserver-agent' root block is popped when the agent is reset and when it
    **                  terminates - this is required because this block will allow dynamic temp-tables to be
    **                  cleaned (i.e. the physical temp-table actually dropped).
    **     CA  20220329 Added support for REST services written directly in Java.
    **     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.
    **     TJD 20220504 Java 11 compatibility related minor changes
    **     CA  20220930 InvokeConfig.target is now a String, not a character BDT.
    **     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.
    **     TJD 20220504 Java 11 compatibility related minor changes
    **     CA  20220428 Fixed memptr and extent output arguments.
    **     GES 20210501 Upgraded table parameter processing to match API changes.
    **     CA  20220602 Added basic support for transport of object instances over appserver call.
    **     CA  20220613 STOP conditions must emit as StopConditionException on the caller side.
    **     SVL 20230113 Improved performance by replacing some "for-each" loops with indexed "for" loops.
    ** 030 GBB 20230403 Init LOG-MANAGER on prepare to handle State-reset.
    ** 031 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
    ** 032 GBB 20230608 RuntimeException handling for LOG-MANAGER initialization.
    ** 033 CA  20230607 The resource delete must be wrapped in 'executeScoped', too.
    ** 034 CA  20230712 Added 'invokeWithArgs', an API which can send any number of arguments to a remote program
    **                  acting as a controller, which can prepare and execute the real target.
    ** 035 GBB 20230825 SecurityManager context & legacy web methods calls updated.
    ** 036 GBB 20240311 Fixing class name in file header.
    ** 037 CA  20240328 Improved processing of remote dataset/table arguments for invokeWithArgs.
    ** 038 AL2 20240416 Called invokeActivate and invokeDeactivate for invokeWithArgs.
    ** 039 GBB 20240429 InterruptedException to log on Level.FINE.
    ** 040 GBB 20240513 Agent thread id and session id added to multi-session agent appserver requests.
    ** 041 CA  20240614 Fixed ACTIVATE/DEACTIVATE execution for persistent and other misc APIs.
    ** 042 CA  20240729 Fixed deactivate on procedure delete: the deactivate procedure must be invoked before the
    **                  agent gets released.
    ** 043 RAA 20240513 Notify the ConnectionManager (for multi-tenancy purposes) that the state has been reset.
    ** 044 GBB 20241010 Making the class public. Adding fields startupTime, pid, runningProc,
    **                  runningProcStartTime. Adding getters for the new fields. Invoke methods to call method
    **                  setRunningProc. New method getState().
    ** 045 GBB 20241022 Fixing invoke method to consider inHandle as instance of DeferredProgram or String.
    ** 046 CA  20241113 Allow posting a task on the agent running in the current context - this can be used to
    **                  post tasks from different threads sharing the agent's context.
    ** 047 AP  20250115 Moved some exception handling logic from being executed on Dispatcher thread to Conversation thread.
    **                  Cleanup some dead code in Agent.invokeWithArgs.
    ** 048 GBB 20250116 Removing redundant arg from AppServerLauncher.notifyLaunch call.
    ** 049 FER 20250220 Added redirection to stdout using NullStream in Agent.prepare().
    ** 050 AP  20250328 Fixed synchronization issue when the Agent is terminated.
    **                  Consumed Thread's interrupted status before executing the shutdown procedure.
    ** 051 GBB 20250403 Move common logic with MSA appserver to CoreAppserver.java.
    */

    /*
    ** 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.util;

    import java.util.*;
    import java.util.Queue;
    import java.util.function.*;
    import java.util.logging.*;

    import javax.servlet.http.*;

    import com.goldencode.p2j.main.*;
    import com.goldencode.p2j.net.*;
    import com.goldencode.p2j.net.Session;
    import com.goldencode.p2j.oo.lang.*;
    import com.goldencode.p2j.oo.web.WebHandler;
    import com.goldencode.p2j.persist.*;
    import com.goldencode.p2j.persist.trigger.*;
    import com.goldencode.p2j.security.*;
    import com.goldencode.p2j.security.SecurityManager;
    import com.goldencode.p2j.ui.*;
    import com.goldencode.p2j.util.appserver.*;
    import com.goldencode.p2j.util.logging.*;

    /**
     * An apserver agent definition.
     */
    public class Agent
    {
       /**
        * List of possible invocation modes:
        * <ul>
        * <li>{@link #IMMEDIATELY} - in this thread (the thread must be an agent thread)</li>
        * <li>{@link #AGENT_WAIT} - in a different agent, wait for response</li>
        * <li>{@link #AGENT_NO_WAIT} in a different agent, do not wait for response</li>
        * </ul>
        */
       enum InvocationModes
       {
          IMMEDIATELY,
          AGENT_WAIT,
          AGENT_NO_WAIT
       }

       /** Logger */
       private static final CentralLogger LOG = CentralLogger.get(AppServerManager.class);

       /** Timeout for the actual thread interruption, expressed in ms. */
       private static final long THREAD_INTERRUPT_TIMEOUT = 1000;

       /**
        * The set of web service tokens currently being active (with an API call in progress).  As the FWD context
        * is not thread-safe, only one API call can be performed at a certain time.
        * TODO: remove this once the FWD context allows multiple concurrent requests.
        */
       private static final Set<String> activeWebServiceTokens = new HashSet<>();

       /** The command queue for this agent. */
       private final Queue<AgentCommand> commands = new LinkedList<AgentCommand>();

       /** The agent's pool. */
       private final AgentPool pool;

       /** The agent's stopper worker. */
       private final AgentStopper stopperWorker = new AgentStopper();

       /**
        * Dedicated thread to stop any job currently being executed by this Agent.
        * The thread is running under the Agent's context.
        */
       private final AssociatedThread stopper = new AssociatedThread(stopperWorker);

       /**
        * Consumer anonymous instance accepting connection id and session id and binding the MSA session to the
        * connection.
        */
       private final BiConsumer<String, Integer> BIND_PERSISTENT_FUNC =
          (connectionId, sessionId) -> bind();

       /**
        * Consumer anonymous instance accepting connection id and session id and unbinding the MSA session from
        * the connection.
        */
       private final BiConsumer<String, Integer> UNBIND_PERSISTENT_FUNC =
          (connectionId, sessionId) -> unbind();

       /**
        * Consumer anonymous instance accepting connection id and program handle and associating the persistent
        * resource with the connection.
        */
       private final BiConsumer<String, handle> ADD_PROC_FUNC =
          (connectionId, ahandle) -> addProcedure(ahandle);

       /**
        * Consumer anonymous instance accepting connection id and program res id and removing the associating of
        * the persistent resource from the connection.
        */
       private final BiConsumer<String, String> REMOVE_PROC_FUNC =
          (connectionId, procId) -> removeProcedure(procId);

       /**
        * Consumer anonymous instance accepting connection id and the name of the currently running program and
        * creating association between them.
        */
       private final BiConsumer<String, String> SET_RUNNING_PROC_FUNC =
          (connectionId, procName) -> setRunningProc(procName);

       /** Flag indicating if the agent has been terminated. */
       private boolean terminated = false;

       /** Flag indicating the agent has been bound to the connection. */
       private boolean bound = false;

       /** The agent's listening thread. */
       private Thread thread;

       /** The agent startup time. */
       private Date startupTime;

       /** The agent pid. */
       private long pid;

       /** The agent's ID, unique. */
       private final int agentId;

       /** The currently running procedure on this agent. */
       private volatile String runningProc;

       /** The time the currently running procedure on this agent started. */
       private volatile Date runningProcStartTime;

       /** The remote procedures ran on this agent. */
       private Map<String, AppserverRemoteProcedure> remoteProcedures = new HashMap<>();

       /**
        * Build a new agent and associate it with the given pool.
        *
        * @param    pool
        *           The agent's pool.
        * @param    agentId
        *           The agent ID.
        */
       public Agent(AgentPool pool, int agentId)
       {
          this.pool = pool;
          this.agentId = agentId;
       }

       /**
        * Check if the specified web service token is currently in use (there is an active API call on an agent).
        *
        * @param    token
        *           The web service token.
        *
        * @return   <code>true</code> if {@link #activeWebServiceTokens} contains this token.
        */
       static boolean isWebServiceTokenInUse(String token)
       {
          synchronized (activeWebServiceTokens)
          {
             return activeWebServiceTokens.contains(token);
          }
       }

       /**
        * Returns the state of the agent: AVAILABLE or RUNNING.
        *
        * @return   See above.
        */
       public String getState()
       {
          return commands.size() == 0 && runningProc == null ? "AVAILABLE" : "RUNNING";
       }

       /**
        * Get the agentId for this session.
        *
        * @return   The {@link #agentId}.
        */
       public int getAgentId()
       {
          return agentId;
       }

       /**
        * Remove the external procedure from this agent, as it was deleted.
        *
        * @param    procId
        *           The remote procedure ID.
        */
       public synchronized void removeProcedure(String procId)
       {
          AppserverRemoteProcedure remote = remoteProcedures.remove(procId);
          String connectionId = remote.getRequesterConnectionId();
          pool.removeProcedure(connectionId, remote.getExtProg().get());
       }

       /**
        * Register the given external procedure with this agent, as a remote procedure.
        *
        * @param    extProg
        *           The external procedure.
        */
       public synchronized void addProcedure(handle extProg)
       {
          AppserverRemoteProcedure remote = new AppserverRemoteProcedure(extProg);

          this.remoteProcedures.put(remote.getExtProgResourceId(), remote);
          pool.addProcedure(remote.getExtProg().get(), this);
       }

       /**
        * Get the external procedure associated with this agent.
        *
        * @return   The procedure associated for this ID or <code>null</code> if is not registered with
        *           {@link #remoteProcedures}.
        */
       public synchronized ExternalProgramWrapper getProcedure(String resId)
       {
          AppserverRemoteProcedure remote = remoteProcedures.get(resId);
          return remote == null ? null : remote.getExtProg();
       }

       /**
        * Check if the given resource ID is for a persistent procedure an on this agent.
        *
        * @param    resId
        *           The procedure ID.
        *
        * @return   See above.
        */
       public synchronized boolean hasProcedure(String resId)
       {
          return remoteProcedures.containsKey(resId);
       }

       /**
        * Get the procedure name for the given resource ID.
        *
        * @param    resId
        *           The procedure ID.
        *
        * @return   See above.
        */
       public synchronized String getProcedureName(String resId)
       {
          AppserverRemoteProcedure remote = remoteProcedures.get(resId);
          return remote == null ? null : remote.getProcedureName();
       }

       /**
        * Get the currently running procedure on this agent.
        *
        * @return   See above.
        */
       public synchronized String getRunningProc()
       {
          return runningProc;
       }

       /**
        * Get the time the currently running procedure on this agent started.
        *
        * @return   See above.
        */
       public synchronized Date getRunningProcStartTime()
       {
          return runningProcStartTime;
       }

       /**
        * Get the agent startup time.
        *
        * @return   See above.
        */
       public synchronized Date getStartupTime()
       {
          return startupTime;
       }

       /**
        * Get the agent pid.
        *
        * @return   See above.
        */
       public synchronized long getPid()
       {
          return pid;
       }

       /**
        * Start the agent and listen for commands.
        */
       public void listen()
       {
          this.thread = Thread.currentThread();
          this.startupTime = new Date();
          this.pid = SecurityManager.getInstance().sessionSm.getCurrentSession().getPid();

          // register a listener to terminate the agent, if its session terminates
          Session sess = SessionManager.get().getSession();
          sess.addSessionListener(new SessionListener()
          {
             /**
              * This callback will be called when the P2J client session is ending.
              *
              * @param    session
              *           The session that is ending.
              */
             @Override
             public void terminate(Session session)
             {
                // terminate the stopper worker explicitly - otherwise it will deadlock when the queue
                // tears down
                stopperWorker.terminate();

                Agent.this.terminate();
             }

             /**
              * This callback will be called after the security context has been set up.
              *
              * @param    session
              *           The session that is starting.
              */
             @Override
             public void initialize(Session session)
             {
                // no-ops
             }
          });

          try
          {
             prepare();

             String startup = pool.getStartup();
             // although official docs mention that state-reset doesn't call the startup procedure,
             // testing showed that startup procedure is called
             handle hStartup = new handle();
             if (startup != null && startup.length() > 0)
             {
                SessionUtils.setClientTypes(AppServerManager.isMultiSession() ?
                                              ClientType.MULTI_SESSION_AGENT :
                                              ClientType.APPSERVER);

                setRunningProc(startup);

                String startupParameter = pool.getStartupParameter();
                if (!invoke(null, null, InvocationModes.IMMEDIATELY, startup, hStartup, "Startup",
                            "I", new character(startupParameter)))
                {
                   LOG.warning("There were problems invoking the startup procedure, terminate the agent: " + pool.getName());
                   // if there were problems invoking the startup procedure, terminate the agent
                   return;
                }
                setRunningProc(null);
             }

             if (LOG.isLoggable(Level.FINE))
             {
                LOG.log(Level.FINE, String.format("About to notify launcher [%s]", pool.getName()), new Exception());
             }
             // notify the launch
             AppServerLauncher.notifyLaunch(pool.getName());

             // start the agent's stopper thread
             stopper.setName("Agent Stopper for " + Thread.currentThread().getName());
             stopper.setDaemon(true);
             stopper.start();

             while (!terminated())
             {
                AgentCommand command = null;

                // this loop will continue until someone sends the terminate command:
                // - in case the session is ended (P2J client gets killed)
                // - in case the manager is ended

                boolean interrupted = false;
                synchronized (this)
                {
                   if (commands.size() == 0)
                   {
                      try
                      {
                         wait();
                      }
                      catch (InterruptedException e)
                      {
                         interrupted = true;
                         LOG.log(Level.FINE, "Agent was interrupted for appserver " + pool.getName(), e);
                      }
                   }

                   // get the last command (or null if none)
                   command = commands.poll();
                }

                // someone has interrupted my waiting, check if the session is still alive
                if (interrupted && !sess.isRunning())
                {
                   // mark the agent as terminated
                   synchronized (this)
                   {
                      terminated = true;
                   }

                   stopper.interrupt();
                   return;
                }

                if (command == null)
                {
                   // no command yet, continue
                   continue;
                }

                // execute only if an explicit command is not already stopped
                int reqId = command.getRequestId();
                String connId = command.getConnectionId();

                if (reqId == 0 || AppServerManager.registerRequest(reqId, this))
                {
                   AppServerManager.addActiveAgent(connId, this);

                   try
                   {
                      executeCommand(command);
                   }
                   finally
                   {
                      AppServerManager.removeActiveAgent(connId, this);

                      if (reqId != 0)
                      {
                         AppServerManager.deregisterRequest(reqId);
                      }
                      command.finished();
                   }
                }
             }

             // consume the interrupted status set by the agent stopper, but which didn't raise the
             // InterruptedException
             if (Thread.interrupted())
             {
                LOG.log(Level.FINE, "Agent was interrupted for appserver " + pool.getName());
             }

             String shutdown = pool.getShutdown();

             setRunningProc(shutdown);

             // although official docs mention that state-reset doesn't call the shutdown procedure,
             // testing showed that shutdown procedure is called
             if (shutdown != null && shutdown.length() > 0)
             {
                // ignore errors here
                invoke(null, null, InvocationModes.IMMEDIATELY, shutdown, null, "Shutdown", null);
                setRunningProc(null);
             }

             // the startup procedure handle is deleted automatically, as the agent context is gone
             // we can't force an explicit delete, because we are outside of a P2J external block.
          }
          catch (Throwable t)
          {
             LOG.warning("", t);
          }
          finally
          {
             // pop the global 'appserver-agent' scope.  this scope is pushed in the 'prepare' method, and it needs
             // to be popped here, to allow processing of the dynamic DMOs and other global finalizables
             TransactionManager.popScope();

             // notify the agent's stopper thread
             stopper.interrupt();
          }
       }

       /**
        * Reset the context of this agent.  Called only if the appserver is in State-reset operating
        * mode.
        *
        * @param    mode
        *           How to execute the reset context command - immediately or send it to the agent.
        */
       public void resetContext(InvocationModes mode)
       {
          AgentCommand command = new ResetContextCommand();
          switch (mode)
          {
             case IMMEDIATELY:
                executeCommand(command);
                break;
             default:
                waitForCommand(null, command);
                break;
          }
       }

       /**
        * Execute the given custom task.
        *
        * @param    work
        *           The task to execute.
        * @param    requestId
        *           The ID of the request.
        * @param    wait
        *           Flag indicating the method will not return until the task executes; otherwise, the method
        *           returns immediately.
        */
       public void postTask(Runnable work, int requestId, boolean wait)
       {
          if (terminated())
          {
             LOG.warning("Trying to execute a command on a terminated agent!");
             return;
          }

          final AppServerInvocationResult invocationResult = new AppServerInvocationResult();
          int timeout = 0;
          String id = "fwd-internal";
          AgentCommand command = new AgentCommand(invocationResult, id, requestId, timeout)
          {
             {
                setRunningProgram("explicit-task");
             }

             @Override
             public void execute()
             {
                CoreAppserver.executeScoped(() ->
                {
                   work.run();

                   return null;
                });
             }
          };

          if (wait)
          {
             waitForCommand(invocationResult, command);
          }
          else
          {
             sendCommand(command);
          }
       }

       /**
        * Create a new instance of the specified qualified class name.
        *
        * @param    id
        *           The appserver connection id.
        * @param    requestId
        *           The ID of the request.
        * @param    timeout
        *           The maximum allowed time for this invocation to complete.  In milliseconds.
        *           Use <code>0</code> to disable the timeout.
        * @param    name
        *           The legacy qualified class name for the legacy class.
        * @param    modes
        *           A string representation of the modes of each parameter. May be <code>null</code>
        * @param    args
        *           The constructor's arguments.
        *
        * @return   A {@link AppServerInvocationResult} instance, which holds the code for the remote
        *           object, all updated OUTPUT/INPUT-OUTPUT arguments and any returned value.
        */
       public AppServerInvocationResult newInstance(String    id,
                                                    int       requestId,
                                                    long      timeout,
                                                    character name,
                                                    String    modes,
                                                    Object... args)
       {
          final AppServerInvocationResult invocationResult = new AppServerInvocationResult();

          waitForCommand(invocationResult, new AgentCommand(invocationResult, id, requestId, timeout)
          {
             @Override
             public void execute()
             {
                CoreAppserver.newInstance(invocationResult,
                                          id,
                                          agentId,
                                          getSessionId(),
                                          pool.getActivate(),
                                          pool.getDeactivate(),
                                          Agent.this::isBound,
                                          SET_RUNNING_PROC_FUNC,
                                          BIND_PERSISTENT_FUNC,
                                          UNBIND_PERSISTENT_FUNC,
                                          ADD_PROC_FUNC,
                                          (connectionId) -> AppServerManager.disconnect(pool, connectionId),
                                          name,
                                          modes,
                                          args);
             }
          });

          return invocationResult;
       }

       /**
        * Invoke a method in a remotely instantiated class.
        *
        * @param    id
        *           The appserver connection id.
        * @param    requestId
        *           The ID of the request.
        * @param    timeout
        *           The maximum allowed time for this invocation to complete.  In milliseconds.
        *           Use <code>0</code> to disable the timeout.
        * @param    returnValue
        *           Flag indicating if the return value is required.
        * @param    name
        *           The legacy 4GL name for the class method.
        * @param    resId
        *           The code of a remotely instantiated object in which to invoke the method.
        * @param    modes
        *           A string representation of the modes of each parameter. May be <code>null</code>
        * @param    args
        *           The method's arguments.
        *
        * @return    A {@link AppServerInvocationResult} instance with all updated OUTPUT/INPUT-OUTPUT
        *            arguments and any returned value.
        */
       public AppServerInvocationResult invokeMethod(String    id,
                                                     int       requestId,
                                                     long      timeout,
                                                     boolean   returnValue,
                                                     character name,
                                                     String    resId,
                                                     String    modes,
                                                     Object... args)
       {
          final AppServerInvocationResult invocationResult = new AppServerInvocationResult();

          waitForCommand(invocationResult, new AgentCommand(invocationResult, id, requestId, timeout)
          {
             @Override
             public void execute()
             {
                CoreAppserver.invokeMethod(invocationResult,
                                           isMultiSession(),
                                           id,
                                           Agent.this::isBound,
                                           SET_RUNNING_PROC_FUNC,
                                           returnValue,
                                           name,
                                           resId,
                                           modes,
                                           args);
             }
          });

          return invocationResult;
       }

       /**
        * Invoke the specified external procedure as persistent and wait (or not) for it to complete.
        * This is used to invoke any connect/disconnect and startup/shutdown procedures.
        * <p>
        * The "kind" parameter will always receive one of the "Startup", "Shutdown", "Connect" or
        * "Disconnect" values.
        *
        * @param    res
        *           The instance where to save the invocation result. May be <code>null</code>.
        * @param    id
        *           The appserver connection id.
        * @param    mode
        *           Flag indicating how to invoke the external procedure.
        * @param    proc
        *           The legacy 4GL name for the external procedure.
        * @param    hProc
        *           If not-null, the handle where to save this procedure.
        * @param    kind
        *           A helper text about this procedure.
        * @param    modes
        *           A string representation of the modes of each parameter. May be <code>null</code>
        * @param    args
        *           The procedure's arguments.
        *
        * @return   <code>true</code> if no error was raised during the procedure invocation.
        */
       public boolean invoke(AppServerInvocationResult res,
                             final String              id,
                             final InvocationModes     mode,
                             final String              proc,
                             final handle              hProc,
                             final String              kind,
                             final String              modes,
                             final Object...           args)
       {
          return invoke(null, res, id, mode, proc, hProc, kind, modes, args);
       }

       /**
        * Invoke the specified external procedure as persistent and wait (or not) for it to complete.
        * This is used to invoke any connect/disconnect and startup/shutdown procedures.
        * <p>
        * The "kind" parameter will always receive one of the "Startup", "Shutdown", "Connect" or
        * "Disconnect" values.
        *
        * @param    requestInfo
        *           The remote {@link com.goldencode.p2j.oo.lang.OerequestInfo} details, as set by
        *           {@link com.goldencode.p2j.oo.lang.OerequestInfo#toArray()}.
        * @param    res
        *           The instance where to save the invocation result. May be <code>null</code>.
        * @param    id
        *           The appserver connection id.
        * @param    mode
        *           Flag indicating how to invoke the external procedure.
        * @param    proc
        *           The legacy 4GL name for the external procedure.
        * @param    hProc
        *           If not-null, the handle where to save this procedure.
        * @param    kind
        *           A helper text about this procedure.
        * @param    modes
        *           A string representation of the modes of each parameter. May be <code>null</code>
        * @param    args
        *           The procedure's arguments.
        *
        * @return   <code>true</code> if no error was raised during the procedure invocation.
        */
       public boolean invoke(Object[]                  requestInfo,
                             AppServerInvocationResult res,
                             final String              id,
                             final InvocationModes     mode,
                             final String              proc,
                             final handle              hProc,
                             final String              kind,
                             final String              modes,
                             final Object...           args)
       {
          final AppServerInvocationResult result = (res == null ? new AppServerInvocationResult()
                                                                : res);

          AgentCommand command = new AgentCommand(requestInfo, result, id)
          {
             {
                setRunningProgram(proc);
             }

             @Override
             public void execute()
             {
                // the procedure is ran persistent only in certain cases:
                // 1. startup procedure
                //   - automatically deleted when agent terminates
                //   - can be explicitly deleted via "delete object this-procedure".
                // 2. connect procedure
                //   - only for State-reset and State-aware modes
                //   - can be explicitly deleted via "delete object this-procedure".
                //   - automatically deleted on context reset, if State-reset

                // in both cases, the explicit delete is postponed until the program (startup or
                // connect) terminates

                AppServerOperatingMode mode = pool.getOperatingMode();
                boolean persistentConnect = ("Connect".equals(kind) &&
                                             (mode == AppServerOperatingMode.STATE_RESET ||
                                              mode == AppServerOperatingMode.STATE_AWARE));
                boolean startup = ("Startup".equals(kind));

                boolean persistent = (persistentConnect || startup);

                // add the topmost scope to the TransactionManager
                TransactionManager.pushScope("startup",
                                             TransactionManager.NO_TRANSACTION,
                                             true,
                                             true,
                                             false,
                                             false);
                try
                {
                   if (persistent)
                   {
                      ControlFlowOps.invokePersistentSetWithMode(proc, hProc, modes, args);

                   }
                   else
                   {
                      ControlFlowOps.invokeExternalProcedure(new character(proc), false, null, false,
                                                             null, modes, args);
                   }
                }
                catch (Throwable t)
                {
                   if (UnstoppableExitException.isCausing(t))
                   {
                      LogicalTerminal.activateBatchMode(true);
                   }
                   else
                   {
                      LOG.log(Level.WARNING, t, "%s procedure has failed for appserver %s:", kind,
                              pool.getName());

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

                if (ControlFlowOps.isReturnValueSetInRootScope())
                {
                   result.setResult(ControlFlowOps.getReturnValue());
                }

                // check if we are calling the disconnect procedure
                if ("Disconnect".equals(kind))
                {
                   pool.terminateConnection(id, true);
                }
             }
          };

          switch (mode)
          {
             case IMMEDIATELY:
                try
                {
                   executeCommand(command);
                }
                catch (Throwable t)
                {
                   result.setError(t);
                }
                break;

             case AGENT_NO_WAIT:
                sendCommand(command);
                break;

             case AGENT_WAIT:
                waitForCommand(result, command);
                break;
          }

          return (result.getError() == null);
       }

       /**
        * Invoke a remote external procedure, internal procedure or user-defined function. The
        * validation for the program name and arguments will be done on the remote side.
        *
        * @param    id
        *           The appserver connection id.
        * @param    requestId
        *           The ID of the request.
        * @param    timeout
        *           The maximum allowed time for this invocation to complete.  In milliseconds.
        *           Use <code>0</code> to disable the timeout.
        * @param    cfg
        *           The invoke configuration.
        *
        * @return    A {@link AppServerInvocationResult} instance with all updated OUTPUT/INPUT-OUTPUT
        *            arguments and any returned value.
        */
       public AppServerInvocationResult invoke(final String    id,
                                               final int       requestId,
                                               final long      timeout,
                                               final InvokeConfig cfg)
       {
          String token = cfg.getWebServiceToken();

          final AppServerInvocationResult invocationResult = new AppServerInvocationResult();
          waitForCommand(invocationResult, token, new AgentCommand(invocationResult, id, requestId, timeout)
          {
             @Override
             public void execute()
             {
                if (!cfg.isClass() && cfg.getProcedureHandle() != null)
                {
                   CoreAppserver.runPersistent(invocationResult,
                                               pool.isMultiSession(),
                                               pool.getExports(),
                                               pool.getActivate(),
                                               pool.getDeactivate(),
                                               Agent.this::isBound,
                                               id,
                                               agentId,
                                               getThreadId(),
                                               getSessionId(),
                                               cfg,
                                               SET_RUNNING_PROC_FUNC,
                                               BIND_PERSISTENT_FUNC,
                                               UNBIND_PERSISTENT_FUNC,
                                               ADD_PROC_FUNC,
                                               (connectionId) -> AppServerManager.disconnect(pool, connectionId));
                }
                else
                {
                   CoreAppserver.invoke(invocationResult,
                                        pool.isMultiSession(),
                                        false,
                                        pool.getExports(),
                                        pool.getActivate(),
                                        pool.getDeactivate(),
                                        Agent.this::isBound,
                                        id,
                                        agentId,
                                        getThreadId(),
                                        getSessionId(),
                                        cfg,
                                        null,
                                        SET_RUNNING_PROC_FUNC,
                                        REMOVE_PROC_FUNC,
                                        (connectionId) -> AppServerManager.disconnect(pool, connectionId));
                }
             }
          });

          return invocationResult;
       }

       /**
        * Invoke a remote external procedure, internal procedure or user-defined function. The
        * validation for the program name and arguments will be done on the remote side.
        *
        * @param    id
        *           The appserver connection id.
        * @param    requestId
        *           The ID of the request.
        * @param    timeout
        *           The maximum allowed time for this invocation to complete.  In milliseconds.
        *           Use <code>0</code> to disable the timeout.
        * @param    name
        *           The legacy 4GL name for the procedure/function.
        * @param    resId
        *           The code of a remote procedure in which to invoke the procedure or function.
        * @param    function
        *           <code>true</code> if this is a function call.
        * @param    dynamicFunction
        *           <code>true</code> if this is a DYNAMIC-FUNCTION call.
        * @param    superCall
        *           <code>true</code> if this is a RUN SUPER or SUPER() call.
        * @param    transactionDistinct
        *           Flag indicating if the TRANSACTION DISTINCT clause is in effect.
        * @param    modes
        *           A string representation of the modes of each parameter. May be <code>null</code>
        * @param    args
        *           The procedure's arguments.
        *
        * @return    A {@link AppServerInvocationResult} instance with all updated OUTPUT/INPUT-OUTPUT
        *            arguments and any returned value.
        */
       public AppServerInvocationResult invoke(final String    id,
                                               final int       requestId,
                                               final long      timeout,
                                               final character name,
                                               final String    resId,
                                               final boolean   function,
                                               final boolean   dynamicFunction,
                                               final boolean   superCall,
                                               final boolean   transactionDistinct,
                                               final String    modes,
                                               final Object... args)
       {
          final AppServerInvocationResult invocationResult = new AppServerInvocationResult();

          waitForCommand(invocationResult, new AgentCommand(invocationResult, id, requestId, timeout)
          {
             private boolean isExternalProgramCall = resId == null;

             {
                String runningProgram;
                if (isExternalProgramCall)
                {
                   runningProgram = name.toStringMessage();
                }
                else
                {
                   ExternalProgramWrapper proc = getProcedure(resId);
                   runningProgram = proc.name().toStringMessage() + " # " + name.toStringMessage();
                }
                setRunningProgram(runningProgram);
             }

             @Override
             public void execute()
             {
                if (!invokeActivate(id, invocationResult))
                {
                   return;
                }

                // resolve the procedure - this is actually 'extProg', validated when the agent was acquired.
                handle inProc = (isExternalProgramCall ? null : handle.fromString(resId));
                boolean isQuit = false;
                try
                {
                   isQuit = ((logical) CoreAppserver.executeScoped(() ->
                   {
                      return new logical(CoreAppserver.invokeImpl(invocationResult,
                                                                  isMultiSession(),
                                                                  pool.getExports(),
                                                                  name,
                                                                  inProc,
                                                                  function,
                                                                  dynamicFunction,
                                                                  superCall,
                                                                  transactionDistinct,
                                                                  modes,
                                                                  args));
                   })).booleanValue();
                }
                finally
                {
                   if (isExternalProgramCall)
                   {
                      CoreAppserver.invokeDeactivate(pool.getDeactivate(), isBound(), id, SET_RUNNING_PROC_FUNC);
                   }
                   else if (!inProc._isValid())
                   {
                      removeProcedure(resId);
                   }

                   if (isQuit)
                   {
                      AppServerManager.disconnect(pool, id);
                   }
                }
             }
          });

          return invocationResult;
       }

       /**
        * Invoke the given Java method directly.
        * <p>
        * All arguments are sent by references: if it was marked as an OUTPUT or INPUT-OUTPUT, any change to that
        * instance will be reflected back to the caller.
        *
        * @param    id
        *           The appserver connection id.
        * @param    requestId
        *           The ID of the request.
        * @param    timeout
        *           The maximum allowed time for this invocation to complete.  In milliseconds.
        *           Use <code>0</code> to disable the timeout.
        * @param    cfg
        *           The configuration with the Java method to be invoked.
        *
        * @return   A {@link AppServerInvocationResult} instance with all updated OUTPUT/INPUT-OUTPUT
        *           arguments and any returned value.
        */
       public AppServerInvocationResult invoke(String id, int requestId, int timeout, JavaInvokeConfig cfg)
       {
          String token = cfg.getWebServiceToken();

          final AppServerInvocationResult invocationResult = new AppServerInvocationResult();
          waitForCommand(invocationResult, token,
          new AgentCommand(invocationResult, id, requestId, timeout)
          {
             @Override
             public void execute()
             {
                CoreAppserver.invokeJavaMethod(invocationResult,
                                               id,
                                               isMultiSession(),
                                               pool.getActivate(),
                                               pool.getDeactivate(),
                                               Agent.this::isBound,
                                               SET_RUNNING_PROC_FUNC,
                                               (connectionId) -> AppServerManager.disconnect(pool, connectionId),
                                               cfg);
             }
          });

          return invocationResult;
       }

       /**
        * Obtain a proxy for the given external procedure. This will also reserve an agent which will
        * be used for all subsequent invocation (including external procedure initialization).
        *
        * @param    hProc
        *           The procedure handle.
        * @param    result
        *           The invocation result container.
        * @param    id
        *           The appserver connection id.
        * @param    name
        *           A name of a remote external procedure.
        */
       void obtainProxy(handle hProc, AppServerInvocationResult result, String id, String name)
       {
          waitForCommand(null, new AgentCommand(null, id)
          {
             @Override
             public void execute()
             {
                CoreAppserver.obtainProxy(hProc,
                                          result,
                                          pool.getExports(),
                                          pool.getActivate(),
                                          pool.getDeactivate(),
                                          Agent.this::isBound,
                                          id,
                                          agentId,
                                          getSessionId(),
                                          name,
                                          SET_RUNNING_PROC_FUNC,
                                          BIND_PERSISTENT_FUNC,
                                          ADD_PROC_FUNC,
                                          (connectionId) -> AppServerManager.disconnect(pool, connectionId));
             }
          });
       }

       /**
        * Initialize the specified proxy procedure, by calling the <code>execute</code> method.
        *
        * @param    id
        *           The appserver connection id.
        * @param    code
        *           The external procedure id.
        * @param    requestId
        *           The ID of the request.
        * @param    transactionDistinct
        *           Flag indicating if the TRANSACTION DISTINCT clause is in effect.
        * @param    modes
        *           A string representation of the modes of each parameter. May be <code>null</code>
        * @param    args
        *           The procedure's arguments.
        *
        * @return   The result of the external program initialization.
        */
       public AppServerInvocationResult initializeProxy(final String   id,
                                                        final String   code,
                                                        final int      requestId,
                                                        final boolean  transactionDistinct,
                                                        final String   modes,
                                                        final Object[] args)
       {
          final AppServerInvocationResult invocationResult = new AppServerInvocationResult();

          waitForCommand(invocationResult, new AgentCommand(invocationResult, id, requestId)
          {
             @Override
             public void execute()
             {
                ExternalProgramWrapper extProg = getProcedure(code);

                CoreAppserver.initializeProxy(invocationResult,
                                              code,
                                              extProg,
                                              id,
                                              agentId,
                                              getSessionId(),
                                              requestId,
                                              transactionDistinct,
                                              modes,
                                              args,
                                              SET_RUNNING_PROC_FUNC,
                                              REMOVE_PROC_FUNC,
                                              UNBIND_PERSISTENT_FUNC,
                                              Agent.this::isBound,
                                              pool.getDeactivate());
             }
          });

          return invocationResult;
       }

       /**
        * Invoke a remote external procedure, persistently. The validation of  the program name and
        * arguments will be done on the remote side.
        *
        * @param    id
        *           The appserver connection id.
        * @param    requestId
        *           The ID of the request.
        * @param    timeout
        *           The maximum allowed time for this invocation to complete.  In milliseconds.
        *           Use <code>0</code> to disable the timeout.
        * @param    name
        *           The legacy 4GL name for the procedure.
        * @param    transactionDistinct
        *           Flag indicating if the TRANSACTION DISTINCT clause is in effect.
        * @param    modes
        *           A string representation of the modes of each parameter. May be <code>null</code>
        * @param    args
        *           The procedure's arguments.
        *
        * @return    A {@link AppServerInvocationResult} instance, which holds the code for the remote
        *            procedure, all updated OUTPUT/INPUT-OUTPUT arguments and any returned value.
        */
       public AppServerInvocationResult invokePersistent(final String    id,
                                                         final int       requestId,
                                                         final long      timeout,
                                                         final character name,
                                                         final boolean   transactionDistinct,
                                                         final String    modes,
                                                         final Object... args)
       {
          final AppServerInvocationResult invocationResult = new AppServerInvocationResult();

          waitForCommand(invocationResult, new AgentCommand(invocationResult, id, requestId, timeout)
          {
             @Override
             public void execute()
             {
                if (!invokeActivate(id, invocationResult))
                {
                   return;
                }

                CoreAppserver.invokePersistentImpl(invocationResult,
                                                   id,
                                                   agentId,
                                                   null,
                                                   name,
                                                   new handle(),
                                                   transactionDistinct,
                                                   pool.getExports(),
                                                   SET_RUNNING_PROC_FUNC,
                                                   BIND_PERSISTENT_FUNC,
                                                   UNBIND_PERSISTENT_FUNC,
                                                   ADD_PROC_FUNC,
                                                   modes,
                                                   args);
             }
          });

          return invocationResult;
       }

       /**
        * Perform a pseudo-dynamic call where the target procedure doesn't receive the parameter as arguments at
        * the Java method definition, but instead they are managed via {@link LegacyOpenClientCaller} APIs.
        * <p>
        * This allows the target program to act as a controller, where it can prepare the arguments, perform
        * security checks, etc, before dispatching the call to the real target, which can be resolved from the
        * arguments or in some other way.
        *
        * @param    id
        *           The appserver connection id.
        * @param    requestId
        *           The ID of the request.
        * @param    timeout
        *           The maximum allowed time for this invocation to complete.  In milliseconds.
        *           Use <code>0</code> to disable the timeout.
        * @param    procedure
        *           The target external program.  Must have no parameters defined.
        * @param    argTypes
        *           The argument types, one of the e.g. {@link LegacyJavaAppserverApi#integer()} values.
        * @param    modes
        *           The argument modes, one of the (I)NPUT, (O)UTPUT or INP(U)T-OUTPUT values.
        * @param    args
        *           The actual arguments.
        *
        * @return   A {@link AppServerInvocationResult} instance with all updated OUTPUT/INPUT-OUTPUT arguments
        *           and any returned value.
        */
       public AppServerInvocationResult invokeWithArgs(String    id,
                                                       int       requestId,
                                                       long      timeout,
                                                       String    procedure,
                                                       int[]     argTypes,
                                                       String    modes,
                                                       Object... args)
       {
          final AppServerInvocationResult invocationResult = new AppServerInvocationResult();

          waitForCommand(invocationResult, new AgentCommand(invocationResult, id, requestId, timeout)
          {
             @Override
             public void execute()
             {
                CoreAppserver.invokeWithArgs(invocationResult,
                                             id,
                                             pool.getActivate(),
                                             pool.getDeactivate(),
                                             Agent.this::isBound,
                                             SET_RUNNING_PROC_FUNC,
                                             (connectionId) -> AppServerManager.disconnect(pool, connectionId),
                                             procedure,
                                             argTypes,
                                             modes,
                                             args);
             }
          });

          return invocationResult;
       }

       /**
        * Invoke the activate procedure.
        *
        * @param    id
        *           The appserver connection id.
        * @param    invocationResult
        *           The result where to save the arguments and any return value.
        *
        * @return   <code>true</code> if the activate procedure is successfully executed, or if it's not
        *           applicable. <code>false</code> if there was an issue with procedure execution.
        */
       private boolean invokeActivate(String id, AppServerInvocationResult invocationResult)
       {
          return CoreAppserver.invokeActivate(pool.getActivate(),
                                              pool.getDeactivate(),
                                              Agent.this::isBound,
                                              id,
                                              invocationResult,
                                              SET_RUNNING_PROC_FUNC,
                                              (connectionId) -> AppServerManager.disconnect(pool, connectionId));
       }

       /**
        * Handle a generic web service request.  This exposes the Jetty request and response objects
        * to the Agent which will process it, so it is required for the appserver to be local.
        *
        * @param    id
        *           The appserver connection id.
        * @param    requestId
        *           The ID of the request.
        * @param    timeout
        *           The maximum allowed time for this invocation to complete.  In milliseconds.
        *           Use <code>0</code> to disable the timeout.
        * @param    handler
        *           The {@link com.goldencode.p2j.oo.web.WebHandler} implementation.
        * @param    basepath
        *           The service basepath.
        * @param    paths
        *           The split paths for this handler.
        * @param    token
        *           When not null, it represents the token of a FWD context created for an authenticated and
        *           authorized web service call.  The API call will be performed in that context.
        * @param    target
        *           The request original target.
        * @param    request
        *           The Jetty request.
        * @param    response
        *           The Jetty response.
        *
        * @return   A {@link AppServerInvocationResult} instance with any error throw during the web service
        *           processing.
        */
       public AppServerInvocationResult handleWebService(String                      id,
                                                         int                         requestId,
                                                         long                        timeout,
                                                         Class<? extends WebHandler> handler,
                                                         String                      basepath,
                                                         String[]                    paths,
                                                         String                      token,
                                                         String                      target,
                                                         HttpServletRequest          request,
                                                         HttpServletResponse         response)
       {
          final AppServerInvocationResult invocationResult = new AppServerInvocationResult();

          waitForCommand(null, token, new AgentCommand(null, id, requestId, timeout)
          {
             @Override
             public void execute()
             {
                CoreAppserver.handleWebService(invocationResult, id, handler, basepath, paths, target, request,
                                               response, SET_RUNNING_PROC_FUNC);
             }
          });

          return invocationResult;
       }

       /**
        * Send the terminate command to this agent.
        */
       public void terminate()
       {
          try
          {
             synchronized (stopperWorker.interruptedMutex)
             {
                // interrupt current work
                stop();

                // ensure the thread was actually interrupted before sending the AgentCommand
                stopperWorker.interruptedMutex.wait(THREAD_INTERRUPT_TIMEOUT);
             }
          }
          catch (InterruptedException e)
          {
             LOG.log(Level.FINE, "Agent was interrupted for appserver " + pool.getName(), e);
          }

          // send the terminate command
          sendCommand(new AgentCommand(null, null)
          {
             @Override
             public void execute()
             {
                synchronized (Agent.this)
                {
                   terminated = true;
                }
             }
          });
       }

       /**
        * Check if this agent has been terminated.
        *
        * @return   See above.
        */
       public synchronized boolean terminated()
       {
          return terminated;
       }

       /**
        * Bind the agent to the connection.
        */
       public synchronized void bind()
       {
          this.bound = true;
       }

       /**
        * Unbind the agent from the connection.
        */
       public synchronized void unbind()
       {
          this.bound = false;
       }

       /**
        * Check if the agent is bound to the connection.
        *
        * @return   See above.
        */
       public synchronized boolean isBound()
       {
          return bound;
       }

       /**
        * Delete the agent's currently bound {@link #remoteProcedures procedures}.
        *
        * @param    id
        *           The connection ID on which this command is executed.
        */
       public void deleteProcedures(String id)
       {
          if (remoteProcedures.isEmpty())
          {
             return;
          }

          ArrayList<AppserverRemoteProcedure> procs = new ArrayList<>(remoteProcedures.values());
          for (int i = 0; i < procs.size(); i++)
          {
             AppserverRemoteProcedure remote = procs.get(i);
             Object referent = remote.getExtProg().get();
             if (referent instanceof _BaseObject_)
             {
                deleteObject(id, remote.getExtProgResourceId());
             }
             else
             {
                deleteProcedure(id, remote.getExtProgResourceId(), null);
             }
          }

          // clear them.
          remoteProcedures.clear();
       }

       /**
        * Delete the specified procedure, under the Agent's context.
        *
        * @param    id
        *           The connection ID on which this command is executed.
        * @param    resId
        *           The resource ID for this procedure.
        * @param    token
        *           When not null, it represents the token of a FWD context created for an authenticated and
        *           authorized web service call.  The API call will be performed in that context.
        */
       public void deleteProcedure(String id, String resId, String token)
       {
          waitForCommand(null, token, new AgentCommand(null, id)
          {
             @Override
             public void execute()
             {
                CoreAppserver.executeScoped(() ->
                {
                   ExternalProgramWrapper proc = getProcedure(resId);

                   if (handle.knownResource(proc))
                   {
                      proc.delete();
                   }

                   removeProcedure(resId);

                   return null;
                });
             }
          });
       }

       /**
        * Delete the specified legacy object, under the Agent's context.
        *
        * @param    id
        *           The connection ID on which this command is executed.
        * @param    resId
        *           The resource ID for this legacy object.
        */
       public void deleteObject(String id, String resId)
       {
          waitForCommand(null, new AgentCommand(null, id)
          {
             @Override
             public void execute()
             {
                ExternalProgramWrapper proc = getProcedure(resId);

                if (handle.knownResource(proc))
                {
                   ObjectOps.delete((BaseObject) ((ExternalProgramWrapper) proc).get());
                }

                removeProcedure(resId);
             }
          });
       }

       /**
        * Set the current Agent's connection ID.
        *
        * @param    id
        *           The connection ID.
        */
       void setConnectionId(String id)
       {
          waitForCommand(null, new AgentCommand(null, id)
          {
             @Override
             public void execute()
             {
                AppServerManager.setConnectionId(id);
             }
          });
       }

       /**
        * Unbind this agent and call the deactivate procedure, if any.
        *
        * @param    id
        *           The connection ID.
        */
       void deactivate(String id)
       {
          unbind();
          CoreAppserver.invokeDeactivate(pool.getDeactivate(), isBound(), id, SET_RUNNING_PROC_FUNC);
       }

       /**
        * Validate the connection ID and procedure ID with this agent - the connection ID must be the same as
        * the {@link AppserverRemoteProcedure#getRequesterConnectionId()} which created the remote procedure,
        * and the procedure ID must be {@link #remoteProcedures registered} with this agent.
        *
        * @param    id
        *           The connection ID to validate.
        * @param    procId
        *           The procedure ID to validate.
        *
        * @throws   IllegalArgumentException
        *           If the connection ID and procedure ID are not for this agent, as the client performs an illegal
        *           request.
        */
       synchronized void validateRequest(String id, String procId)
       {
          AppserverRemoteProcedure remote = remoteProcedures.get(procId);

          if (remote == null || !remote.getRequesterConnectionId().equals(id))
          {
             throw new IllegalArgumentException("The procedure can not be accessed on this connection!");
          }
       }

       /**
        * Get the operating mode for this appserver.
        *
        * @return   See above.
        */
       AppServerOperatingMode getOperatingMode()
       {
          return pool.getOperatingMode();
       }

       /**
        * Check if this agent has persistent procedures ran on that connection ID.
        *
        * @param    id
        *           The connection ID.
        *
        * @return   See above.
        */
       boolean hasProcedures(String id)
       {
          if (remoteProcedures.isEmpty())
          {
             return false;
          }

          for (AppserverRemoteProcedure remote : remoteProcedures.values())
          {
             if (id.equals(remote.getRequesterConnectionId()))
             {
                return true;
             }
          }

          return false;
       }

       /**
        * Check if this definition is for a multi-session agent appserver.
        *
        * @return    See above.
        */
       boolean isMultiSession()
       {
          return pool.isMultiSession();
       }

       /**
        * Send a STOP condition to the request currently being executed on this connection.
        *
        * @throws   IllegalStateException
        *           If the appserver is not connected.
        */
       void stop()
       {
          // the STOP condition needs to be raised from a thread with the agent's context.  assuming
          // the agent's thread is busy executing another command, we can't use the agent's thread
          // to send this stop.  so an AssociatedThread with the same context as the Agent is used
          // instead to wait for STOP commands
          synchronized (stopper)
          {
             stopper.notify();
          }
       }

       /**
        * Get the SERVER-CONNECTION-CONTEXT attribute of this session.
        *
        * @param    connectionID
        *           The connection ID for this appserver.
        *
        * @return   See above.
        */
       String getServerConnectionContext(String connectionID)
       {
          return pool.getServerConnectionContext(connectionID);
       }

       /**
        * Set the SERVER-CONNECTION-CONTEXT attribute of this session.
        *
        * @param    connectionID
        *           The connection ID for this appserver.
        * @param    serverConnectionContext
        *           The attribute's value.
        */
       void setServerConnectionContext(String connectionID, String serverConnectionContext)
       {
          pool.setServerConnectionContext(connectionID, serverConnectionContext);
       }

       /**
        * Returns <code>null</code> if not a multi-session agent appserver, otherwise the security session ID.
        *
        * @return   See above.
        */
       private Integer getSessionId()
       {
          if (!pool.isMultiSession())
          {
             return null;
          }

          return SecurityManager.getInstance().sessionSm.getSessionId();
       }

       /**
        * Returns <code>null</code> if not a MSA appserver, otherwise the ID of agent's listening thread.
        *
        * @return   See above.
        */
       private Integer getThreadId()
       {
          if (!pool.isMultiSession())
          {
             return null;
          }

          return (int) thread.getId();
       }

       /**
        * Send the given command to this agent and return immediately.
        *
        * @param    command
        *           The command to be executed.
        */
       private synchronized void sendCommand(AgentCommand command)
       {
          commands.add(command);

          this.notify();
       }

       /**
        * Send the given command to the agent and wait for it to complete.
        *
        * @param    result
        *           The instance where to place the command result.
        * @param    token
        *           When not null, it represents the token of a FWD context created for an authenticated and
        *           authorized web service call.  The API call will be performed in that context.
        * @param    command
        *           The command to be executed.
        */
       private void waitForCommand(AppServerInvocationResult result, String token, AgentCommand command)
       {
          command.token = token;
          waitForCommand(result, command);
       }

       /**
        * Send the given command to the agent and wait for it to complete.
        *
        * @param    result
        *           The instance where to place the command result.
        * @param    command
        *           The command to be executed.
        */
       private void waitForCommand(AppServerInvocationResult result, AgentCommand command)
       {
          if (terminated())
          {
             LOG.severe("Trying to execute a command on a terminated agent!");
             return;
          }

          if (Thread.currentThread() == thread)
          {
             // this command was posted from the listening thread, execute immediately.
             executeCommand(command);
             return;
          }

          sendCommand(command);

          // we need to synchronize on the loop, else the command might finish between the test and
          // the wait and the notification will never be picked up
          synchronized (command)
          {
             long timeout = command.getTimeout() * 1000; // number of maximum seconds to wait
             boolean waitForFinish = (timeout == 0);
             long start = (!waitForFinish ? 0 : System.currentTimeMillis());

             while (!command.isFinished())
             {
                try
                {
                   command.wait(timeout);
                }
                catch (InterruptedException e)
                {
                   LOG.warning("Command was interrupted for appserver " + pool.getName(), e);
                }

                if (!waitForFinish)
                {
                   long finish = System.currentTimeMillis();
                   long elapsed = finish - start;

                   if (!command.isFinished() && elapsed >= timeout)
                   {
                      // terminate the agent's command, and wait for it to be marked as "finished"
                      stop();
                      timeout = 0;
                      waitForFinish = true;
                   }
                   else
                   {
                      // wait again, for the remainder time
                      timeout = timeout - elapsed;
                   }
                }
             }
          }
       }

       /**
        * Wrapper for {@link AgentCommand#doExecute()} calls, that sets the currently running procedure for the
        * agent.
        *
        * @param    agentCommand
        *           The instance where to place the command result.
        */
       private void executeCommand(AgentCommand agentCommand)
       {
          setRunningProc(agentCommand.getExternalProcedure());
          agentCommand.doExecute();
       }

       /**
        * Sets the currently running procedure for the agent and the time of execution start.
        *
        * @param    proc
        *           The instance where to place the command result.
        */
       private synchronized void setRunningProc(String proc)
       {
          this.runningProc = proc;
          this.runningProcStartTime = proc == null ? null : new Date();
       }

       /**
        * Prepare this agent by setting some batch-processing related configuration.
        */
       private void prepare()
       {
          // CA: 20210405: there were some changes in LT initialization (I think from the TM/PM helpers being
          // added), and now is no longer needed to initialize TM before LT - this is done during PM helper
          // call done by LT.init()

          // avoid the end-of-program message
          LogicalTerminal.setRelogin(true);

          // CA: 20210405: this comment is no longer valid.  it is left as a 'warning' for future readers in case
          // the LT/TM initialization order is changed again and STATE-RESET mode gets broken...
          // CA: 20220831: ScopeableFactory was removed and the comment bellow is no longer valid, but still left
          // for historical purposes.

          // make sure the TM is loaded before LT, as the LT context-local instance is dependent on TM
          // when creating the ScopeableFactory instances, LT will be loaded by TM and this instance
          // will be used as the factory. if we don't do this and load LT first, TM will end up with
          // the wrong LT instance as ScopeableFactory.
          TransactionManager.isHeadless();

          CoreAppserver.prepareSession(false, pool.getName(), pool.getPropath(), "appserver-agent");

          AppServerManager.registerAgent(this);
       }

       /**
        * Implements the reset context command.
        */
       private class ResetContextCommand
       extends AgentCommand
       {
          public ResetContextCommand()
          {
             super(null, null);
          }

          @Override
          public void execute()
          {
             // before cleaning up, cleanup all still-alive dynamic frames
             LogicalTerminal.cleanupDynamicFrames();

             // pop the global 'appserver-agent' scope, which will be pushed again in the 'prepare' method.
             TransactionManager.popScope();

             // reset the context
             try
             {
                SecurityManager.getInstance().contextSm.resetContext();
             }
             catch (RestrictedUseException e)
             {
                // ignore
             }
             finally
             {
                // set some state again
                prepare();
             }
          }
       }

       /**
        * Class used to stop a command currently being executed by this Agent, by sending an interrupt
        * request via {@link Control#interrupt()}.
        */
       private class AgentStopper
       implements Runnable
       {
          /** Flag indicating if the stopper was explicitly terminated. */
          private volatile boolean terminated = false;

          /** Mutex for synchronizing the actions needed to be executed after the interruption.  */
          private final Object interruptedMutex = new Object();

          @Override
          public void run()
          {
             boolean doInterrupt = false;

             while (!terminated && !Agent.this.terminated())
             {
                synchronized (stopper)
                {
                   try
                   {
                      stopper.wait();
                      doInterrupt = !terminated;
                   }
                   catch (InterruptedException e)
                   {
                      // ignore - if the thread gets interrupted, the loop will terminate only when
                      // the agent is terminated
                   }
                }

                synchronized (interruptedMutex)
                {
                   if (doInterrupt)
                   {
                      doInterrupt = false;

                      // received a notification, if agent is still running, stop its command
                      if (!Agent.this.terminated())
                      {
                         Control.interrupt();
                      }
                      else
                      {
                         interruptedMutex.notify();
                         // terminated, finish the loop
                         break;
                      }
                   }
                   interruptedMutex.notify();
                }
             }
          }

          /**
           * Terminate the stopper thread.
           */
          public void terminate()
          {
             this.terminated = true;

             synchronized (stopper)
             {
                stopper.notify();
             }
          }
       }

       /**
        * Defines a command to be sent to an agent.
        */
       private abstract class AgentCommand
       {
          /** The ID of the request. */
          private final int requestId;

          /**
           * The connection ID on which this command is executed.  May be <code>null</code> if this
           * command must terminate execution always (i.e. a procedure proxy initialization, procedure
           * deletion, etc).
           */
          private final String connectionId;

          /**
           * The maximum allowed time for this invocation to complete.  In milliseconds.
           * Use <code>0</code> to disable the timeout.
           */
          private final long timeout;

          /** Any error caught during the command execution. */
          private Throwable error = null;

          /** Flag indicating if this command has finished or not. */
          private boolean finished = false;

          /** The result for this command. */
          private AppServerInvocationResult result;

          /** The legacy 4GL name for the external procedure. */
          private String proc;

          /** The OERequestInfo details for this command. */
          private Object[] requestInfo;

          /**
           * When not null, it represents the token of a FWD context created for an authenticated and authorized
           * web service call.  The API call will be performed in that context.
           */
          private String token = null;

          /**
           * Initialize this command for no explicit request (an internal request).
           *
           * @param    result
           *           The result to be sent back to the remote side.
           * @param    id
           *           The connection ID on which this command is executed.
           */
          public AgentCommand(AppServerInvocationResult result, String id)
          {
             this(result, id, -1);
          }

          /**
           * Initialize this command for no explicit request (an internal request).
           *
           * @param    requestInfo
           *           The remote {@link com.goldencode.p2j.oo.lang.OerequestInfo} details, as set by
           *           {@link com.goldencode.p2j.oo.lang.OerequestInfo#toArray()}.
           * @param    result
           *           The result to be sent back to the remote side.
           * @param    id
           *           The connection ID on which this command is executed.
           */
          public AgentCommand(Object[] requestInfo, AppServerInvocationResult result, String id)
          {
             this(result, id, -1);

             this.requestInfo = requestInfo;
          }

          /**
           * Initialize this command for an explicit request.
           *
           * @param    result
           *           The result to be sent back to the remote side.
           * @param    id
           *           The connection ID on which this command is executed.
           * @param    requestId
           *           The ID of the request.
           */
          public AgentCommand(AppServerInvocationResult result, String id, int requestId)
          {
             this(result, id, requestId, 0);
          }

          /**
           * Initialize this command for an explicit request.
           *
           * @param    result
           *           The result to be sent back to the remote side.
           * @param    id
           *           The connection ID on which this command is executed.
           * @param    requestId
           *           The ID of the request.
           * @param    timeout
           *           The maximum allowed time for this invocation to complete.  In milliseconds.
           *           Use <code>0</code> to disable the timeout.
           */
          public AgentCommand(AppServerInvocationResult result, String id, int requestId, long timeout)
          {
             this.requestId = requestId;
             this.connectionId = id;
             this.timeout = timeout;
             this.result = result;
          }

          /** Override this to define the command body. */
          public abstract void execute();

          /**
           * Perform the actual command.  This will restore the SESSION:CURRENT-REQUEST-INFO from the
           * {@link #requestInfo}, if set.
           */
          public final void doExecute()
          {
             if (token != null)
             {
                LegacyWebSecurityManager wsm = SecurityManager.getInstance().legacyWebSm;
                if (wsm.hasWebRequestContext(token))
                {
                   while (true)
                   {
                      synchronized (activeWebServiceTokens)
                      {
                         if (activeWebServiceTokens.contains(token))
                         {
                            try
                            {
                               activeWebServiceTokens.wait();
                            }
                            catch (InterruptedException e)
                            {
                               // ignore
                            }
                         }
                         else
                         {
                            // lock this token, so no other request can be performed
                            activeWebServiceTokens.add(token);

                            break;
                         }
                      }
                   }

                   try
                   {
                      wsm.executeInContext(token, this::doExecuteImpl);
                   }
                   finally
                   {
                      synchronized (activeWebServiceTokens)
                      {
                         // unlock this token, other requests can be performed
                         activeWebServiceTokens.remove(token);
                         activeWebServiceTokens.notifyAll();
                      }
                   }
                }
                else
                {
                   throw new RuntimeException("Attempt to execute a command with invalid token: " + token);
                }
             }
             else
             {
                doExecuteImpl();
             }
          }

          /**
           * Setter for {@link #proc}.
           *
           * @param    proc
           *           The external procedure to be run by the command.
           */
          void setRunningProgram(String proc)
          {
             this.proc = proc;
          }

          /**
           * Getter for {@link #proc}.
           *
           * @return   The external procedure to be run by the command.
           */
          public String getExternalProcedure()
          {
             return proc;
          }

          private void doExecuteImpl()
          {
             try
             {
                try
                {
                   if (requestInfo != null)
                   {
                      CoreAppserver.executeScoped(() ->
                                                  {
                                                     SessionUtils.currentRequestInfo().ref().fromArray(requestInfo);
                                                     return null;
                                                  });

                      requestInfo = null;
                   }

                   execute();
                }
                finally
                {
                   if (requestInfo != null && result != null)
                   {
                      CoreAppserver.executeScoped(() ->
                                                  {
                                                     result.storeResponse(SessionUtils.currentResponseInfo());
                                                     return null;
                                                  });
                   }
                }
             }
             catch (Throwable t)
             {
                this.setError(t);

                LOG.warning("Agent encountered an error while executing a command for appserver " +
                               pool.getName(), t);
                if (result != null)
                {
                   result.setError(t);
                }
             }
          }

          /**
           * Get the request ID of this command.
           *
           * @return   The request ID.
           */
          public int getRequestId()
          {
             return requestId;
          }

          /**
           * Get the {@link #connectionId connection ID} where this command is executed.
           *
           * @return   The connection ID.
           */
          public String getConnectionId()
          {
             return connectionId;
          }

          /**
           * Get the {@link #timeout} of execution for this command.
           *
           * @return   The connection ID.
           */
          public long getTimeout()
          {
             return timeout;
          }

          /**
           * Check the {@link #finished} flag.
           *
           * @return   See above.
           */
          public synchronized boolean isFinished()
          {
             return finished;
          }

          /**
           * Notify that this command has finished.
           */
          public synchronized void finished()
          {
             this.finished = true;
             this.notify();
          }

          /**
           * Set the error caught during this command execution.
           *
           * @param    t
           *           The caught error.
           */
          public synchronized void setError(Throwable t)
          {
             this.error = t;
          }

          /**
           * Get the error caught during this command execution.
           *
           * @return   See above.
           */
          public synchronized Throwable getError()
          {
             return this.error;
          }
       }
    }