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