# ProcessOps.java

    /*
    ** Module   : ProcessOps.java
    ** Abstract : Progress 4GL compatible process launching interface
    **
    ** Copyright (c) 2005-2024, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- --JPRM-- -------------------Description--------------------
    ** 001 GES 20050629   @21569 First version implementing standard process
    **                           execution support.
    ** 002 GES 20050706   @21666 Integration with the lastError (replacement
    **                           for os-error).
    ** 003 GES 20050712   @21686 Modifications to make silent and wait flags
    **                           independent, to make it possible to execute
    **                           a shell (with no command) and to add a
    **                           version of launch that allows the caller to
    **                           provide the in/out/err streams to which the
    **                           child process' stdio should be attached.
    **                           This is needed for the language statements:
    **                           input/input-output/output through/thru.
    ** 004 GES 20050825   @22246 Modifications to support use of
    **                           StreamWrapper instead of direct references
    **                           to ProcessStream.
    ** 005 GES 20060117   @23957 Moved pause to LogicalTerminal.
    ** 006 GES 20060207   @24345 New signature for launch (handles new unnamed
    **                           streams implementation.
    ** 007 GES 20060211   @24557 A constant was moved into an interface.
    ** 008 GES 20060212   @24564 Rewrite to move launching to the remote
    **                           client.  This class is now just the Progress
    **                           compatible external interface.
    ** 009 GES 20060310   @24961 Added some launch variants.
    ** 010 GES 20061020   @30579 Change lastkey if a pause occurred during
    **                           interactive launches.
    ** 011 GES 20111020          Modified per-session initialization to detect
    **                           when the client code is running inside the server
    **                           process AND to initialize for local redirection
    **                           instead of using a remote object.
    ** 012 EVL 20171129          Adding support for NO-CONSOLE option.
    ** 013 SBI 20181207          Added CommandLineParser and changed launch(String[],boolean,boolean)
    **                           in order to delegate this call to openMimeResource if the launched helper
    **                           application is supported.
    ** 014 GBB 20240826          Moving FileSystem to osresource package.
    ** 015 GBB 20240912          Accessing process Launcher via OSResourceManager.
    */
    /*
    ** 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.Map.*;

    import com.goldencode.p2j.net.*;
    import com.goldencode.p2j.security.ContextLocal;
    import com.goldencode.p2j.util.osresource.*;

    /**
     * Provides static utility methods for Progress 4GL compatible process
     * launching and control.  At this time, the following Progress 4GL
     * language statements are handled:
     * <p>
     * <pre>
     * Progress Language Statement    Java Equivalent
     * ---------------------------    ---------------------------
     * BTOS                           {@link #launch(String[], boolean, boolean, boolean)}
     * DOS                            {@link #launch(String[], boolean, boolean, boolean)}
     * OS-COMMAND                     {@link #launch(String[], boolean, boolean, boolean)}
     * OS2                            {@link #launch(String[], boolean, boolean, boolean)}
     * UNIX                           {@link #launch(String[], boolean, boolean, boolean)}
     * INPUT THROUGH                  {@link #launch(String[], RemoteStream, RemoteStream)}
     * OUTPUT THROUGH                 {@link #launch(String[], RemoteStream, RemoteStream)}
     * INPUT-OUTPUT THROUGH           {@link #launch(String[], RemoteStream, RemoteStream)}
     * </pre>
     * <p>
     *
     * @author    GES
     */
    public class ProcessOps
    {
       /** Stores context-local state variables. */
       private static ContextContainer work = new ContextContainer();

       /**
        * Launches the default shell for this system synchronous operation and
        * displaying a prompt after the child process terminates. The command
        * is always executed as a child process of the current JVM process.
        * <p>
        * <code>STDIN</code>, <code>STDOUT</code> and <code>STDERR</code> are
        * all linked from the child process to the current JVM process using
        * a <code>StreamConnector</code> for each pipe.
        */
       public static void launch()
       {
          launch(null, false, true, true);
       }

       /**
        * Launches the default shell for this system 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 current JVM process.
        * <p>
        * <code>STDIN</code>, <code>STDOUT</code> and <code>STDERR</code> are
        * all linked from the child process to the current JVM process using
        * a <code>StreamConnector</code> for each pipe.
        *
        * @param    silent
        *           If <code>true</code> the default prompt is suppressed.
        *           Otherwise, <code>ThinClient.pause</code> is called 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).
        */
       public static void launch(boolean silent, boolean wait)
       {
          launch(null, silent, wait, true);
       }

       /**
        * Launches the external executible 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 current JVM process.
        * <p>
        * <code>STDIN</code>, <code>STDOUT</code> and <code>STDERR</code> are
        * all linked from the child process to the current JVM process using
        * a <code>StreamConnector</code> for each pipe.
        *
        * @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, <code>ThinClient.pause</code> is called 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).
        */
       public static void launch(String[] cmdlist, boolean silent, boolean wait)
       {
          CommandLineParser parser = new CommandLineParser(cmdlist);

          if (parser.isSupportedHelperApplication())
          {
             if (parser.getHelperApplicationMimeTypes().size() == 1)
             {
                String mimeType = parser.getHelperApplicationMimeTypes().get(0);
                String url = "file:///" + parser.getFirstParameter();
                WebBrowserManager.openMimeResource(mimeType, url, false);
             }
             else
             {
                WebBrowserManager.openMimeResource(parser.getFirstParameter(),
                                                   false,
                                                   parser.getHelperApplicationMimeTypes());
             }
          }
          else
          {
             launch(cmdlist, silent, wait, true);
          }
       }

       /**
        * Launches the external executible 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 current JVM process.
        * <p>
        * <code>STDIN</code>, <code>STDOUT</code> and <code>STDERR</code> are
        * all linked from the child process to the current JVM process using
        * a <code>StreamConnector</code> for each pipe.
        *
        * @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, <code>ThinClient.pause</code> is called 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.
        */
       public static void launch(String[] cmdlist, boolean silent, boolean wait, boolean console)
       {
          // a process launch always sets the last error to 0
          FileSystemOps.setLastError(FileSystem.ERR_NO_ERROR);

          int lastkey = -1;

          try
          {
             lastkey = work.obtain().rl.launch(cmdlist, silent, wait, console);

             if (lastkey != -1)
             {
                KeyReader.setLastKey(lastkey);
             }
          }

          catch (ConditionException ce)
          {
             // make sure that the lastkey is properly maintained
             int kc = ce.getKeyCode();

             // anything other than -1 is a valid key press AND even -1 is a
             // valid indicator of CTRL-C if this was a STOP
             if (kc != -1 || ce instanceof StopConditionException)
             {
                KeyReader.setLastKey(kc);
             }

             // continue the unwinding of the stack
             throw ce;
          }
       }

       /**
        * Launches the external executible 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 current JVM
        * process.
        * <p>
        * <code>STDIN</code>, <code>STDOUT</code> and <code>STDERR</code> are
        * all linked from the child process to the passed parameters if
        * those parameters are not <code>null</code>. Any <code>null</code>
        * pipes are closed immediately and those pipes connected with the
        * passed parameters 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
        *           A <code>StreamWrapper</code> instance which contains the
        *           <code>ProcessStream</code> to be connected to the child
        *           process' <code>STDOUT and STDERR</code> or <code>null</code>
        *           for no connection.  Used for reading.
        * @param    sin
        *           A <code>StreamWrapper</code> instance which contains the
        *           <code>ProcessStream</code> to be connected to the child
        *           process' <code>STDIN</code> or <code>null</code> for no
        *           connection.  Used for writing.
        */
       public static void launch(String[]      cmdlist,
                                 StreamWrapper sout,
                                 StreamWrapper sin)
       {
          launch(cmdlist,
                 (sout == null ? null : (RemoteStream) sout.dereference()),
                 (sin  == null ? null : (RemoteStream) sin.dereference()));
       }

       /**
        * Launches the external executible 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 current JVM
        * process.
        * <p>
        * <code>STDIN</code>, <code>STDOUT</code> and <code>STDERR</code> are
        * all linked from the child process to the passed parameters if
        * those parameters are not <code>null</code>. Any <code>null</code>
        * pipes are closed immediately and those pipes connected with the
        * passed parameters 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 <code>RemoteStream</code> to be connected to the child
        *           process' <code>STDOUT and STDERR</code> or <code>null</code>
        *           for no connection.  Used for reading.
        * @param    sin
        *           The <code>RemoteStream</code> to be connected to the child
        *           process' <code>STDIN</code> or <code>null</code> for no
        *           connection.  Used for writing.
        */
       public static void launch(String[]     cmdlist,
                                 RemoteStream sout,
                                 RemoteStream sin)
       {
          // a process launch always sets the last error to 0
          FileSystemOps.setLastError(FileSystem.ERR_NO_ERROR);

          work.obtain().rl.launch(cmdlist,
                                  (sout == null ? -1 : sout.getId()),
                                  (sin  == null ? -1 : sin.getId()));
       }

       /**
        * Stores global data relating to the state of the current context.
        */
       private static class WorkArea
       {
          /** Remote instance of our process launching server. */
          private Launcher rl = null;
       }

       /**
        * Simple container that stores and returns a context-local instance of
        * the global work area.
        */
       private static class ContextContainer
       extends ContextLocal
       {
          /**
           * Obtains the context-local instance of the contained global work
           * area.
           *
           * @return   The work area associated with this context.
           */
          public WorkArea obtain()
          {
             return (WorkArea) this.get();
          }

          /**
           * Initializes the work area, the first time it is requested within a
           * new context.
           *
           * @return   The newly instantiated work area.
           */
          protected synchronized Object initialValue()
          {
             WorkArea wa = new WorkArea();

             // obtain the our proxy, server-side code is left-side by convention
             wa.rl = OSResourceManager.getProcessLauncher();

             return wa;
          }
       }

       /**
        * Gets a supported helper application parameter from the given command arguments list.
        */
       private static class CommandLineParser
       {
          /**
           * The OS command name
           */
          private final String START = "start";

          /**
           *  The helper application name
           */
          private final String helperApplication;

          /**
           *  The first parameter of the helper application
           */
          private final String firstParameter;

          /**
           *  The rest parameter list of the helper application
           */
          private final List<String> restParameters;

          /**
           *  The boolean value indicating that the helper application is supported or not
           */
          private final boolean isSupported;

          /**
           *  The possible mime types supported by the helper application, the first one is
           *  the most preferable than others.
           */
          private final List<String> mimeTypes;

          /**
           * Creates an instance and calculates a helper application and its parameters.
           *
           * @param    cmdList
           *           The given command arguments list
           */
          public CommandLineParser(String[] cmdList)
          {
             int num = cmdList.length;

             int curr = 0;

             if (num > curr && START.equalsIgnoreCase(cmdList[curr]))
             {
                curr++;
             }

             helperApplication = num > curr ? cmdList[curr] : null;

             curr++;

             firstParameter = num > curr ? cmdList[curr] : null;

             curr++;

             if (num > curr)
             {
                restParameters = new ArrayList<String>(num - curr);

                for (int i = curr; i < num; i++)
                {
                   restParameters.add(cmdList[i]);
                }
             }
             else
             {
                restParameters = null;
             }

             Map<String, List<String>> helpers = EnvironmentOps.getSupportedHelperApplications();

             Optional<Entry<String, List<String>>> result = helpers.entrySet().stream().filter(
                entry -> {
                            if (entry.getKey().equalsIgnoreCase(helperApplication))
                            {
                               return true;
                            }

                            return false;
                         }).findFirst();

             isSupported = result.isPresent();

             if (result.isPresent())
             {
                mimeTypes = result.get().getValue();
             }
             else
             {
                mimeTypes = null;
             }
          }

          /**
           * Gets the result of checking if this helper application is supported.
           *
           * @return   True if this helper application is supported, otherwise false.
           */
          public boolean isSupportedHelperApplication()
          {
             return isSupported;
          }

          /**
           * Gets mime types associated with this helper application.
           *
           * @return   The list of mime types associated with this helper application
           */
          public List<String> getHelperApplicationMimeTypes()
          {
             return mimeTypes;
          }

          /**
           * Gets the file path to this helper application.
           *
           * @return   The file path to this helper application
           */
          public String getHelperApplication()
          {
             return helperApplication;
          }

          /**
           * Gets the first parameter given to this helper application.
           *
           * @return   The first parameter given to this helper application
           */
          public String getFirstParameter()
          {
             return firstParameter;
          }

          /**
           * Gets the rest parameters given to this helper application.
           *
           * @return   The rest parameters given to this helper application
           */
          public List<String> getRestParameters()
          {
             return restParameters;
          }
       }
    }