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