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