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