# ProcessDaemon.java

    /*
    ** Module   : ProcessDaemon.java
    ** Abstract : process launching server
    **
    ** Copyright (c) 2006-2024, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- --JPRM-- --------------------------Description-------------------------------
    ** 001 GES 20060212   @24563 First version supporting 2 forms: a launch
    **                           that has its STDIO connected to the terminal
    **                           and a launch which has STDIO redirected to
    **                           and/or from streams that are accessible from
    **                           the caller.
    ** 002 GES 20060213   @24587 Changes to use custom process launching
    **                           path (bypassed the J2SE launching) when
    **                           in non-streams mode. This is required since
    **                           the J2SE Runtime.exec() does not provide a
    **                           terminal to the child process.  This makes
    **                           it impossible to handle interactive child
    **                           processes.
    ** 003 GES 20060518   @26343 Changes to use custom process launching
    **                           path (bypassed the J2SE launching) when
    **                           in streams mode since Progress provides a
    **                           terminal to the child process.
    ** 004 GES 20060530   @26678 Rethrow ConditionException instances during
    **                           launch (can occur due to end user input in
    **                           pause processing).
    ** 005 GES 20061020   @30578 Return lastkey from interactive forms of
    **                           launch since any pause that occurs during
    **                           that time will update lastkey.
    ** 006 NVS 20070308   @32294 Process launching has been reworked and is no
    **                           longer based on providing a terminal device
    **                           to the child process through pseudo-terminal.
    **                           Instead, the parent process' TTY device is
    **                           directly used by any stdio handle which is
    **                           not redirected to a pipe.
    ** 007 NVS 20070309   @32344 Some minor cleanup.
    ** 008 GES 20080123   @36899 Removed dead code.
    ** 009 NVS 20080212   @37035 Minor changes required. Process launching and waiting for the child
    **                           now take a flag telling the process is interactive.
    ** 010 SIY 20090622   @42774 Detect special "silent" mode, when invalid external application is
    **                           just ignored and pipe assumed live despite the fact that external
    **                           application even was not started.
    ** 011 GES 20111006          Directly use launch mgr for pseudoTerminal methods instead of using
    **                           thin client.
    ** 012 GES 20111025          Modified API export code to capture and use the authentication token
    **                           so that multiple registrations can be done per JVM session. This
    **                           occurs when the client code is running in the server process and
    **                           there is a CTRL-C that causes the client to restart.
    ** 013 GES 20111111          Modified interface export to allow registration to be exclusively
    **                           local or remote as needed.
    ** 014 CS  20121213          Added support for calling the Quoter utility.
    ** 015 EVL 20130408          Adding ability to launch process in Windows via cmd.exe.
    ** 016 EVL 20130829          Eliminating additional pause after not silent process launch for
    **                           Windows OS case.
    ** 017 VIG 20130918          Added link from StreamDeamon and ProcessDaemon (see constructor).
    ** 018 EVL 20131202          Adding support for batch mode in OS-COMMAND statement.
    ** 019 MAG 20140326          Adding support for batch mode in OS-COMMAND statement for Swing and
    *                            Web clients.
    ** 020 CA  20140805          ThinClient is explicitly imported, to isolate ChUI class usage
    **                           outside of ChUI packages.  Replaced the ChuiScreenDriver with the new
    **                           GenericScreenDriver class, which contains common screen-driver code.
    ** 021 ECF 20150126          Added check for background mode when launching a child process.
    **                           Deactivate batch mode for the child process launch in
    **                           launch(String[], boolean, boolean), then reactivate before returning.
    ** 022 GES 20150210          Batch mode should force the silent flag to be on.
    ** 023 GES 20150211          Swing and Web clients on non-Windows platforms need interactive mode
    **                           forced on for the shell that is launched. Without this, any child
    **                           processes that are run with background mode (&) will silently fail
    **                           to launch.  This is because the shell doesn't enable job control
    **                           automatically for the pty case, but with the non-pty case there is
    **                           a native terminal already active for the session and job control
    **                           naturally works.
    ** 024 CA  20150227          Fixed a problem when the "quoter" application is used in a process
    **                           launching command computed via the VALUE clause: the first token
    **                           needs to be separated and interpreted as the program to be launched,
    **                           so that any pre-processing will be possible.
    ** 025 GES 20150410          Reworked to make the interactive launching pluggable, which allows
    **                           all client-specific dependencies to be cleanly separated from the
    **                           process launching behavior.
    ** 026 ECF 20150715          Replace StringBuffer with StringBuilder.
    ** 027 EVL 20160224          Javadoc fixes to make compatible with Oracle Java 8 for Solaris 10.
    ** 028 OM  20160927          Small string operation optimization.
    ** 029 HC  20171024          Runtime support for SYSTEM-DIALOG PRINT-SETUP and OUTPUT TO PRINTER.
    ** 030 EVL 20171129          Adding fix for NO-WAIT Windows mode and support for NO-CONSOLE
    **                           option.
    ** 031 EVL 20180310          Adding fix for SILENT mode support for suspend/resume.
    ** 032 GBB 20231207          Adding FWD_VERSION env var before launching a new child process.
    ** 033 GBB 20240912          Removing single client.
    ** 034 GBB 20240826          Class moved to osresource package. Attempt to abstract out the UI logic.
    */
    /*
    ** 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.osresource;

    import java.io.*;

    import com.goldencode.p2j.*;
    import com.goldencode.p2j.net.*;
    import com.goldencode.p2j.ui.chui.*;
    import com.goldencode.p2j.ui.client.driver.*;
    import com.goldencode.p2j.util.*;
    import com.goldencode.util.PlatformHelper;

    // TODO: this should be removed when the launch(String[], int, int) is disconnected from the UI
    import com.goldencode.p2j.ui.client.OutputManager;

    /**
     * Provides 2 forms of process launching:
     * <p>
     * <ul>
     *    <li> interactive mode - {@link #launch(java.lang.String[],boolean,boolean,boolean)} where
     *         the child process has its STDIO connected to the terminal (such that the user can
     *         interact with the program)
     *    <li> redirected mode - {@link #launch(java.lang.String[],int,int)} where the child
     *         process has its STDIO redirected to and/or from caller specified streams.
     * </ul>
     * <p>
     * This is designed to launch processes on a remote network node.
     */
    class ProcessDaemon
    implements Launcher
    {
       /** Token used to authenticate with the dispatcher when registering APIs. */
       private final Object modToken;

       /** Flag to indicate the process is launched on a Windows OS system. */
       private final boolean isWin;

       /** Output manager. */
       private final OutputManager outputManager;

       /**
        * Initializes the process server.
        *
        * @param    terminal
        *           Provides access to terminal features and helpers.
        * @param    local
        *           <code>true</code> to register purely on a local basis (no remote/network access to these
        *           methods).
        */
       ProcessDaemon(Terminal terminal, boolean local)
       {
          synchronized (ProcessDaemon.class)
          {
             modToken = RemoteObject.registerServer(Launcher.class, this, null, local);
             isWin = PlatformHelper.isUnderWindowsFamily();
             outputManager = terminal.getOutputManager();
          }
       }

       /**
        * Launches the external executable specified by the <code>cmdlist</code> taking into account
        * the flags which specify synchronous or asynchronous operation and whether a prompt is
        * provided after the child process terminates. The command is always executed as a child
        * process of the shell which is a child process of the JVM process on the node in which this
        * is served.
        * <p>
        * <code>STDIN</code>, <code>STDOUT</code> and <code>STDERR</code> of the child process are
        * mapped into the PTY (terminal) of the JVM process. In other words, this child process has
        * capability to be interactive with the user.
        * <p>
        * Since the <code>Runtime.exec()</code> process launching method of J2SE does not provide a
        * terminal/console to the launched child process, that method cannot be used to launch this
        * child process.  For example, it is impossible to get J2SE child processes to be
        * interactive with the user.  For this reason, an alternate process launching infrastructure
        * is used, which relies on native code for launching.  This native code is accessed via a
        * plugin architecture that allows different native code implementations, based on the
        * different client types in use.
        * <p>
        * All proper wait and pause semantics are honored and the current process' terminal
        * processing is properly suspended and resumed as needed.
        *
        * @param    cmdlist
        *           The command line to be executed, with the first element used as the program to
        *           run and all other array elements as the arguments.
        * @param    silent
        *           If <code>true</code> the default prompt is suppressed.  Otherwise, a pause is
        *           implemented when the child process exits.
        * @param    wait
        *           If <code>true</code>, synchronous operation is requested which waits for the
        *           termination of the child process on the current thread.  If <code>false</code>,
        *           a thread will be started to wait for the child process termination and to
        *           subsequently handle cleanup (asynchronous mode).
        * @param    console
        *           If <code>true</code>, the process is starting in a separate new console,
        *           <code>false</code> means using 4GL no-console option.
        *
        * @return   The key code for any key pressed during any pause or -1 if there was no pause.
        */
       @Override
       public int launch(String[] cmdlist, boolean silent, boolean wait, boolean console)
       {
          int lastkey = -1;

          ScreenDriver<?> driver = outputManager.getDriver();
          if (driver == null || driver.getChildProcessFactory() == null)
          {
             return lastkey;
          }

          // get a new interactive child process instance
          InteractiveChildProcess client = driver.getChildProcessFactory().getChildProcess();

          // does the driver in use have an initialized native terminal
          boolean missing = client.isArtificialTerm();

          // override silent mode when in batch because there is no message and no pause
          // this is especially important because the pseudoTerminalWait() will access
          // terminal APIs directly if not in silent mode, which causes the process to
          // fault (at least on Linux, because the ncurses env is not initialized); also
          // the screen clearing in pseudoTerminalLaunch() should not occur
          if (!silent && (client.inBatchMode() || (!wait && isWin)))
          {
             silent = true;
          }

          // preprocess the parms
          String[] list = prepareCommandLine(cmdlist, missing, (!silent && wait), console);

          boolean wasResumed = false;

          try
          {
             // clear the thin client's frame output and suspend normal UI mode
             client.suspend(silent);

             int pid = client.launch(list, silent);

             if (pid < 0)
             {
                // re-initialize the terminal
                client.resume(silent);

                // bypass resume in finally block below
                wasResumed = true;

                ErrorManager.recordOrThrowError(pid, "Process launch failed.");
                return lastkey;
             }

             // synch or asynch?
             if (wait)
             {
                client.waitForExit(pid, silent);
             }
             else
             {
                client.asynchCleanup(pid);
             }

             // if not silent we display a default message and pause indefinitely
             if (!silent && !isWin)
             {
                lastkey = client.pauseForUser();
             }
          }
          catch (ConditionException ce)
          {
             // rethrow any condition exceptions (could occur during pause
             // processing on END-ERROR... for example)
             throw ce;
          }
          catch (Exception exc)
          {
             // silent failure like Progress...
          }
          finally
          {
             if (!wasResumed)
             {
                // resume normal operations and clear any child process output
                client.resume(silent);
             }
          }

          return lastkey;
       }

       /**
        * Launches the external executable specified by the <code>cmdlist</code> on an asynchronous
        * basis. The command is always executed as a child process of the shell which is a child
        * process of the the JVM process on the node in which this is served. This corresponds to
        * the standard concept of "co-processing" where the parent process and child process are
        * both running at the same time and they interact in one direction or both directions by
        * reading and/or writing from connected pipes.
        * <p>
        * This is a redirected mode process launch.  The <code>STDIN</code>, <code>STDOUT</code> and
        * <code>STDERR</code> are all linked from the child process to the passed streams if those
        * parameters are not -1. Any pipes which are not to be linked are closed immediately. Those
        * pipes which are connected with the passed streams must be closed by the caller.  Please see
        * the {@link ProcessStream} for more details.
        *
        * @param    cmdlist
        *           The command line to be executed, with the first element used as the program to
        *           run and all other array elements as the arguments.
        * @param    sout
        *           The id of a <code>ProcessStream</code> instance to be connected to the child
        *           process' <code>STDOUT</code> and <code>STDERR</code> or -1 for no connection.
        *           Used for reading.
        * @param    sin
        *           The id of a <code>ProcessStream</code> instance to be connected to the child
        *           process' <code>STDIN</code> or -1 for no connection.  Used for writing.
        */
       @Override
       public void launch(String[] cmdlist, int sout, int sin)
       {
          try
          {
             boolean broken = false;

             // create file descriptors
             FileDescriptor fdIn  = sin  != -1 ? new FileDescriptor() : null;
             FileDescriptor fdOut = sout != -1 ? new FileDescriptor() : null;
             FileDescriptor fdErr = sout != -1 ? new FileDescriptor() : null;

             if (cmdlist != null && cmdlist.length == 1 && cmdlist[0].equals("?") && fdIn != null)
             {
                // special case of silent error mode
                broken = true;
             }

             // preprocess the parms
             String[] list = prepareCommandLine(cmdlist, false, false, true);

             // are we in background mode?
             boolean background = outputManager.isBatchInBackground();

             // launch the process
             int pid = LaunchManager.pseudoTerminalLaunch(list,
                                                          fdIn,
                                                          fdOut,
                                                          fdErr,
                                                          true,
                                                          background,
                                                          Version.getFWDVersion());

             if (pid < 0)
             {
                ErrorManager.recordOrThrowError(pid, "Process launch failed.");
                return;
             }

             // create the stdio of the child process
             OutputStream cin = fdIn  != null ? new FileOutputStream(fdIn) : null;
             InputStream cout = fdOut != null ? new FileInputStream(fdOut) : null;
             InputStream cerr = fdErr != null ? new FileInputStream(fdErr) : null;

             // casting works only if the process launcher and the streams are both server-side or client-side
             StreamDaemon streamHelper = (StreamDaemon) OSResourceManager.getStreamHelper();
             // if the corresponding stream argument is not null, create pipes to
             // copy the stdout/stderr output from the child process to our
             // stream arguments, otherwise just close the child process' pipe
             if (sout != -1)
             {
                ProcessStream pso = (ProcessStream) streamHelper.streamFromId(sout);
                pso.connectOut(pid, cout, cerr);
             }

             if (sin != -1)
             {
                ProcessStream psi = (ProcessStream) streamHelper.streamFromId(sin);
                psi.connectIn(pid, cin);

                if (broken)
                   psi.setBroken();
             }
          }
          catch (Exception exc)
          {
             // silent failure like Progress...
          }
       }

       /**
        * Converts the given command list into a form that is suitable for process launching.  In
        * particular, it creates an array with the first element set to the system shell and
        * subsequent elements are shell options and the reconstituted command line from the original
        * caller (this is built up from the passed command list, back into a single string).
        *
        * @param    cmdlist
        *           The command to execute (already split into whitespace delimited pieces).
        * @param    missing
        *           If the native terminal is missing from our process, we must force the
        *           shell to enable job control (only on non-Windows platforms).
        * @param    iact
        *           Interactive flag (<code>true</code> if not in silent mode AND if the 4GL session
        *           will wait for the child process to complete before continuing).
        * @param    inConsole
        *           If <code>true</code>, the process is starting in a separate new console,
        *           <code>false</code> means using 4GL no-console option.
        *
        * @return   An arguments array suitable for process launching.
        */
       private String[] prepareCommandLine(String[] cmdlist, boolean missing, boolean iact, boolean inConsole)
       {
          boolean noCmd = (cmdlist == null || cmdlist.length == 0);
          boolean xtra  = (!isWin && missing);

          int arraySize = noCmd || (isWin && !inConsole) ? 1 : (xtra ? 4 : 3);

          // prepare the array by creating a new array with the shell as the first
          // element, then copy the passed array as the subsequent parameters
          String[] list = new String[arraySize];

          if (isWin)
          {
             if (inConsole)
             {
                list[0] = "cmd.exe";
             }
          }
          else
          {
             list[0] = "sh";
          }

          StringBuilder sb = new StringBuilder();

          if (arraySize > 1 || (!noCmd && isWin && !inConsole))
          {
             // build our command back up from parts

             if (cmdlist.length == 1)
             {
                // if the command is not yet tokenized, determine the first token and use it as the
                // command to be executed; the remaining tokens are the arguments.
                String cmd = cmdlist[0];
                int spaceIdx = cmd.indexOf(' ');

                // determine which program to launch
                if (spaceIdx > 0)
                {
                   cmdlist = new String[2];
                   cmdlist[0] = cmd.substring(0, spaceIdx);
                   cmdlist[1] = cmd.substring(spaceIdx + 1);
                }
             }

             for (int i = 0; i < cmdlist.length; i++)
             {
                if (i > 0)
                {
                   sb.append(' ');
                }
                if (Quoter.isQuoterCommand(cmdlist[i]))
                {
                   sb.append("java -cp ")
                     .append(System.getProperty("java.class.path"))
                     .append(" com.goldencode.p2j.util.Quoter");
                }
                else
                {
                   sb.append(cmdlist[i]);
                }
             }
          }

          if (arraySize > 1 && inConsole)
          {
             // pass the command to the shell
             if (isWin)
             {
                if (missing && iact)
                {
                   // TODO: The Windows OS shell cmd.exe doesn't work properly in interactive mode
                   //       because cmd.exe does not use STDIN as input nor STDOUT and STDERR as
                   //       outputs. Using this option is a workaround for now. Executed commands
                   //       using STDIN, STDOUT and STDERR are working properly.
                   list[1] = "/k";
                }
                else
                {
                   // standard approach
                   list[1] = "/c";
                }
             }
             else
             {
                list[1] = "-c";

                if (xtra)
                {
                   // this enables interactive mode for our shell, which is needed to enable
                   // job control and thus allow background mode to work properly
                   list[2] = "-i";
                }
             }

             list[xtra ? 3 : 2] = sb.toString();
          }
          else if (!noCmd && isWin && !inConsole)
          {
             list[0] = sb.toString();
          }

          return list;
       }
    }